runmat_runtime/builtins/math/optim/
optimset.rs1use 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}