Skip to main content

problemreductions/
types.rs

1//! Common types used across the problemreductions library.
2
3use serde::de::{self, DeserializeOwned, Visitor};
4use serde::{Deserialize, Deserializer, Serialize, Serializer};
5use std::fmt;
6
7/// Largest integer magnitude represented exactly by an IEEE 754 `f64`.
8pub const MAX_EXACT_F64_INTEGER: i64 = (1_i64 << 53) - 1;
9
10/// An `i64` cannot cross an exact-integer `f64` boundary without precision loss.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
12#[error(
13    "integer {value} is outside the exactly representable f64 range [{min}, {max}]",
14    min = -MAX_EXACT_F64_INTEGER,
15    max = MAX_EXACT_F64_INTEGER
16)]
17pub struct ExactI64ToF64Error {
18    pub value: i64,
19}
20
21/// Failure while performing checked arithmetic on a numeric value.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
23pub enum NumericArithmeticError {
24    /// An exact integer result is outside the numeric type's range.
25    #[error("integer overflow")]
26    IntegerOverflow,
27    /// A floating-point result is not finite.
28    #[error("non-finite floating-point result")]
29    NonFiniteResult,
30}
31
32/// Convert an `i64` to `f64` only when the integer value remains exact.
33pub fn i64_to_exact_f64(value: i64) -> Result<f64, ExactI64ToF64Error> {
34    if (-MAX_EXACT_F64_INTEGER..=MAX_EXACT_F64_INTEGER).contains(&value) {
35        Ok(value as f64)
36    } else {
37        Err(ExactI64ToF64Error { value })
38    }
39}
40
41/// Bound for objective value types (i64, f64, etc.)
42pub trait NumericSize:
43    Clone
44    + Default
45    + PartialOrd
46    + num_traits::Num
47    + num_traits::Zero
48    + num_traits::Bounded
49    + std::ops::AddAssign
50    + 'static
51{
52    /// Add two values when the exact result remains representable and finite.
53    fn checked_add_value(self, other: Self) -> Result<Self, NumericArithmeticError>;
54    /// Multiply two values when the exact result remains representable and finite.
55    fn checked_mul_value(self, other: Self) -> Result<Self, NumericArithmeticError>;
56}
57
58/// Smallest h >= 1 for which every finite input has magnitude below 2^h.
59/// Halving preserves integer and floating power-of-two boundaries without
60/// taking an absolute value (which would overflow for i64::MIN).
61pub(crate) fn max_numeric_magnitude_bits<C: NumericSize>(
62    values: impl IntoIterator<Item = C>,
63) -> u64 {
64    fn bits<C: NumericSize>(mut value: C) -> u64 {
65        let one = C::one();
66        let two = one.clone() + one.clone();
67        let negative = value < C::zero();
68        let unit = if negative { C::zero() - one } else { one };
69        let mut bits = 0;
70        while if negative {
71            value <= unit
72        } else {
73            value >= unit
74        } {
75            value = value / two.clone();
76            bits += 1;
77        }
78        bits.max(1)
79    }
80
81    // Only the extrema need bit counting; scanning all entries is linear.
82    let (mut minimum, mut maximum) = (C::zero(), C::zero());
83    for value in values {
84        if value < minimum {
85            minimum = value;
86        } else if value > maximum {
87            maximum = value;
88        }
89    }
90    bits(minimum).max(bits(maximum))
91}
92
93macro_rules! impl_integer_numeric_size {
94    ($($type:ty),* $(,)?) => {
95        $(
96            impl NumericSize for $type {
97                fn checked_add_value(self, other: Self) -> Result<Self, NumericArithmeticError> {
98                    self.checked_add(other).ok_or(NumericArithmeticError::IntegerOverflow)
99                }
100
101                fn checked_mul_value(self, other: Self) -> Result<Self, NumericArithmeticError> {
102                    self.checked_mul(other).ok_or(NumericArithmeticError::IntegerOverflow)
103                }
104            }
105        )*
106    };
107}
108
109impl_integer_numeric_size!(i64, u64, usize);
110
111impl NumericSize for f64 {
112    fn checked_add_value(self, other: Self) -> Result<Self, NumericArithmeticError> {
113        let result = self + other;
114        result
115            .is_finite()
116            .then_some(result)
117            .ok_or(NumericArithmeticError::NonFiniteResult)
118    }
119
120    fn checked_mul_value(self, other: Self) -> Result<Self, NumericArithmeticError> {
121        let result = self * other;
122        result
123            .is_finite()
124            .then_some(result)
125            .ok_or(NumericArithmeticError::NonFiniteResult)
126    }
127}
128
129fn evaluation_arithmetic_error(
130    error: NumericArithmeticError,
131    context: &str,
132) -> crate::traits::EvaluationError {
133    match error {
134        NumericArithmeticError::IntegerOverflow => {
135            crate::traits::EvaluationError::IntegerOverflow(context.to_string())
136        }
137        NumericArithmeticError::NonFiniteResult => {
138            crate::traits::EvaluationError::NonFiniteResult(context.to_string())
139        }
140    }
141}
142
143/// Maps a weight element to its sum/metric type.
144///
145/// This decouples the per-element weight type from the accumulation type.
146/// Exact integer weights use a wider accumulation type: `i64` and the unit
147/// weight [`One`] both use `i64`. Approximate `f64` weights continue to sum
148/// into `f64`.
149pub trait WeightElement: Clone + Default + 'static {
150    /// The numeric type used for sums and comparisons.
151    type Sum: NumericSize;
152    /// Whether this is the unit weight type (`One`).
153    const IS_UNIT: bool;
154    /// Construct the multiplicative unit weight.
155    fn unit() -> Self;
156    /// Validate that an element belongs to the public weight domain.
157    fn validate_element(&self, context: &str) -> Result<(), crate::registry::ConstructionError>;
158    /// Convert this weight element to the sum type.
159    fn to_sum(&self) -> Self::Sum;
160    /// Add one element to an evaluated objective without overflowing or producing a non-finite value.
161    fn checked_add_to_sum(
162        total: Self::Sum,
163        value: Self::Sum,
164        context: &str,
165    ) -> Result<Self::Sum, crate::traits::EvaluationError> {
166        total
167            .checked_add_value(value)
168            .map_err(|error| evaluation_arithmetic_error(error, context))
169    }
170    /// Multiply evaluated quantities without overflowing or producing a non-finite value.
171    fn checked_mul_sum(
172        left: Self::Sum,
173        right: Self::Sum,
174        context: &str,
175    ) -> Result<Self::Sum, crate::traits::EvaluationError> {
176        left.checked_mul_value(right)
177            .map_err(|error| evaluation_arithmetic_error(error, context))
178    }
179}
180
181impl WeightElement for i64 {
182    type Sum = i64;
183    const IS_UNIT: bool = false;
184    fn unit() -> Self {
185        1
186    }
187    fn validate_element(&self, _context: &str) -> Result<(), crate::registry::ConstructionError> {
188        Ok(())
189    }
190    fn to_sum(&self) -> i64 {
191        *self
192    }
193}
194
195impl WeightElement for f64 {
196    type Sum = f64;
197    const IS_UNIT: bool = false;
198    fn unit() -> Self {
199        1.0
200    }
201    fn validate_element(&self, context: &str) -> Result<(), crate::registry::ConstructionError> {
202        if self.is_finite() {
203            Ok(())
204        } else {
205            Err(crate::registry::ConstructionError::NonFiniteFloat(format!(
206                "{context} must be finite"
207            )))
208        }
209    }
210    fn to_sum(&self) -> f64 {
211        *self
212    }
213}
214
215/// The constant 1. Unit weight for unweighted problems.
216///
217/// When used as the weight type parameter `W`, indicates that all weights
218/// are uniformly 1. `One::to_sum()` returns `1i64`.
219#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
220pub struct One;
221
222impl Serialize for One {
223    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
224    where
225        S: Serializer,
226    {
227        serializer.serialize_i64(1)
228    }
229}
230
231impl<'de> Deserialize<'de> for One {
232    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
233    where
234        D: Deserializer<'de>,
235    {
236        struct OneVisitor;
237
238        impl<'de> Visitor<'de> for OneVisitor {
239            type Value = One;
240
241            fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
242                formatter.write_str("the unit weight `One` encoded as 1 or unit/null")
243            }
244
245            fn visit_i64<E>(self, value: i64) -> Result<One, E>
246            where
247                E: de::Error,
248            {
249                if value == 1 {
250                    Ok(One)
251                } else {
252                    Err(E::custom(format!("expected 1 for One, got {value}")))
253                }
254            }
255
256            fn visit_u64<E>(self, value: u64) -> Result<One, E>
257            where
258                E: de::Error,
259            {
260                if value == 1 {
261                    Ok(One)
262                } else {
263                    Err(E::custom(format!("expected 1 for One, got {value}")))
264                }
265            }
266
267            fn visit_unit<E>(self) -> Result<One, E>
268            where
269                E: de::Error,
270            {
271                Ok(One)
272            }
273
274            fn visit_none<E>(self) -> Result<One, E>
275            where
276                E: de::Error,
277            {
278                Ok(One)
279            }
280
281            fn visit_str<E>(self, value: &str) -> Result<One, E>
282            where
283                E: de::Error,
284            {
285                if value == "One" {
286                    Ok(One)
287                } else {
288                    Err(E::custom(format!("expected \"One\" for One, got {value}")))
289                }
290            }
291        }
292
293        deserializer.deserialize_any(OneVisitor)
294    }
295}
296
297impl WeightElement for One {
298    type Sum = i64;
299    const IS_UNIT: bool = true;
300    fn unit() -> Self {
301        One
302    }
303    fn validate_element(&self, _context: &str) -> Result<(), crate::registry::ConstructionError> {
304        Ok(())
305    }
306    fn to_sum(&self) -> i64 {
307        1
308    }
309}
310
311impl std::fmt::Display for One {
312    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
313        write!(f, "One")
314    }
315}
316
317/// Failure while combining configuration values during a solve.
318#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
319pub enum AggregationError {
320    #[error("aggregate arithmetic overflow or non-finite result")]
321    ArithmeticOverflow,
322    #[error("aggregate values are not comparable")]
323    UnorderedComparison,
324    #[error("cannot combine extrema with different optimization senses")]
325    IncompatibleExtremumSense,
326}
327
328/// Foldable aggregate values for enumerating a problem's configuration space.
329pub trait Aggregate: Clone + fmt::Debug + Serialize + DeserializeOwned {
330    /// Neutral element for folding.
331    fn identity() -> Self;
332
333    /// Associative combine operation.
334    fn combine(self, other: Self) -> Result<Self, AggregationError>;
335
336    /// Whether no further configuration can change this aggregate value.
337    fn is_absorbing(&self) -> bool {
338        false
339    }
340}
341
342/// Aggregate value whose optimum identifies contributing solutions.
343pub trait SolutionAggregate: Aggregate {
344    /// Whether a solution-level value contributes to the final aggregate value.
345    fn contributes_to_solution(value: &Self, total: &Self) -> bool;
346}
347
348/// Maximum aggregate over feasible values.
349#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
350pub struct Max<V>(pub Option<V>);
351
352impl<V: fmt::Debug + PartialOrd + Clone + Serialize + DeserializeOwned> Aggregate for Max<V> {
353    fn identity() -> Self {
354        Max(None)
355    }
356
357    fn combine(self, other: Self) -> Result<Self, AggregationError> {
358        use std::cmp::Ordering;
359
360        Ok(match (self.0, other.0) {
361            (None, rhs) => Max(rhs),
362            (lhs, None) => Max(lhs),
363            (Some(lhs), Some(rhs)) => {
364                let ord = lhs
365                    .partial_cmp(&rhs)
366                    .ok_or(AggregationError::UnorderedComparison)?;
367                match ord {
368                    Ordering::Less => Max(Some(rhs)),
369                    Ordering::Equal | Ordering::Greater => Max(Some(lhs)),
370                }
371            }
372        })
373    }
374}
375
376impl<V: fmt::Debug + PartialOrd + Clone + Serialize + DeserializeOwned> SolutionAggregate
377    for Max<V>
378{
379    fn contributes_to_solution(value: &Self, total: &Self) -> bool {
380        matches!((value, total), (Max(Some(value)), Max(Some(best))) if value == best)
381    }
382}
383
384impl<V: fmt::Display> fmt::Display for Max<V> {
385    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
386        match &self.0 {
387            Some(value) => write!(f, "Max({value})"),
388            None => write!(f, "Max(None)"),
389        }
390    }
391}
392
393impl<V> Max<V> {
394    pub fn is_valid(&self) -> bool {
395        self.0.is_some()
396    }
397
398    pub fn size(&self) -> Option<&V> {
399        self.0.as_ref()
400    }
401
402    pub fn unwrap(self) -> V {
403        self.0.expect("called unwrap on invalid Max value")
404    }
405}
406
407/// Minimum aggregate over feasible values.
408#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
409pub struct Min<V>(pub Option<V>);
410
411impl<V: fmt::Debug + PartialOrd + Clone + Serialize + DeserializeOwned> Aggregate for Min<V> {
412    fn identity() -> Self {
413        Min(None)
414    }
415
416    fn combine(self, other: Self) -> Result<Self, AggregationError> {
417        use std::cmp::Ordering;
418
419        Ok(match (self.0, other.0) {
420            (None, rhs) => Min(rhs),
421            (lhs, None) => Min(lhs),
422            (Some(lhs), Some(rhs)) => {
423                let ord = lhs
424                    .partial_cmp(&rhs)
425                    .ok_or(AggregationError::UnorderedComparison)?;
426                match ord {
427                    Ordering::Greater => Min(Some(rhs)),
428                    Ordering::Equal | Ordering::Less => Min(Some(lhs)),
429                }
430            }
431        })
432    }
433}
434
435impl<V: fmt::Debug + PartialOrd + Clone + Serialize + DeserializeOwned> SolutionAggregate
436    for Min<V>
437{
438    fn contributes_to_solution(value: &Self, total: &Self) -> bool {
439        matches!((value, total), (Min(Some(value)), Min(Some(best))) if value == best)
440    }
441}
442
443impl<V: fmt::Display> fmt::Display for Min<V> {
444    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
445        match &self.0 {
446            Some(value) => write!(f, "Min({value})"),
447            None => write!(f, "Min(None)"),
448        }
449    }
450}
451
452impl<V> Min<V> {
453    pub fn is_valid(&self) -> bool {
454        self.0.is_some()
455    }
456
457    pub fn size(&self) -> Option<&V> {
458        self.0.as_ref()
459    }
460
461    pub fn unwrap(self) -> V {
462        self.0.expect("called unwrap on invalid Min value")
463    }
464}
465
466/// Trait for aggregate values that represent optimization objectives.
467pub trait OptimizationValue: Aggregate {
468    /// The inner numeric type used for comparisons with decision bounds.
469    type Inner: Clone + PartialOrd + fmt::Debug + Serialize + DeserializeOwned;
470
471    /// Whether this aggregate value satisfies the provided decision bound.
472    fn meets_bound(value: &Self, bound: &Self::Inner) -> bool;
473}
474
475impl<V: fmt::Debug + PartialOrd + Clone + Serialize + DeserializeOwned> OptimizationValue
476    for Min<V>
477{
478    type Inner = V;
479
480    fn meets_bound(value: &Self, bound: &V) -> bool {
481        matches!(&value.0, Some(v) if *v <= *bound)
482    }
483}
484
485impl<V: fmt::Debug + PartialOrd + Clone + Serialize + DeserializeOwned> OptimizationValue
486    for Max<V>
487{
488    type Inner = V;
489
490    fn meets_bound(value: &Self, bound: &V) -> bool {
491        matches!(&value.0, Some(v) if *v >= *bound)
492    }
493}
494
495/// Additive fold value.
496#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
497pub struct Sum<W>(pub W);
498
499impl<W: fmt::Debug + NumericSize + Serialize + DeserializeOwned> Aggregate for Sum<W> {
500    fn identity() -> Self {
501        Sum(W::zero())
502    }
503
504    fn combine(self, other: Self) -> Result<Self, AggregationError> {
505        self.0
506            .checked_add_value(other.0)
507            .map(Sum)
508            .map_err(|_| AggregationError::ArithmeticOverflow)
509    }
510}
511
512impl<W: fmt::Display> fmt::Display for Sum<W> {
513    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
514        write!(f, "Sum({})", self.0)
515    }
516}
517
518/// Disjunction aggregate for existential satisfaction.
519#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
520pub struct Or(pub bool);
521
522impl Or {
523    pub fn is_valid(&self) -> bool {
524        self.0
525    }
526
527    pub fn unwrap(self) -> bool {
528        self.0
529    }
530}
531
532impl Aggregate for Or {
533    fn identity() -> Self {
534        Or(false)
535    }
536
537    fn combine(self, other: Self) -> Result<Self, AggregationError> {
538        Ok(Or(self.0 || other.0))
539    }
540
541    fn is_absorbing(&self) -> bool {
542        self.0
543    }
544}
545
546impl SolutionAggregate for Or {
547    fn contributes_to_solution(value: &Self, total: &Self) -> bool {
548        value.0 && total.0
549    }
550}
551
552impl fmt::Display for Or {
553    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
554        write!(f, "Or({})", self.0)
555    }
556}
557
558impl std::ops::Not for Or {
559    type Output = bool;
560
561    fn not(self) -> Self::Output {
562        !self.0
563    }
564}
565
566impl PartialEq<bool> for Or {
567    fn eq(&self, other: &bool) -> bool {
568        self.0 == *other
569    }
570}
571
572impl PartialEq<Or> for bool {
573    fn eq(&self, other: &Or) -> bool {
574        *self == other.0
575    }
576}
577
578/// Conjunction aggregate for universal satisfaction.
579#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
580pub struct And(pub bool);
581
582impl Aggregate for And {
583    fn identity() -> Self {
584        And(true)
585    }
586
587    fn combine(self, other: Self) -> Result<Self, AggregationError> {
588        Ok(And(self.0 && other.0))
589    }
590
591    fn is_absorbing(&self) -> bool {
592        !self.0
593    }
594}
595
596impl fmt::Display for And {
597    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
598        write!(f, "And({})", self.0)
599    }
600}
601
602#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
603pub enum ExtremumSense {
604    Maximize,
605    Minimize,
606}
607
608#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
609pub struct Extremum<V> {
610    pub sense: ExtremumSense,
611    pub value: Option<V>,
612}
613
614impl<V> Extremum<V> {
615    pub fn maximize(value: Option<V>) -> Self {
616        Self {
617            sense: ExtremumSense::Maximize,
618            value,
619        }
620    }
621
622    pub fn minimize(value: Option<V>) -> Self {
623        Self {
624            sense: ExtremumSense::Minimize,
625            value,
626        }
627    }
628
629    pub fn is_valid(&self) -> bool {
630        self.value.is_some()
631    }
632
633    pub fn size(&self) -> Option<&V> {
634        self.value.as_ref()
635    }
636
637    pub fn unwrap(self) -> V {
638        self.value.expect("called unwrap on invalid Extremum value")
639    }
640}
641
642impl<V: fmt::Debug + PartialOrd + Clone + Serialize + DeserializeOwned> Aggregate for Extremum<V> {
643    fn identity() -> Self {
644        Self::maximize(None)
645    }
646
647    fn combine(self, other: Self) -> Result<Self, AggregationError> {
648        use std::cmp::Ordering;
649
650        Ok(match (self.value, other.value) {
651            (None, rhs) => Self {
652                sense: other.sense,
653                value: rhs,
654            },
655            (lhs, None) => Self {
656                sense: self.sense,
657                value: lhs,
658            },
659            (Some(lhs), Some(rhs)) => {
660                if self.sense != other.sense {
661                    return Err(AggregationError::IncompatibleExtremumSense);
662                }
663                let ord = lhs
664                    .partial_cmp(&rhs)
665                    .ok_or(AggregationError::UnorderedComparison)?;
666                let keep_self = match self.sense {
667                    ExtremumSense::Maximize => matches!(ord, Ordering::Equal | Ordering::Greater),
668                    ExtremumSense::Minimize => matches!(ord, Ordering::Equal | Ordering::Less),
669                };
670                if keep_self {
671                    Self {
672                        sense: self.sense,
673                        value: Some(lhs),
674                    }
675                } else {
676                    Self {
677                        sense: other.sense,
678                        value: Some(rhs),
679                    }
680                }
681            }
682        })
683    }
684}
685
686impl<V: fmt::Debug + PartialOrd + Clone + Serialize + DeserializeOwned> SolutionAggregate
687    for Extremum<V>
688{
689    fn contributes_to_solution(candidate: &Self, total: &Self) -> bool {
690        matches!(
691            (candidate.value.as_ref(), total.value.as_ref()),
692            (Some(value), Some(best)) if candidate.sense == total.sense && value == best
693        )
694    }
695}
696
697impl<V: fmt::Display> fmt::Display for Extremum<V> {
698    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
699        match (&self.sense, &self.value) {
700            (ExtremumSense::Maximize, Some(value)) => write!(f, "Max({value})"),
701            (ExtremumSense::Maximize, None) => write!(f, "Max(None)"),
702            (ExtremumSense::Minimize, Some(value)) => write!(f, "Min({value})"),
703            (ExtremumSense::Minimize, None) => write!(f, "Min(None)"),
704        }
705    }
706}
707
708/// Canonical named parameters for one concrete problem instance.
709#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
710pub struct ProblemParameters {
711    /// Named parameters in canonical declaration order.
712    #[serde(deserialize_with = "deserialize_parameter_components")]
713    pub(crate) components: Vec<(String, u64)>,
714}
715
716impl ProblemParameters {
717    /// Create problem parameters in canonical declaration order.
718    ///
719    /// # Panics
720    /// Panics if a parameter name occurs more than once.
721    pub fn new(components: Vec<(&str, u64)>) -> Self {
722        Self::from_owned(
723            components
724                .into_iter()
725                .map(|(name, value)| (name.to_string(), value))
726                .collect(),
727        )
728    }
729
730    /// Create problem parameters from owned names.
731    ///
732    /// # Panics
733    /// Panics if a parameter name occurs more than once.
734    pub fn from_owned(components: Vec<(String, u64)>) -> Self {
735        if let Some(name) = duplicate_parameter_name(&components) {
736            panic!("duplicate problem parameter `{name}`");
737        }
738        Self { components }
739    }
740
741    /// Iterate over parameters in canonical declaration order.
742    pub fn iter(&self) -> impl Iterator<Item = (&str, u64)> {
743        self.components
744            .iter()
745            .map(|(name, value)| (name.as_str(), *value))
746    }
747
748    /// Get a parameter by name.
749    pub fn get(&self, name: &str) -> Option<u64> {
750        self.components
751            .iter()
752            .find(|(k, _)| k == name)
753            .map(|(_, v)| *v)
754    }
755}
756
757fn duplicate_parameter_name(components: &[(String, u64)]) -> Option<&str> {
758    components
759        .iter()
760        .enumerate()
761        .find_map(|(index, (name, _))| {
762            components[..index]
763                .iter()
764                .any(|(previous, _)| previous == name)
765                .then_some(name.as_str())
766        })
767}
768
769fn deserialize_parameter_components<'de, D>(deserializer: D) -> Result<Vec<(String, u64)>, D::Error>
770where
771    D: serde::Deserializer<'de>,
772{
773    let components = Vec::<(String, u64)>::deserialize(deserializer)?;
774    if let Some(name) = duplicate_parameter_name(&components) {
775        return Err(serde::de::Error::custom(format!(
776            "duplicate problem parameter `{name}`"
777        )));
778    }
779    Ok(components)
780}
781
782impl fmt::Display for ProblemParameters {
783    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
784        write!(f, "ProblemParameters{{")?;
785        for (i, (name, value)) in self.components.iter().enumerate() {
786            if i > 0 {
787                write!(f, ", ")?;
788            }
789            write!(f, "{}: {}", name, value)?;
790        }
791        write!(f, "}}")
792    }
793}
794
795use crate::impl_variant_param;
796
797impl_variant_param!(f64, "weight");
798impl_variant_param!(i64, "weight");
799impl_variant_param!(One, "weight");
800
801#[cfg(test)]
802#[path = "unit_tests/types.rs"]
803mod tests;
804
805#[cfg(test)]
806#[path = "unit_tests/types_optimization_value.rs"]
807mod optimization_value_tests;