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