1use runmat_builtins::{
4 BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinExtensionDescriptor,
5 BuiltinExtensionMode, BuiltinIntegerBackendRule, BuiltinIntegerCapabilityDescriptor,
6 BuiltinIntegerComputationDomain, BuiltinIntegerInputAvailability,
7 BuiltinIntegerInputCapability, BuiltinIntegerOutputClassRule, BuiltinIntegerOverflowRule,
8 BuiltinIntegerOverloadKind, BuiltinIntegerScalarDoubleRule, BuiltinOutputMode,
9 BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
10};
11use runmat_macros::runtime_builtin;
12use runmat_value::Value;
13
14use crate::builtins::common::spec::{
15 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
16 ReductionNaN, ResidencyPolicy, ShapeRequirements,
17};
18use crate::builtins::control::tf_model::{scalar_f64, two_models_ordered};
19use crate::builtins::control::type_resolvers::feedback_type;
20use crate::{BuiltinResult, RuntimeError};
21
22const BUILTIN_NAME: &str = "feedback";
23
24const SINGLE_SYSTEM_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
25 id: "feedback-single-system",
26 mode: BuiltinExtensionMode::RunMatOnly,
27 description: "feedback(sys1) with an implicit unity feedback path is a RunMat extension",
28 error_identifier: Some("RunMat:compatibility:FeedbackSingleSystemExtension"),
29};
30const SCALAR_FORWARD_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
31 id: "feedback-scalar-forward-system",
32 mode: BuiltinExtensionMode::RunMatOnly,
33 description: "feedback with a scalar forward-path system is a RunMat extension",
34 error_identifier: Some("RunMat:compatibility:FeedbackScalarForwardSystemExtension"),
35};
36const SCALAR_SYSTEMS_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
37 id: "feedback-scalar-scalar-systems",
38 mode: BuiltinExtensionMode::RunMatOnly,
39 description: "feedback with scalar forward and feedback-path systems is a RunMat extension",
40 error_identifier: Some("RunMat:compatibility:FeedbackScalarScalarSystemsExtension"),
41};
42const INTEGER_GAIN_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
43 id: "feedback-integer-scalar-gain",
44 mode: BuiltinExtensionMode::RunMatOnly,
45 description: "feedback with a typed-integer scalar system gain is a RunMat extension",
46 error_identifier: Some("RunMat:compatibility:FeedbackIntegerScalarGainExtension"),
47};
48const INTEGER_SIGN_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
49 id: "feedback-integer-sign",
50 mode: BuiltinExtensionMode::RunMatOnly,
51 description: "feedback with a typed-integer sign is a RunMat extension",
52 error_identifier: Some("RunMat:compatibility:FeedbackIntegerSignExtension"),
53};
54const LOGICAL_NUMERIC_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
55 id: "feedback-logical-numeric-input",
56 mode: BuiltinExtensionMode::RunMatOnly,
57 description: "feedback with a logical numeric system or sign is a RunMat extension",
58 error_identifier: Some("RunMat:compatibility:FeedbackLogicalNumericInputExtension"),
59};
60const RESIDENT_NUMERIC_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
61 id: "feedback-resident-numeric-input",
62 mode: BuiltinExtensionMode::RunMatOnly,
63 description: "feedback with a resident numeric system or sign is a RunMat extension",
64 error_identifier: Some("RunMat:compatibility:FeedbackResidentNumericInputExtension"),
65};
66const SINGLE_NUMERIC_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
67 id: "feedback-single-numeric-input",
68 mode: BuiltinExtensionMode::RunMatOnly,
69 description: "feedback with a native-single numeric system or sign is a RunMat extension",
70 error_identifier: Some("RunMat:compatibility:FeedbackSingleNumericInputExtension"),
71};
72
73pub const EXTENSIONS: [BuiltinExtensionDescriptor; 8] = [
74 SINGLE_SYSTEM_EXTENSION,
75 SCALAR_FORWARD_EXTENSION,
76 SCALAR_SYSTEMS_EXTENSION,
77 INTEGER_GAIN_EXTENSION,
78 INTEGER_SIGN_EXTENSION,
79 LOGICAL_NUMERIC_EXTENSION,
80 RESIDENT_NUMERIC_EXTENSION,
81 SINGLE_NUMERIC_EXTENSION,
82];
83
84const INTEGER_GAIN_INPUTS: [BuiltinIntegerInputCapability; 2] = [
85 BuiltinIntegerInputCapability {
86 name: "sys1",
87 classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
88 availability: BuiltinIntegerInputAvailability::RunMatOnly,
89 scalar_double: BuiltinIntegerScalarDoubleRule::Allowed,
90 notes: "A typed-integer forward gain is independently gated, then converted once to the floating/complex tf coefficient domain.",
91 },
92 BuiltinIntegerInputCapability {
93 name: "sys2",
94 classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
95 availability: BuiltinIntegerInputAvailability::RunMatOnly,
96 scalar_double: BuiltinIntegerScalarDoubleRule::Allowed,
97 notes: "A typed-integer feedback-path gain is independently gated, then converted once to the floating/complex tf coefficient domain.",
98 },
99];
100const INTEGER_SIGN_INPUTS: [BuiltinIntegerInputCapability; 1] =
101 [BuiltinIntegerInputCapability {
102 name: "sign",
103 classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
104 availability: BuiltinIntegerInputAvailability::RunMatOnly,
105 scalar_double: BuiltinIntegerScalarDoubleRule::Allowed,
106 notes: "The typed-integer sign is independently gated and must be exactly -1 or +1; unsigned classes can express only +1.",
107 }];
108
109pub const INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 2] = [
110 BuiltinIntegerCapabilityDescriptor {
111 form: "sys = feedback(integer_gain, sys2) or feedback(sys1, integer_gain)",
112 inputs: &INTEGER_GAIN_INPUTS,
113 computation_domain: BuiltinIntegerComputationDomain::FloatingPoint,
114 output_class: BuiltinIntegerOutputClassRule::NotApplicable,
115 overflow: BuiltinIntegerOverflowRule::NotApplicable,
116 backend: BuiltinIntegerBackendRule::GatherFallback,
117 overload: BuiltinIntegerOverloadKind::ScalarOnly,
118 notes: "All eight typed integer classes are RunMat-only scalar gains. The result is a host tf object; int64 and uint64 values outside binary64's exact integer range can round at the explicit floating boundary.",
119 },
120 BuiltinIntegerCapabilityDescriptor {
121 form: "sys = feedback(sys1, sys2, integer_sign)",
122 inputs: &INTEGER_SIGN_INPUTS,
123 computation_domain: BuiltinIntegerComputationDomain::Structural,
124 output_class: BuiltinIntegerOutputClassRule::NotApplicable,
125 overflow: BuiltinIntegerOverflowRule::NotApplicable,
126 backend: BuiltinIntegerBackendRule::GatherFallback,
127 overload: BuiltinIntegerOverloadKind::StructuralParameter,
128 notes: "All eight typed integer classes are admitted as a RunMat-only sign control, but the exact valid set remains -1 or +1. The result is a host tf object.",
129 },
130];
131
132const FEEDBACK_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
133 name: "sys",
134 ty: BuiltinParamType::Any,
135 arity: BuiltinParamArity::Required,
136 default: None,
137 description: "Closed-loop SISO transfer-function object.",
138}];
139const FEEDBACK_PARAM_SYS1: BuiltinParamDescriptor = BuiltinParamDescriptor {
140 name: "sys1",
141 ty: BuiltinParamType::Any,
142 arity: BuiltinParamArity::Required,
143 default: None,
144 description: "Forward-path SISO transfer-function model.",
145};
146const FEEDBACK_PARAM_SYS2: BuiltinParamDescriptor = BuiltinParamDescriptor {
147 name: "sys2",
148 ty: BuiltinParamType::Any,
149 arity: BuiltinParamArity::Optional,
150 default: Some("1"),
151 description: "Feedback-path SISO transfer function or scalar gain.",
152};
153const FEEDBACK_PARAM_SIGN: BuiltinParamDescriptor = BuiltinParamDescriptor {
154 name: "sign",
155 ty: BuiltinParamType::NumericScalar,
156 arity: BuiltinParamArity::Optional,
157 default: Some("-1"),
158 description: "-1 for negative feedback or +1 for positive feedback.",
159};
160const FEEDBACK_INPUT_SYS: [BuiltinParamDescriptor; 1] = [FEEDBACK_PARAM_SYS1];
161const FEEDBACK_INPUT_SYS_OTHER: [BuiltinParamDescriptor; 2] =
162 [FEEDBACK_PARAM_SYS1, FEEDBACK_PARAM_SYS2];
163const FEEDBACK_INPUT_SYS_OTHER_SIGN: [BuiltinParamDescriptor; 3] = [
164 FEEDBACK_PARAM_SYS1,
165 FEEDBACK_PARAM_SYS2,
166 FEEDBACK_PARAM_SIGN,
167];
168const FEEDBACK_SIGNATURES: [BuiltinSignatureDescriptor; 3] = [
169 BuiltinSignatureDescriptor {
170 label: "sys = feedback(sys1)",
171 inputs: &FEEDBACK_INPUT_SYS,
172 outputs: &FEEDBACK_OUTPUT,
173 },
174 BuiltinSignatureDescriptor {
175 label: "sys = feedback(sys1, sys2)",
176 inputs: &FEEDBACK_INPUT_SYS_OTHER,
177 outputs: &FEEDBACK_OUTPUT,
178 },
179 BuiltinSignatureDescriptor {
180 label: "sys = feedback(sys1, sys2, sign)",
181 inputs: &FEEDBACK_INPUT_SYS_OTHER_SIGN,
182 outputs: &FEEDBACK_OUTPUT,
183 },
184];
185const FEEDBACK_ERRORS: [BuiltinErrorDescriptor; 5] = [
186 BuiltinErrorDescriptor {
187 code: "RM.FEEDBACK.INVALID_ARGUMENT",
188 identifier: Some("RunMat:feedback:InvalidArgument"),
189 when: "Inputs do not match supported feedback invocation forms.",
190 message: "feedback: invalid argument",
191 },
192 BuiltinErrorDescriptor {
193 code: "RM.FEEDBACK.INVALID_MODEL",
194 identifier: Some("RunMat:feedback:InvalidModel"),
195 when: "Input systems are not supported SISO transfer-function models.",
196 message: "feedback: invalid model",
197 },
198 BuiltinErrorDescriptor {
199 code: "RM.FEEDBACK.INVALID_SIGN",
200 identifier: Some("RunMat:feedback:InvalidSign"),
201 when: "Feedback sign is not -1 or +1.",
202 message: "feedback: sign must be -1 or +1",
203 },
204 BuiltinErrorDescriptor {
205 code: "RM.FEEDBACK.UNSUPPORTED_MODEL",
206 identifier: Some("RunMat:feedback:UnsupportedModel"),
207 when: "A supported-looking model uses unsupported delays or incompatible sample times.",
208 message: "feedback: unsupported model",
209 },
210 BuiltinErrorDescriptor {
211 code: "RM.FEEDBACK.INTERNAL",
212 identifier: Some("RunMat:feedback:Internal"),
213 when: "Closed-loop transfer-function assembly failed.",
214 message: "feedback: internal error",
215 },
216];
217pub const FEEDBACK_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
218 signatures: &FEEDBACK_SIGNATURES,
219 output_mode: BuiltinOutputMode::Fixed,
220 completion_policy: BuiltinCompletionPolicy::Public,
221 errors: &FEEDBACK_ERRORS,
222};
223
224#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::control::feedback")]
225pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
226 name: "feedback",
227 op_kind: GpuOpKind::Custom("control-feedback-interconnection"),
228 supported_precisions: &[],
229 broadcast: BroadcastSemantics::None,
230 provider_hooks: &[],
231 constant_strategy: ConstantStrategy::InlineLiteral,
232 residency: ResidencyPolicy::GatherImmediately,
233 nan_mode: ReductionNaN::Include,
234 two_pass_threshold: None,
235 workgroup_size: None,
236 accepts_nan_mode: false,
237 notes: "SISO transfer-function interconnection runs on host-side metadata.",
238};
239
240#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::control::feedback")]
241pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
242 name: "feedback",
243 shape: ShapeRequirements::Any,
244 constant_strategy: ConstantStrategy::InlineLiteral,
245 elementwise: None,
246 reduction: None,
247 emits_nan: false,
248 notes: "feedback creates a transfer-function object and terminates numeric fusion chains.",
249};
250
251fn feedback_error(
252 message: impl Into<String>,
253 error: &'static BuiltinErrorDescriptor,
254) -> RuntimeError {
255 let mut builder = crate::build_runtime_error(message).with_builtin(BUILTIN_NAME);
256 if let Some(identifier) = error.identifier {
257 builder = builder.with_identifier(identifier);
258 }
259 builder.build()
260}
261
262#[runtime_builtin(
263 name = "feedback",
264 category = "control",
265 summary = "Form SISO feedback interconnections for transfer-function models.",
266 keywords = "feedback,control system,closed loop,transfer function,tf",
267 type_resolver(feedback_type),
268 descriptor(crate::builtins::control::feedback::FEEDBACK_DESCRIPTOR),
269 extensions(crate::builtins::control::feedback::EXTENSIONS),
270 integer_capabilities(crate::builtins::control::feedback::INTEGER_CAPABILITIES),
271 builtin_path = "crate::builtins::control::feedback"
272)]
273async fn feedback_builtin(sys1: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
274 if rest.len() > 2 {
275 return Err(feedback_error(
276 "feedback: expected feedback(sys1), feedback(sys1, sys2), or feedback(sys1, sys2, sign)",
277 &FEEDBACK_ERRORS[0],
278 ));
279 }
280 ensure_extensions_enabled(&sys1, &rest)?;
281 let sys2 = rest.first().cloned().unwrap_or(Value::Num(1.0));
282 let sign = match rest.get(1) {
283 Some(value) => {
284 let gathered = crate::dispatcher::gather_if_needed_async(value).await?;
285 scalar_f64(&gathered, "sign", BUILTIN_NAME)?
286 }
287 None => -1.0,
288 };
289 if sign != -1.0 && sign != 1.0 {
290 return Err(feedback_error(
291 "feedback: sign must be -1 or +1",
292 &FEEDBACK_ERRORS[2],
293 ));
294 }
295
296 let (forward, feedback_path) = two_models_ordered(sys1, sys2, BUILTIN_NAME).await?;
297 forward
298 .feedback(&feedback_path, sign)?
299 .to_value(BUILTIN_NAME)
300}
301
302fn ensure_extensions_enabled(sys1: &Value, rest: &[Value]) -> BuiltinResult<()> {
303 if rest.is_empty() {
304 ensure_extension(&SINGLE_SYSTEM_EXTENSION)?;
305 }
306
307 let sys2 = rest.first();
308 let sys1_is_scalar = is_scalar_system_value(sys1);
309 if sys1_is_scalar && sys2.is_some_and(is_scalar_system_value) {
310 ensure_extension(&SCALAR_SYSTEMS_EXTENSION)?;
311 }
312 if sys1_is_scalar {
313 ensure_extension(&SCALAR_FORWARD_EXTENSION)?;
314 }
315
316 if is_typed_integer_value(sys1) || sys2.is_some_and(is_typed_integer_value) {
317 ensure_extension(&INTEGER_GAIN_EXTENSION)?;
318 }
319 if rest.get(1).is_some_and(is_typed_integer_value) {
320 ensure_extension(&INTEGER_SIGN_EXTENSION)?;
321 }
322 if is_logical_value(sys1) || rest.iter().any(is_logical_value) {
323 ensure_extension(&LOGICAL_NUMERIC_EXTENSION)?;
324 }
325 if is_resident_value(sys1) || rest.iter().any(is_resident_value) {
326 ensure_extension(&RESIDENT_NUMERIC_EXTENSION)?;
327 }
328 if is_single_numeric_value(sys1) || rest.iter().any(is_single_numeric_value) {
329 ensure_extension(&SINGLE_NUMERIC_EXTENSION)?;
330 }
331 Ok(())
332}
333
334fn ensure_extension(extension: &'static BuiltinExtensionDescriptor) -> BuiltinResult<()> {
335 crate::compatibility::ensure_builtin_extension_enabled(extension, BUILTIN_NAME)
336}
337
338fn is_scalar_system_value(value: &Value) -> bool {
339 match value {
340 Value::Num(_) | Value::Int(_) | Value::Bool(_) | Value::Complex(_, _) => true,
341 Value::Tensor(tensor) => tensor.len() == 1,
342 Value::ComplexTensor(tensor) => tensor.len() == 1,
343 Value::LogicalArray(logical) => logical.data.len() == 1,
344 Value::GpuTensor(handle) => handle.shape.iter().product::<usize>() == 1,
345 _ => false,
346 }
347}
348
349fn is_typed_integer_value(value: &Value) -> bool {
350 matches!(value, Value::Int(_))
351 || matches!(value, Value::Tensor(tensor) if tensor.integer_storage().is_some())
352 || matches!(value, Value::ComplexTensor(tensor) if tensor.integer_storage().is_some())
353 || matches!(value, Value::GpuTensor(handle) if runmat_accelerate_api::handle_integer_type(handle).is_some())
354}
355
356fn is_single_numeric_value(value: &Value) -> bool {
357 matches!(value, Value::Tensor(tensor) if tensor.numeric_dtype() == runmat_value::NumericDType::F32)
358 || matches!(value, Value::ComplexTensor(tensor) if tensor.numeric_dtype() == runmat_value::NumericDType::F32)
359}
360
361fn is_logical_value(value: &Value) -> bool {
362 matches!(value, Value::Bool(_) | Value::LogicalArray(_))
363 || matches!(value, Value::GpuTensor(handle) if runmat_accelerate_api::handle_is_logical(handle))
364}
365
366fn is_resident_value(value: &Value) -> bool {
367 matches!(value, Value::GpuTensor(_))
368}
369
370#[cfg(test)]
371mod tests {
372 use super::*;
373 use futures::executor::block_on;
374 use runmat_value::{
375 ComplexTensor, IntValue, IntegerComplexStorage, IntegerStorage, LogicalArray, NumericDType,
376 Tensor,
377 };
378
379 fn tf(num: Vec<f64>, den: Vec<f64>) -> Value {
380 block_on(crate::call_builtin_async(
381 "tf",
382 &[
383 Value::Tensor(Tensor::new(num.clone(), vec![1, num.len()]).unwrap()),
384 Value::Tensor(Tensor::new(den.clone(), vec![1, den.len()]).unwrap()),
385 ],
386 ))
387 .expect("tf")
388 }
389
390 fn coeff(value: &Value, field: &str) -> Vec<f64> {
391 let Value::Object(object) = value else {
392 panic!("expected tf object");
393 };
394 let Value::Tensor(tensor) = object.properties.get(field).expect(field) else {
395 panic!("expected tensor");
396 };
397 tensor.materialize_f64().clone()
398 }
399
400 #[test]
401 fn unity_negative_feedback_forms_closed_loop() {
402 let g = tf(vec![2.0], vec![1.0, 3.0]);
403 let out = block_on(feedback_builtin(g, vec![Value::Num(1.0)])).expect("feedback");
404 assert_eq!(coeff(&out, "Numerator"), vec![2.0]);
405 assert_eq!(coeff(&out, "Denominator"), vec![1.0, 5.0]);
406 }
407
408 #[test]
409 fn positive_feedback_subtracts_loop_gain() {
410 let g = tf(vec![2.0], vec![1.0, 3.0]);
411 let out = block_on(feedback_builtin(g, vec![Value::Num(1.0), Value::Num(1.0)]))
412 .expect("feedback");
413 assert_eq!(coeff(&out, "Numerator"), vec![2.0]);
414 assert_eq!(coeff(&out, "Denominator"), vec![1.0, 1.0]);
415 }
416
417 #[test]
418 fn integer_metadata_covers_gain_and_sign_for_all_classes() {
419 assert_eq!(INTEGER_CAPABILITIES.len(), 2);
420 assert_eq!(INTEGER_CAPABILITIES[0].inputs.len(), 2);
421 assert_eq!(INTEGER_CAPABILITIES[0].inputs[0].classes.len(), 8);
422 assert_eq!(INTEGER_CAPABILITIES[0].inputs[1].classes.len(), 8);
423 assert_eq!(INTEGER_CAPABILITIES[1].inputs[0].classes.len(), 8);
424 assert_eq!(
425 INTEGER_CAPABILITIES[0].computation_domain,
426 BuiltinIntegerComputationDomain::FloatingPoint
427 );
428 assert_eq!(
429 INTEGER_CAPABILITIES[1].computation_domain,
430 BuiltinIntegerComputationDomain::Structural
431 );
432 }
433
434 #[test]
435 fn integer_gain_and_sign_support_all_eight_classes_in_runmat_mode() {
436 let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
437 let gains = [
438 IntValue::I8(1),
439 IntValue::I16(1),
440 IntValue::I32(1),
441 IntValue::I64(1),
442 IntValue::U8(1),
443 IntValue::U16(1),
444 IntValue::U32(1),
445 IntValue::U64(1),
446 ];
447 for gain in gains {
448 let out = block_on(feedback_builtin(
449 tf(vec![2.0], vec![1.0, 3.0]),
450 vec![Value::Int(gain.clone()), Value::Int(gain)],
451 ))
452 .expect("typed-integer gain and sign");
453 assert_eq!(coeff(&out, "Denominator"), vec![1.0, 1.0]);
454 }
455 }
456
457 #[test]
458 fn integer_tensor_gain_uses_explicit_binary64_boundary() {
459 let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
460 let wide = (1_u64 << 53) + 1;
461 let gain = Tensor::new_integer(IntegerStorage::U64(vec![wide]), vec![1, 1])
462 .expect("integer scalar tensor");
463 let out = block_on(feedback_builtin(
464 tf(vec![1.0], vec![1.0]),
465 vec![Value::Tensor(gain)],
466 ))
467 .expect("wide integer gain");
468 assert_eq!(coeff(&out, "Denominator"), vec![(wide as f64) + 1.0]);
469 }
470
471 #[test]
472 fn integer_sign_keeps_the_exact_plus_or_minus_one_contract() {
473 let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
474 let negative = block_on(feedback_builtin(
475 tf(vec![2.0], vec![1.0, 3.0]),
476 vec![Value::Num(1.0), Value::Int(IntValue::I64(-1))],
477 ))
478 .expect("negative integer sign");
479 assert_eq!(coeff(&negative, "Denominator"), vec![1.0, 5.0]);
480
481 let error = block_on(feedback_builtin(
482 tf(vec![2.0], vec![1.0, 3.0]),
483 vec![Value::Num(1.0), Value::Int(IntValue::U64(0))],
484 ))
485 .expect_err("zero is not a feedback sign");
486 assert_eq!(error.identifier(), FEEDBACK_ERRORS[2].identifier);
487 }
488
489 #[test]
490 fn logical_numeric_inputs_are_a_separate_runmat_extension() {
491 let g = tf(vec![2.0], vec![1.0, 3.0]);
492 {
493 let _compat = crate::compatibility::push_runmat_extensions_enabled(false);
494 let error = block_on(feedback_builtin(g.clone(), vec![Value::Bool(true)]))
495 .expect_err("strict mode rejects logical gain");
496 assert_eq!(
497 error.identifier(),
498 LOGICAL_NUMERIC_EXTENSION.error_identifier
499 );
500 }
501 {
502 let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
503 let logical = LogicalArray::new(vec![1], vec![1, 1]).expect("logical scalar");
504 let out = block_on(feedback_builtin(g, vec![Value::LogicalArray(logical)]))
505 .expect("logical gain");
506 assert_eq!(coeff(&out, "Denominator"), vec![1.0, 5.0]);
507 }
508 }
509
510 #[test]
511 fn runmat_only_forms_have_stable_independent_guards() {
512 let _compat = crate::compatibility::push_runmat_extensions_enabled(false);
513
514 let documented = block_on(feedback_builtin(
515 tf(vec![2.0], vec![1.0, 3.0]),
516 vec![Value::Num(1.0), Value::Num(-1.0)],
517 ))
518 .expect("tf plus ordinary double gain and sign remains admitted");
519 assert_eq!(coeff(&documented, "Denominator"), vec![1.0, 5.0]);
520
521 let error = block_on(feedback_builtin(tf(vec![1.0], vec![1.0]), vec![]))
522 .expect_err("one-argument form");
523 assert_eq!(error.identifier(), SINGLE_SYSTEM_EXTENSION.error_identifier);
524
525 let error = block_on(feedback_builtin(
526 Value::Num(1.0),
527 vec![tf(vec![1.0], vec![1.0])],
528 ))
529 .expect_err("scalar forward form");
530 assert_eq!(
531 error.identifier(),
532 SCALAR_FORWARD_EXTENSION.error_identifier
533 );
534
535 let error = block_on(feedback_builtin(Value::Num(1.0), vec![Value::Num(1.0)]))
536 .expect_err("scalar-scalar form");
537 assert_eq!(
538 error.identifier(),
539 SCALAR_SYSTEMS_EXTENSION.error_identifier
540 );
541
542 let error = block_on(feedback_builtin(
543 tf(vec![1.0], vec![1.0]),
544 vec![Value::Int(IntValue::I16(1))],
545 ))
546 .expect_err("integer gain");
547 assert_eq!(error.identifier(), INTEGER_GAIN_EXTENSION.error_identifier);
548
549 let error = block_on(feedback_builtin(
550 tf(vec![1.0], vec![1.0]),
551 vec![Value::Num(1.0), Value::Int(IntValue::I8(-1))],
552 ))
553 .expect_err("integer sign");
554 assert_eq!(error.identifier(), INTEGER_SIGN_EXTENSION.error_identifier);
555
556 let complex_integer = ComplexTensor::new_integer(
557 IntegerComplexStorage::new(IntegerStorage::I16(vec![1]), IntegerStorage::I16(vec![0]))
558 .expect("paired integer storage"),
559 vec![1, 1],
560 )
561 .expect("complex integer scalar");
562 let error = block_on(feedback_builtin(
563 tf(vec![1.0], vec![1.0]),
564 vec![Value::ComplexTensor(complex_integer)],
565 ))
566 .expect_err("complex integer gain");
567 assert_eq!(error.identifier(), INTEGER_GAIN_EXTENSION.error_identifier);
568
569 let single = Tensor::new_with_dtype(vec![1.0], vec![1, 1], NumericDType::F32)
570 .expect("single scalar");
571 let error = block_on(feedback_builtin(
572 tf(vec![1.0], vec![1.0]),
573 vec![Value::Tensor(single)],
574 ))
575 .expect_err("single gain");
576 assert_eq!(
577 error.identifier(),
578 SINGLE_NUMERIC_EXTENSION.error_identifier
579 );
580 }
581
582 #[test]
583 fn resident_gate_runs_before_provider_access() {
584 let _compat = crate::compatibility::push_runmat_extensions_enabled(false);
585 let resident = Value::GpuTensor(runmat_accelerate_api::GpuTensorHandle {
586 shape: vec![1, 1],
587 device_id: 0,
588 buffer_id: 9_404_001,
589 descriptor: Default::default(),
590 });
591 let error = block_on(feedback_builtin(tf(vec![1.0], vec![1.0]), vec![resident]))
592 .expect_err("resident gate");
593 assert_eq!(
594 error.identifier(),
595 RESIDENT_NUMERIC_EXTENSION.error_identifier
596 );
597 }
598}