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