runmat_runtime/builtins/control/
isstable.rs1use 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}