1use 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}