1use runmat_builtins::{
17 BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
18 BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
19 LogicalArray, StructValue, Tensor, Value,
20};
21use runmat_macros::runtime_builtin;
22
23use crate::builtins::common::spec::{
24 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
25 ReductionNaN, ResidencyPolicy, ShapeRequirements,
26};
27use crate::builtins::math::optim::brent::{
28 brent_min, BrentMinObserver, BrentMinResult, BrentParams, BrentStepKind,
29};
30use crate::builtins::math::optim::type_resolvers::scalar_root_type;
31use crate::{build_runtime_error, BuiltinResult, RuntimeError};
32
33const NAME: &str = "fminbnd";
34const ALGORITHM: &str = "golden section search, parabolic interpolation";
35const DEFAULT_TOL_X: f64 = 1.0e-4;
36const DEFAULT_MAX_ITER: usize = 500;
37const DEFAULT_MAX_FUN_EVALS: usize = 500;
38const DEFAULT_DISPLAY: DisplayMode = DisplayMode::Notify;
39
40const FMINBND_OUTPUT_X: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
41 name: "x",
42 ty: BuiltinParamType::NumericScalar,
43 arity: BuiltinParamArity::Required,
44 default: None,
45 description: "Estimated minimizer location.",
46}];
47
48const FMINBND_OUTPUT_X_FVAL: [BuiltinParamDescriptor; 2] = [
49 BuiltinParamDescriptor {
50 name: "x",
51 ty: BuiltinParamType::NumericScalar,
52 arity: BuiltinParamArity::Required,
53 default: None,
54 description: "Estimated minimizer location.",
55 },
56 BuiltinParamDescriptor {
57 name: "fval",
58 ty: BuiltinParamType::NumericScalar,
59 arity: BuiltinParamArity::Required,
60 default: None,
61 description: "Objective value at x.",
62 },
63];
64
65const FMINBND_OUTPUT_X_FVAL_EXITFLAG: [BuiltinParamDescriptor; 3] = [
66 BuiltinParamDescriptor {
67 name: "x",
68 ty: BuiltinParamType::NumericScalar,
69 arity: BuiltinParamArity::Required,
70 default: None,
71 description: "Estimated minimizer location.",
72 },
73 BuiltinParamDescriptor {
74 name: "fval",
75 ty: BuiltinParamType::NumericScalar,
76 arity: BuiltinParamArity::Required,
77 default: None,
78 description: "Objective value at x.",
79 },
80 BuiltinParamDescriptor {
81 name: "exitflag",
82 ty: BuiltinParamType::NumericScalar,
83 arity: BuiltinParamArity::Required,
84 default: None,
85 description: "Convergence status code.",
86 },
87];
88
89const FMINBND_OUTPUT_ALL: [BuiltinParamDescriptor; 4] = [
90 BuiltinParamDescriptor {
91 name: "x",
92 ty: BuiltinParamType::NumericScalar,
93 arity: BuiltinParamArity::Required,
94 default: None,
95 description: "Estimated minimizer location.",
96 },
97 BuiltinParamDescriptor {
98 name: "fval",
99 ty: BuiltinParamType::NumericScalar,
100 arity: BuiltinParamArity::Required,
101 default: None,
102 description: "Objective value at x.",
103 },
104 BuiltinParamDescriptor {
105 name: "exitflag",
106 ty: BuiltinParamType::NumericScalar,
107 arity: BuiltinParamArity::Required,
108 default: None,
109 description: "Convergence status code.",
110 },
111 BuiltinParamDescriptor {
112 name: "output",
113 ty: BuiltinParamType::Any,
114 arity: BuiltinParamArity::Required,
115 default: None,
116 description: "Iteration/function-count metadata struct.",
117 },
118];
119
120const FMINBND_INPUTS_CORE: [BuiltinParamDescriptor; 3] = [
121 BuiltinParamDescriptor {
122 name: "fun",
123 ty: BuiltinParamType::Any,
124 arity: BuiltinParamArity::Required,
125 default: None,
126 description: "Scalar objective callback.",
127 },
128 BuiltinParamDescriptor {
129 name: "x1",
130 ty: BuiltinParamType::Any,
131 arity: BuiltinParamArity::Required,
132 default: None,
133 description: "Lower bound.",
134 },
135 BuiltinParamDescriptor {
136 name: "x2",
137 ty: BuiltinParamType::Any,
138 arity: BuiltinParamArity::Required,
139 default: None,
140 description: "Upper bound.",
141 },
142];
143
144const FMINBND_INPUTS_WITH_OPTIONS: [BuiltinParamDescriptor; 4] = [
145 BuiltinParamDescriptor {
146 name: "fun",
147 ty: BuiltinParamType::Any,
148 arity: BuiltinParamArity::Required,
149 default: None,
150 description: "Scalar objective callback.",
151 },
152 BuiltinParamDescriptor {
153 name: "x1",
154 ty: BuiltinParamType::Any,
155 arity: BuiltinParamArity::Required,
156 default: None,
157 description: "Lower bound.",
158 },
159 BuiltinParamDescriptor {
160 name: "x2",
161 ty: BuiltinParamType::Any,
162 arity: BuiltinParamArity::Required,
163 default: None,
164 description: "Upper bound.",
165 },
166 BuiltinParamDescriptor {
167 name: "options",
168 ty: BuiltinParamType::Any,
169 arity: BuiltinParamArity::Optional,
170 default: None,
171 description: "Options struct from optimset.",
172 },
173];
174
175const FMINBND_SIGNATURES: [BuiltinSignatureDescriptor; 8] = [
176 BuiltinSignatureDescriptor {
177 label: "x = fminbnd(fun, x1, x2)",
178 inputs: &FMINBND_INPUTS_CORE,
179 outputs: &FMINBND_OUTPUT_X,
180 },
181 BuiltinSignatureDescriptor {
182 label: "x = fminbnd(fun, x1, x2, options)",
183 inputs: &FMINBND_INPUTS_WITH_OPTIONS,
184 outputs: &FMINBND_OUTPUT_X,
185 },
186 BuiltinSignatureDescriptor {
187 label: "[x, fval] = fminbnd(fun, x1, x2)",
188 inputs: &FMINBND_INPUTS_CORE,
189 outputs: &FMINBND_OUTPUT_X_FVAL,
190 },
191 BuiltinSignatureDescriptor {
192 label: "[x, fval] = fminbnd(fun, x1, x2, options)",
193 inputs: &FMINBND_INPUTS_WITH_OPTIONS,
194 outputs: &FMINBND_OUTPUT_X_FVAL,
195 },
196 BuiltinSignatureDescriptor {
197 label: "[x, fval, exitflag] = fminbnd(fun, x1, x2)",
198 inputs: &FMINBND_INPUTS_CORE,
199 outputs: &FMINBND_OUTPUT_X_FVAL_EXITFLAG,
200 },
201 BuiltinSignatureDescriptor {
202 label: "[x, fval, exitflag] = fminbnd(fun, x1, x2, options)",
203 inputs: &FMINBND_INPUTS_WITH_OPTIONS,
204 outputs: &FMINBND_OUTPUT_X_FVAL_EXITFLAG,
205 },
206 BuiltinSignatureDescriptor {
207 label: "[x, fval, exitflag, output] = fminbnd(fun, x1, x2)",
208 inputs: &FMINBND_INPUTS_CORE,
209 outputs: &FMINBND_OUTPUT_ALL,
210 },
211 BuiltinSignatureDescriptor {
212 label: "[x, fval, exitflag, output] = fminbnd(fun, x1, x2, options)",
213 inputs: &FMINBND_INPUTS_WITH_OPTIONS,
214 outputs: &FMINBND_OUTPUT_ALL,
215 },
216];
217
218const FMINBND_ERROR_INVALID_ARGUMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
219 code: "RM.FMINBND.INVALID_ARGUMENT",
220 identifier: Some("RunMat:fminbnd:InvalidArgument"),
221 when: "Argument grammar/options parsing is invalid.",
222 message: "fminbnd: invalid argument",
223};
224
225const FMINBND_ERROR_INVALID_INPUT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
226 code: "RM.FMINBND.INVALID_INPUT",
227 identifier: Some("RunMat:fminbnd:InvalidInput"),
228 when: "Bounds/callback/input scalar semantics are invalid.",
229 message: "fminbnd: invalid input",
230};
231
232const FMINBND_ERRORS: [BuiltinErrorDescriptor; 2] =
233 [FMINBND_ERROR_INVALID_ARGUMENT, FMINBND_ERROR_INVALID_INPUT];
234
235pub const FMINBND_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
236 signatures: &FMINBND_SIGNATURES,
237 output_mode: BuiltinOutputMode::ByRequestedOutputCount,
238 completion_policy: BuiltinCompletionPolicy::Public,
239 errors: &FMINBND_ERRORS,
240};
241
242fn fminbnd_error_with_detail(
243 error: &'static BuiltinErrorDescriptor,
244 detail: impl AsRef<str>,
245) -> RuntimeError {
246 let detail = detail.as_ref();
247 let message = if detail.starts_with("fminbnd:") {
248 detail.to_string()
249 } else {
250 format!("{}: {detail}", error.message)
251 };
252 let mut builder = build_runtime_error(message).with_builtin(NAME);
253 if let Some(identifier) = error.identifier {
254 builder = builder.with_identifier(identifier);
255 }
256 builder.build()
257}
258
259fn fminbnd_map_error(err: RuntimeError, fallback: &'static BuiltinErrorDescriptor) -> RuntimeError {
260 if err.identifier().is_some() {
261 err
262 } else {
263 fminbnd_error_with_detail(fallback, err.message())
264 }
265}
266
267#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::math::optim::fminbnd")]
268pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
269 name: "fminbnd",
270 op_kind: GpuOpKind::Custom("bounded-scalar-min"),
271 supported_precisions: &[],
272 broadcast: BroadcastSemantics::None,
273 provider_hooks: &[],
274 constant_strategy: ConstantStrategy::InlineLiteral,
275 residency: ResidencyPolicy::GatherImmediately,
276 nan_mode: ReductionNaN::Include,
277 two_pass_threshold: None,
278 workgroup_size: None,
279 accepts_nan_mode: false,
280 notes: "Host iterative solver. Callback computations may use GPU-aware builtins, but the minimization loop runs on the CPU.",
281};
282
283#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::math::optim::fminbnd")]
284pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
285 name: "fminbnd",
286 shape: ShapeRequirements::Any,
287 constant_strategy: ConstantStrategy::InlineLiteral,
288 elementwise: None,
289 reduction: None,
290 emits_nan: false,
291 notes:
292 "Bounded scalar minimization repeatedly invokes user code and terminates fusion planning.",
293};
294
295#[runtime_builtin(
296 name = "fminbnd",
297 category = "math/optim",
298 summary = "Find bounded scalar minima with Brent's method.",
299 keywords = "fminbnd,bounded minimization,brent,golden section,parabolic interpolation,optimization",
300 accel = "sink",
301 type_resolver(scalar_root_type),
302 descriptor(crate::builtins::math::optim::fminbnd::FMINBND_DESCRIPTOR),
303 builtin_path = "crate::builtins::math::optim::fminbnd"
304)]
305async fn fminbnd_builtin(
306 function: Value,
307 x1: Value,
308 x2: Value,
309 rest: Vec<Value>,
310) -> BuiltinResult<Value> {
311 if rest.len() > 1 {
312 return Err(fminbnd_error_with_detail(
313 &FMINBND_ERROR_INVALID_ARGUMENT,
314 "too many input arguments",
315 ));
316 }
317 let options_struct = parse_options(rest.first())
318 .map_err(|err| fminbnd_map_error(err, &FMINBND_ERROR_INVALID_ARGUMENT))?;
319 let options = FminbndOptions::from_struct(options_struct.as_ref())
320 .map_err(|err| fminbnd_map_error(err, &FMINBND_ERROR_INVALID_ARGUMENT))?;
321 let x1 = scalar_bound("lower bound", x1)
322 .await
323 .map_err(|err| fminbnd_map_error(err, &FMINBND_ERROR_INVALID_INPUT))?;
324 let x2 = scalar_bound("upper bound", x2)
325 .await
326 .map_err(|err| fminbnd_map_error(err, &FMINBND_ERROR_INVALID_INPUT))?;
327
328 if !x1.is_finite() || !x2.is_finite() {
329 return Err(fminbnd_error_with_detail(
330 &FMINBND_ERROR_INVALID_INPUT,
331 "bounds must be finite",
332 ));
333 }
334 if x1 > x2 {
335 return finalize_inconsistent_bounds(&options);
336 }
337
338 let outcome = run_solver(&function, x1, x2, &options)
339 .await
340 .map_err(|err| fminbnd_map_error(err, &FMINBND_ERROR_INVALID_INPUT))?;
341 finalize(outcome, &options)
342}
343
344#[derive(Debug, Clone, Copy, PartialEq, Eq)]
345enum DisplayMode {
346 Off,
347 Iter,
348 Notify,
349 Final,
350}
351
352impl DisplayMode {
353 fn parse(text: &str) -> BuiltinResult<Self> {
354 match text.to_ascii_lowercase().as_str() {
355 "off" | "none" => Ok(Self::Off),
356 "iter" => Ok(Self::Iter),
357 "notify" => Ok(Self::Notify),
358 "final" => Ok(Self::Final),
359 other => Err(fminbnd_error_with_detail(
360 &FMINBND_ERROR_INVALID_ARGUMENT,
361 format!(
362 "option Display must be 'off', 'iter', 'notify', or 'final', got '{other}'"
363 ),
364 )),
365 }
366 }
367}
368
369#[derive(Debug, Clone, Copy)]
370struct FminbndOptions {
371 tol_x: f64,
372 max_iter: usize,
373 max_fun_evals: usize,
374 display: DisplayMode,
375}
376
377impl FminbndOptions {
378 fn from_struct(options: Option<&StructValue>) -> BuiltinResult<Self> {
379 let display = match options {
380 Some(opts) => match lookup(opts, "Display") {
381 Some(value) => DisplayMode::parse(&option_string("Display", value)?)?,
382 None => DEFAULT_DISPLAY,
383 },
384 None => DEFAULT_DISPLAY,
385 };
386 let tol_x = match options.and_then(|o| lookup(o, "TolX")) {
387 Some(value) => option_f64("TolX", value)?,
388 None => DEFAULT_TOL_X,
389 };
390 if tol_x <= 0.0 {
391 return Err(fminbnd_error_with_detail(
392 &FMINBND_ERROR_INVALID_ARGUMENT,
393 "option TolX must be positive",
394 ));
395 }
396 let max_iter = match options.and_then(|o| lookup(o, "MaxIter")) {
397 Some(value) => option_positive_usize("MaxIter", value)?,
398 None => DEFAULT_MAX_ITER,
399 };
400 let max_fun_evals = match options.and_then(|o| lookup(o, "MaxFunEvals")) {
401 Some(value) => option_positive_usize("MaxFunEvals", value)?,
402 None => DEFAULT_MAX_FUN_EVALS,
403 };
404 Ok(Self {
405 tol_x,
406 max_iter,
407 max_fun_evals,
408 display,
409 })
410 }
411}
412
413fn parse_options(value: Option<&Value>) -> BuiltinResult<Option<StructValue>> {
414 match value {
415 None => Ok(None),
416 Some(Value::Struct(options)) => Ok(Some(options.clone())),
417 Some(other) => Err(fminbnd_error_with_detail(
418 &FMINBND_ERROR_INVALID_ARGUMENT,
419 format!("options must be a struct, got {other:?}"),
420 )),
421 }
422}
423
424fn lookup<'a>(options: &'a StructValue, name: &str) -> Option<&'a Value> {
425 options
426 .fields
427 .iter()
428 .find(|(key, _)| key.eq_ignore_ascii_case(name))
429 .map(|(_, v)| v)
430}
431
432fn option_string(field: &str, value: &Value) -> BuiltinResult<String> {
433 match value {
434 Value::String(s) => Ok(s.clone()),
435 Value::StringArray(sa) if sa.data.len() == 1 => Ok(sa.data[0].clone()),
436 Value::CharArray(chars) if chars.rows == 1 => Ok(chars.data.iter().collect()),
437 other => Err(fminbnd_error_with_detail(
438 &FMINBND_ERROR_INVALID_ARGUMENT,
439 format!("option {field} must be a string, got {other:?}"),
440 )),
441 }
442}
443
444fn option_f64(field: &str, value: &Value) -> BuiltinResult<f64> {
445 let parsed = match value {
446 Value::Num(n) => *n,
447 Value::Int(i) => i.to_f64(),
448 Value::Bool(b) => {
449 if *b {
450 1.0
451 } else {
452 0.0
453 }
454 }
455 Value::Tensor(Tensor { data, .. }) if data.len() == 1 => data[0],
456 Value::LogicalArray(LogicalArray { data, .. }) if data.len() == 1 => {
457 if data[0] != 0 {
458 1.0
459 } else {
460 0.0
461 }
462 }
463 other => {
464 return Err(fminbnd_error_with_detail(
465 &FMINBND_ERROR_INVALID_ARGUMENT,
466 format!("option {field} must be a real scalar, got {other:?}"),
467 ))
468 }
469 };
470 if parsed.is_finite() {
471 Ok(parsed)
472 } else {
473 Err(fminbnd_error_with_detail(
474 &FMINBND_ERROR_INVALID_ARGUMENT,
475 format!("option {field} must be finite"),
476 ))
477 }
478}
479
480fn option_positive_usize(field: &str, value: &Value) -> BuiltinResult<usize> {
481 let parsed = option_f64(field, value)?;
482 if parsed < 1.0 {
483 return Err(fminbnd_error_with_detail(
484 &FMINBND_ERROR_INVALID_ARGUMENT,
485 format!("option {field} must be a positive integer"),
486 ));
487 }
488 if parsed.fract() != 0.0 {
489 return Err(fminbnd_error_with_detail(
490 &FMINBND_ERROR_INVALID_ARGUMENT,
491 format!("option {field} must be an integer scalar"),
492 ));
493 }
494 Ok(parsed as usize)
495}
496
497async fn scalar_bound(label: &str, value: Value) -> BuiltinResult<f64> {
498 let value = crate::dispatcher::gather_if_needed_async(&value).await?;
499 let parsed = match value {
500 Value::Num(n) => n,
501 Value::Int(i) => i.to_f64(),
502 Value::Bool(b) => {
503 if b {
504 1.0
505 } else {
506 0.0
507 }
508 }
509 Value::Tensor(t) if t.data.len() == 1 => t.data[0],
510 Value::LogicalArray(LogicalArray { data, .. }) if data.len() == 1 => {
511 if data[0] != 0 {
512 1.0
513 } else {
514 0.0
515 }
516 }
517 other => {
518 return Err(fminbnd_error_with_detail(
519 &FMINBND_ERROR_INVALID_INPUT,
520 format!("{label} must be a finite real scalar, got {other:?}"),
521 ))
522 }
523 };
524 if parsed.is_finite() {
525 Ok(parsed)
526 } else {
527 Err(fminbnd_error_with_detail(
528 &FMINBND_ERROR_INVALID_INPUT,
529 format!("{label} must be finite"),
530 ))
531 }
532}
533
534#[derive(Debug, Clone)]
535struct Outcome {
536 inner: BrentMinResult,
537}
538
539async fn run_solver(
540 function: &Value,
541 lo: f64,
542 hi: f64,
543 options: &FminbndOptions,
544) -> BuiltinResult<Outcome> {
545 let mut iter_log = IterDisplay::new(options.display);
546 let observer: Option<&mut dyn BrentMinObserver> =
547 if matches!(options.display, DisplayMode::Iter) {
548 Some(&mut iter_log)
549 } else {
550 None
551 };
552 let inner = brent_min(
553 NAME,
554 function,
555 lo,
556 hi,
557 BrentParams {
558 tol_x: options.tol_x,
559 max_iter: options.max_iter,
560 max_fun_evals: options.max_fun_evals,
561 },
562 observer,
563 )
564 .await?;
565 Ok(Outcome { inner })
566}
567
568fn finalize(outcome: Outcome, options: &FminbndOptions) -> BuiltinResult<Value> {
569 let exit_flag = if outcome.inner.converged { 1 } else { 0 };
570 let message = build_message(&outcome.inner);
571
572 emit_summary(&outcome.inner, exit_flag, &message, options);
573
574 let x = Value::Num(outcome.inner.x);
575 let fval = Value::Num(outcome.inner.fval);
576 let exitflag = Value::Num(exit_flag as f64);
577 let output_struct = Value::Struct(build_output_struct(&outcome.inner, &message));
578
579 match crate::output_count::current_output_count() {
580 None => Ok(x),
581 Some(0) => Ok(Value::OutputList(Vec::new())),
582 Some(1) => Ok(crate::output_count::output_list_with_padding(1, vec![x])),
583 Some(2) => Ok(crate::output_count::output_list_with_padding(
584 2,
585 vec![x, fval],
586 )),
587 Some(3) => Ok(crate::output_count::output_list_with_padding(
588 3,
589 vec![x, fval, exitflag],
590 )),
591 Some(n) if n >= 4 => Ok(crate::output_count::output_list_with_padding(
592 n,
593 vec![x, fval, exitflag, output_struct],
594 )),
595 Some(_) => Ok(x),
596 }
597}
598
599fn finalize_inconsistent_bounds(options: &FminbndOptions) -> BuiltinResult<Value> {
600 let message = "Exiting: The bounds are inconsistent because x1 > x2.".to_string();
601 emit_invalid_summary(-2, &message, options);
602
603 let x = empty_double();
604 let fval = empty_double();
605 let exitflag = Value::Num(-2.0);
606 let output_struct = Value::Struct(build_invalid_output_struct(&message));
607
608 match crate::output_count::current_output_count() {
609 None => Ok(x),
610 Some(0) => Ok(Value::OutputList(Vec::new())),
611 Some(1) => Ok(crate::output_count::output_list_with_padding(1, vec![x])),
612 Some(2) => Ok(crate::output_count::output_list_with_padding(
613 2,
614 vec![x, fval],
615 )),
616 Some(3) => Ok(crate::output_count::output_list_with_padding(
617 3,
618 vec![x, fval, exitflag],
619 )),
620 Some(n) if n >= 4 => Ok(crate::output_count::output_list_with_padding(
621 n,
622 vec![x, fval, exitflag, output_struct],
623 )),
624 Some(_) => Ok(empty_double()),
625 }
626}
627
628fn empty_double() -> Value {
629 Value::Tensor(Tensor::zeros(vec![0, 0]))
630}
631
632fn build_output_struct(result: &BrentMinResult, message: &str) -> StructValue {
633 let mut fields = StructValue::new();
634 fields.insert("iterations", Value::Num(result.iterations as f64));
635 fields.insert("funcCount", Value::Num(result.func_count as f64));
636 fields.insert("algorithm", Value::from(ALGORITHM));
637 fields.insert("message", Value::from(message.to_string()));
638 fields
639}
640
641fn build_invalid_output_struct(message: &str) -> StructValue {
642 let mut fields = StructValue::new();
643 fields.insert("iterations", Value::Num(0.0));
644 fields.insert("funcCount", Value::Num(0.0));
645 fields.insert("algorithm", Value::from(ALGORITHM));
646 fields.insert("message", Value::from(message.to_string()));
647 fields
648}
649
650fn build_message(result: &BrentMinResult) -> String {
651 if result.converged {
652 format!(
653 "Optimization terminated: the current x satisfies the termination criteria using OPTIONS.TolX. Iterations: {}, FuncCount: {}.",
654 result.iterations, result.func_count
655 )
656 } else {
657 format!(
658 "Exiting: Maximum number of function evaluations or iterations has been exceeded - increase MaxFunEvals or MaxIter. Iterations: {}, FuncCount: {}.",
659 result.iterations, result.func_count
660 )
661 }
662}
663
664fn emit_summary(result: &BrentMinResult, exit_flag: i32, message: &str, options: &FminbndOptions) {
665 let should_emit = match options.display {
666 DisplayMode::Off => false,
667 DisplayMode::Final | DisplayMode::Iter => true,
668 DisplayMode::Notify => exit_flag != 1,
669 };
670 if !should_emit {
671 return;
672 }
673 let line = format!(
674 "fminbnd: x = {x:.6}, fval = {fval:.6}, exitflag = {exit_flag}. {message}",
675 x = result.x,
676 fval = result.fval,
677 );
678 crate::console::record_console_line(crate::console::ConsoleStream::Stdout, line);
679}
680
681fn emit_invalid_summary(exit_flag: i32, message: &str, options: &FminbndOptions) {
682 let should_emit = match options.display {
683 DisplayMode::Off => false,
684 DisplayMode::Final | DisplayMode::Iter => true,
685 DisplayMode::Notify => exit_flag != 1,
686 };
687 if should_emit {
688 crate::console::record_console_line(
689 crate::console::ConsoleStream::Stdout,
690 format!("fminbnd: exitflag = {exit_flag}. {message}"),
691 );
692 }
693}
694
695struct IterDisplay {
696 mode: DisplayMode,
697 printed_header: bool,
698}
699
700impl IterDisplay {
701 fn new(mode: DisplayMode) -> Self {
702 Self {
703 mode,
704 printed_header: false,
705 }
706 }
707}
708
709impl BrentMinObserver for IterDisplay {
710 fn on_iteration(
711 &mut self,
712 iter: usize,
713 func_count: usize,
714 x: f64,
715 fx: f64,
716 step_kind: BrentStepKind,
717 ) {
718 if !matches!(self.mode, DisplayMode::Iter) {
719 return;
720 }
721 if !self.printed_header {
722 crate::console::record_console_line(
723 crate::console::ConsoleStream::Stdout,
724 " Func-count x f(x) Procedure",
725 );
726 self.printed_header = true;
727 }
728 let procedure = match step_kind {
729 BrentStepKind::Initial => "initial",
730 BrentStepKind::GoldenSection => "golden",
731 BrentStepKind::Parabolic => "parabolic",
732 };
733 let line =
734 format!(" {func_count:>5} {x:13.6e} {fx:13.6e} {procedure} (iter {iter})");
735 crate::console::record_console_line(crate::console::ConsoleStream::Stdout, line);
736 }
737}
738
739#[cfg(test)]
740mod tests {
741 use super::*;
742 use crate::builtins::math::optim::brent::brent_min_tolerance;
743 use futures::executor::block_on;
744 use runmat_builtins::Value as V;
745
746 const FMINBND_HELPER_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
747 name: "fx",
748 ty: BuiltinParamType::NumericScalar,
749 arity: BuiltinParamArity::Required,
750 default: None,
751 description: "Objective scalar value.",
752 }];
753
754 const FMINBND_HELPER_INPUTS: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
755 name: "x",
756 ty: BuiltinParamType::NumericScalar,
757 arity: BuiltinParamArity::Required,
758 default: None,
759 description: "Scalar objective input.",
760 }];
761
762 const FMINBND_HELPER_SIGNATURES: [BuiltinSignatureDescriptor; 1] =
763 [BuiltinSignatureDescriptor {
764 label: "fx = __fminbnd_helper(x)",
765 inputs: &FMINBND_HELPER_INPUTS,
766 outputs: &FMINBND_HELPER_OUTPUT,
767 }];
768
769 const FMINBND_HELPER_ERRORS: [BuiltinErrorDescriptor; 0] = [];
770
771 pub const FMINBND_TEST_HELPER_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
772 signatures: &FMINBND_HELPER_SIGNATURES,
773 output_mode: BuiltinOutputMode::Fixed,
774 completion_policy: BuiltinCompletionPolicy::HiddenInternal,
775 errors: &FMINBND_HELPER_ERRORS,
776 };
777
778 fn run_default(handle: &str, lo: f64, hi: f64) -> Value {
779 block_on(fminbnd_builtin(
780 V::FunctionHandle(handle.into()),
781 V::Num(lo),
782 V::Num(hi),
783 Vec::new(),
784 ))
785 .expect("fminbnd")
786 }
787
788 fn run_with(handle: &str, lo: f64, hi: f64, extra: Vec<Value>) -> Value {
789 block_on(fminbnd_builtin(
790 V::FunctionHandle(handle.into()),
791 V::Num(lo),
792 V::Num(hi),
793 extra,
794 ))
795 .expect("fminbnd")
796 }
797
798 #[test]
799 fn fminbnd_test_helper_descriptor_is_attached_shape() {
800 assert_eq!(
801 FMINBND_TEST_HELPER_DESCRIPTOR.signatures[0].label,
802 "fx = __fminbnd_helper(x)"
803 );
804 }
805
806 #[runtime_builtin(
807 name = "__fminbnd_quad_minus_two",
808 type_resolver(crate::builtins::math::optim::type_resolvers::scalar_root_type),
809 descriptor(crate::builtins::math::optim::fminbnd::tests::FMINBND_TEST_HELPER_DESCRIPTOR),
810 builtin_path = "crate::builtins::math::optim::fminbnd::tests"
811 )]
812 async fn quad_minus_two(x: Value) -> crate::BuiltinResult<Value> {
813 let x = scalar_bound("x", x).await?;
814 let diff = x - 2.0;
815 Ok(Value::Num(diff * diff))
816 }
817
818 #[runtime_builtin(
819 name = "__fminbnd_quad_minus_three",
820 type_resolver(crate::builtins::math::optim::type_resolvers::scalar_root_type),
821 descriptor(crate::builtins::math::optim::fminbnd::tests::FMINBND_TEST_HELPER_DESCRIPTOR),
822 builtin_path = "crate::builtins::math::optim::fminbnd::tests"
823 )]
824 async fn quad_minus_three(x: Value) -> crate::BuiltinResult<Value> {
825 let x = scalar_bound("x", x).await?;
826 let diff = x - 3.0;
827 Ok(Value::Num(diff * diff))
828 }
829
830 #[runtime_builtin(
831 name = "__fminbnd_multi_modal",
832 type_resolver(crate::builtins::math::optim::type_resolvers::scalar_root_type),
833 descriptor(crate::builtins::math::optim::fminbnd::tests::FMINBND_TEST_HELPER_DESCRIPTOR),
834 builtin_path = "crate::builtins::math::optim::fminbnd::tests"
835 )]
836 async fn multi_modal(x: Value) -> crate::BuiltinResult<Value> {
837 let x = scalar_bound("x", x).await?;
839 Ok(Value::Num(1.0 + (3.0 * x).sin()))
840 }
841
842 #[test]
843 fn locates_smooth_quadratic_minimum() {
844 let result = run_default("__fminbnd_quad_minus_two", 0.0, 5.0);
845 match result {
846 V::Num(x) => assert!((x - 2.0).abs() < 1.0e-3, "x = {x}"),
847 other => panic!("unexpected value {other:?}"),
848 }
849 }
850
851 #[test]
852 fn locates_quadratic_minimum_offset_three() {
853 let result = run_default("__fminbnd_quad_minus_three", 0.0, 5.0);
854 match result {
855 V::Num(x) => assert!((x - 3.0).abs() < 1.0e-3, "x = {x}"),
856 other => panic!("unexpected value {other:?}"),
857 }
858 }
859
860 #[test]
861 fn locates_cosine_minimum_at_right_endpoint() {
862 let result = run_default("cos", 0.0, std::f64::consts::PI);
864 match result {
865 V::Num(x) => assert!((x - std::f64::consts::PI).abs() < 1.0e-3, "x = {x}"),
866 other => panic!("unexpected value {other:?}"),
867 }
868 }
869
870 #[test]
871 fn returns_lone_endpoint_when_bounds_collapse() {
872 let result = run_default("__fminbnd_quad_minus_two", 1.5, 1.5);
873 match result {
874 V::Num(x) => assert!((x - 1.5).abs() < 1.0e-12, "x = {x}"),
875 other => panic!("unexpected value {other:?}"),
876 }
877 }
878
879 #[test]
880 fn reports_inconsistent_reversed_bounds() {
881 let _guard = crate::output_count::push_output_count(Some(4));
882 let result = block_on(fminbnd_builtin(
883 V::FunctionHandle("__fminbnd_quad_minus_two".into()),
884 V::Num(5.0),
885 V::Num(0.0),
886 Vec::new(),
887 ))
888 .expect("fminbnd");
889 match result {
890 V::OutputList(outputs) => {
891 assert_eq!(outputs.len(), 4);
892 assert!(matches!(&outputs[0], V::Tensor(t) if t.data.is_empty()));
893 assert!(matches!(&outputs[1], V::Tensor(t) if t.data.is_empty()));
894 assert!(matches!(&outputs[2], V::Num(flag) if *flag == -2.0));
895 match &outputs[3] {
896 V::Struct(s) => {
897 assert!(matches!(s.fields.get("iterations"), Some(V::Num(0.0))));
898 assert!(matches!(s.fields.get("funcCount"), Some(V::Num(0.0))));
899 match s.fields.get("message") {
900 Some(V::String(text)) => assert!(text.contains("bounds")),
901 other => panic!("unexpected message field {other:?}"),
902 }
903 }
904 other => panic!("unexpected output struct {other:?}"),
905 }
906 }
907 other => panic!("unexpected value {other:?}"),
908 }
909 }
910
911 #[test]
912 fn tolerance_is_additive_not_scaled_by_x() {
913 let params = BrentParams {
914 tol_x: 1.0e-4,
915 max_iter: 500,
916 max_fun_evals: 500,
917 };
918 let small = brent_min_tolerance(2.0, params);
919 let large = brent_min_tolerance(1.0e9, params);
920 assert!(small > params.tol_x);
921 assert!(
922 large < params.tol_x * 1.0e9,
923 "large-scale tolerance was {large}"
924 );
925 }
926
927 #[test]
928 fn finds_local_minimum_in_multi_modal_function() {
929 let result = run_default("__fminbnd_multi_modal", 1.5, 3.5);
931 match result {
932 V::Num(x) => {
933 let target = std::f64::consts::PI / 2.0;
934 assert!((x - target).abs() < 5.0e-3, "x = {x}, target = {target}");
935 }
936 other => panic!("unexpected value {other:?}"),
937 }
938 }
939
940 #[test]
941 fn options_struct_overrides_default_tolerance() {
942 let mut opts = StructValue::new();
943 opts.insert("TolX", Value::Num(1.0e-12));
944 let result = run_with(
945 "__fminbnd_quad_minus_two",
946 0.0,
947 5.0,
948 vec![Value::Struct(opts)],
949 );
950 match result {
951 V::Num(x) => assert!((x - 2.0).abs() < 1.0e-6, "x = {x}"),
952 other => panic!("unexpected value {other:?}"),
953 }
954 }
955
956 #[test]
957 fn max_fun_evals_default_is_independent_of_max_iter() {
958 let mut opts = StructValue::new();
959 opts.insert("MaxIter", Value::Num(1000.0));
960 let parsed = FminbndOptions::from_struct(Some(&opts)).unwrap();
961 assert_eq!(parsed.max_iter, 1000);
962 assert_eq!(parsed.max_fun_evals, DEFAULT_MAX_FUN_EVALS);
963 }
964
965 #[test]
966 fn rejects_nonfinite_bounds() {
967 let err = block_on(fminbnd_builtin(
968 V::FunctionHandle("__fminbnd_quad_minus_two".into()),
969 V::Num(f64::NAN),
970 V::Num(5.0),
971 Vec::new(),
972 ))
973 .unwrap_err();
974 assert!(err.message().to_ascii_lowercase().contains("finite"));
975 }
976
977 #[test]
978 fn rejects_invalid_options_type() {
979 let err = block_on(fminbnd_builtin(
980 V::FunctionHandle("__fminbnd_quad_minus_two".into()),
981 V::Num(0.0),
982 V::Num(5.0),
983 vec![Value::Num(1.0)],
984 ))
985 .unwrap_err();
986 assert!(err.message().to_ascii_lowercase().contains("options"));
987 }
988
989 #[test]
990 fn rejects_nonpositive_tol_x() {
991 let mut opts = StructValue::new();
992 opts.insert("TolX", Value::Num(0.0));
993 let err = block_on(fminbnd_builtin(
994 V::FunctionHandle("__fminbnd_quad_minus_two".into()),
995 V::Num(0.0),
996 V::Num(5.0),
997 vec![Value::Struct(opts)],
998 ))
999 .unwrap_err();
1000 assert!(err.message().to_lowercase().contains("tolx"));
1001 }
1002
1003 #[test]
1004 fn rejects_unknown_display_value() {
1005 let mut opts = StructValue::new();
1006 opts.insert("Display", Value::from("loud"));
1007 let err = block_on(fminbnd_builtin(
1008 V::FunctionHandle("__fminbnd_quad_minus_two".into()),
1009 V::Num(0.0),
1010 V::Num(5.0),
1011 vec![Value::Struct(opts)],
1012 ))
1013 .unwrap_err();
1014 assert!(err.message().to_lowercase().contains("display"));
1015 }
1016
1017 #[test]
1018 fn multi_output_two_returns_x_and_fval() {
1019 let _guard = crate::output_count::push_output_count(Some(2));
1020 let result = block_on(fminbnd_builtin(
1021 V::FunctionHandle("__fminbnd_quad_minus_two".into()),
1022 V::Num(0.0),
1023 V::Num(5.0),
1024 Vec::new(),
1025 ))
1026 .expect("fminbnd");
1027 match result {
1028 V::OutputList(outputs) => {
1029 assert_eq!(outputs.len(), 2);
1030 match (&outputs[0], &outputs[1]) {
1031 (V::Num(x), V::Num(fval)) => {
1032 assert!((x - 2.0).abs() < 1.0e-3);
1033 assert!(fval.abs() < 1.0e-5);
1034 }
1035 other => panic!("unexpected outputs {other:?}"),
1036 }
1037 }
1038 other => panic!("unexpected value {other:?}"),
1039 }
1040 }
1041
1042 #[test]
1043 fn multi_output_three_includes_exitflag() {
1044 let _guard = crate::output_count::push_output_count(Some(3));
1045 let result = block_on(fminbnd_builtin(
1046 V::FunctionHandle("__fminbnd_quad_minus_two".into()),
1047 V::Num(0.0),
1048 V::Num(5.0),
1049 Vec::new(),
1050 ))
1051 .expect("fminbnd");
1052 match result {
1053 V::OutputList(outputs) => {
1054 assert_eq!(outputs.len(), 3);
1055 match &outputs[2] {
1056 V::Num(flag) => assert!((*flag - 1.0).abs() < 1.0e-12),
1057 other => panic!("unexpected exitflag {other:?}"),
1058 }
1059 }
1060 other => panic!("unexpected value {other:?}"),
1061 }
1062 }
1063
1064 #[test]
1065 fn multi_output_four_includes_output_struct() {
1066 let _guard = crate::output_count::push_output_count(Some(4));
1067 let result = block_on(fminbnd_builtin(
1068 V::FunctionHandle("__fminbnd_quad_minus_two".into()),
1069 V::Num(0.0),
1070 V::Num(5.0),
1071 Vec::new(),
1072 ))
1073 .expect("fminbnd");
1074 match result {
1075 V::OutputList(outputs) => {
1076 assert_eq!(outputs.len(), 4);
1077 match &outputs[3] {
1078 V::Struct(s) => {
1079 assert!(matches!(s.fields.get("iterations"), Some(V::Num(_))));
1080 assert!(matches!(s.fields.get("funcCount"), Some(V::Num(_))));
1081 match s.fields.get("algorithm") {
1082 Some(V::String(text)) => assert!(text.contains("golden")),
1083 other => panic!("unexpected algorithm field {other:?}"),
1084 }
1085 assert!(s.fields.get("message").is_some());
1086 }
1087 other => panic!("unexpected output struct {other:?}"),
1088 }
1089 }
1090 other => panic!("unexpected value {other:?}"),
1091 }
1092 }
1093
1094 #[test]
1095 fn reports_zero_exitflag_when_max_iter_exhausted() {
1096 let mut opts = StructValue::new();
1097 opts.insert("MaxIter", Value::Num(1.0));
1098 opts.insert("MaxFunEvals", Value::Num(2.0));
1099 opts.insert("Display", Value::from("off"));
1100 let _guard = crate::output_count::push_output_count(Some(3));
1101 let result = block_on(fminbnd_builtin(
1102 V::FunctionHandle("__fminbnd_quad_minus_two".into()),
1103 V::Num(0.0),
1104 V::Num(5.0),
1105 vec![Value::Struct(opts)],
1106 ))
1107 .expect("fminbnd");
1108 match result {
1109 V::OutputList(outputs) => match &outputs[2] {
1110 V::Num(flag) => assert_eq!(*flag, 0.0),
1111 other => panic!("unexpected exitflag {other:?}"),
1112 },
1113 other => panic!("unexpected value {other:?}"),
1114 }
1115 }
1116
1117 #[test]
1118 fn fminbnd_descriptor_signatures_cover_core_forms() {
1119 let labels: Vec<&str> = FMINBND_DESCRIPTOR
1120 .signatures
1121 .iter()
1122 .map(|signature| signature.label)
1123 .collect();
1124 assert_eq!(
1125 labels,
1126 vec![
1127 "x = fminbnd(fun, x1, x2)",
1128 "x = fminbnd(fun, x1, x2, options)",
1129 "[x, fval] = fminbnd(fun, x1, x2)",
1130 "[x, fval] = fminbnd(fun, x1, x2, options)",
1131 "[x, fval, exitflag] = fminbnd(fun, x1, x2)",
1132 "[x, fval, exitflag] = fminbnd(fun, x1, x2, options)",
1133 "[x, fval, exitflag, output] = fminbnd(fun, x1, x2)",
1134 "[x, fval, exitflag, output] = fminbnd(fun, x1, x2, options)",
1135 ]
1136 );
1137
1138 let codes: Vec<&str> = FMINBND_DESCRIPTOR
1139 .errors
1140 .iter()
1141 .map(|error| error.code)
1142 .collect();
1143 assert_eq!(
1144 codes,
1145 vec!["RM.FMINBND.INVALID_ARGUMENT", "RM.FMINBND.INVALID_INPUT"]
1146 );
1147 }
1148
1149 #[test]
1150 fn fminbnd_too_many_args_uses_stable_identifier() {
1151 let err = block_on(fminbnd_builtin(
1152 V::FunctionHandle("__fminbnd_quad_minus_two".into()),
1153 V::Num(0.0),
1154 V::Num(5.0),
1155 vec![
1156 Value::Struct(StructValue::new()),
1157 Value::Struct(StructValue::new()),
1158 ],
1159 ))
1160 .unwrap_err();
1161 assert_eq!(err.identifier(), Some("RunMat:fminbnd:InvalidArgument"));
1162 }
1163}