1use std::cell::Cell;
4use std::collections::HashMap;
5
6use nalgebra::DMatrix;
7use num_complex::Complex64;
8use runmat_builtins::{
9 Access, CharArray, ClassDef, ComplexTensor, MethodDef, ObjectInstance, PropertyDef, Tensor,
10 Value,
11};
12
13use crate::builtins::common::tensor;
14use crate::{build_runtime_error, dispatcher, BuiltinResult, RuntimeError};
15
16pub const TF_CLASS: &str = "tf";
17pub const SS_CLASS: &str = "ss";
18pub const DEFAULT_CONTINUOUS_VARIABLE: &str = "s";
19pub const DEFAULT_DISCRETE_VARIABLE: &str = "z";
20pub const EPS: f64 = 1.0e-12;
21
22thread_local! {
23 static TF_CLASS_REGISTERED: Cell<bool> = const { Cell::new(false) };
24}
25
26#[derive(Clone, Debug)]
27pub struct TfModel {
28 pub numerator: Vec<Complex64>,
29 pub denominator: Vec<Complex64>,
30 pub variable: String,
31 pub sample_time: f64,
32 pub input_delay: f64,
33 pub output_delay: f64,
34}
35
36#[derive(Clone, Debug)]
37pub struct RealTfModel {
38 pub numerator: Vec<f64>,
39 pub denominator: Vec<f64>,
40 pub sample_time: f64,
41 pub input_delay: f64,
42 pub output_delay: f64,
43}
44
45#[derive(Clone, Debug)]
46pub struct TfOptions {
47 pub variable: String,
48 pub sample_time: f64,
49}
50
51impl Default for TfOptions {
52 fn default() -> Self {
53 Self {
54 variable: DEFAULT_CONTINUOUS_VARIABLE.to_string(),
55 sample_time: 0.0,
56 }
57 }
58}
59
60pub fn control_error(
61 builtin: &'static str,
62 identifier: &'static str,
63 message: impl Into<String>,
64) -> RuntimeError {
65 build_runtime_error(message)
66 .with_builtin(builtin)
67 .with_identifier(identifier)
68 .build()
69}
70
71pub fn ensure_tf_class_registered() {
72 TF_CLASS_REGISTERED.with(|registered| {
73 if registered.get() {
74 return;
75 }
76 let mut properties = HashMap::new();
77 for name in [
78 "Numerator",
79 "Denominator",
80 "Variable",
81 "Ts",
82 "InputDelay",
83 "OutputDelay",
84 ] {
85 properties.insert(
86 name.to_string(),
87 PropertyDef {
88 name: name.to_string(),
89 is_static: false,
90 is_constant: false,
91 is_dependent: false,
92 get_access: Access::Public,
93 set_access: Access::Public,
94 default_value: None,
95 },
96 );
97 }
98
99 let mut methods = HashMap::new();
100 for method_name in [
101 "plus", "minus", "uplus", "uminus", "times", "mtimes", "rdivide", "mrdivide",
102 "ldivide", "mldivide", "power", "mpower",
103 ] {
104 methods.insert(
105 method_name.to_string(),
106 MethodDef {
107 name: method_name.to_string(),
108 is_static: false,
109 is_abstract: false,
110 is_sealed: false,
111 access: Access::Public,
112 function_name: format!("{TF_CLASS}.{method_name}"),
113 implicit_class_argument: None,
114 },
115 );
116 }
117
118 runmat_builtins::register_class(ClassDef {
119 name: TF_CLASS.to_string(),
120 parent: None,
121 properties,
122 methods,
123 });
124 registered.set(true);
125 });
126}
127
128impl TfModel {
129 pub fn new(
130 numerator: Vec<Complex64>,
131 denominator: Vec<Complex64>,
132 options: TfOptions,
133 ) -> BuiltinResult<Self> {
134 Self::with_delays(numerator, denominator, options, 0.0, 0.0)
135 }
136
137 pub fn with_delays(
138 numerator: Vec<Complex64>,
139 denominator: Vec<Complex64>,
140 options: TfOptions,
141 input_delay: f64,
142 output_delay: f64,
143 ) -> BuiltinResult<Self> {
144 validate_coefficients("numerator", &numerator, "tf")?;
145 validate_coefficients("denominator", &denominator, "tf")?;
146 if all_zero(&denominator) {
147 return Err(control_error(
148 "tf",
149 "RunMat:tf:DenominatorInvalid",
150 "tf: invalid denominator coefficients: denominator coefficients must not all be zero",
151 ));
152 }
153 let variable = validate_variable(&options.variable, "tf")?;
154 validate_sample_time(options.sample_time, "tf")?;
155 validate_variable_domain(&variable, options.sample_time, "tf")?;
156 validate_delay(input_delay, "InputDelay", "tf")?;
157 validate_delay(output_delay, "OutputDelay", "tf")?;
158 Ok(Self {
159 numerator: trim_leading_complex_zeros(numerator),
160 denominator: trim_leading_complex_zeros(denominator),
161 variable,
162 sample_time: options.sample_time,
163 input_delay,
164 output_delay,
165 })
166 }
167
168 pub fn continuous_variable(variable: impl Into<String>) -> BuiltinResult<Self> {
169 let variable = variable.into();
170 Self::new(
171 vec![Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0)],
172 vec![Complex64::new(1.0, 0.0)],
173 TfOptions {
174 variable,
175 sample_time: 0.0,
176 },
177 )
178 }
179
180 pub fn discrete_variable(variable: impl Into<String>, sample_time: f64) -> BuiltinResult<Self> {
181 Self::new(
182 vec![Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0)],
183 vec![Complex64::new(1.0, 0.0)],
184 TfOptions {
185 variable: variable.into(),
186 sample_time,
187 },
188 )
189 }
190
191 pub fn scalar(value: Complex64, options: TfOptions) -> BuiltinResult<Self> {
192 Self::new(vec![value], vec![Complex64::new(1.0, 0.0)], options)
193 }
194
195 pub async fn from_value_async(value: Value, builtin: &'static str) -> BuiltinResult<Self> {
196 let gathered = dispatcher::gather_if_needed_async(&value).await?;
197 Self::from_value(gathered, builtin)
198 }
199
200 pub fn from_value(value: Value, builtin: &'static str) -> BuiltinResult<Self> {
201 let Value::Object(object) = value else {
202 return Err(control_error(
203 builtin,
204 invalid_model_identifier(builtin),
205 format!("{builtin}: expected a tf object"),
206 ));
207 };
208 if !object.is_class(TF_CLASS) {
209 return Err(control_error(
210 builtin,
211 unsupported_model_identifier(builtin),
212 format!(
213 "{builtin}: unsupported model class '{}'; only SISO tf objects are supported",
214 object.class_name
215 ),
216 ));
217 }
218
219 let numerator = coefficients_from_property(&object, "Numerator", builtin)?;
220 let denominator = coefficients_from_property(&object, "Denominator", builtin)?;
221 let sample_time = scalar_property(&object, "Ts", builtin)?;
222 let input_delay = scalar_property(&object, "InputDelay", builtin)?;
223 let output_delay = scalar_property(&object, "OutputDelay", builtin)?;
224 validate_sample_time(sample_time, builtin)?;
225 validate_delay(input_delay, "InputDelay", builtin)?;
226 validate_delay(output_delay, "OutputDelay", builtin)?;
227 if all_zero(&denominator) {
228 return Err(control_error(
229 builtin,
230 invalid_model_identifier(builtin),
231 format!("{builtin}: denominator coefficients must not all be zero"),
232 ));
233 }
234 let variable = match object.properties.get("Variable") {
235 Some(value) => validate_variable(&scalar_text(value, "Variable", builtin)?, builtin)?,
236 None => {
237 if sample_time > 0.0 {
238 DEFAULT_DISCRETE_VARIABLE.to_string()
239 } else {
240 DEFAULT_CONTINUOUS_VARIABLE.to_string()
241 }
242 }
243 };
244 validate_variable_domain(&variable, sample_time, builtin)?;
245 Ok(Self {
246 numerator: trim_leading_complex_zeros(numerator),
247 denominator: trim_leading_complex_zeros(denominator),
248 variable,
249 sample_time,
250 input_delay,
251 output_delay,
252 })
253 }
254
255 pub fn to_value(&self, builtin: &'static str) -> BuiltinResult<Value> {
256 ensure_tf_class_registered();
257 let mut object = ObjectInstance::new(TF_CLASS.to_string());
258 object.properties.insert(
259 "Numerator".to_string(),
260 coefficient_value(&self.numerator, builtin)?,
261 );
262 object.properties.insert(
263 "Denominator".to_string(),
264 coefficient_value(&self.denominator, builtin)?,
265 );
266 object.properties.insert(
267 "Variable".to_string(),
268 Value::CharArray(CharArray::new_row(&self.variable)),
269 );
270 object
271 .properties
272 .insert("Ts".to_string(), Value::Num(self.sample_time));
273 object
274 .properties
275 .insert("InputDelay".to_string(), Value::Num(self.input_delay));
276 object
277 .properties
278 .insert("OutputDelay".to_string(), Value::Num(self.output_delay));
279 Ok(Value::Object(object))
280 }
281
282 pub fn to_real(&self, builtin: &'static str) -> BuiltinResult<RealTfModel> {
283 let numerator = real_coefficients(&self.numerator, "Numerator", builtin)?;
284 let denominator = real_coefficients(&self.denominator, "Denominator", builtin)?;
285 Ok(RealTfModel {
286 numerator,
287 denominator,
288 sample_time: self.sample_time,
289 input_delay: self.input_delay,
290 output_delay: self.output_delay,
291 })
292 }
293
294 pub fn is_discrete(&self) -> bool {
295 self.sample_time > 0.0
296 }
297
298 pub fn normalized(&self) -> BuiltinResult<Self> {
299 let leading = *self.denominator.first().ok_or_else(|| {
300 control_error(
301 "tf",
302 "RunMat:tf:DenominatorInvalid",
303 "tf: denominator coefficients cannot be empty",
304 )
305 })?;
306 if leading.norm() <= EPS {
307 return Err(control_error(
308 "tf",
309 "RunMat:tf:DenominatorInvalid",
310 "tf: leading denominator coefficient must be non-zero",
311 ));
312 }
313 let mut out = self.clone();
314 out.numerator = out.numerator.iter().map(|value| *value / leading).collect();
315 out.denominator = out
316 .denominator
317 .iter()
318 .map(|value| *value / leading)
319 .collect();
320 Ok(out)
321 }
322
323 pub fn add(&self, rhs: &Self) -> BuiltinResult<Self> {
324 self.ensure_arithmetic_compatible(rhs, "plus")?;
325 let numerator = poly_add(
326 &poly_mul(&self.numerator, &rhs.denominator),
327 &poly_mul(&rhs.numerator, &self.denominator),
328 );
329 let denominator = poly_mul(&self.denominator, &rhs.denominator);
330 self.with_new_coefficients(numerator, denominator)
331 }
332
333 pub fn sub(&self, rhs: &Self) -> BuiltinResult<Self> {
334 self.ensure_arithmetic_compatible(rhs, "minus")?;
335 let numerator = poly_sub(
336 &poly_mul(&self.numerator, &rhs.denominator),
337 &poly_mul(&rhs.numerator, &self.denominator),
338 );
339 let denominator = poly_mul(&self.denominator, &rhs.denominator);
340 self.with_new_coefficients(numerator, denominator)
341 }
342
343 pub fn neg(&self) -> BuiltinResult<Self> {
344 self.with_new_coefficients(
345 poly_scale(&self.numerator, -Complex64::new(1.0, 0.0)),
346 self.denominator.clone(),
347 )
348 }
349
350 pub fn mul(&self, rhs: &Self) -> BuiltinResult<Self> {
351 self.ensure_arithmetic_compatible(rhs, "mtimes")?;
352 self.with_new_coefficients(
353 poly_mul(&self.numerator, &rhs.numerator),
354 poly_mul(&self.denominator, &rhs.denominator),
355 )
356 }
357
358 pub fn div(&self, rhs: &Self) -> BuiltinResult<Self> {
359 self.ensure_arithmetic_compatible(rhs, "mrdivide")?;
360 if all_zero(&rhs.numerator) {
361 return Err(control_error(
362 "tf",
363 "RunMat:tf:DivideByZero",
364 "tf: cannot divide by a zero transfer function",
365 ));
366 }
367 self.with_new_coefficients(
368 poly_mul(&self.numerator, &rhs.denominator),
369 poly_mul(&self.denominator, &rhs.numerator),
370 )
371 }
372
373 pub fn powi(&self, exponent: i64) -> BuiltinResult<Self> {
374 let one = Self::scalar(
375 Complex64::new(1.0, 0.0),
376 TfOptions {
377 variable: self.variable.clone(),
378 sample_time: self.sample_time,
379 },
380 )?;
381 if exponent == 0 {
382 return Ok(one);
383 }
384 let mut base = self.clone();
385 let mut exp = exponent;
386 if exp < 0 {
387 base = one.div(&base)?;
388 exp = exp.checked_neg().ok_or_else(|| {
389 control_error(
390 "tf",
391 "RunMat:tf:InvalidExponent",
392 "tf: exponent magnitude is too large",
393 )
394 })?;
395 }
396 let mut result = one;
397 while exp > 0 {
398 if exp & 1 == 1 {
399 result = result.mul(&base)?;
400 }
401 exp >>= 1;
402 if exp > 0 {
403 base = base.mul(&base)?;
404 }
405 }
406 Ok(result)
407 }
408
409 pub fn poles(&self) -> BuiltinResult<Vec<Complex64>> {
410 polynomial_roots(&self.denominator, "pole")
411 }
412
413 pub fn zeros(&self) -> BuiltinResult<Vec<Complex64>> {
414 polynomial_roots(&self.numerator, "zero")
415 }
416
417 pub fn dc_gain(&self) -> BuiltinResult<Complex64> {
418 let point = if self.is_discrete() {
419 Complex64::new(1.0, 0.0)
420 } else {
421 Complex64::new(0.0, 0.0)
422 };
423 let num = poly_eval(&self.numerator, point);
424 let den = poly_eval(&self.denominator, point);
425 if den.norm() <= EPS {
426 if num.norm() <= EPS {
427 Ok(Complex64::new(f64::NAN, f64::NAN))
428 } else {
429 Ok(num / Complex64::new(0.0, 0.0))
430 }
431 } else {
432 Ok(num / den)
433 }
434 }
435
436 pub fn is_stable(&self) -> BuiltinResult<bool> {
437 let poles = self.poles()?;
438 if self.is_discrete() {
439 Ok(poles.iter().all(|pole| pole.norm() < 1.0 - EPS))
440 } else {
441 Ok(poles.iter().all(|pole| pole.re < -EPS))
442 }
443 }
444
445 pub fn feedback(&self, rhs: &Self, sign: f64) -> BuiltinResult<Self> {
446 if sign != -1.0 && sign != 1.0 {
447 return Err(control_error(
448 "feedback",
449 "RunMat:feedback:InvalidSign",
450 "feedback: sign must be -1 or +1",
451 ));
452 }
453 self.ensure_arithmetic_compatible(rhs, "feedback")?;
454 let numerator = poly_mul(&self.numerator, &rhs.denominator);
455 let base_denominator = poly_mul(&self.denominator, &rhs.denominator);
456 let loop_numerator = poly_mul(&self.numerator, &rhs.numerator);
457 let denominator = if sign < 0.0 {
458 poly_add(&base_denominator, &loop_numerator)
459 } else {
460 poly_sub(&base_denominator, &loop_numerator)
461 };
462 self.with_new_coefficients(numerator, denominator)
463 }
464
465 fn ensure_arithmetic_compatible(&self, rhs: &Self, op: &'static str) -> BuiltinResult<()> {
466 if (self.sample_time - rhs.sample_time).abs() > EPS {
467 return Err(control_error(
468 "tf",
469 "RunMat:tf:SampleTimeMismatch",
470 format!("tf.{op}: sample times must match"),
471 ));
472 }
473 if self.variable != rhs.variable {
474 return Err(control_error(
475 "tf",
476 "RunMat:tf:VariableMismatch",
477 format!("tf.{op}: transfer-function variables must match"),
478 ));
479 }
480 if self.input_delay.abs() > EPS
481 || self.output_delay.abs() > EPS
482 || rhs.input_delay.abs() > EPS
483 || rhs.output_delay.abs() > EPS
484 {
485 return Err(control_error(
486 "tf",
487 "RunMat:tf:UnsupportedDelay",
488 format!("tf.{op}: input and output delays are not supported in arithmetic"),
489 ));
490 }
491 Ok(())
492 }
493
494 fn with_new_coefficients(
495 &self,
496 numerator: Vec<Complex64>,
497 denominator: Vec<Complex64>,
498 ) -> BuiltinResult<Self> {
499 Self::with_delays(
500 numerator,
501 denominator,
502 TfOptions {
503 variable: self.variable.clone(),
504 sample_time: self.sample_time,
505 },
506 self.input_delay,
507 self.output_delay,
508 )
509 }
510}
511
512impl RealTfModel {
513 pub fn normalized(&self, builtin: &'static str) -> BuiltinResult<(Vec<f64>, Vec<f64>)> {
514 let den = trim_leading_real_zeros(self.denominator.clone());
515 if den.is_empty() || den[0].abs() <= EPS {
516 return Err(control_error(
517 builtin,
518 invalid_model_identifier(builtin),
519 format!("{builtin}: leading denominator coefficient must be non-zero"),
520 ));
521 }
522 let num = trim_leading_real_zeros(self.numerator.clone());
523 let leading = den[0];
524 Ok((
525 num.iter().map(|value| value / leading).collect(),
526 den.iter().map(|value| value / leading).collect(),
527 ))
528 }
529
530 pub fn ensure_zero_delays(&self, builtin: &'static str) -> BuiltinResult<()> {
531 if self.input_delay.abs() > EPS || self.output_delay.abs() > EPS {
532 return Err(control_error(
533 builtin,
534 unsupported_model_identifier(builtin),
535 format!(
536 "{builtin}: transfer functions with input or output delays are not supported"
537 ),
538 ));
539 }
540 Ok(())
541 }
542}
543
544pub async fn parse_coefficients(
545 label: &str,
546 value: Value,
547 builtin: &'static str,
548) -> BuiltinResult<Vec<Complex64>> {
549 let gathered = dispatcher::gather_if_needed_async(&value).await?;
550 let coeffs = match gathered {
551 Value::Tensor(tensor) => {
552 ensure_vector_shape(label, &tensor.shape, builtin)?;
553 tensor
554 .data
555 .into_iter()
556 .map(|re| Complex64::new(re, 0.0))
557 .collect()
558 }
559 Value::ComplexTensor(tensor) => {
560 ensure_vector_shape(label, &tensor.shape, builtin)?;
561 tensor
562 .data
563 .into_iter()
564 .map(|(re, im)| Complex64::new(re, im))
565 .collect()
566 }
567 Value::LogicalArray(logical) => {
568 let tensor = tensor::logical_to_tensor(&logical).map_err(|err| {
569 control_error(
570 builtin,
571 invalid_coefficients_identifier(builtin),
572 format!("{builtin}: failed to convert logical array: {err}"),
573 )
574 })?;
575 ensure_vector_shape(label, &tensor.shape, builtin)?;
576 tensor
577 .data
578 .into_iter()
579 .map(|re| Complex64::new(re, 0.0))
580 .collect()
581 }
582 Value::Num(n) => vec![Complex64::new(n, 0.0)],
583 Value::Int(i) => vec![Complex64::new(i.to_f64(), 0.0)],
584 Value::Bool(b) => vec![Complex64::new(if b { 1.0 } else { 0.0 }, 0.0)],
585 Value::Complex(re, im) => vec![Complex64::new(re, im)],
586 other => {
587 return Err(control_error(
588 builtin,
589 invalid_coefficients_identifier(builtin),
590 format!("{builtin}: {label} must be a numeric coefficient vector, got {other:?}"),
591 ));
592 }
593 };
594 validate_coefficients(label, &coeffs, builtin)?;
595 Ok(coeffs)
596}
597
598pub async fn value_to_model_with_reference(
599 value: Value,
600 reference: &TfModel,
601 builtin: &'static str,
602) -> BuiltinResult<TfModel> {
603 let gathered = dispatcher::gather_if_needed_async(&value).await?;
604 if matches!(gathered, Value::Object(_)) {
605 return TfModel::from_value(gathered, builtin);
606 }
607 let scalar = scalar_complex(&gathered, builtin)?;
608 TfModel::scalar(
609 scalar,
610 TfOptions {
611 variable: reference.variable.clone(),
612 sample_time: reference.sample_time,
613 },
614 )
615}
616
617pub async fn two_models_ordered(
618 lhs: Value,
619 rhs: Value,
620 builtin: &'static str,
621) -> BuiltinResult<(TfModel, TfModel)> {
622 let lhs = dispatcher::gather_if_needed_async(&lhs).await?;
623 let rhs = dispatcher::gather_if_needed_async(&rhs).await?;
624 match (lhs, rhs) {
625 (left @ Value::Object(_), right @ Value::Object(_)) => Ok((
626 TfModel::from_value(left, builtin)?,
627 TfModel::from_value(right, builtin)?,
628 )),
629 (left @ Value::Object(_), right) => {
630 let left_model = TfModel::from_value(left, builtin)?;
631 let right_model = value_to_model_with_reference(right, &left_model, builtin).await?;
632 Ok((left_model, right_model))
633 }
634 (left, right @ Value::Object(_)) => {
635 let right_model = TfModel::from_value(right, builtin)?;
636 let left_model = value_to_model_with_reference(left, &right_model, builtin).await?;
637 Ok((left_model, right_model))
638 }
639 (left, right) => {
640 let options = TfOptions::default();
641 Ok((
642 TfModel::scalar(scalar_complex(&left, builtin)?, options.clone())?,
643 TfModel::scalar(scalar_complex(&right, builtin)?, options)?,
644 ))
645 }
646 }
647}
648
649pub fn scalar_text(value: &Value, context: &str, builtin: &'static str) -> BuiltinResult<String> {
650 match value {
651 Value::String(text) => Ok(text.clone()),
652 Value::StringArray(array) if array.data.len() == 1 => Ok(array.data[0].clone()),
653 Value::CharArray(array) if array.rows == 1 => Ok(array.data.iter().collect()),
654 other => Err(control_error(
655 builtin,
656 invalid_argument_identifier(builtin),
657 format!(
658 "{builtin}: {context} must be a string scalar or character vector, got {other:?}"
659 ),
660 )),
661 }
662}
663
664pub fn scalar_f64(value: &Value, context: &str, builtin: &'static str) -> BuiltinResult<f64> {
665 match value {
666 Value::Num(n) => Ok(*n),
667 Value::Int(i) => Ok(i.to_f64()),
668 Value::Bool(b) => Ok(if *b { 1.0 } else { 0.0 }),
669 Value::Tensor(tensor) if tensor.data.len() == 1 => Ok(tensor.data[0]),
670 Value::LogicalArray(logical) if logical.data.len() == 1 => {
671 Ok(if logical.data[0] == 0 { 0.0 } else { 1.0 })
672 }
673 other => Err(control_error(
674 builtin,
675 invalid_argument_identifier(builtin),
676 format!("{builtin}: {context} must be a real scalar, got {other:?}"),
677 )),
678 }
679}
680
681pub fn scalar_complex(value: &Value, builtin: &'static str) -> BuiltinResult<Complex64> {
682 match value {
683 Value::Num(n) => Ok(Complex64::new(*n, 0.0)),
684 Value::Int(i) => Ok(Complex64::new(i.to_f64(), 0.0)),
685 Value::Bool(b) => Ok(Complex64::new(if *b { 1.0 } else { 0.0 }, 0.0)),
686 Value::Complex(re, im) => Ok(Complex64::new(*re, *im)),
687 Value::Tensor(tensor) if tensor.data.len() == 1 => Ok(Complex64::new(tensor.data[0], 0.0)),
688 Value::ComplexTensor(tensor) if tensor.data.len() == 1 => {
689 let (re, im) = tensor.data[0];
690 Ok(Complex64::new(re, im))
691 }
692 Value::LogicalArray(logical) if logical.data.len() == 1 => Ok(Complex64::new(
693 if logical.data[0] == 0 { 0.0 } else { 1.0 },
694 0.0,
695 )),
696 other => Err(control_error(
697 builtin,
698 invalid_argument_identifier(builtin),
699 format!("{builtin}: expected a scalar numeric value or tf object, got {other:?}"),
700 )),
701 }
702}
703
704pub fn validate_variable(variable: &str, builtin: &'static str) -> BuiltinResult<String> {
705 let variable = variable.trim();
706 match variable {
707 "s" | "p" | "z" | "q" | "z^-1" | "q^-1" => Ok(variable.to_string()),
708 _ => Err(control_error(
709 builtin,
710 "RunMat:tf:InvalidVariable",
711 "tf: invalid Variable option: must be one of 's', 'p', 'z', 'q', 'z^-1', or 'q^-1'",
712 )),
713 }
714}
715
716pub fn is_discrete_variable(variable: &str) -> bool {
717 matches!(variable.trim(), "z" | "q" | "z^-1" | "q^-1")
718}
719
720pub fn validate_variable_domain(
721 variable: &str,
722 sample_time: f64,
723 builtin: &'static str,
724) -> BuiltinResult<()> {
725 let discrete_variable = is_discrete_variable(variable);
726 if discrete_variable && sample_time <= 0.0 {
727 return Err(control_error(
728 builtin,
729 "RunMat:tf:InvalidSampleTime",
730 format!(
731 "{builtin}: discrete transfer-function variables require a positive sample time"
732 ),
733 ));
734 }
735 if !discrete_variable && sample_time > 0.0 {
736 return Err(control_error(
737 builtin,
738 "RunMat:tf:InvalidVariable",
739 format!("{builtin}: continuous transfer-function variables require Ts = 0"),
740 ));
741 }
742 Ok(())
743}
744
745pub fn validate_sample_time(sample_time: f64, builtin: &'static str) -> BuiltinResult<()> {
746 if !sample_time.is_finite() || sample_time < 0.0 {
747 return Err(control_error(
748 builtin,
749 invalid_sample_time_identifier(builtin),
750 format!("{builtin}: sample time must be a finite non-negative scalar"),
751 ));
752 }
753 Ok(())
754}
755
756pub fn output_complex_scalar(value: Complex64) -> Value {
757 if value.im.abs() <= EPS {
758 Value::Num(value.re)
759 } else {
760 Value::Complex(value.re, value.im)
761 }
762}
763
764pub fn output_complex_column(
765 values: Vec<Complex64>,
766 builtin: &'static str,
767) -> BuiltinResult<Value> {
768 let rows = values.len();
769 if values.iter().all(|value| value.im.abs() <= EPS) {
770 let data = values.into_iter().map(|value| value.re).collect::<Vec<_>>();
771 Ok(Value::Tensor(Tensor::new(data, vec![rows, 1]).map_err(
772 |err| {
773 control_error(
774 builtin,
775 internal_identifier(builtin),
776 format!("{builtin}: failed to build output tensor: {err}"),
777 )
778 },
779 )?))
780 } else {
781 let data = values
782 .into_iter()
783 .map(|value| (value.re, value.im))
784 .collect::<Vec<_>>();
785 Ok(Value::ComplexTensor(
786 ComplexTensor::new(data, vec![rows, 1]).map_err(|err| {
787 control_error(
788 builtin,
789 internal_identifier(builtin),
790 format!("{builtin}: failed to build complex output tensor: {err}"),
791 )
792 })?,
793 ))
794 }
795}
796
797pub fn ss_poles_from_object(
798 object: &ObjectInstance,
799 builtin: &'static str,
800) -> BuiltinResult<(Vec<Complex64>, f64)> {
801 let a = ss_state_matrix_property(object, "A", builtin)?;
802 let sample_time = scalar_property(object, "Ts", builtin)?;
803 validate_sample_time(sample_time, builtin)?;
804 let eigenvalues = a.eigenvalues().ok_or_else(|| {
805 control_error(
806 builtin,
807 internal_identifier(builtin),
808 format!("{builtin}: failed to compute state matrix eigenvalues"),
809 )
810 })?;
811 Ok((eigenvalues.iter().copied().collect(), sample_time))
812}
813
814fn ss_state_matrix_property(
815 object: &ObjectInstance,
816 name: &'static str,
817 builtin: &'static str,
818) -> BuiltinResult<DMatrix<Complex64>> {
819 let value = object.properties.get(name).ok_or_else(|| {
820 control_error(
821 builtin,
822 invalid_model_identifier(builtin),
823 format!("{builtin}: ss object is missing {name}"),
824 )
825 })?;
826 let tensor = match value {
827 Value::Tensor(tensor) => tensor.clone(),
828 Value::Num(n) => Tensor::new(vec![*n], vec![1, 1]).map_err(|err| {
829 control_error(
830 builtin,
831 internal_identifier(builtin),
832 format!("{builtin}: failed to build scalar matrix: {err}"),
833 )
834 })?,
835 Value::Int(i) => Tensor::new(vec![i.to_f64()], vec![1, 1]).map_err(|err| {
836 control_error(
837 builtin,
838 internal_identifier(builtin),
839 format!("{builtin}: failed to build scalar matrix: {err}"),
840 )
841 })?,
842 other => {
843 return Err(control_error(
844 builtin,
845 unsupported_model_identifier(builtin),
846 format!("{builtin}: ss {name} must be a finite real matrix, got {other:?}"),
847 ));
848 }
849 };
850 if tensor.shape.len() > 2 || tensor.rows != tensor.cols {
851 return Err(control_error(
852 builtin,
853 invalid_model_identifier(builtin),
854 format!(
855 "{builtin}: ss {name} must be square, got {:?}",
856 tensor.shape
857 ),
858 ));
859 }
860 if tensor.data.iter().any(|value| !value.is_finite()) {
861 return Err(control_error(
862 builtin,
863 unsupported_model_identifier(builtin),
864 format!("{builtin}: ss {name} must contain only finite real values"),
865 ));
866 }
867 let mut matrix = DMatrix::<Complex64>::zeros(tensor.rows, tensor.cols);
868 for col in 0..tensor.cols {
869 for row in 0..tensor.rows {
870 matrix[(row, col)] = Complex64::new(tensor.data[row + col * tensor.rows], 0.0);
871 }
872 }
873 Ok(matrix)
874}
875
876pub fn polynomial_roots(
877 coeffs: &[Complex64],
878 builtin: &'static str,
879) -> BuiltinResult<Vec<Complex64>> {
880 let trimmed = trim_leading_complex_zeros(coeffs.to_vec());
881 if trimmed.len() <= 1 {
882 return Ok(Vec::new());
883 }
884 if trimmed.len() == 2 {
885 return Ok(vec![-trimmed[1] / trimmed[0]]);
886 }
887 let degree = trimmed.len() - 1;
888 let leading = trimmed[0];
889 if leading.norm() <= EPS {
890 return Err(control_error(
891 builtin,
892 invalid_model_identifier(builtin),
893 format!("{builtin}: leading polynomial coefficient must be non-zero"),
894 ));
895 }
896 let mut companion = DMatrix::<Complex64>::zeros(degree, degree);
897 for row in 1..degree {
898 companion[(row, row - 1)] = Complex64::new(1.0, 0.0);
899 }
900 for (idx, coeff) in trimmed.iter().enumerate().skip(1) {
901 companion[(0, idx - 1)] = -*coeff / leading;
902 }
903 let eigenvalues = companion.eigenvalues().ok_or_else(|| {
904 control_error(
905 builtin,
906 internal_identifier(builtin),
907 format!("{builtin}: failed to compute polynomial roots"),
908 )
909 })?;
910 Ok(eigenvalues.iter().copied().collect())
911}
912
913pub fn poly_eval(coeffs: &[Complex64], x: Complex64) -> Complex64 {
914 coeffs
915 .iter()
916 .fold(Complex64::new(0.0, 0.0), |acc, coeff| acc * x + *coeff)
917}
918
919pub fn trim_leading_real_zeros(coeffs: Vec<f64>) -> Vec<f64> {
920 let first_nonzero = coeffs
921 .iter()
922 .position(|value| value.abs() > EPS)
923 .unwrap_or(coeffs.len());
924 if first_nonzero == coeffs.len() {
925 return vec![0.0];
926 }
927 coeffs[first_nonzero..].to_vec()
928}
929
930fn coefficients_from_property(
931 object: &ObjectInstance,
932 name: &str,
933 builtin: &'static str,
934) -> BuiltinResult<Vec<Complex64>> {
935 let value = object.properties.get(name).ok_or_else(|| {
936 control_error(
937 builtin,
938 invalid_model_identifier(builtin),
939 format!("{builtin}: tf object is missing {name}"),
940 )
941 })?;
942 match value {
943 Value::Tensor(tensor) => {
944 ensure_vector_shape(name, &tensor.shape, builtin)?;
945 Ok(tensor
946 .data
947 .iter()
948 .map(|value| Complex64::new(*value, 0.0))
949 .collect())
950 }
951 Value::ComplexTensor(tensor) => {
952 ensure_vector_shape(name, &tensor.shape, builtin)?;
953 Ok(tensor
954 .data
955 .iter()
956 .map(|(re, im)| Complex64::new(*re, *im))
957 .collect())
958 }
959 Value::Num(n) => Ok(vec![Complex64::new(*n, 0.0)]),
960 Value::Int(i) => Ok(vec![Complex64::new(i.to_f64(), 0.0)]),
961 Value::Bool(b) => Ok(vec![Complex64::new(if *b { 1.0 } else { 0.0 }, 0.0)]),
962 other => Err(control_error(
963 builtin,
964 invalid_model_identifier(builtin),
965 format!("{builtin}: tf {name} coefficients must be numeric, got {other:?}"),
966 )),
967 }
968}
969
970fn coefficient_value(coeffs: &[Complex64], builtin: &'static str) -> BuiltinResult<Value> {
971 let len = coeffs.len();
972 if coeffs.iter().all(|coeff| coeff.im.abs() <= EPS) {
973 let data = coeffs.iter().map(|coeff| coeff.re).collect::<Vec<_>>();
974 let tensor = Tensor::new(data, vec![1, len]).map_err(|err| {
975 control_error(
976 builtin,
977 internal_identifier(builtin),
978 format!("{builtin}: failed to build coefficient tensor: {err}"),
979 )
980 })?;
981 Ok(Value::Tensor(tensor))
982 } else {
983 let data = coeffs
984 .iter()
985 .map(|coeff| (coeff.re, coeff.im))
986 .collect::<Vec<_>>();
987 let tensor = ComplexTensor::new(data, vec![1, len]).map_err(|err| {
988 control_error(
989 builtin,
990 internal_identifier(builtin),
991 format!("{builtin}: failed to build complex coefficient tensor: {err}"),
992 )
993 })?;
994 Ok(Value::ComplexTensor(tensor))
995 }
996}
997
998fn property<'a>(
999 object: &'a ObjectInstance,
1000 name: &str,
1001 builtin: &'static str,
1002) -> BuiltinResult<&'a Value> {
1003 object.properties.get(name).ok_or_else(|| {
1004 control_error(
1005 builtin,
1006 invalid_model_identifier(builtin),
1007 format!("{builtin}: tf object is missing {name} property"),
1008 )
1009 })
1010}
1011
1012fn scalar_property(
1013 object: &ObjectInstance,
1014 name: &str,
1015 builtin: &'static str,
1016) -> BuiltinResult<f64> {
1017 scalar_f64(property(object, name, builtin)?, name, builtin)
1018}
1019
1020fn validate_delay(value: f64, label: &str, builtin: &'static str) -> BuiltinResult<()> {
1021 if !value.is_finite() || value < 0.0 {
1022 return Err(control_error(
1023 builtin,
1024 invalid_model_identifier(builtin),
1025 format!("{builtin}: {label} must be a finite non-negative scalar"),
1026 ));
1027 }
1028 Ok(())
1029}
1030
1031fn validate_coefficients(
1032 label: &str,
1033 coeffs: &[Complex64],
1034 builtin: &'static str,
1035) -> BuiltinResult<()> {
1036 if coeffs.is_empty() {
1037 return Err(control_error(
1038 builtin,
1039 invalid_coefficients_identifier(builtin),
1040 format!("{builtin}: {label} coefficients cannot be empty"),
1041 ));
1042 }
1043 for coeff in coeffs {
1044 if !coeff.re.is_finite() || !coeff.im.is_finite() {
1045 return Err(control_error(
1046 builtin,
1047 invalid_coefficients_identifier(builtin),
1048 format!("{builtin}: {label} coefficients must be finite"),
1049 ));
1050 }
1051 }
1052 Ok(())
1053}
1054
1055fn real_coefficients(
1056 coeffs: &[Complex64],
1057 label: &str,
1058 builtin: &'static str,
1059) -> BuiltinResult<Vec<f64>> {
1060 let mut out = Vec::with_capacity(coeffs.len());
1061 for coeff in coeffs {
1062 if coeff.im.abs() > EPS {
1063 return Err(control_error(
1064 builtin,
1065 unsupported_model_identifier(builtin),
1066 format!("{builtin}: complex tf {label} coefficients are not supported"),
1067 ));
1068 }
1069 out.push(coeff.re);
1070 }
1071 Ok(out)
1072}
1073
1074fn ensure_vector_shape(label: &str, shape: &[usize], builtin: &'static str) -> BuiltinResult<()> {
1075 let non_unit = shape.iter().copied().filter(|&dim| dim > 1).count();
1076 if non_unit <= 1 {
1077 Ok(())
1078 } else {
1079 Err(control_error(
1080 builtin,
1081 invalid_coefficients_identifier(builtin),
1082 format!("{builtin}: {label} coefficients must be a vector"),
1083 ))
1084 }
1085}
1086
1087fn trim_leading_complex_zeros(coeffs: Vec<Complex64>) -> Vec<Complex64> {
1088 let first_nonzero = coeffs
1089 .iter()
1090 .position(|value| value.norm() > EPS)
1091 .unwrap_or(coeffs.len());
1092 let trimmed = coeffs[first_nonzero..].to_vec();
1093 if trimmed.is_empty() {
1094 vec![Complex64::new(0.0, 0.0)]
1095 } else {
1096 trimmed
1097 }
1098}
1099
1100fn all_zero(coeffs: &[Complex64]) -> bool {
1101 coeffs.iter().all(|coeff| coeff.norm() <= EPS)
1102}
1103
1104fn poly_add(lhs: &[Complex64], rhs: &[Complex64]) -> Vec<Complex64> {
1105 let len = lhs.len().max(rhs.len());
1106 let mut out = vec![Complex64::new(0.0, 0.0); len];
1107 for (idx, value) in lhs.iter().enumerate() {
1108 out[len - lhs.len() + idx] += *value;
1109 }
1110 for (idx, value) in rhs.iter().enumerate() {
1111 out[len - rhs.len() + idx] += *value;
1112 }
1113 trim_leading_complex_zeros(out)
1114}
1115
1116fn poly_sub(lhs: &[Complex64], rhs: &[Complex64]) -> Vec<Complex64> {
1117 poly_add(lhs, &poly_scale(rhs, -Complex64::new(1.0, 0.0)))
1118}
1119
1120fn poly_scale(coeffs: &[Complex64], scale: Complex64) -> Vec<Complex64> {
1121 trim_leading_complex_zeros(coeffs.iter().map(|value| *value * scale).collect())
1122}
1123
1124fn poly_mul(lhs: &[Complex64], rhs: &[Complex64]) -> Vec<Complex64> {
1125 if all_zero(lhs) || all_zero(rhs) {
1126 return vec![Complex64::new(0.0, 0.0)];
1127 }
1128 let mut out = vec![Complex64::new(0.0, 0.0); lhs.len() + rhs.len() - 1];
1129 for (i, a) in lhs.iter().enumerate() {
1130 for (j, b) in rhs.iter().enumerate() {
1131 out[i + j] += *a * *b;
1132 }
1133 }
1134 trim_leading_complex_zeros(out)
1135}
1136
1137fn invalid_argument_identifier(builtin: &str) -> &'static str {
1138 match builtin {
1139 "feedback" => "RunMat:feedback:InvalidArgument",
1140 "stepinfo" => "RunMat:stepinfo:InvalidArgument",
1141 "dcgain" => "RunMat:dcgain:InvalidArgument",
1142 "pole" => "RunMat:pole:InvalidArgument",
1143 "zero" => "RunMat:zero:InvalidArgument",
1144 "damp" => "RunMat:damp:InvalidModel",
1145 "rlocus" => "RunMat:rlocus:InvalidArgument",
1146 "isstable" => "RunMat:isstable:InvalidArgument",
1147 _ => "RunMat:tf:InvalidArgument",
1148 }
1149}
1150
1151fn invalid_coefficients_identifier(builtin: &str) -> &'static str {
1152 match builtin {
1153 "feedback" => "RunMat:feedback:InvalidModel",
1154 "stepinfo" => "RunMat:stepinfo:InvalidData",
1155 "dcgain" => "RunMat:dcgain:InvalidModel",
1156 "pole" => "RunMat:pole:InvalidModel",
1157 "zero" => "RunMat:zero:InvalidModel",
1158 "damp" => "RunMat:damp:InvalidModel",
1159 "rlocus" => "RunMat:rlocus:InvalidModel",
1160 "isstable" => "RunMat:isstable:InvalidModel",
1161 _ => "RunMat:tf:InvalidCoefficients",
1162 }
1163}
1164
1165fn invalid_sample_time_identifier(builtin: &str) -> &'static str {
1166 match builtin {
1167 "feedback" => "RunMat:feedback:InvalidSampleTime",
1168 "stepinfo" => "RunMat:stepinfo:InvalidArgument",
1169 "dcgain" => "RunMat:dcgain:InvalidModel",
1170 "pole" => "RunMat:pole:InvalidModel",
1171 "zero" => "RunMat:zero:InvalidModel",
1172 "damp" => "RunMat:damp:InvalidModel",
1173 "rlocus" => "RunMat:rlocus:InvalidModel",
1174 "isstable" => "RunMat:isstable:InvalidModel",
1175 _ => "RunMat:tf:InvalidSampleTime",
1176 }
1177}
1178
1179fn invalid_model_identifier(builtin: &str) -> &'static str {
1180 match builtin {
1181 "feedback" => "RunMat:feedback:InvalidModel",
1182 "stepinfo" => "RunMat:stepinfo:InvalidSystem",
1183 "dcgain" => "RunMat:dcgain:InvalidModel",
1184 "pole" => "RunMat:pole:InvalidModel",
1185 "zero" => "RunMat:zero:InvalidModel",
1186 "damp" => "RunMat:damp:InvalidModel",
1187 "rlocus" => "RunMat:rlocus:InvalidModel",
1188 "isstable" => "RunMat:isstable:InvalidModel",
1189 _ => "RunMat:tf:InvalidModel",
1190 }
1191}
1192
1193fn unsupported_model_identifier(builtin: &str) -> &'static str {
1194 match builtin {
1195 "feedback" => "RunMat:feedback:UnsupportedModel",
1196 "stepinfo" => "RunMat:stepinfo:UnsupportedModel",
1197 "dcgain" => "RunMat:dcgain:UnsupportedModel",
1198 "pole" => "RunMat:pole:UnsupportedModel",
1199 "zero" => "RunMat:zero:UnsupportedModel",
1200 "damp" => "RunMat:damp:UnsupportedModel",
1201 "rlocus" => "RunMat:rlocus:UnsupportedModel",
1202 "isstable" => "RunMat:isstable:UnsupportedModel",
1203 _ => "RunMat:tf:UnsupportedModel",
1204 }
1205}
1206
1207fn internal_identifier(builtin: &str) -> &'static str {
1208 match builtin {
1209 "feedback" => "RunMat:feedback:Internal",
1210 "stepinfo" => "RunMat:stepinfo:Internal",
1211 "dcgain" => "RunMat:dcgain:Internal",
1212 "pole" => "RunMat:pole:Internal",
1213 "zero" => "RunMat:zero:Internal",
1214 "damp" => "RunMat:damp:Internal",
1215 "rlocus" => "RunMat:rlocus:Internal",
1216 "isstable" => "RunMat:isstable:Internal",
1217 _ => "RunMat:tf:Internal",
1218 }
1219}