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,
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}