diff --git a/pygsti/tools/su2tools.py b/pygsti/tools/su2tools.py index 4eb718790..3716b8fb1 100644 --- a/pygsti/tools/su2tools.py +++ b/pygsti/tools/su2tools.py @@ -189,6 +189,13 @@ def axis_rotation_angle_from_2x2_unitaries(U): theta[theta < 0] += 2*np.pi theta[theta > np.pi] = 2*np.pi - theta[theta > np.pi] return theta + + @staticmethod + def axis_rotation_angle_from_euler_angles(abg): + theta_by_2 = np.atleast_1d(np.arccos(np.cos(abg[1]/2)*np.cos((abg[0]+abg[2])/2))) + theta = 2*theta_by_2 + theta[theta > np.pi] = 2*np.pi - theta[theta > np.pi] + return theta @classmethod def unitaries_from_angles(cls,alpha,beta,gamma): @@ -254,20 +261,22 @@ def angles2irrepchars(cls, angles): angles = np.atleast_1d(angles) angles = np.atleast_2d(angles).T assert angles.shape == (3, angles.size // 3) - out = cls.characters_from_euler_angles(angles).T + out = cls.characters_from_euler_angles(angles) assert out.shape == (angles.size // 3, cls.irrep_labels.size) return out @classmethod def characters_from_euler_angles(cls,abg): - # Require that a,b,g = abg (so either it's an array of shape (3,) or (3,N) for some N) - theta_by_2 = np.arccos(np.cos(abg[1]/2)*np.cos((abg[0]+abg[2])/2)) - theta = 2*theta_by_2 - theta[theta > np.pi] = 2*np.pi - theta[theta > np.pi] + # Input: + # Require that a,b,g = abg (so either it's an array of shape (3,) or (3,N) for some N) + # Output: + # if abg.size == 3, then out.shape == (cls.num_irreps,) + # else, out.shape == (abg.shape[1], ) + theta = SU2.axis_rotation_angle_from_euler_angles(abg) theta_by_2 = theta / 2 def char(_k): return np.sin((2*_k + 1)*theta_by_2)/np.sin(theta_by_2) - out = np.array([ char(_k) for _k in cls.irrep_labels]) + out = np.array([ char(_k) for _k in cls.irrep_labels]).T return out @@ -500,4 +509,5 @@ def all_characters_from_unitary(cls, U): out.append(np.sum(vec)) idx += b_sz out = np.array(out) - return out + return out.reshape((1,-1)) + diff --git a/test/unit/tools/test_su2tools.py b/test/unit/tools/test_su2tools.py index 990cdf7d5..d09071ec3 100644 --- a/test/unit/tools/test_su2tools.py +++ b/test/unit/tools/test_su2tools.py @@ -54,7 +54,6 @@ def ref_handle(t): return la.expm(1j * t * su2class.Jy) self._test_matval_funcs_agree(self.thetas, our_handle, ref_handle) - def _test_expmiJy_batch(self, su2class): actual = su2class.expm_iJy(self.thetas) expect = np.array([ @@ -204,6 +203,20 @@ def reference_from_paper(N: int): #TODO: run KS tests for each of the three angles, comparing against SU2.random_euler_angles. return + def test_rotation_axis_angles(self): + # compare angles extracted from unitaries and from Euler angles + np.random.seed(0) + eas = np.row_stack(SU2.random_euler_angles(5)) + Us = SU2.unitaries_from_angles(*eas) + raas_from_eas = SU2.axis_rotation_angle_from_euler_angles(eas) + raas_from_us = SU2.axis_rotation_angle_from_2x2_unitaries(Us) + for t_ea, t_u in zip(raas_from_eas, raas_from_us): + self.assertGreaterEqual(t_ea, 0.0) + self.assertGreaterEqual(t_u, 0.0) + self.assertLessEqual(t_ea, np.pi) + self.assertLessEqual(t_u, np.pi) + self.assertAlmostEqual(t_ea, t_u) + return class TestSpin72(BaseSU2Tests): @@ -248,11 +261,11 @@ def test_characters_from_euler_angles(self): # be verified by testing a single random SU(2) element, and the diagonalizer's # unitarity can be checked cheaply. np.random.seed(0) - aa = np.column_stack(Spin72.random_euler_angles(5)) - Us = Spin72.unitaries_from_angles(aa[:,0], aa[:,1], aa[:,2]) + aa = np.row_stack(Spin72.random_euler_angles(5)) + Us = Spin72.unitaries_from_angles(*aa) for i,U in enumerate(Us): expect = Spin72.all_characters_from_unitary(U) - actual = Spin72.characters_from_euler_angles(aa[i,:]) + actual = Spin72.characters_from_euler_angles(aa[:,i]) discrepency = la.norm(actual - expect) self.assertLessEqual(discrepency, 64*self.RELTOL) return