1use 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
351pub 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#[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 pub fn value(&self) -> Value {
567 self.value.clone()
568 }
569
570 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 pub fn into_value(self) -> Value {
579 self.value
580 }
581
582 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}