Metadata-Version: 2.4
Name: dmrgpy
Version: 0.1.0
Summary: Quasi-one-dimensional spin, fermionic, parafermion and bosonic systems with matrix product states (DMRG/ED)
Home-page: https://github.com/joselado/dmrgpy
Author: Jose Lado
Author-email: Jose Lado <jose.lado@aalto.fi>
License-Expression: GPL-3.0-or-later
Project-URL: Homepage, https://github.com/joselado/dmrgpy
Project-URL: Repository, https://github.com/joselado/dmrgpy
Project-URL: Issues, https://github.com/joselado/dmrgpy/issues
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: Topic :: Scientific/Engineering :: Physics
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Operating System :: POSIX
Classifier: Operating System :: MacOS
Requires-Python: >=3.6
Description-Content-Type: text/markdown
License-File: LICENSE.md
Requires-Dist: numpy
Requires-Dist: scipy
Requires-Dist: sympy
Requires-Dist: numba
Provides-Extra: julia
Requires-Dist: juliacall; extra == "julia"
Provides-Extra: jax
Requires-Dist: jax; extra == "jax"
Provides-Extra: stats
Requires-Dist: statsmodels; extra == "stats"
Provides-Extra: parallel
Requires-Dist: multiprocess; extra == "parallel"
Requires-Dist: dill; extra == "parallel"
Provides-Extra: full
Requires-Dist: dmrgpy[jax,julia,parallel,stats]; extra == "full"
Dynamic: author
Dynamic: home-page
Dynamic: license-file
Dynamic: requires-python

## DMRGPY ##

# Summary #

This is a Python library to compute quasi-one-dimensional
spin chains and fermionic systems using matrix product states
with the density matrix renormalization group as implemented in ITensor
(C++ v2/v3, Julia, or a pure-Python/NumPy reimplementation that needs
no compiler -- see "Choosing a backend" below). Most
of the computations can be performed both with DMRG and exact
diagonalization for small systems, which allows to benchmark the
results.

Several examples can be found in the examples folder.

# Disclaimer #

This library is still under heavy development.

# How to install #

## With pip ##

```bash
pip install dmrgpy
```

This installs the pure-Python part of the library, which needs no compiler
and no C++ toolchain. Both the exact-diagonalization backend and the
pure-Python DMRG/TDVP backend work out of the box; select the latter with

```python
sc = spinchain.Spin_Chain(["S=1/2"]*n, itensor_version="python")
```

The compiled ITensor (C++) backends are deliberately *not* shipped on PyPI
-- they are built against a large vendored copy of ITensor and are compiled
in place from a clone of this repository, as described below. Optional
extras are available for the non-default backends and tools:
`pip install "dmrgpy[julia]"` (Julia/ITensors.jl backend), `[jax]`,
`[stats]`, `[parallel]`, or `[full]` for all of them.

## Linux and Mac (compiled C++ backend) ##

Execute the script 

```bash
python install.py 
```

It first checks that everything needed to build is present (a C++
compiler, LAPACK/BLAS, `pybind11`, `make`), auto-detecting the right C++
compiler for the Python interpreter you used to run it (including a
conda-provided compiler if you're using Anaconda/Miniconda), and only then
compiles ITensor and the in-process C++ extension that uses it.

If you need to override the auto-detected C++ compiler (version 6 or higher, or a matching Clang), pass it explicitly

```bash
python install.py --gpp=g++-6 
```

If LAPACK/BLAS can't be found automatically, point the script at OpenBLAS explicitly

```bash
python install.py --openblas --openblas_libdir=/path/to/openblas/lib --openblas_includedir=/path/to/openblas/include
```

To check what the script would use (compiler, BLAS/LAPACK) without
actually running the several-minutes-long ITensor build, use

```bash
python install.py --doctor
```

### HPC clusters with environment modules (e.g. Aalto's Triton) ###

On clusters using Lmod/environment modules, no C++ compiler may be on
PATH at all until a module is loaded, e.g. on Triton:

```bash
module load <stack> gcc/<version>   # e.g. triton/2024.1-gcc gcc/12.3.0
python install.py
```

If you're using a module-provided Python distribution (e.g. Triton's
`scicomp-python-env`), its bundled conda-style compiler wrapper can
sometimes be present but broken (missing its `cc1plus` backend) -- if so,
`install.py` automatically falls back to the system/module-loaded
compiler and prints a note about it. Whichever compiler ends up being
used, and whichever modules/conda environment were loaded at install
time, must be loaded/activated again in every later session (including
job scripts) that imports `dmrgpy` -- the compiled extension depends on
them.

Alternatively, in case you just want to use the Julia version,
execute the script 

```bash
python install_julia.py
```

The installation script will
also add dmrgpy to the PYTHONPATH of the python interpreter you used
to execute it.

Afterwards you can import the dmrgpy sublibrary that you want, for example

```python
from dmrgpy import spinchain
```

If you are running it in Triton cluster, load the modules

```bash
module load scicomp-python-env
module load gcc
```

and then compile it as 

```bash
python3 install.py 
```

## Windows ##

For using this program in Windows, the easiest solution is to create a virtual
machine using [Virtual Box](https://www.virtualbox.org/), installing
a version of [Ubuntu](https://releases.ubuntu.com/20.04/)
in that virtual machine, and following the previous
instructions.

# Tutorials #
You can find several tutorials [here](https://github.com/joselado/Advanced_Computational_Methods_Physics_2024), in particular organized around the following topics

- [Many-body quantum magnets](https://github.com/joselado/Advanced_Computational_Methods_Physics_2024/blob/main/jupyter-notebooks/quantum_magnetism.ipynb)
- [Many-body correlated fermionic systems](https://github.com/joselado/Advanced_Computational_Methods_Physics_2024/blob/main/jupyter-notebooks/quantum_interacting_fermions.ipynb)
- [Tensor networks for many-body quantum magnets](https://github.com/joselado/Advanced_Computational_Methods_Physics_2024/blob/main/jupyter-notebooks/mps_quantum_magnets.ipynb)
- [Tensor netowrks for many-body correlated fermionic systems](https://github.com/joselado/Advanced_Computational_Methods_Physics_2024/blob/main/jupyter-notebooks/mps_many_body_fermionic.ipynb)


# Capabilities #
- Possible models include spinless fermions, spinful fermions, spins, parafermions and bosons
- Ground state energy
- Ground state wavefunction
- Excitation energies
- Excited wavefunctions
- Arbitrary expectation values, including static correlation functions
- Time evolution of arbitrary states, with TDVP, TEBD or a Taylor-expanded evolution operator
- MPS algebra: sum of MPS, application of operators, exponential and inverse
- MPO algebra: sums, products, trace, trace of inverse for generic operators
- Dynamical correlation functions computed with the Kernel polynomial method
- Dynamical correlation functions with time dependent DMRG
- Generic operator distributions computed with the Kernel polynomial method
- Iterative MPS Hermitian and non-Hermitian diagonalization solvers 
- Hermitian and non-Hermitian degeneracy detection

# Examples

The snippets below are mirrored as self-contained scripts under
`examples/readme_examples/`; the full `examples/` directory has 100+
further scripts, one per physical model or feature. When adding a new
one there, it's good practice to make it produce a plot (with
matplotlib) in addition to any printed/asserted checks -- a plot is
often the fastest way to sanity-check a result by eye, and makes the
example far more useful as a demonstration for others browsing the
directory.

## Ground state energy of an S=1/2 spin chain
```python
from dmrgpy import spinchain
spins = ["S=1/2" for i in range(30)] # spins in each site
sc = spinchain.Spin_Chain(spins) # create spin chain object
h = 0 # initialize Hamiltonian
for i in range(len(spins)-1): 
  h = h + sc.Sx[i]*sc.Sx[i+1]
  h = h + sc.Sy[i]*sc.Sy[i+1]
  h = h + sc.Sz[i]*sc.Sz[i+1]
sc.set_hamiltonian(h) # create the Hamiltonian
print("Ground state energy",sc.gs_energy())
```


## Static correlator of an S=1/2 spin chain
```python
from dmrgpy import spinchain
n = 30
spins = ["S=1/2" for i in range(n)] # S=1 in each site
sc = spinchain.Spin_Chain(spins) # create spin chain object
h = 0 # initialize Hamiltonian
for i in range(len(spins)-1):
  h = h + sc.Sx[i]*sc.Sx[i+1]
  h = h + sc.Sy[i]*sc.Sy[i+1]
  h = h + sc.Sz[i]*sc.Sz[i+1]
sc.set_hamiltonian(h) # create the Hamiltonian
cs = [sc.vev(sc.Sz[0]*sc.Sz[i]).real for i in range(n)]
```
![Alt text](images/S12chain_correlator.png?raw=true "Static correlator in an S=1/2 chain, showing power-law decay of correlations")


## Static correlator of an S=1 spin chain
```python
from dmrgpy import spinchain
n = 30
spins = ["S=1" for i in range(n)] # S=1 in each site
sc = spinchain.Spin_Chain(spins) # create spin chain object
h = 0 # initialize Hamiltonian
for i in range(len(spins)-1): 
  h = h + sc.Sx[i]*sc.Sx[i+1]
  h = h + sc.Sy[i]*sc.Sy[i+1]
  h = h + sc.Sz[i]*sc.Sz[i+1]
sc.set_hamiltonian(h) # create the Hamiltonian
cs = [sc.vev(sc.Sz[0]*sc.Sz[i]).real for i in range(n)]
```
![Alt text](images/S1chain_correlator.png?raw=true "Static correlator in an S=1 chain, showing coupling of the emergent edge excitations")


## Conformal field theory central charge of a critical Ising model
```python
from dmrgpy import spinchain
n = 100 # number of sites
spins = ["S=1/2" for i in range(n)] # spin 1/2 heisenberg chain
sc = spinchain.Spin_Chain(spins) # create the spin chain
h = 0 # initialize
for i in range(n-1): h = h + sc.Sz[i]*sc.Sz[i+1] # Ising coupling
for i in range(n): h = h + 0.5*sc.Sx[i] # transverse field
sc.set_hamiltonian(h) # set the Hamiltonian
sc.maxm = 200 # increase bond dimension for a critical system
wf = sc.get_gs() # compute ground state
print("Central charge",wf.get_CFT_central_charge()) # compute central charge
```

## Ground state energy of a bilinear-biquadratic Hamiltonian
```python
from dmrgpy import spinchain
ns = 6 # number of sites in the spin chain
spins = ["S=1" for i in range(ns)] # S=1 chain
sc = spinchain.Spin_Chain(spins) # create spin chain object
h = 0 # initialize Hamiltonian
Si = [sc.Sx,sc.Sy,sc.Sz] # store the three components
for i in range(ns-1): # loop 
    for S in Si: h = h + S[i]*S[i+1]  # bilinear
    for S in Si: h = h + 1./3.*S[i]*S[i+1]*S[i]*S[i+1]  # biquadratic
sc.set_hamiltonian(h) # create the Hamiltonian
print("Energy with DMRG",sc.gs_energy(mode="DMRG"))
print("Energy with ED",sc.gs_energy(mode="ED"))
```

## Magnetization of an S=1 spin chain with an edge magnetic field
```python
from dmrgpy import spinchain
n = 40
spins = ["S=1" for i in range(n)] # S=1 chain
sc = spinchain.Spin_Chain(spins) # create spin chain object
h = 0 # initialize Hamiltonian
for i in range(len(spins)-1): 
  h = h + sc.Sx[i]*sc.Sx[i+1]
  h = h + sc.Sy[i]*sc.Sy[i+1]
  h = h + sc.Sz[i]*sc.Sz[i+1]
h = h + sc.Sz[0]*0.1 # edge magnetic field
sc.set_hamiltonian(h) # create the Hamiltonian
mz = [sc.vev(sc.Sz[i]).real for i in range(n)]
print("Mz",mz)
```

## Bond dimension energy convergence for an S=1/2 Heisenberg chain
```python
from dmrgpy import spinchain
import numpy as np
n= 30 # size of the chain
spins = ["S=1/2" for i in range(n)] # S=1/2 chain
sc = spinchain.Spin_Chain(spins) # create spin chain object
h = 0 # initialize Hamiltonian
for i in range(len(spins)-1):
  h = h + sc.Sx[i]*sc.Sx[i+1]
  h = h + sc.Sy[i]*sc.Sy[i+1]
  h = h + sc.Sz[i]*sc.Sz[i+1]
bds = range(3,20,2) # bond dimension
es,des = [],[] # storage of energies and fluctuations
for maxm in bds: # loop over bond dimension
  sc.set_hamiltonian(h) # create the Hamiltonian
  sc.maxm = maxm # set the bond dimension
  e = sc.gs_energy() # get the ground state energy
  wf = sc.get_gs() ; de = wf.dot(h*(h*wf)) # Energy square
  de = np.sqrt(np.abs(de-e**2)) # energy fluctuation
  es.append(e/n) # store energy
  des.append(de/n) # energy fluctuation
```

![Alt text](images/bond_dimension.png?raw=true "Convergence of the energy as a function of the bond dimension for an S=1/2 chain")

## Excited states with DMRG and ED 
```python
from dmrgpy import spinchain
spins = ["S=1/2" for i in range(12)] # 2*S+1=2 for S=1/2
sc = spinchain.Spin_Chain(spins) # create spin chain object
h = 0 # initialize Hamiltonian
for i in range(len(spins)-1): 
  h = h + sc.Sx[i]*sc.Sx[i+1]
  h = h + sc.Sy[i]*sc.Sy[i+1]
  h = h + sc.Sz[i]*sc.Sz[i+1]
sc.set_hamiltonian(h)
es1 = sc.get_excited(n=6,mode="DMRG")
es2 = sc.get_excited(n=6,mode="ED")
print("Excited states with DMRG",es1)
print("Excited states with ED",es2)
```

## Singlet-triplet gap of the Haldane Heisenberg S=1 spin chain
```python
from dmrgpy import spinchain
# Haldane chain with S=1/2 on the edge to remove the topological modes
spins = ["S=1/2"]+["S=1" for i in range(40)]+["S=1/2"]
sc = spinchain.Spin_Chain(spins) # create spin chain object
h = 0 # initialize Hamiltonian
for i in range(len(spins)-1): 
  h = h + sc.Sx[i]*sc.Sx[i+1]
  h = h + sc.Sy[i]*sc.Sy[i+1]
  h = h + sc.Sz[i]*sc.Sz[i+1]
sc.set_hamiltonian(h)
es = sc.get_excited(n=2,mode="DMRG")
gap = es[1]-es[0] # compute gap
print("Gap of the Haldane chain",gap)
```



## Local dynamical spin correlator of an S=1/2 chain
```python
import numpy as np
from dmrgpy import spinchain
n = 40
# create an S=1/2 spin chain
spins = ["S=1/2" for i in range(n)] # spin 1/2 heisenberg chain
# create first neighbor exchange
sc = spinchain.Spin_Chain(spins) # create the spin chain
h = 0
for i in range(n-1):
    h = h + sc.Sx[i]*sc.Sx[i+1]
    h = h + sc.Sy[i]*sc.Sy[i+1]
    h = h + sc.Sz[i]*sc.Sz[i+1]
sc.set_hamiltonian(h)
zs = [] # empty list
for i in range(n): # loop over sites
  name = (sc.Sz[i],sc.Sz[i])
  (e,s) = sc.get_dynamical_correlator(mode="DMRG",name=name,
          es=np.linspace(-0.5,4.0,200),delta=0.05)
  zs.append(s) # store
```

![Alt text](images/dyn_corr_spatial_long.png?raw=true "Dynamical spin correlator for different sites of an S=1/2 chain")

## Local dynamical spin correlator of an S=1 chain
```python
import numpy as np
from dmrgpy import spinchain
n = 40
# create an S=1/2 spin chain
spins = ["S=1" for i in range(n)] # spin 1/2 heisenberg chain
# create first neighbor exchange
sc = spinchain.Spin_Chain(spins) # create the spin chain
h = 0
for i in range(n-1):
    h = h + sc.Sx[i]*sc.Sx[i+1]
    h = h + sc.Sy[i]*sc.Sy[i+1]
    h = h + sc.Sz[i]*sc.Sz[i+1]
sc.set_hamiltonian(h)
zs = [] # empty list
for i in range(n): # loop over sites
  name = (sc.Sz[i],sc.Sz[i])
  (e,s) = sc.get_dynamical_correlator(mode="DMRG",name=name,
          es=np.linspace(-0.5,4.0,200),delta=0.05)
  zs.append(s) # store
```

![Alt text](images/dyn_corr_spatial_long_S1.png?raw=true "Dynamical spin correlator for different sites of an S=1 chain")



## Local dynamical spin correlator of an S=1/2 chain with a S=1 impurity
```python
import numpy as np
from dmrgpy import spinchain
spins = ["S=1/2" for i in range(14)] # spin 1/2 heisenberg chain
spins = spins + ["S=1"] + spins # put S=1 in the middle
n = len(spins) # total number of spins
# create first neighbor exchange
sc = spinchain.Spin_Chain(spins) # create the spin chain
h = 0
for i in range(n-1):
    h = h + sc.Sx[i]*sc.Sx[i+1]
    h = h + sc.Sy[i]*sc.Sy[i+1]
    h = h + sc.Sz[i]*sc.Sz[i+1]
sc.set_hamiltonian(h)
zs = [] # empty list
for i in range(n): # loop over sites
  name = (sc.Sz[i],sc.Sz[i])
  (e,s) = sc.get_dynamical_correlator(mode="DMRG",name=name,
          es=np.linspace(-0.5,4.0,200),delta=0.05)
  zs.append(s.real) # store
```

![Alt text](images/dyn_corr_spatial_impurity.png?raw=true "Dynamical spin correlator for an S=1/2 with an S=1 impurity in the middle")



## Non-local dynamical spin correlator of an S=1/2 chain
```python
import numpy as np
from dmrgpy import spinchain
n = 10
# create an S=1/2 spin chain
spins = ["S=1/2" for i in range(n)] # spin 1/2 heisenberg chain
# create first neighbor exchange
sc = spinchain.Spin_Chain(spins) # create the spin chain
h = 0
for i in range(n-1):
    h = h + sc.Sx[i]*sc.Sx[i+1]
    h = h + sc.Sy[i]*sc.Sy[i+1]
    h = h + sc.Sz[i]*sc.Sz[i+1]
sc.set_hamiltonian(h)
xs = [] # empty list
ys = [] # empty list
zs = [] # empty list
for i in range(n): # loop over sites
  name = (sc.Sz[0],sc.Sz[i])
  (e,s) = sc.get_dynamical_correlator(mode="DMRG",name=name,
          es=np.linspace(-0.5,4.0,200),delta=0.05)
  zs.append(s) # store
```

![Alt text](images/dyn_corr_nonlocal_spatial.png?raw=true "Dynamical non-local spin correlator for different sites of an S=1/2 chain")


## Bulk and edge dynamical correlator of a Haldane chain
```python
from dmrgpy import spinchain
n = 20 ; spins = ["S=1" for i in range(n)] # S=1 chain
sc = spinchain.Spin_Chain(spins) # create spin chain object
h = 0 # initialize Hamiltonian
for i in range(len(spins)-1):
  h = h + sc.Sx[i]*sc.Sx[i+1]
  h = h + sc.Sy[i]*sc.Sy[i+1]
  h = h + sc.Sz[i]*sc.Sz[i+1]
sc.set_hamiltonian(h)
(e0,d0) = sc.get_dynamical_correlator(name=(sc.Sz[0],sc.Sz[0]))
(eb,db) = sc.get_dynamical_correlator(name=(sc.Sz[n//2],sc.Sz[n//2]))
```

![Alt text](images/dyn_corr_bulk_edge.png?raw=true "Dynamical spin correlator for different sites of an S=1 chain")

## Spin and charge correlator of the 1D Hubbard model
```python
from dmrgpy import fermionchain
n = 20 # number of sites
fc = fermionchain.Spinful_Fermionic_Chain(n)
# first neighbor hopping
h = 0
for i in range(n-1):
  h = h + fc.Cdagup[i]*fc.Cup[i+1]
  h = h + fc.Cdagdn[i]*fc.Cdn[i+1]
h = h + h.get_dagger() # Make Hermitian
# Hubbard term
for i in range(n):
  h = h + 4.*(fc.Nup[i]-.5)*(fc.Ndn[i]-.5)
fc.set_hamiltonian(h) # initialize the Hamiltonian
# compute the two correlators
zz = [fc.vev(fc.Sz[0]*fc.Sz[i]).real for i in range(n)]
cc = [fc.vev(fc.Cdagup[0]*fc.Cup[i]).real for i in range(n)]
```

![Alt text](images/hubbard_correlator.png?raw=true "Spin and charge correlator in the Hubbard model")



## Spin correlator in the Hubbard model as function of the interaction
```python
from dmrgpy import fermionchain
import numpy as np
n = 14 # number of sites
fc = fermionchain.Spinful_Fermionic_Chain(n)
# first neighbor hopping
h = 0
for i in range(n-1):
  h = h + fc.Cdagup[i]*fc.Cup[i+1]
  h = h + fc.Cdagdn[i]*fc.Cdn[i+1]
h = h + h.get_dagger() # Make Hermitian
# Hubbard term
hU = 0
for i in range(n):
  hU = hU + (fc.Nup[i]-.5)*(fc.Ndn[i]-.5)

zzs = [] # storage for correlators
Us = np.linspace(0.,4.,6) # Hubbard Us 
for U in Us:
  fc.set_hamiltonian(h+U*hU) # initialize the Hamiltonian
  zz = [fc.vev(fc.Sz[0]*fc.Sz[i]).real for i in range(n)]
  zzs.append(zz) # store zz correlator
```

![Alt text](images/hubbard_correlator_VS_U.png?raw=true "Spin correlator in the Hubbard model for different interactions U")




## Generic interacting fermionic Hamiltonian
```python
import numpy as np
from dmrgpy import fermionchain
n = 6 # number of different spinless fermionic orbitals
# fc is an object that contains the information of the many body system
fc = fermionchain.Fermionic_Chain(n) # create the object
h = 0
# create random hoppings
for i in range(n):
  for j in range(i):
    h = h + fc.Cdag[i]*fc.C[j]*np.random.random()
# create random density interactions
for i in range(n):
  for j in range(i):
    h = h + fc.N[i]*fc.N[j]*np.random.random()
h = h + h.get_dagger() # make the Hamiltonian Hermitian
fc.set_hamiltonian(h) # set the Hamiltonian in the object
print("GS energy with ED",fc.gs_energy(mode="ED")) # energy with exact diag
print("GS energy with DMRG",fc.gs_energy(mode="DMRG")) # energy with DMRG
```


# Choosing a backend #
The library uses ITensor in the background, and exposes the same
public API regardless of which backend actually runs the calculation.
Available backends are

- **ITensor v3 (C++)**, the default, compiled by `python install.py`.
  An in-process pybind11 extension. Real-time evolution defaults to
  TDVP (`tevol_method="TDVP"`), with TEBD (`tevol_method="TEBD"`) also
  available.
- **ITensor v2 (C++)**, compiled with `python install.py
  --itensor-version=2` (or `--itensor-version=both` to build v2 and v3
  together). Real-time evolution uses a Taylor-expanded
  evolution-operator MPO instead (no TDVP/TEBD support).
- **Pure Python** (`pyitensor`), a from-scratch NumPy/SciPy
  reimplementation of the ITensor v3 API subset dmrgpy needs --
  ground/excited-state DMRG, TDVP and TEBD time evolution, KPM dynamical
  correlators, METTS, and more. Needs no C++ compiler or compiled
  extension at all, at the cost of being slower than the compiled
  backends.
- **Julia (ITensors.jl)**, a live in-process Julia session, set up with
  `python install_julia.py` instead of `install.py`.
- **Exact diagonalization (ED)**, a pure Python/NumPy/SciPy fallback.
  Used automatically for small systems, and automatically in place of
  any C++/Julia backend that isn't available (e.g. its extension
  wasn't compiled), so a script keeps running -- just slower -- even
  without a full build.

The default backend is ITensor v3 (C++). Switch an existing chain to
another one with `.setup_cpp(version=2)`, `.setup_python()` or
`.setup_julia()`, for example

```python
from dmrgpy import spinchain
spins = ["S=1/2" for i in range(30)] # spins in each site
sc = spinchain.Spin_Chain(spins) # create spin chain object
sc.setup_cpp(version=2) # switch to ITensor v2 (C++)
# sc.setup_python()     # or: pure Python, no compiler needed
# sc.setup_julia()      # or: ITensors.jl (Julia)
```

and all subsequent computations on `sc` will be performed with that
backend. Most methods also accept a `mode="DMRG"|"ED"` kwarg to
cross-check a single call against ED without switching the chain's
backend, for example `sc.gs_energy(mode="ED")`.

