1use crate::linear_constraints::LinearInequalityConstraints;
25use ndarray::{Array1, Array2, ArrayView1};
26use std::sync::Arc;
27
28#[derive(Clone, Debug)]
40pub struct KhatriRaoConeConstraints {
41 factor: Arc<Array2<f64>>,
43 factor_row_norms: Array1<f64>,
45 coupled_rows: Vec<usize>,
47 p_left: usize,
49 bounds: Option<Array1<f64>>,
53}
54
55impl KhatriRaoConeConstraints {
56 pub fn new(
57 factor: Arc<Array2<f64>>,
58 coupled_rows: Vec<usize>,
59 p_left: usize,
60 ) -> Result<Self, String> {
61 if factor.nrows() == 0 || factor.ncols() == 0 {
62 return Err("KhatriRaoConeConstraints: factor must be non-empty".to_string());
63 }
64 if factor.iter().any(|v| !v.is_finite()) {
65 return Err("KhatriRaoConeConstraints: factor must be finite".to_string());
66 }
67 if coupled_rows.is_empty() {
68 return Err(
69 "KhatriRaoConeConstraints: at least one coupled coefficient row is required"
70 .to_string(),
71 );
72 }
73 let mut seen = vec![false; p_left];
74 for &k in &coupled_rows {
75 if k >= p_left {
76 return Err(format!(
77 "KhatriRaoConeConstraints: coupled row {k} out of range (p_left = {p_left})"
78 ));
79 }
80 if seen[k] {
81 return Err(format!(
82 "KhatriRaoConeConstraints: coupled row {k} is duplicated"
83 ));
84 }
85 seen[k] = true;
86 }
87 let factor_row_norms = Array1::from_iter(
88 factor
89 .rows()
90 .into_iter()
91 .map(|row| row.dot(&row).sqrt()),
92 );
93 Ok(Self {
94 factor,
95 factor_row_norms,
96 coupled_rows,
97 p_left,
98 bounds: None,
99 })
100 }
101
102 pub fn factor(&self) -> &Array2<f64> {
103 self.factor.as_ref()
104 }
105
106 pub fn coupled_rows(&self) -> &[usize] {
107 &self.coupled_rows
108 }
109
110 pub fn p_left(&self) -> usize {
111 self.p_left
112 }
113
114 pub fn single_coupled_slot(&self, slot: usize) -> Result<Self, String> {
120 if slot >= self.coupled_rows.len() {
121 return Err(format!(
122 "KhatriRaoConeConstraints: coupled slot {slot} out of range ({} slots)",
123 self.coupled_rows.len()
124 ));
125 }
126 let n = self.factor.nrows();
127 let bounds = self.bounds.as_ref().map(|all| {
128 all.slice(ndarray::s![slot * n..(slot + 1) * n])
129 .to_owned()
130 });
131 Ok(Self {
132 factor: Arc::clone(&self.factor),
133 factor_row_norms: self.factor_row_norms.clone(),
134 coupled_rows: vec![0],
135 p_left: 1,
136 bounds,
137 })
138 }
139
140 pub fn nrows(&self) -> usize {
141 self.coupled_rows.len() * self.factor.nrows()
142 }
143
144 pub fn ncols(&self) -> usize {
145 self.p_left * self.factor.ncols()
146 }
147
148 #[inline]
150 fn split_row_id(&self, row: usize) -> Result<(usize, usize), String> {
151 let n = self.factor.nrows();
152 let slot = row / n;
153 if slot >= self.coupled_rows.len() {
154 return Err(format!(
155 "KhatriRaoConeConstraints: row id {row} out of range ({} rows)",
156 self.nrows()
157 ));
158 }
159 Ok((slot, row % n))
160 }
161
162 pub fn values(&self, beta: ArrayView1<'_, f64>) -> Result<Array1<f64>, String> {
168 let p_cov = self.factor.ncols();
169 if beta.len() != self.ncols() {
170 return Err(format!(
171 "KhatriRaoConeConstraints: beta length {} != {}",
172 beta.len(),
173 self.ncols()
174 ));
175 }
176 let n = self.factor.nrows();
177 let mut out = Array1::<f64>::zeros(self.nrows());
178 for (slot, &k) in self.coupled_rows.iter().enumerate() {
179 let block = beta.slice(ndarray::s![k * p_cov..(k + 1) * p_cov]);
180 let alpha = self.factor.dot(&block);
181 out.slice_mut(ndarray::s![slot * n..(slot + 1) * n])
182 .assign(&alpha);
183 }
184 Ok(out)
185 }
186
187 pub fn row_norm(&self, row: usize) -> Result<f64, String> {
191 let (_, i) = self.split_row_id(row)?;
192 Ok(self.factor_row_norms[i])
193 }
194
195 pub fn row_column_support(&self, row: usize) -> Result<Vec<usize>, String> {
202 let (slot, i) = self.split_row_id(row)?;
203 let p_cov = self.factor.ncols();
204 let base = self.coupled_rows[slot] * p_cov;
205 Ok((0..p_cov)
206 .filter(|&j| self.factor[[i, j]] != 0.0)
207 .map(|j| base + j)
208 .collect())
209 }
210
211 pub fn bound(&self, row: usize) -> Result<f64, String> {
214 self.split_row_id(row)?;
215 Ok(self.bounds.as_ref().map_or(0.0, |bounds| bounds[row]))
216 }
217
218 pub fn gather_rows(&self, rows: &[usize]) -> Result<LinearInequalityConstraints, String> {
223 let p_cov = self.factor.ncols();
224 let mut a = Array2::<f64>::zeros((rows.len(), self.ncols()));
225 let mut b = Array1::<f64>::zeros(rows.len());
226 for (out_row, &row) in rows.iter().enumerate() {
227 let (slot, i) = self.split_row_id(row)?;
228 let k = self.coupled_rows[slot];
229 a.row_mut(out_row)
230 .slice_mut(ndarray::s![k * p_cov..(k + 1) * p_cov])
231 .assign(&self.factor.row(i));
232 b[out_row] = self.bound(row)?;
233 }
234 LinearInequalityConstraints::new(a, b)
235 }
236
237 pub fn to_dense(&self) -> Result<LinearInequalityConstraints, String> {
240 let all: Vec<usize> = (0..self.nrows()).collect();
241 self.gather_rows(&all)
242 }
243}
244
245#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
261pub struct ConstraintRowId(pub usize);
262
263impl ConstraintRowId {
264 #[inline]
268 pub fn index(self) -> usize {
269 self.0
270 }
271}
272
273#[derive(Clone, Debug)]
277pub struct PlacedConstraintBlock {
278 pub col_start: usize,
279 pub set: ConstraintSet,
280}
281
282#[derive(Clone, Debug)]
284pub enum ConstraintSet {
285 Dense(LinearInequalityConstraints),
287 KhatriRaoCone(KhatriRaoConeConstraints),
289 BlockDiagonal {
293 blocks: Vec<PlacedConstraintBlock>,
294 total_cols: usize,
295 },
296}
297
298impl ConstraintSet {
299 pub fn block_diagonal(
302 blocks: Vec<PlacedConstraintBlock>,
303 total_cols: usize,
304 ) -> Result<Self, String> {
305 let mut ranges: Vec<(usize, usize)> = Vec::with_capacity(blocks.len());
306 for block in &blocks {
307 let end = block.col_start + block.set.ncols();
308 if end > total_cols {
309 return Err(format!(
310 "ConstraintSet::block_diagonal: block columns {}..{} exceed joint width {}",
311 block.col_start, end, total_cols
312 ));
313 }
314 ranges.push((block.col_start, end));
315 }
316 ranges.sort_unstable();
317 for pair in ranges.windows(2) {
318 if pair[1].0 < pair[0].1 {
319 return Err(format!(
320 "ConstraintSet::block_diagonal: overlapping column ranges {:?} and {:?}",
321 pair[0], pair[1]
322 ));
323 }
324 }
325 Ok(ConstraintSet::BlockDiagonal { blocks, total_cols })
326 }
327
328 fn block_for_row<'a>(
330 blocks: &'a [PlacedConstraintBlock],
331 row: usize,
332 ) -> Result<(&'a PlacedConstraintBlock, usize), String> {
333 let mut offset = 0usize;
334 for block in blocks {
335 let rows = block.set.nrows();
336 if row < offset + rows {
337 return Ok((block, row - offset));
338 }
339 offset += rows;
340 }
341 Err(format!(
342 "ConstraintSet: row {row} out of range ({offset} rows)"
343 ))
344 }
345
346 pub fn nrows(&self) -> usize {
347 match self {
348 ConstraintSet::Dense(dense) => dense.a.nrows(),
349 ConstraintSet::KhatriRaoCone(cone) => cone.nrows(),
350 ConstraintSet::BlockDiagonal { blocks, .. } => {
351 blocks.iter().map(|block| block.set.nrows()).sum()
352 }
353 }
354 }
355
356 pub fn ncols(&self) -> usize {
357 match self {
358 ConstraintSet::Dense(dense) => dense.a.ncols(),
359 ConstraintSet::KhatriRaoCone(cone) => cone.ncols(),
360 ConstraintSet::BlockDiagonal { total_cols, .. } => *total_cols,
361 }
362 }
363
364 pub fn values(&self, beta: ArrayView1<'_, f64>) -> Result<Array1<f64>, String> {
366 match self {
367 ConstraintSet::Dense(dense) => {
368 if beta.len() != dense.a.ncols() {
369 return Err(format!(
370 "ConstraintSet: beta length {} != {}",
371 beta.len(),
372 dense.a.ncols()
373 ));
374 }
375 Ok(dense.a.dot(&beta))
376 }
377 ConstraintSet::KhatriRaoCone(cone) => cone.values(beta),
378 ConstraintSet::BlockDiagonal { blocks, total_cols } => {
379 if beta.len() != *total_cols {
380 return Err(format!(
381 "ConstraintSet: beta length {} != {}",
382 beta.len(),
383 total_cols
384 ));
385 }
386 let mut out = Array1::<f64>::zeros(self.nrows());
387 let mut offset = 0usize;
388 for block in blocks {
389 let width = block.set.ncols();
390 let local = beta.slice(ndarray::s![
391 block.col_start..block.col_start + width
392 ]);
393 let values = block.set.values(local)?;
394 let rows = values.len();
395 out.slice_mut(ndarray::s![offset..offset + rows]).assign(&values);
396 offset += rows;
397 }
398 Ok(out)
399 }
400 }
401 }
402
403 pub fn bound(&self, row: usize) -> Result<f64, String> {
405 match self {
406 ConstraintSet::Dense(dense) => {
407 dense.b.get(row).copied().ok_or_else(|| {
408 format!(
409 "ConstraintSet: row {row} out of range ({} rows)",
410 dense.b.len()
411 )
412 })
413 }
414 ConstraintSet::KhatriRaoCone(cone) => cone.bound(row),
415 ConstraintSet::BlockDiagonal { blocks, .. } => {
416 let (block, local) = Self::block_for_row(blocks, row)?;
417 block.set.bound(local)
418 }
419 }
420 }
421
422 pub fn row_norm(&self, row: usize) -> Result<f64, String> {
423 match self {
424 ConstraintSet::Dense(dense) => {
425 if row >= dense.a.nrows() {
426 return Err(format!(
427 "ConstraintSet: row {row} out of range ({} rows)",
428 dense.a.nrows()
429 ));
430 }
431 let r = dense.a.row(row);
432 Ok(r.dot(&r).sqrt())
433 }
434 ConstraintSet::KhatriRaoCone(cone) => cone.row_norm(row),
435 ConstraintSet::BlockDiagonal { blocks, .. } => {
436 let (block, local) = Self::block_for_row(blocks, row)?;
437 block.set.row_norm(local)
438 }
439 }
440 }
441
442 pub fn row_column_support(&self, row: ConstraintRowId) -> Result<Vec<usize>, String> {
455 let row = row.index();
456 match self {
457 ConstraintSet::Dense(dense) => {
458 if row >= dense.a.nrows() {
459 return Err(format!(
460 "ConstraintSet: row {row} out of range ({} rows)",
461 dense.a.nrows()
462 ));
463 }
464 Ok(dense
465 .a
466 .row(row)
467 .iter()
468 .enumerate()
469 .filter(|(_, value)| **value != 0.0)
470 .map(|(col, _)| col)
471 .collect())
472 }
473 ConstraintSet::KhatriRaoCone(cone) => cone.row_column_support(row),
474 ConstraintSet::BlockDiagonal { blocks, .. } => {
475 let (block, local) = Self::block_for_row(blocks, row)?;
476 let mut cols = block.set.row_column_support(ConstraintRowId(local))?;
480 for col in &mut cols {
481 *col += block.col_start;
482 }
483 Ok(cols)
484 }
485 }
486 }
487
488 pub fn shifted_to_delta(&self, beta: ArrayView1<'_, f64>) -> Result<Self, String> {
492 let values = self.values(beta)?;
493 match self {
494 ConstraintSet::Dense(dense) => Ok(ConstraintSet::Dense(
495 LinearInequalityConstraints::new(dense.a.clone(), &dense.b - &values)?,
496 )),
497 ConstraintSet::KhatriRaoCone(cone) => {
498 let mut shifted = cone.clone();
499 let base = shifted
500 .bounds
501 .take()
502 .unwrap_or_else(|| Array1::zeros(values.len()));
503 shifted.bounds = Some(&base - &values);
504 Ok(ConstraintSet::KhatriRaoCone(shifted))
505 }
506 ConstraintSet::BlockDiagonal { blocks, total_cols } => {
507 let mut shifted_blocks = Vec::with_capacity(blocks.len());
508 for block in blocks {
509 let width = block.set.ncols();
510 let local = beta.slice(ndarray::s![
511 block.col_start..block.col_start + width
512 ]);
513 shifted_blocks.push(PlacedConstraintBlock {
514 col_start: block.col_start,
515 set: block.set.shifted_to_delta(local)?,
516 });
517 }
518 Ok(ConstraintSet::BlockDiagonal {
519 blocks: shifted_blocks,
520 total_cols: *total_cols,
521 })
522 }
523 }
524 }
525
526 pub fn max_scaled_violation(
530 &self,
531 beta: ArrayView1<'_, f64>,
532 ) -> Result<(f64, Option<usize>), String> {
533 let values = self.values(beta)?;
534 let mut worst = 0.0_f64;
535 let mut worst_row = None;
536 for (row, &value) in values.iter().enumerate() {
537 let norm = self.row_norm(row)?;
538 if norm <= 0.0 {
539 continue;
540 }
541 let violation = (self.bound(row)? - value) / norm;
542 if violation > worst {
543 worst = violation;
544 worst_row = Some(row);
545 }
546 }
547 Ok((worst, worst_row))
548 }
549
550 pub fn max_feasible_step(
555 &self,
556 beta: ArrayView1<'_, f64>,
557 delta: ArrayView1<'_, f64>,
558 skip_rows: &[usize],
559 ) -> Result<(f64, Option<usize>), String> {
560 let values = self.values(beta)?;
561 let directional = self.values(delta)?;
562 let mut skip = vec![false; values.len()];
563 for &row in skip_rows {
564 if row < skip.len() {
565 skip[row] = true;
566 }
567 }
568 let mut step = 1.0_f64;
569 let mut blocking = None;
570 for row in 0..values.len() {
571 if skip[row] {
572 continue;
573 }
574 let norm = self.row_norm(row)?;
575 if norm <= 0.0 {
576 continue;
577 }
578 let slack = values[row] - self.bound(row)?;
579 let rate = directional[row];
580 if rate >= 0.0 {
581 continue;
582 }
583 let t = slack / (-rate);
584 if t < step {
585 step = t.max(0.0);
586 blocking = Some(row);
587 }
588 }
589 Ok((step, blocking))
590 }
591
592 pub fn gather_rows(&self, rows: &[usize]) -> Result<LinearInequalityConstraints, String> {
594 match self {
595 ConstraintSet::Dense(dense) => {
596 let mut a = Array2::<f64>::zeros((rows.len(), dense.a.ncols()));
597 let mut b = Array1::<f64>::zeros(rows.len());
598 for (out_row, &row) in rows.iter().enumerate() {
599 if row >= dense.a.nrows() {
600 return Err(format!(
601 "ConstraintSet: row {row} out of range ({} rows)",
602 dense.a.nrows()
603 ));
604 }
605 a.row_mut(out_row).assign(&dense.a.row(row));
606 b[out_row] = dense.b[row];
607 }
608 LinearInequalityConstraints::new(a, b)
609 }
610 ConstraintSet::KhatriRaoCone(cone) => cone.gather_rows(rows),
611 ConstraintSet::BlockDiagonal { blocks, total_cols } => {
612 let mut a = Array2::<f64>::zeros((rows.len(), *total_cols));
613 let mut b = Array1::<f64>::zeros(rows.len());
614 for (out_row, &row) in rows.iter().enumerate() {
615 let (block, local) = Self::block_for_row(blocks, row)?;
616 let gathered = block.set.gather_rows(&[local])?;
617 a.row_mut(out_row)
618 .slice_mut(ndarray::s![
619 block.col_start..block.col_start + block.set.ncols()
620 ])
621 .assign(&gathered.a.row(0));
622 b[out_row] = gathered.b[0];
623 }
624 LinearInequalityConstraints::new(a, b)
625 }
626 }
627 }
628
629 pub fn to_dense(&self) -> Result<LinearInequalityConstraints, String> {
631 match self {
632 ConstraintSet::Dense(dense) => Ok(dense.clone()),
633 _ => {
634 let all: Vec<usize> = (0..self.nrows()).collect();
635 self.gather_rows(&all)
636 }
637 }
638 }
639}
640
641impl From<LinearInequalityConstraints> for ConstraintSet {
642 fn from(dense: LinearInequalityConstraints) -> Self {
643 ConstraintSet::Dense(dense)
644 }
645}
646
647#[cfg(test)]
648mod tests {
649 use super::*;
650 use ndarray::array;
651
652 fn cone_fixture() -> KhatriRaoConeConstraints {
653 let psi = array![[1.0_f64, 0.5], [2.0, -1.0], [0.0, 3.0]];
656 KhatriRaoConeConstraints::new(Arc::new(psi), vec![1, 2], 3).expect("cone fixture")
657 }
658
659 fn beta_fixture() -> Array1<f64> {
660 array![9.0_f64, -4.0, 1.0, 2.0, 0.5, -0.25]
662 }
663
664 #[test]
665 fn cone_values_match_dense_system() {
666 let cone = cone_fixture();
667 let set = ConstraintSet::KhatriRaoCone(cone.clone());
668 let dense = ConstraintSet::Dense(cone.to_dense().expect("dense"));
669 let beta = beta_fixture();
670 let via_cone = set.values(beta.view()).expect("cone values");
671 let via_dense = dense.values(beta.view()).expect("dense values");
672 assert_eq!(via_cone.len(), 6);
673 for (a, b) in via_cone.iter().zip(via_dense.iter()) {
674 assert!((a - b).abs() < 1e-14, "cone/dense mismatch: {a} vs {b}");
675 }
676 assert!((via_cone[1] - 0.0).abs() < 1e-15);
679 }
680
681 #[test]
685 fn cone_row_column_support_matches_the_dense_row_nonzeros() {
686 let cone = cone_fixture();
687 let set = ConstraintSet::KhatriRaoCone(cone.clone());
688 let dense = ConstraintSet::Dense(cone.to_dense().expect("dense"));
689 for row in 0..set.nrows() {
690 let via_cone = set
691 .row_column_support(ConstraintRowId(row))
692 .expect("cone support");
693 let via_dense = dense
694 .row_column_support(ConstraintRowId(row))
695 .expect("dense support");
696 assert_eq!(via_cone, via_dense, "row {row} support mismatch");
697 }
698 assert_eq!(
701 set.row_column_support(ConstraintRowId(0)).expect("r0"),
702 vec![2, 3]
703 );
704 assert_eq!(
705 set.row_column_support(ConstraintRowId(2)).expect("r2"),
706 vec![3]
707 );
708 assert_eq!(
710 set.row_column_support(ConstraintRowId(3)).expect("r3"),
711 vec![4, 5]
712 );
713 }
714
715 #[test]
720 fn block_diagonal_row_column_support_uses_col_start_not_the_row_offset() {
721 let narrow = PlacedConstraintBlock {
722 col_start: 0,
723 set: ConstraintSet::Dense(
724 LinearInequalityConstraints::new(
725 array![[1.0_f64, 0.0, 0.0]],
726 Array1::<f64>::zeros(1),
727 )
728 .expect("narrow"),
729 ),
730 };
731 let square = PlacedConstraintBlock {
732 col_start: 3,
733 set: ConstraintSet::Dense(
734 LinearInequalityConstraints::new(
735 array![[1.0_f64, 0.0], [0.0, 1.0]],
736 Array1::<f64>::zeros(2),
737 )
738 .expect("square"),
739 ),
740 };
741 let set = ConstraintSet::block_diagonal(vec![narrow, square], 5).expect("joint");
742 assert_eq!(set.nrows(), 3);
743 assert_eq!(set.ncols(), 5);
744 assert_eq!(
745 set.row_column_support(ConstraintRowId(0)).expect("r0"),
746 vec![0]
747 );
748 assert_eq!(
750 set.row_column_support(ConstraintRowId(1)).expect("r1"),
751 vec![3]
752 );
753 assert_eq!(
754 set.row_column_support(ConstraintRowId(2)).expect("r2"),
755 vec![4]
756 );
757 assert!(set.row_column_support(ConstraintRowId(3)).is_err());
758 }
759
760 #[test]
761 fn cone_row_norms_are_factor_row_norms_for_every_slot() {
762 let cone = cone_fixture();
763 let set = ConstraintSet::KhatriRaoCone(cone);
764 let expected = [
765 (1.0_f64 + 0.25).sqrt(),
766 (4.0_f64 + 1.0).sqrt(),
767 3.0_f64,
768 ];
769 for slot in 0..2 {
770 for i in 0..3 {
771 let norm = set.row_norm(slot * 3 + i).expect("norm");
772 assert!((norm - expected[i]).abs() < 1e-15);
773 }
774 }
775 }
776
777 #[test]
778 fn max_scaled_violation_agrees_with_canonicalized_dense() {
779 let cone = cone_fixture();
780 let set = ConstraintSet::KhatriRaoCone(cone.clone());
781 let beta = beta_fixture();
782 let (violation, row) = set.max_scaled_violation(beta.view()).expect("violation");
783 let dense = cone.to_dense().expect("dense").canonicalized().expect("canon");
785 let values = dense.a.dot(&beta);
786 let mut worst = 0.0_f64;
787 let mut worst_row = None;
788 for r in 0..values.len() {
789 let v = dense.b[r] - values[r];
790 if v > worst {
791 worst = v;
792 worst_row = Some(r);
793 }
794 }
795 assert!((violation - worst).abs() < 1e-14);
796 assert_eq!(row, worst_row);
797 assert!(violation > 0.0, "fixture must have a violated row");
798 }
799
800 #[test]
801 fn max_feasible_step_matches_scalar_ratio_test() {
802 let cone = cone_fixture();
803 let set = ConstraintSet::KhatriRaoCone(cone);
804 let beta = array![0.0_f64, 0.0, 1.0, 0.1, 1.0, 0.1];
808 let delta = array![0.0_f64, 0.0, 0.0, -1.0, 0.0, 0.0];
813 let (step, blocking) = set
814 .max_feasible_step(beta.view(), delta.view(), &[])
815 .expect("step");
816 assert!((step - 0.1).abs() < 1e-14, "expected 0.1, got {step}");
817 assert_eq!(blocking, Some(2));
818 let (step_skipped, blocking_skipped) = set
820 .max_feasible_step(beta.view(), delta.view(), &[2])
821 .expect("step skipped");
822 assert!((step_skipped - 1.0).abs() < 1e-14);
823 assert_eq!(blocking_skipped, None);
824 }
825
826 #[test]
827 fn gather_rows_places_factor_rows_in_the_coupled_slot() {
828 let cone = cone_fixture();
829 let gathered = cone.gather_rows(&[4]).expect("gather");
831 assert_eq!(gathered.a.nrows(), 1);
832 assert_eq!(gathered.a.ncols(), 6);
833 let expected = [0.0, 0.0, 0.0, 0.0, 2.0, -1.0];
834 for (j, &e) in expected.iter().enumerate() {
835 assert_eq!(gathered.a[[0, j]], e);
836 }
837 assert_eq!(gathered.b[0], 0.0);
838 }
839
840 #[test]
841 fn constructor_rejects_bad_coupled_rows() {
842 let psi = array![[1.0_f64, 0.0], [0.0, 1.0]];
843 assert!(KhatriRaoConeConstraints::new(Arc::new(psi.clone()), vec![3], 3).is_err());
844 assert!(
845 KhatriRaoConeConstraints::new(Arc::new(psi.clone()), vec![1, 1], 3).is_err()
846 );
847 assert!(KhatriRaoConeConstraints::new(Arc::new(psi), vec![], 3).is_err());
848 }
849
850 #[test]
851 fn shifted_to_delta_matches_dense_shift() {
852 let cone = cone_fixture();
853 let set = ConstraintSet::KhatriRaoCone(cone);
854 let beta = beta_fixture();
855 let shifted = set.shifted_to_delta(beta.view()).expect("shift");
856 let dense = set.to_dense().expect("dense");
858 let expected_b = &dense.b - &dense.a.dot(&beta);
859 for row in 0..set.nrows() {
860 assert!(
861 (shifted.bound(row).expect("bound") - expected_b[row]).abs() < 1e-14,
862 "shifted bound mismatch at row {row}"
863 );
864 }
865 let zero = Array1::<f64>::zeros(set.ncols());
867 let (viol_delta, row_delta) = shifted
868 .max_scaled_violation(zero.view())
869 .expect("delta violation");
870 let (viol_orig, row_orig) = set.max_scaled_violation(beta.view()).expect("violation");
871 assert!((viol_delta - viol_orig).abs() < 1e-14);
872 assert_eq!(row_delta, row_orig);
873 }
874
875 #[test]
876 fn block_diagonal_composes_ids_bounds_and_values() {
877 let dense = LinearInequalityConstraints::new(
879 array![[1.0_f64, 0.0], [0.0, -2.0]],
880 array![0.5_f64, -1.0],
881 )
882 .expect("dense block");
883 let cone = cone_fixture();
884 let joint = ConstraintSet::block_diagonal(
885 vec![
886 PlacedConstraintBlock {
887 col_start: 0,
888 set: ConstraintSet::Dense(dense.clone()),
889 },
890 PlacedConstraintBlock {
891 col_start: 2,
892 set: ConstraintSet::KhatriRaoCone(cone.clone()),
893 },
894 ],
895 8,
896 )
897 .expect("joint");
898 assert_eq!(joint.nrows(), 2 + 6);
899 assert_eq!(joint.ncols(), 8);
900 let mut beta = Array1::<f64>::zeros(8);
901 beta[0] = 2.0;
902 beta[1] = 1.0;
903 beta.slice_mut(ndarray::s![2..8]).assign(&beta_fixture());
904 let values = joint.values(beta.view()).expect("values");
905 assert!((values[0] - 2.0).abs() < 1e-15);
906 assert!((values[1] + 2.0).abs() < 1e-15);
907 let cone_values = cone.values(beta_fixture().view()).expect("cone values");
908 for (idx, &cv) in cone_values.iter().enumerate() {
909 assert!((values[2 + idx] - cv).abs() < 1e-15);
910 }
911 assert_eq!(joint.bound(0).expect("b0"), 0.5);
912 assert_eq!(joint.bound(2).expect("b2"), 0.0);
913 let gathered = joint.gather_rows(&[3]).expect("gather");
915 assert_eq!(gathered.a.ncols(), 8);
916 assert_eq!(gathered.a[[0, 4]], 2.0);
917 assert_eq!(gathered.a[[0, 5]], -1.0);
918 assert!(
920 ConstraintSet::block_diagonal(
921 vec![
922 PlacedConstraintBlock {
923 col_start: 0,
924 set: ConstraintSet::Dense(dense.clone()),
925 },
926 PlacedConstraintBlock {
927 col_start: 1,
928 set: ConstraintSet::Dense(dense),
929 },
930 ],
931 8,
932 )
933 .is_err()
934 );
935 }
936
937 #[test]
938 fn zero_factor_rows_are_vacuous_not_violations() {
939 let psi = array![[0.0_f64, 0.0], [1.0, 1.0]];
943 let cone = KhatriRaoConeConstraints::new(Arc::new(psi), vec![1], 2).expect("cone");
944 let set = ConstraintSet::KhatriRaoCone(cone);
945 let beta = array![0.0_f64, 0.0, -5.0, 4.0];
946 let (violation, row) = set.max_scaled_violation(beta.view()).expect("violation");
948 assert_eq!(row, Some(1));
949 assert!((violation - 1.0 / 2.0_f64.sqrt()).abs() < 1e-14);
950 }
951}