use ocas_domain::{FiniteField, Integer, IntegerDomain, Rational, RationalDomain};
use ocas_poly::DenseUnivariatePolynomial;
use pyo3::exceptions::{PyTypeError, PyValueError};
use pyo3::prelude::*;
use crate::domain::DomainKind;
#[derive(Clone)]
pub(crate) enum PolyErased {
Int(DenseUnivariatePolynomial<IntegerDomain>),
Rat(DenseUnivariatePolynomial<RationalDomain>),
Fq(DenseUnivariatePolynomial<FiniteField>),
}
#[pyclass(name = "Polynomial", skip_from_py_object)]
#[derive(Clone)]
pub struct PyPolynomial {
pub(crate) inner: PolyErased,
}
#[pyclass(name = "PolynomialFactor", skip_from_py_object)]
pub struct PyPolynomialFactor {
#[pyo3(get)]
pub factor: PyPolynomial,
#[pyo3(get)]
pub multiplicity: usize,
}
fn extract_int_coeffs(obj: &Bound<'_, PyAny>) -> PyResult<Vec<Integer>> {
let ints: Vec<i64> = obj
.extract()
.map_err(|_| PyTypeError::new_err("integer coefficients must be ints"))?;
Ok(ints.into_iter().map(Integer::from).collect())
}
fn extract_rat_coeffs(obj: &Bound<'_, PyAny>) -> PyResult<Vec<Rational>> {
if let Ok(ints) = obj.extract::<Vec<i64>>() {
Ok(ints.into_iter().map(|n| Rational::new(n, 1)).collect())
} else if let Ok(pairs) = obj.extract::<Vec<(i64, i64)>>() {
pairs
.into_iter()
.map(|(num, den)| {
if den == 0 {
Err(PyValueError::new_err("rational denominator cannot be zero"))
} else {
Ok(Rational::new(num, den))
}
})
.collect()
} else {
Err(PyTypeError::new_err(
"rational coefficients must be ints or (num, denom) tuples",
))
}
}
pub(crate) fn build_polynomial(
coeffs: &Bound<'_, PyAny>,
domain: &DomainKind,
) -> PyResult<PyPolynomial> {
let inner = match domain {
DomainKind::Integer => {
let c = extract_int_coeffs(coeffs)?;
PolyErased::Int(DenseUnivariatePolynomial::from_coeffs(IntegerDomain, c))
}
DomainKind::Rational => {
let c = extract_rat_coeffs(coeffs)?;
PolyErased::Rat(DenseUnivariatePolynomial::from_coeffs(RationalDomain, c))
}
DomainKind::FiniteField(p) => {
let field = FiniteField::new(p.clone());
let ints: Vec<i64> = coeffs
.extract()
.map_err(|_| PyTypeError::new_err("finite-field coefficients must be ints"))?;
let c: Vec<_> = ints.into_iter().map(|v| field.element(v)).collect();
PolyErased::Fq(DenseUnivariatePolynomial::from_coeffs(field, c))
}
};
Ok(PyPolynomial { inner })
}
#[pymethods]
impl PyPolynomial {
#[new]
#[pyo3(signature = (coeffs, domain=None))]
fn new(coeffs: &Bound<'_, PyAny>, domain: Option<&Bound<'_, PyAny>>) -> PyResult<Self> {
let kind = match domain {
Some(d) => DomainKind::from_py(d)?,
None => DomainKind::Integer,
};
build_polynomial(coeffs, &kind)
}
fn coeffs(&self) -> Vec<String> {
match &self.inner {
PolyErased::Int(p) => p.coeffs().iter().map(|c| c.to_string()).collect(),
PolyErased::Rat(p) => p.coeffs().iter().map(|c| c.to_string()).collect(),
PolyErased::Fq(p) => p.coeffs().iter().map(|c| c.value().to_string()).collect(),
}
}
fn degree(&self) -> Option<usize> {
match &self.inner {
PolyErased::Int(p) => p.degree(),
PolyErased::Rat(p) => p.degree(),
PolyErased::Fq(p) => p.degree(),
}
}
fn len(&self) -> usize {
match &self.inner {
PolyErased::Int(p) => p.coeffs().len(),
PolyErased::Rat(p) => p.coeffs().len(),
PolyErased::Fq(p) => p.coeffs().len(),
}
}
fn is_zero(&self) -> bool {
self.len() == 0
}
fn eval(&self, x: &Bound<'_, PyAny>) -> PyResult<String> {
match &self.inner {
PolyErased::Int(p) => {
let v = x
.extract::<i64>()
.map_err(|_| PyTypeError::new_err("x must be an int"))?;
Ok(p.eval(&Integer::from(v)).to_string())
}
PolyErased::Rat(p) => {
let v = if let Ok(n) = x.extract::<i64>() {
Rational::new(n, 1)
} else if let Ok((num, den)) = x.extract::<(i64, i64)>() {
Rational::new(num, den)
} else {
return Err(PyTypeError::new_err(
"x must be an int or (num, denom) tuple",
));
};
Ok(p.eval(&v).to_string())
}
PolyErased::Fq(p) => {
let field = p.domain();
let v = x
.extract::<i64>()
.map_err(|_| PyTypeError::new_err("x must be an int"))?;
Ok(p.eval(&field.element(v)).value().to_string())
}
}
}
fn derivative(&self) -> PyPolynomial {
match &self.inner {
PolyErased::Int(p) => PyPolynomial {
inner: PolyErased::Int(p.derivative()),
},
PolyErased::Rat(p) => PyPolynomial {
inner: PolyErased::Rat(p.derivative()),
},
PolyErased::Fq(p) => PyPolynomial {
inner: PolyErased::Fq(p.derivative()),
},
}
}
fn integral(&self) -> PyPolynomial {
match &self.inner {
PolyErased::Int(p) => PyPolynomial {
inner: PolyErased::Int(p.integral()),
},
PolyErased::Rat(p) => PyPolynomial {
inner: PolyErased::Rat(p.integral()),
},
PolyErased::Fq(p) => PyPolynomial {
inner: PolyErased::Fq(p.integral()),
},
}
}
fn primitive_part(&self) -> PyResult<PyPolynomial> {
match &self.inner {
PolyErased::Int(p) => Ok(PyPolynomial {
inner: PolyErased::Int(p.primitive_part()),
}),
_ => Err(PyValueError::new_err(
"primitive_part is only defined over the integers",
)),
}
}
fn factor(&self) -> PyResult<Vec<PyPolynomialFactor>> {
let factors: Vec<_> = match &self.inner {
PolyErased::Int(p) => p
.factor()
.into_iter()
.map(|(f, m)| PyPolynomialFactor {
factor: PyPolynomial {
inner: PolyErased::Int(f),
},
multiplicity: m,
})
.collect(),
PolyErased::Fq(p) => p
.factor()
.into_iter()
.map(|(f, m)| PyPolynomialFactor {
factor: PyPolynomial {
inner: PolyErased::Fq(f),
},
multiplicity: m,
})
.collect(),
PolyErased::Rat(_p) => {
return Err(PyValueError::new_err(
"factor is not implemented over the rationals; use the integer primitive part",
));
}
};
Ok(factors)
}
fn square_free_factorization(&self) -> PyResult<Vec<PyPolynomialFactor>> {
let factors: Vec<_> = match &self.inner {
PolyErased::Int(p) => p
.square_free_factorization()
.into_iter()
.map(|(f, m)| PyPolynomialFactor {
factor: PyPolynomial {
inner: PolyErased::Int(f),
},
multiplicity: m,
})
.collect(),
PolyErased::Rat(p) => p
.square_free_factorization()
.into_iter()
.map(|(f, m)| PyPolynomialFactor {
factor: PyPolynomial {
inner: PolyErased::Rat(f),
},
multiplicity: m,
})
.collect(),
PolyErased::Fq(p) => p
.square_free_factorization()
.into_iter()
.map(|(f, m)| PyPolynomialFactor {
factor: PyPolynomial {
inner: PolyErased::Fq(f),
},
multiplicity: m,
})
.collect(),
};
Ok(factors)
}
fn is_square_free(&self) -> bool {
match &self.inner {
PolyErased::Int(p) => p.is_square_free(),
PolyErased::Rat(p) => p.is_square_free(),
PolyErased::Fq(p) => p.is_square_free(),
}
}
fn gcd(&self, other: &PyPolynomial) -> PyResult<PyPolynomial> {
match (&self.inner, &other.inner) {
(PolyErased::Int(a), PolyErased::Int(b)) => Ok(PyPolynomial {
inner: PolyErased::Int(a.gcd(b)),
}),
(PolyErased::Rat(a), PolyErased::Rat(b)) => Ok(PyPolynomial {
inner: PolyErased::Rat(a.gcd(b)),
}),
(PolyErased::Fq(a), PolyErased::Fq(b)) => Ok(PyPolynomial {
inner: PolyErased::Fq(a.gcd(b)),
}),
_ => Err(PyTypeError::new_err(
"gcd requires both polynomials to share the same coefficient domain",
)),
}
}
fn div_rem(&self, other: &PyPolynomial) -> PyResult<Option<(PyPolynomial, PyPolynomial)>> {
match (&self.inner, &other.inner) {
(PolyErased::Int(a), PolyErased::Int(b)) => Ok(a.div_rem(b).map(|(q, r)| {
(
PyPolynomial {
inner: PolyErased::Int(q),
},
PyPolynomial {
inner: PolyErased::Int(r),
},
)
})),
(PolyErased::Rat(a), PolyErased::Rat(b)) => Ok(a.div_rem(b).map(|(q, r)| {
(
PyPolynomial {
inner: PolyErased::Rat(q),
},
PyPolynomial {
inner: PolyErased::Rat(r),
},
)
})),
(PolyErased::Fq(a), PolyErased::Fq(b)) => Ok(a.div_rem(b).map(|(q, r)| {
(
PyPolynomial {
inner: PolyErased::Fq(q),
},
PyPolynomial {
inner: PolyErased::Fq(r),
},
)
})),
_ => Err(PyTypeError::new_err(
"div_rem requires both polynomials to share the same coefficient domain",
)),
}
}
fn __add__(&self, other: &PyPolynomial) -> PyResult<PyPolynomial> {
match (&self.inner, &other.inner) {
(PolyErased::Int(a), PolyErased::Int(b)) => Ok(PyPolynomial {
inner: PolyErased::Int(a.add(b)),
}),
(PolyErased::Rat(a), PolyErased::Rat(b)) => Ok(PyPolynomial {
inner: PolyErased::Rat(a.add(b)),
}),
(PolyErased::Fq(a), PolyErased::Fq(b)) => Ok(PyPolynomial {
inner: PolyErased::Fq(a.add(b)),
}),
_ => Err(PyTypeError::new_err(
"+ requires both polynomials to share the same coefficient domain",
)),
}
}
fn __sub__(&self, other: &PyPolynomial) -> PyResult<PyPolynomial> {
match (&self.inner, &other.inner) {
(PolyErased::Int(a), PolyErased::Int(b)) => Ok(PyPolynomial {
inner: PolyErased::Int(a.sub(b)),
}),
(PolyErased::Rat(a), PolyErased::Rat(b)) => Ok(PyPolynomial {
inner: PolyErased::Rat(a.sub(b)),
}),
(PolyErased::Fq(a), PolyErased::Fq(b)) => Ok(PyPolynomial {
inner: PolyErased::Fq(a.sub(b)),
}),
_ => Err(PyTypeError::new_err(
"- requires both polynomials to share the same coefficient domain",
)),
}
}
fn __mul__(&self, other: &PyPolynomial) -> PyResult<PyPolynomial> {
match (&self.inner, &other.inner) {
(PolyErased::Int(a), PolyErased::Int(b)) => Ok(PyPolynomial {
inner: PolyErased::Int(a.mul(b)),
}),
(PolyErased::Rat(a), PolyErased::Rat(b)) => Ok(PyPolynomial {
inner: PolyErased::Rat(a.mul(b)),
}),
(PolyErased::Fq(a), PolyErased::Fq(b)) => Ok(PyPolynomial {
inner: PolyErased::Fq(a.mul(b)),
}),
_ => Err(PyTypeError::new_err(
"* requires both polynomials to share the same coefficient domain",
)),
}
}
fn __neg__(&self) -> PyPolynomial {
match &self.inner {
PolyErased::Int(p) => PyPolynomial {
inner: PolyErased::Int(p.mul_scalar(&Integer::from(-1))),
},
PolyErased::Rat(p) => PyPolynomial {
inner: PolyErased::Rat(p.mul_scalar(&Rational::new(-1, 1))),
},
PolyErased::Fq(p) => {
let field = p.domain();
PyPolynomial {
inner: PolyErased::Fq(p.mul_scalar(&field.element(-1))),
}
}
}
}
fn __eq__(&self, other: &PyPolynomial) -> bool {
match (&self.inner, &other.inner) {
(PolyErased::Int(a), PolyErased::Int(b)) => a == b,
(PolyErased::Rat(a), PolyErased::Rat(b)) => a == b,
(PolyErased::Fq(a), PolyErased::Fq(b)) => a == b,
_ => false,
}
}
fn __repr__(&self) -> String {
match &self.inner {
PolyErased::Int(p) => {
format!("Polynomial([{}], 'integer')", fmt_poly_coeffs(p))
}
PolyErased::Rat(p) => {
format!("Polynomial([{}], 'rational')", fmt_poly_coeffs(p))
}
PolyErased::Fq(p) => format!(
"Polynomial([{}], domain=FiniteField({}))",
fmt_poly_coeffs(p),
p.domain().prime()
),
}
}
}
fn fmt_poly_coeffs<D: ocas_domain::Domain>(p: &DenseUnivariatePolynomial<D>) -> String
where
D::Element: std::fmt::Display,
{
p.coeffs()
.iter()
.map(|c| c.to_string())
.collect::<Vec<_>>()
.join(", ")
}