1use ocas_domain::{FiniteField, Integer, IntegerDomain, Rational, RationalDomain};
7use ocas_poly::DenseUnivariatePolynomial;
8use pyo3::exceptions::{PyTypeError, PyValueError};
9use pyo3::prelude::*;
10
11use crate::domain::DomainKind;
12
13#[derive(Clone)]
15pub(crate) enum PolyErased {
16 Int(DenseUnivariatePolynomial<IntegerDomain>),
17 Rat(DenseUnivariatePolynomial<RationalDomain>),
18 Fq(DenseUnivariatePolynomial<FiniteField>),
19}
20
21#[pyclass(name = "Polynomial", skip_from_py_object)]
41#[derive(Clone)]
42pub struct PyPolynomial {
43 pub(crate) inner: PolyErased,
44}
45
46#[pyclass(name = "PolynomialFactor", skip_from_py_object)]
48pub struct PyPolynomialFactor {
49 #[pyo3(get)]
50 pub factor: PyPolynomial,
51 #[pyo3(get)]
52 pub multiplicity: usize,
53}
54
55fn extract_int_coeffs(obj: &Bound<'_, PyAny>) -> PyResult<Vec<Integer>> {
57 let ints: Vec<i64> = obj
58 .extract()
59 .map_err(|_| PyTypeError::new_err("integer coefficients must be ints"))?;
60 Ok(ints.into_iter().map(Integer::from).collect())
61}
62
63fn extract_rat_coeffs(obj: &Bound<'_, PyAny>) -> PyResult<Vec<Rational>> {
67 if let Ok(ints) = obj.extract::<Vec<i64>>() {
68 Ok(ints.into_iter().map(|n| Rational::new(n, 1)).collect())
69 } else if let Ok(pairs) = obj.extract::<Vec<(i64, i64)>>() {
70 pairs
71 .into_iter()
72 .map(|(num, den)| {
73 if den == 0 {
74 Err(PyValueError::new_err("rational denominator cannot be zero"))
75 } else {
76 Ok(Rational::new(num, den))
77 }
78 })
79 .collect()
80 } else {
81 Err(PyTypeError::new_err(
82 "rational coefficients must be ints or (num, denom) tuples",
83 ))
84 }
85}
86
87pub(crate) fn build_polynomial(
89 coeffs: &Bound<'_, PyAny>,
90 domain: &DomainKind,
91) -> PyResult<PyPolynomial> {
92 let inner = match domain {
93 DomainKind::Integer => {
94 let c = extract_int_coeffs(coeffs)?;
95 PolyErased::Int(DenseUnivariatePolynomial::from_coeffs(IntegerDomain, c))
96 }
97 DomainKind::Rational => {
98 let c = extract_rat_coeffs(coeffs)?;
99 PolyErased::Rat(DenseUnivariatePolynomial::from_coeffs(RationalDomain, c))
100 }
101 DomainKind::FiniteField(p) => {
102 let field = FiniteField::new(p.clone());
103 let ints: Vec<i64> = coeffs
104 .extract()
105 .map_err(|_| PyTypeError::new_err("finite-field coefficients must be ints"))?;
106 let c: Vec<_> = ints.into_iter().map(|v| field.element(v)).collect();
107 PolyErased::Fq(DenseUnivariatePolynomial::from_coeffs(field, c))
108 }
109 };
110 Ok(PyPolynomial { inner })
111}
112
113#[pymethods]
114impl PyPolynomial {
115 #[new]
120 #[pyo3(signature = (coeffs, domain=None))]
121 fn new(coeffs: &Bound<'_, PyAny>, domain: Option<&Bound<'_, PyAny>>) -> PyResult<Self> {
122 let kind = match domain {
123 Some(d) => DomainKind::from_py(d)?,
124 None => DomainKind::Integer,
125 };
126 build_polynomial(coeffs, &kind)
127 }
128
129 fn coeffs(&self) -> Vec<String> {
134 match &self.inner {
135 PolyErased::Int(p) => p.coeffs().iter().map(|c| c.to_string()).collect(),
136 PolyErased::Rat(p) => p.coeffs().iter().map(|c| c.to_string()).collect(),
137 PolyErased::Fq(p) => p.coeffs().iter().map(|c| c.value().to_string()).collect(),
138 }
139 }
140
141 fn degree(&self) -> Option<usize> {
143 match &self.inner {
144 PolyErased::Int(p) => p.degree(),
145 PolyErased::Rat(p) => p.degree(),
146 PolyErased::Fq(p) => p.degree(),
147 }
148 }
149
150 fn len(&self) -> usize {
152 match &self.inner {
153 PolyErased::Int(p) => p.coeffs().len(),
154 PolyErased::Rat(p) => p.coeffs().len(),
155 PolyErased::Fq(p) => p.coeffs().len(),
156 }
157 }
158
159 fn is_zero(&self) -> bool {
161 self.len() == 0
162 }
163
164 fn eval(&self, x: &Bound<'_, PyAny>) -> PyResult<String> {
170 match &self.inner {
171 PolyErased::Int(p) => {
172 let v = x
173 .extract::<i64>()
174 .map_err(|_| PyTypeError::new_err("x must be an int"))?;
175 Ok(p.eval(&Integer::from(v)).to_string())
176 }
177 PolyErased::Rat(p) => {
178 let v = if let Ok(n) = x.extract::<i64>() {
179 Rational::new(n, 1)
180 } else if let Ok((num, den)) = x.extract::<(i64, i64)>() {
181 Rational::new(num, den)
182 } else {
183 return Err(PyTypeError::new_err(
184 "x must be an int or (num, denom) tuple",
185 ));
186 };
187 Ok(p.eval(&v).to_string())
188 }
189 PolyErased::Fq(p) => {
190 let field = p.domain();
191 let v = x
192 .extract::<i64>()
193 .map_err(|_| PyTypeError::new_err("x must be an int"))?;
194 Ok(p.eval(&field.element(v)).value().to_string())
195 }
196 }
197 }
198
199 fn derivative(&self) -> PyPolynomial {
201 match &self.inner {
202 PolyErased::Int(p) => PyPolynomial {
203 inner: PolyErased::Int(p.derivative()),
204 },
205 PolyErased::Rat(p) => PyPolynomial {
206 inner: PolyErased::Rat(p.derivative()),
207 },
208 PolyErased::Fq(p) => PyPolynomial {
209 inner: PolyErased::Fq(p.derivative()),
210 },
211 }
212 }
213
214 fn integral(&self) -> PyPolynomial {
216 match &self.inner {
217 PolyErased::Int(p) => PyPolynomial {
218 inner: PolyErased::Int(p.integral()),
219 },
220 PolyErased::Rat(p) => PyPolynomial {
221 inner: PolyErased::Rat(p.integral()),
222 },
223 PolyErased::Fq(p) => PyPolynomial {
224 inner: PolyErased::Fq(p.integral()),
225 },
226 }
227 }
228
229 fn primitive_part(&self) -> PyResult<PyPolynomial> {
231 match &self.inner {
232 PolyErased::Int(p) => Ok(PyPolynomial {
233 inner: PolyErased::Int(p.primitive_part()),
234 }),
235 _ => Err(PyValueError::new_err(
236 "primitive_part is only defined over the integers",
237 )),
238 }
239 }
240
241 fn factor(&self) -> PyResult<Vec<PyPolynomialFactor>> {
246 let factors: Vec<_> = match &self.inner {
247 PolyErased::Int(p) => p
248 .factor()
249 .into_iter()
250 .map(|(f, m)| PyPolynomialFactor {
251 factor: PyPolynomial {
252 inner: PolyErased::Int(f),
253 },
254 multiplicity: m,
255 })
256 .collect(),
257 PolyErased::Fq(p) => p
258 .factor()
259 .into_iter()
260 .map(|(f, m)| PyPolynomialFactor {
261 factor: PyPolynomial {
262 inner: PolyErased::Fq(f),
263 },
264 multiplicity: m,
265 })
266 .collect(),
267 PolyErased::Rat(_p) => {
268 return Err(PyValueError::new_err(
269 "factor is not implemented over the rationals; use the integer primitive part",
270 ));
271 }
272 };
273 Ok(factors)
274 }
275
276 fn square_free_factorization(&self) -> PyResult<Vec<PyPolynomialFactor>> {
278 let factors: Vec<_> = match &self.inner {
279 PolyErased::Int(p) => p
280 .square_free_factorization()
281 .into_iter()
282 .map(|(f, m)| PyPolynomialFactor {
283 factor: PyPolynomial {
284 inner: PolyErased::Int(f),
285 },
286 multiplicity: m,
287 })
288 .collect(),
289 PolyErased::Rat(p) => p
290 .square_free_factorization()
291 .into_iter()
292 .map(|(f, m)| PyPolynomialFactor {
293 factor: PyPolynomial {
294 inner: PolyErased::Rat(f),
295 },
296 multiplicity: m,
297 })
298 .collect(),
299 PolyErased::Fq(p) => p
300 .square_free_factorization()
301 .into_iter()
302 .map(|(f, m)| PyPolynomialFactor {
303 factor: PyPolynomial {
304 inner: PolyErased::Fq(f),
305 },
306 multiplicity: m,
307 })
308 .collect(),
309 };
310 Ok(factors)
311 }
312
313 fn is_square_free(&self) -> bool {
315 match &self.inner {
316 PolyErased::Int(p) => p.is_square_free(),
317 PolyErased::Rat(p) => p.is_square_free(),
318 PolyErased::Fq(p) => p.is_square_free(),
319 }
320 }
321
322 fn gcd(&self, other: &PyPolynomial) -> PyResult<PyPolynomial> {
326 match (&self.inner, &other.inner) {
327 (PolyErased::Int(a), PolyErased::Int(b)) => Ok(PyPolynomial {
328 inner: PolyErased::Int(a.gcd(b)),
329 }),
330 (PolyErased::Rat(a), PolyErased::Rat(b)) => Ok(PyPolynomial {
331 inner: PolyErased::Rat(a.gcd(b)),
332 }),
333 (PolyErased::Fq(a), PolyErased::Fq(b)) => Ok(PyPolynomial {
334 inner: PolyErased::Fq(a.gcd(b)),
335 }),
336 _ => Err(PyTypeError::new_err(
337 "gcd requires both polynomials to share the same coefficient domain",
338 )),
339 }
340 }
341
342 fn div_rem(&self, other: &PyPolynomial) -> PyResult<Option<(PyPolynomial, PyPolynomial)>> {
345 match (&self.inner, &other.inner) {
346 (PolyErased::Int(a), PolyErased::Int(b)) => Ok(a.div_rem(b).map(|(q, r)| {
347 (
348 PyPolynomial {
349 inner: PolyErased::Int(q),
350 },
351 PyPolynomial {
352 inner: PolyErased::Int(r),
353 },
354 )
355 })),
356 (PolyErased::Rat(a), PolyErased::Rat(b)) => Ok(a.div_rem(b).map(|(q, r)| {
357 (
358 PyPolynomial {
359 inner: PolyErased::Rat(q),
360 },
361 PyPolynomial {
362 inner: PolyErased::Rat(r),
363 },
364 )
365 })),
366 (PolyErased::Fq(a), PolyErased::Fq(b)) => Ok(a.div_rem(b).map(|(q, r)| {
367 (
368 PyPolynomial {
369 inner: PolyErased::Fq(q),
370 },
371 PyPolynomial {
372 inner: PolyErased::Fq(r),
373 },
374 )
375 })),
376 _ => Err(PyTypeError::new_err(
377 "div_rem requires both polynomials to share the same coefficient domain",
378 )),
379 }
380 }
381
382 fn __add__(&self, other: &PyPolynomial) -> PyResult<PyPolynomial> {
384 match (&self.inner, &other.inner) {
385 (PolyErased::Int(a), PolyErased::Int(b)) => Ok(PyPolynomial {
386 inner: PolyErased::Int(a.add(b)),
387 }),
388 (PolyErased::Rat(a), PolyErased::Rat(b)) => Ok(PyPolynomial {
389 inner: PolyErased::Rat(a.add(b)),
390 }),
391 (PolyErased::Fq(a), PolyErased::Fq(b)) => Ok(PyPolynomial {
392 inner: PolyErased::Fq(a.add(b)),
393 }),
394 _ => Err(PyTypeError::new_err(
395 "+ requires both polynomials to share the same coefficient domain",
396 )),
397 }
398 }
399
400 fn __sub__(&self, other: &PyPolynomial) -> PyResult<PyPolynomial> {
402 match (&self.inner, &other.inner) {
403 (PolyErased::Int(a), PolyErased::Int(b)) => Ok(PyPolynomial {
404 inner: PolyErased::Int(a.sub(b)),
405 }),
406 (PolyErased::Rat(a), PolyErased::Rat(b)) => Ok(PyPolynomial {
407 inner: PolyErased::Rat(a.sub(b)),
408 }),
409 (PolyErased::Fq(a), PolyErased::Fq(b)) => Ok(PyPolynomial {
410 inner: PolyErased::Fq(a.sub(b)),
411 }),
412 _ => Err(PyTypeError::new_err(
413 "- requires both polynomials to share the same coefficient domain",
414 )),
415 }
416 }
417
418 fn __mul__(&self, other: &PyPolynomial) -> PyResult<PyPolynomial> {
420 match (&self.inner, &other.inner) {
421 (PolyErased::Int(a), PolyErased::Int(b)) => Ok(PyPolynomial {
422 inner: PolyErased::Int(a.mul(b)),
423 }),
424 (PolyErased::Rat(a), PolyErased::Rat(b)) => Ok(PyPolynomial {
425 inner: PolyErased::Rat(a.mul(b)),
426 }),
427 (PolyErased::Fq(a), PolyErased::Fq(b)) => Ok(PyPolynomial {
428 inner: PolyErased::Fq(a.mul(b)),
429 }),
430 _ => Err(PyTypeError::new_err(
431 "* requires both polynomials to share the same coefficient domain",
432 )),
433 }
434 }
435
436 fn __neg__(&self) -> PyPolynomial {
438 match &self.inner {
439 PolyErased::Int(p) => PyPolynomial {
440 inner: PolyErased::Int(p.mul_scalar(&Integer::from(-1))),
441 },
442 PolyErased::Rat(p) => PyPolynomial {
443 inner: PolyErased::Rat(p.mul_scalar(&Rational::new(-1, 1))),
444 },
445 PolyErased::Fq(p) => {
446 let field = p.domain();
447 PyPolynomial {
448 inner: PolyErased::Fq(p.mul_scalar(&field.element(-1))),
449 }
450 }
451 }
452 }
453
454 fn __eq__(&self, other: &PyPolynomial) -> bool {
456 match (&self.inner, &other.inner) {
457 (PolyErased::Int(a), PolyErased::Int(b)) => a == b,
458 (PolyErased::Rat(a), PolyErased::Rat(b)) => a == b,
459 (PolyErased::Fq(a), PolyErased::Fq(b)) => a == b,
460 _ => false,
461 }
462 }
463
464 fn __repr__(&self) -> String {
465 match &self.inner {
466 PolyErased::Int(p) => {
467 format!("Polynomial([{}], 'integer')", fmt_poly_coeffs(p))
468 }
469 PolyErased::Rat(p) => {
470 format!("Polynomial([{}], 'rational')", fmt_poly_coeffs(p))
471 }
472 PolyErased::Fq(p) => format!(
473 "Polynomial([{}], domain=FiniteField({}))",
474 fmt_poly_coeffs(p),
475 p.domain().prime()
476 ),
477 }
478 }
479}
480
481fn fmt_poly_coeffs<D: ocas_domain::Domain>(p: &DenseUnivariatePolynomial<D>) -> String
484where
485 D::Element: std::fmt::Display,
486{
487 p.coeffs()
488 .iter()
489 .map(|c| c.to_string())
490 .collect::<Vec<_>>()
491 .join(", ")
492}