1use runmat_builtins::{
4 BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinExtensionDescriptor,
5 BuiltinExtensionMode, BuiltinIntegerBackendRule, BuiltinIntegerCapabilityDescriptor,
6 BuiltinIntegerComputationDomain, BuiltinIntegerInputAvailability,
7 BuiltinIntegerInputCapability, BuiltinIntegerOutputClassRule, BuiltinIntegerOverflowRule,
8 BuiltinIntegerOverloadKind, BuiltinIntegerScalarDoubleRule, BuiltinOutputMode,
9 BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
10};
11use runmat_macros::runtime_builtin;
12use runmat_value::{StructValue, Value};
13
14use crate::builtins::common::spec::{
15 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
16 ReductionNaN, ResidencyPolicy, ShapeRequirements,
17};
18use crate::builtins::math::optim::common::{canonical_option_name, field_name};
19use crate::builtins::math::optim::type_resolvers::optim_options_type;
20use crate::{build_runtime_error, BuiltinResult, RuntimeError};
21
22const NAME: &str = "optimset";
23
24const INTEGER_OPTION_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
25 id: "optimset-integer-option",
26 mode: BuiltinExtensionMode::RunMatOnly,
27 description: "optimset with native-class integer option payloads is a RunMat extension",
28 error_identifier: Some("RunMat:compatibility:OptimsetIntegerOptionExtension"),
29};
30const RESIDENT_OPTION_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
31 id: "optimset-resident-option",
32 mode: BuiltinExtensionMode::RunMatOnly,
33 description: "optimset preserving explicit gpuArray option payloads is a RunMat extension",
34 error_identifier: Some("RunMat:compatibility:OptimsetResidentOptionExtension"),
35};
36pub const EXTENSIONS: [BuiltinExtensionDescriptor; 2] =
37 [INTEGER_OPTION_EXTENSION, RESIDENT_OPTION_EXTENSION];
38
39const INTEGER_OPTION_INPUT: [BuiltinIntegerInputCapability; 1] = [BuiltinIntegerInputCapability {
40 name: "option value",
41 classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
42 availability: BuiltinIntegerInputAvailability::RunMatOnly,
43 scalar_double: BuiltinIntegerScalarDoubleRule::NotApplicable,
44 notes: "Documented numeric optimset options use single or double; RunMat can preserve exact native integer payloads in its struct extension.",
45}];
46pub const INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 1] =
47 [BuiltinIntegerCapabilityDescriptor { form: "options = optimset(___, name, integer_value, ___)", inputs: &INTEGER_OPTION_INPUT, computation_domain: BuiltinIntegerComputationDomain::Structural, output_class: BuiltinIntegerOutputClassRule::PreserveInput, overflow: BuiltinIntegerOverflowRule::NotApplicable, backend: BuiltinIntegerBackendRule::GatherFallback, overload: BuiltinIntegerOverloadKind::StructuralParameter, notes: "Typed integer payloads are gated and retained exactly in the RunMat options struct; automatic resident payloads gather while explicit resident preservation is separately gated." }];
48
49const OPTIMSET_OUTPUT_OPTIONS: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
50 name: "options",
51 ty: BuiltinParamType::Any,
52 arity: BuiltinParamArity::Required,
53 default: None,
54 description: "Options struct for optimization solvers.",
55}];
56
57const OPTIMSET_INPUTS_PAIRS: [BuiltinParamDescriptor; 2] = [
58 BuiltinParamDescriptor {
59 name: "name",
60 ty: BuiltinParamType::Any,
61 arity: BuiltinParamArity::Required,
62 default: None,
63 description: "Option field name.",
64 },
65 BuiltinParamDescriptor {
66 name: "value",
67 ty: BuiltinParamType::Any,
68 arity: BuiltinParamArity::Required,
69 default: None,
70 description: "Option value.",
71 },
72];
73
74const OPTIMSET_INPUTS_EXISTING_AND_PAIRS: [BuiltinParamDescriptor; 3] = [
75 BuiltinParamDescriptor {
76 name: "oldopts",
77 ty: BuiltinParamType::Any,
78 arity: BuiltinParamArity::Required,
79 default: None,
80 description: "Existing options struct to update.",
81 },
82 BuiltinParamDescriptor {
83 name: "name",
84 ty: BuiltinParamType::Any,
85 arity: BuiltinParamArity::Optional,
86 default: None,
87 description: "Option field name.",
88 },
89 BuiltinParamDescriptor {
90 name: "value",
91 ty: BuiltinParamType::Any,
92 arity: BuiltinParamArity::Variadic,
93 default: None,
94 description: "Option value(s) and additional name/value pairs.",
95 },
96];
97
98const OPTIMSET_SIGNATURES: [BuiltinSignatureDescriptor; 3] = [
99 BuiltinSignatureDescriptor {
100 label: "options = optimset()",
101 inputs: &[],
102 outputs: &OPTIMSET_OUTPUT_OPTIONS,
103 },
104 BuiltinSignatureDescriptor {
105 label: "options = optimset(name, value, ...)",
106 inputs: &OPTIMSET_INPUTS_PAIRS,
107 outputs: &OPTIMSET_OUTPUT_OPTIONS,
108 },
109 BuiltinSignatureDescriptor {
110 label: "options = optimset(oldopts, name, value, ...)",
111 inputs: &OPTIMSET_INPUTS_EXISTING_AND_PAIRS,
112 outputs: &OPTIMSET_OUTPUT_OPTIONS,
113 },
114];
115
116const OPTIMSET_ERROR_INVALID_ARGUMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
117 code: "RM.OPTIMSET.INVALID_ARGUMENT",
118 identifier: Some("RunMat:optimset:InvalidArgument"),
119 when: "Name/value argument grammar is invalid.",
120 message: "optimset: invalid argument",
121};
122
123const OPTIMSET_ERROR_INVALID_INPUT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
124 code: "RM.OPTIMSET.INVALID_INPUT",
125 identifier: Some("RunMat:optimset:InvalidInput"),
126 when: "Option field names are not valid string scalars.",
127 message: "optimset: invalid input",
128};
129
130const OPTIMSET_ERRORS: [BuiltinErrorDescriptor; 2] = [
131 OPTIMSET_ERROR_INVALID_ARGUMENT,
132 OPTIMSET_ERROR_INVALID_INPUT,
133];
134
135pub const OPTIMSET_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
136 signatures: &OPTIMSET_SIGNATURES,
137 output_mode: BuiltinOutputMode::Fixed,
138 completion_policy: BuiltinCompletionPolicy::Public,
139 errors: &OPTIMSET_ERRORS,
140};
141
142fn optimset_error_with_detail(
143 error: &'static BuiltinErrorDescriptor,
144 detail: impl AsRef<str>,
145) -> RuntimeError {
146 let detail = detail.as_ref();
147 let message = if detail.starts_with("optimset:") {
148 detail.to_string()
149 } else {
150 format!("{}: {detail}", error.message)
151 };
152 let mut builder = build_runtime_error(message).with_builtin(NAME);
153 if let Some(identifier) = error.identifier {
154 builder = builder.with_identifier(identifier);
155 }
156 builder.build()
157}
158
159#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::math::optim::optimset")]
160pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
161 name: "optimset",
162 op_kind: GpuOpKind::Custom("options"),
163 supported_precisions: &[],
164 broadcast: BroadcastSemantics::None,
165 provider_hooks: &[],
166 constant_strategy: ConstantStrategy::InlineLiteral,
167 residency: ResidencyPolicy::InheritInputs,
168 nan_mode: ReductionNaN::Include,
169 two_pass_threshold: None,
170 workgroup_size: None,
171 accepts_nan_mode: false,
172 notes: "Host metadata construction. Automatic resident option payloads gather transparently; explicitly resident payload preservation is a gated RunMat extension.",
173};
174
175#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::math::optim::optimset")]
176pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
177 name: "optimset",
178 shape: ShapeRequirements::Any,
179 constant_strategy: ConstantStrategy::InlineLiteral,
180 elementwise: None,
181 reduction: None,
182 emits_nan: false,
183 notes: "Option struct construction is host metadata work and does not fuse.",
184};
185
186#[runtime_builtin(
187 name = "optimset",
188 category = "math/optim",
189 summary = "Create or update optimization options structures.",
190 keywords = "optimset,options,TolX,TolFun,MaxIter,Display",
191 type_resolver(optim_options_type),
192 descriptor(crate::builtins::math::optim::optimset::OPTIMSET_DESCRIPTOR),
193 extensions(crate::builtins::math::optim::optimset::EXTENSIONS),
194 integer_capabilities(crate::builtins::math::optim::optimset::INTEGER_CAPABILITIES),
195 builtin_path = "crate::builtins::math::optim::optimset"
196)]
197async fn optimset_builtin(rest: Vec<Value>) -> BuiltinResult<Value> {
198 ensure_optimset_extensions(&rest)?;
199 let mut fields = StructValue::new();
200 let mut args = rest.into_iter();
201
202 if let Some(first) = args.next() {
203 match first {
204 Value::Struct(existing) => {
205 for (name, value) in existing.fields {
206 fields.insert(name, prepare_optimset_payload(value).await?);
207 }
208 }
209 other => {
210 let second = args.next().ok_or_else(|| {
211 optimset_error_with_detail(
212 &OPTIMSET_ERROR_INVALID_ARGUMENT,
213 "expected option name/value pairs",
214 )
215 })?;
216 let name = field_name(&other).map_err(|err| {
217 optimset_error_with_detail(&OPTIMSET_ERROR_INVALID_INPUT, err.message())
218 })?;
219 fields.insert(
220 canonical_option_name(&name),
221 prepare_optimset_payload(second).await?,
222 );
223 }
224 }
225 }
226
227 let remaining = args.collect::<Vec<_>>();
228 if remaining.len() % 2 != 0 {
229 return Err(optimset_error_with_detail(
230 &OPTIMSET_ERROR_INVALID_ARGUMENT,
231 "expected option name/value pairs",
232 ));
233 }
234 for pair in remaining.chunks(2) {
235 let name = field_name(&pair[0]).map_err(|err| {
236 optimset_error_with_detail(&OPTIMSET_ERROR_INVALID_INPUT, err.message())
237 })?;
238 fields.insert(
239 canonical_option_name(&name),
240 prepare_optimset_payload(pair[1].clone()).await?,
241 );
242 }
243
244 Ok(Value::Struct(fields))
245}
246
247fn ensure_optimset_extensions(args: &[Value]) -> BuiltinResult<()> {
248 let mut payloads = Vec::new();
249 let mut pair_start = 0usize;
250 if let Some(Value::Struct(existing)) = args.first() {
251 payloads.extend(existing.fields.values());
252 pair_start = 1;
253 }
254 let mut index = pair_start + 1;
255 while index < args.len() {
256 payloads.push(&args[index]);
257 index += 2;
258 }
259 let integer = payloads.iter().any(|value| {
260 crate::builtins::common::validation::value_contains_native_integer_class(value)
261 });
262 if integer {
263 crate::compatibility::ensure_builtin_extension_enabled(&INTEGER_OPTION_EXTENSION, NAME)?;
264 }
265 let explicit = payloads
266 .iter()
267 .any(|value| crate::builtins::common::validation::value_contains_explicit_gpu(value));
268 if explicit {
269 crate::compatibility::ensure_builtin_extension_enabled(&RESIDENT_OPTION_EXTENSION, NAME)?;
270 }
271 Ok(())
272}
273
274async fn prepare_optimset_payload(value: Value) -> BuiltinResult<Value> {
275 if crate::builtins::common::validation::value_contains_explicit_gpu(&value) {
276 return Ok(value);
277 }
278 crate::dispatcher::gather_if_needed_async(&value)
279 .await
280 .map_err(|error| {
281 optimset_error_with_detail(
282 &OPTIMSET_ERROR_INVALID_INPUT,
283 format!("failed to gather option value: {error}"),
284 )
285 })
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291 use crate::builtins::common::test_support;
292 use futures::executor::block_on;
293 use runmat_accelerate_api::HostTensorView;
294 use runmat_value::{IntegerStorage, Tensor};
295
296 #[test]
297 fn optimset_builds_struct_from_pairs() {
298 let value = block_on(optimset_builtin(vec![
299 Value::from("TolX"),
300 Value::Num(1.0e-8),
301 Value::from("Display"),
302 Value::from("off"),
303 ]))
304 .unwrap();
305 match value {
306 Value::Struct(options) => {
307 assert!(matches!(options.fields.get("TolX"), Some(Value::Num(_))));
308 assert!(matches!(
309 options.fields.get("Display"),
310 Some(Value::String(_))
311 ));
312 }
313 other => panic!("unexpected value {other:?}"),
314 }
315 }
316
317 #[test]
318 fn optimset_descriptor_signatures_cover_core_forms() {
319 let labels: Vec<&str> = OPTIMSET_DESCRIPTOR
320 .signatures
321 .iter()
322 .map(|signature| signature.label)
323 .collect();
324 assert_eq!(
325 labels,
326 vec![
327 "options = optimset()",
328 "options = optimset(name, value, ...)",
329 "options = optimset(oldopts, name, value, ...)",
330 ]
331 );
332
333 let codes: Vec<&str> = OPTIMSET_DESCRIPTOR
334 .errors
335 .iter()
336 .map(|error| error.code)
337 .collect();
338 assert_eq!(
339 codes,
340 vec!["RM.OPTIMSET.INVALID_ARGUMENT", "RM.OPTIMSET.INVALID_INPUT"]
341 );
342 }
343
344 #[test]
345 fn optimset_odd_name_value_pairs_use_stable_identifier() {
346 let err = block_on(optimset_builtin(vec![Value::from("TolX")])).unwrap_err();
347 assert_eq!(err.identifier(), Some("RunMat:optimset:InvalidArgument"));
348 }
349
350 #[test]
351 fn optimset_strict_mode_rejects_integer_option_payload() {
352 let _strict = crate::compatibility::push_runmat_extensions_enabled(false);
353 let value = Tensor::new_integer(IntegerStorage::I64(vec![17]), vec![1, 1]).unwrap();
354
355 let error = block_on(optimset_builtin(vec![
356 Value::from("MaxIter"),
357 Value::Tensor(value),
358 ]))
359 .expect_err("integer payload is a RunMat-only extension");
360
361 assert_eq!(
362 error.identifier(),
363 INTEGER_OPTION_EXTENSION.error_identifier
364 );
365 }
366
367 #[test]
368 fn optimset_runmat_mode_preserves_wide_integer_payload_exactly() {
369 let _extensions = crate::compatibility::push_runmat_extensions_enabled(true);
370 let expected = u64::MAX;
371 let value = Tensor::new_integer(IntegerStorage::U64(vec![expected]), vec![1, 1]).unwrap();
372
373 let result = block_on(optimset_builtin(vec![
374 Value::from("MaxIter"),
375 Value::Tensor(value),
376 ]))
377 .expect("RunMat integer payload");
378
379 let Value::Struct(options) = result else {
380 panic!("expected options struct")
381 };
382 let Some(Value::Tensor(stored)) = options.fields.get("MaxIter") else {
383 panic!("expected exact MaxIter tensor")
384 };
385 assert!(matches!(
386 stored.numeric_value_at(0),
387 Some(runmat_value::NumericScalar::U64(value)) if value == expected
388 ));
389 }
390
391 #[test]
392 fn optimset_automatic_resident_payload_gathers_but_explicit_payload_is_gated() {
393 test_support::with_test_provider(|provider| {
394 let values = [3.5];
395 let shape = [1, 1];
396 let automatic = provider
397 .upload(&HostTensorView {
398 data: &values,
399 shape: &shape,
400 })
401 .expect("automatic upload");
402 let automatic =
403 automatic.with_provenance(runmat_accelerate_api::GpuHandleProvenance::Automatic);
404
405 let automatic_result = block_on(optimset_builtin(vec![
406 Value::from("TolX"),
407 Value::GpuTensor(automatic),
408 ]))
409 .expect("automatic residency gathers");
410 assert!(matches!(
411 automatic_result,
412 Value::Struct(ref options)
413 if matches!(options.fields.get("TolX"), Some(Value::Tensor(tensor)) if tensor.materialize_f64() == [3.5])
414 ));
415
416 let explicit = provider
417 .upload(&HostTensorView {
418 data: &values,
419 shape: &shape,
420 })
421 .expect("explicit upload");
422 let explicit =
423 explicit.with_provenance(runmat_accelerate_api::GpuHandleProvenance::Explicit);
424 let strict = crate::compatibility::push_runmat_extensions_enabled(false);
425 let error = block_on(optimset_builtin(vec![
426 Value::from("TolX"),
427 Value::GpuTensor(explicit.clone()),
428 ]))
429 .expect_err("explicit payload must be gated");
430 assert_eq!(
431 error.identifier(),
432 RESIDENT_OPTION_EXTENSION.error_identifier
433 );
434 drop(strict);
435
436 let _extensions = crate::compatibility::push_runmat_extensions_enabled(true);
437 let mixed_automatic = provider
438 .upload(&HostTensorView {
439 data: &values,
440 shape: &shape,
441 })
442 .expect("mixed automatic upload");
443 let mixed_automatic = mixed_automatic
444 .with_provenance(runmat_accelerate_api::GpuHandleProvenance::Automatic);
445 let explicit_result = block_on(optimset_builtin(vec![
446 Value::from("TolX"),
447 Value::GpuTensor(explicit),
448 Value::from("MaxFunEvals"),
449 Value::GpuTensor(mixed_automatic),
450 ]))
451 .expect("RunMat preserves explicit and gathers automatic payloads independently");
452 assert!(matches!(
453 explicit_result,
454 Value::Struct(ref options)
455 if matches!(options.fields.get("TolX"), Some(Value::GpuTensor(_)))
456 && matches!(options.fields.get("MaxFunEvals"), Some(Value::Tensor(tensor)) if tensor.materialize_f64() == [3.5])
457 ));
458 });
459 }
460}