Skip to main content

runmat_runtime/builtins/math/optim/
optimset.rs

1//! Minimal MATLAB-compatible `optimset` options struct builder.
2
3use runmat_builtins::{
4    BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
5    BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
6};
7use runmat_builtins::{StructValue, Value};
8use runmat_macros::runtime_builtin;
9
10use crate::builtins::common::spec::{
11    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
12    ReductionNaN, ResidencyPolicy, ShapeRequirements,
13};
14use crate::builtins::math::optim::common::{canonical_option_name, field_name};
15use crate::builtins::math::optim::type_resolvers::optim_options_type;
16use crate::{build_runtime_error, BuiltinResult, RuntimeError};
17
18const NAME: &str = "optimset";
19
20const OPTIMSET_OUTPUT_OPTIONS: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
21    name: "options",
22    ty: BuiltinParamType::Any,
23    arity: BuiltinParamArity::Required,
24    default: None,
25    description: "Options struct for optimization solvers.",
26}];
27
28const OPTIMSET_INPUTS_PAIRS: [BuiltinParamDescriptor; 2] = [
29    BuiltinParamDescriptor {
30        name: "name",
31        ty: BuiltinParamType::Any,
32        arity: BuiltinParamArity::Required,
33        default: None,
34        description: "Option field name.",
35    },
36    BuiltinParamDescriptor {
37        name: "value",
38        ty: BuiltinParamType::Any,
39        arity: BuiltinParamArity::Required,
40        default: None,
41        description: "Option value.",
42    },
43];
44
45const OPTIMSET_INPUTS_EXISTING_AND_PAIRS: [BuiltinParamDescriptor; 3] = [
46    BuiltinParamDescriptor {
47        name: "oldopts",
48        ty: BuiltinParamType::Any,
49        arity: BuiltinParamArity::Required,
50        default: None,
51        description: "Existing options struct to update.",
52    },
53    BuiltinParamDescriptor {
54        name: "name",
55        ty: BuiltinParamType::Any,
56        arity: BuiltinParamArity::Optional,
57        default: None,
58        description: "Option field name.",
59    },
60    BuiltinParamDescriptor {
61        name: "value",
62        ty: BuiltinParamType::Any,
63        arity: BuiltinParamArity::Variadic,
64        default: None,
65        description: "Option value(s) and additional name/value pairs.",
66    },
67];
68
69const OPTIMSET_SIGNATURES: [BuiltinSignatureDescriptor; 3] = [
70    BuiltinSignatureDescriptor {
71        label: "options = optimset()",
72        inputs: &[],
73        outputs: &OPTIMSET_OUTPUT_OPTIONS,
74    },
75    BuiltinSignatureDescriptor {
76        label: "options = optimset(name, value, ...)",
77        inputs: &OPTIMSET_INPUTS_PAIRS,
78        outputs: &OPTIMSET_OUTPUT_OPTIONS,
79    },
80    BuiltinSignatureDescriptor {
81        label: "options = optimset(oldopts, name, value, ...)",
82        inputs: &OPTIMSET_INPUTS_EXISTING_AND_PAIRS,
83        outputs: &OPTIMSET_OUTPUT_OPTIONS,
84    },
85];
86
87const OPTIMSET_ERROR_INVALID_ARGUMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
88    code: "RM.OPTIMSET.INVALID_ARGUMENT",
89    identifier: Some("RunMat:optimset:InvalidArgument"),
90    when: "Name/value argument grammar is invalid.",
91    message: "optimset: invalid argument",
92};
93
94const OPTIMSET_ERROR_INVALID_INPUT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
95    code: "RM.OPTIMSET.INVALID_INPUT",
96    identifier: Some("RunMat:optimset:InvalidInput"),
97    when: "Option field names are not valid string scalars.",
98    message: "optimset: invalid input",
99};
100
101const OPTIMSET_ERRORS: [BuiltinErrorDescriptor; 2] = [
102    OPTIMSET_ERROR_INVALID_ARGUMENT,
103    OPTIMSET_ERROR_INVALID_INPUT,
104];
105
106pub const OPTIMSET_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
107    signatures: &OPTIMSET_SIGNATURES,
108    output_mode: BuiltinOutputMode::Fixed,
109    completion_policy: BuiltinCompletionPolicy::Public,
110    errors: &OPTIMSET_ERRORS,
111};
112
113fn optimset_error_with_detail(
114    error: &'static BuiltinErrorDescriptor,
115    detail: impl AsRef<str>,
116) -> RuntimeError {
117    let detail = detail.as_ref();
118    let message = if detail.starts_with("optimset:") {
119        detail.to_string()
120    } else {
121        format!("{}: {detail}", error.message)
122    };
123    let mut builder = build_runtime_error(message).with_builtin(NAME);
124    if let Some(identifier) = error.identifier {
125        builder = builder.with_identifier(identifier);
126    }
127    builder.build()
128}
129
130#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::math::optim::optimset")]
131pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
132    name: "optimset",
133    op_kind: GpuOpKind::Custom("options"),
134    supported_precisions: &[],
135    broadcast: BroadcastSemantics::None,
136    provider_hooks: &[],
137    constant_strategy: ConstantStrategy::InlineLiteral,
138    residency: ResidencyPolicy::InheritInputs,
139    nan_mode: ReductionNaN::Include,
140    two_pass_threshold: None,
141    workgroup_size: None,
142    accepts_nan_mode: false,
143    notes: "Host metadata construction. GPU values used as option payloads are preserved without gathering.",
144};
145
146#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::math::optim::optimset")]
147pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
148    name: "optimset",
149    shape: ShapeRequirements::Any,
150    constant_strategy: ConstantStrategy::InlineLiteral,
151    elementwise: None,
152    reduction: None,
153    emits_nan: false,
154    notes: "Option struct construction is host metadata work and does not fuse.",
155};
156
157#[runtime_builtin(
158    name = "optimset",
159    category = "math/optim",
160    summary = "Create or update optimization options structures.",
161    keywords = "optimset,options,TolX,TolFun,MaxIter,Display",
162    type_resolver(optim_options_type),
163    descriptor(crate::builtins::math::optim::optimset::OPTIMSET_DESCRIPTOR),
164    builtin_path = "crate::builtins::math::optim::optimset"
165)]
166async fn optimset_builtin(rest: Vec<Value>) -> BuiltinResult<Value> {
167    let mut fields = StructValue::new();
168    let mut args = rest.into_iter();
169
170    if let Some(first) = args.next() {
171        match first {
172            Value::Struct(existing) => fields = existing,
173            other => {
174                let second = args.next().ok_or_else(|| {
175                    optimset_error_with_detail(
176                        &OPTIMSET_ERROR_INVALID_ARGUMENT,
177                        "expected option name/value pairs",
178                    )
179                })?;
180                let name = field_name(&other).map_err(|err| {
181                    optimset_error_with_detail(&OPTIMSET_ERROR_INVALID_INPUT, err.message())
182                })?;
183                fields.insert(canonical_option_name(&name), second);
184            }
185        }
186    }
187
188    let remaining = args.collect::<Vec<_>>();
189    if remaining.len() % 2 != 0 {
190        return Err(optimset_error_with_detail(
191            &OPTIMSET_ERROR_INVALID_ARGUMENT,
192            "expected option name/value pairs",
193        ));
194    }
195    for pair in remaining.chunks(2) {
196        let name = field_name(&pair[0]).map_err(|err| {
197            optimset_error_with_detail(&OPTIMSET_ERROR_INVALID_INPUT, err.message())
198        })?;
199        fields.insert(canonical_option_name(&name), pair[1].clone());
200    }
201
202    Ok(Value::Struct(fields))
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208    use futures::executor::block_on;
209
210    #[test]
211    fn optimset_builds_struct_from_pairs() {
212        let value = block_on(optimset_builtin(vec![
213            Value::from("TolX"),
214            Value::Num(1.0e-8),
215            Value::from("Display"),
216            Value::from("off"),
217        ]))
218        .unwrap();
219        match value {
220            Value::Struct(options) => {
221                assert!(matches!(options.fields.get("TolX"), Some(Value::Num(_))));
222                assert!(matches!(
223                    options.fields.get("Display"),
224                    Some(Value::String(_))
225                ));
226            }
227            other => panic!("unexpected value {other:?}"),
228        }
229    }
230
231    #[test]
232    fn optimset_descriptor_signatures_cover_core_forms() {
233        let labels: Vec<&str> = OPTIMSET_DESCRIPTOR
234            .signatures
235            .iter()
236            .map(|signature| signature.label)
237            .collect();
238        assert_eq!(
239            labels,
240            vec![
241                "options = optimset()",
242                "options = optimset(name, value, ...)",
243                "options = optimset(oldopts, name, value, ...)",
244            ]
245        );
246
247        let codes: Vec<&str> = OPTIMSET_DESCRIPTOR
248            .errors
249            .iter()
250            .map(|error| error.code)
251            .collect();
252        assert_eq!(
253            codes,
254            vec!["RM.OPTIMSET.INVALID_ARGUMENT", "RM.OPTIMSET.INVALID_INPUT"]
255        );
256    }
257
258    #[test]
259    fn optimset_odd_name_value_pairs_use_stable_identifier() {
260        let err = block_on(optimset_builtin(vec![Value::from("TolX")])).unwrap_err();
261        assert_eq!(err.identifier(), Some("RunMat:optimset:InvalidArgument"));
262    }
263}