pounce_sens_core/barrier.rs
1//! What the corrector and the activity classification read off a converged
2//! barrier iterate, beyond the back-solve [`SensBacksolver`] provides.
3//!
4//! Both work in the compound layout every engine here shares with pounce's
5//! interior-point solver: one flat vector holding eight blocks in the order
6//!
7//! ```text
8//! x, s, y_c, y_d, z_l, z_u, v_l, v_u
9//! ```
10//!
11//! where `s` is the slack of the inequality rows, `y_c` and `y_d` are the
12//! multipliers of the equality and inequality rows, `z_l` and `z_u` the
13//! multipliers of the finite variable bounds, and `v_l` and `v_u` those of
14//! the finite inequality-row bounds. The signs follow Ipopt's:
15//!
16//! ```text
17//! ∇ₓL = ∇f + J_cᵀ y_c + J_dᵀ y_d − P_xl z_l + P_xu z_u
18//! ∇ₛL = −y_d − P_dl v_l + P_du v_u
19//! ```
20//!
21//! An engine that implements [`BarrierIterate`] gets [`crate::corrector`] and
22//! [`crate::activity`] over its own converged point. pounce's interior-point
23//! solver implements it in `pounce-sensitivity`, and an engine outside pounce
24//! implements it from its own final iterate, its own model callbacks and its
25//! own factorization of the KKT matrix.
26
27use std::rc::Rc;
28
29use pounce_common::types::Number;
30use pounce_linalg::{Matrix, SymMatrix};
31
32use crate::backsolver::SensBacksolver;
33
34/// A converged iterate of a barrier method, with the evaluations the
35/// corrector and the activity classification need.
36///
37/// Two frames appear below. The **natural** frame is the model as the user
38/// wrote it, and it is the frame [`SensBacksolver::solve`] answers in. The
39/// **engine** frame is whatever the engine iterated in, which differs from the
40/// natural one when it scaled the model. [`SensBacksolver::natural_units_factor`]
41/// carries a quantity from the engine frame into the natural one, and
42/// [`Self::scaled_rhs_factor`] carries a right-hand side the other way. An
43/// engine that did not scale returns `None` from both and has one frame.
44pub trait BarrierIterate: SensBacksolver {
45 /// Lengths of the eight blocks, in the order `x, s, y_c, y_d, z_l, z_u,
46 /// v_l, v_u`. They sum to [`SensBacksolver::dim`].
47 fn block_dims(&self) -> [usize; 8];
48
49 /// Where each block starts in the flat vector, with the total length in
50 /// the last entry.
51 fn offsets(&self) -> [usize; 9] {
52 let d = self.block_dims();
53 let mut o = [0usize; 9];
54 for i in 0..8 {
55 o[i + 1] = o[i] + d[i];
56 }
57 o
58 }
59
60 /// The barrier parameter the converged point sits on, as the level of
61 /// the complementarity products of the slacks and multipliers this engine
62 /// reports. These are the products [`Self::barrier_residual`] writes, at
63 /// the point [`Self::converged_iterate`] writes, in the engine frame.
64 ///
65 /// An engine reports the multipliers and `μ` in the frame its own
66 /// iteration ran in, and sets [`ActivityData::obj_scale`] to the factor it
67 /// multiplied the objective by. The classification's ratios do not depend
68 /// on that frame, but its band edges come from `√μ`, which does. In that
69 /// frame an engine classifies as pounce does. Ipopt
70 /// multiplies the objective, and with it every bound multiplier, by a
71 /// scaling factor `f` it prints, and its `.sol` reports the multipliers
72 /// divided by `f` again. An engine built from Ipopt's result therefore
73 /// multiplies those multipliers by `f`, returns the `μ` Ipopt printed, and
74 /// sets `obj_scale` to `f`.
75 ///
76 /// Reporting the unscaled multipliers with `μ / f` instead is consistent,
77 /// since each product then sits at `μ / f`. But the band edges move by
78 /// `1/√f` from pounce's, and the corrector measures its residual in a
79 /// different frame.
80 fn barrier_mu(&self) -> Number;
81
82 /// Writes the converged iterate into `out`, in the engine frame. The error
83 /// says why there is none, or that `out` is the wrong length.
84 fn converged_iterate(&self, out: &mut [Number]) -> Result<(), String>;
85
86 /// Per-row factor `E` that takes a natural-units right-hand side into the
87 /// engine frame, indexed like the compound vector. `None` is the
88 /// identity.
89 ///
90 /// The corrector reads its residual in the engine frame and hands it to
91 /// a solve that expects natural units, so it divides by this first.
92 fn scaled_rhs_factor(&self) -> Option<&[Number]> {
93 None
94 }
95
96 /// Writes the barrier KKT residual at `point` into `out`, block by block
97 /// in the compound layout and in the engine frame. `point` is in the
98 /// engine frame too.
99 ///
100 /// The eight blocks are `∇ₓL`, `∇ₛL`, `c(x)`, `d(x) − s`, and the four
101 /// complementarity products `(x − x_l) z_l`, `(x_u − x) z_u`,
102 /// `(s − d_l) v_l` and `(d_u − s) v_u`, each over the bounds that exist.
103 /// The products carry no barrier shift. The corrector subtracts `μ`
104 /// itself, and it moves the equality rows a parametric perturbation
105 /// pinned, so `c(x)` here is measured against the unperturbed
106 /// right-hand side.
107 ///
108 /// A point outside the domain of the model's functions writes non-finite
109 /// entries rather than failing. The error is for a malformed call.
110 fn barrier_residual(&self, point: &[Number], out: &mut [Number]) -> Result<(), String>;
111
112 /// The Newton operator of the barrier system at a predicted point, for
113 /// the corrector to iterate against.
114 ///
115 /// `predicted` is a compound vector in natural units. The operator is
116 /// the Jacobian of the residual [`Self::barrier_residual`] describes,
117 /// evaluated there, with two changes:
118 ///
119 /// * Each `(row, add)` in `pinned` names a primal row, a row of the `x`
120 /// or `s` block, whose diagonal gets `add` on top of what the barrier
121 /// puts there. These are bounds the step brought into the active set.
122 /// * With `exact_hessian` the Hessian of the Lagrangian is evaluated at
123 /// `predicted`, with its multipliers. Without it the engine keeps the
124 /// Hessian it last held, which is all a quasi-Newton engine has.
125 ///
126 /// A bound the corrector took out of the active set needs nothing from
127 /// the engine. Its multiplier is zero at `predicted` and the corrector
128 /// zeroes its row of every right-hand side, so with a positive slack the
129 /// bound adds nothing to the diagonal and its row returns a zero step.
130 ///
131 /// The returned operator answers in natural units like
132 /// [`SensBacksolver::solve`]. An engine whose evaluations depend on state
133 /// it changed to build the operator restores that state when the operator
134 /// is dropped.
135 fn corrector_operator<'a>(
136 &'a self,
137 predicted: &[Number],
138 pinned: &[(usize, Number)],
139 exact_hessian: bool,
140 ) -> Result<Box<dyn KktOperator + 'a>, String>;
141
142 /// Everything the activity classification reads off the converged point.
143 fn activity_data(&self) -> Result<ActivityData, String>;
144
145 /// How the engine's variables and rows correspond to the model's.
146 fn model_maps(&self) -> Result<ModelMaps, String>;
147
148 /// The scaling the engine applied to the model.
149 /// [`EngineScaling::none`] is the answer for an engine that applied none.
150 fn scaling(&self) -> EngineScaling;
151
152 /// The bounds the engine iterated against over the primal prefix of the
153 /// compound vector, `x` then `s`, in the engine frame: the variables'
154 /// bounds, then the inequality rows' limits. An entry with no bound on a
155 /// side holds an infinity there.
156 ///
157 /// These are the bounds the slacks of [`Self::activity_data`] are
158 /// measured from, so they carry whatever relaxation the engine applied.
159 fn primal_bounds(&self) -> Result<(Vec<Number>, Vec<Number>), String>;
160
161 /// The perturbations `(δ_x, δ_s, δ_c, δ_d)` the engine added to its KKT
162 /// matrix to correct its inertia, as its factor holds them. `None` when
163 /// the engine does not report them, which is the default.
164 ///
165 /// All four zero means every back-solve inverts the exact KKT matrix.
166 /// Anything else perturbs every sensitivity read off the factor, the
167 /// covariance in particular.
168 fn kkt_perturbations(&self) -> Option<[Number; 4]> {
169 None
170 }
171
172 /// An operator for the system with the bounds in `released`, compound
173 /// bound-multiplier rows, taken out of the active set, for a calculation
174 /// that solves against one released system many times.
175 ///
176 /// Each solve answers as [`SensBacksolver::solve_released`] does. The
177 /// default calls it on every solve. An engine that can factor the
178 /// released system once and back-solve against it thereafter overrides
179 /// this, and the answers do not change.
180 fn released_operator<'a>(
181 &'a self,
182 released: &[usize],
183 ) -> Result<Box<dyn KktOperator + 'a>, String> {
184 Ok(Box::new(ReleasedBySolve {
185 bs: self,
186 released: released.to_vec(),
187 }))
188 }
189}
190
191/// [`BarrierIterate::released_operator`]'s default: every solve is a call
192/// to [`SensBacksolver::solve_released`].
193struct ReleasedBySolve<'a, B: SensBacksolver + ?Sized> {
194 bs: &'a B,
195 released: Vec<usize>,
196}
197
198impl<B: SensBacksolver + ?Sized> KktOperator for ReleasedBySolve<'_, B> {
199 fn solve(&self, rhs: &[Number], lhs: &mut [Number]) -> bool {
200 self.bs.solve_released(&self.released, rhs, lhs)
201 }
202}
203
204/// A factored KKT operator, solved against in natural units. The corrector
205/// iterates against one (see [`BarrierIterate::corrector_operator`]), and
206/// the bound-aware steps solve against a released one (see
207/// [`BarrierIterate::released_operator`]).
208pub trait KktOperator {
209 /// Solves `K · lhs = rhs` in natural units. Returns `false` on failure.
210 ///
211 /// The solve has to be refined against the matrix, as Ipopt's and
212 /// pounce's are. A bound held on its constraint gives `K` a diagonal entry
213 /// many orders above the rest, and an unrefined solve loses the digits
214 /// that entry costs. Where a step direction is roundoff-sized, that can
215 /// change its sign, and the corrector's iterations then end differently
216 /// from pounce's.
217 fn solve(&self, rhs: &[Number], lhs: &mut [Number]) -> bool;
218}
219
220/// The bounds on one side of one block: which entries carry a finite bound,
221/// and at each of them the multiplier and the slack, in that order.
222///
223/// `pos` indexes the block the bound constrains, the `x` block for a
224/// variable bound and the `s` block for an inequality-row bound. `mult[k]`
225/// and `slack[k]` belong to entry `pos[k]`. The slack is the distance to the
226/// bound the engine iterated against, which is the declared bound widened by
227/// whatever relaxation the engine applied.
228#[derive(Debug, Clone, Default)]
229pub struct BoundSide {
230 /// Entry of the constrained block, per bound.
231 pub pos: Vec<usize>,
232 /// The bound's multiplier, per bound.
233 pub mult: Vec<Number>,
234 /// The distance from the entry to its bound, per bound.
235 pub slack: Vec<Number>,
236}
237
238/// How an engine's variables and rows correspond to the model's.
239///
240/// An engine may drop variables whose bounds coincide, and it splits the
241/// model's rows into equality rows, the `y_c` block, and inequality rows, the
242/// `s` and `y_d` blocks, so neither numbering is the model's. Every result a
243/// caller reads in the model's indices goes through these maps.
244#[derive(Debug, Clone, PartialEq)]
245pub struct ModelMaps {
246 /// The number of variables the model declares, counting any the engine
247 /// removed because their bounds coincide.
248 pub n_full_x: usize,
249 /// The model's index of each variable the engine kept.
250 pub var_x_to_full_x: Vec<usize>,
251 /// For each of the model's rows, its position among the equality rows,
252 /// or `None` for an inequality row.
253 pub full_g_to_c: Vec<Option<usize>>,
254 /// For each of the model's rows, its position among the inequality rows,
255 /// or `None` for an equality row.
256 pub full_g_to_d: Vec<Option<usize>>,
257}
258
259impl ModelMaps {
260 /// The number of rows the model declares.
261 pub fn n_full_g(&self) -> usize {
262 self.full_g_to_c.len()
263 }
264
265 /// The engine's index of each of the model's variables, `None` for one
266 /// the engine removed.
267 pub fn full_x_to_var_x(&self) -> Vec<Option<usize>> {
268 let mut out = vec![None; self.n_full_x];
269 for (i, &f) in self.var_x_to_full_x.iter().enumerate() {
270 if let Some(slot) = out.get_mut(f) {
271 *slot = Some(i);
272 }
273 }
274 out
275 }
276
277 /// Checks the maps against the engine's block lengths: one model
278 /// variable per engine variable, each named once, and each model row in
279 /// exactly one of the two row blocks, filling each block once.
280 pub fn check(&self, dims: &[usize; 8]) -> Result<(), String> {
281 let (n, m_c, m_d) = (dims[0], dims[2], dims[3]);
282 if self.var_x_to_full_x.len() != n {
283 return Err(format!(
284 "model maps: {} entries in the variable map for {n} variables",
285 self.var_x_to_full_x.len()
286 ));
287 }
288 let mut seen = vec![false; self.n_full_x];
289 for &i in &self.var_x_to_full_x {
290 match seen.get_mut(i) {
291 Some(slot) if !*slot => *slot = true,
292 _ => {
293 return Err(format!(
294 "model maps: the variable map names variable {i} twice or out of {}",
295 self.n_full_x
296 ));
297 }
298 }
299 }
300 if self.full_g_to_d.len() != self.full_g_to_c.len() {
301 return Err(format!(
302 "model maps: {} rows in the equality map and {} in the inequality map",
303 self.full_g_to_c.len(),
304 self.full_g_to_d.len()
305 ));
306 }
307 let mut seen_c = vec![false; m_c];
308 let mut seen_d = vec![false; m_d];
309 for (g, (c, d)) in self.full_g_to_c.iter().zip(&self.full_g_to_d).enumerate() {
310 let (block, seen, p) = match (c, d) {
311 (Some(p), None) => ("equality", &mut seen_c, *p),
312 (None, Some(p)) => ("inequality", &mut seen_d, *p),
313 _ => {
314 return Err(format!(
315 "model maps: row {g} is not in exactly one of the two row blocks"
316 ));
317 }
318 };
319 match seen.get_mut(p) {
320 Some(slot) if !*slot => *slot = true,
321 _ => {
322 return Err(format!(
323 "model maps: the row map names {block} row {p} twice or out of {}",
324 seen.len()
325 ));
326 }
327 }
328 }
329 if seen_c.iter().chain(&seen_d).any(|s| !s) {
330 return Err("model maps: the row map leaves an engine row unnamed".into());
331 }
332 Ok(())
333 }
334}
335
336/// The scaling an engine applied to the model, as the factors it multiplied
337/// each quantity by.
338#[derive(Debug, Clone, PartialEq)]
339pub struct EngineScaling {
340 /// The factor the engine multiplied the objective by, `1.0` when it did
341 /// not, and negative for a maximization it turned into a minimization.
342 pub objective: Number,
343 /// The engine's change of variables `x̃ = d ⊙ x`, one factor per
344 /// variable the engine kept, when it made one.
345 pub variables: Option<Vec<Number>>,
346 /// The factor the engine multiplied each equality row by, when it did.
347 pub equality_rows: Option<Vec<Number>>,
348 /// The factor the engine multiplied each inequality row by, when it did.
349 pub inequality_rows: Option<Vec<Number>>,
350}
351
352impl EngineScaling {
353 /// No scaling at all.
354 pub fn none() -> Self {
355 Self {
356 objective: 1.0,
357 variables: None,
358 equality_rows: None,
359 inequality_rows: None,
360 }
361 }
362
363 /// Checks each factor vector against the block it scales.
364 pub fn check(&self, dims: &[usize; 8]) -> Result<(), String> {
365 for (what, v, len) in [
366 ("variable", &self.variables, dims[0]),
367 ("equality row", &self.equality_rows, dims[2]),
368 ("inequality row", &self.inequality_rows, dims[3]),
369 ] {
370 if let Some(v) = v
371 && v.len() != len
372 {
373 return Err(format!(
374 "engine scaling: {} {what} scale factors for {len} entries",
375 v.len()
376 ));
377 }
378 }
379 Ok(())
380 }
381}
382
383/// The converged quantities the activity classification reads, in the
384/// engine frame. See [`crate::activity`] for what it does with them. The
385/// barrier parameter comes from [`BarrierIterate::barrier_mu`], and the maps
386/// and scaling from [`BarrierIterate::model_maps`] and
387/// [`BarrierIterate::scaling`].
388pub struct ActivityData {
389 /// Lower variable bounds.
390 pub x_l: BoundSide,
391 /// Upper variable bounds.
392 pub x_u: BoundSide,
393 /// Lower bounds of the inequality rows.
394 pub d_l: BoundSide,
395 /// Upper bounds of the inequality rows.
396 pub d_u: BoundSide,
397 /// The barrier diagonal of the `x` block, one entry per variable the
398 /// engine kept, both sides summed.
399 ///
400 /// An engine that bounds the diagonal in no way of its own writes
401 /// `mult / slack` from [`Self::x_l`] and [`Self::x_u`], summed. pounce
402 /// writes the diagonal it factored instead, which differs from that sum
403 /// where the gh#737 ceiling capped an entry or a crossover point took the
404 /// declared-bound frame (gh#654).
405 pub sigma_x: Vec<Number>,
406 /// The barrier diagonal of the `s` block, one entry per inequality row,
407 /// from [`Self::d_l`] and [`Self::d_u`] as for [`Self::sigma_x`].
408 pub sigma_s: Vec<Number>,
409 /// The exact Hessian of the Lagrangian at the converged point.
410 pub hessian: Rc<dyn SymMatrix>,
411 /// The Jacobian of the equality rows at the converged point.
412 pub jac_c: Rc<dyn Matrix>,
413 /// The Jacobian of the inequality rows at the converged point.
414 pub jac_d: Rc<dyn Matrix>,
415}
416
417impl ActivityData {
418 /// Checks the lengths agree with each other, so the classification can
419 /// index without bounds failures on data an outside engine supplied.
420 pub fn check(&self) -> Result<(), String> {
421 let n = self.sigma_x.len();
422 let m_d = self.sigma_s.len();
423 let side = |name: &str, s: &BoundSide, len: usize| -> Result<(), String> {
424 if s.mult.len() != s.pos.len() || s.slack.len() != s.pos.len() {
425 return Err(format!(
426 "activity data: {name} has {} positions, {} multipliers and {} slacks",
427 s.pos.len(),
428 s.mult.len(),
429 s.slack.len()
430 ));
431 }
432 if let Some(&p) = s.pos.iter().find(|&&p| p >= len) {
433 return Err(format!(
434 "activity data: {name} names entry {p} of a block of {len}"
435 ));
436 }
437 Ok(())
438 };
439 side("x_l", &self.x_l, n)?;
440 side("x_u", &self.x_u, n)?;
441 side("d_l", &self.d_l, m_d)?;
442 side("d_u", &self.d_u, m_d)?;
443 if self.hessian.n_rows() as usize != n || self.hessian.n_cols() as usize != n {
444 return Err(format!(
445 "activity data: the Hessian is {}x{}, expected {n}x{n}",
446 self.hessian.n_rows(),
447 self.hessian.n_cols()
448 ));
449 }
450 if self.jac_d.n_rows() as usize != m_d || self.jac_d.n_cols() as usize != n {
451 return Err(format!(
452 "activity data: the inequality Jacobian is {}x{}, expected {m_d}x{n}",
453 self.jac_d.n_rows(),
454 self.jac_d.n_cols()
455 ));
456 }
457 if self.jac_c.n_cols() as usize != n {
458 return Err(format!(
459 "activity data: the equality Jacobian has {} columns, expected {n}",
460 self.jac_c.n_cols()
461 ));
462 }
463 Ok(())
464 }
465}
466
467#[cfg(test)]
468mod tests {
469 use super::*;
470 use pounce_linalg::triplet::{GenTMatrix, GenTMatrixSpace, SymTMatrix, SymTMatrixSpace};
471
472 /// Two variables, one equality row, one inequality row, a lower bound on
473 /// each variable and an upper bound on the inequality row: consistent in
474 /// every length.
475 fn data() -> ActivityData {
476 let hess = SymTMatrix::new(SymTMatrixSpace::new(2, vec![1, 2], vec![1, 2]));
477 let jac = |m| GenTMatrix::new(GenTMatrixSpace::new(m, 2, vec![1, 1], vec![1, 2]));
478 let side = |pos: Vec<usize>| BoundSide {
479 mult: vec![1.0; pos.len()],
480 slack: vec![1.0; pos.len()],
481 pos,
482 };
483 ActivityData {
484 x_l: side(vec![0, 1]),
485 x_u: side(vec![]),
486 d_l: side(vec![]),
487 d_u: side(vec![0]),
488 sigma_x: vec![1.0, 1.0],
489 sigma_s: vec![1.0],
490 hessian: Rc::new(hess),
491 jac_c: Rc::new(jac(1)),
492 jac_d: Rc::new(jac(1)),
493 }
494 }
495
496 /// The block lengths of [`data`]'s engine: two variables, one slack,
497 /// one equality and one inequality multiplier, two variable bounds and
498 /// one row bound.
499 const DIMS: [usize; 8] = [2, 1, 1, 1, 2, 0, 0, 1];
500
501 /// Three model variables, of which the engine dropped the second, and
502 /// two model rows, an inequality then an equality.
503 fn maps() -> ModelMaps {
504 ModelMaps {
505 n_full_x: 3,
506 var_x_to_full_x: vec![0, 2],
507 full_g_to_c: vec![None, Some(0)],
508 full_g_to_d: vec![Some(0), None],
509 }
510 }
511
512 #[test]
513 fn consistent_data_passes() {
514 assert_eq!(data().check(), Ok(()));
515 assert_eq!(maps().check(&DIMS), Ok(()));
516 assert_eq!(EngineScaling::none().check(&DIMS), Ok(()));
517 }
518
519 #[test]
520 fn a_bound_side_with_mismatched_lengths_is_refused() {
521 let mut d = data();
522 d.x_l.slack.pop();
523 assert!(d.check().unwrap_err().contains("x_l"));
524 let mut d = data();
525 d.d_u.mult.push(1.0);
526 assert!(d.check().unwrap_err().contains("d_u"));
527 }
528
529 #[test]
530 fn a_bound_outside_its_block_is_refused() {
531 let mut d = data();
532 d.x_l.pos[1] = 2;
533 assert!(d.check().unwrap_err().contains("entry 2"));
534 let mut d = data();
535 d.d_u.pos[0] = 1;
536 assert!(d.check().unwrap_err().contains("entry 1"));
537 }
538
539 #[test]
540 fn matrices_of_the_wrong_shape_are_refused() {
541 let mut d = data();
542 d.hessian = Rc::new(SymTMatrix::new(SymTMatrixSpace::new(3, vec![], vec![])));
543 assert!(d.check().unwrap_err().contains("Hessian"));
544 let mut d = data();
545 d.jac_d = Rc::new(GenTMatrix::new(GenTMatrixSpace::new(1, 3, vec![], vec![])));
546 assert!(d.check().unwrap_err().contains("inequality Jacobian"));
547 let mut d = data();
548 d.jac_c = Rc::new(GenTMatrix::new(GenTMatrixSpace::new(1, 3, vec![], vec![])));
549 assert!(d.check().unwrap_err().contains("equality Jacobian"));
550 }
551
552 #[test]
553 fn scale_factors_of_the_wrong_length_are_refused() {
554 for (scaling, what) in [
555 (
556 EngineScaling {
557 variables: Some(vec![1.0]),
558 ..EngineScaling::none()
559 },
560 "variable scale",
561 ),
562 (
563 EngineScaling {
564 equality_rows: Some(vec![1.0, 1.0]),
565 ..EngineScaling::none()
566 },
567 "equality row scale",
568 ),
569 (
570 EngineScaling {
571 inequality_rows: Some(vec![]),
572 ..EngineScaling::none()
573 },
574 "inequality row scale",
575 ),
576 ] {
577 assert!(scaling.check(&DIMS).unwrap_err().contains(what), "{what}");
578 }
579 }
580
581 #[test]
582 fn a_variable_map_that_repeats_overruns_or_has_the_wrong_length_is_refused() {
583 let mut m = maps();
584 m.var_x_to_full_x.pop();
585 assert!(m.check(&DIMS).unwrap_err().contains("variable map for"));
586 let mut m = maps();
587 m.var_x_to_full_x[1] = 3;
588 assert!(m.check(&DIMS).unwrap_err().contains("names variable 3"));
589 let mut m = maps();
590 m.var_x_to_full_x[1] = 0;
591 assert!(
592 m.check(&DIMS)
593 .unwrap_err()
594 .contains("names variable 0 twice")
595 );
596 }
597
598 #[test]
599 fn a_row_map_that_repeats_skips_splits_or_overruns_is_refused() {
600 let mut m = maps();
601 m.full_g_to_d = vec![Some(0), Some(0)];
602 assert!(m.check(&DIMS).unwrap_err().contains("exactly one"));
603 let mut m = maps();
604 m.full_g_to_d = vec![Some(1), None];
605 assert!(m.check(&DIMS).unwrap_err().contains("twice or out of"));
606 let mut m = maps();
607 m.full_g_to_c = vec![None, Some(0), Some(0)];
608 assert!(m.check(&DIMS).unwrap_err().contains("equality map"));
609 let mut m = maps();
610 m.full_g_to_c = vec![None, None];
611 m.full_g_to_d = vec![Some(0), None];
612 assert!(m.check(&DIMS).unwrap_err().contains("exactly one"));
613 let mut m = maps();
614 m.full_g_to_c = vec![Some(0), Some(0)];
615 m.full_g_to_d = vec![None, None];
616 assert!(m.check(&DIMS).unwrap_err().contains("twice or out of"));
617 let m = ModelMaps {
618 full_g_to_c: vec![Some(0)],
619 full_g_to_d: vec![None],
620 ..maps()
621 };
622 assert!(m.check(&DIMS).unwrap_err().contains("unnamed"));
623 }
624
625 #[test]
626 fn the_inverse_variable_map_marks_a_dropped_variable() {
627 assert_eq!(maps().full_x_to_var_x(), vec![Some(0), None, Some(1)]);
628 assert_eq!(maps().n_full_g(), 2);
629 }
630}