1use log::trace;
4use num_complex::Complex64;
5use runmat_builtins::{
6 BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
7 BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
8 ComplexTensor, Tensor, Value,
9};
10use runmat_macros::runtime_builtin;
11
12use crate::builtins::common::random_args::complex_tensor_into_value;
13use crate::builtins::common::spec::{
14 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
15 ProviderHook, ReductionNaN, ResidencyPolicy, ScalarType, ShapeRequirements,
16};
17use crate::builtins::common::{tensor, tensor::tensor_into_value};
18use crate::builtins::math::poly::type_resolvers::polyder_type;
19use crate::dispatcher;
20use crate::{build_runtime_error, BuiltinResult, RuntimeError};
21
22const EPS: f64 = 1.0e-12;
23const BUILTIN_NAME: &str = "polyder";
24
25const POLYDER_OUTPUT_D: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
26 name: "d",
27 ty: BuiltinParamType::Any,
28 arity: BuiltinParamArity::Required,
29 default: None,
30 description: "Derivative coefficient vector.",
31}];
32
33const POLYDER_OUTPUT_NUM_DEN: [BuiltinParamDescriptor; 2] = [
34 BuiltinParamDescriptor {
35 name: "num",
36 ty: BuiltinParamType::Any,
37 arity: BuiltinParamArity::Required,
38 default: None,
39 description: "Quotient-rule numerator coefficients.",
40 },
41 BuiltinParamDescriptor {
42 name: "den",
43 ty: BuiltinParamType::Any,
44 arity: BuiltinParamArity::Required,
45 default: None,
46 description: "Quotient-rule denominator coefficients.",
47 },
48];
49
50const POLYDER_INPUTS_SINGLE: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
51 name: "p",
52 ty: BuiltinParamType::Any,
53 arity: BuiltinParamArity::Required,
54 default: None,
55 description: "Polynomial coefficient vector.",
56}];
57
58const POLYDER_INPUTS_BINARY: [BuiltinParamDescriptor; 2] = [
59 BuiltinParamDescriptor {
60 name: "a",
61 ty: BuiltinParamType::Any,
62 arity: BuiltinParamArity::Required,
63 default: None,
64 description: "First polynomial coefficient vector.",
65 },
66 BuiltinParamDescriptor {
67 name: "b",
68 ty: BuiltinParamType::Any,
69 arity: BuiltinParamArity::Required,
70 default: None,
71 description: "Second polynomial coefficient vector.",
72 },
73];
74
75const POLYDER_SIGNATURES: [BuiltinSignatureDescriptor; 3] = [
76 BuiltinSignatureDescriptor {
77 label: "d = polyder(p)",
78 inputs: &POLYDER_INPUTS_SINGLE,
79 outputs: &POLYDER_OUTPUT_D,
80 },
81 BuiltinSignatureDescriptor {
82 label: "d = polyder(a, b)",
83 inputs: &POLYDER_INPUTS_BINARY,
84 outputs: &POLYDER_OUTPUT_D,
85 },
86 BuiltinSignatureDescriptor {
87 label: "[num, den] = polyder(u, v)",
88 inputs: &POLYDER_INPUTS_BINARY,
89 outputs: &POLYDER_OUTPUT_NUM_DEN,
90 },
91];
92
93const POLYDER_ERROR_INVALID_ARGUMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
94 code: "RM.POLYDER.INVALID_ARGUMENT",
95 identifier: Some("RunMat:polyder:InvalidArgument"),
96 when: "Input arity/output mode combination is invalid.",
97 message: "polyder: invalid argument",
98};
99
100const POLYDER_ERROR_INVALID_INPUT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
101 code: "RM.POLYDER.INVALID_INPUT",
102 identifier: Some("RunMat:polyder:InvalidInput"),
103 when: "Inputs cannot be interpreted as numeric coefficient vectors.",
104 message: "polyder: invalid input",
105};
106
107const POLYDER_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
108 code: "RM.POLYDER.INTERNAL",
109 identifier: Some("RunMat:polyder:Internal"),
110 when: "Runtime fails while building derivative outputs or provider fallback paths.",
111 message: "polyder: internal runtime failure",
112};
113
114const POLYDER_ERRORS: [BuiltinErrorDescriptor; 3] = [
115 POLYDER_ERROR_INVALID_ARGUMENT,
116 POLYDER_ERROR_INVALID_INPUT,
117 POLYDER_ERROR_INTERNAL,
118];
119
120pub const POLYDER_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
121 signatures: &POLYDER_SIGNATURES,
122 output_mode: BuiltinOutputMode::ByRequestedOutputCount,
123 completion_policy: BuiltinCompletionPolicy::Public,
124 errors: &POLYDER_ERRORS,
125};
126
127#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::math::poly::polyder")]
128pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
129 name: "polyder",
130 op_kind: GpuOpKind::Custom("polynomial-derivative"),
131 supported_precisions: &[ScalarType::F32, ScalarType::F64],
132 broadcast: BroadcastSemantics::None,
133 provider_hooks: &[
134 ProviderHook::Custom("polyder-single"),
135 ProviderHook::Custom("polyder-product"),
136 ProviderHook::Custom("polyder-quotient"),
137 ],
138 constant_strategy: ConstantStrategy::InlineLiteral,
139 residency: ResidencyPolicy::NewHandle,
140 nan_mode: ReductionNaN::Include,
141 two_pass_threshold: None,
142 workgroup_size: None,
143 accepts_nan_mode: false,
144 notes: "Runs on-device when providers expose polyder hooks; falls back to the host for complex coefficients or unsupported shapes.",
145};
146
147fn polyder_error(message: impl Into<String>) -> RuntimeError {
148 polyder_error_with(message, &POLYDER_ERROR_INVALID_INPUT)
149}
150
151fn polyder_argument_error(message: impl Into<String>) -> RuntimeError {
152 polyder_error_with(message, &POLYDER_ERROR_INVALID_ARGUMENT)
153}
154
155fn polyder_error_with(
156 message: impl Into<String>,
157 error: &'static BuiltinErrorDescriptor,
158) -> RuntimeError {
159 let mut builder = build_runtime_error(message).with_builtin(BUILTIN_NAME);
160 if let Some(identifier) = error.identifier {
161 builder = builder.with_identifier(identifier);
162 }
163 builder.build()
164}
165
166#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::math::poly::polyder")]
167pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
168 name: "polyder",
169 shape: ShapeRequirements::Any,
170 constant_strategy: ConstantStrategy::InlineLiteral,
171 elementwise: None,
172 reduction: None,
173 emits_nan: false,
174 notes: "Symbolic operation on coefficient vectors; fusion bypasses this builtin.",
175};
176
177#[runtime_builtin(
178 name = "polyder",
179 category = "math/poly",
180 summary = "Differentiate polynomials, products, and ratios.",
181 keywords = "polyder,polynomial,derivative,product,quotient",
182 type_resolver(polyder_type),
183 descriptor(crate::builtins::math::poly::polyder::POLYDER_DESCRIPTOR),
184 builtin_path = "crate::builtins::math::poly::polyder"
185)]
186async fn polyder_builtin(first: Value, rest: Vec<Value>) -> crate::BuiltinResult<Value> {
187 if let Some(out_count) = crate::output_count::current_output_count() {
188 if out_count <= 1 {
189 let result = match rest.len() {
190 0 => derivative_single(first).await,
191 1 => derivative_product(first, rest.into_iter().next().unwrap()).await,
192 _ => Err(polyder_argument_error("polyder: too many input arguments")),
193 }?;
194 if out_count == 0 {
195 return Ok(Value::OutputList(Vec::new()));
196 }
197 return Ok(Value::OutputList(vec![result]));
198 }
199 if rest.len() != 1 {
200 return Err(polyder_argument_error(
201 "Not enough input arguments for quotient form.",
202 ));
203 }
204 let eval = evaluate_quotient(first, rest.into_iter().next().unwrap()).await?;
205 let outputs = vec![eval.numerator(), eval.denominator()];
206 return Ok(crate::output_count::output_list_with_padding(
207 out_count, outputs,
208 ));
209 }
210 match rest.len() {
211 0 => derivative_single(first).await,
212 1 => derivative_product(first, rest.into_iter().next().unwrap()).await,
213 _ => Err(polyder_argument_error("polyder: too many input arguments")),
214 }
215}
216
217async fn try_gpu_derivative_single(value: &Value) -> BuiltinResult<Option<Value>> {
218 let Value::GpuTensor(handle) = value else {
219 return Ok(None);
220 };
221 let Some(provider) = runmat_accelerate_api::provider() else {
222 return Ok(None);
223 };
224 match provider.polyder_single(handle).await {
225 Ok(out) => Ok(Some(Value::GpuTensor(out))),
226 Err(err) => {
227 trace!("polyder: provider polyder_single fallback: {err}");
228 Ok(None)
229 }
230 }
231}
232
233async fn try_gpu_derivative_product(first: &Value, second: &Value) -> BuiltinResult<Option<Value>> {
234 match (first, second) {
235 (Value::GpuTensor(p), Value::GpuTensor(q)) => {
236 let Some(provider) = runmat_accelerate_api::provider() else {
237 return Ok(None);
238 };
239 match provider.polyder_product(p, q).await {
240 Ok(out) => Ok(Some(Value::GpuTensor(out))),
241 Err(err) => {
242 trace!("polyder: provider polyder_product fallback: {err}");
243 Ok(None)
244 }
245 }
246 }
247 _ => Ok(None),
248 }
249}
250
251async fn try_gpu_quotient(u: &Value, v: &Value) -> BuiltinResult<Option<PolyderEval>> {
252 match (u, v) {
253 (Value::GpuTensor(uh), Value::GpuTensor(vh)) => {
254 let Some(provider) = runmat_accelerate_api::provider() else {
255 return Ok(None);
256 };
257 match provider.polyder_quotient(uh, vh).await {
258 Ok(result) => Ok(Some(PolyderEval {
259 numerator: Value::GpuTensor(result.numerator),
260 denominator: Value::GpuTensor(result.denominator),
261 })),
262 Err(err) => {
263 trace!("polyder: provider polyder_quotient fallback: {err}");
264 Ok(None)
265 }
266 }
267 }
268 _ => Ok(None),
269 }
270}
271
272pub async fn evaluate_quotient(u: Value, v: Value) -> BuiltinResult<PolyderEval> {
274 if let Some(eval) = try_gpu_quotient(&u, &v).await? {
275 return Ok(eval);
276 }
277 let u_poly = parse_polynomial("polyder", "U", u).await?;
278 let v_poly = parse_polynomial("polyder", "V", v).await?;
279 let numerator = quotient_numerator(&u_poly, &v_poly)?;
280 let denominator = quotient_denominator(&v_poly)?;
281 Ok(PolyderEval {
282 numerator,
283 denominator,
284 })
285}
286
287#[derive(Clone)]
289pub struct PolyderEval {
290 numerator: Value,
291 denominator: Value,
292}
293
294impl PolyderEval {
295 pub fn numerator(&self) -> Value {
297 self.numerator.clone()
298 }
299
300 pub fn denominator(&self) -> Value {
302 self.denominator.clone()
303 }
304}
305
306pub async fn derivative_single(value: Value) -> BuiltinResult<Value> {
307 if let Some(out) = try_gpu_derivative_single(&value).await? {
308 return Ok(out);
309 }
310 let poly = parse_polynomial("polyder", "P", value).await?;
311 differentiate_polynomial(&poly)
312}
313
314pub async fn derivative_product(first: Value, second: Value) -> BuiltinResult<Value> {
315 if let Some(out) = try_gpu_derivative_product(&first, &second).await? {
316 return Ok(out);
317 }
318 let p = parse_polynomial("polyder", "P", first).await?;
319 let q = parse_polynomial("polyder", "A", second).await?;
320 product_derivative(&p, &q)
321}
322
323fn quotient_numerator(u: &Polynomial, v: &Polynomial) -> BuiltinResult<Value> {
324 let du = raw_derivative(&u.coeffs);
325 let dv = raw_derivative(&v.coeffs);
326 let term1 = poly_convolve(&du, &v.coeffs);
327 let term2 = poly_convolve(&u.coeffs, &dv);
328 let mut numerator = poly_sub(&term1, &term2);
329 numerator = trim_leading_zeros(&numerator);
330 coeffs_to_value(&numerator, u.orientation)
331}
332
333fn quotient_denominator(v: &Polynomial) -> BuiltinResult<Value> {
334 let mut denominator = poly_convolve(&v.coeffs, &v.coeffs);
335 denominator = trim_leading_zeros(&denominator);
336 coeffs_to_value(&denominator, v.orientation)
337}
338
339fn differentiate_polynomial(poly: &Polynomial) -> BuiltinResult<Value> {
340 let mut coeffs = raw_derivative(&poly.coeffs);
341 coeffs = trim_leading_zeros(&coeffs);
342 coeffs_to_value(&coeffs, poly.orientation)
343}
344
345fn product_derivative(p: &Polynomial, q: &Polynomial) -> BuiltinResult<Value> {
346 let dp = raw_derivative(&p.coeffs);
347 let dq = raw_derivative(&q.coeffs);
348 let term1 = poly_convolve(&dp, &q.coeffs);
349 let term2 = poly_convolve(&p.coeffs, &dq);
350 let mut result = poly_add(&term1, &term2);
351 result = trim_leading_zeros(&result);
352 coeffs_to_value(&result, p.orientation)
353}
354
355fn raw_derivative(coeffs: &[Complex64]) -> Vec<Complex64> {
356 if coeffs.len() <= 1 {
357 return vec![Complex64::new(0.0, 0.0)];
358 }
359 let mut output = Vec::with_capacity(coeffs.len() - 1);
360 let mut power = coeffs.len() - 1;
361 for coeff in coeffs.iter().take(coeffs.len() - 1) {
362 output.push(*coeff * (power as f64));
363 power -= 1;
364 }
365 output
366}
367
368fn poly_convolve(a: &[Complex64], b: &[Complex64]) -> Vec<Complex64> {
369 if a.is_empty() || b.is_empty() {
370 return Vec::new();
371 }
372 let mut result = vec![Complex64::new(0.0, 0.0); a.len() + b.len() - 1];
373 for (i, &ai) in a.iter().enumerate() {
374 for (j, &bj) in b.iter().enumerate() {
375 result[i + j] += ai * bj;
376 }
377 }
378 result
379}
380
381fn poly_add(a: &[Complex64], b: &[Complex64]) -> Vec<Complex64> {
382 let len = a.len().max(b.len());
383 let mut result = vec![Complex64::new(0.0, 0.0); len];
384 for (idx, &value) in a.iter().enumerate() {
385 result[len - a.len() + idx] += value;
386 }
387 for (idx, &value) in b.iter().enumerate() {
388 result[len - b.len() + idx] += value;
389 }
390 result
391}
392
393fn poly_sub(a: &[Complex64], b: &[Complex64]) -> Vec<Complex64> {
394 let len = a.len().max(b.len());
395 let mut result = vec![Complex64::new(0.0, 0.0); len];
396 for (idx, &value) in a.iter().enumerate() {
397 result[len - a.len() + idx] += value;
398 }
399 for (idx, &value) in b.iter().enumerate() {
400 result[len - b.len() + idx] -= value;
401 }
402 result
403}
404
405fn trim_leading_zeros(coeffs: &[Complex64]) -> Vec<Complex64> {
406 let mut first = None;
407 for (idx, coeff) in coeffs.iter().enumerate() {
408 if coeff.norm() > EPS {
409 first = Some(idx);
410 break;
411 }
412 }
413 match first {
414 Some(idx) => coeffs[idx..].to_vec(),
415 None => vec![Complex64::new(0.0, 0.0)],
416 }
417}
418
419fn coeffs_to_value(coeffs: &[Complex64], orientation: Orientation) -> BuiltinResult<Value> {
420 if coeffs.iter().all(|c| c.im.abs() <= EPS) {
421 let data: Vec<f64> = coeffs.iter().map(|c| c.re).collect();
422 let shape = orientation.shape_for_len(data.len());
423 let tensor =
424 Tensor::new(data, shape).map_err(|e| polyder_error(format!("polyder: {e}")))?;
425 Ok(tensor_into_value(tensor))
426 } else {
427 let data: Vec<(f64, f64)> = coeffs.iter().map(|c| (c.re, c.im)).collect();
428 let shape = orientation.shape_for_len(data.len());
429 let tensor =
430 ComplexTensor::new(data, shape).map_err(|e| polyder_error(format!("polyder: {e}")))?;
431 Ok(complex_tensor_into_value(tensor))
432 }
433}
434
435async fn parse_polynomial(context: &str, label: &str, value: Value) -> BuiltinResult<Polynomial> {
436 let gathered = dispatcher::gather_if_needed_async(&value).await?;
437 let (coeffs, orientation) = match gathered {
438 Value::Tensor(tensor) => {
439 ensure_vector_shape(context, label, &tensor.shape)?;
440 let orientation = orientation_from_shape(&tensor.shape);
441 if tensor.data.is_empty() {
442 (vec![Complex64::new(0.0, 0.0)], orientation)
443 } else {
444 (
445 tensor
446 .data
447 .into_iter()
448 .map(|re| Complex64::new(re, 0.0))
449 .collect(),
450 orientation,
451 )
452 }
453 }
454 Value::ComplexTensor(tensor) => {
455 ensure_vector_shape(context, label, &tensor.shape)?;
456 let orientation = orientation_from_shape(&tensor.shape);
457 if tensor.data.is_empty() {
458 (vec![Complex64::new(0.0, 0.0)], orientation)
459 } else {
460 (
461 tensor
462 .data
463 .into_iter()
464 .map(|(re, im)| Complex64::new(re, im))
465 .collect(),
466 orientation,
467 )
468 }
469 }
470 Value::LogicalArray(logical) => {
471 let tensor = tensor::logical_to_tensor(&logical).map_err(polyder_error)?;
472 ensure_vector_shape(context, label, &tensor.shape)?;
473 let orientation = orientation_from_shape(&tensor.shape);
474 if tensor.data.is_empty() {
475 (vec![Complex64::new(0.0, 0.0)], orientation)
476 } else {
477 (
478 tensor
479 .data
480 .into_iter()
481 .map(|re| Complex64::new(re, 0.0))
482 .collect(),
483 orientation,
484 )
485 }
486 }
487 Value::Num(n) => (vec![Complex64::new(n, 0.0)], Orientation::Scalar),
488 Value::Int(i) => (vec![Complex64::new(i.to_f64(), 0.0)], Orientation::Scalar),
489 Value::Bool(b) => (
490 vec![Complex64::new(if b { 1.0 } else { 0.0 }, 0.0)],
491 Orientation::Scalar,
492 ),
493 Value::Complex(re, im) => (vec![Complex64::new(re, im)], Orientation::Scalar),
494 other => {
495 return Err(polyder_error(format!(
496 "{context}: expected {label} to be a numeric vector, got {other:?}"
497 )));
498 }
499 };
500
501 Ok(Polynomial {
502 coeffs,
503 orientation,
504 })
505}
506
507fn ensure_vector_shape(context: &str, label: &str, shape: &[usize]) -> BuiltinResult<()> {
508 let non_unit = shape.iter().copied().filter(|&dim| dim > 1).count();
509 if non_unit <= 1 {
510 Ok(())
511 } else {
512 Err(polyder_error(format!(
513 "{context}: {label} must be a vector of coefficients"
514 )))
515 }
516}
517
518#[derive(Clone, Copy)]
519enum Orientation {
520 Scalar,
521 Row,
522 Column,
523}
524
525impl Orientation {
526 fn shape_for_len(self, len: usize) -> Vec<usize> {
527 if len <= 1 {
528 return vec![1, 1];
529 }
530 match self {
531 Orientation::Scalar => vec![1, len],
532 Orientation::Row => vec![1, len],
533 Orientation::Column => vec![len, 1],
534 }
535 }
536}
537
538fn orientation_from_shape(shape: &[usize]) -> Orientation {
539 for (idx, &dim) in shape.iter().enumerate() {
540 if dim != 1 {
541 return match idx {
542 0 => Orientation::Column,
543 1 => Orientation::Row,
544 _ => Orientation::Column,
545 };
546 }
547 }
548 Orientation::Scalar
549}
550
551#[derive(Clone)]
552struct Polynomial {
553 coeffs: Vec<Complex64>,
554 orientation: Orientation,
555}
556
557#[cfg(test)]
558pub(crate) mod tests {
559 use super::*;
560 use crate::builtins::common::test_support;
561 use futures::executor::block_on;
562 use runmat_builtins::{IntValue, Tensor};
563
564 fn assert_error_contains(err: crate::RuntimeError, needle: &str) {
565 assert!(
566 err.message().contains(needle),
567 "expected error containing '{needle}', got '{}'",
568 err.message()
569 );
570 }
571
572 #[test]
573 fn polyder_descriptor_signatures_cover_core_forms() {
574 let labels: Vec<&str> = POLYDER_DESCRIPTOR
575 .signatures
576 .iter()
577 .map(|signature| signature.label)
578 .collect();
579 assert!(labels.contains(&"d = polyder(p)"));
580 assert!(labels.contains(&"d = polyder(a, b)"));
581 assert!(labels.contains(&"[num, den] = polyder(u, v)"));
582 }
583
584 #[test]
585 fn polyder_descriptor_errors_have_stable_codes() {
586 let codes: Vec<&str> = POLYDER_DESCRIPTOR
587 .errors
588 .iter()
589 .map(|error| error.code)
590 .collect();
591 assert!(codes.contains(&"RM.POLYDER.INVALID_ARGUMENT"));
592 assert!(codes.contains(&"RM.POLYDER.INVALID_INPUT"));
593 assert!(codes.contains(&"RM.POLYDER.INTERNAL"));
594 }
595
596 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
597 #[test]
598 fn derivative_of_cubic_polynomial_is_correct() {
599 let tensor = Tensor::new(vec![3.0, -2.0, 5.0, 7.0], vec![1, 4]).unwrap();
600 let result = derivative_single(Value::Tensor(tensor)).expect("polyder");
601 match result {
602 Value::Tensor(t) => {
603 assert_eq!(t.shape, vec![1, 3]);
604 assert!(t
605 .data
606 .iter()
607 .zip([9.0, -4.0, 5.0])
608 .all(|(lhs, rhs)| (lhs - rhs).abs() < 1e-12));
609 }
610 other => panic!("expected tensor result, got {other:?}"),
611 }
612 }
613
614 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
615 #[test]
616 fn derivative_of_product_matches_manual_rule() {
617 let p = Tensor::new(vec![1.0, 0.0, -2.0], vec![1, 3]).unwrap();
618 let a = Tensor::new(vec![1.0, 1.0], vec![1, 2]).unwrap();
619 let result =
620 derivative_product(Value::Tensor(p), Value::Tensor(a)).expect("polyder product");
621 match result {
622 Value::Tensor(t) => {
623 assert_eq!(t.shape, vec![1, 3]);
624 assert!(t
625 .data
626 .iter()
627 .zip([3.0, 2.0, -2.0])
628 .all(|(lhs, rhs)| (lhs - rhs).abs() < 1e-12));
629 }
630 other => panic!("expected tensor result, got {other:?}"),
631 }
632 }
633
634 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
635 #[test]
636 fn quotient_rule_produces_expected_num_and_den() {
637 let u = Tensor::new(vec![1.0, 0.0, -4.0], vec![1, 3]).unwrap();
638 let v = Tensor::new(vec![1.0, -1.0], vec![1, 2]).unwrap();
639 let eval = evaluate_quotient(Value::Tensor(u), Value::Tensor(v)).expect("polyder quotient");
640 match eval.numerator() {
641 Value::Tensor(t) => {
642 assert_eq!(t.shape, vec![1, 3]);
643 assert!(t
644 .data
645 .iter()
646 .zip([1.0, -2.0, 4.0])
647 .all(|(lhs, rhs)| (lhs - rhs).abs() < 1e-12));
648 }
649 other => panic!("expected tensor numerator, got {other:?}"),
650 }
651 match eval.denominator() {
652 Value::Tensor(t) => {
653 assert_eq!(t.shape, vec![1, 3]);
654 assert!(t
655 .data
656 .iter()
657 .zip([1.0, -2.0, 1.0])
658 .all(|(lhs, rhs)| (lhs - rhs).abs() < 1e-12));
659 }
660 other => panic!("expected tensor denominator, got {other:?}"),
661 }
662 }
663
664 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
665 #[test]
666 fn column_vector_orientation_is_preserved() {
667 let tensor = Tensor::new(vec![1.0, 0.0, -3.0], vec![3, 1]).unwrap();
668 let result = derivative_single(Value::Tensor(tensor)).expect("polyder column");
669 match result {
670 Value::Tensor(t) => {
671 assert_eq!(t.shape, vec![2, 1]);
672 assert!(t
673 .data
674 .iter()
675 .zip([2.0, 0.0])
676 .all(|(lhs, rhs)| (lhs - rhs).abs() < 1e-12));
677 }
678 other => panic!("expected column tensor, got {other:?}"),
679 }
680 }
681
682 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
683 #[test]
684 fn complex_coefficients_are_supported() {
685 let tensor =
686 ComplexTensor::new(vec![(1.0, 2.0), (-3.0, 0.0), (0.0, 4.0)], vec![1, 3]).unwrap();
687 let result = derivative_single(Value::ComplexTensor(tensor)).expect("polyder complex");
688 match result {
689 Value::ComplexTensor(t) => {
690 assert_eq!(t.shape, vec![1, 2]);
691 let expected = [Complex64::new(2.0, 4.0), Complex64::new(-3.0, 0.0)];
692 assert!(t
693 .data
694 .iter()
695 .zip(expected.iter())
696 .all(|((re, im), expected)| {
697 (re - expected.re).abs() < 1e-12 && (im - expected.im).abs() < 1e-12
698 }));
699 }
700 other => panic!("expected complex tensor, got {other:?}"),
701 }
702 }
703
704 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
705 #[test]
706 fn empty_polynomial_returns_zero() {
707 let tensor = Tensor::new(Vec::new(), vec![1, 0]).unwrap();
708 let result = derivative_single(Value::Tensor(tensor)).expect("polyder empty");
709 assert_eq!(result, Value::Num(0.0));
710 }
711
712 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
713 #[test]
714 fn rejects_matrix_input() {
715 let tensor = Tensor::new(vec![1.0, 2.0, 3.0, 4.0], vec![2, 2]).unwrap();
716 let err = derivative_single(Value::Tensor(tensor)).unwrap_err();
717 assert_error_contains(err, "vector of coefficients");
718 }
719
720 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
721 #[test]
722 fn rejects_string_input() {
723 let err = derivative_single(Value::String("abc".into())).unwrap_err();
724 assert_error_contains(err, "numeric vector");
725 }
726
727 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
728 #[test]
729 fn mixed_gpu_cpu_product_falls_back_to_host() {
730 test_support::with_test_provider(|provider| {
731 let p = Tensor::new(vec![1.0, 0.0, -2.0], vec![1, 3]).unwrap();
732 let q = Tensor::new(vec![1.0, 1.0], vec![1, 2]).unwrap();
733 let cpu_expected =
734 derivative_product(Value::Tensor(p.clone()), Value::Tensor(q.clone()))
735 .expect("cpu product");
736 let Value::Tensor(cpu_tensor) = cpu_expected else {
737 panic!("expected tensor result");
738 };
739
740 let view_p = runmat_accelerate_api::HostTensorView {
741 data: &p.data,
742 shape: &p.shape,
743 };
744 let handle_p = provider.upload(&view_p).expect("upload p");
745 let result = derivative_product(Value::GpuTensor(handle_p), Value::Tensor(q))
746 .expect("mixed product");
747 let Value::Tensor(host_tensor) = result else {
748 panic!("expected host tensor result");
749 };
750 assert_eq!(host_tensor.shape, cpu_tensor.shape);
751 assert!(host_tensor
752 .data
753 .iter()
754 .zip(cpu_tensor.data.iter())
755 .all(|(lhs, rhs)| (lhs - rhs).abs() < 1e-12));
756 });
757 }
758
759 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
760 #[test]
761 fn builtin_rejects_too_many_inputs() {
762 let err = futures::executor::block_on(super::polyder_builtin(
763 Value::Num(1.0),
764 vec![Value::Num(2.0), Value::Num(3.0)],
765 ))
766 .unwrap_err();
767 assert_eq!(err.identifier(), POLYDER_ERROR_INVALID_ARGUMENT.identifier);
768 assert_error_contains(err, "too many input arguments");
769 }
770
771 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
772 #[test]
773 fn gpu_inputs_remain_on_device() {
774 test_support::with_test_provider(|provider| {
775 let tensor = Tensor::new(vec![2.0, 0.0, -5.0, 4.0], vec![1, 4]).unwrap();
776 let view = runmat_accelerate_api::HostTensorView {
777 data: &tensor.data,
778 shape: &tensor.shape,
779 };
780 let handle = provider.upload(&view).expect("upload");
781 let result = derivative_single(Value::GpuTensor(handle)).expect("polyder gpu");
782 let Value::GpuTensor(out_handle) = result else {
783 panic!("expected GPU tensor result");
784 };
785 let gathered = test_support::gather(Value::GpuTensor(out_handle)).expect("gather");
786 assert_eq!(gathered.shape, vec![1, 3]);
787 assert!(gathered
788 .data
789 .iter()
790 .zip([6.0, 0.0, -5.0])
791 .all(|(lhs, rhs)| (lhs - rhs).abs() < 1e-12));
792 });
793 }
794
795 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
796 #[test]
797 fn gpu_product_matches_cpu() {
798 test_support::with_test_provider(|provider| {
799 let p = Tensor::new(vec![1.0, 0.0, -2.0], vec![1, 3]).unwrap();
800 let q = Tensor::new(vec![1.0, 1.0], vec![1, 2]).unwrap();
801 let expected = derivative_product(Value::Tensor(p.clone()), Value::Tensor(q.clone()))
802 .expect("cpu product");
803 let Value::Tensor(expected_tensor) = expected else {
804 panic!("expected tensor output");
805 };
806
807 let view_p = runmat_accelerate_api::HostTensorView {
808 data: &p.data,
809 shape: &p.shape,
810 };
811 let view_q = runmat_accelerate_api::HostTensorView {
812 data: &q.data,
813 shape: &q.shape,
814 };
815 let handle_p = provider.upload(&view_p).expect("upload p");
816 let handle_q = provider.upload(&view_q).expect("upload q");
817 let gpu_result =
818 derivative_product(Value::GpuTensor(handle_p), Value::GpuTensor(handle_q))
819 .expect("gpu product");
820 let Value::GpuTensor(gpu_handle) = gpu_result else {
821 panic!("expected GPU tensor");
822 };
823 let gathered = test_support::gather(Value::GpuTensor(gpu_handle)).expect("gather");
824 assert_eq!(gathered.shape, expected_tensor.shape);
825 assert!(gathered
826 .data
827 .iter()
828 .zip(expected_tensor.data.iter())
829 .all(|(lhs, rhs)| (lhs - rhs).abs() < 1e-12));
830 });
831 }
832
833 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
834 #[test]
835 fn gpu_quotient_matches_cpu() {
836 test_support::with_test_provider(|provider| {
837 let u = Tensor::new(vec![1.0, 0.0, -4.0], vec![1, 3]).unwrap();
838 let v = Tensor::new(vec![1.0, -1.0], vec![1, 2]).unwrap();
839 let expected = evaluate_quotient(Value::Tensor(u.clone()), Value::Tensor(v.clone()))
840 .expect("cpu quotient");
841 let Value::Tensor(expected_num) = expected.numerator() else {
842 panic!("expected tensor numerator");
843 };
844 let Value::Tensor(expected_den) = expected.denominator() else {
845 panic!("expected tensor denominator");
846 };
847
848 let view_u = runmat_accelerate_api::HostTensorView {
849 data: &u.data,
850 shape: &u.shape,
851 };
852 let view_v = runmat_accelerate_api::HostTensorView {
853 data: &v.data,
854 shape: &v.shape,
855 };
856 let handle_u = provider.upload(&view_u).expect("upload u");
857 let handle_v = provider.upload(&view_v).expect("upload v");
858 let gpu_eval =
859 evaluate_quotient(Value::GpuTensor(handle_u), Value::GpuTensor(handle_v))
860 .expect("gpu quotient");
861 let gpu_num = test_support::gather(gpu_eval.numerator()).expect("gather num");
862 let gpu_den = test_support::gather(gpu_eval.denominator()).expect("gather den");
863 assert_eq!(gpu_num.shape, expected_num.shape);
864 assert_eq!(gpu_den.shape, expected_den.shape);
865 assert!(gpu_num
866 .data
867 .iter()
868 .zip(expected_num.data.iter())
869 .all(|(lhs, rhs)| (lhs - rhs).abs() < 1e-12));
870 assert!(gpu_den
871 .data
872 .iter()
873 .zip(expected_den.data.iter())
874 .all(|(lhs, rhs)| (lhs - rhs).abs() < 1e-12));
875 });
876 }
877
878 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
879 #[test]
880 #[cfg(feature = "wgpu")]
881 fn wgpu_polyder_single_matches_cpu() {
882 let _ = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
883 runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
884 );
885 let provider = runmat_accelerate_api::provider().expect("wgpu provider");
886 let tensor = Tensor::new(vec![3.0, -2.0, 5.0, 7.0], vec![1, 4]).unwrap();
887 let expected = derivative_single(Value::Tensor(tensor.clone())).expect("cpu polyder");
888 let Value::Tensor(expected_tensor) = expected else {
889 panic!("expected tensor");
890 };
891 let view = runmat_accelerate_api::HostTensorView {
892 data: &tensor.data,
893 shape: &tensor.shape,
894 };
895 let handle = provider.upload(&view).expect("upload");
896 let gpu_result = derivative_single(Value::GpuTensor(handle)).expect("gpu polyder");
897 let gathered = test_support::gather(gpu_result).expect("gather");
898 assert_eq!(gathered.shape, expected_tensor.shape);
899 assert!(gathered
900 .data
901 .iter()
902 .zip(expected_tensor.data.iter())
903 .all(|(lhs, rhs)| (lhs - rhs).abs() < 1e-12));
904 }
905
906 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
907 #[test]
908 #[cfg(feature = "wgpu")]
909 fn wgpu_polyder_product_matches_cpu() {
910 let _ = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
911 runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
912 );
913 let provider = runmat_accelerate_api::provider().expect("wgpu provider");
914 let p = Tensor::new(vec![1.0, 0.0, -2.0], vec![1, 3]).unwrap();
915 let q = Tensor::new(vec![1.0, 1.0], vec![1, 2]).unwrap();
916 let expected = derivative_product(Value::Tensor(p.clone()), Value::Tensor(q.clone()))
917 .expect("cpu product");
918 let Value::Tensor(expected_tensor) = expected else {
919 panic!("expected tensor");
920 };
921 let view_p = runmat_accelerate_api::HostTensorView {
922 data: &p.data,
923 shape: &p.shape,
924 };
925 let view_q = runmat_accelerate_api::HostTensorView {
926 data: &q.data,
927 shape: &q.shape,
928 };
929 let handle_p = provider.upload(&view_p).expect("upload p");
930 let handle_q = provider.upload(&view_q).expect("upload q");
931 let gpu_result = derivative_product(Value::GpuTensor(handle_p), Value::GpuTensor(handle_q))
932 .expect("gpu product");
933 let gathered = test_support::gather(gpu_result).expect("gather");
934 assert_eq!(gathered.shape, expected_tensor.shape);
935 assert!(gathered
936 .data
937 .iter()
938 .zip(expected_tensor.data.iter())
939 .all(|(lhs, rhs)| (lhs - rhs).abs() < 1e-12));
940 }
941
942 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
943 #[test]
944 #[cfg(feature = "wgpu")]
945 fn wgpu_polyder_quotient_matches_cpu() {
946 let _ = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
947 runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
948 );
949 let provider = runmat_accelerate_api::provider().expect("wgpu provider");
950 let u = Tensor::new(vec![1.0, 0.0, -4.0], vec![1, 3]).unwrap();
951 let v = Tensor::new(vec![1.0, -1.0], vec![1, 2]).unwrap();
952 let expected = evaluate_quotient(Value::Tensor(u.clone()), Value::Tensor(v.clone()))
953 .expect("cpu quotient");
954 let expected_num = match expected.numerator() {
955 Value::Tensor(t) => t,
956 other => panic!("expected tensor numerator, got {other:?}"),
957 };
958 let expected_den = match expected.denominator() {
959 Value::Tensor(t) => t,
960 other => panic!("expected tensor denominator, got {other:?}"),
961 };
962 let view_u = runmat_accelerate_api::HostTensorView {
963 data: &u.data,
964 shape: &u.shape,
965 };
966 let view_v = runmat_accelerate_api::HostTensorView {
967 data: &v.data,
968 shape: &v.shape,
969 };
970 let handle_u = provider.upload(&view_u).expect("upload u");
971 let handle_v = provider.upload(&view_v).expect("upload v");
972 let gpu_eval = evaluate_quotient(Value::GpuTensor(handle_u), Value::GpuTensor(handle_v))
973 .expect("gpu quotient");
974 let gpu_num = test_support::gather(gpu_eval.numerator()).expect("gather num");
975 let gpu_den = test_support::gather(gpu_eval.denominator()).expect("gather den");
976 assert_eq!(gpu_num.shape, expected_num.shape);
977 assert_eq!(gpu_den.shape, expected_den.shape);
978 assert!(gpu_num
979 .data
980 .iter()
981 .zip(expected_num.data.iter())
982 .all(|(lhs, rhs)| (lhs - rhs).abs() < 1e-12));
983 assert!(gpu_den
984 .data
985 .iter()
986 .zip(expected_den.data.iter())
987 .all(|(lhs, rhs)| (lhs - rhs).abs() < 1e-12));
988 }
989
990 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
991 #[test]
992 fn derivative_promotes_integers() {
993 let value = Value::Int(IntValue::I32(5));
994 let result = derivative_single(value).expect("polyder int");
995 assert_eq!(result, Value::Num(0.0));
996 }
997
998 fn derivative_single(value: Value) -> BuiltinResult<Value> {
999 block_on(super::derivative_single(value))
1000 }
1001
1002 fn derivative_product(first: Value, second: Value) -> BuiltinResult<Value> {
1003 block_on(super::derivative_product(first, second))
1004 }
1005
1006 fn evaluate_quotient(u: Value, v: Value) -> BuiltinResult<PolyderEval> {
1007 block_on(super::evaluate_quotient(u, v))
1008 }
1009}