Source code for tdg.observable.contact
import torch
import functorch
import tdg
from tdg.observable import observable, derived
import logging
logger = logging.getLogger(__name__)
[docs]@observable
def doubleOccupancy(ensemble):
r'''
The double occupancy of a site is :math:`n_{\uparrow} n_{\downarrow}` on that site (or the equivalent along any direction; not just the :math:`z`-axis),
which as an operator is equal to :math:`\frac{1}{2}(n^2-n)`, where :math:`n` is the total number operator :func:`~.n`.
Configuration slowest, then space.
'''
# The double occupancy on site a is given by the expectation value of
#
# 2 * doubleOccupancy =
# sum_{st}
# + [ (1+UU)^-1 U ]^{ss}_{aa} [ (1+UU)^-1 U ]^{tt}_{aa}
# - [ (1+UU)^-1 U ]^{st}_{aa} [ (1+UU)^-1 U ]^{ts}_{aa}
#
# where the lower indices are spatial and the upper indices are spin.
#
# The first term is the square of the contraction of the fermionic n operator.
first = ensemble.n**2
# The second term is a bit more annoying;
second = torch.einsum('caast,caats->ca',
ensemble.G,
ensemble.G,
)
# To get the double occupancy itensemble, take half the difference.
return 0.5*(first - second)
[docs]@observable
def DoubleOccupancy(ensemble):
r'''
The spatial sum of the :func:`~.doubleOccupancy`; one per configurtion.
'''
return ensemble.doubleOccupancy.sum(axis=1)
[docs]@observable
def Contact(ensemble):
r'''
The `contact`, :math:`C L^2 = 2\pi\frac{d\tilde{H}}{d\log a}`.
This observable is much less noisy than :func:`~.Contact_bosonic`.
In the case where the only :class:`~.LegoSphere` in the interaction is the on-site interaction, the ``fermionic`` method is accelerated by computing the :func:`~.DoubleOccupancy`.
.. note::
The contact :math:`C` is extensive, and :math:`L^2` is extensive, so this observable is *doubly extensive*!
You may want to compute something intensive, such as the derived contact density :func:`~.contact_by_kF4` :cite:`Beane:2022wcn`,
:math:`c/k_F^4 = \texttt{Contact} / (2\pi \texttt{N})^2`, where this observable is divided by two powers of an extensive observable!
'''
# For the on-site interaction we have a shortcut, which is a good acceleration because the LegoSphere is a delta function
# and thus we can do contractions that don't cost volume^2 but simply volume.
if len(ensemble.Action.Tuning.radii) == 1 and all(r==0 for r in ensemble.Action.Tuning.radii[0]):
# This shortcut was implemented and tested AFTER the remaining portion of this routine was,
# so that what is below was checked to be correct for the on-site interaction.
logger.debug('Calculating the fermionic Contact via the double occupancy.')
return 2*torch.pi * ensemble.Action.Spacetime.Lattice.sites * ensemble.Action.Tuning.dC_dloga[0] * ensemble.DoubleOccupancy
logger.debug('Using the general form of the fermionic Contact.')
L = ensemble.Action.Spacetime.Lattice
S = torch.stack(tuple(tdg.LegoSphere(r, c).spatial(L) for c,r in zip(ensemble.Action.Tuning.dC_dloga, ensemble.Action.Tuning.radii) )).sum(axis=0).to(ensemble.G.dtype)
# The contractions looks just like the doubleOccupancy contractions, but with two spatial indices tied together just like the spins are,
# and then summed with the derivative LegoSphere stencil.
first = torch.einsum(
'ab,caass,cbbtt->c',
S,
ensemble.G,
ensemble.G,
)
second = torch.einsum(
'ab,cbats,cabst->c',
S,
ensemble.G,
ensemble.G,
)
return torch.pi * L.sites * (first-second)
[docs]@observable
def Contact_bosonic(ensemble):
r'''
The same expectation value as :func:`~.Contact` using automatic differentiation and the chain rule, evaluating :math:`dH/dC_R` and the ensemble's :class:`Tuning` to compute :math:`dC_R / d\log a`.
Just as :func:`~.n_bosonic` is extremely noisy in comparison to :func:`~.n`, so too is this noisy compared to :func:`~.Contact`.
.. todo::
In fact, it is SO NOISY that it has not been checked for correctness by comparing with an exact Trotterized two-body calcuation.
'''
# This is a "straightforward" application of differentiating Z with respect to log a.
# We implement it using forward-mode automatic differentiation, promoting the LegoSphere
# coefficients to dual numbers whose derivatives are given by the derivative of the tuning.
with torch.autograd.forward_ad.dual_level():
C0_dual = torch.autograd.forward_ad.make_dual(ensemble.Action.Tuning.C, ensemble.Action.Tuning.dC_dloga)
V_dual = tdg.Potential(*[c * tdg.LegoSphere(r) for c,r in zip(C0_dual, ensemble.Action.Tuning.radii)])
S_dual = tdg.Action(ensemble.Action.Spacetime, V_dual, ensemble.Action.beta, ensemble.Action.mu, ensemble.Action.h, ensemble.Action.fermion)
s_dual = functorch.vmap(S_dual)(ensemble.configurations)
return (2*torch.pi / ensemble.Action.beta)* torch.autograd.forward_ad.unpack_dual(s_dual).tangent
[docs]@derived
def contact_by_kF4(ensemble):
r'''
The contact density normalized by :ref:`the Fermi momentum <fermi>`.
.. math::
\frac{c}{k_F^4} = \frac{C}{k_F^4 L^2} = \frac{CL^2}{(k_F L)^4} = \frac{\texttt{Contact}}{(2\pi \texttt{N})^2}
'''
return ensemble.Contact / (2*torch.pi * ensemble.N)**2