Skip to main content

runmat_runtime/builtins/control/
db.rs

1//! MATLAB-compatible `db` decibel conversion builtin for RunMat.
2
3use runmat_builtins::{
4    BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
5    BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
6    ComplexTensor, Tensor, Value,
7};
8use runmat_macros::runtime_builtin;
9
10use crate::builtins::common::broadcast::BroadcastPlan;
11use crate::builtins::common::spec::{
12    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
13    ReductionNaN, ResidencyPolicy, ShapeRequirements,
14};
15use crate::builtins::common::tensor;
16use crate::builtins::control::type_resolvers::db_type;
17use crate::{build_runtime_error, BuiltinResult, RuntimeError};
18
19const BUILTIN_NAME: &str = "db";
20const DB_OUTPUT_YDB: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
21    name: "yDb",
22    ty: BuiltinParamType::NumericArray,
23    arity: BuiltinParamArity::Required,
24    default: None,
25    description: "Decibel-converted output.",
26}];
27const DB_INPUTS_Y: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
28    name: "y",
29    ty: BuiltinParamType::Any,
30    arity: BuiltinParamArity::Required,
31    default: None,
32    description: "Input signal magnitude or power quantity.",
33}];
34const DB_INPUTS_Y_MODE: [BuiltinParamDescriptor; 2] = [
35    BuiltinParamDescriptor {
36        name: "y",
37        ty: BuiltinParamType::Any,
38        arity: BuiltinParamArity::Required,
39        default: None,
40        description: "Input signal magnitude or power quantity.",
41    },
42    BuiltinParamDescriptor {
43        name: "modeOrR",
44        ty: BuiltinParamType::Any,
45        arity: BuiltinParamArity::Optional,
46        default: Some("\"voltage\""),
47        description: "Mode string ('voltage' or 'power') or resistance reference.",
48    },
49];
50const DB_SIGNATURES: [BuiltinSignatureDescriptor; 4] = [
51    BuiltinSignatureDescriptor {
52        label: "yDb = db(y)",
53        inputs: &DB_INPUTS_Y,
54        outputs: &DB_OUTPUT_YDB,
55    },
56    BuiltinSignatureDescriptor {
57        label: "yDb = db(y, \"voltage\")",
58        inputs: &DB_INPUTS_Y_MODE,
59        outputs: &DB_OUTPUT_YDB,
60    },
61    BuiltinSignatureDescriptor {
62        label: "yDb = db(y, \"power\")",
63        inputs: &DB_INPUTS_Y_MODE,
64        outputs: &DB_OUTPUT_YDB,
65    },
66    BuiltinSignatureDescriptor {
67        label: "yDb = db(y, R)",
68        inputs: &DB_INPUTS_Y_MODE,
69        outputs: &DB_OUTPUT_YDB,
70    },
71];
72const DB_ERROR_INVALID_ARGUMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
73    code: "RM.DB.INVALID_ARGUMENT",
74    identifier: Some("RunMat:db:InvalidArgument"),
75    when: "Inputs do not match supported db invocation forms.",
76    message: "db: invalid argument",
77};
78const DB_ERROR_INVALID_MODE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
79    code: "RM.DB.INVALID_MODE",
80    identifier: Some("RunMat:db:InvalidMode"),
81    when: "Mode string is not recognized or is not a scalar text value.",
82    message: "db: invalid mode",
83};
84const DB_ERROR_INVALID_INPUT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
85    code: "RM.DB.INVALID_INPUT",
86    identifier: Some("RunMat:db:InvalidInput"),
87    when: "Input signal cannot be interpreted as numeric magnitude data.",
88    message: "db: invalid input",
89};
90const DB_ERROR_INVALID_RESISTANCE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
91    code: "RM.DB.INVALID_RESISTANCE",
92    identifier: Some("RunMat:db:InvalidResistance"),
93    when: "Resistance reference is non-numeric, complex, non-finite, or non-positive.",
94    message: "db: invalid resistance",
95};
96const DB_ERROR_SIZE_MISMATCH: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
97    code: "RM.DB.SIZE_MISMATCH",
98    identifier: Some("RunMat:db:SizeMismatch"),
99    when: "Signal and resistance inputs are not broadcast compatible.",
100    message: "db: array sizes are not compatible for broadcasting",
101};
102const DB_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
103    code: "RM.DB.INTERNAL",
104    identifier: Some("RunMat:db:Internal"),
105    when: "Internal tensor conversion or allocation failed.",
106    message: "db: internal error",
107};
108const DB_ERRORS: [BuiltinErrorDescriptor; 6] = [
109    DB_ERROR_INVALID_ARGUMENT,
110    DB_ERROR_INVALID_MODE,
111    DB_ERROR_INVALID_INPUT,
112    DB_ERROR_INVALID_RESISTANCE,
113    DB_ERROR_SIZE_MISMATCH,
114    DB_ERROR_INTERNAL,
115];
116pub const DB_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
117    signatures: &DB_SIGNATURES,
118    output_mode: BuiltinOutputMode::Fixed,
119    completion_policy: BuiltinCompletionPolicy::Public,
120    errors: &DB_ERRORS,
121};
122
123#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::control::db")]
124pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
125    name: "db",
126    op_kind: GpuOpKind::Custom("decibel-conversion"),
127    supported_precisions: &[],
128    broadcast: BroadcastSemantics::Matlab,
129    provider_hooks: &[],
130    constant_strategy: ConstantStrategy::InlineLiteral,
131    residency: ResidencyPolicy::GatherImmediately,
132    nan_mode: ReductionNaN::Include,
133    two_pass_threshold: None,
134    workgroup_size: None,
135    accepts_nan_mode: false,
136    notes: "Host-side decibel conversion; gpuArray inputs are gathered before applying mode parsing, complex magnitudes, and optional resistance broadcasting.",
137};
138
139#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::control::db")]
140pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
141    name: "db",
142    shape: ShapeRequirements::BroadcastCompatible,
143    constant_strategy: ConstantStrategy::InlineLiteral,
144    elementwise: None,
145    reduction: None,
146    emits_nan: false,
147    notes: "db is a compound element-wise conversion with string mode parsing and optional resistance input; it terminates fusion and executes on the host.",
148};
149
150fn db_error_with_detail(
151    error: &'static BuiltinErrorDescriptor,
152    detail: impl AsRef<str>,
153) -> RuntimeError {
154    db_error_with_message(format!("{}: {}", error.message, detail.as_ref()), error)
155}
156
157fn db_error_with_message(
158    message: impl Into<String>,
159    error: &'static BuiltinErrorDescriptor,
160) -> RuntimeError {
161    let mut builder = build_runtime_error(message).with_builtin(BUILTIN_NAME);
162    if let Some(identifier) = error.identifier {
163        builder = builder.with_identifier(identifier);
164    }
165    builder.build()
166}
167
168#[derive(Clone, Debug)]
169enum DbMode {
170    Voltage,
171    Power,
172    Resistance(Value),
173}
174
175#[runtime_builtin(
176    name = "db",
177    category = "control",
178    summary = "Convert numeric values to decibels.",
179    keywords = "db,decibel,voltage,power,resistance,complex",
180    accel = "metadata",
181    type_resolver(db_type),
182    descriptor(crate::builtins::control::db::DB_DESCRIPTOR),
183    builtin_path = "crate::builtins::control::db"
184)]
185async fn db_builtin(y: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
186    if rest.len() > 1 {
187        return Err(db_error_with_detail(
188            &DB_ERROR_INVALID_ARGUMENT,
189            "expected db(y), db(y, 'voltage'), db(y, 'power'), or db(y, R)",
190        ));
191    }
192
193    let y = crate::gather_if_needed_async(&y).await?;
194    let mode = match rest.into_iter().next() {
195        Some(arg) => parse_mode(crate::gather_if_needed_async(&arg).await?)?,
196        None => DbMode::Voltage,
197    };
198
199    let magnitudes = magnitude_tensor(y)?;
200    match mode {
201        DbMode::Voltage => map_magnitudes(magnitudes, |m| 20.0 * m.log10()),
202        DbMode::Power => map_magnitudes(magnitudes, |m| 10.0 * m.log10()),
203        DbMode::Resistance(reference) => {
204            let reference = resistance_tensor(reference)?;
205            db_with_resistance(&magnitudes, &reference)
206        }
207    }
208}
209
210fn parse_mode(value: Value) -> BuiltinResult<DbMode> {
211    match value {
212        Value::String(text) => parse_mode_string(&text),
213        Value::StringArray(array) if array.data.len() == 1 => parse_mode_string(&array.data[0]),
214        Value::StringArray(_) => Err(db_error_with_detail(
215            &DB_ERROR_INVALID_MODE,
216            "mode must be a scalar string",
217        )),
218        Value::CharArray(array) if array.rows == 1 => {
219            let text = array.data.iter().collect::<String>();
220            parse_mode_string(&text)
221        }
222        Value::CharArray(_) => Err(db_error_with_detail(
223            &DB_ERROR_INVALID_MODE,
224            "mode must be a character row vector",
225        )),
226        other => Ok(DbMode::Resistance(other)),
227    }
228}
229
230fn parse_mode_string(text: &str) -> BuiltinResult<DbMode> {
231    match text.to_ascii_lowercase().as_str() {
232        "voltage" => Ok(DbMode::Voltage),
233        "power" => Ok(DbMode::Power),
234        _ => Err(db_error_with_detail(
235            &DB_ERROR_INVALID_MODE,
236            format!("unknown mode '{text}', expected 'voltage' or 'power'"),
237        )),
238    }
239}
240
241fn magnitude_tensor(value: Value) -> BuiltinResult<Tensor> {
242    match value {
243        Value::Complex(re, im) => Tensor::new(vec![re.hypot(im)], vec![1, 1]).map_err(|e| {
244            db_error_with_detail(
245                &DB_ERROR_INTERNAL,
246                format!("failed to build scalar magnitude tensor: {e}"),
247            )
248        }),
249        Value::ComplexTensor(tensor) => complex_magnitudes(tensor),
250        Value::String(_) | Value::StringArray(_) | Value::CharArray(_) => Err(
251            db_error_with_detail(&DB_ERROR_INVALID_INPUT, "expected numeric input"),
252        ),
253        other => {
254            let mut tensor = tensor::value_into_tensor_for(BUILTIN_NAME, other)
255                .map_err(|e| db_error_with_detail(&DB_ERROR_INVALID_INPUT, e))?;
256            for value in &mut tensor.data {
257                *value = value.abs();
258            }
259            Ok(tensor)
260        }
261    }
262}
263
264fn complex_magnitudes(tensor: ComplexTensor) -> BuiltinResult<Tensor> {
265    let data = tensor
266        .data
267        .iter()
268        .map(|&(re, im)| re.hypot(im))
269        .collect::<Vec<_>>();
270    Tensor::new(data, tensor.shape).map_err(|e| {
271        db_error_with_detail(
272            &DB_ERROR_INTERNAL,
273            format!("failed to build magnitude tensor: {e}"),
274        )
275    })
276}
277
278fn resistance_tensor(value: Value) -> BuiltinResult<Tensor> {
279    match value {
280        Value::Complex(_, _) | Value::ComplexTensor(_) => Err(db_error_with_detail(
281            &DB_ERROR_INVALID_RESISTANCE,
282            "resistance must be real",
283        )),
284        Value::String(_) | Value::StringArray(_) | Value::CharArray(_) => Err(
285            db_error_with_detail(&DB_ERROR_INVALID_RESISTANCE, "resistance must be numeric"),
286        ),
287        other => {
288            let tensor = tensor::value_into_tensor_for(BUILTIN_NAME, other)
289                .map_err(|e| db_error_with_detail(&DB_ERROR_INVALID_RESISTANCE, e))?;
290            for &resistance in &tensor.data {
291                if !resistance.is_finite() || resistance <= 0.0 {
292                    return Err(db_error_with_detail(
293                        &DB_ERROR_INVALID_RESISTANCE,
294                        "resistance values must be finite and positive",
295                    ));
296                }
297            }
298            Ok(tensor)
299        }
300    }
301}
302
303fn map_magnitudes<F>(input: Tensor, op: F) -> BuiltinResult<Value>
304where
305    F: Fn(f64) -> f64,
306{
307    let data = input
308        .data
309        .iter()
310        .map(|&value| op(value))
311        .collect::<Vec<_>>();
312    let tensor = Tensor::new(data, input.shape).map_err(|e| {
313        db_error_with_detail(
314            &DB_ERROR_INTERNAL,
315            format!("failed to build output tensor: {e}"),
316        )
317    })?;
318    Ok(tensor::tensor_into_value(tensor))
319}
320
321fn db_with_resistance(magnitudes: &Tensor, reference: &Tensor) -> BuiltinResult<Value> {
322    let plan = BroadcastPlan::new(&magnitudes.shape, &reference.shape)
323        .map_err(|err| db_error_with_detail(&DB_ERROR_SIZE_MISMATCH, err))?;
324    if plan.is_empty() {
325        let tensor = Tensor::new(Vec::new(), plan.output_shape().to_vec()).map_err(|e| {
326            db_error_with_detail(
327                &DB_ERROR_INTERNAL,
328                format!("failed to build empty output tensor: {e}"),
329            )
330        })?;
331        return Ok(tensor::tensor_into_value(tensor));
332    }
333
334    let mut data = vec![0.0; plan.len()];
335    for (out_idx, y_idx, r_idx) in plan.iter() {
336        let magnitude = magnitudes.data[y_idx];
337        let resistance = reference.data[r_idx];
338        data[out_idx] = 10.0 * ((magnitude * magnitude) / resistance).log10();
339    }
340    let tensor = Tensor::new(data, plan.output_shape().to_vec()).map_err(|e| {
341        db_error_with_detail(
342            &DB_ERROR_INTERNAL,
343            format!("failed to build output tensor: {e}"),
344        )
345    })?;
346    Ok(tensor::tensor_into_value(tensor))
347}
348
349#[cfg(test)]
350pub(crate) mod tests {
351    use super::*;
352    use crate::builtins::common::test_support;
353    use futures::executor::block_on;
354    use runmat_builtins::{CharArray, IntValue, LogicalArray, ResolveContext, StringArray, Type};
355
356    fn db_builtin(y: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
357        block_on(super::db_builtin(y, rest))
358    }
359
360    fn assert_num_close(value: Value, expected: f64) {
361        match value {
362            Value::Num(actual) => assert!(
363                (actual - expected).abs() < 1e-12,
364                "expected {expected}, got {actual}"
365            ),
366            other => panic!("expected scalar result, got {other:?}"),
367        }
368    }
369
370    fn assert_tensor_close(value: Value, expected_shape: &[usize], expected: &[f64]) {
371        match value {
372            Value::Tensor(tensor) => {
373                assert_eq!(tensor.shape, expected_shape);
374                assert_eq!(tensor.data.len(), expected.len());
375                for (&actual, &expected) in tensor.data.iter().zip(expected) {
376                    if expected.is_infinite() {
377                        assert_eq!(actual, expected);
378                    } else {
379                        assert!(
380                            (actual - expected).abs() < 1e-12,
381                            "expected {expected}, got {actual}"
382                        );
383                    }
384                }
385            }
386            other => panic!("expected tensor result, got {other:?}"),
387        }
388    }
389
390    #[test]
391    fn db_descriptor_signatures_cover_core_forms() {
392        let labels: Vec<&str> = DB_DESCRIPTOR
393            .signatures
394            .iter()
395            .map(|sig| sig.label)
396            .collect();
397        assert!(labels.contains(&"yDb = db(y)"));
398        assert!(labels.contains(&"yDb = db(y, \"voltage\")"));
399        assert!(labels.contains(&"yDb = db(y, \"power\")"));
400        assert!(labels.contains(&"yDb = db(y, R)"));
401    }
402
403    #[test]
404    fn db_type_unary_preserves_tensor_shape() {
405        let out = db_type(
406            &[Type::Tensor {
407                shape: Some(vec![Some(2), Some(3)]),
408            }],
409            &ResolveContext::new(Vec::new()),
410        );
411        assert_eq!(
412            out,
413            Type::Tensor {
414                shape: Some(vec![Some(2), Some(3)])
415            }
416        );
417    }
418
419    #[test]
420    fn db_type_scalar_returns_num() {
421        let out = db_type(&[Type::Num], &ResolveContext::new(Vec::new()));
422        assert_eq!(out, Type::Num);
423    }
424
425    #[test]
426    fn db_type_string_mode_uses_input_shape() {
427        let out = db_type(
428            &[
429                Type::Tensor {
430                    shape: Some(vec![Some(4), Some(1)]),
431                },
432                Type::String,
433            ],
434            &ResolveContext::new(Vec::new()),
435        );
436        assert_eq!(
437            out,
438            Type::Tensor {
439                shape: Some(vec![Some(4), Some(1)])
440            }
441        );
442    }
443
444    #[test]
445    fn db_type_text_modes_use_unary_shape_rules() {
446        let string_array_type = Type::from_value(&Value::StringArray(
447            StringArray::new(vec!["power".into()], vec![1, 1]).unwrap(),
448        ));
449        let char_array_type = Type::from_value(&Value::CharArray(CharArray::new_row("power")));
450
451        for mode in [Type::String, string_array_type, char_array_type] {
452            let out = db_type(
453                &[
454                    Type::Tensor {
455                        shape: Some(vec![Some(1), Some(1)]),
456                    },
457                    mode,
458                ],
459                &ResolveContext::new(Vec::new()),
460            );
461            assert_eq!(out, Type::Num);
462        }
463    }
464
465    #[test]
466    fn db_type_resistance_broadcasts_shapes() {
467        let out = db_type(
468            &[
469                Type::Tensor {
470                    shape: Some(vec![Some(2), Some(1)]),
471                },
472                Type::Tensor {
473                    shape: Some(vec![Some(1), Some(3)]),
474                },
475            ],
476            &ResolveContext::new(Vec::new()),
477        );
478        assert_eq!(
479            out,
480            Type::Tensor {
481                shape: Some(vec![Some(2), Some(3)])
482            }
483        );
484    }
485
486    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
487    #[test]
488    fn db_default_voltage_scalar() {
489        assert_num_close(db_builtin(Value::Num(10.0), Vec::new()).expect("db"), 20.0);
490    }
491
492    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
493    #[test]
494    fn db_voltage_mode_matches_default() {
495        let result = db_builtin(
496            Value::Num(10.0),
497            vec![Value::CharArray(CharArray::new_row("voltage"))],
498        )
499        .expect("db");
500        assert_num_close(result, 20.0);
501    }
502
503    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
504    #[test]
505    fn db_power_mode_scalar() {
506        let result = db_builtin(
507            Value::Num(100.0),
508            vec![Value::CharArray(CharArray::new_row("power"))],
509        )
510        .expect("db");
511        assert_num_close(result, 20.0);
512    }
513
514    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
515    #[test]
516    fn db_negative_input_uses_magnitude() {
517        assert_num_close(db_builtin(Value::Num(-10.0), Vec::new()).expect("db"), 20.0);
518    }
519
520    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
521    #[test]
522    fn db_zero_input_returns_negative_infinity() {
523        match db_builtin(Value::Num(0.0), Vec::new()).expect("db") {
524            Value::Num(value) => assert_eq!(value, f64::NEG_INFINITY),
525            other => panic!("expected scalar result, got {other:?}"),
526        }
527    }
528
529    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
530    #[test]
531    fn db_complex_scalar_uses_magnitude() {
532        assert_num_close(
533            db_builtin(Value::Complex(3.0, 4.0), Vec::new()).expect("db"),
534            20.0 * 5.0f64.log10(),
535        );
536    }
537
538    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
539    #[test]
540    fn db_tensor_elements() {
541        let tensor = Tensor::new(vec![1.0, 10.0, 100.0], vec![1, 3]).unwrap();
542        let result = db_builtin(Value::Tensor(tensor), Vec::new()).expect("db");
543        assert_tensor_close(result, &[1, 3], &[0.0, 20.0, 40.0]);
544    }
545
546    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
547    #[test]
548    fn db_complex_tensor_returns_real_tensor() {
549        let tensor = ComplexTensor::new(vec![(3.0, 4.0), (0.0, -10.0)], vec![2, 1]).unwrap();
550        let result = db_builtin(Value::ComplexTensor(tensor), Vec::new()).expect("db");
551        assert_tensor_close(result, &[2, 1], &[20.0 * 5.0f64.log10(), 20.0]);
552    }
553
554    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
555    #[test]
556    fn db_resistance_scalar() {
557        let result = db_builtin(Value::Num(10.0), vec![Value::Num(50.0)]).expect("db");
558        assert_num_close(result, 10.0 * (2.0f64).log10());
559    }
560
561    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
562    #[test]
563    fn db_resistance_broadcasts() {
564        let y = Tensor::new(vec![10.0, 20.0], vec![2, 1]).unwrap();
565        let r = Tensor::new(vec![50.0, 100.0, 200.0], vec![1, 3]).unwrap();
566        let result = db_builtin(Value::Tensor(y), vec![Value::Tensor(r)]).expect("db");
567        assert_tensor_close(
568            result,
569            &[2, 3],
570            &[
571                10.0 * (100.0f64 / 50.0).log10(),
572                10.0 * (400.0f64 / 50.0).log10(),
573                10.0 * (100.0f64 / 100.0).log10(),
574                10.0 * (400.0f64 / 100.0).log10(),
575                10.0 * (100.0f64 / 200.0).log10(),
576                10.0 * (400.0f64 / 200.0).log10(),
577            ],
578        );
579    }
580
581    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
582    #[test]
583    fn db_logical_and_integer_inputs_promote_to_double() {
584        let logical = LogicalArray::new(vec![1, 0], vec![1, 2]).unwrap();
585        let result = db_builtin(Value::LogicalArray(logical), Vec::new()).expect("db");
586        assert_tensor_close(result, &[1, 2], &[0.0, f64::NEG_INFINITY]);
587
588        let result = db_builtin(Value::Int(IntValue::I32(10)), Vec::new()).expect("db");
589        assert_num_close(result, 20.0);
590    }
591
592    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
593    #[test]
594    fn db_rejects_invalid_mode() {
595        let err = db_builtin(
596            Value::Num(1.0),
597            vec![Value::CharArray(CharArray::new_row("energy"))],
598        )
599        .expect_err("invalid mode");
600        assert!(err.message().contains("unknown mode"));
601        assert_eq!(err.identifier(), DB_ERROR_INVALID_MODE.identifier);
602    }
603
604    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
605    #[test]
606    fn db_rejects_nonpositive_resistance() {
607        let err =
608            db_builtin(Value::Num(1.0), vec![Value::Num(0.0)]).expect_err("invalid resistance");
609        assert!(err.message().contains("finite and positive"));
610        assert_eq!(err.identifier(), DB_ERROR_INVALID_RESISTANCE.identifier);
611    }
612
613    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
614    #[test]
615    fn db_rejects_nonnumeric_input() {
616        let err = db_builtin(Value::from("hello"), Vec::new()).expect_err("invalid input");
617        assert!(err.message().contains("expected numeric"));
618        assert_eq!(err.identifier(), DB_ERROR_INVALID_INPUT.identifier);
619    }
620
621    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
622    #[test]
623    fn db_gpu_input_gathers_to_host() {
624        test_support::with_test_provider(|provider| {
625            let tensor = Tensor::new(vec![1.0, 10.0, 100.0], vec![1, 3]).unwrap();
626            let view = runmat_accelerate_api::HostTensorView {
627                data: &tensor.data,
628                shape: &tensor.shape,
629            };
630            let handle = provider.upload(&view).expect("upload");
631            let result = db_builtin(Value::GpuTensor(handle), Vec::new()).expect("db");
632            assert_tensor_close(result, &[1, 3], &[0.0, 20.0, 40.0]);
633        });
634    }
635}