Skip to main content

runmat_runtime/builtins/math/poly/
polyder.rs

1//! MATLAB-compatible `polyder` builtin with GPU-aware semantics for RunMat.
2
3use 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
272/// Evaluate the quotient rule derivative `[num, den] = polyder(u, v)`.
273pub 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/// Differentiated outputs for the quotient rule.
288#[derive(Clone)]
289pub struct PolyderEval {
290    numerator: Value,
291    denominator: Value,
292}
293
294impl PolyderEval {
295    /// Numerator coefficients of the derivative `(u' * v - u * v')`.
296    pub fn numerator(&self) -> Value {
297        self.numerator.clone()
298    }
299
300    /// Denominator coefficients `v^2`.
301    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}