use crate::error::{SpecialError, SpecialResult};
#[non_exhaustive]
#[derive(Debug, Clone)]
pub enum HyperbolicSurface {
ModularCurve {
level: usize,
},
PrincipalCongruence {
level: usize,
},
Custom {
geodesic_lengths: Vec<f64>,
},
}
#[derive(Debug, Clone)]
pub struct SelbergConfig {
pub n_geodesics: usize,
pub max_k: usize,
pub tol: f64,
pub max_trace: usize,
}
impl Default for SelbergConfig {
fn default() -> Self {
SelbergConfig {
n_geodesics: 50,
max_k: 5,
tol: 1e-10,
max_trace: 30,
}
}
}
fn gcd(a: usize, b: usize) -> usize {
if b == 0 {
a
} else {
gcd(b, a % b)
}
}
fn hyperbolic_norm_from_trace(t: i64) -> f64 {
let t = t.abs();
if t <= 2 {
return 0.0; }
let tf = t as f64;
let lambda = (tf + (tf * tf - 4.0).sqrt()) / 2.0;
lambda * lambda
}
pub fn enumerate_primitive_geodesics(max_trace: usize) -> Vec<f64> {
let mut norms: Vec<f64> = Vec::new();
for t in 3..=(max_trace as i64) {
let norm = hyperbolic_norm_from_trace(t);
if norm > 1.0 {
norms.push(norm);
}
}
let mut extra_norms = collect_small_primitive_norms(max_trace.min(15));
extra_norms.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
for &norm in &extra_norms {
if norm > 1.0 && !norms.iter().any(|&n| (n - norm).abs() < 1e-6) {
norms.push(norm);
}
}
norms.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
norms
}
fn collect_small_primitive_norms(bound: usize) -> Vec<f64> {
let mut norms = Vec::new();
let b = bound as i64;
for a in -b..=b {
for d in -b..=b {
let trace = a + d;
if trace.abs() <= 2 {
continue; }
let det_target = a * d - 1; for c in 1..=b {
if det_target % c == 0 {
let bb = det_target / c;
if bb.abs() <= b {
let disc = trace * trace - 4;
if disc > 0 && is_not_perfect_square(disc) {
let norm = hyperbolic_norm_from_trace(trace);
norms.push(norm);
}
}
}
}
}
}
norms
}
fn is_not_perfect_square(n: i64) -> bool {
if n <= 0 {
return false;
}
let s = (n as f64).sqrt() as i64;
s * s != n && (s + 1) * (s + 1) != n
}
fn get_geodesic_norms(surface: &HyperbolicSurface, config: &SelbergConfig) -> Vec<f64> {
match surface {
HyperbolicSurface::ModularCurve { level: _ } => {
let mut norms = enumerate_primitive_geodesics(config.max_trace);
norms.truncate(config.n_geodesics);
norms
}
HyperbolicSurface::PrincipalCongruence { level } => {
let base_norms = enumerate_primitive_geodesics(config.max_trace);
let mut norms = Vec::new();
for &norm in &base_norms {
if norms.len() >= config.n_geodesics {
break;
}
let lifted_norm = norm.powf(*level as f64);
norms.push(lifted_norm);
}
norms.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
norms.truncate(config.n_geodesics);
norms
}
HyperbolicSurface::Custom { geodesic_lengths } => {
let mut norms: Vec<f64> = geodesic_lengths
.iter()
.take(config.n_geodesics)
.copied()
.collect();
norms.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
norms
}
}
}
pub fn selberg_zeta(
surface: &HyperbolicSurface,
s: f64,
config: &SelbergConfig,
) -> SpecialResult<f64> {
if s <= 1.0 {
return Err(SpecialError::DomainError(format!(
"Selberg zeta convergence requires s > 1, got s = {s}"
)));
}
let norms = get_geodesic_norms(surface, config);
if norms.is_empty() {
return Err(SpecialError::ComputationError(
"No primitive geodesics found for the given surface".to_string(),
));
}
let mut log_z = 0.0f64;
for &norm in &norms {
if norm <= 1.0 {
continue; }
for k in 0..=config.max_k {
let exp_val = norm.powf(-(s + k as f64));
if exp_val >= 1.0 {
return Err(SpecialError::ComputationError(format!(
"Euler product factor 1 - N^{{-(s+k)}} ≤ 0 for N={norm}, s={s}, k={k}"
)));
}
let factor = (1.0 - exp_val).ln();
if !factor.is_finite() {
return Err(SpecialError::ComputationError(
"Non-finite log factor in Selberg zeta Euler product".to_string(),
));
}
log_z += factor;
}
}
Ok(log_z.exp())
}
pub fn selberg_trace_formula_check(s: f64, n_eigenvalues: usize) -> f64 {
let area = std::f64::consts::PI / 3.0;
let mut sum = 0.0f64;
for n in 1..=n_eigenvalues {
let lambda_n = 4.0 * std::f64::consts::PI * n as f64 / area;
let _ = s; sum += 1.0 / lambda_n;
}
sum
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_enumerate_primitive_geodesics_nonempty() {
let norms = enumerate_primitive_geodesics(15);
assert!(
!norms.is_empty(),
"Expected non-empty list of primitive geodesics"
);
}
#[test]
fn test_enumerate_primitive_geodesics_sorted() {
let norms = enumerate_primitive_geodesics(20);
for window in norms.windows(2) {
assert!(
window[0] <= window[1],
"Geodesic norms should be sorted: {} > {}",
window[0],
window[1]
);
}
}
#[test]
fn test_enumerate_primitive_geodesics_first_norm() {
let norms = enumerate_primitive_geodesics(10);
assert!(!norms.is_empty());
let n0 = norms[0];
assert!(n0 > 1.0, "First geodesic norm should be > 1, got {n0}");
assert!((n0 - 6.854).abs() < 0.5, "First norm ≈ 6.854, got {n0}");
}
#[test]
fn test_selberg_zeta_positive_at_s2() {
let surface = HyperbolicSurface::ModularCurve { level: 1 };
let config = SelbergConfig::default();
let z = selberg_zeta(&surface, 2.0, &config).expect("Selberg zeta at s=2");
assert!(z > 0.0, "Z(2) should be positive, got {z}");
}
#[test]
fn test_selberg_zeta_converges_to_1_for_large_s() {
let surface = HyperbolicSurface::ModularCurve { level: 1 };
let config = SelbergConfig {
n_geodesics: 10,
max_k: 3,
..Default::default()
};
let z_large = selberg_zeta(&surface, 20.0, &config).expect("Selberg zeta at s=20");
assert!(
(z_large - 1.0).abs() < 0.1,
"Z(20) should be ≈ 1, got {z_large}"
);
}
#[test]
fn test_selberg_zeta_domain_error() {
let surface = HyperbolicSurface::ModularCurve { level: 1 };
let config = SelbergConfig::default();
let result = selberg_zeta(&surface, 0.5, &config);
assert!(result.is_err());
}
#[test]
fn test_selberg_zeta_custom_surface() {
let surface = HyperbolicSurface::Custom {
geodesic_lengths: vec![6.854, 13.928, 27.0],
};
let config = SelbergConfig::default();
let z = selberg_zeta(&surface, 3.0, &config).expect("custom selberg zeta");
assert!(z > 0.0 && z <= 1.0, "Z(3) for custom surface: {z}");
}
#[test]
fn test_hyperbolic_norm_trace3() {
let norm = hyperbolic_norm_from_trace(3);
let expected = {
let lambda = (3.0 + 5.0f64.sqrt()) / 2.0;
lambda * lambda
};
assert!(
(norm - expected).abs() < 1e-10,
"norm({}) ≈ {}, expected {}",
3,
norm,
expected
);
}
#[test]
fn test_selberg_trace_formula_check() {
let result = selberg_trace_formula_check(2.0, 10);
assert!(
result > 0.0,
"Trace formula sum should be positive: {result}"
);
}
}