Skip to main content

runmat_runtime/builtins/control/
pole.rs

1//! Pole extraction for SISO transfer-function 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::pole_type;
15use crate::BuiltinResult;
16
17const POLE_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
18    name: "p",
19    ty: BuiltinParamType::Any,
20    arity: BuiltinParamArity::Required,
21    default: None,
22    description: "Poles of the SISO tf model as a column vector.",
23}];
24const POLE_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 POLE_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
32    label: "p = pole(sys)",
33    inputs: &POLE_INPUTS,
34    outputs: &POLE_OUTPUT,
35}];
36const POLE_ERRORS: [BuiltinErrorDescriptor; 3] = [
37    BuiltinErrorDescriptor {
38        code: "RM.POLE.INVALID_MODEL",
39        identifier: Some("RunMat:pole:InvalidModel"),
40        when: "Input system is not a valid SISO tf object.",
41        message: "pole: invalid model",
42    },
43    BuiltinErrorDescriptor {
44        code: "RM.POLE.UNSUPPORTED_MODEL",
45        identifier: Some("RunMat:pole:UnsupportedModel"),
46        when: "Model form is unsupported.",
47        message: "pole: unsupported model",
48    },
49    BuiltinErrorDescriptor {
50        code: "RM.POLE.INTERNAL",
51        identifier: Some("RunMat:pole:Internal"),
52        when: "Root calculation or output construction failed.",
53        message: "pole: internal error",
54    },
55];
56pub const POLE_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
57    signatures: &POLE_SIGNATURES,
58    output_mode: BuiltinOutputMode::Fixed,
59    completion_policy: BuiltinCompletionPolicy::Public,
60    errors: &POLE_ERRORS,
61};
62
63#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::control::pole")]
64pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
65    name: "pole",
66    op_kind: GpuOpKind::Custom("control-poles"),
67    supported_precisions: &[],
68    broadcast: BroadcastSemantics::None,
69    provider_hooks: &[],
70    constant_strategy: ConstantStrategy::InlineLiteral,
71    residency: ResidencyPolicy::GatherImmediately,
72    nan_mode: ReductionNaN::Include,
73    two_pass_threshold: None,
74    workgroup_size: None,
75    accepts_nan_mode: false,
76    notes: "pole computes roots from host-side transfer-function metadata.",
77};
78
79#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::control::pole")]
80pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
81    name: "pole",
82    shape: ShapeRequirements::Any,
83    constant_strategy: ConstantStrategy::InlineLiteral,
84    elementwise: None,
85    reduction: None,
86    emits_nan: false,
87    notes: "pole is model analysis and is not fused.",
88};
89
90#[runtime_builtin(
91    name = "pole",
92    category = "control",
93    summary = "Return poles of SISO transfer-function models.",
94    keywords = "pole,poles,control system,stability,transfer function,tf",
95    type_resolver(pole_type),
96    descriptor(crate::builtins::control::pole::POLE_DESCRIPTOR),
97    builtin_path = "crate::builtins::control::pole"
98)]
99async fn pole_builtin(sys: Value) -> BuiltinResult<Value> {
100    let model = TfModel::from_value_async(sys, "pole").await?;
101    output_complex_column(model.poles()?, "pole")
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use futures::executor::block_on;
108    use runmat_builtins::Tensor;
109
110    #[test]
111    fn pole_returns_roots_of_denominator() {
112        let sys = block_on(crate::call_builtin_async(
113            "tf",
114            &[
115                Value::Num(1.0),
116                Value::Tensor(Tensor::new(vec![1.0, 3.0, 2.0], vec![1, 3]).unwrap()),
117            ],
118        ))
119        .expect("tf");
120        let Value::Tensor(poles) = block_on(pole_builtin(sys)).expect("pole") else {
121            panic!("expected real poles");
122        };
123        assert_eq!(poles.shape, vec![2, 1]);
124        assert!(poles.data.iter().any(|p| (*p + 1.0).abs() < 1.0e-8));
125        assert!(poles.data.iter().any(|p| (*p + 2.0).abs() < 1.0e-8));
126    }
127}