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