Skip to main content

ommx/instance/
serialize.rs

1use super::*;
2use crate::{message_io, v1, v2, ConstraintType, Message, Parse};
3use anyhow::Result;
4
5impl Instance {
6    /// Serialize this instance using the v1 wire format.
7    ///
8    /// # Errors
9    ///
10    /// `ommx.v1.Instance` cannot represent the presence of an
11    /// [`Instance::output_objective`], even when it currently matches the
12    /// active objective.
13    ///
14    /// ```
15    /// use ommx::{linear, DecisionVariable, Function, Instance, Sense, VariableID};
16    /// use std::collections::BTreeMap;
17    ///
18    /// let mut instance = Instance::builder()
19    ///     .sense(Sense::Maximize)
20    ///     .objective(Function::from(linear!(1)))
21    ///     .decision_variables(BTreeMap::from([(
22    ///         VariableID::from(1),
23    ///         DecisionVariable::binary(),
24    ///     )]))
25    ///     .constraints(BTreeMap::new())
26    ///     .build()
27    ///     .unwrap();
28    /// assert!(instance.convert_active_objective(Sense::Minimize));
29    /// assert!(instance.convert_active_objective(Sense::Maximize));
30    /// assert_eq!(instance.output_objective().unwrap().function(), instance.objective());
31    ///
32    /// assert!(instance.to_v1_bytes().is_err());
33    /// ```
34    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    /// Serialize this instance using the v2 wire format.
40    ///
41    /// # Postconditions
42    ///
43    /// v2 serialization round-trips the output objective.
44    ///
45    /// ```
46    /// use ommx::{linear, DecisionVariable, Function, Instance, Sense, VariableID};
47    /// use std::collections::BTreeMap;
48    ///
49    /// let mut instance = Instance::builder()
50    ///     .sense(Sense::Maximize)
51    ///     .objective(Function::from(linear!(1)))
52    ///     .decision_variables(BTreeMap::from([(
53    ///         VariableID::from(1),
54    ///         DecisionVariable::binary(),
55    ///     )]))
56    ///     .constraints(BTreeMap::new())
57    ///     .build()
58    ///     .unwrap();
59    /// assert!(instance.convert_active_objective(Sense::Minimize));
60    ///
61    /// let restored = Instance::from_v2_bytes(&instance.to_v2_bytes()).unwrap();
62    /// assert_eq!(restored.sense(), Sense::Minimize);
63    /// assert_eq!(restored.output_objective().unwrap().sense(), Sense::Maximize);
64    /// assert_eq!(restored, instance);
65    /// ```
66    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    /// Serialize this parametric instance using the v1 wire format.
84    ///
85    /// # Errors
86    ///
87    /// `ommx.v1.ParametricInstance` cannot represent the presence of a
88    /// [`ParametricInstance::output_objective`], even when it currently matches
89    /// the active objective.
90    ///
91    /// ```
92    /// use ommx::{linear, DecisionVariable, Function, Instance, ParametricInstance, Sense, VariableID};
93    /// use std::collections::BTreeMap;
94    ///
95    /// let mut source = Instance::builder()
96    ///     .sense(Sense::Maximize)
97    ///     .objective(Function::from(linear!(1)))
98    ///     .decision_variables(BTreeMap::from([(
99    ///         VariableID::from(1),
100    ///         DecisionVariable::binary(),
101    ///     )]))
102    ///     .constraints(BTreeMap::new())
103    ///     .build()
104    ///     .unwrap();
105    /// assert!(source.convert_active_objective(Sense::Minimize));
106    /// assert!(source.convert_active_objective(Sense::Maximize));
107    /// let instance = ParametricInstance::from(source);
108    /// assert_eq!(instance.output_objective().unwrap().function(), instance.objective());
109    ///
110    /// assert!(instance.to_v1_bytes().is_err());
111    /// ```
112    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    /// Serialize this parametric instance using the v2 wire format.
118    ///
119    /// # Postconditions
120    ///
121    /// v2 serialization round-trips the parametric output objective.
122    ///
123    /// ```
124    /// use ommx::{linear, DecisionVariable, Function, Instance, ParametricInstance, Sense, VariableID};
125    /// use std::collections::BTreeMap;
126    ///
127    /// let mut source = Instance::builder()
128    ///     .sense(Sense::Maximize)
129    ///     .objective(Function::from(linear!(1)))
130    ///     .decision_variables(BTreeMap::from([(
131    ///         VariableID::from(1),
132    ///         DecisionVariable::binary(),
133    ///     )]))
134    ///     .constraints(BTreeMap::new())
135    ///     .build()
136    ///     .unwrap();
137    /// assert!(source.convert_active_objective(Sense::Minimize));
138    /// let instance = ParametricInstance::from(source);
139    ///
140    /// let restored = ParametricInstance::from_v2_bytes(&instance.to_v2_bytes()).unwrap();
141    /// assert_eq!(restored.output_objective().unwrap().sense(), Sense::Maximize);
142    /// assert_eq!(restored, instance);
143    /// ```
144    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}