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