Skip to main content

ocas_calc/
solve.rs

1//! Equation solving for oCAS.
2//!
3//! Provides solvers for linear systems, polynomial systems, and
4//! numeric root-finding. Results are returned as expression trees.
5
6use std::fmt;
7
8use ocas_domain::{IntegerDomain, RationalDomain};
9use ocas_poly::matrix::{Matrix, MatrixError};
10
11/// Errors that can occur when solving equations.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum SolveError {
14    /// The system contains no equations.
15    EmptySystem,
16    /// The system is not linear in the requested variables.
17    NonLinear,
18    /// The number of equations does not match the number of unknowns.
19    NonSquare,
20    /// The linear system has no solution.
21    Inconsistent,
22    /// The system is underdetermined (infinitely many solutions).
23    Underdetermined {
24        /// Rank of the coefficient matrix.
25        rank: usize,
26    },
27    /// The solution does not lie in the expected domain.
28    ResultNotInDomain,
29    /// An internal matrix operation failed.
30    Matrix(MatrixError),
31    /// Other error with a description.
32    Other(String),
33}
34
35impl fmt::Display for SolveError {
36    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37        match self {
38            SolveError::EmptySystem => f.write_str("empty system"),
39            SolveError::NonLinear => f.write_str("system is not linear"),
40            SolveError::NonSquare => {
41                f.write_str("number of equations must match number of unknowns")
42            }
43            SolveError::Inconsistent => f.write_str("inconsistent system"),
44            SolveError::Underdetermined { rank } => {
45                write!(f, "underdetermined system (rank {})", rank)
46            }
47            SolveError::ResultNotInDomain => {
48                f.write_str("solution does not lie in the expected domain")
49            }
50            SolveError::Matrix(e) => write!(f, "matrix error: {}", e),
51            SolveError::Other(msg) => f.write_str(msg),
52        }
53    }
54}
55
56impl std::error::Error for SolveError {}
57
58impl From<MatrixError> for SolveError {
59    fn from(e: MatrixError) -> Self {
60        match e {
61            MatrixError::Inconsistent => SolveError::Inconsistent,
62            MatrixError::Underdetermined { rank } => SolveError::Underdetermined { rank },
63            MatrixError::ResultNotInDomain => SolveError::ResultNotInDomain,
64            MatrixError::ShapeMismatch => SolveError::NonSquare,
65            MatrixError::RightHandSideIsNotVector => {
66                SolveError::Other("right-hand side is not a vector".into())
67            }
68        }
69    }
70}
71
72/// Solve a linear system of equations over the rational numbers.
73///
74/// Given an `n × n` coefficient matrix `A` and a right-hand side vector `b`
75/// (both as nested `i64` values interpreted as rational numbers), returns
76/// the solution vector as rational numbers represented as `(num, den)` pairs.
77///
78/// # Example
79///
80/// ```
81/// use ocas_calc::solve::solve_linear_rational;
82///
83/// // 2x + y = 5
84/// // x - y = 1  → x=2, y=1
85/// let a = vec![vec![2, 1], vec![1, -1]];
86/// let b = vec![5, 1];
87/// let x = solve_linear_rational(&a, &b).unwrap();
88/// assert_eq!(x, vec![(2, 1), (1, 1)]);
89/// ```
90pub fn solve_linear_rational(a: &[Vec<i64>], b: &[i64]) -> Result<Vec<(i64, i64)>, SolveError> {
91    if a.is_empty() || b.is_empty() {
92        return Err(SolveError::EmptySystem);
93    }
94    let n = a.len();
95    if b.len() != n {
96        return Err(SolveError::NonSquare);
97    }
98    for row in a {
99        if row.len() != n {
100            return Err(SolveError::NonSquare);
101        }
102    }
103
104    let d = RationalDomain;
105    use ocas_domain::Rational;
106
107    let rows: Vec<Vec<Rational>> = a
108        .iter()
109        .map(|row| row.iter().map(|&v| Rational::new(v, 1)).collect())
110        .collect();
111    let b_vec: Vec<Rational> = b.iter().map(|&v| Rational::new(v, 1)).collect();
112
113    let mat = Matrix::from_rows(rows, d);
114    let solution = mat.solve(&b_vec)?;
115
116    Ok(solution
117        .into_iter()
118        .map(|r| {
119            let numer = r.numer().to_i64().unwrap_or(0);
120            let denom = r.denom().to_i64().unwrap_or(1);
121            (numer, denom)
122        })
123        .collect())
124}
125
126/// Solve a linear system of equations over the integers.
127///
128/// Returns the exact integer solution, or an error if the solution is not
129/// integral or the system is inconsistent.
130///
131/// # Example
132///
133/// ```
134/// use ocas_calc::solve::solve_linear_integer;
135///
136/// // x + y = 3
137/// // x - y = 1  → x=2, y=1
138/// let a = vec![vec![1, 1], vec![1, -1]];
139/// let b = vec![3, 1];
140/// let x = solve_linear_integer(&a, &b).unwrap();
141/// assert_eq!(x, vec![2, 1]);
142/// ```
143pub fn solve_linear_integer(a: &[Vec<i64>], b: &[i64]) -> Result<Vec<i64>, SolveError> {
144    if a.is_empty() || b.is_empty() {
145        return Err(SolveError::EmptySystem);
146    }
147    let n = a.len();
148    if b.len() != n {
149        return Err(SolveError::NonSquare);
150    }
151    for row in a {
152        if row.len() != n {
153            return Err(SolveError::NonSquare);
154        }
155    }
156
157    let d = IntegerDomain;
158    use ocas_domain::Integer;
159
160    let rows: Vec<Vec<Integer>> = a
161        .iter()
162        .map(|row| row.iter().map(|&v| Integer::from(v)).collect())
163        .collect();
164    let b_vec: Vec<Integer> = b.iter().map(|&v| Integer::from(v)).collect();
165
166    let mat = Matrix::from_rows(rows, d);
167    let solution = mat.solve(&b_vec)?;
168
169    Ok(solution
170        .into_iter()
171        .map(|r| {
172            // Integer::from produces BigInt; convert back to i64.
173            r.to_i64().unwrap_or(0)
174        })
175        .collect())
176}
177
178/// A solution to a linear Diophantine equation ax + by = c.
179#[derive(Debug, Clone, PartialEq, Eq)]
180pub struct DiophantineSolution {
181    /// A particular solution (x0, y0).
182    pub particular: (i64, i64),
183    /// The general solution is (x0 + k*tx, y0 + k*ty) for any integer k.
184    pub general: (i64, i64),
185}
186
187/// Solve the linear Diophantine equation `a*x + b*y = c`.
188///
189/// Returns a particular solution and the homogeneous part, or `None` if
190/// no solution exists.
191///
192/// # Example
193///
194/// ```
195/// use ocas_calc::solve::solve_diophantine;
196///
197/// // 3x + 5y = 1 → particular: (2, -1), general: (5k, -3k)
198/// let sol = solve_diophantine(3, 5, 1).unwrap();
199/// assert_eq!(sol.particular, (2, -1));
200/// assert_eq!(sol.general, (5, -3));
201/// ```
202pub fn solve_diophantine(a: i64, b: i64, c: i64) -> Option<DiophantineSolution> {
203    use ocas_domain::{EuclideanDomain, Integer, IntegerDomain};
204
205    if a == 0 && b == 0 {
206        return if c == 0 {
207            Some(DiophantineSolution {
208                particular: (0, 0),
209                general: (1, 0),
210            })
211        } else {
212            None
213        };
214    }
215
216    let d = IntegerDomain;
217    let ai = Integer::from(a);
218    let bi = Integer::from(b);
219
220    let (g, x, y) = d.extended_gcd(&ai, &bi);
221    let g_val: i64 = g.to_i64()?;
222    let x_val: i64 = x.to_i64()?;
223    let y_val: i64 = y.to_i64()?;
224
225    // Check if g divides c.
226    if c % g_val != 0 {
227        return None;
228    }
229
230    let scale = c / g_val;
231    let x0 = x_val * scale;
232    let y0 = y_val * scale;
233
234    Some(DiophantineSolution {
235        particular: (x0, y0),
236        general: (b / g_val, -(a / g_val)),
237    })
238}
239
240/// A polynomial represented as a list of (coefficient, exponent_vector) pairs.
241type RawPoly = Vec<(i64, Vec<usize>)>;
242
243/// Solve a system of polynomial equations using Gröbner bases.
244///
245/// Each polynomial is given as a list of `(coefficient, exponent_vector)` pairs.
246/// The exponent vector `[e1, e2, ...]` represents `x1^e1 * x2^e2 * ...`.
247///
248/// Returns the reduced Gröbner basis with respect to the lexicographic order,
249/// which is triangular and can be used for back-substitution.
250///
251/// # Example
252///
253/// ```
254/// use ocas_calc::solve::solve_polynomial_system;
255///
256/// // x + y = 0, x - y = 0  →  basis = {x, y}
257/// let polys = vec![
258///     vec![(1, vec![1, 0]), (1, vec![0, 1])],
259///     vec![(1, vec![1, 0]), (-1, vec![0, 1])],
260/// ];
261/// let gb = solve_polynomial_system(&polys, 2).unwrap();
262/// assert!(gb.len() >= 2);
263/// ```
264pub fn solve_polynomial_system(
265    polys: &[RawPoly],
266    n_vars: usize,
267) -> Result<Vec<RawPoly>, SolveError> {
268    use ocas_domain::Rational;
269    use ocas_domain::RationalDomain;
270    use ocas_poly::SparseMultivariatePolynomial;
271    use ocas_poly::buchberger;
272    use ocas_poly::sparse::Lex;
273
274    if polys.is_empty() {
275        return Err(SolveError::EmptySystem);
276    }
277
278    let d = RationalDomain;
279
280    let ideal: Vec<SparseMultivariatePolynomial<RationalDomain, Lex>> = polys
281        .iter()
282        .map(|terms| {
283            let terms: Vec<(Vec<usize>, Rational)> = terms
284                .iter()
285                .map(|(c, exp)| (exp.clone(), Rational::new(*c, 1)))
286                .collect();
287            SparseMultivariatePolynomial::from_terms(d, n_vars, terms)
288        })
289        .collect();
290
291    let gb = buchberger(&ideal);
292
293    let result: Vec<RawPoly> = gb
294        .basis
295        .iter()
296        .map(|p| {
297            p.sorted_terms()
298                .into_iter()
299                .map(|(exp, coeff)| {
300                    let numer = coeff.numer().to_i64().unwrap_or(0);
301                    let _denom = coeff.denom().to_i64().unwrap_or(1);
302                    (numer, exp.to_vec())
303                })
304                .collect()
305        })
306        .collect();
307
308    Ok(result)
309}
310
311#[cfg(test)]
312mod tests {
313    use super::*;
314
315    #[test]
316    fn diophantine_3x_5y_eq_1() {
317        let sol = solve_diophantine(3, 5, 1).unwrap();
318        // Verify: 3*2 + 5*(-1) = 6 - 5 = 1 ✓
319        let (x, y) = sol.particular;
320        assert_eq!(3 * x + 5 * y, 1);
321    }
322
323    #[test]
324    fn diophantine_no_solution() {
325        // 2x + 4y = 3 has no solution (gcd(2,4)=2 doesn't divide 3)
326        assert!(solve_diophantine(2, 4, 3).is_none());
327    }
328
329    #[test]
330    fn diophantine_zero_coeffs() {
331        assert!(solve_diophantine(0, 0, 1).is_none());
332        let sol = solve_diophantine(0, 0, 0).unwrap();
333        assert_eq!(sol.particular, (0, 0));
334    }
335
336    #[test]
337    fn solve_2x2_rational() {
338        let a = vec![vec![1, 2], vec![3, 4]];
339        let b = vec![5, 11];
340        let x = solve_linear_rational(&a, &b).unwrap();
341        // x + 2y = 5, 3x + 4y = 11 → x=1, y=2
342        assert_eq!(x, vec![(1, 1), (2, 1)]);
343    }
344
345    #[test]
346    fn solve_2x2_integer() {
347        let a = vec![vec![1, 1], vec![1, -1]];
348        let b = vec![3, 1];
349        let x = solve_linear_integer(&a, &b).unwrap();
350        assert_eq!(x, vec![2, 1]);
351    }
352
353    #[test]
354    fn solve_3x3_rational() {
355        // x + y + z = 6
356        // 2x - y + z = 3
357        // x + 2y - z = 2  → x=1, y=2, z=3
358        let a = vec![vec![1, 1, 1], vec![2, -1, 1], vec![1, 2, -1]];
359        let b = vec![6, 3, 2];
360        let x = solve_linear_rational(&a, &b).unwrap();
361        assert_eq!(x, vec![(1, 1), (2, 1), (3, 1)]);
362    }
363
364    #[test]
365    fn inconsistent_system() {
366        let a = vec![vec![1, 1], vec![2, 2]];
367        let b = vec![1, 3];
368        assert!(matches!(
369            solve_linear_rational(&a, &b),
370            Err(SolveError::Inconsistent)
371        ));
372    }
373}