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(¶ms),
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 ¶ms,
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 ¶ms,
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(¶ms)?,
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(¶ms));
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 ¶ms,
1421 first + warmup_bars(¶ms),
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}