Skip to main content

vector_ta/indicators/
smooth_theil_sen.rs

1#[cfg(feature = "python")]
2use numpy::{IntoPyArray, PyArray1, PyArrayMethods, PyReadonlyArray1};
3#[cfg(feature = "python")]
4use pyo3::exceptions::PyValueError;
5#[cfg(feature = "python")]
6use pyo3::prelude::*;
7#[cfg(feature = "python")]
8use pyo3::types::PyDict;
9#[cfg(feature = "python")]
10use pyo3::wrap_pyfunction;
11
12#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
13use serde::{Deserialize, Serialize};
14#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
15use wasm_bindgen::prelude::*;
16
17use crate::utilities::data_loader::{source_type, Candles};
18use crate::utilities::enums::Kernel;
19use crate::utilities::helpers::{
20    alloc_with_nan_prefix, detect_best_batch_kernel, init_matrix_prefixes, make_uninit_matrix,
21};
22#[cfg(feature = "python")]
23use crate::utilities::kernel_validation::validate_kernel;
24#[cfg(not(target_arch = "wasm32"))]
25use rayon::prelude::*;
26use std::collections::VecDeque;
27use std::mem::ManuallyDrop;
28use std::str::FromStr;
29use thiserror::Error;
30
31const DEFAULT_SOURCE: &str = "close";
32const DEFAULT_LENGTH: usize = 25;
33const DEFAULT_OFFSET: usize = 0;
34const DEFAULT_MULTIPLIER: f64 = 2.0;
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37#[cfg_attr(
38    all(target_arch = "wasm32", feature = "wasm"),
39    derive(Serialize, Deserialize),
40    serde(rename_all = "snake_case")
41)]
42pub enum SmoothTheilSenStatStyle {
43    Mean,
44    SmoothMedian,
45    Median,
46}
47
48impl Default for SmoothTheilSenStatStyle {
49    fn default() -> Self {
50        Self::SmoothMedian
51    }
52}
53
54impl SmoothTheilSenStatStyle {
55    #[inline(always)]
56    fn blend(self) -> f64 {
57        match self {
58            Self::Mean => 1.0,
59            Self::SmoothMedian => 0.5,
60            Self::Median => 0.0,
61        }
62    }
63
64    #[inline(always)]
65    fn as_str(self) -> &'static str {
66        match self {
67            Self::Mean => "mean",
68            Self::SmoothMedian => "smooth_median",
69            Self::Median => "median",
70        }
71    }
72}
73
74impl FromStr for SmoothTheilSenStatStyle {
75    type Err = String;
76
77    fn from_str(value: &str) -> Result<Self, Self::Err> {
78        let normalized = value.trim().to_ascii_lowercase();
79        match normalized.as_str() {
80            "mean" => Ok(Self::Mean),
81            "smooth_median" | "smooth median" => Ok(Self::SmoothMedian),
82            "median" => Ok(Self::Median),
83            _ => Err(format!("invalid stat_style: {value}")),
84        }
85    }
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89#[cfg_attr(
90    all(target_arch = "wasm32", feature = "wasm"),
91    derive(Serialize, Deserialize),
92    serde(rename_all = "snake_case")
93)]
94pub enum SmoothTheilSenDeviationType {
95    Mad,
96    Rmsd,
97}
98
99impl Default for SmoothTheilSenDeviationType {
100    fn default() -> Self {
101        Self::Mad
102    }
103}
104
105impl SmoothTheilSenDeviationType {
106    #[inline(always)]
107    fn as_str(self) -> &'static str {
108        match self {
109            Self::Mad => "mad",
110            Self::Rmsd => "rmsd",
111        }
112    }
113}
114
115impl FromStr for SmoothTheilSenDeviationType {
116    type Err = String;
117
118    fn from_str(value: &str) -> Result<Self, Self::Err> {
119        let normalized = value.trim().to_ascii_lowercase();
120        match normalized.as_str() {
121            "mad" | "median_absolute_deviation" => Ok(Self::Mad),
122            "rmsd" | "root_mean_square_deviation" => Ok(Self::Rmsd),
123            _ => Err(format!("invalid deviation_style: {value}")),
124        }
125    }
126}
127
128#[derive(Debug, Clone)]
129pub enum SmoothTheilSenData<'a> {
130    Candles {
131        candles: &'a Candles,
132        source: &'a str,
133    },
134    Slice(&'a [f64]),
135}
136
137#[derive(Debug, Clone)]
138pub struct SmoothTheilSenOutput {
139    pub value: Vec<f64>,
140    pub upper: Vec<f64>,
141    pub lower: Vec<f64>,
142    pub slope: Vec<f64>,
143    pub intercept: Vec<f64>,
144    pub deviation: Vec<f64>,
145}
146
147#[derive(Debug, Clone)]
148#[cfg_attr(
149    all(target_arch = "wasm32", feature = "wasm"),
150    derive(Serialize, Deserialize)
151)]
152pub struct SmoothTheilSenParams {
153    pub length: Option<usize>,
154    pub offset: Option<usize>,
155    pub multiplier: Option<f64>,
156    pub slope_style: Option<SmoothTheilSenStatStyle>,
157    pub residual_style: Option<SmoothTheilSenStatStyle>,
158    pub deviation_style: Option<SmoothTheilSenDeviationType>,
159    pub mad_style: Option<SmoothTheilSenStatStyle>,
160    pub include_prediction_in_deviation: Option<bool>,
161}
162
163impl Default for SmoothTheilSenParams {
164    fn default() -> Self {
165        Self {
166            length: Some(DEFAULT_LENGTH),
167            offset: Some(DEFAULT_OFFSET),
168            multiplier: Some(DEFAULT_MULTIPLIER),
169            slope_style: Some(SmoothTheilSenStatStyle::SmoothMedian),
170            residual_style: Some(SmoothTheilSenStatStyle::SmoothMedian),
171            deviation_style: Some(SmoothTheilSenDeviationType::Mad),
172            mad_style: Some(SmoothTheilSenStatStyle::SmoothMedian),
173            include_prediction_in_deviation: Some(false),
174        }
175    }
176}
177
178#[derive(Debug, Clone)]
179pub struct SmoothTheilSenInput<'a> {
180    pub data: SmoothTheilSenData<'a>,
181    pub params: SmoothTheilSenParams,
182}
183
184impl<'a> SmoothTheilSenInput<'a> {
185    #[inline]
186    pub fn from_candles(
187        candles: &'a Candles,
188        source: &'a str,
189        params: SmoothTheilSenParams,
190    ) -> Self {
191        Self {
192            data: SmoothTheilSenData::Candles { candles, source },
193            params,
194        }
195    }
196
197    #[inline]
198    pub fn from_slice(data: &'a [f64], params: SmoothTheilSenParams) -> Self {
199        Self {
200            data: SmoothTheilSenData::Slice(data),
201            params,
202        }
203    }
204
205    #[inline]
206    pub fn with_default_candles(candles: &'a Candles) -> Self {
207        Self::from_candles(candles, DEFAULT_SOURCE, SmoothTheilSenParams::default())
208    }
209}
210
211#[derive(Copy, Clone, Debug)]
212pub struct SmoothTheilSenBuilder {
213    source: Option<&'static str>,
214    length: Option<usize>,
215    offset: Option<usize>,
216    multiplier: Option<f64>,
217    slope_style: Option<SmoothTheilSenStatStyle>,
218    residual_style: Option<SmoothTheilSenStatStyle>,
219    deviation_style: Option<SmoothTheilSenDeviationType>,
220    mad_style: Option<SmoothTheilSenStatStyle>,
221    include_prediction_in_deviation: Option<bool>,
222    kernel: Kernel,
223}
224
225impl Default for SmoothTheilSenBuilder {
226    fn default() -> Self {
227        Self {
228            source: None,
229            length: None,
230            offset: None,
231            multiplier: None,
232            slope_style: None,
233            residual_style: None,
234            deviation_style: None,
235            mad_style: None,
236            include_prediction_in_deviation: None,
237            kernel: Kernel::Auto,
238        }
239    }
240}
241
242impl SmoothTheilSenBuilder {
243    #[inline(always)]
244    pub fn new() -> Self {
245        Self::default()
246    }
247
248    #[inline(always)]
249    pub fn source(mut self, value: &'static str) -> Self {
250        self.source = Some(value);
251        self
252    }
253
254    #[inline(always)]
255    pub fn length(mut self, value: usize) -> Self {
256        self.length = Some(value);
257        self
258    }
259
260    #[inline(always)]
261    pub fn offset(mut self, value: usize) -> Self {
262        self.offset = Some(value);
263        self
264    }
265
266    #[inline(always)]
267    pub fn multiplier(mut self, value: f64) -> Self {
268        self.multiplier = Some(value);
269        self
270    }
271
272    #[inline(always)]
273    pub fn slope_style(mut self, value: SmoothTheilSenStatStyle) -> Self {
274        self.slope_style = Some(value);
275        self
276    }
277
278    #[inline(always)]
279    pub fn residual_style(mut self, value: SmoothTheilSenStatStyle) -> Self {
280        self.residual_style = Some(value);
281        self
282    }
283
284    #[inline(always)]
285    pub fn deviation_style(mut self, value: SmoothTheilSenDeviationType) -> Self {
286        self.deviation_style = Some(value);
287        self
288    }
289
290    #[inline(always)]
291    pub fn mad_style(mut self, value: SmoothTheilSenStatStyle) -> Self {
292        self.mad_style = Some(value);
293        self
294    }
295
296    #[inline(always)]
297    pub fn include_prediction_in_deviation(mut self, value: bool) -> Self {
298        self.include_prediction_in_deviation = Some(value);
299        self
300    }
301
302    #[inline(always)]
303    pub fn kernel(mut self, value: Kernel) -> Self {
304        self.kernel = value;
305        self
306    }
307
308    #[inline(always)]
309    pub fn apply(self, candles: &Candles) -> Result<SmoothTheilSenOutput, SmoothTheilSenError> {
310        let input = SmoothTheilSenInput::from_candles(
311            candles,
312            self.source.unwrap_or(DEFAULT_SOURCE),
313            SmoothTheilSenParams {
314                length: self.length,
315                offset: self.offset,
316                multiplier: self.multiplier,
317                slope_style: self.slope_style,
318                residual_style: self.residual_style,
319                deviation_style: self.deviation_style,
320                mad_style: self.mad_style,
321                include_prediction_in_deviation: self.include_prediction_in_deviation,
322            },
323        );
324        smooth_theil_sen_with_kernel(&input, self.kernel)
325    }
326
327    #[inline(always)]
328    pub fn apply_slice(self, data: &[f64]) -> Result<SmoothTheilSenOutput, SmoothTheilSenError> {
329        let input = SmoothTheilSenInput::from_slice(
330            data,
331            SmoothTheilSenParams {
332                length: self.length,
333                offset: self.offset,
334                multiplier: self.multiplier,
335                slope_style: self.slope_style,
336                residual_style: self.residual_style,
337                deviation_style: self.deviation_style,
338                mad_style: self.mad_style,
339                include_prediction_in_deviation: self.include_prediction_in_deviation,
340            },
341        );
342        smooth_theil_sen_with_kernel(&input, self.kernel)
343    }
344
345    #[inline(always)]
346    pub fn into_stream(self) -> Result<SmoothTheilSenStream, SmoothTheilSenError> {
347        SmoothTheilSenStream::try_new(SmoothTheilSenParams {
348            length: self.length,
349            offset: self.offset,
350            multiplier: self.multiplier,
351            slope_style: self.slope_style,
352            residual_style: self.residual_style,
353            deviation_style: self.deviation_style,
354            mad_style: self.mad_style,
355            include_prediction_in_deviation: self.include_prediction_in_deviation,
356        })
357    }
358}
359
360#[derive(Debug, Error)]
361pub enum SmoothTheilSenError {
362    #[error("smooth_theil_sen: Input data slice is empty.")]
363    EmptyInputData,
364    #[error("smooth_theil_sen: All values are NaN.")]
365    AllValuesNaN,
366    #[error("smooth_theil_sen: Invalid length: {length}")]
367    InvalidLength { length: usize },
368    #[error("smooth_theil_sen: Invalid multiplier: {multiplier}")]
369    InvalidMultiplier { multiplier: f64 },
370    #[error("smooth_theil_sen: Invalid source: {source_name}")]
371    InvalidSource { source_name: String },
372    #[error("smooth_theil_sen: Not enough valid data: needed={needed}, valid={valid}")]
373    NotEnoughValidData { needed: usize, valid: usize },
374    #[error("smooth_theil_sen: Output length mismatch: expected={expected}, got={got}")]
375    OutputLengthMismatch { expected: usize, got: usize },
376    #[error("smooth_theil_sen: Invalid range: start={start}, end={end}, step={step}")]
377    InvalidRange {
378        start: String,
379        end: String,
380        step: String,
381    },
382    #[error("smooth_theil_sen: Invalid kernel for batch: {0:?}")]
383    InvalidKernelForBatch(Kernel),
384}
385
386#[derive(Clone, Copy, Debug)]
387struct ResolvedParams {
388    length: usize,
389    offset: usize,
390    multiplier: f64,
391    slope_style: SmoothTheilSenStatStyle,
392    residual_style: SmoothTheilSenStatStyle,
393    deviation_style: SmoothTheilSenDeviationType,
394    mad_style: SmoothTheilSenStatStyle,
395    include_prediction_in_deviation: bool,
396}
397
398#[derive(Clone, Debug)]
399struct KernelCache {
400    slope_weights: Option<Vec<f64>>,
401    residual_weights: Option<Vec<f64>>,
402    error_weights: Option<Vec<f64>>,
403}
404
405#[derive(Clone, Debug)]
406struct WorkBuffers {
407    slopes: Vec<f64>,
408    residuals: Vec<f64>,
409    errors: Vec<f64>,
410}
411
412#[derive(Clone, Copy, Debug)]
413struct PointOutput {
414    value: f64,
415    upper: f64,
416    lower: f64,
417    slope: f64,
418    intercept: f64,
419    deviation: f64,
420}
421
422#[inline(always)]
423fn extract_slice<'a>(input: &'a SmoothTheilSenInput<'a>) -> Result<&'a [f64], SmoothTheilSenError> {
424    let data = match &input.data {
425        SmoothTheilSenData::Candles { candles, source } => {
426            let source_values = source_type(candles, source);
427            if source_values.is_empty() {
428                return Err(SmoothTheilSenError::InvalidSource {
429                    source_name: (*source).to_string(),
430                });
431            }
432            source_values
433        }
434        SmoothTheilSenData::Slice(data) => *data,
435    };
436    if data.is_empty() {
437        return Err(SmoothTheilSenError::EmptyInputData);
438    }
439    Ok(data)
440}
441
442#[inline(always)]
443fn first_valid(data: &[f64]) -> Option<usize> {
444    data.iter().position(|v| v.is_finite())
445}
446
447#[inline(always)]
448fn resolve_params(params: &SmoothTheilSenParams) -> Result<ResolvedParams, SmoothTheilSenError> {
449    let length = params.length.unwrap_or(DEFAULT_LENGTH);
450    if length < 2 {
451        return Err(SmoothTheilSenError::InvalidLength { length });
452    }
453    let multiplier = params.multiplier.unwrap_or(DEFAULT_MULTIPLIER);
454    if !multiplier.is_finite() || multiplier < 0.0 {
455        return Err(SmoothTheilSenError::InvalidMultiplier { multiplier });
456    }
457    Ok(ResolvedParams {
458        length,
459        offset: params.offset.unwrap_or(DEFAULT_OFFSET),
460        multiplier,
461        slope_style: params.slope_style.unwrap_or_default(),
462        residual_style: params.residual_style.unwrap_or_default(),
463        deviation_style: params.deviation_style.unwrap_or_default(),
464        mad_style: params.mad_style.unwrap_or_default(),
465        include_prediction_in_deviation: params.include_prediction_in_deviation.unwrap_or(false),
466    })
467}
468
469#[inline(always)]
470fn warmup_bars(params: &ResolvedParams) -> usize {
471    params.length + params.offset - 1
472}
473
474#[inline(always)]
475fn validate_input<'a>(
476    input: &'a SmoothTheilSenInput<'a>,
477    kernel: Kernel,
478) -> Result<(&'a [f64], ResolvedParams, usize, usize, Kernel), SmoothTheilSenError> {
479    let data = extract_slice(input)?;
480    let params = resolve_params(&input.params)?;
481    let first = first_valid(data).ok_or(SmoothTheilSenError::AllValuesNaN)?;
482    let needed = params.length + params.offset;
483    let valid = data.len().saturating_sub(first);
484    if valid < needed {
485        return Err(SmoothTheilSenError::NotEnoughValidData { needed, valid });
486    }
487    Ok((
488        data,
489        params,
490        first,
491        first + warmup_bars(&params),
492        kernel.to_non_batch(),
493    ))
494}
495
496#[inline(always)]
497fn pairwise_count(length: usize) -> usize {
498    length * (length - 1) / 2
499}
500
501#[inline(always)]
502fn exponential_interpolation(k: f64, endpoint: f64) -> f64 {
503    let clamp = k.clamp(0.0, 1.0);
504    let min = 0.5;
505    (endpoint - min) * 1024.0f64.powf(clamp - 1.0) + min
506}
507
508#[inline(always)]
509fn gaussian_kernel(source: f64, bandwidth: f64) -> f64 {
510    let ratio = source / bandwidth;
511    (-(ratio * ratio) / 4.0).exp() / (2.0 * std::f64::consts::PI).sqrt()
512}
513
514fn kernel_weights(size: usize, style: SmoothTheilSenStatStyle) -> Option<Vec<f64>> {
515    if style != SmoothTheilSenStatStyle::SmoothMedian || size == 0 {
516        return None;
517    }
518    let width = exponential_interpolation(style.blend(), size as f64);
519    let center = (size as f64 - 1.0) * 0.5;
520    let mut weights = Vec::with_capacity(size);
521    let mut normalization = 0.0;
522    for i in 0..size {
523        let position = i as f64 - center;
524        let weight = gaussian_kernel(position, width);
525        weights.push(weight);
526        normalization += weight;
527    }
528    if normalization != 0.0 {
529        for weight in &mut weights {
530            *weight /= normalization;
531        }
532    }
533    Some(weights)
534}
535
536#[inline(always)]
537fn median_in_place(values: &mut [f64]) -> f64 {
538    values.sort_unstable_by(f64::total_cmp);
539    let n = values.len();
540    if n % 2 == 1 {
541        values[n / 2]
542    } else {
543        (values[n / 2 - 1] + values[n / 2]) * 0.5
544    }
545}
546
547#[inline(always)]
548fn estimator(values: &mut [f64], style: SmoothTheilSenStatStyle, weights: Option<&[f64]>) -> f64 {
549    match style {
550        SmoothTheilSenStatStyle::Mean => values.iter().sum::<f64>() / values.len() as f64,
551        SmoothTheilSenStatStyle::Median => median_in_place(values),
552        SmoothTheilSenStatStyle::SmoothMedian => {
553            values.sort_unstable_by(f64::total_cmp);
554            values
555                .iter()
556                .zip(weights.expect("smooth_median requires weights"))
557                .map(|(value, weight)| value * weight)
558                .sum()
559        }
560    }
561}
562
563#[inline(always)]
564fn build_kernel_cache(params: &ResolvedParams) -> KernelCache {
565    let error_len =
566        params.length + usize::from(params.include_prediction_in_deviation) * params.offset;
567    KernelCache {
568        slope_weights: kernel_weights(pairwise_count(params.length), params.slope_style),
569        residual_weights: kernel_weights(params.length, params.residual_style),
570        error_weights: kernel_weights(error_len, params.mad_style),
571    }
572}
573
574#[inline(always)]
575fn create_work_buffers(params: &ResolvedParams) -> WorkBuffers {
576    WorkBuffers {
577        slopes: Vec::with_capacity(pairwise_count(params.length)),
578        residuals: Vec::with_capacity(params.length),
579        errors: Vec::with_capacity(
580            params.length + usize::from(params.include_prediction_in_deviation) * params.offset,
581        ),
582    }
583}
584
585#[inline(always)]
586fn output_len_check(out: &[f64], len: usize) -> Result<(), SmoothTheilSenError> {
587    if out.len() != len {
588        return Err(SmoothTheilSenError::OutputLengthMismatch {
589            expected: len,
590            got: out.len(),
591        });
592    }
593    Ok(())
594}
595
596#[inline(always)]
597fn required_finite_segment(
598    data: &[f64],
599    idx: usize,
600    params: &ResolvedParams,
601) -> Option<(usize, usize)> {
602    let base = idx.checked_sub(params.offset)?;
603    let start = base + 1 - params.length;
604    let end = if params.include_prediction_in_deviation {
605        idx
606    } else {
607        base
608    };
609    Some((start, end))
610}
611
612#[inline(always)]
613fn segment_all_finite(data: &[f64], start: usize, end: usize) -> bool {
614    data[start..=end].iter().all(|value| value.is_finite())
615}
616
617fn compute_point(
618    data: &[f64],
619    idx: usize,
620    params: &ResolvedParams,
621    cache: &KernelCache,
622    work: &mut WorkBuffers,
623) -> Option<PointOutput> {
624    let (start, end) = required_finite_segment(data, idx, params)?;
625    if !segment_all_finite(data, start, end) {
626        return None;
627    }
628
629    let base = idx - params.offset;
630    work.slopes.clear();
631    for i in 0..params.length - 1 {
632        let value_i = data[base - i];
633        for j in i + 1..params.length {
634            let value_j = data[base - j];
635            work.slopes.push((value_j - value_i) / (j - i) as f64);
636        }
637    }
638    let beta_1 = estimator(
639        work.slopes.as_mut_slice(),
640        params.slope_style,
641        cache.slope_weights.as_deref(),
642    );
643
644    work.residuals.clear();
645    for j in 0..params.length {
646        work.residuals.push(data[base - j] - beta_1 * j as f64);
647    }
648    let beta_0 = estimator(
649        work.residuals.as_mut_slice(),
650        params.residual_style,
651        cache.residual_weights.as_deref(),
652    );
653
654    let predicted = beta_0 - beta_1 * params.offset as f64;
655
656    let deviation = match params.deviation_style {
657        SmoothTheilSenDeviationType::Mad => {
658            work.errors.clear();
659            let start_point = if params.include_prediction_in_deviation {
660                -(params.offset as isize)
661            } else {
662                0
663            };
664            for point in start_point..=(params.length as isize - 1) {
665                let source_idx = (idx as isize - params.offset as isize - point) as usize;
666                let predicted_point = beta_0 + beta_1 * point as f64;
667                work.errors.push((data[source_idx] - predicted_point).abs());
668            }
669            estimator(
670                work.errors.as_mut_slice(),
671                params.mad_style,
672                cache.error_weights.as_deref(),
673            ) * params.multiplier
674        }
675        SmoothTheilSenDeviationType::Rmsd => {
676            let start_point = if params.include_prediction_in_deviation {
677                -(params.offset as isize)
678            } else {
679                0
680            };
681            let mut square_errors = 0.0;
682            let mut count = 0usize;
683            for point in start_point..=(params.length as isize - 1) {
684                let source_idx = (idx as isize - params.offset as isize - point) as usize;
685                let predicted_point = beta_0 + beta_1 * point as f64;
686                let error = data[source_idx] - predicted_point;
687                square_errors += error * error;
688                count += 1;
689            }
690            square_errors
691                .sqrt()
692                .mul_add(params.multiplier / (count as f64).sqrt(), 0.0)
693        }
694    };
695
696    Some(PointOutput {
697        value: predicted,
698        upper: predicted + deviation,
699        lower: predicted - deviation,
700        slope: beta_1,
701        intercept: beta_0,
702        deviation,
703    })
704}
705
706fn smooth_theil_sen_compute_into(
707    data: &[f64],
708    params: &ResolvedParams,
709    warmup: usize,
710    out_value: &mut [f64],
711    out_upper: &mut [f64],
712    out_lower: &mut [f64],
713    out_slope: &mut [f64],
714    out_intercept: &mut [f64],
715    out_deviation: &mut [f64],
716) -> Result<(), SmoothTheilSenError> {
717    let len = data.len();
718    output_len_check(out_value, len)?;
719    output_len_check(out_upper, len)?;
720    output_len_check(out_lower, len)?;
721    output_len_check(out_slope, len)?;
722    output_len_check(out_intercept, len)?;
723    output_len_check(out_deviation, len)?;
724
725    let cache = build_kernel_cache(params);
726    let mut work = create_work_buffers(params);
727    for idx in warmup..len {
728        let Some(point) = compute_point(data, idx, params, &cache, &mut work) else {
729            continue;
730        };
731        out_value[idx] = point.value;
732        out_upper[idx] = point.upper;
733        out_lower[idx] = point.lower;
734        out_slope[idx] = point.slope;
735        out_intercept[idx] = point.intercept;
736        out_deviation[idx] = point.deviation;
737    }
738    Ok(())
739}
740
741#[inline]
742pub fn smooth_theil_sen(
743    input: &SmoothTheilSenInput,
744) -> Result<SmoothTheilSenOutput, SmoothTheilSenError> {
745    smooth_theil_sen_with_kernel(input, Kernel::Auto)
746}
747
748#[inline]
749pub fn smooth_theil_sen_with_kernel(
750    input: &SmoothTheilSenInput,
751    kernel: Kernel,
752) -> Result<SmoothTheilSenOutput, SmoothTheilSenError> {
753    let (data, params, _first, warmup, _kernel) = validate_input(input, kernel)?;
754    let mut value = alloc_with_nan_prefix(data.len(), warmup);
755    let mut upper = alloc_with_nan_prefix(data.len(), warmup);
756    let mut lower = alloc_with_nan_prefix(data.len(), warmup);
757    let mut slope = alloc_with_nan_prefix(data.len(), warmup);
758    let mut intercept = alloc_with_nan_prefix(data.len(), warmup);
759    let mut deviation = alloc_with_nan_prefix(data.len(), warmup);
760    smooth_theil_sen_compute_into(
761        data,
762        &params,
763        warmup,
764        &mut value,
765        &mut upper,
766        &mut lower,
767        &mut slope,
768        &mut intercept,
769        &mut deviation,
770    )?;
771    Ok(SmoothTheilSenOutput {
772        value,
773        upper,
774        lower,
775        slope,
776        intercept,
777        deviation,
778    })
779}
780
781#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
782#[inline]
783pub fn smooth_theil_sen_into(
784    out_value: &mut [f64],
785    out_upper: &mut [f64],
786    out_lower: &mut [f64],
787    out_slope: &mut [f64],
788    out_intercept: &mut [f64],
789    out_deviation: &mut [f64],
790    input: &SmoothTheilSenInput,
791    kernel: Kernel,
792) -> Result<(), SmoothTheilSenError> {
793    smooth_theil_sen_into_slice(
794        out_value,
795        out_upper,
796        out_lower,
797        out_slope,
798        out_intercept,
799        out_deviation,
800        input,
801        kernel,
802    )
803}
804
805#[inline]
806pub fn smooth_theil_sen_into_slice(
807    out_value: &mut [f64],
808    out_upper: &mut [f64],
809    out_lower: &mut [f64],
810    out_slope: &mut [f64],
811    out_intercept: &mut [f64],
812    out_deviation: &mut [f64],
813    input: &SmoothTheilSenInput,
814    kernel: Kernel,
815) -> Result<(), SmoothTheilSenError> {
816    let (data, params, _first, warmup, _kernel) = validate_input(input, kernel)?;
817    out_value.fill(f64::NAN);
818    out_upper.fill(f64::NAN);
819    out_lower.fill(f64::NAN);
820    out_slope.fill(f64::NAN);
821    out_intercept.fill(f64::NAN);
822    out_deviation.fill(f64::NAN);
823    smooth_theil_sen_compute_into(
824        data,
825        &params,
826        warmup,
827        out_value,
828        out_upper,
829        out_lower,
830        out_slope,
831        out_intercept,
832        out_deviation,
833    )
834}
835
836#[derive(Clone, Debug)]
837pub struct SmoothTheilSenStream {
838    params: ResolvedParams,
839    window: VecDeque<f64>,
840}
841
842impl SmoothTheilSenStream {
843    pub fn try_new(params: SmoothTheilSenParams) -> Result<Self, SmoothTheilSenError> {
844        Ok(Self {
845            params: resolve_params(&params)?,
846            window: VecDeque::new(),
847        })
848    }
849
850    #[inline(always)]
851    pub fn update(&mut self, value: f64) -> (f64, f64, f64, f64, f64, f64) {
852        let needed = self.params.length + self.params.offset;
853        self.window.push_back(value);
854        if self.window.len() > needed {
855            self.window.pop_front();
856        }
857        if self.window.len() < needed {
858            return (f64::NAN, f64::NAN, f64::NAN, f64::NAN, f64::NAN, f64::NAN);
859        }
860        let buffer: Vec<f64> = self.window.iter().copied().collect();
861        let cache = build_kernel_cache(&self.params);
862        let mut work = create_work_buffers(&self.params);
863        let Some(point) = compute_point(&buffer, buffer.len() - 1, &self.params, &cache, &mut work)
864        else {
865            return (f64::NAN, f64::NAN, f64::NAN, f64::NAN, f64::NAN, f64::NAN);
866        };
867        (
868            point.value,
869            point.upper,
870            point.lower,
871            point.slope,
872            point.intercept,
873            point.deviation,
874        )
875    }
876}
877
878#[derive(Clone, Copy, Debug)]
879pub struct SmoothTheilSenBatchRange {
880    pub length: (usize, usize, usize),
881    pub offset: (usize, usize, usize),
882    pub multiplier: (f64, f64, f64),
883    pub slope_style: SmoothTheilSenStatStyle,
884    pub residual_style: SmoothTheilSenStatStyle,
885    pub deviation_style: SmoothTheilSenDeviationType,
886    pub mad_style: SmoothTheilSenStatStyle,
887    pub include_prediction_in_deviation: bool,
888}
889
890impl Default for SmoothTheilSenBatchRange {
891    fn default() -> Self {
892        Self {
893            length: (DEFAULT_LENGTH, DEFAULT_LENGTH, 0),
894            offset: (DEFAULT_OFFSET, DEFAULT_OFFSET, 0),
895            multiplier: (DEFAULT_MULTIPLIER, DEFAULT_MULTIPLIER, 0.0),
896            slope_style: SmoothTheilSenStatStyle::SmoothMedian,
897            residual_style: SmoothTheilSenStatStyle::SmoothMedian,
898            deviation_style: SmoothTheilSenDeviationType::Mad,
899            mad_style: SmoothTheilSenStatStyle::SmoothMedian,
900            include_prediction_in_deviation: false,
901        }
902    }
903}
904
905#[derive(Clone, Debug)]
906pub struct SmoothTheilSenBatchOutput {
907    pub value: Vec<f64>,
908    pub upper: Vec<f64>,
909    pub lower: Vec<f64>,
910    pub slope: Vec<f64>,
911    pub intercept: Vec<f64>,
912    pub deviation: Vec<f64>,
913    pub combos: Vec<SmoothTheilSenParams>,
914    pub rows: usize,
915    pub cols: usize,
916}
917
918#[derive(Copy, Clone, Debug)]
919pub struct SmoothTheilSenBatchBuilder {
920    source: Option<&'static str>,
921    range: SmoothTheilSenBatchRange,
922    kernel: Kernel,
923}
924
925impl Default for SmoothTheilSenBatchBuilder {
926    fn default() -> Self {
927        Self {
928            source: None,
929            range: SmoothTheilSenBatchRange::default(),
930            kernel: Kernel::Auto,
931        }
932    }
933}
934
935impl SmoothTheilSenBatchBuilder {
936    #[inline(always)]
937    pub fn new() -> Self {
938        Self::default()
939    }
940
941    #[inline(always)]
942    pub fn source(mut self, value: &'static str) -> Self {
943        self.source = Some(value);
944        self
945    }
946
947    #[inline(always)]
948    pub fn length_range(mut self, value: (usize, usize, usize)) -> Self {
949        self.range.length = value;
950        self
951    }
952
953    #[inline(always)]
954    pub fn offset_range(mut self, value: (usize, usize, usize)) -> Self {
955        self.range.offset = value;
956        self
957    }
958
959    #[inline(always)]
960    pub fn multiplier_range(mut self, value: (f64, f64, f64)) -> Self {
961        self.range.multiplier = value;
962        self
963    }
964
965    #[inline(always)]
966    pub fn slope_style(mut self, value: SmoothTheilSenStatStyle) -> Self {
967        self.range.slope_style = value;
968        self
969    }
970
971    #[inline(always)]
972    pub fn residual_style(mut self, value: SmoothTheilSenStatStyle) -> Self {
973        self.range.residual_style = value;
974        self
975    }
976
977    #[inline(always)]
978    pub fn deviation_style(mut self, value: SmoothTheilSenDeviationType) -> Self {
979        self.range.deviation_style = value;
980        self
981    }
982
983    #[inline(always)]
984    pub fn mad_style(mut self, value: SmoothTheilSenStatStyle) -> Self {
985        self.range.mad_style = value;
986        self
987    }
988
989    #[inline(always)]
990    pub fn include_prediction_in_deviation(mut self, value: bool) -> Self {
991        self.range.include_prediction_in_deviation = value;
992        self
993    }
994
995    #[inline(always)]
996    pub fn kernel(mut self, value: Kernel) -> Self {
997        self.kernel = value;
998        self
999    }
1000
1001    #[inline(always)]
1002    pub fn apply(
1003        self,
1004        candles: &Candles,
1005    ) -> Result<SmoothTheilSenBatchOutput, SmoothTheilSenError> {
1006        smooth_theil_sen_batch_with_kernel(
1007            source_type(candles, self.source.unwrap_or(DEFAULT_SOURCE)),
1008            &self.range,
1009            self.kernel,
1010        )
1011    }
1012
1013    #[inline(always)]
1014    pub fn apply_slice(
1015        self,
1016        data: &[f64],
1017    ) -> Result<SmoothTheilSenBatchOutput, SmoothTheilSenError> {
1018        smooth_theil_sen_batch_with_kernel(data, &self.range, self.kernel)
1019    }
1020}
1021
1022#[inline(always)]
1023fn expand_usize_range(
1024    start: usize,
1025    end: usize,
1026    step: usize,
1027) -> Result<Vec<usize>, SmoothTheilSenError> {
1028    if step == 0 {
1029        if start != end {
1030            return Err(SmoothTheilSenError::InvalidRange {
1031                start: start.to_string(),
1032                end: end.to_string(),
1033                step: step.to_string(),
1034            });
1035        }
1036        return Ok(vec![start]);
1037    }
1038    if start > end {
1039        return Err(SmoothTheilSenError::InvalidRange {
1040            start: start.to_string(),
1041            end: end.to_string(),
1042            step: step.to_string(),
1043        });
1044    }
1045    let mut out = Vec::new();
1046    let mut current = start;
1047    while current <= end {
1048        out.push(current);
1049        if out.len() > 1_000_000 {
1050            return Err(SmoothTheilSenError::InvalidRange {
1051                start: start.to_string(),
1052                end: end.to_string(),
1053                step: step.to_string(),
1054            });
1055        }
1056        current = current.saturating_add(step);
1057        if current == usize::MAX && current != end {
1058            break;
1059        }
1060    }
1061    Ok(out)
1062}
1063
1064#[inline(always)]
1065fn expand_float_range(start: f64, end: f64, step: f64) -> Result<Vec<f64>, SmoothTheilSenError> {
1066    if !start.is_finite() || !end.is_finite() || !step.is_finite() {
1067        return Err(SmoothTheilSenError::InvalidRange {
1068            start: start.to_string(),
1069            end: end.to_string(),
1070            step: step.to_string(),
1071        });
1072    }
1073    if step == 0.0 {
1074        if (start - end).abs() > 1e-12 {
1075            return Err(SmoothTheilSenError::InvalidRange {
1076                start: start.to_string(),
1077                end: end.to_string(),
1078                step: step.to_string(),
1079            });
1080        }
1081        return Ok(vec![start]);
1082    }
1083    if start > end || step < 0.0 {
1084        return Err(SmoothTheilSenError::InvalidRange {
1085            start: start.to_string(),
1086            end: end.to_string(),
1087            step: step.to_string(),
1088        });
1089    }
1090    let mut out = Vec::new();
1091    let mut current = start;
1092    while current <= end + 1e-12 {
1093        out.push(current);
1094        if out.len() > 1_000_000 {
1095            return Err(SmoothTheilSenError::InvalidRange {
1096                start: start.to_string(),
1097                end: end.to_string(),
1098                step: step.to_string(),
1099            });
1100        }
1101        current += step;
1102    }
1103    Ok(out)
1104}
1105
1106pub fn smooth_theil_sen_expand_grid(
1107    sweep: &SmoothTheilSenBatchRange,
1108) -> Result<Vec<SmoothTheilSenParams>, SmoothTheilSenError> {
1109    let lengths = expand_usize_range(sweep.length.0, sweep.length.1, sweep.length.2)?;
1110    let offsets = expand_usize_range(sweep.offset.0, sweep.offset.1, sweep.offset.2)?;
1111    let multipliers =
1112        expand_float_range(sweep.multiplier.0, sweep.multiplier.1, sweep.multiplier.2)?;
1113    let mut out = Vec::with_capacity(lengths.len() * offsets.len() * multipliers.len());
1114    for length in lengths {
1115        for offset in &offsets {
1116            for multiplier in &multipliers {
1117                out.push(SmoothTheilSenParams {
1118                    length: Some(length),
1119                    offset: Some(*offset),
1120                    multiplier: Some(*multiplier),
1121                    slope_style: Some(sweep.slope_style),
1122                    residual_style: Some(sweep.residual_style),
1123                    deviation_style: Some(sweep.deviation_style),
1124                    mad_style: Some(sweep.mad_style),
1125                    include_prediction_in_deviation: Some(sweep.include_prediction_in_deviation),
1126                });
1127            }
1128        }
1129    }
1130    Ok(out)
1131}
1132
1133#[inline(always)]
1134fn batch_shape(rows: usize, cols: usize) -> Result<usize, SmoothTheilSenError> {
1135    rows.checked_mul(cols)
1136        .ok_or_else(|| SmoothTheilSenError::InvalidRange {
1137            start: rows.to_string(),
1138            end: cols.to_string(),
1139            step: "rows*cols".to_string(),
1140        })
1141}
1142
1143fn validate_raw_data(data: &[f64]) -> Result<usize, SmoothTheilSenError> {
1144    if data.is_empty() {
1145        return Err(SmoothTheilSenError::EmptyInputData);
1146    }
1147    first_valid(data).ok_or(SmoothTheilSenError::AllValuesNaN)
1148}
1149
1150pub fn smooth_theil_sen_batch_with_kernel(
1151    data: &[f64],
1152    sweep: &SmoothTheilSenBatchRange,
1153    kernel: Kernel,
1154) -> Result<SmoothTheilSenBatchOutput, SmoothTheilSenError> {
1155    let batch_kernel = match kernel {
1156        Kernel::Auto => detect_best_batch_kernel(),
1157        other if other.is_batch() => other,
1158        _ => return Err(SmoothTheilSenError::InvalidKernelForBatch(kernel)),
1159    };
1160    smooth_theil_sen_batch_par_slice(data, sweep, batch_kernel.to_non_batch())
1161}
1162
1163#[inline(always)]
1164pub fn smooth_theil_sen_batch_slice(
1165    data: &[f64],
1166    sweep: &SmoothTheilSenBatchRange,
1167    kernel: Kernel,
1168) -> Result<SmoothTheilSenBatchOutput, SmoothTheilSenError> {
1169    smooth_theil_sen_batch_inner(data, sweep, kernel, false)
1170}
1171
1172#[inline(always)]
1173pub fn smooth_theil_sen_batch_par_slice(
1174    data: &[f64],
1175    sweep: &SmoothTheilSenBatchRange,
1176    kernel: Kernel,
1177) -> Result<SmoothTheilSenBatchOutput, SmoothTheilSenError> {
1178    smooth_theil_sen_batch_inner(data, sweep, kernel, true)
1179}
1180
1181fn smooth_theil_sen_batch_inner(
1182    data: &[f64],
1183    sweep: &SmoothTheilSenBatchRange,
1184    kernel: Kernel,
1185    parallel: bool,
1186) -> Result<SmoothTheilSenBatchOutput, SmoothTheilSenError> {
1187    let combos = smooth_theil_sen_expand_grid(sweep)?;
1188    let first = validate_raw_data(data)?;
1189    let rows = combos.len();
1190    let cols = data.len();
1191    let total = batch_shape(rows, cols)?;
1192    let mut warmups = Vec::with_capacity(rows);
1193    for combo in &combos {
1194        let params = resolve_params(combo)?;
1195        let needed = params.length + params.offset;
1196        let valid = data.len().saturating_sub(first);
1197        if valid < needed {
1198            return Err(SmoothTheilSenError::NotEnoughValidData { needed, valid });
1199        }
1200        warmups.push(first + warmup_bars(&params));
1201    }
1202
1203    let mut value_buf = make_uninit_matrix(rows, cols);
1204    let mut upper_buf = make_uninit_matrix(rows, cols);
1205    let mut lower_buf = make_uninit_matrix(rows, cols);
1206    let mut slope_buf = make_uninit_matrix(rows, cols);
1207    let mut intercept_buf = make_uninit_matrix(rows, cols);
1208    let mut deviation_buf = make_uninit_matrix(rows, cols);
1209    init_matrix_prefixes(&mut value_buf, cols, &warmups);
1210    init_matrix_prefixes(&mut upper_buf, cols, &warmups);
1211    init_matrix_prefixes(&mut lower_buf, cols, &warmups);
1212    init_matrix_prefixes(&mut slope_buf, cols, &warmups);
1213    init_matrix_prefixes(&mut intercept_buf, cols, &warmups);
1214    init_matrix_prefixes(&mut deviation_buf, cols, &warmups);
1215
1216    let mut value_guard = ManuallyDrop::new(value_buf);
1217    let mut upper_guard = ManuallyDrop::new(upper_buf);
1218    let mut lower_guard = ManuallyDrop::new(lower_buf);
1219    let mut slope_guard = ManuallyDrop::new(slope_buf);
1220    let mut intercept_guard = ManuallyDrop::new(intercept_buf);
1221    let mut deviation_guard = ManuallyDrop::new(deviation_buf);
1222
1223    let out_value: &mut [f64] = unsafe {
1224        core::slice::from_raw_parts_mut(value_guard.as_mut_ptr() as *mut f64, value_guard.len())
1225    };
1226    let out_upper: &mut [f64] = unsafe {
1227        core::slice::from_raw_parts_mut(upper_guard.as_mut_ptr() as *mut f64, upper_guard.len())
1228    };
1229    let out_lower: &mut [f64] = unsafe {
1230        core::slice::from_raw_parts_mut(lower_guard.as_mut_ptr() as *mut f64, lower_guard.len())
1231    };
1232    let out_slope: &mut [f64] = unsafe {
1233        core::slice::from_raw_parts_mut(slope_guard.as_mut_ptr() as *mut f64, slope_guard.len())
1234    };
1235    let out_intercept: &mut [f64] = unsafe {
1236        core::slice::from_raw_parts_mut(
1237            intercept_guard.as_mut_ptr() as *mut f64,
1238            intercept_guard.len(),
1239        )
1240    };
1241    let out_deviation: &mut [f64] = unsafe {
1242        core::slice::from_raw_parts_mut(
1243            deviation_guard.as_mut_ptr() as *mut f64,
1244            deviation_guard.len(),
1245        )
1246    };
1247
1248    smooth_theil_sen_batch_inner_into(
1249        data,
1250        sweep,
1251        kernel,
1252        parallel,
1253        out_value,
1254        out_upper,
1255        out_lower,
1256        out_slope,
1257        out_intercept,
1258        out_deviation,
1259    )?;
1260
1261    let value = unsafe {
1262        Vec::from_raw_parts(
1263            value_guard.as_mut_ptr() as *mut f64,
1264            total,
1265            value_guard.capacity(),
1266        )
1267    };
1268    let upper = unsafe {
1269        Vec::from_raw_parts(
1270            upper_guard.as_mut_ptr() as *mut f64,
1271            total,
1272            upper_guard.capacity(),
1273        )
1274    };
1275    let lower = unsafe {
1276        Vec::from_raw_parts(
1277            lower_guard.as_mut_ptr() as *mut f64,
1278            total,
1279            lower_guard.capacity(),
1280        )
1281    };
1282    let slope = unsafe {
1283        Vec::from_raw_parts(
1284            slope_guard.as_mut_ptr() as *mut f64,
1285            total,
1286            slope_guard.capacity(),
1287        )
1288    };
1289    let intercept = unsafe {
1290        Vec::from_raw_parts(
1291            intercept_guard.as_mut_ptr() as *mut f64,
1292            total,
1293            intercept_guard.capacity(),
1294        )
1295    };
1296    let deviation = unsafe {
1297        Vec::from_raw_parts(
1298            deviation_guard.as_mut_ptr() as *mut f64,
1299            total,
1300            deviation_guard.capacity(),
1301        )
1302    };
1303
1304    Ok(SmoothTheilSenBatchOutput {
1305        value,
1306        upper,
1307        lower,
1308        slope,
1309        intercept,
1310        deviation,
1311        combos,
1312        rows,
1313        cols,
1314    })
1315}
1316
1317pub fn smooth_theil_sen_batch_into_slice(
1318    out_value: &mut [f64],
1319    out_upper: &mut [f64],
1320    out_lower: &mut [f64],
1321    out_slope: &mut [f64],
1322    out_intercept: &mut [f64],
1323    out_deviation: &mut [f64],
1324    data: &[f64],
1325    sweep: &SmoothTheilSenBatchRange,
1326    kernel: Kernel,
1327) -> Result<(), SmoothTheilSenError> {
1328    smooth_theil_sen_batch_inner_into(
1329        data,
1330        sweep,
1331        kernel,
1332        false,
1333        out_value,
1334        out_upper,
1335        out_lower,
1336        out_slope,
1337        out_intercept,
1338        out_deviation,
1339    )?;
1340    Ok(())
1341}
1342
1343fn smooth_theil_sen_batch_inner_into(
1344    data: &[f64],
1345    sweep: &SmoothTheilSenBatchRange,
1346    _kernel: Kernel,
1347    parallel: bool,
1348    out_value: &mut [f64],
1349    out_upper: &mut [f64],
1350    out_lower: &mut [f64],
1351    out_slope: &mut [f64],
1352    out_intercept: &mut [f64],
1353    out_deviation: &mut [f64],
1354) -> Result<Vec<SmoothTheilSenParams>, SmoothTheilSenError> {
1355    let combos = smooth_theil_sen_expand_grid(sweep)?;
1356    let first = validate_raw_data(data)?;
1357    let rows = combos.len();
1358    let cols = data.len();
1359    let total = batch_shape(rows, cols)?;
1360    if out_value.len() != total {
1361        return Err(SmoothTheilSenError::OutputLengthMismatch {
1362            expected: total,
1363            got: out_value.len(),
1364        });
1365    }
1366    if out_upper.len() != total {
1367        return Err(SmoothTheilSenError::OutputLengthMismatch {
1368            expected: total,
1369            got: out_upper.len(),
1370        });
1371    }
1372    if out_lower.len() != total {
1373        return Err(SmoothTheilSenError::OutputLengthMismatch {
1374            expected: total,
1375            got: out_lower.len(),
1376        });
1377    }
1378    if out_slope.len() != total {
1379        return Err(SmoothTheilSenError::OutputLengthMismatch {
1380            expected: total,
1381            got: out_slope.len(),
1382        });
1383    }
1384    if out_intercept.len() != total {
1385        return Err(SmoothTheilSenError::OutputLengthMismatch {
1386            expected: total,
1387            got: out_intercept.len(),
1388        });
1389    }
1390    if out_deviation.len() != total {
1391        return Err(SmoothTheilSenError::OutputLengthMismatch {
1392            expected: total,
1393            got: out_deviation.len(),
1394        });
1395    }
1396
1397    out_value.fill(f64::NAN);
1398    out_upper.fill(f64::NAN);
1399    out_lower.fill(f64::NAN);
1400    out_slope.fill(f64::NAN);
1401    out_intercept.fill(f64::NAN);
1402    out_deviation.fill(f64::NAN);
1403
1404    let do_row = |row: usize,
1405                  dst_value: &mut [f64],
1406                  dst_upper: &mut [f64],
1407                  dst_lower: &mut [f64],
1408                  dst_slope: &mut [f64],
1409                  dst_intercept: &mut [f64],
1410                  dst_dev: &mut [f64]|
1411     -> Result<(), SmoothTheilSenError> {
1412        let params = resolve_params(&combos[row])?;
1413        let needed = params.length + params.offset;
1414        let valid = data.len().saturating_sub(first);
1415        if valid < needed {
1416            return Err(SmoothTheilSenError::NotEnoughValidData { needed, valid });
1417        }
1418        smooth_theil_sen_compute_into(
1419            data,
1420            &params,
1421            first + warmup_bars(&params),
1422            dst_value,
1423            dst_upper,
1424            dst_lower,
1425            dst_slope,
1426            dst_intercept,
1427            dst_dev,
1428        )
1429    };
1430
1431    if parallel {
1432        #[cfg(not(target_arch = "wasm32"))]
1433        {
1434            out_value
1435                .par_chunks_mut(cols)
1436                .zip(out_upper.par_chunks_mut(cols))
1437                .zip(out_lower.par_chunks_mut(cols))
1438                .zip(out_slope.par_chunks_mut(cols))
1439                .zip(out_intercept.par_chunks_mut(cols))
1440                .zip(out_deviation.par_chunks_mut(cols))
1441                .enumerate()
1442                .try_for_each(
1443                    |(
1444                        row,
1445                        (
1446                            ((((value_row, upper_row), lower_row), slope_row), intercept_row),
1447                            deviation_row,
1448                        ),
1449                    )| {
1450                        do_row(
1451                            row,
1452                            value_row,
1453                            upper_row,
1454                            lower_row,
1455                            slope_row,
1456                            intercept_row,
1457                            deviation_row,
1458                        )
1459                    },
1460                )?;
1461        }
1462        #[cfg(target_arch = "wasm32")]
1463        {
1464            for row in 0..rows {
1465                let start = row * cols;
1466                let end = start + cols;
1467                do_row(
1468                    row,
1469                    &mut out_value[start..end],
1470                    &mut out_upper[start..end],
1471                    &mut out_lower[start..end],
1472                    &mut out_slope[start..end],
1473                    &mut out_intercept[start..end],
1474                    &mut out_deviation[start..end],
1475                )?;
1476            }
1477        }
1478    } else {
1479        for row in 0..rows {
1480            let start = row * cols;
1481            let end = start + cols;
1482            do_row(
1483                row,
1484                &mut out_value[start..end],
1485                &mut out_upper[start..end],
1486                &mut out_lower[start..end],
1487                &mut out_slope[start..end],
1488                &mut out_intercept[start..end],
1489                &mut out_deviation[start..end],
1490            )?;
1491        }
1492    }
1493
1494    Ok(combos)
1495}
1496
1497#[cfg(feature = "python")]
1498#[pyfunction(name = "smooth_theil_sen")]
1499#[pyo3(signature = (data, length=25, offset=0, multiplier=2.0, slope_style="smooth_median", residual_style="smooth_median", deviation_style="mad", mad_style="smooth_median", include_prediction_in_deviation=false, kernel=None))]
1500pub fn smooth_theil_sen_py<'py>(
1501    py: Python<'py>,
1502    data: PyReadonlyArray1<'py, f64>,
1503    length: usize,
1504    offset: usize,
1505    multiplier: f64,
1506    slope_style: &str,
1507    residual_style: &str,
1508    deviation_style: &str,
1509    mad_style: &str,
1510    include_prediction_in_deviation: bool,
1511    kernel: Option<&str>,
1512) -> PyResult<Bound<'py, PyDict>> {
1513    let data = data.as_slice()?;
1514    let params = SmoothTheilSenParams {
1515        length: Some(length),
1516        offset: Some(offset),
1517        multiplier: Some(multiplier),
1518        slope_style: Some(
1519            SmoothTheilSenStatStyle::from_str(slope_style)
1520                .map_err(|_| PyValueError::new_err("Invalid slope_style"))?,
1521        ),
1522        residual_style: Some(
1523            SmoothTheilSenStatStyle::from_str(residual_style)
1524                .map_err(|_| PyValueError::new_err("Invalid residual_style"))?,
1525        ),
1526        deviation_style: Some(
1527            SmoothTheilSenDeviationType::from_str(deviation_style)
1528                .map_err(|_| PyValueError::new_err("Invalid deviation_style"))?,
1529        ),
1530        mad_style: Some(
1531            SmoothTheilSenStatStyle::from_str(mad_style)
1532                .map_err(|_| PyValueError::new_err("Invalid mad_style"))?,
1533        ),
1534        include_prediction_in_deviation: Some(include_prediction_in_deviation),
1535    };
1536    let input = SmoothTheilSenInput::from_slice(data, params);
1537    let kernel = validate_kernel(kernel, false)?;
1538    let out = py
1539        .allow_threads(|| smooth_theil_sen_with_kernel(&input, kernel))
1540        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1541    let dict = PyDict::new(py);
1542    dict.set_item("value", out.value.into_pyarray(py))?;
1543    dict.set_item("upper", out.upper.into_pyarray(py))?;
1544    dict.set_item("lower", out.lower.into_pyarray(py))?;
1545    dict.set_item("slope", out.slope.into_pyarray(py))?;
1546    dict.set_item("intercept", out.intercept.into_pyarray(py))?;
1547    dict.set_item("deviation", out.deviation.into_pyarray(py))?;
1548    Ok(dict)
1549}
1550
1551#[cfg(feature = "python")]
1552#[pyclass(name = "SmoothTheilSenStream")]
1553pub struct SmoothTheilSenStreamPy {
1554    stream: SmoothTheilSenStream,
1555}
1556
1557#[cfg(feature = "python")]
1558#[pymethods]
1559impl SmoothTheilSenStreamPy {
1560    #[new]
1561    #[pyo3(signature = (length=25, offset=0, multiplier=2.0, slope_style="smooth_median", residual_style="smooth_median", deviation_style="mad", mad_style="smooth_median", include_prediction_in_deviation=false))]
1562    fn new(
1563        length: usize,
1564        offset: usize,
1565        multiplier: f64,
1566        slope_style: &str,
1567        residual_style: &str,
1568        deviation_style: &str,
1569        mad_style: &str,
1570        include_prediction_in_deviation: bool,
1571    ) -> PyResult<Self> {
1572        let stream = SmoothTheilSenStream::try_new(SmoothTheilSenParams {
1573            length: Some(length),
1574            offset: Some(offset),
1575            multiplier: Some(multiplier),
1576            slope_style: Some(
1577                SmoothTheilSenStatStyle::from_str(slope_style)
1578                    .map_err(|_| PyValueError::new_err("Invalid slope_style"))?,
1579            ),
1580            residual_style: Some(
1581                SmoothTheilSenStatStyle::from_str(residual_style)
1582                    .map_err(|_| PyValueError::new_err("Invalid residual_style"))?,
1583            ),
1584            deviation_style: Some(
1585                SmoothTheilSenDeviationType::from_str(deviation_style)
1586                    .map_err(|_| PyValueError::new_err("Invalid deviation_style"))?,
1587            ),
1588            mad_style: Some(
1589                SmoothTheilSenStatStyle::from_str(mad_style)
1590                    .map_err(|_| PyValueError::new_err("Invalid mad_style"))?,
1591            ),
1592            include_prediction_in_deviation: Some(include_prediction_in_deviation),
1593        })
1594        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1595        Ok(Self { stream })
1596    }
1597
1598    fn update(&mut self, value: f64) -> (f64, f64, f64, f64, f64, f64) {
1599        self.stream.update(value)
1600    }
1601}
1602
1603#[cfg(feature = "python")]
1604#[pyfunction(name = "smooth_theil_sen_batch")]
1605#[pyo3(signature = (data, length_range=(25,25,0), offset_range=(0,0,0), multiplier_range=(2.0,2.0,0.0), slope_style="smooth_median", residual_style="smooth_median", deviation_style="mad", mad_style="smooth_median", include_prediction_in_deviation=false, kernel=None))]
1606pub fn smooth_theil_sen_batch_py<'py>(
1607    py: Python<'py>,
1608    data: PyReadonlyArray1<'py, f64>,
1609    length_range: (usize, usize, usize),
1610    offset_range: (usize, usize, usize),
1611    multiplier_range: (f64, f64, f64),
1612    slope_style: &str,
1613    residual_style: &str,
1614    deviation_style: &str,
1615    mad_style: &str,
1616    include_prediction_in_deviation: bool,
1617    kernel: Option<&str>,
1618) -> PyResult<Bound<'py, PyDict>> {
1619    let data = data.as_slice()?;
1620    let sweep = SmoothTheilSenBatchRange {
1621        length: length_range,
1622        offset: offset_range,
1623        multiplier: multiplier_range,
1624        slope_style: SmoothTheilSenStatStyle::from_str(slope_style)
1625            .map_err(|_| PyValueError::new_err("Invalid slope_style"))?,
1626        residual_style: SmoothTheilSenStatStyle::from_str(residual_style)
1627            .map_err(|_| PyValueError::new_err("Invalid residual_style"))?,
1628        deviation_style: SmoothTheilSenDeviationType::from_str(deviation_style)
1629            .map_err(|_| PyValueError::new_err("Invalid deviation_style"))?,
1630        mad_style: SmoothTheilSenStatStyle::from_str(mad_style)
1631            .map_err(|_| PyValueError::new_err("Invalid mad_style"))?,
1632        include_prediction_in_deviation,
1633    };
1634    let combos =
1635        smooth_theil_sen_expand_grid(&sweep).map_err(|e| PyValueError::new_err(e.to_string()))?;
1636    let rows = combos.len();
1637    let cols = data.len();
1638    let total = rows
1639        .checked_mul(cols)
1640        .ok_or_else(|| PyValueError::new_err("rows*cols overflow"))?;
1641
1642    let out_value = unsafe { PyArray1::<f64>::new(py, [total], false) };
1643    let out_upper = unsafe { PyArray1::<f64>::new(py, [total], false) };
1644    let out_lower = unsafe { PyArray1::<f64>::new(py, [total], false) };
1645    let out_slope = unsafe { PyArray1::<f64>::new(py, [total], false) };
1646    let out_intercept = unsafe { PyArray1::<f64>::new(py, [total], false) };
1647    let out_deviation = unsafe { PyArray1::<f64>::new(py, [total], false) };
1648    let value_slice = unsafe { out_value.as_slice_mut()? };
1649    let upper_slice = unsafe { out_upper.as_slice_mut()? };
1650    let lower_slice = unsafe { out_lower.as_slice_mut()? };
1651    let slope_slice = unsafe { out_slope.as_slice_mut()? };
1652    let intercept_slice = unsafe { out_intercept.as_slice_mut()? };
1653    let deviation_slice = unsafe { out_deviation.as_slice_mut()? };
1654    let kernel = validate_kernel(kernel, true)?;
1655
1656    py.allow_threads(|| {
1657        let batch_kernel = match kernel {
1658            Kernel::Auto => detect_best_batch_kernel(),
1659            other => other,
1660        };
1661        smooth_theil_sen_batch_inner_into(
1662            data,
1663            &sweep,
1664            batch_kernel.to_non_batch(),
1665            true,
1666            value_slice,
1667            upper_slice,
1668            lower_slice,
1669            slope_slice,
1670            intercept_slice,
1671            deviation_slice,
1672        )
1673    })
1674    .map_err(|e| PyValueError::new_err(e.to_string()))?;
1675
1676    let dict = PyDict::new(py);
1677    dict.set_item("value", out_value.reshape((rows, cols))?)?;
1678    dict.set_item("upper", out_upper.reshape((rows, cols))?)?;
1679    dict.set_item("lower", out_lower.reshape((rows, cols))?)?;
1680    dict.set_item("slope", out_slope.reshape((rows, cols))?)?;
1681    dict.set_item("intercept", out_intercept.reshape((rows, cols))?)?;
1682    dict.set_item("deviation", out_deviation.reshape((rows, cols))?)?;
1683    dict.set_item(
1684        "lengths",
1685        combos
1686            .iter()
1687            .map(|combo| combo.length.unwrap_or(DEFAULT_LENGTH) as u64)
1688            .collect::<Vec<_>>()
1689            .into_pyarray(py),
1690    )?;
1691    dict.set_item(
1692        "offsets",
1693        combos
1694            .iter()
1695            .map(|combo| combo.offset.unwrap_or(DEFAULT_OFFSET) as u64)
1696            .collect::<Vec<_>>()
1697            .into_pyarray(py),
1698    )?;
1699    dict.set_item(
1700        "multipliers",
1701        combos
1702            .iter()
1703            .map(|combo| combo.multiplier.unwrap_or(DEFAULT_MULTIPLIER))
1704            .collect::<Vec<_>>()
1705            .into_pyarray(py),
1706    )?;
1707    dict.set_item("rows", rows)?;
1708    dict.set_item("cols", cols)?;
1709    Ok(dict)
1710}
1711
1712#[cfg(feature = "python")]
1713pub fn register_smooth_theil_sen_module(m: &Bound<'_, pyo3::types::PyModule>) -> PyResult<()> {
1714    m.add_function(wrap_pyfunction!(smooth_theil_sen_py, m)?)?;
1715    m.add_function(wrap_pyfunction!(smooth_theil_sen_batch_py, m)?)?;
1716    m.add_class::<SmoothTheilSenStreamPy>()?;
1717    Ok(())
1718}
1719
1720#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1721#[derive(Serialize, Deserialize)]
1722pub struct SmoothTheilSenJsOutput {
1723    pub value: Vec<f64>,
1724    pub upper: Vec<f64>,
1725    pub lower: Vec<f64>,
1726    pub slope: Vec<f64>,
1727    pub intercept: Vec<f64>,
1728    pub deviation: Vec<f64>,
1729}
1730
1731#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1732fn parse_stat_style(value: String) -> Result<SmoothTheilSenStatStyle, JsValue> {
1733    SmoothTheilSenStatStyle::from_str(&value).map_err(|_| JsValue::from_str("Invalid stat style"))
1734}
1735
1736#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1737fn parse_deviation_style(value: String) -> Result<SmoothTheilSenDeviationType, JsValue> {
1738    SmoothTheilSenDeviationType::from_str(&value)
1739        .map_err(|_| JsValue::from_str("Invalid deviation style"))
1740}
1741
1742#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1743#[wasm_bindgen(js_name = "smooth_theil_sen_js")]
1744pub fn smooth_theil_sen_js(
1745    data: &[f64],
1746    length: usize,
1747    offset: usize,
1748    multiplier: f64,
1749    slope_style: String,
1750    residual_style: String,
1751    deviation_style: String,
1752    mad_style: String,
1753    include_prediction_in_deviation: bool,
1754) -> Result<JsValue, JsValue> {
1755    let input = SmoothTheilSenInput::from_slice(
1756        data,
1757        SmoothTheilSenParams {
1758            length: Some(length),
1759            offset: Some(offset),
1760            multiplier: Some(multiplier),
1761            slope_style: Some(parse_stat_style(slope_style)?),
1762            residual_style: Some(parse_stat_style(residual_style)?),
1763            deviation_style: Some(parse_deviation_style(deviation_style)?),
1764            mad_style: Some(parse_stat_style(mad_style)?),
1765            include_prediction_in_deviation: Some(include_prediction_in_deviation),
1766        },
1767    );
1768    let out = smooth_theil_sen_with_kernel(&input, Kernel::Auto)
1769        .map_err(|e| JsValue::from_str(&e.to_string()))?;
1770    serde_wasm_bindgen::to_value(&SmoothTheilSenJsOutput {
1771        value: out.value,
1772        upper: out.upper,
1773        lower: out.lower,
1774        slope: out.slope,
1775        intercept: out.intercept,
1776        deviation: out.deviation,
1777    })
1778    .map_err(|e| JsValue::from_str(&format!("Serialization error: {e}")))
1779}
1780
1781#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1782#[derive(Serialize, Deserialize)]
1783pub struct SmoothTheilSenBatchConfig {
1784    pub length_range: Vec<usize>,
1785    pub offset_range: Vec<usize>,
1786    pub multiplier_range: Vec<f64>,
1787    pub slope_style: Option<String>,
1788    pub residual_style: Option<String>,
1789    pub deviation_style: Option<String>,
1790    pub mad_style: Option<String>,
1791    pub include_prediction_in_deviation: Option<bool>,
1792}
1793
1794#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1795#[derive(Serialize, Deserialize)]
1796pub struct SmoothTheilSenBatchJsOutput {
1797    pub value: Vec<f64>,
1798    pub upper: Vec<f64>,
1799    pub lower: Vec<f64>,
1800    pub slope: Vec<f64>,
1801    pub intercept: Vec<f64>,
1802    pub deviation: Vec<f64>,
1803    pub lengths: Vec<usize>,
1804    pub offsets: Vec<usize>,
1805    pub multipliers: Vec<f64>,
1806    pub rows: usize,
1807    pub cols: usize,
1808}
1809
1810#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1811fn js_vec3_to_usize(name: &str, values: &[usize]) -> Result<(usize, usize, usize), JsValue> {
1812    if values.len() != 3 {
1813        return Err(JsValue::from_str(&format!(
1814            "Invalid config: {name} must have exactly 3 elements [start, end, step]"
1815        )));
1816    }
1817    Ok((values[0], values[1], values[2]))
1818}
1819
1820#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1821fn js_vec3_to_f64(name: &str, values: &[f64]) -> Result<(f64, f64, f64), JsValue> {
1822    if values.len() != 3 {
1823        return Err(JsValue::from_str(&format!(
1824            "Invalid config: {name} must have exactly 3 elements [start, end, step]"
1825        )));
1826    }
1827    if !values.iter().all(|v| v.is_finite()) {
1828        return Err(JsValue::from_str(&format!(
1829            "Invalid config: {name} entries must be finite numbers"
1830        )));
1831    }
1832    Ok((values[0], values[1], values[2]))
1833}
1834
1835#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1836#[wasm_bindgen(js_name = "smooth_theil_sen_batch_js")]
1837pub fn smooth_theil_sen_batch_js(data: &[f64], config: JsValue) -> Result<JsValue, JsValue> {
1838    let config: SmoothTheilSenBatchConfig = serde_wasm_bindgen::from_value(config)
1839        .map_err(|e| JsValue::from_str(&format!("Invalid config: {e}")))?;
1840    let sweep = SmoothTheilSenBatchRange {
1841        length: js_vec3_to_usize("length_range", &config.length_range)?,
1842        offset: js_vec3_to_usize("offset_range", &config.offset_range)?,
1843        multiplier: js_vec3_to_f64("multiplier_range", &config.multiplier_range)?,
1844        slope_style: parse_stat_style(
1845            config
1846                .slope_style
1847                .unwrap_or_else(|| SmoothTheilSenStatStyle::SmoothMedian.as_str().to_string()),
1848        )?,
1849        residual_style: parse_stat_style(
1850            config
1851                .residual_style
1852                .unwrap_or_else(|| SmoothTheilSenStatStyle::SmoothMedian.as_str().to_string()),
1853        )?,
1854        deviation_style: parse_deviation_style(
1855            config
1856                .deviation_style
1857                .unwrap_or_else(|| SmoothTheilSenDeviationType::Mad.as_str().to_string()),
1858        )?,
1859        mad_style: parse_stat_style(
1860            config
1861                .mad_style
1862                .unwrap_or_else(|| SmoothTheilSenStatStyle::SmoothMedian.as_str().to_string()),
1863        )?,
1864        include_prediction_in_deviation: config.include_prediction_in_deviation.unwrap_or(false),
1865    };
1866    let out = smooth_theil_sen_batch_with_kernel(data, &sweep, Kernel::Auto)
1867        .map_err(|e| JsValue::from_str(&e.to_string()))?;
1868    let lengths = out
1869        .combos
1870        .iter()
1871        .map(|combo| combo.length.unwrap_or(DEFAULT_LENGTH))
1872        .collect();
1873    let offsets = out
1874        .combos
1875        .iter()
1876        .map(|combo| combo.offset.unwrap_or(DEFAULT_OFFSET))
1877        .collect();
1878    let multipliers = out
1879        .combos
1880        .iter()
1881        .map(|combo| combo.multiplier.unwrap_or(DEFAULT_MULTIPLIER))
1882        .collect();
1883    serde_wasm_bindgen::to_value(&SmoothTheilSenBatchJsOutput {
1884        value: out.value,
1885        upper: out.upper,
1886        lower: out.lower,
1887        slope: out.slope,
1888        intercept: out.intercept,
1889        deviation: out.deviation,
1890        lengths,
1891        offsets,
1892        multipliers,
1893        rows: out.rows,
1894        cols: out.cols,
1895    })
1896    .map_err(|e| JsValue::from_str(&format!("Serialization error: {e}")))
1897}
1898
1899#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1900#[wasm_bindgen]
1901pub fn smooth_theil_sen_alloc(len: usize) -> *mut f64 {
1902    let mut vec = Vec::<f64>::with_capacity(len);
1903    let ptr = vec.as_mut_ptr();
1904    std::mem::forget(vec);
1905    ptr
1906}
1907
1908#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1909#[wasm_bindgen]
1910pub fn smooth_theil_sen_free(ptr: *mut f64, len: usize) {
1911    if !ptr.is_null() {
1912        unsafe {
1913            let _ = Vec::from_raw_parts(ptr, len, len);
1914        }
1915    }
1916}
1917
1918#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1919#[wasm_bindgen]
1920pub fn smooth_theil_sen_into(
1921    data_ptr: *const f64,
1922    out_value_ptr: *mut f64,
1923    out_upper_ptr: *mut f64,
1924    out_lower_ptr: *mut f64,
1925    out_slope_ptr: *mut f64,
1926    out_intercept_ptr: *mut f64,
1927    out_deviation_ptr: *mut f64,
1928    len: usize,
1929    length: usize,
1930    offset: usize,
1931    multiplier: f64,
1932    slope_style: String,
1933    residual_style: String,
1934    deviation_style: String,
1935    mad_style: String,
1936    include_prediction_in_deviation: bool,
1937) -> Result<(), JsValue> {
1938    if data_ptr.is_null()
1939        || out_value_ptr.is_null()
1940        || out_upper_ptr.is_null()
1941        || out_lower_ptr.is_null()
1942        || out_slope_ptr.is_null()
1943        || out_intercept_ptr.is_null()
1944        || out_deviation_ptr.is_null()
1945    {
1946        return Err(JsValue::from_str("Null pointer provided"));
1947    }
1948    unsafe {
1949        let data = std::slice::from_raw_parts(data_ptr, len);
1950        let out_value = std::slice::from_raw_parts_mut(out_value_ptr, len);
1951        let out_upper = std::slice::from_raw_parts_mut(out_upper_ptr, len);
1952        let out_lower = std::slice::from_raw_parts_mut(out_lower_ptr, len);
1953        let out_slope = std::slice::from_raw_parts_mut(out_slope_ptr, len);
1954        let out_intercept = std::slice::from_raw_parts_mut(out_intercept_ptr, len);
1955        let out_deviation = std::slice::from_raw_parts_mut(out_deviation_ptr, len);
1956        let input = SmoothTheilSenInput::from_slice(
1957            data,
1958            SmoothTheilSenParams {
1959                length: Some(length),
1960                offset: Some(offset),
1961                multiplier: Some(multiplier),
1962                slope_style: Some(parse_stat_style(slope_style)?),
1963                residual_style: Some(parse_stat_style(residual_style)?),
1964                deviation_style: Some(parse_deviation_style(deviation_style)?),
1965                mad_style: Some(parse_stat_style(mad_style)?),
1966                include_prediction_in_deviation: Some(include_prediction_in_deviation),
1967            },
1968        );
1969        smooth_theil_sen_into_slice(
1970            out_value,
1971            out_upper,
1972            out_lower,
1973            out_slope,
1974            out_intercept,
1975            out_deviation,
1976            &input,
1977            Kernel::Auto,
1978        )
1979        .map_err(|e| JsValue::from_str(&e.to_string()))
1980    }
1981}
1982
1983#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1984#[wasm_bindgen]
1985pub fn smooth_theil_sen_batch_into(
1986    data_ptr: *const f64,
1987    out_value_ptr: *mut f64,
1988    out_upper_ptr: *mut f64,
1989    out_lower_ptr: *mut f64,
1990    out_slope_ptr: *mut f64,
1991    out_intercept_ptr: *mut f64,
1992    out_deviation_ptr: *mut f64,
1993    len: usize,
1994    length_start: usize,
1995    length_end: usize,
1996    length_step: usize,
1997    offset_start: usize,
1998    offset_end: usize,
1999    offset_step: usize,
2000    multiplier_start: f64,
2001    multiplier_end: f64,
2002    multiplier_step: f64,
2003    slope_style: String,
2004    residual_style: String,
2005    deviation_style: String,
2006    mad_style: String,
2007    include_prediction_in_deviation: bool,
2008) -> Result<usize, JsValue> {
2009    if data_ptr.is_null()
2010        || out_value_ptr.is_null()
2011        || out_upper_ptr.is_null()
2012        || out_lower_ptr.is_null()
2013        || out_slope_ptr.is_null()
2014        || out_intercept_ptr.is_null()
2015        || out_deviation_ptr.is_null()
2016    {
2017        return Err(JsValue::from_str(
2018            "null pointer passed to smooth_theil_sen_batch_into",
2019        ));
2020    }
2021    unsafe {
2022        let data = std::slice::from_raw_parts(data_ptr, len);
2023        let sweep = SmoothTheilSenBatchRange {
2024            length: (length_start, length_end, length_step),
2025            offset: (offset_start, offset_end, offset_step),
2026            multiplier: (multiplier_start, multiplier_end, multiplier_step),
2027            slope_style: parse_stat_style(slope_style)?,
2028            residual_style: parse_stat_style(residual_style)?,
2029            deviation_style: parse_deviation_style(deviation_style)?,
2030            mad_style: parse_stat_style(mad_style)?,
2031            include_prediction_in_deviation,
2032        };
2033        let combos =
2034            smooth_theil_sen_expand_grid(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
2035        let rows = combos.len();
2036        let total = rows.checked_mul(len).ok_or_else(|| {
2037            JsValue::from_str("rows*cols overflow in smooth_theil_sen_batch_into")
2038        })?;
2039        let out_value = std::slice::from_raw_parts_mut(out_value_ptr, total);
2040        let out_upper = std::slice::from_raw_parts_mut(out_upper_ptr, total);
2041        let out_lower = std::slice::from_raw_parts_mut(out_lower_ptr, total);
2042        let out_slope = std::slice::from_raw_parts_mut(out_slope_ptr, total);
2043        let out_intercept = std::slice::from_raw_parts_mut(out_intercept_ptr, total);
2044        let out_deviation = std::slice::from_raw_parts_mut(out_deviation_ptr, total);
2045        smooth_theil_sen_batch_into_slice(
2046            out_value,
2047            out_upper,
2048            out_lower,
2049            out_slope,
2050            out_intercept,
2051            out_deviation,
2052            data,
2053            &sweep,
2054            Kernel::Auto,
2055        )
2056        .map_err(|e| JsValue::from_str(&e.to_string()))?;
2057        Ok(rows)
2058    }
2059}
2060
2061#[cfg(test)]
2062mod tests {
2063    use super::*;
2064    use crate::indicators::dispatch::{
2065        compute_cpu_batch, IndicatorBatchRequest, IndicatorDataRef, IndicatorParamSet, ParamKV,
2066        ParamValue,
2067    };
2068
2069    fn sample_data(len: usize) -> Vec<f64> {
2070        (0..len)
2071            .map(|i| 100.0 + (i as f64) * 0.12 + ((i as f64) * 0.17).sin() * 1.5)
2072            .collect()
2073    }
2074
2075    fn assert_vec_close_with_nan(left: &[f64], right: &[f64]) {
2076        assert_eq!(left.len(), right.len());
2077        for (a, b) in left.iter().zip(right.iter()) {
2078            if a.is_nan() && b.is_nan() {
2079                continue;
2080            }
2081            assert!((a - b).abs() < 1e-10, "left={a} right={b}");
2082        }
2083    }
2084
2085    #[test]
2086    fn perfect_linear_series_has_zero_deviation() {
2087        let data: Vec<f64> = (0..128).map(|i| i as f64).collect();
2088        let input = SmoothTheilSenInput::from_slice(
2089            &data,
2090            SmoothTheilSenParams {
2091                length: Some(25),
2092                ..SmoothTheilSenParams::default()
2093            },
2094        );
2095        let out = smooth_theil_sen(&input).unwrap();
2096        for i in 24..data.len() {
2097            assert!((out.value[i] - data[i]).abs() < 1e-10);
2098            assert!(out.deviation[i].abs() < 1e-10);
2099            assert!((out.slope[i] + 1.0).abs() < 1e-10);
2100        }
2101    }
2102
2103    #[test]
2104    fn stream_matches_batch() {
2105        let data = sample_data(160);
2106        let params = SmoothTheilSenParams {
2107            length: Some(21),
2108            offset: Some(2),
2109            multiplier: Some(1.5),
2110            slope_style: Some(SmoothTheilSenStatStyle::SmoothMedian),
2111            residual_style: Some(SmoothTheilSenStatStyle::Median),
2112            deviation_style: Some(SmoothTheilSenDeviationType::Mad),
2113            mad_style: Some(SmoothTheilSenStatStyle::SmoothMedian),
2114            include_prediction_in_deviation: Some(true),
2115        };
2116        let batch =
2117            smooth_theil_sen(&SmoothTheilSenInput::from_slice(&data, params.clone())).unwrap();
2118        let mut stream = SmoothTheilSenStream::try_new(params).unwrap();
2119        for (i, value) in data.iter().copied().enumerate() {
2120            let got = stream.update(value);
2121            let expected = [
2122                batch.value[i],
2123                batch.upper[i],
2124                batch.lower[i],
2125                batch.slope[i],
2126                batch.intercept[i],
2127                batch.deviation[i],
2128            ];
2129            let actual = [got.0, got.1, got.2, got.3, got.4, got.5];
2130            for (left, right) in actual.into_iter().zip(expected) {
2131                if left.is_nan() && right.is_nan() {
2132                    continue;
2133                }
2134                assert!((left - right).abs() < 1e-10);
2135            }
2136        }
2137    }
2138
2139    #[test]
2140    fn batch_first_row_matches_single() {
2141        let data = sample_data(144);
2142        let single = smooth_theil_sen(&SmoothTheilSenInput::from_slice(
2143            &data,
2144            SmoothTheilSenParams {
2145                length: Some(21),
2146                offset: Some(1),
2147                multiplier: Some(1.5),
2148                slope_style: Some(SmoothTheilSenStatStyle::Mean),
2149                residual_style: Some(SmoothTheilSenStatStyle::SmoothMedian),
2150                deviation_style: Some(SmoothTheilSenDeviationType::Rmsd),
2151                mad_style: Some(SmoothTheilSenStatStyle::Median),
2152                include_prediction_in_deviation: Some(false),
2153            },
2154        ))
2155        .unwrap();
2156        let batch = smooth_theil_sen_batch_with_kernel(
2157            &data,
2158            &SmoothTheilSenBatchRange {
2159                length: (21, 23, 2),
2160                offset: (1, 1, 0),
2161                multiplier: (1.5, 1.5, 0.0),
2162                slope_style: SmoothTheilSenStatStyle::Mean,
2163                residual_style: SmoothTheilSenStatStyle::SmoothMedian,
2164                deviation_style: SmoothTheilSenDeviationType::Rmsd,
2165                mad_style: SmoothTheilSenStatStyle::Median,
2166                include_prediction_in_deviation: false,
2167            },
2168            Kernel::Auto,
2169        )
2170        .unwrap();
2171        assert_vec_close_with_nan(&batch.value[..data.len()], single.value.as_slice());
2172        assert_vec_close_with_nan(&batch.upper[..data.len()], single.upper.as_slice());
2173        assert_vec_close_with_nan(&batch.lower[..data.len()], single.lower.as_slice());
2174        assert_vec_close_with_nan(&batch.slope[..data.len()], single.slope.as_slice());
2175        assert_vec_close_with_nan(&batch.intercept[..data.len()], single.intercept.as_slice());
2176        assert_vec_close_with_nan(&batch.deviation[..data.len()], single.deviation.as_slice());
2177    }
2178
2179    #[test]
2180    fn invalid_length_fails() {
2181        let data = sample_data(32);
2182        let input = SmoothTheilSenInput::from_slice(
2183            &data,
2184            SmoothTheilSenParams {
2185                length: Some(1),
2186                ..SmoothTheilSenParams::default()
2187            },
2188        );
2189        match smooth_theil_sen(&input) {
2190            Err(SmoothTheilSenError::InvalidLength { length }) => assert_eq!(length, 1),
2191            other => panic!("unexpected result: {other:?}"),
2192        }
2193    }
2194
2195    #[test]
2196    fn cpu_dispatch_matches_direct_output() {
2197        let data = sample_data(128);
2198        let expected = smooth_theil_sen(&SmoothTheilSenInput::from_slice(
2199            &data,
2200            SmoothTheilSenParams {
2201                length: Some(21),
2202                offset: Some(2),
2203                multiplier: Some(1.75),
2204                slope_style: Some(SmoothTheilSenStatStyle::SmoothMedian),
2205                residual_style: Some(SmoothTheilSenStatStyle::SmoothMedian),
2206                deviation_style: Some(SmoothTheilSenDeviationType::Mad),
2207                mad_style: Some(SmoothTheilSenStatStyle::Median),
2208                include_prediction_in_deviation: Some(true),
2209            },
2210        ))
2211        .unwrap();
2212        let combos = [IndicatorParamSet {
2213            params: &[
2214                ParamKV {
2215                    key: "length",
2216                    value: ParamValue::Int(21),
2217                },
2218                ParamKV {
2219                    key: "offset",
2220                    value: ParamValue::Int(2),
2221                },
2222                ParamKV {
2223                    key: "multiplier",
2224                    value: ParamValue::Float(1.75),
2225                },
2226                ParamKV {
2227                    key: "slope_style",
2228                    value: ParamValue::EnumString("smooth_median"),
2229                },
2230                ParamKV {
2231                    key: "residual_style",
2232                    value: ParamValue::EnumString("smooth_median"),
2233                },
2234                ParamKV {
2235                    key: "deviation_style",
2236                    value: ParamValue::EnumString("mad"),
2237                },
2238                ParamKV {
2239                    key: "mad_style",
2240                    value: ParamValue::EnumString("median"),
2241                },
2242                ParamKV {
2243                    key: "include_prediction_in_deviation",
2244                    value: ParamValue::Bool(true),
2245                },
2246            ],
2247        }];
2248        let out = compute_cpu_batch(IndicatorBatchRequest {
2249            indicator_id: "smooth_theil_sen",
2250            output_id: Some("value"),
2251            data: IndicatorDataRef::Slice { values: &data },
2252            combos: &combos,
2253            kernel: Kernel::Auto,
2254        })
2255        .unwrap();
2256        assert_vec_close_with_nan(
2257            out.values_f64.unwrap().as_slice(),
2258            expected.value.as_slice(),
2259        );
2260    }
2261}