1use super::*;
2use crate::{message_io, v1, v2, ConstraintType, Message, Parse};
3use anyhow::Result;
4
5impl Instance {
6 pub fn to_v1_bytes(&self) -> Result<Vec<u8>> {
35 let v1_instance = v1::Instance::try_from(self.clone())?;
36 Ok(v1_instance.encode_to_vec())
37 }
38
39 pub fn to_v2_bytes(&self) -> Vec<u8> {
67 let v2_instance = v2::Instance::from(self.clone());
68 v2_instance.encode_to_vec()
69 }
70
71 pub fn from_v1_bytes(bytes: &[u8]) -> Result<Self> {
72 let inner = message_io::decode::<v1::Instance>(bytes, "ommx.v1.Instance")?;
73 Ok(Parse::parse(inner, &())?)
74 }
75
76 pub fn from_v2_bytes(bytes: &[u8]) -> Result<Self> {
77 let inner = message_io::decode::<v2::Instance>(bytes, "ommx.v2.Instance")?;
78 Ok(Parse::parse(inner, &())?)
79 }
80}
81
82impl ParametricInstance {
83 pub fn to_v1_bytes(&self) -> Result<Vec<u8>> {
113 let v1_instance = v1::ParametricInstance::try_from(self.clone())?;
114 Ok(v1_instance.encode_to_vec())
115 }
116
117 pub fn to_v2_bytes(&self) -> Vec<u8> {
145 let v2_instance = v2::ParametricInstance::from(self.clone());
146 v2_instance.encode_to_vec()
147 }
148
149 pub fn from_v1_bytes(bytes: &[u8]) -> Result<Self> {
150 let inner =
151 message_io::decode::<v1::ParametricInstance>(bytes, "ommx.v1.ParametricInstance")?;
152 Ok(Parse::parse(inner, &())?)
153 }
154
155 pub fn from_v2_bytes(bytes: &[u8]) -> Result<Self> {
156 let inner =
157 message_io::decode::<v2::ParametricInstance>(bytes, "ommx.v2.ParametricInstance")?;
158 Ok(Parse::parse(inner, &())?)
159 }
160}
161
162impl From<Instance> for v2::Instance {
163 fn from(value: Instance) -> Self {
164 let mut required_features = crate::v2_io::required_features(
165 created_collection_has_payload(&value.indicator_constraint_collection),
166 created_collection_has_payload(&value.one_hot_constraint_collection),
167 created_collection_has_payload(&value.sos1_constraint_collection),
168 );
169 if value.output_objective.is_some() {
170 required_features.push(v2::Feature::OutputObjective as i32);
171 }
172
173 let Instance {
174 sense,
175 objective,
176 output_objective,
177 decision_variables,
178 constraint_collection,
179 indicator_constraint_collection,
180 one_hot_constraint_collection,
181 sos1_constraint_collection,
182 decision_variable_dependency,
183 named_functions,
184 parameters,
185 description,
186 annotations,
187 } = value;
188
189 Self {
190 required_features,
191 description,
192 decision_variables: Some(decision_variables.into()),
193 objective: Some(objective.into()),
194 regular_constraints: Some(constraint_collection.into()),
195 sense: sense.into(),
196 parameters,
197 indicator_constraints: Some(indicator_constraint_collection.into()),
198 one_hot_constraints: Some(one_hot_constraint_collection.into()),
199 sos1_constraints: Some(sos1_constraint_collection.into()),
200 decision_variable_dependency: decision_variable_dependency_to_v2_map(
201 decision_variable_dependency,
202 ),
203 named_functions: Some(named_functions.into()),
204 annotations: crate::v2_io::extension_annotations_to_v2_map(annotations),
205 output_objective: output_objective.map(Into::into),
206 }
207 }
208}
209
210impl From<OutputObjective> for v2::OutputObjective {
211 fn from(value: OutputObjective) -> Self {
212 Self {
213 sense: value.sense.into(),
214 function: Some(value.function.into()),
215 preserves_optimality: value.preserves_optimality,
216 }
217 }
218}
219
220impl From<ParametricInstance> for v2::ParametricInstance {
221 fn from(value: ParametricInstance) -> Self {
222 let mut required_features = crate::v2_io::required_features(
223 created_collection_has_payload(&value.indicator_constraint_collection),
224 created_collection_has_payload(&value.one_hot_constraint_collection),
225 created_collection_has_payload(&value.sos1_constraint_collection),
226 );
227 if value.output_objective.is_some() {
228 required_features.push(v2::Feature::OutputObjective as i32);
229 }
230
231 let ParametricInstance {
232 sense,
233 objective,
234 output_objective,
235 decision_variables,
236 parameters,
237 constraint_collection,
238 indicator_constraint_collection,
239 one_hot_constraint_collection,
240 sos1_constraint_collection,
241 decision_variable_dependency,
242 named_functions,
243 description,
244 annotations,
245 } = value;
246
247 Self {
248 required_features,
249 description,
250 decision_variables: Some(decision_variables.into()),
251 parameters: Some(parameters.into()),
252 objective: Some(objective.into()),
253 regular_constraints: Some(constraint_collection.into()),
254 sense: sense.into(),
255 indicator_constraints: Some(indicator_constraint_collection.into()),
256 one_hot_constraints: Some(one_hot_constraint_collection.into()),
257 sos1_constraints: Some(sos1_constraint_collection.into()),
258 decision_variable_dependency: decision_variable_dependency_to_v2_map(
259 decision_variable_dependency,
260 ),
261 named_functions: Some(named_functions.into()),
262 annotations: crate::v2_io::extension_annotations_to_v2_map(annotations),
263 output_objective: output_objective.map(Into::into),
264 }
265 }
266}
267
268fn created_collection_has_payload<T: ConstraintType>(collection: &ConstraintCollection<T>) -> bool {
269 !collection.active().is_empty() || !collection.removed().is_empty()
270}
271
272fn decision_variable_dependency_to_v2_map(
273 dependency: AcyclicAssignments,
274) -> std::collections::BTreeMap<u64, v1::Function> {
275 dependency
276 .into_iter()
277 .map(|(id, function)| (id.into_inner(), function.into()))
278 .collect()
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284 use crate::{
285 linear, v2, ATol, DecisionVariable, Equality, Evaluate, Function, IndicatorConstraint,
286 IndicatorConstraintID, OneHotConstraint, OneHotConstraintID, ParameterLabelStore,
287 ParameterTable, Sampled, Sos1Constraint, Sos1ConstraintID, VariableID,
288 };
289 use proptest::prelude::*;
290 use std::{
291 collections::{BTreeMap, BTreeSet, HashMap},
292 error::Error as _,
293 };
294
295 fn deeply_composed_function(depth: usize) -> Function {
296 (0..depth).fold(Function::from(linear!(1)), |function, level| {
297 if level % 2 == 0 {
298 function.abs()
299 } else {
300 function.signum()
301 }
302 })
303 }
304
305 #[test]
306 fn deeply_composed_function_roundtrips_in_instance_roots() {
307 let instance = Instance::builder()
308 .sense(Sense::Minimize)
309 .objective(deeply_composed_function(4096))
310 .decision_variables(BTreeMap::from([(
311 VariableID::from(1),
312 DecisionVariable::continuous(),
313 )]))
314 .constraints(BTreeMap::new())
315 .build()
316 .unwrap();
317
318 let v1_bytes = instance.to_v1_bytes().unwrap();
319 assert_eq!(Instance::from_v1_bytes(&v1_bytes).unwrap(), instance);
320 let v2_bytes = instance.to_v2_bytes();
321 assert_eq!(Instance::from_v2_bytes(&v2_bytes).unwrap(), instance);
322
323 let parametric: ParametricInstance = instance.into();
324 let v1_bytes = parametric.to_v1_bytes().unwrap();
325 assert_eq!(
326 ParametricInstance::from_v1_bytes(&v1_bytes).unwrap(),
327 parametric
328 );
329 let v2_bytes = parametric.to_v2_bytes();
330 assert_eq!(
331 ParametricInstance::from_v2_bytes(&v2_bytes).unwrap(),
332 parametric
333 );
334 }
335
336 fn instance_with_special_constraints() -> Instance {
337 let variable_1 = VariableID::from(1);
338 let variable_2 = VariableID::from(2);
339
340 let indicator_id = IndicatorConstraintID::from(10);
341 let one_hot_id = OneHotConstraintID::from(20);
342 let sos1_id = Sos1ConstraintID::from(30);
343
344 let mut indicator_context = ConstraintContextStore::default();
345 indicator_context.set_name(indicator_id, "indicator");
346 let mut one_hot_context = ConstraintContextStore::default();
347 one_hot_context.set_name(one_hot_id, "one_hot");
348 let mut sos1_context = ConstraintContextStore::default();
349 sos1_context.set_name(sos1_id, "sos1");
350
351 Instance::builder()
352 .sense(Sense::Minimize)
353 .objective(Function::Zero)
354 .decision_variables(BTreeMap::from([
355 (variable_1, DecisionVariable::binary()),
356 (variable_2, DecisionVariable::binary()),
357 ]))
358 .constraints(BTreeMap::new())
359 .indicator_constraints(BTreeMap::from([(
360 indicator_id,
361 IndicatorConstraint::new(
362 variable_1,
363 Equality::LessThanOrEqualToZero,
364 Function::Zero,
365 ),
366 )]))
367 .indicator_constraint_context(indicator_context)
368 .one_hot_constraints(BTreeMap::from([(
369 one_hot_id,
370 OneHotConstraint::new(BTreeSet::from([variable_1, variable_2])).unwrap(),
371 )]))
372 .one_hot_constraint_context(one_hot_context)
373 .sos1_constraints(BTreeMap::from([(
374 sos1_id,
375 Sos1Constraint::new(BTreeSet::from([variable_1, variable_2])).unwrap(),
376 )]))
377 .sos1_constraint_context(sos1_context)
378 .build()
379 .unwrap()
380 }
381
382 fn expected_special_features() -> Vec<i32> {
383 vec![
384 v2::Feature::ConstraintIndicator as i32,
385 v2::Feature::ConstraintOneHot as i32,
386 v2::Feature::ConstraintSos1 as i32,
387 ]
388 }
389
390 fn instance_with_output_objective() -> Instance {
391 let mut instance = Instance::builder()
392 .sense(Sense::Maximize)
393 .objective(Function::from(linear!(1)))
394 .decision_variables(BTreeMap::from([
395 (VariableID::from(1), DecisionVariable::binary()),
396 (VariableID::from(2), DecisionVariable::binary()),
397 ]))
398 .constraints(BTreeMap::new())
399 .build()
400 .unwrap();
401 assert!(instance.convert_active_objective(Sense::Minimize));
402 instance
403 }
404
405 fn parametric_instance_with_output_objective() -> ParametricInstance {
406 let parameter_id = VariableID::from(100);
407 let output_function =
408 Function::from((linear!(1) + linear!(parameter_id.into_inner())).unwrap());
409 let mut instance = ParametricInstance::builder()
410 .sense(Sense::Minimize)
411 .objective(output_function.clone())
412 .decision_variables(BTreeMap::from([(
413 VariableID::from(1),
414 DecisionVariable::binary(),
415 )]))
416 .parameters(ParameterTable::from_ids(BTreeSet::from([parameter_id])))
417 .constraints(BTreeMap::new())
418 .build()
419 .unwrap();
420 instance.output_objective = Some(OutputObjective::new(
421 Sense::Maximize,
422 output_function,
423 false,
424 ));
425 instance
426 }
427
428 fn assert_btree_map<K: Ord, V>(_: &BTreeMap<K, V>) {}
429
430 #[test]
431 fn v1_instance_serialization_rejects_special_constraints() {
432 let err = instance_with_special_constraints()
433 .to_v1_bytes()
434 .unwrap_err();
435
436 assert!(
437 err.to_string().contains("to_v2_bytes"),
438 "unexpected error: {err}"
439 );
440 }
441
442 #[test]
443 fn v1_parametric_instance_serialization_rejects_special_constraints() {
444 let instance: ParametricInstance = instance_with_special_constraints().into();
445 let err = instance.to_v1_bytes().unwrap_err();
446
447 assert!(
448 err.to_string().contains("to_v2_bytes"),
449 "unexpected error: {err}"
450 );
451 }
452
453 #[test]
454 fn v2_instance_serializes_special_constraint_collections() {
455 let proto = v2::Instance::from(instance_with_special_constraints());
456
457 assert_eq!(proto.required_features, expected_special_features());
458 let indicator_constraints = proto.indicator_constraints.unwrap();
459 assert!(indicator_constraints.active.contains_key(&10));
460 assert_eq!(
461 indicator_constraints
462 .contexts
463 .get(&10)
464 .and_then(|context| context.label.as_ref())
465 .and_then(|label| label.name.as_deref()),
466 Some("indicator")
467 );
468
469 let one_hot_constraints = proto.one_hot_constraints.unwrap();
470 assert_eq!(
471 one_hot_constraints.active.get(&20).unwrap().variables,
472 vec![1, 2]
473 );
474
475 let sos1_constraints = proto.sos1_constraints.unwrap();
476 assert_eq!(
477 sos1_constraints.active.get(&30).unwrap().variables,
478 vec![1, 2]
479 );
480 }
481
482 #[test]
483 fn v2_instance_deserializes_special_constraint_collections() {
484 let instance = instance_with_special_constraints();
485 let restored = Instance::from_v2_bytes(&instance.to_v2_bytes()).unwrap();
486
487 assert_eq!(restored, instance);
488 assert_eq!(
489 restored
490 .indicator_constraint_context()
491 .name(IndicatorConstraintID::from(10)),
492 Some("indicator")
493 );
494 assert_eq!(
495 restored
496 .one_hot_constraint_context()
497 .name(OneHotConstraintID::from(20)),
498 Some("one_hot")
499 );
500 assert_eq!(
501 restored
502 .sos1_constraint_context()
503 .name(Sos1ConstraintID::from(30)),
504 Some("sos1")
505 );
506 }
507
508 #[test]
509 fn v2_instance_parse_preserves_explicit_output_objective() {
510 let mut expected = Instance::default();
511 let mut proto = v2::Instance::from(expected.clone());
512 proto
513 .required_features
514 .push(v2::Feature::OutputObjective as i32);
515 proto.output_objective = Some(v2::OutputObjective {
516 sense: proto.sense,
517 function: proto.objective.clone(),
518 preserves_optimality: true,
519 });
520 expected.output_objective = Some(OutputObjective::new(
521 expected.sense(),
522 expected.objective().clone(),
523 true,
524 ));
525
526 let restored = Instance::try_from(proto).unwrap();
527
528 assert_eq!(restored, expected);
529 assert!(restored.to_v1_bytes().is_err());
530 }
531
532 #[test]
533 fn v2_instance_reader_accepts_output_objective_without_feature_declaration() {
534 let instance = instance_with_output_objective();
535 let mut proto = v2::Instance::from(instance.clone());
536 assert!(proto
537 .required_features
538 .contains(&(v2::Feature::OutputObjective as i32)));
539 proto.required_features.clear();
540
541 let restored = Instance::try_from(proto).unwrap();
542 assert_eq!(restored, instance);
543 }
544
545 #[test]
546 fn v2_instance_rejects_undefined_output_objective_variable() {
547 let mut proto = v2::Instance::from(instance_with_output_objective());
548 proto.output_objective.as_mut().unwrap().function =
549 Some(Function::from(linear!(999)).into());
550
551 let err = Instance::try_from(proto).unwrap_err();
552
553 assert!(
554 err.to_string().contains("output_objective.function")
555 && err.to_string().contains("Undefined variable ID"),
556 "unexpected error: {err}",
557 );
558 }
559
560 #[test]
561 fn v2_parametric_instance_round_trip_preserves_parameterized_output_objective() {
562 let instance = parametric_instance_with_output_objective();
563 let proto = v2::ParametricInstance::from(instance.clone());
564
565 assert!(proto
566 .required_features
567 .contains(&(v2::Feature::OutputObjective as i32)));
568 let output = proto.output_objective.as_ref().unwrap();
569 assert_eq!(
570 output.sense,
571 i32::from(crate::v1::instance::Sense::Maximize)
572 );
573 assert!(!output.preserves_optimality);
574 let output_function: Function = output
575 .function
576 .as_ref()
577 .unwrap()
578 .clone()
579 .parse(&())
580 .unwrap();
581 assert!(output_function
582 .required_ids()
583 .contains(&VariableID::from(100)));
584
585 let restored = ParametricInstance::try_from(proto).unwrap();
586 assert_eq!(restored, instance);
587 }
588
589 #[test]
590 fn v2_parametric_parse_preserves_explicit_output_objective() {
591 let mut expected = ParametricInstance::default();
592 let mut proto = v2::ParametricInstance::from(expected.clone());
593 proto
594 .required_features
595 .push(v2::Feature::OutputObjective as i32);
596 proto.output_objective = Some(v2::OutputObjective {
597 sense: proto.sense,
598 function: proto.objective.clone(),
599 preserves_optimality: true,
600 });
601 expected.output_objective = Some(OutputObjective::new(
602 *expected.sense(),
603 expected.objective().clone(),
604 true,
605 ));
606
607 let restored = ParametricInstance::try_from(proto).unwrap();
608
609 assert_eq!(restored, expected);
610 assert!(restored.to_v1_bytes().is_err());
611 }
612
613 #[test]
614 fn v2_parametric_instance_rejects_undefined_output_objective_id() {
615 let mut proto = v2::ParametricInstance::from(parametric_instance_with_output_objective());
616 proto.output_objective.as_mut().unwrap().function =
617 Some(Function::from(linear!(999)).into());
618
619 let err = ParametricInstance::try_from(proto).unwrap_err();
620 assert!(
621 err.to_string().contains("output_objective.function")
622 && err.to_string().contains("Undefined variable ID"),
623 "unexpected error: {err}",
624 );
625 }
626
627 #[test]
628 fn v2_solution_serializes_evaluated_special_constraint_collections() {
629 let instance = instance_with_special_constraints();
630 let solution = instance
631 .evaluate(
632 &v1::State {
633 entries: HashMap::from([(1, 1.0), (2, 0.0)]),
634 },
635 ATol::default(),
636 )
637 .unwrap();
638
639 let proto = v2::Solution::from(solution);
640
641 assert_eq!(proto.required_features, expected_special_features());
642 assert!(proto
643 .evaluated_indicator_constraints
644 .unwrap()
645 .entries
646 .contains_key(&10));
647 assert!(proto
648 .evaluated_one_hot_constraints
649 .unwrap()
650 .entries
651 .contains_key(&20));
652 assert!(proto
653 .evaluated_sos1_constraints
654 .unwrap()
655 .entries
656 .contains_key(&30));
657 }
658
659 #[test]
660 fn v2_solution_deserializes_evaluated_special_constraint_collections() {
661 let instance = instance_with_special_constraints();
662 let solution = instance
663 .evaluate(
664 &v1::State {
665 entries: HashMap::from([(1, 1.0), (2, 0.0)]),
666 },
667 ATol::default(),
668 )
669 .unwrap();
670
671 let restored = crate::Solution::from_v2_bytes(&solution.to_v2_bytes()).unwrap();
672
673 assert_eq!(restored, solution);
674 assert_eq!(
675 restored
676 .evaluated_indicator_constraints()
677 .context()
678 .name(IndicatorConstraintID::from(10)),
679 Some("indicator")
680 );
681 assert!(restored
682 .evaluated_one_hot_constraints()
683 .contains_key(&OneHotConstraintID::from(20)));
684 assert!(restored
685 .evaluated_sos1_constraints()
686 .contains_key(&Sos1ConstraintID::from(30)));
687 }
688
689 #[test]
690 fn v2_solution_deserialization_rejects_unknown_structural_special_variable() {
691 let instance = instance_with_special_constraints();
692 let solution = instance
693 .evaluate(
694 &v1::State {
695 entries: HashMap::from([(1, 1.0), (2, 0.0)]),
696 },
697 ATol::default(),
698 )
699 .unwrap();
700 let mut proto = v2::Solution::from(solution);
701 let one_hot = proto
702 .evaluated_one_hot_constraints
703 .as_mut()
704 .unwrap()
705 .entries
706 .get_mut(&20)
707 .unwrap();
708 one_hot.variables = vec![999];
709 one_hot.active_variable = Some(999);
710 one_hot.used_decision_variable_ids.clear();
711
712 let err = crate::Solution::try_from(proto).unwrap_err();
713
714 assert!(matches!(
715 err.source()
716 .and_then(|error| error.downcast_ref::<crate::SolutionError>()),
717 Some(crate::SolutionError::InvalidConstraintStructure {
718 constraint_family: "one-hot",
719 constraint_id,
720 message,
721 }) if constraint_id == "OneHotConstraintID(20)"
722 && message == "variable VariableID(999) is not in decision_variables"
723 ));
724 }
725
726 #[test]
727 fn v2_sample_set_serializes_sampled_special_constraint_collections() {
728 let instance = instance_with_special_constraints();
729 let samples = Sampled::from(v1::State {
730 entries: HashMap::from([(1, 1.0), (2, 0.0)]),
731 });
732 let sample_set = instance
733 .evaluate_samples(&samples, ATol::default())
734 .unwrap();
735
736 let proto = v2::SampleSet::from(sample_set);
737
738 assert_eq!(proto.required_features, expected_special_features());
739 assert!(proto
740 .sampled_indicator_constraints
741 .unwrap()
742 .entries
743 .contains_key(&10));
744 assert!(proto
745 .sampled_one_hot_constraints
746 .unwrap()
747 .entries
748 .contains_key(&20));
749 assert!(proto
750 .sampled_sos1_constraints
751 .unwrap()
752 .entries
753 .contains_key(&30));
754 }
755
756 #[test]
757 fn v2_sample_set_deserializes_sampled_special_constraint_collections() {
758 let instance = instance_with_special_constraints();
759 let samples = Sampled::from(v1::State {
760 entries: HashMap::from([(1, 1.0), (2, 0.0)]),
761 });
762 let sample_set = instance
763 .evaluate_samples(&samples, ATol::default())
764 .unwrap();
765
766 let restored = crate::SampleSet::from_v2_bytes(&sample_set.to_v2_bytes()).unwrap();
767
768 assert_eq!(restored.feasible(), sample_set.feasible());
769 assert_eq!(restored.feasible_relaxed(), sample_set.feasible_relaxed());
770 assert_eq!(restored.indicator_constraints().len(), 1);
771 assert_eq!(restored.one_hot_constraints().len(), 1);
772 assert_eq!(restored.sos1_constraints().len(), 1);
773 assert_eq!(
774 restored
775 .indicator_constraints()
776 .context()
777 .name(IndicatorConstraintID::from(10)),
778 Some("indicator")
779 );
780 }
781
782 #[test]
783 fn v2_sample_set_deserialization_rejects_missing_feasible_sample_id() {
784 let instance = instance_with_special_constraints();
785 let samples = Sampled::from(v1::State {
786 entries: HashMap::from([(1, 1.0), (2, 0.0)]),
787 });
788 let sample_set = instance
789 .evaluate_samples(&samples, ATol::default())
790 .unwrap();
791 let mut proto = v2::SampleSet::from(sample_set);
792 let sample_id = *proto.feasible.keys().next().unwrap();
793 proto.feasible.remove(&sample_id);
794
795 let err = crate::SampleSet::try_from(proto).unwrap_err();
796
797 assert!(
798 err.to_string().contains("feasible")
799 && err.to_string().contains("Inconsistent sample IDs"),
800 "unexpected error: {err}",
801 );
802 }
803
804 #[test]
805 fn to_v2_bytes_encodes_v2_instance() {
806 let bytes = instance_with_special_constraints().to_v2_bytes();
807 let proto = v2::Instance::decode(bytes.as_slice()).unwrap();
808
809 assert_eq!(proto.required_features, expected_special_features());
810 }
811
812 #[test]
813 fn v2_generated_maps_are_ordered_for_deterministic_encoding() {
814 let proto = v2::Instance::from(instance_with_special_constraints());
815
816 assert_btree_map(&proto.annotations);
817 let decision_variables = proto.decision_variables.as_ref().unwrap();
818 assert_btree_map(&decision_variables.entries);
819 assert_btree_map(&decision_variables.labels);
820 let indicator_constraints = proto.indicator_constraints.as_ref().unwrap();
821 assert_btree_map(&indicator_constraints.active);
822 assert_btree_map(&indicator_constraints.contexts);
823 }
824
825 #[test]
826 fn v2_parametric_instance_serializes_parameter_table() {
827 let decision_variable_id = VariableID::from(1);
828 let parameter_id = VariableID::from(100);
829 let mut parameter_labels = ParameterLabelStore::default();
830 parameter_labels.set_name(parameter_id, "p");
831
832 let instance = ParametricInstance::builder()
833 .sense(Sense::Minimize)
834 .objective(Function::Zero)
835 .decision_variables(BTreeMap::from([(
836 decision_variable_id,
837 DecisionVariable::binary(),
838 )]))
839 .parameters(
840 ParameterTable::new(BTreeSet::from([parameter_id]), parameter_labels).unwrap(),
841 )
842 .constraints(BTreeMap::new())
843 .build()
844 .unwrap();
845
846 let proto = v2::ParametricInstance::from(instance);
847 let parameters = proto.parameters.unwrap();
848
849 assert_eq!(parameters.ids, vec![100]);
850 assert_eq!(
851 parameters
852 .labels
853 .get(&100)
854 .and_then(|label| label.name.as_deref()),
855 Some("p")
856 );
857 }
858
859 #[test]
860 fn v2_parametric_instance_deserializes_parameter_table() {
861 let decision_variable_id = VariableID::from(1);
862 let parameter_id = VariableID::from(100);
863 let mut parameter_labels = ParameterLabelStore::default();
864 parameter_labels.set_name(parameter_id, "p");
865
866 let instance = ParametricInstance::builder()
867 .sense(Sense::Minimize)
868 .objective(Function::Zero)
869 .decision_variables(BTreeMap::from([(
870 decision_variable_id,
871 DecisionVariable::binary(),
872 )]))
873 .parameters(
874 ParameterTable::new(BTreeSet::from([parameter_id]), parameter_labels).unwrap(),
875 )
876 .constraints(BTreeMap::new())
877 .build()
878 .unwrap();
879
880 let restored = ParametricInstance::from_v2_bytes(&instance.to_v2_bytes()).unwrap();
881
882 assert_eq!(restored, instance);
883 assert_eq!(restored.parameters().labels().name(parameter_id), Some("p"));
884 }
885
886 proptest! {
887 #[test]
888 fn v2_instance_round_trip_preserves_full_v3_semantics(
889 instance in Instance::arbitrary_with(crate::InstanceParameters::full_v3())
890 ) {
891 let restored = Instance::from_v2_bytes(&instance.to_v2_bytes()).unwrap();
892 prop_assert_eq!(restored, instance);
893 }
894 }
895}