1use log::{trace, warn};
4use num_complex::Complex64;
5use runmat_builtins::{
6 BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinIntegerBackendRule,
7 BuiltinIntegerCapabilityDescriptor, BuiltinIntegerComputationDomain,
8 BuiltinIntegerInputAvailability, BuiltinIntegerInputCapability, BuiltinIntegerOutputClassRule,
9 BuiltinIntegerOverflowRule, BuiltinIntegerOverloadKind, BuiltinIntegerScalarDoubleRule,
10 BuiltinOutputMode, BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType,
11 BuiltinSignatureDescriptor,
12};
13use runmat_macros::runtime_builtin;
14use runmat_value::{ComplexTensor, NumericDType, NumericStorage, Tensor, Value};
15
16use crate::builtins::common::gpu_helpers;
17use crate::builtins::common::spec::{
18 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
19 ProviderHook, ReductionNaN, ResidencyPolicy, ScalarType, ShapeRequirements,
20};
21use crate::builtins::common::tensor;
22use crate::builtins::math::poly::type_resolvers::polyint_type;
23use crate::dispatcher;
24use crate::{build_runtime_error, BuiltinResult, RuntimeError};
25
26const EPS: f64 = 1.0e-12;
27const BUILTIN_NAME: &str = "polyint";
28
29const POLYINT_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
30 name: "q",
31 ty: BuiltinParamType::Any,
32 arity: BuiltinParamArity::Required,
33 default: None,
34 description: "Integrated polynomial coefficient vector.",
35}];
36
37const POLYINT_INPUTS: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
38 name: "p",
39 ty: BuiltinParamType::Any,
40 arity: BuiltinParamArity::Required,
41 default: None,
42 description: "Polynomial coefficient vector.",
43}];
44
45const POLYINT_INPUTS_WITH_K: [BuiltinParamDescriptor; 2] = [
46 BuiltinParamDescriptor {
47 name: "p",
48 ty: BuiltinParamType::Any,
49 arity: BuiltinParamArity::Required,
50 default: None,
51 description: "Polynomial coefficient vector.",
52 },
53 BuiltinParamDescriptor {
54 name: "k",
55 ty: BuiltinParamType::Any,
56 arity: BuiltinParamArity::Optional,
57 default: None,
58 description: "Constant of integration.",
59 },
60];
61
62const POLYINT_SIGNATURES: [BuiltinSignatureDescriptor; 2] = [
63 BuiltinSignatureDescriptor {
64 label: "q = polyint(p)",
65 inputs: &POLYINT_INPUTS,
66 outputs: &POLYINT_OUTPUT,
67 },
68 BuiltinSignatureDescriptor {
69 label: "q = polyint(p, k)",
70 inputs: &POLYINT_INPUTS_WITH_K,
71 outputs: &POLYINT_OUTPUT,
72 },
73];
74
75const POLYINT_ERROR_INVALID_ARGUMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
76 code: "RM.POLYINT.INVALID_ARGUMENT",
77 identifier: Some("RunMat:polyint:InvalidArgument"),
78 when: "Input arity or integration-constant argument is malformed.",
79 message: "polyint: invalid argument",
80};
81
82const POLYINT_ERROR_INVALID_INPUT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
83 code: "RM.POLYINT.INVALID_INPUT",
84 identifier: Some("RunMat:polyint:InvalidInput"),
85 when: "Inputs are not single- or double-precision coefficient/constant values, or coefficients do not form a vector.",
86 message: "polyint: invalid input",
87};
88
89const POLYINT_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
90 code: "RM.POLYINT.INTERNAL",
91 identifier: Some("RunMat:polyint:Internal"),
92 when: "Runtime fails while building output tensors or provider fallback paths.",
93 message: "polyint: internal runtime failure",
94};
95
96const POLYINT_ERRORS: [BuiltinErrorDescriptor; 3] = [
97 POLYINT_ERROR_INVALID_ARGUMENT,
98 POLYINT_ERROR_INVALID_INPUT,
99 POLYINT_ERROR_INTERNAL,
100];
101
102pub const POLYINT_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
103 signatures: &POLYINT_SIGNATURES,
104 output_mode: BuiltinOutputMode::Fixed,
105 completion_policy: BuiltinCompletionPolicy::Public,
106 errors: &POLYINT_ERRORS,
107};
108
109const INTEGER_INPUTS: [BuiltinIntegerInputCapability; 2] = [
110 BuiltinIntegerInputCapability {
111 name: "p",
112 classes: &[],
113 availability: BuiltinIntegerInputAvailability::Rejected,
114 scalar_double: BuiltinIntegerScalarDoubleRule::NotApplicable,
115 notes: "Integer and logical coefficient vectors are rejected before floating host/provider dispatch.",
116 },
117 BuiltinIntegerInputCapability {
118 name: "k",
119 classes: &[],
120 availability: BuiltinIntegerInputAvailability::Rejected,
121 scalar_double: BuiltinIntegerScalarDoubleRule::NotApplicable,
122 notes: "Integer and logical integration constants are rejected; only single/double real or complex values are supported.",
123 },
124];
125
126pub const INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 1] =
127 [BuiltinIntegerCapabilityDescriptor {
128 form: "q = polyint(p, k)",
129 inputs: &INTEGER_INPUTS,
130 computation_domain: BuiltinIntegerComputationDomain::FloatingPoint,
131 output_class: BuiltinIntegerOutputClassRule::NotApplicable,
132 overflow: BuiltinIntegerOverflowRule::NotApplicable,
133 backend: BuiltinIntegerBackendRule::HostAndGpu,
134 overload: BuiltinIntegerOverloadKind::Multiple,
135 notes: "This builtin has no integer overload; the empty accepted-class masks are intentional and prevent generic numeric coercion from admitting integers.",
136 }];
137
138#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::math::poly::polyint")]
139pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
140 name: "polyint",
141 op_kind: GpuOpKind::Custom("polynomial-integral"),
142 supported_precisions: &[ScalarType::F32, ScalarType::F64],
143 broadcast: BroadcastSemantics::None,
144 provider_hooks: &[ProviderHook::Custom("polyint")],
145 constant_strategy: ConstantStrategy::InlineLiteral,
146 residency: ResidencyPolicy::NewHandle,
147 nan_mode: ReductionNaN::Include,
148 two_pass_threshold: None,
149 workgroup_size: None,
150 accepts_nan_mode: false,
151 notes: "Providers implement the polyint hook for real and complex-interleaved coefficient vectors; complex integration constants fall back to host integration and re-upload.",
152};
153
154fn polyint_error(message: impl Into<String>) -> RuntimeError {
155 polyint_error_with(message, &POLYINT_ERROR_INVALID_INPUT)
156}
157
158fn polyint_argument_error(message: impl Into<String>) -> RuntimeError {
159 polyint_error_with(message, &POLYINT_ERROR_INVALID_ARGUMENT)
160}
161
162fn polyint_error_with(
163 message: impl Into<String>,
164 error: &'static BuiltinErrorDescriptor,
165) -> RuntimeError {
166 let mut builder = build_runtime_error(message).with_builtin(BUILTIN_NAME);
167 if let Some(identifier) = error.identifier {
168 builder = builder.with_identifier(identifier);
169 }
170 builder.build()
171}
172
173#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::math::poly::polyint")]
174pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
175 name: "polyint",
176 shape: ShapeRequirements::Any,
177 constant_strategy: ConstantStrategy::InlineLiteral,
178 elementwise: None,
179 reduction: None,
180 emits_nan: false,
181 notes: "Symbolic operation on coefficient vectors; fusion does not apply.",
182};
183
184#[runtime_builtin(
185 name = "polyint",
186 category = "math/poly",
187 summary = "Integrate polynomial coefficient vectors and append a constant of integration.",
188 keywords = "polyint,polynomial,integral,antiderivative",
189 type_resolver(polyint_type),
190 descriptor(crate::builtins::math::poly::polyint::POLYINT_DESCRIPTOR),
191 integer_capabilities(crate::builtins::math::poly::polyint::INTEGER_CAPABILITIES),
192 builtin_path = "crate::builtins::math::poly::polyint"
193)]
194async fn polyint_builtin(coeffs: Value, rest: Vec<Value>) -> crate::BuiltinResult<Value> {
195 if rest.len() > 1 {
196 return Err(polyint_argument_error("polyint: too many input arguments"));
197 }
198 reject_unsupported_numeric_class(&coeffs, "coefficient")?;
199 for value in &rest {
200 reject_unsupported_numeric_class(value, "constant")?;
201 }
202
203 let constant = match rest.into_iter().next() {
204 Some(value) => parse_constant(value).await?,
205 None => Complex64::new(0.0, 0.0),
206 };
207
208 if let Value::GpuTensor(handle) = &coeffs {
209 if let Some(device_result) = try_polyint_gpu(handle, constant)? {
210 return Ok(Value::GpuTensor(device_result));
211 }
212 }
213
214 let source_gpu = match &coeffs {
215 Value::GpuTensor(handle) => Some(handle.clone()),
216 _ => None,
217 };
218 polyint_host_value(coeffs, constant, source_gpu).await
219}
220
221async fn polyint_host_value(
222 coeffs: Value,
223 constant: Complex64,
224 source_gpu: Option<runmat_accelerate_api::GpuTensorHandle>,
225) -> BuiltinResult<Value> {
226 let polynomial = parse_polynomial(coeffs).await?;
227 let mut integrated = integrate_coeffs(&polynomial.coeffs);
228 if integrated.is_empty() {
229 integrated.push(constant);
230 } else if let Some(last) = integrated.last_mut() {
231 *last += constant;
232 }
233 let value = coeffs_to_value(&integrated, polynomial.class)?;
234 maybe_return_gpu(value, source_gpu.as_ref())
235}
236
237fn reject_unsupported_numeric_class(value: &Value, role: &str) -> BuiltinResult<()> {
238 let unsupported = match value {
239 Value::Int(_) | Value::Bool(_) | Value::LogicalArray(_) => true,
240 Value::Tensor(tensor) => !matches!(
241 tensor.numeric_dtype(),
242 NumericDType::F64 | NumericDType::F32
243 ),
244 Value::ComplexTensor(tensor) => tensor.integer_storage().is_some(),
245 Value::GpuTensor(handle) => {
246 runmat_accelerate_api::handle_integer_type(handle).is_some()
247 || runmat_accelerate_api::handle_is_logical(handle)
248 }
249 _ => false,
250 };
251 if unsupported {
252 return Err(polyint_error(format!(
253 "polyint: {role} input must be single or double"
254 )));
255 }
256 Ok(())
257}
258
259fn try_polyint_gpu(
260 handle: &runmat_accelerate_api::GpuTensorHandle,
261 constant: Complex64,
262) -> BuiltinResult<Option<runmat_accelerate_api::GpuTensorHandle>> {
263 if constant.im.abs() > EPS {
264 return Ok(None);
265 }
266 ensure_vector_shape(&handle.shape)?;
267 let Some(provider) =
268 runmat_accelerate_api::provider_for_handle(handle).or_else(runmat_accelerate_api::provider)
269 else {
270 return Ok(None);
271 };
272 match provider.polyint(handle, constant.re) {
273 Ok(result) => Ok(Some(result)),
274 Err(err) => {
275 trace!("polyint: provider hook unavailable, falling back to host: {err}");
276 Ok(None)
277 }
278 }
279}
280
281fn integrate_coeffs(coeffs: &[Complex64]) -> Vec<Complex64> {
282 if coeffs.is_empty() {
283 return Vec::new();
284 }
285 let mut result = Vec::with_capacity(coeffs.len() + 1);
286 for (idx, coeff) in coeffs.iter().enumerate() {
287 let power = (coeffs.len() - idx) as f64;
288 if power <= 0.0 {
289 result.push(Complex64::new(0.0, 0.0));
290 } else {
291 result.push(*coeff / Complex64::new(power, 0.0));
292 }
293 }
294 result.push(Complex64::new(0.0, 0.0));
295 result
296}
297
298fn maybe_return_gpu(
299 value: Value,
300 source_gpu: Option<&runmat_accelerate_api::GpuTensorHandle>,
301) -> BuiltinResult<Value> {
302 let Some(source_gpu) = source_gpu else {
303 return Ok(value);
304 };
305 let provider = runmat_accelerate_api::provider_for_handle(source_gpu);
306 match value {
307 Value::Tensor(tensor) => {
308 if let Some(provider) = provider {
309 match gpu_helpers::upload_tensor(provider, &tensor) {
310 Ok(handle) => return Ok(Value::GpuTensor(handle)),
311 Err(err) => {
312 warn!("polyint: provider upload failed, keeping result on host: {err}");
313 }
314 }
315 } else {
316 trace!("polyint: no provider available to re-upload result");
317 }
318 Ok(Value::Tensor(tensor))
319 }
320 Value::ComplexTensor(tensor) => {
321 if let Some(provider) = provider {
322 match gpu_helpers::upload_complex_tensor(provider, &tensor) {
323 Ok(handle) => return Ok(gpu_helpers::complex_gpu_value(handle)),
324 Err(err) => {
325 warn!(
326 "polyint: provider complex upload failed, keeping result on host: {err}"
327 );
328 }
329 }
330 } else {
331 trace!("polyint: no provider available to re-upload complex result");
332 }
333 Ok(Value::ComplexTensor(tensor))
334 }
335 other => Ok(other),
336 }
337}
338
339fn coeffs_to_value(coeffs: &[Complex64], class: FloatingClass) -> BuiltinResult<Value> {
340 let shape = vec![1, coeffs.len()];
341 if coeffs.iter().all(|c| c.im.abs() <= EPS) {
342 let tensor = match class {
343 FloatingClass::Double => {
344 let data = coeffs.iter().map(|c| c.re).collect();
345 Tensor::new(data, shape)
346 }
347 FloatingClass::Single => {
348 let data = coeffs.iter().map(|c| c.re as f32).collect();
349 Tensor::from_f32(data, shape)
350 }
351 }
352 .map_err(|e| polyint_error(format!("polyint: {e}")))?;
353 Ok(tensor::tensor_into_value(tensor))
354 } else {
355 let data: Vec<(f64, f64)> = coeffs.iter().map(|c| (c.re, c.im)).collect();
356 let tensor =
357 ComplexTensor::new(data, shape).map_err(|e| polyint_error(format!("polyint: {e}")))?;
358 Ok(Value::ComplexTensor(tensor))
359 }
360}
361
362async fn parse_polynomial(value: Value) -> BuiltinResult<Polynomial> {
363 let gathered = dispatcher::gather_if_needed_async(&value).await?;
364 match gathered {
365 Value::Tensor(tensor) => parse_tensor_coeffs(tensor),
366 Value::ComplexTensor(tensor) => parse_complex_tensor_coeffs(&tensor),
367 Value::Num(n) => Ok(Polynomial {
368 coeffs: vec![Complex64::new(n, 0.0)],
369 class: FloatingClass::Double,
370 }),
371 Value::Complex(re, im) => Ok(Polynomial {
372 coeffs: vec![Complex64::new(re, im)],
373 class: FloatingClass::Double,
374 }),
375 other => Err(polyint_error(format!(
376 "polyint: expected a numeric coefficient vector, got {:?}",
377 other
378 ))),
379 }
380}
381
382fn parse_tensor_coeffs(tensor: Tensor) -> BuiltinResult<Polynomial> {
383 ensure_vector_shape(&tensor.shape)?;
384 let storage = tensor
385 .into_numeric_storage()
386 .map_err(|error| polyint_error(format!("polyint: {error}")))?;
387 let (coeffs, class) = match storage {
388 NumericStorage::F64(values) => (values, FloatingClass::Double),
389 NumericStorage::F32(values) => (
390 values.into_iter().map(f64::from).collect(),
391 FloatingClass::Single,
392 ),
393 storage => {
394 return Err(polyint_error(format!(
395 "polyint: coefficient input must be single or double, got {}",
396 storage.class_name()
397 )))
398 }
399 };
400 Ok(Polynomial {
401 coeffs: coeffs.into_iter().map(|v| Complex64::new(v, 0.0)).collect(),
402 class,
403 })
404}
405
406fn parse_complex_tensor_coeffs(tensor: &ComplexTensor) -> BuiltinResult<Polynomial> {
407 ensure_vector_shape(&tensor.shape)?;
408 Ok(Polynomial {
409 coeffs: tensor
410 .materialize_f64()
411 .iter()
412 .map(|&(re, im)| Complex64::new(re, im))
413 .collect(),
414 class: FloatingClass::Double,
415 })
416}
417
418async fn parse_constant(value: Value) -> BuiltinResult<Complex64> {
419 let gathered = dispatcher::gather_if_needed_async(&value).await?;
420 match gathered {
421 Value::Tensor(tensor) => {
422 if !tensor::is_scalar_tensor(&tensor) {
423 return Err(polyint_error(
424 "polyint: constant of integration must be a scalar",
425 ));
426 }
427 let value = match tensor
428 .into_numeric_storage()
429 .map_err(|error| polyint_error(format!("polyint: {error}")))?
430 {
431 NumericStorage::F64(values) => values[0],
432 NumericStorage::F32(values) => f64::from(values[0]),
433 storage => {
434 return Err(polyint_error(format!(
435 "polyint: constant input must be single or double, got {}",
436 storage.class_name()
437 )))
438 }
439 };
440 Ok(Complex64::new(value, 0.0))
441 }
442 Value::ComplexTensor(tensor) => {
443 if tensor.materialize_f64().len() != 1 {
444 return Err(polyint_error(
445 "polyint: constant of integration must be a scalar",
446 ));
447 }
448 let (re, im) = tensor.materialize_f64()[0];
449 Ok(Complex64::new(re, im))
450 }
451 Value::Num(n) => Ok(Complex64::new(n, 0.0)),
452 Value::Complex(re, im) => Ok(Complex64::new(re, im)),
453 other => Err(polyint_error(format!(
454 "polyint: constant of integration must be numeric, got {:?}",
455 other
456 ))),
457 }
458}
459
460fn ensure_vector_shape(shape: &[usize]) -> BuiltinResult<()> {
461 let non_unit = shape.iter().filter(|&&dim| dim > 1).count();
462 if non_unit <= 1 {
463 Ok(())
464 } else {
465 Err(polyint_error("polyint: coefficients must form a vector"))
466 }
467}
468
469#[derive(Clone)]
470struct Polynomial {
471 coeffs: Vec<Complex64>,
472 class: FloatingClass,
473}
474
475#[derive(Clone, Copy)]
476enum FloatingClass {
477 Double,
478 Single,
479}
480
481#[cfg(test)]
482pub(crate) mod tests {
483 use super::*;
484 use crate::builtins::common::gpu_helpers;
485 use crate::builtins::common::test_support;
486 use futures::executor::block_on;
487 #[cfg(feature = "wgpu")]
488 use runmat_accelerate_api::AccelProvider;
489 use runmat_value::{IntegerStorage, LogicalArray};
490
491 fn assert_error_contains(err: crate::RuntimeError, needle: &str) {
492 assert!(
493 err.message().contains(needle),
494 "expected error containing '{needle}', got '{}'",
495 err.message()
496 );
497 }
498
499 #[test]
500 fn polyint_descriptor_signatures_cover_core_forms() {
501 let labels: Vec<&str> = POLYINT_DESCRIPTOR
502 .signatures
503 .iter()
504 .map(|signature| signature.label)
505 .collect();
506 assert!(labels.contains(&"q = polyint(p)"));
507 assert!(labels.contains(&"q = polyint(p, k)"));
508 }
509
510 #[test]
511 fn polyint_descriptor_errors_have_stable_codes() {
512 let codes: Vec<&str> = POLYINT_DESCRIPTOR
513 .errors
514 .iter()
515 .map(|error| error.code)
516 .collect();
517 assert!(codes.contains(&"RM.POLYINT.INVALID_ARGUMENT"));
518 assert!(codes.contains(&"RM.POLYINT.INVALID_INPUT"));
519 assert!(codes.contains(&"RM.POLYINT.INTERNAL"));
520 }
521
522 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
523 #[test]
524 fn integrates_polynomial_without_constant() {
525 let tensor = Tensor::new(vec![3.0, -2.0, 5.0, 7.0], vec![1, 4]).unwrap();
526 let result = polyint_builtin(Value::Tensor(tensor), Vec::new()).expect("polyint");
527 match result {
528 Value::Tensor(t) => {
529 assert_eq!(t.shape, vec![1, 5]);
530 let expected = [0.75, -2.0 / 3.0, 2.5, 7.0, 0.0];
531 assert!(tensor::tensor_values_f64(&t)
532 .iter()
533 .zip(expected.iter())
534 .all(|(lhs, rhs)| (lhs - rhs).abs() < 1e-12));
535 }
536 other => panic!("expected tensor result, got {other:?}"),
537 }
538 }
539
540 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
541 #[test]
542 fn integrates_with_constant() {
543 let tensor = Tensor::new(vec![4.0, 0.0, -8.0], vec![1, 3]).unwrap();
544 let args = vec![Value::Num(3.0)];
545 let result = polyint_builtin(Value::Tensor(tensor), args).expect("polyint");
546 match result {
547 Value::Tensor(t) => {
548 assert_eq!(t.shape, vec![1, 4]);
549 let expected = [4.0 / 3.0, 0.0, -8.0, 3.0];
550 assert!(tensor::tensor_values_f64(&t)
551 .iter()
552 .zip(expected.iter())
553 .all(|(lhs, rhs)| (lhs - rhs).abs() < 1e-12));
554 }
555 other => panic!("expected tensor result, got {other:?}"),
556 }
557 }
558
559 #[test]
560 fn polyint_rejects_every_integer_class_for_coefficients_and_constant() {
561 let cases = [
562 IntegerStorage::I8(vec![1]),
563 IntegerStorage::I16(vec![1]),
564 IntegerStorage::I32(vec![1]),
565 IntegerStorage::I64(vec![1]),
566 IntegerStorage::U8(vec![1]),
567 IntegerStorage::U16(vec![1]),
568 IntegerStorage::U32(vec![1]),
569 IntegerStorage::U64(vec![1]),
570 ];
571 for storage in cases {
572 let integer = Tensor::new_integer(storage, vec![1, 1]).unwrap();
573 let err = polyint_builtin(Value::Tensor(integer.clone()), Vec::new())
574 .expect_err("integer coefficients must be rejected");
575 assert_error_contains(err, "must be single or double");
576
577 let coefficients = Tensor::new(vec![4.0, 0.0, -8.0], vec![1, 3]).unwrap();
578 let err = polyint_builtin(Value::Tensor(coefficients), vec![Value::Tensor(integer)])
579 .expect_err("integer constant must be rejected");
580 assert_error_contains(err, "must be single or double");
581 }
582 }
583
584 #[test]
585 fn polyint_preserves_native_single_output_storage() {
586 let tensor = Tensor::from_f32(vec![3.0, -2.0, 5.0], vec![3, 1]).unwrap();
587 let constant = Tensor::from_f32(vec![2.0], vec![1, 1]).unwrap();
588 let result =
589 polyint_builtin(Value::Tensor(tensor), vec![Value::Tensor(constant)]).expect("polyint");
590 let Value::Tensor(tensor) = result else {
591 panic!("expected native-single tensor");
592 };
593 assert_eq!(tensor.shape, vec![1, 4]);
594 assert_eq!(tensor.numeric_dtype(), NumericDType::F32);
595 assert_eq!(
596 tensor.into_numeric_storage().unwrap(),
597 NumericStorage::F32(vec![1.0, -1.0, 5.0, 2.0])
598 );
599 }
600
601 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
602 #[test]
603 fn integrates_scalar_value() {
604 let result = polyint_builtin(Value::Num(5.0), Vec::new()).expect("polyint");
605 match result {
606 Value::Tensor(t) => {
607 assert_eq!(t.shape, vec![1, 2]);
608 let values = tensor::tensor_values_f64(&t);
609 assert!((values[0] - 5.0).abs() < 1e-12);
610 assert!(values[1].abs() < 1e-12);
611 }
612 other => panic!("expected tensor result, got {other:?}"),
613 }
614 }
615
616 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
617 #[test]
618 fn rejects_logical_coefficients_and_constant() {
619 let logical = LogicalArray::new(vec![1, 0, 1], vec![1, 3]).unwrap();
620 let err = polyint_builtin(Value::LogicalArray(logical), Vec::new())
621 .expect_err("logical coefficients must be rejected");
622 assert_error_contains(err, "must be single or double");
623
624 let coefficients = Tensor::new(vec![1.0, 0.0], vec![1, 2]).unwrap();
625 let constant = LogicalArray::new(vec![1], vec![1, 1]).unwrap();
626 let err = polyint_builtin(
627 Value::Tensor(coefficients),
628 vec![Value::LogicalArray(constant)],
629 )
630 .expect_err("logical constant must be rejected");
631 assert_error_contains(err, "must be single or double");
632 }
633
634 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
635 #[test]
636 fn returns_row_vector_for_column_input() {
637 let tensor = Tensor::new(vec![2.0, 0.0, -6.0], vec![3, 1]).unwrap();
638 let result = polyint_builtin(Value::Tensor(tensor), Vec::new()).expect("polyint");
639 match result {
640 Value::Tensor(t) => {
641 assert_eq!(t.shape, vec![1, 4]);
642 let expected = [2.0 / 3.0, 0.0, -6.0, 0.0];
643 assert!(tensor::tensor_values_f64(&t)
644 .iter()
645 .zip(expected.iter())
646 .all(|(lhs, rhs)| (lhs - rhs).abs() < 1e-12));
647 }
648 other => panic!("expected row tensor, got {other:?}"),
649 }
650 }
651
652 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
653 #[test]
654 fn integrates_complex_coefficients() {
655 let tensor =
656 ComplexTensor::new(vec![(1.0, 2.0), (-3.0, 0.0), (0.0, 4.0)], vec![1, 3]).unwrap();
657 let args = vec![Value::Complex(0.0, -1.0)];
658 let result = polyint_builtin(Value::ComplexTensor(tensor), args).expect("polyint");
659 match result {
660 Value::ComplexTensor(t) => {
661 assert_eq!(t.shape, vec![1, 4]);
662 let expected = [(1.0 / 3.0, 2.0 / 3.0), (-1.5, 0.0), (0.0, 4.0), (0.0, -1.0)];
663 assert!(t.materialize_f64().iter().zip(expected.iter()).all(
664 |((lre, lim), (rre, rim))| {
665 (lre - rre).abs() < 1e-12 && (lim - rim).abs() < 1e-12
666 }
667 ));
668 }
669 other => panic!("expected complex tensor, got {other:?}"),
670 }
671 }
672
673 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
674 #[test]
675 fn rejects_matrix_coefficients() {
676 let tensor = Tensor::new(vec![1.0, 2.0, 3.0, 4.0], vec![2, 2]).unwrap();
677 let err = polyint_builtin(Value::Tensor(tensor), Vec::new()).expect_err("expected error");
678 assert_error_contains(err, "vector");
679 }
680
681 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
682 #[test]
683 fn rejects_non_scalar_constant() {
684 let coeffs = Tensor::new(vec![1.0, -4.0, 6.0], vec![1, 3]).unwrap();
685 let constant = Tensor::new(vec![1.0, 2.0], vec![1, 2]).unwrap();
686 let err = polyint_builtin(Value::Tensor(coeffs), vec![Value::Tensor(constant)])
687 .expect_err("expected error");
688 assert_error_contains(err, "constant of integration must be a scalar");
689 }
690
691 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
692 #[test]
693 fn rejects_excess_arguments() {
694 let tensor = Tensor::new(vec![1.0, 0.0], vec![1, 2]).unwrap();
695 let err = polyint_builtin(
696 Value::Tensor(tensor),
697 vec![Value::Num(1.0), Value::Num(2.0)],
698 )
699 .expect_err("expected error");
700 assert_eq!(err.identifier(), POLYINT_ERROR_INVALID_ARGUMENT.identifier);
701 assert_error_contains(err, "too many input arguments");
702 }
703
704 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
705 #[test]
706 fn handles_empty_input_as_zero_polynomial() {
707 let tensor = Tensor::new(vec![], vec![1, 0]).unwrap();
708 let result = polyint_builtin(Value::Tensor(tensor), Vec::new()).expect("polyint");
709 match result {
710 Value::Num(v) => assert!(v.abs() < 1e-12),
711 Value::Tensor(t) => {
712 assert_eq!(t.len(), 1);
714 assert!(tensor::tensor_value_f64(&t, 0).abs() < 1e-12);
715 }
716 other => panic!("expected numeric result, got {other:?}"),
717 }
718 }
719
720 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
721 #[test]
722 fn empty_input_with_constant() {
723 let tensor = Tensor::new(vec![], vec![1, 0]).unwrap();
724 let result = polyint_builtin(Value::Tensor(tensor), vec![Value::Complex(1.5, -2.0)])
725 .expect("polyint");
726 match result {
727 Value::ComplexTensor(t) => {
728 assert_eq!(t.shape, vec![1, 1]);
729 assert_eq!(t.materialize_f64().len(), 1);
730 let (re, im) = t.materialize_f64()[0];
731 assert!((re - 1.5).abs() < 1e-12);
732 assert!((im + 2.0).abs() < 1e-12);
733 }
734 other => panic!("expected complex tensor result, got {other:?}"),
735 }
736 }
737
738 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
739 #[test]
740 fn polyint_gpu_roundtrip() {
741 test_support::with_test_provider(|provider| {
742 let tensor = Tensor::new(vec![1.0, -4.0, 6.0], vec![1, 3]).unwrap();
743 let handle = gpu_helpers::upload_tensor(provider, &tensor).expect("upload");
744 let result = polyint_builtin(Value::GpuTensor(handle), Vec::new()).expect("polyint");
745 match result {
746 Value::GpuTensor(handle) => {
747 let gathered = test_support::gather(Value::GpuTensor(handle)).expect("gather");
748 assert_eq!(gathered.shape, vec![1, 4]);
749 let expected = [1.0 / 3.0, -2.0, 6.0, 0.0];
750 assert!(tensor::tensor_values_f64(&gathered)
751 .iter()
752 .zip(expected.iter())
753 .all(|(lhs, rhs)| (lhs - rhs).abs() < 1e-12));
754 }
755 other => panic!("expected GPU tensor result, got {other:?}"),
756 }
757 });
758 }
759
760 #[test]
761 fn polyint_gpu_rejects_every_integer_class_before_dispatch() {
762 test_support::with_test_provider(|provider| {
763 let cases = [
764 IntegerStorage::I8(vec![1]),
765 IntegerStorage::I16(vec![1]),
766 IntegerStorage::I32(vec![1]),
767 IntegerStorage::I64(vec![1]),
768 IntegerStorage::U8(vec![1]),
769 IntegerStorage::U16(vec![1]),
770 IntegerStorage::U32(vec![1]),
771 IntegerStorage::U64(vec![1]),
772 ];
773 for storage in cases {
774 let tensor = Tensor::new_integer(storage, vec![1, 1]).unwrap();
775 let handle = gpu_helpers::upload_tensor(provider, &tensor).expect("integer upload");
776 let err = polyint_builtin(Value::GpuTensor(handle), Vec::new())
777 .expect_err("integer gpuArray coefficients must be rejected");
778 assert_error_contains(err, "must be single or double");
779 }
780 });
781 }
782
783 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
784 #[test]
785 fn polyint_gpu_complex_constant_reuploads_complex_result() {
786 test_support::with_test_provider(|provider| {
787 let tensor = Tensor::new(vec![1.0, 0.0], vec![1, 2]).unwrap();
788 let handle = gpu_helpers::upload_tensor(provider, &tensor).expect("upload");
789 let result = polyint_builtin(Value::GpuTensor(handle), vec![Value::Complex(0.0, 2.0)])
790 .expect("polyint");
791 match result {
792 Value::GpuTensor(handle) => {
793 assert_eq!(
794 runmat_accelerate_api::handle_storage(&handle),
795 runmat_accelerate_api::GpuTensorStorage::ComplexInterleaved
796 );
797 let gathered =
798 block_on(gpu_helpers::gather_value_async(&Value::GpuTensor(handle)))
799 .expect("gather");
800 let Value::ComplexTensor(ct) = gathered else {
801 panic!("expected complex tensor");
802 };
803 assert_eq!(ct.shape, vec![1, 3]);
804 let expected = [(0.5, 0.0), (0.0, 0.0), (0.0, 2.0)];
805 assert!(ct.materialize_f64().iter().zip(expected.iter()).all(
806 |((lre, lim), (rre, rim))| {
807 (lre - rre).abs() < 1e-12 && (lim - rim).abs() < 1e-12
808 }
809 ));
810 }
811 other => panic!("expected complex gpu tensor, got {other:?}"),
812 }
813 });
814 }
815
816 #[test]
817 fn polyint_complex_gpu_coefficients_stay_resident() {
818 test_support::with_test_provider(|provider| {
819 let coeffs = ComplexTensor::new(vec![(1.0, 1.0), (2.0, -1.0)], vec![1, 2]).unwrap();
820 let handle = gpu_helpers::upload_complex_tensor(provider, &coeffs).expect("upload");
821 let result =
822 polyint_builtin(Value::GpuTensor(handle), vec![Value::Num(2.0)]).expect("polyint");
823 let Value::GpuTensor(handle) = result else {
824 panic!("expected complex gpu tensor");
825 };
826 assert_eq!(
827 runmat_accelerate_api::handle_storage(&handle),
828 runmat_accelerate_api::GpuTensorStorage::ComplexInterleaved
829 );
830 let gathered = block_on(gpu_helpers::gather_value_async(&Value::GpuTensor(handle)))
831 .expect("gather");
832 let Value::ComplexTensor(ct) = gathered else {
833 panic!("expected complex tensor");
834 };
835 assert_eq!(ct.shape, vec![1, 3]);
836 let expected = [(0.5, 0.5), (2.0, -1.0), (2.0, 0.0)];
837 assert!(ct.materialize_f64().iter().zip(expected.iter()).all(
838 |((lre, lim), (rre, rim))| {
839 (lre - rre).abs() < 1e-12 && (lim - rim).abs() < 1e-12
840 }
841 ));
842 });
843 }
844
845 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
846 #[test]
847 fn polyint_gpu_with_gpu_constant() {
848 test_support::with_test_provider(|provider| {
849 let coeffs = Tensor::new(vec![2.0, 0.0], vec![1, 2]).unwrap();
850 let coeff_handle =
851 gpu_helpers::upload_tensor(provider, &coeffs).expect("upload coeffs");
852 let constant = Tensor::new(vec![3.0], vec![1, 1]).unwrap();
853 let constant_handle =
854 gpu_helpers::upload_tensor(provider, &constant).expect("upload constant");
855 let result = polyint_builtin(
856 Value::GpuTensor(coeff_handle),
857 vec![Value::GpuTensor(constant_handle)],
858 )
859 .expect("polyint");
860 match result {
861 Value::GpuTensor(handle) => {
862 let gathered =
863 test_support::gather(Value::GpuTensor(handle)).expect("gather result");
864 assert_eq!(gathered.shape, vec![1, 3]);
865 let expected = [1.0, 0.0, 3.0];
866 assert!(tensor::tensor_values_f64(&gathered)
867 .iter()
868 .zip(expected.iter())
869 .all(|(lhs, rhs)| (lhs - rhs).abs() < 1e-12));
870 }
871 other => panic!("expected gpu tensor result, got {other:?}"),
872 }
873 });
874 }
875
876 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
877 #[test]
878 #[cfg(feature = "wgpu")]
879 fn polyint_wgpu_matches_cpu() {
880 let _guard = test_support::accel_test_lock();
881 let Ok(provider) = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
882 runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
883 ) else {
884 return;
885 };
886 let tensor = Tensor::new(vec![3.0, -2.0, 5.0, 7.0], vec![1, 4]).unwrap();
887 let handle = gpu_helpers::upload_tensor(provider, &tensor).expect("upload");
888 let gpu_value = polyint_builtin(Value::GpuTensor(handle), Vec::new()).expect("polyint gpu");
889 let gathered = test_support::gather(gpu_value).expect("gather");
890 let cpu_value =
891 polyint_builtin(Value::Tensor(tensor.clone()), Vec::new()).expect("polyint cpu");
892 let expected = match cpu_value {
893 Value::Tensor(t) => t,
894 Value::Num(n) => Tensor::new(vec![n], vec![1, 1]).unwrap(),
895 other => panic!("unexpected cpu result {other:?}"),
896 };
897 assert_eq!(gathered.shape, expected.shape);
898 let tol = match provider.precision() {
899 runmat_accelerate_api::ProviderPrecision::F64 => 1e-12,
900 runmat_accelerate_api::ProviderPrecision::F32 => 1e-5,
901 };
902 tensor::tensor_values_f64(&gathered)
903 .iter()
904 .zip(tensor::tensor_values_f64(&expected).iter())
905 .for_each(|(lhs, rhs)| assert!((lhs - rhs).abs() < tol));
906 }
907
908 #[test]
909 #[cfg(feature = "wgpu")]
910 fn polyint_wgpu_complex_coefficients_match_cpu() {
911 let _guard = test_support::accel_test_lock();
912 let Ok(provider) = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
913 runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
914 ) else {
915 return;
916 };
917 let coeffs =
918 ComplexTensor::new(vec![(3.0, 1.5), (-2.0, 0.5), (5.0, -1.0)], vec![1, 3]).unwrap();
919 let cpu_value =
920 polyint_builtin(Value::ComplexTensor(coeffs.clone()), vec![Value::Num(2.0)])
921 .expect("polyint cpu");
922 let cpu = match cpu_value {
923 Value::ComplexTensor(t) => t,
924 other => panic!("unexpected cpu result {other:?}"),
925 };
926
927 let handle = gpu_helpers::upload_complex_tensor(provider, &coeffs).expect("upload");
928 let gpu_value =
929 polyint_builtin(Value::GpuTensor(handle), vec![Value::Num(2.0)]).expect("polyint gpu");
930 let Value::GpuTensor(handle) = gpu_value else {
931 panic!("expected gpu tensor");
932 };
933 assert_eq!(
934 runmat_accelerate_api::handle_storage(&handle),
935 runmat_accelerate_api::GpuTensorStorage::ComplexInterleaved
936 );
937 let gathered =
938 block_on(gpu_helpers::gather_value_async(&Value::GpuTensor(handle))).expect("gather");
939 let Value::ComplexTensor(gpu) = gathered else {
940 panic!("expected complex tensor");
941 };
942 assert_eq!(gpu.shape, cpu.shape);
943 let tol = match provider.precision() {
944 runmat_accelerate_api::ProviderPrecision::F64 => 1e-12,
945 runmat_accelerate_api::ProviderPrecision::F32 => 1e-5,
946 };
947 gpu.materialize_f64()
948 .iter()
949 .zip(cpu.materialize_f64().iter())
950 .for_each(|((lre, lim), (rre, rim))| {
951 assert!((lre - rre).abs() < tol);
952 assert!((lim - rim).abs() < tol);
953 });
954 }
955
956 fn polyint_builtin(coeffs: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
957 block_on(super::polyint_builtin(coeffs, rest))
958 }
959}