Skip to content

Commit 9728c8a

Browse files
committed
add Rq.conjugate
1 parent 39fd58f commit 9728c8a

1 file changed

Lines changed: 118 additions & 0 deletions

File tree

src/ring.rs

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,23 @@ impl<const Q: u64, const D: usize> Rq<Q, D> {
116116
}
117117
coeffs
118118
}
119+
120+
/// Negate and rotate all coefficients but the constant term.
121+
///
122+
/// E.g. a(x) = a_0 + a_1 x^1 + a_2 x^2 + a_3 x^3
123+
/// \bar a(x) = a(x^{-1})
124+
/// = a_0 - a_3 x^1 - a_2 x^2 - a_1 x^3
125+
fn conjugate(&self) -> Self {
126+
Self {
127+
coeffs: std::array::from_fn(|i| {
128+
if i == 0 {
129+
self.coeffs[i].clone()
130+
} else {
131+
Zq::<Q>::new(Q - self.coeffs[D-i].value())
132+
}
133+
})
134+
}
135+
}
119136
}
120137

121138
impl<const Q: u64, const D: usize> Add for Rq<Q, D> {
@@ -570,4 +587,105 @@ mod tests {
570587
let one = Ring4::one();
571588
assert_eq!((a.clone().ntt() * one.ntt()).intt(), a);
572589
}
590+
591+
// ─── conjugate tests ───
592+
//
593+
// Conjugation in R_q = Z_q[X]/(X^d+1) is the Galois automorphism σ_{-1}: X ↦ X^{-1}.
594+
// Since X^d = -1, X^{-1} = -X^{d-1}. So a(X) = Σ a_i X^i maps to
595+
// ā(X) = a(X^{-1}) = a_0 - a_{d-1} X - a_{d-2} X^2 - ... - a_1 X^{d-1}
596+
// i.e. reverse the non-constant coefficients AND negate them (constant a_0 stays).
597+
//
598+
// Reference: 06_salsaa/ring.py — `conjugate(r) = Rq(r.lift()(-x**(d-1)))`.
599+
600+
#[test]
601+
fn test_conjugate_constant_is_identity() {
602+
// Constant polynomial: ā = a. No non-constant terms to flip.
603+
assert_eq!(rp([7, 0, 0, 0]).conjugate(), rp([7, 0, 0, 0]));
604+
assert_eq!(Ring4::one().conjugate(), Ring4::one());
605+
assert_eq!(Ring4::zero().conjugate(), Ring4::zero());
606+
}
607+
608+
#[test]
609+
fn test_conjugate_reverses_and_negates_non_constant() {
610+
// a = 1 + 2x + 3x^2 + 4x^3
611+
// ā = 1 - 4x - 3x^2 - 2x^3 = 1 + 13x + 14x^2 + 15x^3 (mod 17)
612+
assert_eq!(rp([1, 2, 3, 4]).conjugate(), rp([1, 13, 14, 15]));
613+
}
614+
615+
#[test]
616+
fn test_conjugate_of_x_is_minus_x_cubed() {
617+
// a = X. ā = -X^{d-1} = -X^3 = 16 X^3 (mod 17).
618+
assert_eq!(rp([0, 1, 0, 0]).conjugate(), rp([0, 0, 0, 16]));
619+
}
620+
621+
#[test]
622+
fn test_conjugate_involution() {
623+
// σ_{-1} ∘ σ_{-1} = id (it's an order-2 automorphism: X ↦ X^{-1} ↦ X).
624+
let a = rp([3, 5, 7, 11]);
625+
assert_eq!(a.conjugate().conjugate(), a);
626+
}
627+
628+
#[test]
629+
fn test_conjugate_involution_random() {
630+
let mut rng = rand::rng();
631+
for _ in 0..20 {
632+
let a = Ring4::random(&mut rng);
633+
assert_eq!(a.conjugate().conjugate(), a);
634+
}
635+
}
636+
637+
#[test]
638+
fn test_conjugate_is_ring_homomorphism_additive() {
639+
// conjugate(a + b) == conjugate(a) + conjugate(b)
640+
let a = rp([1, 2, 3, 4]);
641+
let b = rp([5, 6, 7, 8]);
642+
assert_eq!((a + b).conjugate(), a.conjugate() + b.conjugate());
643+
}
644+
645+
#[test]
646+
fn test_conjugate_is_ring_homomorphism_multiplicative() {
647+
// conjugate(a * b) == conjugate(a) * conjugate(b)
648+
// This is THE property that makes norm-check work — if this fails,
649+
// LDE[a · ā] = LDE[a] · LDE[ā] in canonical embedding breaks.
650+
let mut rng = rand::rng();
651+
for _ in 0..20 {
652+
let a = Ring4::random(&mut rng);
653+
let b = Ring4::random(&mut rng);
654+
assert_eq!((a * b).conjugate(), a.conjugate() * b.conjugate());
655+
}
656+
}
657+
658+
#[test]
659+
fn test_conjugate_a_times_a_bar_has_norm_in_constant() {
660+
// The defining identity for norm check:
661+
// constant_term(a · ā) = Σ a_i^2 (the squared ℓ2 norm of a, BEFORE centered lift)
662+
//
663+
// For a = 1 + X: ā = 1 - X^3.
664+
// a · ā = (1+X)(1-X^3) = 1 + X - X^3 - X^4 = 1 + X - X^3 - (-1) = 2 + X - X^3.
665+
// constant term = 2 = 1^2 + 1^2 ✓
666+
let a = rp([1, 1, 0, 0]);
667+
let product = a * a.conjugate();
668+
assert_eq!(product.coeffs()[0], F::new(2));
669+
}
670+
671+
#[test]
672+
fn test_conjugate_constant_term_equals_norm_squared_random() {
673+
// Generalize the previous test: for ANY a with small coefficients (so the
674+
// sum-of-squares doesn't overflow mod q), constant_term(a · ā) should equal
675+
// Σ a_i^2 (computed as plain integers then reduced mod q).
676+
//
677+
// We use small coeffs (0..=2) to keep Σ a_i^2 < q = 17.
678+
let mut rng = rand::rng();
679+
for _ in 0..20 {
680+
let raw: [u64; D] = std::array::from_fn(|_| rng.random_range(0..=2));
681+
let a = rp(raw);
682+
let sum_sq: u64 = raw.iter().map(|&v| v * v).sum();
683+
let product = a * a.conjugate();
684+
assert_eq!(
685+
product.coeffs()[0],
686+
F::new(sum_sq),
687+
"a = {raw:?}, expected constant = {sum_sq}",
688+
);
689+
}
690+
}
573691
}

0 commit comments

Comments
 (0)