def __init__( self, *args, spin_range=Range(r'\uparrow\downarrow', 0, 2), spin_dumms=tuple(Symbol('sigma{}'.format(i)) for i in range(50)), **kwargs ): """Initialize the restricted particle-hole drudge.""" super().__init__( *args, spin=(spin_range, spin_dumms), **kwargs ) self.add_resolver({ UP: spin_range, DOWN: spin_range }) self.spin_range = spin_range self.spin_dumms = self.dumms.value[spin_range] sigma = self.dumms.value[spin_range][0] p = Symbol('p') q = Symbol('q') self.e_ = TensorDef(Vec('E'), (p, q), self.sum( (sigma, spin_range), self.cr[p, sigma] * self.an[q, sigma] )) self.set_name(e_=self.e_)
class ReducedBCSDrudge(SU2LatticeDrudge): r"""Drudge for reduced BCS (pairing) Hamiltonian. The reduced BCS drudge contains utilities for solving problems around the reduced BCS Hamiltonian, or pairing Hamiltonian. In this problem, we have an :math:`\mathfrak{su}(2)`-like algebra generated by subscriptable generators :math:`N`, :math:`P`, and :math:`P^\dag`, which satisfies the commutations rules .. math:: [N_p, P_q^\dag] & = 2 \delta_{pq} P_q^\dag \\ [N_p, P_q] &= -2 \delta_{pq} P_q \\ [P_p, P_q^\dag] &= \delta_{pq} \left( 1 - N_p \right) \\ where the symbols :math:`p` and :math:`q` can be over two disjoint ranges, the particle range and the hole range. The usage of these two ranges, as well as their default symbols, are exactly the same as those in the :py:class:`PartHoleDrudge`. In addition to the commutation rules, this drudge also has a Hamiltonian stored as ``ham`` attribute, which reads .. math:: \epsilon_p N_p + G_{p, q} P^\dag_p P_q where :math:`p` and :math:`q` are summed over the two ranges. """ DEFAULT_CARTAN = Vec('N') DEFAULT_RAISE = Vec(r'P^\dagger') DEFAULT_LOWER = Vec('P') def __init__(self, ctx, part_range=Range('V', 0, Symbol('nv')), part_dumms=PartHoleDrudge.DEFAULT_PART_DUMMS, hole_range=Range('O', 0, Symbol('no')), hole_dumms=PartHoleDrudge.DEFAULT_HOLE_DUMMS, all_orb_range=Range('A', 0, Symbol('na')), all_orb_dumms=PartHoleDrudge.DEFAULT_ORB_DUMMS, energies=IndexedBase('epsilon'), interact=IndexedBase('G'), cartan=DEFAULT_CARTAN, raise_=DEFAULT_RAISE, lower=DEFAULT_LOWER, root=Integer(2), norm=Integer(1), shift=Integer(-1), specials=None, **kwargs): """Initialize the drudge object.""" # Initialize the base su2 problem. super().__init__(ctx, cartan=cartan, raise_=raise_, lower=lower, root=root, norm=norm, shift=shift, specials=specials, **kwargs) # Set the range and dummies. self.part_range = part_range self.hole_range = hole_range self.all_orb_range = all_orb_range self.set_dumms(part_range, part_dumms) self.set_dumms(hole_range, hole_dumms) self.raise_ = raise_ self.cartan = cartan self.lower = lower self.shift = shift #------------------------------------------------------------------------# #------GH tweak here to deal with all-orbital-range separately-----------# #------NOTE that this renders the breaking of an arbitrary dummy---------# #------ into a occ and virtual useless ------------------------------# self.all_orb_dumms = tuple(all_orb_dumms) self.set_name(*self.all_orb_dumms) self.set_dumms(all_orb_range, self.all_orb_dumms) self.add_resolver_for_dumms() #------ GH commented the next 3 lines # self.add_resolver({ # i: (self.part_range, self.hole_range) for i in all_orb_dumms # }) #--------------------GH tweak ends here----------------------------------# # Make additional name definition for the operators. self.set_name(cartan, lower, Pdag=raise_) # Create the underlying particle-hole drudge with spin. Note that this # drudge is only use internally for VEV evaluation. ph_dr = SpinOneHalfPartHoleDrudge(ctx, part_orb=(part_range, part_dumms), hole_orb=(hole_range, hole_dumms)) self._ph_dr = ph_dr # Translation from su2 generator to the actual fermion operators. cr = ph_dr.cr an = ph_dr.an up, down = ph_dr.spin_vals gen_idx, gen_idx2 = self.all_orb_dumms[:2] cartan_def = self.define( cartan, gen_idx, cr[gen_idx, up] * an[gen_idx, up] + cr[gen_idx, down] * an[gen_idx, down]) raise_def = self.define(raise_, gen_idx, cr[gen_idx, up] * cr[gen_idx, down]) lower_def = self.define(lower, gen_idx, an[gen_idx, down] * an[gen_idx, up]) self._defs = [cartan_def, raise_def, lower_def] # Define the Hamiltonian. ham = self.einst(energies[gen_idx] * cartan[gen_idx] + interact[gen_idx, gen_idx2] * raise_[gen_idx] * lower[gen_idx2]) self.ham = ham.simplify() # Set additional tensor methods. self.set_tensor_method('eval_vev', self.eval_vev) # # Additional customization of the simplification # def normal_order(self, terms, **kwargs): """Take the operators into normal order. Here, in addition to the common normal-ordering operation, we remove any term with a cartan operator followed by an lowering operator with the same index, and any term with a raising operator followed by a cartan operator with the same index. """ noed = super().normal_order(terms, **kwargs) if self.shift == Integer(-1): noed = noed.filter( functools.partial(_nonzero_by_cartan, raise_=self.raise_, cartan=self.cartan, lower=self.lower)) return noed.filter(_is_not_zero_by_nilp) return noed # # Vacuum expectation value # def _transl2fermi(self, tensor: Tensor): """Translate a tensor object in terms of the fermion operators. This is an internally utility. The resulted tensor has the internal fermion drudge object as its owner. """ return Tensor(self._ph_dr, tensor.subst_all(self._defs).terms) def eval_vev(self, tensor: Tensor): r"""Evaluate the vacuum expectation value. The VEV facility works *as if* we do substitution .. math:: P_p &= c_{p \downarrow} c_{p \uparrow} \\ P^\dag_p &= c^\dag_{p \uparrow} c^\dag_{p \downarrow} \\ N_p &= c^\dag_{p \uparrow} c_{p \uparrow} + c^\dag_{p \downarrow} c_{p \downarrow} \\ for :math:`p` in either particle or hole range and evaluate the expectation value with respect to the Fermi vacuum. """ transled = self._transl2fermi(tensor) res = self._ph_dr.eval_fermi_vev(transled) return Tensor(self, res.terms) def eval_agp(self, tensor: Tensor, zlist): r"""Evaluate the vacuum expectation value. The AGP expectation value facility works *as if* we do substitution Takes as input the list of z matrices .. math:: \langle P^\dag_p P_q \rangle &= A^{(0)}_{pq} \\ \langle P^\dag_p N_r P_q \rangle &= A^{(1)}_{pqr} \\ \langle P^\dag_p P^\dag_q P_rP_s\rangle &= A^{(0)}_{pqrs} \\ """ num_ = self.cartan pdag_ = self.raise_ p_ = self.lower def get_vev_of_term(term): """Return the vev mapping of the term""" vecs = term.vecs t_amp = term.amp pdag_cnt = 0 p_cnt = 0 n_cnt = 0 pdag_indlist = [] p_indlist = [] n_indlist = [] if len(vecs) == 0: return [ Term(sums=term.sums, amp=t_amp * zlist[0][0], vecs=vecs) ] # Classify the indices into pdag, n, and p indicies for i in vecs: if i.base == pdag_: pdag_indlist.extend(list(i.indices)) elif i.base == p_: p_indlist.extend(list(i.indices)) elif i.base == num_: n_indlist.extend(list(i.indices)) else: return [] # Count the number of indices pdag_cnt = len(pdag_indlist) p_cnt = len(p_indlist) # First, we extract only the unique N indices unique_n_indlist = list(set(n_indlist)) n_cnt = len(unique_n_indlist) # Introduce appropriate power of 2 in amplitude to include the effect # of unique indices only being considered in N ldiff = len(n_indlist) - len(unique_n_indlist) t_amp *= 2**(ldiff) if pdag_cnt != p_cnt: # The expression must have equal number of Pdag's and P_'s return [] elif len(pdag_indlist) != len(set(pdag_indlist)): return [] elif len(p_indlist) != len(set(p_indlist)): return [] else: # Combining all the indices indcs = pdag_indlist indcs.extend(unique_n_indlist) indcs.extend(p_indlist) # Counting the different number of indices in order # to select the right symbol 'asymb' idx1 = int(n_cnt) idx_ppdag = len(indcs) - n_cnt idx2 = int((idx_ppdag / 2)) asymb = zlist[idx1][idx2] t_amp = t_amp * asymb[indcs] return [Term(sums=term.sums, amp=t_amp, vecs=())] return tensor.bind(get_vev_of_term)
class SU4LatticeDrudge(GenQuadDrudge): """Drudge for a lattice of SU(4) algebras, constructed strictly to cater the needs of Coupled Cluster with Thermofield Dynamics (TFD) in Lipkin. DESCRIPTION TO BE VERIFIED This drudge has the commutation rules for SU(4) algebras in Cartan-Weyl form (Ladder operators). Here both the shift and Cartan operators can have additional *lattice indices*. Operators on different lattice sites always commute. The the normal-ordering operation would try to put raising operators before the Cartan operators, which come before the lowering operators. """ DEFAULT_CARTAN1 = Vec('J^z') DEFAULT_CARTAN2 = Vec('K^z') DEFAULT_RAISE1 = Vec('J^+') DEFAULT_RAISE2 = Vec('K^+') DEFAULT_LOWER1 = Vec('J^-') DEFAULT_LOWER2 = Vec('K^-') DEFAULT_Ypp = Vec('Y^{++}') DEFAULT_Ypm = Vec('Y^{+-}') DEFAULT_Ymp = Vec('Y^{-+}') DEFAULT_Ymm = Vec('Y^{--}') DEFAULT_Yzz = Vec('Y^{zz}') DEFAULT_Ypz = Vec('Y^{+z}') DEFAULT_Yzp = Vec('Y^{z+}') DEFAULT_Ymz = Vec('Y^{-z}') DEFAULT_Yzm = Vec('Y^{z-}') def __init__(self, ctx, root=Integer(1), cartan1=DEFAULT_CARTAN1, raise1=DEFAULT_RAISE1, lower1=DEFAULT_LOWER1, cartan2=DEFAULT_CARTAN2, raise2=DEFAULT_RAISE2, lower2=DEFAULT_LOWER2, ypp=DEFAULT_Ypp, ypm=DEFAULT_Ypm, ymp=DEFAULT_Ymp, ymm=DEFAULT_Ymm, yzz=DEFAULT_Yzz, ypz=DEFAULT_Ypz, yzp=DEFAULT_Yzp, ymz=DEFAULT_Ymz, yzm=DEFAULT_Yzm, **kwargs): """Initialize the drudge. Parameters ---------- ctx The Spark context for the drudge. 2 cartan The basis operator for the Cartan subalgebra (:math:`J^z` operator for spin problem). It is registered in the name archive by the first letter in its label followed by an underscore. 2 raise The raising operator. It is also also registered in the name archive by the first letter in its label followed by ``_p``. 2lower The lowering operator, registered by the first letter followed by ``_m``. 9 Cross operators (Y_ab) Mix of the two SU(2)'s described by the cartans, raise and lower operators above. root The coefficient for the commutator between the Cartan and shift operators. norm (Feature removed from SU2 -> SU4, seems unused) The coefficient for the commutator between the raising and lowering operators. kwargs All other keyword arguments are given to the base class :py:class:`GenQuadDrudge`. """ super().__init__(ctx, **kwargs) self.cartan1 = cartan1 self.cartan2 = cartan2 self.raise1 = raise1 self.lower1 = lower1 self.raise2 = raise2 self.lower2 = lower2 self.ypp = ypp self.ymm = ymm self.yzz = yzz self.ypm = ypm self.ymp = ymp self.ypz = ypz self.yzp = yzp self.ymz = ymz self.yzm = yzm self.set_name( **{ cartan1.label[0] + '_': cartan1, cartan2.label[0] + '_': cartan2, raise1.label[0] + '_p': raise1, raise2.label[0] + '_p': raise2, lower1.label[0] + '_m': lower1, lower2.label[0] + '_m': lower2, ypp.label[0] + '_pp': ypp, ymm.label[0] + '_mm': ymm, yzz.label[0] + '_zz': yzz, ypm.label[0] + '_pm': ypm, ymp.label[0] + '_mp': ymp, ypz.label[0] + '_pz': ypz, yzp.label[0] + '_zp': yzp, ymz.label[0] + '_mz': ymz, yzm.label[0] + '_zm': yzm }) spec = _SU4Spec(cartan1=cartan1, raise1=raise1, lower1=lower1, root=root, cartan2=cartan2, raise2=raise2, lower2=lower2, ypp=ypp, ymm=ymm, yzz=yzz, ypm=ypm, ymp=ymp, ypz=ypz, yzp=yzp, ymz=ymz, yzm=yzm) self._spec = spec self._swapper = functools.partial(_swap_su4, spec=spec) @property def swapper(self) -> GenQuadDrudge.Swapper: """The swapper for the spin algebra.""" return self._swapper def eval_exp(self, h_tsr: Tensor, n1: Symbol): """Function to evaluate the expectation on hartree fock ground state of n-particle Lipkin Hamiltonian """ return h_tsr.bind(lambda x: get_vev_of_term(x, n1))
class UnitaryGroupDrudge(GenQuadDrudge): r"""Drudge to manipulate and work with Unitary Group Generators defined as: ..math:: E_{p,q} = c_{p,\uparrow}^\dagger c_{q,\uparrow} + \mathrm{h.c.} where the symbols :math: `p` and :math: `q` denote a general orbital index. This Unitary Group drudge comtains utilites to work with either the pairing Hamiltonian or any other system in the language of the unitary group gens. Without the presence of a reference, such as the Fermi sea or the pairing- ground state, there is no one way to define a canonical ordering for a string of :math: `E_{p,q}'\mathrm{s}`. Here we follow an lexicological or alphabetical ordering based on the first index followed by the second. For example, following is a representative of the correct canonical ordering: ..math:: E_{p,q} E_{p,r} E_{q,r} E_{r,p} """ DEFAULT_GEN = Vec('E') def __init__(self, ctx, all_orb_range=Range('A', 0, Symbol('na')), all_orb_dumms=PartHoleDrudge.DEFAULT_ORB_DUMMS, energies=IndexedBase(r'\epsilon'), interact=IndexedBase('G'), uga=DEFAULT_GEN, **kwargs): """Initialize the drudge object.""" # Initialize the base su2 problem. super().__init__(ctx, **kwargs) # Set the range and dummies. # self.add_resolver_for_dumms() self.all_orb_range = all_orb_range self.all_orb_dumms = tuple(all_orb_dumms) self.set_dumms(all_orb_range, all_orb_dumms) self.set_name(*self.all_orb_dumms) self.add_resolver({i: (all_orb_range) for i in all_orb_dumms}) # Set the operator attributes self.uga = uga # Make additional name definition for the operators. self.set_name(**{ uga.label[0] + '_': uga, 'eps': energies, 'G': interact }) #uga generaters can also be called using just 'uga[p,q]' instead of 'E_[p,q]' self.set_name(uga) # Defining spec for passing to an external function - the Swapper spec = _UGSpec(uga=uga, ) self._spec = spec # Create an instance of the ProjectedBCS or AGP Drudge to map from E_pq # to D_dag, N, D so that normal ordering and vev evaluation can be done self.eta = _eta self.sigma = _sigma self.set_name(**{'eta': self.eta, 'sigma': self.sigma}) # set the Swapper self._swapper = functools.partial(_swap_ug, spec=spec) @property def swapper(self) -> GenQuadDrudge.Swapper: """Swapper for the new AGP Algebra.""" return self._swapper def _isUGA(self, term): """Function returns True if the vectors are uga generators, otherwise False""" vecs = term.vecs for v in vecs: if v.base != self.uga: raise ValueError('Unexpected generator of the Unitary Group', v) return [Term(sums=term.sums, amp=term.amp, vecs=term.vecs)]
class ProjectedBCSDrudge(GenQuadDrudge): r"""Drudge to manipulate and work with new kind of creation, cartan and annihilation operators defined with respect to the Projected BCS reference, alternatively known as the AGP state. The three generators are: :math:`\mathbf{D}_{p,q}^\dagger`, :math:`\mathbf{N}_p` and :math:`\mathbf{D}_{pq}` which are defined as a local rotation of the unitary group generators. While the Projected BCS drudge is self contained, one would generally want to use it to study the pairing hamiltonian in the basis of these AGP operators. And it is much faster to work in the basis of unitary group generators using the UnitaryGroupDrudge. Eventually one can map the final expressions into this Projected BCS Drudge and compute VEV with respect to the AGP states. The commutation rules are very complicated. Some definitions for the symbols are described here :math: `\sigma_{pq} = \frac{1}{\eta_p^2 - \eta_q^2}` :math: `\eta_p` defines the transformation :math: `\mathbf{D}_{pq} = \frac{\eta_p E^p_q - \eta_q E^q_p}{\eta_p + \eta_q}` """ DEFAULT_CARTAN = Vec('N') DEFAULT_RAISE = Vec(r'D^\dagger') DEFAULT_LOWER = Vec('D') def __init__(self, ctx, all_orb_range=Range('A', 0, Symbol('na')), all_orb_dumms=PartHoleDrudge.DEFAULT_ORB_DUMMS, cartan=DEFAULT_CARTAN, raise_=DEFAULT_RAISE, lower=DEFAULT_LOWER, **kwargs): """Initialize the drudge object.""" # Initialize the base su2 problem. super().__init__(ctx, **kwargs) # Set the range and dummies. self.all_orb_range = all_orb_range self.all_orb_dumms = tuple(all_orb_dumms) self.set_dumms(all_orb_range, all_orb_dumms) self.set_name(*self.all_orb_dumms) self.add_resolver({i: (all_orb_range) for i in all_orb_dumms}) # Set the operator attributes self.cartan = cartan self.raise_ = raise_ self.lower = lower self.set_symm(self.raise_, Perm([1, 0], NEG), valence=2) self.set_symm(self.lower, Perm([1, 0], NEG), valence=2) # Set the indexed objects attributes # NOTE: sigma is not assigned any symmetry because # while sigma[p,q] = -sigma[q,p] for p != q, # sigma[p,p] = not defined, and in fact, sigma[p,p] = sigma[p,p] # --------------- self.eta = _eta self.sigma = _sigma # Make additional name definition for the operators. self.set_name( **{ cartan.label[0] + '_': cartan, raise_.label[0] + '_p': raise_, raise_.label[0] + '_dag': raise_, lower.label[0] + '_m': lower, lower.label[0] + '_': lower, 'eta': self.eta, 'sigma': self.sigma }) self.set_name(cartan, lower, Ddag=raise_) # Defining spec for passing to an external function - the Swapper spec = _AGPSpec(cartan=cartan, raise_=raise_, lower=lower, eta=self.eta, sigma=self.sigma) self._spec = spec # set the Swapper self._swapper = functools.partial(_swap_agp, spec=spec) # Definitions for translating to UGA self._uga_dr = UnitaryGroupDrudge(ctx, all_orb_range=self.all_orb_range, all_orb_dumms=self.all_orb_dumms) # Mapping from D to E _uga = self._uga_dr.uga gen_idx1, gen_idx2 = self.all_orb_dumms[:2] dm_pq_def = self.define( lower, gen_idx1, gen_idx2, (self.eta[gen_idx1] * _uga[gen_idx1, gen_idx2] - self.eta[gen_idx2] * _uga[gen_idx2, gen_idx1])) dp_pq_def = self.define( raise_, gen_idx1, gen_idx2, (self.eta[gen_idx1] * _uga[gen_idx2, gen_idx1] - self.eta[gen_idx2] * _uga[gen_idx1, gen_idx2])) np_def = self.define(cartan, gen_idx1, _uga[gen_idx1, gen_idx1]) self._agp2uga_defs = [dm_pq_def, dp_pq_def, np_def] @property def swapper(self) -> GenQuadDrudge.Swapper: """Swapper for the new AGP Algebra.""" return self._swapper def _isAGP(self, term): """Returns True if the vectors in the term are generators of the AGP algebra, otherwise False""" vecs = term.vecs for v in vecs: if v.base not in (self.cartan, self.raise_, self.lower): raise ValueError('Unexpected generator of the AGP Algebra', v) return [Term(sums=term.sums, amp=term.amp, vecs=term.vecs)] def get_vev(self, h_tsr: Tensor): """Function to evaluate the expectation value of a normal ordered tensor 'h_tsr' with respect to the projected BCS ground state h_tsr = tensor whose VEV is to be evaluated Ntot = total number of orbitals available Note that here we follow the notation that Np = Number of Fermions and not number of pairs """ ctan = self.cartan gam = IndexedBase(r'\gamma') def vev_of_term(term): """Return the VEV of a given term""" vecs = term.vecs t_amp = term.amp ind_list = [] if len(vecs) == 0: return term elif all(v.base == ctan for v in vecs): for i in vecs: if set(i.indices).issubset(set(ind_list)): t_amp = t_amp * 2 # NOTE: This is only true when evaluating expectation over AGP, # or any other seniority zero state else: ind_list.extend(list(i.indices)) else: return [] t_amp = t_amp * gam[ind_list] return [Term(sums=term.sums, amp=t_amp, vecs=())] return h_tsr.bind(vev_of_term) def asym_filter(self, terms, **kwargs): """Take the operators into normal order. Here, in addition to the common normal-ordering operation, we remove any term with a cartan operator followed by an lowering operator with the same index, and any term with a raising operator followed by a cartan operator with the same index. """ def _dppfilter(term): """ Function that filters out the D, Ddag terms with same index """ vecs = term.vecs for v in vecs: indcs = vecs.indices if indcs[0] == indcs[1]: return False return True noed = self.simplify(terms, **kwargs) noed = noed.filter(_dppfilter) noed = self.simplify(noed, **kwargs) return noed # NOTE: Initially, we assumed that only terms with same number of D and Ddag would contribute # But later, we realized that even D.D.Ddag can give some N term when normal ordered # This function and the comments are eventually meant to be deleted. def _transl2uga(self, tensor: Tensor): """Translate a tensor object in terms of the fermion operators. This is an internal utility. The resulting tensor has the internal '_uga_dr' drudge object as its owner. """ return Tensor(self, tensor.subst_all(self._agp2uga_defs).terms)
class UnitaryGroupDrudge(GenQuadDrudge): r"""Drudge to manipulate and work with Unitary Group Generators defined as: ..math:: E_{p,q} = c_{p,\uparrow}^\dagger c_{q,\uparrow} + \mathrm{h.c.} where the symbols :math: `p` and :math: `q` denote a general orbital index. This Unitary Group drudge comtains utilites to work with either the pairing Hamiltonian or any other system in the language of the unitary group gens. Without the presence of a reference, such as the Fermi sea or the pairing- ground state, there is no one way to define a canonical ordering for a string of :math: `E_{p,q}'\mathrm{s}`. Here we follow an lexicological or alphabetical ordering based on the first index followed by the second. For example, following is a representative of the correct canonical ordering: ..math:: E_{p,q} E_{p,r} E_{q,r} E_{r,p} """ DEFAULT_GEN = Vec('E') def __init__( self, ctx, all_orb_range=Range('A' ,0, Symbol('na')), all_orb_dumms=PartHoleDrudge.DEFAULT_ORB_DUMMS, energies=IndexedBase(r'\epsilon'), interact=IndexedBase('G'), uga=DEFAULT_GEN, **kwargs ): """Initialize the drudge object.""" # Initialize the base su2 problem. super().__init__(ctx, **kwargs) # Set the range and dummies. # self.add_resolver_for_dumms() self.all_orb_range = all_orb_range self.all_orb_dumms = tuple(all_orb_dumms) self.set_dumms(all_orb_range, all_orb_dumms) self.set_name(*self.all_orb_dumms) self.add_resolver({ i: (all_orb_range) for i in all_orb_dumms }) # Set the operator attributes self.uga = uga # Make additional name definition for the operators. self.set_name(**{ uga.label[0]+'_':uga, 'eps':energies, 'G':interact }) #uga generaters can also be called using just 'uga[p,q]' instead of 'E_[p,q]' self.set_name(uga) # Defining spec for passing to an external function - the Swapper spec = _UGSpec( uga=uga, ) self._spec = spec # Create an instance of the ProjectedBCS or AGP Drudge to map from E_pq # to D_dag, N, D so that normal ordering and vev evaluation can be done self.eta = _eta self.sigma = _sigma self.set_name(**{ 'eta':self.eta, 'sigma':self.sigma }) self._agp_dr = ProjectedBCSDrudge( ctx, all_orb_range=self.all_orb_range, all_orb_dumms=self.all_orb_dumms ) """Mapping E_pq to D_dag, N, D""" D_p = self._agp_dr.raise_ N_ = self._agp_dr.cartan D_m = self._agp_dr.lower self.D_p = D_p self.N_ = N_ self.D_m = D_m gen_idx1, gen_idx2 = self.all_orb_dumms[:2] epq_def = self.define( uga,gen_idx1,gen_idx2, self._agp_dr.sigma[gen_idx1,gen_idx2]*( self.eta[gen_idx1]*D_m[gen_idx1,gen_idx2] + \ self.eta[gen_idx2]*D_p[gen_idx1,gen_idx2] ) + \ KroneckerDelta(gen_idx1,gen_idx2)*N_[gen_idx1] ) #Tensor Definitions for uga2agp self._uga2agp_defs = [ epq_def ] """Mapping D_dag, N, D to E_pq""" dm_pq_def = self.define( D_m, gen_idx1, gen_idx2, (self.eta[gen_idx1]*uga[gen_idx1,gen_idx2] - self.eta[gen_idx2]*uga[gen_idx2,gen_idx1]) ) dp_pq_def = self.define( D_p, gen_idx1, gen_idx2, (self.eta[gen_idx1]*uga[gen_idx2,gen_idx1] - self.eta[gen_idx2]*uga[gen_idx1,gen_idx2]) ) np_def = self.define( N_, gen_idx1, uga[gen_idx1,gen_idx1] ) #Tensor definitions for agp2uga self._agp2uga_defs = [ dm_pq_def, dp_pq_def, np_def ] # set the Swapper self._swapper = functools.partial(_swap_ug, spec=spec) @property def swapper(self) -> GenQuadDrudge.Swapper: """Swapper for the new AGP Algebra.""" return self._swapper def _isUGA(self,term): """Function returns True if the vectors are uga generators, otherwise False""" vecs = term.vecs for v in vecs: if v.base!=self.uga: raise ValueError('Unexpected generator of the Unitary Group',v) return [Term(sums=term.sums, amp=term.amp, vecs=term.vecs)] def _transl2agp(self, tensor: Tensor): """Translate a tensor object in terms of the fermion operators. This is an internal utility. The resulting tensor has the internal AGP drudge object as its owner. """ chk = tensor.bind(self._isUGA) # for t in trms: # if ~_isUGA(t): # raise ValueError('Unexpected generator of the Unitary Group',vec) return Tensor( self._agp_dr, tensor.subst_all(self._uga2agp_defs).terms ) def _transl2uga(self, tensor: Tensor): """Translate a tensor object in terms of the fermion operators. This is an internal utility. The resulting tensor has the internal AGP drudge object as its owner. """ chk = tensor.bind(self._agp_dr._isAGP) # for t in trms: # if ~self._agp_dr._isAGP(t): # raise ValueError('Unexpected generator of the AGP Algebra',vec) return Tensor( self, tensor.subst_all(self._agp2uga_defs).terms ) def get_vev_agp(self,h_tsr: Tensor,): """Function to evaluate the expectation value of a normal ordered tensor 'h_tsr' with respect to the projected BCS or the AGP state. This can be done after translating the the unitary group terms into the AGP basis algebra. h_tsr = tensor whose VEV is to be evaluated """ transled = self._transl2agp(h_tsr) transled = self._agp_dr.agp_simplify(transled,final_step=True) res = self._agp_dr.get_vev(transled) return Tensor(self, res.terms)
def __init__(self, ctx, op_label='c', all_orb_range=Range('A', 0, Symbol(r'M_orb')), all_orb_dumms=DEFAULT_ORB_DUMMS, spin_range=Range(r'\uparrow \downarrow', Integer(0), Integer(2)), spin_dumms=tuple( Symbol('sigma{}'.format(i)) for i in range(50)), bcs_N=PAIRING_CARTAN, bcs_Pdag=PAIRING_RAISE, bcs_P=PAIRING_LOWER, bcs_Nup=NUMBER_UP, bcs_Ndn=NUMBER_DN, su2_Jz=SPIN_CARTAN, su2_Jp=SPIN_RAISE, su2_Jm=SPIN_LOWER, bcs_root=Integer(2), bcs_norm=Integer(1), bcs_shift=Integer(-1), su2_root=Integer(1), su2_norm=Integer(2), su2_shift=Integer(0), **kwargs): # initialize super super().__init__(ctx, **kwargs) # Initialize SpinOneHalfGenDrudge with the described orbital ranges orb = ((all_orb_range, all_orb_dumms), (spin_range, spin_dumms)) fermi_dr = SpinOneHalfGenDrudge(ctx, orb=orb, op_label=op_label, **kwargs) self.fermi_dr = fermi_dr cr = fermi_dr.cr an = fermi_dr.an self.cr = cr self.an = an # set the dummies self.set_dumms(all_orb_range, all_orb_dumms) self.set_dumms(spin_range, spin_dumms) # Add resolver for all orbital dummies self.add_resolver({i: (all_orb_range) for i in all_orb_dumms}) # Define and add the spin range and dummy indices to the drudge module # XXX: Note that the spin dummies are useless in this module and must # be removed eventually self.add_resolver({UP: spin_range, DOWN: spin_range}) self.spin_range = spin_range self.spin_dumms = self.dumms.value[spin_range] # Pairing operators bcs_dr = ReducedBCSDrudge( ctx, all_orb_range=all_orb_range, all_orb_dumms=all_orb_dumms, cartan=bcs_N, raise_=bcs_Pdag, lower=bcs_P, ) self.bcs_dr = bcs_dr N_ = bcs_dr.cartan Pdag_ = bcs_dr.raise_ P_ = bcs_dr.lower self.eval_agp = bcs_dr.eval_agp # SU2 operators su2_dr = SU2LatticeDrudge( ctx, cartan=su2_Jz, raise_=su2_Jp, lower=su2_Jm, ) self.su2_dr = su2_dr Sz_ = su2_dr.cartan Sp_ = su2_dr.raise_ Sm_ = su2_dr.lower # Assign these operators to the self self.all_orb_range = all_orb_range self.all_orb_dumms = tuple(all_orb_dumms) self.Pdag = Pdag_ self.N = N_ self.N_up = bcs_Nup self.N_dn = bcs_Ndn self.P = P_ self.S_z = Sz_ self.S_p = Sp_ self.S_m = Sm_ # Define the unitary group operators p = Symbol('p') q = Symbol('q') self.sigma = self.dumms.value[spin_range][0] self.e_ = TensorDef( Vec('E'), (p, q), self.sum(self.cr[p, UP] * self.an[q, UP] + self.cr[p, DOWN] * self.an[q, DOWN])) self.set_name(e_=self.e_) # set of unique dummies: # The idea is to declare a set of (free) dummy indices to be unique, # i.e. they have unique, different values by construction. # This is a feature of this module / class but has potential to be a # part of the drudge system. # The way I want to implement this is as follows: # 1. User specifies a tuple/list of indices to be set unique # 2. Then we construct a dictionary of all proosible kronecker deltas # which will be zero # 3. in simplify / get_seniority_zero, we use this substitution. # Dictionary of substitutions self.unique_del_lists = [] # XXX: To be doen / can be done # Define the Dpq, Ddag_pq operators # Set the names self.set_name(*self.all_orb_dumms) self.set_name( **{ op_label + '_': an, op_label + '_dag': cr, op_label + 'dag_': cr, Sz_.label[0] + '_z': Sz_, Sp_.label[0] + '_p': Sp_, Sm_.label[0] + '_m': Sm_, N_.label[0]: N_, N_.label[0] + '_': N_, Pdag_.label[0] + 'dag': Pdag_, Pdag_.label[0] + '_dag': Pdag_, P_.label[0]: P_, P_.label[0] + '_': P_, }) # Define spec for all the class methods needed for # extracting the su2 operators spec = _AGPFSpec(c_=self.an, c_dag=self.cr, N=self.N, Nup=self.N_up, Ndn=self.N_dn, P=self.P, Pdag=self.Pdag, agproot=bcs_root, agpnorm=bcs_norm, agpshift=bcs_shift, S_p=self.S_p, S_z=self.S_z, S_m=self.S_m, su2root=su2_root, su2norm=su2_norm, su2shift=su2_shift, unique_ind=self.unique_del_lists) self._spec = spec # Swapper dummy function for commutation rules self._swapper = functools.partial(_swap_agpf, spec=spec) # Extracting SU2 dummy function self._extract_su2 = functools.partial(_get_su2_vecs, spec=spec)
class PartHoleAGPFermi(AGPFermi): """ Particle-hole variation of the AGP Fermi module. """ PAIRING_CARTAN = Vec(r'N') PAIRING_RAISE = Vec(r'P^\dagger') PAIRING_LOWER = Vec(r'P') NUMBER_UP = Vec(r'n^{\uparrow}') NUMBER_DN = Vec(r'n^{\downarrow}') SPIN_CARTAN = Vec(r'J^z') SPIN_RAISE = Vec(r'J^+') SPIN_LOWER = Vec(r'J^-') DEFAULT_ORB_DUMMS = tuple(Symbol(i) for i in 'pqrs') + tuple( Symbol('p{}'.format(i)) for i in range(50)) DEFAULT_PART_DUMMS = tuple(Symbol(i) for i in 'ijkl') + tuple( Symbol('i{}'.format(i)) for i in range(50)) DEFAULT_HOLE_DUMMS = tuple(Symbol(i) for i in 'abcd') + tuple( Symbol('a{}'.format(i)) for i in range(50)) def __init__(self, ctx, op_label='c', all_orb_range=Range('A', 0, Symbol(r'M')), all_orb_dumms=DEFAULT_ORB_DUMMS, part_range=Range('O', 0, Symbol('no')), part_dumms=DEFAULT_PART_DUMMS, hole_range=Range('V', 0, Symbol('nv')), hole_dumms=DEFAULT_HOLE_DUMMS, spin_range=Range(r'\uparrow \downarrow', Integer(0), Integer(2)), spin_dumms=tuple( Symbol('sigma{}'.format(i)) for i in range(50)), bcs_N=PAIRING_CARTAN, bcs_Pdag=PAIRING_RAISE, bcs_P=PAIRING_LOWER, bcs_Nup=NUMBER_UP, bcs_Ndn=NUMBER_DN, su2_Jz=SPIN_CARTAN, su2_Jp=SPIN_RAISE, su2_Jm=SPIN_LOWER, bcs_root=Integer(2), bcs_norm=Integer(1), bcs_shift=Integer(-1), su2_root=Integer(1), su2_norm=Integer(2), su2_shift=Integer(0), **kwargs): # Initialize super super().__init__(ctx, op_label=op_label, all_orb_range=all_orb_range, all_orb_dumms=all_orb_dumms, spin_range=spin_range, spin_dumms=spin_dumms, bcs_N=bcs_N, bcs_Pdag=bcs_Pdag, bcs_P=bcs_P, bcs_Nup=bcs_Nup, bcs_Ndn=bcs_Ndn, su2_Jz=su2_Jz, su2_Jp=su2_Jp, su2_Jm=su2_Jm, bcs_root=bcs_root, bcs_norm=bcs_norm, bcs_shift=bcs_shift, su2_root=su2_root, su2_norm=su2_norm, su2_shift=su2_shift, **kwargs) # Add the part-hole indices and ranges to the class varables self.part_dumms = tuple(part_dumms) self.hole_dumms = tuple(hole_dumms) self.part_range = part_range self.hole_range = hole_range # Add the indices to the name space self.set_name(*self.part_dumms) # Link the dummy indices to their respective ranges self.set_dumms(self.part_range, self.part_dumms) self.set_dumms(self.hole_range, self.hole_dumms) # Clean up the default resolver self._resolvers.var.clear() # Add the resolver self.add_resolver({i: (self.part_range) for i in self.part_dumms}) self.add_resolver({i: (self.hole_range) for i in self.hole_dumms}) self.add_resolver({ i: (self.part_range, self.hole_range) for i in self.all_orb_dumms }) self.add_resolver( {i: (self.all_orb_range) for i in self.all_orb_dumms}) def purge_unique_indices(self): """ Reset the unique_del_substs dictionary to empty """ self.unique_del_lists.clear() # Reset the dummy values self.set_dumms(self.all_orb_range, self.all_orb_dumms) self.set_dumms(self.part_range, self.part_dumms) self.set_dumms(self.hole_range, self.hole_dumms) return
class AGPFermi(GenQuadDrudge): r""" Drudge module that deals primarily with fermions, but provides a functionality to evaluate expectation values over AGP wavefunction. In the process, it is convenient to have the following set of operators in the drudge module: :math: `P^\dagger_p = c_{p,\uparrow}^\dagger c_{p,\downarrow}^\dagger` :math: `N_p = n_{p,\uparrow} + n_{p,\downarrow}` :math: `P_p = c_{p,\downarrow} c_{p,\uparrow}` :math: `S^+_p = c_{p,\uparrow}^\dagger c_{p,\downarrow}` :math: `S^z_p = \frac{1}{2}(n_{p,\uparrow} - n_{p,\downarrow})` :math: `S^-_p = c_{p,\downarrow}^\dagger c_{p,\uparrow}` along with the spin-one-half fermion creation//annihilation operators """ PAIRING_CARTAN = Vec(r'N') PAIRING_RAISE = Vec(r'P^\dagger') PAIRING_LOWER = Vec(r'P') NUMBER_UP = Vec(r'n^{\uparrow}') NUMBER_DN = Vec(r'n^{\downarrow}') SPIN_CARTAN = Vec(r'J^z') SPIN_RAISE = Vec(r'J^+') SPIN_LOWER = Vec(r'J^-') DEFAULT_ORB_DUMMS = tuple(Symbol(i) for i in 'pqrs') + tuple( Symbol('p{}'.format(i)) for i in range(50)) def __init__(self, ctx, op_label='c', all_orb_range=Range('A', 0, Symbol(r'M_orb')), all_orb_dumms=DEFAULT_ORB_DUMMS, spin_range=Range(r'\uparrow \downarrow', Integer(0), Integer(2)), spin_dumms=tuple( Symbol('sigma{}'.format(i)) for i in range(50)), bcs_N=PAIRING_CARTAN, bcs_Pdag=PAIRING_RAISE, bcs_P=PAIRING_LOWER, bcs_Nup=NUMBER_UP, bcs_Ndn=NUMBER_DN, su2_Jz=SPIN_CARTAN, su2_Jp=SPIN_RAISE, su2_Jm=SPIN_LOWER, bcs_root=Integer(2), bcs_norm=Integer(1), bcs_shift=Integer(-1), su2_root=Integer(1), su2_norm=Integer(2), su2_shift=Integer(0), **kwargs): # initialize super super().__init__(ctx, **kwargs) # Initialize SpinOneHalfGenDrudge with the described orbital ranges orb = ((all_orb_range, all_orb_dumms), (spin_range, spin_dumms)) fermi_dr = SpinOneHalfGenDrudge(ctx, orb=orb, op_label=op_label, **kwargs) self.fermi_dr = fermi_dr cr = fermi_dr.cr an = fermi_dr.an self.cr = cr self.an = an # set the dummies self.set_dumms(all_orb_range, all_orb_dumms) self.set_dumms(spin_range, spin_dumms) # Add resolver for all orbital dummies self.add_resolver({i: (all_orb_range) for i in all_orb_dumms}) # Define and add the spin range and dummy indices to the drudge module # XXX: Note that the spin dummies are useless in this module and must # be removed eventually self.add_resolver({UP: spin_range, DOWN: spin_range}) self.spin_range = spin_range self.spin_dumms = self.dumms.value[spin_range] # Pairing operators bcs_dr = ReducedBCSDrudge( ctx, all_orb_range=all_orb_range, all_orb_dumms=all_orb_dumms, cartan=bcs_N, raise_=bcs_Pdag, lower=bcs_P, ) self.bcs_dr = bcs_dr N_ = bcs_dr.cartan Pdag_ = bcs_dr.raise_ P_ = bcs_dr.lower self.eval_agp = bcs_dr.eval_agp # SU2 operators su2_dr = SU2LatticeDrudge( ctx, cartan=su2_Jz, raise_=su2_Jp, lower=su2_Jm, ) self.su2_dr = su2_dr Sz_ = su2_dr.cartan Sp_ = su2_dr.raise_ Sm_ = su2_dr.lower # Assign these operators to the self self.all_orb_range = all_orb_range self.all_orb_dumms = tuple(all_orb_dumms) self.Pdag = Pdag_ self.N = N_ self.N_up = bcs_Nup self.N_dn = bcs_Ndn self.P = P_ self.S_z = Sz_ self.S_p = Sp_ self.S_m = Sm_ # Define the unitary group operators p = Symbol('p') q = Symbol('q') self.sigma = self.dumms.value[spin_range][0] self.e_ = TensorDef( Vec('E'), (p, q), self.sum(self.cr[p, UP] * self.an[q, UP] + self.cr[p, DOWN] * self.an[q, DOWN])) self.set_name(e_=self.e_) # set of unique dummies: # The idea is to declare a set of (free) dummy indices to be unique, # i.e. they have unique, different values by construction. # This is a feature of this module / class but has potential to be a # part of the drudge system. # The way I want to implement this is as follows: # 1. User specifies a tuple/list of indices to be set unique # 2. Then we construct a dictionary of all proosible kronecker deltas # which will be zero # 3. in simplify / get_seniority_zero, we use this substitution. # Dictionary of substitutions self.unique_del_lists = [] # XXX: To be doen / can be done # Define the Dpq, Ddag_pq operators # Set the names self.set_name(*self.all_orb_dumms) self.set_name( **{ op_label + '_': an, op_label + '_dag': cr, op_label + 'dag_': cr, Sz_.label[0] + '_z': Sz_, Sp_.label[0] + '_p': Sp_, Sm_.label[0] + '_m': Sm_, N_.label[0]: N_, N_.label[0] + '_': N_, Pdag_.label[0] + 'dag': Pdag_, Pdag_.label[0] + '_dag': Pdag_, P_.label[0]: P_, P_.label[0] + '_': P_, }) # Define spec for all the class methods needed for # extracting the su2 operators spec = _AGPFSpec(c_=self.an, c_dag=self.cr, N=self.N, Nup=self.N_up, Ndn=self.N_dn, P=self.P, Pdag=self.Pdag, agproot=bcs_root, agpnorm=bcs_norm, agpshift=bcs_shift, S_p=self.S_p, S_z=self.S_z, S_m=self.S_m, su2root=su2_root, su2norm=su2_norm, su2shift=su2_shift, unique_ind=self.unique_del_lists) self._spec = spec # Swapper dummy function for commutation rules self._swapper = functools.partial(_swap_agpf, spec=spec) # Extracting SU2 dummy function self._extract_su2 = functools.partial(_get_su2_vecs, spec=spec) # Do not use `\otimes' in latex expressions for the operators. _latex_vec_mul = ' ' @property def swapper(self) -> GenQuadDrudge.Swapper: """The swapper for the AGPF algebra -- invoked only when at least one of the two vectors is SU2 or BCS generator """ return self._swapper def _latex_vec(self, vec): """Get the LaTeX form of operators. This needs over-writing because the fermionic expressions encode creation and annihilation as an index, while the SU2 operators have daggers or + defined in the symbol definition. """ if ((vec.base == self.cr.base) or (vec.base == self.an.base)): return self.fermi_dr._latex_vec(vec) else: return super()._latex_vec(vec) def normal_order(self, terms, **kwargs): """Normal ordering sequence for algebra """ noed = super().normal_order(terms, **kwargs) noed = noed.filter(_nonzero_by_nilp) noed = noed.filter(_nonzero_by_cartan) noed = noed.flatMap( functools.partial(_canonicalize_indices, spec=self._spec)) return noed def unique_indices(self, indlist): """ Function that takes a list / tuple of indices, which would be unique among themselves, and then update the dictionary of substitutions """ # Extract the unique set of indices unq_ind = set(indlist) # Update the list self.unique_del_lists.append(unq_ind) # Remove the indices from the dummy indices for ind in indlist: orb_range = try_resolve_range(ind, {}, self.resolvers.value) if orb_range is None: continue if not isinstance(orb_range, collections.Iterable): orb_range = (orb_range, ) if all([ind not in self._dumms.var[rg] for rg in orb_range]): continue else: for rg in tuple(orb_range): self._dumms.var[rg].remove(ind) return def purge_unique_indices(self): """ Reset the unique_del_substs dictionary to empty """ self.unique_del_lists.clear() # Reset the dummy values self.set_dumms(self.all_orb_range, self.all_orb_dumms) return def canon_indices(self, expression: Tensor): """Bind function to canonicalize free / external indices. """ return expression.bind( functools.partial(_canonicalize_indices, spec=self._spec)) def extract_su2(self, expression: Tensor): """Bind function to map fermion strings to obvious SU2 (Pairing as well as spin-flip operators) """ return expression.bind(self._extract_su2) def spin_flip_to_fermi(self, tnsr: Tensor): """Substitute all the Spin flip operators with their respective fermionic strings""" gen_idx = self.all_orb_dumms[0] sp_def = self.define( SPIN_RAISE, gen_idx, cr[gen_idx, SpinOneHalf.UP] * an[gen_idx, SpinOneHalf.DOWN]) sm_def = self.define( SPIN_LOWER, gen_idx, cr[gen_idx, SpinOneHalf.DOWN] * an[gen_idx, SpinOneHalf.UP]) spin_defs = [sp_def, sm_def] return Tensor(self, tnsr.subst_all(spin_defs).terms) def get_seniority_zero(self, tnsr: Tensor): """Get the seniority zero component of the given operator expression. """ # 1, simplify -- includes canonicalization expr1 = self.simplify(self.simplify(tnsr)) # 2, throw away terms with odd number of fermion terms expr = self.simplify( expr1.filter(functools.partial(_even_fermi_filter, spec=self._spec))) # 3, extract su2 terms expr1 = self.simplify(self.simplify(self.extract_su2(expr))) # 4, get partitions expr = self.simplify( self.simplify( expr1.bind( functools.partial(_get_fermi_partitions, spec=self._spec)))) # 5, Follow steps 0, 1 and 3, a few times expr2 = self.simplify(expr) for i in range(10): expr1 = expr2 * Integer(1) expr2 = self.simplify(self.extract_su2(expr1)) if self.simplify(expr - expr2) == 0: break # 6, Anything remaining with cdag, c --> we drop expr = self.simplify( expr2.filter(functools.partial(_no_fermi_filter, spec=self._spec))) # 7. Substitute N_up and N_dn with N/2 p = self.all_orb_dumms[0] expr2 = self.simplify(expr.subst(self.N_up[p], self.N[p] / Integer(2))) expr = self.simplify(expr2.subst(self.N_dn[p], self.N[p] / Integer(2))) return expr