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