use std::fmt;
use rug::Rational;
use super::divisor::{Divisor, Place};
use super::error::FunctionFieldError;
use super::util::{constant, content_normalise, eval, is_zero, squarefree_split};
use crate::integrate::risch::poly_rde::{degree, poly_mul, poly_scale, qpoly_to_expr, trim, QPoly};
use crate::integrate::risch::rational_rde::poly_sub;
use crate::kernel::{ExprId, ExprPool};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Normalisation {
pub leading: Rational,
pub linear_term: QPoly,
pub square_factor: QPoly,
pub scale: rug::Rational,
}
impl Normalisation {
pub fn is_identity(&self) -> bool {
self.leading == 1
&& is_zero(&self.linear_term)
&& degree(&self.square_factor) == 0
&& self.scale == 1
}
}
impl fmt::Display for Normalisation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.is_identity() {
return write!(f, "Y = y (identity)");
}
write!(
f,
"Y = {}·(2·{}·y + c₁(x)) / b(x), with deg c₁ = {} and deg b = {}",
self.scale,
self.leading,
degree(&self.linear_term),
degree(&self.square_factor)
)
}
}
#[derive(Clone, Debug)]
pub struct FunctionField {
a: QPoly,
genus: usize,
normalisation: Normalisation,
}
impl PartialEq for FunctionField {
fn eq(&self, other: &Self) -> bool {
self.a == other.a
}
}
impl Eq for FunctionField {}
impl FunctionField {
pub fn new(coeffs: &[QPoly]) -> Result<Self, FunctionFieldError> {
let coeffs: Vec<QPoly> = coeffs.iter().map(|c| trim(c.clone())).collect();
let mut top = coeffs.len();
while top > 0 && is_zero(&coeffs[top - 1]) {
top -= 1;
}
if top != 3 {
return Err(FunctionFieldError::UnsupportedModel {
reason: format!(
"deg_y f = {}, but only quadratic models (deg_y f = 2) are implemented",
top as i64 - 1
),
});
}
let c2 = &coeffs[2];
if degree(c2) != 0 {
return Err(FunctionFieldError::UnsupportedModel {
reason: format!(
"the y² coefficient has degree {} in x; only a non-zero rational constant is accepted",
degree(c2)
),
});
}
let gamma = c2[0].clone();
let c1 = coeffs[1].clone();
let c0 = coeffs[0].clone();
let four_c2_c0 = poly_scale(&c0, &(Rational::from(4) * &gamma));
let big_a = trim(poly_sub(&poly_mul(&c1, &c1), &four_c2_c0));
Self::from_discriminant(big_a, gamma, c1)
}
pub fn hyperelliptic(a: &QPoly) -> Result<Self, FunctionFieldError> {
Self::from_discriminant(trim(a.clone()), Rational::from(1), Vec::new())
}
pub fn hyperelliptic_from_i64(a: &[i64]) -> Result<Self, FunctionFieldError> {
let q: QPoly = a.iter().map(|&c| Rational::from(c)).collect();
Self::hyperelliptic(&trim(q))
}
fn from_discriminant(
big_a: QPoly,
gamma: Rational,
c1: QPoly,
) -> Result<Self, FunctionFieldError> {
if is_zero(&big_a) {
return Err(FunctionFieldError::UnsupportedModel {
reason: "the discriminant c₁² − 4c₂c₀ is identically zero, so f is a perfect \
square and defines a double line, not a curve"
.into(),
});
}
let (a, b) = squarefree_split(&big_a);
let (a, scale) = content_normalise(&a);
if degree(&a) < 1 {
return Err(FunctionFieldError::UnsupportedModel {
reason: format!(
"after removing square factors the discriminant is the constant {}, so \
y is algebraic over ℚ and f factors over a quadratic number field; \
this is not a function field of a curve",
a.first().cloned().unwrap_or_else(|| Rational::from(0))
),
});
}
let genus = crate::integrate::algebraic::find_order::genus(2, &a).ok_or_else(|| {
FunctionFieldError::UnsupportedModel {
reason: "the genus formula declined the normalised model (a is not squarefree \
of positive degree)"
.into(),
}
})?;
Ok(FunctionField {
a,
genus,
normalisation: Normalisation {
leading: gamma,
linear_term: c1,
square_factor: b,
scale,
},
})
}
pub fn curve(&self) -> &QPoly {
&self.a
}
pub fn curve_degree(&self) -> usize {
degree(&self.a) as usize
}
pub fn genus(&self) -> usize {
self.genus
}
pub fn is_imaginary(&self) -> bool {
self.curve_degree() % 2 == 1
}
pub fn normalisation(&self) -> &Normalisation {
&self.normalisation
}
pub(crate) fn require_imaginary(
&self,
operation: &'static str,
) -> Result<(), FunctionFieldError> {
if self.is_imaginary() {
Ok(())
} else {
Err(FunctionFieldError::RealModel {
degree: self.curve_degree(),
genus: self.genus,
operation,
})
}
}
pub fn contains_point(&self, x: &Rational, y: &Rational) -> bool {
Rational::from(y * y) == eval(&self.a, x)
}
pub fn is_branch_point(&self, x: &Rational) -> bool {
eval(&self.a, x) == 0
}
pub(crate) fn require_same(&self, other: &Self) -> Result<(), FunctionFieldError> {
if self == other {
Ok(())
} else {
Err(FunctionFieldError::CurveMismatch {
left: self.to_string(),
right: other.to_string(),
})
}
}
pub fn canonical_divisor(&self) -> Result<Divisor, FunctionFieldError> {
self.require_imaginary("canonical_divisor")?;
let k = 2 * self.genus as i64 - 2;
Divisor::from_terms(self.clone(), [(Place::Infinity, rug::Integer::from(k))])
}
pub fn curve_expr(&self, var: ExprId, pool: &ExprPool) -> ExprId {
qpoly_to_expr(&self.a, var, pool)
}
pub(crate) fn one(&self) -> QPoly {
constant(Rational::from(1))
}
}
impl fmt::Display for FunctionField {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "y² = ")?;
let d = degree(&self.a);
if d < 0 {
return write!(f, "0");
}
let mut first = true;
for k in (0..=d as usize).rev() {
let c = &self.a[k];
if *c == 0 {
continue;
}
if !first {
write!(f, " + ")?;
}
first = false;
match k {
0 => write!(f, "{c}")?,
1 if *c == 1 => write!(f, "x")?,
1 => write!(f, "{c}·x")?,
_ if *c == 1 => write!(f, "x^{k}")?,
_ => write!(f, "{c}·x^{k}")?,
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn qp(cs: &[i64]) -> QPoly {
trim(cs.iter().map(|&c| Rational::from(c)).collect())
}
#[test]
fn genus_of_the_standard_models() {
let e = FunctionField::hyperelliptic_from_i64(&[0, -1, 0, 1]).unwrap();
assert_eq!(e.genus(), 1);
assert!(e.is_imaginary());
let c2 = FunctionField::hyperelliptic_from_i64(&[1, 0, 0, 0, 0, 1]).unwrap();
assert_eq!(c2.genus(), 2);
assert!(c2.is_imaginary());
let r = FunctionField::hyperelliptic_from_i64(&[0, 1]).unwrap();
assert_eq!(r.genus(), 0);
assert!(r.is_imaginary());
}
#[test]
fn even_degree_models_report_genus_but_are_real() {
let f = FunctionField::hyperelliptic_from_i64(&[1, 0, 0, 0, 1]).unwrap();
assert_eq!(f.genus(), 1);
assert!(!f.is_imaginary());
let f = FunctionField::hyperelliptic_from_i64(&[1, 1, 0, 0, 0, 0, 1]).unwrap();
assert_eq!(f.genus(), 2);
assert!(!f.is_imaginary());
}
#[test]
fn a_real_model_refuses_divisor_operations_but_not_genus() {
let f = FunctionField::hyperelliptic_from_i64(&[1, 0, 0, 0, 1]).unwrap();
assert_eq!(f.genus(), 1);
let err = f.canonical_divisor().unwrap_err();
assert!(matches!(err, FunctionFieldError::RealModel { .. }));
use crate::errors::AlkahestError;
assert_eq!(err.code(), "E-FFLD-002");
}
#[test]
fn completing_the_square_normalises_a_general_quadratic() {
let f = FunctionField::new(&[qp(&[0, 0, 1, -1]), qp(&[0, 2]), qp(&[1])]).unwrap();
assert_eq!(f.curve(), &qp(&[0, 1]));
assert_eq!(f.genus(), 0);
assert!(!f.normalisation().is_identity());
}
#[test]
fn a_scalar_multiple_of_a_model_is_the_same_field() {
let a = FunctionField::hyperelliptic_from_i64(&[0, -1, 0, 1]).unwrap();
let b = FunctionField::new(&[qp(&[0, 2, 0, -2]), Vec::new(), qp(&[2])]).unwrap();
assert_eq!(a, b, "the normalised models must coincide");
assert_eq!(b.genus(), 1);
}
#[test]
fn identity_normalisation_is_reported_as_such() {
let f = FunctionField::hyperelliptic_from_i64(&[0, -1, 0, 1]).unwrap();
assert!(f.normalisation().is_identity());
}
#[test]
fn higher_degree_models_are_refused_not_guessed() {
use crate::errors::AlkahestError;
let err = FunctionField::new(&[qp(&[1, 0, 0, 0, -1]), Vec::new(), Vec::new(), qp(&[1])])
.unwrap_err();
assert_eq!(err.code(), "E-FFLD-001");
assert!(format!("{err}").contains("deg_y f = 3"));
}
#[test]
fn a_non_constant_leading_coefficient_is_refused() {
use crate::errors::AlkahestError;
let err = FunctionField::new(&[qp(&[1, 0, 0, -1]), Vec::new(), qp(&[0, 1])]).unwrap_err();
assert_eq!(err.code(), "E-FFLD-001");
}
#[test]
fn a_perfect_square_is_refused() {
use crate::errors::AlkahestError;
let err = FunctionField::new(&[qp(&[0, 0, 1]), qp(&[0, 2]), qp(&[1])]).unwrap_err();
assert_eq!(err.code(), "E-FFLD-001");
assert!(format!("{err}").contains("identically zero"));
}
#[test]
fn a_constant_discriminant_is_refused() {
use crate::errors::AlkahestError;
let err = FunctionField::hyperelliptic_from_i64(&[2]).unwrap_err();
assert_eq!(err.code(), "E-FFLD-001");
}
#[test]
fn contains_point_checks_the_normalised_model() {
let f = FunctionField::hyperelliptic_from_i64(&[0, -1, 0, 1]).unwrap();
for x in [-1i64, 0, 1] {
assert!(f.contains_point(&Rational::from(x), &Rational::from(0)));
assert!(f.is_branch_point(&Rational::from(x)));
}
assert!(!f.contains_point(&Rational::from(2), &Rational::from(0)));
}
#[test]
fn display_renders_the_normalised_curve() {
let f = FunctionField::hyperelliptic_from_i64(&[0, -1, 0, 1]).unwrap();
assert_eq!(f.to_string(), "y² = x^3 + -1·x");
}
}