1use 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}