1use 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}