Skip to main content

runmat_runtime/builtins/math/poly/
polyval.rs

1//! MATLAB-compatible `polyval` builtin with GPU-aware semantics for RunMat.
2
3use log::debug;
4use num_complex::Complex64;
5use runmat_accelerate_api::{HostTensorView, ProviderPolyvalMu, ProviderPolyvalOptions};
6use runmat_builtins::{
7    BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinExtensionDescriptor,
8    BuiltinExtensionMode, BuiltinIntegerBackendRule, BuiltinIntegerCapabilityDescriptor,
9    BuiltinIntegerComputationDomain, BuiltinIntegerInputAvailability,
10    BuiltinIntegerInputCapability, BuiltinIntegerOutputClassRule, BuiltinIntegerOverflowRule,
11    BuiltinIntegerOverloadKind, BuiltinIntegerScalarDoubleRule, BuiltinOutputMode,
12    BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
13};
14use runmat_macros::runtime_builtin;
15use runmat_value::{ComplexTensor, LogicalArray, Tensor, Value};
16
17use crate::builtins::common::spec::{
18    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
19    ProviderHook, ReductionNaN, ResidencyPolicy, ScalarType, ShapeRequirements,
20};
21use crate::builtins::common::{gpu_helpers, tensor};
22use crate::builtins::math::poly::type_resolvers::polyval_type;
23use crate::{build_runtime_error, BuiltinResult, RuntimeError};
24
25const EPS: f64 = 1.0e-12;
26const BUILTIN_NAME: &str = "polyval";
27
28const POLYVAL_OUTPUT_Y: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
29    name: "y",
30    ty: BuiltinParamType::Any,
31    arity: BuiltinParamArity::Required,
32    default: None,
33    description: "Evaluated polynomial values at x.",
34}];
35
36const POLYVAL_OUTPUT_Y_DELTA: [BuiltinParamDescriptor; 2] = [
37    BuiltinParamDescriptor {
38        name: "y",
39        ty: BuiltinParamType::Any,
40        arity: BuiltinParamArity::Required,
41        default: None,
42        description: "Evaluated polynomial values at x.",
43    },
44    BuiltinParamDescriptor {
45        name: "delta",
46        ty: BuiltinParamType::Any,
47        arity: BuiltinParamArity::Required,
48        default: None,
49        description: "Prediction interval values when S is supplied.",
50    },
51];
52
53const POLYVAL_INPUTS: [BuiltinParamDescriptor; 2] = [
54    BuiltinParamDescriptor {
55        name: "p",
56        ty: BuiltinParamType::Any,
57        arity: BuiltinParamArity::Required,
58        default: None,
59        description: "Polynomial coefficient vector.",
60    },
61    BuiltinParamDescriptor {
62        name: "x",
63        ty: BuiltinParamType::Any,
64        arity: BuiltinParamArity::Required,
65        default: None,
66        description: "Evaluation points.",
67    },
68];
69
70const POLYVAL_INPUTS_WITH_S: [BuiltinParamDescriptor; 3] = [
71    BuiltinParamDescriptor {
72        name: "p",
73        ty: BuiltinParamType::Any,
74        arity: BuiltinParamArity::Required,
75        default: None,
76        description: "Polynomial coefficient vector.",
77    },
78    BuiltinParamDescriptor {
79        name: "x",
80        ty: BuiltinParamType::Any,
81        arity: BuiltinParamArity::Required,
82        default: None,
83        description: "Evaluation points.",
84    },
85    BuiltinParamDescriptor {
86        name: "S",
87        ty: BuiltinParamType::Any,
88        arity: BuiltinParamArity::Optional,
89        default: None,
90        description: "Optional polyfit statistics structure.",
91    },
92];
93
94const POLYVAL_INPUTS_WITH_S_MU: [BuiltinParamDescriptor; 4] = [
95    BuiltinParamDescriptor {
96        name: "p",
97        ty: BuiltinParamType::Any,
98        arity: BuiltinParamArity::Required,
99        default: None,
100        description: "Polynomial coefficient vector.",
101    },
102    BuiltinParamDescriptor {
103        name: "x",
104        ty: BuiltinParamType::Any,
105        arity: BuiltinParamArity::Required,
106        default: None,
107        description: "Evaluation points.",
108    },
109    BuiltinParamDescriptor {
110        name: "S",
111        ty: BuiltinParamType::Any,
112        arity: BuiltinParamArity::Optional,
113        default: None,
114        description: "Optional polyfit statistics structure (or []).",
115    },
116    BuiltinParamDescriptor {
117        name: "mu",
118        ty: BuiltinParamType::Any,
119        arity: BuiltinParamArity::Optional,
120        default: None,
121        description: "Optional centering/scaling vector [mean, std].",
122    },
123];
124
125const POLYVAL_SIGNATURES: [BuiltinSignatureDescriptor; 6] = [
126    BuiltinSignatureDescriptor {
127        label: "y = polyval(p, x)",
128        inputs: &POLYVAL_INPUTS,
129        outputs: &POLYVAL_OUTPUT_Y,
130    },
131    BuiltinSignatureDescriptor {
132        label: "y = polyval(p, x, S)",
133        inputs: &POLYVAL_INPUTS_WITH_S,
134        outputs: &POLYVAL_OUTPUT_Y,
135    },
136    BuiltinSignatureDescriptor {
137        label: "y = polyval(p, x, S, mu)",
138        inputs: &POLYVAL_INPUTS_WITH_S_MU,
139        outputs: &POLYVAL_OUTPUT_Y,
140    },
141    BuiltinSignatureDescriptor {
142        label: "[y, delta] = polyval(p, x)",
143        inputs: &POLYVAL_INPUTS,
144        outputs: &POLYVAL_OUTPUT_Y_DELTA,
145    },
146    BuiltinSignatureDescriptor {
147        label: "[y, delta] = polyval(p, x, S)",
148        inputs: &POLYVAL_INPUTS_WITH_S,
149        outputs: &POLYVAL_OUTPUT_Y_DELTA,
150    },
151    BuiltinSignatureDescriptor {
152        label: "[y, delta] = polyval(p, x, S, mu)",
153        inputs: &POLYVAL_INPUTS_WITH_S_MU,
154        outputs: &POLYVAL_OUTPUT_Y_DELTA,
155    },
156];
157
158const POLYVAL_ERROR_INVALID_ARGUMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
159    code: "RM.POLYVAL.INVALID_ARGUMENT",
160    identifier: Some("RunMat:polyval:InvalidArgument"),
161    when: "Option arguments (S/mu/output arity) are malformed or unsupported.",
162    message: "polyval: invalid argument",
163};
164
165const POLYVAL_ERROR_INVALID_INPUT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
166    code: "RM.POLYVAL.INVALID_INPUT",
167    identifier: Some("RunMat:polyval:InvalidInput"),
168    when: "Polynomial coefficients or evaluation points cannot be interpreted as numeric inputs.",
169    message: "polyval: invalid input",
170};
171
172const POLYVAL_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
173    code: "RM.POLYVAL.INTERNAL",
174    identifier: Some("RunMat:polyval:Internal"),
175    when: "Runtime fails while building output tensors, deltas, or provider fallbacks.",
176    message: "polyval: internal runtime failure",
177};
178
179const POLYVAL_ERRORS: [BuiltinErrorDescriptor; 3] = [
180    POLYVAL_ERROR_INVALID_ARGUMENT,
181    POLYVAL_ERROR_INVALID_INPUT,
182    POLYVAL_ERROR_INTERNAL,
183];
184
185pub const POLYVAL_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
186    signatures: &POLYVAL_SIGNATURES,
187    output_mode: BuiltinOutputMode::ByRequestedOutputCount,
188    completion_policy: BuiltinCompletionPolicy::Public,
189    errors: &POLYVAL_ERRORS,
190};
191
192const POLYVAL_INTEGER_COEFFICIENTS_EXTENSION: BuiltinExtensionDescriptor =
193    BuiltinExtensionDescriptor {
194        id: "polyval-integer-coefficients",
195        mode: BuiltinExtensionMode::RunMatOnly,
196        description: "polyval accepts typed-integer polynomial coefficients as a RunMat extension",
197        error_identifier: Some("RunMat:compatibility:PolyvalIntegerCoefficientsExtension"),
198    };
199const POLYVAL_INTEGER_POINTS_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
200    id: "polyval-integer-points",
201    mode: BuiltinExtensionMode::RunMatOnly,
202    description: "polyval accepts typed-integer query points as a RunMat extension",
203    error_identifier: Some("RunMat:compatibility:PolyvalIntegerPointsExtension"),
204};
205const POLYVAL_INTEGER_OPTIONS_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
206    id: "polyval-integer-fit-options",
207    mode: BuiltinExtensionMode::RunMatOnly,
208    description: "polyval accepts typed-integer S or mu data as a RunMat extension",
209    error_identifier: Some("RunMat:compatibility:PolyvalIntegerFitOptionsExtension"),
210};
211pub const POLYVAL_EXTENSIONS: [BuiltinExtensionDescriptor; 3] = [
212    POLYVAL_INTEGER_COEFFICIENTS_EXTENSION,
213    POLYVAL_INTEGER_POINTS_EXTENSION,
214    POLYVAL_INTEGER_OPTIONS_EXTENSION,
215];
216const POLYVAL_INTEGER_COEFFICIENTS_INPUT: [BuiltinIntegerInputCapability; 1] =
217    [BuiltinIntegerInputCapability {
218        name: "p",
219        classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
220        availability: BuiltinIntegerInputAvailability::RunMatOnly,
221        scalar_double: BuiltinIntegerScalarDoubleRule::NotApplicable,
222        notes: "The compatibility target documents single and double polynomial coefficients; RunMat admits typed integers only after exact floating conversion is proved.",
223    }];
224const POLYVAL_INTEGER_POINTS_INPUT: [BuiltinIntegerInputCapability; 1] =
225    [BuiltinIntegerInputCapability {
226        name: "x",
227        classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
228        availability: BuiltinIntegerInputAvailability::RunMatOnly,
229        scalar_double: BuiltinIntegerScalarDoubleRule::NotApplicable,
230        notes: "The compatibility target documents single and double query points; RunMat admits typed integers only at the checked Horner-evaluation boundary.",
231    }];
232const POLYVAL_INTEGER_OPTIONS_INPUT: [BuiltinIntegerInputCapability; 1] =
233    [BuiltinIntegerInputCapability {
234        name: "S or mu numeric fields",
235        classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
236        availability: BuiltinIntegerInputAvailability::RunMatOnly,
237        scalar_double: BuiltinIntegerScalarDoubleRule::NotApplicable,
238        notes: "The documented fit statistics and scaling vector are floating outputs of polyfit; native integer replacements are a checked RunMat extension.",
239    }];
240pub const POLYVAL_INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 3] = [
241    BuiltinIntegerCapabilityDescriptor {
242        form: "y = polyval(integer_p,x,___)",
243        inputs: &POLYVAL_INTEGER_COEFFICIENTS_INPUT,
244        computation_domain: BuiltinIntegerComputationDomain::FloatingPoint,
245        output_class: BuiltinIntegerOutputClassRule::FunctionSpecific,
246        overflow: BuiltinIntegerOverflowRule::Error,
247        backend: BuiltinIntegerBackendRule::GatherFallback,
248        overload: BuiltinIntegerOverloadKind::Multiple,
249        notes: "Integer coefficients are independently gated before provider or host Horner evaluation.",
250    },
251    BuiltinIntegerCapabilityDescriptor {
252        form: "y = polyval(p,integer_x,___)",
253        inputs: &POLYVAL_INTEGER_POINTS_INPUT,
254        computation_domain: BuiltinIntegerComputationDomain::FloatingPoint,
255        output_class: BuiltinIntegerOutputClassRule::FunctionSpecific,
256        overflow: BuiltinIntegerOverflowRule::Error,
257        backend: BuiltinIntegerBackendRule::GatherFallback,
258        overload: BuiltinIntegerOverloadKind::Multiple,
259        notes: "Integer query points are independently gated before provider or host Horner evaluation.",
260    },
261    BuiltinIntegerCapabilityDescriptor {
262        form: "[y,delta] = polyval(p,x,integer_S_or_mu)",
263        inputs: &POLYVAL_INTEGER_OPTIONS_INPUT,
264        computation_domain: BuiltinIntegerComputationDomain::FloatingPoint,
265        output_class: BuiltinIntegerOutputClassRule::FunctionSpecific,
266        overflow: BuiltinIntegerOverflowRule::Error,
267        backend: BuiltinIntegerBackendRule::GatherFallback,
268        overload: BuiltinIntegerOverloadKind::Multiple,
269        notes: "Typed integer fit metadata is independently gated and checked recursively before prediction-interval computation.",
270    },
271];
272
273#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::math::poly::polyval")]
274pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
275    name: "polyval",
276    op_kind: GpuOpKind::Custom("polyval"),
277    supported_precisions: &[ScalarType::F32, ScalarType::F64],
278    broadcast: BroadcastSemantics::Matlab,
279    provider_hooks: &[ProviderHook::Custom("polyval")],
280    constant_strategy: ConstantStrategy::UniformBuffer,
281    residency: ResidencyPolicy::NewHandle,
282    nan_mode: ReductionNaN::Include,
283    two_pass_threshold: None,
284    workgroup_size: None,
285    accepts_nan_mode: false,
286    notes:
287        "Uses provider-level Horner kernels for real coefficients/inputs; falls back to host evaluation (with upload) for complex or prediction-interval paths.",
288};
289
290fn polyval_error(message: impl Into<String>) -> RuntimeError {
291    polyval_error_with(message, &POLYVAL_ERROR_INVALID_INPUT)
292}
293
294fn polyval_argument_error(message: impl Into<String>) -> RuntimeError {
295    polyval_error_with(message, &POLYVAL_ERROR_INVALID_ARGUMENT)
296}
297
298fn polyval_error_with(
299    message: impl Into<String>,
300    error: &'static BuiltinErrorDescriptor,
301) -> RuntimeError {
302    let mut builder = build_runtime_error(message).with_builtin(BUILTIN_NAME);
303    if let Some(identifier) = error.identifier {
304        builder = builder.with_identifier(identifier);
305    }
306    builder.build()
307}
308
309#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::math::poly::polyval")]
310pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
311    name: "polyval",
312    shape: ShapeRequirements::Any,
313    constant_strategy: ConstantStrategy::UniformBuffer,
314    elementwise: None,
315    reduction: None,
316    emits_nan: true,
317    notes: "Acts as a fusion sink; real-valued workloads stay on device, while complex/delta paths gather to the host.",
318};
319
320#[runtime_builtin(
321    name = "polyval",
322    category = "math/poly",
323    summary = "Evaluate polynomials at specified points.",
324    keywords = "polyval,polynomial,polyfit,delta,gpu",
325    accel = "sink",
326    sink = true,
327    type_resolver(polyval_type),
328    descriptor(crate::builtins::math::poly::polyval::POLYVAL_DESCRIPTOR),
329    extensions(crate::builtins::math::poly::polyval::POLYVAL_EXTENSIONS),
330    integer_capabilities(crate::builtins::math::poly::polyval::POLYVAL_INTEGER_CAPABILITIES),
331    builtin_path = "crate::builtins::math::poly::polyval"
332)]
333async fn polyval_builtin(p: Value, x: Value, rest: Vec<Value>) -> crate::BuiltinResult<Value> {
334    if let Some(out_count) = crate::output_count::current_output_count() {
335        let eval = evaluate(p, x, &rest, out_count >= 2).await?;
336        if out_count == 0 {
337            return Ok(Value::OutputList(Vec::new()));
338        }
339        let mut outputs = vec![eval.value()];
340        if out_count >= 2 {
341            outputs.push(eval.delta()?);
342        }
343        return Ok(crate::output_count::output_list_with_padding(
344            out_count, outputs,
345        ));
346    }
347    let eval = evaluate(p, x, &rest, false).await?;
348    Ok(eval.value())
349}
350
351/// Evaluate `polyval`, optionally computing the prediction interval.
352pub async fn evaluate(
353    coefficients: Value,
354    points: Value,
355    rest: &[Value],
356    want_delta: bool,
357) -> BuiltinResult<PolyvalEval> {
358    crate::builtins::common::validation::reject_typed_complex_integer(&coefficients, BUILTIN_NAME)?;
359    crate::builtins::common::validation::reject_typed_complex_integer(&points, BUILTIN_NAME)?;
360    for option in rest {
361        crate::builtins::common::validation::reject_typed_complex_integer(option, BUILTIN_NAME)?;
362    }
363    crate::builtins::common::validation::ensure_runmat_integer_f64_boundary(
364        &coefficients,
365        &POLYVAL_INTEGER_COEFFICIENTS_EXTENSION,
366        BUILTIN_NAME,
367        "coefficient",
368    )
369    .await?;
370    crate::builtins::common::validation::ensure_runmat_integer_f64_boundary(
371        &points,
372        &POLYVAL_INTEGER_POINTS_EXTENSION,
373        BUILTIN_NAME,
374        "query point",
375    )
376    .await?;
377    for option in rest {
378        crate::builtins::common::validation::ensure_runmat_integer_f64_boundary(
379            option,
380            &POLYVAL_INTEGER_OPTIONS_EXTENSION,
381            BUILTIN_NAME,
382            "fit-option",
383        )
384        .await?;
385    }
386    let options = parse_option_values(rest).await?;
387
388    let coeff_clone = coefficients.clone();
389    let points_clone = points.clone();
390
391    let coeff_was_gpu = matches!(coefficients, Value::GpuTensor(_));
392    let (coeffs, coeff_real) = convert_coefficients(coeff_clone).await?;
393
394    let (mut inputs, prefer_gpu_points) = convert_points(points_clone).await?;
395    let prefer_gpu_output = prefer_gpu_points || coeff_was_gpu;
396
397    let mu = match options.mu.clone() {
398        Some(mu_value) => Some(parse_mu(mu_value).await?),
399        None => None,
400    };
401
402    if prefer_gpu_output && !want_delta && options.s.is_none() {
403        if let Some(value) =
404            try_gpu_polyval(&coeffs, coeff_real, &inputs, mu, prefer_gpu_output).await?
405        {
406            return Ok(PolyvalEval::new(value, None));
407        }
408    }
409
410    if let Some(mu_val) = mu {
411        apply_mu(&mut inputs.data, mu_val)?;
412    }
413
414    let stats = if let Some(s_value) = options.s {
415        parse_stats(s_value, coeffs.len()).await?
416    } else {
417        None
418    };
419
420    if want_delta && stats.is_none() {
421        return Err(polyval_argument_error(
422            "polyval: S input (structure returned by polyfit) is required for delta output",
423        ));
424    }
425
426    if inputs.data.is_empty() {
427        let y = zeros_like(&inputs.shape, prefer_gpu_output)?;
428        let delta = if want_delta {
429            Some(zeros_like(&inputs.shape, prefer_gpu_output)?)
430        } else {
431            None
432        };
433        return Ok(PolyvalEval::new(y, delta));
434    }
435
436    if coeffs.is_empty() {
437        let zeros = zeros_like(&inputs.shape, prefer_gpu_output)?;
438        let delta = if want_delta {
439            Some(zeros_like(&inputs.shape, prefer_gpu_output)?)
440        } else {
441            None
442        };
443        return Ok(PolyvalEval::new(zeros, delta));
444    }
445
446    let output_real = coeff_real && inputs.all_real;
447    let values = evaluate_polynomial(&coeffs, &inputs.data);
448    let result_value = finalize_values(
449        &values,
450        &inputs.shape,
451        prefer_gpu_output,
452        output_real && values_are_real(&values),
453    )?;
454
455    let delta_value = if want_delta {
456        let stats = stats.expect("delta requires stats");
457        let delta = compute_prediction_interval(&coeffs, &inputs.data, &stats)?;
458        let prefer = prefer_gpu_output && stats.is_real;
459        Some(finalize_delta(delta, &inputs.shape, prefer)?)
460    } else {
461        None
462    };
463
464    Ok(PolyvalEval::new(result_value, delta_value))
465}
466
467async fn try_gpu_polyval(
468    coeffs: &[Complex64],
469    coeff_real: bool,
470    inputs: &NumericArray,
471    mu: Option<Mu>,
472    prefer_gpu_output: bool,
473) -> BuiltinResult<Option<Value>> {
474    if !coeff_real || !inputs.all_real {
475        return Ok(None);
476    }
477    if coeffs.is_empty() || inputs.data.is_empty() {
478        return Ok(None);
479    }
480    let Some(provider) = runmat_accelerate_api::provider() else {
481        return Ok(None);
482    };
483
484    let coeff_data: Vec<f64> = coeffs.iter().map(|c| c.re).collect();
485    let coeff_shape = vec![1usize, coeffs.len()];
486    let coeff_view = HostTensorView {
487        data: &coeff_data,
488        shape: &coeff_shape,
489    };
490    let coeff_handle = match provider.upload(&coeff_view) {
491        Ok(handle) => handle,
492        Err(err) => {
493            debug!("polyval: GPU upload of coefficients failed, falling back: {err}");
494            return Ok(None);
495        }
496    };
497
498    let input_data: Vec<f64> = inputs.data.iter().map(|c| c.re).collect();
499    let input_shape = inputs.shape.clone();
500    let input_view = HostTensorView {
501        data: &input_data,
502        shape: &input_shape,
503    };
504    let input_handle = match provider.upload(&input_view) {
505        Ok(handle) => handle,
506        Err(err) => {
507            debug!("polyval: GPU upload of evaluation points failed, falling back: {err}");
508            let _ = provider.free(&coeff_handle);
509            return Ok(None);
510        }
511    };
512
513    let options = ProviderPolyvalOptions {
514        mu: mu.map(|m| ProviderPolyvalMu {
515            mean: m.mean,
516            scale: m.scale,
517        }),
518    };
519
520    let result_handle = match provider.polyval(&coeff_handle, &input_handle, &options) {
521        Ok(handle) => handle,
522        Err(err) => {
523            debug!("polyval: GPU kernel execution failed, falling back: {err}");
524            let _ = provider.free(&coeff_handle);
525            let _ = provider.free(&input_handle);
526            return Ok(None);
527        }
528    };
529
530    let _ = provider.free(&coeff_handle);
531    let _ = provider.free(&input_handle);
532
533    if prefer_gpu_output {
534        return Ok(Some(Value::GpuTensor(result_handle)));
535    }
536
537    let host = match gpu_helpers::download_floating_projection_async(provider, &result_handle).await
538    {
539        Ok(host) => host,
540        Err(err) => {
541            debug!("polyval: GPU download failed, falling back: {err}");
542            let _ = provider.free(&result_handle);
543            return Ok(None);
544        }
545    };
546    let _ = provider.free(&result_handle);
547
548    let tensor =
549        Tensor::new(host.data, host.shape).map_err(|e| polyval_error(format!("polyval: {e}")))?;
550    Ok(Some(tensor::tensor_into_value(tensor)))
551}
552
553/// Result object for polyval evaluation.
554#[derive(Debug)]
555pub struct PolyvalEval {
556    value: Value,
557    delta: Option<Value>,
558}
559
560impl PolyvalEval {
561    fn new(value: Value, delta: Option<Value>) -> Self {
562        Self { value, delta }
563    }
564
565    /// Primary output (`y`).
566    pub fn value(&self) -> Value {
567        self.value.clone()
568    }
569
570    /// Optional prediction interval (`delta`).
571    pub fn delta(&self) -> BuiltinResult<Value> {
572        self.delta
573            .clone()
574            .ok_or_else(|| polyval_argument_error("polyval: delta output not computed"))
575    }
576
577    /// Consume into the main value.
578    pub fn into_value(self) -> Value {
579        self.value
580    }
581
582    /// Consume into `(value, delta)` pair.
583    pub fn into_pair(self) -> BuiltinResult<(Value, Value)> {
584        match self.delta {
585            Some(delta) => Ok((self.value, delta)),
586            None => Err(polyval_argument_error("polyval: delta output not computed")),
587        }
588    }
589}
590
591#[derive(Clone, Copy)]
592struct Mu {
593    mean: f64,
594    scale: f64,
595}
596
597impl Mu {
598    fn new(mean: f64, scale: f64) -> BuiltinResult<Self> {
599        if !mean.is_finite() || !scale.is_finite() {
600            return Err(polyval_error("polyval: mu values must be finite"));
601        }
602        if scale.abs() <= EPS {
603            return Err(polyval_error("polyval: mu(2) must be non-zero"));
604        }
605        Ok(Self { mean, scale })
606    }
607}
608
609#[derive(Clone)]
610struct NumericArray {
611    data: Vec<Complex64>,
612    shape: Vec<usize>,
613    all_real: bool,
614}
615
616#[derive(Clone)]
617struct PolyfitStats {
618    r: Matrix,
619    df: f64,
620    normr: f64,
621    is_real: bool,
622}
623
624impl PolyfitStats {
625    fn is_effective(&self) -> bool {
626        self.r.len() > 0 && self.df > 0.0 && self.normr.is_finite()
627    }
628}
629
630#[derive(Clone)]
631struct Matrix {
632    rows: usize,
633    cols: usize,
634    data: Vec<Complex64>,
635}
636
637impl Matrix {
638    fn get(&self, row: usize, col: usize) -> Complex64 {
639        self.data[row + col * self.rows]
640    }
641
642    fn len(&self) -> usize {
643        self.rows * self.cols
644    }
645}
646
647struct ParsedOptions {
648    s: Option<Value>,
649    mu: Option<Value>,
650}
651
652async fn parse_option_values(rest: &[Value]) -> BuiltinResult<ParsedOptions> {
653    match rest.len() {
654        0 => Ok(ParsedOptions { s: None, mu: None }),
655        1 => Ok(ParsedOptions {
656            s: if is_empty_value(&rest[0]).await? {
657                None
658            } else {
659                Some(rest[0].clone())
660            },
661            mu: None,
662        }),
663        2 => Ok(ParsedOptions {
664            s: if is_empty_value(&rest[0]).await? {
665                None
666            } else {
667                Some(rest[0].clone())
668            },
669            mu: Some(rest[1].clone()),
670        }),
671        _ => Err(polyval_argument_error("polyval: too many input arguments")),
672    }
673}
674
675#[async_recursion::async_recursion(?Send)]
676async fn convert_coefficients(value: Value) -> BuiltinResult<(Vec<Complex64>, bool)> {
677    match value {
678        Value::GpuTensor(handle) => {
679            let gathered =
680                gpu_helpers::gather_value_async(&Value::GpuTensor(handle.clone())).await?;
681            convert_coefficients(gathered).await
682        }
683        Value::Tensor(tensor) => {
684            ensure_vector_shape("polyval", &tensor.shape)?;
685            let data = tensor::tensor_values_f64(&tensor)
686                .into_iter()
687                .map(|re| Complex64::new(re, 0.0))
688                .collect();
689            Ok((data, true))
690        }
691        Value::ComplexTensor(mut tensor) => {
692            ensure_vector_shape("polyval", &tensor.shape)?;
693            let all_real = complex_tensor_values_are_real(&tensor);
694            let data = complex_tensor_values(&mut tensor);
695            Ok((data, all_real))
696        }
697        Value::LogicalArray(mut array) => {
698            ensure_vector_data_shape("polyval", &array.shape)?;
699            let data = array
700                .data
701                .drain(..)
702                .map(|bit| Complex64::new(if bit != 0 { 1.0 } else { 0.0 }, 0.0))
703                .collect();
704            Ok((data, true))
705        }
706        Value::Num(n) => Ok((vec![Complex64::new(n, 0.0)], true)),
707        Value::Int(i) => Ok((vec![Complex64::new(i.to_f64(), 0.0)], true)),
708        Value::Bool(flag) => Ok((
709            vec![Complex64::new(if flag { 1.0 } else { 0.0 }, 0.0)],
710            true,
711        )),
712        Value::Complex(re, im) => Ok((vec![Complex64::new(re, im)], im.abs() <= EPS)),
713        other => Err(polyval_error(format!(
714            "polyval: coefficients must be numeric, got {other:?}"
715        ))),
716    }
717}
718
719async fn convert_points(value: Value) -> BuiltinResult<(NumericArray, bool)> {
720    match value {
721        Value::GpuTensor(handle) => {
722            let tensor = gpu_helpers::gather_tensor_async(&handle).await?;
723            let array = NumericArray {
724                data: tensor::tensor_values_f64(&tensor)
725                    .into_iter()
726                    .map(|re| Complex64::new(re, 0.0))
727                    .collect(),
728                shape: tensor.shape.clone(),
729                all_real: true,
730            };
731            Ok((array, true))
732        }
733        Value::Tensor(tensor) => Ok((
734            NumericArray {
735                data: tensor::tensor_values_f64(&tensor)
736                    .into_iter()
737                    .map(|re| Complex64::new(re, 0.0))
738                    .collect(),
739                shape: tensor.shape.clone(),
740                all_real: true,
741            },
742            false,
743        )),
744        Value::ComplexTensor(tensor) => Ok((
745            NumericArray {
746                data: complex_tensor_values_ref(&tensor),
747                shape: tensor.shape.clone(),
748                all_real: complex_tensor_values_are_real(&tensor),
749            },
750            false,
751        )),
752        Value::LogicalArray(array) => Ok((
753            NumericArray {
754                data: array
755                    .data
756                    .iter()
757                    .map(|&bit| Complex64::new(if bit != 0 { 1.0 } else { 0.0 }, 0.0))
758                    .collect(),
759                shape: array.shape.clone(),
760                all_real: true,
761            },
762            false,
763        )),
764        Value::Num(n) => Ok((
765            NumericArray {
766                data: vec![Complex64::new(n, 0.0)],
767                shape: vec![1, 1],
768                all_real: true,
769            },
770            false,
771        )),
772        Value::Int(i) => Ok((
773            NumericArray {
774                data: vec![Complex64::new(i.to_f64(), 0.0)],
775                shape: vec![1, 1],
776                all_real: true,
777            },
778            false,
779        )),
780        Value::Bool(flag) => Ok((
781            NumericArray {
782                data: vec![Complex64::new(if flag { 1.0 } else { 0.0 }, 0.0)],
783                shape: vec![1, 1],
784                all_real: true,
785            },
786            false,
787        )),
788        Value::Complex(re, im) => Ok((
789            NumericArray {
790                data: vec![Complex64::new(re, im)],
791                shape: vec![1, 1],
792                all_real: im.abs() <= EPS,
793            },
794            false,
795        )),
796        other => Err(polyval_error(format!(
797            "polyval: X must be numeric, got {other:?}"
798        ))),
799    }
800}
801
802#[async_recursion::async_recursion(?Send)]
803async fn parse_mu(value: Value) -> BuiltinResult<Mu> {
804    match value {
805        Value::GpuTensor(handle) => {
806            let gathered = gpu_helpers::gather_tensor_async(&handle).await?;
807            parse_mu(Value::Tensor(gathered)).await
808        }
809        Value::Tensor(tensor) => {
810            if tensor_element_len(&tensor) < 2 {
811                return Err(polyval_error(
812                    "polyval: mu must contain at least two elements",
813                ));
814            }
815            let values = tensor::tensor_values_f64(&tensor);
816            Mu::new(values[0], values[1])
817        }
818        Value::LogicalArray(array) => {
819            if array.data.len() < 2 {
820                return Err(polyval_error(
821                    "polyval: mu must contain at least two elements",
822                ));
823            }
824            let mean = if array.data[0] != 0 { 1.0 } else { 0.0 };
825            let scale = if array.data[1] != 0 { 1.0 } else { 0.0 };
826            Mu::new(mean, scale)
827        }
828        Value::Num(_) | Value::Int(_) | Value::Bool(_) | Value::Complex(_, _) => Err(
829            polyval_error("polyval: mu must be a numeric vector with at least two values"),
830        ),
831        Value::ComplexTensor(tensor) => {
832            if complex_tensor_element_len(&tensor) < 2 {
833                return Err(polyval_error(
834                    "polyval: mu must contain at least two elements",
835                ));
836            }
837            let ((mean_re, mean_im), (scale_re, scale_im)) =
838                if let Some(storage) = tensor.integer_storage() {
839                    let mean_re = storage
840                        .real
841                        .value_at(0)
842                        .expect("complex integer mu real mean")
843                        .to_f64();
844                    let mean_im = storage
845                        .imag
846                        .value_at(0)
847                        .expect("complex integer mu imag mean")
848                        .to_f64();
849                    let scale_re = storage
850                        .real
851                        .value_at(1)
852                        .expect("complex integer mu real scale")
853                        .to_f64();
854                    let scale_im = storage
855                        .imag
856                        .value_at(1)
857                        .expect("complex integer mu imag scale")
858                        .to_f64();
859                    ((mean_re, mean_im), (scale_re, scale_im))
860                } else {
861                    (tensor.materialize_f64()[0], tensor.materialize_f64()[1])
862                };
863            if mean_im.abs() > EPS || scale_im.abs() > EPS {
864                return Err(polyval_error("polyval: mu values must be real"));
865            }
866            Mu::new(mean_re, scale_re)
867        }
868        _ => Err(polyval_error(
869            "polyval: mu must be a numeric vector with at least two values",
870        )),
871    }
872}
873
874#[async_recursion::async_recursion(?Send)]
875async fn parse_stats(value: Value, coeff_len: usize) -> BuiltinResult<Option<PolyfitStats>> {
876    if is_empty_value(&value).await? {
877        return Ok(None);
878    }
879    let struct_value = match value {
880        Value::Struct(s) => s,
881        Value::GpuTensor(handle) => {
882            let gathered = gpu_helpers::gather_value_async(&Value::GpuTensor(handle)).await?;
883            return parse_stats(gathered, coeff_len).await;
884        }
885        other => {
886            return Err(polyval_error(format!(
887                "polyval: S input must be the structure returned by polyfit, got {other:?}"
888            )))
889        }
890    };
891    let r_value = struct_value
892        .fields
893        .get("R")
894        .cloned()
895        .ok_or_else(|| polyval_error("polyval: S input is missing the field 'R'"))?;
896    let df_value = struct_value
897        .fields
898        .get("df")
899        .cloned()
900        .ok_or_else(|| polyval_error("polyval: S input is missing the field 'df'"))?;
901    let normr_value = struct_value
902        .fields
903        .get("normr")
904        .cloned()
905        .ok_or_else(|| polyval_error("polyval: S input is missing the field 'normr'"))?;
906
907    let (matrix, is_real) = convert_matrix(r_value, coeff_len).await?;
908    let df = scalar_to_f64(df_value, "polyval: S.df").await?;
909    let normr = scalar_to_f64(normr_value, "polyval: S.normr").await?;
910
911    Ok(Some(PolyfitStats {
912        r: matrix,
913        df,
914        normr,
915        is_real,
916    }))
917}
918
919#[async_recursion::async_recursion(?Send)]
920async fn convert_matrix(value: Value, coeff_len: usize) -> BuiltinResult<(Matrix, bool)> {
921    match value {
922        Value::GpuTensor(handle) => {
923            let tensor = gpu_helpers::gather_tensor_async(&handle).await?;
924            convert_matrix(Value::Tensor(tensor), coeff_len).await
925        }
926        Value::Tensor(tensor) => {
927            let rows = tensor.rows;
928            let cols = tensor.cols;
929            if rows != coeff_len || cols != coeff_len {
930                return Err(polyval_error("polyval: size of S.R must match the coefficient vector"));
931            }
932            let data = tensor::tensor_values_f64(&tensor)
933                .into_iter()
934                .map(|re| Complex64::new(re, 0.0))
935                .collect();
936            Ok((Matrix { rows, cols, data }, true))
937        }
938        Value::ComplexTensor(mut tensor) => {
939            let rows = tensor.rows;
940            let cols = tensor.cols;
941            if rows != coeff_len || cols != coeff_len {
942                return Err(polyval_error("polyval: size of S.R must match the coefficient vector"));
943            }
944            let imag_small = complex_tensor_values_are_real(&tensor);
945            let data = complex_tensor_values(&mut tensor);
946            Ok((Matrix { rows, cols, data }, imag_small))
947        }
948        Value::LogicalArray(array) => {
949            let LogicalArray { data, shape } = array;
950            let rows = shape.first().copied().unwrap_or(0);
951            let cols = shape.get(1).copied().unwrap_or(0);
952            if rows != coeff_len || cols != coeff_len {
953                return Err(polyval_error("polyval: size of S.R must match the coefficient vector"));
954            }
955            let data = data
956                .into_iter()
957                .map(|bit| Complex64::new(if bit != 0 { 1.0 } else { 0.0 }, 0.0))
958                .collect();
959            Ok((Matrix { rows, cols, data }, true))
960        }
961        Value::Num(_) | Value::Int(_) | Value::Bool(_) | Value::Complex(_, _) => Err(
962            polyval_error(
963                "polyval: S.R must be a square numeric matrix matching the coefficient vector length",
964            ),
965        ),
966        Value::Struct(_)
967        | Value::Cell(_)
968        | Value::String(_)
969        | Value::StringArray(_)
970        | Value::CharArray(_) => Err(
971            polyval_error(
972                "polyval: S.R must be a square numeric matrix matching the coefficient vector length",
973            ),
974        ),
975        _ => Err(
976            polyval_error(
977                "polyval: S.R must be a square numeric matrix matching the coefficient vector length",
978            ),
979        ),
980    }
981}
982
983#[async_recursion::async_recursion(?Send)]
984async fn scalar_to_f64(value: Value, context: &str) -> BuiltinResult<f64> {
985    match value {
986        Value::Num(n) => Ok(n),
987        Value::Int(i) => Ok(i.to_f64()),
988        Value::Bool(flag) => Ok(if flag { 1.0 } else { 0.0 }),
989        Value::Tensor(tensor) => {
990            if tensor_element_len(&tensor) != 1 {
991                return Err(polyval_error(format!("{context} must be a scalar")));
992            }
993            Ok(tensor::tensor_value_f64(&tensor, 0))
994        }
995        Value::LogicalArray(array) => {
996            if array.data.len() != 1 {
997                return Err(polyval_error(format!("{context} must be a scalar")));
998            }
999            Ok(if array.data[0] != 0 { 1.0 } else { 0.0 })
1000        }
1001        Value::GpuTensor(handle) => {
1002            let tensor = gpu_helpers::gather_tensor_async(&handle).await?;
1003            scalar_to_f64(Value::Tensor(tensor), context).await
1004        }
1005        Value::Complex(_, _) | Value::ComplexTensor(_) => {
1006            Err(polyval_error(format!("{context} must be real-valued")))
1007        }
1008        other => Err(polyval_error(format!(
1009            "{context} must be a scalar, got {other:?}"
1010        ))),
1011    }
1012}
1013
1014fn apply_mu(values: &mut [Complex64], mu: Mu) -> BuiltinResult<()> {
1015    let mean = Complex64::new(mu.mean, 0.0);
1016    let scale = Complex64::new(mu.scale, 0.0);
1017    for v in values.iter_mut() {
1018        *v = (*v - mean) / scale;
1019    }
1020    Ok(())
1021}
1022
1023fn evaluate_polynomial(coeffs: &[Complex64], inputs: &[Complex64]) -> Vec<Complex64> {
1024    let mut outputs = Vec::with_capacity(inputs.len());
1025    for &x in inputs {
1026        let mut acc = Complex64::new(0.0, 0.0);
1027        for &c in coeffs {
1028            acc = acc * x + c;
1029        }
1030        outputs.push(acc);
1031    }
1032    outputs
1033}
1034
1035fn compute_prediction_interval(
1036    coeffs: &[Complex64],
1037    inputs: &[Complex64],
1038    stats: &PolyfitStats,
1039) -> BuiltinResult<Vec<f64>> {
1040    if !stats.is_effective() {
1041        return Ok(vec![0.0; inputs.len()]);
1042    }
1043    let n = coeffs.len();
1044    let mut delta = Vec::with_capacity(inputs.len());
1045    for &x in inputs {
1046        let row = vandermonde_row(x, n);
1047        let solved = solve_row_against_upper(&row, &stats.r)?;
1048        let sum_sq: f64 = solved.iter().map(|c| c.norm_sqr()).sum();
1049        let interval = (1.0 + sum_sq).sqrt() * (stats.normr / stats.df.sqrt());
1050        delta.push(interval);
1051    }
1052    Ok(delta)
1053}
1054
1055fn vandermonde_row(x: Complex64, len: usize) -> Vec<Complex64> {
1056    if len == 0 {
1057        return vec![Complex64::new(1.0, 0.0)];
1058    }
1059    let degree = len - 1;
1060    let mut powers = vec![Complex64::new(1.0, 0.0); degree + 1];
1061    for idx in 1..=degree {
1062        powers[idx] = powers[idx - 1] * x;
1063    }
1064    let mut row = vec![Complex64::new(0.0, 0.0); degree + 1];
1065    for (i, value) in powers.into_iter().enumerate() {
1066        row[degree - i] = value;
1067    }
1068    row
1069}
1070
1071fn solve_row_against_upper(row: &[Complex64], matrix: &Matrix) -> BuiltinResult<Vec<Complex64>> {
1072    let n = row.len();
1073    if matrix.rows != n || matrix.cols != n {
1074        return Err(polyval_error(
1075            "polyval: size of S.R must match the coefficient vector",
1076        ));
1077    }
1078    let mut result = vec![Complex64::new(0.0, 0.0); n];
1079    for j in (0..n).rev() {
1080        let mut acc = row[j];
1081        for (k, value) in result.iter().enumerate().skip(j + 1) {
1082            acc -= *value * matrix.get(k, j);
1083        }
1084        let diag = matrix.get(j, j);
1085        if diag.norm() <= EPS {
1086            return Err(polyval_error("polyval: S.R is singular"));
1087        }
1088        result[j] = acc / diag;
1089    }
1090    Ok(result)
1091}
1092
1093fn finalize_values(
1094    data: &[Complex64],
1095    shape: &[usize],
1096    prefer_gpu: bool,
1097    real_only: bool,
1098) -> BuiltinResult<Value> {
1099    if real_only {
1100        let real_data: Vec<f64> = data.iter().map(|c| c.re).collect();
1101        finalize_real(real_data, shape, prefer_gpu)
1102    } else if data.len() == 1 {
1103        let value = data[0];
1104        Ok(Value::Complex(value.re, value.im))
1105    } else {
1106        let complex_data: Vec<(f64, f64)> = data.iter().map(|c| (c.re, c.im)).collect();
1107        let tensor = ComplexTensor::new(complex_data, shape.to_vec())
1108            .map_err(|e| polyval_error(format!("polyval: failed to build complex tensor: {e}")))?;
1109        Ok(Value::ComplexTensor(tensor))
1110    }
1111}
1112
1113fn finalize_delta(data: Vec<f64>, shape: &[usize], prefer_gpu: bool) -> BuiltinResult<Value> {
1114    finalize_real(data, shape, prefer_gpu)
1115}
1116
1117fn finalize_real(data: Vec<f64>, shape: &[usize], prefer_gpu: bool) -> BuiltinResult<Value> {
1118    let tensor = Tensor::new(data, shape.to_vec())
1119        .map_err(|e| polyval_error(format!("polyval: failed to build tensor: {e}")))?;
1120    if prefer_gpu {
1121        if let Some(provider) = runmat_accelerate_api::provider() {
1122            let data = tensor::tensor_values_f64_cow(&tensor);
1123            let view = HostTensorView {
1124                data: data.as_ref(),
1125                shape: &tensor.shape,
1126            };
1127            if let Ok(handle) = provider.upload(&view) {
1128                return Ok(Value::GpuTensor(handle));
1129            }
1130        }
1131    }
1132    Ok(tensor::tensor_into_value(tensor))
1133}
1134
1135fn zeros_like(shape: &[usize], prefer_gpu: bool) -> BuiltinResult<Value> {
1136    let len = shape.iter().product();
1137    finalize_real(vec![0.0; len], shape, prefer_gpu)
1138}
1139
1140fn ensure_vector_shape(name: &str, shape: &[usize]) -> BuiltinResult<()> {
1141    if !is_vector_shape(shape) {
1142        Err(polyval_error(format!(
1143            "{name}: coefficients must be a scalar, row vector, or column vector"
1144        )))
1145    } else {
1146        Ok(())
1147    }
1148}
1149
1150fn ensure_vector_data_shape(name: &str, shape: &[usize]) -> BuiltinResult<()> {
1151    if !is_vector_shape(shape) {
1152        Err(polyval_error(format!(
1153            "{name}: inputs must be vectors or scalars"
1154        )))
1155    } else {
1156        Ok(())
1157    }
1158}
1159
1160fn is_vector_shape(shape: &[usize]) -> bool {
1161    shape.iter().filter(|&&dim| dim > 1).count() <= 1
1162}
1163
1164fn tensor_element_len(tensor: &Tensor) -> usize {
1165    tensor.len()
1166}
1167
1168fn complex_tensor_element_len(tensor: &ComplexTensor) -> usize {
1169    tensor
1170        .integer_storage()
1171        .as_ref()
1172        .map_or(tensor.materialize_f64().len(), |storage| storage.len())
1173}
1174
1175fn complex_tensor_values(tensor: &mut ComplexTensor) -> Vec<Complex64> {
1176    if let Some(storage) = tensor.integer_storage() {
1177        return storage
1178            .real
1179            .exact_values()
1180            .into_iter()
1181            .zip(storage.imag.exact_values())
1182            .map(|(re, im)| Complex64::new(re.to_f64(), im.to_f64()))
1183            .collect();
1184    }
1185    tensor
1186        .materialize_f64()
1187        .drain(..)
1188        .map(|(re, im)| Complex64::new(re, im))
1189        .collect()
1190}
1191
1192fn complex_tensor_values_ref(tensor: &ComplexTensor) -> Vec<Complex64> {
1193    if let Some(storage) = tensor.integer_storage() {
1194        return storage
1195            .real
1196            .exact_values()
1197            .into_iter()
1198            .zip(storage.imag.exact_values())
1199            .map(|(re, im)| Complex64::new(re.to_f64(), im.to_f64()))
1200            .collect();
1201    }
1202    tensor
1203        .materialize_f64()
1204        .iter()
1205        .map(|&(re, im)| Complex64::new(re, im))
1206        .collect()
1207}
1208
1209fn complex_tensor_values_are_real(tensor: &ComplexTensor) -> bool {
1210    if let Some(storage) = tensor.integer_storage() {
1211        return storage
1212            .imag
1213            .exact_values()
1214            .iter()
1215            .all(|value| value.is_zero());
1216    }
1217    tensor
1218        .materialize_f64()
1219        .iter()
1220        .all(|&(_, im)| im.abs() <= EPS)
1221}
1222
1223#[async_recursion::async_recursion(?Send)]
1224async fn is_empty_value(value: &Value) -> BuiltinResult<bool> {
1225    match value {
1226        Value::Tensor(t) => Ok(tensor_element_len(t) == 0),
1227        Value::LogicalArray(l) => Ok(l.data.is_empty()),
1228        Value::Cell(ca) => Ok(ca.data.is_empty()),
1229        Value::GpuTensor(handle) => {
1230            let gathered =
1231                gpu_helpers::gather_value_async(&Value::GpuTensor(handle.clone())).await?;
1232            is_empty_value(&gathered).await
1233        }
1234        _ => Ok(false),
1235    }
1236}
1237
1238fn values_are_real(values: &[Complex64]) -> bool {
1239    values.iter().all(|c| c.im.abs() <= EPS)
1240}
1241
1242#[cfg(test)]
1243pub(crate) mod tests {
1244    use super::*;
1245    use crate::builtins::common::test_support;
1246    use futures::executor::block_on;
1247    use runmat_value::{IntegerComplexStorage, IntegerStorage, StructValue};
1248
1249    fn assert_error_contains(err: crate::RuntimeError, needle: &str) {
1250        assert!(
1251            err.message().contains(needle),
1252            "expected error containing '{needle}', got '{}'",
1253            err.message()
1254        );
1255    }
1256
1257    #[test]
1258    fn polyval_descriptor_signatures_cover_core_forms() {
1259        let labels: Vec<&str> = POLYVAL_DESCRIPTOR
1260            .signatures
1261            .iter()
1262            .map(|signature| signature.label)
1263            .collect();
1264        assert!(labels.contains(&"y = polyval(p, x)"));
1265        assert!(labels.contains(&"y = polyval(p, x, S)"));
1266        assert!(labels.contains(&"y = polyval(p, x, S, mu)"));
1267        assert!(labels.contains(&"[y, delta] = polyval(p, x, S)"));
1268    }
1269
1270    #[test]
1271    fn polyval_descriptor_errors_have_stable_codes() {
1272        let codes: Vec<&str> = POLYVAL_DESCRIPTOR
1273            .errors
1274            .iter()
1275            .map(|error| error.code)
1276            .collect();
1277        assert!(codes.contains(&"RM.POLYVAL.INVALID_ARGUMENT"));
1278        assert!(codes.contains(&"RM.POLYVAL.INVALID_INPUT"));
1279        assert!(codes.contains(&"RM.POLYVAL.INTERNAL"));
1280    }
1281
1282    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1283    #[test]
1284    fn polyval_scalar() {
1285        let coeffs = Tensor::new(vec![2.0, -3.0, 5.0], vec![1, 3]).unwrap();
1286        let value =
1287            polyval_builtin(Value::Tensor(coeffs), Value::Num(4.0), Vec::new()).expect("polyval");
1288        match value {
1289            Value::Num(n) => assert!((n - (2.0 * 16.0 - 12.0 + 5.0)).abs() < 1e-12),
1290            other => panic!("expected scalar, got {other:?}"),
1291        }
1292    }
1293
1294    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1295    #[test]
1296    fn polyval_matrix_input() {
1297        let coeffs = Tensor::new(vec![1.0, 0.0, -2.0, 1.0], vec![1, 4]).unwrap();
1298        let points = Tensor::new(vec![-2.0, -1.0, 0.0, 1.0, 2.0], vec![5, 1]).unwrap();
1299        let value = polyval_builtin(
1300            Value::Tensor(coeffs),
1301            Value::Tensor(points.clone()),
1302            Vec::new(),
1303        )
1304        .expect("polyval");
1305        match value {
1306            Value::Tensor(tensor) => {
1307                assert_eq!(tensor.shape, points.shape);
1308                let expected = vec![-3.0, 2.0, 1.0, 0.0, 5.0];
1309                assert_eq!(tensor.materialize_f64(), expected);
1310            }
1311            other => panic!("expected tensor output, got {other:?}"),
1312        }
1313    }
1314
1315    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1316    #[test]
1317    fn polyval_complex_inputs() {
1318        let coeffs =
1319            ComplexTensor::new(vec![(1.0, 2.0), (-3.0, 0.0), (0.0, 4.0)], vec![1, 3]).unwrap();
1320        let points =
1321            ComplexTensor::new(vec![(-1.0, 1.0), (0.0, 0.0), (1.0, -2.0)], vec![1, 3]).unwrap();
1322        let value = polyval_builtin(
1323            Value::ComplexTensor(coeffs),
1324            Value::ComplexTensor(points.clone()),
1325            Vec::new(),
1326        )
1327        .expect("polyval");
1328        match value {
1329            Value::ComplexTensor(tensor) => {
1330                assert_eq!(tensor.shape, points.shape);
1331                assert_eq!(tensor.materialize_f64().len(), 3);
1332            }
1333            other => panic!("expected complex tensor, got {other:?}"),
1334        }
1335    }
1336
1337    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1338    #[test]
1339    fn polyval_with_mu() {
1340        let coeffs = Tensor::new(vec![1.0, 0.0, 0.0], vec![1, 3]).unwrap();
1341        let points = Tensor::new(vec![0.0, 1.0, 2.0], vec![1, 3]).unwrap();
1342        let mu = Tensor::new(vec![1.0, 2.0], vec![1, 2]).unwrap();
1343        let value = polyval_builtin(
1344            Value::Tensor(coeffs),
1345            Value::Tensor(points),
1346            vec![
1347                Value::Tensor(Tensor::new(vec![], vec![0, 0]).unwrap()),
1348                Value::Tensor(mu),
1349            ],
1350        )
1351        .expect("polyval");
1352        match value {
1353            Value::Tensor(tensor) => {
1354                assert_eq!(tensor.materialize_f64(), vec![0.25, 0.0, 0.25]);
1355            }
1356            other => panic!("expected tensor output, got {other:?}"),
1357        }
1358    }
1359
1360    #[test]
1361    fn polyval_typed_integer_coefficients_points_and_mu_cross_double_boundary_exactly() {
1362        let _extensions = crate::compatibility::push_runmat_extensions_enabled(true);
1363        let coeffs = Tensor::new_integer(IntegerStorage::I16(vec![1, 0, 0]), vec![1, 3]).unwrap();
1364        let points = Tensor::new_integer(IntegerStorage::U16(vec![0, 1, 2]), vec![1, 3]).unwrap();
1365        let mu = Tensor::new_integer(IntegerStorage::I16(vec![1, 2]), vec![1, 2]).unwrap();
1366        let value = polyval_builtin(
1367            Value::Tensor(coeffs),
1368            Value::Tensor(points),
1369            vec![
1370                Value::Tensor(Tensor::new(vec![], vec![0, 0]).unwrap()),
1371                Value::Tensor(mu),
1372            ],
1373        )
1374        .expect("polyval");
1375        match value {
1376            Value::Tensor(tensor) => {
1377                assert_eq!(tensor.shape, vec![1, 3]);
1378                assert_eq!(tensor.materialize_f64(), vec![0.25, 0.0, 0.25]);
1379                assert!(tensor.integer_storage().is_none());
1380            }
1381            other => panic!("expected tensor output, got {other:?}"),
1382        }
1383    }
1384
1385    #[test]
1386    fn polyval_complex_integer_mu_rejects_before_conversion() {
1387        let _extensions = crate::compatibility::push_runmat_extensions_enabled(true);
1388        let coeffs = Tensor::new(vec![1.0, 0.0, 0.0], vec![1, 3]).unwrap();
1389        let points = Tensor::new(vec![0.0, 1.0, 2.0], vec![1, 3]).unwrap();
1390        let storage = IntegerComplexStorage::new(
1391            IntegerStorage::I16(vec![1, 2]),
1392            IntegerStorage::I16(vec![0, 0]),
1393        )
1394        .expect("complex integer mu");
1395        let mu = ComplexTensor::new_integer(storage, vec![1, 2]).expect("mu tensor");
1396
1397        let error = polyval_builtin(
1398            Value::Tensor(coeffs),
1399            Value::Tensor(points),
1400            vec![
1401                Value::Tensor(Tensor::new(vec![], vec![0, 0]).unwrap()),
1402                Value::ComplexTensor(mu),
1403            ],
1404        )
1405        .expect_err("typed complex integer fit options must reject");
1406        assert!(
1407            error
1408                .message()
1409                .contains("complex numbers with integer types are not supported"),
1410            "{error:?}"
1411        );
1412    }
1413
1414    #[test]
1415    fn polyval_stats_fields_read_typed_integer_storage_exactly() {
1416        let _extensions = crate::compatibility::push_runmat_extensions_enabled(true);
1417        let coeffs = Tensor::new(vec![1.0, -3.0, 2.0], vec![1, 3]).unwrap();
1418        let points = Tensor::new(vec![0.0, 1.0, 2.0], vec![1, 3]).unwrap();
1419        let mut st = StructValue::new();
1420        let r = Tensor::new_integer(
1421            IntegerStorage::I16(vec![1, 0, 0, 0, 1, 0, 0, 0, 1]),
1422            vec![3, 3],
1423        )
1424        .unwrap();
1425        let df = Tensor::new_integer(IntegerStorage::U16(vec![4]), vec![1, 1]).unwrap();
1426        let normr = Tensor::new_integer(IntegerStorage::U16(vec![2]), vec![1, 1]).unwrap();
1427        st.fields.insert("R".to_string(), Value::Tensor(r));
1428        st.fields.insert("df".to_string(), Value::Tensor(df));
1429        st.fields.insert("normr".to_string(), Value::Tensor(normr));
1430
1431        let eval = futures::executor::block_on(evaluate(
1432            Value::Tensor(coeffs),
1433            Value::Tensor(points),
1434            &[Value::Struct(st)],
1435            true,
1436        ))
1437        .expect("polyval");
1438
1439        let (_, delta) = eval.into_pair().expect("delta available");
1440        match delta {
1441            Value::Tensor(tensor) => {
1442                assert_eq!(tensor.shape, vec![1, 3]);
1443                assert!(tensor
1444                    .materialize_f64()
1445                    .iter()
1446                    .all(|value| value.is_finite()));
1447            }
1448            other => panic!("expected tensor delta, got {other:?}"),
1449        }
1450    }
1451
1452    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1453    #[test]
1454    fn polyval_delta_computation() {
1455        let coeffs = Tensor::new(vec![1.0, -3.0, 2.0], vec![1, 3]).unwrap();
1456        let points = Tensor::new(vec![0.0, 1.0, 2.0], vec![1, 3]).unwrap();
1457        let mut st = StructValue::new();
1458        let r = Tensor::new(
1459            vec![1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0],
1460            vec![3, 3],
1461        )
1462        .unwrap();
1463        st.fields.insert("R".to_string(), Value::Tensor(r));
1464        st.fields.insert("df".to_string(), Value::Num(4.0));
1465        st.fields.insert("normr".to_string(), Value::Num(2.0));
1466        let stats = Value::Struct(st);
1467        let eval = futures::executor::block_on(evaluate(
1468            Value::Tensor(coeffs),
1469            Value::Tensor(points),
1470            &[stats],
1471            true,
1472        ))
1473        .expect("polyval");
1474        let (_, delta) = eval.into_pair().expect("delta available");
1475        match delta {
1476            Value::Tensor(tensor) => {
1477                assert_eq!(tensor.shape, vec![1, 3]);
1478                assert_eq!(tensor.materialize_f64().len(), 3);
1479            }
1480            other => panic!("expected tensor delta, got {other:?}"),
1481        }
1482    }
1483
1484    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1485    #[test]
1486    fn polyval_delta_requires_stats() {
1487        let coeffs = Tensor::new(vec![1.0, 0.0], vec![1, 2]).unwrap();
1488        let points = Tensor::new(vec![1.0], vec![1, 1]).unwrap();
1489        let err = futures::executor::block_on(evaluate(
1490            Value::Tensor(coeffs),
1491            Value::Tensor(points),
1492            &[],
1493            true,
1494        ))
1495        .expect_err("expected error");
1496        assert_error_contains(err, "S input");
1497    }
1498
1499    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1500    #[test]
1501    fn polyval_invalid_mu_length_errors() {
1502        let coeffs = Tensor::new(vec![1.0, 0.0], vec![1, 2]).unwrap();
1503        let points = Tensor::new(vec![0.0], vec![1, 1]).unwrap();
1504        let mu = Tensor::new(vec![1.0], vec![1, 1]).unwrap();
1505        let placeholder = Tensor::new(vec![], vec![0, 0]).unwrap();
1506        let err = polyval_builtin(
1507            Value::Tensor(coeffs),
1508            Value::Tensor(points),
1509            vec![Value::Tensor(placeholder), Value::Tensor(mu)],
1510        )
1511        .expect_err("expected mu length error");
1512        assert_error_contains(err, "mu must contain at least two elements");
1513    }
1514
1515    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1516    #[test]
1517    fn polyval_rejects_excess_optional_arguments() {
1518        let coeffs = Tensor::new(vec![1.0, 0.0], vec![1, 2]).unwrap();
1519        let points = Tensor::new(vec![0.0], vec![1, 1]).unwrap();
1520        let err = polyval_builtin(
1521            Value::Tensor(coeffs),
1522            Value::Tensor(points),
1523            vec![Value::Num(1.0), Value::Num(2.0), Value::Num(3.0)],
1524        )
1525        .expect_err("expected too many arguments error");
1526        assert_eq!(err.identifier(), POLYVAL_ERROR_INVALID_ARGUMENT.identifier);
1527        assert_error_contains(err, "too many input arguments");
1528    }
1529
1530    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1531    #[test]
1532    fn polyval_complex_mu_rejected() {
1533        let coeffs = Tensor::new(vec![1.0, 0.0], vec![1, 2]).unwrap();
1534        let points = Tensor::new(vec![0.0], vec![1, 1]).unwrap();
1535        let complex_mu =
1536            ComplexTensor::new(vec![(0.0, 0.0), (1.0, 0.5)], vec![1, 2]).expect("complex mu");
1537        let placeholder = Tensor::new(vec![], vec![0, 0]).unwrap();
1538        let err = polyval_builtin(
1539            Value::Tensor(coeffs),
1540            Value::Tensor(points),
1541            vec![Value::Tensor(placeholder), Value::ComplexTensor(complex_mu)],
1542        )
1543        .expect_err("expected complex mu error");
1544        assert_error_contains(err, "mu values must be real");
1545    }
1546
1547    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1548    #[test]
1549    fn polyval_invalid_stats_missing_r() {
1550        let coeffs = Tensor::new(vec![1.0, -3.0, 2.0], vec![1, 3]).unwrap();
1551        let points = Tensor::new(vec![0.0], vec![1, 1]).unwrap();
1552        let mut st = StructValue::new();
1553        st.fields.insert("df".to_string(), Value::Num(1.0));
1554        st.fields.insert("normr".to_string(), Value::Num(1.0));
1555        let stats = Value::Struct(st);
1556        let err = polyval_builtin(Value::Tensor(coeffs), Value::Tensor(points), vec![stats])
1557            .expect_err("expected missing R error");
1558        assert_error_contains(err, "missing the field 'R'");
1559    }
1560
1561    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1562    #[test]
1563    fn polyval_gpu_roundtrip() {
1564        test_support::with_test_provider(|provider| {
1565            let coeffs = Tensor::new(vec![1.0, 0.0, 1.0], vec![1, 3]).unwrap();
1566            let points = Tensor::new(vec![-1.0, 0.0, 1.0], vec![3, 1]).unwrap();
1567            let coeff_handle = provider
1568                .upload(&HostTensorView {
1569                    data: &coeffs.materialize_f64(),
1570                    shape: &coeffs.shape,
1571                })
1572                .expect("upload coeff");
1573            let point_handle = provider
1574                .upload(&HostTensorView {
1575                    data: &points.materialize_f64(),
1576                    shape: &points.shape,
1577                })
1578                .expect("upload points");
1579            let value = polyval_builtin(
1580                Value::GpuTensor(coeff_handle),
1581                Value::GpuTensor(point_handle),
1582                Vec::new(),
1583            )
1584            .expect("polyval");
1585            match value {
1586                Value::GpuTensor(handle) => {
1587                    let gathered = test_support::gather(Value::GpuTensor(handle)).expect("gather");
1588                    assert_eq!(gathered.materialize_f64(), vec![2.0, 1.0, 2.0]);
1589                }
1590                other => panic!("expected gpu tensor, got {other:?}"),
1591            }
1592        });
1593    }
1594
1595    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1596    #[test]
1597    #[cfg(feature = "wgpu")]
1598    fn polyval_wgpu_matches_cpu_real_inputs() {
1599        let _ = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
1600            runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
1601        );
1602        let coeffs = Tensor::new(vec![1.0, -3.0, 2.0], vec![1, 3]).unwrap();
1603        let points = Tensor::new(vec![-2.0, -1.0, 0.5, 2.5], vec![4, 1]).unwrap();
1604
1605        let provider = runmat_accelerate_api::provider().expect("wgpu provider");
1606        let coeff_handle = provider
1607            .upload(&HostTensorView {
1608                data: &coeffs.materialize_f64(),
1609                shape: &coeffs.shape,
1610            })
1611            .expect("upload coeffs");
1612        let point_handle = provider
1613            .upload(&HostTensorView {
1614                data: &points.materialize_f64(),
1615                shape: &points.shape,
1616            })
1617            .expect("upload points");
1618
1619        let gpu_value = polyval_builtin(
1620            Value::GpuTensor(coeff_handle.clone()),
1621            Value::GpuTensor(point_handle.clone()),
1622            Vec::new(),
1623        )
1624        .expect("polyval gpu");
1625
1626        let _ = provider.free(&coeff_handle);
1627        let _ = provider.free(&point_handle);
1628
1629        let gathered = test_support::gather(gpu_value).expect("gather");
1630
1631        let coeff_complex: Vec<Complex64> = coeffs
1632            .materialize_f64()
1633            .iter()
1634            .map(|&c| Complex64::new(c, 0.0))
1635            .collect();
1636        let point_complex: Vec<Complex64> = points
1637            .materialize_f64()
1638            .iter()
1639            .map(|&x| Complex64::new(x, 0.0))
1640            .collect();
1641        let expected_vals = evaluate_polynomial(&coeff_complex, &point_complex);
1642        let expected: Vec<f64> = expected_vals.iter().map(|c| c.re).collect();
1643
1644        assert_eq!(gathered.shape, vec![4, 1]);
1645        assert_eq!(gathered.materialize_f64(), expected);
1646    }
1647
1648    fn polyval_builtin(p: Value, x: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
1649        block_on(super::polyval_builtin(p, x, rest))
1650    }
1651}