1use std::collections::{BTreeMap, HashSet};
4
5use runmat_builtins::{
6 BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
7 BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
8 CellArray, CharArray, LogicalArray, ObjectInstance, ResolveContext, StringArray, Tensor, Type,
9 Value,
10};
11use runmat_macros::runtime_builtin;
12
13use crate::builtins::common::spec::{
14 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
15 ReductionNaN, ResidencyPolicy, ShapeRequirements,
16};
17use crate::{build_runtime_error, gather_if_needed_async, BuiltinResult, RuntimeError};
18
19const NAME: &str = "optimizableVariable";
20const CLASS_NAME: &str = "optimizableVariable";
21
22const OUTPUT_VAR: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
23 name: "variable",
24 ty: BuiltinParamType::Any,
25 arity: BuiltinParamArity::Required,
26 default: None,
27 description: "Bayesian optimization variable metadata object.",
28}];
29
30const INPUTS_BASE: [BuiltinParamDescriptor; 2] = [
31 BuiltinParamDescriptor {
32 name: "Name",
33 ty: BuiltinParamType::StringScalar,
34 arity: BuiltinParamArity::Required,
35 default: None,
36 description: "Variable name.",
37 },
38 BuiltinParamDescriptor {
39 name: "Range",
40 ty: BuiltinParamType::Any,
41 arity: BuiltinParamArity::Required,
42 default: None,
43 description: "Numeric two-element bounds or categorical choices.",
44 },
45];
46
47const INPUTS_OPTIONS: [BuiltinParamDescriptor; 3] = [
48 BuiltinParamDescriptor {
49 name: "Name",
50 ty: BuiltinParamType::StringScalar,
51 arity: BuiltinParamArity::Required,
52 default: None,
53 description: "Variable name.",
54 },
55 BuiltinParamDescriptor {
56 name: "Range",
57 ty: BuiltinParamType::Any,
58 arity: BuiltinParamArity::Required,
59 default: None,
60 description: "Numeric two-element bounds or categorical choices.",
61 },
62 BuiltinParamDescriptor {
63 name: "NameValue",
64 ty: BuiltinParamType::Any,
65 arity: BuiltinParamArity::Variadic,
66 default: None,
67 description: "Name-value options Type, Transform, and Optimize.",
68 },
69];
70
71const SIGNATURES: [BuiltinSignatureDescriptor; 2] = [
72 BuiltinSignatureDescriptor {
73 label: "v = optimizableVariable(Name, Range)",
74 inputs: &INPUTS_BASE,
75 outputs: &OUTPUT_VAR,
76 },
77 BuiltinSignatureDescriptor {
78 label: "v = optimizableVariable(Name, Range, Name, Value)",
79 inputs: &INPUTS_OPTIONS,
80 outputs: &OUTPUT_VAR,
81 },
82];
83
84const ERROR_INVALID_ARGUMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
85 code: "RM.OPTIMIZABLE_VARIABLE.INVALID_ARGUMENT",
86 identifier: Some("RunMat:optimizableVariable:InvalidArgument"),
87 when: "The variable name, range, or name-value arguments are malformed.",
88 message: "optimizableVariable: invalid argument",
89};
90
91const ERROR_INVALID_RANGE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
92 code: "RM.OPTIMIZABLE_VARIABLE.INVALID_RANGE",
93 identifier: Some("RunMat:optimizableVariable:InvalidRange"),
94 when: "The supplied range is incompatible with the requested variable type.",
95 message: "optimizableVariable: invalid range",
96};
97
98const ERROR_INVALID_OPTION: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
99 code: "RM.OPTIMIZABLE_VARIABLE.INVALID_OPTION",
100 identifier: Some("RunMat:optimizableVariable:InvalidOption"),
101 when: "A name-value option is unknown or has an unsupported value.",
102 message: "optimizableVariable: invalid option",
103};
104
105const ERROR_FLOW: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
106 code: "RM.OPTIMIZABLE_VARIABLE.FLOW",
107 identifier: Some("RunMat:optimizableVariable:Flow"),
108 when: "Gathering a metadata input fails.",
109 message: "optimizableVariable: flow failure",
110};
111
112const ERRORS: [BuiltinErrorDescriptor; 4] = [
113 ERROR_INVALID_ARGUMENT,
114 ERROR_INVALID_RANGE,
115 ERROR_INVALID_OPTION,
116 ERROR_FLOW,
117];
118
119pub const OPTIMIZABLE_VARIABLE_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
120 signatures: &SIGNATURES,
121 output_mode: BuiltinOutputMode::Fixed,
122 completion_policy: BuiltinCompletionPolicy::Public,
123 errors: &ERRORS,
124};
125
126#[runmat_macros::register_gpu_spec(
127 builtin_path = "crate::builtins::stats::ml::optimizable_variable"
128)]
129pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
130 name: NAME,
131 op_kind: GpuOpKind::Custom("optimizable-variable-metadata"),
132 supported_precisions: &[],
133 broadcast: BroadcastSemantics::None,
134 provider_hooks: &[],
135 constant_strategy: ConstantStrategy::InlineLiteral,
136 residency: ResidencyPolicy::GatherImmediately,
137 nan_mode: ReductionNaN::Include,
138 two_pass_threshold: None,
139 workgroup_size: None,
140 accepts_nan_mode: false,
141 notes: "Host metadata construction for bayesopt; gpuArray range inputs are gathered before validation.",
142};
143
144#[runmat_macros::register_fusion_spec(
145 builtin_path = "crate::builtins::stats::ml::optimizable_variable"
146)]
147pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
148 name: NAME,
149 shape: ShapeRequirements::Any,
150 constant_strategy: ConstantStrategy::InlineLiteral,
151 elementwise: None,
152 reduction: None,
153 emits_nan: false,
154 notes: "optimizableVariable constructs host metadata and terminates fusion planning.",
155};
156
157#[runtime_builtin(
158 name = "optimizableVariable",
159 category = "stats/ml",
160 summary = "Create Bayesian optimization variable metadata.",
161 keywords = "optimizableVariable,bayesopt,optimization,variable,Type,Transform,Optimize",
162 accel = "cpu",
163 type_resolver(optimizable_variable_type),
164 descriptor(crate::builtins::stats::ml::optimizable_variable::OPTIMIZABLE_VARIABLE_DESCRIPTOR),
165 builtin_path = "crate::builtins::stats::ml::optimizable_variable"
166)]
167async fn optimizable_variable_builtin(
168 name: Value,
169 range: Value,
170 rest: Vec<Value>,
171) -> BuiltinResult<Value> {
172 let name = gather_if_needed_async(&name)
173 .await
174 .map_err(|err| remap_flow(err, "name"))?;
175 let range = gather_if_needed_async(&range)
176 .await
177 .map_err(|err| remap_flow(err, "range"))?;
178 let mut gathered = Vec::with_capacity(rest.len());
179 for value in rest {
180 gathered.push(
181 gather_if_needed_async(&value)
182 .await
183 .map_err(|err| remap_flow(err, "name-value argument"))?,
184 );
185 }
186
187 let variable_name = parse_text_scalar(&name, "Name", &ERROR_INVALID_ARGUMENT)?;
188 if variable_name.trim().is_empty() {
189 return Err(error(
190 "optimizableVariable: Name must be a nonempty text scalar",
191 &ERROR_INVALID_ARGUMENT,
192 ));
193 }
194 let options = Options::parse(gathered)?;
195 let range = OptimizableRange::parse(range, options.var_type)?;
196 let var_type = options.var_type.unwrap_or(range.inferred_type());
197 range.validate_for(var_type, options.transform)?;
198
199 let mut object = ObjectInstance::new(CLASS_NAME.to_string());
200 object
201 .properties
202 .insert("Name".to_string(), Value::String(variable_name));
203 object
204 .properties
205 .insert("Range".to_string(), range.value().clone());
206 object.properties.insert(
207 "Type".to_string(),
208 Value::String(var_type.as_str().to_string()),
209 );
210 object.properties.insert(
211 "Transform".to_string(),
212 Value::String(options.transform.as_str().to_string()),
213 );
214 object
215 .properties
216 .insert("Optimize".to_string(), Value::Bool(options.optimize));
217 Ok(Value::Object(object))
218}
219
220fn optimizable_variable_type(_args: &[Type], _ctx: &ResolveContext) -> Type {
221 Type::Unknown
222}
223
224#[derive(Debug, Clone, Copy, PartialEq, Eq)]
225enum VariableType {
226 Real,
227 Integer,
228 Categorical,
229}
230
231impl VariableType {
232 fn parse(value: &Value) -> BuiltinResult<Self> {
233 match parse_text_scalar(value, "Type", &ERROR_INVALID_OPTION)?
234 .trim()
235 .to_ascii_lowercase()
236 .as_str()
237 {
238 "real" => Ok(Self::Real),
239 "integer" => Ok(Self::Integer),
240 "categorical" => Ok(Self::Categorical),
241 other => Err(error(
242 format!("optimizableVariable: unsupported Type '{other}'"),
243 &ERROR_INVALID_OPTION,
244 )),
245 }
246 }
247
248 fn as_str(self) -> &'static str {
249 match self {
250 Self::Real => "real",
251 Self::Integer => "integer",
252 Self::Categorical => "categorical",
253 }
254 }
255}
256
257#[derive(Debug, Clone, Copy, PartialEq, Eq)]
258enum Transform {
259 None,
260 Log,
261}
262
263impl Transform {
264 fn parse(value: &Value) -> BuiltinResult<Self> {
265 match parse_text_scalar(value, "Transform", &ERROR_INVALID_OPTION)?
266 .trim()
267 .to_ascii_lowercase()
268 .as_str()
269 {
270 "none" => Ok(Self::None),
271 "log" => Ok(Self::Log),
272 other => Err(error(
273 format!("optimizableVariable: unsupported Transform '{other}'"),
274 &ERROR_INVALID_OPTION,
275 )),
276 }
277 }
278
279 fn as_str(self) -> &'static str {
280 match self {
281 Self::None => "none",
282 Self::Log => "log",
283 }
284 }
285}
286
287#[derive(Debug)]
288struct Options {
289 var_type: Option<VariableType>,
290 transform: Transform,
291 optimize: bool,
292}
293
294impl Options {
295 fn parse(args: Vec<Value>) -> BuiltinResult<Self> {
296 if !args.len().is_multiple_of(2) {
297 return Err(error(
298 "optimizableVariable: name-value options must be paired",
299 &ERROR_INVALID_ARGUMENT,
300 ));
301 }
302 let mut options = Self {
303 var_type: None,
304 transform: Transform::None,
305 optimize: true,
306 };
307 let mut pairs = BTreeMap::new();
308 for pair in args.chunks_exact(2) {
309 let name = parse_text_scalar(&pair[0], "option name", &ERROR_INVALID_OPTION)?
310 .trim()
311 .to_ascii_lowercase();
312 pairs.insert(name, pair[1].clone());
313 }
314 for (name, value) in pairs {
315 match name.as_str() {
316 "type" => options.var_type = Some(VariableType::parse(&value)?),
317 "transform" => options.transform = Transform::parse(&value)?,
318 "optimize" => options.optimize = parse_bool(&value, "Optimize")?,
319 other => {
320 return Err(error(
321 format!("optimizableVariable: unknown option '{other}'"),
322 &ERROR_INVALID_OPTION,
323 ));
324 }
325 }
326 }
327 Ok(options)
328 }
329}
330
331#[derive(Debug, Clone)]
332enum OptimizableRange {
333 Numeric {
334 value: Value,
335 lower: f64,
336 upper: f64,
337 },
338 Categorical {
339 value: Value,
340 categories: Vec<String>,
341 },
342}
343
344impl OptimizableRange {
345 fn parse(value: Value, explicit_type: Option<VariableType>) -> BuiltinResult<Self> {
346 match value {
347 Value::Tensor(tensor) => {
348 Self::numeric_from_values(Value::Tensor(tensor.clone()), &tensor.data)
349 }
350 Value::Num(n) => Self::numeric_from_values(
351 Value::Tensor(Tensor::new(vec![n], vec![1, 1]).unwrap()),
352 &[n],
353 ),
354 Value::Int(i) => {
355 let n = i.to_f64();
356 Self::numeric_from_values(
357 Value::Tensor(Tensor::new(vec![n], vec![1, 1]).unwrap()),
358 &[n],
359 )
360 }
361 Value::StringArray(array) => {
362 let categories = text_categories_from_string_array(&array)?;
363 Ok(Self::Categorical {
364 value: Value::StringArray(array),
365 categories,
366 })
367 }
368 Value::String(text) if explicit_type == Some(VariableType::Categorical) => {
369 check_categories(std::slice::from_ref(&text))?;
370 Ok(Self::Categorical {
371 value: Value::String(text.clone()),
372 categories: vec![text],
373 })
374 }
375 Value::Cell(cell) => {
376 let categories = text_categories_from_cell(&cell)?;
377 Ok(Self::Categorical {
378 value: Value::Cell(cell),
379 categories,
380 })
381 }
382 Value::CharArray(chars) if explicit_type == Some(VariableType::Categorical) => {
383 let category = char_row_to_string(&chars, "Range", &ERROR_INVALID_RANGE)?;
384 check_categories(std::slice::from_ref(&category))?;
385 let value = Value::Cell(
386 CellArray::new(vec![Value::String(category.clone())], 1, 1)
387 .map_err(|err| error(err, &ERROR_INVALID_RANGE))?,
388 );
389 Ok(Self::Categorical {
390 value,
391 categories: vec![category],
392 })
393 }
394 other => Err(error(
395 format!("optimizableVariable: unsupported Range value {other:?}"),
396 &ERROR_INVALID_RANGE,
397 )),
398 }
399 }
400
401 fn numeric_from_values(value: Value, values: &[f64]) -> BuiltinResult<Self> {
402 if values.len() != 2 {
403 return Err(error(
404 "optimizableVariable: numeric Range must contain exactly two elements",
405 &ERROR_INVALID_RANGE,
406 ));
407 }
408 let lower = values[0];
409 let upper = values[1];
410 if !lower.is_finite() || !upper.is_finite() || lower >= upper {
411 return Err(error(
412 "optimizableVariable: numeric Range bounds must be finite and increasing",
413 &ERROR_INVALID_RANGE,
414 ));
415 }
416 Ok(Self::Numeric {
417 value,
418 lower,
419 upper,
420 })
421 }
422
423 fn inferred_type(&self) -> VariableType {
424 match self {
425 Self::Numeric { .. } => VariableType::Real,
426 Self::Categorical { .. } => VariableType::Categorical,
427 }
428 }
429
430 fn value(&self) -> &Value {
431 match self {
432 Self::Numeric { value, .. } | Self::Categorical { value, .. } => value,
433 }
434 }
435
436 fn validate_for(&self, var_type: VariableType, transform: Transform) -> BuiltinResult<()> {
437 match (self, var_type) {
438 (Self::Numeric { lower, .. }, VariableType::Real) => {
439 validate_real_transform(*lower, transform)
440 }
441 (Self::Numeric { lower, upper, .. }, VariableType::Integer) => {
442 if lower.fract().abs() > f64::EPSILON || upper.fract().abs() > f64::EPSILON {
443 return Err(error(
444 "optimizableVariable: integer Range bounds must be whole numbers",
445 &ERROR_INVALID_RANGE,
446 ));
447 }
448 validate_integer_transform(*lower, transform)
449 }
450 (Self::Numeric { .. }, VariableType::Categorical) => Err(error(
451 "optimizableVariable: categorical variables require a categorical text Range",
452 &ERROR_INVALID_RANGE,
453 )),
454 (Self::Categorical { categories, .. }, VariableType::Categorical) => {
455 check_categories(categories)?;
456 if transform != Transform::None {
457 return Err(error(
458 "optimizableVariable: categorical variables only support Transform 'none'",
459 &ERROR_INVALID_OPTION,
460 ));
461 }
462 Ok(())
463 }
464 (Self::Categorical { .. }, _) => Err(error(
465 "optimizableVariable: real and integer variables require a two-element numeric Range",
466 &ERROR_INVALID_RANGE,
467 )),
468 }
469 }
470}
471
472fn validate_real_transform(lower: f64, transform: Transform) -> BuiltinResult<()> {
473 if transform == Transform::Log && lower <= 0.0 {
474 return Err(error(
475 "optimizableVariable: log transform requires positive real Range bounds",
476 &ERROR_INVALID_RANGE,
477 ));
478 }
479 Ok(())
480}
481
482fn validate_integer_transform(lower: f64, transform: Transform) -> BuiltinResult<()> {
483 if transform == Transform::Log && lower < 0.0 {
484 return Err(error(
485 "optimizableVariable: log transform requires nonnegative integer Range bounds",
486 &ERROR_INVALID_RANGE,
487 ));
488 }
489 Ok(())
490}
491
492fn text_categories_from_string_array(array: &StringArray) -> BuiltinResult<Vec<String>> {
493 let categories = array.data.clone();
494 check_categories(&categories)?;
495 Ok(categories)
496}
497
498fn text_categories_from_cell(cell: &CellArray) -> BuiltinResult<Vec<String>> {
499 let mut categories = Vec::with_capacity(cell.data.len());
500 for value in &cell.data {
501 categories.push(parse_text_scalar(value, "Range", &ERROR_INVALID_RANGE)?);
502 }
503 check_categories(&categories)?;
504 Ok(categories)
505}
506
507fn check_categories(categories: &[String]) -> BuiltinResult<()> {
508 if categories.is_empty() {
509 return Err(error(
510 "optimizableVariable: categorical Range must contain at least one category",
511 &ERROR_INVALID_RANGE,
512 ));
513 }
514 let mut seen = HashSet::with_capacity(categories.len());
515 for category in categories {
516 if category.trim().is_empty() {
517 return Err(error(
518 "optimizableVariable: categorical Range values must be nonempty text",
519 &ERROR_INVALID_RANGE,
520 ));
521 }
522 if !seen.insert(category.clone()) {
523 return Err(error(
524 "optimizableVariable: categorical Range values must be unique",
525 &ERROR_INVALID_RANGE,
526 ));
527 }
528 }
529 Ok(())
530}
531
532fn parse_text_scalar(
533 value: &Value,
534 label: &str,
535 descriptor: &'static BuiltinErrorDescriptor,
536) -> BuiltinResult<String> {
537 match value {
538 Value::String(text) => Ok(text.clone()),
539 Value::CharArray(chars) if chars.rows == 1 => char_row_to_string(chars, label, descriptor),
540 Value::StringArray(array) if array.data.len() == 1 => Ok(array.data[0].clone()),
541 other => Err(error(
542 format!("optimizableVariable: {label} must be a text scalar, got {other:?}"),
543 descriptor,
544 )),
545 }
546}
547
548fn char_row_to_string(
549 chars: &CharArray,
550 label: &str,
551 descriptor: &'static BuiltinErrorDescriptor,
552) -> BuiltinResult<String> {
553 if chars.rows != 1 {
554 return Err(error(
555 format!("optimizableVariable: {label} char array must be a row vector"),
556 descriptor,
557 ));
558 }
559 Ok(chars.data.iter().collect())
560}
561
562fn parse_bool(value: &Value, label: &str) -> BuiltinResult<bool> {
563 match value {
564 Value::Bool(flag) => Ok(*flag),
565 Value::LogicalArray(LogicalArray { data, .. }) if data.len() == 1 => Ok(data[0] != 0),
566 Value::Num(n) if *n == 0.0 || *n == 1.0 => Ok(*n != 0.0),
567 Value::Int(i) if i.to_f64() == 0.0 || i.to_f64() == 1.0 => Ok(i.to_f64() != 0.0),
568 other => Err(error(
569 format!("optimizableVariable: {label} must be a scalar logical value, got {other:?}"),
570 &ERROR_INVALID_OPTION,
571 )),
572 }
573}
574
575fn error(message: impl Into<String>, descriptor: &'static BuiltinErrorDescriptor) -> RuntimeError {
576 let mut builder = build_runtime_error(message).with_builtin(NAME);
577 if let Some(identifier) = descriptor.identifier {
578 builder = builder.with_identifier(identifier);
579 }
580 builder.build()
581}
582
583fn remap_flow(err: RuntimeError, label: &str) -> RuntimeError {
584 error(
585 format!(
586 "optimizableVariable: failed to gather {label}: {}",
587 err.message()
588 ),
589 &ERROR_FLOW,
590 )
591}
592
593#[cfg(test)]
594mod tests {
595 use super::*;
596 use futures::executor::block_on;
597
598 fn object(value: Value) -> ObjectInstance {
599 let Value::Object(object) = value else {
600 panic!("expected object");
601 };
602 object
603 }
604
605 #[test]
606 fn builds_real_variable_with_defaults() {
607 let out = block_on(optimizable_variable_builtin(
608 Value::String("depth".into()),
609 Value::Tensor(Tensor::new(vec![1.0, 10.0], vec![1, 2]).unwrap()),
610 vec![],
611 ))
612 .expect("optimizableVariable");
613 let object = object(out);
614 assert_eq!(object.class_name, CLASS_NAME);
615 assert_eq!(
616 object.properties.get("Name"),
617 Some(&Value::String("depth".into()))
618 );
619 assert_eq!(
620 object.properties.get("Type"),
621 Some(&Value::String("real".into()))
622 );
623 assert_eq!(
624 object.properties.get("Transform"),
625 Some(&Value::String("none".into()))
626 );
627 assert_eq!(object.properties.get("Optimize"), Some(&Value::Bool(true)));
628 }
629
630 #[test]
631 fn builds_integer_log_variable_with_optimize_false() {
632 let out = block_on(optimizable_variable_builtin(
633 Value::String("trees".into()),
634 Value::Tensor(Tensor::new(vec![0.0, 1000.0], vec![1, 2]).unwrap()),
635 vec![
636 Value::String("Type".into()),
637 Value::String("integer".into()),
638 Value::String("Transform".into()),
639 Value::String("log".into()),
640 Value::String("Optimize".into()),
641 Value::Bool(false),
642 ],
643 ))
644 .expect("optimizableVariable integer");
645 let object = object(out);
646 assert_eq!(
647 object.properties.get("Type"),
648 Some(&Value::String("integer".into()))
649 );
650 assert_eq!(
651 object.properties.get("Transform"),
652 Some(&Value::String("log".into()))
653 );
654 assert_eq!(object.properties.get("Optimize"), Some(&Value::Bool(false)));
655 }
656
657 #[test]
658 fn builds_categorical_variable_from_cellstr() {
659 let categories = Value::Cell(
660 CellArray::new(
661 vec![Value::String("linear".into()), Value::String("rbf".into())],
662 1,
663 2,
664 )
665 .unwrap(),
666 );
667 let out = block_on(optimizable_variable_builtin(
668 Value::String("kernel".into()),
669 categories.clone(),
670 vec![
671 Value::String("Type".into()),
672 Value::String("categorical".into()),
673 ],
674 ))
675 .expect("optimizableVariable categorical");
676 let object = object(out);
677 assert_eq!(
678 object.properties.get("Type"),
679 Some(&Value::String("categorical".into()))
680 );
681 assert_eq!(object.properties.get("Range"), Some(&categories));
682 }
683
684 #[test]
685 fn infers_categorical_type_from_text_range() {
686 let out = block_on(optimizable_variable_builtin(
687 Value::String("method".into()),
688 Value::StringArray(StringArray::new(vec!["a".into(), "b".into()], vec![1, 2]).unwrap()),
689 vec![],
690 ))
691 .expect("optimizableVariable categorical inferred");
692 let object = object(out);
693 assert_eq!(
694 object.properties.get("Type"),
695 Some(&Value::String("categorical".into()))
696 );
697 }
698
699 #[test]
700 fn accepts_explicit_categorical_scalar_string_range() {
701 let out = block_on(optimizable_variable_builtin(
702 Value::String("method".into()),
703 Value::String("rbf".into()),
704 vec![
705 Value::String("Type".into()),
706 Value::String("categorical".into()),
707 ],
708 ))
709 .expect("optimizableVariable scalar categorical");
710 let object = object(out);
711 assert_eq!(
712 object.properties.get("Range"),
713 Some(&Value::String("rbf".into()))
714 );
715 assert_eq!(
716 object.properties.get("Type"),
717 Some(&Value::String("categorical".into()))
718 );
719 }
720
721 #[test]
722 fn rejects_invalid_ranges_and_options() {
723 let err = block_on(optimizable_variable_builtin(
724 Value::String("bad".into()),
725 Value::Tensor(Tensor::new(vec![2.0, 1.0], vec![1, 2]).unwrap()),
726 vec![],
727 ))
728 .unwrap_err();
729 assert!(err.to_string().contains("finite and increasing"));
730
731 let err = block_on(optimizable_variable_builtin(
732 Value::String("bad".into()),
733 Value::Tensor(Tensor::new(vec![1.5, 3.0], vec![1, 2]).unwrap()),
734 vec![
735 Value::String("Type".into()),
736 Value::String("integer".into()),
737 ],
738 ))
739 .unwrap_err();
740 assert!(err.to_string().contains("whole numbers"));
741
742 let err = block_on(optimizable_variable_builtin(
743 Value::String("bad".into()),
744 Value::Tensor(Tensor::new(vec![0.0, 3.0], vec![1, 2]).unwrap()),
745 vec![
746 Value::String("Transform".into()),
747 Value::String("log".into()),
748 ],
749 ))
750 .unwrap_err();
751 assert!(err.to_string().contains("positive real"));
752
753 let err = block_on(optimizable_variable_builtin(
754 Value::String("bad".into()),
755 Value::Tensor(Tensor::new(vec![-1.0, 3.0], vec![1, 2]).unwrap()),
756 vec![
757 Value::String("Type".into()),
758 Value::String("integer".into()),
759 Value::String("Transform".into()),
760 Value::String("log".into()),
761 ],
762 ))
763 .unwrap_err();
764 assert!(err.to_string().contains("nonnegative integer"));
765
766 let err = block_on(optimizable_variable_builtin(
767 Value::String("bad".into()),
768 Value::Tensor(Tensor::new(vec![1.0, 3.0], vec![1, 2]).unwrap()),
769 vec![Value::String("Type".into()), Value::String("int".into())],
770 ))
771 .unwrap_err();
772 assert!(err.to_string().contains("unsupported Type"));
773
774 let err = block_on(optimizable_variable_builtin(
775 Value::String("bad".into()),
776 Value::StringArray(StringArray::new(vec!["a".into(), "a".into()], vec![1, 2]).unwrap()),
777 vec![
778 Value::String("Type".into()),
779 Value::String("categorical".into()),
780 ],
781 ))
782 .unwrap_err();
783 assert!(err.to_string().contains("unique"));
784 }
785}