Skip to main content

problemreductions/models/algebraic/
feasible_basis_extension.rs

1//! Feasible Basis Extension problem implementation.
2//!
3//! Given an m x n integer matrix A (m < n), a column vector a_bar of length m,
4//! and a subset S of column indices with |S| < m, determine whether there exists
5//! a feasible basis B (a set of m column indices including S) such that the
6//! m x m submatrix A_B is nonsingular and A_B^{-1} a_bar >= 0.
7//!
8//! NP-complete (Murty, 1972).
9
10use crate::registry::{CreateSpec, ProblemSchemaEntry};
11use crate::traits::Problem;
12use serde::{Deserialize, Serialize};
13
14inventory::submit! {
15    ProblemSchemaEntry {
16        name: "FeasibleBasisExtension",
17        display_name: "Feasible Basis Extension",
18        aliases: &[],
19        dimensions: &[],
20        category: crate::registry::ProblemCategory::Algebraic,
21        module_path: module_path!(),
22        description: "Given matrix A, vector a_bar, and required columns S, find a feasible basis extending S",
23        fields: FeasibleBasisExtensionCreateSpec::FIELDS,
24    }
25}
26
27/// The Feasible Basis Extension problem.
28///
29/// Given an m x n integer matrix A with m < n, a column vector a_bar of length m,
30/// and a subset S of column indices with |S| < m, determine whether there exists
31/// a feasible basis B of m columns (including all of S) such that the submatrix
32/// A_B is nonsingular and A_B^{-1} a_bar >= 0.
33///
34/// # Representation
35///
36/// Each non-required column has a binary variable: `x_j = 1` if column j is
37/// selected. A valid config must select exactly m - |S| additional columns.
38/// The problem is satisfiable iff the resulting A_B is nonsingular and
39/// A_B^{-1} a_bar >= 0.
40///
41/// # Example
42///
43/// ```
44/// use problemreductions::models::algebraic::FeasibleBasisExtension;
45/// use problemreductions::{Problem, BruteForce};
46///
47/// let matrix = vec![
48///     vec![1, 0, 1, 2, -1, 0],
49///     vec![0, 1, 0, 1,  1, 2],
50///     vec![0, 0, 1, 1,  0, 1],
51/// ];
52/// let rhs = vec![7, 5, 3];
53/// let required = vec![0, 1];
54/// let problem = FeasibleBasisExtension::new(matrix, rhs, required);
55/// let solver = BruteForce::new();
56/// let solution = solver.solve(&problem).unwrap();
57/// assert!(solution.is_some());
58/// ```
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct FeasibleBasisExtension {
61    matrix: Vec<Vec<i64>>,
62    rhs: Vec<i64>,
63    required_columns: Vec<usize>,
64}
65
66#[derive(Debug, Deserialize, crate::CreateSpec)]
67struct FeasibleBasisExtensionCreateSpec {
68    /// Integer matrix as JSON.
69    #[create(codec = "json")]
70    matrix: Vec<Vec<i64>>,
71    /// Right-hand side vector.
72    #[create(codec = "comma-separated")]
73    rhs: Vec<i64>,
74    /// Required column indices.
75    #[create(codec = "comma-separated")]
76    required_columns: Vec<usize>,
77}
78
79impl TryFrom<FeasibleBasisExtensionCreateSpec> for FeasibleBasisExtension {
80    type Error = crate::registry::ConstructionError;
81    fn try_from(spec: FeasibleBasisExtensionCreateSpec) -> Result<Self, Self::Error> {
82        let m = spec.matrix.len();
83        let first = spec
84            .matrix
85            .first()
86            .ok_or("matrix must have at least one row")?;
87        let n = first.len();
88        if spec.matrix.iter().any(|row| row.len() != n) {
89            return Err("all matrix rows must have the same length".into());
90        }
91        if m >= n {
92            return Err("number of rows must be less than number of columns".into());
93        }
94        if spec.rhs.len() != m {
95            return Err("rhs length must equal number of rows".into());
96        }
97        if spec.required_columns.len() >= m {
98            return Err("required_columns length must be less than number of rows".into());
99        }
100        let mut seen = std::collections::HashSet::new();
101        for &column in &spec.required_columns {
102            if column >= n {
103                return Err(format!("required column {column} is out of bounds").into());
104            }
105            if !seen.insert(column) {
106                return Err(format!("duplicate required column {column}").into());
107            }
108        }
109        Ok(Self {
110            matrix: spec.matrix,
111            rhs: spec.rhs,
112            required_columns: spec.required_columns,
113        })
114    }
115}
116
117impl FeasibleBasisExtension {
118    /// Create a new FeasibleBasisExtension instance.
119    ///
120    /// # Panics
121    ///
122    /// Panics if:
123    /// - The matrix is empty or has inconsistent row lengths
124    /// - m >= n (must have more columns than rows)
125    /// - rhs length does not equal m
126    /// - |S| >= m (must have room for at least one additional column)
127    /// - Any required column index is out of bounds
128    /// - Required columns contain duplicates
129    pub fn new(matrix: Vec<Vec<i64>>, rhs: Vec<i64>, required_columns: Vec<usize>) -> Self {
130        let m = matrix.len();
131        assert!(m > 0, "Matrix must have at least one row");
132        let n = matrix[0].len();
133        for row in &matrix {
134            assert_eq!(row.len(), n, "All rows must have the same length");
135        }
136        assert!(
137            m < n,
138            "Number of rows ({m}) must be less than number of columns ({n})"
139        );
140        assert_eq!(
141            rhs.len(),
142            m,
143            "rhs length ({}) must equal number of rows ({m})",
144            rhs.len()
145        );
146        assert!(
147            required_columns.len() < m,
148            "|S| ({}) must be less than m ({m})",
149            required_columns.len()
150        );
151        for &col in &required_columns {
152            assert!(col < n, "Required column index {col} out of bounds (n={n})");
153        }
154        // Check for duplicates
155        let mut sorted = required_columns.clone();
156        sorted.sort_unstable();
157        for i in 1..sorted.len() {
158            assert_ne!(
159                sorted[i - 1],
160                sorted[i],
161                "Duplicate required column index {}",
162                sorted[i]
163            );
164        }
165        Self {
166            matrix,
167            rhs,
168            required_columns,
169        }
170    }
171
172    /// Returns the matrix A.
173    pub fn matrix(&self) -> &[Vec<i64>] {
174        &self.matrix
175    }
176
177    /// Returns the right-hand side vector a_bar.
178    pub fn rhs(&self) -> &[i64] {
179        &self.rhs
180    }
181
182    /// Returns the required column indices S.
183    pub fn required_columns(&self) -> &[usize] {
184        &self.required_columns
185    }
186
187    /// Returns the number of rows (m).
188    pub fn num_rows(&self) -> usize {
189        self.matrix.len()
190    }
191
192    /// Returns the number of columns (n).
193    pub fn num_columns(&self) -> usize {
194        self.matrix[0].len()
195    }
196
197    /// Returns the number of required columns (|S|).
198    pub fn num_required(&self) -> usize {
199        self.required_columns.len()
200    }
201
202    /// Returns the indices of non-required columns (the "free" columns).
203    fn free_columns(&self) -> Vec<usize> {
204        let required_set: std::collections::HashSet<usize> =
205            self.required_columns.iter().copied().collect();
206        (0..self.num_columns())
207            .filter(|c| !required_set.contains(c))
208            .collect()
209    }
210
211    /// Check if basis columns form a nonsingular system and the solution is non-negative.
212    ///
213    /// Uses exact rational arithmetic via integer Gaussian elimination with
214    /// numerator/denominator tracking to avoid floating-point errors.
215    #[allow(clippy::needless_range_loop)]
216    fn check_feasible_basis(
217        &self,
218        basis_cols: &[usize],
219    ) -> Result<bool, crate::traits::EvaluationError> {
220        let m = self.num_rows();
221        assert_eq!(basis_cols.len(), m);
222
223        // Build augmented matrix [A_B | a_bar] for Bareiss elimination.
224        let mut augmented: Vec<Vec<i64>> = Vec::with_capacity(m);
225        for i in 0..m {
226            let mut row = Vec::with_capacity(m + 1);
227            for &col in basis_cols {
228                row.push(self.matrix[i][col]);
229            }
230            row.push(self.rhs[i]);
231            augmented.push(row);
232        }
233
234        // Bareiss algorithm: fraction-free Gaussian elimination.
235        // After elimination, the system is upper-triangular.
236        let mut prev_pivot = 1_i64;
237
238        for k in 0..m {
239            // Partial pivoting
240            let mut max_row = k;
241            let mut max_val = augmented[k][k].checked_abs().ok_or_else(|| {
242                crate::traits::EvaluationError::IntegerOverflow(
243                    "taking an elimination pivot magnitude".into(),
244                )
245            })?;
246            for i in (k + 1)..m {
247                let candidate = augmented[i][k].checked_abs().ok_or_else(|| {
248                    crate::traits::EvaluationError::IntegerOverflow(
249                        "taking an elimination candidate magnitude".into(),
250                    )
251                })?;
252                if candidate > max_val {
253                    max_val = candidate;
254                    max_row = i;
255                }
256            }
257            if max_val == 0 {
258                return Ok(false); // singular
259            }
260            if max_row != k {
261                augmented.swap(k, max_row);
262            }
263
264            for i in (k + 1)..m {
265                for j in (k + 1)..=m {
266                    let left = augmented[k][k]
267                        .checked_mul(augmented[i][j])
268                        .ok_or_else(|| {
269                            crate::traits::EvaluationError::IntegerOverflow(
270                                "multiplying Bareiss pivot and row entry".into(),
271                            )
272                        })?;
273                    let right = augmented[i][k]
274                        .checked_mul(augmented[k][j])
275                        .ok_or_else(|| {
276                            crate::traits::EvaluationError::IntegerOverflow(
277                                "multiplying Bareiss elimination entries".into(),
278                            )
279                        })?;
280                    let numerator = left.checked_sub(right).ok_or_else(|| {
281                        crate::traits::EvaluationError::IntegerOverflow(
282                            "subtracting Bareiss products".into(),
283                        )
284                    })?;
285                    augmented[i][j] = numerator.checked_div(prev_pivot).ok_or_else(|| {
286                        crate::traits::EvaluationError::IntegerOverflow(
287                            "dividing by the previous Bareiss pivot".into(),
288                        )
289                    })?;
290                }
291                augmented[i][k] = 0;
292            }
293            prev_pivot = augmented[k][k];
294        }
295
296        // Back-substitution to solve. We solve in rational form: x_i = num_i / det.
297        // The solution x = A_B^{-1} a_bar must satisfy x >= 0, which means
298        // num_i / det >= 0 for all i, i.e., num_i and det have the same sign (or num_i = 0).
299        // Back-substitution using rational arithmetic to check x >= 0.
300        // Simple rational back-substitution:
301        // x[i] = (aug128[i][m] - sum_{j>i} aug128[i][j] * x[j]) / aug128[i][i]
302        // We track x[i] as (numerator, denominator) pairs.
303
304        let mut x_nums = vec![0_i64; m];
305        let mut x_dens = vec![1_i64; m];
306
307        for i in (0..m).rev() {
308            // numerator of (aug128[i][m] - sum_{j>i} aug128[i][j] * x[j])
309            let mut num = augmented[i][m];
310            let mut den = 1_i64;
311
312            for j in (i + 1)..m {
313                // subtract aug128[i][j] * (x_nums[j] / x_dens[j])
314                // num/den - aug128[i][j] * x_nums[j] / x_dens[j]
315                // = (num * x_dens[j] - den * aug128[i][j] * x_nums[j]) / (den * x_dens[j])
316                let left = num.checked_mul(x_dens[j]).ok_or_else(|| {
317                    crate::traits::EvaluationError::IntegerOverflow(
318                        "multiplying basis-solution numerator".into(),
319                    )
320                })?;
321                let right = den
322                    .checked_mul(augmented[i][j])
323                    .and_then(|value| value.checked_mul(x_nums[j]))
324                    .ok_or_else(|| {
325                        crate::traits::EvaluationError::IntegerOverflow(
326                            "multiplying basis-solution subtraction term".into(),
327                        )
328                    })?;
329                num = left.checked_sub(right).ok_or_else(|| {
330                    crate::traits::EvaluationError::IntegerOverflow(
331                        "subtracting basis-solution terms".into(),
332                    )
333                })?;
334                den = den.checked_mul(x_dens[j]).ok_or_else(|| {
335                    crate::traits::EvaluationError::IntegerOverflow(
336                        "multiplying basis-solution denominators".into(),
337                    )
338                })?;
339                // Simplify to avoid overflow
340                let g = gcd_i64(
341                    num.checked_abs().ok_or_else(|| {
342                        crate::traits::EvaluationError::IntegerOverflow(
343                            "taking basis-solution numerator magnitude".into(),
344                        )
345                    })?,
346                    den.checked_abs().ok_or_else(|| {
347                        crate::traits::EvaluationError::IntegerOverflow(
348                            "taking basis-solution denominator magnitude".into(),
349                        )
350                    })?,
351                );
352                if g > 1 {
353                    num /= g;
354                    den /= g;
355                }
356            }
357            // x[i] = (num/den) / aug128[i][i] = num / (den * aug128[i][i])
358            let diag = augmented[i][i];
359            x_nums[i] = num;
360            x_dens[i] = den.checked_mul(diag).ok_or_else(|| {
361                crate::traits::EvaluationError::IntegerOverflow(
362                    "multiplying basis-solution denominator by diagonal".into(),
363                )
364            })?;
365            // Normalize sign: make denominator positive
366            if x_dens[i] < 0 {
367                x_nums[i] = x_nums[i].checked_neg().ok_or_else(|| {
368                    crate::traits::EvaluationError::IntegerOverflow(
369                        "normalizing basis-solution numerator sign".into(),
370                    )
371                })?;
372                x_dens[i] = x_dens[i].checked_neg().ok_or_else(|| {
373                    crate::traits::EvaluationError::IntegerOverflow(
374                        "normalizing basis-solution denominator sign".into(),
375                    )
376                })?;
377            }
378            let g = gcd_i64(
379                x_nums[i].checked_abs().ok_or_else(|| {
380                    crate::traits::EvaluationError::IntegerOverflow(
381                        "taking normalized numerator magnitude".into(),
382                    )
383                })?,
384                x_dens[i].checked_abs().ok_or_else(|| {
385                    crate::traits::EvaluationError::IntegerOverflow(
386                        "taking normalized denominator magnitude".into(),
387                    )
388                })?,
389            );
390            if g > 1 {
391                x_nums[i] /= g;
392                x_dens[i] /= g;
393            }
394        }
395
396        // Check x >= 0: each x_nums[i] / x_dens[i] >= 0
397        // Since x_dens[i] > 0 (normalized), we need x_nums[i] >= 0
398        Ok(x_nums.iter().take(m).all(|&num| num >= 0))
399    }
400}
401
402/// Compute GCD of two nonnegative i64 values.
403fn gcd_i64(mut a: i64, mut b: i64) -> i64 {
404    while b != 0 {
405        let t = b;
406        b = a % b;
407        a = t;
408    }
409    a
410}
411
412impl Problem for FeasibleBasisExtension {
413    const NAME: &'static str = "FeasibleBasisExtension";
414    type Solution = Vec<bool>;
415    type Value = crate::types::Or;
416
417    crate::problem_parameters![
418        ("num_rows", num_rows),
419        ("num_columns", num_columns),
420        ("num_required", num_required),
421    ];
422
423    fn variant() -> Vec<(&'static str, &'static str)> {
424        crate::variant_params![]
425    }
426
427    fn evaluate(
428        &self,
429        config: &Self::Solution,
430    ) -> Result<crate::types::Or, crate::traits::EvaluationError> {
431        Ok({
432            let free_cols = self.free_columns();
433            let num_free = free_cols.len();
434
435            if config.len() != num_free {
436                return Err(crate::traits::EvaluationError::InvalidConfiguration(
437                    "free-column selection length does not match the matrix".into(),
438                ));
439            }
440            let m = self.num_rows();
441            let s = self.num_required();
442            let needed = m - s;
443
444            // Count selected free columns
445            let selected_free: Vec<usize> = config
446                .iter()
447                .enumerate()
448                .filter(|(_, &v)| v)
449                .map(|(i, _)| free_cols[i])
450                .collect();
451
452            if selected_free.len() != needed {
453                return Ok(crate::types::Or(false));
454            }
455
456            // Form basis: required columns + selected free columns
457            let mut basis_cols: Vec<usize> = self.required_columns.clone();
458            basis_cols.extend_from_slice(&selected_free);
459
460            crate::types::Or(self.check_feasible_basis(&basis_cols)?)
461        })
462    }
463}
464
465impl crate::solvers::BruteForceProblem for FeasibleBasisExtension {
466    fn dimensions(&self) -> Vec<usize> {
467        vec![2; self.num_columns() - self.num_required()]
468    }
469}
470
471crate::declare_variants! {
472    default FeasibleBasisExtension => "2^num_columns * num_rows^3" create FeasibleBasisExtensionCreateSpec,
473}
474
475crate::register_brute_force! {
476    FeasibleBasisExtension decode |_, indices: Vec<usize>| crate::config::config_to_bits(&indices),
477}
478
479#[cfg(feature = "example-db")]
480pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
481    vec![crate::example_db::specs::ModelExampleSpec {
482        id: "feasible_basis_extension",
483        // 3x6 matrix, rhs=[7,5,3], required={0,1}, select col 2 -> B={0,1,2}, x=(4,5,3)>=0
484        instance: Box::new(FeasibleBasisExtension::new(
485            vec![
486                vec![1, 0, 1, 2, -1, 0],
487                vec![0, 1, 0, 1, 1, 2],
488                vec![0, 0, 1, 1, 0, 1],
489            ],
490            vec![7, 5, 3],
491            vec![0, 1],
492        )),
493        optimal_config: serde_json::json!(vec![true, false, false, false]), // select col 2 (first free column)
494        optimal_value: serde_json::json!(true),
495    }]
496}
497
498#[cfg(test)]
499#[path = "../../unit_tests/models/algebraic/feasible_basis_extension.rs"]
500mod tests;