@@ -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
121138impl < 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