from dataclasses import dataclass, field from enum import Enum import numpy as np from src.data.frequency import Frequency @dataclass class GeneratorParams: """Base class for generator parameters.""" global_seed: int = 42 length: int = 2048 frequency: list[Frequency] | None = None start: list[np.datetime64] | None = None def update(self, **kwargs): """Update parameters from keyword arguments.""" for k, v in kwargs.items(): if hasattr(self, k): setattr(self, k, v) @dataclass class ForecastPFNGeneratorParams(GeneratorParams): """Parameters for the ForecastPFNGenerator.""" trend_exp: bool = True scale_noise: tuple[float, float] = (0.6, 0.3) harmonic_scale_ratio: float = 0.5 harmonic_rate: float = 1.0 period_factor: float = 1.0 seasonal_only: bool = False trend_additional: bool = True transition_ratio: float = 1.0 # Probability of applying transition between two series random_walk: bool = False # Multivariate augmentation parameters (applied in wrapper) mixup_prob: float = 0.1 # Probability of applying mixup augmentation mixup_series: int = 4 # Maximum number of series to mix in mixup damp_and_spike: bool = False # Whether to apply damping and spike augmentations damping_noise_ratio: float = 0.05 # Ratio of batch to apply damping spike_noise_ratio: float = 0.05 # Ratio of batch to apply spike noise spike_signal_ratio: float = 0.05 # Probability of applying spike signal replacement spike_batch_ratio: float = 0.05 # Ratio of batch for spike signal replacement # Univariate augmentation parameters (applied in generator) time_warp_prob: float = 0.1 # Probability of applying time warping time_warp_strength: float = 0.05 # Strength of time warping effect magnitude_scale_prob: float = 0.2 # Probability of applying magnitude scaling magnitude_scale_range: tuple[float, float] = ( 0.9, 1.1, ) # Range for magnitude scaling damping_prob: float = 0.1 # Probability of applying damping augmentation spike_prob: float = 0.15 # Probability of applying spike augmentation pure_spike_prob: float = 0.02 # Probability of replacing with pure spike signal # Built-in filtering parameters max_absolute_spread: float = 300.0 # Maximum allowed spread (max - min) for generated series max_absolute_value: float = 300.0 max_retries: int = 10 @dataclass class GPGeneratorParams(GeneratorParams): """ Parameters for the Gaussian Process (GP) Prior synthetic data generator. """ max_kernels: int = 6 likelihood_noise_level: float = 0.1 noise_level: str = "low" # Options: ["random", "high", "moderate", "low"] use_original_gp: bool = False gaussians_periodic: bool = True peak_spike_ratio: float = 0.1 subfreq_ratio: float = 0.2 periods_per_freq: float = 0.5 gaussian_sampling_ratio: float = 0.2 max_period_ratio: float = 0.5 kernel_periods: tuple[int, ...] = (4, 5, 7, 21, 24, 30, 60, 120) kernel_bank: dict[str, float] = field( default_factory=lambda: { "matern_kernel": 1.5, "linear_kernel": 1.0, "periodic_kernel": 5.0, "polynomial_kernel": 0.0, "spectral_mixture_kernel": 0.0, } ) @dataclass class KernelGeneratorParams(GeneratorParams): """Parameters for the KernelSynthGenerator.""" max_kernels: int = 5 @dataclass class SineWaveGeneratorParams(GeneratorParams): """Parameters for the SineWaveGenerator - focused on diverse sinusoidal patterns.""" # Core sinusoidal parameters num_components_range: tuple[int, int] = (1, 3) period_range: tuple[float, float] | tuple[tuple[float, float], tuple[float, float]] = (10.0, 200.0) amplitude_range: tuple[float, float] | tuple[tuple[float, float], tuple[float, float]] = (0.5, 3.0) phase_range: tuple[float, float] | tuple[tuple[float, float], tuple[float, float]] = (0.0, 2.0 * np.pi) # Trend parameters trend_slope_range: tuple[float, float] = (-0.01, 0.01) base_level_range: tuple[float, float] = (0.0, 2.0) # Noise parameters noise_probability: float = 0.7 # Probability of adding noise (70% of series have noise) noise_level_range: tuple[float, float] = ( 0.05, 0.2, ) # Small noise as fraction of amplitude (when noise is applied) # Time-varying parameters (subtle modulation) enable_amplitude_modulation: bool = True amplitude_modulation_strength: float = 0.1 # Max 10% amplitude variation enable_frequency_modulation: bool = True frequency_modulation_strength: float = 0.05 # Max 5% frequency variation @dataclass class SawToothGeneratorParams(GeneratorParams): """Parameters for the SawToothGenerator.""" periods: tuple[int, int] = (2, 7) # Number of sawtooth periods in the series amplitude_range: tuple[float, float] | tuple[tuple[float, float], tuple[float, float]] = (0.5, 3.0) phase_range: tuple[float, float] | tuple[tuple[float, float], tuple[float, float]] = ( 0.0, 1.0, ) # Phase shift as fraction of period trend_slope_range: tuple[float, float] | tuple[tuple[float, float], tuple[float, float]] = ( -0.001, 0.001, ) # Slightly stronger linear trend slope for more straight lines seasonality_amplitude_range: tuple[float, float] | tuple[tuple[float, float], tuple[float, float]] = ( 0.0, 0.02, ) # Minimal seasonal component amplitude add_trend: bool = True # Whether to add linear trend add_seasonality: bool = True # Whether to add seasonal component class StepPatternType(Enum): """Types of step patterns that can be generated.""" STABLE = "stable" # Flat line with minimal variation GRADUAL_INCREASE = "gradual_increase" # Gradual upward steps GRADUAL_DECREASE = "gradual_decrease" # Gradual downward steps SPIKE_UP = "spike_up" # Sharp increase then gradual decrease SPIKE_DOWN = "spike_down" # Sharp decrease then gradual increase OSCILLATING = "oscillating" # Up and down pattern RANDOM_WALK = "random_walk" # Random steps (current behavior) @dataclass class SubseriesConfig: """Configuration for a single subseries pattern.""" pattern_type: StepPatternType length_range: tuple[int, int] # Min and max length for this subseries num_changepoints_range: tuple[int, int] # Number of changepoints in this subseries step_size_range: tuple[float, float] # Step size range for this pattern level_drift_range: tuple[float, float] = (0.0, 0.0) # Overall level drift step_size_decay: float = 1.0 # Decay factor for step sizes over time weight: float = 1.0 # Probability weight for selecting this pattern @dataclass class StepGeneratorParams(GeneratorParams): """Parameters for the StepGenerator with subseries support.""" # Subseries configuration subseries_configs: list[SubseriesConfig] = field( default_factory=lambda: [ # Stable beginning (20-30% of series) SubseriesConfig( pattern_type=StepPatternType.STABLE, length_range=(200, 600), num_changepoints_range=(0, 3), step_size_range=(-1.0, 1.0), weight=0.8, ), # Gradual increase pattern (15-25% of series) SubseriesConfig( pattern_type=StepPatternType.GRADUAL_INCREASE, length_range=(300, 700), num_changepoints_range=(5, 15), step_size_range=(1.0, 5.0), level_drift_range=(0.0, 0.1), weight=0.6, ), # Gradual decrease pattern (15-25% of series) SubseriesConfig( pattern_type=StepPatternType.GRADUAL_DECREASE, length_range=(300, 700), num_changepoints_range=(5, 15), step_size_range=(-5.0, -1.0), level_drift_range=(-0.1, 0.0), weight=0.6, ), # Spike up pattern (10-20% of series) SubseriesConfig( pattern_type=StepPatternType.SPIKE_UP, length_range=(200, 500), num_changepoints_range=(3, 8), step_size_range=(3.0, 10.0), step_size_decay=0.7, weight=0.4, ), # Spike down pattern (10-20% of series) SubseriesConfig( pattern_type=StepPatternType.SPIKE_DOWN, length_range=(200, 500), num_changepoints_range=(3, 8), step_size_range=(-10.0, -3.0), step_size_decay=0.7, weight=0.4, ), # Oscillating pattern (10-15% of series) SubseriesConfig( pattern_type=StepPatternType.OSCILLATING, length_range=(400, 800), num_changepoints_range=(8, 20), step_size_range=(-4.0, 4.0), weight=0.3, ), # Random walk pattern (fallback) SubseriesConfig( pattern_type=StepPatternType.RANDOM_WALK, length_range=(100, 400), num_changepoints_range=(5, 20), step_size_range=(-3.0, 3.0), weight=0.2, ), ] ) # Minimum number of subseries to combine min_subseries: int = 10 max_subseries: int = 100 # Transition smoothing between subseries enable_smooth_transitions: bool = False transition_length: int = 5 # Base level and global parameters base_level_range: tuple[float, float] = (5.0, 15.0) noise_level_range: tuple[float, float] = (0.001, 0.01) # Seasonal component parameters add_seasonality: bool = True daily_seasonality_amplitude_range: tuple[float, float] = (0.0, 0.8) weekly_seasonality_amplitude_range: tuple[float, float] = (0.0, 0.7) # Trend parameters add_trend: bool = False trend_slope_range: tuple[float, float] = (-0.005, 0.005) # Scaling parameters scale_range: tuple[float, float] = (0.1, 10.0) # Anomaly injection parameters inject_anomalies: bool = False anomaly_probability: float = 0.02 anomaly_magnitude_range: tuple[float, float] = (2.0, 5.0) # Level continuity between subseries maintain_level_continuity: bool = True max_level_jump_between_subseries: float = 5.0 class AnomalyType(Enum): """Types of anomalies that can be generated.""" SPIKE_UP = "spike_up" SPIKE_DOWN = "spike_down" class MagnitudePattern(Enum): """Spike magnitude patterns.""" CONSTANT = "constant" # All spikes have similar magnitude INCREASING = "increasing" # Magnitude increases over time DECREASING = "decreasing" # Magnitude decreases over time CYCLICAL = "cyclical" # Magnitude follows a cyclical pattern RANDOM_BOUNDED = "random_bounded" # Random within bounds but with some correlation @dataclass class AnomalyGeneratorParams(GeneratorParams): """Parameters for anomaly time series generation.""" # Base signal parameters base_level_range: tuple[float, float] = (-100.0, 100.0) # Spike direction (50% up-only, 50% down-only series) spike_direction_probability: float = 0.5 # Probability of up-only vs down-only series # Periodicity parameters (uniform singles are always generated; variance/jitter ignored for base schedule) base_period_range: tuple[int, int] = (100, 300) # Base period between spike events period_variance: float = 0.0 # Not used for base schedule anymore # Series-level behavior probabilities cluster_series_probability: float = 0.25 # 25% of series add clusters near base spikes random_series_probability: float = 0.25 # 25% of series add random single spikes # Cluster augmentation parameters (relative to base uniform spikes) # Fraction of base spike events that will receive nearby extra spikes cluster_event_fraction: float = 0.3 # Number of additional spikes to add per selected event (upper bound exclusive like np.random.randint) cluster_additional_spikes_range: tuple[int, int] = (1, 4) # yields 1..3 # Offset window (in time steps) around the base spike for additional spikes (inclusive of negatives) cluster_offset_range: tuple[int, int] = (-10, 11) # yields [-10..10] # Random single spikes augmentation across the series (not tied to base events) # Number of random spikes as a fraction of the number of base spikes random_spike_fraction_of_base: float = 0.3 # Spike magnitude parameters magnitude_pattern: MagnitudePattern = MagnitudePattern.RANDOM_BOUNDED base_magnitude_range: tuple[float, float] = (10.0, 50.0) magnitude_correlation: float = 0.7 # Correlation between consecutive spike magnitudes (0-1) magnitude_trend_strength: float = 0.02 # Strength of increasing/decreasing trend cyclical_period_ratio: float = 0.3 # Ratio of cyclical period to series length # Noise parameters magnitude_noise: float = 0.1 # Random noise added to magnitude (as fraction of base magnitude) timing_jitter: float = 0.0 # Not used for base schedule anymore def __post_init__(self): """Validate parameters after initialization.""" if not (0 <= self.spike_direction_probability <= 1): raise ValueError("spike_direction_probability must be between 0 and 1") if not (0 <= self.period_variance <= 0.5): raise ValueError("period_variance must be between 0 and 0.5") if not (0 <= self.magnitude_correlation <= 1): raise ValueError("magnitude_correlation must be between 0 and 1") if self.base_period_range[0] >= self.base_period_range[1]: raise ValueError("base_period_range must have min < max") # Validate series-type probabilities if not (0.0 <= self.cluster_series_probability <= 1.0): raise ValueError("cluster_series_probability must be between 0 and 1") if not (0.0 <= self.random_series_probability <= 1.0): raise ValueError("random_series_probability must be between 0 and 1") if self.cluster_series_probability + self.random_series_probability > 1.0: raise ValueError("Sum of cluster_series_probability and random_series_probability must be <= 1") # Validate cluster augmentation if not (0.0 <= self.cluster_event_fraction <= 1.0): raise ValueError("cluster_event_fraction must be between 0 and 1") if self.cluster_additional_spikes_range[0] >= self.cluster_additional_spikes_range[1]: raise ValueError("cluster_additional_spikes_range must have min < max") if self.cluster_offset_range[0] >= self.cluster_offset_range[1]: raise ValueError("cluster_offset_range must have min < max") # Validate random augmentation if not (0.0 <= self.random_spike_fraction_of_base <= 1.0): raise ValueError("random_spike_fraction_of_base must be between 0 and 1") class SpikeShape(Enum): """Enumeration of spike shapes.""" V_SHAPE = "v" INVERTED_V = "inverted_v" CHOPPED_V = "chopped_v" CHOPPED_INVERTED_V = "chopped_inverted_v" @dataclass class SpikesGeneratorParams(GeneratorParams): """Parameters for spike time series generation.""" # Separate spike counts for different modes spike_count_burst: tuple[int, int] = (2, 4) spike_count_uniform: tuple[int, int] = (4, 7) # Spike amplitude parameters (absolute values, sign determined per series) spike_amplitude: float | tuple[float, float] = (50.0, 300.0) # Spike angle range in degrees (controls steepness) - sampled once per series spike_angle_range: tuple[float, float] = (70.0, 85.0) # Probability of burst mode vs spread mode (5% burst, 95% spread) burst_mode_probability: float = 0.05 # Plateau duration for chopped spikes (in time steps) plateau_duration: tuple[int, int] = (30, 50) # Baseline value (should be close to zero) baseline: float | tuple[float, float] = (-200, 200) # Burst clustering parameters - fraction of series length for burst width burst_width_fraction: tuple[float, float] = (0.1, 0.25) # Spread mode edge margin ratio: edges are set to this fraction of the # inter-spike spacing. Smaller values yield smaller left/right margins and # larger spacing between spikes. Example: 0.2 => edge margins are 20% of # the spacing between spikes. edge_margin_ratio: float = 0.2 # Probability of spikes being above baseline (vs below baseline) per series spikes_above_baseline_probability: float = 0.5 # Probability of each series type series_type_probabilities: dict[str, float] = field( default_factory=lambda: { "v_only": 0.4, "chopped_only": 0.3, "mixed": 0.3, } ) # Minimum spike width in time steps (to ensure visible spikes) min_spike_width: int = 30 # Maximum spike width in time steps (to prevent overly wide spikes) max_spike_width: int = 100 # Minimum margin between spikes (only used in burst mode) min_spike_margin: int = 10 # Noise parameters - applied to entire signal noise_std: float = 2 noise_probability: float = 0.5 brown_noise_alpha: float = 2.0 # Power law exponent (2.0 = brown noise) noise_cutoff_freq: float = 0.1 # Relative to Nyquist frequency @dataclass class CauKerGeneratorParams(GeneratorParams): """Parameters for the CauKer (SCM-GP) generator.""" # Number of channels (features) to sample per series. If a tuple(range) # or list is provided, the wrapper will pick a single value for the whole batch. num_channels: int | tuple[int, int] | list[int] = 6 # Maximum number of parents per node in the DAG max_parents: int = 3 # Total number of nodes in the underlying DAG num_nodes: int = 6 class TrendType(Enum): """Types of trends that can be applied to the OU process.""" NONE = "none" # No trend, classic OU behavior LINEAR = "linear" # Linear drift in mu over time EXPONENTIAL = "exponential" # Exponential growth/decay in mu LOGISTIC = "logistic" # S-curve growth pattern SINUSOIDAL = "sinusoidal" # Cyclical trend PIECEWISE_LINEAR = "piecewise_linear" # Multiple linear segments POLYNOMIAL = "polynomial" # Polynomial trend (quadratic/cubic) @dataclass class TrendConfig: """Configuration for time-varying trends in OU process parameters.""" trend_type: TrendType = TrendType.NONE # Linear trend parameters linear_slope_range: tuple[float, float] = (-0.01, 0.01) # Exponential trend parameters exp_rate_range: tuple[float, float] = (-0.005, 0.005) exp_asymptote_range: tuple[float, float] = (-5.0, 5.0) # Logistic trend parameters logistic_growth_rate_range: tuple[float, float] = (0.01, 0.1) logistic_capacity_range: tuple[float, float] = (5.0, 20.0) logistic_midpoint_ratio_range: tuple[float, float] = ( 0.3, 0.7, ) # As fraction of series length # Sinusoidal trend parameters sin_amplitude_range: tuple[float, float] = (1.0, 5.0) sin_period_ratio_range: tuple[float, float] = ( 0.1, 0.5, ) # As fraction of series length sin_phase_range: tuple[float, float] = (0.0, 2.0 * np.pi) # Piecewise linear parameters num_segments_range: tuple[int, int] = (2, 5) segment_slope_range: tuple[float, float] = (-0.02, 0.02) # Polynomial trend parameters poly_degree_range: tuple[int, int] = (2, 3) poly_coeff_range: tuple[float, float] = ( -1e-6, 1e-6, ) # Small coefficients for stability # Structural change parameters enable_structural_changes: bool = True num_structural_changes_range: tuple[int, int] = (0, 3) structural_change_magnitude_range: tuple[float, float] = (1.0, 5.0) min_segment_length: int = 200 # Minimum length between structural changes @dataclass class OrnsteinUhlenbeckProcessGeneratorParams(GeneratorParams): """Parameters for the Regime-Switching Ornstein-Uhlenbeck generator. The generator samples concrete values per series using these ranges. Enhanced with time-varying parameter support for realistic non-stationary behavior. """ # Integration step size used inside the generator dt: float = 0.01 # Regime 0 parameter distributions regime0_theta_range: tuple[float, float] = (1.0, 5.0) regime0_mu_mean_std: tuple[float, float] = (-2.0, 1.0) regime0_sigma_lognormal_params: tuple[float, float] = (float(np.log(0.3)), 0.3) # Regime 0 volatility process parameters regime0_vol_reversion_range: tuple[float, float] = (2.0, 5.0) # kappa_v regime0_vol_mean_range: tuple[float, float] = (0.2, 0.4) # theta_v regime0_vol_vol_range: tuple[float, float] = (0.1, 0.3) # xi_v # Regime 1 parameter distributions regime1_theta_range: tuple[float, float] = (0.05, 0.5) regime1_mu_mean_std: tuple[float, float] = (2.0, 1.0) regime1_sigma_lognormal_params: tuple[float, float] = (float(np.log(1.5)), 0.5) # Regime 1 volatility process parameters regime1_vol_reversion_range: tuple[float, float] = (0.5, 2.0) # kappa_v regime1_vol_mean_range: tuple[float, float] = (0.8, 1.2) # theta_v regime1_vol_vol_range: tuple[float, float] = (0.3, 0.5) # xi_v # Initial value distributions x0_mean_std: tuple[float, float] = (0.0, 2.0) # Transition matrix diagonal probabilities (allow more frequent regime changes) p00_range: tuple[float, float] = (0.85, 0.999) # Allow more frequent transitions p11_range: tuple[float, float] = (0.85, 0.999) # Time-varying parameter support trend_config: TrendConfig = field(default_factory=TrendConfig) # Probability of applying trends to different parameters mu_trend_probability: float = 0.7 # High probability for realistic non-stationarity theta_trend_probability: float = 0.2 # Occasional changes in mean reversion speed sigma_trend_probability: float = 0.3 # Occasional changes in volatility # Global scaling and level parameters for real-world applicability global_level_range: tuple[float, float] = ( -100.0, 100.0, ) # Base level around which process evolves global_scale_range: tuple[float, float] = ( 0.1, 50.0, ) # Scale factor for entire series # Noise injection for additional realism measurement_noise_std_range: tuple[float, float] = ( 0.0, 0.1, ) # Additive measurement noise # Long-term memory parameters (for more realistic autocorrelation) enable_long_memory: bool = False hurst_exponent_range: tuple[float, float] = ( 0.3, 0.8, ) # Fractional Brownian motion component # Seasonality parameters enable_seasonality: bool = True num_seasonal_components_range: tuple[int, int] = ( 1, 3, ) # Number of seasonal components seasonal_periods: tuple[float, ...] = ( 7.0, # Weekly 30.0, # Monthly 90.0, # Quarterly 365.25, # Yearly 182.625, # Semi-annual ) # Available seasonal periods (in time units) seasonal_amplitude_range: tuple[float, float] = ( 0.5, 3.0, ) # Amplitude of seasonal components seasonal_phase_range: tuple[float, float] = (0.0, 2.0 * np.pi) # Phase shift range seasonal_period_jitter: float = 0.05 # Jitter applied to periods for realism (±5%) # Probability of applying seasonality to different parameters mu_seasonality_probability: float = 0.6 # Probability of seasonal mean sigma_seasonality_probability: float = 0.3 # Probability of seasonal volatility # Seasonal component decay/growth over time enable_seasonal_evolution: bool = True seasonal_amplitude_trend_range: tuple[float, float] = ( -0.001, 0.001, ) # Trend in seasonal amplitude def __post_init__(self): if self.dt <= 0: raise ValueError("dt must be positive for OU process simulation") if not (0.0 <= self.mu_trend_probability <= 1.0): raise ValueError("mu_trend_probability must be between 0 and 1") if not (0.0 <= self.theta_trend_probability <= 1.0): raise ValueError("theta_trend_probability must be between 0 and 1") if not (0.0 <= self.sigma_trend_probability <= 1.0): raise ValueError("sigma_trend_probability must be between 0 and 1") if self.global_level_range[0] >= self.global_level_range[1]: raise ValueError("global_level_range must have min < max") if self.global_scale_range[0] <= 0: raise ValueError("global_scale_range values must be positive") # ===================== # Audio generator params # ===================== @dataclass class AudioGeneratorParams(GeneratorParams): """Common parameters for audio-based time series generators (pyo-backed).""" # Offline pyo rendering configuration server_duration: float = 2.0 # seconds sample_rate: int = 44100 # Hz # Output post-processing normalize_output: bool = True # Normalize to unit max abs before returning @dataclass class FinancialVolatilityAudioParams(AudioGeneratorParams): """Parameters for the FinancialVolatility audio generator.""" # Trend LFO controlling slow drift trend_lfo_freq_range: tuple[float, float] = (0.1, 0.5) trend_lfo_mul_range: tuple[float, float] = (0.2, 0.5) # Volatility clustering volatility_carrier_freq_range: tuple[float, float] = (1.0, 5.0) follower_freq_range: tuple[float, float] = (1.0, 4.0) volatility_range: tuple[float, float] = (0.1, 0.8) # Market jumps/shocks jump_metro_time_range: tuple[float, float] = (0.3, 1.0) jump_env_start_range: tuple[float, float] = (0.5, 1.0) jump_env_decay_time_range: tuple[float, float] = (0.05, 0.2) jump_freq_range: tuple[float, float] = (20.0, 80.0) jump_direction_up_probability: float = 0.5 @dataclass class MultiScaleFractalAudioParams(AudioGeneratorParams): """Parameters for the Multi-Scale Fractal audio generator.""" base_noise_mul_range: tuple[float, float] = (0.3, 0.8) num_scales_range: tuple[int, int] = (3, 6) scale_freq_base_range: tuple[float, float] = (20.0, 2000.0) q_factor_range: tuple[float, float] = (0.5, 3.0) per_scale_attenuation_range: tuple[float, float] = ( 0.5, 0.8, ) # multiplier per scale index @dataclass class StochasticRhythmAudioParams(AudioGeneratorParams): """Parameters for the Stochastic Rhythm audio generator.""" base_tempo_hz_range: tuple[float, float] = (2.0, 8.0) num_layers_range: tuple[int, int] = (3, 5) subdivisions: tuple[int, ...] = (1, 2, 3, 4, 6, 8) attack_range: tuple[float, float] = (0.001, 0.01) decay_range: tuple[float, float] = (0.05, 0.3) tone_freq_range: tuple[float, float] = (50.0, 800.0) tone_mul_range: tuple[float, float] = (0.2, 0.5) @dataclass class NetworkTopologyAudioParams(AudioGeneratorParams): """Parameters for the Network Topology audio generator.""" # Base traffic flow traffic_lfo_freq_range: tuple[float, float] = (0.2, 1.0) traffic_lfo_mul_range: tuple[float, float] = (0.2, 0.5) # Packet bursts burst_rate_hz_range: tuple[float, float] = (3.0, 12.0) burst_duration_range: tuple[float, float] = (0.02, 0.1) burst_mul_range: tuple[float, float] = (0.2, 0.6) # Periodic congestion congestion_period_range: tuple[float, float] = (1.0, 3.0) # seconds between events congestion_depth_range: tuple[float, float] = (-0.6, -0.2) congestion_release_time_range: tuple[float, float] = (0.3, 0.8) # Protocol overhead overhead_lfo_freq_range: tuple[float, float] = (20.0, 50.0) overhead_mul_range: tuple[float, float] = (0.05, 0.15) # DDoS-like spikes / attacks attack_period_range: tuple[float, float] = (2.0, 5.0) attack_env_points: tuple[tuple[float, float], tuple[float, float], tuple[float, float]] = ( (0.0, 1.2), (0.1, 0.8), (0.8, 0.0), ) attack_mul_range: tuple[float, float] = (0.4, 0.8)