runmat_runtime/builtins/control/
zero.rs1use runmat_builtins::{
4 BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
5 BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
6};
7use runmat_builtins::{BuiltinIntegerAuditDescriptor, BuiltinIntegerAuditKind};
8use runmat_macros::runtime_builtin;
9use runmat_value::Value;
10
11use crate::builtins::common::spec::{
12 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
13 ReductionNaN, ResidencyPolicy, ShapeRequirements,
14};
15use crate::builtins::control::tf_model::{output_complex_column, TfModel};
16use crate::builtins::control::type_resolvers::zero_type;
17use crate::BuiltinResult;
18
19const ZERO_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
20 name: "z",
21 ty: BuiltinParamType::Any,
22 arity: BuiltinParamArity::Required,
23 default: None,
24 description: "Zeros of the SISO tf model as a column vector.",
25}];
26const ZERO_INPUTS: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
27 name: "sys",
28 ty: BuiltinParamType::Any,
29 arity: BuiltinParamArity::Required,
30 default: None,
31 description: "SISO tf model.",
32}];
33const ZERO_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
34 label: "z = zero(sys)",
35 inputs: &ZERO_INPUTS,
36 outputs: &ZERO_OUTPUT,
37}];
38const ZERO_ERRORS: [BuiltinErrorDescriptor; 4] = [
39 BuiltinErrorDescriptor {
40 code: "RM.ZERO.INVALID_MODEL",
41 identifier: Some("RunMat:zero:InvalidModel"),
42 when: "Input system is not a valid SISO tf object.",
43 message: "zero: invalid model",
44 },
45 BuiltinErrorDescriptor {
46 code: "RM.ZERO.UNSUPPORTED_MODEL",
47 identifier: Some("RunMat:zero:UnsupportedModel"),
48 when: "Model form is unsupported.",
49 message: "zero: unsupported model",
50 },
51 BuiltinErrorDescriptor {
52 code: "RM.ZERO.INVALID_ARGUMENT",
53 identifier: Some("RunMat:zero:InvalidArgument"),
54 when: "Model metadata or arguments are malformed.",
55 message: "zero: invalid argument",
56 },
57 BuiltinErrorDescriptor {
58 code: "RM.ZERO.INTERNAL",
59 identifier: Some("RunMat:zero:Internal"),
60 when: "Root calculation or output construction failed.",
61 message: "zero: internal error",
62 },
63];
64pub const ZERO_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
65 signatures: &ZERO_SIGNATURES,
66 output_mode: BuiltinOutputMode::Fixed,
67 completion_policy: BuiltinCompletionPolicy::Public,
68 errors: &ZERO_ERRORS,
69};
70pub const ZERO_INTEGER_AUDIT: BuiltinIntegerAuditDescriptor = BuiltinIntegerAuditDescriptor {
71 kind: BuiltinIntegerAuditKind::NotApplicable,
72 canonical_builtin: None,
73 notes: "zero accepts a dynamic-system model; fundamental integer values are invalid model inputs and reject before model payload or provider access.",
74};
75
76#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::control::zero")]
77pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
78 name: "zero",
79 op_kind: GpuOpKind::Custom("control-zeros"),
80 supported_precisions: &[],
81 broadcast: BroadcastSemantics::None,
82 provider_hooks: &[],
83 constant_strategy: ConstantStrategy::InlineLiteral,
84 residency: ResidencyPolicy::GatherImmediately,
85 nan_mode: ReductionNaN::Include,
86 two_pass_threshold: None,
87 workgroup_size: None,
88 accepts_nan_mode: false,
89 notes: "zero computes roots from host-side transfer-function metadata.",
90};
91
92#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::control::zero")]
93pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
94 name: "zero",
95 shape: ShapeRequirements::Any,
96 constant_strategy: ConstantStrategy::InlineLiteral,
97 elementwise: None,
98 reduction: None,
99 emits_nan: false,
100 notes: "zero is model analysis and is not fused.",
101};
102
103#[runtime_builtin(
104 name = "zero",
105 category = "control",
106 summary = "Return zeros of SISO transfer-function models.",
107 keywords = "zero,zeros,control system,transfer function,tf",
108 type_resolver(zero_type),
109 descriptor(crate::builtins::control::zero::ZERO_DESCRIPTOR),
110 integer_audit(crate::builtins::control::zero::ZERO_INTEGER_AUDIT),
111 builtin_path = "crate::builtins::control::zero"
112)]
113async fn zero_builtin(sys: Value) -> BuiltinResult<Value> {
114 if crate::builtins::common::validation::value_has_native_integer_class(&sys) {
115 return Err(crate::build_runtime_error(
116 "zero: input must be a dynamic-system model, not an integer value",
117 )
118 .with_builtin("zero")
119 .with_identifier(
120 ZERO_ERRORS[0]
121 .identifier
122 .expect("zero invalid-model descriptor identifier"),
123 )
124 .build()
125 .into());
126 }
127 let model = TfModel::from_value_async(sys, "zero").await?;
128 output_complex_column(model.zeros()?, "zero")
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use futures::executor::block_on;
135 use runmat_value::Tensor;
136
137 #[test]
138 fn zero_returns_roots_of_numerator() {
139 let sys = block_on(crate::call_builtin_async(
140 "tf",
141 &[
142 Value::Tensor(Tensor::new(vec![1.0, 3.0, 2.0], vec![1, 3]).unwrap()),
143 Value::Tensor(Tensor::new(vec![1.0, 4.0], vec![1, 2]).unwrap()),
144 ],
145 ))
146 .expect("tf");
147 let Value::Tensor(zeros) = block_on(zero_builtin(sys)).expect("zero") else {
148 panic!("expected real zeros");
149 };
150 assert_eq!(zeros.shape, vec![2, 1]);
151 assert!(zeros
152 .materialize_f64()
153 .iter()
154 .any(|z| (*z + 1.0).abs() < 1.0e-8));
155 assert!(zeros
156 .materialize_f64()
157 .iter()
158 .any(|z| (*z + 2.0).abs() < 1.0e-8));
159 }
160
161 #[test]
162 fn zero_returns_complex_conjugate_roots() {
163 let sys = block_on(crate::call_builtin_async(
164 "tf",
165 &[
166 Value::Tensor(Tensor::new(vec![1.0, 0.0, 1.0], vec![1, 3]).unwrap()),
167 Value::Tensor(Tensor::new(vec![1.0, 1.0], vec![1, 2]).unwrap()),
168 ],
169 ))
170 .expect("tf");
171 let Value::ComplexTensor(zeros) = block_on(zero_builtin(sys)).expect("zero") else {
172 panic!("expected complex zeros");
173 };
174 assert_eq!(zeros.shape, vec![2, 1]);
175 assert!(zeros
176 .materialize_f64()
177 .iter()
178 .all(|(re, _)| re.abs() < 1.0e-8));
179 assert!(zeros
180 .materialize_f64()
181 .iter()
182 .any(|(_, im)| (*im - 1.0).abs() < 1.0e-8));
183 assert!(zeros
184 .materialize_f64()
185 .iter()
186 .any(|(_, im)| (*im + 1.0).abs() < 1.0e-8));
187 }
188
189 #[test]
190 fn zero_static_gain_returns_empty_column() {
191 let sys = block_on(crate::call_builtin_async(
192 "tf",
193 &[Value::Num(5.0), Value::Num(2.0)],
194 ))
195 .expect("tf");
196 let Value::Tensor(zeros) = block_on(zero_builtin(sys)).expect("zero") else {
197 panic!("expected real empty column");
198 };
199 assert_eq!(zeros.shape, vec![0, 1]);
200 assert!(zeros.materialize_f64().is_empty());
201 }
202}