1use ocas_domain::{Domain, Rational, RationalDomain};
4use ocas_poly::ideal::{self, PolynomialSystemSolution};
5use ocas_poly::sparse::Lex;
6use ocas_poly::{
7 Algorithm, GroebnerBasis, SparseMultivariatePolynomial, eliminate, groebner_basis,
8};
9use pyo3::exceptions::{PyTypeError, PyValueError};
10use pyo3::prelude::*;
11
12use crate::polynomial::PyPolynomial;
13
14#[pyclass(name = "MultivariatePolynomial")]
24pub struct PyMultivariatePolynomial {
25 pub inner: SparseMultivariatePolynomial<RationalDomain, Lex>,
26}
27
28#[pymethods]
29impl PyMultivariatePolynomial {
30 #[new]
31 fn new(terms: &Bound<'_, PyAny>, n_vars: usize) -> PyResult<Self> {
32 let dict: &Bound<'_, pyo3::types::PyDict> = terms.cast().map_err(|_| {
33 PyTypeError::new_err("expected a dict mapping exponent tuples to coefficients")
34 })?;
35
36 let domain = RationalDomain;
37 let mut poly_terms: Vec<(Vec<usize>, Rational)> = Vec::new();
38
39 for (key, val) in dict.iter() {
40 let exp_tuple: &Bound<'_, pyo3::types::PyTuple> = key
41 .cast()
42 .map_err(|_| PyTypeError::new_err("exponent keys must be tuples of ints"))?;
43 let exp: Vec<usize> = exp_tuple
44 .iter()
45 .map(|x| x.extract::<usize>())
46 .collect::<PyResult<Vec<_>>>()?;
47 if exp.len() != n_vars {
48 return Err(PyValueError::new_err(format!(
49 "exponent tuple length {} does not match n_vars={}",
50 exp.len(),
51 n_vars
52 )));
53 }
54
55 let coeff: Rational = if let Ok(i) = val.extract::<i64>() {
56 Rational::new(i, 1)
57 } else if let Ok((n, d)) = val.extract::<(i64, i64)>() {
58 Rational::new(n, d)
59 } else if let Ok(f) = val.extract::<f64>() {
60 let bits = 52u32; let scaled = (f * (1i64 << bits) as f64).round() as i64;
63 Rational::new(scaled, 1i64 << bits)
64 } else {
65 return Err(PyTypeError::new_err("coefficients must be int or float"));
66 };
67
68 if !domain.is_zero(&coeff) {
69 poly_terms.push((exp, coeff));
70 }
71 }
72
73 Ok(Self {
74 inner: SparseMultivariatePolynomial::from_terms(domain, n_vars, poly_terms),
75 })
76 }
77
78 fn __repr__(&self) -> String {
79 format!("MultivariatePolynomial(n_vars={})", self.inner.n_vars())
80 }
81
82 fn __str__(&self) -> String {
83 format!("MultivariatePolynomial(n_vars={})", self.inner.n_vars())
84 }
85
86 fn n_vars(&self) -> usize {
87 self.inner.n_vars()
88 }
89}
90
91fn extract_multivariate_polys(
98 polys: &Bound<'_, PyAny>,
99 n_vars: usize,
100) -> PyResult<Vec<SparseMultivariatePolynomial<RationalDomain, Lex>>> {
101 let list: Vec<Bound<'_, PyAny>> = polys
102 .extract()
103 .map_err(|_| PyTypeError::new_err("expected a list of polynomials"))?;
104
105 if list.is_empty() {
106 return Ok(vec![]);
107 }
108
109 let mut result = Vec::with_capacity(list.len());
110 for item in &list {
111 if let Ok(mv_poly) = item.extract::<PyRef<'_, PyMultivariatePolynomial>>() {
113 result.push(mv_poly.inner.clone());
114 continue;
115 }
116 if let Ok(py_poly) = item.extract::<PyRef<'_, PyPolynomial>>() {
118 let poly = &py_poly.inner;
119 match poly {
120 crate::polynomial::PolyErased::Rat(p) => {
121 let terms: Vec<(Vec<usize>, Rational)> = p
122 .coeffs()
123 .iter()
124 .enumerate()
125 .filter(|(_, c)| !RationalDomain.is_zero(c))
126 .map(|(i, c)| {
127 let mut exp = vec![0usize; n_vars];
128 exp[0] = i;
129 (exp, c.clone())
130 })
131 .collect();
132 result.push(SparseMultivariatePolynomial::from_terms(
133 RationalDomain,
134 n_vars,
135 terms,
136 ));
137 }
138 _ => {
139 return Err(PyTypeError::new_err(
140 "Gröbner basis operations require rational polynomials",
141 ));
142 }
143 }
144 } else {
145 return Err(PyTypeError::new_err(
146 "expected Polynomial or MultivariatePolynomial objects in the generators list",
147 ));
148 }
149 }
150 Ok(result)
151}
152
153#[pyclass(name = "GroebnerBasis")]
155pub struct PyGroebnerBasis {
156 #[pyo3(get)]
157 pub n_vars: usize,
158 pub basis: Vec<SparseMultivariatePolynomial<RationalDomain, Lex>>,
159}
160
161#[pymethods]
162impl PyGroebnerBasis {
163 fn __len__(&self) -> usize {
164 self.basis.len()
165 }
166
167 fn __repr__(&self) -> String {
168 format!(
169 "GroebnerBasis({} elements, {} vars)",
170 self.basis.len(),
171 self.n_vars
172 )
173 }
174
175 fn is_groebner_basis(&self) -> bool {
176 GroebnerBasis {
177 basis: self.basis.clone(),
178 }
179 .is_groebner_basis()
180 }
181}
182
183#[pyclass(name = "RealSolution")]
185pub struct PyRealSolution {
186 #[pyo3(get)]
187 pub values: Vec<f64>,
188 #[pyo3(get)]
189 pub multiplicity: usize,
190}
191
192#[pymethods]
193impl PyRealSolution {
194 fn __repr__(&self) -> String {
195 let vals: Vec<String> = self.values.iter().map(|v| format!("{:.6}", v)).collect();
196 format!(
197 "RealSolution([{}], mult={})",
198 vals.join(", "),
199 self.multiplicity
200 )
201 }
202}
203
204#[pyclass(name = "PolynomialSystemSolution")]
206pub struct PyPolynomialSystemSolution {
207 pub inner: PolynomialSystemSolution,
208}
209
210#[pymethods]
211impl PyPolynomialSystemSolution {
212 #[getter]
213 fn kind(&self) -> &str {
214 match &self.inner {
215 PolynomialSystemSolution::ZeroDimensional(_) => "zero_dimensional",
216 PolynomialSystemSolution::PositiveDimensional(_) => "positive_dimensional",
217 PolynomialSystemSolution::Empty => "empty",
218 }
219 }
220
221 fn solutions(&self) -> Vec<PyRealSolution> {
222 match &self.inner {
223 PolynomialSystemSolution::ZeroDimensional(z) => z
224 .solutions
225 .iter()
226 .map(|s| PyRealSolution {
227 values: s.values.clone(),
228 multiplicity: s.multiplicity,
229 })
230 .collect(),
231 _ => vec![],
232 }
233 }
234
235 #[getter]
236 fn vector_space_dimension(&self) -> Option<usize> {
237 match &self.inner {
238 PolynomialSystemSolution::ZeroDimensional(z) => Some(z.vector_space_dimension),
239 _ => None,
240 }
241 }
242
243 fn __repr__(&self) -> String {
244 match &self.inner {
245 PolynomialSystemSolution::ZeroDimensional(z) => {
246 format!("Solution(zero_dim, {} solutions)", z.solutions.len())
247 }
248 PolynomialSystemSolution::PositiveDimensional(_) => {
249 "Solution(positive_dimensional)".to_string()
250 }
251 PolynomialSystemSolution::Empty => "Solution(empty)".to_string(),
252 }
253 }
254}
255
256#[pyclass(name = "HilbertSeries")]
258pub struct PyHilbertSeries {
259 inner: ocas_poly::groebner::hilbert::HilbertSeries,
260}
261
262#[pymethods]
263impl PyHilbertSeries {
264 fn hilbert_function(&self, degree: usize) -> i64 {
265 self.inner.hilbert_function(degree)
266 }
267
268 #[getter]
269 fn dimension(&self) -> usize {
270 self.inner.dimension()
271 }
272
273 #[getter]
274 fn degree(&self) -> i64 {
275 self.inner.degree()
276 }
277
278 #[getter]
279 fn numerator(&self) -> Vec<i64> {
280 self.inner.numerator.clone()
281 }
282
283 fn __repr__(&self) -> String {
284 format!(
285 "HilbertSeries(dim={}, degree={}, numerator={:?})",
286 self.inner.dimension(),
287 self.inner.degree(),
288 self.inner.numerator
289 )
290 }
291}
292
293#[pyclass(name = "PrimaryComponent")]
295pub struct PyPrimaryComponent {
296 #[pyo3(get)]
297 pub n_vars: usize,
298 pub primary: Vec<SparseMultivariatePolynomial<RationalDomain, Lex>>,
299 pub prime: Vec<SparseMultivariatePolynomial<RationalDomain, Lex>>,
300}
301
302#[pymethods]
303impl PyPrimaryComponent {
304 fn __repr__(&self) -> String {
305 format!(
306 "PrimaryComponent(primary={} gens, prime={} gens)",
307 self.primary.len(),
308 self.prime.len()
309 )
310 }
311}
312
313#[pyfunction]
318#[pyo3(signature = (generators, n_vars=1, algorithm="auto"))]
319pub fn py_groebner_basis(
320 generators: &Bound<'_, PyAny>,
321 n_vars: usize,
322 algorithm: &str,
323) -> PyResult<PyGroebnerBasis> {
324 let polys = extract_multivariate_polys(generators, n_vars)?;
325 let algo = parse_algorithm(algorithm)?;
326 let gb = groebner_basis(&polys, algo);
327 Ok(PyGroebnerBasis {
328 n_vars: polys.first().map(|p| p.n_vars()).unwrap_or(0),
329 basis: gb.basis,
330 })
331}
332
333#[pyfunction]
334#[pyo3(signature = (generators, f, n_vars=1, algorithm="auto"))]
335pub fn py_ideal_contains(
336 generators: &Bound<'_, PyAny>,
337 f: &Bound<'_, PyAny>,
338 n_vars: usize,
339 algorithm: &str,
340) -> PyResult<bool> {
341 let gens = extract_multivariate_polys(generators, n_vars)?;
342 let fs = extract_multivariate_polys(f, n_vars)?;
343 let poly = fs
344 .into_iter()
345 .next()
346 .ok_or_else(|| PyValueError::new_err("f must be a non-empty polynomial"))?;
347 let algo = parse_algorithm(algorithm)?;
348 Ok(ideal::ideal_contains(&gens, &poly, algo))
349}
350
351#[pyfunction]
352#[pyo3(signature = (equations, n_vars=1, algorithm="auto"))]
353pub fn py_solve_polynomial_system(
354 equations: &Bound<'_, PyAny>,
355 n_vars: usize,
356 algorithm: &str,
357) -> PyResult<PyPolynomialSystemSolution> {
358 let polys = extract_multivariate_polys(equations, n_vars)?;
359 let algo = parse_algorithm(algorithm)?;
360 let sol = ideal::solve_polynomial_system(&polys, algo);
361 Ok(PyPolynomialSystemSolution { inner: sol })
362}
363
364#[pyfunction]
365pub fn py_hilbert_series(gb: &PyGroebnerBasis) -> PyResult<PyHilbertSeries> {
366 let gb_struct = GroebnerBasis {
367 basis: gb.basis.clone(),
368 };
369 let hs = ocas_poly::groebner::hilbert::hilbert_series(&gb_struct);
370 Ok(PyHilbertSeries { inner: hs })
371}
372
373#[pyfunction]
374#[pyo3(signature = (generators, n_vars=1))]
375pub fn py_ideal_radical(generators: &Bound<'_, PyAny>, n_vars: usize) -> PyResult<PyGroebnerBasis> {
376 let gens = extract_multivariate_polys(generators, n_vars)?;
377 let rad = ideal::ideal_radical(&gens);
378 Ok(PyGroebnerBasis {
379 n_vars: gens.first().map(|p| p.n_vars()).unwrap_or(0),
380 basis: rad.basis,
381 })
382}
383
384#[pyfunction]
385#[pyo3(signature = (generators, n_vars=1))]
386pub fn py_primary_decomposition(
387 generators: &Bound<'_, PyAny>,
388 n_vars: usize,
389) -> PyResult<Vec<PyPrimaryComponent>> {
390 let gens = extract_multivariate_polys(generators, n_vars)?;
391 let decomp = ideal::primary_decomposition(&gens);
392 let n_vars = gens.first().map(|p| p.n_vars()).unwrap_or(0);
393 Ok(decomp
394 .into_iter()
395 .map(|comp| PyPrimaryComponent {
396 n_vars,
397 primary: comp.primary,
398 prime: comp.prime,
399 })
400 .collect())
401}
402
403#[pyfunction]
404pub fn py_is_zero_dimensional(gb: &PyGroebnerBasis) -> bool {
405 let gb_struct = GroebnerBasis {
406 basis: gb.basis.clone(),
407 };
408 ideal::is_zero_dimensional(&gb_struct)
409}
410
411#[pyfunction]
412#[pyo3(signature = (generators, elim_vars, n_vars=1, algorithm="auto"))]
413pub fn py_eliminate(
414 generators: &Bound<'_, PyAny>,
415 elim_vars: usize,
416 n_vars: usize,
417 algorithm: &str,
418) -> PyResult<PyGroebnerBasis> {
419 let polys = extract_multivariate_polys(generators, n_vars)?;
420 let algo = parse_algorithm(algorithm)?;
421 let result = eliminate(&polys, elim_vars, algo);
422 Ok(PyGroebnerBasis {
423 n_vars: polys.first().map(|p| p.n_vars()).unwrap_or(0),
424 basis: result.basis,
425 })
426}
427
428fn parse_algorithm(s: &str) -> PyResult<Algorithm> {
429 match s.to_lowercase().as_str() {
430 "auto" => Ok(Algorithm::Auto),
431 "f4" => Ok(Algorithm::F4),
432 "f5" => Ok(Algorithm::F5),
433 "buchberger" => Ok(Algorithm::Buchberger),
434 "multi_modular" | "multimodular" | "mm" => Ok(Algorithm::MultiModular),
435 _ => Err(PyValueError::new_err(format!(
436 "unknown algorithm '{}': expected auto, f4, f5, buchberger, or multi_modular",
437 s
438 ))),
439 }
440}