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