Skip to main content

runmat_runtime/builtins/control/
zero.rs

1//! Zero extraction for SISO transfer-function control models.
2
3use 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}