problemreductions/models/algebraic/
feasible_basis_extension.rs1use 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#[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 #[create(codec = "json")]
71 matrix: Vec<Vec<i64>>,
72 #[create(codec = "comma-separated")]
74 rhs: Vec<i64>,
75 #[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 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 pub fn matrix(&self) -> &[Vec<i64>] {
144 &self.matrix
145 }
146
147 pub fn rhs(&self) -> &[i64] {
149 &self.rhs
150 }
151
152 pub fn required_columns(&self) -> &[usize] {
154 &self.required_columns
155 }
156
157 pub fn num_rows(&self) -> usize {
159 self.matrix.len()
160 }
161
162 pub fn num_columns(&self) -> usize {
164 self.matrix[0].len()
165 }
166
167 pub fn num_required(&self) -> usize {
169 self.required_columns.len()
170 }
171
172 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 #[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 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 let mut prev_pivot = 1_i64;
207
208 for k in 0..m {
209 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); }
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 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 let mut num = augmented[i][m];
280 let mut den = 1_i64;
281
282 for j in (i + 1)..m {
283 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 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 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 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 Ok(x_nums.iter().take(m).all(|&num| num >= 0))
369 }
370}
371
372fn 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 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 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 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]), optimal_value: serde_json::json!(true),
465 }]
466}
467
468#[cfg(test)]
469#[path = "../../unit_tests/models/algebraic/feasible_basis_extension.rs"]
470mod tests;