def _get_e7r_912_inner(filename, verbose=True, e7=e7): """Computes a basis for the E7 912-irrep in 56 x 133.""" # The 912-irrep is characterized by (2.14) in # https://arxiv.org/pdf/1112.3345.pdf / # (2.12) in https://arxiv.org/pdf/0705.2101.pdf # # (t_b t^a)_M^N Theta_N^b = -0.5 Theta_M^a try: return numpy.load(filename)['arr_0'] except (IOError, OSError): if verbose: print('Need to compute t912.') t0 = time.time() k_ab = mu.nsum('aMN,bNM->ab', e7.t56r, e7.t56r) # Notation in the papers is slightly ambiguous here. # We want '_bM^P,_aP^N' here or '_bP^N,_aM^P'? # Only the former option is a correct interpretation. M0 = mu.nsum('_bM^P,_aP^N,^aA->^M_A^N_b', e7.t56r, e7.t56r, numpy.linalg.inv(k_ab)).reshape(56 * 133, 56 * 133) for k in range(56 * 133): M0[k, k] += 0.5 ret = scipy.linalg.null_space(M0.real, rcond=1e-5).T if verbose: # This computation typically takes ~5 minutes. # We hence cache the result to disk (~50M). print('Computing the 912-basis took %.3f sec.' % (time.time() - t0)) numpy.savez_compressed(filename, ret) # Note that this null_space is orthonormal. return ret
def get_quadratic_constraints(thetas_in_912, e7=e7): """For a collection of Theta-tensors, returns the quadratic constraints. Note: This is typically a memory-wise expensive operation. Args: thetas_in_912: [k, 56, 133]-array of k Theta-tensors that must belong to the 912-irrep of potentially gaugeable Thetas. Here, the 56-index refers to the e7.t56r basis. e7: The algebra.E7 instance to use for E7-conventions. Returns: [c, k, k]-array of quadratic constraints `qcs`. The c-th entry represents the c-th quadratic constraint, i.e. einsum('pma,p->ma', thetas_in_912, coeffs) is gaugeable if einsum('kpq,p,q->k', qcs, coeffs, coeffs) vanishes. """ # arXiv:1112.3345, below (2.15): For Theta-tensors that satisfy # the linear 912-constraint, the two quadratic constraints are equivalent, so # we only have to check the Omega-constraint: Theta_M^a Theta_N^b Omega^MN = 0 thetas_qc = mu.nsum('W_M^a,Z_N^b,^MN->WZab', thetas_in_912, thetas_in_912, e7.omega) # Symmetrize in W,Z, since we use the same linear combination of thetas # for both. Update entries for memory efficiency. thetas_qc += mu.nsum('WZab->ZWab', thetas_qc) ttsu, ttss, ttsvh = numpy.linalg.svd(thetas_qc.reshape(-1, 133 * 133)) del ttsvh # Unused. q_constraints = ttsu.T[ttss > 1e-9].reshape([-1] + [thetas_in_912.shape[0]] * 2) for level in (2, 1, 0): gc.collect(level) return q_constraints
def test_get_normalizing_subalgebra_generic(self): """Normalizing subalgebra is as expected in a generic situation.""" e7 = algebra.g.e7 su8 = e7.su8 so8_algebra = e7.f_abC[-28:, :70, :70] # If we pick the subspace of (8s x 8s)_sym_traceless matrices that # are block-diagonal with entries only in the top-left 3x3 block, # then there is an obvious SO(5) centralizer and an obvious # SO(3)xSO(5) normalizer. the_subspace = mu.numpy_from_nonzero_entries( [70, 5], [ (1.0, 0, 0), (1.0, 1, 1), # The diagonal-traceless parts. (1.0, 7 + su8.inv_ij_map[(0, 1)], 2), (1.0, 7 + su8.inv_ij_map[(0, 2)], 3), (1.0, 7 + su8.inv_ij_map[(1, 2)], 4) ]) # Let us actually rotate around this subspace with some # randomly-picked generic small SO(8)-rotation. rotated_subspace = mu.nsum( 'an,ba->bn', the_subspace, scipy.linalg.expm( mu.nsum('abc,a->cb', so8_algebra, mu.rng(0).normal(size=(28, ), scale=0.1)))) normalizer = algebra.get_normalizing_subalgebra( so8_algebra, rotated_subspace) self.assertEqual((28, 3 + 10), normalizer.shape) # dim(SO(3)xSO(5)) = 13.
def get_theta_u4xr12(c=1.0): """Returns the Dyonic-U(4)|xR12 Theta-tensor.""" spin8 = algebra.g.spin8 su8 = algebra.g.su8 theta = numpy.zeros([56, 133]) d6 = numpy.diag([0.0] * 6 + [1.0] * 2) cd6 = numpy.eye(8) - d6 theta[:28, 105:] += (-1 / 64.0) * mu.nsum( 'Iij,Kkl,ijab,klcd,ac,bd->IK', su8.m_28_8_8, su8.m_28_8_8, spin8.gamma_vvss, spin8.gamma_vvss, cd6, numpy.eye(8)) theta[28:, 105:] += (-1 / 64.0) * mu.nsum( 'Iij,Kkl,ijab,klcd,ac,bd->IK', su8.m_28_8_8, su8.m_28_8_8, spin8.gamma_vvss, spin8.gamma_vvss, c * d6, numpy.eye(8)) theta[:28, :35] += -(1 / 16.0) * mu.nsum( 'Iij,Aab,ijcd,ac,bd->IA', su8.m_28_8_8, su8.m_35_8_8, spin8.gamma_vvss, numpy.eye(8), d6) theta[28:, :35] += -(1 / 16.0) * mu.nsum( 'Iij,Aab,ijcd,ac,bd->IA', su8.m_28_8_8, su8.m_35_8_8, spin8.gamma_vvss, numpy.eye(8), c * d6) return theta
def _canonicalize_equilibrium_sc(self, v70, diagonalize_8x8s=True, rng=None, verbose=True): """Simplifies a location on the scalar manifold by rotation.""" if rng is None: rng = numpy.random.RandomState() m8x8s = mu.nsum('Aij,A->ij', self.e7.su8.m_35_8_8.real, v70[:35]) m8x8c = mu.nsum('Aij,A->ij', self.e7.su8.m_35_8_8.real, v70[35:]) rot = self.e7.spin8.get_diagonalizing_rotation( m8x8s if diagonalize_8x8s else m8x8c) decomposed_rot = mu.product_decompose_rotation(rot) resynthesized_rot = mu.resynthesize_rotation_for_rep( 8, 8, decomposed_rot, 'ab,->ab', numpy.ones([])) if not numpy.allclose(rot, resynthesized_rot, rtol=1e-3, atol=1e-5): raise ValueError( 'Resynthesized rotation does not match original rotation.') generator_mapping_spec = 'sS,sScC->cC' if diagonalize_8x8s else 'cC,sScC->sS' rep_action = 0.25 * self.e7.spin8.gamma_sscc rot_other_rep = mu.resynthesize_rotation_for_rep( 8, 8, decomposed_rot, generator_mapping_spec, rep_action) (rot_s, rot_c) = ((rot, rot_other_rep) if diagonalize_8x8s else (rot_other_rep, rot)) canon_m8x8s = rot_s.T @ m8x8s @ rot_s canon_m8x8c = rot_c.T @ m8x8c @ rot_c if diagonalize_8x8s: gens_postdiag = mu.get_generators_for_post_diagonalization_reduction( numpy.diag(canon_m8x8s), 'gsS,sScC->gcC', self.e7.spin8.gamma_sscc) else: gens_postdiag = mu.get_generators_for_post_diagonalization_reduction( numpy.diag(canon_m8x8c), 'gcC,sScC->gsS', self.e7.spin8.gamma_sscc) tc_rot_gens = mu.tff64(gens_postdiag) tc_8x8s = mu.tff64(canon_m8x8s) tc_8x8c = mu.tff64(canon_m8x8c) @tf.function def tf_rotated_8x8(t_rot_params): t_rot = mu.tf_expm( tf.einsum('gab,g->ab', tc_rot_gens, t_rot_params)) if diagonalize_8x8s: tc_rotated_8x8 = tf.linalg.matmul( t_rot @ tc_8x8c, t_rot, transpose_b=True) else: tc_rotated_8x8 = tf.linalg.matmul( t_rot @ tc_8x8s, t_rot, transpose_b=True) return tc_rotated_8x8 @tf.function def tf_loss(t_rot_params): t_8x8 = tf_rotated_8x8(t_rot_params) ret = tf.reduce_sum(tf.abs(t_8x8)) return ret if gens_postdiag.shape[0] == 0: return self.e7.v70_from_35s35c(canon_m8x8s, canon_m8x8c) _, opt_rot_params = mu.tf_minimize_v2( tf_loss, rng.normal(scale=1.0, size=gens_postdiag.shape[0]), default_gtol=1e-14) opt_8x8 = tf_rotated_8x8(mu.tff64(opt_rot_params)).numpy() if diagonalize_8x8s: return self.e7.v70_from_35s35c(canon_m8x8s, opt_8x8) else: return self.e7.v70_from_35s35c(opt_8x8, canon_m8x8c)
def align_thetas(theta_to_align, thetas_target, e7=e7, debug=True, maxiter=10**4, x0_hint=None, strategy=(('BFGS', None), ), seed=11): """Aligns a linear combination of thetas_input with thetas_target.""" # thetas_target.shape == (num_thetas, 56, 133). t0 = t_now = time.time() num_thetas = thetas_target.shape[0] tf_turners = tf_e7_turners_56_133o(e7=e7) # 'Split rotation' here means that on an ^a-index, we first apply a # 'compact SU(8)' and then a 'noncompact E7' rotation. theta_to_align_56x133o = mu.nsum('Ma,ca->Mc', theta_to_align, e7.v133o_from_v133) tc_theta_to_align_56x133o = mu.tff64(theta_to_align_56x133o) thetas_target_56x133o = mu.nsum('nMa,ca->nMc', thetas_target, e7.v133o_from_v133) tc_thetas_target_56x133o = mu.tff64(thetas_target_56x133o) def tf_rotated(t_133o): t_rot_133o, t_rot_56 = tf_turners(t_133o) return tf.einsum('Ma,ba,MN->Nb', tc_theta_to_align_56x133o, t_rot_133o, t_rot_56) def tf_rotation_loss(t_133ox): nonlocal t_now t_rotated = tf_rotated(t_133ox[:133]) t_target = mu.nsum('nMa,n->Ma', tc_thetas_target_56x133o, t_133ox[133:]) t_loss = tf.math.reduce_sum(tf.math.square(t_rotated - t_target)) if debug: t_next = time.time() print(f'T={t_next - t0:8.3f} sec (+{t_next - t_now:8.3f} sec) ' f'Loss: {t_loss.numpy():.12g}') t_now = t_next return t_loss if x0_hint is not None: x0 = numpy.asarray(x0_hint) else: x0 = (mu.rng(seed).normal(size=133, scale=0.05).tolist() + mu.rng(seed + 1).normal(size=num_thetas, scale=0.25).tolist()) opt_val, opt_133ox = mu.tf_minimize_v2(tf_rotation_loss, x0, strategy=strategy, default_maxiter=maxiter, default_gtol=1e-14) # Note that output-index of the projector is ^a. # To this, we apply NC @ C. return (opt_val, opt_133ox, numpy.concatenate( [e7.v133_from_v133o.dot(opt_133ox[:133]), opt_133ox[133:]], axis=0), mu.nsum('Ma,ba->Mb', tf_rotated(mu.tff64(opt_133ox[:133])).numpy(), e7.v133_from_v133o))
def gauge_group_gens_from_theta(theta, threshold=1e-4, e7_gens=None): """Determines the gauge group generators, given the Theta-tensor.""" # First, we need to know how each of the E7-generators acts on the # Theta-tensor. We can express this as a (56 x 133) x 133-matrix. if e7_gens is None: e7_gens = numpy.eye(133) d_theta = (-mu.nsum('M^b,_aN^M,an->N^bn', theta, e7.t56r, e7_gens) + mu.nsum('M^b,_ab^c,an->M^cn', theta, e7.f_abC, e7_gens)) su, ss, svh = numpy.linalg.svd(d_theta.reshape(-1, e7_gens.shape[-1])) del su # Unused. return svh[ss <= threshold].T
def get_boosted_theta_so8(omega, v70, e7=None): """Returns a boosted Theta-tensor for dyonic SO(8) gauging.""" if e7 is None: e7 = algebra.g.e7 theta = numpy.zeros([56, 133]) cw, sw = numpy.cos(omega), numpy.sin(omega) theta[:28, 3 * 35:] = +cw * numpy.eye(28) * 0.25 theta[28:, 3 * 35:] = sw * numpy.eye(28) * 0.25 g56 = mu.expm(mu.nsum('a,_aM^N->^N_M', v70, e7.t56r[:70])) g133 = mu.expm(mu.nsum('a,_ab^C->^C_b', -v70, e7.f_abC[:70])) return mu.nsum('_M^a,^M_N,^b_a->_N^b', theta, g56, g133)
def f_MNP_from_Theta(Theta, e7=e7, rcond=1e-15): """Computes the embedded gauge group structure constants.""" X = get_X(Theta, e7=e7) # [X_M, X_N] = f_MN^P X_P kX_ab = mu.nsum('MPQ,NQP->MN', X, X) # kX_ab will have rank 28, not 56. So, we need to use a pseudo-inverse here. kX_inv_ab = numpy.linalg.pinv(kX_ab, rcond=rcond) X_MN = mu.asymm2(mu.nsum('MPR,NRQ->MNPQ', X, X), 'MNPQ->NMPQ') X_MN_P = mu.nsum('MNRS,PSR->MNP', X_MN, X) # Problem: kX_ab has rank < 56. # Let's try getting the f_MNP from solving a least-squares problem. f_MNP = mu.nsum('MNQ,QP->MNP', X_MN_P, kX_inv_ab) return f_MNP
def test_e7_56(self): """Tests the weightspace decomposition of the E7 56-irrep.""" e7 = algebra.g.e7 spin8 = algebra.g.spin8 t_aMN = e7.t_a_ij_kl fano_mabs = numpy.stack( [spin8.gamma_vvvvss[ijkl] for ijkl in e7.fano_ijkl], axis=0) cartan7_op56_exact = mu.nsum('nsS,asS,aMN->nNM', fano_mabs, e7.v70_from_sc8x8[:, 0, :, :], t_aMN[:70, :, :]) / 8 # Simulate numerically-noisy (with reproducible noise) Cartan generators. cartan7_op56 = cartan7_op56_exact + numpy.random.RandomState(0).normal( size=cartan7_op56_exact.shape, scale=1e-10) e7_56_ws = list(cartan_dynkin.get_weightspaces(cartan7_op56).items()) self.assertEqual({w.shape for _, w in e7_56_ws}, {(1, 56)}) weights_with_bad_eigenvalues = [ weight for weight, _ in e7_56_ws if any(w not in (-0.5, 0, 0.5) for w in weight) ] self.assertEqual(weights_with_bad_eigenvalues, []) weights_with_bad_allocation = [ weight for weight, _ in e7_56_ws if sum(w == 0 for w in weight) != 4 ] self.assertEqual(weights_with_bad_allocation, [])
def get_subspace_aligner(self, target_subspace_an, rcond=1e-10): """Returns a closure that aligns a scalar vector with a target space.""" target_subspace_an = numpy.asarray(target_subspace_an) if target_subspace_an.shape[0] != 70 or len(target_subspace_an.shape) != 2: raise ValueError( 'Target subspace must be a [70, D]-array, ' f'shape is: {target_subspace_an.shape}') tc_f_abC = mu.tff64(self.e7.f_abC) v70o_target_subspace_an = mu.nsum( 'an,Aa->An', target_subspace_an, self.e7.v70o_from_v70) svd_u, svd_s, svd_vh = numpy.linalg.svd(v70o_target_subspace_an, full_matrices=False) del svd_vh # Unused, named for documentation purposes only. v70o_target_subspace_an_basis = svd_u[:, svd_s > rcond] tc_v70o_proj_complement = mu.tff64( numpy.eye(70) - v70o_target_subspace_an_basis.dot(v70o_target_subspace_an_basis.T)) tc_v70o_from_v70 = mu.tff64(self.e7.v70o_from_v70) # def f_do_align(v70, **kwargs): tc_v70 = mu.tff64(v70) def tf_loss(t_rot_params): t_gen_so8 = tf.einsum('abC,a->Cb', tc_f_abC[-28:, :70, :70], t_rot_params) t_rot_so8 = tf.linalg.expm(t_gen_so8) t_rotated = tf.einsum('ab,b->a', t_rot_so8, tc_v70) t_deviation = tf.einsum( 'a,Aa,BA->B', t_rotated, tc_v70o_from_v70, tc_v70o_proj_complement) return tf.reduce_sum(tf.math.square(t_deviation)) return mu.tf_minimize_v2(tf_loss, v70, **kwargs) # return f_do_align
def v70o_goldstone_basis_and_projector(self, v70o, ev_threshold=1e-5): """Computes a basis and a projector onto the 'goldstone directions'. Given a vector of 70 scalar field parameters in the orthonormal basis, determines a basis and projector for the subspace of directions that we get by applying so(8) generators to the vector (again, referring to the orthonormal basis.) Args: v70o: Optional [70]-numpy.ndarray, the scalar field parameters in the orthonormal basis. ev_threshold: Threshold for SVD singular values. Returns: A tuple (dim_goldstone_basis, basis, goldstone_projector) that gives us the dimensionality D == dim_goldstone_basis of the vector space V spanned by applying so(8) generators to the input vector, plus a [70, 70]-array B that provides an orthonormal basis for the scalars where B[:, :D] is an orthonormal basis of the D-dimensional subspace V, plus a [70, 70]-projector matrix that performs orthogonal projection onto V. """ e7 = self.e7 so8_rotated_v70o = mu.nsum('abC,b->Ca', e7.fo_abC[105:, :70, :70], v70o) svd_so8_rot_u, svd_so8_rot_s, svd_so8_rot_vh = ( numpy.linalg.svd(so8_rotated_v70o, full_matrices=True)) del svd_so8_rot_vh # Unused, named for documentation only. num_goldstone_directions = (svd_so8_rot_s > ev_threshold).sum() goldstone_directions = svd_so8_rot_u[:, :num_goldstone_directions] proj_goldstone = goldstone_directions.dot(goldstone_directions.T) return num_goldstone_directions, svd_so8_rot_u, proj_goldstone
def holomorphic_w_sugra(zs): gs = mu.undiskify(zs) * 0.25 v70 = mu.nsum('zna,zn->a', algebra.g.e7.sl2x7[:2, :, :70], numpy.stack([gs.real, gs.imag], axis=0)) A1 = SUGRA.tf_ext_sugra_tensors(mu.tff64(v70), with_stationarity=False)[2].numpy() kaehler_factor = numpy.sqrt((1 - zs * zs.conjugate()).prod()) return a123.superpotential(A1, direction_A1) * kaehler_factor
def show_theta(Theta, e7=e7): """Prints a Theta-tensor and the associated gauging's Killing form.""" f_MNP = f_MNP_from_Theta(Theta, rcond=1e-15, e7=e7) k_ab = mu.nsum('MPQ,NQP->MN', f_MNP, f_MNP) mu.tprint(Theta, d=5, name='Theta') mu.tprint(k_ab, d=5, name='k_MN') print( 'K_ab eigvals:', sorted( collections.Counter(numpy.linalg.eigvals(k_ab).round(6)).items()))
def test_sl2x7(self): # TODO(tfish): Parametrize by Spin(8)-conventions. e7 = algebra.g.e7 for scv in range(3): self.assertTrue( numpy.allclose( 0, mu.nsum('ma,nb,abC->mnC', e7.sl2x7[scv], e7.sl2x7[scv], e7.f_abC))) # The (s, c)-commutators must produce the v-generators. comms_sc = mu.nsum('ma,nb,abC->mnC', e7.sl2x7[0], e7.sl2x7[1], e7.f_abC) d777 = numpy.zeros([7, 7, 7]) # 'diagonal-promoter'. for i in range(7): d777[i, i, i] = 1 comms_expected = 2 * numpy.pad( mu.nsum('Aab,nab,AB,npq->pqB', e7.su8.m_35_8_8, e7.sl2x7_88v, e7.su8.ginv35, d777), [(0, 0), (0, 0), (70, 28)]) self.assertTrue(numpy.allclose(comms_sc, comms_expected))
def show_position_tex(self, position, digits=6): """Returns a text-string that shows the position.""" m35s = mu.nsum('Iij,I->ij', self.e7.su8.m_35_8_8, position[:35]) m35c = mu.nsum('Iij,I->ij', self.e7.su8.m_35_8_8, position[35:70]) fmt_num = lambda x: f'{x:.0{digits}f}' def dotified_m(sc, ij, is_positive): sign_str = '' if is_positive else '-' indices = (r'\dot{}\dot{}' if sc else '{}{}').format(*ij) return '%sM_{%s}' % (sign_str, indices) pos_sign_by_text = collections.defaultdict(list) for sc, m35 in ((0, m35s), (1, m35c)): for ij in itertools.product(range(8), range(8)): if not ij[0] <= ij[1]: # We only report entries of the upper-triangular part of these # symmetric matrices. continue num = m35[ij] abs_num_str = fmt_num(abs(num)) if float(abs_num_str) == 0: # Skip zeroes. continue pos_sign_by_text[abs_num_str].append((sc, ij, num > 0)) groups = sorted( [(sorted(locations), abs_num_str) for abs_num_str, locations in pos_sign_by_text.items()]) tex_pieces = [] for raw_locations, abs_num_str in groups: is_plus_first = raw_locations[0][-1] if is_plus_first: locations = raw_locations num_str = abs_num_str else: # 'is_plus' gets replaced by relative sign w.r.t. first such entry. locations = [(sc, ij, not is_plus) for sc, ij, is_plus in raw_locations] num_str = '-' + abs_num_str tex_pieces.append( r'$\scriptstyle %s\approx%s$' % ( r'{\approx}'.join(dotified_m(*sc_ij_relative_sign) for sc_ij_relative_sign in locations), num_str)) return (r'{\begin{minipage}[t]{10cm}' r'\begin{flushleft}%s\end{flushleft}\end{minipage}}\\' % ', '.join(tex_pieces))
def tf_rotation_loss(t_133ox): nonlocal t_now t_rotated = tf_rotated(t_133ox[:133]) t_target = mu.nsum('nMa,n->Ma', tc_thetas_target_56x133o, t_133ox[133:]) t_loss = tf.math.reduce_sum(tf.math.square(t_rotated - t_target)) if debug: t_next = time.time() print(f'T={t_next - t0:8.3f} sec (+{t_next - t_now:8.3f} sec) ' f'Loss: {t_loss.numpy():.12g}') t_now = t_next return t_loss
def reduce_thetas(thetas_now, e7_gen): # Z = batch-index. d_thetas = get_d_thetas(thetas_now, e7_gen) # We want to find those thetas that are invariant under this particular # rotation with some E7-element. Let us try to do this via a SVD. su, ss, svh = numpy.linalg.svd(d_thetas.reshape(-1, 56 * 133)) del svh # Unused. # d_thetas[k, :].reshape(...) gives us what the k-th Theta got mapped to. # We want to know those weight-vectors for the original Theta-s that give # us zeroes. if verbose: print('SVD ss:', sorted(collections.Counter(ss.round(3)).items())) kernel_basis = su.T[ss <= threshold] return mu.nsum('YZ,Z_M^b->Y_M^b', kernel_basis, thetas_now)
def show_position_text(self, position): """Returns a text-string that shows the position.""" m288 = mu.nsum('a,axij->xij', position, self.e7.v70_as_sc8x8) m8x8s = m288[0].round(5) m8x8c = m288[1].round(5) def fmt8x8(m): # Pylint wrongly complains about `row` not being defined. # pylint: disable=undefined-loop-variable return '\n'.join( ', '.join('%+.5f' % x if x else ' 0 ' for x in row) for row in m) # pylint: enable=undefined-loop-variable return (f'=== 8x8s ===\n{fmt8x8(m8x8s)}\n' f'=== 8x8c ===\n{fmt8x8(m8x8c)}\n')
def e7_rotate_theta(theta, e7_rotation, order133_c_first=True, e7=e7): """E7-rotates a Theta-tensor. Args: theta: [56, 133]-numpy.ndarray, the Theta-tensor to rotate. e7_rotation: [133]-numpy.ndarray, the e7 generator parameters to build the rotation from. order133_c_first: If true, transform the ^a index on the Theta-tensor via noncompact_cb compact_ba theta...^a, i.e. applying the compact rotation built from the su(8) part of e7_rotation first. If false, the noncompact rotation will get applied first. (Useful for applying an inverted rotation.) e7: The e7 algebra that provides the relevant definitions. Returns: A [56, 133]-numpy.ndarray, the rotated Theta-tensor. """ # The 'rotation' is split here: We separately exponentiate # the 'compact' and 'noncompact' part, and, on an ^a-index, # apply noncompact @ compact. rot_nc133 = scipy.linalg.expm( mu.nsum('abC,a->Cb', e7.f_abC[:70], e7_rotation[:70])) rot_c133 = scipy.linalg.expm( mu.nsum('abC,a->Cb', e7.f_abC[70:], e7_rotation[70:])) rot_nc56 = scipy.linalg.expm( mu.nsum('aMN,a->NM', e7.t56r[:70], -e7_rotation[:70])) rot_c56 = scipy.linalg.expm( mu.nsum('aMN,a->NM', e7.t56r[70:], -e7_rotation[70:])) if order133_c_first: rot_133 = rot_nc133 @ rot_c133 rot_56 = rot_c56 @ rot_nc56 else: rot_133 = rot_c133 @ rot_nc133 rot_56 = rot_nc56 @ rot_c56 return mu.nsum('Ma,ba,MN->Nb', theta, rot_133, rot_56)
def tf_e7_turners_56_133o(e7=e7): """Returns closure that maps 70+63 e7-rotation to _56 and ^133o matrices.""" # This is a somewhat technical helper function which however # is useful to have in quite a few places. tc_fo_abC = mu.tff64(e7.fo_abC) tc_to56r = mu.tff64(mu.nsum('aMN,ac->cMN', e7.t56r, e7.v133_from_v133o)) def tf_turners(t_133o, order133_c_first=True): """Returns 133o x 133o and 56 x 56 rotation matrices. Args: t_133o: numpy.ndarray, rotation-parameters in the 133o-irrep of e7 from which we build two rotations (per Theta-index, so four in total), one for the compact and one for the noncompact part of the algebra. order133_c_first: If true, the 133o-rotation is built by first performing the compact, then the noncompact rotation. (Useful for taking inverses.) Returns: A pair `(t_rot_133o, t_rot_56)` of a [133, 133]-float64-tf.Tensor `t_rot133o` rotating the 133o-index on Theta and a [56, 56]-float64-tf.Tensor rotating the 56-index on Theta. """ t_70 = t_133o[:70] t_63 = t_133o[70:] t_rot_nc133o = tf.linalg.expm( tf.einsum('abC,a->Cb', tc_fo_abC[:70], t_70)) t_rot_c133o = tf.linalg.expm( tf.einsum('abC,a->Cb', tc_fo_abC[70:], t_63)) t_rot_nc56 = tf.linalg.expm( tf.einsum('aMN,a->NM', tc_to56r[:70], -t_70)) t_rot_c56 = tf.linalg.expm(tf.einsum('aMN,a->NM', tc_to56r[70:], -t_63)) if order133_c_first: t_rot_133o = t_rot_nc133o @ t_rot_c133o t_rot_56 = t_rot_c56 @ t_rot_nc56 else: t_rot_133o = t_rot_nc133o @ t_rot_c133o t_rot_56 = t_rot_c56 @ t_rot_nc56 return t_rot_133o, t_rot_56 return tf_turners
def get_gaugeability_condition_violations(Theta, checks=('omega', 'XX', 'linear_a', 'linear_b'), e7=e7, atol=1e-8, early_exit=True): """Checks whether the Theta-tensor satisfies the gaugeability constraints.""" violations = {} X = get_X(Theta, e7=e7) if 'omega' in checks: # (2.11) in arXiv:1112.3345 omega_theta2 = mu.nsum('MN,Ma,Nb->ab', e7.omega, Theta, Theta) if not numpy.allclose(0, omega_theta2, atol=atol): violations['omega'] = sum(abs(omega_theta2).ravel()**2)**.5 if early_exit: return violations if 'XX' in checks: # (2.12) in arXiv:1112.3345 XX_products = numpy.einsum('MPQ,NQR->MNPR', X, X) XX_comm = XX_products - numpy.einsum('MNPR->NMPR', XX_products) XX_MN = -numpy.einsum('MNP,PQR->MNQR', X, X) if not numpy.allclose(XX_comm, XX_MN, atol=atol): violations['XX'] = sum(abs(XX_comm - XX_MN).ravel()**2)**.5 if early_exit: return violations if 'linear_a' in checks: # (2.14) in arXiv:1112.3345 deviation_a = numpy.einsum('Na,aMN->M', Theta, e7.t56r) if not numpy.allclose(0, deviation_a, atol=atol): violations['linear_a'] = sum(abs(deviation_a)**2) if early_exit: return violations if 'linear_b' in checks: k56_inv = numpy.linalg.inv( numpy.einsum('aMN,bNM->ab', e7.t56r, e7.t56r)) tt = numpy.einsum('bMP,APN->bAMN', e7.t56r, numpy.einsum('aPN,aA->APN', e7.t56r, k56_inv)) tt_Theta = numpy.einsum('bAMN,Nb->MA', tt, Theta) deviation_b = tt_Theta + 0.5 * Theta if not numpy.allclose(0, deviation_b, atol=atol): violations['linear_b'] = sum(abs(deviation_b.ravel())**2)**.5 return frozenset(violations.items())
def get_gauging_from_theta(theta, max_residuals=1e-4): """Analyzes a Theta-tensor and returns the corresponding Gauging.""" gg_gens = gauge_group_gens_from_theta(theta, threshold=0.1) # Inner product on the gauge group generators, # induced by the e7-algebra's inner product. k_gg = mu.nsum('am,bn,ab->mn', gg_gens, gg_gens, e7.k133) gg_onb, gg_onbi = mu.get_gramian_onb(k_gg, eigenvalue_threshold=1.0) del gg_onbi # Unused, named for documentation only. gg_gens_onb = mu.nsum('am,nm->an', gg_gens, gg_onb) # # Let us check that we indeed did it right. k_gg_onb = mu.nsum('am,bn,ab->mn', gg_gens_onb, gg_gens_onb, e7.k133) diag_k_gg_onb = numpy.diag(k_gg_onb) assert numpy.allclose(k_gg_onb, numpy.diag(diag_k_gg_onb), atol=0.01), ( 'Inner product on the gauge group is off in the orthonormal basis') rel_diag_k_gg_onb = diag_k_gg_onb / abs(diag_k_gg_onb).max() # gg_null = gg_gens_onb[:, abs(rel_diag_k_gg_onb) < 0.01] gg_compact = gg_gens_onb[:, rel_diag_k_gg_onb < -0.01] gg_compact_commutators = mu.nsum('abC,am,bn->Cmn', e7.f_abC, gg_compact, gg_compact) (gg_compact_commutators_decomp, gg_decomp_residuals, *_) = numpy.linalg.lstsq(gg_compact, gg_compact_commutators.reshape(133, -1), rcond=1e-5) assert abs(gg_decomp_residuals).max() < max_residuals, ( '[Compact, Compact] ~ Compact decomposition has large residuals.') dim_compact = gg_compact.shape[-1] f_abC_compact = gg_compact_commutators_decomp[:dim_compact].reshape( [dim_compact] * 3) k_ab_compact = mu.nsum('mbc,ncb->mn', f_abC_compact, f_abC_compact) gg_compact_onb, gg_compact_onbi = mu.get_gramian_onb( k_ab_compact, eigenvalue_threshold=0.01) del gg_compact_onbi # Unused, named for documentation only. gg_compact_gens_onb = mu.nsum('am,nm->an', gg_compact, gg_compact_onb) k_ab_compact_onb = mu.nsum('Mm,Nn,mn->MN', gg_compact_onb, gg_compact_onb, k_ab_compact) k_ab_compact_onb_diag = numpy.diag(k_ab_compact_onb) gg_u1s = gg_compact_gens_onb[:, abs(k_ab_compact_onb_diag) < 0.5] gg_compact_semisimple = gg_compact_gens_onb[:, abs(k_ab_compact_onb_diag ) >= 0.5] dim_compact_semisimple = gg_compact_semisimple.shape[-1] random_gg_semisimple_elem = gg_compact_semisimple.dot( mu.rng(0).normal(size=dim_compact_semisimple)) # Let us see what the subspace of the compact-semisimple algebra # looks like that commutes with this random element. m_commute_with_random = mu.nsum('abC,a,bn->Cn', e7.f_abC, random_gg_semisimple_elem, gg_compact_semisimple) # 'csa' == 'Cartan Subalgebra' svd_csa_u, svd_csa_s, svd_csa_vh = numpy.linalg.svd(m_commute_with_random, full_matrices=False) del svd_csa_u # Unused, named for documentation only. csa_basis = svd_csa_vh[svd_csa_s < 1e-5, :].T gg_gens = numpy.concatenate([gg_compact_semisimple, gg_u1s, gg_null], axis=-1) gg_ranges = (0, gg_compact_semisimple.shape[1], gg_compact_semisimple.shape[1] + gg_u1s.shape[1], 28) assert gg_gens.shape[1] == 28, 'Gauge group is not 28-dimensional.' return Gauging(theta=theta, gg_gens=gg_gens, gg_ranges=gg_ranges, cartan_subalgebra=csa_basis)
def theta_key(theta): return (mu.nsum('Ma,Mb,ab->', theta, theta, e7.k133), mu.nsum('Ma,Ma->', theta[28:], theta[28:]))
def get_d_thetas(thetas, e7_gen): return (-mu.nsum('Z_M^b,_aN^M,a->Z_N^b', thetas, e7.t56r, e7_gen) + mu.nsum('Z_M^b,_ab^c,a->Z_M^c', thetas, e7.f_abC, e7_gen))
def get_random_gen(): return mu.nsum('na,n->a', e7_gens, rng.normal(size=len(e7_gens)))
def get_invariant_thetas( e7_gens, seed=0, num_rounds_max=100, threshold=1e-4, thetas_start=None, filename=None, onb_form=True, # Whether to transform to ONB. e7=e7, # The E7 algebra to use. verbose=True): """Computes a basis for e7_gens-invariant Thetas in the 912-irrep.""" # Index structure: Theta_M^a, with M a e7-real index; # returns a [N, 56, 133]-array. if filename is not None: try: return numpy.load(filename) except (IOError, OSError): pass # If we reach this point, loading cached thetas did not work. t0 = time.time() if verbose: print('Computing thetas.') rng = numpy.random.RandomState(seed=seed) def get_random_gen(): return mu.nsum('na,n->a', e7_gens, rng.normal(size=len(e7_gens))) def get_d_thetas(thetas, e7_gen): return (-mu.nsum('Z_M^b,_aN^M,a->Z_N^b', thetas, e7.t56r, e7_gen) + mu.nsum('Z_M^b,_ab^c,a->Z_M^c', thetas, e7.f_abC, e7_gen)) def reduce_thetas(thetas_now, e7_gen): # Z = batch-index. d_thetas = get_d_thetas(thetas_now, e7_gen) # We want to find those thetas that are invariant under this particular # rotation with some E7-element. Let us try to do this via a SVD. su, ss, svh = numpy.linalg.svd(d_thetas.reshape(-1, 56 * 133)) del svh # Unused. # d_thetas[k, :].reshape(...) gives us what the k-th Theta got mapped to. # We want to know those weight-vectors for the original Theta-s that give # us zeroes. if verbose: print('SVD ss:', sorted(collections.Counter(ss.round(3)).items())) kernel_basis = su.T[ss <= threshold] return mu.nsum('YZ,Z_M^b->Y_M^b', kernel_basis, thetas_now) # thetas_now = (thetas_start if thetas_start is not None else numpy.eye( 56 * 133).reshape(-1, 56, 133)) # A 7448 x 7448 matrix! prev_num_thetas = -1 # Impossible int at start. for num_round in range(num_rounds_max): thetas_now = reduce_thetas(thetas_now, get_random_gen()) num_thetas = thetas_now.shape[0] if verbose: print(f'Round {num_round}, num_thetas={num_thetas}') if num_thetas == prev_num_thetas: break prev_num_thetas = num_thetas gramian = mu.nsum('ZMa,WMb,ab->ZW', thetas_now, thetas_now, e7.k133) if verbose: t1 = time.time() print('Done computing thetas, T=%.3f sec' % (t1 - t0)) if not onb_form: return thetas_now, gramian # Need to bring to ONB-form. onb, onbi = mu.get_gramian_onb(gramian) del onbi # Unused. thetas_onb = mu.nsum('ZMa,WZ->WMa', thetas_now, onb) gramian_onb = mu.nsum('ZMa,WMb,ab->ZW', thetas_onb, thetas_onb, e7.k133) assert numpy.allclose(gramian_onb, numpy.diag(numpy.diag(gramian_onb))) if filename is not None: numpy.savez_compressed(filename, thetas_now, gramian_onb) return thetas_onb, gramian_onb