use num_complex::Complex64;
pub trait IntCoeffsSlice {
fn int_coeffs_slice(&self) -> &[i64];
}
#[inline]
pub fn add_basis<const PHI: usize>(x: &[i64; PHI], y: &[i64; PHI]) -> [i64; PHI] {
let mut out = [0i64; PHI];
let mut i = 0;
while i < PHI {
out[i] = x[i] + y[i];
i += 1;
}
out
}
#[inline]
pub fn sub_basis<const PHI: usize>(x: &[i64; PHI], y: &[i64; PHI]) -> [i64; PHI] {
let mut out = [0i64; PHI];
let mut i = 0;
while i < PHI {
out[i] = x[i] - y[i];
i += 1;
}
out
}
#[inline]
pub fn neg_basis<const PHI: usize>(x: &[i64; PHI]) -> [i64; PHI] {
let mut out = [0i64; PHI];
let mut i = 0;
while i < PHI {
out[i] = -x[i];
i += 1;
}
out
}
#[inline]
pub fn mul_basis<const PHI: usize>(
x: &[i64; PHI],
y: &[i64; PHI],
reduction: &[i64; PHI],
) -> [i64; PHI] {
let mut scratch = [0i64; 32];
debug_assert!(
2 * PHI <= scratch.len(),
"mul_basis: PHI={} too large for 32-slot scratch",
PHI,
);
let mut i = 0;
while i < PHI {
let xi = x[i];
if xi != 0 {
let mut j = 0;
while j < PHI {
scratch[i + j] += xi * y[j];
j += 1;
}
}
i += 1;
}
let mut k = 2 * PHI - 2;
while k >= PHI {
let coeff = scratch[k];
if coeff != 0 {
scratch[k] = 0;
let base = k - PHI;
let mut j = 0;
while j < PHI {
scratch[base + j] += coeff * reduction[j];
j += 1;
}
}
k -= 1;
}
let mut out = [0i64; PHI];
let mut i = 0;
while i < PHI {
out[i] = scratch[i];
i += 1;
}
out
}
#[inline]
pub fn conj_basis<const PHI: usize>(x: &[i64; PHI], conj_matrix: &[[i64; PHI]; PHI]) -> [i64; PHI] {
let mut out = [0i64; PHI];
let mut i = 0;
while i < PHI {
let mut acc: i64 = 0;
let mut j = 0;
while j < PHI {
acc += conj_matrix[i][j] * x[j];
j += 1;
}
out[i] = acc;
i += 1;
}
out
}
#[inline]
pub fn complex64_basis<const PHI: usize>(
x: &[i64; PHI],
cartesian: &[Complex64; PHI],
) -> Complex64 {
let mut acc = Complex64::new(0.0, 0.0);
let mut i = 0;
while i < PHI {
let xi = x[i] as f64;
acc += Complex64::new(xi * cartesian[i].re, xi * cartesian[i].im);
i += 1;
}
acc
}
#[inline]
pub fn re_sign_basis<const PHI: usize, const K: usize>(
x: &[i64; PHI],
re_decomp: &[[i64; K]; PHI],
real_sign: fn(&[i64; K]) -> i8,
) -> i8 {
let mut acc = [0i64; K];
let mut i = 0;
while i < PHI {
let xi = x[i];
if xi != 0 {
let mut j = 0;
while j < K {
acc[j] += xi * re_decomp[i][j];
j += 1;
}
}
i += 1;
}
real_sign(&acc)
}
#[inline]
pub fn im_sign_basis<const PHI: usize, const K: usize>(
x: &[i64; PHI],
im_decomp: &[[i64; K]; PHI],
real_sign: fn(&[i64; K]) -> i8,
) -> i8 {
let mut acc = [0i64; K];
let mut i = 0;
while i < PHI {
let xi = x[i];
if xi != 0 {
let mut j = 0;
while j < K {
acc[j] += xi * im_decomp[i][j];
j += 1;
}
}
i += 1;
}
real_sign(&acc)
}
#[inline]
#[allow(clippy::too_many_arguments)]
pub fn intersect_unit_segments_basis<const PHI: usize, const K: usize>(
s1: &([i64; PHI], [i64; PHI]),
s2: &([i64; PHI], [i64; PHI]),
reduction: &[i64; PHI],
conj_matrix: &[[i64; PHI]; PHI],
re_decomp: &[[i64; K]; PHI],
im_decomp: &[[i64; K]; PHI],
one_in_real_basis: &[i64; K],
real_sign: fn(&[i64; K]) -> i8,
) -> bool {
let (a, b) = (s1.0, s1.1);
let (c, d) = (s2.0, s2.1);
if a == c || a == d || b == c || b == d {
return false;
}
let u_a = sub_basis::<PHI>(&b, &a);
let u_b = sub_basis::<PHI>(&d, &c);
let delta = sub_basis::<PHI>(&c, &a);
let conj_u_a = conj_basis::<PHI>(&u_a, conj_matrix);
let v_z = mul_basis::<PHI>(&conj_u_a, &delta, reduction);
let sign_v = im_sign_basis::<PHI, K>(&v_z, im_decomp, real_sign);
let k_z = mul_basis::<PHI>(&conj_u_a, &u_b, reduction);
let sign_k = im_sign_basis::<PHI, K>(&k_z, im_decomp, real_sign);
if sign_k == 0 {
if sign_v != 0 {
return false;
}
let t = project_re::<PHI, K>(&v_z, re_decomp);
let k_real = project_re::<PHI, K>(&k_z, re_decomp);
let sign_k_real = real_sign(&k_real);
if sign_k_real > 0 {
let t_plus_1 = add_kvec::<K>(&t, one_in_real_basis);
let t_minus_1 = sub_kvec::<K>(&t, one_in_real_basis);
return real_sign(&t_plus_1) > 0 && real_sign(&t_minus_1) < 0;
} else {
let two = scale_kvec::<K>(one_in_real_basis, 2);
let t_minus_2 = sub_kvec::<K>(&t, &two);
return real_sign(&t) > 0 && real_sign(&t_minus_2) < 0;
}
}
let in_open_0_1 = |t: &[i64; K]| -> bool {
let t_minus_1 = sub_kvec::<K>(t, one_in_real_basis);
real_sign(t) > 0 && real_sign(&t_minus_1) < 0
};
let in_open_neg1_0 = |t: &[i64; K]| -> bool {
let t_plus_1 = add_kvec::<K>(t, one_in_real_basis);
real_sign(t) < 0 && real_sign(&t_plus_1) > 0
};
let v_plus_k = add_basis::<PHI>(&v_z, &k_z);
let sign_v_plus_k = im_sign_basis::<PHI, K>(&v_plus_k, im_decomp, real_sign);
if sign_v == 0 {
return in_open_0_1(&project_re::<PHI, K>(&v_z, re_decomp));
}
if sign_v_plus_k == 0 {
return in_open_0_1(&project_re::<PHI, K>(&v_plus_k, re_decomp));
}
if (sign_v > 0) == (sign_v_plus_k > 0) {
return false;
}
let conj_delta = conj_basis::<PHI>(&delta, conj_matrix);
let w_z = mul_basis::<PHI>(&conj_delta, &u_b, reduction);
let w_minus_k = sub_basis::<PHI>(&w_z, &k_z);
let sign_w = im_sign_basis::<PHI, K>(&w_z, im_decomp, real_sign);
let sign_w_minus_k = im_sign_basis::<PHI, K>(&w_minus_k, im_decomp, real_sign);
if sign_w == 0 {
return in_open_neg1_0(&project_re::<PHI, K>(&w_z, re_decomp));
}
if sign_w_minus_k == 0 {
return in_open_neg1_0(&project_re::<PHI, K>(&w_minus_k, re_decomp));
}
(sign_w > 0) != (sign_w_minus_k > 0)
}
#[inline]
fn project_re<const PHI: usize, const K: usize>(
x: &[i64; PHI],
re_decomp: &[[i64; K]; PHI],
) -> [i64; K] {
let mut acc = [0i64; K];
let mut i = 0;
while i < PHI {
let xi = x[i];
if xi != 0 {
let mut j = 0;
while j < K {
acc[j] += xi * re_decomp[i][j];
j += 1;
}
}
i += 1;
}
acc
}
#[inline]
fn add_kvec<const K: usize>(x: &[i64; K], y: &[i64; K]) -> [i64; K] {
let mut out = [0i64; K];
let mut i = 0;
while i < K {
out[i] = x[i] + y[i];
i += 1;
}
out
}
#[inline]
fn sub_kvec<const K: usize>(x: &[i64; K], y: &[i64; K]) -> [i64; K] {
let mut out = [0i64; K];
let mut i = 0;
while i < K {
out[i] = x[i] - y[i];
i += 1;
}
out
}
#[inline]
fn scale_kvec<const K: usize>(x: &[i64; K], s: i64) -> [i64; K] {
let mut out = [0i64; K];
let mut i = 0;
while i < K {
out[i] = x[i] * s;
i += 1;
}
out
}
pub fn derive_conj_matrix<const PHI: usize>(n: usize, reduction: &[i64; PHI]) -> [[i64; PHI]; PHI] {
let units = derive_units_lookup::<PHI>(n, reduction);
let mut out = [[0i64; PHI]; PHI];
let mut j = 0;
while j < PHI {
let idx = if j == 0 { 0 } else { n - j };
let col = &units[idx];
let mut i = 0;
while i < PHI {
out[i][j] = col[i];
i += 1;
}
j += 1;
}
out
}
pub fn derive_units_lookup<const PHI: usize>(n: usize, reduction: &[i64; PHI]) -> Vec<[i64; PHI]> {
assert!(PHI >= 1, "PHI must be >= 1");
let mut out: Vec<[i64; PHI]> = Vec::with_capacity(n + 1);
let mut one = [0i64; PHI];
one[0] = 1;
out.push(one);
if PHI == 1 {
let mut k = 1;
while k <= n {
let prev = out[k - 1];
let mut next = [0i64; PHI];
next[0] = prev[0] * reduction[0];
out.push(next);
k += 1;
}
return out;
}
let mut zeta = [0i64; PHI];
zeta[1] = 1;
let mut k = 1;
while k <= n {
let prev = out[k - 1];
let next = mul_basis::<PHI>(&prev, &zeta, reduction);
out.push(next);
k += 1;
}
out
}
#[macro_export]
macro_rules! define_integral_zz {
(
name: $name:ident,
n: $n:expr,
phi: $phi:expr,
real_dim: $k:expr,
reduction: $reduction:expr,
re_decomp: $re_decomp:expr,
im_decomp: $im_decomp:expr,
cartesian: $cartesian:expr,
one_in_real_basis: $one_real:expr,
display_fn: $display_fn:path,
complex64_fn: $complex64_fn:path,
has: [$($has:ident),* $(,)?] $(,)?
) => {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct $name {
coeffs: [i64; $phi],
}
impl $name {
pub const N: usize = $n;
pub const PHI: usize = $phi;
pub const REAL_DIM: usize = $k;
pub const REDUCTION: [i64; $phi] = $reduction;
#[doc(hidden)]
pub const RE_DECOMP: [[i64; $k]; $phi] = $re_decomp;
#[doc(hidden)]
pub const IM_DECOMP: [[i64; $k]; $phi] = $im_decomp;
#[doc(hidden)]
pub const CARTESIAN: [num_complex::Complex64; $phi] = $cartesian;
#[doc(hidden)]
pub const ONE_IN_REAL_BASIS: [i64; $k] = $one_real;
#[inline]
pub const fn from_int_coeffs(coeffs: [i64; $phi]) -> Self {
Self { coeffs }
}
#[inline]
pub const fn int_coeffs(&self) -> [i64; $phi] {
self.coeffs
}
}
impl $name {
#[doc(hidden)]
#[inline]
pub fn __conj_matrix() -> &'static [[i64; $phi]; $phi] {
static M: std::sync::OnceLock<[[i64; $phi]; $phi]> =
std::sync::OnceLock::new();
M.get_or_init(|| {
$crate::cyclotomic::integral_basis::derive_conj_matrix::<$phi>(
$n, &Self::REDUCTION,
)
})
}
#[doc(hidden)]
#[inline]
pub fn __units_table() -> &'static Vec<[i64; $phi]> {
static U: std::sync::OnceLock<Vec<[i64; $phi]>> =
std::sync::OnceLock::new();
U.get_or_init(|| {
$crate::cyclotomic::integral_basis::derive_units_lookup::<$phi>(
$n, &Self::REDUCTION,
)
})
}
}
impl std::fmt::Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
$display_fn(&self.coeffs, f)
}
}
impl std::ops::Neg for $name {
type Output = Self;
#[inline]
fn neg(self) -> Self {
Self {
coeffs: $crate::cyclotomic::integral_basis::neg_basis::<$phi>(&self.coeffs),
}
}
}
impl std::ops::Add<$name> for $name {
type Output = Self;
#[inline]
fn add(self, other: Self) -> Self {
Self {
coeffs: $crate::cyclotomic::integral_basis::add_basis::<$phi>(
&self.coeffs,
&other.coeffs,
),
}
}
}
impl std::ops::Sub<$name> for $name {
type Output = Self;
#[inline]
fn sub(self, other: Self) -> Self {
Self {
coeffs: $crate::cyclotomic::integral_basis::sub_basis::<$phi>(
&self.coeffs,
&other.coeffs,
),
}
}
}
impl num_traits::Zero for $name {
#[inline]
fn zero() -> Self {
Self { coeffs: [0i64; $phi] }
}
#[inline]
fn is_zero(&self) -> bool {
self.coeffs == [0i64; $phi]
}
}
impl num_traits::One for $name {
#[inline]
fn one() -> Self {
let mut c = [0i64; $phi];
c[0] = 1;
Self { coeffs: c }
}
#[inline]
fn is_one(&self) -> bool {
if self.coeffs[0] != 1 {
return false;
}
let mut i = 1;
while i < $phi {
if self.coeffs[i] != 0 {
return false;
}
i += 1;
}
true
}
}
impl $crate::cyclotomic::IntRing for $name {}
impl num_traits::Pow<u8> for $name {
type Output = Self;
fn pow(self, other: u8) -> Self {
use $crate::cyclotomic::SymNum;
self.zz_pow(other)
}
}
impl num_traits::Pow<i8> for $name {
type Output = Self;
fn pow(self, other: i8) -> Self {
use $crate::cyclotomic::SymNum;
assert!(other >= 0, "Negative powers are not supported!");
self.zz_pow(other as u8)
}
}
impl $crate::cyclotomic::numtraits::InnerIntType for $name {
type IntType = i64;
}
impl From<i64> for $name {
#[inline]
fn from(value: i64) -> Self {
let mut c = [0i64; $phi];
c[0] = value;
Self { coeffs: c }
}
}
impl $crate::cyclotomic::SymNum for $name {
type Scalar = i64;
#[inline]
fn zz_coeffs(&self) -> &[Self::Scalar] {
&self.coeffs
}
#[inline]
fn zz_coeffs_mut(&mut self) -> &mut [Self::Scalar] {
&mut self.coeffs
}
#[inline]
fn turn() -> i8 {
$n as i8
}
#[inline]
fn complex64(&self) -> num_complex::Complex64 {
$complex64_fn(&self.coeffs)
}
}
impl $crate::cyclotomic::Ccw for $name {
#[inline]
fn ccw() -> Self {
<Self as $crate::cyclotomic::Units>::unit(1)
}
#[inline]
fn is_ccw(&self) -> bool {
*self == <Self as $crate::cyclotomic::Ccw>::ccw()
}
}
impl $crate::cyclotomic::ZZComplex for $name {
fn is_real(&self) -> bool {
<Self as $crate::cyclotomic::ReImSign>::im_sign(self) == 0
}
fn is_imag(&self) -> bool {
<Self as $crate::cyclotomic::ReImSign>::re_sign(self) == 0
}
}
impl $crate::cyclotomic::integral_basis::IntCoeffsSlice for $name {
#[inline]
fn int_coeffs_slice(&self) -> &[i64] {
&self.coeffs
}
}
impl $crate::cyclotomic::IsRing for $name {}
$(
impl $crate::cyclotomic::traits::$has for $name {}
)*
};
}
#[macro_export]
macro_rules! impl_integral_units_via_basis {
($name:ident, $n:expr) => {
impl $crate::cyclotomic::Units for $name {
#[inline]
fn unit(angle: i8) -> Self {
let tab = <$name>::__units_table();
let idx = angle.rem_euclid($n as i8) as usize;
Self::from_int_coeffs(tab[idx])
}
}
};
}
#[macro_export]
macro_rules! impl_integral_mul_via_basis {
($name:ident, $phi:expr) => {
impl std::ops::Mul<$name> for $name {
type Output = Self;
#[inline]
fn mul(self, other: Self) -> Self {
Self::from_int_coeffs($crate::cyclotomic::integral_basis::mul_basis::<$phi>(
&self.int_coeffs(),
&other.int_coeffs(),
&<$name>::REDUCTION,
))
}
}
};
}
#[macro_export]
macro_rules! impl_integral_conj_via_basis {
($name:ident, $phi:expr) => {
impl $crate::cyclotomic::Conj for $name {
#[inline]
fn conj(&self) -> Self {
Self::from_int_coeffs($crate::cyclotomic::integral_basis::conj_basis::<$phi>(
&self.int_coeffs(),
<$name>::__conj_matrix(),
))
}
}
};
}
#[macro_export]
macro_rules! impl_integral_re_im_sign_via_basis {
($name:ident, $phi:expr, $k:expr, $real_sign:path) => {
impl $crate::cyclotomic::ReImSign for $name {
#[inline]
fn re_sign(&self) -> i8 {
$crate::cyclotomic::integral_basis::re_sign_basis::<$phi, $k>(
&<$name>::int_coeffs(self),
&<$name>::RE_DECOMP,
$real_sign,
)
}
#[inline]
fn im_sign(&self) -> i8 {
$crate::cyclotomic::integral_basis::im_sign_basis::<$phi, $k>(
&<$name>::int_coeffs(self),
&<$name>::IM_DECOMP,
$real_sign,
)
}
}
};
}
#[macro_export]
macro_rules! impl_integral_intersect_unit_segments_via_basis {
($name:ident, $phi:expr, $k:expr, $real_sign:path) => {
impl $crate::cyclotomic::IntersectUnitSegments for $name {
#[inline]
fn intersect_unit_segments(s1: &($name, $name), s2: &($name, $name)) -> bool {
$crate::cyclotomic::integral_basis::intersect_unit_segments_basis::<$phi, $k>(
&(s1.0.int_coeffs(), s1.1.int_coeffs()),
&(s2.0.int_coeffs(), s2.1.int_coeffs()),
&<$name>::REDUCTION,
<$name>::__conj_matrix(),
&<$name>::RE_DECOMP,
&<$name>::IM_DECOMP,
&<$name>::ONE_IN_REAL_BASIS,
$real_sign,
)
}
}
};
}
#[macro_export]
macro_rules! impl_integral_within_radius_via_norm_sq {
($name:ident) => {
impl $crate::cyclotomic::WithinRadius for $name {
#[inline]
fn within_radius(&self, radius: i64) -> bool {
use $crate::cyclotomic::{Conj, ReImSign};
let norm_sq = *self * self.conj();
let r_sq = <$name as From<i64>>::from(radius * radius);
(r_sq - norm_sq).re_sign() >= 0
}
}
};
}
#[macro_export]
macro_rules! zz_integral_ring_tests {
(name: $ring:ident) => {
::paste::paste! {
#[cfg(test)]
mod [<$ring:lower _generic_ring_tests>] {
#[allow(unused_imports)]
use super::*;
use $crate::cyclotomic::{
Ccw, Conj, IntersectUnitSegments, ReImSign, SymNum, Units, WithinRadius,
};
use $crate::cyclotomic::geometry::intersect;
use num_traits::{One, Zero};
type R = $ring;
fn sample_elems() -> Vec<R> {
let n = R::turn();
let mut out: Vec<R> = vec![
R::zero(),
R::one(),
-R::one(),
R::from(2i64),
R::from(-2i64),
];
let mut k = 0i8;
while k < n {
out.push(<R as Units>::unit(k));
k += 1;
}
out.push(<R as Units>::unit(1) + <R as Units>::unit(2));
out.push(<R as Units>::unit(0) - <R as Units>::unit(3));
out.push(<R as Units>::unit(1) + R::from(3i64));
out.push(R::from(2i64) * <R as Units>::unit(1) - <R as Units>::unit(2));
out
}
fn small_sample_elems() -> Vec<R> {
vec![
R::zero(),
R::one(),
-R::one(),
<R as Units>::unit(1),
<R as Units>::unit(2),
<R as Units>::unit(1) + <R as Units>::unit(2),
R::from(2i64) - <R as Units>::unit(1),
]
}
#[test]
fn add_identity() {
let zero = R::zero();
for x in sample_elems() {
assert_eq!(x + zero, x, "x + 0 != x for x = {x}");
assert_eq!(zero + x, x, "0 + x != x for x = {x}");
}
}
#[test]
fn add_inverse() {
let zero = R::zero();
for x in sample_elems() {
assert_eq!(x + (-x), zero, "x + (-x) != 0 for x = {x}");
}
}
#[test]
fn add_commutativity() {
let xs = sample_elems();
for x in &xs {
for y in &xs {
assert_eq!(*x + *y, *y + *x, "x + y != y + x for x={x}, y={y}");
}
}
}
#[test]
fn add_associativity() {
let xs = small_sample_elems();
for x in &xs {
for y in &xs {
for z in &xs {
assert_eq!(
(*x + *y) + *z,
*x + (*y + *z),
"associativity of + failed at x={x}, y={y}, z={z}",
);
}
}
}
}
#[test]
fn mul_identity() {
let one = R::one();
for x in sample_elems() {
assert_eq!(x * one, x, "x * 1 != x for x = {x}");
assert_eq!(one * x, x, "1 * x != x for x = {x}");
}
}
#[test]
fn mul_commutativity() {
let xs = sample_elems();
for x in &xs {
for y in &xs {
assert_eq!(*x * *y, *y * *x, "x * y != y * x for x={x}, y={y}");
}
}
}
#[test]
fn mul_associativity() {
let xs = small_sample_elems();
for x in &xs {
for y in &xs {
for z in &xs {
assert_eq!(
(*x * *y) * *z,
*x * (*y * *z),
"associativity of * failed at x={x}, y={y}, z={z}",
);
}
}
}
}
#[test]
fn distributivity() {
let xs = small_sample_elems();
for x in &xs {
for y in &xs {
for z in &xs {
assert_eq!(
*x * (*y + *z),
*x * *y + *x * *z,
"x*(y+z) != x*y + x*z for x={x}, y={y}, z={z}",
);
}
}
}
}
#[test]
fn zero_absorbing() {
let zero = R::zero();
for x in sample_elems() {
assert_eq!(x * zero, zero, "x * 0 != 0 for x = {x}");
assert_eq!(zero * x, zero, "0 * x != 0 for x = {x}");
}
}
#[test]
fn sub_eq_add_neg() {
let xs = sample_elems();
for x in &xs {
for y in &xs {
assert_eq!(
*x - *y,
*x + (-(*y)),
"x - y != x + (-y) for x={x}, y={y}",
);
}
}
}
#[test]
fn unit_zero_is_one() {
assert_eq!(<R as Units>::unit(0), R::one());
let n = R::turn();
assert_eq!(<R as Units>::unit(n), R::one());
if n % 2 == 0 {
assert_eq!(<R as Units>::unit(n / 2), -R::one());
}
}
#[test]
fn unit_cycle_group() {
let n = R::turn();
for i in 0..n {
for j in 0..n {
let lhs = <R as Units>::unit(i) * <R as Units>::unit(j);
let rhs = <R as Units>::unit(
((i as i16 + j as i16).rem_euclid(n as i16)) as i8,
);
assert_eq!(lhs, rhs, "unit({i})*unit({j}) != unit((i+j) mod N)");
}
}
}
#[test]
fn powers_of_zeta() {
let n = R::turn();
let zeta = R::ccw();
let mut acc = R::one();
let mut k = 0i8;
while k < n {
assert_eq!(
acc,
<R as Units>::unit(k),
"unit(1)^{k} != unit({k})",
);
acc = acc * zeta;
k += 1;
}
assert_eq!(acc, R::one(), "unit(1)^N != 1");
}
#[test]
fn conj_involution() {
for x in sample_elems() {
assert_eq!(x.conj().conj(), x, "conj(conj(x)) != x for x = {x}");
}
}
#[test]
fn conj_distributive_over_add() {
let xs = sample_elems();
for x in &xs {
for y in &xs {
assert_eq!(
(*x + *y).conj(),
x.conj() + y.conj(),
"conj(x+y) != conj(x)+conj(y) for x={x}, y={y}",
);
}
}
}
#[test]
fn conj_distributive_over_mul() {
let xs = small_sample_elems();
for x in &xs {
for y in &xs {
assert_eq!(
(*x * *y).conj(),
x.conj() * y.conj(),
"conj(x*y) != conj(x)*conj(y) for x={x}, y={y}",
);
}
}
}
#[test]
fn conj_fixes_one() {
let one = <R as Units>::unit(0);
assert_eq!(one.conj(), one);
}
#[test]
fn x_times_conj_x_is_real() {
for x in sample_elems() {
let prod = x * x.conj();
assert_eq!(
prod.im_sign(),
0,
"x*conj(x) is not real for x = {x}: prod = {prod}",
);
}
}
const COMPLEX_EPS: f64 = 1e-9;
fn approx_eq(a: num_complex::Complex64, b: num_complex::Complex64, eps: f64) -> bool {
(a.re - b.re).abs() < eps && (a.im - b.im).abs() < eps
}
#[test]
fn complex64_linear() {
let xs = sample_elems();
for x in &xs {
for y in &xs {
let lhs = (*x + *y).complex64();
let rhs = x.complex64() + y.complex64();
assert!(
approx_eq(lhs, rhs, COMPLEX_EPS),
"(x+y).complex64() != x.complex64() + y.complex64() for x={x}, y={y}: {lhs} vs {rhs}",
);
}
}
}
#[test]
fn complex64_mul_matches() {
let xs = small_sample_elems();
for x in &xs {
for y in &xs {
let lhs = (*x * *y).complex64();
let rhs = x.complex64() * y.complex64();
assert!(
approx_eq(lhs, rhs, 1e-8),
"(x*y).complex64() != x.complex64() * y.complex64() for x={x}, y={y}: {lhs} vs {rhs}",
);
}
}
}
#[test]
fn complex64_unit_matches_exp() {
let n = R::turn();
for k in 0..n {
let u = <R as Units>::unit(k);
let c = u.complex64();
let angle = 2.0 * std::f64::consts::PI * (k as f64) / (n as f64);
let expected = num_complex::Complex64::new(angle.cos(), angle.sin());
assert!(
approx_eq(c, expected, COMPLEX_EPS),
"unit({k}).complex64() != exp(2*pi*i*{k}/{n}): {c} vs {expected}",
);
}
}
#[test]
fn re_im_sign_agrees_with_complex64() {
for x in sample_elems() {
let c = x.complex64();
let re_sign = x.re_sign();
let im_sign = x.im_sign();
if c.re.abs() > 1e-9 {
let expected = if c.re > 0.0 { 1 } else { -1 };
assert_eq!(
re_sign, expected,
"re_sign disagrees with complex64 for x = {x}: re={}, sign={re_sign}",
c.re,
);
}
if c.im.abs() > 1e-9 {
let expected = if c.im > 0.0 { 1 } else { -1 };
assert_eq!(
im_sign, expected,
"im_sign disagrees with complex64 for x = {x}: im={}, sign={im_sign}",
c.im,
);
}
}
}
#[test]
fn within_radius_agrees_with_norm() {
let xs = sample_elems();
let radii: [i64; 5] = [0, 1, 2, 3, 5];
for x in &xs {
let n = x.complex64().norm();
for &r in &radii {
let expected_inside = n <= (r as f64) + 1e-9;
let expected_outside = n >= (r as f64) + 1e-9;
let actual = x.within_radius(r);
if (n - r as f64).abs() < 1e-6 {
continue;
}
if expected_inside {
assert!(
actual,
"within_radius({r}) should be true for x = {x} (|x| = {n})",
);
}
if expected_outside && n > (r as f64) + 1e-6 {
assert!(
!actual,
"within_radius({r}) should be false for x = {x} (|x| = {n})",
);
}
}
}
}
#[test]
fn intersect_unit_segments_matches_generic() {
let n = R::turn();
let zero = R::zero();
let anchors: Vec<R> = vec![
zero,
<R as Units>::unit(0),
<R as Units>::unit(1),
<R as Units>::unit(2),
-<R as Units>::unit(0),
];
let step = (n / 6).max(1);
for ai in (0..n).step_by(step as usize) {
for bi in (0..n).step_by(step as usize) {
for p in &anchors {
let s1 = (zero, <R as Units>::unit(ai));
let s2 = (
*p,
*p + <R as Units>::unit(bi),
);
let specialized = <R as IntersectUnitSegments>::intersect_unit_segments(&s1, &s2);
let generic = intersect::<R>(&s1, &s2);
assert_eq!(
specialized, generic,
"intersect_unit_segments disagrees with generic intersect: s1=({}, {}), s2=({}, {}); specialized={specialized}, generic={generic}",
s1.0, s1.1, s2.0, s2.1,
);
}
}
}
}
#[test]
fn intersect_unit_segments_symmetric() {
let n = R::turn();
let zero = R::zero();
let anchors: Vec<R> = vec![
zero,
<R as Units>::unit(0),
<R as Units>::unit(1),
<R as Units>::unit(2),
];
let step = (n / 6).max(1);
for ai in (0..n).step_by(step as usize) {
for bi in (0..n).step_by(step as usize) {
for p in &anchors {
let s1 = (zero, <R as Units>::unit(ai));
let s2 = (
*p,
*p + <R as Units>::unit(bi),
);
let ab = <R as IntersectUnitSegments>::intersect_unit_segments(&s1, &s2);
let ba = <R as IntersectUnitSegments>::intersect_unit_segments(&s2, &s1);
assert_eq!(ab, ba, "intersect_unit_segments not symmetric: s1=({}, {}), s2=({}, {})", s1.0, s1.1, s2.0, s2.1);
}
}
}
}
#[test]
fn is_real_imag_complex_predicates() {
use $crate::cyclotomic::ZZComplex;
assert!(R::zero().is_real(), "0 should be real");
assert!(R::zero().is_imag(), "0 should be imag");
assert!(!R::zero().is_complex(), "0 should not be (strictly) complex");
assert!(R::one().is_real(), "1 should be real");
assert!((-R::one()).is_real(), "-1 should be real");
assert!(!R::one().is_imag(), "1 should not be imag");
assert!(!R::one().is_complex(), "1 should not be (strictly) complex");
let n = R::turn();
let qturn = n / 4;
if n % 4 == 0 {
let i = <R as Units>::unit(qturn);
assert!(!i.is_real(), "i should not be real");
assert!(i.is_imag(), "i should be imag");
assert!(!i.is_complex(), "i should not be (strictly) complex");
assert!((-i).is_imag(), "-i should be imag");
}
if n >= 8 && n % 4 == 0 {
let p = <R as Units>::unit(1);
assert!(!p.is_real(), "unit(1) should not be real for N={n}");
assert!(!p.is_imag(), "unit(1) should not be imag for N={n}");
assert!(p.is_complex(), "unit(1) should be strictly complex for N={n}");
}
}
#[test]
fn pow_at_hturn_is_neg_one() {
use num_traits::Pow;
let n = R::turn();
let hturn = (n / 2) as u8;
let zeta = R::ccw();
assert_eq!(zeta.pow(hturn), -R::one(), "zeta^(N/2) should equal -1");
let turn = n as u8;
assert_eq!(zeta.pow(turn), R::one(), "zeta^N should equal 1");
}
#[test]
#[should_panic]
fn pow_negative_panics() {
use num_traits::Pow;
let _ = R::one().pow(-1i8);
}
#[test]
fn complex64_sample_values() {
use num_complex::Complex64;
use num_traits::{One, Zero};
assert_eq!(R::zero().complex64(), Complex64::zero());
assert_eq!(R::one().complex64(), Complex64::one());
assert_eq!((-R::one()).complex64(), -Complex64::one());
let two = R::one() + R::one();
assert!(
approx_eq(two.complex64(), Complex64::new(2.0, 0.0), COMPLEX_EPS),
"(1+1).complex64() != 2",
);
}
#[test]
fn hashable_round_trip() {
use std::collections::HashSet;
let mut s: HashSet<R> = HashSet::new();
s.insert(R::zero());
s.insert(R::one());
s.insert(R::ccw());
assert!(s.contains(&R::ccw()));
assert!(s.contains(&(R::ccw() + R::zero())));
assert!(!s.contains(&(R::ccw() + R::one())));
}
}
}
};
}
#[cfg(test)]
mod tests {
use super::*;
const RED_I: [i64; 2] = [-1, 0];
#[test]
fn add_sub_neg() {
let x = [3i64, 4];
let y = [1i64, 2];
assert_eq!(add_basis::<2>(&x, &y), [4, 6]);
assert_eq!(sub_basis::<2>(&x, &y), [2, 2]);
assert_eq!(neg_basis::<2>(&x), [-3, -4]);
}
#[test]
fn mul_smoke_z_i() {
let x = [3i64, 4]; let y = [1i64, 2]; assert_eq!(mul_basis::<2>(&x, &y, &RED_I), [-5, 10]);
let i = [0i64, 1];
assert_eq!(mul_basis::<2>(&i, &i, &RED_I), [-1, 0]);
}
#[test]
fn derive_conj_matrix_z_i() {
let m = derive_conj_matrix::<2>(4, &RED_I);
assert_eq!(m, [[1, 0], [0, -1]]);
let z = [3i64, 4];
assert_eq!(conj_basis::<2>(&z, &m), [3, -4]);
}
#[test]
fn derive_units_lookup_z_i() {
let u = derive_units_lookup::<2>(4, &RED_I);
assert_eq!(u.len(), 5);
assert_eq!(u[0], [1, 0]);
assert_eq!(u[1], [0, 1]);
assert_eq!(u[2], [-1, 0]);
assert_eq!(u[3], [0, -1]);
assert_eq!(u[4], [1, 0]);
}
#[test]
fn mul_smoke_zz12_pattern() {
const RED_ZZ12: [i64; 4] = [-1, 0, 1, 0];
let zeta = [0i64, 1, 0, 0];
assert_eq!(mul_basis::<4>(&zeta, &zeta, &RED_ZZ12), [0, 0, 1, 0]);
let zeta_sq = [0i64, 0, 1, 0];
assert_eq!(mul_basis::<4>(&zeta_sq, &zeta_sq, &RED_ZZ12), [-1, 0, 1, 0],);
let zeta_cu = [0i64, 0, 0, 1];
assert_eq!(mul_basis::<4>(&zeta_cu, &zeta_cu, &RED_ZZ12), [-1, 0, 0, 0]);
}
#[test]
fn re_im_sign_z_i() {
const RE: [[i64; 1]; 2] = [[1], [0]];
const IM: [[i64; 1]; 2] = [[0], [1]];
fn sgn(x: &[i64; 1]) -> i8 {
x[0].signum() as i8
}
let z = [3i64, -4];
assert_eq!(re_sign_basis::<2, 1>(&z, &RE, sgn), 1);
assert_eq!(im_sign_basis::<2, 1>(&z, &IM, sgn), -1);
}
#[test]
fn complex64_z_i() {
use num_complex::Complex64;
let cart = [Complex64::new(1.0, 0.0), Complex64::new(0.0, 1.0)];
let z = [3i64, -4];
let c = complex64_basis::<2>(&z, &cart);
assert!((c.re - 3.0).abs() < 1e-12);
assert!((c.im + 4.0).abs() < 1e-12);
}
}