Skip to main content

ocas_py/
algebraic.rs

1//! Python bindings for algebraic number fields and polynomials over them.
2//!
3//! Wraps [`ocas_domain::AlgebraicNumberField`] (an extension
4//! $\mathbb{Q}(\alpha)$ defined by a monic minimal polynomial) and
5//! [`ocas_poly::DenseUnivariatePolynomial`] over it, exposing construction,
6//! arithmetic on elements, and Trager factorization.
7//!
8//! ```python
9//! from ocas import AlgebraicExtension, AlgebraicPolynomial
10//!
11//! # ℚ(√2): minimal polynomial α² − 2 (ascending coefficients).
12//! field = AlgebraicExtension([-2, 0, 1])
13//! print(field.extension_degree())   # 2
14//!
15//! # x² − 2 splits over ℚ(√2) as (x − α)(x + α).
16//! p = AlgebraicPolynomial(field, [-2, 0, 1])
17//! for fac in p.factor():
18//!     print(fac.factor.to_string(), fac.multiplicity)
19//! ```
20
21use ocas_domain::{AlgebraicElement, AlgebraicNumberField, Rational, RationalDomain};
22use ocas_poly::DenseUnivariatePolynomial;
23use pyo3::exceptions::{PyTypeError, PyValueError};
24use pyo3::prelude::*;
25
26/// An algebraic number field $K = \mathbb{Q}(\alpha)$ defined by a monic
27/// minimal polynomial with rational coefficients.
28///
29/// The minimal polynomial is given as a list of coefficients in ascending
30/// degree order; the leading (last) coefficient must be `1`. For example,
31/// `AlgebraicExtension([-2, 0, 1])` defines $\alpha^2 - 2$ (i.e. $\mathbb{Q}(\sqrt{2})$).
32#[pyclass(name = "AlgebraicExtension")]
33pub struct PyAlgebraicExtension {
34    pub(crate) field: AlgebraicNumberField,
35}
36
37/// An element of an [`AlgebraicExtension`], stored as a polynomial in $\alpha$
38/// with rational coefficients (ascending degree).
39#[pyclass(name = "AlgebraicElement")]
40pub struct PyAlgebraicElement {
41    pub(crate) elem: AlgebraicElement<Rational>,
42}
43
44/// A dense univariate polynomial over an [`AlgebraicExtension`].
45#[pyclass(name = "AlgebraicPolynomial", skip_from_py_object)]
46#[derive(Clone)]
47pub struct PyAlgebraicPolynomial {
48    pub(crate) inner: DenseUnivariatePolynomial<AlgebraicNumberField>,
49}
50
51/// A single (polynomial, multiplicity) factor returned by
52/// [`AlgebraicPolynomial.factor`][PyAlgebraicPolynomial::factor].
53#[pyclass(name = "AlgebraicFactor", skip_from_py_object)]
54pub struct PyAlgebraicFactor {
55    #[pyo3(get)]
56    pub factor: PyAlgebraicPolynomial,
57    #[pyo3(get)]
58    pub multiplicity: usize,
59}
60
61// ------------------------------------------------------------------
62//  Parsing helpers
63// ------------------------------------------------------------------
64
65/// Parse a Python `int` or `(num, denom)` tuple into a [`Rational`].
66fn py_to_rational(obj: &Bound<'_, PyAny>) -> PyResult<Rational> {
67    if let Ok(n) = obj.extract::<i64>() {
68        Ok(Rational::new(n, 1))
69    } else if let Ok((num, den)) = obj.extract::<(i64, i64)>() {
70        if den == 0 {
71            Err(PyValueError::new_err("rational denominator cannot be zero"))
72        } else {
73            Ok(Rational::new(num, den))
74        }
75    } else {
76        Err(PyTypeError::new_err("expected int or (num, denom) tuple"))
77    }
78}
79
80/// Parse minimal-polynomial coefficients: a list of ints or `(num, denom)` tuples.
81fn parse_min_poly(coeffs: &Bound<'_, PyAny>) -> PyResult<Vec<Rational>> {
82    let iter = coeffs.try_iter().map_err(|_| {
83        PyTypeError::new_err(
84            "minimal polynomial coefficients must be a list of ints or (num, denom) tuples",
85        )
86    })?;
87    iter.map(|c| py_to_rational(&c?)).collect()
88}
89
90/// Build an algebraic-field element from a Python value.
91///
92/// Accepts:
93/// - an `int` or `(num, denom)` tuple (embedded as a base-domain constant),
94/// - an [`AlgebraicElement`] instance, or
95/// - a list of ints / `(num, denom)` tuples (ascending $\alpha$-polynomial).
96fn py_to_anf_element(
97    field: &AlgebraicNumberField,
98    obj: &Bound<'_, PyAny>,
99) -> PyResult<AlgebraicElement<Rational>> {
100    if let Ok(r) = py_to_rational(obj) {
101        return Ok(field.from_base(r));
102    }
103    if let Ok(elem) = obj.extract::<PyRef<'_, PyAlgebraicElement>>() {
104        return Ok(elem.elem.clone());
105    }
106    if obj.try_iter().is_ok() {
107        let cs: PyResult<Vec<Rational>> = obj
108            .try_iter()
109            .unwrap()
110            .map(|c| py_to_rational(&c?))
111            .collect();
112        return Ok(field.element(cs?));
113    }
114    Err(PyTypeError::new_err(
115        "coefficient must be int, (num, denom), list, or AlgebraicElement",
116    ))
117}
118
119// ------------------------------------------------------------------
120//  AlgebraicExtension
121// ------------------------------------------------------------------
122
123#[pymethods]
124impl PyAlgebraicExtension {
125    /// Create an algebraic number field from its monic minimal polynomial.
126    ///
127    /// `min_poly` is the list of rational coefficients in ascending degree
128    /// order; the leading coefficient must be `1`.
129    #[new]
130    fn new(min_poly: &Bound<'_, PyAny>) -> PyResult<Self> {
131        let coeffs = parse_min_poly(min_poly)?;
132        if coeffs.len() < 2 {
133            return Err(PyValueError::new_err(
134                "minimal polynomial must have degree at least 1",
135            ));
136        }
137        if coeffs.last() != Some(&Rational::new(1, 1)) {
138            return Err(PyValueError::new_err("minimal polynomial must be monic"));
139        }
140        Ok(Self {
141            field: AlgebraicNumberField::new(RationalDomain, coeffs),
142        })
143    }
144
145    /// Return the extension degree $\deg(m)$.
146    fn extension_degree(&self) -> usize {
147        self.field.extension_degree()
148    }
149
150    /// Return the generator $\alpha$ of the extension.
151    fn alpha(&self) -> PyAlgebraicElement {
152        PyAlgebraicElement {
153            elem: self.field.alpha(),
154        }
155    }
156
157    /// Embed a rational constant (int or `(num, denom)`) into the field.
158    #[allow(clippy::wrong_self_convention)]
159    fn from_base(&self, c: &Bound<'_, PyAny>) -> PyResult<PyAlgebraicElement> {
160        let r = py_to_rational(c)?;
161        Ok(PyAlgebraicElement {
162            elem: self.field.from_base(r),
163        })
164    }
165
166    /// Create an element from $\alpha$-polynomial coefficients (ascending).
167    fn element(&self, coeffs: &Bound<'_, PyAny>) -> PyResult<PyAlgebraicElement> {
168        let iter = coeffs.try_iter().map_err(|_| {
169            PyTypeError::new_err(
170                "element coefficients must be a list of ints or (num, denom) tuples",
171            )
172        })?;
173        let cs: PyResult<Vec<Rational>> = iter.map(|c| py_to_rational(&c?)).collect();
174        Ok(PyAlgebraicElement {
175            elem: self.field.element(cs?),
176        })
177    }
178
179    fn __repr__(&self) -> String {
180        format!("AlgebraicExtension(deg={})", self.field.extension_degree())
181    }
182}
183
184// ------------------------------------------------------------------
185//  AlgebraicElement
186// ------------------------------------------------------------------
187
188#[pymethods]
189impl PyAlgebraicElement {
190    /// Return the $\alpha$-polynomial coefficients (ascending) as decimal
191    /// strings (rationals render as `n/d`).
192    fn coeffs(&self) -> Vec<String> {
193        self.elem.coeffs().iter().map(|c| c.to_string()).collect()
194    }
195
196    fn __str__(&self) -> String {
197        format!("{}", self.elem)
198    }
199
200    fn __repr__(&self) -> String {
201        format!("AlgebraicElement({})", self.elem)
202    }
203}
204
205// ------------------------------------------------------------------
206//  AlgebraicPolynomial
207// ------------------------------------------------------------------
208
209#[pymethods]
210impl PyAlgebraicPolynomial {
211    /// Create a polynomial over an algebraic number field.
212    ///
213    /// `coeffs` is a list with the constant term first. Each item is one of:
214    /// - an `int` or `(num, denom)` tuple (a rational constant),
215    /// - a list of ints / `(num, denom)` tuples (ascending $\alpha$-polynomial), or
216    /// - an `AlgebraicElement`.
217    #[new]
218    fn new(field: PyRef<'_, PyAlgebraicExtension>, coeffs: &Bound<'_, PyAny>) -> PyResult<Self> {
219        let f = &field.field;
220        let iter = coeffs
221            .try_iter()
222            .map_err(|_| PyTypeError::new_err("polynomial coefficients must be a list"))?;
223        let mut out = Vec::new();
224        for c in iter {
225            out.push(py_to_anf_element(f, &c?)?);
226        }
227        Ok(Self {
228            inner: DenseUnivariatePolynomial::from_coeffs(f.clone(), out),
229        })
230    }
231
232    /// Return the degree, or `None` for the zero polynomial.
233    fn degree(&self) -> Option<usize> {
234        self.inner.degree()
235    }
236
237    /// Return the number of stored coefficients.
238    fn len(&self) -> usize {
239        self.inner.coeffs().len()
240    }
241
242    /// Return `True` if this is the zero polynomial.
243    fn is_zero(&self) -> bool {
244        self.inner.is_zero()
245    }
246
247    /// Return the coefficients (constant term first). Each coefficient is a
248    /// list of decimal strings giving the $\alpha$-polynomial in ascending
249    /// degree order.
250    fn coeffs(&self) -> Vec<Vec<String>> {
251        self.inner
252            .coeffs()
253            .iter()
254            .map(|c| c.coeffs().iter().map(|r| r.to_string()).collect())
255            .collect()
256    }
257
258    fn __str__(&self) -> String {
259        format!("{}", PolyDisplay(&self.inner))
260    }
261
262    /// Return the list of irreducible factors with multiplicities.
263    fn factor(&self) -> Vec<PyAlgebraicFactor> {
264        self.inner
265            .factor()
266            .into_iter()
267            .map(|(f, m)| PyAlgebraicFactor {
268                factor: PyAlgebraicPolynomial { inner: f },
269                multiplicity: m,
270            })
271            .collect()
272    }
273}
274
275// ------------------------------------------------------------------
276//  Display helper
277// ------------------------------------------------------------------
278
279/// Wrapper to render an algebraic-field polynomial as `c0 + c1*x + ...`.
280struct PolyDisplay<'a>(&'a DenseUnivariatePolynomial<AlgebraicNumberField>);
281
282impl std::fmt::Display for PolyDisplay<'_> {
283    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
284        let coeffs = self.0.coeffs();
285        if coeffs.is_empty() {
286            return write!(f, "0");
287        }
288        let mut first = true;
289        for (i, c) in coeffs.iter().enumerate() {
290            // Skip zero coefficients for compactness.
291            if c.coeffs().is_empty() {
292                continue;
293            }
294            if !first {
295                write!(f, " + ")?;
296            }
297            first = false;
298            match i {
299                0 => write!(f, "({})", c)?,
300                1 => write!(f, "({})*x", c)?,
301                _ => write!(f, "({})*x^{}", c, i)?,
302            }
303        }
304        if first {
305            write!(f, "0")?;
306        }
307        Ok(())
308    }
309}