1use std::hash::Hasher;
2
3use oximo_core::{Model, ModelKind, ObjectiveSense, Variable, var_name};
4use oximo_expr::{ExprArena, VarId, describe_nonlinear_term, extract_linear};
5use rustc_hash::FxHasher;
6
7use crate::status::SolverError;
8
9#[derive(Clone, Debug, PartialEq)]
23pub struct Snapshot {
24 pub obj_costs: Vec<f64>,
26 pub obj_constant: f64,
28 pub lb: Vec<f64>,
30 pub ub: Vec<f64>,
32 pub fingerprint: u64,
34}
35
36pub fn snapshot(model: &Model) -> Result<Snapshot, SolverError> {
49 model.ensure_objective_declared().map_err(SolverError::Core)?;
50 let kind = model.kind();
51 if model.num_soc_constraints() > 0 || matches!(kind, ModelKind::SOCP | ModelKind::MISOCP) {
52 return Err(SolverError::UnsupportedKind(kind));
53 }
54 let arena = model.arena();
55 let vars = model.variables();
56 let model_constraints = model.constraints();
57 let constraints = model_constraints.algebraic();
58
59 let objective = model.objective();
60 let obj = objective.as_ref();
61 let sense = obj.map_or(ObjectiveSense::Minimize, |o| o.sense);
62 let (obj_by_id, obj_constant) = match obj {
63 Some(o) => {
64 let lin = extract_linear(&arena, o.expr).ok_or_else(|| SolverError::Nonlinear {
65 location: "the objective".into(),
66 term: describe_nonlinear_term(&arena, o.expr, &|v| var_name(&vars, v))
67 .unwrap_or_else(|| "<nonlinear>".into()),
68 })?;
69 let mut by_id = vec![0.0; vars.len()];
70 for (v, c) in &lin.coeffs {
71 by_id[v.index()] = *c;
72 }
73 (by_id, lin.constant)
74 }
75 None => (vec![0.0; vars.len()], 0.0),
76 };
77
78 let mut obj_costs = Vec::with_capacity(vars.len());
79 let mut lb = Vec::with_capacity(vars.len());
80 let mut ub = Vec::with_capacity(vars.len());
81 let mut hasher = FxHasher::default();
82 hash_header(&mut hasher, &vars, sense);
83 for v in vars.iter() {
84 obj_costs.push(obj_by_id[v.id.index()]);
85 lb.push(v.lb);
86 ub.push(v.ub);
87 }
88
89 let arena_ref: &ExprArena = &arena;
90 for c in constraints {
91 let t = extract_linear(arena_ref, c.lhs).ok_or_else(|| SolverError::Nonlinear {
92 location: format!("constraint {:?}", c.name),
93 term: describe_nonlinear_term(arena_ref, c.lhs, &|v| var_name(&vars, v))
94 .unwrap_or_else(|| "<nonlinear>".into()),
95 })?;
96 hash_row(&mut hasher, c.lower - t.constant, c.upper - t.constant, &t.coeffs);
97 }
98
99 Ok(Snapshot { obj_costs, obj_constant, lb, ub, fingerprint: hasher.finish() })
100}
101
102fn hash_header(h: &mut FxHasher, vars: &[Variable], sense: ObjectiveSense) {
104 h.write_usize(vars.len());
105 h.write_u8(match sense {
106 ObjectiveSense::Minimize => 0,
107 ObjectiveSense::Maximize => 1,
108 });
109 for v in vars {
110 h.write_u8(u8::from(v.domain.is_integer()));
111 }
112}
113
114fn hash_row(h: &mut FxHasher, lower: f64, upper: f64, coeffs: &[(VarId, f64)]) {
117 h.write_u64(lower.to_bits());
118 h.write_u64(upper.to_bits());
119 let mut terms: Vec<(usize, u64)> =
120 coeffs.iter().map(|(v, c)| (v.index(), c.to_bits())).collect();
121 terms.sort_unstable();
122 for (vi, cb) in terms {
123 h.write_usize(vi);
124 h.write_u64(cb);
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use oximo_core::prelude::*;
131
132 use super::snapshot;
133
134 #[test]
135 fn objective_coeff_change_keeps_fingerprint() {
136 let m = Model::new("t");
137 param!(m, p = 1.0);
138 variable!(m, x >= 0.0);
139 variable!(m, y >= 0.0);
140 constraint!(m, c, x + y <= 10.0);
141 objective!(m, Max, p * x + 2.0 * y);
142
143 let s1 = snapshot(&m).unwrap();
144 p.set_param_value(5.0);
145 let s2 = snapshot(&m).unwrap();
146 assert_eq!(s1.fingerprint, s2.fingerprint, "structure unchanged");
147 assert_ne!(s1.obj_costs, s2.obj_costs, "coefficient moved");
148 }
149
150 #[test]
151 fn bound_change_keeps_fingerprint() {
152 let m = Model::new("t");
153 variable!(m, x >= 0.0);
154 constraint!(m, c, x <= 10.0);
155 objective!(m, Max, x);
156
157 let s1 = snapshot(&m).unwrap();
158 m.fix(x, 3.0);
159 let s2 = snapshot(&m).unwrap();
160 assert_eq!(s1.fingerprint, s2.fingerprint, "structure unchanged");
161 assert_ne!(s1.ub, s2.ub, "bound moved");
162 }
163
164 #[test]
165 fn constraint_rhs_change_breaks_fingerprint() {
166 let m = Model::new("t");
167 param!(m, cap = 10.0);
168 variable!(m, x >= 0.0);
169 constraint!(m, c, x <= cap);
170 objective!(m, Max, x);
171
172 let s1 = snapshot(&m).unwrap();
173 cap.set_param_value(20.0);
174 let s2 = snapshot(&m).unwrap();
175 assert_ne!(s1.fingerprint, s2.fingerprint, "row bound changed");
176 }
177
178 #[test]
179 fn constraint_coeff_change_breaks_fingerprint() {
180 let m = Model::new("t");
181 param!(m, a = 1.0);
182 variable!(m, x >= 0.0);
183 variable!(m, y >= 0.0);
184 constraint!(m, c, a * x + y <= 10.0);
185 objective!(m, Max, x + y);
186
187 let s1 = snapshot(&m).unwrap();
188 a.set_param_value(3.0);
189 let s2 = snapshot(&m).unwrap();
190 assert_ne!(s1.fingerprint, s2.fingerprint, "matrix coefficient changed");
191 }
192
193 #[test]
194 fn nonlinear_objective_is_rejected() {
195 let m = Model::new("t");
196 variable!(m, x >= 0.0);
197 objective!(m, Min, x.powi(2));
198 assert!(snapshot(&m).is_err());
199 }
200
201 #[test]
202 fn soc_constraint_is_rejected() {
203 let m = Model::new("t");
204 variable!(m, x >= 0.0);
205 variable!(m, t >= 0.0);
206 m.add_soc_constraint("cone", [x], t);
207 objective!(m, Min, t);
208 assert!(matches!(
209 snapshot(&m),
210 Err(crate::status::SolverError::UnsupportedKind(ModelKind::SOCP))
211 ));
212 }
213
214 #[test]
215 fn feasibility_is_a_zero_objective() {
216 let m = Model::new("feas");
217 variable!(m, x >= 0.0);
218 variable!(m, y >= 0.0);
219 constraint!(m, c, x + y == 5.0);
220 objective!(m, Feasibility);
221
222 let s = snapshot(&m).expect("feasibility model snapshots");
223 assert!(s.obj_costs.iter().all(|&c| c.abs() < 1e-12), "costs = {:?}", s.obj_costs);
224 assert!(s.obj_constant.abs() < 1e-12, "constant = {}", s.obj_constant);
225 }
226
227 #[test]
228 fn undeclared_objective_is_rejected() {
229 let m = Model::new("undeclared");
230 variable!(m, x >= 0.0);
231 constraint!(m, c, x <= 5.0);
232 assert!(matches!(
233 snapshot(&m),
234 Err(crate::status::SolverError::Core(oximo_core::Error::NoObjective))
235 ));
236 }
237}