use runmat_builtins::{LogicalArray, StructValue, Tensor, Value};
use runmat_macros::runtime_builtin;
use crate::builtins::common::spec::{
BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
ReductionNaN, ResidencyPolicy, ShapeRequirements,
};
use crate::builtins::math::optim::brent::{
brent_min, BrentMinObserver, BrentMinResult, BrentParams, BrentStepKind,
};
use crate::builtins::math::optim::common::optim_error;
use crate::builtins::math::optim::type_resolvers::scalar_root_type;
use crate::BuiltinResult;
const NAME: &str = "fminbnd";
const ALGORITHM: &str = "golden section search, parabolic interpolation";
const DEFAULT_TOL_X: f64 = 1.0e-4;
const DEFAULT_MAX_ITER: usize = 500;
const DEFAULT_MAX_FUN_EVALS: usize = 500;
const DEFAULT_DISPLAY: DisplayMode = DisplayMode::Notify;
#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::math::optim::fminbnd")]
pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
name: "fminbnd",
op_kind: GpuOpKind::Custom("bounded-scalar-min"),
supported_precisions: &[],
broadcast: BroadcastSemantics::None,
provider_hooks: &[],
constant_strategy: ConstantStrategy::InlineLiteral,
residency: ResidencyPolicy::GatherImmediately,
nan_mode: ReductionNaN::Include,
two_pass_threshold: None,
workgroup_size: None,
accepts_nan_mode: false,
notes: "Host iterative solver. Callback computations may use GPU-aware builtins, but the minimization loop runs on the CPU.",
};
#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::math::optim::fminbnd")]
pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
name: "fminbnd",
shape: ShapeRequirements::Any,
constant_strategy: ConstantStrategy::InlineLiteral,
elementwise: None,
reduction: None,
emits_nan: false,
notes:
"Bounded scalar minimization repeatedly invokes user code and terminates fusion planning.",
};
#[runtime_builtin(
name = "fminbnd",
category = "math/optim",
summary = "Find a local minimum of a scalar function on a bounded interval using Brent's method.",
keywords = "fminbnd,bounded minimization,brent,golden section,parabolic interpolation,optimization",
accel = "sink",
type_resolver(scalar_root_type),
builtin_path = "crate::builtins::math::optim::fminbnd"
)]
async fn fminbnd_builtin(
function: Value,
x1: Value,
x2: Value,
rest: Vec<Value>,
) -> BuiltinResult<Value> {
if rest.len() > 1 {
return Err(optim_error(NAME, "fminbnd: too many input arguments"));
}
let options_struct = parse_options(rest.first())?;
let options = FminbndOptions::from_struct(options_struct.as_ref())?;
let x1 = scalar_bound("lower bound", x1).await?;
let x2 = scalar_bound("upper bound", x2).await?;
if !x1.is_finite() || !x2.is_finite() {
return Err(optim_error(NAME, "fminbnd: bounds must be finite"));
}
if x1 > x2 {
return finalize_inconsistent_bounds(&options);
}
let outcome = run_solver(&function, x1, x2, &options).await?;
finalize(outcome, &options)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DisplayMode {
Off,
Iter,
Notify,
Final,
}
impl DisplayMode {
fn parse(text: &str) -> BuiltinResult<Self> {
match text.to_ascii_lowercase().as_str() {
"off" | "none" => Ok(Self::Off),
"iter" => Ok(Self::Iter),
"notify" => Ok(Self::Notify),
"final" => Ok(Self::Final),
other => Err(optim_error(
NAME,
format!(
"fminbnd: option Display must be 'off', 'iter', 'notify', or 'final', got '{other}'"
),
)),
}
}
}
#[derive(Debug, Clone, Copy)]
struct FminbndOptions {
tol_x: f64,
max_iter: usize,
max_fun_evals: usize,
display: DisplayMode,
}
impl FminbndOptions {
fn from_struct(options: Option<&StructValue>) -> BuiltinResult<Self> {
let display = match options {
Some(opts) => match lookup(opts, "Display") {
Some(value) => DisplayMode::parse(&option_string("Display", value)?)?,
None => DEFAULT_DISPLAY,
},
None => DEFAULT_DISPLAY,
};
let tol_x = match options.and_then(|o| lookup(o, "TolX")) {
Some(value) => option_f64("TolX", value)?,
None => DEFAULT_TOL_X,
};
if tol_x <= 0.0 {
return Err(optim_error(NAME, "fminbnd: option TolX must be positive"));
}
let max_iter = match options.and_then(|o| lookup(o, "MaxIter")) {
Some(value) => option_positive_usize("MaxIter", value)?,
None => DEFAULT_MAX_ITER,
};
let max_fun_evals = match options.and_then(|o| lookup(o, "MaxFunEvals")) {
Some(value) => option_positive_usize("MaxFunEvals", value)?,
None => DEFAULT_MAX_FUN_EVALS,
};
Ok(Self {
tol_x,
max_iter,
max_fun_evals,
display,
})
}
}
fn parse_options(value: Option<&Value>) -> BuiltinResult<Option<StructValue>> {
match value {
None => Ok(None),
Some(Value::Struct(options)) => Ok(Some(options.clone())),
Some(other) => Err(optim_error(
NAME,
format!("fminbnd: options must be a struct, got {other:?}"),
)),
}
}
fn lookup<'a>(options: &'a StructValue, name: &str) -> Option<&'a Value> {
options
.fields
.iter()
.find(|(key, _)| key.eq_ignore_ascii_case(name))
.map(|(_, v)| v)
}
fn option_string(field: &str, value: &Value) -> BuiltinResult<String> {
match value {
Value::String(s) => Ok(s.clone()),
Value::StringArray(sa) if sa.data.len() == 1 => Ok(sa.data[0].clone()),
Value::CharArray(chars) if chars.rows == 1 => Ok(chars.data.iter().collect()),
other => Err(optim_error(
NAME,
format!("fminbnd: option {field} must be a string, got {other:?}"),
)),
}
}
fn option_f64(field: &str, value: &Value) -> BuiltinResult<f64> {
let parsed = match value {
Value::Num(n) => *n,
Value::Int(i) => i.to_f64(),
Value::Bool(b) => {
if *b {
1.0
} else {
0.0
}
}
Value::Tensor(Tensor { data, .. }) if data.len() == 1 => data[0],
Value::LogicalArray(LogicalArray { data, .. }) if data.len() == 1 => {
if data[0] != 0 {
1.0
} else {
0.0
}
}
other => {
return Err(optim_error(
NAME,
format!("fminbnd: option {field} must be a real scalar, got {other:?}"),
))
}
};
if parsed.is_finite() {
Ok(parsed)
} else {
Err(optim_error(
NAME,
format!("fminbnd: option {field} must be finite"),
))
}
}
fn option_positive_usize(field: &str, value: &Value) -> BuiltinResult<usize> {
let parsed = option_f64(field, value)?;
if parsed < 1.0 {
return Err(optim_error(
NAME,
format!("fminbnd: option {field} must be a positive integer"),
));
}
if parsed.fract() != 0.0 {
return Err(optim_error(
NAME,
format!("fminbnd: option {field} must be an integer scalar"),
));
}
Ok(parsed as usize)
}
async fn scalar_bound(label: &str, value: Value) -> BuiltinResult<f64> {
let value = crate::dispatcher::gather_if_needed_async(&value).await?;
let parsed = match value {
Value::Num(n) => n,
Value::Int(i) => i.to_f64(),
Value::Bool(b) => {
if b {
1.0
} else {
0.0
}
}
Value::Tensor(t) if t.data.len() == 1 => t.data[0],
Value::LogicalArray(LogicalArray { data, .. }) if data.len() == 1 => {
if data[0] != 0 {
1.0
} else {
0.0
}
}
other => {
return Err(optim_error(
NAME,
format!("fminbnd: {label} must be a finite real scalar, got {other:?}"),
))
}
};
if parsed.is_finite() {
Ok(parsed)
} else {
Err(optim_error(
NAME,
format!("fminbnd: {label} must be finite"),
))
}
}
#[derive(Debug, Clone)]
struct Outcome {
inner: BrentMinResult,
}
async fn run_solver(
function: &Value,
lo: f64,
hi: f64,
options: &FminbndOptions,
) -> BuiltinResult<Outcome> {
let mut iter_log = IterDisplay::new(options.display);
let observer: Option<&mut dyn BrentMinObserver> =
if matches!(options.display, DisplayMode::Iter) {
Some(&mut iter_log)
} else {
None
};
let inner = brent_min(
NAME,
function,
lo,
hi,
BrentParams {
tol_x: options.tol_x,
max_iter: options.max_iter,
max_fun_evals: options.max_fun_evals,
},
observer,
)
.await?;
Ok(Outcome { inner })
}
fn finalize(outcome: Outcome, options: &FminbndOptions) -> BuiltinResult<Value> {
let exit_flag = if outcome.inner.converged { 1 } else { 0 };
let message = build_message(&outcome.inner);
emit_summary(&outcome.inner, exit_flag, &message, options);
let x = Value::Num(outcome.inner.x);
let fval = Value::Num(outcome.inner.fval);
let exitflag = Value::Num(exit_flag as f64);
let output_struct = Value::Struct(build_output_struct(&outcome.inner, &message));
match crate::output_count::current_output_count() {
None => Ok(x),
Some(0) => Ok(Value::OutputList(Vec::new())),
Some(1) => Ok(crate::output_count::output_list_with_padding(1, vec![x])),
Some(2) => Ok(crate::output_count::output_list_with_padding(
2,
vec![x, fval],
)),
Some(3) => Ok(crate::output_count::output_list_with_padding(
3,
vec![x, fval, exitflag],
)),
Some(n) if n >= 4 => Ok(crate::output_count::output_list_with_padding(
n,
vec![x, fval, exitflag, output_struct],
)),
Some(_) => Ok(x),
}
}
fn finalize_inconsistent_bounds(options: &FminbndOptions) -> BuiltinResult<Value> {
let message = "Exiting: The bounds are inconsistent because x1 > x2.".to_string();
emit_invalid_summary(-2, &message, options);
let x = empty_double();
let fval = empty_double();
let exitflag = Value::Num(-2.0);
let output_struct = Value::Struct(build_invalid_output_struct(&message));
match crate::output_count::current_output_count() {
None => Ok(x),
Some(0) => Ok(Value::OutputList(Vec::new())),
Some(1) => Ok(crate::output_count::output_list_with_padding(1, vec![x])),
Some(2) => Ok(crate::output_count::output_list_with_padding(
2,
vec![x, fval],
)),
Some(3) => Ok(crate::output_count::output_list_with_padding(
3,
vec![x, fval, exitflag],
)),
Some(n) if n >= 4 => Ok(crate::output_count::output_list_with_padding(
n,
vec![x, fval, exitflag, output_struct],
)),
Some(_) => Ok(empty_double()),
}
}
fn empty_double() -> Value {
Value::Tensor(Tensor::zeros(vec![0, 0]))
}
fn build_output_struct(result: &BrentMinResult, message: &str) -> StructValue {
let mut fields = StructValue::new();
fields.insert("iterations", Value::Num(result.iterations as f64));
fields.insert("funcCount", Value::Num(result.func_count as f64));
fields.insert("algorithm", Value::from(ALGORITHM));
fields.insert("message", Value::from(message.to_string()));
fields
}
fn build_invalid_output_struct(message: &str) -> StructValue {
let mut fields = StructValue::new();
fields.insert("iterations", Value::Num(0.0));
fields.insert("funcCount", Value::Num(0.0));
fields.insert("algorithm", Value::from(ALGORITHM));
fields.insert("message", Value::from(message.to_string()));
fields
}
fn build_message(result: &BrentMinResult) -> String {
if result.converged {
format!(
"Optimization terminated: the current x satisfies the termination criteria using OPTIONS.TolX. Iterations: {}, FuncCount: {}.",
result.iterations, result.func_count
)
} else {
format!(
"Exiting: Maximum number of function evaluations or iterations has been exceeded - increase MaxFunEvals or MaxIter. Iterations: {}, FuncCount: {}.",
result.iterations, result.func_count
)
}
}
fn emit_summary(result: &BrentMinResult, exit_flag: i32, message: &str, options: &FminbndOptions) {
let should_emit = match options.display {
DisplayMode::Off => false,
DisplayMode::Final | DisplayMode::Iter => true,
DisplayMode::Notify => exit_flag != 1,
};
if !should_emit {
return;
}
let line = format!(
"fminbnd: x = {x:.6}, fval = {fval:.6}, exitflag = {exit_flag}. {message}",
x = result.x,
fval = result.fval,
);
crate::console::record_console_line(crate::console::ConsoleStream::Stdout, line);
}
fn emit_invalid_summary(exit_flag: i32, message: &str, options: &FminbndOptions) {
let should_emit = match options.display {
DisplayMode::Off => false,
DisplayMode::Final | DisplayMode::Iter => true,
DisplayMode::Notify => exit_flag != 1,
};
if should_emit {
crate::console::record_console_line(
crate::console::ConsoleStream::Stdout,
format!("fminbnd: exitflag = {exit_flag}. {message}"),
);
}
}
struct IterDisplay {
mode: DisplayMode,
printed_header: bool,
}
impl IterDisplay {
fn new(mode: DisplayMode) -> Self {
Self {
mode,
printed_header: false,
}
}
}
impl BrentMinObserver for IterDisplay {
fn on_iteration(
&mut self,
iter: usize,
func_count: usize,
x: f64,
fx: f64,
step_kind: BrentStepKind,
) {
if !matches!(self.mode, DisplayMode::Iter) {
return;
}
if !self.printed_header {
crate::console::record_console_line(
crate::console::ConsoleStream::Stdout,
" Func-count x f(x) Procedure",
);
self.printed_header = true;
}
let procedure = match step_kind {
BrentStepKind::Initial => "initial",
BrentStepKind::GoldenSection => "golden",
BrentStepKind::Parabolic => "parabolic",
};
let line =
format!(" {func_count:>5} {x:13.6e} {fx:13.6e} {procedure} (iter {iter})");
crate::console::record_console_line(crate::console::ConsoleStream::Stdout, line);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::builtins::math::optim::brent::brent_min_tolerance;
use futures::executor::block_on;
use runmat_builtins::Value as V;
fn run_default(handle: &str, lo: f64, hi: f64) -> Value {
block_on(fminbnd_builtin(
V::FunctionHandle(handle.into()),
V::Num(lo),
V::Num(hi),
Vec::new(),
))
.expect("fminbnd")
}
fn run_with(handle: &str, lo: f64, hi: f64, extra: Vec<Value>) -> Value {
block_on(fminbnd_builtin(
V::FunctionHandle(handle.into()),
V::Num(lo),
V::Num(hi),
extra,
))
.expect("fminbnd")
}
#[runtime_builtin(
name = "__fminbnd_quad_minus_two",
type_resolver(crate::builtins::math::optim::type_resolvers::scalar_root_type),
builtin_path = "crate::builtins::math::optim::fminbnd::tests"
)]
async fn quad_minus_two(x: Value) -> crate::BuiltinResult<Value> {
let x = scalar_bound("x", x).await?;
let diff = x - 2.0;
Ok(Value::Num(diff * diff))
}
#[runtime_builtin(
name = "__fminbnd_quad_minus_three",
type_resolver(crate::builtins::math::optim::type_resolvers::scalar_root_type),
builtin_path = "crate::builtins::math::optim::fminbnd::tests"
)]
async fn quad_minus_three(x: Value) -> crate::BuiltinResult<Value> {
let x = scalar_bound("x", x).await?;
let diff = x - 3.0;
Ok(Value::Num(diff * diff))
}
#[runtime_builtin(
name = "__fminbnd_multi_modal",
type_resolver(crate::builtins::math::optim::type_resolvers::scalar_root_type),
builtin_path = "crate::builtins::math::optim::fminbnd::tests"
)]
async fn multi_modal(x: Value) -> crate::BuiltinResult<Value> {
let x = scalar_bound("x", x).await?;
Ok(Value::Num(1.0 + (3.0 * x).sin()))
}
#[test]
fn locates_smooth_quadratic_minimum() {
let result = run_default("__fminbnd_quad_minus_two", 0.0, 5.0);
match result {
V::Num(x) => assert!((x - 2.0).abs() < 1.0e-3, "x = {x}"),
other => panic!("unexpected value {other:?}"),
}
}
#[test]
fn locates_quadratic_minimum_offset_three() {
let result = run_default("__fminbnd_quad_minus_three", 0.0, 5.0);
match result {
V::Num(x) => assert!((x - 3.0).abs() < 1.0e-3, "x = {x}"),
other => panic!("unexpected value {other:?}"),
}
}
#[test]
fn locates_cosine_minimum_at_right_endpoint() {
let result = run_default("cos", 0.0, std::f64::consts::PI);
match result {
V::Num(x) => assert!((x - std::f64::consts::PI).abs() < 1.0e-3, "x = {x}"),
other => panic!("unexpected value {other:?}"),
}
}
#[test]
fn returns_lone_endpoint_when_bounds_collapse() {
let result = run_default("__fminbnd_quad_minus_two", 1.5, 1.5);
match result {
V::Num(x) => assert!((x - 1.5).abs() < 1.0e-12, "x = {x}"),
other => panic!("unexpected value {other:?}"),
}
}
#[test]
fn reports_inconsistent_reversed_bounds() {
let _guard = crate::output_count::push_output_count(Some(4));
let result = block_on(fminbnd_builtin(
V::FunctionHandle("__fminbnd_quad_minus_two".into()),
V::Num(5.0),
V::Num(0.0),
Vec::new(),
))
.expect("fminbnd");
match result {
V::OutputList(outputs) => {
assert_eq!(outputs.len(), 4);
assert!(matches!(&outputs[0], V::Tensor(t) if t.data.is_empty()));
assert!(matches!(&outputs[1], V::Tensor(t) if t.data.is_empty()));
assert!(matches!(&outputs[2], V::Num(flag) if *flag == -2.0));
match &outputs[3] {
V::Struct(s) => {
assert!(matches!(s.fields.get("iterations"), Some(V::Num(0.0))));
assert!(matches!(s.fields.get("funcCount"), Some(V::Num(0.0))));
match s.fields.get("message") {
Some(V::String(text)) => assert!(text.contains("bounds")),
other => panic!("unexpected message field {other:?}"),
}
}
other => panic!("unexpected output struct {other:?}"),
}
}
other => panic!("unexpected value {other:?}"),
}
}
#[test]
fn tolerance_is_additive_not_scaled_by_x() {
let params = BrentParams {
tol_x: 1.0e-4,
max_iter: 500,
max_fun_evals: 500,
};
let small = brent_min_tolerance(2.0, params);
let large = brent_min_tolerance(1.0e9, params);
assert!(small > params.tol_x);
assert!(
large < params.tol_x * 1.0e9,
"large-scale tolerance was {large}"
);
}
#[test]
fn finds_local_minimum_in_multi_modal_function() {
let result = run_default("__fminbnd_multi_modal", 1.5, 3.5);
match result {
V::Num(x) => {
let target = std::f64::consts::PI / 2.0;
assert!((x - target).abs() < 5.0e-3, "x = {x}, target = {target}");
}
other => panic!("unexpected value {other:?}"),
}
}
#[test]
fn options_struct_overrides_default_tolerance() {
let mut opts = StructValue::new();
opts.insert("TolX", Value::Num(1.0e-12));
let result = run_with(
"__fminbnd_quad_minus_two",
0.0,
5.0,
vec![Value::Struct(opts)],
);
match result {
V::Num(x) => assert!((x - 2.0).abs() < 1.0e-6, "x = {x}"),
other => panic!("unexpected value {other:?}"),
}
}
#[test]
fn max_fun_evals_default_is_independent_of_max_iter() {
let mut opts = StructValue::new();
opts.insert("MaxIter", Value::Num(1000.0));
let parsed = FminbndOptions::from_struct(Some(&opts)).unwrap();
assert_eq!(parsed.max_iter, 1000);
assert_eq!(parsed.max_fun_evals, DEFAULT_MAX_FUN_EVALS);
}
#[test]
fn rejects_nonfinite_bounds() {
let err = block_on(fminbnd_builtin(
V::FunctionHandle("__fminbnd_quad_minus_two".into()),
V::Num(f64::NAN),
V::Num(5.0),
Vec::new(),
))
.unwrap_err();
assert!(err.message().to_ascii_lowercase().contains("finite"));
}
#[test]
fn rejects_invalid_options_type() {
let err = block_on(fminbnd_builtin(
V::FunctionHandle("__fminbnd_quad_minus_two".into()),
V::Num(0.0),
V::Num(5.0),
vec![Value::Num(1.0)],
))
.unwrap_err();
assert!(err.message().to_ascii_lowercase().contains("options"));
}
#[test]
fn rejects_nonpositive_tol_x() {
let mut opts = StructValue::new();
opts.insert("TolX", Value::Num(0.0));
let err = block_on(fminbnd_builtin(
V::FunctionHandle("__fminbnd_quad_minus_two".into()),
V::Num(0.0),
V::Num(5.0),
vec![Value::Struct(opts)],
))
.unwrap_err();
assert!(err.message().to_lowercase().contains("tolx"));
}
#[test]
fn rejects_unknown_display_value() {
let mut opts = StructValue::new();
opts.insert("Display", Value::from("loud"));
let err = block_on(fminbnd_builtin(
V::FunctionHandle("__fminbnd_quad_minus_two".into()),
V::Num(0.0),
V::Num(5.0),
vec![Value::Struct(opts)],
))
.unwrap_err();
assert!(err.message().to_lowercase().contains("display"));
}
#[test]
fn multi_output_two_returns_x_and_fval() {
let _guard = crate::output_count::push_output_count(Some(2));
let result = block_on(fminbnd_builtin(
V::FunctionHandle("__fminbnd_quad_minus_two".into()),
V::Num(0.0),
V::Num(5.0),
Vec::new(),
))
.expect("fminbnd");
match result {
V::OutputList(outputs) => {
assert_eq!(outputs.len(), 2);
match (&outputs[0], &outputs[1]) {
(V::Num(x), V::Num(fval)) => {
assert!((x - 2.0).abs() < 1.0e-3);
assert!(fval.abs() < 1.0e-5);
}
other => panic!("unexpected outputs {other:?}"),
}
}
other => panic!("unexpected value {other:?}"),
}
}
#[test]
fn multi_output_three_includes_exitflag() {
let _guard = crate::output_count::push_output_count(Some(3));
let result = block_on(fminbnd_builtin(
V::FunctionHandle("__fminbnd_quad_minus_two".into()),
V::Num(0.0),
V::Num(5.0),
Vec::new(),
))
.expect("fminbnd");
match result {
V::OutputList(outputs) => {
assert_eq!(outputs.len(), 3);
match &outputs[2] {
V::Num(flag) => assert!((*flag - 1.0).abs() < 1.0e-12),
other => panic!("unexpected exitflag {other:?}"),
}
}
other => panic!("unexpected value {other:?}"),
}
}
#[test]
fn multi_output_four_includes_output_struct() {
let _guard = crate::output_count::push_output_count(Some(4));
let result = block_on(fminbnd_builtin(
V::FunctionHandle("__fminbnd_quad_minus_two".into()),
V::Num(0.0),
V::Num(5.0),
Vec::new(),
))
.expect("fminbnd");
match result {
V::OutputList(outputs) => {
assert_eq!(outputs.len(), 4);
match &outputs[3] {
V::Struct(s) => {
assert!(matches!(s.fields.get("iterations"), Some(V::Num(_))));
assert!(matches!(s.fields.get("funcCount"), Some(V::Num(_))));
match s.fields.get("algorithm") {
Some(V::String(text)) => assert!(text.contains("golden")),
other => panic!("unexpected algorithm field {other:?}"),
}
assert!(s.fields.get("message").is_some());
}
other => panic!("unexpected output struct {other:?}"),
}
}
other => panic!("unexpected value {other:?}"),
}
}
#[test]
fn reports_zero_exitflag_when_max_iter_exhausted() {
let mut opts = StructValue::new();
opts.insert("MaxIter", Value::Num(1.0));
opts.insert("MaxFunEvals", Value::Num(2.0));
opts.insert("Display", Value::from("off"));
let _guard = crate::output_count::push_output_count(Some(3));
let result = block_on(fminbnd_builtin(
V::FunctionHandle("__fminbnd_quad_minus_two".into()),
V::Num(0.0),
V::Num(5.0),
vec![Value::Struct(opts)],
))
.expect("fminbnd");
match result {
V::OutputList(outputs) => match &outputs[2] {
V::Num(flag) => assert_eq!(*flag, 0.0),
other => panic!("unexpected exitflag {other:?}"),
},
other => panic!("unexpected value {other:?}"),
}
}
}