1use runmat_builtins::{
13 BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinExtensionDescriptor,
14 BuiltinExtensionMode, BuiltinIntegerBackendRule, BuiltinIntegerCapabilityDescriptor,
15 BuiltinIntegerComputationDomain, BuiltinIntegerInputAvailability,
16 BuiltinIntegerInputCapability, BuiltinIntegerOutputClassRule, BuiltinIntegerOverflowRule,
17 BuiltinIntegerOverloadKind, BuiltinIntegerScalarDoubleRule, BuiltinOutputMode,
18 BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
19};
20use runmat_macros::runtime_builtin;
21use runmat_value::{StructValue, Tensor, Value};
22
23use crate::builtins::common::spec::{
24 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
25 ReductionNaN, ResidencyPolicy, ShapeRequirements,
26};
27use crate::builtins::common::tensor;
28use crate::builtins::math::optim::brent::{
29 brent_zero, BrentParams, BrentZeroBracket, BrentZeroObserver, BrentZeroResult,
30 BrentZeroStepKind,
31};
32use crate::builtins::math::optim::common::{
33 call_scalar_function_with_precision, call_scalar_function_with_precision_info, option_f64,
34 option_string, option_usize,
35};
36use crate::builtins::math::optim::type_resolvers::scalar_root_type;
37use crate::{build_runtime_error, BuiltinResult, RuntimeError};
38
39const NAME: &str = "fzero";
40const ALGORITHM: &str = "bisection, interpolation";
41const DEFAULT_TOL_X: f64 = f64::EPSILON;
42const DEFAULT_MAX_ITER: usize = 400;
43const DEFAULT_MAX_FUN_EVALS: usize = 500;
44
45const FZERO_OUTPUT_X: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
46 name: "x",
47 ty: BuiltinParamType::NumericScalar,
48 arity: BuiltinParamArity::Required,
49 default: None,
50 description: "Estimated root location.",
51}];
52
53const FZERO_OUTPUT_X_FVAL: [BuiltinParamDescriptor; 2] = [
54 BuiltinParamDescriptor {
55 name: "x",
56 ty: BuiltinParamType::NumericScalar,
57 arity: BuiltinParamArity::Required,
58 default: None,
59 description: "Estimated root location.",
60 },
61 BuiltinParamDescriptor {
62 name: "fval",
63 ty: BuiltinParamType::NumericScalar,
64 arity: BuiltinParamArity::Required,
65 default: None,
66 description: "Function value at x.",
67 },
68];
69
70const FZERO_OUTPUT_X_FVAL_EXITFLAG: [BuiltinParamDescriptor; 3] = [
71 BuiltinParamDescriptor {
72 name: "x",
73 ty: BuiltinParamType::NumericScalar,
74 arity: BuiltinParamArity::Required,
75 default: None,
76 description: "Estimated root location.",
77 },
78 BuiltinParamDescriptor {
79 name: "fval",
80 ty: BuiltinParamType::NumericScalar,
81 arity: BuiltinParamArity::Required,
82 default: None,
83 description: "Function value at x.",
84 },
85 BuiltinParamDescriptor {
86 name: "exitflag",
87 ty: BuiltinParamType::NumericScalar,
88 arity: BuiltinParamArity::Required,
89 default: None,
90 description: "Convergence status code.",
91 },
92];
93
94const FZERO_OUTPUT_ALL: [BuiltinParamDescriptor; 4] = [
95 BuiltinParamDescriptor {
96 name: "x",
97 ty: BuiltinParamType::NumericScalar,
98 arity: BuiltinParamArity::Required,
99 default: None,
100 description: "Estimated root location.",
101 },
102 BuiltinParamDescriptor {
103 name: "fval",
104 ty: BuiltinParamType::NumericScalar,
105 arity: BuiltinParamArity::Required,
106 default: None,
107 description: "Function value at x.",
108 },
109 BuiltinParamDescriptor {
110 name: "exitflag",
111 ty: BuiltinParamType::NumericScalar,
112 arity: BuiltinParamArity::Required,
113 default: None,
114 description: "Convergence status code.",
115 },
116 BuiltinParamDescriptor {
117 name: "output",
118 ty: BuiltinParamType::Any,
119 arity: BuiltinParamArity::Required,
120 default: None,
121 description: "Iteration/function-count metadata struct.",
122 },
123];
124
125const FZERO_INPUTS_CORE: [BuiltinParamDescriptor; 2] = [
126 BuiltinParamDescriptor {
127 name: "fun",
128 ty: BuiltinParamType::Any,
129 arity: BuiltinParamArity::Required,
130 default: None,
131 description: "Scalar-valued callback.",
132 },
133 BuiltinParamDescriptor {
134 name: "x0",
135 ty: BuiltinParamType::Any,
136 arity: BuiltinParamArity::Required,
137 default: None,
138 description: "Initial point or two-element bracket.",
139 },
140];
141
142const FZERO_INPUTS_WITH_OPTIONS: [BuiltinParamDescriptor; 3] = [
143 BuiltinParamDescriptor {
144 name: "fun",
145 ty: BuiltinParamType::Any,
146 arity: BuiltinParamArity::Required,
147 default: None,
148 description: "Scalar-valued callback.",
149 },
150 BuiltinParamDescriptor {
151 name: "x0",
152 ty: BuiltinParamType::Any,
153 arity: BuiltinParamArity::Required,
154 default: None,
155 description: "Initial point or two-element bracket.",
156 },
157 BuiltinParamDescriptor {
158 name: "options",
159 ty: BuiltinParamType::Any,
160 arity: BuiltinParamArity::Optional,
161 default: None,
162 description: "Options struct from optimset.",
163 },
164];
165
166const FZERO_SIGNATURES: [BuiltinSignatureDescriptor; 8] = [
167 BuiltinSignatureDescriptor {
168 label: "x = fzero(fun, x0)",
169 inputs: &FZERO_INPUTS_CORE,
170 outputs: &FZERO_OUTPUT_X,
171 },
172 BuiltinSignatureDescriptor {
173 label: "x = fzero(fun, x0, options)",
174 inputs: &FZERO_INPUTS_WITH_OPTIONS,
175 outputs: &FZERO_OUTPUT_X,
176 },
177 BuiltinSignatureDescriptor {
178 label: "[x, fval] = fzero(fun, x0)",
179 inputs: &FZERO_INPUTS_CORE,
180 outputs: &FZERO_OUTPUT_X_FVAL,
181 },
182 BuiltinSignatureDescriptor {
183 label: "[x, fval] = fzero(fun, x0, options)",
184 inputs: &FZERO_INPUTS_WITH_OPTIONS,
185 outputs: &FZERO_OUTPUT_X_FVAL,
186 },
187 BuiltinSignatureDescriptor {
188 label: "[x, fval, exitflag] = fzero(fun, x0)",
189 inputs: &FZERO_INPUTS_CORE,
190 outputs: &FZERO_OUTPUT_X_FVAL_EXITFLAG,
191 },
192 BuiltinSignatureDescriptor {
193 label: "[x, fval, exitflag] = fzero(fun, x0, options)",
194 inputs: &FZERO_INPUTS_WITH_OPTIONS,
195 outputs: &FZERO_OUTPUT_X_FVAL_EXITFLAG,
196 },
197 BuiltinSignatureDescriptor {
198 label: "[x, fval, exitflag, output] = fzero(fun, x0)",
199 inputs: &FZERO_INPUTS_CORE,
200 outputs: &FZERO_OUTPUT_ALL,
201 },
202 BuiltinSignatureDescriptor {
203 label: "[x, fval, exitflag, output] = fzero(fun, x0, options)",
204 inputs: &FZERO_INPUTS_WITH_OPTIONS,
205 outputs: &FZERO_OUTPUT_ALL,
206 },
207];
208
209const FZERO_ERROR_INVALID_ARGUMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
210 code: "RM.FZERO.INVALID_ARGUMENT",
211 identifier: Some("RunMat:fzero:InvalidArgument"),
212 when: "Argument grammar/options struct are invalid.",
213 message: "fzero: invalid argument",
214};
215
216const FZERO_ERROR_INVALID_INPUT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
217 code: "RM.FZERO.INVALID_INPUT",
218 identifier: Some("RunMat:fzero:InvalidInput"),
219 when: "Callback/bracket/initial-point semantics are invalid.",
220 message: "fzero: invalid input",
221};
222
223const FZERO_ERROR_TOO_MANY_OUTPUTS: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
224 code: "RM.FZERO.TOO_MANY_OUTPUTS",
225 identifier: Some("RunMat:fzero:TooManyOutputs"),
226 when: "`fzero` is called with more than four requested output arguments.",
227 message: "fzero: too many output arguments",
228};
229
230const FZERO_ERRORS: [BuiltinErrorDescriptor; 3] = [
231 FZERO_ERROR_INVALID_ARGUMENT,
232 FZERO_ERROR_INVALID_INPUT,
233 FZERO_ERROR_TOO_MANY_OUTPUTS,
234];
235
236const FZERO_INTEGER_INITIAL_INPUTS: [BuiltinIntegerInputCapability; 1] =
237 [BuiltinIntegerInputCapability {
238 name: "x0 or [a,b]",
239 classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
240 availability: BuiltinIntegerInputAvailability::RunMatOnly,
241 scalar_double: BuiltinIntegerScalarDoubleRule::NotApplicable,
242 notes: "Typed-integer initial points and intervals are independently gated and every value must convert exactly to binary64.",
243 }];
244const FZERO_INTEGER_CALLBACK_INPUTS: [BuiltinIntegerInputCapability; 1] =
245 [BuiltinIntegerInputCapability {
246 name: "fun(x) result",
247 classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
248 availability: BuiltinIntegerInputAvailability::RunMatOnly,
249 scalar_double: BuiltinIntegerScalarDoubleRule::NotApplicable,
250 notes: "Typed-integer function values are gated before resident gather and must convert exactly to binary64.",
251 }];
252const FZERO_INTEGER_TOLERANCE_INPUTS: [BuiltinIntegerInputCapability; 1] =
253 [BuiltinIntegerInputCapability {
254 name: "TolX",
255 classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
256 availability: BuiltinIntegerInputAvailability::RunMatOnly,
257 scalar_double: BuiltinIntegerScalarDoubleRule::Allowed,
258 notes: "Typed-integer TolX is independently gated and must convert exactly to a positive binary64 scalar.",
259 }];
260const FZERO_INTEGER_COUNT_INPUTS: [BuiltinIntegerInputCapability; 2] = [
261 BuiltinIntegerInputCapability {
262 name: "MaxIter",
263 classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
264 availability: BuiltinIntegerInputAvailability::RunMatOnly,
265 scalar_double: BuiltinIntegerScalarDoubleRule::Allowed,
266 notes: "Typed-integer iteration counts are independently gated and decoded exactly through platform bounds.",
267 },
268 BuiltinIntegerInputCapability {
269 name: "MaxFunEvals",
270 classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
271 availability: BuiltinIntegerInputAvailability::RunMatOnly,
272 scalar_double: BuiltinIntegerScalarDoubleRule::Allowed,
273 notes: "Typed-integer evaluation counts are independently gated and decoded exactly through platform bounds.",
274 },
275];
276pub const FZERO_INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 4] = [
277 BuiltinIntegerCapabilityDescriptor {
278 form: "x = fzero(fun, integer_x0_or_interval, options)",
279 inputs: &FZERO_INTEGER_INITIAL_INPUTS,
280 computation_domain: BuiltinIntegerComputationDomain::FloatingPoint,
281 output_class: BuiltinIntegerOutputClassRule::FunctionSpecific,
282 overflow: BuiltinIntegerOverflowRule::Error,
283 backend: BuiltinIntegerBackendRule::GatherFallback,
284 overload: BuiltinIntegerOverloadKind::Multiple,
285 notes: "Documented x0 supports single or double, not typed-integer classes. Strict compatibility rejects typed integers; RunMat mode admits exact binary64 values and returns double x, fval, and exitflag. A documented single x0 separately yields single numeric outputs.",
286 },
287 BuiltinIntegerCapabilityDescriptor {
288 form: "fzero callback returns an integer function value",
289 inputs: &FZERO_INTEGER_CALLBACK_INPUTS,
290 computation_domain: BuiltinIntegerComputationDomain::FloatingPoint,
291 output_class: BuiltinIntegerOutputClassRule::FunctionSpecific,
292 overflow: BuiltinIntegerOverflowRule::Error,
293 backend: BuiltinIntegerBackendRule::GatherFallback,
294 overload: BuiltinIntegerOverloadKind::ScalarOnly,
295 notes: "Strict compatibility rejects typed function values before provider access. RunMat mode converts exact values at the binary64 solver boundary; numeric output precision remains determined by x0.",
296 },
297 BuiltinIntegerCapabilityDescriptor {
298 form: "fzero(..., options.TolX=integer)",
299 inputs: &FZERO_INTEGER_TOLERANCE_INPUTS,
300 computation_domain: BuiltinIntegerComputationDomain::FloatingPoint,
301 output_class: BuiltinIntegerOutputClassRule::FunctionSpecific,
302 overflow: BuiltinIntegerOverflowRule::Error,
303 backend: BuiltinIntegerBackendRule::HostOnly,
304 overload: BuiltinIntegerOverloadKind::StructuralParameter,
305 notes: "Typed-integer TolX is RunMat-only. It overrides the precision-specific default without changing output precision.",
306 },
307 BuiltinIntegerCapabilityDescriptor {
308 form: "fzero(..., options.MaxIter=integer, options.MaxFunEvals=integer)",
309 inputs: &FZERO_INTEGER_COUNT_INPUTS,
310 computation_domain: BuiltinIntegerComputationDomain::Structural,
311 output_class: BuiltinIntegerOutputClassRule::FunctionSpecific,
312 overflow: BuiltinIntegerOverflowRule::Error,
313 backend: BuiltinIntegerBackendRule::HostOnly,
314 overload: BuiltinIntegerOverloadKind::StructuralParameter,
315 notes: "The documented controls are integer-valued numeric scalars, not documented typed-integer classes. RunMat's typed forms preserve exact counts without a binary64 round trip.",
316 },
317];
318
319pub(crate) const FZERO_INPUT_NUMERIC_EXTENSION: BuiltinExtensionDescriptor =
320 BuiltinExtensionDescriptor {
321 id: "fzero-nonfloating-initial-point",
322 mode: BuiltinExtensionMode::RunMatOnly,
323 description:
324 "fzero with a typed-integer or logical initial point or interval is a RunMat extension",
325 error_identifier: Some("RunMat:compatibility:FzeroNumericInputExtension"),
326 };
327pub(crate) const FZERO_CALLBACK_NUMERIC_EXTENSION: BuiltinExtensionDescriptor =
328 BuiltinExtensionDescriptor {
329 id: "fzero-nonfloating-callback-output",
330 mode: BuiltinExtensionMode::RunMatOnly,
331 description: "fzero with typed-integer or logical function values is a RunMat extension",
332 error_identifier: Some("RunMat:compatibility:FzeroCallbackExtension"),
333 };
334pub(crate) const FZERO_OPTION_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
335 id: "fzero-typed-option-controls",
336 mode: BuiltinExtensionMode::RunMatOnly,
337 description: "fzero with typed-integer option controls is a RunMat extension",
338 error_identifier: Some("RunMat:compatibility:FzeroOptionExtension"),
339};
340pub(crate) const FZERO_RESIDENT_EXTENSION: BuiltinExtensionDescriptor =
341 BuiltinExtensionDescriptor {
342 id: "fzero-resident-fallback",
343 mode: BuiltinExtensionMode::RunMatOnly,
344 description:
345 "fzero with provider-resident numeric input or callback output is a RunMat extension",
346 error_identifier: Some("RunMat:compatibility:FzeroResidentExtension"),
347 };
348pub const FZERO_EXTENSIONS: [BuiltinExtensionDescriptor; 4] = [
349 FZERO_INPUT_NUMERIC_EXTENSION,
350 FZERO_CALLBACK_NUMERIC_EXTENSION,
351 FZERO_OPTION_EXTENSION,
352 FZERO_RESIDENT_EXTENSION,
353];
354
355pub const FZERO_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
356 signatures: &FZERO_SIGNATURES,
357 output_mode: BuiltinOutputMode::ByRequestedOutputCount,
358 completion_policy: BuiltinCompletionPolicy::Public,
359 errors: &FZERO_ERRORS,
360};
361
362fn fzero_error_with_detail(
363 error: &'static BuiltinErrorDescriptor,
364 detail: impl AsRef<str>,
365) -> RuntimeError {
366 let detail = detail.as_ref();
367 let message = if detail.starts_with("fzero:") {
368 detail.to_string()
369 } else {
370 format!("{}: {detail}", error.message)
371 };
372 let mut builder = build_runtime_error(message).with_builtin(NAME);
373 if let Some(identifier) = error.identifier {
374 builder = builder.with_identifier(identifier);
375 }
376 builder.build()
377}
378
379fn fzero_map_error(err: RuntimeError, fallback: &'static BuiltinErrorDescriptor) -> RuntimeError {
380 if err.identifier().is_some() {
381 err
382 } else {
383 fzero_error_with_detail(fallback, err.message())
384 }
385}
386
387fn validate_requested_outputs() -> BuiltinResult<()> {
388 if matches!(crate::output_count::current_output_count(), Some(n) if n > 4) {
389 return Err(fzero_too_many_outputs_error());
390 }
391 Ok(())
392}
393
394fn fzero_too_many_outputs_error() -> RuntimeError {
395 fzero_error_with_detail(
396 &FZERO_ERROR_TOO_MANY_OUTPUTS,
397 "fzero: too many output arguments; maximum is 4",
398 )
399}
400
401#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::math::optim::fzero")]
402pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
403 name: "fzero",
404 op_kind: GpuOpKind::Custom("scalar-root-find"),
405 supported_precisions: &[],
406 broadcast: BroadcastSemantics::None,
407 provider_hooks: &[],
408 constant_strategy: ConstantStrategy::InlineLiteral,
409 residency: ResidencyPolicy::GatherImmediately,
410 nan_mode: ReductionNaN::Include,
411 two_pass_threshold: None,
412 workgroup_size: None,
413 accepts_nan_mode: false,
414 notes: "Host iterative solver. Callback values may use GPU-aware builtins, but the root search runs on the CPU.",
415};
416
417#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::math::optim::fzero")]
418pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
419 name: "fzero",
420 shape: ShapeRequirements::Any,
421 constant_strategy: ConstantStrategy::InlineLiteral,
422 elementwise: None,
423 reduction: None,
424 emits_nan: false,
425 notes: "Root finding repeatedly invokes user code and terminates fusion planning.",
426};
427
428#[runtime_builtin(
429 name = "fzero",
430 category = "math/optim",
431 summary = "Find scalar function zeros with bracketed root-finding.",
432 keywords = "fzero,root finding,zero,brent,optimization",
433 accel = "sink",
434 type_resolver(scalar_root_type),
435 descriptor(crate::builtins::math::optim::fzero::FZERO_DESCRIPTOR),
436 extensions(crate::builtins::math::optim::fzero::FZERO_EXTENSIONS),
437 integer_capabilities(crate::builtins::math::optim::fzero::FZERO_INTEGER_CAPABILITIES),
438 builtin_path = "crate::builtins::math::optim::fzero"
439)]
440async fn fzero_builtin(function: Value, x: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
441 if rest.len() > 1 {
442 return Err(fzero_error_with_detail(
443 &FZERO_ERROR_INVALID_ARGUMENT,
444 "too many input arguments",
445 ));
446 }
447 validate_requested_outputs()?;
448 let options = parse_options(rest.first())
449 .map_err(|err| fzero_map_error(err, &FZERO_ERROR_INVALID_ARGUMENT))?;
450 let mut opts = FzeroOptions::from_struct(options.as_ref())
451 .map_err(|err| fzero_map_error(err, &FZERO_ERROR_INVALID_ARGUMENT))?;
452 let x = crate::builtins::math::optim::common::prepare_floating_value(
453 NAME,
454 x,
455 &FZERO_INPUT_NUMERIC_EXTENSION,
456 &FZERO_RESIDENT_EXTENSION,
457 "initial point or interval",
458 )
459 .await
460 .map_err(|err| fzero_map_error(err, &FZERO_ERROR_INVALID_INPUT))?;
461 let initial_single = matches!(&x, Value::Tensor(tensor) if tensor.numeric_dtype() == runmat_value::NumericDType::F32);
462 let bracket = initial_bracket(&function, x, &opts, initial_single)
463 .await
464 .map_err(|err| fzero_map_error(err, &FZERO_ERROR_INVALID_INPUT))?;
465 if !opts.tol_x_explicit {
466 opts.tol_x = default_tol_x(initial_single || bracket.function_single);
467 }
468 let mut iter_log = IterDisplay::new(opts.display);
469 let observer: Option<&mut dyn BrentZeroObserver> = if matches!(opts.display, DisplayMode::Iter)
470 {
471 Some(&mut iter_log)
472 } else {
473 None
474 };
475 let result = brent_zero(
476 NAME,
477 &function,
478 BrentZeroBracket {
479 a: bracket.a,
480 b: bracket.b,
481 fa: bracket.fa,
482 fb: bracket.fb,
483 evals: bracket.evals,
484 },
485 BrentParams {
486 tol_x: opts.tol_x,
487 max_iter: opts.max_iter,
488 max_fun_evals: opts.max_fun_evals,
489 },
490 initial_single,
491 observer,
492 )
493 .await
494 .map_err(|err| fzero_map_error(err, &FZERO_ERROR_INVALID_INPUT))?;
495 finalize(result, &opts, initial_single)
496}
497
498fn default_tol_x(single: bool) -> f64 {
499 if single {
500 f32::EPSILON as f64
501 } else {
502 f64::EPSILON
503 }
504}
505
506fn parse_options(value: Option<&Value>) -> BuiltinResult<Option<StructValue>> {
507 match value {
508 None => Ok(None),
509 Some(Value::Struct(options)) => Ok(Some(options.clone())),
510 Some(other) => Err(fzero_error_with_detail(
511 &FZERO_ERROR_INVALID_ARGUMENT,
512 format!("options must be a struct, got {other:?}"),
513 )),
514 }
515}
516
517#[derive(Clone, Copy)]
518struct FzeroOptions {
519 tol_x: f64,
520 tol_x_explicit: bool,
521 max_iter: usize,
522 max_fun_evals: usize,
523 display: DisplayMode,
524}
525
526impl FzeroOptions {
527 fn from_struct(options: Option<&StructValue>) -> BuiltinResult<Self> {
528 crate::builtins::math::optim::common::ensure_option_extensions(
529 NAME,
530 options,
531 &FZERO_OPTION_EXTENSION,
532 &FZERO_RESIDENT_EXTENSION,
533 )?;
534 let display = DisplayMode::parse(&option_string(options, "Display", "off")?)?;
535 let tol_x = option_f64(NAME, options, "TolX", DEFAULT_TOL_X)?;
536 if tol_x <= 0.0 {
537 return Err(fzero_error_with_detail(
538 &FZERO_ERROR_INVALID_ARGUMENT,
539 "option TolX must be positive",
540 ));
541 }
542 let max_iter = option_usize(NAME, options, "MaxIter", DEFAULT_MAX_ITER)?;
543 let max_fun_evals = option_usize(NAME, options, "MaxFunEvals", DEFAULT_MAX_FUN_EVALS)?;
544 Ok(Self {
545 tol_x,
546 tol_x_explicit: options.is_some_and(|options| {
547 options
548 .fields
549 .keys()
550 .any(|key| key.eq_ignore_ascii_case("TolX"))
551 }),
552 max_iter: max_iter.max(1),
553 max_fun_evals: max_fun_evals.max(1),
554 display,
555 })
556 }
557}
558
559#[derive(Debug, Clone, Copy, PartialEq, Eq)]
560enum DisplayMode {
561 Off,
562 Iter,
563 Final,
564}
565
566impl DisplayMode {
567 fn parse(text: &str) -> BuiltinResult<Self> {
568 match text.to_ascii_lowercase().as_str() {
569 "off" | "none" => Ok(Self::Off),
570 "iter" => Ok(Self::Iter),
571 "final" => Ok(Self::Final),
572 other => Err(fzero_error_with_detail(
573 &FZERO_ERROR_INVALID_ARGUMENT,
574 format!("option Display must be 'off', 'none', 'final', or 'iter', got '{other}'"),
575 )),
576 }
577 }
578}
579
580#[derive(Clone, Copy)]
581struct Bracket {
582 a: f64,
583 b: f64,
584 fa: f64,
585 fb: f64,
586 evals: usize,
587 function_single: bool,
588}
589
590async fn initial_bracket(
591 function: &Value,
592 x: Value,
593 options: &FzeroOptions,
594 single: bool,
595) -> BuiltinResult<Bracket> {
596 let x = crate::dispatcher::gather_if_needed_async(&x).await?;
597 match x {
598 Value::Tensor(tensor) => {
599 let values = tensor::tensor_values_f64(&tensor);
600 match values.as_slice() {
601 [a, b] => bracket_from_endpoints(function, *a, *b, single).await,
602 [guess] => expand_bracket(function, *guess, options, single).await,
603 _ => Err(fzero_error_with_detail(
604 &FZERO_ERROR_INVALID_INPUT,
605 "initial point must be a scalar or two-element bracket",
606 )),
607 }
608 }
609 Value::Num(n) => expand_bracket(function, n, options, single).await,
610 Value::Int(i) => expand_bracket(function, i.to_f64(), options, single).await,
611 Value::Bool(b) => {
612 expand_bracket(function, if b { 1.0 } else { 0.0 }, options, single).await
613 }
614 other => Err(fzero_error_with_detail(
615 &FZERO_ERROR_INVALID_INPUT,
616 format!("initial point must be a scalar or two-element bracket, got {other:?}"),
617 )),
618 }
619}
620
621async fn bracket_from_endpoints(
622 function: &Value,
623 a: f64,
624 b: f64,
625 single: bool,
626) -> BuiltinResult<Bracket> {
627 if !a.is_finite() || !b.is_finite() || a == b {
628 return Err(fzero_error_with_detail(
629 &FZERO_ERROR_INVALID_INPUT,
630 "bracket endpoints must be finite and distinct",
631 ));
632 }
633 let (fa, fa_single) =
634 call_scalar_function_with_precision_info(NAME, function, a, single).await?;
635 if fa == 0.0 {
636 return Ok(Bracket {
637 a,
638 b: a,
639 fa,
640 fb: fa,
641 evals: 1,
642 function_single: fa_single,
643 });
644 }
645 let (fb, fb_single) =
646 call_scalar_function_with_precision_info(NAME, function, b, single).await?;
647 if fb == 0.0 || fa.signum() != fb.signum() {
648 Ok(Bracket {
649 a,
650 b,
651 fa,
652 fb,
653 evals: 2,
654 function_single: fa_single || fb_single,
655 })
656 } else {
657 Err(fzero_error_with_detail(
658 &FZERO_ERROR_INVALID_INPUT,
659 "function values at bracket endpoints must differ in sign",
660 ))
661 }
662}
663
664async fn expand_bracket(
665 function: &Value,
666 x0: f64,
667 options: &FzeroOptions,
668 single: bool,
669) -> BuiltinResult<Bracket> {
670 if !x0.is_finite() {
671 return Err(fzero_error_with_detail(
672 &FZERO_ERROR_INVALID_INPUT,
673 "initial point must be finite",
674 ));
675 }
676 let (f0, function_single) =
677 call_scalar_function_with_precision_info(NAME, function, x0, single).await?;
678 if f0 == 0.0 {
679 return Ok(Bracket {
680 a: x0,
681 b: x0,
682 fa: f0,
683 fb: f0,
684 evals: 1,
685 function_single,
686 });
687 }
688
689 let mut evals = 1usize;
690 let mut step = (x0.abs() * 0.01).max(0.01);
691 while evals + 2 <= options.max_fun_evals {
692 let a = x0 - step;
693 let b = x0 + step;
694 let fa = call_scalar_function_with_precision(NAME, function, a, single).await?;
695 let fb = call_scalar_function_with_precision(NAME, function, b, single).await?;
696 evals += 2;
697 if fa == 0.0 {
698 return Ok(Bracket {
699 a,
700 b: a,
701 fa,
702 fb: fa,
703 evals,
704 function_single,
705 });
706 }
707 if fa.signum() != f0.signum() {
708 return Ok(Bracket {
709 a,
710 b: x0,
711 fa,
712 fb: f0,
713 evals,
714 function_single,
715 });
716 }
717 if fb.signum() != f0.signum() {
718 return Ok(Bracket {
719 a: x0,
720 b,
721 fa: f0,
722 fb,
723 evals,
724 function_single,
725 });
726 }
727 if fb == 0.0 || fa.signum() != fb.signum() {
728 return Ok(Bracket {
729 a,
730 b,
731 fa,
732 fb,
733 evals,
734 function_single,
735 });
736 }
737 step *= 1.6;
738 }
739
740 Err(fzero_error_with_detail(
741 &FZERO_ERROR_INVALID_INPUT,
742 "could not find a sign-changing bracket around the initial point",
743 ))
744}
745
746fn finalize(result: BrentZeroResult, options: &FzeroOptions, single: bool) -> BuiltinResult<Value> {
747 let exit_flag = if result.converged { 1 } else { 0 };
748 let message = build_message(&result);
749 emit_summary(&result, exit_flag, &message, options);
750
751 let scalar = |value: f64| -> BuiltinResult<Value> {
752 if single {
753 Tensor::new_with_dtype(vec![value], vec![1, 1], runmat_value::NumericDType::F32)
754 .map(Value::Tensor)
755 .map_err(|error| fzero_error_with_detail(&FZERO_ERROR_INVALID_INPUT, error))
756 } else {
757 Ok(Value::Num(value))
758 }
759 };
760 let x = scalar(result.x)?;
761 let fval = scalar(result.fval)?;
762 let exitflag = scalar(exit_flag as f64)?;
763 let output_struct = Value::Struct(build_output_struct(&result, &message));
764
765 match crate::output_count::current_output_count() {
766 None => Ok(x),
767 Some(0) => Ok(Value::OutputList(Vec::new())),
768 Some(1) => Ok(crate::output_count::output_list_with_padding(1, vec![x])),
769 Some(2) => Ok(crate::output_count::output_list_with_padding(
770 2,
771 vec![x, fval],
772 )),
773 Some(3) => Ok(crate::output_count::output_list_with_padding(
774 3,
775 vec![x, fval, exitflag],
776 )),
777 Some(4) => Ok(crate::output_count::output_list_with_padding(
778 4,
779 vec![x, fval, exitflag, output_struct],
780 )),
781 Some(_) => Err(fzero_too_many_outputs_error()),
782 }
783}
784
785fn build_output_struct(result: &BrentZeroResult, message: &str) -> StructValue {
786 let mut fields = StructValue::new();
787 fields.insert("iterations", Value::Num(result.iterations as f64));
788 fields.insert("funcCount", Value::Num(result.func_count as f64));
789 fields.insert("algorithm", Value::from(ALGORITHM));
790 fields.insert("message", Value::from(message.to_string()));
791 fields
792}
793
794fn build_message(result: &BrentZeroResult) -> String {
795 if result.converged {
796 format!(
797 "Zero found within OPTIONS.TolX. Iterations: {}, FuncCount: {}.",
798 result.iterations, result.func_count
799 )
800 } else {
801 format!(
802 "Exiting: Maximum number of function evaluations or iterations has been exceeded - increase MaxFunEvals or MaxIter. Iterations: {}, FuncCount: {}.",
803 result.iterations, result.func_count
804 )
805 }
806}
807
808fn emit_summary(result: &BrentZeroResult, exit_flag: i32, message: &str, options: &FzeroOptions) {
809 if !matches!(options.display, DisplayMode::Final | DisplayMode::Iter) {
810 return;
811 }
812 crate::console::record_console_line(
813 crate::console::ConsoleStream::Stdout,
814 format!(
815 "fzero: x = {x:.6}, fval = {fval:.6}, exitflag = {exit_flag}. {message}",
816 x = result.x,
817 fval = result.fval,
818 ),
819 );
820}
821
822struct IterDisplay {
823 mode: DisplayMode,
824 printed_header: bool,
825}
826
827impl IterDisplay {
828 fn new(mode: DisplayMode) -> Self {
829 Self {
830 mode,
831 printed_header: false,
832 }
833 }
834}
835
836impl BrentZeroObserver for IterDisplay {
837 fn on_iteration(
838 &mut self,
839 iter: usize,
840 func_count: usize,
841 x: f64,
842 fx: f64,
843 step_kind: BrentZeroStepKind,
844 ) {
845 if !matches!(self.mode, DisplayMode::Iter) {
846 return;
847 }
848 if !self.printed_header {
849 crate::console::record_console_line(
850 crate::console::ConsoleStream::Stdout,
851 " Func-count x f(x) Procedure",
852 );
853 self.printed_header = true;
854 }
855 let procedure = match step_kind {
856 BrentZeroStepKind::Initial => "initial",
857 BrentZeroStepKind::Bisection => "bisection",
858 BrentZeroStepKind::Interpolation => "interpolation",
859 };
860 let line =
861 format!(" {func_count:>5} {x:13.6e} {fx:13.6e} {procedure} (iter {iter})");
862 crate::console::record_console_line(crate::console::ConsoleStream::Stdout, line);
863 }
864}
865
866#[cfg(test)]
867mod tests {
868 use super::*;
869 use crate::builtins::math::optim::brent::interpolation_step_accepted;
870 use futures::executor::block_on;
871 use runmat_value::{IntegerStorage, Tensor};
872 use std::sync::Arc;
873
874 #[test]
875 fn fzero_bracketed_builtin_handle() {
876 let bracket = Tensor::new(vec![3.0, 4.0], vec![1, 2]).unwrap();
877 let root = block_on(fzero_builtin(
878 Value::FunctionHandle("sin".into()),
879 Value::Tensor(bracket),
880 Vec::new(),
881 ))
882 .unwrap();
883 match root {
884 Value::Num(n) => assert!((n - std::f64::consts::PI).abs() < 1.0e-6),
885 other => panic!("unexpected value {other:?}"),
886 }
887 }
888
889 #[test]
890 fn fzero_single_interval_returns_single_numerical_outputs() {
891 let bracket =
892 Tensor::new_with_dtype(vec![3.0, 4.0], vec![1, 2], runmat_value::NumericDType::F32)
893 .unwrap();
894 let _outputs = crate::output_count::push_output_count(Some(3));
895 let result = block_on(fzero_builtin(
896 Value::FunctionHandle("sin".into()),
897 Value::Tensor(bracket),
898 Vec::new(),
899 ))
900 .unwrap();
901 let Value::OutputList(outputs) = result else {
902 panic!("expected three outputs");
903 };
904 assert_eq!(outputs.len(), 3);
905 for output in outputs {
906 assert!(
907 matches!(output, Value::Tensor(tensor) if tensor.numeric_dtype() == runmat_value::NumericDType::F32)
908 );
909 }
910 }
911
912 #[test]
913 fn fzero_single_precision_is_explicitly_threaded_through_all_callback_calls() {
914 let calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
915 let observed_calls = calls.clone();
916 let _invoker = crate::user_functions::install_semantic_function_invoker(Some(Arc::new(
917 move |_function, args, _requested_outputs| {
918 observed_calls.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
919 let Value::Tensor(argument) = &args[0] else {
920 panic!("expected a single-precision scalar tensor")
921 };
922 assert_eq!(argument.numeric_dtype(), runmat_value::NumericDType::F32);
923 let residual = argument.materialize_f64()[0] - std::f64::consts::PI;
924 Box::pin(async move {
925 Ok(Value::Tensor(
926 Tensor::new_with_dtype(
927 vec![residual],
928 vec![1, 1],
929 runmat_value::NumericDType::F32,
930 )
931 .unwrap(),
932 ))
933 })
934 },
935 )));
936 let bracket =
937 Tensor::new_with_dtype(vec![3.0, 4.0], vec![1, 2], runmat_value::NumericDType::F32)
938 .unwrap();
939 let root = block_on(fzero_builtin(
940 Value::BoundFunctionHandle {
941 name: "single_root".to_string(),
942 function: 7_709,
943 },
944 Value::Tensor(bracket),
945 Vec::new(),
946 ))
947 .unwrap();
948 assert!(
949 matches!(root, Value::Tensor(tensor) if tensor.numeric_dtype() == runmat_value::NumericDType::F32)
950 );
951 assert!(calls.load(std::sync::atomic::Ordering::Relaxed) > 2);
952 }
953
954 #[test]
955 fn fzero_single_callback_result_selects_single_default_tolerance() {
956 let _invoker = crate::user_functions::install_semantic_function_invoker(Some(Arc::new(
957 |_function, args, _requested_outputs| {
958 assert!(matches!(args[0], Value::Num(_)));
959 let x = match &args[0] {
960 Value::Num(value) => *value,
961 _ => unreachable!(),
962 };
963 Box::pin(async move {
964 Ok(Value::Tensor(
965 Tensor::new_with_dtype(
966 vec![x - 1.0],
967 vec![1, 1],
968 runmat_value::NumericDType::F32,
969 )
970 .unwrap(),
971 ))
972 })
973 },
974 )));
975 let options = FzeroOptions::from_struct(None).unwrap();
976 let bracket = block_on(bracket_from_endpoints(
977 &Value::BoundFunctionHandle {
978 name: "single_result".to_string(),
979 function: 7_710,
980 },
981 0.0,
982 2.0,
983 false,
984 ))
985 .unwrap();
986 assert!(bracket.function_single);
987 assert_eq!(default_tol_x(bracket.function_single), f32::EPSILON as f64);
988 assert_eq!(default_tol_x(false), f64::EPSILON);
989 assert!(!options.tol_x_explicit);
990 }
991
992 #[test]
993 fn fzero_bracket_reads_typed_integer_storage_exactly() {
994 let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
995 let bracket =
996 Tensor::new_integer(IntegerStorage::U16(vec![3, 4]), vec![1, 2]).expect("bracket");
997
998 let root = block_on(fzero_builtin(
999 Value::FunctionHandle("sin".into()),
1000 Value::Tensor(bracket),
1001 Vec::new(),
1002 ))
1003 .unwrap();
1004 match root {
1005 Value::Num(n) => assert!((n - std::f64::consts::PI).abs() < 1.0e-6),
1006 other => panic!("unexpected value {other:?}"),
1007 }
1008 }
1009
1010 #[test]
1011 fn fzero_scalar_initial_guess_expands_bracket() {
1012 let root = block_on(fzero_builtin(
1013 Value::FunctionHandle("cos".into()),
1014 Value::Num(1.0),
1015 Vec::new(),
1016 ))
1017 .unwrap();
1018 match root {
1019 Value::Num(n) => assert!((n - std::f64::consts::FRAC_PI_2).abs() < 1.0e-6),
1020 other => panic!("unexpected value {other:?}"),
1021 }
1022 }
1023
1024 #[test]
1025 fn fzero_initial_guess_reads_typed_integer_storage_exactly() {
1026 let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
1027 let guess = Tensor::new_integer(IntegerStorage::U16(vec![1]), vec![1, 1]).expect("guess");
1028
1029 let root = block_on(fzero_builtin(
1030 Value::FunctionHandle("cos".into()),
1031 Value::Tensor(guess),
1032 Vec::new(),
1033 ))
1034 .unwrap();
1035 match root {
1036 Value::Num(n) => assert!((n - std::f64::consts::FRAC_PI_2).abs() < 1.0e-6),
1037 other => panic!("unexpected value {other:?}"),
1038 }
1039 }
1040
1041 #[test]
1042 fn fzero_scalar_initial_guess_uses_center_sign_for_bracket() {
1043 let root = block_on(fzero_builtin(
1044 Value::FunctionHandle("sin".into()),
1045 Value::Num(std::f64::consts::FRAC_PI_2),
1046 Vec::new(),
1047 ))
1048 .unwrap();
1049 match root {
1050 Value::Num(n) => assert!(n.abs() < 1.0e-6),
1051 other => panic!("unexpected value {other:?}"),
1052 }
1053 }
1054 #[test]
1055 fn fzero_accepts_semantic_function_handle_callback() {
1056 let _invoker = crate::user_functions::install_semantic_function_invoker(Some(Arc::new(
1057 |function, args, requested_outputs| {
1058 assert_eq!(function, 42);
1059 assert_eq!(requested_outputs, 1);
1060 let x = match &args[0] {
1061 Value::Num(value) => *value,
1062 other => panic!("expected scalar numeric argument, got {other:?}"),
1063 };
1064 Box::pin(async move { Ok(Value::Num(x - 2.0)) })
1065 },
1066 )));
1067
1068 let root = block_on(fzero_builtin(
1069 Value::BoundFunctionHandle {
1070 name: "root_function".to_string(),
1071 function: 42,
1072 },
1073 Value::Num(0.0),
1074 Vec::new(),
1075 ))
1076 .unwrap();
1077 match root {
1078 Value::Num(n) => assert!((n - 2.0).abs() < 1.0e-6),
1079 other => panic!("unexpected value {other:?}"),
1080 }
1081 }
1082
1083 #[test]
1084 fn fzero_multi_output_two_returns_root_and_fval() {
1085 let _guard = crate::output_count::push_output_count(Some(2));
1086 let bracket = Tensor::new(vec![3.0, 4.0], vec![1, 2]).unwrap();
1087 let result = block_on(fzero_builtin(
1088 Value::FunctionHandle("sin".into()),
1089 Value::Tensor(bracket),
1090 Vec::new(),
1091 ))
1092 .expect("fzero");
1093 match result {
1094 Value::OutputList(outputs) => {
1095 assert_eq!(outputs.len(), 2);
1096 match (&outputs[0], &outputs[1]) {
1097 (Value::Num(x), Value::Num(fval)) => {
1098 assert!((x - std::f64::consts::PI).abs() < 1.0e-6);
1099 assert!(fval.abs() < 1.0e-6);
1100 }
1101 other => panic!("unexpected outputs {other:?}"),
1102 }
1103 }
1104 other => panic!("unexpected value {other:?}"),
1105 }
1106 }
1107
1108 #[test]
1109 fn fzero_multi_output_four_includes_output_struct() {
1110 let _guard = crate::output_count::push_output_count(Some(4));
1111 let bracket = Tensor::new(vec![3.0, 4.0], vec![1, 2]).unwrap();
1112 let result = block_on(fzero_builtin(
1113 Value::FunctionHandle("sin".into()),
1114 Value::Tensor(bracket),
1115 Vec::new(),
1116 ))
1117 .expect("fzero");
1118 match result {
1119 Value::OutputList(outputs) => {
1120 assert_eq!(outputs.len(), 4);
1121 assert!(matches!(&outputs[2], Value::Num(flag) if *flag == 1.0));
1122 match &outputs[3] {
1123 Value::Struct(output) => {
1124 assert!(matches!(
1125 output.fields.get("iterations"),
1126 Some(Value::Num(_))
1127 ));
1128 assert!(matches!(
1129 output.fields.get("funcCount"),
1130 Some(Value::Num(_))
1131 ));
1132 match output.fields.get("algorithm") {
1133 Some(Value::String(text)) => assert!(text.contains("bisection")),
1134 other => panic!("unexpected algorithm field {other:?}"),
1135 }
1136 match output.fields.get("message") {
1137 Some(Value::String(text)) => assert!(text.contains("Zero found")),
1138 other => panic!("unexpected message field {other:?}"),
1139 }
1140 }
1141 other => panic!("unexpected output struct {other:?}"),
1142 }
1143 }
1144 other => panic!("unexpected value {other:?}"),
1145 }
1146 }
1147
1148 #[test]
1149 fn fzero_reports_zero_exitflag_when_iteration_budget_exhausted() {
1150 let mut opts = StructValue::new();
1151 opts.insert("MaxIter", Value::Num(1.0));
1152 opts.insert("MaxFunEvals", Value::Num(2.0));
1153 opts.insert("Display", Value::from("off"));
1154 let _guard = crate::output_count::push_output_count(Some(3));
1155 let bracket = Tensor::new(vec![3.0, 4.0], vec![1, 2]).unwrap();
1156 let result = block_on(fzero_builtin(
1157 Value::FunctionHandle("sin".into()),
1158 Value::Tensor(bracket),
1159 vec![Value::Struct(opts)],
1160 ))
1161 .expect("fzero");
1162 match result {
1163 Value::OutputList(outputs) => match &outputs[2] {
1164 Value::Num(flag) => assert_eq!(*flag, 0.0),
1165 other => panic!("unexpected exitflag {other:?}"),
1166 },
1167 other => panic!("unexpected value {other:?}"),
1168 }
1169 }
1170
1171 #[test]
1172 fn fzero_reports_convergence_when_final_step_hits_root() {
1173 let mut opts = StructValue::new();
1174 opts.insert("MaxIter", Value::Num(1.0));
1175 opts.insert("Display", Value::from("off"));
1176 let _guard = crate::output_count::push_output_count(Some(3));
1177 let bracket = Tensor::new(vec![-1.0, 1.0], vec![1, 2]).unwrap();
1178 let result = block_on(fzero_builtin(
1179 Value::FunctionHandle("sin".into()),
1180 Value::Tensor(bracket),
1181 vec![Value::Struct(opts)],
1182 ))
1183 .expect("fzero");
1184 match result {
1185 Value::OutputList(outputs) => {
1186 assert!(matches!(&outputs[0], Value::Num(x) if x.abs() < 1.0e-12));
1187 assert!(matches!(&outputs[1], Value::Num(fval) if fval.abs() < 1.0e-12));
1188 assert!(matches!(&outputs[2], Value::Num(flag) if *flag == 1.0));
1189 }
1190 other => panic!("unexpected value {other:?}"),
1191 }
1192 }
1193
1194 #[test]
1195 fn fzero_rejects_more_than_four_outputs() {
1196 let _guard = crate::output_count::push_output_count(Some(5));
1197 let bracket = Tensor::new(vec![3.0, 4.0], vec![1, 2]).unwrap();
1198 let err = block_on(fzero_builtin(
1199 Value::FunctionHandle("sin".into()),
1200 Value::Tensor(bracket),
1201 Vec::new(),
1202 ))
1203 .expect_err("too many outputs should fail");
1204 assert_eq!(err.identifier(), Some("RunMat:fzero:TooManyOutputs"));
1205 assert!(err.message().contains("maximum is 4"));
1206 }
1207
1208 #[test]
1209 fn fzero_iter_display_records_iteration_rows() {
1210 crate::console::reset_thread_buffer();
1211 let mut opts = StructValue::new();
1212 opts.insert("Display", Value::from("iter"));
1213 let bracket = Tensor::new(vec![3.0, 4.0], vec![1, 2]).unwrap();
1214 let result = block_on(fzero_builtin(
1215 Value::FunctionHandle("sin".into()),
1216 Value::Tensor(bracket),
1217 vec![Value::Struct(opts)],
1218 ))
1219 .expect("fzero");
1220 assert!(matches!(result, Value::Num(_)));
1221
1222 let joined = crate::console::take_thread_buffer()
1223 .into_iter()
1224 .map(|entry| entry.text)
1225 .collect::<String>();
1226 assert!(joined.contains("Func-count"), "{joined}");
1227 assert!(joined.contains("initial"), "{joined}");
1228 assert!(
1229 joined.contains("interpolation") || joined.contains("bisection"),
1230 "{joined}"
1231 );
1232 assert!(joined.contains("exitflag = 1"), "{joined}");
1233 }
1234
1235 #[test]
1236 fn brent_interpolation_acceptance_uses_signed_q() {
1237 assert!(!interpolation_step_accepted(1.0, -2.0, 1.0, 0.1, 10.0));
1238 assert!(interpolation_step_accepted(1.0, -2.0, -1.0, 0.1, 10.0));
1239 }
1240
1241 #[test]
1242 fn fzero_descriptor_signatures_cover_core_forms() {
1243 let labels: Vec<&str> = FZERO_DESCRIPTOR
1244 .signatures
1245 .iter()
1246 .map(|signature| signature.label)
1247 .collect();
1248 assert_eq!(
1249 labels,
1250 vec![
1251 "x = fzero(fun, x0)",
1252 "x = fzero(fun, x0, options)",
1253 "[x, fval] = fzero(fun, x0)",
1254 "[x, fval] = fzero(fun, x0, options)",
1255 "[x, fval, exitflag] = fzero(fun, x0)",
1256 "[x, fval, exitflag] = fzero(fun, x0, options)",
1257 "[x, fval, exitflag, output] = fzero(fun, x0)",
1258 "[x, fval, exitflag, output] = fzero(fun, x0, options)",
1259 ]
1260 );
1261
1262 let codes: Vec<&str> = FZERO_DESCRIPTOR
1263 .errors
1264 .iter()
1265 .map(|error| error.code)
1266 .collect();
1267 assert_eq!(
1268 codes,
1269 vec![
1270 "RM.FZERO.INVALID_ARGUMENT",
1271 "RM.FZERO.INVALID_INPUT",
1272 "RM.FZERO.TOO_MANY_OUTPUTS",
1273 ]
1274 );
1275 }
1276
1277 #[test]
1278 fn fzero_too_many_args_uses_stable_identifier() {
1279 let err = block_on(fzero_builtin(
1280 Value::FunctionHandle("sin".into()),
1281 Value::Num(0.0),
1282 vec![
1283 Value::Struct(StructValue::new()),
1284 Value::Struct(StructValue::new()),
1285 ],
1286 ))
1287 .unwrap_err();
1288 assert_eq!(err.identifier(), Some("RunMat:fzero:InvalidArgument"));
1289 }
1290}