Skip to main content

runmat_runtime/builtins/math/optim/
optimoptions.rs

1//! MATLAB-compatible `optimoptions` options struct builder.
2
3use std::collections::VecDeque;
4
5use runmat_builtins::{
6    BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
7    BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
8    CharArray, LogicalArray, StructValue, Tensor, Value,
9};
10use runmat_macros::runtime_builtin;
11
12use crate::builtins::common::spec::{
13    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
14    ReductionNaN, ResidencyPolicy, ShapeRequirements,
15};
16use crate::builtins::math::optim::common::canonical_option_name;
17use crate::builtins::math::optim::type_resolvers::optim_options_type;
18use crate::{build_runtime_error, gather_if_needed_async, BuiltinResult, RuntimeError};
19
20const NAME: &str = "optimoptions";
21
22const OPTIMOPTIONS_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
23    name: "options",
24    ty: BuiltinParamType::Any,
25    arity: BuiltinParamArity::Required,
26    default: None,
27    description: "Options struct for optimization solvers.",
28}];
29
30const OPTIMOPTIONS_INPUTS_SOLVER: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
31    name: "solver",
32    ty: BuiltinParamType::StringScalar,
33    arity: BuiltinParamArity::Required,
34    default: None,
35    description:
36        "Solver name, such as coneprog, fminbnd, fminunc, fzero, fsolve, lsqcurvefit, or lsqnonlin.",
37}];
38
39const OPTIMOPTIONS_INPUTS_SOLVER_PAIRS: [BuiltinParamDescriptor; 3] = [
40    BuiltinParamDescriptor {
41        name: "solver",
42        ty: BuiltinParamType::StringScalar,
43        arity: BuiltinParamArity::Required,
44        default: None,
45        description:
46            "Solver name, such as coneprog, fminbnd, fminunc, fzero, fsolve, lsqcurvefit, or lsqnonlin.",
47    },
48    BuiltinParamDescriptor {
49        name: "name",
50        ty: BuiltinParamType::StringScalar,
51        arity: BuiltinParamArity::Optional,
52        default: None,
53        description: "Option field name.",
54    },
55    BuiltinParamDescriptor {
56        name: "value",
57        ty: BuiltinParamType::Any,
58        arity: BuiltinParamArity::Variadic,
59        default: None,
60        description: "Option value(s) and additional name/value pairs.",
61    },
62];
63
64const OPTIMOPTIONS_INPUTS_EXISTING_PAIRS: [BuiltinParamDescriptor; 3] = [
65    BuiltinParamDescriptor {
66        name: "oldopts",
67        ty: BuiltinParamType::Any,
68        arity: BuiltinParamArity::Required,
69        default: None,
70        description: "Existing options struct to update.",
71    },
72    BuiltinParamDescriptor {
73        name: "name",
74        ty: BuiltinParamType::StringScalar,
75        arity: BuiltinParamArity::Optional,
76        default: None,
77        description: "Option field name.",
78    },
79    BuiltinParamDescriptor {
80        name: "value",
81        ty: BuiltinParamType::Any,
82        arity: BuiltinParamArity::Variadic,
83        default: None,
84        description: "Option value(s), additional name/value pairs, or another options struct.",
85    },
86];
87
88const OPTIMOPTIONS_SIGNATURES: [BuiltinSignatureDescriptor; 3] = [
89    BuiltinSignatureDescriptor {
90        label: "options = optimoptions(solver)",
91        inputs: &OPTIMOPTIONS_INPUTS_SOLVER,
92        outputs: &OPTIMOPTIONS_OUTPUT,
93    },
94    BuiltinSignatureDescriptor {
95        label: "options = optimoptions(solver, name, value, ...)",
96        inputs: &OPTIMOPTIONS_INPUTS_SOLVER_PAIRS,
97        outputs: &OPTIMOPTIONS_OUTPUT,
98    },
99    BuiltinSignatureDescriptor {
100        label: "options = optimoptions(oldopts, name, value, ...)",
101        inputs: &OPTIMOPTIONS_INPUTS_EXISTING_PAIRS,
102        outputs: &OPTIMOPTIONS_OUTPUT,
103    },
104];
105
106const OPTIMOPTIONS_ERROR_INVALID_ARGUMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
107    code: "RM.OPTIMOPTIONS.INVALID_ARGUMENT",
108    identifier: Some("RunMat:optimoptions:InvalidArgument"),
109    when: "Argument grammar does not match supported optimoptions forms.",
110    message: "optimoptions: invalid argument",
111};
112const OPTIMOPTIONS_ERROR_INVALID_SOLVER: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
113    code: "RM.OPTIMOPTIONS.INVALID_SOLVER",
114    identifier: Some("RunMat:optimoptions:InvalidSolver"),
115    when: "The solver argument is not one of the supported optimization builtins.",
116    message: "optimoptions: invalid solver",
117};
118const OPTIMOPTIONS_ERROR_INVALID_OPTION_NAME: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
119    code: "RM.OPTIMOPTIONS.INVALID_OPTION_NAME",
120    identifier: Some("RunMat:optimoptions:InvalidOptionName"),
121    when: "An option name is not a text scalar.",
122    message: "optimoptions: invalid option name",
123};
124const OPTIMOPTIONS_ERROR_MISSING_OPTION_VALUE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
125    code: "RM.OPTIMOPTIONS.MISSING_OPTION_VALUE",
126    identifier: Some("RunMat:optimoptions:MissingOptionValue"),
127    when: "A name-value option key is not followed by a value.",
128    message: "optimoptions: missing option value",
129};
130const OPTIMOPTIONS_ERROR_UNKNOWN_OPTION: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
131    code: "RM.OPTIMOPTIONS.UNKNOWN_OPTION",
132    identifier: Some("RunMat:optimoptions:UnknownOption"),
133    when: "An option name is not supported by the selected solver.",
134    message: "optimoptions: unknown option",
135};
136const OPTIMOPTIONS_ERROR_INVALID_OPTION_VALUE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
137    code: "RM.OPTIMOPTIONS.INVALID_OPTION_VALUE",
138    identifier: Some("RunMat:optimoptions:InvalidOptionValue"),
139    when: "An option value fails type or domain validation.",
140    message: "optimoptions: invalid option value",
141};
142const OPTIMOPTIONS_ERROR_FLOW: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
143    code: "RM.OPTIMOPTIONS.FLOW",
144    identifier: Some("RunMat:optimoptions:Flow"),
145    when: "Nested flow fails while gathering input values.",
146    message: "optimoptions: flow failure",
147};
148
149const OPTIMOPTIONS_ERRORS: [BuiltinErrorDescriptor; 7] = [
150    OPTIMOPTIONS_ERROR_INVALID_ARGUMENT,
151    OPTIMOPTIONS_ERROR_INVALID_SOLVER,
152    OPTIMOPTIONS_ERROR_INVALID_OPTION_NAME,
153    OPTIMOPTIONS_ERROR_MISSING_OPTION_VALUE,
154    OPTIMOPTIONS_ERROR_UNKNOWN_OPTION,
155    OPTIMOPTIONS_ERROR_INVALID_OPTION_VALUE,
156    OPTIMOPTIONS_ERROR_FLOW,
157];
158
159pub const OPTIMOPTIONS_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
160    signatures: &OPTIMOPTIONS_SIGNATURES,
161    output_mode: BuiltinOutputMode::Fixed,
162    completion_policy: BuiltinCompletionPolicy::Public,
163    errors: &OPTIMOPTIONS_ERRORS,
164};
165
166#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::math::optim::optimoptions")]
167pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
168    name: "optimoptions",
169    op_kind: GpuOpKind::Custom("optimization-options"),
170    supported_precisions: &[],
171    broadcast: BroadcastSemantics::None,
172    provider_hooks: &[],
173    constant_strategy: ConstantStrategy::InlineLiteral,
174    residency: ResidencyPolicy::GatherImmediately,
175    nan_mode: ReductionNaN::Include,
176    two_pass_threshold: None,
177    workgroup_size: None,
178    accepts_nan_mode: false,
179    notes: "Host metadata construction. gpuArray option values are gathered before validation.",
180};
181
182#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::math::optim::optimoptions")]
183pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
184    name: "optimoptions",
185    shape: ShapeRequirements::Any,
186    constant_strategy: ConstantStrategy::InlineLiteral,
187    elementwise: None,
188    reduction: None,
189    emits_nan: false,
190    notes: "Option struct construction is host metadata work and terminates fusion planning.",
191};
192
193#[runtime_builtin(
194    name = "optimoptions",
195    category = "math/optim",
196    summary = "Create or update a typed optimization options structure for coneprog, fminbnd, fminunc, fzero, fsolve, lsqcurvefit, and lsqnonlin.",
197    keywords = "optimoptions,options,TolX,TolFun,FunctionTolerance,StepTolerance,MaxIter,MaxFunEvals,Display,Algorithm,SpecifyObjectiveGradient,coneprog,lsqnonlin",
198    accel = "cpu",
199    type_resolver(optim_options_type),
200    descriptor(crate::builtins::math::optim::optimoptions::OPTIMOPTIONS_DESCRIPTOR),
201    builtin_path = "crate::builtins::math::optim::optimoptions"
202)]
203async fn optimoptions_builtin(rest: Vec<Value>) -> BuiltinResult<Value> {
204    let mut gathered = Vec::with_capacity(rest.len());
205    for value in rest {
206        gathered.push(gather_if_needed_async(&value).await.map_err(|err| {
207            remap_optimoptions_flow(&OPTIMOPTIONS_ERROR_FLOW, err, |source| {
208                format!("optimoptions: {}", source.message())
209            })
210        })?);
211    }
212
213    let mut queue: VecDeque<Value> = gathered.into();
214    let first = queue.pop_front().ok_or_else(|| {
215        optimoptions_error_with(
216            &OPTIMOPTIONS_ERROR_INVALID_ARGUMENT,
217            "optimoptions: expected a solver name or options struct",
218        )
219    })?;
220
221    let mut solver;
222    let explicit_solver;
223    let mut options = match first {
224        Value::Struct(existing) => {
225            explicit_solver = false;
226            solver = solver_from_options(&existing)?;
227            canonicalize_existing_options(&existing, solver)?
228        }
229        other => {
230            explicit_solver = true;
231            solver = parse_solver(&other)?;
232            default_options(solver)
233        }
234    };
235
236    while let Some(arg) = queue.pop_front() {
237        match arg {
238            Value::Struct(existing) => {
239                if explicit_solver {
240                    let next_solver = solver_from_options(&existing)?;
241                    let skip_defaults_from = match next_solver {
242                        Solver::Generic => None,
243                        other => Some(other),
244                    };
245                    apply_struct_fields(
246                        &existing,
247                        &mut options,
248                        solver,
249                        false,
250                        skip_defaults_from,
251                    )?;
252                    options.insert("Solver", Value::from(solver.name()));
253                    continue;
254                } else {
255                    let next_solver = solver_from_options(&existing)?;
256                    let skip_defaults_from;
257                    if next_solver != Solver::Generic && next_solver != solver {
258                        options = if solver == Solver::Generic {
259                            merge_generic_into_defaults(&options, next_solver)?
260                        } else {
261                            default_options(next_solver)
262                        };
263                        solver = next_solver;
264                        skip_defaults_from = Some(next_solver);
265                    } else if next_solver != Solver::Generic {
266                        solver = next_solver;
267                        skip_defaults_from = Some(next_solver);
268                    } else {
269                        skip_defaults_from = None;
270                    }
271                    apply_struct_fields(&existing, &mut options, solver, true, skip_defaults_from)?;
272                    continue;
273                }
274            }
275            name_value => {
276                let name = expect_string_scalar(
277                    &name_value,
278                    "optimoptions: option names must be character vectors or string scalars",
279                    &OPTIMOPTIONS_ERROR_INVALID_OPTION_NAME,
280                )?;
281                let value = queue.pop_front().ok_or_else(|| {
282                    optimoptions_error_with(
283                        &OPTIMOPTIONS_ERROR_MISSING_OPTION_VALUE,
284                        format!("optimoptions: missing value for option '{name}'"),
285                    )
286                })?;
287                set_option_field(&mut options, solver, &name, &value)?;
288            }
289        }
290    }
291
292    Ok(Value::Struct(options))
293}
294
295fn optimoptions_error_with(
296    error: &'static BuiltinErrorDescriptor,
297    message: impl Into<String>,
298) -> RuntimeError {
299    let mut builder = build_runtime_error(message).with_builtin(NAME);
300    if let Some(identifier) = error.identifier {
301        builder = builder.with_identifier(identifier);
302    }
303    builder.build()
304}
305
306fn remap_optimoptions_flow<F>(
307    error: &'static BuiltinErrorDescriptor,
308    err: RuntimeError,
309    message: F,
310) -> RuntimeError
311where
312    F: FnOnce(&RuntimeError) -> String,
313{
314    let mut builder = build_runtime_error(message(&err))
315        .with_builtin(NAME)
316        .with_source(err);
317    if let Some(identifier) = error.identifier {
318        builder = builder.with_identifier(identifier);
319    }
320    builder.build()
321}
322
323#[derive(Debug, Clone, Copy, PartialEq, Eq)]
324enum Solver {
325    Coneprog,
326    Fminbnd,
327    Fminunc,
328    Fzero,
329    Fsolve,
330    Lsqcurvefit,
331    Lsqnonlin,
332    Generic,
333}
334
335impl Solver {
336    fn name(self) -> &'static str {
337        match self {
338            Self::Coneprog => "coneprog",
339            Self::Fminbnd => "fminbnd",
340            Self::Fminunc => "fminunc",
341            Self::Fzero => "fzero",
342            Self::Fsolve => "fsolve",
343            Self::Lsqcurvefit => "lsqcurvefit",
344            Self::Lsqnonlin => "lsqnonlin",
345            Self::Generic => "",
346        }
347    }
348
349    fn default_display(self) -> &'static str {
350        match self {
351            Self::Fminbnd => "notify",
352            Self::Coneprog
353            | Self::Fminunc
354            | Self::Fzero
355            | Self::Fsolve
356            | Self::Lsqcurvefit
357            | Self::Lsqnonlin
358            | Self::Generic => "off",
359        }
360    }
361
362    fn accepts_tol_fun(self) -> bool {
363        matches!(
364            self,
365            Self::Coneprog
366                | Self::Fminunc
367                | Self::Fsolve
368                | Self::Lsqcurvefit
369                | Self::Lsqnonlin
370                | Self::Generic
371        )
372    }
373
374    fn accepts_option(self, canonical: &str) -> bool {
375        match canonical {
376            "TolX" | "MaxIter" | "MaxFunEvals" | "Display" => true,
377            "TolFun" => self.accepts_tol_fun(),
378            "Algorithm" => matches!(
379                self,
380                Self::Coneprog
381                    | Self::Fminunc
382                    | Self::Lsqcurvefit
383                    | Self::Lsqnonlin
384                    | Self::Generic
385            ),
386            "SpecifyObjectiveGradient" => matches!(self, Self::Fminunc | Self::Generic),
387            _ => false,
388        }
389    }
390
391    fn accepts_display(self, display: &str) -> bool {
392        match self {
393            Self::Fminbnd | Self::Fminunc | Self::Generic => {
394                matches!(display, "off" | "none" | "iter" | "notify" | "final")
395            }
396            Self::Coneprog | Self::Fzero | Self::Fsolve | Self::Lsqcurvefit | Self::Lsqnonlin => {
397                matches!(display, "off" | "none" | "iter" | "final")
398            }
399        }
400    }
401
402    fn accepts_algorithm(self, algorithm: &str) -> bool {
403        match self {
404            Self::Fminunc => matches!(algorithm, "quasi-newton" | "bfgs"),
405            Self::Coneprog => matches!(algorithm, "interior-point" | "interior-point-convex"),
406            Self::Lsqcurvefit | Self::Lsqnonlin | Self::Generic => {
407                matches!(
408                    algorithm,
409                    "quasi-newton" | "bfgs" | "levenberg-marquardt" | "trust-region-reflective"
410                )
411            }
412            _ => false,
413        }
414    }
415}
416
417fn parse_solver(value: &Value) -> BuiltinResult<Solver> {
418    let text = expect_string_scalar(
419        value,
420        "optimoptions: solver must be a character vector or string scalar",
421        &OPTIMOPTIONS_ERROR_INVALID_SOLVER,
422    )?;
423    parse_solver_name(&text)
424}
425
426fn parse_solver_name(text: &str) -> BuiltinResult<Solver> {
427    match text.trim().to_ascii_lowercase().as_str() {
428        "fminbnd" => Ok(Solver::Fminbnd),
429        "coneprog" => Ok(Solver::Coneprog),
430        "fminunc" => Ok(Solver::Fminunc),
431        "fzero" => Ok(Solver::Fzero),
432        "fsolve" => Ok(Solver::Fsolve),
433        "lsqcurvefit" => Ok(Solver::Lsqcurvefit),
434        "lsqnonlin" => Ok(Solver::Lsqnonlin),
435        other => Err(optimoptions_error_with(
436            &OPTIMOPTIONS_ERROR_INVALID_SOLVER,
437            format!("optimoptions: unsupported solver '{other}'"),
438        )),
439    }
440}
441
442fn solver_from_options(options: &StructValue) -> BuiltinResult<Solver> {
443    let Some(value) = lookup_case_insensitive(options, "Solver") else {
444        return Ok(Solver::Generic);
445    };
446    parse_solver(value)
447}
448
449fn default_options(solver: Solver) -> StructValue {
450    let mut out = StructValue::new();
451    if solver != Solver::Generic {
452        out.insert("Solver", Value::from(solver.name()));
453    }
454    match solver {
455        Solver::Coneprog => {
456            out.insert("Algorithm", Value::from("interior-point"));
457            out.insert("TolX", Value::Num(1.0e-7));
458            out.insert("TolFun", Value::Num(1.0e-7));
459            out.insert("MaxIter", Value::Num(200.0));
460            out.insert("MaxFunEvals", Value::Num(20000.0));
461            out.insert("Display", Value::from(solver.default_display()));
462        }
463        Solver::Fminbnd => {
464            out.insert("TolX", Value::Num(1.0e-4));
465            out.insert("MaxIter", Value::Num(500.0));
466            out.insert("MaxFunEvals", Value::Num(500.0));
467            out.insert("Display", Value::from(solver.default_display()));
468        }
469        Solver::Fminunc => {
470            out.insert("Algorithm", Value::from("quasi-newton"));
471            out.insert("TolX", Value::Num(1.0e-6));
472            out.insert("TolFun", Value::Num(1.0e-6));
473            out.insert("MaxIter", Value::Num(400.0));
474            out.insert("MaxFunEvals", Value::Num(40000.0));
475            out.insert("Display", Value::from(solver.default_display()));
476            out.insert("SpecifyObjectiveGradient", Value::Bool(false));
477        }
478        Solver::Fzero => {
479            out.insert("TolX", Value::Num(1.0e-6));
480            out.insert("MaxIter", Value::Num(400.0));
481            out.insert("MaxFunEvals", Value::Num(500.0));
482            out.insert("Display", Value::from(solver.default_display()));
483        }
484        Solver::Fsolve => {
485            out.insert("TolX", Value::Num(1.0e-6));
486            out.insert("TolFun", Value::Num(1.0e-6));
487            out.insert("MaxIter", Value::Num(400.0));
488            out.insert("MaxFunEvals", Value::Num(40000.0));
489            out.insert("Display", Value::from(solver.default_display()));
490        }
491        Solver::Lsqcurvefit => {
492            out.insert("Algorithm", Value::from("levenberg-marquardt"));
493            out.insert("TolX", Value::Num(1.0e-6));
494            out.insert("TolFun", Value::Num(1.0e-6));
495            out.insert("MaxIter", Value::Num(400.0));
496            out.insert("MaxFunEvals", Value::Num(40000.0));
497            out.insert("Display", Value::from(solver.default_display()));
498        }
499        Solver::Lsqnonlin => {
500            out.insert("Algorithm", Value::from("trust-region-reflective"));
501            out.insert("TolX", Value::Num(1.0e-6));
502            out.insert("TolFun", Value::Num(1.0e-6));
503            out.insert("MaxIter", Value::Num(400.0));
504            out.insert("MaxFunEvals", Value::Num(40000.0));
505            out.insert("Display", Value::from(solver.default_display()));
506        }
507        Solver::Generic => {}
508    }
509    out
510}
511
512fn canonicalize_existing_options(
513    existing: &StructValue,
514    solver: Solver,
515) -> BuiltinResult<StructValue> {
516    let mut out = if solver == Solver::Generic {
517        StructValue::new()
518    } else {
519        default_options(solver)
520    };
521    apply_struct_fields(existing, &mut out, solver, true, None)?;
522    Ok(out)
523}
524
525fn merge_generic_into_defaults(
526    generic: &StructValue,
527    solver: Solver,
528) -> BuiltinResult<StructValue> {
529    let mut out = default_options(solver);
530    for (key, value) in &generic.fields {
531        if key.eq_ignore_ascii_case("Solver") {
532            continue;
533        }
534        let canonical = canonical_option_name(key);
535        if !solver.accepts_option(&canonical) {
536            continue;
537        }
538        if canonical == "Display" && display_value(solver, value).is_err() {
539            continue;
540        }
541        set_option_field(&mut out, solver, key, value)?;
542    }
543    Ok(out)
544}
545
546fn apply_struct_fields(
547    source: &StructValue,
548    target: &mut StructValue,
549    solver: Solver,
550    copy_solver_field: bool,
551    skip_defaults_from: Option<Solver>,
552) -> BuiltinResult<()> {
553    let source_defaults = skip_defaults_from.map(default_options);
554    for (key, value) in &source.fields {
555        if key.eq_ignore_ascii_case("Solver") {
556            if !copy_solver_field {
557                continue;
558            }
559            let parsed = parse_solver(value)?;
560            target.insert("Solver", Value::from(parsed.name()));
561            continue;
562        }
563        let canonical = canonical_option_name(key);
564        if let Some(defaults) = &source_defaults {
565            if solver.accepts_option(&canonical)
566                && lookup_case_insensitive(defaults, &canonical).is_some_and(|default| {
567                    normalized_option_value(solver, &canonical, value)
568                        .is_ok_and(|normalized| default == &normalized)
569                })
570            {
571                continue;
572            }
573        }
574        set_option_field(target, solver, key, value)?;
575    }
576    Ok(())
577}
578
579fn set_option_field(
580    options: &mut StructValue,
581    solver: Solver,
582    name: &str,
583    value: &Value,
584) -> BuiltinResult<()> {
585    let canonical = canonical_option_name(name);
586    if !solver.accepts_option(&canonical) {
587        return Err(optimoptions_error_with(
588            &OPTIMOPTIONS_ERROR_UNKNOWN_OPTION,
589            format!(
590                "optimoptions: option '{}' is not supported for {}",
591                name,
592                solver_label(solver)
593            ),
594        ));
595    }
596
597    let value = normalized_option_value(solver, &canonical, value)?;
598    options.insert(canonical, value);
599    Ok(())
600}
601
602fn normalized_option_value(solver: Solver, canonical: &str, value: &Value) -> BuiltinResult<Value> {
603    match canonical {
604        "TolX" | "TolFun" => Ok(Value::Num(positive_finite_scalar(canonical, value)?)),
605        "MaxIter" | "MaxFunEvals" => {
606            Ok(Value::Num(positive_integer_scalar(canonical, value)? as f64))
607        }
608        "Display" => Ok(Value::from(display_value(solver, value)?)),
609        "Algorithm" => Ok(Value::from(algorithm_value(solver, value)?)),
610        "SpecifyObjectiveGradient" => Ok(Value::Bool(logical_value(canonical, value)?)),
611        _ => unreachable!("unsupported option passed accepts_option"),
612    }
613}
614
615fn solver_label(solver: Solver) -> &'static str {
616    match solver {
617        Solver::Generic => "optimization solvers",
618        _ => solver.name(),
619    }
620}
621
622fn positive_finite_scalar(field: &str, value: &Value) -> BuiltinResult<f64> {
623    let parsed = numeric_scalar(field, value)?;
624    if parsed > 0.0 {
625        Ok(parsed)
626    } else {
627        Err(optimoptions_error_with(
628            &OPTIMOPTIONS_ERROR_INVALID_OPTION_VALUE,
629            format!("optimoptions: option {field} must be a finite positive scalar"),
630        ))
631    }
632}
633
634fn positive_integer_scalar(field: &str, value: &Value) -> BuiltinResult<usize> {
635    let parsed = positive_finite_scalar(field, value)?;
636    if parsed.fract() != 0.0 {
637        return Err(optimoptions_error_with(
638            &OPTIMOPTIONS_ERROR_INVALID_OPTION_VALUE,
639            format!("optimoptions: option {field} must be an integer scalar"),
640        ));
641    }
642    if parsed >= 2f64.powi(usize::BITS as i32) {
643        return Err(optimoptions_error_with(
644            &OPTIMOPTIONS_ERROR_INVALID_OPTION_VALUE,
645            format!("optimoptions: option {field} is too large"),
646        ));
647    }
648    Ok(parsed as usize)
649}
650
651fn numeric_scalar(field: &str, value: &Value) -> BuiltinResult<f64> {
652    let parsed = match value {
653        Value::Num(n) => *n,
654        Value::Int(i) => i.to_f64(),
655        Value::Tensor(Tensor { data, .. }) if data.len() == 1 => data[0],
656        Value::LogicalArray(LogicalArray { data, .. }) if data.len() == 1 => {
657            if data[0] == 0 {
658                0.0
659            } else {
660                1.0
661            }
662        }
663        other => {
664            return Err(optimoptions_error_with(
665                &OPTIMOPTIONS_ERROR_INVALID_OPTION_VALUE,
666                format!("optimoptions: option {field} must be a numeric scalar, got {other:?}"),
667            ))
668        }
669    };
670    if parsed.is_finite() {
671        Ok(parsed)
672    } else {
673        Err(optimoptions_error_with(
674            &OPTIMOPTIONS_ERROR_INVALID_OPTION_VALUE,
675            format!("optimoptions: option {field} must be finite"),
676        ))
677    }
678}
679
680fn logical_value(field: &str, value: &Value) -> BuiltinResult<bool> {
681    match value {
682        Value::Bool(flag) => Ok(*flag),
683        Value::LogicalArray(LogicalArray { data, .. }) if data.len() == 1 => Ok(data[0] != 0),
684        Value::Num(n) => logical_from_number(field, *n),
685        Value::Int(i) => logical_from_number(field, i.to_f64()),
686        Value::Tensor(Tensor { data, .. }) if data.len() == 1 => {
687            logical_from_number(field, data[0])
688        }
689        Value::String(s) => logical_from_text(field, s),
690        Value::StringArray(sa) if sa.data.len() == 1 => logical_from_text(field, &sa.data[0]),
691        Value::CharArray(CharArray { data, rows: 1, .. }) => {
692            let text: String = data.iter().collect();
693            logical_from_text(field, &text)
694        }
695        other => Err(optimoptions_error_with(
696            &OPTIMOPTIONS_ERROR_INVALID_OPTION_VALUE,
697            format!("optimoptions: option {field} must be logical, got {other:?}"),
698        )),
699    }
700}
701
702fn logical_from_number(field: &str, value: f64) -> BuiltinResult<bool> {
703    if value == 0.0 {
704        Ok(false)
705    } else if value == 1.0 {
706        Ok(true)
707    } else {
708        Err(optimoptions_error_with(
709            &OPTIMOPTIONS_ERROR_INVALID_OPTION_VALUE,
710            format!("optimoptions: option {field} must be logical 0 or 1"),
711        ))
712    }
713}
714
715fn logical_from_text(field: &str, value: &str) -> BuiltinResult<bool> {
716    match value.trim().to_ascii_lowercase().as_str() {
717        "on" | "true" | "yes" => Ok(true),
718        "off" | "false" | "no" => Ok(false),
719        other => Err(optimoptions_error_with(
720            &OPTIMOPTIONS_ERROR_INVALID_OPTION_VALUE,
721            format!("optimoptions: option {field} must be 'on' or 'off', got '{other}'"),
722        )),
723    }
724}
725
726fn display_value(solver: Solver, value: &Value) -> BuiltinResult<String> {
727    let display = expect_string_scalar(
728        value,
729        "optimoptions: Display must be a character vector or string scalar",
730        &OPTIMOPTIONS_ERROR_INVALID_OPTION_VALUE,
731    )?
732    .trim()
733    .to_ascii_lowercase();
734    if solver.accepts_display(&display) {
735        Ok(display)
736    } else {
737        Err(optimoptions_error_with(
738            &OPTIMOPTIONS_ERROR_INVALID_OPTION_VALUE,
739            format!(
740                "optimoptions: unsupported Display '{}' for {}",
741                display,
742                solver_label(solver)
743            ),
744        ))
745    }
746}
747
748fn algorithm_value(solver: Solver, value: &Value) -> BuiltinResult<String> {
749    let algorithm = expect_string_scalar(
750        value,
751        "optimoptions: Algorithm must be a character vector or string scalar",
752        &OPTIMOPTIONS_ERROR_INVALID_OPTION_VALUE,
753    )?
754    .trim()
755    .to_ascii_lowercase();
756    if solver.accepts_algorithm(&algorithm) {
757        Ok(algorithm)
758    } else {
759        Err(optimoptions_error_with(
760            &OPTIMOPTIONS_ERROR_INVALID_OPTION_VALUE,
761            format!(
762                "optimoptions: unsupported Algorithm '{}' for {}",
763                algorithm,
764                solver_label(solver)
765            ),
766        ))
767    }
768}
769
770fn expect_string_scalar(
771    value: &Value,
772    context: &str,
773    error: &'static BuiltinErrorDescriptor,
774) -> BuiltinResult<String> {
775    match value {
776        Value::String(s) => Ok(s.clone()),
777        Value::StringArray(sa) if sa.data.len() == 1 => Ok(sa.data[0].clone()),
778        Value::CharArray(CharArray { data, rows: 1, .. }) => Ok(data.iter().collect()),
779        _ => Err(optimoptions_error_with(error, context)),
780    }
781}
782
783fn lookup_case_insensitive<'a>(options: &'a StructValue, name: &str) -> Option<&'a Value> {
784    options
785        .fields
786        .iter()
787        .find(|(key, _)| key.eq_ignore_ascii_case(name))
788        .map(|(_, value)| value)
789}
790
791#[cfg(test)]
792mod tests {
793    use super::*;
794    use crate::call_builtin_async;
795    use futures::executor::block_on;
796    use runmat_builtins::IntValue;
797
798    fn run_optimoptions(rest: Vec<Value>) -> BuiltinResult<Value> {
799        block_on(optimoptions_builtin(rest))
800    }
801
802    fn run_call_builtin(name: &str, args: &[Value]) -> BuiltinResult<Value> {
803        block_on(call_builtin_async(name, args))
804    }
805
806    fn struct_result(value: Value) -> StructValue {
807        match value {
808            Value::Struct(options) => options,
809            other => panic!("expected struct, got {other:?}"),
810        }
811    }
812
813    fn num_field(options: &StructValue, field: &str) -> f64 {
814        match options.fields.get(field) {
815            Some(Value::Num(value)) => *value,
816            other => panic!("expected numeric field {field}, got {other:?}"),
817        }
818    }
819
820    fn string_field<'a>(options: &'a StructValue, field: &str) -> &'a str {
821        match options.fields.get(field) {
822            Some(Value::String(value)) => value.as_str(),
823            other => panic!("expected string field {field}, got {other:?}"),
824        }
825    }
826
827    fn bool_field(options: &StructValue, field: &str) -> bool {
828        match options.fields.get(field) {
829            Some(Value::Bool(value)) => *value,
830            other => panic!("expected bool field {field}, got {other:?}"),
831        }
832    }
833
834    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
835    #[test]
836    fn optimoptions_descriptor_signatures_and_errors_cover_core_forms() {
837        let labels: Vec<&str> = OPTIMOPTIONS_DESCRIPTOR
838            .signatures
839            .iter()
840            .map(|signature| signature.label)
841            .collect();
842        assert_eq!(
843            labels,
844            vec![
845                "options = optimoptions(solver)",
846                "options = optimoptions(solver, name, value, ...)",
847                "options = optimoptions(oldopts, name, value, ...)",
848            ]
849        );
850
851        let codes: Vec<&str> = OPTIMOPTIONS_DESCRIPTOR
852            .errors
853            .iter()
854            .map(|error| error.code)
855            .collect();
856        assert_eq!(
857            codes,
858            vec![
859                "RM.OPTIMOPTIONS.INVALID_ARGUMENT",
860                "RM.OPTIMOPTIONS.INVALID_SOLVER",
861                "RM.OPTIMOPTIONS.INVALID_OPTION_NAME",
862                "RM.OPTIMOPTIONS.MISSING_OPTION_VALUE",
863                "RM.OPTIMOPTIONS.UNKNOWN_OPTION",
864                "RM.OPTIMOPTIONS.INVALID_OPTION_VALUE",
865                "RM.OPTIMOPTIONS.FLOW",
866            ]
867        );
868    }
869
870    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
871    #[test]
872    fn optimoptions_fminbnd_defaults_match_solver() {
873        let options = struct_result(
874            run_optimoptions(vec![Value::from("fminbnd")]).expect("optimoptions fminbnd"),
875        );
876        assert_eq!(string_field(&options, "Solver"), "fminbnd");
877        assert_eq!(num_field(&options, "TolX"), 1.0e-4);
878        assert_eq!(num_field(&options, "MaxIter"), 500.0);
879        assert_eq!(num_field(&options, "MaxFunEvals"), 500.0);
880        assert_eq!(string_field(&options, "Display"), "notify");
881        assert!(!options.fields.contains_key("TolFun"));
882    }
883
884    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
885    #[test]
886    fn optimoptions_coneprog_defaults_match_solver() {
887        let options = struct_result(
888            run_optimoptions(vec![Value::from("coneprog")]).expect("optimoptions coneprog"),
889        );
890        assert_eq!(string_field(&options, "Solver"), "coneprog");
891        assert_eq!(string_field(&options, "Algorithm"), "interior-point");
892        assert_eq!(num_field(&options, "TolX"), 1.0e-7);
893        assert_eq!(num_field(&options, "TolFun"), 1.0e-7);
894        assert_eq!(num_field(&options, "MaxIter"), 200.0);
895    }
896
897    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
898    #[test]
899    fn optimoptions_fzero_defaults_match_solver() {
900        let options = struct_result(
901            run_optimoptions(vec![Value::from("fzero")]).expect("optimoptions fzero"),
902        );
903        assert_eq!(string_field(&options, "Solver"), "fzero");
904        assert_eq!(num_field(&options, "TolX"), 1.0e-6);
905        assert_eq!(num_field(&options, "MaxIter"), 400.0);
906        assert_eq!(num_field(&options, "MaxFunEvals"), 500.0);
907        assert_eq!(string_field(&options, "Display"), "off");
908        assert!(!options.fields.contains_key("TolFun"));
909    }
910
911    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
912    #[test]
913    fn optimoptions_fsolve_defaults_match_solver() {
914        let options = struct_result(
915            run_optimoptions(vec![Value::from("fsolve")]).expect("optimoptions fsolve"),
916        );
917        assert_eq!(string_field(&options, "Solver"), "fsolve");
918        assert_eq!(num_field(&options, "TolX"), 1.0e-6);
919        assert_eq!(num_field(&options, "TolFun"), 1.0e-6);
920        assert_eq!(num_field(&options, "MaxIter"), 400.0);
921        assert_eq!(num_field(&options, "MaxFunEvals"), 40000.0);
922        assert_eq!(string_field(&options, "Display"), "off");
923    }
924
925    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
926    #[test]
927    fn optimoptions_fminunc_defaults_match_solver() {
928        let options = struct_result(
929            run_optimoptions(vec![Value::from("fminunc")]).expect("optimoptions fminunc"),
930        );
931        assert_eq!(string_field(&options, "Solver"), "fminunc");
932        assert_eq!(string_field(&options, "Algorithm"), "quasi-newton");
933        assert_eq!(num_field(&options, "TolX"), 1.0e-6);
934        assert_eq!(num_field(&options, "TolFun"), 1.0e-6);
935        assert_eq!(num_field(&options, "MaxIter"), 400.0);
936        assert_eq!(num_field(&options, "MaxFunEvals"), 40000.0);
937        assert_eq!(string_field(&options, "Display"), "off");
938        assert!(!bool_field(&options, "SpecifyObjectiveGradient"));
939    }
940
941    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
942    #[test]
943    fn optimoptions_fminunc_accepts_gradient_and_algorithm_options() {
944        let options = struct_result(
945            run_optimoptions(vec![
946                Value::from("fminunc"),
947                Value::from("SpecifyObjectiveGradient"),
948                Value::from("on"),
949                Value::from("Algorithm"),
950                Value::from("bfgs"),
951                Value::from("Display"),
952                Value::from("notify"),
953            ])
954            .expect("optimoptions fminunc"),
955        );
956        assert!(bool_field(&options, "SpecifyObjectiveGradient"));
957        assert_eq!(string_field(&options, "Algorithm"), "bfgs");
958        assert_eq!(string_field(&options, "Display"), "notify");
959    }
960
961    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
962    #[test]
963    fn optimoptions_lsqcurvefit_defaults_match_solver() {
964        let options = struct_result(
965            run_optimoptions(vec![Value::from("lsqcurvefit")]).expect("optimoptions lsqcurvefit"),
966        );
967        assert_eq!(string_field(&options, "Solver"), "lsqcurvefit");
968        assert_eq!(string_field(&options, "Algorithm"), "levenberg-marquardt");
969        assert_eq!(num_field(&options, "TolX"), 1.0e-6);
970        assert_eq!(num_field(&options, "TolFun"), 1.0e-6);
971        assert_eq!(num_field(&options, "MaxIter"), 400.0);
972        assert_eq!(num_field(&options, "MaxFunEvals"), 40000.0);
973        assert_eq!(string_field(&options, "Display"), "off");
974    }
975
976    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
977    #[test]
978    fn optimoptions_lsqcurvefit_accepts_modern_tolerance_aliases_and_algorithm() {
979        let options = struct_result(
980            run_optimoptions(vec![
981                Value::from("lsqcurvefit"),
982                Value::from("FunctionTolerance"),
983                Value::Num(1.0e-9),
984                Value::from("StepTolerance"),
985                Value::Num(1.0e-8),
986                Value::from("Algorithm"),
987                Value::from("trust-region-reflective"),
988            ])
989            .expect("optimoptions lsqcurvefit aliases"),
990        );
991        assert_eq!(num_field(&options, "TolFun"), 1.0e-9);
992        assert_eq!(num_field(&options, "TolX"), 1.0e-8);
993        assert_eq!(
994            string_field(&options, "Algorithm"),
995            "trust-region-reflective"
996        );
997    }
998
999    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1000    #[test]
1001    fn optimoptions_lsqnonlin_defaults_and_aliases_match_solver() {
1002        let options = struct_result(
1003            run_optimoptions(vec![
1004                Value::from("lsqnonlin"),
1005                Value::from("FunctionTolerance"),
1006                Value::Num(1.0e-9),
1007                Value::from("StepTolerance"),
1008                Value::Num(1.0e-8),
1009                Value::from("Algorithm"),
1010                Value::from("levenberg-marquardt"),
1011            ])
1012            .expect("optimoptions lsqnonlin"),
1013        );
1014        assert_eq!(string_field(&options, "Solver"), "lsqnonlin");
1015        assert_eq!(string_field(&options, "Algorithm"), "levenberg-marquardt");
1016        assert_eq!(num_field(&options, "TolFun"), 1.0e-9);
1017        assert_eq!(num_field(&options, "TolX"), 1.0e-8);
1018        assert_eq!(num_field(&options, "MaxIter"), 400.0);
1019        assert_eq!(num_field(&options, "MaxFunEvals"), 40000.0);
1020        assert_eq!(string_field(&options, "Display"), "off");
1021    }
1022
1023    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1024    #[test]
1025    fn optimoptions_name_value_pairs_are_case_insensitive() {
1026        let options = struct_result(
1027            run_optimoptions(vec![
1028                Value::from("fsolve"),
1029                Value::from("tolx"),
1030                Value::Num(1.0e-8),
1031                Value::from("DISPLAY"),
1032                Value::from("Final"),
1033            ])
1034            .expect("optimoptions overrides"),
1035        );
1036        assert_eq!(num_field(&options, "TolX"), 1.0e-8);
1037        assert_eq!(string_field(&options, "Display"), "final");
1038    }
1039
1040    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1041    #[test]
1042    fn optimoptions_updates_existing_options_with_pairs() {
1043        let base = run_optimoptions(vec![
1044            Value::from("fzero"),
1045            Value::from("TolX"),
1046            Value::Num(1.0e-5),
1047        ])
1048        .expect("base options");
1049        let options = struct_result(
1050            run_optimoptions(vec![base, Value::from("MaxIter"), Value::Num(25.0)])
1051                .expect("updated options"),
1052        );
1053        assert_eq!(string_field(&options, "Solver"), "fzero");
1054        assert_eq!(num_field(&options, "TolX"), 1.0e-5);
1055        assert_eq!(num_field(&options, "MaxIter"), 25.0);
1056    }
1057
1058    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1059    #[test]
1060    fn optimoptions_merges_existing_options_structs() {
1061        let first = run_optimoptions(vec![
1062            Value::from("fsolve"),
1063            Value::from("TolX"),
1064            Value::Num(1.0e-5),
1065        ])
1066        .expect("first");
1067        let second = run_optimoptions(vec![
1068            Value::from("fsolve"),
1069            Value::from("TolX"),
1070            Value::Num(1.0e-8),
1071            Value::from("MaxIter"),
1072            Value::Num(30.0),
1073        ])
1074        .expect("second");
1075        let options = struct_result(run_optimoptions(vec![first, second]).expect("merged options"));
1076        assert_eq!(num_field(&options, "TolX"), 1.0e-8);
1077        assert_eq!(num_field(&options, "MaxIter"), 30.0);
1078    }
1079
1080    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1081    #[test]
1082    fn optimoptions_same_solver_struct_merge_preserves_prior_overrides() {
1083        let first = run_optimoptions(vec![
1084            Value::from("fsolve"),
1085            Value::from("MaxFunEvals"),
1086            Value::Num(2000.0),
1087        ])
1088        .expect("first");
1089        let second = run_optimoptions(vec![
1090            Value::from("fsolve"),
1091            Value::from("TolX"),
1092            Value::Num(1.0e-8),
1093        ])
1094        .expect("second");
1095
1096        let options = struct_result(run_optimoptions(vec![first, second]).expect("merged options"));
1097
1098        assert_eq!(string_field(&options, "Solver"), "fsolve");
1099        assert_eq!(num_field(&options, "TolX"), 1.0e-8);
1100        assert_eq!(num_field(&options, "MaxFunEvals"), 2000.0);
1101    }
1102
1103    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1104    #[test]
1105    fn optimoptions_solver_form_same_solver_struct_preserves_prior_overrides() {
1106        let later = run_optimoptions(vec![
1107            Value::from("fsolve"),
1108            Value::from("TolX"),
1109            Value::Num(1.0e-8),
1110        ])
1111        .expect("later options");
1112
1113        let options = struct_result(
1114            run_optimoptions(vec![
1115                Value::from("fsolve"),
1116                Value::from("MaxFunEvals"),
1117                Value::Num(2000.0),
1118                later,
1119            ])
1120            .expect("merged options"),
1121        );
1122
1123        assert_eq!(string_field(&options, "Solver"), "fsolve");
1124        assert_eq!(num_field(&options, "TolX"), 1.0e-8);
1125        assert_eq!(num_field(&options, "MaxFunEvals"), 2000.0);
1126    }
1127
1128    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1129    #[test]
1130    fn optimoptions_default_skipping_compares_normalized_values() {
1131        let first = run_optimoptions(vec![
1132            Value::from("fsolve"),
1133            Value::from("MaxFunEvals"),
1134            Value::Num(2000.0),
1135            Value::from("Display"),
1136            Value::from("final"),
1137        ])
1138        .expect("first");
1139
1140        let mut later = StructValue::new();
1141        later.insert("Solver", Value::from("fsolve"));
1142        later.insert("TolX", Value::Num(1.0e-8));
1143        later.insert("MaxFunEvals", Value::Int(IntValue::I32(40000)));
1144        later.insert("Display", Value::CharArray(CharArray::new_row("off")));
1145
1146        let options = struct_result(
1147            run_optimoptions(vec![first, Value::Struct(later)]).expect("merged options"),
1148        );
1149
1150        assert_eq!(string_field(&options, "Solver"), "fsolve");
1151        assert_eq!(num_field(&options, "TolX"), 1.0e-8);
1152        assert_eq!(num_field(&options, "MaxFunEvals"), 2000.0);
1153        assert_eq!(string_field(&options, "Display"), "final");
1154    }
1155
1156    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1157    #[test]
1158    fn optimoptions_generic_to_concrete_solver_preserves_valid_generic_overrides() {
1159        let mut generic = StructValue::new();
1160        generic.insert("MaxFunEvals", Value::Num(2000.0));
1161        generic.insert("Display", Value::from("final"));
1162
1163        let later = run_optimoptions(vec![
1164            Value::from("fsolve"),
1165            Value::from("TolX"),
1166            Value::Num(1.0e-8),
1167        ])
1168        .expect("later options");
1169
1170        let options = struct_result(
1171            run_optimoptions(vec![Value::Struct(generic), later]).expect("merged options"),
1172        );
1173
1174        assert_eq!(string_field(&options, "Solver"), "fsolve");
1175        assert_eq!(num_field(&options, "TolX"), 1.0e-8);
1176        assert_eq!(num_field(&options, "TolFun"), 1.0e-6);
1177        assert_eq!(num_field(&options, "MaxFunEvals"), 2000.0);
1178        assert_eq!(string_field(&options, "Display"), "final");
1179    }
1180
1181    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1182    #[test]
1183    fn optimoptions_solver_form_keeps_requested_solver_when_struct_has_solver() {
1184        let fzero_options = run_optimoptions(vec![
1185            Value::from("fzero"),
1186            Value::from("TolX"),
1187            Value::Num(1.0e-8),
1188            Value::from("MaxIter"),
1189            Value::Num(30.0),
1190        ])
1191        .expect("fzero options");
1192
1193        let options = struct_result(
1194            run_optimoptions(vec![Value::from("fsolve"), fzero_options])
1195                .expect("merged into fsolve options"),
1196        );
1197
1198        assert_eq!(string_field(&options, "Solver"), "fsolve");
1199        assert_eq!(num_field(&options, "TolX"), 1.0e-8);
1200        assert_eq!(num_field(&options, "MaxIter"), 30.0);
1201        assert_eq!(num_field(&options, "TolFun"), 1.0e-6);
1202        assert_eq!(num_field(&options, "MaxFunEvals"), 40000.0);
1203    }
1204
1205    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1206    #[test]
1207    fn optimoptions_rejects_unknown_option_names() {
1208        let err = run_optimoptions(vec![
1209            Value::from("fzero"),
1210            Value::from("TolFun"),
1211            Value::Num(1.0e-8),
1212        ])
1213        .expect_err("TolFun is not accepted by fzero");
1214        assert_eq!(err.identifier(), Some("RunMat:optimoptions:UnknownOption"));
1215    }
1216
1217    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1218    #[test]
1219    fn optimoptions_rejects_missing_option_values() {
1220        let err = run_optimoptions(vec![Value::from("fsolve"), Value::from("TolX")])
1221            .expect_err("missing option value");
1222        assert_eq!(
1223            err.identifier(),
1224            Some("RunMat:optimoptions:MissingOptionValue")
1225        );
1226    }
1227
1228    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1229    #[test]
1230    fn optimoptions_rejects_invalid_option_values() {
1231        let err = run_optimoptions(vec![
1232            Value::from("fsolve"),
1233            Value::from("MaxIter"),
1234            Value::Num(1.5),
1235        ])
1236        .expect_err("noninteger MaxIter should fail");
1237        assert_eq!(
1238            err.identifier(),
1239            Some("RunMat:optimoptions:InvalidOptionValue")
1240        );
1241    }
1242
1243    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1244    #[test]
1245    fn optimoptions_rejects_out_of_range_integer_options() {
1246        let err = run_optimoptions(vec![
1247            Value::from("fsolve"),
1248            Value::from("MaxIter"),
1249            Value::Num(2f64.powi(usize::BITS as i32)),
1250        ])
1251        .expect_err("out-of-range MaxIter should fail");
1252        assert_eq!(
1253            err.identifier(),
1254            Some("RunMat:optimoptions:InvalidOptionValue")
1255        );
1256    }
1257
1258    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1259    #[test]
1260    fn fminbnd_accepts_optimoptions_output() {
1261        let options = run_optimoptions(vec![
1262            Value::from("fminbnd"),
1263            Value::from("TolX"),
1264            Value::Num(1.0e-8),
1265            Value::from("Display"),
1266            Value::from("off"),
1267        ])
1268        .expect("optimoptions");
1269        let result = run_call_builtin(
1270            "fminbnd",
1271            &[
1272                Value::FunctionHandle("cos".into()),
1273                Value::Num(0.0),
1274                Value::Num(std::f64::consts::PI),
1275                options,
1276            ],
1277        )
1278        .expect("fminbnd");
1279        match result {
1280            Value::Num(value) => assert!((value - std::f64::consts::PI).abs() < 1.0e-4),
1281            other => panic!("unexpected fminbnd result {other:?}"),
1282        }
1283    }
1284
1285    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1286    #[test]
1287    fn fzero_accepts_optimoptions_output() {
1288        let options = run_optimoptions(vec![
1289            Value::from("fzero"),
1290            Value::from("TolX"),
1291            Value::Num(1.0e-8),
1292        ])
1293        .expect("optimoptions");
1294        let bracket = Tensor::new(vec![3.0, 4.0], vec![1, 2]).unwrap();
1295        let result = run_call_builtin(
1296            "fzero",
1297            &[
1298                Value::FunctionHandle("sin".into()),
1299                Value::Tensor(bracket),
1300                options,
1301            ],
1302        )
1303        .expect("fzero");
1304        match result {
1305            Value::Num(value) => assert!((value - std::f64::consts::PI).abs() < 1.0e-6),
1306            other => panic!("unexpected fzero result {other:?}"),
1307        }
1308    }
1309
1310    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1311    #[test]
1312    fn fsolve_accepts_optimoptions_output() {
1313        let options = run_optimoptions(vec![
1314            Value::from("fsolve"),
1315            Value::from("TolX"),
1316            Value::Num(1.0e-8),
1317            Value::from("TolFun"),
1318            Value::Num(1.0e-8),
1319        ])
1320        .expect("optimoptions");
1321        let result = run_call_builtin(
1322            "fsolve",
1323            &[
1324                Value::FunctionHandle("sin".into()),
1325                Value::Num(3.0),
1326                options,
1327            ],
1328        )
1329        .expect("fsolve");
1330        match result {
1331            Value::Num(value) => assert!((value - std::f64::consts::PI).abs() < 1.0e-6),
1332            other => panic!("unexpected fsolve result {other:?}"),
1333        }
1334    }
1335}