1use runmat_builtins::{LogicalArray, StructValue, Tensor, Value};
17use runmat_macros::runtime_builtin;
18
19use crate::builtins::common::spec::{
20 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
21 ReductionNaN, ResidencyPolicy, ShapeRequirements,
22};
23use crate::builtins::math::optim::brent::{
24 brent_min, BrentMinObserver, BrentMinResult, BrentParams, BrentStepKind,
25};
26use crate::builtins::math::optim::common::optim_error;
27use crate::builtins::math::optim::type_resolvers::scalar_root_type;
28use crate::BuiltinResult;
29
30const NAME: &str = "fminbnd";
31const ALGORITHM: &str = "golden section search, parabolic interpolation";
32const DEFAULT_TOL_X: f64 = 1.0e-4;
33const DEFAULT_MAX_ITER: usize = 500;
34const DEFAULT_MAX_FUN_EVALS: usize = 500;
35const DEFAULT_DISPLAY: DisplayMode = DisplayMode::Notify;
36
37#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::math::optim::fminbnd")]
38pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
39 name: "fminbnd",
40 op_kind: GpuOpKind::Custom("bounded-scalar-min"),
41 supported_precisions: &[],
42 broadcast: BroadcastSemantics::None,
43 provider_hooks: &[],
44 constant_strategy: ConstantStrategy::InlineLiteral,
45 residency: ResidencyPolicy::GatherImmediately,
46 nan_mode: ReductionNaN::Include,
47 two_pass_threshold: None,
48 workgroup_size: None,
49 accepts_nan_mode: false,
50 notes: "Host iterative solver. Callback computations may use GPU-aware builtins, but the minimization loop runs on the CPU.",
51};
52
53#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::math::optim::fminbnd")]
54pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
55 name: "fminbnd",
56 shape: ShapeRequirements::Any,
57 constant_strategy: ConstantStrategy::InlineLiteral,
58 elementwise: None,
59 reduction: None,
60 emits_nan: false,
61 notes:
62 "Bounded scalar minimization repeatedly invokes user code and terminates fusion planning.",
63};
64
65#[runtime_builtin(
66 name = "fminbnd",
67 category = "math/optim",
68 summary = "Find a local minimum of a scalar function on a bounded interval using Brent's method.",
69 keywords = "fminbnd,bounded minimization,brent,golden section,parabolic interpolation,optimization",
70 accel = "sink",
71 type_resolver(scalar_root_type),
72 builtin_path = "crate::builtins::math::optim::fminbnd"
73)]
74async fn fminbnd_builtin(
75 function: Value,
76 x1: Value,
77 x2: Value,
78 rest: Vec<Value>,
79) -> BuiltinResult<Value> {
80 if rest.len() > 1 {
81 return Err(optim_error(NAME, "fminbnd: too many input arguments"));
82 }
83 let options_struct = parse_options(rest.first())?;
84 let options = FminbndOptions::from_struct(options_struct.as_ref())?;
85 let x1 = scalar_bound("lower bound", x1).await?;
86 let x2 = scalar_bound("upper bound", x2).await?;
87
88 if !x1.is_finite() || !x2.is_finite() {
89 return Err(optim_error(NAME, "fminbnd: bounds must be finite"));
90 }
91 if x1 > x2 {
92 return finalize_inconsistent_bounds(&options);
93 }
94
95 let outcome = run_solver(&function, x1, x2, &options).await?;
96 finalize(outcome, &options)
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100enum DisplayMode {
101 Off,
102 Iter,
103 Notify,
104 Final,
105}
106
107impl DisplayMode {
108 fn parse(text: &str) -> BuiltinResult<Self> {
109 match text.to_ascii_lowercase().as_str() {
110 "off" | "none" => Ok(Self::Off),
111 "iter" => Ok(Self::Iter),
112 "notify" => Ok(Self::Notify),
113 "final" => Ok(Self::Final),
114 other => Err(optim_error(
115 NAME,
116 format!(
117 "fminbnd: option Display must be 'off', 'iter', 'notify', or 'final', got '{other}'"
118 ),
119 )),
120 }
121 }
122}
123
124#[derive(Debug, Clone, Copy)]
125struct FminbndOptions {
126 tol_x: f64,
127 max_iter: usize,
128 max_fun_evals: usize,
129 display: DisplayMode,
130}
131
132impl FminbndOptions {
133 fn from_struct(options: Option<&StructValue>) -> BuiltinResult<Self> {
134 let display = match options {
135 Some(opts) => match lookup(opts, "Display") {
136 Some(value) => DisplayMode::parse(&option_string("Display", value)?)?,
137 None => DEFAULT_DISPLAY,
138 },
139 None => DEFAULT_DISPLAY,
140 };
141 let tol_x = match options.and_then(|o| lookup(o, "TolX")) {
142 Some(value) => option_f64("TolX", value)?,
143 None => DEFAULT_TOL_X,
144 };
145 if tol_x <= 0.0 {
146 return Err(optim_error(NAME, "fminbnd: option TolX must be positive"));
147 }
148 let max_iter = match options.and_then(|o| lookup(o, "MaxIter")) {
149 Some(value) => option_positive_usize("MaxIter", value)?,
150 None => DEFAULT_MAX_ITER,
151 };
152 let max_fun_evals = match options.and_then(|o| lookup(o, "MaxFunEvals")) {
153 Some(value) => option_positive_usize("MaxFunEvals", value)?,
154 None => DEFAULT_MAX_FUN_EVALS,
155 };
156 Ok(Self {
157 tol_x,
158 max_iter,
159 max_fun_evals,
160 display,
161 })
162 }
163}
164
165fn parse_options(value: Option<&Value>) -> BuiltinResult<Option<StructValue>> {
166 match value {
167 None => Ok(None),
168 Some(Value::Struct(options)) => Ok(Some(options.clone())),
169 Some(other) => Err(optim_error(
170 NAME,
171 format!("fminbnd: options must be a struct, got {other:?}"),
172 )),
173 }
174}
175
176fn lookup<'a>(options: &'a StructValue, name: &str) -> Option<&'a Value> {
177 options
178 .fields
179 .iter()
180 .find(|(key, _)| key.eq_ignore_ascii_case(name))
181 .map(|(_, v)| v)
182}
183
184fn option_string(field: &str, value: &Value) -> BuiltinResult<String> {
185 match value {
186 Value::String(s) => Ok(s.clone()),
187 Value::StringArray(sa) if sa.data.len() == 1 => Ok(sa.data[0].clone()),
188 Value::CharArray(chars) if chars.rows == 1 => Ok(chars.data.iter().collect()),
189 other => Err(optim_error(
190 NAME,
191 format!("fminbnd: option {field} must be a string, got {other:?}"),
192 )),
193 }
194}
195
196fn option_f64(field: &str, value: &Value) -> BuiltinResult<f64> {
197 let parsed = match value {
198 Value::Num(n) => *n,
199 Value::Int(i) => i.to_f64(),
200 Value::Bool(b) => {
201 if *b {
202 1.0
203 } else {
204 0.0
205 }
206 }
207 Value::Tensor(Tensor { data, .. }) if data.len() == 1 => data[0],
208 Value::LogicalArray(LogicalArray { data, .. }) if data.len() == 1 => {
209 if data[0] != 0 {
210 1.0
211 } else {
212 0.0
213 }
214 }
215 other => {
216 return Err(optim_error(
217 NAME,
218 format!("fminbnd: option {field} must be a real scalar, got {other:?}"),
219 ))
220 }
221 };
222 if parsed.is_finite() {
223 Ok(parsed)
224 } else {
225 Err(optim_error(
226 NAME,
227 format!("fminbnd: option {field} must be finite"),
228 ))
229 }
230}
231
232fn option_positive_usize(field: &str, value: &Value) -> BuiltinResult<usize> {
233 let parsed = option_f64(field, value)?;
234 if parsed < 1.0 {
235 return Err(optim_error(
236 NAME,
237 format!("fminbnd: option {field} must be a positive integer"),
238 ));
239 }
240 if parsed.fract() != 0.0 {
241 return Err(optim_error(
242 NAME,
243 format!("fminbnd: option {field} must be an integer scalar"),
244 ));
245 }
246 Ok(parsed as usize)
247}
248
249async fn scalar_bound(label: &str, value: Value) -> BuiltinResult<f64> {
250 let value = crate::dispatcher::gather_if_needed_async(&value).await?;
251 let parsed = match value {
252 Value::Num(n) => n,
253 Value::Int(i) => i.to_f64(),
254 Value::Bool(b) => {
255 if b {
256 1.0
257 } else {
258 0.0
259 }
260 }
261 Value::Tensor(t) if t.data.len() == 1 => t.data[0],
262 Value::LogicalArray(LogicalArray { data, .. }) if data.len() == 1 => {
263 if data[0] != 0 {
264 1.0
265 } else {
266 0.0
267 }
268 }
269 other => {
270 return Err(optim_error(
271 NAME,
272 format!("fminbnd: {label} must be a finite real scalar, got {other:?}"),
273 ))
274 }
275 };
276 if parsed.is_finite() {
277 Ok(parsed)
278 } else {
279 Err(optim_error(
280 NAME,
281 format!("fminbnd: {label} must be finite"),
282 ))
283 }
284}
285
286#[derive(Debug, Clone)]
287struct Outcome {
288 inner: BrentMinResult,
289}
290
291async fn run_solver(
292 function: &Value,
293 lo: f64,
294 hi: f64,
295 options: &FminbndOptions,
296) -> BuiltinResult<Outcome> {
297 let mut iter_log = IterDisplay::new(options.display);
298 let observer: Option<&mut dyn BrentMinObserver> =
299 if matches!(options.display, DisplayMode::Iter) {
300 Some(&mut iter_log)
301 } else {
302 None
303 };
304 let inner = brent_min(
305 NAME,
306 function,
307 lo,
308 hi,
309 BrentParams {
310 tol_x: options.tol_x,
311 max_iter: options.max_iter,
312 max_fun_evals: options.max_fun_evals,
313 },
314 observer,
315 )
316 .await?;
317 Ok(Outcome { inner })
318}
319
320fn finalize(outcome: Outcome, options: &FminbndOptions) -> BuiltinResult<Value> {
321 let exit_flag = if outcome.inner.converged { 1 } else { 0 };
322 let message = build_message(&outcome.inner);
323
324 emit_summary(&outcome.inner, exit_flag, &message, options);
325
326 let x = Value::Num(outcome.inner.x);
327 let fval = Value::Num(outcome.inner.fval);
328 let exitflag = Value::Num(exit_flag as f64);
329 let output_struct = Value::Struct(build_output_struct(&outcome.inner, &message));
330
331 match crate::output_count::current_output_count() {
332 None => Ok(x),
333 Some(0) => Ok(Value::OutputList(Vec::new())),
334 Some(1) => Ok(crate::output_count::output_list_with_padding(1, vec![x])),
335 Some(2) => Ok(crate::output_count::output_list_with_padding(
336 2,
337 vec![x, fval],
338 )),
339 Some(3) => Ok(crate::output_count::output_list_with_padding(
340 3,
341 vec![x, fval, exitflag],
342 )),
343 Some(n) if n >= 4 => Ok(crate::output_count::output_list_with_padding(
344 n,
345 vec![x, fval, exitflag, output_struct],
346 )),
347 Some(_) => Ok(x),
348 }
349}
350
351fn finalize_inconsistent_bounds(options: &FminbndOptions) -> BuiltinResult<Value> {
352 let message = "Exiting: The bounds are inconsistent because x1 > x2.".to_string();
353 emit_invalid_summary(-2, &message, options);
354
355 let x = empty_double();
356 let fval = empty_double();
357 let exitflag = Value::Num(-2.0);
358 let output_struct = Value::Struct(build_invalid_output_struct(&message));
359
360 match crate::output_count::current_output_count() {
361 None => Ok(x),
362 Some(0) => Ok(Value::OutputList(Vec::new())),
363 Some(1) => Ok(crate::output_count::output_list_with_padding(1, vec![x])),
364 Some(2) => Ok(crate::output_count::output_list_with_padding(
365 2,
366 vec![x, fval],
367 )),
368 Some(3) => Ok(crate::output_count::output_list_with_padding(
369 3,
370 vec![x, fval, exitflag],
371 )),
372 Some(n) if n >= 4 => Ok(crate::output_count::output_list_with_padding(
373 n,
374 vec![x, fval, exitflag, output_struct],
375 )),
376 Some(_) => Ok(empty_double()),
377 }
378}
379
380fn empty_double() -> Value {
381 Value::Tensor(Tensor::zeros(vec![0, 0]))
382}
383
384fn build_output_struct(result: &BrentMinResult, message: &str) -> StructValue {
385 let mut fields = StructValue::new();
386 fields.insert("iterations", Value::Num(result.iterations as f64));
387 fields.insert("funcCount", Value::Num(result.func_count as f64));
388 fields.insert("algorithm", Value::from(ALGORITHM));
389 fields.insert("message", Value::from(message.to_string()));
390 fields
391}
392
393fn build_invalid_output_struct(message: &str) -> StructValue {
394 let mut fields = StructValue::new();
395 fields.insert("iterations", Value::Num(0.0));
396 fields.insert("funcCount", Value::Num(0.0));
397 fields.insert("algorithm", Value::from(ALGORITHM));
398 fields.insert("message", Value::from(message.to_string()));
399 fields
400}
401
402fn build_message(result: &BrentMinResult) -> String {
403 if result.converged {
404 format!(
405 "Optimization terminated: the current x satisfies the termination criteria using OPTIONS.TolX. Iterations: {}, FuncCount: {}.",
406 result.iterations, result.func_count
407 )
408 } else {
409 format!(
410 "Exiting: Maximum number of function evaluations or iterations has been exceeded - increase MaxFunEvals or MaxIter. Iterations: {}, FuncCount: {}.",
411 result.iterations, result.func_count
412 )
413 }
414}
415
416fn emit_summary(result: &BrentMinResult, exit_flag: i32, message: &str, options: &FminbndOptions) {
417 let should_emit = match options.display {
418 DisplayMode::Off => false,
419 DisplayMode::Final | DisplayMode::Iter => true,
420 DisplayMode::Notify => exit_flag != 1,
421 };
422 if !should_emit {
423 return;
424 }
425 let line = format!(
426 "fminbnd: x = {x:.6}, fval = {fval:.6}, exitflag = {exit_flag}. {message}",
427 x = result.x,
428 fval = result.fval,
429 );
430 crate::console::record_console_line(crate::console::ConsoleStream::Stdout, line);
431}
432
433fn emit_invalid_summary(exit_flag: i32, message: &str, options: &FminbndOptions) {
434 let should_emit = match options.display {
435 DisplayMode::Off => false,
436 DisplayMode::Final | DisplayMode::Iter => true,
437 DisplayMode::Notify => exit_flag != 1,
438 };
439 if should_emit {
440 crate::console::record_console_line(
441 crate::console::ConsoleStream::Stdout,
442 format!("fminbnd: exitflag = {exit_flag}. {message}"),
443 );
444 }
445}
446
447struct IterDisplay {
448 mode: DisplayMode,
449 printed_header: bool,
450}
451
452impl IterDisplay {
453 fn new(mode: DisplayMode) -> Self {
454 Self {
455 mode,
456 printed_header: false,
457 }
458 }
459}
460
461impl BrentMinObserver for IterDisplay {
462 fn on_iteration(
463 &mut self,
464 iter: usize,
465 func_count: usize,
466 x: f64,
467 fx: f64,
468 step_kind: BrentStepKind,
469 ) {
470 if !matches!(self.mode, DisplayMode::Iter) {
471 return;
472 }
473 if !self.printed_header {
474 crate::console::record_console_line(
475 crate::console::ConsoleStream::Stdout,
476 " Func-count x f(x) Procedure",
477 );
478 self.printed_header = true;
479 }
480 let procedure = match step_kind {
481 BrentStepKind::Initial => "initial",
482 BrentStepKind::GoldenSection => "golden",
483 BrentStepKind::Parabolic => "parabolic",
484 };
485 let line =
486 format!(" {func_count:>5} {x:13.6e} {fx:13.6e} {procedure} (iter {iter})");
487 crate::console::record_console_line(crate::console::ConsoleStream::Stdout, line);
488 }
489}
490
491#[cfg(test)]
492mod tests {
493 use super::*;
494 use crate::builtins::math::optim::brent::brent_min_tolerance;
495 use futures::executor::block_on;
496 use runmat_builtins::Value as V;
497
498 fn run_default(handle: &str, lo: f64, hi: f64) -> Value {
499 block_on(fminbnd_builtin(
500 V::FunctionHandle(handle.into()),
501 V::Num(lo),
502 V::Num(hi),
503 Vec::new(),
504 ))
505 .expect("fminbnd")
506 }
507
508 fn run_with(handle: &str, lo: f64, hi: f64, extra: Vec<Value>) -> Value {
509 block_on(fminbnd_builtin(
510 V::FunctionHandle(handle.into()),
511 V::Num(lo),
512 V::Num(hi),
513 extra,
514 ))
515 .expect("fminbnd")
516 }
517
518 #[runtime_builtin(
519 name = "__fminbnd_quad_minus_two",
520 type_resolver(crate::builtins::math::optim::type_resolvers::scalar_root_type),
521 builtin_path = "crate::builtins::math::optim::fminbnd::tests"
522 )]
523 async fn quad_minus_two(x: Value) -> crate::BuiltinResult<Value> {
524 let x = scalar_bound("x", x).await?;
525 let diff = x - 2.0;
526 Ok(Value::Num(diff * diff))
527 }
528
529 #[runtime_builtin(
530 name = "__fminbnd_quad_minus_three",
531 type_resolver(crate::builtins::math::optim::type_resolvers::scalar_root_type),
532 builtin_path = "crate::builtins::math::optim::fminbnd::tests"
533 )]
534 async fn quad_minus_three(x: Value) -> crate::BuiltinResult<Value> {
535 let x = scalar_bound("x", x).await?;
536 let diff = x - 3.0;
537 Ok(Value::Num(diff * diff))
538 }
539
540 #[runtime_builtin(
541 name = "__fminbnd_multi_modal",
542 type_resolver(crate::builtins::math::optim::type_resolvers::scalar_root_type),
543 builtin_path = "crate::builtins::math::optim::fminbnd::tests"
544 )]
545 async fn multi_modal(x: Value) -> crate::BuiltinResult<Value> {
546 let x = scalar_bound("x", x).await?;
548 Ok(Value::Num(1.0 + (3.0 * x).sin()))
549 }
550
551 #[test]
552 fn locates_smooth_quadratic_minimum() {
553 let result = run_default("__fminbnd_quad_minus_two", 0.0, 5.0);
554 match result {
555 V::Num(x) => assert!((x - 2.0).abs() < 1.0e-3, "x = {x}"),
556 other => panic!("unexpected value {other:?}"),
557 }
558 }
559
560 #[test]
561 fn locates_quadratic_minimum_offset_three() {
562 let result = run_default("__fminbnd_quad_minus_three", 0.0, 5.0);
563 match result {
564 V::Num(x) => assert!((x - 3.0).abs() < 1.0e-3, "x = {x}"),
565 other => panic!("unexpected value {other:?}"),
566 }
567 }
568
569 #[test]
570 fn locates_cosine_minimum_at_right_endpoint() {
571 let result = run_default("cos", 0.0, std::f64::consts::PI);
573 match result {
574 V::Num(x) => assert!((x - std::f64::consts::PI).abs() < 1.0e-3, "x = {x}"),
575 other => panic!("unexpected value {other:?}"),
576 }
577 }
578
579 #[test]
580 fn returns_lone_endpoint_when_bounds_collapse() {
581 let result = run_default("__fminbnd_quad_minus_two", 1.5, 1.5);
582 match result {
583 V::Num(x) => assert!((x - 1.5).abs() < 1.0e-12, "x = {x}"),
584 other => panic!("unexpected value {other:?}"),
585 }
586 }
587
588 #[test]
589 fn reports_inconsistent_reversed_bounds() {
590 let _guard = crate::output_count::push_output_count(Some(4));
591 let result = block_on(fminbnd_builtin(
592 V::FunctionHandle("__fminbnd_quad_minus_two".into()),
593 V::Num(5.0),
594 V::Num(0.0),
595 Vec::new(),
596 ))
597 .expect("fminbnd");
598 match result {
599 V::OutputList(outputs) => {
600 assert_eq!(outputs.len(), 4);
601 assert!(matches!(&outputs[0], V::Tensor(t) if t.data.is_empty()));
602 assert!(matches!(&outputs[1], V::Tensor(t) if t.data.is_empty()));
603 assert!(matches!(&outputs[2], V::Num(flag) if *flag == -2.0));
604 match &outputs[3] {
605 V::Struct(s) => {
606 assert!(matches!(s.fields.get("iterations"), Some(V::Num(0.0))));
607 assert!(matches!(s.fields.get("funcCount"), Some(V::Num(0.0))));
608 match s.fields.get("message") {
609 Some(V::String(text)) => assert!(text.contains("bounds")),
610 other => panic!("unexpected message field {other:?}"),
611 }
612 }
613 other => panic!("unexpected output struct {other:?}"),
614 }
615 }
616 other => panic!("unexpected value {other:?}"),
617 }
618 }
619
620 #[test]
621 fn tolerance_is_additive_not_scaled_by_x() {
622 let params = BrentParams {
623 tol_x: 1.0e-4,
624 max_iter: 500,
625 max_fun_evals: 500,
626 };
627 let small = brent_min_tolerance(2.0, params);
628 let large = brent_min_tolerance(1.0e9, params);
629 assert!(small > params.tol_x);
630 assert!(
631 large < params.tol_x * 1.0e9,
632 "large-scale tolerance was {large}"
633 );
634 }
635
636 #[test]
637 fn finds_local_minimum_in_multi_modal_function() {
638 let result = run_default("__fminbnd_multi_modal", 1.5, 3.5);
640 match result {
641 V::Num(x) => {
642 let target = std::f64::consts::PI / 2.0;
643 assert!((x - target).abs() < 5.0e-3, "x = {x}, target = {target}");
644 }
645 other => panic!("unexpected value {other:?}"),
646 }
647 }
648
649 #[test]
650 fn options_struct_overrides_default_tolerance() {
651 let mut opts = StructValue::new();
652 opts.insert("TolX", Value::Num(1.0e-12));
653 let result = run_with(
654 "__fminbnd_quad_minus_two",
655 0.0,
656 5.0,
657 vec![Value::Struct(opts)],
658 );
659 match result {
660 V::Num(x) => assert!((x - 2.0).abs() < 1.0e-6, "x = {x}"),
661 other => panic!("unexpected value {other:?}"),
662 }
663 }
664
665 #[test]
666 fn max_fun_evals_default_is_independent_of_max_iter() {
667 let mut opts = StructValue::new();
668 opts.insert("MaxIter", Value::Num(1000.0));
669 let parsed = FminbndOptions::from_struct(Some(&opts)).unwrap();
670 assert_eq!(parsed.max_iter, 1000);
671 assert_eq!(parsed.max_fun_evals, DEFAULT_MAX_FUN_EVALS);
672 }
673
674 #[test]
675 fn rejects_nonfinite_bounds() {
676 let err = block_on(fminbnd_builtin(
677 V::FunctionHandle("__fminbnd_quad_minus_two".into()),
678 V::Num(f64::NAN),
679 V::Num(5.0),
680 Vec::new(),
681 ))
682 .unwrap_err();
683 assert!(err.message().to_ascii_lowercase().contains("finite"));
684 }
685
686 #[test]
687 fn rejects_invalid_options_type() {
688 let err = block_on(fminbnd_builtin(
689 V::FunctionHandle("__fminbnd_quad_minus_two".into()),
690 V::Num(0.0),
691 V::Num(5.0),
692 vec![Value::Num(1.0)],
693 ))
694 .unwrap_err();
695 assert!(err.message().to_ascii_lowercase().contains("options"));
696 }
697
698 #[test]
699 fn rejects_nonpositive_tol_x() {
700 let mut opts = StructValue::new();
701 opts.insert("TolX", Value::Num(0.0));
702 let err = block_on(fminbnd_builtin(
703 V::FunctionHandle("__fminbnd_quad_minus_two".into()),
704 V::Num(0.0),
705 V::Num(5.0),
706 vec![Value::Struct(opts)],
707 ))
708 .unwrap_err();
709 assert!(err.message().to_lowercase().contains("tolx"));
710 }
711
712 #[test]
713 fn rejects_unknown_display_value() {
714 let mut opts = StructValue::new();
715 opts.insert("Display", Value::from("loud"));
716 let err = block_on(fminbnd_builtin(
717 V::FunctionHandle("__fminbnd_quad_minus_two".into()),
718 V::Num(0.0),
719 V::Num(5.0),
720 vec![Value::Struct(opts)],
721 ))
722 .unwrap_err();
723 assert!(err.message().to_lowercase().contains("display"));
724 }
725
726 #[test]
727 fn multi_output_two_returns_x_and_fval() {
728 let _guard = crate::output_count::push_output_count(Some(2));
729 let result = block_on(fminbnd_builtin(
730 V::FunctionHandle("__fminbnd_quad_minus_two".into()),
731 V::Num(0.0),
732 V::Num(5.0),
733 Vec::new(),
734 ))
735 .expect("fminbnd");
736 match result {
737 V::OutputList(outputs) => {
738 assert_eq!(outputs.len(), 2);
739 match (&outputs[0], &outputs[1]) {
740 (V::Num(x), V::Num(fval)) => {
741 assert!((x - 2.0).abs() < 1.0e-3);
742 assert!(fval.abs() < 1.0e-5);
743 }
744 other => panic!("unexpected outputs {other:?}"),
745 }
746 }
747 other => panic!("unexpected value {other:?}"),
748 }
749 }
750
751 #[test]
752 fn multi_output_three_includes_exitflag() {
753 let _guard = crate::output_count::push_output_count(Some(3));
754 let result = block_on(fminbnd_builtin(
755 V::FunctionHandle("__fminbnd_quad_minus_two".into()),
756 V::Num(0.0),
757 V::Num(5.0),
758 Vec::new(),
759 ))
760 .expect("fminbnd");
761 match result {
762 V::OutputList(outputs) => {
763 assert_eq!(outputs.len(), 3);
764 match &outputs[2] {
765 V::Num(flag) => assert!((*flag - 1.0).abs() < 1.0e-12),
766 other => panic!("unexpected exitflag {other:?}"),
767 }
768 }
769 other => panic!("unexpected value {other:?}"),
770 }
771 }
772
773 #[test]
774 fn multi_output_four_includes_output_struct() {
775 let _guard = crate::output_count::push_output_count(Some(4));
776 let result = block_on(fminbnd_builtin(
777 V::FunctionHandle("__fminbnd_quad_minus_two".into()),
778 V::Num(0.0),
779 V::Num(5.0),
780 Vec::new(),
781 ))
782 .expect("fminbnd");
783 match result {
784 V::OutputList(outputs) => {
785 assert_eq!(outputs.len(), 4);
786 match &outputs[3] {
787 V::Struct(s) => {
788 assert!(matches!(s.fields.get("iterations"), Some(V::Num(_))));
789 assert!(matches!(s.fields.get("funcCount"), Some(V::Num(_))));
790 match s.fields.get("algorithm") {
791 Some(V::String(text)) => assert!(text.contains("golden")),
792 other => panic!("unexpected algorithm field {other:?}"),
793 }
794 assert!(s.fields.get("message").is_some());
795 }
796 other => panic!("unexpected output struct {other:?}"),
797 }
798 }
799 other => panic!("unexpected value {other:?}"),
800 }
801 }
802
803 #[test]
804 fn reports_zero_exitflag_when_max_iter_exhausted() {
805 let mut opts = StructValue::new();
806 opts.insert("MaxIter", Value::Num(1.0));
807 opts.insert("MaxFunEvals", Value::Num(2.0));
808 opts.insert("Display", Value::from("off"));
809 let _guard = crate::output_count::push_output_count(Some(3));
810 let result = block_on(fminbnd_builtin(
811 V::FunctionHandle("__fminbnd_quad_minus_two".into()),
812 V::Num(0.0),
813 V::Num(5.0),
814 vec![Value::Struct(opts)],
815 ))
816 .expect("fminbnd");
817 match result {
818 V::OutputList(outputs) => match &outputs[2] {
819 V::Num(flag) => assert_eq!(*flag, 0.0),
820 other => panic!("unexpected exitflag {other:?}"),
821 },
822 other => panic!("unexpected value {other:?}"),
823 }
824 }
825}