Skip to main content

runmat_runtime/builtins/control/
isstable.rs

1//! Stability test for SISO transfer-function 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::TfModel;
16use crate::builtins::control::type_resolvers::isstable_type;
17use crate::BuiltinResult;
18
19const ISSTABLE_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
20    name: "tf",
21    ty: BuiltinParamType::LogicalArray,
22    arity: BuiltinParamArity::Required,
23    default: None,
24    description: "True when all continuous poles are in the open left-half plane or all discrete poles are inside the unit circle.",
25}];
26const ISSTABLE_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 ISSTABLE_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
34    label: "tf = isstable(sys)",
35    inputs: &ISSTABLE_INPUTS,
36    outputs: &ISSTABLE_OUTPUT,
37}];
38const ISSTABLE_ERRORS: [BuiltinErrorDescriptor; 3] = [
39    BuiltinErrorDescriptor {
40        code: "RM.ISSTABLE.INVALID_MODEL",
41        identifier: Some("RunMat:isstable:InvalidModel"),
42        when: "Input system is not a valid SISO tf object.",
43        message: "isstable: invalid model",
44    },
45    BuiltinErrorDescriptor {
46        code: "RM.ISSTABLE.UNSUPPORTED_MODEL",
47        identifier: Some("RunMat:isstable:UnsupportedModel"),
48        when: "Model form is unsupported.",
49        message: "isstable: unsupported model",
50    },
51    BuiltinErrorDescriptor {
52        code: "RM.ISSTABLE.INTERNAL",
53        identifier: Some("RunMat:isstable:Internal"),
54        when: "Pole calculation failed.",
55        message: "isstable: internal error",
56    },
57];
58pub const ISSTABLE_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
59    signatures: &ISSTABLE_SIGNATURES,
60    output_mode: BuiltinOutputMode::Fixed,
61    completion_policy: BuiltinCompletionPolicy::Public,
62    errors: &ISSTABLE_ERRORS,
63};
64pub const ISSTABLE_INTEGER_AUDIT: BuiltinIntegerAuditDescriptor = BuiltinIntegerAuditDescriptor {
65    kind: BuiltinIntegerAuditKind::NotApplicable,
66    canonical_builtin: None,
67    notes: "isstable requires a dynamic-system model; fundamental integer host or resident values are invalid-domain inputs and reject before payload or provider access.",
68};
69
70#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::control::isstable")]
71pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
72    name: "isstable",
73    op_kind: GpuOpKind::Custom("control-stability"),
74    supported_precisions: &[],
75    broadcast: BroadcastSemantics::None,
76    provider_hooks: &[],
77    constant_strategy: ConstantStrategy::InlineLiteral,
78    residency: ResidencyPolicy::GatherImmediately,
79    nan_mode: ReductionNaN::Include,
80    two_pass_threshold: None,
81    workgroup_size: None,
82    accepts_nan_mode: false,
83    notes: "isstable analyzes host-side transfer-function metadata.",
84};
85
86#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::control::isstable")]
87pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
88    name: "isstable",
89    shape: ShapeRequirements::Any,
90    constant_strategy: ConstantStrategy::InlineLiteral,
91    elementwise: None,
92    reduction: None,
93    emits_nan: false,
94    notes: "isstable returns scalar model metadata and is not fused.",
95};
96
97#[runtime_builtin(
98    name = "isstable",
99    category = "control",
100    summary = "Test stability of SISO transfer-function models.",
101    keywords = "isstable,control system,stability,poles,tf",
102    type_resolver(isstable_type),
103    descriptor(crate::builtins::control::isstable::ISSTABLE_DESCRIPTOR),
104    integer_audit(crate::builtins::control::isstable::ISSTABLE_INTEGER_AUDIT),
105    builtin_path = "crate::builtins::control::isstable"
106)]
107async fn isstable_builtin(sys: Value) -> BuiltinResult<Value> {
108    if value_has_integer_storage(&sys) {
109        return Err(crate::build_runtime_error(
110            "isstable: input must be a dynamic-system model, not an integer value",
111        )
112        .with_builtin("isstable")
113        .with_identifier(
114            ISSTABLE_ERRORS[0]
115                .identifier
116                .expect("isstable invalid-model descriptor identifier"),
117        )
118        .build()
119        .into());
120    }
121    let model = TfModel::from_value_async(sys, "isstable").await?;
122    Ok(Value::Bool(model.is_stable()?))
123}
124
125fn value_has_integer_storage(value: &Value) -> bool {
126    matches!(value, Value::Int(_))
127        || matches!(value, Value::Tensor(tensor) if tensor.integer_storage().is_some())
128        || matches!(value, Value::ComplexTensor(tensor) if tensor.integer_storage().is_some())
129        || matches!(value, Value::SparseTensor(tensor) if tensor.integer_storage().is_some())
130        || matches!(value, Value::GpuTensor(handle) if runmat_accelerate_api::handle_integer_type(handle).is_some())
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136    use futures::executor::block_on;
137    use runmat_value::Tensor;
138    use runmat_value::{IntValue, IntegerStorage};
139
140    #[test]
141    fn stable_continuous_model_returns_true() {
142        let sys = block_on(crate::call_builtin_async(
143            "tf",
144            &[
145                Value::Num(1.0),
146                Value::Tensor(Tensor::new(vec![1.0, 3.0, 2.0], vec![1, 3]).unwrap()),
147            ],
148        ))
149        .expect("tf");
150        assert_eq!(
151            block_on(isstable_builtin(sys)).expect("isstable"),
152            Value::Bool(true)
153        );
154    }
155
156    #[test]
157    fn all_integer_classes_are_invalid_model_inputs() {
158        for integer in [
159            IntValue::I8(-1),
160            IntValue::I16(-2),
161            IntValue::I32(-3),
162            IntValue::I64(i64::MIN),
163            IntValue::U8(1),
164            IntValue::U16(2),
165            IntValue::U32(3),
166            IntValue::U64(u64::MAX),
167        ] {
168            let error = block_on(isstable_builtin(Value::Int(integer)))
169                .expect_err("integer is not a system model");
170            assert_eq!(
171                error.identifier.as_deref(),
172                Some("RunMat:isstable:InvalidModel")
173            );
174        }
175    }
176
177    #[test]
178    fn resident_integer_rejects_before_model_gather() {
179        let tensor = Tensor::new_integer(IntegerStorage::U64(vec![u64::MAX]), vec![1, 1])
180            .expect("integer tensor");
181        crate::builtins::common::test_support::with_test_provider(|provider| {
182            let handle = crate::builtins::common::gpu_helpers::upload_tensor(provider, &tensor)
183                .expect("upload integer");
184            let error = block_on(isstable_builtin(Value::GpuTensor(handle)))
185                .expect_err("resident integer is not a system model");
186            assert_eq!(
187                error.identifier.as_deref(),
188                Some("RunMat:isstable:InvalidModel")
189            );
190        });
191    }
192}