ommx/instance/
stats.rs

1use serde::{Deserialize, Serialize};
2
3/// Statistics about decision variables categorized by kind.
4#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5pub struct VariableStatsByKind {
6    /// Number of binary variables
7    pub binary: usize,
8    /// Number of integer variables
9    pub integer: usize,
10    /// Number of continuous variables
11    pub continuous: usize,
12    /// Number of semi-integer variables
13    pub semi_integer: usize,
14    /// Number of semi-continuous variables
15    pub semi_continuous: usize,
16}
17
18/// Statistics about decision variables categorized by usage.
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20pub struct VariableStatsByUsage {
21    /// Number of variables used in the objective function
22    pub used_in_objective: usize,
23    /// Number of variables used in constraints
24    pub used_in_constraints: usize,
25    /// Number of variables used in either objective or constraints
26    pub used: usize,
27    /// Number of fixed variables
28    pub fixed: usize,
29    /// Number of dependent variables
30    pub dependent: usize,
31    /// Number of irrelevant variables (not used, fixed, or dependent)
32    pub irrelevant: usize,
33}
34
35/// Statistics about decision variables in an instance.
36///
37/// This struct provides counts of decision variables categorized by:
38/// - Kind: binary, integer, continuous, semi-integer, semi-continuous
39/// - Usage: used (in objective or constraints), fixed, dependent, irrelevant
40///
41/// Note on usage categories:
42/// The usage-based categories (used, fixed, dependent, irrelevant) are mutually exclusive.
43/// A variable belongs to exactly one category, determined by this priority:
44/// 1. `fixed`: Variables with substituted values
45/// 2. `dependent`: Variables defined by assignments in decision_variable_dependency
46/// 3. `used`: Variables appearing in objective or active constraints (not in categories 1-2)
47/// 4. `irrelevant`: All other variables (not in categories 1-3)
48#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
49pub struct DecisionVariableStats {
50    /// Total number of decision variables
51    pub total: usize,
52    /// Statistics categorized by variable kind
53    pub by_kind: VariableStatsByKind,
54    /// Statistics categorized by variable usage
55    pub by_usage: VariableStatsByUsage,
56}
57
58/// Statistics about constraints in an instance.
59#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
60pub struct ConstraintStats {
61    /// Total number of constraints (active + removed)
62    pub total: usize,
63    /// Number of active constraints
64    pub active: usize,
65    /// Number of removed constraints
66    pub removed: usize,
67}
68
69/// Statistics about an optimization problem instance.
70///
71/// This struct provides a summary of the instance structure,
72/// including counts of variables and constraints by category.
73/// It is designed to be serializable for snapshot testing and reporting.
74#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
75pub struct InstanceStats {
76    /// Statistics about decision variables
77    pub decision_variables: DecisionVariableStats,
78    /// Statistics about constraints
79    pub constraints: ConstraintStats,
80}
81
82impl super::Instance {
83    /// Compute statistics about this instance.
84    ///
85    /// # Returns
86    ///
87    /// An `InstanceStats` struct containing counts of variables and constraints
88    /// categorized by kind, usage, and status.
89    ///
90    /// # Example
91    ///
92    /// ```
93    /// use ommx::Instance;
94    ///
95    /// let instance = Instance::default();
96    /// let stats = instance.stats();
97    /// println!("Total variables: {}", stats.decision_variables.total);
98    /// println!("Active constraints: {}", stats.constraints.active);
99    /// ```
100    pub fn stats(&self) -> InstanceStats {
101        let analysis = self.analyze_decision_variables();
102
103        let by_kind = VariableStatsByKind {
104            binary: analysis.binary().len(),
105            integer: analysis.integer().len(),
106            continuous: analysis.continuous().len(),
107            semi_integer: analysis.semi_integer().len(),
108            semi_continuous: analysis.semi_continuous().len(),
109        };
110
111        let by_usage = VariableStatsByUsage {
112            used_in_objective: analysis.used_in_objective().len(),
113            used_in_constraints: analysis
114                .used_in_constraints()
115                .values()
116                .flat_map(|vars| vars.iter())
117                .collect::<std::collections::HashSet<_>>()
118                .len(),
119            used: analysis.used().len(),
120            fixed: analysis.fixed().len(),
121            dependent: analysis.dependent().len(),
122            irrelevant: analysis.irrelevant().len(),
123        };
124
125        let decision_variables = DecisionVariableStats {
126            total: self.decision_variables.len(),
127            by_kind,
128            by_usage,
129        };
130
131        let constraints = ConstraintStats {
132            total: self.constraints.len() + self.removed_constraints.len(),
133            active: self.constraints.len(),
134            removed: self.removed_constraints.len(),
135        };
136
137        InstanceStats {
138            decision_variables,
139            constraints,
140        }
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147    use crate::{
148        coeff, linear, Constraint, ConstraintID, DecisionVariable, Instance, Sense, VariableID,
149    };
150    use maplit::btreemap;
151    use std::collections::BTreeMap;
152
153    #[test]
154    fn test_empty_instance_stats() {
155        let instance = Instance::default();
156        let stats = instance.stats();
157
158        assert_eq!(stats.decision_variables.total, 0);
159        assert_eq!(stats.decision_variables.by_kind.binary, 0);
160        assert_eq!(stats.decision_variables.by_kind.integer, 0);
161        assert_eq!(stats.decision_variables.by_kind.continuous, 0);
162        assert_eq!(stats.decision_variables.by_usage.used, 0);
163        assert_eq!(stats.decision_variables.by_usage.fixed, 0);
164        assert_eq!(stats.decision_variables.by_usage.dependent, 0);
165        assert_eq!(stats.decision_variables.by_usage.irrelevant, 0);
166
167        assert_eq!(stats.constraints.total, 0);
168        assert_eq!(stats.constraints.active, 0);
169        assert_eq!(stats.constraints.removed, 0);
170    }
171
172    #[test]
173    fn test_instance_with_variables_stats() {
174        let decision_variables = btreemap! {
175            VariableID::from(1) => DecisionVariable::binary(VariableID::from(1)),
176            VariableID::from(2) => DecisionVariable::binary(VariableID::from(2)),
177            VariableID::from(3) => DecisionVariable::integer(VariableID::from(3)),
178            VariableID::from(4) => DecisionVariable::continuous(VariableID::from(4)),
179        };
180
181        // Set objective using variable 1 and 2
182        let objective = (linear!(1) + linear!(2)).into();
183
184        let instance = Instance::new(
185            Sense::Minimize,
186            objective,
187            decision_variables,
188            BTreeMap::new(),
189        )
190        .unwrap();
191
192        let stats = instance.stats();
193
194        assert_eq!(stats.decision_variables.total, 4);
195        assert_eq!(stats.decision_variables.by_kind.binary, 2);
196        assert_eq!(stats.decision_variables.by_kind.integer, 1);
197        assert_eq!(stats.decision_variables.by_kind.continuous, 1);
198        assert_eq!(stats.decision_variables.by_usage.used_in_objective, 2);
199        assert_eq!(stats.decision_variables.by_usage.used, 2);
200        assert_eq!(stats.decision_variables.by_usage.irrelevant, 2); // variables 3 and 4
201    }
202
203    #[test]
204    fn test_instance_with_constraints_stats() {
205        let decision_variables = btreemap! {
206            VariableID::from(1) => DecisionVariable::binary(VariableID::from(1)),
207            VariableID::from(2) => DecisionVariable::binary(VariableID::from(2)),
208            VariableID::from(3) => DecisionVariable::binary(VariableID::from(3)),
209        };
210
211        let objective = (linear!(1) + coeff!(1.0)).into();
212
213        let constraints = btreemap! {
214            ConstraintID::from(1) => Constraint::equal_to_zero(
215                ConstraintID::from(1),
216                (linear!(1) + linear!(2) + coeff!(-1.0)).into(),
217            ),
218            ConstraintID::from(2) => Constraint::equal_to_zero(
219                ConstraintID::from(2),
220                (linear!(3) + coeff!(-1.0)).into(),
221            ),
222        };
223
224        let mut instance =
225            Instance::new(Sense::Minimize, objective, decision_variables, constraints).unwrap();
226
227        // Remove one constraint
228        instance
229            .relax_constraint(ConstraintID::from(2), "Test removal".to_string(), [])
230            .unwrap();
231
232        let stats = instance.stats();
233
234        assert_eq!(stats.constraints.total, 2);
235        assert_eq!(stats.constraints.active, 1);
236        assert_eq!(stats.constraints.removed, 1);
237        // Variables 1 and 2 are used in constraint 1 (active)
238        // Note: Removed constraints are NOT counted in used_in_constraints
239        assert_eq!(stats.decision_variables.by_usage.used_in_constraints, 2);
240        // Variable 1 is used in both objective and constraint 1, variable 2 is used in constraint 1
241        assert_eq!(stats.decision_variables.by_usage.used, 2);
242    }
243
244    #[test]
245    fn test_stats_serialization() {
246        let decision_variables = btreemap! {
247            VariableID::from(1) => DecisionVariable::binary(VariableID::from(1)),
248        };
249
250        let objective = (linear!(1) + coeff!(1.0)).into();
251
252        let instance = Instance::new(
253            Sense::Minimize,
254            objective,
255            decision_variables,
256            BTreeMap::new(),
257        )
258        .unwrap();
259
260        let stats = instance.stats();
261
262        // Test that stats can be serialized and deserialized
263        let json = serde_json::to_string(&stats).unwrap();
264        let deserialized: InstanceStats = serde_json::from_str(&json).unwrap();
265
266        assert_eq!(stats, deserialized);
267    }
268
269    #[test]
270    fn test_stats_snapshot_empty() {
271        let instance = Instance::default();
272        let stats = instance.stats();
273        insta::assert_yaml_snapshot!(stats);
274    }
275
276    #[test]
277    fn test_stats_snapshot_with_variables() {
278        let decision_variables = btreemap! {
279            VariableID::from(1) => DecisionVariable::binary(VariableID::from(1)),
280            VariableID::from(2) => DecisionVariable::binary(VariableID::from(2)),
281            VariableID::from(3) => DecisionVariable::integer(VariableID::from(3)),
282            VariableID::from(4) => DecisionVariable::continuous(VariableID::from(4)),
283            VariableID::from(5) => DecisionVariable::semi_integer(VariableID::from(5)),
284        };
285
286        let objective = (linear!(1) + linear!(2)).into();
287
288        let instance = Instance::new(
289            Sense::Minimize,
290            objective,
291            decision_variables,
292            BTreeMap::new(),
293        )
294        .unwrap();
295
296        let stats = instance.stats();
297        insta::assert_yaml_snapshot!(stats);
298    }
299
300    #[test]
301    fn test_stats_snapshot_with_constraints() {
302        let decision_variables = btreemap! {
303            VariableID::from(1) => DecisionVariable::binary(VariableID::from(1)),
304            VariableID::from(2) => DecisionVariable::binary(VariableID::from(2)),
305            VariableID::from(3) => DecisionVariable::integer(VariableID::from(3)),
306        };
307
308        let objective = (linear!(1) + coeff!(1.0)).into();
309
310        let constraints = btreemap! {
311            ConstraintID::from(1) => Constraint::equal_to_zero(
312                ConstraintID::from(1),
313                (linear!(1) + linear!(2) + coeff!(-1.0)).into(),
314            ),
315            ConstraintID::from(2) => Constraint::equal_to_zero(
316                ConstraintID::from(2),
317                (linear!(2) + linear!(3) + coeff!(-5.0)).into(),
318            ),
319            ConstraintID::from(3) => Constraint::equal_to_zero(
320                ConstraintID::from(3),
321                (linear!(3) + coeff!(-10.0)).into(),
322            ),
323        };
324
325        let mut instance =
326            Instance::new(Sense::Minimize, objective, decision_variables, constraints).unwrap();
327
328        // Remove one constraint
329        instance
330            .relax_constraint(ConstraintID::from(3), "Not needed".to_string(), [])
331            .unwrap();
332
333        let stats = instance.stats();
334        insta::assert_yaml_snapshot!(stats);
335    }
336}