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, fzero, or fsolve.",
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, fzero, or fsolve.",
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, fzero, and fsolve.",
195 keywords = "optimoptions,options,TolX,TolFun,MaxIter,MaxFunEvals,Display",
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 Fzero,
325 Fsolve,
326 Generic,
327}
328
329impl Solver {
330 fn name(self) -> &'static str {
331 match self {
332 Self::Fminbnd => "fminbnd",
333 Self::Fzero => "fzero",
334 Self::Fsolve => "fsolve",
335 Self::Generic => "",
336 }
337 }
338
339 fn default_display(self) -> &'static str {
340 match self {
341 Self::Fminbnd => "notify",
342 Self::Fzero | Self::Fsolve | Self::Generic => "off",
343 }
344 }
345
346 fn accepts_tol_fun(self) -> bool {
347 matches!(self, Self::Fsolve | Self::Generic)
348 }
349
350 fn accepts_option(self, canonical: &str) -> bool {
351 match canonical {
352 "TolX" | "MaxIter" | "MaxFunEvals" | "Display" => true,
353 "TolFun" => self.accepts_tol_fun(),
354 _ => false,
355 }
356 }
357
358 fn accepts_display(self, display: &str) -> bool {
359 match self {
360 Self::Fminbnd | Self::Generic => {
361 matches!(display, "off" | "none" | "iter" | "notify" | "final")
362 }
363 Self::Fzero | Self::Fsolve => matches!(display, "off" | "none" | "iter" | "final"),
364 }
365 }
366}
367
368fn parse_solver(value: &Value) -> BuiltinResult<Solver> {
369 let text = expect_string_scalar(
370 value,
371 "optimoptions: solver must be a character vector or string scalar",
372 &OPTIMOPTIONS_ERROR_INVALID_SOLVER,
373 )?;
374 parse_solver_name(&text)
375}
376
377fn parse_solver_name(text: &str) -> BuiltinResult<Solver> {
378 match text.trim().to_ascii_lowercase().as_str() {
379 "fminbnd" => Ok(Solver::Fminbnd),
380 "fzero" => Ok(Solver::Fzero),
381 "fsolve" => Ok(Solver::Fsolve),
382 other => Err(optimoptions_error_with(
383 &OPTIMOPTIONS_ERROR_INVALID_SOLVER,
384 format!("optimoptions: unsupported solver '{other}'"),
385 )),
386 }
387}
388
389fn solver_from_options(options: &StructValue) -> BuiltinResult<Solver> {
390 let Some(value) = lookup_case_insensitive(options, "Solver") else {
391 return Ok(Solver::Generic);
392 };
393 parse_solver(value)
394}
395
396fn default_options(solver: Solver) -> StructValue {
397 let mut out = StructValue::new();
398 if solver != Solver::Generic {
399 out.insert("Solver", Value::from(solver.name()));
400 }
401 match solver {
402 Solver::Fminbnd => {
403 out.insert("TolX", Value::Num(1.0e-4));
404 out.insert("MaxIter", Value::Num(500.0));
405 out.insert("MaxFunEvals", Value::Num(500.0));
406 out.insert("Display", Value::from(solver.default_display()));
407 }
408 Solver::Fzero => {
409 out.insert("TolX", Value::Num(1.0e-6));
410 out.insert("MaxIter", Value::Num(400.0));
411 out.insert("MaxFunEvals", Value::Num(500.0));
412 out.insert("Display", Value::from(solver.default_display()));
413 }
414 Solver::Fsolve => {
415 out.insert("TolX", Value::Num(1.0e-6));
416 out.insert("TolFun", Value::Num(1.0e-6));
417 out.insert("MaxIter", Value::Num(400.0));
418 out.insert("MaxFunEvals", Value::Num(40000.0));
419 out.insert("Display", Value::from(solver.default_display()));
420 }
421 Solver::Generic => {}
422 }
423 out
424}
425
426fn canonicalize_existing_options(
427 existing: &StructValue,
428 solver: Solver,
429) -> BuiltinResult<StructValue> {
430 let mut out = if solver == Solver::Generic {
431 StructValue::new()
432 } else {
433 default_options(solver)
434 };
435 apply_struct_fields(existing, &mut out, solver, true, None)?;
436 Ok(out)
437}
438
439fn merge_generic_into_defaults(
440 generic: &StructValue,
441 solver: Solver,
442) -> BuiltinResult<StructValue> {
443 let mut out = default_options(solver);
444 for (key, value) in &generic.fields {
445 if key.eq_ignore_ascii_case("Solver") {
446 continue;
447 }
448 let canonical = canonical_option_name(key);
449 if !solver.accepts_option(&canonical) {
450 continue;
451 }
452 if canonical == "Display" && display_value(solver, value).is_err() {
453 continue;
454 }
455 set_option_field(&mut out, solver, key, value)?;
456 }
457 Ok(out)
458}
459
460fn apply_struct_fields(
461 source: &StructValue,
462 target: &mut StructValue,
463 solver: Solver,
464 copy_solver_field: bool,
465 skip_defaults_from: Option<Solver>,
466) -> BuiltinResult<()> {
467 let source_defaults = skip_defaults_from.map(default_options);
468 for (key, value) in &source.fields {
469 if key.eq_ignore_ascii_case("Solver") {
470 if !copy_solver_field {
471 continue;
472 }
473 let parsed = parse_solver(value)?;
474 target.insert("Solver", Value::from(parsed.name()));
475 continue;
476 }
477 let canonical = canonical_option_name(key);
478 if let Some(defaults) = &source_defaults {
479 if solver.accepts_option(&canonical)
480 && lookup_case_insensitive(defaults, &canonical).is_some_and(|default| {
481 normalized_option_value(solver, &canonical, value)
482 .is_ok_and(|normalized| default == &normalized)
483 })
484 {
485 continue;
486 }
487 }
488 set_option_field(target, solver, key, value)?;
489 }
490 Ok(())
491}
492
493fn set_option_field(
494 options: &mut StructValue,
495 solver: Solver,
496 name: &str,
497 value: &Value,
498) -> BuiltinResult<()> {
499 let canonical = canonical_option_name(name);
500 if !solver.accepts_option(&canonical) {
501 return Err(optimoptions_error_with(
502 &OPTIMOPTIONS_ERROR_UNKNOWN_OPTION,
503 format!(
504 "optimoptions: option '{}' is not supported for {}",
505 name,
506 solver_label(solver)
507 ),
508 ));
509 }
510
511 let value = normalized_option_value(solver, &canonical, value)?;
512 options.insert(canonical, value);
513 Ok(())
514}
515
516fn normalized_option_value(solver: Solver, canonical: &str, value: &Value) -> BuiltinResult<Value> {
517 match canonical {
518 "TolX" | "TolFun" => Ok(Value::Num(positive_finite_scalar(canonical, value)?)),
519 "MaxIter" | "MaxFunEvals" => {
520 Ok(Value::Num(positive_integer_scalar(canonical, value)? as f64))
521 }
522 "Display" => Ok(Value::from(display_value(solver, value)?)),
523 _ => unreachable!("unsupported option passed accepts_option"),
524 }
525}
526
527fn solver_label(solver: Solver) -> &'static str {
528 match solver {
529 Solver::Generic => "optimization solvers",
530 _ => solver.name(),
531 }
532}
533
534fn positive_finite_scalar(field: &str, value: &Value) -> BuiltinResult<f64> {
535 let parsed = numeric_scalar(field, value)?;
536 if parsed > 0.0 {
537 Ok(parsed)
538 } else {
539 Err(optimoptions_error_with(
540 &OPTIMOPTIONS_ERROR_INVALID_OPTION_VALUE,
541 format!("optimoptions: option {field} must be a finite positive scalar"),
542 ))
543 }
544}
545
546fn positive_integer_scalar(field: &str, value: &Value) -> BuiltinResult<usize> {
547 let parsed = positive_finite_scalar(field, value)?;
548 if parsed.fract() != 0.0 {
549 return Err(optimoptions_error_with(
550 &OPTIMOPTIONS_ERROR_INVALID_OPTION_VALUE,
551 format!("optimoptions: option {field} must be an integer scalar"),
552 ));
553 }
554 if parsed >= 2f64.powi(usize::BITS as i32) {
555 return Err(optimoptions_error_with(
556 &OPTIMOPTIONS_ERROR_INVALID_OPTION_VALUE,
557 format!("optimoptions: option {field} is too large"),
558 ));
559 }
560 Ok(parsed as usize)
561}
562
563fn numeric_scalar(field: &str, value: &Value) -> BuiltinResult<f64> {
564 let parsed = match value {
565 Value::Num(n) => *n,
566 Value::Int(i) => i.to_f64(),
567 Value::Tensor(Tensor { data, .. }) if data.len() == 1 => data[0],
568 Value::LogicalArray(LogicalArray { data, .. }) if data.len() == 1 => {
569 if data[0] == 0 {
570 0.0
571 } else {
572 1.0
573 }
574 }
575 other => {
576 return Err(optimoptions_error_with(
577 &OPTIMOPTIONS_ERROR_INVALID_OPTION_VALUE,
578 format!("optimoptions: option {field} must be a numeric scalar, got {other:?}"),
579 ))
580 }
581 };
582 if parsed.is_finite() {
583 Ok(parsed)
584 } else {
585 Err(optimoptions_error_with(
586 &OPTIMOPTIONS_ERROR_INVALID_OPTION_VALUE,
587 format!("optimoptions: option {field} must be finite"),
588 ))
589 }
590}
591
592fn display_value(solver: Solver, value: &Value) -> BuiltinResult<String> {
593 let display = expect_string_scalar(
594 value,
595 "optimoptions: Display must be a character vector or string scalar",
596 &OPTIMOPTIONS_ERROR_INVALID_OPTION_VALUE,
597 )?
598 .trim()
599 .to_ascii_lowercase();
600 if solver.accepts_display(&display) {
601 Ok(display)
602 } else {
603 Err(optimoptions_error_with(
604 &OPTIMOPTIONS_ERROR_INVALID_OPTION_VALUE,
605 format!(
606 "optimoptions: unsupported Display '{}' for {}",
607 display,
608 solver_label(solver)
609 ),
610 ))
611 }
612}
613
614fn expect_string_scalar(
615 value: &Value,
616 context: &str,
617 error: &'static BuiltinErrorDescriptor,
618) -> BuiltinResult<String> {
619 match value {
620 Value::String(s) => Ok(s.clone()),
621 Value::StringArray(sa) if sa.data.len() == 1 => Ok(sa.data[0].clone()),
622 Value::CharArray(CharArray { data, rows: 1, .. }) => Ok(data.iter().collect()),
623 _ => Err(optimoptions_error_with(error, context)),
624 }
625}
626
627fn lookup_case_insensitive<'a>(options: &'a StructValue, name: &str) -> Option<&'a Value> {
628 options
629 .fields
630 .iter()
631 .find(|(key, _)| key.eq_ignore_ascii_case(name))
632 .map(|(_, value)| value)
633}
634
635#[cfg(test)]
636mod tests {
637 use super::*;
638 use crate::call_builtin_async;
639 use futures::executor::block_on;
640 use runmat_builtins::IntValue;
641
642 fn run_optimoptions(rest: Vec<Value>) -> BuiltinResult<Value> {
643 block_on(optimoptions_builtin(rest))
644 }
645
646 fn run_call_builtin(name: &str, args: &[Value]) -> BuiltinResult<Value> {
647 block_on(call_builtin_async(name, args))
648 }
649
650 fn struct_result(value: Value) -> StructValue {
651 match value {
652 Value::Struct(options) => options,
653 other => panic!("expected struct, got {other:?}"),
654 }
655 }
656
657 fn num_field(options: &StructValue, field: &str) -> f64 {
658 match options.fields.get(field) {
659 Some(Value::Num(value)) => *value,
660 other => panic!("expected numeric field {field}, got {other:?}"),
661 }
662 }
663
664 fn string_field<'a>(options: &'a StructValue, field: &str) -> &'a str {
665 match options.fields.get(field) {
666 Some(Value::String(value)) => value.as_str(),
667 other => panic!("expected string field {field}, got {other:?}"),
668 }
669 }
670
671 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
672 #[test]
673 fn optimoptions_descriptor_signatures_and_errors_cover_core_forms() {
674 let labels: Vec<&str> = OPTIMOPTIONS_DESCRIPTOR
675 .signatures
676 .iter()
677 .map(|signature| signature.label)
678 .collect();
679 assert_eq!(
680 labels,
681 vec![
682 "options = optimoptions(solver)",
683 "options = optimoptions(solver, name, value, ...)",
684 "options = optimoptions(oldopts, name, value, ...)",
685 ]
686 );
687
688 let codes: Vec<&str> = OPTIMOPTIONS_DESCRIPTOR
689 .errors
690 .iter()
691 .map(|error| error.code)
692 .collect();
693 assert_eq!(
694 codes,
695 vec![
696 "RM.OPTIMOPTIONS.INVALID_ARGUMENT",
697 "RM.OPTIMOPTIONS.INVALID_SOLVER",
698 "RM.OPTIMOPTIONS.INVALID_OPTION_NAME",
699 "RM.OPTIMOPTIONS.MISSING_OPTION_VALUE",
700 "RM.OPTIMOPTIONS.UNKNOWN_OPTION",
701 "RM.OPTIMOPTIONS.INVALID_OPTION_VALUE",
702 "RM.OPTIMOPTIONS.FLOW",
703 ]
704 );
705 }
706
707 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
708 #[test]
709 fn optimoptions_fminbnd_defaults_match_solver() {
710 let options = struct_result(
711 run_optimoptions(vec![Value::from("fminbnd")]).expect("optimoptions fminbnd"),
712 );
713 assert_eq!(string_field(&options, "Solver"), "fminbnd");
714 assert_eq!(num_field(&options, "TolX"), 1.0e-4);
715 assert_eq!(num_field(&options, "MaxIter"), 500.0);
716 assert_eq!(num_field(&options, "MaxFunEvals"), 500.0);
717 assert_eq!(string_field(&options, "Display"), "notify");
718 assert!(!options.fields.contains_key("TolFun"));
719 }
720
721 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
722 #[test]
723 fn optimoptions_fzero_defaults_match_solver() {
724 let options = struct_result(
725 run_optimoptions(vec![Value::from("fzero")]).expect("optimoptions fzero"),
726 );
727 assert_eq!(string_field(&options, "Solver"), "fzero");
728 assert_eq!(num_field(&options, "TolX"), 1.0e-6);
729 assert_eq!(num_field(&options, "MaxIter"), 400.0);
730 assert_eq!(num_field(&options, "MaxFunEvals"), 500.0);
731 assert_eq!(string_field(&options, "Display"), "off");
732 assert!(!options.fields.contains_key("TolFun"));
733 }
734
735 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
736 #[test]
737 fn optimoptions_fsolve_defaults_match_solver() {
738 let options = struct_result(
739 run_optimoptions(vec![Value::from("fsolve")]).expect("optimoptions fsolve"),
740 );
741 assert_eq!(string_field(&options, "Solver"), "fsolve");
742 assert_eq!(num_field(&options, "TolX"), 1.0e-6);
743 assert_eq!(num_field(&options, "TolFun"), 1.0e-6);
744 assert_eq!(num_field(&options, "MaxIter"), 400.0);
745 assert_eq!(num_field(&options, "MaxFunEvals"), 40000.0);
746 assert_eq!(string_field(&options, "Display"), "off");
747 }
748
749 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
750 #[test]
751 fn optimoptions_name_value_pairs_are_case_insensitive() {
752 let options = struct_result(
753 run_optimoptions(vec![
754 Value::from("fsolve"),
755 Value::from("tolx"),
756 Value::Num(1.0e-8),
757 Value::from("DISPLAY"),
758 Value::from("Final"),
759 ])
760 .expect("optimoptions overrides"),
761 );
762 assert_eq!(num_field(&options, "TolX"), 1.0e-8);
763 assert_eq!(string_field(&options, "Display"), "final");
764 }
765
766 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
767 #[test]
768 fn optimoptions_updates_existing_options_with_pairs() {
769 let base = run_optimoptions(vec![
770 Value::from("fzero"),
771 Value::from("TolX"),
772 Value::Num(1.0e-5),
773 ])
774 .expect("base options");
775 let options = struct_result(
776 run_optimoptions(vec![base, Value::from("MaxIter"), Value::Num(25.0)])
777 .expect("updated options"),
778 );
779 assert_eq!(string_field(&options, "Solver"), "fzero");
780 assert_eq!(num_field(&options, "TolX"), 1.0e-5);
781 assert_eq!(num_field(&options, "MaxIter"), 25.0);
782 }
783
784 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
785 #[test]
786 fn optimoptions_merges_existing_options_structs() {
787 let first = run_optimoptions(vec![
788 Value::from("fsolve"),
789 Value::from("TolX"),
790 Value::Num(1.0e-5),
791 ])
792 .expect("first");
793 let second = run_optimoptions(vec![
794 Value::from("fsolve"),
795 Value::from("TolX"),
796 Value::Num(1.0e-8),
797 Value::from("MaxIter"),
798 Value::Num(30.0),
799 ])
800 .expect("second");
801 let options = struct_result(run_optimoptions(vec![first, second]).expect("merged options"));
802 assert_eq!(num_field(&options, "TolX"), 1.0e-8);
803 assert_eq!(num_field(&options, "MaxIter"), 30.0);
804 }
805
806 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
807 #[test]
808 fn optimoptions_same_solver_struct_merge_preserves_prior_overrides() {
809 let first = run_optimoptions(vec![
810 Value::from("fsolve"),
811 Value::from("MaxFunEvals"),
812 Value::Num(2000.0),
813 ])
814 .expect("first");
815 let second = run_optimoptions(vec![
816 Value::from("fsolve"),
817 Value::from("TolX"),
818 Value::Num(1.0e-8),
819 ])
820 .expect("second");
821
822 let options = struct_result(run_optimoptions(vec![first, second]).expect("merged options"));
823
824 assert_eq!(string_field(&options, "Solver"), "fsolve");
825 assert_eq!(num_field(&options, "TolX"), 1.0e-8);
826 assert_eq!(num_field(&options, "MaxFunEvals"), 2000.0);
827 }
828
829 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
830 #[test]
831 fn optimoptions_solver_form_same_solver_struct_preserves_prior_overrides() {
832 let later = run_optimoptions(vec![
833 Value::from("fsolve"),
834 Value::from("TolX"),
835 Value::Num(1.0e-8),
836 ])
837 .expect("later options");
838
839 let options = struct_result(
840 run_optimoptions(vec![
841 Value::from("fsolve"),
842 Value::from("MaxFunEvals"),
843 Value::Num(2000.0),
844 later,
845 ])
846 .expect("merged options"),
847 );
848
849 assert_eq!(string_field(&options, "Solver"), "fsolve");
850 assert_eq!(num_field(&options, "TolX"), 1.0e-8);
851 assert_eq!(num_field(&options, "MaxFunEvals"), 2000.0);
852 }
853
854 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
855 #[test]
856 fn optimoptions_default_skipping_compares_normalized_values() {
857 let first = run_optimoptions(vec![
858 Value::from("fsolve"),
859 Value::from("MaxFunEvals"),
860 Value::Num(2000.0),
861 Value::from("Display"),
862 Value::from("final"),
863 ])
864 .expect("first");
865
866 let mut later = StructValue::new();
867 later.insert("Solver", Value::from("fsolve"));
868 later.insert("TolX", Value::Num(1.0e-8));
869 later.insert("MaxFunEvals", Value::Int(IntValue::I32(40000)));
870 later.insert("Display", Value::CharArray(CharArray::new_row("off")));
871
872 let options = struct_result(
873 run_optimoptions(vec![first, Value::Struct(later)]).expect("merged options"),
874 );
875
876 assert_eq!(string_field(&options, "Solver"), "fsolve");
877 assert_eq!(num_field(&options, "TolX"), 1.0e-8);
878 assert_eq!(num_field(&options, "MaxFunEvals"), 2000.0);
879 assert_eq!(string_field(&options, "Display"), "final");
880 }
881
882 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
883 #[test]
884 fn optimoptions_generic_to_concrete_solver_preserves_valid_generic_overrides() {
885 let mut generic = StructValue::new();
886 generic.insert("MaxFunEvals", Value::Num(2000.0));
887 generic.insert("Display", Value::from("final"));
888
889 let later = run_optimoptions(vec![
890 Value::from("fsolve"),
891 Value::from("TolX"),
892 Value::Num(1.0e-8),
893 ])
894 .expect("later options");
895
896 let options = struct_result(
897 run_optimoptions(vec![Value::Struct(generic), later]).expect("merged options"),
898 );
899
900 assert_eq!(string_field(&options, "Solver"), "fsolve");
901 assert_eq!(num_field(&options, "TolX"), 1.0e-8);
902 assert_eq!(num_field(&options, "TolFun"), 1.0e-6);
903 assert_eq!(num_field(&options, "MaxFunEvals"), 2000.0);
904 assert_eq!(string_field(&options, "Display"), "final");
905 }
906
907 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
908 #[test]
909 fn optimoptions_solver_form_keeps_requested_solver_when_struct_has_solver() {
910 let fzero_options = run_optimoptions(vec![
911 Value::from("fzero"),
912 Value::from("TolX"),
913 Value::Num(1.0e-8),
914 Value::from("MaxIter"),
915 Value::Num(30.0),
916 ])
917 .expect("fzero options");
918
919 let options = struct_result(
920 run_optimoptions(vec![Value::from("fsolve"), fzero_options])
921 .expect("merged into fsolve options"),
922 );
923
924 assert_eq!(string_field(&options, "Solver"), "fsolve");
925 assert_eq!(num_field(&options, "TolX"), 1.0e-8);
926 assert_eq!(num_field(&options, "MaxIter"), 30.0);
927 assert_eq!(num_field(&options, "TolFun"), 1.0e-6);
928 assert_eq!(num_field(&options, "MaxFunEvals"), 40000.0);
929 }
930
931 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
932 #[test]
933 fn optimoptions_rejects_unknown_option_names() {
934 let err = run_optimoptions(vec![
935 Value::from("fzero"),
936 Value::from("TolFun"),
937 Value::Num(1.0e-8),
938 ])
939 .expect_err("TolFun is not accepted by fzero");
940 assert_eq!(err.identifier(), Some("RunMat:optimoptions:UnknownOption"));
941 }
942
943 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
944 #[test]
945 fn optimoptions_rejects_missing_option_values() {
946 let err = run_optimoptions(vec![Value::from("fsolve"), Value::from("TolX")])
947 .expect_err("missing option value");
948 assert_eq!(
949 err.identifier(),
950 Some("RunMat:optimoptions:MissingOptionValue")
951 );
952 }
953
954 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
955 #[test]
956 fn optimoptions_rejects_invalid_option_values() {
957 let err = run_optimoptions(vec![
958 Value::from("fsolve"),
959 Value::from("MaxIter"),
960 Value::Num(1.5),
961 ])
962 .expect_err("noninteger MaxIter should fail");
963 assert_eq!(
964 err.identifier(),
965 Some("RunMat:optimoptions:InvalidOptionValue")
966 );
967 }
968
969 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
970 #[test]
971 fn optimoptions_rejects_out_of_range_integer_options() {
972 let err = run_optimoptions(vec![
973 Value::from("fsolve"),
974 Value::from("MaxIter"),
975 Value::Num(2f64.powi(usize::BITS as i32)),
976 ])
977 .expect_err("out-of-range MaxIter should fail");
978 assert_eq!(
979 err.identifier(),
980 Some("RunMat:optimoptions:InvalidOptionValue")
981 );
982 }
983
984 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
985 #[test]
986 fn fminbnd_accepts_optimoptions_output() {
987 let options = run_optimoptions(vec![
988 Value::from("fminbnd"),
989 Value::from("TolX"),
990 Value::Num(1.0e-8),
991 Value::from("Display"),
992 Value::from("off"),
993 ])
994 .expect("optimoptions");
995 let result = run_call_builtin(
996 "fminbnd",
997 &[
998 Value::FunctionHandle("cos".into()),
999 Value::Num(0.0),
1000 Value::Num(std::f64::consts::PI),
1001 options,
1002 ],
1003 )
1004 .expect("fminbnd");
1005 match result {
1006 Value::Num(value) => assert!((value - std::f64::consts::PI).abs() < 1.0e-4),
1007 other => panic!("unexpected fminbnd result {other:?}"),
1008 }
1009 }
1010
1011 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1012 #[test]
1013 fn fzero_accepts_optimoptions_output() {
1014 let options = run_optimoptions(vec![
1015 Value::from("fzero"),
1016 Value::from("TolX"),
1017 Value::Num(1.0e-8),
1018 ])
1019 .expect("optimoptions");
1020 let bracket = Tensor::new(vec![3.0, 4.0], vec![1, 2]).unwrap();
1021 let result = run_call_builtin(
1022 "fzero",
1023 &[
1024 Value::FunctionHandle("sin".into()),
1025 Value::Tensor(bracket),
1026 options,
1027 ],
1028 )
1029 .expect("fzero");
1030 match result {
1031 Value::Num(value) => assert!((value - std::f64::consts::PI).abs() < 1.0e-6),
1032 other => panic!("unexpected fzero result {other:?}"),
1033 }
1034 }
1035
1036 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1037 #[test]
1038 fn fsolve_accepts_optimoptions_output() {
1039 let options = run_optimoptions(vec![
1040 Value::from("fsolve"),
1041 Value::from("TolX"),
1042 Value::Num(1.0e-8),
1043 Value::from("TolFun"),
1044 Value::Num(1.0e-8),
1045 ])
1046 .expect("optimoptions");
1047 let result = run_call_builtin(
1048 "fsolve",
1049 &[
1050 Value::FunctionHandle("sin".into()),
1051 Value::Num(3.0),
1052 options,
1053 ],
1054 )
1055 .expect("fsolve");
1056 match result {
1057 Value::Num(value) => assert!((value - std::f64::consts::PI).abs() < 1.0e-6),
1058 other => panic!("unexpected fsolve result {other:?}"),
1059 }
1060 }
1061}