1use log::trace;
4use num_complex::Complex64;
5use runmat_builtins::{ComplexTensor, Tensor, Value};
6use runmat_macros::runtime_builtin;
7
8use crate::builtins::common::random_args::complex_tensor_into_value;
9use crate::builtins::common::spec::{
10 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
11 ProviderHook, ReductionNaN, ResidencyPolicy, ScalarType, ShapeRequirements,
12};
13use crate::builtins::common::{tensor, tensor::tensor_into_value};
14use crate::dispatcher;
15#[cfg(feature = "doc_export")]
16use crate::register_builtin_doc_text;
17use crate::{register_builtin_fusion_spec, register_builtin_gpu_spec};
18
19const EPS: f64 = 1.0e-12;
20
21#[cfg(feature = "doc_export")]
22pub const DOC_MD: &str = r#"---
23title: "polyder"
24category: "math/poly"
25keywords: ["polyder", "polynomial derivative", "product rule", "quotient rule", "gpu"]
26summary: "Differentiate polynomials, products, and ratios with MATLAB-compatible coefficient vectors."
27references:
28 - title: "MATLAB polyder documentation"
29 url: "https://www.mathworks.com/help/matlab/ref/polyder.html"
30gpu_support:
31 elementwise: false
32 reduction: false
33 precisions: ["f32", "f64"]
34 broadcasting: "none"
35 notes: "When the active provider implements the polyder hooks, differentiation runs entirely on the GPU; otherwise coefficients are gathered back to the host. Complex coefficients continue to fall back to the CPU."
36fusion:
37 elementwise: false
38 reduction: false
39 max_inputs: 2
40 constants: "inline"
41requires_feature: null
42tested:
43 unit: "builtins::math::poly::polyder::tests"
44 integration: "builtins::math::poly::polyder::tests::{gpu_inputs_remain_on_device,gpu_product_matches_cpu,gpu_quotient_matches_cpu,wgpu_polyder_single_matches_cpu,wgpu_polyder_product_matches_cpu,wgpu_polyder_quotient_matches_cpu}"
45 vm: "ignition::vm polyder multi-output dispatch"
46 doc: "builtins::math::poly::polyder::tests::doc_examples_present"
47---
48
49# What does the `polyder` function do in MATLAB / RunMat?
50`polyder` differentiates polynomials represented by their coefficient vectors. The coefficients
51follow MATLAB’s convention: the first element corresponds to the highest power of `x`. The builtin
52supports three related operations:
53
541. `polyder(p)` returns the derivative of a polynomial `p`.
552. `polyder(p, a)` applies the product rule to the convolution `conv(p, a)`.
563. `[num, den] = polyder(u, v)` returns the derivative of a rational function `u(x) / v(x)`
57 using the quotient rule, yielding the numerator `num` and denominator `den`.
58
59## How does the `polyder` function behave in MATLAB / RunMat?
60- Accepts real or complex scalars, row vectors, column vectors, or empty vectors. Inputs with more
61 than one non-singleton dimension raise MATLAB-compatible errors.
62- Logical and integer coefficients are promoted to double precision before differentiation.
63- Leading zeros are removed in outputs (unless the polynomial is identically zero, in which case a
64 single zero coefficient is returned).
65- The orientation of the first input polynomial is preserved for the derivative of a single
66 polynomial or a product; the denominator in the quotient rule preserves the orientation of `v`.
67- Calling `polyder(p, a)` with a single output applies the product rule. Capturing two outputs
68 alongside two input polynomials returns the quotient-rule numerator and denominator
69 (`u' * v - u * v'`, `v * v`).
70- Empty inputs are treated as the zero polynomial.
71- When inputs live on the GPU (e.g., `gpuArray`) and the active provider exposes the polyder
72 hooks, differentiation runs entirely on-device and returns trimmed GPU tensors. Providers that
73 lack the hooks fall back to gathering coefficients to the host, executing the reference CPU
74 implementation, and returning host-resident results. Explicit calls to `gpuArray` remain
75 available if you need to force residency.
76
77## `polyder` Function GPU Execution Behaviour
78When a provider advertises the `polyder` hooks (the in-process and WGPU backends both do), single
79polynomials and the product/quotient forms execute fully on the GPU. Outputs preserve the
80orientation of the leading input while trimming leading zeros exactly as the CPU path does. If the
81provider declines the request—because the coefficients are complex or the backend lacks support—
82RunMat automatically gathers the inputs and falls back to the CPU reference implementation to
83preserve MATLAB compatibility.
84
85## Examples of using the `polyder` function in MATLAB / RunMat
86
87### Differentiating a cubic polynomial
88
89```matlab
90p = [3 -2 5 7]; % 3x^3 - 2x^2 + 5x + 7
91dp = polyder(p);
92```
93
94Expected output:
95
96```matlab
97dp = [9 -4 5];
98```
99
100### Applying the product rule
101
102```matlab
103p = [1 0 -2]; % x^2 - 2
104a = [1 1]; % x + 1
105dp = polyder(p, a); % derivative of conv(p, a)
106```
107
108Expected output:
109
110```matlab
111dp = [3 2 -2];
112```
113
114### Differentiating a rational function
115
116```matlab
117u = [1 0 -4]; % x^2 - 4
118v = [1 -1]; % x - 1
119[num, den] = polyder(u, v); % derivative of (u / v)
120```
121
122Expected output:
123
124```matlab
125num = [1 -2 4];
126den = [1 -2 1];
127```
128
129### Preserving column-vector orientation
130
131```matlab
132p = [1; 0; -3]; % column vector coefficients
133dp = polyder(p);
134```
135
136Expected output:
137
138```matlab
139dp =
140 2
141 0
142```
143
144### Differentiating complex-valued coefficients
145
146```matlab
147p = [1+2i, -3, 4i];
148dp = polyder(p);
149```
150
151Expected output:
152
153```matlab
154dp = [2+4i, -3];
155```
156
157### Working with gpuArray inputs
158
159```matlab
160g = gpuArray([2 0 -5 4]);
161dp = polyder(g); % stays on the GPU when provider hooks are available
162result = gather(dp);
163```
164
165Expected behaviour:
166
167```matlab
168result = [6 0 -5];
169```
170
171## FAQ
172
173### What happens if I pass an empty coefficient vector?
174The empty vector represents the zero polynomial. `polyder([])` returns `[0]`, and the product and
175quotient forms treat empty inputs as zeros.
176
177### Does `polyder` support column-vector coefficients?
178Yes. The orientation of the first polynomial is preserved for single-polynomial and product
179derivatives. For the quotient rule, the numerator inherits the orientation of `u` and the
180denominator inherits the orientation of `v`.
181
182### How are leading zeros handled in the result?
183Leading zeros are removed automatically to mirror MATLAB. If all coefficients cancel out, a single
184zero coefficient is returned.
185
186### Can I differentiate logical or integer coefficient vectors?
187Yes. Logical and integer inputs are promoted to double precision before differentiation, matching
188MATLAB semantics.
189
190### How do I compute the derivative of a rational function?
191Call `[num, den] = polyder(u, v)`. The numerator and denominator are the coefficients of
192`(u' * v - u * v')` and `v * v`, respectively, with leading zeros removed.
193
194### Does the builtin launch GPU kernels?
195Yes whenever the active acceleration provider implements the `polyder` hooks. The in-process and
196WGPU backends both execute the derivative on the device. Providers that lack the hooks—or inputs that
197require complex arithmetic—fall back to the CPU reference implementation, returning the result on the
198host. Wrap the output in `gpuArray` manually if you need to restore device residency in that case.
199
200### What if I request more than two outputs?
201`polyder` only defines one or two outputs. Additional requested outputs are filled with `0`, just as
202RunMat currently does for other MATLAB builtins whose extra outputs are ignored.
203
204### Are complex coefficients supported in the quotient rule?
205Yes. Both the numerator and denominator are computed using full complex arithmetic, so mixed
206real/complex inputs work without additional steps.
207
208### Do I need to normalise or pad my coefficient vectors?
209No. Provide coefficients exactly as MATLAB expects (highest power first). The builtin takes care of
210padding, trimming, and orientation.
211
212### How precise is the computation?
213All arithmetic uses IEEE 754 double precision (`f64`), matching MATLAB’s default numeric type.
214
215## See Also
216[polyval](./polyval), [polyfit](./polyfit), [conv](../signal/conv), [deconv](../signal/deconv), [gpuArray](../../acceleration/gpu/gpuArray), [gather](../../acceleration/gpu/gather)
217"#;
218
219pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
220 name: "polyder",
221 op_kind: GpuOpKind::Custom("polynomial-derivative"),
222 supported_precisions: &[ScalarType::F32, ScalarType::F64],
223 broadcast: BroadcastSemantics::None,
224 provider_hooks: &[
225 ProviderHook::Custom("polyder-single"),
226 ProviderHook::Custom("polyder-product"),
227 ProviderHook::Custom("polyder-quotient"),
228 ],
229 constant_strategy: ConstantStrategy::InlineLiteral,
230 residency: ResidencyPolicy::NewHandle,
231 nan_mode: ReductionNaN::Include,
232 two_pass_threshold: None,
233 workgroup_size: None,
234 accepts_nan_mode: false,
235 notes: "Runs on-device when providers expose polyder hooks; falls back to the host for complex coefficients or unsupported shapes.",
236};
237
238register_builtin_gpu_spec!(GPU_SPEC);
239
240pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
241 name: "polyder",
242 shape: ShapeRequirements::Any,
243 constant_strategy: ConstantStrategy::InlineLiteral,
244 elementwise: None,
245 reduction: None,
246 emits_nan: false,
247 notes: "Symbolic operation on coefficient vectors; fusion bypasses this builtin.",
248};
249
250register_builtin_fusion_spec!(FUSION_SPEC);
251
252#[cfg(feature = "doc_export")]
253register_builtin_doc_text!("polyder", DOC_MD);
254
255#[runtime_builtin(
256 name = "polyder",
257 category = "math/poly",
258 summary = "Differentiate polynomials, products, and ratios with MATLAB-compatible coefficient vectors.",
259 keywords = "polyder,polynomial,derivative,product,quotient"
260)]
261fn polyder_builtin(first: Value, rest: Vec<Value>) -> Result<Value, String> {
262 match rest.len() {
263 0 => derivative_single(first),
264 1 => derivative_product(first, rest.into_iter().next().unwrap()),
265 _ => Err("polyder: too many input arguments".to_string()),
266 }
267}
268
269fn try_gpu_derivative_single(value: &Value) -> Result<Option<Value>, String> {
270 let Value::GpuTensor(handle) = value else {
271 return Ok(None);
272 };
273 let Some(provider) = runmat_accelerate_api::provider() else {
274 return Ok(None);
275 };
276 match provider.polyder_single(handle) {
277 Ok(out) => Ok(Some(Value::GpuTensor(out))),
278 Err(err) => {
279 trace!("polyder: provider polyder_single fallback: {err}");
280 Ok(None)
281 }
282 }
283}
284
285fn try_gpu_derivative_product(first: &Value, second: &Value) -> Result<Option<Value>, String> {
286 match (first, second) {
287 (Value::GpuTensor(p), Value::GpuTensor(q)) => {
288 let Some(provider) = runmat_accelerate_api::provider() else {
289 return Ok(None);
290 };
291 match provider.polyder_product(p, q) {
292 Ok(out) => Ok(Some(Value::GpuTensor(out))),
293 Err(err) => {
294 trace!("polyder: provider polyder_product fallback: {err}");
295 Ok(None)
296 }
297 }
298 }
299 _ => Ok(None),
300 }
301}
302
303fn try_gpu_quotient(u: &Value, v: &Value) -> Result<Option<PolyderEval>, String> {
304 match (u, v) {
305 (Value::GpuTensor(uh), Value::GpuTensor(vh)) => {
306 let Some(provider) = runmat_accelerate_api::provider() else {
307 return Ok(None);
308 };
309 match provider.polyder_quotient(uh, vh) {
310 Ok(result) => Ok(Some(PolyderEval {
311 numerator: Value::GpuTensor(result.numerator),
312 denominator: Value::GpuTensor(result.denominator),
313 })),
314 Err(err) => {
315 trace!("polyder: provider polyder_quotient fallback: {err}");
316 Ok(None)
317 }
318 }
319 }
320 _ => Ok(None),
321 }
322}
323
324pub fn evaluate_quotient(u: Value, v: Value) -> Result<PolyderEval, String> {
326 if let Some(eval) = try_gpu_quotient(&u, &v)? {
327 return Ok(eval);
328 }
329 let u_poly = parse_polynomial("polyder", "U", u)?;
330 let v_poly = parse_polynomial("polyder", "V", v)?;
331 let numerator = quotient_numerator(&u_poly, &v_poly)?;
332 let denominator = quotient_denominator(&v_poly)?;
333 Ok(PolyderEval {
334 numerator,
335 denominator,
336 })
337}
338
339#[derive(Clone)]
341pub struct PolyderEval {
342 numerator: Value,
343 denominator: Value,
344}
345
346impl PolyderEval {
347 pub fn numerator(&self) -> Value {
349 self.numerator.clone()
350 }
351
352 pub fn denominator(&self) -> Value {
354 self.denominator.clone()
355 }
356}
357
358pub fn derivative_single(value: Value) -> Result<Value, String> {
359 if let Some(out) = try_gpu_derivative_single(&value)? {
360 return Ok(out);
361 }
362 let poly = parse_polynomial("polyder", "P", value)?;
363 differentiate_polynomial(&poly)
364}
365
366pub fn derivative_product(first: Value, second: Value) -> Result<Value, String> {
367 if let Some(out) = try_gpu_derivative_product(&first, &second)? {
368 return Ok(out);
369 }
370 let p = parse_polynomial("polyder", "P", first)?;
371 let q = parse_polynomial("polyder", "A", second)?;
372 product_derivative(&p, &q)
373}
374
375fn quotient_numerator(u: &Polynomial, v: &Polynomial) -> Result<Value, String> {
376 let du = raw_derivative(&u.coeffs);
377 let dv = raw_derivative(&v.coeffs);
378 let term1 = poly_convolve(&du, &v.coeffs);
379 let term2 = poly_convolve(&u.coeffs, &dv);
380 let mut numerator = poly_sub(&term1, &term2);
381 numerator = trim_leading_zeros(&numerator);
382 coeffs_to_value(&numerator, u.orientation)
383}
384
385fn quotient_denominator(v: &Polynomial) -> Result<Value, String> {
386 let mut denominator = poly_convolve(&v.coeffs, &v.coeffs);
387 denominator = trim_leading_zeros(&denominator);
388 coeffs_to_value(&denominator, v.orientation)
389}
390
391fn differentiate_polynomial(poly: &Polynomial) -> Result<Value, String> {
392 let mut coeffs = raw_derivative(&poly.coeffs);
393 coeffs = trim_leading_zeros(&coeffs);
394 coeffs_to_value(&coeffs, poly.orientation)
395}
396
397fn product_derivative(p: &Polynomial, q: &Polynomial) -> Result<Value, String> {
398 let dp = raw_derivative(&p.coeffs);
399 let dq = raw_derivative(&q.coeffs);
400 let term1 = poly_convolve(&dp, &q.coeffs);
401 let term2 = poly_convolve(&p.coeffs, &dq);
402 let mut result = poly_add(&term1, &term2);
403 result = trim_leading_zeros(&result);
404 coeffs_to_value(&result, p.orientation)
405}
406
407fn raw_derivative(coeffs: &[Complex64]) -> Vec<Complex64> {
408 if coeffs.len() <= 1 {
409 return vec![Complex64::new(0.0, 0.0)];
410 }
411 let mut output = Vec::with_capacity(coeffs.len() - 1);
412 let mut power = coeffs.len() - 1;
413 for coeff in coeffs.iter().take(coeffs.len() - 1) {
414 output.push(*coeff * (power as f64));
415 power -= 1;
416 }
417 output
418}
419
420fn poly_convolve(a: &[Complex64], b: &[Complex64]) -> Vec<Complex64> {
421 if a.is_empty() || b.is_empty() {
422 return Vec::new();
423 }
424 let mut result = vec![Complex64::new(0.0, 0.0); a.len() + b.len() - 1];
425 for (i, &ai) in a.iter().enumerate() {
426 for (j, &bj) in b.iter().enumerate() {
427 result[i + j] += ai * bj;
428 }
429 }
430 result
431}
432
433fn poly_add(a: &[Complex64], b: &[Complex64]) -> Vec<Complex64> {
434 let len = a.len().max(b.len());
435 let mut result = vec![Complex64::new(0.0, 0.0); len];
436 for (idx, &value) in a.iter().enumerate() {
437 result[len - a.len() + idx] += value;
438 }
439 for (idx, &value) in b.iter().enumerate() {
440 result[len - b.len() + idx] += value;
441 }
442 result
443}
444
445fn poly_sub(a: &[Complex64], b: &[Complex64]) -> Vec<Complex64> {
446 let len = a.len().max(b.len());
447 let mut result = vec![Complex64::new(0.0, 0.0); len];
448 for (idx, &value) in a.iter().enumerate() {
449 result[len - a.len() + idx] += value;
450 }
451 for (idx, &value) in b.iter().enumerate() {
452 result[len - b.len() + idx] -= value;
453 }
454 result
455}
456
457fn trim_leading_zeros(coeffs: &[Complex64]) -> Vec<Complex64> {
458 let mut first = None;
459 for (idx, coeff) in coeffs.iter().enumerate() {
460 if coeff.norm() > EPS {
461 first = Some(idx);
462 break;
463 }
464 }
465 match first {
466 Some(idx) => coeffs[idx..].to_vec(),
467 None => vec![Complex64::new(0.0, 0.0)],
468 }
469}
470
471fn coeffs_to_value(coeffs: &[Complex64], orientation: Orientation) -> Result<Value, String> {
472 if coeffs.iter().all(|c| c.im.abs() <= EPS) {
473 let data: Vec<f64> = coeffs.iter().map(|c| c.re).collect();
474 let shape = orientation.shape_for_len(data.len());
475 let tensor = Tensor::new(data, shape).map_err(|e| format!("polyder: {e}"))?;
476 Ok(tensor_into_value(tensor))
477 } else {
478 let data: Vec<(f64, f64)> = coeffs.iter().map(|c| (c.re, c.im)).collect();
479 let shape = orientation.shape_for_len(data.len());
480 let tensor = ComplexTensor::new(data, shape).map_err(|e| format!("polyder: {e}"))?;
481 Ok(complex_tensor_into_value(tensor))
482 }
483}
484
485fn parse_polynomial(context: &str, label: &str, value: Value) -> Result<Polynomial, String> {
486 let gathered = dispatcher::gather_if_needed(&value).map_err(|e| format!("{context}: {e}"))?;
487 let (coeffs, orientation) = match gathered {
488 Value::Tensor(tensor) => {
489 ensure_vector_shape(context, label, &tensor.shape)?;
490 let orientation = orientation_from_shape(&tensor.shape);
491 if tensor.data.is_empty() {
492 (vec![Complex64::new(0.0, 0.0)], orientation)
493 } else {
494 (
495 tensor
496 .data
497 .into_iter()
498 .map(|re| Complex64::new(re, 0.0))
499 .collect(),
500 orientation,
501 )
502 }
503 }
504 Value::ComplexTensor(tensor) => {
505 ensure_vector_shape(context, label, &tensor.shape)?;
506 let orientation = orientation_from_shape(&tensor.shape);
507 if tensor.data.is_empty() {
508 (vec![Complex64::new(0.0, 0.0)], orientation)
509 } else {
510 (
511 tensor
512 .data
513 .into_iter()
514 .map(|(re, im)| Complex64::new(re, im))
515 .collect(),
516 orientation,
517 )
518 }
519 }
520 Value::LogicalArray(logical) => {
521 let tensor = tensor::logical_to_tensor(&logical)?;
522 ensure_vector_shape(context, label, &tensor.shape)?;
523 let orientation = orientation_from_shape(&tensor.shape);
524 if tensor.data.is_empty() {
525 (vec![Complex64::new(0.0, 0.0)], orientation)
526 } else {
527 (
528 tensor
529 .data
530 .into_iter()
531 .map(|re| Complex64::new(re, 0.0))
532 .collect(),
533 orientation,
534 )
535 }
536 }
537 Value::Num(n) => (vec![Complex64::new(n, 0.0)], Orientation::Scalar),
538 Value::Int(i) => (vec![Complex64::new(i.to_f64(), 0.0)], Orientation::Scalar),
539 Value::Bool(b) => (
540 vec![Complex64::new(if b { 1.0 } else { 0.0 }, 0.0)],
541 Orientation::Scalar,
542 ),
543 Value::Complex(re, im) => (vec![Complex64::new(re, im)], Orientation::Scalar),
544 other => {
545 return Err(format!(
546 "{context}: expected {label} to be a numeric vector, got {other:?}"
547 ));
548 }
549 };
550
551 Ok(Polynomial {
552 coeffs,
553 orientation,
554 })
555}
556
557fn ensure_vector_shape(context: &str, label: &str, shape: &[usize]) -> Result<(), String> {
558 let non_unit = shape.iter().copied().filter(|&dim| dim > 1).count();
559 if non_unit <= 1 {
560 Ok(())
561 } else {
562 Err(format!(
563 "{context}: {label} must be a vector of coefficients"
564 ))
565 }
566}
567
568#[derive(Clone, Copy)]
569enum Orientation {
570 Scalar,
571 Row,
572 Column,
573}
574
575impl Orientation {
576 fn shape_for_len(self, len: usize) -> Vec<usize> {
577 if len <= 1 {
578 return vec![1, 1];
579 }
580 match self {
581 Orientation::Scalar => vec![1, len],
582 Orientation::Row => vec![1, len],
583 Orientation::Column => vec![len, 1],
584 }
585 }
586}
587
588fn orientation_from_shape(shape: &[usize]) -> Orientation {
589 for (idx, &dim) in shape.iter().enumerate() {
590 if dim != 1 {
591 return match idx {
592 0 => Orientation::Column,
593 1 => Orientation::Row,
594 _ => Orientation::Column,
595 };
596 }
597 }
598 Orientation::Scalar
599}
600
601#[derive(Clone)]
602struct Polynomial {
603 coeffs: Vec<Complex64>,
604 orientation: Orientation,
605}
606
607#[cfg(test)]
608mod tests {
609 use super::*;
610 use crate::builtins::common::test_support;
611 use runmat_builtins::{IntValue, Tensor};
612
613 #[test]
614 fn derivative_of_cubic_polynomial_is_correct() {
615 let tensor = Tensor::new(vec![3.0, -2.0, 5.0, 7.0], vec![1, 4]).unwrap();
616 let result = derivative_single(Value::Tensor(tensor)).expect("polyder");
617 match result {
618 Value::Tensor(t) => {
619 assert_eq!(t.shape, vec![1, 3]);
620 assert!(t
621 .data
622 .iter()
623 .zip([9.0, -4.0, 5.0])
624 .all(|(lhs, rhs)| (lhs - rhs).abs() < 1e-12));
625 }
626 other => panic!("expected tensor result, got {other:?}"),
627 }
628 }
629
630 #[test]
631 fn derivative_of_product_matches_manual_rule() {
632 let p = Tensor::new(vec![1.0, 0.0, -2.0], vec![1, 3]).unwrap();
633 let a = Tensor::new(vec![1.0, 1.0], vec![1, 2]).unwrap();
634 let result =
635 derivative_product(Value::Tensor(p), Value::Tensor(a)).expect("polyder product");
636 match result {
637 Value::Tensor(t) => {
638 assert_eq!(t.shape, vec![1, 3]);
639 assert!(t
640 .data
641 .iter()
642 .zip([3.0, 2.0, -2.0])
643 .all(|(lhs, rhs)| (lhs - rhs).abs() < 1e-12));
644 }
645 other => panic!("expected tensor result, got {other:?}"),
646 }
647 }
648
649 #[test]
650 fn quotient_rule_produces_expected_num_and_den() {
651 let u = Tensor::new(vec![1.0, 0.0, -4.0], vec![1, 3]).unwrap();
652 let v = Tensor::new(vec![1.0, -1.0], vec![1, 2]).unwrap();
653 let eval = evaluate_quotient(Value::Tensor(u), Value::Tensor(v)).expect("polyder quotient");
654 match eval.numerator() {
655 Value::Tensor(t) => {
656 assert_eq!(t.shape, vec![1, 3]);
657 assert!(t
658 .data
659 .iter()
660 .zip([1.0, -2.0, 4.0])
661 .all(|(lhs, rhs)| (lhs - rhs).abs() < 1e-12));
662 }
663 other => panic!("expected tensor numerator, got {other:?}"),
664 }
665 match eval.denominator() {
666 Value::Tensor(t) => {
667 assert_eq!(t.shape, vec![1, 3]);
668 assert!(t
669 .data
670 .iter()
671 .zip([1.0, -2.0, 1.0])
672 .all(|(lhs, rhs)| (lhs - rhs).abs() < 1e-12));
673 }
674 other => panic!("expected tensor denominator, got {other:?}"),
675 }
676 }
677
678 #[test]
679 fn column_vector_orientation_is_preserved() {
680 let tensor = Tensor::new(vec![1.0, 0.0, -3.0], vec![3, 1]).unwrap();
681 let result = derivative_single(Value::Tensor(tensor)).expect("polyder column");
682 match result {
683 Value::Tensor(t) => {
684 assert_eq!(t.shape, vec![2, 1]);
685 assert!(t
686 .data
687 .iter()
688 .zip([2.0, 0.0])
689 .all(|(lhs, rhs)| (lhs - rhs).abs() < 1e-12));
690 }
691 other => panic!("expected column tensor, got {other:?}"),
692 }
693 }
694
695 #[test]
696 fn complex_coefficients_are_supported() {
697 let tensor =
698 ComplexTensor::new(vec![(1.0, 2.0), (-3.0, 0.0), (0.0, 4.0)], vec![1, 3]).unwrap();
699 let result = derivative_single(Value::ComplexTensor(tensor)).expect("polyder complex");
700 match result {
701 Value::ComplexTensor(t) => {
702 assert_eq!(t.shape, vec![1, 2]);
703 let expected = [Complex64::new(2.0, 4.0), Complex64::new(-3.0, 0.0)];
704 assert!(t
705 .data
706 .iter()
707 .zip(expected.iter())
708 .all(|((re, im), expected)| {
709 (re - expected.re).abs() < 1e-12 && (im - expected.im).abs() < 1e-12
710 }));
711 }
712 other => panic!("expected complex tensor, got {other:?}"),
713 }
714 }
715
716 #[test]
717 fn empty_polynomial_returns_zero() {
718 let tensor = Tensor::new(Vec::new(), vec![1, 0]).unwrap();
719 let result = derivative_single(Value::Tensor(tensor)).expect("polyder empty");
720 assert_eq!(result, Value::Num(0.0));
721 }
722
723 #[test]
724 fn rejects_matrix_input() {
725 let tensor = Tensor::new(vec![1.0, 2.0, 3.0, 4.0], vec![2, 2]).unwrap();
726 let err = derivative_single(Value::Tensor(tensor)).unwrap_err();
727 assert!(
728 err.contains("vector of coefficients"),
729 "unexpected error: {err}"
730 );
731 }
732
733 #[test]
734 fn rejects_string_input() {
735 let err = derivative_single(Value::String("abc".into())).unwrap_err();
736 assert!(
737 err.contains("numeric vector"),
738 "unexpected error message: {err}"
739 );
740 }
741
742 #[test]
743 fn mixed_gpu_cpu_product_falls_back_to_host() {
744 test_support::with_test_provider(|provider| {
745 let p = Tensor::new(vec![1.0, 0.0, -2.0], vec![1, 3]).unwrap();
746 let q = Tensor::new(vec![1.0, 1.0], vec![1, 2]).unwrap();
747 let cpu_expected =
748 derivative_product(Value::Tensor(p.clone()), Value::Tensor(q.clone()))
749 .expect("cpu product");
750 let Value::Tensor(cpu_tensor) = cpu_expected else {
751 panic!("expected tensor result");
752 };
753
754 let view_p = runmat_accelerate_api::HostTensorView {
755 data: &p.data,
756 shape: &p.shape,
757 };
758 let handle_p = provider.upload(&view_p).expect("upload p");
759 let result = derivative_product(Value::GpuTensor(handle_p), Value::Tensor(q))
760 .expect("mixed product");
761 let Value::Tensor(host_tensor) = result else {
762 panic!("expected host tensor result");
763 };
764 assert_eq!(host_tensor.shape, cpu_tensor.shape);
765 assert!(host_tensor
766 .data
767 .iter()
768 .zip(cpu_tensor.data.iter())
769 .all(|(lhs, rhs)| (lhs - rhs).abs() < 1e-12));
770 });
771 }
772
773 #[test]
774 fn builtin_rejects_too_many_inputs() {
775 let err = super::polyder_builtin(Value::Num(1.0), vec![Value::Num(2.0), Value::Num(3.0)])
776 .unwrap_err();
777 assert!(
778 err.contains("too many input arguments"),
779 "unexpected error: {err}"
780 );
781 }
782
783 #[test]
784 fn gpu_inputs_remain_on_device() {
785 test_support::with_test_provider(|provider| {
786 let tensor = Tensor::new(vec![2.0, 0.0, -5.0, 4.0], vec![1, 4]).unwrap();
787 let view = runmat_accelerate_api::HostTensorView {
788 data: &tensor.data,
789 shape: &tensor.shape,
790 };
791 let handle = provider.upload(&view).expect("upload");
792 let result = derivative_single(Value::GpuTensor(handle)).expect("polyder gpu");
793 let Value::GpuTensor(out_handle) = result else {
794 panic!("expected GPU tensor result");
795 };
796 let gathered = test_support::gather(Value::GpuTensor(out_handle)).expect("gather");
797 assert_eq!(gathered.shape, vec![1, 3]);
798 assert!(gathered
799 .data
800 .iter()
801 .zip([6.0, 0.0, -5.0])
802 .all(|(lhs, rhs)| (lhs - rhs).abs() < 1e-12));
803 });
804 }
805
806 #[test]
807 fn gpu_product_matches_cpu() {
808 test_support::with_test_provider(|provider| {
809 let p = Tensor::new(vec![1.0, 0.0, -2.0], vec![1, 3]).unwrap();
810 let q = Tensor::new(vec![1.0, 1.0], vec![1, 2]).unwrap();
811 let expected = derivative_product(Value::Tensor(p.clone()), Value::Tensor(q.clone()))
812 .expect("cpu product");
813 let Value::Tensor(expected_tensor) = expected else {
814 panic!("expected tensor output");
815 };
816
817 let view_p = runmat_accelerate_api::HostTensorView {
818 data: &p.data,
819 shape: &p.shape,
820 };
821 let view_q = runmat_accelerate_api::HostTensorView {
822 data: &q.data,
823 shape: &q.shape,
824 };
825 let handle_p = provider.upload(&view_p).expect("upload p");
826 let handle_q = provider.upload(&view_q).expect("upload q");
827 let gpu_result =
828 derivative_product(Value::GpuTensor(handle_p), Value::GpuTensor(handle_q))
829 .expect("gpu product");
830 let Value::GpuTensor(gpu_handle) = gpu_result else {
831 panic!("expected GPU tensor");
832 };
833 let gathered = test_support::gather(Value::GpuTensor(gpu_handle)).expect("gather");
834 assert_eq!(gathered.shape, expected_tensor.shape);
835 assert!(gathered
836 .data
837 .iter()
838 .zip(expected_tensor.data.iter())
839 .all(|(lhs, rhs)| (lhs - rhs).abs() < 1e-12));
840 });
841 }
842
843 #[test]
844 fn gpu_quotient_matches_cpu() {
845 test_support::with_test_provider(|provider| {
846 let u = Tensor::new(vec![1.0, 0.0, -4.0], vec![1, 3]).unwrap();
847 let v = Tensor::new(vec![1.0, -1.0], vec![1, 2]).unwrap();
848 let expected = evaluate_quotient(Value::Tensor(u.clone()), Value::Tensor(v.clone()))
849 .expect("cpu quotient");
850 let Value::Tensor(expected_num) = expected.numerator() else {
851 panic!("expected tensor numerator");
852 };
853 let Value::Tensor(expected_den) = expected.denominator() else {
854 panic!("expected tensor denominator");
855 };
856
857 let view_u = runmat_accelerate_api::HostTensorView {
858 data: &u.data,
859 shape: &u.shape,
860 };
861 let view_v = runmat_accelerate_api::HostTensorView {
862 data: &v.data,
863 shape: &v.shape,
864 };
865 let handle_u = provider.upload(&view_u).expect("upload u");
866 let handle_v = provider.upload(&view_v).expect("upload v");
867 let gpu_eval =
868 evaluate_quotient(Value::GpuTensor(handle_u), Value::GpuTensor(handle_v))
869 .expect("gpu quotient");
870 let gpu_num = test_support::gather(gpu_eval.numerator()).expect("gather num");
871 let gpu_den = test_support::gather(gpu_eval.denominator()).expect("gather den");
872 assert_eq!(gpu_num.shape, expected_num.shape);
873 assert_eq!(gpu_den.shape, expected_den.shape);
874 assert!(gpu_num
875 .data
876 .iter()
877 .zip(expected_num.data.iter())
878 .all(|(lhs, rhs)| (lhs - rhs).abs() < 1e-12));
879 assert!(gpu_den
880 .data
881 .iter()
882 .zip(expected_den.data.iter())
883 .all(|(lhs, rhs)| (lhs - rhs).abs() < 1e-12));
884 });
885 }
886
887 #[test]
888 #[cfg(feature = "wgpu")]
889 fn wgpu_polyder_single_matches_cpu() {
890 let _ = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
891 runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
892 );
893 let provider = runmat_accelerate_api::provider().expect("wgpu provider");
894 let tensor = Tensor::new(vec![3.0, -2.0, 5.0, 7.0], vec![1, 4]).unwrap();
895 let expected = derivative_single(Value::Tensor(tensor.clone())).expect("cpu polyder");
896 let Value::Tensor(expected_tensor) = expected else {
897 panic!("expected tensor");
898 };
899 let view = runmat_accelerate_api::HostTensorView {
900 data: &tensor.data,
901 shape: &tensor.shape,
902 };
903 let handle = provider.upload(&view).expect("upload");
904 let gpu_result = derivative_single(Value::GpuTensor(handle)).expect("gpu polyder");
905 let gathered = test_support::gather(gpu_result).expect("gather");
906 assert_eq!(gathered.shape, expected_tensor.shape);
907 assert!(gathered
908 .data
909 .iter()
910 .zip(expected_tensor.data.iter())
911 .all(|(lhs, rhs)| (lhs - rhs).abs() < 1e-12));
912 }
913
914 #[test]
915 #[cfg(feature = "wgpu")]
916 fn wgpu_polyder_product_matches_cpu() {
917 let _ = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
918 runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
919 );
920 let provider = runmat_accelerate_api::provider().expect("wgpu provider");
921 let p = Tensor::new(vec![1.0, 0.0, -2.0], vec![1, 3]).unwrap();
922 let q = Tensor::new(vec![1.0, 1.0], vec![1, 2]).unwrap();
923 let expected = derivative_product(Value::Tensor(p.clone()), Value::Tensor(q.clone()))
924 .expect("cpu product");
925 let Value::Tensor(expected_tensor) = expected else {
926 panic!("expected tensor");
927 };
928 let view_p = runmat_accelerate_api::HostTensorView {
929 data: &p.data,
930 shape: &p.shape,
931 };
932 let view_q = runmat_accelerate_api::HostTensorView {
933 data: &q.data,
934 shape: &q.shape,
935 };
936 let handle_p = provider.upload(&view_p).expect("upload p");
937 let handle_q = provider.upload(&view_q).expect("upload q");
938 let gpu_result = derivative_product(Value::GpuTensor(handle_p), Value::GpuTensor(handle_q))
939 .expect("gpu product");
940 let gathered = test_support::gather(gpu_result).expect("gather");
941 assert_eq!(gathered.shape, expected_tensor.shape);
942 assert!(gathered
943 .data
944 .iter()
945 .zip(expected_tensor.data.iter())
946 .all(|(lhs, rhs)| (lhs - rhs).abs() < 1e-12));
947 }
948
949 #[test]
950 #[cfg(feature = "wgpu")]
951 fn wgpu_polyder_quotient_matches_cpu() {
952 let _ = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
953 runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
954 );
955 let provider = runmat_accelerate_api::provider().expect("wgpu provider");
956 let u = Tensor::new(vec![1.0, 0.0, -4.0], vec![1, 3]).unwrap();
957 let v = Tensor::new(vec![1.0, -1.0], vec![1, 2]).unwrap();
958 let expected = evaluate_quotient(Value::Tensor(u.clone()), Value::Tensor(v.clone()))
959 .expect("cpu quotient");
960 let expected_num = match expected.numerator() {
961 Value::Tensor(t) => t,
962 other => panic!("expected tensor numerator, got {other:?}"),
963 };
964 let expected_den = match expected.denominator() {
965 Value::Tensor(t) => t,
966 other => panic!("expected tensor denominator, got {other:?}"),
967 };
968 let view_u = runmat_accelerate_api::HostTensorView {
969 data: &u.data,
970 shape: &u.shape,
971 };
972 let view_v = runmat_accelerate_api::HostTensorView {
973 data: &v.data,
974 shape: &v.shape,
975 };
976 let handle_u = provider.upload(&view_u).expect("upload u");
977 let handle_v = provider.upload(&view_v).expect("upload v");
978 let gpu_eval = evaluate_quotient(Value::GpuTensor(handle_u), Value::GpuTensor(handle_v))
979 .expect("gpu quotient");
980 let gpu_num = test_support::gather(gpu_eval.numerator()).expect("gather num");
981 let gpu_den = test_support::gather(gpu_eval.denominator()).expect("gather den");
982 assert_eq!(gpu_num.shape, expected_num.shape);
983 assert_eq!(gpu_den.shape, expected_den.shape);
984 assert!(gpu_num
985 .data
986 .iter()
987 .zip(expected_num.data.iter())
988 .all(|(lhs, rhs)| (lhs - rhs).abs() < 1e-12));
989 assert!(gpu_den
990 .data
991 .iter()
992 .zip(expected_den.data.iter())
993 .all(|(lhs, rhs)| (lhs - rhs).abs() < 1e-12));
994 }
995
996 #[test]
997 fn derivative_promotes_integers() {
998 let value = Value::Int(IntValue::I32(5));
999 let result = derivative_single(value).expect("polyder int");
1000 assert_eq!(result, Value::Num(0.0));
1001 }
1002
1003 #[test]
1004 #[cfg(feature = "doc_export")]
1005 fn doc_examples_present() {
1006 let blocks = test_support::doc_examples(DOC_MD);
1007 assert!(!blocks.is_empty());
1008 }
1009}