use std::fmt::Debug;
use std::{fmt, marker::PhantomData};
use nalgebra::{ArrayStorage, Complex, Const, SMatrix};
use crate::arithmetic_utils::{Field, sigma_3, sigma_5};
#[derive(PartialEq, Eq, Debug, Clone, Copy)]
pub enum ModularError {
NonIntegerEntry,
NotInGroup,
NotEnoughConstraints,
SingularSystem,
NonFiniteCoefficient,
Unavailable,
InequivalentTransformationGroups,
}
impl fmt::Display for ModularError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NonIntegerEntry => write!(f, "matrix entry is not an integer"),
Self::NotInGroup => write!(f, "matrix fails this group's membership condition"),
Self::NotEnoughConstraints => {
write!(
f,
"fewer constraints supplied than the dimension of the space"
)
}
Self::SingularSystem => {
write!(f, "constraints do not pin down a unique linear combination")
}
Self::NonFiniteCoefficient => write!(f, "a solved coefficient is not finite"),
Self::Unavailable => write!(f, "no implementation is available to compute this value"),
Self::InequivalentTransformationGroups => write!(
f,
"The two subgroups of SL(2,Z) are not the same as groups or do not have the same multiplier system."
),
}
}
}
impl std::error::Error for ModularError {}
pub trait ModularTransformationGroup<R: Field>: Sized {
#[allow(dead_code)]
fn new(raw_matrix: [[R; 2]; 2]) -> Result<Self, ModularError>;
#[allow(dead_code)]
fn transform_q(&self, q: &R) -> R;
#[allow(dead_code)]
fn transform_tau(&self, tau: &R) -> Option<R> {
let numerator = tau.clone().mul_add(self.raw_a(), self.raw_b());
let denominator = tau.clone().mul_add(self.raw_c(), self.raw_d());
if denominator.is_zero() {
None
} else {
let den_inv = denominator.inv();
Some(numerator * den_inv)
}
}
#[allow(dead_code)]
fn multiplier_system(&self) -> R;
fn is_trivial_multiplier_system() -> bool;
fn raw_a(&self) -> R;
fn raw_b(&self) -> R;
fn raw_c(&self) -> R;
fn raw_d(&self) -> R;
fn raw_matrix(&self) -> [[R; 2]; 2] {
[[self.raw_a(), self.raw_b()], [self.raw_c(), self.raw_d()]]
}
}
pub enum CoerceTransformation<
R: Field,
T1: ModularTransformationGroup<R>,
T2: ModularTransformationGroup<R>,
> {
FORCED(PhantomData<R>, PhantomData<T1>, PhantomData<T2>),
}
impl<R: Field, T1: ModularTransformationGroup<R>, T2: ModularTransformationGroup<R>>
CoerceTransformation<R, T1, T2>
{
#[allow(clippy::similar_names)]
pub fn validate(
self,
gens_t1: &[T1],
gens_t2: &[T2],
close_enough: fn(&R, &R) -> bool,
) -> Result<Self, ModularError> {
for gen_t1 in gens_t1 {
let gen_t2 = T2::new(gen_t1.raw_matrix())
.map_err(|_| ModularError::InequivalentTransformationGroups)?;
let mult_t1 = gen_t1.multiplier_system();
let mult_t2 = gen_t2.multiplier_system();
if !close_enough(&mult_t1, &mult_t2) {
return Err(ModularError::InequivalentTransformationGroups);
}
}
for gen_t2 in gens_t2 {
let gen_t1 = T1::new(gen_t2.raw_matrix())
.map_err(|_| ModularError::InequivalentTransformationGroups)?;
let mult_t1 = gen_t1.multiplier_system();
let mult_t2 = gen_t2.multiplier_system();
if !close_enough(&mult_t1, &mult_t2) {
return Err(ModularError::InequivalentTransformationGroups);
}
}
Ok(self)
}
}
pub struct Sl2Z {
a: i128,
b: i128,
c: i128,
d: i128,
}
impl ModularTransformationGroup<f64> for Sl2Z {
fn new(raw_matrix: [[f64; 2]; 2]) -> Result<Self, ModularError> {
const EPSILON: f64 = 1e-9;
let [[a, b], [c, d]] = raw_matrix;
if [a, b, c, d]
.into_iter()
.any(|x| (x - x.round()).abs() > EPSILON)
{
return Err(ModularError::NonIntegerEntry);
}
#[allow(clippy::cast_possible_truncation)]
let (a, b, c, d) = (
a.round() as i128,
b.round() as i128,
c.round() as i128,
d.round() as i128,
);
if a * d - b * c != 1 {
return Err(ModularError::NotInGroup);
}
Ok(Self { a, b, c, d })
}
fn transform_q(&self, q: &f64) -> f64 {
*q
}
fn multiplier_system(&self) -> f64 {
1.0
}
#[allow(clippy::cast_precision_loss)]
fn raw_a(&self) -> f64 {
self.a as f64
}
#[allow(clippy::cast_precision_loss)]
fn raw_b(&self) -> f64 {
self.b as f64
}
#[allow(clippy::cast_precision_loss)]
fn raw_c(&self) -> f64 {
self.c as f64
}
#[allow(clippy::cast_precision_loss)]
fn raw_d(&self) -> f64 {
self.d as f64
}
fn is_trivial_multiplier_system() -> bool {
true
}
}
#[allow(dead_code)]
pub struct EtaTransformationGroup {
a: i128,
b: i128,
c: i128,
d: i128,
}
impl ModularTransformationGroup<Complex<f64>> for EtaTransformationGroup {
fn new(raw_matrix: [[Complex<f64>; 2]; 2]) -> Result<Self, ModularError> {
const EPSILON: f64 = 1e-9;
let [[a, b], [c, d]] = raw_matrix;
if [a, b, c, d]
.into_iter()
.any(|x| x.im.abs() > EPSILON || (x.re - x.re.round()).abs() > EPSILON)
{
return Err(ModularError::NonIntegerEntry);
}
#[allow(clippy::cast_possible_truncation)]
let (a, b, c, d) = (
a.re.round() as i128,
b.re.round() as i128,
c.re.round() as i128,
d.re.round() as i128,
);
if a * d - b * c != 1 {
return Err(ModularError::NotInGroup);
}
Ok(Self { a, b, c, d })
}
fn transform_q(&self, q: &Complex<f64>) -> Complex<f64> {
*q
}
fn multiplier_system(&self) -> Complex<f64> {
todo!("Dedekind eta multiplier system (Dedekind sums) not yet implemented")
}
#[allow(clippy::cast_precision_loss)]
fn raw_a(&self) -> Complex<f64> {
Complex::new(self.a as f64, 0.0)
}
#[allow(clippy::cast_precision_loss)]
fn raw_b(&self) -> Complex<f64> {
Complex::new(self.b as f64, 0.0)
}
#[allow(clippy::cast_precision_loss)]
fn raw_c(&self) -> Complex<f64> {
Complex::new(self.c as f64, 0.0)
}
#[allow(clippy::cast_precision_loss)]
fn raw_d(&self) -> Complex<f64> {
Complex::new(self.d as f64, 0.0)
}
fn is_trivial_multiplier_system() -> bool {
false
}
}
#[allow(dead_code)]
pub struct CombinedTransformationGroup<G1, G2> {
pub first: G1,
pub second: G2,
}
impl<R: Field, G1: ModularTransformationGroup<R>, G2: ModularTransformationGroup<R>>
ModularTransformationGroup<R> for CombinedTransformationGroup<G1, G2>
{
fn new(raw_matrix: [[R; 2]; 2]) -> Result<Self, ModularError> {
let [[a, b], [c, d]] = raw_matrix;
let first = G1::new([[a.clone(), b.clone()], [c.clone(), d.clone()]])?;
let second = G2::new([[a, b], [c, d]])?;
Ok(Self { first, second })
}
fn transform_q(&self, q: &R) -> R {
self.first.transform_q(q)
}
fn multiplier_system(&self) -> R {
self.first.multiplier_system() * self.second.multiplier_system()
}
fn raw_a(&self) -> R {
self.first.raw_a()
}
fn raw_b(&self) -> R {
self.first.raw_b()
}
fn raw_c(&self) -> R {
self.first.raw_c()
}
fn raw_d(&self) -> R {
self.first.raw_d()
}
fn is_trivial_multiplier_system() -> bool {
G1::is_trivial_multiplier_system() && G2::is_trivial_multiplier_system()
}
}
pub trait ModularForm<const TWICE_WEIGHT: usize, R: Field> {
type TransformationGroup: ModularTransformationGroup<R>;
fn extract_coeffs(&self, which_coeff: usize) -> Result<R, ModularError>;
fn evaluate_at(&self, q: &R) -> Result<R, ModularError>;
}
#[derive(Clone, Copy)]
pub struct EisensteinE4;
#[allow(clippy::cast_precision_loss)]
impl ModularForm<8, f64> for EisensteinE4 {
type TransformationGroup = Sl2Z;
fn extract_coeffs(&self, which_coeff: usize) -> Result<f64, ModularError> {
if which_coeff == 0 {
Ok(1.0)
} else {
Ok(240.0 * sigma_3(which_coeff) as f64)
}
}
fn evaluate_at(&self, _q: &f64) -> Result<f64, ModularError> {
Err(ModularError::Unavailable)
}
}
#[allow(dead_code)]
#[derive(Clone, Copy)]
pub struct EisensteinE6;
#[allow(clippy::cast_precision_loss)]
impl ModularForm<12, f64> for EisensteinE6 {
type TransformationGroup = Sl2Z;
fn extract_coeffs(&self, which_coeff: usize) -> Result<f64, ModularError> {
if which_coeff == 0 {
Ok(1.0)
} else {
Ok(-504.0 * sigma_5(which_coeff) as f64)
}
}
fn evaluate_at(&self, _q: &f64) -> Result<f64, ModularError> {
Err(ModularError::Unavailable)
}
}
#[allow(dead_code)]
pub struct UnitModularForm;
impl ModularForm<0, f64> for UnitModularForm {
type TransformationGroup = Sl2Z;
fn extract_coeffs(&self, which_coeff: usize) -> Result<f64, ModularError> {
Ok(if which_coeff == 0 { 1.0 } else { 0.0 })
}
fn evaluate_at(&self, _q: &f64) -> Result<f64, ModularError> {
Ok(1.0)
}
}
#[allow(dead_code)]
pub struct SumModularForm<
const TWICE_WEIGHT: usize,
R: Field,
TRANSFORM: ModularTransformationGroup<R>,
> {
pub(crate) summands: Vec<(
Box<dyn ModularForm<TWICE_WEIGHT, R, TransformationGroup = TRANSFORM>>,
R,
)>,
}
impl<const TWICE_WEIGHT: usize, R, TRANSFORM: ModularTransformationGroup<R>>
SumModularForm<TWICE_WEIGHT, R, TRANSFORM>
where
R: Debug + Field + 'static,
{
#[allow(dead_code, unused_variables, unused_mut)]
#[allow(clippy::type_complexity)]
pub fn new_from_some_coeffs<const DIM_SPACE: usize>(
basis_of_space: [Box<dyn ModularForm<TWICE_WEIGHT, R, TransformationGroup = TRANSFORM>>;
DIM_SPACE],
constrained_coeffs_values: &[Result<(usize, R), (R, R)>],
) -> Result<Self, ModularError>
where
R: nalgebra::ComplexField,
{
if constrained_coeffs_values.len() < DIM_SPACE {
return Err(ModularError::NotEnoughConstraints);
}
let mut matrix = SMatrix::<R, DIM_SPACE, DIM_SPACE>::zeros();
let mut b_col_vector = SMatrix::<R, DIM_SPACE, 1>::zeros();
todo!("Know that this modular form is in the vector space spanned by basis_of_space and
that certain linear maps gotten from extracting coefficients and/or evaluation values evaluate to
_constrained_coeffs_values");
#[allow(unreachable_code)]
let mut out = matrix.clone();
let succeeded =
nalgebra::try_invert_to::<R, Const<DIM_SPACE>, ArrayStorage<R, DIM_SPACE, DIM_SPACE>>(
matrix, &mut out,
);
if !succeeded {
return Err(ModularError::SingularSystem);
}
let w = out * b_col_vector;
let mut summands = Vec::with_capacity(DIM_SPACE);
for (idx, cur_summand) in basis_of_space.into_iter().enumerate() {
let cur_w = w[idx];
if cur_w.is_zero() {
continue;
}
if !cur_w.is_finite() {
return Err(ModularError::NonFiniteCoefficient);
}
let value = (cur_summand, cur_w);
summands.push(value);
}
Ok(Self { summands })
}
#[allow(dead_code)]
pub fn extract_coeffs(&self, which_coeff: usize) -> Result<R, ModularError> {
let mut to_return = R::zero();
for summand in &self.summands {
to_return += summand.0.extract_coeffs(which_coeff)? * summand.1.clone();
}
Ok(to_return)
}
#[allow(dead_code)]
pub fn evaluate_at(&self, q: &R) -> Result<R, ModularError> {
let mut to_return = R::zero();
for summand in &self.summands {
to_return += summand.0.evaluate_at(q)? * summand.1.clone();
}
Ok(to_return)
}
}
#[allow(dead_code)]
pub struct ProductModularForm<
R: Field,
F1,
F2,
const TWICE_WEIGHT_1: usize,
const TWICE_WEIGHT_2: usize,
const TWICE_WEIGHT_SUM: usize,
> {
pub first: F1,
pub second: F2,
r: PhantomData<R>,
}
impl<
R: Field,
F1: ModularForm<TWICE_WEIGHT_1, R>,
F2: ModularForm<TWICE_WEIGHT_2, R>,
const TWICE_WEIGHT_1: usize,
const TWICE_WEIGHT_2: usize,
const TWICE_WEIGHT_SUM: usize,
> ProductModularForm<R, F1, F2, TWICE_WEIGHT_1, TWICE_WEIGHT_2, TWICE_WEIGHT_SUM>
{
const WEIGHT_CHECK: () = assert!(
TWICE_WEIGHT_SUM == TWICE_WEIGHT_1 + TWICE_WEIGHT_2,
"TWICE_WEIGHT_SUM must equal TWICE_WEIGHT_1 + TWICE_WEIGHT_2 for ProductModularForm"
);
#[must_use = "The factors are now inside the product"]
#[allow(dead_code)]
pub fn new(first: F1, second: F2) -> Self {
let () = Self::WEIGHT_CHECK;
Self {
first,
second,
r: PhantomData,
}
}
}
impl<
R: Field,
F1: ModularForm<TWICE_WEIGHT_1, R>,
F2: ModularForm<TWICE_WEIGHT_2, R>,
const TWICE_WEIGHT_1: usize,
const TWICE_WEIGHT_2: usize,
const TWICE_WEIGHT_SUM: usize,
> ModularForm<TWICE_WEIGHT_SUM, R>
for ProductModularForm<R, F1, F2, TWICE_WEIGHT_1, TWICE_WEIGHT_2, TWICE_WEIGHT_SUM>
{
type TransformationGroup =
CombinedTransformationGroup<F1::TransformationGroup, F2::TransformationGroup>;
fn extract_coeffs(&self, which_coeff: usize) -> Result<R, ModularError> {
let mut to_return = R::zero();
for k in 0..=which_coeff {
let a_k = self.first.extract_coeffs(k)?;
let b_rest = self.second.extract_coeffs(which_coeff - k)?;
to_return += a_k * b_rest;
}
Ok(to_return)
}
fn evaluate_at(&self, q: &R) -> Result<R, ModularError> {
Ok(self.first.evaluate_at(q)? * self.second.evaluate_at(q)?)
}
}
pub fn square_modular_form<
R: Field,
const TWICE_WEIGHT: usize,
const FOUR_WEIGHT: usize,
T: ModularForm<TWICE_WEIGHT, R> + Clone,
>(
t: T,
) -> ProductModularForm<R, T, T, TWICE_WEIGHT, TWICE_WEIGHT, FOUR_WEIGHT> {
ProductModularForm::new(t.clone(), t)
}
pub fn cube_modular_form<
R: Field,
const TWICE_WEIGHT: usize,
const FOUR_WEIGHT: usize,
const SIX_WEIGHT: usize,
T: ModularForm<TWICE_WEIGHT, R> + Clone,
>(
t: T,
) -> impl ModularForm<SIX_WEIGHT, R> {
ProductModularForm::new(
t.clone(),
ProductModularForm::<R, T, T, TWICE_WEIGHT, TWICE_WEIGHT, FOUR_WEIGHT>::new(t.clone(), t),
)
}
pub struct EquivalentTransportedMF<
const TWICE_WEIGHT: usize,
R: Field,
M: ModularForm<TWICE_WEIGHT, R>,
TG: ModularTransformationGroup<R>,
> {
#[allow(dead_code)]
coercion: CoerceTransformation<R, M::TransformationGroup, TG>,
underlying: M,
}
impl<
const TWICE_WEIGHT: usize,
R: Field,
M: ModularForm<TWICE_WEIGHT, R>,
TG: ModularTransformationGroup<R>,
> EquivalentTransportedMF<TWICE_WEIGHT, R, M, TG>
{
pub fn coerce(
underlying: M,
coercion: CoerceTransformation<R, M::TransformationGroup, TG>,
) -> Self {
Self {
coercion,
underlying,
}
}
}
impl<
const TWICE_WEIGHT: usize,
R: Field,
M: ModularForm<TWICE_WEIGHT, R>,
TG: ModularTransformationGroup<R>,
> ModularForm<TWICE_WEIGHT, R> for EquivalentTransportedMF<TWICE_WEIGHT, R, M, TG>
{
type TransformationGroup = TG;
fn extract_coeffs(&self, which_coeff: usize) -> Result<R, ModularError> {
self.underlying.extract_coeffs(which_coeff)
}
fn evaluate_at(&self, q: &R) -> Result<R, ModularError> {
self.underlying.evaluate_at(q)
}
}
#[cfg(test)]
mod tests {
use std::marker::PhantomData;
use crate::lattice::EquivalentTransportedMF;
use super::{
EisensteinE4, EisensteinE6, ModularForm, ProductModularForm, Sl2Z, SumModularForm,
cube_modular_form, square_modular_form,
};
#[allow(dead_code)]
fn e8_theta_series_is_e4_eisenstein_series() {
let basis_of_space: [Box<dyn ModularForm<8, f64, TransformationGroup = Sl2Z>>; 1] =
[Box::new(EisensteinE4)];
let constrained_coeffs_values = [Ok((0, 1.0))];
let theta_e8 = SumModularForm::<8, f64, Sl2Z>::new_from_some_coeffs(
basis_of_space,
&constrained_coeffs_values,
)
.expect("E4 alone spans a 1-dimensional space pinned down by its constant term");
assert_eq!(theta_e8.summands.len(), 1);
assert_eq!(theta_e8.summands[0].1, 1.0);
assert_eq!(theta_e8.extract_coeffs(1), Ok(240.0));
}
#[test]
fn e4_sum_coeffs() {
const COEFF1: f64 = 1.0;
const COEFF2: f64 = 5.0;
let sum: SumModularForm<8, f64, Sl2Z> = SumModularForm {
summands: vec![
(
Box::new(EisensteinE4)
as Box<dyn ModularForm<8, f64, TransformationGroup = Sl2Z>>,
COEFF1,
),
(
Box::new(EisensteinE4)
as Box<dyn ModularForm<8, f64, TransformationGroup = Sl2Z>>,
COEFF2,
),
],
};
assert_eq!(sum.extract_coeffs(0), Ok(COEFF1 + COEFF2));
assert_eq!(sum.extract_coeffs(1), Ok((COEFF1 + COEFF2) * 240.0));
assert_eq!(sum.extract_coeffs(2), Ok((COEFF1 + COEFF2) * 2160.0));
}
#[test]
fn e4_times_e6_is_e10_eisenstein_series() {
let product = ProductModularForm::<f64, EisensteinE4, EisensteinE6, 8, 12, 20>::new(
EisensteinE4,
EisensteinE6,
);
assert_eq!(product.extract_coeffs(0), Ok(1.0));
assert_eq!(product.extract_coeffs(1), Ok(-264.0)); assert_eq!(product.extract_coeffs(2), Ok(-135_432.0)); assert_eq!(product.extract_coeffs(3), Ok(-5_196_576.0)); }
#[test]
fn e4_cubed_and_e6_squared() {
let e4_cubed = cube_modular_form::<f64, 8, 16, 24, EisensteinE4>(EisensteinE4);
let e6_squared = square_modular_form::<f64, 12, 24, EisensteinE6>(EisensteinE6);
assert_eq!(e4_cubed.extract_coeffs(0), Ok(1.0));
assert_eq!(e4_cubed.extract_coeffs(1), Ok(720.0));
assert_eq!(e4_cubed.extract_coeffs(2), Ok(179_280.0));
assert_eq!(e4_cubed.extract_coeffs(3), Ok(16_954_560.0));
let coercion = crate::lattice::CoerceTransformation::FORCED(
PhantomData,
PhantomData,
PhantomData::<Sl2Z>,
);
let e4_cubed_fixed = EquivalentTransportedMF::coerce(e4_cubed, coercion);
assert_eq!(e4_cubed_fixed.extract_coeffs(0), Ok(1.0));
assert_eq!(e4_cubed_fixed.extract_coeffs(1), Ok(720.0));
assert_eq!(e4_cubed_fixed.extract_coeffs(2), Ok(179_280.0));
assert_eq!(e4_cubed_fixed.extract_coeffs(3), Ok(16_954_560.0));
assert_eq!(e6_squared.extract_coeffs(0), Ok(1.0));
assert_eq!(e6_squared.extract_coeffs(1), Ok(-1008.0));
assert_eq!(e6_squared.extract_coeffs(2), Ok(220_752.0));
assert_eq!(e6_squared.extract_coeffs(3), Ok(16_519_104.0));
let coercion = crate::lattice::CoerceTransformation::FORCED(
PhantomData,
PhantomData,
PhantomData::<Sl2Z>,
);
let e6_squared_fixed = EquivalentTransportedMF::coerce(e6_squared, coercion);
assert_eq!(e6_squared_fixed.extract_coeffs(0), Ok(1.0));
assert_eq!(e6_squared_fixed.extract_coeffs(1), Ok(-1008.0));
assert_eq!(e6_squared_fixed.extract_coeffs(2), Ok(220_752.0));
assert_eq!(e6_squared_fixed.extract_coeffs(3), Ok(16_519_104.0));
let sum: SumModularForm<24, f64, Sl2Z> = SumModularForm {
summands: vec![
(
Box::new(e4_cubed_fixed)
as Box<dyn ModularForm<24, f64, TransformationGroup = Sl2Z>>,
1.0,
),
(
Box::new(e6_squared_fixed)
as Box<dyn ModularForm<24, f64, TransformationGroup = Sl2Z>>,
-1.0,
),
],
};
assert_eq!(sum.extract_coeffs(0), Ok(0.0));
assert_eq!(sum.extract_coeffs(1), Ok(1_728.0));
assert_eq!(sum.extract_coeffs(2), Ok(-41_472.0));
assert_eq!(sum.extract_coeffs(3), Ok(435_456.0));
}
}