///|
/// Deterministic synthetic data scenario.
pub struct SyntheticScenario {
values : Array[Double]
clean : Array[Double]
anomaly_indices : Array[Int]
seed : Int
name : String
}
///|
/// Parameters for production-like fixture generation.
pub struct SyntheticRule {
length : Int
level : Double
trend : Double
seasonal_period : Int
seasonal_amplitude : Double
noise_scale : Double
outlier_rate : Double
outlier_scale : Double
seed : Int
}
///|
pub fn synthetic_default_rule() -> SyntheticRule {
{
length: 256,
level: 100.0,
trend: 0.02,
seasonal_period: 7,
seasonal_amplitude: 2.0,
noise_scale: 0.5,
outlier_rate: 0.02,
outlier_scale: 20.0,
seed: 20260819,
}
}
///|
pub fn synthetic_rule(
length : Int,
level : Double,
trend : Double,
seasonal_period : Int,
seasonal_amplitude : Double,
noise_scale : Double,
outlier_rate : Double,
outlier_scale : Double,
seed : Int,
) -> SyntheticRule {
{
length: if length < 0 {
0
} else {
length
},
level,
trend,
seasonal_period: if seasonal_period < 2 {
2
} else {
seasonal_period
},
seasonal_amplitude: if seasonal_amplitude < 0.0 {
-seasonal_amplitude
} else {
seasonal_amplitude
},
noise_scale: if noise_scale < 0.0 {
-noise_scale
} else {
noise_scale
},
outlier_rate: if outlier_rate < 0.0 {
0.0
} else if outlier_rate > 1.0 {
1.0
} else {
outlier_rate
},
outlier_scale: if outlier_scale < 0.0 {
-outlier_scale
} else {
outlier_scale
},
seed,
}
}
///|
fn synthetic_safe_length(length : Int) -> Int {
if length < 0 {
0
} else {
length
}
}
///|
pub fn synthetic_uniform(length : Int, seed : Int) -> Array[Double] {
let result : Array[Double] = []
let rng = DeterministicRng::new(seed)
let count = synthetic_safe_length(length)
for _ in 0.. Array[Double] {
let values = synthetic_uniform(length, seed)
let result = []
let amplitude = if scale < 0.0 { -scale } else { scale }
for value in values {
result.push((value * 2.0 - 1.0) * amplitude)
}
result
}
///|
pub fn synthetic_level(length : Int, level : Double) -> Array[Double] {
let result = []
let count = synthetic_safe_length(length)
for _ in 0.. Array[Double] {
let result = []
let count = synthetic_safe_length(length)
for index = 0; index < count; index = index + 1 {
result.push(level + slope * index.to_double())
}
result
}
///|
pub fn synthetic_seasonal(
length : Int,
period : Int,
amplitude : Double,
) -> Array[Double] {
let safe_period = if period < 2 { 2 } else { period }
let result = []
let count = synthetic_safe_length(length)
for index = 0; index < count; index = index + 1 {
let phase = (index % safe_period).to_double() / safe_period.to_double()
result.push(amplitude * (phase * 2.0 - 1.0))
}
result
}
///|
pub fn synthetic_clean(rule : SyntheticRule) -> Array[Double] {
let trend = synthetic_trend(rule.length, rule.level, rule.trend)
let seasonal = synthetic_seasonal(
rule.length,
rule.seasonal_period,
rule.seasonal_amplitude,
)
let noise = synthetic_noise(rule.length, rule.noise_scale, rule.seed)
let result = []
for index = 0; index < rule.length; index = index + 1 {
result.push(trend[index] + seasonal[index] + noise[index])
}
result
}
///|
pub fn synthetic_anomaly_indices(
length : Int,
rate : Double,
seed : Int,
) -> Array[Int] {
let result = []
if length <= 0 {
return result
}
let rng = DeterministicRng::new(seed)
for index = 0; index < length; index = index + 1 {
if rng.next_unit() < rate {
result.push(index)
}
}
result
}
///|
pub fn synthetic_inject_spikes(
clean : Array[Double],
rate : Double,
scale : Double,
seed : Int,
) -> SyntheticScenario {
let values = clean.copy()
let indices = synthetic_anomaly_indices(clean.length(), rate, seed)
let rng = DeterministicRng::new(seed + 17)
for index in indices {
let sign = if rng.next_unit() < 0.5 { -1.0 } else { 1.0 }
values[index] += sign * scale * (0.5 + rng.next_unit())
}
{ values, clean, anomaly_indices: indices, seed, name: "spikes" }
}
///|
pub fn synthetic_inject_level_shift(
clean : Array[Double],
start : Int,
shift : Double,
) -> SyntheticScenario {
let values = clean.copy()
let index = if start < 0 { 0 } else { start }
for cursor = index; cursor < values.length(); cursor = cursor + 1 {
values[cursor] += shift
}
{ values, clean, anomaly_indices: [], seed: 0, name: "level-shift" }
}
///|
pub fn synthetic_inject_variance_shift(
clean : Array[Double],
start : Int,
scale : Double,
seed : Int,
) -> SyntheticScenario {
let values = clean.copy()
let rng = DeterministicRng::new(seed)
let index = if start < 0 { 0 } else { start }
for cursor = index; cursor < values.length(); cursor = cursor + 1 {
values[cursor] += (rng.next_unit() * 2.0 - 1.0) * scale
}
{ values, clean, anomaly_indices: [], seed, name: "variance-shift" }
}
///|
pub fn synthetic_scenario(rule : SyntheticRule) -> SyntheticScenario {
synthetic_inject_spikes(
synthetic_clean(rule),
rule.outlier_rate,
rule.outlier_scale,
rule.seed,
)
}
///|
pub fn synthetic_scenario_values(scenario : SyntheticScenario) -> Array[Double] {
scenario.values.copy()
}
///|
pub fn synthetic_scenario_clean(scenario : SyntheticScenario) -> Array[Double] {
scenario.clean.copy()
}
///|
pub fn synthetic_scenario_labels(scenario : SyntheticScenario) -> Array[Bool] {
let labels = []
for _ in scenario.values {
labels.push(false)
}
for index in scenario.anomaly_indices {
if index >= 0 && index < labels.length() {
labels[index] = true
}
}
labels
}
///|
pub fn synthetic_scenario_detection_score(
scenario : SyntheticScenario,
threshold : Double,
) -> Double {
let predicted = outlier_indices_z(scenario.values, threshold~)
let truth = synthetic_scenario_labels(scenario)
let predicted_labels = []
for _ in scenario.values {
predicted_labels.push(false)
}
for index in predicted {
if index >= 0 && index < predicted_labels.length() {
predicted_labels[index] = true
}
}
f1_score(confusion_matrix(truth, predicted_labels))
}
///|
pub fn synthetic_mixture(
left : Array[Double],
right : Array[Double],
weight : Double,
) -> Array[Double] {
let result = []
let probability = if weight < 0.0 {
0.0
} else if weight > 1.0 {
1.0
} else {
weight
}
for value in left {
result.push(value * probability)
}
for value in right {
result.push(value * (1.0 - probability))
}
result
}
///|
pub fn synthetic_correlated(
length : Int,
correlation : Double,
seed : Int,
) -> Array[Array[Double]] {
let first = synthetic_noise(length, 1.0, seed)
let second_noise = synthetic_noise(length, 1.0, seed + 1)
let coefficient = if correlation < -1.0 {
-1.0
} else if correlation > 1.0 {
1.0
} else {
correlation
}
let second = []
for index = 0; index < first.length(); index = index + 1 {
second.push(
coefficient * first[index] +
(1.0 - abs_double(coefficient)) * second_noise[index],
)
}
[first, second]
}
///|
pub fn synthetic_missing(
data : Array[Double],
rate : Double,
seed : Int,
) -> Array[Double] {
let result = data.copy()
let rng = DeterministicRng::new(seed)
for index = 0; index < result.length(); index = index + 1 {
if rng.next_unit() < rate {
result[index] = result[index] / 0.0
}
}
result
}
///|
pub fn synthetic_duplicate_blocks(
data : Array[Double],
block_size : Int,
copies : Int,
) -> Array[Double] {
let result = []
let width = if block_size < 1 { 1 } else { block_size }
let count = if copies < 1 { 1 } else { copies }
for start = 0; start < data.length(); start = start + width {
let end = if start + width > data.length() {
data.length()
} else {
start + width
}
for _ in 0.. Array[Double] {
let result = []
let safe_period = if period < 1 { 1 } else { period }
for index = 0; index < data.length(); index = index + 1 {
let scale = if index / safe_period % 2 == 0 {
first_scale
} else {
second_scale
}
result.push(data[index] * scale)
}
result
}
///|
pub fn synthetic_step_signal(
length : Int,
start : Int,
before : Double,
after : Double,
) -> Array[Double] {
let result = []
let count = synthetic_safe_length(length)
for index = 0; index < count; index = index + 1 {
result.push(if index < start { before } else { after })
}
result
}
///|
pub fn synthetic_ramp_signal(
length : Int,
start : Int,
slope : Double,
) -> Array[Double] {
let result = []
let count = synthetic_safe_length(length)
for index = 0; index < count; index = index + 1 {
result.push(
if index < start {
0.0
} else {
(index - start).to_double() * slope
},
)
}
result
}
///|
pub fn synthetic_pulse_signal(
length : Int,
start : Int,
duration : Int,
amplitude : Double,
) -> Array[Double] {
let result = []
let safe_duration = if duration < 0 { 0 } else { duration }
let end = start + safe_duration
let count = synthetic_safe_length(length)
for index = 0; index < count; index = index + 1 {
result.push(if index >= start && index < end { amplitude } else { 0.0 })
}
result
}
///|
pub fn synthetic_periodic_outliers(
data : Array[Double],
period : Int,
amplitude : Double,
) -> SyntheticScenario {
let values = data.copy()
let indices = []
let safe_period = if period < 1 { 1 } else { period }
for index = 0; index < values.length(); index = index + 1 {
if index % safe_period == 0 {
values[index] += amplitude
indices.push(index)
}
}
{
values,
clean: data,
anomaly_indices: indices,
seed: 0,
name: "periodic-outliers",
}
}
///|
pub fn synthetic_score_summary(scenario : SyntheticScenario) -> Array[Double] {
let detected = outlier_indices_z(scenario.values, threshold=3.5)
let truth = synthetic_scenario_labels(scenario)
let predicted = []
for _ in truth {
predicted.push(false)
}
for index in detected {
if index >= 0 && index < predicted.length() {
predicted[index] = true
}
}
let matrix = confusion_matrix(truth, predicted)
[
precision(matrix),
recall(matrix),
f1_score(matrix),
matthews_correlation(matrix),
benchmark_contamination_fraction(scenario.values),
]
}
///|
pub fn synthetic_robust_gain(scenario : SyntheticScenario) -> Double {
let raw_error = abs_double(mean(scenario.values) - mean(scenario.clean))
let robust_error = abs_double(
median(scenario.values) - median(scenario.clean),
)
if raw_error == 0.0 {
0.0
} else {
(raw_error - robust_error) / raw_error
}
}
///|
pub fn synthetic_reproducible(rule : SyntheticRule) -> Bool {
let left = synthetic_scenario(rule)
let right = synthetic_scenario(rule)
left.values == right.values && left.anomaly_indices == right.anomaly_indices
}
///|
pub fn synthetic_seed_sweep(
rule : SyntheticRule,
seeds : Array[Int],
) -> Array[Double] {
let result = []
for seed in seeds {
let variant = synthetic_scenario({ ..rule, seed, })
result.push(synthetic_robust_gain(variant))
}
result
}
///|
pub fn synthetic_length_sweep(
rule : SyntheticRule,
lengths : Array[Int],
) -> Array[Double] {
let result = []
for length in lengths {
let variant = synthetic_scenario({ ..rule, length, })
result.push(synthetic_robust_gain(variant))
}
result
}
///|
pub fn synthetic_contamination_sweep(
rule : SyntheticRule,
rates : Array[Double],
) -> Array[Double] {
let result = []
for rate in rates {
let variant = synthetic_scenario({ ..rule, outlier_rate: rate })
result.push(synthetic_robust_gain(variant))
}
result
}
///|
pub fn synthetic_noise_sweep(
rule : SyntheticRule,
scales : Array[Double],
) -> Array[Double] {
let result = []
for scale in scales {
let variant = synthetic_scenario({ ..rule, noise_scale: scale })
result.push(synthetic_robust_gain(variant))
}
result
}
///|
pub fn synthetic_compare_estimators(
scenario : SyntheticScenario,
) -> Array[Double] {
[
abs_double(mean(scenario.values) - mean(scenario.clean)),
abs_double(median(scenario.values) - median(scenario.clean)),
abs_double(huber_location(scenario.values) - huber_location(scenario.clean)),
abs_double(
trimmed_mean(scenario.values, 0.1) - trimmed_mean(scenario.clean, 0.1),
),
abs_double(
winsorized_mean(scenario.values, 0.1) -
winsorized_mean(scenario.clean, 0.1),
),
]
}
///|
pub fn synthetic_estimator_rank(scenario : SyntheticScenario) -> Array[Int] {
let errors = synthetic_compare_estimators(scenario)
let indices = []
for index = 0; index < errors.length(); index = index + 1 {
indices.push(index)
}
for left = 0; left < indices.length(); left = left + 1 {
for right = left + 1; right < indices.length(); right = right + 1 {
if errors[indices[right]] < errors[indices[left]] {
let value = indices[left]
indices[left] = indices[right]
indices[right] = value
}
}
}
indices
}
///|
pub fn synthetic_residuals(scenario : SyntheticScenario) -> Array[Double] {
let result = []
for index = 0
index < scenario.values.length() && index < scenario.clean.length()
index = index + 1 {
result.push(scenario.values[index] - scenario.clean[index])
}
result
}
///|
pub fn synthetic_residual_summary(
scenario : SyntheticScenario,
) -> Array[Double] {
let residuals = synthetic_residuals(scenario)
[
mean(residuals),
mad(residuals),
sample_stddev(residuals),
max_value(residuals),
min_value(residuals),
]
}