11//! Pass to convert a Pauli graph into canonical form.
22use tk_pg_core:: {
3- BlackBoxData , ConditionalBoxData , GateData , GateType , Op , PGPass , PauliGraph , TableauData ,
3+ BlackBoxData , ConditionalBoxData , GateData , GateType , Op , PGPass , Pauli , PauliGraph ,
4+ RotationData , TableauData ,
45} ;
56use tk_pg_ir_kernels:: { PGTableau , get_dagger, is_clifford, is_clifford_gate_type} ;
67use tk_pg_qm_tableau:: Tableau as QubitMajorTableau ;
@@ -260,9 +261,7 @@ fn process_op(
260261 if compose {
261262 tab. precompose_op ( & op) ;
262263 } else {
263- for conjugated_op in tab. conjugate ( & op) {
264- pg. add_op ( conjugated_op) ;
265- }
264+ process_op ( pg, & op, tab, forward, cliff_eval) ;
266265 }
267266 }
268267 return ;
@@ -283,7 +282,25 @@ fn process_op(
283282 if !forward {
284283 conjugated_ops. reverse ( ) ;
285284 }
286- for conjugated_op in conjugated_ops {
285+ for mut conjugated_op in conjugated_ops {
286+ if cliff_eval && let Op :: Rotation { data } = & mut conjugated_op {
287+ let angle = data. get_angle ( ) ;
288+ if angle. is_finite ( ) && angle. abs ( ) >= 0.5 {
289+ let remainder = angle % 0.5 ;
290+ let correction = angle - remainder;
291+ // The string is already conjugated, so append the correction to the tableau.
292+ // Forward traversal maintains the inverse tableau.
293+ if data. get_string ( ) . iter ( ) . any ( |p| * p != Pauli :: I ) {
294+ tab. postcompose_op ( & Op :: Rotation {
295+ data : RotationData :: new (
296+ data. get_string ( ) . clone ( ) ,
297+ if forward { -correction } else { correction } ,
298+ ) ,
299+ } ) ;
300+ }
301+ * data = RotationData :: new ( data. get_string ( ) . clone ( ) , remainder) ;
302+ }
303+ }
287304 pg. add_op ( conjugated_op) ;
288305 }
289306}
@@ -339,7 +356,9 @@ fn to_canonical_form(pg: &PauliGraph, forward: bool, cliff_eval: bool) -> PauliG
339356/// tableaux, interleaved with any obstructions and the non-Clifford ops (rotations, measures,
340357/// resets, conditional operations) that are conjugated through and left in place.
341358/// `cliff_eval` controls whether rotations with a Clifford angle are folded into the tableau
342- /// (`true`) or kept as explicit rotations (`false`).
359+ /// (`true`) or kept as explicit rotations (`false`). When enabled, it also extracts
360+ /// Clifford corrections from unconditional rotations, leaving their angles strictly
361+ /// between -0.5 and 0.5 half turns.
343362///
344363/// # Panics
345364///
@@ -369,7 +388,9 @@ impl CanonicalFormPass {
369388 self . forward = forward;
370389 self
371390 }
372- /// Set whether to evaluate Clifford angle rotations as Clifford gates or keep them as rotations.
391+ /// Set whether to absorb Clifford rotations and the Clifford part of larger angles
392+ /// into the tableau. Enabled by default; remaining unconditional rotation angles
393+ /// lie strictly between -0.5 and 0.5 half turns.
373394 pub fn with_cliff_eval ( mut self , cliff_eval : bool ) -> Self {
374395 self . cliff_eval = cliff_eval;
375396 self
@@ -455,6 +476,87 @@ mod tests {
455476 assert ! ( compare_unitaries_via_tk( & pg, & transformed) ) ;
456477 }
457478
479+ #[ rstest]
480+ #[ case( true , true , 0.75 , 0.25 ) ]
481+ #[ case( false , true , 0.75 , 0.25 ) ]
482+ #[ case( true , true , -0.75 , -0.25 ) ]
483+ #[ case( false , true , -0.75 , -0.25 ) ]
484+ #[ case( true , true , 2.1 , 0.1 ) ]
485+ #[ case( false , true , 2.1 , 0.1 ) ]
486+ #[ case( true , true , -2.1 , -0.1 ) ]
487+ #[ case( false , true , -2.1 , -0.1 ) ]
488+ #[ case( true , false , 0.75 , 0.75 ) ]
489+ #[ case( false , false , 0.75 , 0.75 ) ]
490+ #[ case( true , false , -0.75 , -0.75 ) ]
491+ #[ case( false , false , -0.75 , -0.75 ) ]
492+ #[ case( true , false , 2.1 , 2.1 ) ]
493+ #[ case( false , false , 2.1 , 2.1 ) ]
494+ #[ case( true , false , -2.1 , -2.1 ) ]
495+ #[ case( false , false , -2.1 , -2.1 ) ]
496+ fn test_extract_clifford_corrections (
497+ #[ case] forward : bool ,
498+ #[ case] cliff_eval : bool ,
499+ #[ case] angle : f64 ,
500+ #[ case] expected_angle : f64 ,
501+ ) {
502+ let mut pg = PauliGraph :: new ( 2 ) ;
503+ pg. add_op ( Op :: Gate {
504+ data : GateData :: new ( GateType :: RZ , vec ! [ 0 ] ) . with_params ( vec ! [ angle] ) ,
505+ } ) ;
506+ pg. add_op ( Op :: Rotation {
507+ data : RotationData :: new ( vec ! [ Pauli :: I , Pauli :: Z ] , angle) ,
508+ } ) ;
509+ let pass = CanonicalFormPass :: new ( )
510+ . with_forward ( forward)
511+ . with_cliff_eval ( cliff_eval) ;
512+ let transformed = pass. transform ( & pg) ;
513+
514+ assert_eq ! ( transformed. get_ops( ) . len( ) , 3 ) ;
515+ for op in transformed. get_ops ( ) {
516+ if let Op :: Rotation { data } = op {
517+ assert ! ( ( data. get_angle( ) - expected_angle) . abs( ) < 1e-10 ) ;
518+ }
519+ }
520+ assert ! ( compare_unitaries_via_tk( & pg, & transformed) ) ;
521+ }
522+
523+ #[ rstest]
524+ #[ case( true ) ]
525+ #[ case( false ) ]
526+ fn test_clifford_corrections_propagate ( #[ case] forward : bool ) {
527+ let mut pg = PauliGraph :: new ( 2 ) ;
528+ pg. add_op ( Op :: Gate {
529+ data : GateData :: new ( GateType :: H , vec ! [ 0 ] ) ,
530+ } ) ;
531+ pg. add_op ( Op :: Gate {
532+ data : GateData :: new ( GateType :: RX , vec ! [ 0 ] ) . with_params ( vec ! [ 0.75 ] ) ,
533+ } ) ;
534+ pg. add_op ( Op :: Gate {
535+ data : GateData :: new ( GateType :: RY , vec ! [ 0 ] ) . with_params ( vec ! [ -0.75 ] ) ,
536+ } ) ;
537+ pg. add_op ( Op :: Rotation {
538+ data : RotationData :: new ( vec ! [ Pauli :: X , Pauli :: Y ] , 1.25 ) ,
539+ } ) ;
540+ pg. add_op ( Op :: Gate {
541+ data : GateData :: new ( GateType :: PHASEDX , vec ! [ 0 ] ) . with_params ( vec ! [ 0.75 , 1.25 ] ) ,
542+ } ) ;
543+ pg. add_op ( Op :: Gate {
544+ data : GateData :: new ( GateType :: ZZPHASE , vec ! [ 0 , 1 ] ) . with_params ( vec ! [ -1.25 ] ) ,
545+ } ) ;
546+ pg. add_op ( Op :: Gate {
547+ data : GateData :: new ( GateType :: H , vec ! [ 1 ] ) ,
548+ } ) ;
549+ let pass = CanonicalFormPass :: new ( ) . with_forward ( forward) ;
550+ let transformed = pass. transform ( & pg) ;
551+
552+ for op in transformed. get_ops ( ) {
553+ if let Op :: Rotation { data } = op {
554+ assert ! ( data. get_angle( ) . abs( ) < 0.5 ) ;
555+ }
556+ }
557+ assert ! ( compare_unitaries_via_tk( & pg, & transformed) ) ;
558+ }
559+
458560 #[ rstest]
459561 #[ case( true , true , vec![ 0.5 , 1.0 ] , 1 ) ]
460562 #[ case( false , true , vec![ 0.5 , 1.0 ] , 1 ) ]
0 commit comments