Exemple #1
0
    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_)
Exemple #2
0
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)
Exemple #3
0
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))
Exemple #4
0
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)]
Exemple #5
0
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)
Exemple #6
0
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)
Exemple #7
0
    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)
Exemple #8
0
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
Exemple #9
0
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