The Grand Canonical Ensemble
Our general approach to the quantum many-body problem is to discretize an approximation of the partition function \(Z = \text{tr}[ \exp(-\beta(H - \mu N - h \cdot S))]\), a grand canonical ensemble, via a Suzuki-Trotter decomposition or simply a Trotterization.
For a discretization of space into \(N_x\) sites on a side we regroup all the dimensionful parameters into
With an on-site interaction, holding \(\tilde{a}=2\pi a/L\) fixed yields a dimensionless Hamiltonian that depends only on \(N_x\) and the scattering through the AnalyticTuning.
This convention makes it simple to understand the Hamiltonian limit (\(N_t\rightarrow\infty\) holding \(\tilde{\beta}\) fixed) in which the Trotterization disappears, and the spatial continuum limit (\(N_x\rightarrow\infty\) holding the physical parameters, and therefore, \(\tilde{a}\), \(\tilde{\beta}\), \(\tilde{\mu}\), \(\tilde{\vec{h}}\) fixed).
The Fermionic Discretization
Trotterizing the thermal circle yields a euclidean time, which can be combined with the Lattice into a SpaceTime.
tdg.Spacetime
- class tdg.spacetime.Spacetime(nt, lattice)[source]
Bases:
H5able- nt
The number of timeslices.
- Lattice
The spatial lattice.
- t
The coordinates in the time direction.
- sites
The total spacetime (integer) volume.
- dims
The spacetime dimensions.
- coordinates
A tensor of size
[sites, len(dims)]. Each row contains a set of coordinates. Time is slowest and each timeslice matches Lattice.coordinates in order.
tdg.FermionMatrix
Fermions are tricky to handle numerically, as they are anticommuting.
Instead we introduce an auxiliary field to linearize the action in fermion operators, and then integrate the fermions out, yielding a FermionMatrix.
Including the determinant of the FermionMatrix in the sampling includes the fermions, even though the auxiliary fields are bosonic.
Note
The arguments are dimensionless, as above! Henceforth with only write the tilde if clarity requires.
- class tdg.FermionMatrix(spacetime, beta, mu=tensor(0.), h=tensor([0., 0., 0.]))[source]
Bases:
H5ableThe fermion matrix \(\mathbb{d}\) which corresponds to
\[\begin{split}\begin{align} \mathbb{d} &= \begin{pmatrix} -\mathbb{1} & 0 & 0 & 0 & \cdots & 0 & -\mathbb{B}^{-1}\mathbb{F}_{N_t}( h ) \\ +\mathbb{B}^{-1}\mathbb{F}_{1}( h ) & -\mathbb{1} & 0 & 0 & \cdots & 0 & 0 \\ 0 & +\mathbb{B}^{-1}\mathbb{F}_{2}( h ) & -\mathbb{1} & 0 & \cdots i & 0 & 0 \\ 0 & 0 & +\mathbb{B}^{-1}\mathbb{F}_{3}( h ) & -\mathbb{1} & \cdots & 0 & 0 \\ \vdots & \vdots & \vdots & \vdots & \ddots & \vdots & \vdots \\ 0 & 0 & 0 & 0 & \cdots & -\mathbb{1} & 0 \\ 0 & 0 & 0 & 0 & \cdots & +\mathbb{B}^{-1}\mathbb{F}_{N_t-1}( h ) & -\mathbb{1} \end{pmatrix} \end{align}\end{split}\]where the double-struck quantities
\[\begin{align} \mathbb{F}_t &= \left(e^{A_{t}+\Delta t\mu + \frac{1}{2}\Delta t\vec{h}\cdot\vec{\sigma}}\right) & \mathbb{B} &= (B = \exp \Delta t \kappa) \otimes \mathbb{1} \end{align}\]are space ⊗ spin in size.
In the language of Ref. [8] this is called the \(\alpha=0\) exponential discretization because the hopping matrix \(\kappa\) which encodes the kinetic piece of the Hamiltonian appears inside an exponential, and we’re in the attractive channel, so the auxiliary field appears without an explicit \(i\) in \(\mathbb{F}\).
- Parameters
spacetime (
Spacetime) –beta (torch.tensor scalar) – The inverse temperature
mu (torch.tensor scalar) – The chemical potential
h (torch.tensor triplet) – The external field
Todo
We should add a method to do fermion matrix-vector multiply, perhaps as
__call__. The programming puzzle is that when \(\vec{h}=0\) or \(\vec{h}\parallel \hat{z}\) we can split \(\mathbb{d}\) into two independent pieces that each operate on spacetime vectors, while for generic \(\vec{h}\) we need one matrix that operates on a spacetime ⊗ spin vector.- U(A)[source]
- \[U(A) = B^{-1} F_{N_t} B^{-1} F_{N_t-1} \cdots B^{-1} F_{2} B^{-1} F_{1}\]
where \(F_t = \exp A_t\) is a digonal matrix and does not include the chemical potential \(\mu\).
\(B\), \(F\) and \(U\) are space × space, with rather than (space ⊗ spin) × (space ⊗ spin).
- Parameters
A (torch.tensor) – An auxiliary-field configuration
- Returns
\(U(A)\)
- Return type
torch.tensor
- UU_tensor(A)[source]
- \[\mathbb{U} = \mathbb{B}^{-1} \mathbb{F}_{N_t} \cdots \mathbb{B}^{-1} \mathbb{F}_{2} \mathbb{B}^{-1} \mathbb{F}_{1}\]
where \(\mathbb{F}\) contains the chemical potential and external field terms. However, since those terms are space-independent we can commute them with \(\mathbb{B}\) so that
\[\begin{split}\mathbb{U} = \exp\left(\frac{1}{2}\beta h\cdot\sigma\right) \begin{pmatrix} zU & 0 \\ 0 & zU\end{pmatrix}\end{split}\]where \(U\) is given by
U()and is (space × space).- Parameters
A (torch.tensor) – An auxiliary-field configuration.
- Returns
\(\mathbb{U}\) not as a matrix but with shape [space, space, spin, spin]. This makes the spin indices broadcastable, which is often useful for future manipulation.
- Return type
torch.tensor
- UU(A)[source]
- Parameters
A (torch.tensor) – An auxiliary-field configuration
- Returns
The same as
UU_tensor()but as a true matrix of shape (space ⊗ spin) × (space ⊗ spin), with spin fastest.- Return type
torch.tensor
Todo
Leveraging this makes some code much slower. For example,
>>> UU = torch.stack( ... tuple(Action.FermionMatrix.UU_tensor(cfg) for cfg in configurations) ... ).permute(0,1,3,2,4).reshape(-1, ... 2*Action.FermionMatrix.Spacetime.Lattice.sites, ... 2*Action.FermionMatrix.Spacetime.Lattice.sites)
is much faster to subsequently manipulate than
>>> UU = torch.stack( ... tuple(Action.FermionMatrix.UU(cfg) for cfg in configuration))
which is something that ought to be understood / addressed. Perhaps it is autograd- or computational-graph-related?
Is it possible to construct this code to distribute over a whole ensemble of auxiliary fields? This perhaps should inform the way we code other observables.
- logdet(A)[source]
The log determinant of the fermion matrix appears in the action.
- Parameters
A (torch.tensor) – An auxiliary-field configuration.
- Returns
\(\log \det \mathbb{d}(A)\)
- Return type
torch.tensor
Note
On implementation:
One may show that \(\det \mathbb{d} = \det \mathbb{1} + \mathbb{U}\) where all of the space-independent pieces of \(\mathbb{F}\) can be pulled through so that
\[\begin{split}\mathbb{U} = \exp\left( \frac{1}{2} \beta \vec{h}\cdot\vec{\sigma} \right) \begin{pmatrix} zU & 0 \\ 0 & zU \end{pmatrix}\end{split}\]Furthermore, an orthogonal transformation can diagonalize the spin-dependent factor and leave the spin-independent factor alone. The determinant of \(\mathbb{d}\) can then be expressed as
\[\det \mathbb{d} = \det\left( 1 + e^{+\frac{1}{2}\beta \sqrt{h^2}} z U\right) \det\left( 1 + e^{-\frac{1}{2}\beta \sqrt{h^2}} z U\right)\]where the fugacity \(z = \exp(\beta \mu)\) and the \(\sqrt{h^2}\) helps correctly handle complex \(h\).
tdg.Action
The Action is combination of the fermion determinant and the gaussian piece of the auxiliary field.
- class tdg.action.Action(spacetime, potential, beta, mu=tensor(0.), h=tensor([0., 0., 0.]), fermion=<class 'tdg.fermionMatrix.FermionMatrix'>)[source]
Bases:
H5able- Parameters
spacetime (tdg.Spacetime) – the euclidean spacetime
potential (tdg.Potential) – the interaction with which the fermions interact; must be negative-definite for the Hubbard-Stratanovich transformation used to make sense.
beta (torch.tensor scalar) – the inverse temperature
mu (torch.tensor scalar) – the chemical potential
h (torch.tensor) – the spin chemical potential; a triplet
fermion (tdg.FermionMatrix) – the fermion matrix corresponding to the desired discretization
An auxiliary-field spacetime action for fermions interacting via a potential with inverse temperature \(\beta\), chemical potential \(\mu\), spin chemical potential \(\vec{h}\).
The action is used for importance sampling, since the partition function Z is
\[\begin{align} Z &= \int DA\; e^{-S} & S &= \frac{1}{2} \sum_t A_t (-\Delta t V)^{-1} A - \log \det \mathbb{d} + \frac{N_t}{2} \text{tr} \log \left(-2\pi \Delta t V\right) \end{align}\]where \(\mathbb{d}\) is the fermion matrix, the time step \(dt = \beta/N_t\), and everything should be understood to be dimensionless.
The last term is a constant normalization to make \(Z\) truly equal to the Trotterization of \(\text{tr} e^{-\beta H}\).
- beta
The dimensionless inverse temperature \(\tilde{\beta} = \beta/ML^2\).
- dt
The temporal discretization \(dt = \texttt{beta} / N_t\).
- mu
The chemical potential \(\tilde{\mu} = \mu ML^2\).
- h
The spin chemical potential \(\tilde{\vec{h}} = \vec{h} ML^2\).
- FermionMatrix
The fermion matrix that gives the discretization.
Note
On the chemical potential:
Recall that requiring the contact interaction be written as the quadratic \(nVn\) induces a term in the Hamiltonian proportional to \(n\) itself, which looks just like a chemical potential term. This term comes with a coefficient equal to \(-N_x^2 C_0/2\).
Our sign convention is that the internal energy is \(H-\mu N - h\cdot S\) so the the signs conspire. The fermion matrix is constructed with the ‘offset’ chemical potential \(\mu + N_x^2 C_0/2\).
- normalizing_offset
The A-independent contribution to the action needed to match Z to the Trotterized operator definition.
\[\frac{N_t}{2} \text{tr} \log\left(-2\pi \Delta t V\right)\]
- gauge(A)[source]
- Parameters
A (torch.tensor) – an auxiliary field configuration
- Returns
\(\frac{1}{2} \sum_t A_t (-\Delta t V)^{-1} A_t\)
- Return type
torch.tensor
- fermionic(A)[source]
- Parameters
A (torch.tensor) – an auxiliary field configuration
- Returns
\(-\log \det \mathbb{d}\)
- Return type
torch.tensor
- __call__(A)[source]
- Parameters
A (torch.tensor) – an auxiliary field configuration
- Returns
\(S(A) =\texttt{S.gauge}(A) + \texttt{S.fermionic}(A) + \texttt{S.normalizing_offset}\).
- Return type
torch.tensor
- quenched_sample(sample_shape=torch.Size([]))[source]
Provides sample auxiliary fields drawn from the gaussian
\[p(A) \propto \exp\left( - \frac{1}{2} \sum_t A_t (-\Delta t V)^{-1} A_t \right)\]- Parameters
sample_shape (torch.Size) – See the torch.distributions interface for details.
- Returns
Shape is
[*sample_shape, *spacetime.dims]. Called with no arguments the default is to produce just one sample.- Return type
torch.tensor
- projected(n, s)[source]
Provides a convenience constructor for actions derived from this one that are needed in the implementation of
Sector. Shifts the chemical potential and external field, via\[\begin{align} \mu &\rightarrow \mu + \frac{2\pi i}{2V+1} \frac{n}{\beta} & \vec{h} &\rightarrow \vec{h} + \frac{2\pi i}{2V+1} \frac{2s}{\beta} \hat{h} \end{align}\]- Parameters
n (integer) – Fourier sector for particle number projection
s (half-integer) – Fourier sector for spin projection
- Return type
The Grand Canonical Ensemble
With the auxiliary field Action in our hands we can sample the distribution \(\exp(-S)\).
An assignment for the auxiliar field is a configuration, and set of configurations is called an ensemble.
We can generate() an ensemble using a sampling scheme (described in Hybrid Monte Carlo) or build one from_configurations() we already have.
tdg.ensemble.GrandCanonical
- class tdg.ensemble.GrandCanonical(Action)[source]
Bases:
H5ableA grand-canonical ensemble of configurations and associated observables, importance-sampled according to
Action.Note
GrandCanonicalalso supports a large number of observables.- Parameters
Action (tdg.Action) – An action which describes a Euclidean path integral equal to a Trotterization of the physics of interest.
- Action
The action with which the ensemble was constructed.
- from_configurations(configurations, weights=None, index=None)[source]
- Parameters
configurations (torch.tensor) – A set of pre-computed configurations.
weights (torch.tensor) – Weights for each configuration.
index (torch.tensor) – Where in Markov chain time did each configuration come from.
- Returns
the ensemble itself, so that one can do
ensemble = GrandCanonical(action).from_configurations(phi).If
weightsisNone, the weights are all 1. IfindexisNone, the index counts up from 0.
- generate(steps, generator, start='hot', progress=<function _no_op>, starting_index=0)[source]
- Parameters
steps (int) – Number of configurations to generate.
generator – Something which produces a new configuration if called as generator.step(previous_configuration). Often an
hmcinstance. May be provided with a default in the future.start ('hot', 'cold', or torch.tensor) – A hot start begins with a configuration drawn from the quenched action. A cold start beins with the zero configuration. If a tensor is passed that tensor is used as the first configuration.
progress (something which wraps an iterator and provides a progress bar.) – In a script you might use tqdm.tqdm, and in a notebook tqdm.notebook. Defaults to no progress reporting.
starting_index (int) – The generated steps will have
[starting_index, starting_index+steps).
- Return type
the ensemble itself, so that one can do
ensemble = GrandCanonical(action).generate(...).
Populates the
indexattribute, a torch tensor counting up fromstarting_index, once for each call to the generator, so that each configuration has an index. This index is kept track of throughcut(),every(),binned()(by theBinning).
- classmethod continue_from(ensemble, steps, progress=<function _no_op>)[source]
Use the last configuration and generator of
ensembleto produce a new ensemble ofstepsconfigurations.- Parameters
ensemble (tdg.ensemble.GrandCanonical or an h5py.Group that encodes such an ensemble) – The ensemble to continue. Raises a ValueError if it is not a tdg.ensemble.GrandCanonical or an h5py.Group with an action, generator, and at least one configuration.
steps (int) – Number of configurations to generate.
- Returns
An ensemble with
stepsnew configurataions generted in the same way asensemble.- Return type
Todo
The starting weight should automatically be read in; currently not.
- measure(*observables)[source]
Compute each @observable in observables; log an error for any unregistered observable.
- Parameters
observables (strings) – Names of observables.
- Return type
GrandCanonical; itself, now with some observables measured.
Note
If no observables are passed, evaluates every registered @observable.
- to_h5(group, _top=True)[source]
Just like
tdg.h5.H5able.to_h5()but some data are written as resizable datasets
- extend_h5(group, fields=None)[source]
Append the measured values for this ensemble onto the resizable datasets (configurations, index, weights, and observables).
Warning
Currently does not check that the actions match! So sloppy behavior on your part can result in really weird, probably-meaningless data.
- Parameters
group (h5 group) – Should be an h5 group written from an ensemble with the same action.
fields – Tuple of properties to extend. If
None, extends configurations, index, weights, and all observbles evaluated; does not trigger the measurement of observables.
- classmethod from_h5(group, strict=True, observables=None, selection=(), _top=True)[source]
- Parameters
group (h5 group) – Where the data for this ensemble is found; same as
tdg.h5.H5Data.from_h5().strict (boole) – Same as
tdg.h5.H5Data.from_h5().observables (iterable of strings) – Which observables should be read. If
Noneread all the observables on disk.selection (fancy indexing) – A subset of numpy fancy indexing is supported; this selection is used for selecting configurations bassed on their location in the dataset, rather than their
.index. Some valid choics are[1,2,3],slice(1,4),slice(1,10,2). If you want a singleton, you should use[1,]or the configurations will have the wrong number of dimensions.
- cut(start)[source]
- Parameters
start (int) – The number of configurations to drop from the beginning of the ensemble.
- Return type
GrandCanonicalwithout some configurations at the start. Useful for performing a thermalization cut.
- every(frequency)[source]
- Parameters
frequency (int) – The frequency with which to keep configurations.
- Return type
GrandCanonicalwith configurations reduced in size by a factor of the frequency. Useful for Markov Chain decorrelation.