Skip to main content

runmat_runtime/builtins/control/
tf_model.rs

1//! Shared SISO transfer-function object parsing, construction, and algebra.
2use 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                // Direct division by a zero complex denominator produces a spurious NaN
422                // component. Factor the pole so the infinite result retains the local
423                // signed/complex-axis direction of the positive-real approach to the DC point.
424                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    // Synthetic division by (x - point) removes one zero at the evaluation point per pass.
922    let mut quotient = coeffs.to_vec();
923    while quotient.len() > 1 && poly_eval(&quotient, 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(&quotient, 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}