Skip to main content

runmat_runtime/builtins/control/
tf.rs

1//! MATLAB-compatible `tf` transfer-function constructor and SISO operator methods for RunMat.
2
3use runmat_builtins::{
4    BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
5    BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor, Value,
6};
7use runmat_macros::runtime_builtin;
8
9use crate::builtins::common::spec::{
10    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
11    ReductionNaN, ResidencyPolicy, ShapeRequirements,
12};
13use crate::builtins::control::tf_model::{
14    control_error, is_discrete_variable, parse_coefficients, scalar_f64, scalar_text,
15    two_models_ordered, validate_sample_time, validate_variable, validate_variable_domain, TfModel,
16    TfOptions,
17};
18use crate::builtins::control::type_resolvers::tf_type;
19use crate::{build_runtime_error, dispatcher, BuiltinResult, RuntimeError};
20
21const BUILTIN_NAME: &str = "tf";
22const DEFAULT_VARIABLE: &str = "s";
23
24const TF_OUTPUT_SYS: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
25    name: "sys",
26    ty: BuiltinParamType::Any,
27    arity: BuiltinParamArity::Required,
28    default: None,
29    description: "SISO transfer-function object.",
30}];
31const TF_PARAM_NUMERATOR: BuiltinParamDescriptor = BuiltinParamDescriptor {
32    name: "numerator",
33    ty: BuiltinParamType::Any,
34    arity: BuiltinParamArity::Required,
35    default: None,
36    description: "Numerator coefficient vector.",
37};
38const TF_PARAM_DENOMINATOR: BuiltinParamDescriptor = BuiltinParamDescriptor {
39    name: "denominator",
40    ty: BuiltinParamType::Any,
41    arity: BuiltinParamArity::Required,
42    default: None,
43    description: "Denominator coefficient vector.",
44};
45const TF_PARAM_VARIABLE_SYMBOL: BuiltinParamDescriptor = BuiltinParamDescriptor {
46    name: "variable",
47    ty: BuiltinParamType::StringScalar,
48    arity: BuiltinParamArity::Required,
49    default: None,
50    description: "Transfer-function indeterminate ('s', 'p', 'z', 'q', 'z^-1', or 'q^-1').",
51};
52const TF_PARAM_TS: BuiltinParamDescriptor = BuiltinParamDescriptor {
53    name: "Ts",
54    ty: BuiltinParamType::NumericScalar,
55    arity: BuiltinParamArity::Optional,
56    default: Some("0.0"),
57    description: "Sample time (0 for continuous-time model).",
58};
59const TF_INPUTS_VARIABLE: [BuiltinParamDescriptor; 1] = [TF_PARAM_VARIABLE_SYMBOL];
60const TF_INPUTS_VARIABLE_TS: [BuiltinParamDescriptor; 2] = [TF_PARAM_VARIABLE_SYMBOL, TF_PARAM_TS];
61const TF_INPUTS_NUM_DEN: [BuiltinParamDescriptor; 2] = [TF_PARAM_NUMERATOR, TF_PARAM_DENOMINATOR];
62const TF_INPUTS_NUM_DEN_TS: [BuiltinParamDescriptor; 3] =
63    [TF_PARAM_NUMERATOR, TF_PARAM_DENOMINATOR, TF_PARAM_TS];
64const TF_INPUTS_NUM_DEN_NAMEVALUE: [BuiltinParamDescriptor; 4] = [
65    TF_PARAM_NUMERATOR,
66    TF_PARAM_DENOMINATOR,
67    BuiltinParamDescriptor {
68        name: "name",
69        ty: BuiltinParamType::StringScalar,
70        arity: BuiltinParamArity::Variadic,
71        default: None,
72        description: "Option name ('Variable' or 'Ts').",
73    },
74    BuiltinParamDescriptor {
75        name: "value",
76        ty: BuiltinParamType::Any,
77        arity: BuiltinParamArity::Variadic,
78        default: None,
79        description: "Option value.",
80    },
81];
82const TF_SIGNATURES: [BuiltinSignatureDescriptor; 6] = [
83    BuiltinSignatureDescriptor {
84        label: "s = tf('s')",
85        inputs: &TF_INPUTS_VARIABLE,
86        outputs: &TF_OUTPUT_SYS,
87    },
88    BuiltinSignatureDescriptor {
89        label: "z = tf('z', Ts)",
90        inputs: &TF_INPUTS_VARIABLE_TS,
91        outputs: &TF_OUTPUT_SYS,
92    },
93    BuiltinSignatureDescriptor {
94        label: "sys = tf(numerator, denominator)",
95        inputs: &TF_INPUTS_NUM_DEN,
96        outputs: &TF_OUTPUT_SYS,
97    },
98    BuiltinSignatureDescriptor {
99        label: "sys = tf(numerator, denominator, Ts)",
100        inputs: &TF_INPUTS_NUM_DEN_TS,
101        outputs: &TF_OUTPUT_SYS,
102    },
103    BuiltinSignatureDescriptor {
104        label: "sys = tf(numerator, denominator, \"Variable\", variableName)",
105        inputs: &TF_INPUTS_NUM_DEN_NAMEVALUE,
106        outputs: &TF_OUTPUT_SYS,
107    },
108    BuiltinSignatureDescriptor {
109        label: "sys = tf(numerator, denominator, name, value, ...)",
110        inputs: &TF_INPUTS_NUM_DEN_NAMEVALUE,
111        outputs: &TF_OUTPUT_SYS,
112    },
113];
114const TF_ERROR_INVALID_ARGUMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
115    code: "RM.TF.INVALID_ARGUMENT",
116    identifier: Some("RunMat:tf:InvalidArgument"),
117    when: "Arguments do not match supported tf invocation forms.",
118    message: "tf: invalid argument",
119};
120const TF_ERROR_INVALID_OPTION: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
121    code: "RM.TF.INVALID_OPTION",
122    identifier: Some("RunMat:tf:InvalidOption"),
123    when: "A name/value option token is unsupported or malformed.",
124    message: "tf: invalid option",
125};
126const TF_ERROR_INVALID_SAMPLE_TIME: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
127    code: "RM.TF.INVALID_SAMPLE_TIME",
128    identifier: Some("RunMat:tf:InvalidSampleTime"),
129    when: "Sample time is not a finite non-negative scalar.",
130    message: "tf: sample time must be a finite non-negative scalar",
131};
132const TF_ERROR_INVALID_VARIABLE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
133    code: "RM.TF.INVALID_VARIABLE",
134    identifier: Some("RunMat:tf:InvalidVariable"),
135    when: "Variable option is not a supported control variable name.",
136    message: "tf: invalid Variable option",
137};
138const TF_ERROR_INVALID_COEFFICIENTS: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
139    code: "RM.TF.INVALID_COEFFICIENTS",
140    identifier: Some("RunMat:tf:InvalidCoefficients"),
141    when: "Numerator/denominator coefficients are not valid finite vectors.",
142    message: "tf: invalid coefficients",
143};
144const TF_ERROR_DENOMINATOR_INVALID: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
145    code: "RM.TF.DENOMINATOR_INVALID",
146    identifier: Some("RunMat:tf:DenominatorInvalid"),
147    when: "Denominator coefficient vector is empty or all zeros.",
148    message: "tf: invalid denominator coefficients",
149};
150const TF_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
151    code: "RM.TF.INTERNAL",
152    identifier: Some("RunMat:tf:Internal"),
153    when: "Internal tensor/object construction failed.",
154    message: "tf: internal error",
155};
156const TF_ERRORS: [BuiltinErrorDescriptor; 7] = [
157    TF_ERROR_INVALID_ARGUMENT,
158    TF_ERROR_INVALID_OPTION,
159    TF_ERROR_INVALID_SAMPLE_TIME,
160    TF_ERROR_INVALID_VARIABLE,
161    TF_ERROR_INVALID_COEFFICIENTS,
162    TF_ERROR_DENOMINATOR_INVALID,
163    TF_ERROR_INTERNAL,
164];
165pub const TF_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
166    signatures: &TF_SIGNATURES,
167    output_mode: BuiltinOutputMode::Fixed,
168    completion_policy: BuiltinCompletionPolicy::Public,
169    errors: &TF_ERRORS,
170};
171
172const TF_METHOD_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
173    name: "sys",
174    ty: BuiltinParamType::Any,
175    arity: BuiltinParamArity::Required,
176    default: None,
177    description: "Resulting SISO transfer-function object.",
178}];
179const TF_METHOD_INPUTS_BINARY: [BuiltinParamDescriptor; 2] = [
180    BuiltinParamDescriptor {
181        name: "lhs",
182        ty: BuiltinParamType::Any,
183        arity: BuiltinParamArity::Required,
184        default: None,
185        description: "Left operand.",
186    },
187    BuiltinParamDescriptor {
188        name: "rhs",
189        ty: BuiltinParamType::Any,
190        arity: BuiltinParamArity::Required,
191        default: None,
192        description: "Right operand.",
193    },
194];
195const TF_METHOD_INPUTS_UNARY: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
196    name: "sys",
197    ty: BuiltinParamType::Any,
198    arity: BuiltinParamArity::Required,
199    default: None,
200    description: "Operand.",
201}];
202const TF_METHOD_SIGNATURES_BINARY: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
203    label: "sys = tf.operator(lhs, rhs)",
204    inputs: &TF_METHOD_INPUTS_BINARY,
205    outputs: &TF_METHOD_OUTPUT,
206}];
207const TF_METHOD_SIGNATURES_UNARY: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
208    label: "sys = tf.operator(sys)",
209    inputs: &TF_METHOD_INPUTS_UNARY,
210    outputs: &TF_METHOD_OUTPUT,
211}];
212const TF_METHOD_ERROR: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
213    code: "RM.TF.OPERATOR",
214    identifier: Some("RunMat:tf:OperatorError"),
215    when: "Transfer-function operator arguments are invalid or incompatible.",
216    message: "tf operator failed",
217};
218const TF_METHOD_ERRORS: [BuiltinErrorDescriptor; 1] = [TF_METHOD_ERROR];
219pub const TF_METHOD_BINARY_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
220    signatures: &TF_METHOD_SIGNATURES_BINARY,
221    output_mode: BuiltinOutputMode::Fixed,
222    completion_policy: BuiltinCompletionPolicy::MethodOnly,
223    errors: &TF_METHOD_ERRORS,
224};
225pub const TF_METHOD_UNARY_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
226    signatures: &TF_METHOD_SIGNATURES_UNARY,
227    output_mode: BuiltinOutputMode::Fixed,
228    completion_policy: BuiltinCompletionPolicy::MethodOnly,
229    errors: &TF_METHOD_ERRORS,
230};
231
232#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::control::tf")]
233pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
234    name: "tf",
235    op_kind: GpuOpKind::Custom("transfer-function-constructor"),
236    supported_precisions: &[],
237    broadcast: BroadcastSemantics::None,
238    provider_hooks: &[],
239    constant_strategy: ConstantStrategy::InlineLiteral,
240    residency: ResidencyPolicy::GatherImmediately,
241    nan_mode: ReductionNaN::Include,
242    two_pass_threshold: None,
243    workgroup_size: None,
244    accepts_nan_mode: false,
245    notes: "Object construction runs on the host. gpuArray coefficient inputs are gathered before storing the transfer-function metadata.",
246};
247
248#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::control::tf")]
249pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
250    name: "tf",
251    shape: ShapeRequirements::Any,
252    constant_strategy: ConstantStrategy::InlineLiteral,
253    elementwise: None,
254    reduction: None,
255    emits_nan: false,
256    notes: "Transfer-function construction is metadata-only and terminates numeric fusion chains.",
257};
258
259fn tf_error(error: &'static BuiltinErrorDescriptor) -> RuntimeError {
260    tf_error_with_message(error.message, error)
261}
262
263fn tf_error_with_detail(
264    error: &'static BuiltinErrorDescriptor,
265    detail: impl AsRef<str>,
266) -> RuntimeError {
267    tf_error_with_message(format!("{}: {}", error.message, detail.as_ref()), error)
268}
269
270fn tf_error_with_message(
271    message: impl Into<String>,
272    error: &'static BuiltinErrorDescriptor,
273) -> RuntimeError {
274    let mut builder = build_runtime_error(message).with_builtin(BUILTIN_NAME);
275    if let Some(identifier) = error.identifier {
276        builder = builder.with_identifier(identifier);
277    }
278    builder.build()
279}
280
281#[runtime_builtin(
282    name = "tf",
283    category = "control",
284    summary = "Create SISO transfer-function objects from numerator and denominator coefficients.",
285    keywords = "tf,transfer function,control system,filter,polynomial",
286    type_resolver(tf_type),
287    descriptor(crate::builtins::control::tf::TF_DESCRIPTOR),
288    builtin_path = "crate::builtins::control::tf"
289)]
290async fn tf_builtin(args: Vec<Value>) -> BuiltinResult<Value> {
291    match args.as_slice() {
292        [variable] if is_text_value(variable) => variable_model(variable, None)?.to_value("tf"),
293        [variable, sample_time] if is_text_value(variable) => {
294            variable_model(variable, Some(sample_time))?.to_value("tf")
295        }
296        [numerator, denominator, rest @ ..] => {
297            let options = TfConstructorOptions::parse(rest)?;
298            let numerator = parse_coefficients("numerator", numerator.clone(), "tf").await?;
299            let denominator = parse_coefficients("denominator", denominator.clone(), "tf").await?;
300            TfModel::new(
301                numerator,
302                denominator,
303                TfOptions {
304                    variable: options.variable,
305                    sample_time: options.sample_time,
306                },
307            )?
308            .to_value("tf")
309        }
310        [] => Err(tf_error_with_detail(
311            &TF_ERROR_INVALID_ARGUMENT,
312            "expected tf('s'), tf(num, den), or tf(num, den, ...)",
313        )),
314        _ => Err(tf_error_with_detail(
315            &TF_ERROR_INVALID_ARGUMENT,
316            "unsupported tf invocation",
317        )),
318    }
319}
320
321#[derive(Clone)]
322struct TfConstructorOptions {
323    variable: String,
324    sample_time: f64,
325    variable_explicit: bool,
326    sample_time_explicit: bool,
327}
328
329impl TfConstructorOptions {
330    fn parse(rest: &[Value]) -> BuiltinResult<Self> {
331        let mut options = Self {
332            variable: DEFAULT_VARIABLE.to_string(),
333            sample_time: 0.0,
334            variable_explicit: false,
335            sample_time_explicit: false,
336        };
337
338        match rest {
339            [] => {}
340            [sample_time] => {
341                options.sample_time = parse_sample_time(sample_time)?;
342                options.sample_time_explicit = true;
343                if options.sample_time > 0.0 {
344                    options.variable = "z".to_string();
345                }
346            }
347            _ => {
348                if !rest.len().is_multiple_of(2) {
349                    return Err(tf_error_with_detail(
350                        &TF_ERROR_INVALID_ARGUMENT,
351                        "optional arguments must be name-value pairs or a scalar sample time",
352                    ));
353                }
354                let mut idx = 0;
355                while idx < rest.len() {
356                    let name = scalar_text(&rest[idx], "option name", "tf")?;
357                    let lowered = name.trim().to_ascii_lowercase();
358                    let value = &rest[idx + 1];
359                    match lowered.as_str() {
360                        "variable" => {
361                            options.variable = parse_variable(value)?;
362                            options.variable_explicit = true;
363                        }
364                        "ts" | "sampletime" => {
365                            options.sample_time = parse_sample_time(value)?;
366                            options.sample_time_explicit = true;
367                        }
368                        _ => {
369                            return Err(tf_error_with_detail(
370                                &TF_ERROR_INVALID_OPTION,
371                                format!("unsupported option '{name}'"),
372                            ));
373                        }
374                    }
375                    idx += 2;
376                }
377                if options.sample_time > 0.0 && !options.variable_explicit {
378                    options.variable = "z".to_string();
379                }
380            }
381        }
382
383        if options.variable_explicit
384            && is_discrete_variable(&options.variable)
385            && !options.sample_time_explicit
386        {
387            options.sample_time = 1.0;
388        }
389        validate_variable_domain(&options.variable, options.sample_time, "tf").map_err(|err| {
390            let identifier = err.identifier();
391            if identifier == TF_ERROR_INVALID_SAMPLE_TIME.identifier {
392                tf_error_with_detail(&TF_ERROR_INVALID_SAMPLE_TIME, err.message())
393            } else {
394                tf_error_with_detail(&TF_ERROR_INVALID_VARIABLE, err.message())
395            }
396        })?;
397        Ok(options)
398    }
399}
400
401fn parse_sample_time(value: &Value) -> BuiltinResult<f64> {
402    let sample_time = scalar_f64(value, "sample time", "tf").map_err(|_| {
403        tf_error_with_detail(
404            &TF_ERROR_INVALID_SAMPLE_TIME,
405            format!("expected non-negative scalar, got {value:?}"),
406        )
407    })?;
408    if let Err(err) = validate_sample_time(sample_time, "tf") {
409        let _ = err;
410        return Err(tf_error(&TF_ERROR_INVALID_SAMPLE_TIME));
411    }
412    Ok(sample_time)
413}
414
415fn parse_variable(value: &Value) -> BuiltinResult<String> {
416    let variable = scalar_text(value, "Variable", "tf")?;
417    validate_variable(&variable, "tf").map_err(|_| {
418        tf_error_with_detail(
419            &TF_ERROR_INVALID_VARIABLE,
420            "must be one of 's', 'p', 'z', 'q', 'z^-1', or 'q^-1'",
421        )
422    })
423}
424
425fn is_text_value(value: &Value) -> bool {
426    match value {
427        Value::String(_) => true,
428        Value::StringArray(array) => array.data.len() == 1,
429        Value::CharArray(array) => array.rows == 1,
430        _ => false,
431    }
432}
433
434fn variable_model(value: &Value, sample_time: Option<&Value>) -> BuiltinResult<TfModel> {
435    let variable = parse_variable(value)?;
436    match variable.as_str() {
437        "s" | "p" => {
438            if let Some(sample_time) = sample_time {
439                let parsed = parse_sample_time(sample_time)?;
440                if parsed > 0.0 {
441                    return Err(tf_error_with_detail(
442                        &TF_ERROR_INVALID_SAMPLE_TIME,
443                        "continuous transfer-function variables require Ts = 0",
444                    ));
445                }
446            }
447            TfModel::continuous_variable(variable)
448        }
449        "z" | "q" | "z^-1" | "q^-1" => {
450            let sample_time = match sample_time {
451                Some(value) => parse_sample_time(value)?,
452                None => 1.0,
453            };
454            if sample_time <= 0.0 {
455                return Err(tf_error_with_detail(
456                    &TF_ERROR_INVALID_SAMPLE_TIME,
457                    "discrete transfer-function variables require a positive sample time",
458                ));
459            }
460            TfModel::discrete_variable(variable, sample_time)
461        }
462        _ => unreachable!("validated variable"),
463    }
464}
465
466async fn tf_binary(
467    lhs: Value,
468    rhs: Value,
469    op: fn(&TfModel, &TfModel) -> BuiltinResult<TfModel>,
470) -> BuiltinResult<Value> {
471    let (lhs, rhs) = two_models_ordered(lhs, rhs, "tf").await?;
472    op(&lhs, &rhs)?.to_value("tf")
473}
474
475fn parse_integer_exponent(value: &Value) -> BuiltinResult<i64> {
476    let exponent = scalar_f64(value, "exponent", "tf")?;
477    if !exponent.is_finite() || exponent.fract().abs() > 0.0 {
478        return Err(control_error(
479            "tf",
480            "RunMat:tf:InvalidExponent",
481            "tf: transfer-function powers require an integer scalar exponent",
482        ));
483    }
484    if exponent < i64::MIN as f64 || exponent > i64::MAX as f64 {
485        return Err(control_error(
486            "tf",
487            "RunMat:tf:InvalidExponent",
488            "tf: exponent exceeds integer range",
489        ));
490    }
491    Ok(exponent as i64)
492}
493
494#[runtime_builtin(
495    name = "tf.plus",
496    descriptor(crate::builtins::control::tf::TF_METHOD_BINARY_DESCRIPTOR),
497    builtin_path = "crate::builtins::control::tf"
498)]
499async fn tf_plus(lhs: Value, rhs: Value) -> BuiltinResult<Value> {
500    tf_binary(lhs, rhs, TfModel::add).await
501}
502
503#[runtime_builtin(
504    name = "tf.minus",
505    descriptor(crate::builtins::control::tf::TF_METHOD_BINARY_DESCRIPTOR),
506    builtin_path = "crate::builtins::control::tf"
507)]
508async fn tf_minus(lhs: Value, rhs: Value) -> BuiltinResult<Value> {
509    tf_binary(lhs, rhs, TfModel::sub).await
510}
511
512#[runtime_builtin(
513    name = "tf.uplus",
514    descriptor(crate::builtins::control::tf::TF_METHOD_UNARY_DESCRIPTOR),
515    builtin_path = "crate::builtins::control::tf"
516)]
517async fn tf_uplus(sys: Value) -> BuiltinResult<Value> {
518    TfModel::from_value_async(sys, "tf").await?.to_value("tf")
519}
520
521#[runtime_builtin(
522    name = "tf.uminus",
523    descriptor(crate::builtins::control::tf::TF_METHOD_UNARY_DESCRIPTOR),
524    builtin_path = "crate::builtins::control::tf"
525)]
526async fn tf_uminus(sys: Value) -> BuiltinResult<Value> {
527    TfModel::from_value_async(sys, "tf")
528        .await?
529        .neg()?
530        .to_value("tf")
531}
532
533#[runtime_builtin(
534    name = "tf.times",
535    descriptor(crate::builtins::control::tf::TF_METHOD_BINARY_DESCRIPTOR),
536    builtin_path = "crate::builtins::control::tf"
537)]
538async fn tf_times(lhs: Value, rhs: Value) -> BuiltinResult<Value> {
539    tf_binary(lhs, rhs, TfModel::mul).await
540}
541
542#[runtime_builtin(
543    name = "tf.mtimes",
544    descriptor(crate::builtins::control::tf::TF_METHOD_BINARY_DESCRIPTOR),
545    builtin_path = "crate::builtins::control::tf"
546)]
547async fn tf_mtimes(lhs: Value, rhs: Value) -> BuiltinResult<Value> {
548    tf_binary(lhs, rhs, TfModel::mul).await
549}
550
551#[runtime_builtin(
552    name = "tf.rdivide",
553    descriptor(crate::builtins::control::tf::TF_METHOD_BINARY_DESCRIPTOR),
554    builtin_path = "crate::builtins::control::tf"
555)]
556async fn tf_rdivide(lhs: Value, rhs: Value) -> BuiltinResult<Value> {
557    tf_binary(lhs, rhs, TfModel::div).await
558}
559
560#[runtime_builtin(
561    name = "tf.mrdivide",
562    descriptor(crate::builtins::control::tf::TF_METHOD_BINARY_DESCRIPTOR),
563    builtin_path = "crate::builtins::control::tf"
564)]
565async fn tf_mrdivide(lhs: Value, rhs: Value) -> BuiltinResult<Value> {
566    tf_binary(lhs, rhs, TfModel::div).await
567}
568
569#[runtime_builtin(
570    name = "tf.ldivide",
571    descriptor(crate::builtins::control::tf::TF_METHOD_BINARY_DESCRIPTOR),
572    builtin_path = "crate::builtins::control::tf"
573)]
574async fn tf_ldivide(lhs: Value, rhs: Value) -> BuiltinResult<Value> {
575    tf_binary(rhs, lhs, TfModel::div).await
576}
577
578#[runtime_builtin(
579    name = "tf.mldivide",
580    descriptor(crate::builtins::control::tf::TF_METHOD_BINARY_DESCRIPTOR),
581    builtin_path = "crate::builtins::control::tf"
582)]
583async fn tf_mldivide(lhs: Value, rhs: Value) -> BuiltinResult<Value> {
584    tf_binary(rhs, lhs, TfModel::div).await
585}
586
587#[runtime_builtin(
588    name = "tf.power",
589    descriptor(crate::builtins::control::tf::TF_METHOD_BINARY_DESCRIPTOR),
590    builtin_path = "crate::builtins::control::tf"
591)]
592async fn tf_power(lhs: Value, rhs: Value) -> BuiltinResult<Value> {
593    let sys = TfModel::from_value_async(lhs, "tf").await?;
594    let rhs = dispatcher::gather_if_needed_async(&rhs).await?;
595    sys.powi(parse_integer_exponent(&rhs)?)?.to_value("tf")
596}
597
598#[runtime_builtin(
599    name = "tf.mpower",
600    descriptor(crate::builtins::control::tf::TF_METHOD_BINARY_DESCRIPTOR),
601    builtin_path = "crate::builtins::control::tf"
602)]
603async fn tf_mpower(lhs: Value, rhs: Value) -> BuiltinResult<Value> {
604    tf_power(lhs, rhs).await
605}
606
607#[cfg(test)]
608mod tests {
609    use super::*;
610    use futures::executor::block_on;
611    use runmat_builtins::{CharArray, IntValue, Tensor};
612
613    fn run_tf(numerator: Value, denominator: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
614        let mut args = vec![numerator, denominator];
615        args.extend(rest);
616        block_on(tf_builtin(args))
617    }
618
619    fn run_tf_args(args: Vec<Value>) -> BuiltinResult<Value> {
620        block_on(tf_builtin(args))
621    }
622
623    fn property<'a>(value: &'a Value, name: &str) -> &'a Value {
624        let Value::Object(object) = value else {
625            panic!("expected object, got {value:?}");
626        };
627        object
628            .properties
629            .get(name)
630            .unwrap_or_else(|| panic!("missing property {name}"))
631    }
632
633    fn tensor_property(value: &Value, name: &str) -> Vec<f64> {
634        match property(value, name) {
635            Value::Tensor(tensor) => tensor.data.clone(),
636            other => panic!("expected tensor property {name}, got {other:?}"),
637        }
638    }
639
640    #[test]
641    fn tf_descriptor_signatures_cover_core_forms() {
642        let labels: Vec<&str> = TF_DESCRIPTOR
643            .signatures
644            .iter()
645            .map(|sig| sig.label)
646            .collect();
647        assert!(labels.contains(&"s = tf('s')"));
648        assert!(labels.contains(&"z = tf('z', Ts)"));
649        assert!(labels.contains(&"sys = tf(numerator, denominator)"));
650        assert!(labels.contains(&"sys = tf(numerator, denominator, Ts)"));
651        assert!(labels.contains(&"sys = tf(numerator, denominator, \"Variable\", variableName)"));
652        assert!(labels.contains(&"sys = tf(numerator, denominator, name, value, ...)"));
653    }
654
655    #[test]
656    fn tf_constructs_continuous_siso_object() {
657        let sys = run_tf(
658            Value::Num(20.0),
659            Value::Tensor(Tensor::new(vec![1.0, 5.0], vec![1, 2]).unwrap()),
660            Vec::new(),
661        )
662        .expect("tf");
663
664        let Value::Object(object) = &sys else {
665            panic!("expected object");
666        };
667        assert_eq!(object.class_name, "tf");
668        assert_eq!(
669            property(&sys, "Variable"),
670            &Value::CharArray(CharArray::new_row("s"))
671        );
672        assert_eq!(property(&sys, "Ts"), &Value::Num(0.0));
673        match property(&sys, "Numerator") {
674            Value::Tensor(tensor) => {
675                assert_eq!(tensor.shape, vec![1, 1]);
676                assert_eq!(tensor.data, vec![20.0]);
677            }
678            other => panic!("expected numerator tensor, got {other:?}"),
679        }
680        match property(&sys, "Denominator") {
681            Value::Tensor(tensor) => {
682                assert_eq!(tensor.shape, vec![1, 2]);
683                assert_eq!(tensor.data, vec![1.0, 5.0]);
684            }
685            other => panic!("expected denominator tensor, got {other:?}"),
686        }
687    }
688
689    #[test]
690    fn tf_accepts_continuous_variable_constructor() {
691        let sys = run_tf_args(vec![Value::from("s")]).expect("tf('s')");
692        assert_eq!(
693            property(&sys, "Variable"),
694            &Value::CharArray(CharArray::new_row("s"))
695        );
696        assert_eq!(property(&sys, "Ts"), &Value::Num(0.0));
697        assert_eq!(tensor_property(&sys, "Numerator"), vec![1.0, 0.0]);
698        assert_eq!(tensor_property(&sys, "Denominator"), vec![1.0]);
699    }
700
701    #[test]
702    fn tf_accepts_discrete_variable_constructor() {
703        let sys = run_tf_args(vec![Value::from("z"), Value::Num(0.2)]).expect("tf('z', Ts)");
704        assert_eq!(
705            property(&sys, "Variable"),
706            &Value::CharArray(CharArray::new_row("z"))
707        );
708        assert_eq!(property(&sys, "Ts"), &Value::Num(0.2));
709        assert_eq!(tensor_property(&sys, "Numerator"), vec![1.0, 0.0]);
710        assert_eq!(tensor_property(&sys, "Denominator"), vec![1.0]);
711    }
712
713    #[test]
714    fn tf_rejects_continuous_variable_constructor_with_positive_sample_time() {
715        let err = run_tf_args(vec![Value::from("s"), Value::Num(0.2)])
716            .expect_err("tf('s', positive Ts) should fail");
717        assert_eq!(err.identifier(), TF_ERROR_INVALID_SAMPLE_TIME.identifier);
718    }
719
720    #[test]
721    fn tf_arithmetic_builds_polynomial_transfer_functions() {
722        let s = run_tf_args(vec![Value::from("s")]).expect("tf('s')");
723        let s_squared = block_on(tf_power(s.clone(), Value::Num(2.0))).expect("s^2");
724        let quadratic = block_on(tf_plus(
725            block_on(tf_plus(
726                block_on(tf_mtimes(Value::Num(0.4), s_squared)).expect("0.4*s^2"),
727                block_on(tf_mtimes(Value::Num(1.8), s.clone())).expect("1.8*s"),
728            ))
729            .expect("sum terms"),
730            Value::Num(1.0),
731        ))
732        .expect("add constant");
733        let g = block_on(tf_mrdivide(Value::Num(2.5), quadratic)).expect("2.5/poly");
734
735        assert_eq!(tensor_property(&g, "Numerator"), vec![2.5]);
736        assert_eq!(tensor_property(&g, "Denominator"), vec![0.4, 1.8, 1.0]);
737    }
738
739    #[test]
740    fn tf_normalizes_column_coefficients_to_rows() {
741        let sys = run_tf(
742            Value::Tensor(Tensor::new(vec![1.0, 2.0], vec![2, 1]).unwrap()),
743            Value::Tensor(Tensor::new(vec![1.0, 3.0, 2.0], vec![3, 1]).unwrap()),
744            Vec::new(),
745        )
746        .expect("tf");
747
748        match property(&sys, "Numerator") {
749            Value::Tensor(tensor) => {
750                assert_eq!(tensor.shape, vec![1, 2]);
751                assert_eq!(tensor.data, vec![1.0, 2.0]);
752            }
753            other => panic!("expected numerator tensor, got {other:?}"),
754        }
755        match property(&sys, "Denominator") {
756            Value::Tensor(tensor) => {
757                assert_eq!(tensor.shape, vec![1, 3]);
758                assert_eq!(tensor.data, vec![1.0, 3.0, 2.0]);
759            }
760            other => panic!("expected denominator tensor, got {other:?}"),
761        }
762    }
763
764    #[test]
765    fn tf_accepts_discrete_sample_time() {
766        let sys = run_tf(
767            Value::Int(IntValue::I32(1)),
768            Value::Tensor(Tensor::new(vec![1.0, -0.5], vec![1, 2]).unwrap()),
769            vec![Value::Num(0.1)],
770        )
771        .expect("tf");
772
773        assert_eq!(
774            property(&sys, "Variable"),
775            &Value::CharArray(CharArray::new_row("z"))
776        );
777        assert_eq!(property(&sys, "Ts"), &Value::Num(0.1));
778    }
779
780    #[test]
781    fn tf_positional_zero_sample_time_remains_continuous() {
782        let sys = run_tf(
783            Value::Int(IntValue::I32(1)),
784            Value::Tensor(Tensor::new(vec![1.0, 5.0], vec![1, 2]).unwrap()),
785            vec![Value::Num(0.0)],
786        )
787        .expect("tf");
788
789        assert_eq!(
790            property(&sys, "Variable"),
791            &Value::CharArray(CharArray::new_row("s"))
792        );
793        assert_eq!(property(&sys, "Ts"), &Value::Num(0.0));
794    }
795
796    #[test]
797    fn tf_accepts_variable_name_value_option() {
798        let sys = run_tf(
799            Value::Num(1.0),
800            Value::Tensor(Tensor::new(vec![1.0, 1.0], vec![1, 2]).unwrap()),
801            vec![Value::from("Variable"), Value::from("p")],
802        )
803        .expect("tf");
804
805        assert_eq!(
806            property(&sys, "Variable"),
807            &Value::CharArray(CharArray::new_row("p"))
808        );
809    }
810
811    #[test]
812    fn tf_explicit_discrete_variable_defaults_positive_sample_time() {
813        let sys = run_tf(
814            Value::Num(1.0),
815            Value::Tensor(Tensor::new(vec![1.0, 1.0], vec![1, 2]).unwrap()),
816            vec![Value::from("Variable"), Value::from("z")],
817        )
818        .expect("tf");
819
820        assert_eq!(
821            property(&sys, "Variable"),
822            &Value::CharArray(CharArray::new_row("z"))
823        );
824        assert_eq!(property(&sys, "Ts"), &Value::Num(1.0));
825    }
826
827    #[test]
828    fn tf_rejects_explicit_continuous_variable_with_positive_sample_time() {
829        let err = run_tf(
830            Value::Num(1.0),
831            Value::Tensor(Tensor::new(vec![1.0, 1.0], vec![1, 2]).unwrap()),
832            vec![
833                Value::from("Variable"),
834                Value::from("s"),
835                Value::from("Ts"),
836                Value::Num(0.5),
837            ],
838        )
839        .expect_err("continuous variable with positive Ts should fail");
840
841        assert_eq!(err.identifier(), TF_ERROR_INVALID_VARIABLE.identifier);
842    }
843
844    #[test]
845    fn tf_rejects_explicit_discrete_variable_with_zero_sample_time() {
846        let err = run_tf(
847            Value::Num(1.0),
848            Value::Tensor(Tensor::new(vec![1.0, 1.0], vec![1, 2]).unwrap()),
849            vec![
850                Value::from("Variable"),
851                Value::from("z"),
852                Value::from("Ts"),
853                Value::Num(0.0),
854            ],
855        )
856        .expect_err("discrete variable with zero Ts should fail");
857
858        assert_eq!(err.identifier(), TF_ERROR_INVALID_SAMPLE_TIME.identifier);
859    }
860
861    #[test]
862    fn tf_accepts_explicit_discrete_variable_with_positive_sample_time() {
863        let sys = run_tf(
864            Value::Num(1.0),
865            Value::Tensor(Tensor::new(vec![1.0, 1.0], vec![1, 2]).unwrap()),
866            vec![
867                Value::from("Variable"),
868                Value::from("z"),
869                Value::from("Ts"),
870                Value::Num(0.5),
871            ],
872        )
873        .expect("tf");
874
875        assert_eq!(
876            property(&sys, "Variable"),
877            &Value::CharArray(CharArray::new_row("z"))
878        );
879        assert_eq!(property(&sys, "Ts"), &Value::Num(0.5));
880    }
881
882    #[test]
883    fn tf_rejects_zero_denominator() {
884        let err = run_tf(
885            Value::Num(1.0),
886            Value::Tensor(Tensor::new(vec![0.0, 0.0], vec![1, 2]).unwrap()),
887            Vec::new(),
888        )
889        .expect_err("zero denominator should fail");
890        assert!(err.message().contains("must not all be zero"));
891        assert_eq!(err.identifier(), TF_ERROR_DENOMINATOR_INVALID.identifier);
892    }
893
894    #[test]
895    fn tf_rejects_matrix_coefficients() {
896        let err = run_tf(
897            Value::Tensor(Tensor::new(vec![1.0, 2.0, 3.0, 4.0], vec![2, 2]).unwrap()),
898            Value::Tensor(Tensor::new(vec![1.0, 5.0], vec![1, 2]).unwrap()),
899            Vec::new(),
900        )
901        .expect_err("matrix numerator should fail");
902        assert!(err
903            .message()
904            .contains("numerator coefficients must be a vector"));
905        assert_eq!(err.identifier(), TF_ERROR_INVALID_COEFFICIENTS.identifier);
906    }
907}