diff --git a/docs/make.jl b/docs/make.jl index 2d71a20a0..5b620acdd 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -53,20 +53,58 @@ makedocs(; ), pages = [ "Home" => "index.md", - "Manual" => [ - "man/intro.md", - "man/states.md", - "man/operators.md", - "man/algorithms.md", - # "man/environments.md", - "man/parallelism.md", - "man/lattices.md", + "Tutorials" => [ + "tutorials/installation.md", + "tutorials/first_groundstate.md", + "tutorials/thermodynamic_limit.md", + "tutorials/time_evolution.md", + "tutorials/excitations.md", + "tutorials/using_symmetries.md", + ], + "How-to" => [ + "howto/index.md", + "howto/states.md", + "howto/hamiltonians.md", + "howto/groundstate_algorithms.md", + "howto/bond_dimension.md", + "howto/time_evolution.md", + "howto/observables.md", + "howto/entanglement.md", + "howto/excitations.md", + "howto/statmech.md", + "howto/quasi_1d_geometries.md", + "howto/convergence_troubleshooting.md", + "howto/parallelism_gpu.md", + "howto/saving_loading.md", + ], + "Concepts" => [ + "concepts/vector_spaces.md", + "concepts/matrix_product_states.md", + "concepts/finite_vs_infinite.md", + "concepts/operators_and_hamiltonians.md", + "concepts/symmetries.md", + "concepts/algorithm_landscape.md", + "concepts/environments.md", + "concepts/parallelism_model.md", + "concepts/numerics.md", ], "Examples" => [ "Overview" => "examples/index.md", example_pages..., ], - "Library" => "lib/lib.md", + "Library" => [ + "lib/public.md", + "lib/states.md", + "lib/operators.md", + "lib/groundstate.md", + "lib/bond_dimension.md", + "lib/time_evolution.md", + "lib/excitations.md", + "lib/observables.md", + "lib/environments.md", + "lib/internals.md", + "lib/lib.md", + ], "References" => "references.md", "Changelog" => "changelog.md", ], diff --git a/docs/src/concepts/algorithm_landscape.md b/docs/src/concepts/algorithm_landscape.md new file mode 100644 index 000000000..d57e3114b --- /dev/null +++ b/docs/src/concepts/algorithm_landscape.md @@ -0,0 +1,106 @@ +# [The algorithm landscape](@id concept_algorithm_landscape) + +MPSKit deliberately separates *what* you want to compute from *how* it gets computed. +Entry points such as [`find_groundstate`](@ref), [`timestep`](@ref), [`excitations`](@ref), [`leading_boundary`](@ref), and [`approximate`](@ref) each accept several interchangeable algorithm structs, and the package ships more than a dozen of them. +That flexibility exists because no single algorithm wins everywhere: some only apply to finite or only to infinite systems, some can grow the bond dimension while others cannot, and their relative performance depends on the model at hand. + +This page is the decision guide. +It starts from a table that maps each task onto the algorithm(s) of choice, and then walks through the reasoning behind each row. +It explains *why* you would pick one algorithm over another; for the *how* — the actual calls, keywords, and worked recipes — follow the links into the how-to pages. + +## The decision table + +| Task | Finite system | Infinite system | +|:-----|:--------------|:----------------| +| **Ground state** ([`find_groundstate`](@ref)) | [`DMRG`](@ref) (workhorse, fixed bond dimension); [`DMRG2`](@ref) (grows bond dimension, requires `trunc`); [`GradientGrassmann`](@ref) (final polish) | [`VUMPS`](@ref) (workhorse, needs a unique ground state); [`IDMRG`](@ref) / [`IDMRG2`](@ref) (two-site requires `trunc` and a unit cell of at least two sites); [`GradientGrassmann`](@ref) (final polish) | +| **Time evolution** ([`timestep`](@ref) / [`time_evolve`](@ref)) | [`TDVP`](@ref) (fixed bond dimension); [`TDVP2`](@ref) (grows bond dimension, requires `trunc`); [`BUG`](@ref) (rank-adaptive with a truncating `trunc`); or [`make_time_mpo`](@ref) ([`WI`](@ref) / [`WII`](@ref) / [`TaylorCluster`](@ref)) applied with [`approximate`](@ref) | [`TDVP`](@ref) (no two-site variant exists); or [`make_time_mpo`](@ref) applied with [`approximate`](@ref) | +| **Excitations** ([`excitations`](@ref)) | [`QuasiparticleAnsatz`](@ref) (the only one supporting charged `sector`s); [`FiniteExcited`](@ref) (penalty method); [`ChepigaAnsatz`](@ref) / [`ChepigaAnsatz2`](@ref) (cheap, from ground-state environments) | [`QuasiparticleAnsatz`](@ref) (momentum-resolved, the only choice) | +| **Boundary / statistical mechanics** ([`leading_boundary`](@ref)) | apply the transfer MPO row by row with [`approximate`](@ref) | [`VUMPS`](@ref); [`VOMPS`](@ref) (power method); [`IDMRG`](@ref) / [`IDMRG2`](@ref); [`GradientGrassmann`](@ref) (hermitian, positive transfer matrices) | +| **Compression / approximation** ([`approximate`](@ref), [`changebonds`](@ref)) | [`approximate`](@ref) with [`DMRG`](@ref) / [`DMRG2`](@ref), or [`Zipup`](@ref) for a single-sweep MPO–MPS product; [`SvdCut`](@ref) via [`changebonds`](@ref) for local truncation | [`approximate`](@ref) with [`IDMRG`](@ref) / [`IDMRG2`](@ref) / [`VOMPS`](@ref); [`SvdCut`](@ref) via [`changebonds`](@ref) for local truncation | + + +A few structural facts hold across the whole table and are worth internalizing early. +Every two-site algorithm (`DMRG2`, `IDMRG2`, `TDVP2`) requires an explicit `trunc` keyword and can change the bond dimension as it runs; the single-site variants with their default settings cannot. +`BUG` is the exception on the single-site side: it defaults to no truncation, but given a truncating `trunc` it becomes rank-adaptive. +`IDMRG2` additionally needs a unit cell of at least two sites, and `TDVP2` exists only for finite MPS. +Finally, algorithms compose: the `&` operator chains two algorithms into one, running the first to completion and handing its result to the second, which is how two-site warm-up passes and gradient-descent polishing stages are combined with a workhorse algorithm in a single call. + +## Ground states + +The classic approach is alternating local optimization: [`DMRG`](@ref) sweeps back and forth through a finite chain, optimizing one site while all others are held fixed, which in practice converges to the ground state. +The catch is the fixed bond dimension: a single-site update can never enlarge the virtual spaces, so the precision of the calculation is locked in by the initial state. +This bites hardest when symmetries are involved, because then not just the total bond dimension but its distribution over charge sectors is frozen, and a poor initial distribution cannot be repaired. +[`DMRG2`](@ref) fixes this by optimizing two neighbouring sites jointly and truncating back down, which lets the bond dimension (and its sector distribution) adapt, at a higher cost per sweep. + +For infinite systems, two philosophies compete. +[`IDMRG`](@ref) grows the system from the middle outwards, repeatedly inserting and optimizing new sites until the boundary is no longer felt; [`IDMRG2`](@ref) is its two-site, bond-growing variant. +Because convergence requires the effective system to outgrow the correlation length, IDMRG can be slow to converge for critical systems, where that length diverges. +[`VUMPS`](@ref) instead works with a genuinely uniform state: each local update is followed by a re-gauging step that replaces *every* tensor in the infinite chain with the updated one, so the effect of an update is felt throughout the system immediately. +This often gives VUMPS a higher convergence rate than IDMRG, which is why it is the default infinite-system workhorse. +The price is an injectivity requirement: VUMPS assumes a unique ground state, and it is not the right tool when the state it should converge to is non-injective. +Like DMRG, VUMPS is single-site and cannot alter the bond dimension. + +[`GradientGrassmann`](@ref) approaches the problem from a third direction: the MPS tensors form a Riemannian manifold (a Grassmann manifold), and one can run gradient descent directly on it, for finite and infinite states alike. +Its niche is the tail of the optimization: close to convergence its rate is often the best of the lot, while far from convergence the sweeping algorithms tend to make faster progress. +The practical consequence is the chaining pattern: run a cheap workhorse first, then hand over to gradient descent, e.g. `VUMPS(...) & GradientGrassmann(...)`. +This pattern is baked into `find_groundstate` itself: called with only keywords, it picks `DMRG` for a finite state and `VUMPS` for an infinite one, appends a `GradientGrassmann` stage on infinite states when the requested tolerance is tighter than `1e-4`, and prepends a two-site pass (`DMRG2` or `IDMRG2`) whenever you supply a `trunc`. +Since gradient descent is also a single-site method, growing the bond dimension remains the job of that two-site pre-pass or of [`changebonds`](@ref). + +For call syntax, keyword tables, and worked chaining examples, see [Ground-state algorithms](@ref howto_groundstate_algorithms). + +## Time evolution + +MPSKit solves the time-dependent Schrödinger equation along two distinct routes, and the choice between them is a genuine trade-off rather than a finite/infinite split. + +The first route, [`TDVP`](@ref), never builds the evolution operator at all. +It projects the Schrödinger equation onto the tangent space of the current MPS, solves the projected equation for a small time step, and repeats. +Its two-site variant [`TDVP2`](@ref) plays the same role as `DMRG2` does for `DMRG`: it lets the bond dimension grow to absorb the entanglement generated by the evolution, at extra cost, and it exists only for finite systems. + +[`BUG`](@ref) is a third option on that same route, also finite-only. +It is a single-site integrator built on the Basis-Update & Galerkin scheme [ceruti2022](@cite), and its distinguishing feature is structural: it advances the basis and the core tensor both *forward* in time, with none of the backward-in-time substep that projector splitting gives `TDVP`. +That backward step is what can misbehave at large imaginary-time steps, so `BUG` is the more natural choice for dissipative evolution. +Given a truncating `trunc` it is rank-adaptive, letting the bond dimension follow the entanglement rather than being fixed in advance — at the cost of ending each sweep at twice the requested rank, since the basis augmentation of one half-sweep is only truncated by the next. + +The second route splits the problem in two: first approximate the evolution operator ``\exp(-iH\,dt)`` itself as an MPO using [`make_time_mpo`](@ref) — with [`WI`](@ref), [`WII`](@ref), or [`TaylorCluster`](@ref) as the approximation scheme — and then apply that MPO to the state with [`approximate`](@ref). +The appeal is amortization: for a time-independent Hamiltonian and a fixed step size the MPO is built once and reused for every step, and the accuracy of the operator approximation is controlled independently of the accuracy of its application. + +Both routes accept an `imaginary_evolution` keyword for evolution in imaginary time. +Renormalization is a separate concern, controlled by `normalize` (default `false`): left off, the norm is preserved and carries information — the accumulated truncation error in real time, the decaying weight in imaginary time — while `normalize = true` is what an imaginary-time ground-state search wants. +For step-by-step recipes along either route, see [Time evolution](@ref howto_time_evolution). + +## Excitations + +Resolving states deep in the spectrum is generally out of reach, but three families of algorithms target the low-lying part, each with a distinct character. + +The [`QuasiparticleAnsatz`](@ref) is the most broadly applicable: it works for finite and infinite systems, and it is the only algorithm that can target excitations carrying a nontrivial symmetry charge, via the `sector` keyword. +It builds an excited state by replacing a single tensor of the ground-state MPS — summed over all positions on a finite chain, or in a momentum-carrying plane-wave superposition on an infinite one — and solves the resulting eigenvalue problem. +Because the variational class consists of local perturbations on top of the ground state, it is the natural choice for quasiparticle-like excitations, and on infinite systems it is the only option, giving direct access to dispersion relations. + +[`FiniteExcited`](@ref) takes a brute-force approach available only on finite chains: it reruns a full ground-state optimization on a modified Hamiltonian that carries an energy penalty for overlapping with all previously found states. +Each new excited state therefore costs another complete ground-state search, and the orthogonality to earlier states is only approximate (enforced by the penalty `weight`, not exactly). +Its advantage is that it makes no assumption about the *form* of the excited state: since each state is a fully variational `FiniteMPS`, it can in principle capture excitations that a local perturbation of the ground state would describe poorly. + +The [`ChepigaAnsatz`](@ref) (and its two-site refinement [`ChepigaAnsatz2`](@ref)) is the cheapest of the three, also finite-only. +It observes that the gauged ground-state MPS tensors act as isometries projecting the Hamiltonian into a low-energy subspace, so the low-lying spectrum can be read off by diagonalizing the effective Hamiltonian already available from the ground-state environments, with no additional sweeping. +This works best precisely where excitations are hard for the other methods: in critical systems with long-range correlations, where the excitation weight is spread across the whole chain. + +For the call signatures, momentum scans, and sector-targeting recipes, see [Excited states](@ref howto_excitations). + +## Boundaries and statistical mechanics + +MPS algorithms are not limited to Hamiltonian problems. +A two-dimensional classical partition function can be written as an infinite power of a row-to-row transfer MPO, and contracting the network amounts to finding that operator's dominant eigenvector — a boundary MPS. +This is the job of [`leading_boundary`](@ref), which accepts a familiar cast: [`VUMPS`](@ref) and [`IDMRG`](@ref)/[`IDMRG2`](@ref) carry over directly from the ground-state problem, [`GradientGrassmann`](@ref) applies when the transfer MPO is hermitian and positive, and [`VOMPS`](@ref) is a power method specific to this setting, which iteratively approximates the operator-times-state product by a new state of the same bond dimension. + +## Compression and changing bond dimension + +Two mechanisms round out the landscape by manipulating states rather than solving for new ones. +[`approximate`](@ref) variationally fits a new MPS, typically of different bond dimension, to the result of applying an MPO to a state; the sweeping ground-state algorithms (`DMRG`/`DMRG2` for finite, `IDMRG`/`IDMRG2`/`VOMPS` for infinite) double as its optimization engines. +This is the same machinery that applies time-evolution MPOs, and combined with [`SvdCut`](@ref) it yields a globally optimal truncation of a state. +[`changebonds`](@ref), by contrast, performs direct local surgery on a state: truncating with [`SvdCut`](@ref), or expanding with [`OptimalExpand`](@ref), [`RandExpand`](@ref), or [`VUMPSSvdCut`](@ref) so that the single-site algorithms above have room to work with. +The trade-offs between those expansion schemes, and recipes for when to grow, are covered in [Controlling bond dimension](@ref howto_bond_dimension). + +## Where to go next + +The how-to pages turn each row of the table into runnable recipes: [Ground-state algorithms](@ref howto_groundstate_algorithms), [Time evolution](@ref howto_time_evolution), and [Excited states](@ref howto_excitations), with [Controlling bond dimension](@ref howto_bond_dimension) supporting all three. +For the complete signatures, keyword lists, and docstrings of every algorithm named here, see the library reference: [Ground-state algorithms](@ref lib_groundstate), [Time evolution](@ref lib_time_evolution), and [Excitations](@ref lib_excitations). diff --git a/docs/src/concepts/environments.md b/docs/src/concepts/environments.md new file mode 100644 index 000000000..6f4ea355c --- /dev/null +++ b/docs/src/concepts/environments.md @@ -0,0 +1,99 @@ +# [Environments](@id concept_environments) + +Almost every MPS algorithm spends most of its time contracting the same tensor network over and over. +In DMRG, optimizing the tensor on one site requires the sum of all Hamiltonian contributions sitting to its left and to its right; in time evolution the same partial contractions reappear at every step. +Recomputing them from scratch each time would be wasteful, because moving attention from one site to a neighbour changes only a little of the network. +The *environment* objects are what let MPSKit avoid that waste. + +This page explains what environments are and why they exist, so that the optional `environments` argument that appears throughout the API stops looking like a mystery. +It is about understanding, not tuning: for the mechanics of a particular algorithm follow the links into the how-to pages. + +## What an environment is + +An environment is a partially contracted piece of a tensor network — the part that does not change when you shift your focus by one site. +Consider the network whose value an algorithm ultimately wants: a state `below` (the bra), an operator, and a state `above` (the ket), all contracted together. +Fixing attention on a single site splits that network into three parts: the tensor at the site itself, everything to its left, and everything to its right. +The left and right parts are exactly the *left environment* and *right environment* of that site. + +The key observation is that these two blocks are shared between neighbouring sites. +The left environment at site `i+1` is the left environment at site `i` with a single extra column contracted onto it. +So once you have paid to build the environment at one site, advancing to the next is cheap: you add one new contribution instead of recontracting the whole chain. +Caching the environments and reusing them across a sweep is what turns an algorithm that would be quadratic in the system size into a linear one. + +In MPSKit these cached blocks live in an environment object, constructed with the exported [`environments`](@ref) function. +The canonical form sandwiches an operator between two states, + +```julia +using MPSKit, MPSKitModels, TensorKit + +state = FiniteMPS(20, ℂ^2, ℂ^10) +H = transverse_field_ising(FiniteChain(20); g = 0.5) +envs = environments(state, H, state) +``` + +while the two-argument form `environments(below, above)` builds the operator-free *overlap* environments between two states. +The individual blocks are then queried with the exported [`leftenv`](@ref) and [`rightenv`](@ref) functions, + +```julia +GL = leftenv(envs, 10, state) # everything to the left of site 10 +GR = rightenv(envs, 10, state) # everything to the right of site 10 +``` + +each of which returns a tensor gauge-compatible with the state tensor at that site, ready to be contracted onto it. + +## Why you rarely build them yourself + +Most of the time you never touch an environment object at all. +The high-level entry points — [`find_groundstate`](@ref), [`timestep`](@ref), [`excitations`](@ref), and the rest — build whatever environments they need internally. +What they also do, uniformly, is accept an *optional* environments argument and return an updated environment object alongside their main result. + +That return value is the reason to care about environments even when you never construct one. +Handing the environments from one call into the next lets the algorithm reuse the cached blocks instead of rebuilding them from nothing. +For iterated procedures such as time evolution — where each step starts from a state only slightly different from the last — feeding the updated environments back in every step avoids repeating work that the previous step already did. +The [Time evolution](@ref howto_time_evolution) how-to shows this threading pattern in a concrete recipe. + +## Finite environments and the `===` cache + +For a finite state the environment object manages its own validity automatically, and understanding how is worthwhile because it comes with one sharp edge. + +When it computes a left environment, the cache records *which* state tensors it contracted to get there — the gauged tensors of `state` up to that site. +On a later query it compares the tensors it would need now against the ones it used before, testing them with Julia's identity operator `===`. +If they are the same objects, the cached block is still valid and is returned immediately. +If some differ, the cache recomputes only the affected part of the network and updates its record. +This is what makes repeated queries during a sweep cheap: the first `leftenv` at a far site pays for the full contraction, and neighbouring queries reuse almost all of it. + +The sharp edge is that `===` tests object identity, not numerical equality. +If you mutate a state tensor *in place* — changing its data while keeping the same object — the cache still sees the same object under `===` and concludes, wrongly, that its stored environment is still valid. +It will then hand back a block computed from the old data. +Building a *new* tensor and assigning it into the state is fine, because that is a different object and the `===` check catches it; only in-place mutation defeats the mechanism. +Because algorithms that use the public API replace tensors rather than mutating them, this is rarely a problem in normal use, but it is the thing to suspect if a hand-written routine that mutates tensors starts returning stale results. + +!!! warning "In-place mutation is invisible to the cache" + The finite-environment cache detects changes by object identity (`===`), so mutating a + state tensor in place leaves the cache convinced its stored environment is still current. + The internal, non-exported helper `MPSKit.poison!(envs, i)` marks the dependencies at + site `i` as stale so the next query recomputes them; needing it is a sign that a tensor + was mutated in place rather than replaced. + +## Infinite environments + +Infinite environments serve the same role but are computed differently, and the difference matters for how you use them. + +A finite chain has genuine boundaries, so its environments can be built by contracting inward from the ends in a finite number of steps. +An infinite chain has no ends. +Its environments are instead the fixed points of the transfer operator — the object you get by contracting one repeating unit cell — and finding a fixed point means solving a linear or eigenvalue problem. +Those problems are solved *iteratively*, to a finite tolerance, rather than by an exact finite contraction. +Building an infinite environment is therefore a small numerical solve, and its result is only as accurate as the tolerance of that solve. + +The precision of that solve is controlled by the `tol`, `maxiter`, and `krylovdim` keyword arguments to [`environments`](@ref), which configure the underlying iterative solver — an Arnoldi eigensolver for a transfer-matrix fixed point, or GMRES for the linear problem that arises with an `InfiniteMPOHamiltonian`. +It is fixed when the environments are built, rather than read from or written to the environment object afterwards. + +The second difference is that infinite environments are **not** recomputed automatically. +The finite cache re-validates itself against the current state on every query; the infinite one does not. +If the state changes, the stored fixed points no longer correspond to it, and there is no automatic re-solve. +Bringing an infinite environment up to date for a changed state is an explicit step, handled internally by the non-exported `MPSKit.recalculate!`.In practice the high-level algorithms perform this recomputation for you as part of their own iteration, which is again why threading the returned environments through successive calls is the efficient pattern. + +## Where to go next + +For the full signatures and docstrings of the environment functions, see [`environments`](@ref), [`leftenv`](@ref), and [`rightenv`](@ref) in the library reference. +For the algorithm-facing side — how ground-state and time-evolution routines consume and return environments — see [Ground-state algorithms](@ref howto_groundstate_algorithms) and [Time evolution](@ref howto_time_evolution). diff --git a/docs/src/man/finite_mps_definition.png b/docs/src/concepts/finite_mps_definition.png similarity index 100% rename from docs/src/man/finite_mps_definition.png rename to docs/src/concepts/finite_mps_definition.png diff --git a/docs/src/concepts/finite_vs_infinite.md b/docs/src/concepts/finite_vs_infinite.md new file mode 100644 index 000000000..99de33e2e --- /dev/null +++ b/docs/src/concepts/finite_vs_infinite.md @@ -0,0 +1,110 @@ +```@meta +DocTestSetup = quote + using MPSKit, TensorKit +end +``` + +# [Finite versus infinite MPS](@id concept_finite_vs_infinite) + +The [matrix product state](@ref concept_matrix_product_states) machinery — the site tensors, the virtual bonds, the canonical gauge — is shared by two rather different physical objects, and MPSKit gives each its own type. +A [`FiniteMPS`](@ref) is the wavefunction of a chain with a definite number of sites and two open ends: a genuine vector in a finite-dimensional Hilbert space. +An [`InfiniteMPS`](@ref) instead stores a small, repeating *unit cell* of tensors and imagines it tiled forever along the chain, so that it represents a translation-invariant state directly in the thermodynamic limit `L = ∞`. +This page explains what that difference *means* — why the two share almost all of their code yet answer subtly different questions, and in particular why an infinite state is always normalized to one while a finite state is not. +It is about understanding rather than construction: to *build* either kind of state see [Constructing states](@ref howto_states), and for the type signatures see the [States](@ref lib_states) reference. + +## Two different objects + +A [`FiniteMPS`](@ref) is what you reach for whenever the system genuinely has a fixed size and boundaries: a chain of `N` sites, each a separate mutable tensor, with trivial (dimension-one) bonds capping the two ends. +It is a literal, if compressed, representation of a state vector `|ψ⟩` living in the tensor-product Hilbert space of those `N` sites, and every question you could ask of an ordinary state vector — its norm, its overlap with another state, an expectation value at a particular site — has a finite, exactly computable answer. +The open ends are part of the physics: sites near a boundary are in a different environment from sites in the bulk, and any measured quantity still carries a dependence on the length `N`. + +An [`InfiniteMPS`](@ref) throws both of those features away on purpose. +It represents a state that is exactly invariant under translation by one unit cell, so there is no boundary anywhere and no length `N` left to depend on. +What is actually stored is a finite list of tensors — the unit cell — together with the gauge data needed to treat the infinite periodic contraction; indexing the state is periodic, so `ψ.AL[i]` and `ψ.AL[i + length(ψ)]` return the same tensor. +This is the representation used throughout [The thermodynamic limit](@ref tutorial_thermodynamic_limit), where the payoff — no boundary effects, no finite-size extrapolation — is put to work on the transverse-field Ising model. + +## The unit cell + +The single number that characterizes the periodicity of an [`InfiniteMPS`](@ref) is its unit-cell length, returned by `length`. +The most common choice is a one-site unit cell, in which a single tensor is repeated across the whole chain: + +```@example finite-infinite +using MPSKit, TensorKit # hide +ψ_infinite = InfiniteMPS(ℂ^2, ℂ^8) +length(ψ_infinite) +``` + +A larger unit cell is specified by passing a vector of physical and virtual spaces, one entry per site of the cell: + +```@example finite-infinite +ψ_cell = InfiniteMPS([ℂ^2, ℂ^2], [ℂ^8, ℂ^8]) +length(ψ_cell) +``` + +The unit-cell length is not a free accuracy knob like the bond dimension; it is a physical statement about the *period* of the state you intend to represent. +A translation-invariant ansatz of period `L` can only capture states whose own spatial period divides `L`. +Choosing a cell that is commensurate with the physical period of the model — the magnetic period of an ordered phase, or a period imposed by the Hamiltonian's own unit cell — is therefore a modelling decision, not a numerical one, and picking too small a cell forces the algorithm to approximate a state it structurally cannot represent. +A [`FiniteMPS`](@ref), by contrast, has no notion of a unit cell at all: its `length` is simply the number of physical sites, and each of those sites carries its own independent tensor. + +## Why an infinite MPS is normalized to one + +The sharpest practical consequence of the finite/infinite distinction shows up in the norm, and it is worth understanding rather than memorizing. + +For a [`FiniteMPS`](@ref) the norm is exactly the Euclidean norm `√⟨ψ|ψ⟩` of the state vector it represents — a genuine, finite number. +The space-based constructors normalize by default, so a freshly built state has norm one, but nothing forces that: the norm is a real degree of freedom you can set at will, and rescaling the state rescales it in the obvious way. + +```@example finite-infinite +ψ_finite = FiniteMPS(rand, ComplexF64, 16, ℂ^2, ℂ^8) +norm(ψ_finite) +``` + +```@example finite-infinite +norm(3 * ψ_finite) +``` + +For an [`InfiniteMPS`](@ref) that same quantity does not exist. +The overlap `⟨ψ|ψ⟩` of an infinite state is, formally, a product of one transfer-matrix factor per unit cell, so for a chain of `n` cells it grows (or decays) like `λⁿ`, where `λ` is the leading eigenvalue of the transfer matrix. +As `n → ∞` this is `0` if `λ < 1` and `∞` if `λ > 1`, and the *only* value that yields a finite, well-defined state is `λ = 1`. +MPSKit therefore fixes the gauge so that the transfer matrix has leading eigenvalue exactly one, which we can read straight off its spectrum: + +```@example finite-infinite +first(transfer_spectrum(ψ_infinite)) ≈ 1 +``` + +With that fixed, `norm` of an [`InfiniteMPS`](@ref) is defined *per site* rather than globally: it is the norm of a single center-gauged unit-cell tensor, and it is always one. + +```@example finite-infinite +norm(ψ_infinite) ≈ 1 +``` + +```@example finite-infinite +norm(ψ_infinite) ≈ norm(ψ_infinite.AC[1]) +``` + +Because the normalization is intensive, it does not grow with the unit cell: a two-site cell is normalized to one just as a one-site cell is. + +```@example finite-infinite +norm(ψ_cell) ≈ 1 +``` + +This is why scalar multiplication of an [`InfiniteMPS`](@ref) is simply not defined — there is no overall amplitude to rescale — and why every physically meaningful quantity in the infinite setting is a *density*. +The energy returned for the state is an energy per site, an order parameter is measured at one representative site of the cell, and quantities with no finite-chain analogue, such as the [`correlation_length`](@ref), are extracted from the transfer-matrix spectrum of the uniform state rather than from any global overlap. + +## The same algorithms, two settings + +Because the two types share the canonical-form vocabulary, most of MPSKit's high-level entry points accept either one, and it is the *algorithm* passed to them that is specialized to the finite or the infinite case. +Ground-state search is the clearest example: [`find_groundstate`](@ref) dispatches on the state it is handed, running [`DMRG`](@ref) — which sweeps back and forth across a chain with two ends — for a [`FiniteMPS`](@ref), and [`VUMPS`](@ref) or [`IDMRG`](@ref)/[`IDMRG2`](@ref) — which converge a single uniform unit cell — for an [`InfiniteMPS`](@ref). +The distinction is not incidental: a boundary-sweeping method like DMRG has no meaning without ends to sweep between, while VUMPS' re-gauging step, which replaces every tensor in the chain at once, only makes sense for a genuinely translation-invariant state. +Some routines instead span both worlds: [`TDVP`](@ref) time-evolves finite and infinite states alike, its two-site bond-growing variant existing only for the finite case. +For which algorithm fits which task — and why one is preferred over another within each column — see [The algorithm landscape](@ref concept_algorithm_landscape). + +Two further state types sit between the finite and infinite poles rather than at them, reusing the same machinery: a [`WindowMPS`](@ref) embeds a finite, mutable window inside two infinite environments, and a [`MultilineMPS`](@ref) stacks several infinite states to represent two-dimensional networks. +Both are introduced in [Constructing states](@ref howto_states). + +## Where to go next + +- For the flagship finite-then-infinite walkthrough of the same model, see [The thermodynamic limit](@ref tutorial_thermodynamic_limit). +- For the gauge and canonical-form machinery both types share, see [Matrix product states](@ref concept_matrix_product_states). +- For choosing the right algorithm in each setting, see [The algorithm landscape](@ref concept_algorithm_landscape). +- For how to construct each state type, see [Constructing states](@ref howto_states); for type signatures, the [States](@ref lib_states) reference. +``` diff --git a/docs/src/concepts/matrix_product_states.md b/docs/src/concepts/matrix_product_states.md new file mode 100644 index 000000000..89eabfa25 --- /dev/null +++ b/docs/src/concepts/matrix_product_states.md @@ -0,0 +1,130 @@ +```@meta +DocTestSetup = quote + using MPSKit, TensorKit +end +``` + +# [Matrix product states](@id concept_matrix_product_states) + +A matrix product state (MPS) represents the wavefunction of a one-dimensional quantum system as a chain of tensors, one per site, contracted along shared *virtual* bonds. +The physical indices carry the local degrees of freedom, while the virtual bonds carry the entanglement between the two halves of the system that meet at that bond. + +```@raw html +A finite MPS drawn as a chain of tensors, each with one physical leg pointing out and virtual legs joining it to its neighbours. +``` + +*The diagram shows a finite MPS as a row of site tensors, each carrying a physical index and linked to its neighbours through virtual bonds; the two ends carry trivial (dimension-one) boundary bonds.* + +This page explains the *gauge freedom* inherent in that representation and the *canonical forms* MPSKit uses to fix it, so that the `AL`, `AR`, `C`, and `AC` you see throughout the API stop looking like arbitrary labels. +It is about understanding rather than construction: for how to *build* a state see [Constructing states](@ref howto_states), and for the full type signatures see the [States](@ref lib_states) reference. + +## Gauge freedom + +The tensors that make up an MPS are not uniquely determined by the physical state they encode. +On any virtual bond you can insert an invertible matrix `C` together with its inverse `C⁻¹`, since their product is the identity and leaves the contracted network unchanged. +Absorbing `C` into the tensor on one side of the bond and `C⁻¹` into the tensor on the other redefines both local tensors while representing exactly the same physical state. + +```@raw html +Inserting C times its inverse on a virtual bond and reabsorbing each factor into the neighbouring tensor, leaving the physical state unchanged. +``` + +*The diagram shows an identity `C · C⁻¹` inserted on a virtual bond, with each factor then absorbed into the tensor on its side of the bond — a change of representation that leaves the physical state untouched.* + +This freedom is not a nuisance to be tolerated; it is a resource. +Because the local tensors can be reshaped at will, we can choose the gauge on every bond to give the tensors especially convenient properties, without ever changing the state they describe. +The two choices below are the ones that matter in practice. + +## Canonical forms + +At each site there are two particularly convenient gauges, the *left*- and *right-canonical* forms. + +In the left-canonical form a site tensor is a **left isometry**: contracting it with its own conjugate over the left virtual and physical indices yields the identity on the right virtual space. +By convention these tensors are called `AL`. + +```jldoctest mps_states +julia> state = FiniteMPS(rand, ComplexF64, 10, ℂ^2, ℂ^4); + +julia> al = state.AL[3]; + +julia> al' * al ≈ id(right_virtualspace(al)) +true +``` + +In the right-canonical form a site tensor is instead a **right isometry**, an identity when contracted over its right virtual and physical indices; these are called `AR`. +The check uses TensorKit's `repartition` to regroup the tensor's indices so that the isometry contraction can be written directly. + +```jldoctest mps_states +julia> ar = state.AR[3]; + +julia> repartition(ar, 1, 2) * repartition(ar, 1, 2)' ≈ id(left_virtualspace(ar)) +true +``` + +The two forms can be mixed: every tensor to the left of a chosen bond is put in the left gauge and every tensor to its right in the right gauge. +The gauge transformation sitting on that one bond can no longer be absorbed without spoiling the isometry property on one side, so it remains as an explicit **center bond tensor** `C`. +`C` is exactly the transformation that relates the left- and right-gauged tensors across its bond. +For convenience a single site tensor can also be left in the *center-site* form `AC`, which is the center tensor absorbed into the neighbouring isometry from either side: + +```jldoctest mps_states +julia> al * state.C[3] ≈ state.AC[3] +true +``` + +Equivalently, absorbing the center tensor on bond `2` into the right isometry at site `3` reproduces the same center-site tensor: + +```jldoctest mps_states +julia> repartition(state.C[2] * repartition(ar, 1, 2), 2, 1) ≈ state.AC[3] +true +``` + +These relations — `AL' * AL = 1`, `AR * AR' = 1`, and `AC = AL · C = C · AR` — hold for any validly gauged MPS, which is why the checks above return `true` even for a random state. + +## Automatic gauge management + +MPS algorithms move through these forms constantly: a DMRG sweep, for instance, carries the center site across the chain, gauging each tensor as it goes. +Doing that bookkeeping by hand would be tedious and error-prone, so the state objects do it for you. +A [`FiniteMPS`](@ref) (and likewise an [`InfiniteMPS`](@ref)) behaves as an automatic gauge manager: querying `state.AL`, `state.AR`, `state.C`, or `state.AC` returns the requested form, computing and caching it on demand and recomputing it when the underlying tensors have changed. +The intended experience is that you never think about how the state is gauged — it is handled automagically. + +!!! warning "In-place mutation defeats the cache" + A `FiniteMPS` detects that a form needs recomputing only when a tensor is *replaced* through an indexing assignment. + Changing a tensor's data in place keeps the same object, so the automatic recomputation is not triggered and stale gauged tensors may be returned. + Assign a new tensor rather than mutating an existing one. + +### The center-gauge overlap insight + +The payoff of the mixed gauge is visible in a computation as basic as the norm. +To compute the overlap of a state with itself, bring any bond into the center gauge. +Everything to the left of that bond is built from left isometries and contracts to the identity, everything to the right is built from right isometries and does the same, and the entire network collapses to the overlap of the center bond tensor `C` with itself. +The overlap is therefore the same whichever bond you pick: + +```jldoctest mps_states +julia> using LinearAlgebra + +julia> d = dot(state, state); + +julia> all(c -> dot(c, c) ≈ d, state.C) +true +``` + +This is not a special trick for the norm; the same collapse-to-the-center reasoning is what makes environments (see [Environments](@ref concept_environments)) and local expectation values cheap to evaluate in the canonical gauge. + +## Finite versus infinite gauging + +The gauge machinery is shared between finite and infinite states, but the way the forms are kept current differs, because the two have very different structure. + +A [`FiniteMPS`](@ref) has genuine boundaries and mutable per-site tensors, so it gauges *lazily*: each form is recomputed only for the tensors it actually depends on, and invalidation is decided by object identity (`===`) — replacing a tensor marks the left-gauged tensors to its right and the right-gauged tensors to its left as stale, leaving the rest cached. +An [`InfiniteMPS`](@ref) instead repeats a finite unit cell periodically, so there is no left or right end to anchor a partial recompute: every tensor lies both to the right and to the left of any change, and all forms are recomputed together whenever a tensor changes. + +## Variants + +Two further state types reuse the same canonical-form vocabulary for more specialized settings: + +- A [`WindowMPS`](@ref) represents a finite window of mutable tensors embedded in an infinite environment on both sides — a finite region living inside two [`InfiniteMPS`](@ref) tails. +- A [`MultilineMPS`](@ref) is a stack of [`InfiniteMPS`](@ref) objects used to represent the two-dimensional networks that arise in boundary-MPS methods. + +## Where to go next + +- To build states from tensors, from spaces, or as product states, see [Constructing states](@ref howto_states). +- For the full type signatures and docstrings, see the [States](@ref lib_states) reference. +- For how the canonical gauge makes contractions cheap, see [Environments](@ref concept_environments). diff --git a/docs/src/man/mps_gauge_freedom.png b/docs/src/concepts/mps_gauge_freedom.png similarity index 100% rename from docs/src/man/mps_gauge_freedom.png rename to docs/src/concepts/mps_gauge_freedom.png diff --git a/docs/src/concepts/numerics.md b/docs/src/concepts/numerics.md new file mode 100644 index 000000000..541fa8763 --- /dev/null +++ b/docs/src/concepts/numerics.md @@ -0,0 +1,153 @@ +```@meta +DocTestSetup = quote + using MPSKit, TensorKit +end +``` + +# [Numerical considerations](@id concept_numerics) + +Every MPSKit calculation is an approximation controlled by a handful of numerical knobs, and understanding what those knobs actually measure is what separates a trustworthy result from a plausible-looking one. +A ground state is only approached to a finite tolerance; a bond dimension only captures so much entanglement; a floating-point number only stores so many digits. +This page explains the three quantities that govern accuracy — the *truncation error* that bounds how well the ansatz can represent a state, the *convergence criterion* that tells an iterative algorithm when to stop, and the *precision* of the underlying element type — and then surveys the failure modes that these considerations give rise to. +It is about understanding *why* a calculation is or is not accurate; for the diagnostic recipe when one goes wrong, follow the links into [Convergence troubleshooting](@ref howto_convergence_troubleshooting). + +## Truncation and the bond dimension + +The single most important approximation in the whole framework is truncation. +An MPS represents the wavefunction as a chain of tensors joined along virtual bonds, and the dimension of those bonds — the *bond dimension* — is the ansatz's capacity: it is the number of Schmidt coefficients kept when the state is split into two halves at that bond. +Cutting the chain at one bond and performing a singular value decomposition of the resulting bipartition yields exactly the Schmidt decomposition, whose singular values are the Schmidt coefficients. +Keeping only the largest of them is the truncation, and the bond dimension is the number kept. + +The quality of that truncation is measured by the *discarded weight*: the sum of the squares of the Schmidt coefficients that were thrown away. +Because the Schmidt coefficients of a normalized state satisfy ``\sum_i \lambda_i^2 = 1``, the discarded weight is the fraction of the state's norm that the truncation sacrifices, and it is the natural error measure of the approximation. +A small discarded weight means the kept bond dimension already captures almost all of the state's entanglement across that cut, so enlarging it further buys little. + +We can watch this directly. +After optimizing a ground state we read off its Schmidt spectrum at the central bond with [`entanglement_spectrum`](@ref), and see how quickly the coefficients decay: + +```@example numerics +using MPSKit, MPSKitModels, TensorKit +ψ = FiniteMPS(16, ℂ^2, ℂ^24) +H = transverse_field_ising(FiniteChain(16); g = 1.0) +ψ, envs, ϵ = find_groundstate(ψ, H, DMRG(; tol = 1e-10, verbosity = 0)) +schmidt = sort(collect(entanglement_spectrum(ψ, 8)); rev = true) +round.(schmidt[1:6]; digits = 4) +``` + +The tail beyond the first few coefficients is tiny, so truncating the bond back down to keep only the six largest discards only a small weight: + +```@example numerics +discarded = sum(abs2, schmidt[7:end]) +``` + +Performing that truncation with [`SvdCut`](@ref) through [`changebonds`](@ref) and comparing the energy before and after shows the corresponding cost in the observable of interest: + +```@example numerics +ψcut = changebonds(ψ, SvdCut(; trunc = truncrank(6))) +ΔE = real(expectation_value(ψcut, H) - expectation_value(ψ, H, envs)) +``` + +The bond dimension is thus a genuine accuracy/cost dial: a larger bond dimension lowers the discarded weight and the truncation error, at the price of more expensive tensor contractions. + +How much bond dimension a state actually *needs* is set by its entanglement. +Ground states of gapped, local one-dimensional Hamiltonians obey an entanglement *area law* — their bipartite entanglement entropy saturates to a constant as the system grows — which is precisely why a finite bond dimension can represent them efficiently; at a critical point the entropy instead grows without bound and no fixed bond dimension suffices. + +### Choosing what to truncate + +The rule for *which* coefficients to discard is a truncation scheme. +MPSKit itself does not define these; they come from the tensor backend, so the schemes below require `using TensorKit` (which re-exports them from `MatrixAlgebraKit`) rather than `using MPSKit` alone. +The ones you will meet most often are: + +- `truncrank(n)` — keep a fixed number of coefficients (a hard bond-dimension cap). +- `trunctol(; atol)` — discard every coefficient below a threshold. +- `truncerror(; atol)` — keep as many coefficients as needed to hold the discarded weight below a target. +- `truncspace(V)` — truncate to a prescribed vector space, used mostly internally to match bond spaces. +- `notrunc()` — keep everything; this is the default `trunc` of the bond-preserving single-site algorithms. + +These schemes compose with `&`, so `trunctol(; atol = 1e-8) & truncrank(16)` applies both bounds at once. + +Every bond-growing algorithm — [`DMRG2`](@ref), [`IDMRG2`](@ref), [`TDVP2`](@ref) — and every explicit bond-surgery tool — [`SvdCut`](@ref), [`OptimalExpand`](@ref) — requires a `trunc` keyword, because their whole job is to decide a new bond dimension. +The single-site workhorses ([`DMRG`](@ref), [`VUMPS`](@ref), [`TDVP`](@ref)) default to `notrunc()` and keep the bond dimension fixed. +The recipes for growing and shrinking bonds live in [Controlling bond dimension](@ref howto_bond_dimension). + +## Convergence criteria + +Every iterative algorithm needs a rule for when it has done enough, and "converged" means something specific and measurable rather than "looks stable." +For the single-site variational algorithms the measure is the **Galerkin error**: the norm of the component of the local energy gradient that points out of the current tangent space of the MPS. +Intuitively, it is how far the exact update at a site wants to push the state in a direction the fixed-bond-dimension ansatz cannot follow; when it is small everywhere, the state is a fixed point of the update to within the ansatz's reach. +This is the quantity [`DMRG`](@ref), [`VUMPS`](@ref), and [`VOMPS`](@ref) drive to zero, and it is returned to you as the third output of the entry point: + +```@example numerics +ϵ +``` + +That returned `ϵ` is the final Galerkin error, and convergence is declared when it drops below the algorithm's `tol`. +The default tolerance is `1e-10`, defined together with the other numerical defaults in the (public but unexported) `MPSKit.Defaults` module, alongside a default `maxiter` of `200` and a default Krylov dimension of `30`: + +```@example numerics +MPSKit.Defaults.tol +``` + +Not every algorithm reports the same measure, and the differences matter when comparing runs: + +- The two-site [`DMRG2`](@ref) does not use the Galerkin error during its sweep; it monitors instead the local infidelity between each two-site tensor before and after truncation, which is a different — and generally less directly interpretable — proxy for convergence. +- [`IDMRG`](@ref) and [`IDMRG2`](@ref) judge convergence by the change in the bond matrix between successive iterations, ``\lVert C - C_\text{old}\rVert``, rather than by a gradient norm. +- [`GradientGrassmann`](@ref) converges on the Riemannian gradient norm reported by its underlying optimizer, with `tol` passed through as the gradient tolerance. + +A subtlety worth knowing is that these tolerances are, by default, *dynamic*: MPSKit tightens the tolerances of the inner linear and eigenvalue solvers as the outer iteration converges, so that early iterations are not over-solved and late ones are not under-solved. +This adaptive behavior is on by default (`dynamic_tols = true` in `Defaults`) and is why the inner solvers do not simply run at the outer `tol` from the first sweep. + +The Galerkin error certifies that the algorithm reached a fixed point of *its own* update, which is necessary but not sufficient for the state to be a good eigenstate. +An independent check is the energy [`variance`](@ref) ``\langle H^2\rangle - \langle H\rangle^2``, which vanishes exactly for a true eigenstate and does not rely on the ansatz's tangent space: + +```@example numerics +variance(ψ, H, envs) +``` + +## Precision and the element type + +Underneath the tensors is an ordinary floating-point element type, and by default it is complex double precision, `ComplexF64`. +A randomly initialized state carries that type unless you ask for another: + +```@example numerics +scalartype(FiniteMPS(16, ℂ^2, ℂ^24)) +``` + +Double precision is the right default: at `Float64` the relative rounding error is about ``10^{-16}``, comfortably below the `1e-10` convergence tolerance, so floating-point noise is rarely what limits an MPSKit result — truncation and incomplete convergence dominate long before precision does. + +The choice between a real and a complex element type is occasionally load-bearing rather than cosmetic. +Real-time evolution and the ``W^{II}`` time-evolution MPO intrinsically require complex arithmetic, so a real-valued state must be promoted before it can be evolved; MPSKit provides `Base.complex` on an MPS for exactly this, and it is a no-op when the state is already complex. +Conversely, a purely real problem — a real Hamiltonian with a real ground state — can in principle be run in `Float64` to save memory and time, but this is an optimization to reach for deliberately, not the default. + +## Common pitfalls + +Most non-convergence has one of a small number of causes, and recognizing them conceptually is half the battle; the concrete diagnostics are collected in [Convergence troubleshooting](@ref howto_convergence_troubleshooting). + +**Too small a bond dimension.** +If the state genuinely needs more entanglement than the bond dimension can hold, no amount of iterating will converge it — the discarded weight is bounded away from zero by the ansatz itself. +This is aggravated by the single-site algorithms, which cannot enlarge the bond dimension: with a symmetry, a single-site sweep freezes not only the total bond dimension but its distribution over charge sectors, so a poor initial distribution cannot be repaired without a two-site pass or an explicit [`changebonds`](@ref) expansion. + +**Local minima.** +The variational optimization is non-convex, and an algorithm can settle into a state that is a fixed point of its update but not the global ground state. +The single-site methods are more prone to this than bond-growing ones, which is part of why a two-site warm-up (or a gradient-descent polishing stage, chained with `&`) is often used before or after a single-site run. + +**Symmetry-sector trapping.** +When the state carries a conserved quantum number, the optimization runs within a fixed set of symmetry sectors on each bond. +If the true ground state lives in a sector distribution the initial state does not span, the algorithm converges — cleanly, by its own criterion — to the best state in the *wrong* variational space. +This is a sharper, symmetry-specific version of the too-small-bond-dimension trap, and it is why the sector structure of the initial state matters. + +**Non-injectivity and a near-degenerate transfer matrix (infinite systems).** +[`VUMPS`](@ref) assumes a unique, injective fixed point. +When the state it should converge to is non-injective — for instance a cat state superposing symmetry-broken sectors, or a genuinely degenerate ground space — the transfer matrix has more than one eigenvalue of magnitude one, and the algorithm has no well-defined single fixed point to find. +MPSKit's [`correlation_length`](@ref) machinery detects this: it is computed from the gap between the leading and next-to-leading transfer-matrix eigenvalues (the correlation length is the inverse of that gap), and [`transfer_spectrum`](@ref) exposes the spectrum directly. +Internally the routine emits a `"Non-injective mps?"` warning when it finds more than one eigenvalue near magnitude one at the same complex angle — a heuristic flag, not a hard error, so it is worth watching for. + +**Finite-entanglement effects at criticality.** +At or near a critical point the true correlation length diverges, but a finite bond dimension can only support a finite correlation length, so the simulated correlation length saturates at a value set by the bond dimension rather than by the physics. +Extracting critical data therefore requires studying how results drift as the bond dimension grows, rather than trusting any single bond dimension. + +## Where to go next + +For the step-by-step diagnosis of a calculation that will not converge, see [Convergence troubleshooting](@ref howto_convergence_troubleshooting). +For the mechanics of changing the bond dimension, see [Controlling bond dimension](@ref howto_bond_dimension), and for the reasoning behind each algorithm's convergence behavior see [The algorithm landscape](@ref concept_algorithm_landscape). +The full signatures of the diagnostic functions named here — [`entanglement_spectrum`](@ref), [`entropy`](@ref), [`variance`](@ref), [`correlation_length`](@ref), and [`transfer_spectrum`](@ref) — are in the library reference. diff --git a/docs/src/concepts/operators_and_hamiltonians.md b/docs/src/concepts/operators_and_hamiltonians.md new file mode 100644 index 000000000..469d90b58 --- /dev/null +++ b/docs/src/concepts/operators_and_hamiltonians.md @@ -0,0 +1,149 @@ +# [Operators and Hamiltonians](@id concept_operators_and_hamiltonians) + +Just as a matrix product state factorises a wavefunction into a chain of local tensors, an operator on a one-dimensional system can be factorised in exactly the same way. +The result is a *matrix product operator* (MPO): the operator analogue of an MPS. +This page explains what that factorisation is, why the local tensors are not unique, and — above all — the particular upper-triangular *Jordan-block* structure that lets a sum of local terms be written as a single MPO. +It is about understanding rather than construction: for how to *build* a Hamiltonian see [Building Hamiltonians](@ref howto_hamiltonians), and for the full type signatures see the [Operators](@ref lib_operators) reference. + +## What an MPO is + +An MPO is a collection of local [`MPOTensor`](@ref MPSKit.MPOTensor) objects contracted along a line. +Where an MPS site tensor has one physical leg and two virtual legs, an MPO site tensor has *two* physical legs — one incoming and one outgoing — because it maps states to states, and again two virtual legs that thread the operator together along the chain. + +```@raw html +An MPO drawn as a chain of tensors, each with an incoming and an outgoing physical leg and virtual legs joining it to its neighbours. +``` + +*The diagram shows an MPO as a row of site tensors, each carrying a pair of physical indices (one in, one out) and linked to its neighbours through virtual bonds.* + +As with states, the construction comes in a finite and an infinite flavour. +A [`FiniteMPO`](@ref) is a plain vector of `MPOTensor` objects with trivial (dimension-one) virtual spaces at the two ends, so that the network describes a genuine operator on a finite chain. +An [`InfiniteMPO`](@ref) instead repeats a finite unit cell periodically, and is therefore stored as a periodic array of `MPOTensor` objects rather than an ordinary vector. + +### Gauge non-uniqueness + +The local tensors of an MPO are not uniquely determined by the operator they encode. +Exactly as for an MPS, an invertible gauge transformation can be inserted on any virtual bond and reabsorbed into the two neighbouring tensors without changing the contracted network. +The individual site tensors are therefore defined only up to this virtual-space gauge freedom. + +!!! warning "Element-wise comparison is unsafe" + Because two different sets of local tensors can represent the very same operator, comparing MPOs tensor-by-tensor is not meaningful. + Test for equality through gauge-invariant quantities instead. + +### Products and sums grow the virtual dimension + +MPOs support the usual linear-algebra operations — addition, subtraction, and multiplication, either among themselves or acting on an MPS. +Each such operation combines the virtual spaces of its operands, so the virtual dimension of the result is (generically) the *product* or *sum* of the input dimensions rather than staying fixed. +Composing operators naively therefore makes the representation grow, and the growth compounds under repeated multiplication. +This growth is precisely what motivates the *approximate* algorithms that re-express a product or sum within a bounded virtual dimension; see the [algorithm landscape](@ref concept_algorithm_landscape) for where those methods fit. + +## MPO Hamiltonians and the Jordan-block form + +A quantum Hamiltonian is a *sum* of local terms rather than a single dense operator, yet it too can be written as one MPO. +The trick is a characteristic upper-triangular block structure, so distinctive that the resulting object is usually called a *Jordan-block MPO*. +In MPSKit this is the [`MPOHamiltonian`](@ref) family — [`FiniteMPOHamiltonian`](@ref) and [`InfiniteMPOHamiltonian`](@ref) — and it is what the Hamiltonian constructors assemble under the hood. + +In its most general form, the per-site block matrix ``W`` reads + +```math +W = \begin{pmatrix} +1 & C & D \\ +0 & A & B \\ +0 & 0 & 1 +\end{pmatrix} +``` + +where the corner entries `1` are identity operators and ``A``, ``B``, ``C``, ``D`` are (blocks of) local operators. +The Hamiltonian on ``N`` sites is recovered by contracting one copy of ``W`` per site between two boundary vectors, + +```math +v_L = \begin{pmatrix} 1 & 0 & 0 \end{pmatrix}, +\qquad +v_R = \begin{pmatrix} 0 \\ 0 \\ 1 \end{pmatrix}, +\qquad +H = V_L\, W^{\otimes N}\, V_R . +``` + +### A finite-state automaton + +The upper-triangular shape makes ``W`` behave like a finite-state automaton that reads the chain from left to right. +The boundary vector ``v_L`` starts in the top-left "identity" state; the automaton may stay there (the leading `1`), it may *finish* immediately by placing a single-site term through ``D``, or it may *start* an interaction through ``C``, propagate it across intermediate sites through ``A``, and *close* it through ``B`` into the bottom-right "identity" state selected by ``v_R``. +Every complete left-to-right path through the block matrix contributes one term of the Hamiltonian: + +- ``D`` alone generates the single-site terms, +- ``C \cdot B`` generates the two-site terms, +- ``C \cdot A \cdot B`` generates the three-site terms, +- and in general ``C \cdot A^{k} \cdot B`` generates a term spanning ``k+2`` sites. + +The repeated ``A`` block is what makes longer-range interactions possible at fixed virtual dimension: choosing ``A`` to be (a multiple of) the identity gives every additional site the same weight, while a decaying ``A`` gives geometrically decaying couplings. +A sum of such geometric series can approximate a power-law interaction to any desired accuracy, which is how (exponentially decaying) infinite-range and approximate power-law couplings are represented. + +### The transverse-field Ising Hamiltonian + +For the [transverse-field Ising model](https://en.wikipedia.org/wiki/Transverse-field_Ising_model), + +```math +H = -J \sum_{\langle i, j \rangle} X_i X_j - h \sum_j Z_j , +``` + +the block matrix specialises to + +```math +W = \begin{pmatrix} +1 & X & -hZ \\ +0 & 0 & -JX \\ +0 & 0 & 1 +\end{pmatrix} . +``` + +Here ``D = -hZ`` is the single-site field term, and the nearest-neighbour coupling ``-J X_i X_{i+1}`` is produced by ``C = X`` on one site meeting ``B = -JX`` on the next. +The middle block ``A = 0`` truncates the automaton after two sites, which is exactly what a nearest-neighbour model needs — there are no longer-range paths. + +### Verifying the expansion symbolically + +Because ``H = V_L\, W^{\otimes N}\, V_R`` is just repeated matrix multiplication, the term-generation rule above can be checked with a symbolic algebra system. +Filling ``W`` with abstract symbols ``A``, ``B``, ``C``, ``D`` on each site and expanding the product exposes exactly which combinations survive. + +```@example operators +using Symbolics +L = 4 +# generate W matrices, one per site +@variables A[1:L] B[1:L] C[1:L] D[1:L] +Ws = map(1:L) do l + return [1 C[l] D[l] + 0 A[l] B[l] + 0 0 1] +end + +# left and right boundary vectors +Vₗ = [1, 0, 0]' +Vᵣ = [0, 0, 1] + +# expand the contraction H = V_L W^{⊗L} V_R +expand(Vₗ * prod(Ws) * Vᵣ) +``` + +Reading off the result, the lone ``D`` terms are the single-site contributions, the ``C \cdot B`` products are the two-site terms, the ``C \cdot A \cdot B`` products are the three-site terms, and so on — precisely the automaton paths described above. + +## Sparse and block structure + +Because an [`MPOHamiltonian`](@ref) is an MPO with the extra Jordan-block structure, its virtual space is not a single space but a *direct sum* of spaces, one for each row (or column) of the block matrix ``W``. +The site tensors are therefore stored as [`BlockTensorMap`](@extref BlockTensorKit.BlockTensorMap) objects rather than ordinary dense tensor maps, with each block occupying one cell of the ``W`` matrix. +MPSKit exposes the specialised [`JordanMPOTensor`](@ref) for exactly this layout. + +!!! note "Sparsity is what keeps it efficient" + Most cells of ``W`` are zero — the whole lower-left triangle, and typically much of the interior ``A`` block. + Storing only the non-zero blocks is what makes the Jordan-block representation compact, so the cost tracks the number of distinct interaction terms rather than the nominal size of ``W``. + +The [`JordanMPOTensor`](@ref) type and its internal accessors are implementation detail and may change; treat them as unstable and prefer the public constructors and `@ref`-documented interface. + +## Beyond nearest-neighbour, 1D chains + +The same machinery is not limited to nearest-neighbour couplings or to strictly one-dimensional systems: quasi-1D cylinders and 2D lattices are obtained by snaking the MPO through a multi-dimensional array of physical spaces, and longer-range interactions slot into the ``A`` block as described above. +See [Building Hamiltonians](@ref howto_hamiltonians) for the construction recipes and [MPSKitModels.jl](https://quantumkithub.github.io/MPSKitModels.jl/dev/) for ready-made lattices and models. + +## Where to go next + +- To build Hamiltonians from local terms, in 1D or on lattices, see [Building Hamiltonians](@ref howto_hamiltonians). +- For the full type signatures and docstrings, see the [Operators](@ref lib_operators) reference. +- For the approximate algorithms that keep MPO products and sums bounded, see the [algorithm landscape](@ref concept_algorithm_landscape). diff --git a/docs/src/concepts/parallelism_model.md b/docs/src/concepts/parallelism_model.md new file mode 100644 index 000000000..e8e3bccbc --- /dev/null +++ b/docs/src/concepts/parallelism_model.md @@ -0,0 +1,73 @@ +# [The parallelism model](@id concept_parallelism_model) + +Julia has excellent [parallelism infrastructure](https://julialang.org/blog/2019/07/multithreading/), +but there is a caveat that touches every algorithm in MPSKit: Julia's own threads do not +compose cleanly with the threads that BLAS uses internally for linear algebra. +Since `gemm` (general matrix-matrix multiplication) is a core routine throughout MPSKit, +this interaction has a real effect on performance. + +This page explains the model behind the settings, so that the recipes on +[Parallelism and GPU support](@ref howto_parallelism_gpu) are more than a list of magic +incantations. + +## Julia threads versus BLAS threads + +Much of the confusion here comes from the fact that BLAS threading behaviour is not +consistent between vendors, and that performance depends strongly on the hardware, the +specifics of the problem, and the availability of resources such as total memory and memory +bandwidth. +There is no one-size-fits-all setting, which is why the how-to page frames its advice as +starting points to be measured rather than guarantees. + +The two vendors most commonly used with Julia treat the BLAS thread count differently. +With OpenBLAS (the default), the configured number of BLAS threads is the **total** size of a +single thread pool that is shared by all Julia threads: 4 Julia threads and 4 BLAS threads +means all 4 Julia threads draw from the same pool of 4 BLAS threads. +Setting the BLAS thread count to `1` instead frees OpenBLAS to run its work on the Julia +threads themselves, so that MPSKit's Julia-level parallelism is the thing that scales. + +With [MKL.jl](https://github.com/JuliaLinearAlgebra/MKL.jl), which often outperforms OpenBLAS, +the count is instead the number of threads spawned by **each** Julia thread: 4 Julia threads +with 4 BLAS threads each gives 16 BLAS threads in total. +Getting this wrong oversubscribes the physical cores — more software threads than hardware +can run — which degrades rather than improves performance. + +## Where MPSKit parallelizes + +When Julia is started with more than one thread, MPSKit uses +[OhMyThreads.jl](https://juliafolds2.github.io/OhMyThreads.jl/stable/) to parallelize its +algorithms wherever possible. +In practice this happens where a unit cell (or a chain of sites) lets local updates run +independently: the work is distributed across the sites of the system, with the tensor at +each site updated in parallel. +This is exactly why setting the BLAS thread count to `1` on OpenBLAS tends to help: it keeps +the Julia threads free to work through the sites, rather than contending with a shared BLAS +pool. + +The amount of speedup you can expect therefore tracks how much independent per-site work an +algorithm exposes. + +## Parallelism over symmetry sectors + +There is a second, orthogonal layer of parallelism for tensors that carry an internal +symmetry. +Such tensors are block-diagonal over their symmetry sectors, and the work can be spread +across those blocks. +This is handled by [TensorKit](https://quantumkithub.github.io/TensorKit.jl/stable/) at the +level of the individual tensor operations, below MPSKit's site-level parallelism, so the two +layers are independent of one another. + +## Why memory pressure arises + +The same task-based parallelism that speeds MPSKit up can also drive its memory usage high. +The algorithms spawn tasks in a nested fashion, and each of those tasks allocates and +deallocates a fair amount of memory in a tight loop. +This can produce enough garbage, quickly enough, that the garbage collector cannot keep up; +in the worst case memory is exhausted and an `OutOfMemory` error is thrown before the garbage +can be cleared. + +The most memory-intensive step is reportedly the application of the `derivatives` — the +effective local operators built during the sweeps — which is why the practical mitigation is +to disable MPSKit's multithreading there. +The concrete recipe for doing so is on +[Parallelism and GPU support](@ref howto_parallelism_gpu). diff --git a/docs/src/concepts/symmetries.md b/docs/src/concepts/symmetries.md new file mode 100644 index 000000000..10e75c695 --- /dev/null +++ b/docs/src/concepts/symmetries.md @@ -0,0 +1,151 @@ +```@meta +DocTestSetup = quote + using MPSKit, MPSKitModels, TensorKit +end +``` + +# [Symmetries](@id concept_symmetries) + +[TensorKit for MPS users](@ref concept_vector_spaces) already showed the key fact: a symmetry in MPSKit is not a flag passed to an algorithm, it is a property of the vector *spaces* that a tensor is built from, and every algorithm is written once, generically, for any such space. +That page built the mental model of a `TensorMap` and introduced graded spaces through a single ℤ₂ example. +This page stays at the same level of abstraction but widens the lens: what kinds of symmetry a graded space can encode, what changes qualitatively as you move from an abelian group to a non-abelian one or to fermionic or anyonic statistics, and — the question every user eventually asks — when the extra bookkeeping of a bigger symmetry group is actually worth it. +For the hands-on version of the ℤ₂ case worked all the way through a ground-state search, see [Using symmetries](@ref tutorial_using_symmetries); this page explains the reasoning behind that recipe and extends it to the other symmetry classes MPSKit supports. + +As on the sibling page, none of the symmetry machinery lives in `MPSKit` or `MPSKitModels` itself: `using MPSKit` does not bring a single sector or space type into scope, and neither does `using MPSKitModels`. +Every symmetric object — `Z2Irrep`, `U1Irrep`, `SU2Irrep`, `Z2Space`, `U1Space`, and so on — comes from `TensorKit`, so every example on this page loads all three packages explicitly. + +```@example symmetries +using MPSKit, MPSKitModels, TensorKit +``` + +## Sectors, charges, and block-sparsity + +A **sector** is a label for an irreducible representation of the symmetry: for a group symmetry it is one irrep, and a graded space is built by declaring how many copies ("degeneracy" or "multiplicity") of each sector it contains. +[TensorKit for MPS users](@ref concept_vector_spaces) did this for ℤ₂ with `Z2Space(0 => 1, 1 => 1)`; the same `sector => degeneracy` syntax works for every symmetry, so a U(1)-graded space that keeps track of, say, a conserved particle number or magnetization from `-1` to `1` reads: + +```@example symmetries +V = U1Space(-1 => 1, 0 => 1, 1 => 1) +dim(V) +``` + +The space still knows its full sector content, queryable with [`sectors`](https://quantumkithub.github.io/TensorKit.jl/stable/) and [`dim`](https://quantumkithub.github.io/TensorKit.jl/stable/) applied to a specific sector: + +```@example symmetries +collect(sectors(V)) +``` + +```@example symmetries +[dim(V, c) for c in sectors(V)] +``` + +Charge conservation is the statement that a symmetric tensor may only have nonzero entries between sectors whose charges add up correctly (for a Hamiltonian term, incoming and outgoing charge must match). +Concretely this means the tensor is **block-diagonal** in the sector label: what would be one dense array for a plain `ℂ^n` space becomes a handful of smaller, independent dense blocks, one per allowed sector combination, and the entries that connect different sectors are not merely zero — they are never allocated or touched at all. +This is the mechanism behind everything that follows: the *type* of symmetry only changes what the sector labels are and how they combine (their *fusion rules*); the block-sparse storage and the charge-conservation bookkeeping are handled identically underneath. + +## A taxonomy of symmetry types + +MPSKitModels' [`heisenberg_XXX`](https://quantumkithub.github.io/MPSKitModels.jl/stable/) model is a convenient single thread through the taxonomy, because the same Heisenberg Hamiltonian can be built with a trivial symmetry or with any of the three main non-trivial types below, purely by passing a different sector type as the first argument: + +```@example symmetries +H_triv = heisenberg_XXX(FiniteChain(4); spin = 1 // 2) +H_Z2 = heisenberg_XXX(Z2Irrep, FiniteChain(4); spin = 1 // 2) +H_U1 = heisenberg_XXX(U1Irrep, FiniteChain(4); spin = 1 // 2) +H_SU2 = heisenberg_XXX(SU2Irrep, FiniteChain(4); spin = 1 // 2) +``` + +Four `Hamiltonian`s, four different tensor structures, one physical model. + +### Abelian symmetries: ℤ_N and U(1) + +`Z2Irrep`, `Z3Irrep`, `Z4Irrep`, and the general `ZNIrrep`, together with `U1Irrep`, are the abelian family: their sectors are literally the elements of ℤ_N or of the integers (or half-integers), and two sectors fuse by addition modulo N, or ordinary addition for U(1). +Every irrep is one-dimensional, so an abelian symmetry buys exactly the block-sparsity described above and nothing more: `H_Z2` above encodes the same spin-flip parity used throughout [Using symmetries](@ref tutorial_using_symmetries), while `H_U1` encodes conservation of total magnetization ``S^z_{\mathrm{tot}}``, with sectors running over the possible values of ``S^z_{\mathrm{tot}}``. +`transverse_field_ising` is a useful reminder that not every model has every symmetry available: it accepts `Trivial`, `Z2Irrep`, or `FermionParity`, but raises an error for `U1Irrep`, because the transverse-field Ising model genuinely only has the ℤ₂ spin-flip symmetry — there is no conserved U(1) charge to exploit. + +### Non-abelian symmetries: SU(2) + +`SU2Irrep` sectors are labelled by a total spin ``j = 0, \tfrac12, 1, \tfrac32, \dots``, and fusing two of them follows the angular-momentum addition (Clebsch–Gordan) rule rather than simple addition: fusing spin ``j_1`` and ``j_2`` can produce any ``j`` from ``|j_1-j_2|`` to ``j_1+j_2``. +The qualitative difference from the abelian case is that each sector ``j`` is not one-dimensional but ``(2j+1)``-dimensional, and a symmetric tensor need only store the multiplicity of each ``j`` — the internal ``(2j+1)`` structure of every multiplet is fixed by representation theory and is never stored explicitly. +`H_SU2` above is built exactly this way: it is the same Heisenberg chain, only now every eigenstate additionally carries a total-spin label, and the tensors only ever store one number per multiplet rather than one number per individual magnetic sublevel. +The next section makes this saving concrete. + +### Fermionic symmetries and product sectors + +`FermionParity` grades a space into an even and an odd fermion-number sector, and — crucially — TensorKit's fermionic tensor category attaches the anticommutation sign directly to the braiding of `FermionParity`-graded legs, so that once physical and virtual legs carry this sector, index permutations automatically pick up the correct fermionic signs instead of requiring the sign rule to be implemented by hand in every algorithm [mortier2025](@cite). +Models with more than one physical species combine sectors with `⊠` (typed `\boxtimes`) into a `ProductSector`, for instance an odd fermion paired with unit U(1) charge: + +```@example symmetries +FermionParity(1) ⊠ U1Irrep(1) +``` + +`hubbard_model` exercises this directly: it takes an independent *particle* symmetry and *spin* symmetry, and assembles the physical space internally out of `FermionParity ⊠ (particle symmetry)` and `FermionParity ⊠ (spin symmetry)` pieces. +Choosing U(1) for particle number and SU(2) for spin gives the maximally symmetric Hubbard chain: + +```@example symmetries +H_hub = hubbard_model(ComplexF64, U1Irrep, SU2Irrep, FiniteChain(4); t = 1.0, U = 8.0) +``` + +The first argument is the scalar element type, required here because a lattice is given explicitly; the two symmetry types then set the particle-number and spin symmetries in that order. + +By contrast, `bose_hubbard_model` only accepts `Trivial` or `U1Irrep`: bosons carry no parity grading, so there is no fermionic sign to encode and no spin degree of freedom to make non-abelian. +At the far end of this spectrum, `quantum_chemistry_hamiltonian` does not expose a symmetry choice at all — it always builds its tensors with the fixed, maximal ``U(1) \boxtimes SU(2) \boxtimes \mathrm{FermionParity}`` symmetry (particle number, total spin, and fermionic sign), because for realistic molecular Hamiltonians that full symmetry is essentially always worth imposing. + +### Anyonic symmetries + +The generality goes further than groups. +Sectors such as `FibonacciAnyon` or `IsingAnyon` are not group representations at all — their fusion rules come from a modular tensor category — yet because every MPSKit algorithm is written against the abstract `Sector` interface, they are handled by exactly the same code paths, with no special-casing. +The [hard-hexagon model](@ref "The Hard Hexagon model") example puts this to work: its transfer matrix is built from `FibonacciAnyon`-graded tensors (`Vect[FibonacciAnyon](:I => …, :τ => …)`), and the standard statistical-mechanics workflow computes its partition function just as it would for an ordinary symmetry. + +## When does SU(2) pay off + +Two distinct effects are at play whenever a symmetry is switched on, and it is worth separating them because only one of them scales with the size of the symmetry group. + +The first effect is the block-sparsity already described: at a fixed total bond dimension, the computer multiplies several smaller dense blocks instead of one large one, and the (forbidden) cross-sector entries are never stored. +[Using symmetries](@ref tutorial_using_symmetries) demonstrates this concretely for ℤ₂: the same 16-dimensional bond becomes two roughly-8-dimensional blocks. +This first effect is present for *any* symmetry, abelian or not, and its benefit grows with the number of distinct sectors the bond dimension gets spread over. + +The second effect is specific to non-abelian symmetries and is qualitatively larger: because a whole ``(2j+1)``-dimensional multiplet is represented by a single stored block, the *number of stored parameters* needed to reach a given *total*, physical bond dimension shrinks. +This can be checked directly: build a graded SU(2) space and compare its total dimension against the multiplicities it actually stores per sector. + +```@example symmetries +V_SU2 = SU2Space(0 => 2, 1 // 2 => 4) +dim(V_SU2) +``` + +```@example symmetries +[dim(V_SU2, c) for c in sectors(V_SU2)] +``` + +The total dimension `dim(V_SU2)` is `10`, because each spin-``j`` sector contributes its ``(2j+1)``-fold multiplet: ``2 \times (2\cdot 0 + 1) + 4 \times (2\cdot\tfrac12 + 1) = 2 + 8 = 10``. +But `dim(V_SU2, c)` returns the *stored* multiplicity of each sector — here `[2, 4]`, just six numbers in total — because the ``(2j+1)`` internal structure of every multiplet is fixed by representation theory and never stored. +For an abelian symmetry the two coincide (every irrep is one-dimensional, so the multiplicities and the total dimension agree, as with the U(1) space above); it is precisely for a non-abelian group that the stored count falls below the physical dimension. + +The gap between the physical dimension (`10`) and the six numbers actually stored is the source of SU(2)'s reputation for letting DMRG reach much larger effective bond dimensions at the same computational cost — the same principle used to push non-abelian symmetric uniform MPS to large SU(3) bond dimensions in practice [devos2022](@cite). + +None of this is free. +Every symmetric block carries the overhead of tracking fusion trees and recombining Clebsch–Gordan coefficients whenever legs are permuted or contracted, and for a non-abelian group this bookkeeping is genuinely more expensive per block than for an abelian one. +In practice this means SU(2) (or any non-abelian symmetry) is worth reaching for when the physics genuinely has that symmetry — a spin chain with full rotational invariance, for instance — and when the bond dimension is large enough that the multiplet-reduction saving dominates the per-block overhead; for small bond dimensions, or for a symmetry the Hamiltonian does not actually have, the abelian or even trivial case is often simpler and just as fast. + +## Fixing the total charge + +Sector labels are not only a storage optimization: they are physical quantum numbers, and MPSKit lets a calculation target a specific one directly. + +For an MPS, the total charge is fixed by giving the state a non-trivial `left` or `right` virtual space, rather than the default unit (trivial-charge) one: + +```@example symmetries +ψ_odd = FiniteMPS( + 4, Z2Space(0 => 1, 1 => 1), Z2Space(0 => 2, 1 => 2); + left = Z2Space(1 => 1) +) +left_virtualspace(ψ_odd, 1) +``` + +Every tensor in `ψ_odd` is now forced, by charge conservation, to represent a state of odd total parity — there is no way for a symmetric MPS built this way to drift into the even sector. +The same idea appears for excited states and for transfer-matrix spectra: the `sector` keyword of [`excitations`](@ref) and of `transfer_spectrum` restricts the search to a chosen total charge instead of the default trivial one, exactly as used to isolate the odd-parity excitation of the TFIM in [Using symmetries](@ref tutorial_using_symmetries). +[Excited states](@ref howto_excitations) collects further recipes for working with `sector`, and [Constructing states](@ref howto_states) collects the analogous recipes for building states with a prescribed symmetry and charge. + +## Where to go next + +- For the tensor mechanics underneath all of this — spaces, `TensorMap`s, index conventions — see [TensorKit for MPS users](@ref concept_vector_spaces). +- For the fully worked ℤ₂ example, from Hamiltonian to ground state to a sector-targeted excitation, see [Using symmetries](@ref tutorial_using_symmetries). +- For how symmetric tensors assemble into states and operators, see [Matrix product states](@ref concept_matrix_product_states) and [Operators and Hamiltonians](@ref concept_operators_and_hamiltonians). +- For task recipes that use a `sector` or a charged virtual space, see [Constructing states](@ref howto_states), [Excited states](@ref howto_excitations), and [Entanglement entropy and spectrum](@ref howto_entanglement). diff --git a/docs/src/concepts/vector_spaces.md b/docs/src/concepts/vector_spaces.md new file mode 100644 index 000000000..38beb36da --- /dev/null +++ b/docs/src/concepts/vector_spaces.md @@ -0,0 +1,179 @@ +```@meta +DocTestSetup = quote + using MPSKit, TensorKit +end +``` + +# [TensorKit for MPS users](@id concept_vector_spaces) + +Every tensor in MPSKit is a TensorKit [`TensorMap`](https://quantumkithub.github.io/TensorKit.jl/stable/), and this single choice is what makes the library generic over symmetry. +The same MPS and MPO code runs for a plain complex vector space, an abelian symmetry such as ℤ₂ or U(1), a non-abelian symmetry such as SU(2), and for fermionic or anyonic systems, because the symmetry lives inside the tensor rather than in the algorithms. +This page builds the mental model you need to read that API comfortably: what a `TensorMap` is, how its indices are typed by vector *spaces*, and the index conventions MPSKit adopts for its state and operator tensors. +It is about understanding rather than construction: once the tensors introduced here feel familiar they are put to work in [Matrix product states](@ref concept_matrix_product_states) and [Operators and Hamiltonians](@ref concept_operators_and_hamiltonians). + +## Tensors as linear maps + +The mental shift from a multi-dimensional array to a `TensorMap` is small but important. +An array is a bag of numbers indexed by integer sizes; a `TensorMap` is a **linear map** from one space to another, and its indices carry *types* — vector spaces — rather than bare sizes. +The legs are partitioned into a **codomain** (the outputs of the map) and a **domain** (its inputs), so that a tensor with codomain `W` and domain `V` is read as a map `W ← V`. + +Throughout this page we use a single running example: the two-dimensional complex space of a spin-1/2 degree of freedom. +It is written `ℂ^2` (the `ℂ` is typed `\bbC`), which constructs a [`ComplexSpace`](https://quantumkithub.github.io/TensorKit.jl/stable/) of dimension two. + +```jldoctest vspace +julia> V = ℂ^2 +ℂ^2 + +julia> dim(V) +2 +``` + +A space knows its dimension, and that dimension — not a Julia integer — is what a tensor's legs are built from. + +## Building tensors + +Constructing a tensor mirrors constructing an array, with the `axes`/`size` specifiers replaced by spaces. +The two most common constructors are `rand` and `zeros`, which take a scalar type followed by the codomain and domain. +The domain and codomain can be passed as two separate arguments, or joined with the `←` arrow (typed `\leftarrow`): + +```jldoctest vspace +julia> t = rand(Float64, V ⊗ V, V); + +julia> space(t) +(ℂ^2 ⊗ ℂ^2) ← ℂ^2 + +julia> codomain(t) +(ℂ^2 ⊗ ℂ^2) +``` + +Here `t` is a map from one spin-1/2 space to two of them, built with `⊗` (typed `\otimes`) to combine spaces. +Querying [`space`](https://quantumkithub.github.io/TensorKit.jl/stable/), [`codomain`](https://quantumkithub.github.io/TensorKit.jl/stable/), and [`domain`](https://quantumkithub.github.io/TensorKit.jl/stable/) always prints deterministically, even though the tensor's entries are random. +The `zeros` form takes the codomain and domain as separate positional arguments: + +```jldoctest vspace +julia> z = zeros(ComplexF64, V, V); + +julia> space(z) +ℂ^2 ← ℂ^2 +``` + +## Symmetric tensors: the payoff + +The reason for all of this typing of indices is that the very same interface represents *symmetric* tensors, at no extra cost to the code that uses them. +Instead of `ℂ^2` we hand the constructor a space that has been split into charge **sectors**. +For a ℤ₂ symmetry, `Z2Space(0 => 1, 1 => 1)` is a two-dimensional space whose dimension is distributed as one dimension in the even (charge `0`) sector and one in the odd (charge `1`) sector: + +```jldoctest vspace +julia> V2 = Z2Space(0 => 1, 1 => 1) +Rep[ℤ₂](…) of dim 2: + 0 => 1 + 1 => 1 + +julia> dim(V2) +2 +``` + +A tensor built on this space is a genuinely block-sparse, symmetry-respecting object, yet it is constructed and queried exactly like the plain one above: + +```jldoctest vspace +julia> t3 = rand(Float64, V2 ⊗ V2, V2); + +julia> space(t3) +(Rep[ℤ₂](0 => 1, 1 => 1) ⊗ Rep[ℤ₂](0 => 1, 1 => 1)) ← Rep[ℤ₂](0 => 1, 1 => 1) +``` + +Only the space changed; the tensor stores just the symmetry-allowed blocks and enforces charge conservation for you. +Swapping `Z2Space` for a U(1), SU(2), or fermionic space would be an equally local change, and this is exactly why MPSKit's algorithms never mention a symmetry: it is carried entirely by the spaces. +See [Using symmetries](@ref tutorial_using_symmetries) for the full progression of symmetry types. + +## Reading a partition error + +One feature of `TensorMap`s has no counterpart in plain arrays and is worth meeting deliberately, because it produces an error message that is puzzling the first time. +The partition of legs into codomain and domain is *part of a tensor's type*: two tensors are compatible for addition only when their codomains and domains match, arrows included. +Take a tensor and re-partition it so that every leg sits in the codomain (moving a leg across the `←` also flips its arrow to the dual space): + +```@example vspace +using MPSKit, TensorKit # hide +V = ℂ^2 # hide +t = rand(Float64, V ⊗ V, V) +t2 = permute(t, ((1, 2, 3), ())) +space(t), space(t2) +``` + +`t` and `t2` describe the same legs but with different partitions, so adding them directly fails: + +```@example vspace +try #hide +t + t2 # partitions do not match +catch err; Base.showerror(stderr, err); end #hide +``` + +The fix is [`permute`](https://quantumkithub.github.io/TensorKit.jl/stable/), which regroups the legs into a chosen partition. +Bringing `t2` back to the partition of `t` makes the addition well-defined again: + +```@example vspace +space(t + permute(t2, ((1, 2), (3,)))) +``` + +The lesson is not to avoid re-partitioning but to read such an error as "the same legs, grouped differently" and reach for `permute`. + +## MPSKit's index conventions + +With the `TensorMap` model in hand, we can state the leg conventions MPSKit uses for its own tensors — and, more importantly, *why* it uses them. + +An MPS site tensor has a left virtual space `Vₗ`, one or more physical spaces `P`, and a right virtual space `Vᵣ`, and MPSKit orders them so that the left virtual and physical legs form the codomain while the right virtual leg forms the domain, i.e. `Vₗ ⊗ P ← Vᵣ` (an MPO tensor, with an incoming and an outgoing physical leg, reads `Vₗ ⊗ P ← P ⊗ Vᵣ`). +At first glance this ordering looks arbitrary, but it is chosen to keep the tensor networks **planar**: the legs run left-to-right without any lines having to cross. +Planarity is what lets the algorithms be written without spurious crossings, and this matters most for **fermionic systems**, where every extra line crossing carries a sign and unnecessary crossings would reintroduce a sign problem. + + +### The MPS tensor + +```@raw html +An MPS tensor drawn as a box: a left virtual leg and one or more physical legs on the left, a right virtual leg on the right. +``` + +The diagram encodes the ordering + +```math +V_\ell \otimes P_1 \otimes \cdots \otimes P_{k} \leftarrow V_r, +``` + +i.e. leg 1 is the left virtual space, the physical spaces come next (the picture labels them `physical (2:N-1)`), and the final leg is the right virtual space. +Crucially, an MPS tensor may carry an **arbitrary number of physical legs**, and both [`FiniteMPS`](@ref) and [`InfiniteMPS`](@ref) handle the resulting objects. +This is what allows, for example, boundary tensors in PEPS code, which carry two physical legs. + +### The bond tensor + +```@raw html +A bond tensor drawn as a box with one virtual leg on the left and one virtual leg on the right. +``` + +A bond tensor sits between two MPS site tensors and has only the two virtual legs, ordered + +```math +V_\ell \leftarrow V_r, +``` + +i.e. the left virtual space is the codomain and the right virtual space is the domain. + +### The MPO tensor + +```@raw html +An MPO tensor drawn as a box: a left virtual leg and an outgoing physical leg on the left, an incoming physical leg and a right virtual leg on the right. +``` + +An MPO tensor, used to represent both quantum Hamiltonians and classical statistical-mechanics problems, carries two physical legs (one outgoing, one incoming) and two virtual legs, ordered + +```math +V_\ell \otimes P \leftarrow P \otimes V_r. +``` + +The picture labels these `virtual (1)` and `physical (2)` in the codomain, and `physical (3)` and `virtual (4)` in the domain. + +## Where to go next + +- To see these tensors assembled into states and gauged into canonical form, read [Matrix product states](@ref concept_matrix_product_states). +- For the operator side and the Jordan-block structure of Hamiltonians, read [Operators and Hamiltonians](@ref concept_operators_and_hamiltonians). +- For the full range of symmetry types and when each pays off, see [Using symmetries](@ref tutorial_using_symmetries). +- For the underlying tensor library, consult the [TensorKit documentation](https://quantumkithub.github.io/TensorKit.jl/stable/). +``` diff --git a/docs/src/howto/bond_dimension.md b/docs/src/howto/bond_dimension.md new file mode 100644 index 000000000..18ed88ad4 --- /dev/null +++ b/docs/src/howto/bond_dimension.md @@ -0,0 +1,293 @@ +# [Controlling bond dimension](@id howto_bond_dimension) + +The examples on this page use MPSKit.jl, TensorKit.jl, and TensorKitTensors.jl. +See [Installation](@ref tutorial_installation) for how to add these packages to your environment. + +Bond dimension is the key knob in every MPS calculation: too small and the ansatz cannot represent the state, too large and computation slows to a crawl. +This page gives concrete recipes for inspecting, growing, and shrinking bond dimension in MPSKit.jl. +All examples share a single namespace: + +```@example bond_dim +using MPSKit, TensorKit +using TensorKitTensors.SpinOperators: σˣ, σᶻ +``` + +--- + +## 1. Inspecting the current bond dimension + +MPSKit exposes the virtual spaces through `left_virtualspace` and `right_virtualspace`. +This returns the raw vector spaces, which carry the information about the different sectors, but we can obtain a single number using `dim`: + +```@example bond_dim +L = 10 +ψ = FiniteMPS(L, ℂ^2, ℂ^8) # finite MPS, max bond dim 8 + +# Bond dimension between sites i and i+1 equals dim(left_virtualspace(ψ, i+1)) +# or equivalently dim(right_virtualspace(ψ, i)). +dim(left_virtualspace(ψ, 5)) # bond to the left of site 5 +``` + +```@example bond_dim +# All bond dimensions in one go +[dim(left_virtualspace(ψ, i)) for i in 1:L] +``` + +!!! note + For a `FiniteMPS` the leftmost and rightmost virtual spaces are typically one-dimensional (the trivial boundary space), + so `left_virtualspace(ψ, 1)` and `left_virtualspace(ψ, L+1)` have dimension 1. + +For an `InfiniteMPS` the same call works per unit-cell site: + +```@example bond_dim +ψ_inf = InfiniteMPS(ℂ^2, ℂ^8) +dim(left_virtualspace(ψ_inf, 1)) +``` + +--- + +## 2. Growing bond dimension + +### 2a. Random expansion (no Hamiltonian required) + +[`RandExpand`](@ref) pads the MPS with orthogonal random vectors drawn from the two-site null space. +It does **not** need the Hamiltonian, so it is cheap and works for any MPS type. + +`trunc` is **mandatory** and controls how many new directions are added. +Use `truncrank(n)` from MatrixAlgebraKit (re-exported by TensorKit) to add at most `n` extra singular values: + +```@example bond_dim +ψ_small = FiniteMPS(L, ℂ^2, ℂ^4) # start with D = 4 +dim(left_virtualspace(ψ_small, 5)) +``` + +```@example bond_dim +ψ_grown = changebonds(ψ_small, RandExpand(; trunc = truncrank(8))) +dim(left_virtualspace(ψ_grown, 5)) # expanded, but ≤ 4 + 8 = 12 +``` + +The new vectors are orthogonal to the original state, so the state it represents is unchanged (its overlap with the original is 1) while the variational manifold grows. + +For an `InfiniteMPS` the call is identical: + +```@example bond_dim +ψ_inf_small = InfiniteMPS(ℂ^2, ℂ^4) +ψ_inf_grown = changebonds(ψ_inf_small, RandExpand(; trunc = truncrank(8))) +dim(left_virtualspace(ψ_inf_grown, 1)) +``` + +### 2b. Optimal expansion (requires Hamiltonian) + +[`OptimalExpand`](@ref) selects the dominant contributions of the two-site-updated MPS tensor that are orthogonal to the current state, as described by [Zauner-Stauber et al., Phys. Rev. B 97, 045145 (2018)](https://doi.org/10.1103/PhysRevB.97.045145). +It needs both the state and the Hamiltonian: + +```@example bond_dim +# Build a finite TFIM Hamiltonian manually +J = 1.0; g = 0.5 +lattice = fill(ℂ^2, L) +X = σˣ() +Z = σᶻ() +H = FiniteMPOHamiltonian(lattice, (i, i + 1) => -J * X ⊗ X for i in 1:(L - 1)) + + FiniteMPOHamiltonian(lattice, (i,) => -g * Z for i in 1:L) + +ψ_opt, envs_opt = changebonds(ψ_small, H, OptimalExpand(; trunc = truncrank(8))) +dim(left_virtualspace(ψ_opt, 5)) +``` + +`OptimalExpand` also works on `InfiniteMPS` with an `InfiniteMPOHamiltonian`. +The environment argument is optional and defaults to a freshly computed set: + +```@example bond_dim +lattice_inf = PeriodicVector([ℂ^2]) +H_inf = InfiniteMPOHamiltonian(lattice_inf, (1, 2) => -J * X ⊗ X, (1,) => -g * Z) + +ψ_inf_opt, _ = changebonds(ψ_inf_small, H_inf, OptimalExpand(; trunc = truncrank(8))) +dim(left_virtualspace(ψ_inf_opt, 1)) +``` + +!!! note + `OptimalExpand` and `VUMPSSvdCut` (see [§5](#5-growing-during-infinite-mps-optimization)) + both require the Hamiltonian. + Pass environments as the optional fourth argument to avoid recomputing them if you + already have them from a previous `find_groundstate` call. + +--- + +## 3. Reducing bond dimension + +[`SvdCut`](@ref) truncates the bond dimension by an SVD sweep. +It does **not** need the Hamiltonian and is the standard tool for compression. + +```@example bond_dim +# compress ψ_grown (D up to 12) back to at most 6 singular values per bond +ψ_cut = changebonds(ψ_grown, SvdCut(; trunc = truncrank(6))) +dim(left_virtualspace(ψ_cut, 5)) +``` + +An in-place variant, `changebonds!`, exists for `FiniteMPS` and avoids allocating a copy. +It also accepts a `normalize` keyword (default `true`): + +```@example bond_dim +ψ_inplace = FiniteMPS(L, ℂ^2, ℂ^12) +changebonds!(ψ_inplace, SvdCut(; trunc = truncrank(6)); normalize = true) +dim(left_virtualspace(ψ_inplace, 5)) +``` + +`SvdCut` also works on `InfiniteMPS` (2-arg form only; no in-place variant): + +```@example bond_dim +ψ_inf_cut = changebonds(ψ_inf_grown, SvdCut(; trunc = truncrank(6))) +dim(left_virtualspace(ψ_inf_cut, 1)) +``` + +--- + +## 4. Truncation schemes + +Every bond-change algorithm takes a mandatory `trunc` keyword drawn from **MatrixAlgebraKit** (re-exported by TensorKit). +The main schemes are: + +| Scheme | Meaning | +|:-------|:--------| +| `truncrank(n)` | Keep at most `n` singular values | +| `trunctol(; atol)` | Drop singular values below `atol` times the largest | +| `notrunc()` | Keep all singular values (no truncation) | +| `truncspace(V)` | Keep only singular values whose index fits in the given space `V` | + +Schemes compose with `&` to apply multiple criteria simultaneously. +For example, to keep at most 16 singular values **and** also drop anything below `1e-8`: + +```@example bond_dim +trunc_combined = trunctol(; atol = 1.0e-8) & truncrank(16) +ψ_combined = changebonds(ψ_grown, SvdCut(; trunc = trunc_combined)) +dim(left_virtualspace(ψ_combined, 5)) +``` + +!!! warning + `trunc` is **required** on every algorithm; there is no default. + Omitting it will throw a `MethodError` at construction time. + +--- + +## 5. Growing during finite MPS optimization + +The two-site DMRG variant, [`DMRG2`](@ref), performs a bond expansion at every sweep step by keeping both sites together in the update. +Pass `trunc` to control which singular values are retained: + +```@example bond_dim +ψ_dmrg2_start = FiniteMPS(L, ℂ^2, ℂ^2) # start small + +ψ_dmrg2, envs_dmrg2, _ = find_groundstate( + ψ_dmrg2_start, H, + DMRG2(; trunc = truncrank(16), maxiter = 5) +) +dim(left_virtualspace(ψ_dmrg2, 5)) +``` + +A common pattern is to warm up with `DMRG2` to grow the bond dimension, then refine with single-site `DMRG` for efficiency. +The algorithm chaining operator `&` makes this easy (see [§7](#7-chaining-algorithms)): + +```@example bond_dim +warmup_then_refine = DMRG2(; trunc = truncrank(16), maxiter = 3) & + DMRG(; maxiter = 20) + +ψ_dmrg2, envs_dmrg2, _ = find_groundstate(ψ_dmrg2_start, H, warmup_then_refine) +dim(left_virtualspace(ψ_dmrg2, 5)) +``` + +The `find_groundstate` convenience function also accepts a `trunc` keyword that triggers the same warm-up automatically: + +```@example bond_dim +ψ_conv, envs_conv, _ = find_groundstate( + ψ_dmrg2_start, H; + trunc = truncrank(16), maxiter = 20 +) +dim(left_virtualspace(ψ_conv, 5)) +``` + +The `trunc` keyword makes `find_groundstate` prepend a `DMRG2` pass before switching to the default `DMRG`. + +TDVP2 also supports `trunc` for two-site real- or imaginary-time evolution, but that is covered in the time-evolution documentation rather than here. + +--- + +## 6. Growing during infinite MPS optimization + +### IDMRG2 (two-site infinite DMRG) + +[`IDMRG2`](@ref) is the infinite analogue of `DMRG2`. + +!!! warning + `IDMRG2` requires a unit cell of **at least 2 sites**. + Passing a single-site `InfiniteMPS` will throw an `ArgumentError`. + +```@example bond_dim +# 2-site unit cell: lattice, Hamiltonian, and initial state +lattice_2 = PeriodicVector([ℂ^2, ℂ^2]) +H_inf_2 = InfiniteMPOHamiltonian( + lattice_2, + (1, 2) => -J * X ⊗ X, + (2, 3) => -J * X ⊗ X, + (1,) => -g * Z, + (2,) => -g * Z, +) + +ψ_idmrg2_start = InfiniteMPS([ℂ^2, ℂ^2], [ℂ^2, ℂ^2]) + +ψ_idmrg2, _, _ = find_groundstate( + ψ_idmrg2_start, H_inf_2, + IDMRG2(; trunc = truncrank(16), maxiter = 5) +) +dim(left_virtualspace(ψ_idmrg2, 1)) +``` + +### VUMPSSvdCut + +[`VUMPSSvdCut`](@ref) grows the bond dimension of an `InfiniteMPS` by performing a two-site VUMPS update followed by an SVD truncation. +It requires the Hamiltonian and returns a new state with updated environments: + +```@example bond_dim +ψ_vs, _ = changebonds(ψ_inf_small, H_inf, VUMPSSvdCut(; trunc = truncrank(16))) +dim(left_virtualspace(ψ_vs, 1)) +``` + +The typical workflow for infinite systems is to grow the bond dimension first (with `VUMPSSvdCut` or `IDMRG2`), then converge with [`VUMPS`](@ref) as a separate step, reusing the expanded state `ψ_vs` from above: + +```@example bond_dim +ψ_vc, = find_groundstate(ψ_vs, H_inf, VUMPS(; maxiter = 10)) +dim(left_virtualspace(ψ_vc, 1)) +``` + +!!! note + Bond-changing algorithms such as `VUMPSSvdCut` are applied through + [`changebonds`](@ref), not `find_groundstate`. Grow the state first, then pass + the result to a ground-state algorithm. + +--- + +## 7. Chaining algorithms + +The `&` operator chains any two algorithms that share the same interface, applying them in sequence. +This works for both ground-state algorithms and `changebonds` algorithms: + +```@example bond_dim +# Expand with random vectors, then compress to a target rank +grow_and_cut = RandExpand(; trunc = truncrank(12)) & + SvdCut(; trunc = truncrank(6)) + +ψ_final = changebonds(ψ_small, grow_and_cut) +dim(left_virtualspace(ψ_final, 5)) +``` + +```@example bond_dim +# Alternatively: combine changebonds with a ground-state algorithm +ψ_expanded, envs_expanded = changebonds( + ψ_small, H, OptimalExpand(; trunc = truncrank(8)) +) +ψ_gs, _, _ = find_groundstate(ψ_expanded, H, DMRG(; maxiter = 10), envs_expanded) +dim(left_virtualspace(ψ_gs, 5)) +``` + +For background on when each algorithm is appropriate and how convergence is assessed, see [Ground-state algorithms](@ref lib_groundstate). +For constructing MPS objects from scratch, see [Constructing states](@ref howto_states). + diff --git a/docs/src/howto/convergence_troubleshooting.md b/docs/src/howto/convergence_troubleshooting.md new file mode 100644 index 000000000..d5372f5ce --- /dev/null +++ b/docs/src/howto/convergence_troubleshooting.md @@ -0,0 +1,289 @@ +# [Troubleshooting convergence](@id howto_convergence_troubleshooting) + +```@meta +DocTestSetup = quote + using MPSKit, MPSKitModels, TensorKit +end +``` + +When [`find_groundstate`](@ref), [`leading_boundary`](@ref), or a time-evolution call does +not converge, the fix is almost always one of a handful of causes: too few iterations, a +bond dimension that is too small, a bad initial state, or a mismatch between the ansatz and +the physics (wrong unit cell, wrong symmetry sector). +This page is a diagnostic checklist: each section is a symptom, the diagnostic that +confirms it, and the concrete API knob that fixes it. +It assumes you already know how to run the algorithms — see +[Ground-state algorithms](@ref howto_groundstate_algorithms) and +[Controlling bond dimension](@ref howto_bond_dimension) for that. + +All examples share a single namespace: + +```@example conv +using MPSKit, MPSKitModels, TensorKit +``` + +--- + +## 1. Read the convergence report + +Every optimizer returns three things, `(ψ, envs, ϵ)`, and `ϵ` is your primary diagnostic: + +```@example conv +L = 16 +H = transverse_field_ising(FiniteChain(L); g = 1.0) +ψ₀ = FiniteMPS(L, ℂ^2, ℂ^16) + +ψ, envs, ϵ = find_groundstate(ψ₀, H, DMRG(; tol = 1.0e-10, maxiter = 30, verbosity = 0)) +ϵ +``` + +`ϵ` is the convergence-error measure of whichever algorithm ran last, compared against its +`tol` each sweep. +The algorithm has converged when `ϵ < tol`; if it stops because it hit `maxiter` first, `ϵ` +tells you how far it still had to go. +What `ϵ` actually measures differs by algorithm: + +| Algorithm | What `ϵ` measures | +|:----------|:------------------| +| [`DMRG`](@ref), [`VUMPS`](@ref) | Galerkin residual (norm of the projected gradient) | +| [`DMRG2`](@ref) | `1 - abs(overlap)` of the two-site tensor across the truncation | +| [`IDMRG`](@ref), [`IDMRG2`](@ref) | change in the bond matrix `C` between iterations | +| [`GradientGrassmann`](@ref) | norm of the Riemannian gradient | + +Because the measures differ, a raw `ϵ` value is only meaningful *within* one algorithm; do +not compare `ϵ` from a `DMRG2` warm-up against `ϵ` from the following `DMRG` refinement. + +To watch convergence as it happens rather than after the fact, raise `verbosity`. +The levels are shared by every algorithm and documented in +[Ground-state algorithms](@ref howto_groundstate_algorithms); the named constants +`MPSKit.VERBOSE_NONE` (`0`) through `MPSKit.VERBOSE_ALL` (`4`) are public but not exported. +`verbosity = 2` prints one convergence line per sweep, which is usually enough to see +whether `ϵ` is dropping, plateauing, or oscillating. + +--- + +## 2. Converged but wrong: check the variance + +A small `ϵ` does **not** by itself guarantee a good ground state. +At a fixed bond dimension the optimizer converges to the best MPS *within that manifold*, and +its convergence measure can drop below `tol` while the state is still far from the true +ground state. +The independent check is the energy variance +``\langle H^2 \rangle - \langle H \rangle^2``, which is zero only for an exact eigenstate: + +```@example conv +ψ_small, _, ϵ_small = find_groundstate( + FiniteMPS(L, ℂ^2, ℂ^2), H, DMRG(; tol = 1.0e-10, maxiter = 30, verbosity = 0) +) +(ϵ_small, variance(ψ_small, H)) +``` + +Here `ϵ` sits comfortably below `tol` — the run reports success — yet the variance is large: +a bond dimension of 2 simply cannot represent this (critical) ground state. +Grow the bond dimension and the variance collapses: + +```@example conv +ψ_big, _, ϵ_big = find_groundstate( + FiniteMPS(L, ℂ^2, ℂ^32), H, DMRG(; tol = 1.0e-10, maxiter = 30, verbosity = 0) +) +(ϵ_big, variance(ψ_big, H)) +``` + +!!! tip "Variance is your ground-truth check" + Whenever a result looks suspicious despite a small `ϵ`, compute [`variance`](@ref). + A variance that will not drop as you add bond dimension points at an undersized ansatz, + not at an unconverged optimization. + +The fix is to grow the bond dimension: use [`DMRG2`](@ref)/[`IDMRG2`](@ref), or expand +explicitly with [`changebonds`](@ref) and [`OptimalExpand`](@ref)/[`RandExpand`](@ref). +See [Controlling bond dimension](@ref howto_bond_dimension) for the full set of recipes. + +--- + +## 3. `ϵ` is still dropping, or stalls just above `tol` + +If the algorithm stopped on `maxiter` with `ϵ` still decreasing, it simply needs more +iterations — raise `maxiter`: + +```@example conv +ψ_more, _, ϵ_more = find_groundstate( + ψ₀, H, DMRG(; tol = 1.0e-10, maxiter = 100, verbosity = 0) +) +ϵ_more +``` + +You can also resume from an already-optimized state instead of restarting, passing the +previous environments as the optional fourth argument so they are reused rather than +recomputed: + +```@example conv +ψ_resume, _, ϵ_resume = find_groundstate( + ψ, H, DMRG(; tol = 1.0e-12, maxiter = 50, verbosity = 0), envs +) +ϵ_resume +``` + +If instead `ϵ` *plateaus* well above `tol` and more iterations do not help, the ansatz is +the bottleneck, not the iteration count: check the variance and grow the bond dimension as in +[§2](#2-converged-but-wrong-check-the-variance), or treat it as a local minimum +([§4](#4-stuck-in-a-local-minimum)). + +!!! note "Adaptive sub-tolerances" + By default MPSKit tightens the tolerances of the inner eigensolver, gauge, and + environment solvers automatically as the outer error `ϵ` shrinks (the `DynamicTol` + mechanism). + You therefore rarely need to touch `alg_eigsolve`/`alg_gauge`/`alg_environments` by + hand; set the outer `tol` and let the inner solvers follow. + +--- + +## 4. Stuck in a local minimum + +Symptom: `ϵ` plateaus above `tol`, adding bond dimension does not help, and the variance +stays stubbornly high. +Variational optimizers can get trapped in local minima, especially from an unlucky random +start or a symmetry-frustrated initial state. + +Things to try, roughly in order: + +- **Restart from a different initial state.** `FiniteMPS`/`InfiniteMPS` with a size argument + produce a *random* state, so simply rebuilding the initial guess reseeds the search: + + ```@example conv + ψ_restart, _, ϵ_restart = find_groundstate( + FiniteMPS(L, ℂ^2, ℂ^16), H, DMRG(; tol = 1.0e-10, maxiter = 100, verbosity = 0) + ) + ϵ_restart + ``` + +- **Mix algorithms.** Different optimizers have different failure modes, so chaining them + with `&` often escapes a minimum that traps one of them. + A robust infinite-system default is a [`VUMPS`](@ref) pass to get close, polished by + [`GradientGrassmann`](@ref) — exactly what `find_groundstate` does automatically once + `tol` is tighter than `1e-4`: + + ```@example conv + H_inf = transverse_field_ising(; g = 0.5) + ψ_inf, _, ϵ_inf = find_groundstate( + InfiniteMPS(ℂ^2, ℂ^6), H_inf, + VUMPS(; tol = 1.0e-8, maxiter = 50, verbosity = 0) & + GradientGrassmann(; tol = 1.0e-10, maxiter = 50, verbosity = 0) + ) + ϵ_inf + ``` + +- **Inject noise, then re-optimize.** Padding the state with orthogonal random directions via + [`RandExpand`](@ref) perturbs it off the current (possibly stuck) point without changing + what it represents, giving the next optimization new directions to explore: + + ```@example conv + ψ_noisy = changebonds(ψ_small, RandExpand(; trunc = truncrank(8))) + ψ_kick, _, ϵ_kick = find_groundstate( + ψ_noisy, H, DMRG(; tol = 1.0e-10, maxiter = 100, verbosity = 0) + ) + (ϵ_kick, variance(ψ_kick, H)) + ``` + + A two-site algorithm ([`DMRG2`](@ref)) or the Hamiltonian-aware + [`OptimalExpand`](@ref)/CBE variants achieve a similar effect while also improving the + energy, and are usually the better first choice — see + [Controlling bond dimension](@ref howto_bond_dimension). + +--- + +## 5. Infinite MPS won't converge — look at the transfer matrix + +A distinctive infinite-system failure is a state whose transfer matrix has several +eigenvalues crowding the unit circle. +This signals that the state is close to *non-injective* — a superposition of several +injective states — which is numerically ill-conditioned and usually means the unit cell is +too small for the order you are trying to represent. + +Diagnose it with [`transfer_spectrum`](@ref) (the leading transfer-matrix eigenvalues) or its +distilled form [`correlation_length`](@ref): + +```@example conv +ψ_gs, _, _ = find_groundstate( + InfiniteMPS(ℂ^2, ℂ^16), transverse_field_ising(; g = 2.0), + VUMPS(; tol = 1.0e-10, maxiter = 100, verbosity = 0) +) +maximum(values(correlation_length(ψ_gs))) +``` + +```@example conv +abs.(transfer_spectrum(ψ_gs; howmany = 5)) +``` + +The leading eigenvalue is `1` (a normalized state); the *gap* between it and the next +eigenvalue sets the correlation length. +When several eigenvalues sit almost at `1`, the correlation length diverges and the state is +near-degenerate. + +!!! tip "The fix is usually a larger unit cell" + If the transfer spectrum is near-degenerate, rebuild the initial state and the + Hamiltonian on a larger unit cell (e.g. `InfiniteMPS(fill(ℂ^2, 2), fill(ℂ^16, 2))` and a + matching two-site `InfiniteMPOHamiltonian`) and re-optimize. + The gallery example [The XXZ model](@ref "The XXZ model") walks through exactly this + diagnostic — a VUMPS run that refuses to converge, a `transferplot` revealing + near-degeneracy, and the fix of moving to a two-site unit cell. + +For extracting a physically meaningful correlation length from the finite-bond-dimension +spectrum, [`marek_gap`](@ref) implements the standard finite-entanglement-scaling gap +extrapolation. + +--- + +## 6. Wrong symmetry sector + +With a symmetric Hamiltonian, an MPS is confined to the symmetry sector fixed by its virtual +spaces at construction time. +The optimizer never leaves that sector, so if you build the initial state in the wrong one +you converge to the lowest state *of that sector*, not the global ground state. + +Inspect the sector content of a state through its virtual spaces: + +```@example conv +using TensorKit: sectors +L2 = 12 +H_z2 = transverse_field_ising(Z2Irrep, FiniteChain(L2); g = 0.5) +ψ_z2 = FiniteMPS(L2, Z2Space(0 => 1, 1 => 1), Z2Space(0 => 8, 1 => 8)) +collect(sectors(left_virtualspace(ψ_z2, 1))) +``` + +If the sectors present are not the ones the physics requires, rebuild the initial state with +the intended sectors in its physical and virtual spaces (see +[Using symmetries](@ref tutorial_using_symmetries) and +[Constructing states](@ref howto_states) for the `sector => dimension` syntax) before +optimizing. + +--- + +## 7. `leading_boundary` and time evolution + +[`leading_boundary`](@ref) reuses the same infinite-MPS optimizers ([`VUMPS`](@ref), +[`VOMPS`](@ref), [`IDMRG`](@ref), and friends) and returns the same `(ψ, envs, ϵ)` triple, so +every recipe above applies: read `ϵ`, raise `maxiter`/`tol`, grow the bond dimension, and +watch the transfer-matrix spectrum of the boundary MPS. + +For time evolution, "non-convergence" instead means accumulated error over the trajectory. +The knobs are different: + +- **Time step.** A smaller `dt` reduces the per-step integration and (for + [`make_time_mpo`](@ref) with [`WII`](@ref)/[`TaylorCluster`](@ref)) Trotter-type error. +- **Bond dimension.** Real-time evolution grows entanglement, so a fixed bond dimension + eventually cannot follow the state. + Use the two-site [`TDVP2`](@ref) (which takes a `trunc`) or a CBE-enabled + [`TDVP`](@ref) to let the bond dimension grow during evolution. + +See [Time evolution](@ref howto_time_evolution) for the full time-evolution interface. + +--- + +## Where to go next + +For growing and inspecting bond dimension, the most common single fix, see +[Controlling bond dimension](@ref howto_bond_dimension). +For choosing and chaining the ground-state algorithms referenced here, see +[Ground-state algorithms](@ref howto_groundstate_algorithms). +For the reasoning behind when each algorithm applies and how they compare, see +[The algorithm landscape](@ref concept_algorithm_landscape). diff --git a/docs/src/howto/entanglement.md b/docs/src/howto/entanglement.md new file mode 100644 index 000000000..88e446212 --- /dev/null +++ b/docs/src/howto/entanglement.md @@ -0,0 +1,141 @@ +# [Entanglement entropy and spectrum](@id howto_entanglement) + +The examples on this page use MPSKit.jl, TensorKit.jl, and TensorKitTensors.jl. +See [Installation](@ref tutorial_installation) for how to add these packages to your environment. + +This page collects recipes for extracting the entanglement entropy and the entanglement spectrum from the gauge (bond) tensors of an MPS. +For general expectation values and correlators see [Computing observables](@ref howto_observables); for building the state objects used below see [Constructing states](@ref howto_states). +The reference page for these and related functions is [Observables and analysis](@ref lib_observables). + +```@example entanglement +using MPSKit, TensorKit +using TensorKitTensors.SpinOperators: σˣ, σᶻ +``` + +--- + +## Setup: a TFIM ground state + +The examples below reuse a spin-1/2 `FiniteMPS` and the transverse-field Ising Hamiltonian, optimized with DMRG so the entanglement structure reflects an actual ground state rather than a random tensor: + +```@example entanglement +L = 8 +ψ0 = FiniteMPS(L, ℂ^2, ℂ^8) + +# single-site Pauli operators +X = σˣ() +Z = σᶻ() + +lattice = fill(ℂ^2, L) +H = FiniteMPOHamiltonian(lattice, (i, i + 1) => -(X ⊗ X) for i in 1:(L - 1)) + + FiniteMPOHamiltonian(lattice, (i,) => -0.5 * Z for i in 1:L) + +ψ, envs, _ = find_groundstate(ψ0, H, DMRG(; maxiter = 10)) +``` + +--- + +## 1. Entanglement entropy at a single cut + +[`entropy`](@ref) returns the von Neumann entanglement entropy across the cut to the right of a given site. +For a `FiniteMPS` the site is a required argument: + +```@example entanglement +entropy(ψ, L ÷ 2) # entropy across the central cut +``` + +--- + +## 2. Entropy profile across every cut + +Collecting `entropy(ψ, i)` over the valid range of sites gives the full entropy profile of the chain: + +```@example entanglement +[entropy(ψ, i) for i in 1:L] +``` + +!!! warning + For `FiniteMPS` the cut site is required and must lie in `1:length(ψ)`. + `site = 0` — a valid default for `InfiniteMPS` and `WindowMPS` (see recipe 5) — throws a `BoundsError` for `FiniteMPS`. + +--- + +## 3. The entanglement spectrum + +[`entanglement_spectrum`](@ref) returns the singular values of the gauge tensor to the right of a site, packaged as a sector-resolved vector: + +```@example entanglement +spectrum = entanglement_spectrum(ψ, L ÷ 2) +``` + +The entropy can equivalently be computed directly from this spectrum with [`entropy`](@ref): + +```@example entanglement +entropy(spectrum) +``` + +```@example entanglement +entropy(ψ, L ÷ 2) ≈ entropy(spectrum) +``` + +Both routes agree, since `entropy(ψ, site)` computes the entropy from exactly this spectrum internally. + +--- + +## 4. Sector-resolved spectrum + +Because the returned spectrum is indexed by symmetry sector, you can inspect the singular values sector by sector. +Use `keys` to list the sectors present at a cut, and index the spectrum with a sector to obtain its singular values: + +```@example entanglement +collect(keys(spectrum)) +``` + +```@example entanglement +spectrum[only(keys(spectrum))] +``` + +For the plain (no explicit symmetry) `FiniteMPS` built above there is a single sector, `Trivial()`, so all singular values live in one block. +`pairs(spectrum)` iterates `sector => values` pairs and is the natural entry point for a symmetric state where multiple sectors are populated at a cut: + +```@example entanglement +collect(pairs(spectrum)) +``` + +--- + +## 5. Entanglement of an infinite MPS + +For `InfiniteMPS`, the cut site defaults to `0`, and `entropy` without a site argument returns one entropy per site in the unit cell: + +```@example entanglement +ψ∞ = InfiniteMPS(ℂ^2, ℂ^8) +entropy(ψ∞) +``` + +```@example entanglement +entanglement_spectrum(ψ∞) # site defaults to 0 +``` + +!!! note + `ψ∞` here is a random `InfiniteMPS`, not a converged ground state, so the values above illustrate the interface rather than any physical entanglement profile. + For a physically meaningful result, compute the entropy of a state obtained from [`find_groundstate`](@ref) (for example via VUMPS). + +!!! note + `WindowMPS` also supports `entropy(ψ, site)` with a required site argument, mirroring the `FiniteMPS` form. + +--- + +## Plotting the spectrum + +MPSKit defines an `entanglementplot` recipe via `RecipesBase`, but does not depend on Plots.jl itself. +To use it, add `using Plots` (or another Plots-backed package) in your own environment: + +```julia +using Plots +entanglementplot(ψ; site = L ÷ 2) +``` + +!!! note + `entanglementplot` is a plotting *recipe*: it only becomes available once `Plots` (or a compatible plotting package) is loaded. + This block is not executed on this page to keep the docs build free of the Plots.jl dependency. diff --git a/docs/src/howto/excitations.md b/docs/src/howto/excitations.md new file mode 100644 index 000000000..b88cc7ab8 --- /dev/null +++ b/docs/src/howto/excitations.md @@ -0,0 +1,165 @@ +# [Excited states](@id howto_excitations) + +The examples on this page use MPSKit.jl, MPSKitModels.jl, and TensorKit.jl. +See [Installation](@ref tutorial_installation) for how to add these packages to your environment. + +[`excitations`](@ref) is the single entry point for computing energy eigenstates beyond the ground state. +This page shows how to call it for a gap, a full dispersion relation, a charged excitation, and a handful of excited states on a finite chain. +For what each algorithm actually does and why you would choose one over another, see [Excitations](@ref lib_excitations). +All examples share a single namespace: + +```@example excitations_howto +using MPSKit, MPSKitModels, TensorKit +``` + +--- + +## 1. Get a single excitation gap on an infinite chain + +On an `InfiniteMPS`, [`QuasiparticleAnsatz`](@ref) perturbs every site of the unit cell in a plane-wave superposition with a fixed `momentum`, given as a `Real` in radians per unit cell. +Pass the ground state and (optionally) its environments straight through from [`find_groundstate`](@ref): + +```@example excitations_howto +g = 2.0 +H_inf = transverse_field_ising(; g) +ψ₀_inf = InfiniteMPS(ℂ^2, ℂ^12) +ψ_inf, envs_inf, = find_groundstate(ψ₀_inf, H_inf; verbosity = 0) + +Es_inf, ϕs_inf = excitations(H_inf, QuasiparticleAnsatz(), 0.0, ψ_inf, envs_inf; num = 1) +Es_inf[1] +``` + +The values in `Es_inf` are excitation *gaps* above the ground-state energy density, not total energies: internally the ground-state energy per site is subtracted before diagonalizing. +`ϕs_inf` holds the corresponding quasiparticle states (`num` of them), which behave like normal vectors for `eigsolve`-style post-processing but are not `FiniteMPS`/`InfiniteMPS` objects themselves. +Raise `num` to get more than one state at the same momentum, e.g. `num = 3` for the three lowest excitations at that momentum. + +--- + +## 2. Scan the dispersion relation + +Pass a range (or any vector) of momenta instead of a single number to sweep the whole Brillouin zone in one call: + +```@example excitations_howto +momenta = range(0, π, 5) +Es_disp, ϕs_disp = excitations( + H_inf, QuasiparticleAnsatz(), momenta, ψ_inf, envs_inf; + num = 1, verbosity = 0 +) +size(Es_disp) +``` + +With a vector of `length(momenta)` momenta, `Es_disp` and `ϕs_disp` come back as `(length(momenta), num)` matrices rather than plain vectors — index `Es_disp[:, n]` for the dispersion of the `n`-th branch, or use `vec(Es_disp)` when `num = 1`. +`verbosity = 0` silences the per-momentum `@info` line that this method otherwise prints; raise it to see progress on a longer scan. +Momenta are independent of each other, so this method also accepts `parallel = true` (the default) to distribute them over available threads/workers; pass `parallel = false` to force sequential evaluation. + +--- + +## 3. Target a symmetry sector + +By default the optimization looks for the lowest excitation with trivial (vacuum) total charge, `sector = leftunit(ψ)`. +Passing a different `TensorKit` sector targets a quasiparticle with that charge instead — only `QuasiparticleAnsatz` supports this keyword. +Build the ground state with symmetric tensors first, then request the sector on the excitation call: + +```@example excitations_howto +g = 10.0 +L = 12 +H_Z2 = transverse_field_ising(Z2Irrep, FiniteChain(L); g) +ψ₀_Z2 = FiniteMPS(L, Z2Space(0 => 1, 1 => 1), Z2Space(0 => 8, 1 => 8)) +ψ_Z2, envs_Z2, = find_groundstate(ψ₀_Z2, H_Z2; verbosity = 0) + +Es_triv, = excitations(H_Z2, QuasiparticleAnsatz(), ψ_Z2, envs_Z2; num = 1) +Es_charged, ϕs_charged = excitations( + H_Z2, QuasiparticleAnsatz(), ψ_Z2, envs_Z2; + num = 1, sector = Z2Irrep(1) +) +Es_triv[1], Es_charged[1] +``` + +Here the `Z2Irrep(1)` excitation corresponds to a single flipped spin, the lowest physical excitation of the transverse-field Ising model. +[`ChepigaAnsatz`](@ref)/[`ChepigaAnsatz2`](@ref) do not support charged excitations at all: passing a nontrivial `sector` to either raises an error, and [`FiniteExcited`](@ref) has no `sector` keyword in the first place. + +--- + +## 4. Excited states on a finite chain + +Momentum is not a conserved quantity on a finite chain, so the finite method of `excitations` has no momentum argument; drop it entirely and call `QuasiparticleAnsatz` on the ground state directly: + +```@example excitations_howto +L = 12 +H_fin = transverse_field_ising(FiniteChain(L); g) +ψ₀_fin = FiniteMPS(L, ℂ^2, ℂ^16) +ψ_fin, envs_fin, = find_groundstate(ψ₀_fin, H_fin; verbosity = 0) + +Es_qp, ϕs_qp = excitations(H_fin, QuasiparticleAnsatz(), ψ_fin, envs_fin; num = 1) +Es_qp[1] +``` + +[`FiniteExcited`](@ref) takes a different approach: it repeatedly finds the ground state of `H + weight * Σᵢ |ψᵢ⟩⟨ψᵢ|`, penalizing overlap with the ground state and any excited states already found, and returns full `FiniteMPS` objects instead of quasiparticle states: + +```@example excitations_howto +fe_alg = FiniteExcited(; gsalg = DMRG(; verbosity = 0), weight = 10.0) +Es_fe, ψs_fe = excitations(H_fin, fe_alg, ψ_fin; num = 2) +Es_fe +``` + +Unlike `QuasiparticleAnsatz`, the values in `Es_fe` are total energies of the excited states, directly comparable to `expectation_value(ψ_fin, H_fin)` on the ground state. +Because each call to `FiniteExcited` reruns a full ground-state optimization under the hood, it scales worse with `num` than the other methods here; reach for it when you need excited states of a genuinely different character than the ground state (so the projector penalty, rather than a local perturbation, is what finds them). + +[`ChepigaAnsatz`](@ref) is a cheaper alternative for excitations that are qualitatively similar to the ground state: it diagonalizes the effective Hamiltonian at a single site `pos` (default the middle of the chain) using the ground-state environments, with no extra sweeping: + +```@example excitations_howto +Es_ch, ψs_ch = excitations(H_fin, ChepigaAnsatz(), ψ_fin, envs_fin; num = 1, pos = L ÷ 2) +Es_ch[1] +``` + +[`ChepigaAnsatz2`](@ref) does the same with a two-site block at `pos, pos + 1`, which costs more but is typically more accurate; it truncates the optimized two-site tensor back down with a `trunc` keyword (`notrunc()` by default): + +```@example excitations_howto +Es_ch2, ψs_ch2 = excitations(H_fin, ChepigaAnsatz2(; trunc = truncrank(16)), ψ_fin, envs_fin; num = 1) +Es_ch2[1] +``` + +Like `FiniteExcited`, the energies returned by both Chepiga variants are total energies, not gaps. + +--- + +## 5. Check excitation quality + +[`variance`](@ref) accepts a quasiparticle state directly and reports the variance of the energy, with smaller values indicating a better-converged excitation: + +```@example excitations_howto +variance(ϕs_qp[1], H_fin) +``` + +It also works on the infinite quasiparticle states from §1–§3: + +```@example excitations_howto +variance(ϕs_inf[1], H_inf) +``` + +!!! warning "Variance of infinite quasiparticle states" + `variance` on an infinite quasiparticle state carries an unresolved implementation note in `src/algorithms/toolbox.jl` and may be unreliable; verify its output before relying on it as a convergence diagnostic. + It also throws an `ArgumentError` for domain-wall (topological) excitations, where it is not implemented at all. + +To measure other observables on a finite quasiparticle state, convert it to a plain `FiniteMPS` first: + +```@example excitations_howto +excited_state = convert(FiniteMPS, ϕs_qp[1]) +real(expectation_value(excited_state, H_fin)) +``` + +--- + +## 6. Domain-wall excitations + +`excitations` also accepts two *different* ground states, `excitations(H, QuasiparticleAnsatz(), momentum, ψ_left, envs_left, ψ_right, envs_right; ...)`, which builds a quasiparticle that interpolates between them — a domain-wall (topological) excitation rather than a local perturbation on top of a single ground state. +This is real, exported functionality, but it has no dedicated test or example in the repository at the time of writing, and constructing two genuinely distinct, well-converged ground states to feed it (for instance the two symmetry-broken ground states of an ordered phase) is itself nontrivial to set up reliably in a short recipe. + +--- + +## Where to go next + +For growing the bond dimension of the ground states these recipes start from, see [Controlling bond dimension](@ref howto_bond_dimension). +For background on the quasiparticle ansatz and the other algorithms used here, see [Excitations](@ref lib_excitations). +For general expectation values and correlators, including on the converted excited states from §5, see [Computing observables](@ref howto_observables). +Spectral functions built from these excitations (`propagator`, `DynamicalDMRG`, and related solvers) are a separate topic covered in the "Linear problems and spectral functions" section of the [Public API](@ref public_api) reference. diff --git a/docs/src/howto/groundstate_algorithms.md b/docs/src/howto/groundstate_algorithms.md new file mode 100644 index 000000000..362ee8a76 --- /dev/null +++ b/docs/src/howto/groundstate_algorithms.md @@ -0,0 +1,202 @@ +# [Ground-state algorithms](@id howto_groundstate_algorithms) + +The examples on this page use MPSKit.jl, MPSKitModels.jl, TensorKit.jl, and TensorKitTensors.jl. +See [Installation](@ref tutorial_installation) for how to add these packages to your environment. + +[`find_groundstate`](@ref) is the single entry point for optimizing an MPS towards the ground state of a Hamiltonian. +This page shows how to pick and configure the algorithm it runs, for both finite and infinite systems. +For what each algorithm actually does and why you would choose one over another, see [Ground-state algorithms](@ref lib_groundstate). +All examples share a single namespace: + +```@example groundstate_algs +using MPSKit, MPSKitModels, TensorKit +using TensorKitTensors.SpinOperators: σˣ, σᶻ +``` + +--- + +## 1. Get a ground state with defaults + +Called with just a state and a Hamiltonian, `find_groundstate` inspects the type of the initial state and picks a matching algorithm for you. +For a `FiniteMPS` it runs [`DMRG`](@ref) with the keywords you pass through (`tol`, `maxiter`, `verbosity`): + +```@example groundstate_algs +L = 8 +ψ₀ = FiniteMPS(L, ℂ^2, ℂ^8) +H = transverse_field_ising(FiniteChain(L); g = 0.5) + +ψ, envs, ϵ = find_groundstate(ψ₀, H; tol = 1.0e-8, maxiter = 50, verbosity = 0) +ϵ +``` + +For an `InfiniteMPS` it instead runs [`VUMPS`](@ref), and if the requested `tol` is tighter than `1e-4` it chains a [`GradientGrassmann`](@ref) pass afterwards to polish the last few digits (see [§4](#4-refine-convergence-with-gradientgrassmann)): + +```@example groundstate_algs +ψ₀_inf = InfiniteMPS(ℂ^2, ℂ^6) +H_inf = transverse_field_ising(; g = 0.5) + +ψ_inf, envs_inf, ϵ_inf = find_groundstate(ψ₀_inf, H_inf; verbosity = 0) +ϵ_inf +``` + +Passing a `trunc` keyword switches on a two-site pre-pass that can grow the bond dimension before the single-site algorithm takes over. +On a `FiniteMPS` this prepends [`DMRG2`](@ref); on an `InfiniteMPS` it prepends [`IDMRG2`](@ref) (which needs a unit cell of at least two sites, see [§3](#3-configure-infinite-system-algorithms)). + +```@example groundstate_algs +ψ_auto, envs_auto, ϵ_auto = find_groundstate( + ψ₀, H; + trunc = truncrank(16), verbosity = 0 +) +ϵ_auto +``` + +!!! note "When to reach for an explicit algorithm" + The keyword form above covers the common cases. + Reach for an explicit algorithm struct (`DMRG`, `DMRG2`, `VUMPS`, `IDMRG`, `IDMRG2`, `GradientGrassmann`), or a chain of them with `&`, whenever you need finer control than the heuristic provides — the rest of this page shows how. + +--- + +## 2. Configure finite-system DMRG + +Pass a [`DMRG`](@ref) struct explicitly to set `tol`, `maxiter`, and `verbosity` directly: + +```@example groundstate_algs +ψ_dmrg, envs_dmrg, ϵ_dmrg = find_groundstate( + ψ₀, H, + DMRG(; tol = 1.0e-8, maxiter = 50, verbosity = 0) +) +ϵ_dmrg +``` + +`DMRG` updates one site at a time, so with its defaults it cannot change the bond dimension: whatever bond dimension `ψ₀` starts with is what it keeps. +[`DMRG2`](@ref) optimizes two sites at once and truncates back down, which lets it grow (or shrink) the bond dimension as it sweeps, at extra cost per step. +Unlike `DMRG`, `DMRG2` has no default truncation scheme, so `trunc` is required: + +```@example groundstate_algs +ψ_dmrg2, envs_dmrg2, ϵ_dmrg2 = find_groundstate( + ψ₀, H, + DMRG2(; trunc = truncrank(16), maxiter = 5, verbosity = 0) +) +ϵ_dmrg2 +``` + +A common pattern is to warm up with `DMRG2` to grow the bond dimension, then refine with the cheaper single-site `DMRG`. +The `&` chaining operator runs the first algorithm to completion, then feeds its result into the second: + +```@example groundstate_algs +warmup_then_refine = DMRG2(; trunc = truncrank(16), maxiter = 3, verbosity = 0) & + DMRG(; tol = 1.0e-8, maxiter = 30, verbosity = 0) + +ψ_c, envs_c, ϵ_c = find_groundstate(ψ₀, H, warmup_then_refine) +ϵ_c +``` + +For more on choosing `trunc` and growing bond dimension in general, see [Controlling bond dimension](@ref howto_bond_dimension). + +--- + +## 3. Configure infinite-system algorithms + +[`VUMPS`](@ref) is the default single-site algorithm for an `InfiniteMPS`, and takes the same `tol`/`maxiter`/`verbosity` keywords: + +```@example groundstate_algs +ψ_v, envs_v, ϵ_v = find_groundstate( + ψ₀_inf, H_inf, + VUMPS(; tol = 1.0e-8, maxiter = 50, verbosity = 0) +) +ϵ_v +``` + +[`IDMRG`](@ref) is the infinite analogue of `DMRG`: it grows the system by repeatedly inserting sites in the middle and re-optimizing, until boundary effects wash out. + +```@example groundstate_algs +ψ_i, envs_i, ϵ_i = find_groundstate( + ψ₀_inf, H_inf, + IDMRG(; tol = 1.0e-8, maxiter = 50, verbosity = 0) +) +ϵ_i +``` + +In practice, prefer `VUMPS` unless you specifically need `IDMRG`'s ability to change the bond dimension one site at a time. + +[`IDMRG2`](@ref) is the two-site, bond-dimension-changing variant, and mirrors `DMRG2`: `trunc` is required, and it needs a unit cell of at least two sites. + +!!! warning "Unit cell size" + `IDMRG2` throws an `ArgumentError` on a single-site `InfiniteMPS`. + Build the initial state and Hamiltonian with a unit cell of two (or more) sites instead. + +```@example groundstate_algs +J = 1.0 +g = 0.5 +X = σˣ() +Z = σᶻ() + +lattice_2 = PeriodicVector([ℂ^2, ℂ^2]) +H_inf_2 = InfiniteMPOHamiltonian( + lattice_2, + (1, 2) => -J * X ⊗ X, + (2, 3) => -J * X ⊗ X, + (1,) => -g * Z, + (2,) => -g * Z, +) +ψ₀_2 = InfiniteMPS([ℂ^2, ℂ^2], [ℂ^2, ℂ^2]) + +ψ_i2, envs_i2, ϵ_i2 = find_groundstate( + ψ₀_2, H_inf_2, + IDMRG2(; trunc = truncrank(16), maxiter = 5, verbosity = 0) +) +ϵ_i2 +``` + +--- + +## 4. Refine convergence with GradientGrassmann + +[`GradientGrassmann`](@ref) performs Riemannian gradient descent directly on the manifold of (finite or infinite) MPS, using an optimizer from OptimKit (`ConjugateGradient` by default via the `method` keyword). +Chain it after `VUMPS` (or `DMRG`) with `&` to combine both regimes in one call — this is exactly what `find_groundstate`'s heuristic does once `tol` is tighter than `1e-4`: + +```@example groundstate_algs +refine = VUMPS(; tol = 1.0e-6, maxiter = 20, verbosity = 0) & + GradientGrassmann(; tol = 1.0e-10, maxiter = 50, verbosity = 0) + +ψ_g, envs_g, ϵ_g = find_groundstate(ψ₀_inf, H_inf, refine) +ϵ_g +``` + +Since `GradientGrassmann` is also a single-site algorithm, it cannot change the bond dimension either: grow it beforehand with `DMRG2`/`IDMRG2` or the `changebonds` recipes in [Controlling bond dimension](@ref howto_bond_dimension). + +--- + +## 5. Control output and tolerances + +Every algorithm accepts a `verbosity` keyword as a plain integer: + +| `verbosity` | Output | +|:-----------:|:-------| +| `0` | nothing | +| `1` | warnings only | +| `2` | convergence information | +| `3` | per-iteration information (the default) | +| `4` | everything | + +`find_groundstate` returns `(ψ, envs, ϵ)`. +`ϵ` is the final convergence-error measure (a Galerkin residual) of whichever algorithm ran last — it quantifies how well the sweeps converged, not the error in the energy itself. + +The optional third positional argument to `find_groundstate` lets you reuse `envs` from a previous call instead of recomputing it, which is useful when tightening the tolerance on a state you already optimized: + +```@example groundstate_algs +ψ_v2, envs_v2, ϵ_v2 = find_groundstate( + ψ_v, H_inf, + VUMPS(; tol = 1.0e-10, maxiter = 50, verbosity = 0), + envs_v +) +ϵ_v2 +``` + +--- + +## Where to go next + +For growing, shrinking, and inspecting bond dimension during or between these calculations, see [Controlling bond dimension](@ref howto_bond_dimension). +For background on when each algorithm applies and how it relates to the others, see [Ground-state algorithms](@ref lib_groundstate). +To see `find_groundstate` used end to end on a finite chain, start from [Your first ground state](@ref tutorial_first_groundstate); for the infinite-system counterpart, see [The thermodynamic limit](@ref tutorial_thermodynamic_limit). diff --git a/docs/src/howto/hamiltonians.md b/docs/src/howto/hamiltonians.md new file mode 100644 index 000000000..9b8206684 --- /dev/null +++ b/docs/src/howto/hamiltonians.md @@ -0,0 +1,107 @@ +# [Building Hamiltonians](@id howto_hamiltonians) + +The examples on this page use MPSKit.jl, TensorKit.jl, and TensorKitTensors.jl. +See [Installation](@ref tutorial_installation) for how to add these packages to your environment. + +This page collects recipes for constructing MPO Hamiltonians from local operators, for both finite and infinite (translation-invariant) lattices. +It also covers converting an infinite Hamiltonian to finite open or periodic boundary conditions, and carving a finite window out of an infinite Hamiltonian. +For building the matching state objects see [Constructing states](@ref howto_states); for evaluating a Hamiltonian's energy on a state see [Computing observables](@ref howto_observables). +The reference page for the underlying MPO structure is [Operators](@ref lib_operators). + +```@example hamiltonians +using MPSKit, TensorKit +using TensorKitTensors.SpinOperators: σˣ, σᶻ +``` + +--- + +## Setup: local operators + +The examples below build the transverse-field Ising model (TFIM), the same flagship model used elsewhere in these docs. +It couples neighbouring spins through `X ⊗ X` and applies a transverse field of strength `g` along `Z`. +The model has a quantum phase transition at `g = 1`, separating an ordered (ferromagnetic) phase at small `g` from a disordered (paramagnetic) phase at large `g`. +The single-site Pauli operators come from [TensorKitTensors.jl](https://github.com/QuantumKitHub/TensorKitTensors.jl), which returns `ComplexF64` `TensorMap`s on the spin-1/2 physical space `ℂ^2`: + +```@example hamiltonians +X = σˣ() +Z = σᶻ() +g = 0.5 +``` + +--- + +## 1. Finite Hamiltonian from local terms + +[`FiniteMPOHamiltonian`](@ref) takes an array of `VectorSpace` objects describing the local Hilbert spaces, followed by any number of `inds => operator` pairs. +A single-site term uses a one-element tuple `(i,) => O`; a nearest-neighbour term uses a two-element tuple `(i, i + 1) => O₁₂`, where `O₁₂` is a two-site operator built with `⊗`: + +```@example hamiltonians +L = 8 +lattice = fill(ℂ^2, L) + +H_finite = FiniteMPOHamiltonian(lattice, (i, i + 1) => -(X ⊗ X) for i in 1:(L - 1)) + + FiniteMPOHamiltonian(lattice, (i,) => -g * Z for i in 1:L) +``` + +Adding the two `FiniteMPOHamiltonian` objects combines the bond terms and the field terms into a single Jordan-block MPO. +Equivalently, all terms can be passed as one call by splatting a single collection of `inds => operator` pairs; see [Operators](@ref lib_operators) for that form. + +!!! note + The index tuples must refer to contiguous sites for the two-site pairs shown here. + See [Operators](@ref lib_operators) for the general, non-nearest-neighbour "expert mode" construction, which is not covered on this task-oriented page. + +--- + +## 2. Infinite (translation-invariant) Hamiltonian + +[`InfiniteMPOHamiltonian`](@ref) uses the same `inds => operator` convention, but the lattice argument is a single unit cell, and site indices wrap around it periodically. +For the 1-site TFIM unit cell, `(1, 2) => O₁₂` couples site 1 to site 2 of the *next* unit cell: + +```@example hamiltonians +unitcell = fill(ℂ^2, 1) +H_inf = InfiniteMPOHamiltonian(unitcell, (1, 2) => -(X ⊗ X), (1,) => -g * Z) +``` + +The resulting operator repeats this single bond-plus-field pattern along the whole infinite chain. +Use it directly with an [`InfiniteMPS`](@ref) in `expectation_value` or `find_groundstate`, exactly as described in [Computing observables](@ref howto_observables). + +!!! tip + Hand-assembling local operators works for any model, but for standard lattice models MPSKitModels.jl provides ready-made Hamiltonian builders and the `@mpoham` macro for a more compact syntax. + See the MPSKitModels.jl documentation for that higher-level interface; it is a separate package from MPSKit and not covered here. + +--- + +## 3. Converting between boundary conditions + +Starting from an `InfiniteMPOHamiltonian`, [`open_boundary_conditions`](@ref) truncates it to a finite chain of length `L` with open ends, and [`periodic_boundary_conditions`](@ref) instead closes it into a finite ring. +In both cases `L` must be a multiple of the unit-cell length: + +```@example hamiltonians +L_finite = 6 # multiple of the 1-site unit cell + +H_open = open_boundary_conditions(H_inf, L_finite) +``` + +```@example hamiltonians +H_periodic = periodic_boundary_conditions(H_inf, L_finite) +``` + +`H_open` is the same finite-chain Hamiltonian you would get from writing out the terms by hand, as in recipe 1 above, restricted to `L_finite` sites. +`H_periodic` additionally couples the last site back to the first, forming a ring. + +!!! note + Both functions return a [`FiniteMPOHamiltonian`](@ref). + There is no boundary-condition keyword on the `FiniteMPOHamiltonian`/`InfiniteMPOHamiltonian` constructors themselves; boundary conditions are chosen by picking which constructor (or conversion function) to call. + +--- + +## 4. A window Hamiltonian + +[`WindowMPOHamiltonian`](@ref) carves a finite interval out of an infinite Hamiltonian while keeping the infinite left and right environments intact. +This is the operator counterpart of a [`WindowMPS`](@ref) (see [Constructing states](@ref howto_states)), and the two are used together to study a finite region embedded in, and coupled to, an infinite bulk: + +```@example hamiltonians +H_window = WindowMPOHamiltonian(H_inf, 1:6) +``` + +The interval `1:6` selects which unit cells of `H_inf` become the mutable finite window; everything outside it is treated as the fixed infinite environment. diff --git a/docs/src/howto/index.md b/docs/src/howto/index.md new file mode 100644 index 000000000..b8fcac55f --- /dev/null +++ b/docs/src/howto/index.md @@ -0,0 +1,90 @@ +# [How-to guides](@id howto_index) + +These pages are task recipes: short, runnable answers to "how do I do X?". +They assume you already know the basics — if you are new to MPSKit, start with [Your first ground state](@ref tutorial_first_groundstate) instead. +Each recipe below stands on its own, so feel free to jump straight to the one you need. + +## States and operators + +**[Constructing states](@ref howto_states)** — building `FiniteMPS`, `InfiniteMPS`, `WindowMPS`, and `MultilineMPS` objects. +- A finite MPS — random states, initializers and element types, per-site spaces, product states, and wrapping your own site tensors. +- An infinite MPS — single- and multi-site unit cells, from spaces or from tensors. +- A window MPS — a mutable finite region embedded in infinite environments. +- A multiline MPS — stacking `InfiniteMPS` rows for boundary-MPS methods. +- States with symmetries — building MPS with `Rep[G]` graded spaces. + +**[Building Hamiltonians](@ref howto_hamiltonians)** — assembling MPO Hamiltonians from local operators. +- Finite Hamiltonian from local terms — `FiniteMPOHamiltonian` from `inds => operator` pairs. +- Infinite (translation-invariant) Hamiltonian — `InfiniteMPOHamiltonian` on a unit cell. +- Converting between boundary conditions — open vs. periodic finite chains from an infinite Hamiltonian. +- A window Hamiltonian — carving a finite interval out of an infinite Hamiltonian with `WindowMPOHamiltonian`. + +## Finding ground states + +**[Ground-state algorithms](@ref howto_groundstate_algorithms)** — configuring `find_groundstate`. +- Get a ground state with defaults — letting `find_groundstate` pick an algorithm for you. +- Configure finite-system DMRG — explicit `DMRG`/`DMRG2`, and chaining them with `&`. +- Configure infinite-system algorithms — `VUMPS`, `IDMRG`, and `IDMRG2`. +- Refine convergence with GradientGrassmann — Riemannian gradient descent after a cheaper warm-up. +- Control output and tolerances — `verbosity` levels and reusing `envs` between calls. + +**[Controlling bond dimension](@ref howto_bond_dimension)** — inspecting, growing, and shrinking bond dimension. +- Inspecting the current bond dimension — `left_virtualspace`/`right_virtualspace` and `dim`. +- Growing bond dimension — `RandExpand` (no Hamiltonian needed) and `OptimalExpand`. +- Reducing bond dimension — `SvdCut` and the in-place `changebonds!`. +- Truncation schemes — `truncrank`, `trunctol`, `notrunc`, `truncspace`, and combining them with `&`. +- Growing during finite MPS optimization — `DMRG2` and the `trunc` keyword of `find_groundstate`. +- Growing during infinite MPS optimization — `IDMRG2` and `VUMPSSvdCut`. +- Chaining algorithms — composing bond-change and ground-state algorithms with `&`. + +## Dynamics + +**[Time evolution](@ref howto_time_evolution)** — real- and imaginary-time evolution of an MPS. +- Evolve a state through one time step — `timestep` with `TDVP`. +- Evolve over a time span — `time_evolve` across a vector of time points. +- Grow the bond dimension while evolving — `TDVP2` with a mandatory `trunc`. +- Evolve an infinite state — single-site `TDVP` on an `InfiniteMPS`. +- Imaginary-time evolution — `imaginary_evolution = true` to cool towards the ground state, with `normalize = true` to keep the state normalized. +- Let the bond dimension adapt — `BUG` with a truncating `trunc`. +- Build a time-evolution MPO — `make_time_mpo` (`WII`, `TaylorCluster`, `WI`) plus `approximate`, or `Zipup` for a single-sweep application. + +## Measurements + +**[Computing observables](@ref howto_observables)** — extracting physical quantities from an MPS. +- Local (one-site) expectation value — `expectation_value(ψ, i => O)`. +- Multi-site (contiguous) expectation value — tensor-product operators on an index tuple. +- Energy (full-MPO expectation value) — `expectation_value(ψ, H)` for a Hamiltonian MPO. +- Two-point correlators — `correlator`, including a full correlation profile over a range. +- Energy variance as a convergence check — `variance` as a diagnostic after a ground-state search. + +**[Entanglement entropy and spectrum](@ref howto_entanglement)** — reading off entanglement from the gauge tensors. +- Entanglement entropy at a single cut — `entropy(ψ, site)`. +- Entropy profile across every cut — collecting `entropy` over all sites. +- The entanglement spectrum — `entanglement_spectrum` as a sector-resolved vector. +- Sector-resolved spectrum — indexing the spectrum by symmetry sector with `keys`/`pairs`. +- Entanglement of an infinite MPS — `entropy`/`entanglement_spectrum` per unit-cell site. +- Plotting the spectrum — the `entanglementplot` recipe (requires Plots.jl). + +## Excitations + +**[Excited states](@ref howto_excitations)** — computing energy eigenstates beyond the ground state. +- Get a single excitation gap on an infinite chain — `QuasiparticleAnsatz` at a fixed momentum. +- Scan the dispersion relation — passing a range of momenta in one call. +- Target a symmetry sector — a charged quasiparticle via the `sector` keyword. +- Excited states on a finite chain — `QuasiparticleAnsatz`, `FiniteExcited`, and the Chepiga ansätze. +- Check excitation quality — `variance` on a quasiparticle state. +- Domain-wall excitations — quasiparticles interpolating between two distinct ground states. + +## Performance and hardware + +**[Parallelism and GPU support](@ref howto_parallelism_gpu)** — tuning how MPSKit uses the hardware. +- Setting BLAS threads — `BLAS.set_num_threads` and the OpenBLAS/MKL difference. +- Setting the MPSKit scheduler — `MPSKit.Defaults.set_scheduler!` with `:serial`/`:greedy`/`:dynamic`. +- Diagnosing the thread layout — ThreadPinning.jl `threadinfo`. +- Reducing memory usage — disabling multithreading to avoid `OutOfMemory`. +- GPU support — the experimental Adapt-based path for moving states onto a GPU. + +## Missing a recipe? + +If the task you're after isn't listed here, please open an issue at [QuantumKitHub/MPSKit.jl](https://github.com/QuantumKitHub/MPSKit.jl/issues) describing what you're trying to do. +Concrete task descriptions make the best new recipes. diff --git a/docs/src/howto/observables.md b/docs/src/howto/observables.md new file mode 100644 index 000000000..a69288767 --- /dev/null +++ b/docs/src/howto/observables.md @@ -0,0 +1,167 @@ +# [Computing observables](@id howto_observables) + +The examples on this page use MPSKit.jl, TensorKit.jl, and TensorKitTensors.jl. +See [Installation](@ref tutorial_installation) for how to add these packages to your environment. + +This page collects recipes for extracting physical quantities from an MPS: local and multi-site expectation values, the energy of a Hamiltonian, two-point correlators, and the energy variance as a convergence diagnostic. +All examples share a single namespace and build on state and operator objects you would have in hand after a ground-state calculation. + +```@example observables +using MPSKit, TensorKit +using TensorKitTensors.SpinOperators: σˣ, σᶻ +``` + +For building MPS objects see [Constructing states](@ref howto_states). +For controlling the bond dimension during optimization see [Controlling bond dimension](@ref howto_bond_dimension). +The reference page for ground-state algorithms is [Ground-state algorithms](@ref lib_groundstate). + +--- + +## Setup: state and operators + +The examples below use a spin-1/2 `FiniteMPS` together with the Pauli operators from [TensorKitTensors.jl](https://github.com/QuantumKitHub/TensorKitTensors.jl). +These are `ComplexF64` `TensorMap`s, matching the default element type of the state. + +```@example observables +L = 8 +ψ = FiniteMPS(L, ℂ^2, ℂ^8) # random finite MPS, bond dim ≤ 8 + +# single-site Pauli operators +X = σˣ() +Z = σᶻ() +``` + +The finite TFIM Hamiltonian used in recipes 3 and 5 is built from these: + +```@example observables +lattice = fill(ℂ^2, L) +H = FiniteMPOHamiltonian(lattice, (i, i + 1) => -(X ⊗ X) for i in 1:(L - 1)) + + FiniteMPOHamiltonian(lattice, (i,) => -0.5 * Z for i in 1:L) +``` + +--- + +## 1. Local (one-site) expectation value + +Use `expectation_value(ψ, i => O)` to evaluate ⟨ψ|Oᵢ|ψ⟩ at a single site `i`. +The pair `i => O` identifies the site and the single-site operator. + +```@example observables +expectation_value(ψ, 4 => Z) # ⟨Z⟩ at site 4 +``` + +To compute a local observable at every site, broadcast over the indices: + +```@example observables +[expectation_value(ψ, i => Z) for i in 1:L] +``` + +!!! note + The state `ψ` must be normalised for the expectation value to be meaningful. + A freshly constructed `FiniteMPS` is normalised by default; if you modified + the tensors by hand, call `normalize!(ψ)` first. + +--- + +## 2. Multi-site (contiguous) expectation value + +For a product of operators on a contiguous range of sites, pass a tuple of indices together with a multi-site operator formed by taking tensor products `⊗`: + +```@example observables +# ⟨X₂ X₃⟩ — two-site operator on sites 2 and 3 +expectation_value(ψ, (2, 3) => X ⊗ X) +``` + +The operator `X ⊗ X` is a `{2,2}` `TensorMap` (two incoming, two outgoing legs) matching the two-site index tuple `(2, 3)`. +The tuple must be contiguous; arbitrary non-adjacent index sets are not supported by this form. + +```@example observables +# ⟨Z₁ Z₂ Z₃⟩ — three-site operator +expectation_value(ψ, (1, 2, 3) => Z ⊗ Z ⊗ Z) +``` + +--- + +## 3. Energy (full-MPO expectation value) + +When the operator is an [`AbstractMPO`](@ref) (e.g. a Hamiltonian), pass it directly without an index argument. +MPSKit evaluates the full contraction ⟨ψ|H|ψ⟩: + +```@example observables +E = expectation_value(ψ, H) +``` + +The result is a scalar; for a Hermitian `H` and a normalised `ψ` its imaginary part is zero up to floating-point noise. + +The same form works for `InfiniteMPS` with an `InfiniteMPOHamiltonian`, where the returned value is the energy **per unit cell**. + +!!! note + The full-MPO form automatically computes and caches the environments. + If you already have environments from a prior `find_groundstate` call you can + pass them as a trailing argument to avoid recomputation, but this is optional; + omitting them is always safe and correct. + +--- + +## 4. Two-point correlators + +[`correlator`](@ref) computes ⟨O₁ᵢ O₂ⱼ⟩ for two sites with `i < j`. +The recommended call uses a single two-site operator `O₁₂`: + +```@example observables +# ⟨Z₂ Zⱼ⟩ for a single target site j = 6 +correlator(ψ, Z ⊗ Z, 2, 6) +``` + +!!! warning + `i` must be strictly less than `j`. + Calling `correlator(ψ, O₁₂, i, j)` with `i ≥ j` will throw an error. + +### Correlation profile over a range + +Pass a range as `j` to obtain a vector of correlators — one entry per target site. +This is the efficient route for a full correlation profile: + +```@example observables +# ⟨Z₂ Zⱼ⟩ for j = 3, 4, …, L +corr = correlator(ψ, Z ⊗ Z, 2, 3:L) +``` + +The result is a `Vector` whose `k`-th element corresponds to `j = 3 + k - 1`. + +A common pattern is to normalise the correlator by ⟨Z⟩² to extract the connected part: + +```@example observables +z_mean = expectation_value(ψ, 2 => Z) +connected = [c - z_mean * expectation_value(ψ, j => Z) for (j, c) in zip(3:L, corr)] +``` + +This subtracts the disconnected part ``\langle Z_i\rangle\langle Z_j\rangle`` to leave the connected correlator ``\langle Z_i Z_j\rangle - \langle Z_i\rangle\langle Z_j\rangle``. + +--- + +## 5. Energy variance as a convergence check + +[`variance`](@ref) returns ⟨H²⟩ − ⟨H⟩², which is zero if and only if `ψ` is an exact eigenstate of `H`. +Use it as a quantitative convergence diagnostic after a ground-state search: + +```@example observables +var_E = variance(ψ, H) +``` + +A smaller variance indicates that `ψ` is closer to a true eigenstate. + +After running a ground-state algorithm the variance should have dropped significantly compared to the random starting state above: + +```@example observables +ψ_gs, envs, _ = find_groundstate(ψ, H, DMRG(; maxiter = 10)) +variance(ψ_gs, H) +``` + +!!! note + The `variance` function also accepts an optional pre-computed `envs` argument. + Pass the environments returned by `find_groundstate` to skip recomputation: + + ```julia + variance(ψ_gs, H, envs) + ``` diff --git a/docs/src/howto/parallelism_gpu.md b/docs/src/howto/parallelism_gpu.md new file mode 100644 index 000000000..dd5b6fe9c --- /dev/null +++ b/docs/src/howto/parallelism_gpu.md @@ -0,0 +1,156 @@ +# [Parallelism and GPU support](@id howto_parallelism_gpu) + +This page collects the practical knobs for controlling how MPSKit uses the hardware: +how to set BLAS threads, how to pick the MPSKit multithreading scheduler, how to inspect +the resulting thread layout, and what to do when a calculation runs out of memory. +It closes with a short, experimental note on moving states onto a GPU. + +For the reasoning behind these settings — why Julia threads and BLAS threads interact the +way they do, and where MPSKit actually parallelizes — see +[The parallelism model](@ref concept_parallelism_model). + +!!! note + Threading performance depends heavily on the hardware, the BLAS vendor, the size of + the problem, and the availability of memory and memory bandwidth. + There is no single setting that is optimal everywhere; the recipes below are sensible + starting points, and you should measure on your own machine. + +## Setting the number of BLAS threads + +Most of the heavy linear algebra in MPSKit ends up in BLAS routines (in particular `gemm`, +general matrix-matrix multiplication). +The number of BLAS threads is controlled through `LinearAlgebra.BLAS.set_num_threads`: + +```julia +using LinearAlgebra: BLAS +BLAS.set_num_threads(1) +``` + +With OpenBLAS (the default vendor), `set_num_threads` sets the **total** number of BLAS +threads held in a shared pool across all Julia threads. +When Julia is started with multiple threads, setting this to `1` lets MPSKit drive the +parallelism through its own (Julia-thread) machinery instead, which is often the best +option for OpenBLAS. + +With [MKL.jl](https://github.com/JuliaLinearAlgebra/MKL.jl) the semantics differ: the BLAS +thread count applies **per Julia thread**, so 4 Julia threads with 4 BLAS threads each spawn +16 BLAS threads in total. +In that case you typically want to lower the BLAS thread count to avoid oversubscribing the +physical cores. + +## Setting the MPSKit scheduler + +When Julia runs with multiple threads, MPSKit parallelizes parts of its algorithms through +[OhMyThreads.jl](https://juliafolds2.github.io/OhMyThreads.jl/stable/). +The behaviour is controlled by a global scheduler, set with `MPSKit.Defaults.set_scheduler!`: + +```julia +MPSKit.Defaults.set_scheduler!(:serial) # disable multithreading +MPSKit.Defaults.set_scheduler!(:greedy) # multithreading with greedy load-balancing +MPSKit.Defaults.set_scheduler!(:dynamic) # multithreading with dynamic load-balancing +``` + +`set_scheduler!` also accepts an `OhMyThreads.Scheduler` directly, or a symbol together with +keyword arguments that are forwarded to the corresponding OhMyThreads scheduler. +When left unset, the default is a serial scheduler if Julia was started with a single thread, +and a dynamic scheduler otherwise. +For the full list of schedulers and their keyword arguments, see the +[OhMyThreads.jl documentation](https://juliafolds2.github.io/OhMyThreads.jl/stable/refs/api/#Schedulers). + +## Diagnosing the thread layout + +Because the interaction between Julia threads and BLAS threads is easy to get wrong, it helps +to inspect the actual layout. +[ThreadPinning.jl](https://github.com/carstenbauer/ThreadPinning.jl) provides `threadinfo`, +which reports the Julia threads, their CPU mapping, and the BLAS backend and thread count: + +```julia-repl +julia> Threads.nthreads() +4 + +julia> using ThreadPinning; threadinfo(; blas = true, hints = true) + +System: 8 cores (2-way SMT), 1 sockets, 1 NUMA domains + +| 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15 | + +# = Julia thread, # = HT, # = Julia thread on HT, | = Socket separator + +Julia threads: 4 +├ Occupied CPU-threads: 4 +└ Mapping (Thread => CPUID): 1 => 8, 2 => 5, 3 => 9, 4 => 2, + +BLAS: libopenblas64_.so +└ openblas_get_num_threads: 8 + +[ Info: jlthreads != 1 && blasthreads < cputhreads. You should either set BLAS.set_num_threads(1) (recommended!) or at least BLAS.set_num_threads(16). +[ Info: jlthreads < cputhreads. Perhaps increase number of Julia threads to 16? +``` + +Passing `hints = true` makes ThreadPinning emit the advisory messages shown above. +Loading a different BLAS backend changes the report; with MKL, for example, `threadinfo` +reports `libmkl_rt.so` and warns when the per-Julia-thread BLAS thread count exceeds the +available CPU threads per Julia thread: + +```julia-repl +julia> using MKL; threadinfo(; blas = true, hints = true) + +System: 8 cores (2-way SMT), 1 sockets, 1 NUMA domains + +| 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15 | + +# = Julia thread, # = HT, # = Julia thread on HT, | = Socket separator + +Julia threads: 4 +├ Occupied CPU-threads: 4 +└ Mapping (Thread => CPUID): 1 => 11, 2 => 12, 3 => 1, 4 => 2, + +BLAS: libmkl_rt.so +├ mkl_get_num_threads: 8 +└ mkl_get_dynamic: true + +┌ Warning: blasthreads_per_jlthread > cputhreads_per_jlthread. You should decrease the number of MKL threads, i.e. BLAS.set_num_threads(4). +└ @ ThreadPinning ~/.julia/packages/ThreadPinning/qV2Cd/src/threadinfo.jl:256 +[ Info: jlthreads < cputhreads. Perhaps increase number of Julia threads to 16? +``` + +## Reducing memory usage + +MPSKit's multithreading spawns tasks in a nested fashion, each allocating and deallocating +memory in a tight loop. +This can put enough pressure on the garbage collector that memory usage climbs and, in the +worst case, an `OutOfMemory` error occurs before the garbage can be cleared. + +If you hit this, the most effective remedy is usually to disable MPSKit's multithreading, +by setting the scheduler to serial: + +```julia +MPSKit.Defaults.set_scheduler!(:serial) +``` + +The `derivatives` (the effective local operators applied during the sweeps) are reported to +be the most memory-intensive part, so this is where switching off multithreading helps most. + +For why this pressure arises, see +[Why memory pressure arises](@ref concept_parallelism_model). + +## GPU support + +!!! warning "Experimental" + GPU support in MPSKit is **experimental and minimal**. + There are no GPU-specific algorithms, kernels, or tuning options; the only surface is + an [Adapt.jl](https://github.com/JuliaGPU/Adapt.jl)-based mechanism for moving a state + or operator onto a different array type. + Treat this as preparatory infrastructure rather than a supported workflow. + +The package extension `MPSKitAdaptExt` defines `Adapt.adapt_structure` for `FiniteMPS`, +`InfiniteMPS`, `MPO`, and `MPOHamiltonian`. +This lets you convert the underlying tensors to a GPU array type with `Adapt.adapt`, after +which the algorithms dispatch through that array type. +A move onto a CUDA array would look like the following: + +```julia +using Adapt, CUDA +ψ_gpu = adapt(CuArray, ψ) +``` + diff --git a/docs/src/howto/quasi_1d_geometries.md b/docs/src/howto/quasi_1d_geometries.md new file mode 100644 index 000000000..1142de805 --- /dev/null +++ b/docs/src/howto/quasi_1d_geometries.md @@ -0,0 +1,150 @@ +# [Quasi-1D geometries](@id howto_quasi_1d_geometries) + +```@meta +DocTestSetup = quote + using MPSKit, MPSKitModels, TensorKit +end +``` + +MPSKit works with matrix product states, which are intrinsically one-dimensional objects. +A two-dimensional lattice can still be studied by winding it onto a single chain: the sites of the 2D lattice are placed in a linear order, and a 2D coupling becomes a (possibly long-ranged) coupling between two positions on that chain. +This is the standard "quasi-1D" or "cylinder" approach to 2D systems with tensor networks. + +This page collects recipes for building such geometries. +The lattice types and the `@mpoham` helper used here come from [MPSKitModels.jl](https://quantumkithub.github.io/MPSKitModels.jl/dev/), a companion package that supplies lattices, local operators, and ready-made model Hamiltonians. +For the underlying one-dimensional Hamiltonian construction see [Building Hamiltonians](@ref howto_hamiltonians); for the algorithms that consume the resulting operator see [Ground-state algorithms](@ref lib_groundstate). + +```@example quasi1d +using MPSKit, MPSKitModels, TensorKit +``` + +--- + +## Available lattice geometries + +MPSKitModels exposes a small family of lattice types, all subtypes of `AbstractLattice`. +The one-dimensional lattices are `FiniteChain` and `InfiniteChain`. +The genuinely two-dimensional geometries, wrapped for use on a 1D chain, are: + +- `FiniteCylinder(L, N)` and `InfiniteCylinder(L, N)` — a strip of circumference `L` rolled into a tube, so the two edges in the transverse direction are identified (periodic around the circumference). +- `FiniteStrip(L, N)` and `InfiniteStrip(L, N)` — the same rectangular patch but with *open* boundaries in the transverse direction (no wrap-around). +- `FiniteLadder(N)` and `InfiniteLadder(N)` — convenience constructors for the width-2 strip, i.e. `FiniteStrip(2, N)` / `InfiniteStrip(2, N)`. +- `FiniteHelix(L, N)` and `InfiniteHelix(L, N)` — a helical winding of the cylinder. +- `HoneycombYC(L, N)` — a honeycomb lattice on an infinite cylinder. + +For the square-lattice geometries the two integer arguments are the circumference `L` (number of sites per rung) and the total number of sites `N`; `N` must be a multiple of `L`, and it defaults to `L` (a single rung). +Constructing a lattice does not build any operator; it only fixes the geometry and the site ordering. + +```@example quasi1d +InfiniteCylinder(3) # circumference 3, one rung per unit cell +``` + +```@example quasi1d +InfiniteLadder(4) # a two-leg ladder, four sites per unit cell +``` + +!!! note + The `Finite*` variants describe a finite patch and produce a [`FiniteMPOHamiltonian`](@ref); the `Infinite*` variants describe a unit cell that repeats along the chain axis and produce an [`InfiniteMPOHamiltonian`](@ref). + Choose the pair that matches the state you intend to optimize. + +--- + +## Building a Hamiltonian on a cylinder + +The model builders in MPSKitModels accept a lattice as an optional positional argument, so switching from a chain to a cylinder is a one-word change. +Here is the transverse-field Ising model on an infinite cylinder of circumference 3: + +```@example quasi1d +H_cyl = transverse_field_ising(InfiniteCylinder(3); g = 3.0) +``` + +The result is an ordinary [`InfiniteMPOHamiltonian`](@ref) with one MPO tensor per site of the unit cell. +It is used exactly like a chain Hamiltonian: pair it with an [`InfiniteMPS`](@ref) whose unit cell has the same length and feed it to `find_groundstate` (see [Ground-state algorithms](@ref lib_groundstate)). +Because the cylinder wraps a 2D coupling onto the chain, the bond dimension required for a converged result grows quickly with the circumference; see the note at the end of this page. + +--- + +## Building a Hamiltonian on a ladder + +The same model builders work for a ladder: + +```@example quasi1d +H_ladder = heisenberg_XXX(InfiniteLadder(4); spin = 1 // 2) +``` + +For couplings that are not covered by a ready-made model, assemble the Hamiltonian directly with the `@mpoham` macro. +It sums single-site and two-site local operators over the vertices and bonds that the lattice reports. +The building blocks are `vertices(lattice)` (all sites) and `nearest_neighbours(lattice)` (all nearest-neighbour bonds, including the transverse rung and wrap-around bonds): + +```@example quasi1d +lat = InfiniteLadder(4) +H_manual = @mpoham sum(σᶻᶻ(){i, j} for (i, j) in nearest_neighbours(lat)) + + sum(2.0 * σˣ(){i} for i in vertices(lat)) +``` + +The `O{i, j}` syntax marks `O` as a local operator acting on sites `i` and `j`, where the indices are lattice points; `@mpoham` handles their placement on the 1D chain. +This is the two-dimensional counterpart of the manual chain construction in [Building Hamiltonians](@ref howto_hamiltonians). + +--- + +## How the 2D lattice maps onto the 1D chain + +Each 2D lattice defines a linear order on its sites through `linearize_index`, which turns a `(row, column)` coordinate into a single position along the MPS chain. +For a cylinder the sites of one rung are numbered first, then the next rung, and so on. +The rung index runs fastest: + +```@example quasi1d +cyl = InfiniteCylinder(3) +linearize_index.(collect(vertices(cyl))) +``` + +The bonds that `nearest_neighbours` returns reveal how far apart coupled sites end up on the chain. +Listing them as linear-index pairs for the same circumference-3 cylinder: + +```@example quasi1d +[linearize_index(i) => linearize_index(j) for (i, j) in nearest_neighbours(cyl)] +``` + +Two kinds of bonds appear. +Bonds along the chain axis connect a site to the corresponding site one rung over, at chain distance `L` (here `1 => 4`, `2 => 5`, `3 => 6`). +Bonds around the circumference connect neighbours within a rung; the bond that closes the ring connects the first and last site of a rung (here `3 => 1`), spanning `L - 1` sites on the chain. +This wrap-around bond is the longest-ranged coupling in the problem, and it is what makes a wider cylinder more expensive: the MPO must carry that coupling across `L - 1` sites, and the entanglement cut through the chain now spans the whole circumference. + +!!! warning "Circumference controls the cost" + The bond dimension needed for a given accuracy grows rapidly with the circumference `L`, because the entanglement across a cut scales with the length of the boundary it severs (the circumference). + Keep `L` small in exploratory runs and increase it while watching convergence. + +--- + +## Choosing the site ordering + +The default ordering above is not the only option: any permutation of the sites is a valid 1D chain, and a better ordering can shorten the longest-ranged bonds. +`SnakePattern` wraps a lattice together with a permutation function that maps the lattice's natural linear index to its position on the chain. +The wrapped lattice reports the same vertices and bonds, but re-indexed through the pattern. + +The example below reverses the site order within every second rung of a finite circumference-3 cylinder — a "boustrophedon" (back-and-forth) snake that keeps successive rungs adjacent: + +```@example quasi1d +finite_cyl = FiniteCylinder(3, 6) +pattern = i -> [1, 2, 3, 6, 5, 4][i] # reverse the second rung +snake = SnakePattern(finite_cyl, pattern) + +before = [linearize_index(i) => linearize_index(j) for (i, j) in nearest_neighbours(finite_cyl)] +after = [linearize_index(i) => linearize_index(j) for (i, j) in nearest_neighbours(snake)] +(before, after) +``` + +Passing `snake` to `@mpoham` (in place of `finite_cyl`) then builds the Hamiltonian in this reordered basis. +A `SnakePattern` built without a pattern, `SnakePattern(lattice)`, uses the identity ordering. + +!!! warning "Ordering helpers are broken at v0.4.7" + MPSKitModels also exports `backandforth_pattern` and `frontandback_pattern` as pre-built cylinder orderings, but at the pinned version (v0.4.7) they error: the returned closure indexes a lazy `Iterators.flatten` object, which has no `getindex` method. + This is a known upstream bug, so this page shows an explicit permutation instead. + + Any custom permutation must be defined for every linear index that the lattice's bonds reference. + On an *infinite* lattice the nearest-neighbour bonds reach into the next unit cell, so a permutation defined only on `1:N` errors there with a `BoundsError`; a pattern for an infinite cylinder has to wrap periodically, e.g. `pattern(i) = ((i - 1) ÷ N) * N + perm[mod1(i, N)]`. + The finite cylinder above sidesteps this. + +```@meta +DocTestSetup = nothing +``` diff --git a/docs/src/howto/saving_loading.md b/docs/src/howto/saving_loading.md new file mode 100644 index 000000000..8c4ebeba7 --- /dev/null +++ b/docs/src/howto/saving_loading.md @@ -0,0 +1,139 @@ +# [Saving and loading](@id howto_saving_loading) + +The examples on this page use MPSKit.jl and TensorKit.jl. +See [Installation](@ref tutorial_installation) for how to add these packages to your environment. + +MPSKit does **not** ship its own save/load functions. +States such as [`FiniteMPS`](@ref) and [`InfiniteMPS`](@ref) are ordinary Julia objects that wrap TensorKit `TensorMap`s, so any general-purpose Julia serializer stores and restores them. +Two options cover essentially all use cases: + +- **`Serialization`** — a standard-library module, always available, no extra dependency. + Best for quick "save a result and pick it up in the next session" workflows. + Its file format is **not** guaranteed stable across Julia or package versions (see [Caveats](@ref howto_saving_loading_caveats)). +- **[JLD2.jl](https://github.com/JuliaIO/JLD2.jl)** — a widely used HDF5-compatible format with named datasets and better long-term robustness. + Recommended when you want to archive data or share files between machines. + JLD2 is a separate package you must add with `] add JLD2`. + +The runnable recipes below use `Serialization` so they execute with no extra dependency. +The JLD2 variants are shown separately and are equivalent in what they store. + +```@example saveload +using MPSKit, TensorKit +using Serialization +``` + +--- + +## 1. Save and reload a finite MPS + +[`serialize`](https://docs.julialang.org/en/v1/stdlib/Serialization/#Serialization.serialize) writes any object to a file; `deserialize` reads it back. +Here we write to a temporary path and check that the reloaded state is identical by taking the overlap `⟨ψ | ψ_loaded⟩`, which is `1` (up to rounding) when the two states coincide. + +```@example saveload +ψ = FiniteMPS(10, ℂ^2, ℂ^16) + +path = tempname() # a fresh temporary file path +serialize(path, ψ) + +ψ_loaded = deserialize(path) +abs(dot(ψ, ψ_loaded)) # ≈ 1: the reloaded state equals the original +``` + +The reloaded object is a genuine [`FiniteMPS`](@ref), ready for any further computation: + +```@example saveload +ψ_loaded isa FiniteMPS +``` + +Nothing here is specific to a *random* state — the same holds for a state returned by [`find_groundstate`](@ref) or [`timestep`](@ref). +Save the state you actually care about the moment you have it. + +## 2. Save and reload an infinite MPS + +[`InfiniteMPS`](@ref) works exactly the same way. +Because an infinite state is normalized by its gauge, the overlap check above is not the natural diagnostic; instead compare the gauged tensors directly. + +```@example saveload +ψ∞ = InfiniteMPS(ℂ^2, ℂ^16) + +path∞ = tempname() +serialize(path∞, ψ∞) + +ψ∞_loaded = deserialize(path∞) +ψ∞_loaded.AL[1] ≈ ψ∞.AL[1] # left-gauged tensors match +``` + +## 3. Store several objects together + +To keep a state alongside metadata (parameters, a description, the energy you measured), serialize a `NamedTuple` or `Dict` in one file. +This keeps everything that belongs together in a single artifact. + +```@example saveload +result = (state = ψ, χ = 16, note = "TFIM ground state") + +path_result = tempname() +serialize(path_result, result) + +back = deserialize(path_result) +back.note +``` + +```@example saveload +abs(dot(back.state, ψ)) # the embedded state round-trips too +``` + +## 4. The JLD2 variant + +[JLD2.jl](https://github.com/JuliaIO/JLD2.jl) stores objects under string keys and is the more portable choice for archival data. +Add it with `] add JLD2` first. +The following is equivalent to the `Serialization` recipes above; it is not executed here because JLD2 is not a dependency of this documentation build. + +```julia +using JLD2 + +# save one or more named objects +jldsave("state.jld2"; ψ, χ = 16, note = "TFIM ground state") + +# load them back by name +ψ_loaded = load("state.jld2", "ψ") +note = load("state.jld2", "note") +``` + +Symmetric states round-trip through JLD2 without any extra work: the `TensorMap`s carry their own symmetry sectors and vector spaces, so a state built on, e.g., `Z2Space` is restored with its full symmetry structure intact. + +--- + +## Environments + +Cached [environments](@ref concept_environments) (the objects returned by `environments`, held inside the value from [`find_groundstate`](@ref) and friends) are serializable in exactly the same way as states — they are also just tensors. +In practice, however, **it is usually not worth saving them**: environments are derived data, tied to one specific state, and recomputing them from a stored state is cheap compared to the optimization that produced the state. + +The recommended workflow is therefore to save only the *state* and rebuild the environments after loading: + +```@example saveload +using MPSKitModels # for the Hamiltonian +H = transverse_field_ising(FiniteChain(10)) + +envs = environments(ψ_loaded, H, ψ_loaded) # rebuilt from the reloaded state +nothing # hide +``` + +If you do have a reason to persist environments (e.g. to resume an expensive iterative build), `serialize`/`deserialize` them just like a state. + +## [Caveats](@id howto_saving_loading_caveats) + +- **Version compatibility.** + `Serialization` files are **not** guaranteed to be readable by a different Julia version, nor after MPSKit or TensorKit change their internal type layout. + Treat `Serialization` output as a scratch artifact within one environment; use **JLD2** for anything you need to reopen weeks later or on another machine. + +- **Symmetric tensors are self-describing.** + A saved state carries the vector spaces and symmetry sectors of every tensor, so you do not need to record the symmetry separately — loading reconstructs the full space structure. + This was verified for a `Z2`-symmetric state round-tripping through both `Serialization` and JLD2. + +- **File size.** + A stored state is roughly the size of its tensors, which grows with the bond dimension (and, for symmetric states, the sector structure). + For large-bond-dimension states these files can be substantial; write them to scratch/bulk storage rather than a quota-limited home directory, and consider saving only the final state rather than every intermediate. + +- **What to save.** + Prefer saving the state (and the parameters needed to rebuild its Hamiltonian) over saving derived caches like environments. + A state plus its model definition is enough to reconstruct everything else. diff --git a/docs/src/howto/states.md b/docs/src/howto/states.md new file mode 100644 index 000000000..3ea69ea04 --- /dev/null +++ b/docs/src/howto/states.md @@ -0,0 +1,267 @@ +# [Constructing states](@id howto_states) + +The examples on this page use MPSKit.jl and TensorKit.jl. +See [Installation](@ref tutorial_installation) for how to add these packages to your environment. + +This page collects recipes for building [`FiniteMPS`](@ref), [`InfiniteMPS`](@ref), [`WindowMPS`](@ref), and [`MultilineMPS`](@ref) objects. +All constructors live in the `MPSKit` namespace; the examples below assume + +```@example howto_states +using MPSKit, TensorKit +``` + +For background on what these types represent and how gauging works, see the [States](@ref lib_states) reference page. + +--- + +## 1. A finite MPS + +### From length, physical space, and maximum bond dimension + +The most common starting point: give the chain length `N`, the local physical `VectorSpace`, and the maximum allowed virtual space. +The constructor fills the tensors with random `ComplexF64` entries and trims the actual bond dimensions to full rank, so passing an over-large `maxVspace` is safe. + +```@example howto_states +L = 10 +d = ℂ^2 # spin-1/2 physical space (dim 2) +D = ℂ^16 # maximum bond dimension + +ψ = FiniteMPS(L, d, D) +``` + +Inspect the resulting virtual spaces with `left_virtualspace` and `right_virtualspace`. +To get the numeric bond dimension at bond `i` use `dim`: + +```@example howto_states +dim(left_virtualspace(ψ, 3)) # bond dimension between sites 2 and 3 +``` + +```@example howto_states +physicalspace(ψ, 1) # local Hilbert space at site 1 +``` + +### Choosing the initializer and element type + +Pass an initializer function (`rand` or `randn`) and an element type as the first two arguments: + +```@example howto_states +ψ_rand = FiniteMPS(rand, ComplexF64, L, d, D) # default — same as FiniteMPS(L, d, D) +ψ_randn = FiniteMPS(randn, ComplexF64, L, d, D) # normally distributed entries +``` + +The element type sets the scalar type of the tensors, e.g. `ComplexF64` (the default) or `Float64` for a real-valued state. + +### Per-site physical and virtual spaces + +When the physical space varies from site to site — or you want fine control over which bond gets which maximum dimension — pass vectors instead of scalars. +The `maxVspaces` vector must have length `N - 1` (one entry per bond): + +```@example howto_states +Pspaces = [ℂ^2, ℂ^3, ℂ^2, ℂ^3, ℂ^2] # alternating physical spaces +maxVspaces = [ℂ^8, ℂ^8, ℂ^8, ℂ^8] # one per bond (length N-1) + +ψ_het = FiniteMPS(rand, ComplexF64, Pspaces, maxVspaces) +``` + +```@example howto_states +physicalspace(ψ_het, 2) # ℂ^3 +``` + +### A product state (trivial virtual space) + +A product (bond-dimension-1) state has no entanglement: each site carries its own single-site state, independent of the others (with `rand`, a random such state per site). +Achieve this by passing `oneunit(d)` — the one-dimensional unit space of the same symmetry sector — as the maximum virtual space: + +```@example howto_states +ψ_prod = FiniteMPS(rand, ComplexF64, L, ℂ^2, oneunit(ℂ^2)) +dim(left_virtualspace(ψ_prod, 5)) # should be 1 +``` + +!!! note + `oneunit(V)` returns the one-dimensional trivial space matching the symmetry type of `V`. + For plain complex spaces, `oneunit(ℂ^2) == ℂ^1`. + +### From your own site tensors + +If you already have a vector of `TensorMap` objects with the correct index structure (virtual ⊗ physical ← virtual), pass them directly. +The constructor performs a left-to-right QR sweep to bring the state into a canonical form: + +```@example howto_states +# build three-site rank-1 tensors by hand +site_tensors = [rand(ComplexF64, ℂ^1 ⊗ ℂ^2 ← ℂ^1) for _ in 1:L] +ψ_from_tensors = FiniteMPS(site_tensors) +``` + +Set `normalize = true` to also normalize the state during construction (the default is `false` when passing raw tensors): + +```@example howto_states +ψ_normed = FiniteMPS(site_tensors; normalize = true) +``` + +--- + +## 2. An infinite MPS + +### Scalar convenience form + +Provide `d` and `D` as integers or spaces; the constructor builds a single-site unit cell: + +```@example howto_states +ψ_inf = InfiniteMPS(2, 20) # integers → plain ComplexSpace dimensions +``` + +```@example howto_states +ψ_inf2 = InfiniteMPS(ℂ^2, ℂ^20) # same, spelled out as spaces +``` + +### Multi-site unit cell + +Pass vectors of physical and virtual spaces. +The virtual spaces are those to the *right* of the corresponding sites: + +```@example howto_states +ψ_2site = InfiniteMPS([ℂ^2, ℂ^2], [ℂ^20, ℂ^20]) +``` + +```@example howto_states +physicalspace(ψ_2site, 1) +``` + +```@example howto_states +right_virtualspace(ψ_2site, 1) # virtual space to the right of site 1 +``` + +### Choosing element type and initializer + +```@example howto_states +ψ_inf_r = InfiniteMPS(rand, Float64, [ℂ^2], [ℂ^10]) +``` + +### From site tensors + +Tensors must form a valid periodic chain (virtual spaces must match across the unit-cell boundary): + +```@example howto_states +inf_tensors = [rand(ComplexF64, ℂ^4 ⊗ ℂ^2 ← ℂ^4)] +ψ_inf_t = InfiniteMPS(inf_tensors) +``` + +--- + +## 3. A window MPS + +A [`WindowMPS`](@ref) embeds a mutable finite window inside two infinite environments. + +### Slice an existing InfiniteMPS + +The simplest route: pick a region of length `L` from an `InfiniteMPS`. +Both environments are set to the same object (the original infinite state): + +```@example howto_states +ψ_bulk = InfiniteMPS(ℂ^2, ℂ^8) +ψ_win = WindowMPS(ψ_bulk, 6) # window of 6 sites +``` + +```@example howto_states +length(ψ_win) # 6 +``` + +### From space specifications + +Provide the window dimensions together with the infinite environments. +The boundary virtual spaces are taken automatically from `ψₗ`/`ψᵣ`: + +```@example howto_states +ψ_win2 = WindowMPS(rand, ComplexF64, 6, ℂ^2, ℂ^8, ψ_bulk) +``` + +### From a FiniteMPS and two environments + +Build a `FiniteMPS` with matching boundary virtual spaces first, then wrap: + +```@example howto_states +finite_part = FiniteMPS(6, ℂ^2, ℂ^8; left = ℂ^8, right = ℂ^8) +ψ_win3 = WindowMPS(ψ_bulk, finite_part, ψ_bulk) +``` + +!!! warning + When `ψᵣ` is omitted in the outer constructors, the right environment is + **the same object** as the left environment (no copy is made). + If you later evolve the two environments independently, pass `copy(ψ_bulk)` + explicitly as the right argument to avoid aliasing: + + ```julia + ψ_win_safe = WindowMPS(rand, ComplexF64, 6, ℂ^2, ℂ^8, ψ_bulk, copy(ψ_bulk)) + ``` + +--- + +## 4. A multiline MPS + +[`MultilineMPS`](@ref) stacks several [`InfiniteMPS`](@ref) rows and is used in boundary-MPS methods for 2D classical partition functions. + +### From a vector of InfiniteMPS rows + +```@example howto_states +row1 = InfiniteMPS(ℂ^2, ℂ^8) +row2 = InfiniteMPS(ℂ^2, ℂ^8) +ψ_ml = MultilineMPS([row1, row2]) +``` + +Access tensors with Cartesian `[row, col]` indexing: + +```@example howto_states +ψ_ml.AL[1, 1] # left-gauged tensor of row 1, unit-cell site 1 +``` + +### From space matrices + +Pass matrices whose rows correspond to MPS rows and columns to unit-cell sites: + +```@example howto_states +pspaces = fill(ℂ^2, 2, 2) # 2 rows × 2-site unit cell +Dspaces = fill(ℂ^8, 2, 2) +ψ_ml2 = MultilineMPS(pspaces, Dspaces) +``` + +--- + +## 5. States with symmetries + +All constructors accept TensorKit graded spaces. +Pass a `Rep[G]` physical space and a `Rep[G]` maximum virtual space; the constructor automatically selects the consistent fusion channels. + +### Finite MPS with U(1) symmetry + +```@example howto_states +# U(1) spin-1/2: physical space = spin up (charge +1/2) + spin down (charge -1/2) +d_u1 = Rep[U₁](1 // 2 => 1, -1 // 2 => 1) # dim 2 total +# the virtual space must span both charge parities (integer and half-integer): +# with only ±1/2 on each site, the total charge alternates parity bond to bond, +# so a purely half-integer virtual space would starve every even bond +D_u1 = Rep[U₁](0 => 2, 1 // 2 => 2, -1 // 2 => 2, 1 => 1, -1 => 1) + +ψ_u1 = FiniteMPS(rand, ComplexF64, L, d_u1, D_u1) +physicalspace(ψ_u1, 1) +``` + +```@example howto_states +dim(left_virtualspace(ψ_u1, 5)) # actual trimmed bond dimension ≤ dim(D_u1) +``` + +!!! note + The boundary virtual spaces default to `oneunit(spacetype(d_u1))`, i.e. the charge-0 sector. + Use the `left` and `right` keywords to target a different total charge: + + ```julia + # state in total charge-sector +1 (one more up-spin than down-spin) + ψ_charged = FiniteMPS(rand, ComplexF64, L, d_u1, D_u1; + right = Rep[U₁](1 => 1)) + ``` + +### Infinite MPS with U(1) symmetry + +```@example howto_states +ψ_inf_u1 = InfiniteMPS(d_u1, D_u1) +physicalspace(ψ_inf_u1, 1) +``` diff --git a/docs/src/howto/statmech.md b/docs/src/howto/statmech.md new file mode 100644 index 000000000..ff41f49c5 --- /dev/null +++ b/docs/src/howto/statmech.md @@ -0,0 +1,161 @@ +# [Statistical mechanics](@id howto_statmech) + +```@meta +DocTestSetup = quote + using MPSKit, MPSKitModels, TensorKit +end +``` + +This page collects recipes for the *boundary-MPS* (transfer-matrix) approach to two-dimensional classical statistical mechanics. +The partition function of a classical lattice model is written as a contraction of a two-dimensional tensor network, one row of which is an [`InfiniteMPO`](@ref) — the *transfer matrix*. +Contracting the network in the thermodynamic limit amounts to finding the leading eigenvector of that transfer matrix, which MPSKit approximates by an [`InfiniteMPS`](@ref) via [`leading_boundary`](@ref). + +The examples on this page use MPSKit.jl, MPSKitModels.jl, and TensorKit.jl. +See [Installation](@ref tutorial_installation) for how to add these packages to your environment. + +```@example statmech +using MPSKit, MPSKitModels, TensorKit +``` + +For the structure of MPOs see [Operators and Hamiltonians](@ref concept_operators_and_hamiltonians). +For a full worked case using anyonic symmetries, see the gallery example [The Hard Hexagon model](@ref "The Hard Hexagon model"). + +--- + +## 1. Build the transfer matrix + +MPSKitModels ships ready-made transfer-matrix MPOs for several classical models. +The two-dimensional classical Ising model is provided by `classical_ising`; other options include `sixvertex` and `hard_hexagon`. + +```@example statmech +mpo = classical_ising() +``` + +The returned object is an `InfiniteMPO` with a single-site unit cell: one tensor whose four legs are the two horizontal (virtual) and two vertical (physical) bonds of the Boltzmann-weight tensor. +Its physical space is read off with `physicalspace`: + +```@example statmech +P = physicalspace(mpo, 1) +``` + +By default `classical_ising` uses the inverse temperature `beta = log(1 + sqrt(2)) / 2`. + +--- + +## 2. Find the leading boundary MPS + +[`leading_boundary`](@ref) approximates the dominant eigenvector of the transfer matrix by an `InfiniteMPS`. +Supply an initial guess with a chosen bond dimension and an optimization algorithm — [`VUMPS`](@ref) is the usual choice. + +The transfer matrix of a classical model is generally **not Hermitian**, so pass a non-Hermitian eigensolver to `VUMPS`: + +```@example statmech +alg = VUMPS(; + verbosity = 0, + alg_eigsolve = MPSKit.Defaults.alg_eigsolve(; ishermitian = false), +) + +ψ₀ = InfiniteMPS([P], [ℂ^16]) # initial guess, bond dimension 16 +ψ, envs, ϵ = leading_boundary(ψ₀, mpo, alg) +ϵ # final convergence error +``` + +`leading_boundary` returns a triple `(ψ, environments, ϵ)`: the converged boundary MPS, its environment manager, and the final convergence error. +Reuse the returned `envs` in subsequent calls to avoid recomputing environments. + +!!! note + The initial guess sets the bond dimension `D` of the boundary MPS. + A larger `D` gives a better approximation of the leading eigenvector; near a critical point the accessible correlation length grows with `D` (see [Controlling bond dimension](@ref howto_bond_dimension) for growing `D` on the fly). + +--- + +## 3. Free energy and partition function per site + +The expectation value of the transfer matrix in the converged boundary MPS is the partition function per site ``\Lambda = \mathcal{Z}^{1/N}`` in the thermodynamic limit: + +```@example statmech +Λ = expectation_value(ψ, mpo) +``` + +For a Hermitian-normalised model the imaginary part is zero up to floating-point noise. +The free energy per site follows from ``f = -\tfrac{1}{\beta}\log\Lambda``: + +```@example statmech +β = log(1 + sqrt(2)) / 2 +f = -1 / β * log(real(Λ)) +``` + +--- + +## 4. Correlation length and entanglement entropy + +The boundary MPS encodes the correlations of the two-dimensional system. +[`correlation_length`](@ref) returns the (largest) correlation length of the transfer matrix, and [`entropy`](@ref) the entanglement entropy of the boundary MPS across a virtual bond: + +```@example statmech +ξ = maximum(values(correlation_length(ψ))) +``` + +```@example statmech +S = real(first(entropy(ψ))) +``` + +At a critical point the true correlation length diverges; the finite bond dimension of the boundary MPS cuts it off at a finite value that grows with `D`. +This finite-entanglement scaling of `S` against `log(ξ)` is exactly what the gallery example [The Hard Hexagon model](@ref "The Hard Hexagon model") exploits to extract a central charge. + +--- + +## 5. Symmetric transfer matrices + +When the classical model has a global symmetry, the transfer matrix can be built from symmetric tensors, which makes the boundary computation cheaper and more stable. +`classical_ising` accepts a symmetry type; the ``\mathbb{Z}_2`` spin-flip symmetry gives a `Z2Irrep`-graded MPO: + +```@example statmech +mpo_z2 = classical_ising(Z2Irrep) +P_z2 = physicalspace(mpo_z2, 1) +``` + +The workflow is identical — only the virtual space of the initial guess is now a graded space: + +```@example statmech +V_z2 = Z2Space(0 => 8, 1 => 8) +ψ_z2, = leading_boundary(InfiniteMPS([P_z2], [V_z2]), mpo_z2, alg) +real(expectation_value(ψ_z2, mpo_z2)) +``` + +For anyonic (non-invertible) symmetries the same recipe applies with a `Vect[FibonacciAnyon]` virtual space — this is the case worked out in [The Hard Hexagon model](@ref "The Hard Hexagon model"). +See [Symmetries](@ref concept_symmetries) for how to choose graded spaces. + +--- + +## 6. Multi-row unit cells + +If the transfer matrix has a unit cell spanning several rows, the boundary object becomes a [`MultilineMPS`](@ref) and the operator a [`MultilineMPO`](@ref). +`repeat` stacks copies of a single-row MPO into a multi-row transfer matrix: + +```@example statmech +mmpo = repeat(mpo, 2, 1) # 2 rows × 1 column +``` + +Build a `MultilineMPS` with one `InfiniteMPS` per row and call `leading_boundary` exactly as before; the returned environments are a `MultilineEnvironments`: + +```@example statmech +mψ = MultilineMPS([InfiniteMPS([P], [ℂ^12]), InfiniteMPS([P], [ℂ^12])]) +mψ, menvs, = leading_boundary(mψ, mmpo, alg) +real(expectation_value(mψ, mmpo)) +``` + +The `expectation_value` of a multi-row transfer matrix is the product of the per-site weights over the rows of the unit cell; divide `log` of it by the number of rows to recover the per-site free energy. + +--- + +## See also + +- [The Hard Hexagon model](@ref "The Hard Hexagon model") — a full worked study (central charge from finite-entanglement scaling) using an anyonic transfer matrix. +- [Operators and Hamiltonians](@ref concept_operators_and_hamiltonians) — MPO structure and construction. +- [Controlling bond dimension](@ref howto_bond_dimension) — growing the boundary-MPS bond dimension. +- [Computing observables](@ref howto_observables) — `expectation_value`, `correlation_length`, and related tools. + +```@meta +DocTestSetup = nothing +``` diff --git a/docs/src/howto/time_evolution.md b/docs/src/howto/time_evolution.md new file mode 100644 index 000000000..ef80595a2 --- /dev/null +++ b/docs/src/howto/time_evolution.md @@ -0,0 +1,210 @@ +# [Time evolution](@id howto_time_evolution) + +The examples on this page use MPSKit.jl, MPSKitModels.jl, TensorKit.jl, and TensorKitTensors.jl. +See [Installation](@ref tutorial_installation) for how to add these packages to your environment. + +MPSKit solves the (real- or imaginary-time) Schrödinger equation `i ∂ψ/∂t = H ψ` in two ways: by projecting the equation onto the MPS tangent space at every step ([`TDVP`](@ref)/[`TDVP2`](@ref)), or by first building an approximate evolution operator as an MPO ([`make_time_mpo`](@ref)) and repeatedly applying it. +This page gives task recipes for both routes. +All examples share a single namespace: + +```@example time_evo +using MPSKit, MPSKitModels, TensorKit +using TensorKitTensors.SpinOperators: σˣ, σᶻ +``` + +--- + +## 1. Evolve a state through one time step + +[`timestep`](@ref) advances a state by a single `dt` under a Hamiltonian, using whichever [`TDVP`](@ref)-family algorithm you pass. +Its argument order is state, Hamiltonian, current time, time step, algorithm: + +```@example time_evo +L = 8 +g₀ = 0.5 +H₀ = transverse_field_ising(FiniteChain(L); g = g₀) + +ψ₀ = FiniteMPS(L, ℂ^2, ℂ^8) +ψ₀, = find_groundstate(ψ₀, H₀, DMRG(; verbosity = 0)) +expectation_value(ψ₀, 4 => σᶻ()) +``` + +Now quench to a different transverse field `g₁` and take a single step: + +```@example time_evo +g₁ = 2.0 +H₁ = transverse_field_ising(FiniteChain(L); g = g₁) + +dt = 0.05 +ψ₁, envs₁ = timestep(ψ₀, H₁, 0.0, dt, TDVP()) +expectation_value(ψ₁, 4 => σᶻ()) +``` + +`timestep` returns the updated state together with an `envs` cache; reuse `envs₁` in the next call to avoid recomputation. +An in-place `timestep!` also exists, but only for finite MPS. + +--- + +## 2. Evolve over a time span + +[`time_evolve`](@ref) steps through an explicit vector of time points instead of a single `dt`, carrying the algorithm and environments through the whole span: + +```@example time_evo +t_span = 0:dt:(4dt) +ψ_span, envs_span = time_evolve(ψ₀, H₁, t_span, TDVP(); verbosity = 0) +expectation_value(ψ_span, 4 => σᶻ()) +``` + +`t_span` need not be uniformly spaced — `time_evolve` steps pairwise between consecutive entries, so any `AbstractVector` of increasing times works. +There is no exported `time_evolve!`; use `time_evolve` and rebind the result. + +--- + +## 3. Grow the bond dimension while evolving + +Single-site `TDVP` cannot change the bond dimension: whatever `ψ₀` starts with is what it keeps. +After a quench, entanglement typically grows and a fixed bond dimension eventually becomes insufficient. +[`TDVP2`](@ref) updates two sites at a time and truncates back down, so it can grow (or shrink) the bond dimension as it evolves. +Unlike `TDVP`, `TDVP2` requires `trunc` — there is no default: + +```@example time_evo +ψ_tdvp2, envs_tdvp2 = timestep(ψ₀, H₁, 0.0, dt, TDVP2(; trunc = truncrank(16))) +dim(left_virtualspace(ψ_tdvp2, 4)) +``` + +`TDVP2` only has a finite-MPS method. +For finite systems, an alternative to switching algorithms entirely is single-site `TDVP` with `alg_expand` set to a bond-expansion algorithm such as [`OptimalExpand`](@ref) (Controlled Bond Expansion, "CBE-TDVP"); see [Controlling bond dimension](@ref howto_bond_dimension) for `OptimalExpand` and other `changebonds` recipes. + +--- + +## 4. Evolve an infinite state + +Single-site `TDVP` also works directly on an `InfiniteMPS`: + +```@example time_evo +ψ₀_inf = InfiniteMPS(ℂ^2, ℂ^8) +H₀_inf = transverse_field_ising(; g = g₀) +ψ₀_inf, = find_groundstate(ψ₀_inf, H₀_inf, VUMPS(; verbosity = 0)) + +H₁_inf = transverse_field_ising(; g = g₁) +ψ₁_inf, envs₁_inf = timestep(ψ₀_inf, H₁_inf, 0.0, dt, TDVP()) +expectation_value(ψ₁_inf, 1 => σᶻ()) +``` + +`TDVP2` has no `InfiniteMPS` method, and single-site `TDVP` cannot change the bond dimension on an infinite state either. +Grow the bond dimension beforehand with `changebonds` (e.g. `OptimalExpand` or `VUMPSSvdCut`) — see [Controlling bond dimension](@ref howto_bond_dimension) — then evolve at the fixed, larger bond dimension. + +--- + +## 5. Imaginary-time evolution + +Passing `imaginary_evolution = true` evolves under `exp(-H dt)` instead of `exp(-iH dt)`, using the same real `dt`: + +```@example time_evo +ψ_im, envs_im = timestep(ψ₀, H₁, 0.0, dt, TDVP(); imaginary_evolution = true) +norm(ψ_im) +``` + +The norm is *not* renormalized by default, so `norm(ψ_im)` carries the decaying weight of the un-normalized state rather than staying at `1`. +Pass `normalize = true` to renormalize after every step: + +```@example time_evo +ψ_norm, = timestep(ψ₀, H₁, 0.0, dt, TDVP(); imaginary_evolution = true, normalize = true) +norm(ψ_norm) +``` + +Repeated imaginary-time steps damp excited-state components faster than the ground state, so this is often used as a (slower) alternative to `find_groundstate` for driving a state towards the ground state of `H₁` — and that is the case where `normalize = true` is wanted, since otherwise the state's weight decays away. +`time_evolve` accepts both keywords for a span of imaginary-time steps. + +!!! note "`normalize` is independent of `imaginary_evolution`" + Renormalization used to be tied to imaginary time. It is now a separate `normalize` keyword defaulting to `false`, so the norm is preserved in both real and imaginary time — in real time it accumulates the truncation error, and in imaginary time it holds the decaying weight. + +--- + +## 6. Let the bond dimension adapt with `BUG` + +[`BUG`](@ref) is a single-site integrator for finite MPS that, unlike [`TDVP`](@ref), advances both the basis and the core tensor *forward* in time — it has no backward-in-time substep, which is what makes `TDVP`'s core step awkward at large imaginary-time steps. +Passing a truncating `trunc` makes it rank-adaptive, so the bond dimension tracks the entanglement instead of being fixed up front: + +```@example time_evo +ψ_bug, envs_bug = timestep(ψ₀, H₁, 0.0, dt, BUG(; trunc = truncrank(8))) +maximum(i -> dim(left_virtualspace(ψ_bug, i)), 1:length(ψ_bug)) +``` + +!!! warning "`truncrank(D)` leaves a state of dimension `2D`" + Each local update truncates the bond *ahead* of it and then augments the basis with the newly discovered directions without truncating, so the augmentation of one half-sweep is what the next half-sweep truncates. A `truncrank(D)` therefore ends the sweep at dimension `2D`. To come back down to `D`, follow up with [`changebonds`](@ref) and an [`SvdCut`](@ref): + + ```@example time_evo + ψ_bug_cut = changebonds(ψ_bug, SvdCut(; trunc = truncrank(8))) + ``` + +`BUG` is finite-only; there is no `InfiniteMPS` method. +Like the other integrators it leaves the norm alone unless you pass `normalize = true`. + +--- + +## 7. Build a time-evolution MPO + +When `H` is time-independent and `dt` is fixed, an alternative to repeated `timestep` calls is to build the evolution operator once as an MPO with [`make_time_mpo`](@ref), then apply it repeatedly with [`approximate`](@ref). + +[`WII`](@ref) builds a low-order MPO approximation and works for both finite and infinite Hamiltonians: + +```@example time_evo +O = make_time_mpo(H₁, dt, WII()) +``` + +[`TaylorCluster`](@ref) gives higher-order control via its `N` keyword (and accepts an additional `tol` keyword in `make_time_mpo`): + +```@example time_evo +O_taylor = make_time_mpo(H₁, dt, TaylorCluster(; N = 2); tol = 1.0e-10) +``` + +[`WI`](@ref) is a ready-made first-order `TaylorCluster` constant, so it is used as a value, not called as a constructor: + +```@example time_evo +O_wi = make_time_mpo(H₁, dt, WI) +``` + +Applying the MPO to a finite state uses [`approximate`](@ref) with a finite ground-state-style algorithm such as [`DMRG2`](@ref), which returns a 3-tuple including the final convergence error: + +```@example time_evo +ψ_mpo, envs_mpo, ϵ_mpo = approximate( + ψ₀, (O, ψ₀), DMRG2(; trunc = truncrank(16), verbosity = 0) +) +expectation_value(ψ_mpo, 4 => σᶻ()) +``` + +Repeat the `approximate` call with the same `O` for successive time steps to build up a longer evolution. +Imaginary-time MPOs are built the same way, by passing `imaginary_evolution = true` to `make_time_mpo`; a real `dt` is promoted internally, so no manual complex conversion is needed. + +### A cheaper alternative, with a caveat + +[`Zipup`](@ref) approximates a finite MPO–MPS product in a single sweep instead of optimizing variationally: it contracts one site at a time and truncates the enlarged bond immediately. +It needs no initial guess, so the state is passed only as the operand, and it returns just `(ψ, ϵ)` rather than the variational 3-tuple. +The call shape, on an MPO built from plain tensors: + +```@example time_evo +Vs = [oneunit(ℂ^3); fill(ℂ^3, L - 1); oneunit(ℂ^3)] +O_plain = FiniteMPO([rand(ComplexF64, Vs[i] ⊗ ℂ^2 ← ℂ^2 ⊗ Vs[i + 1]) for i in 1:L]) + +ψ_zip, ϵ_zip = approximate((O_plain, ψ₀), Zipup(; trunc = truncrank(16))) +ϵ_zip +``` + +Following [paeckel2019](@cite), a sharper result for a target bond dimension comes from zipping up permissively and imposing the final truncation on the way back, which `trunc` accepts as a tuple: + +```@example time_evo +ψ_zip2, ϵ_zip2 = approximate((O_plain, ψ₀), Zipup(; trunc = (truncrank(32), truncrank(16)))) +ϵ_zip2 +``` + +`Zipup` is for finite, open-boundary MPO–MPS products only. + +--- + +## Where to go next + +For choosing and configuring ground-state algorithms to prepare the pre-quench state, see [Ground-state algorithms](@ref howto_groundstate_algorithms). +For growing or shrinking bond dimension between or during evolution steps, see [Controlling bond dimension](@ref howto_bond_dimension). +For extracting expectation values and correlators from the evolved state, see [Computing observables](@ref howto_observables). +For background on the TDVP and time-evolution-MPO approaches and how they relate, see [Time evolution](@ref lib_time_evolution). diff --git a/docs/src/index.md b/docs/src/index.md index dd86bb1ba..efbb2bb9b 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -6,14 +6,14 @@ layout: home hero: name: MPSKit.jl text: Matrix product states in Julia - tagline: Efficient and versatile tools for working with matrix product states + tagline: Finite and infinite systems through one interface, with abelian, non-abelian, fermionic, and anyonic symmetries built in. image: src: /logo.svg alt: MPSKit.jl actions: - theme: brand - text: Manual - link: /man/intro + text: Get started + link: /tutorials/installation - theme: alt text: Examples link: /examples/ @@ -22,211 +22,157 @@ hero: link: https://github.com/QuantumKitHub/MPSKit.jl features: - - icon: 🔗 - title: States - details: Construction and manipulation of finite and infinite Matrix Product States (MPS). - - icon: 📏 - title: Observables - details: Calculation of observables and expectation values. - - icon: 🎯 - title: Optimization - details: Various optimization methods for obtaining MPS fixed points. - - icon: ⚛️ - title: Symmetries - details: Support for a wide variety of symmetries, including Abelian, non-Abelian, fermionic and anyonic symmetries. + - icon: + src: /icons/finite-infinite.svg + alt: A finite chain above an infinite one + title: Finite & infinite, one interface + details: Run the same calculation on a finite chain or directly in the thermodynamic limit. FiniteMPS and InfiniteMPS share an API, so switching between them is a one-line change. + - icon: + src: /icons/symmetry.svg + alt: A symmetric hexagon + title: Every symmetry + details: Abelian, non-Abelian, fermionic, and anyonic symmetries out of the box via the TensorKit backend — smaller bond dimensions and exact quantum numbers. + - icon: + src: /icons/algorithms.svg + alt: An energy minimum + title: A complete algorithm suite + details: Ground states with DMRG, VUMPS, and IDMRG; real- and imaginary-time evolution with TDVP; and momentum-resolved excitations via the quasiparticle ansatz. + - icon: + src: /icons/fast.svg + alt: A lightning bolt + title: Fast by design + details: Type-stable code paths and deliberate allocation strategies keep calculations quick out of the box, and non-Abelian symmetries such as SU(2) shrink the tensors you store and contract. --- ``` -## Table of contents - -- [Prerequisites](@ref) -- [States](@ref um_states) -- [Operators](@ref um_operators) -- [Algorithms](@ref um_algorithms) -- [Parallelism in julia](@ref) -- [Lattices](@ref lattices) +MPSKit.jl simulates one-dimensional quantum many-body systems with matrix product states and operators, at finite size or directly in the thermodynamic limit. +Built on the [TensorKit.jl](https://github.com/Jutho/TensorKit.jl) tensor backend, it is aimed at researchers and students who want tensor-network calculations without reimplementing the underlying machinery. ## Installation -MPSKit.jl is a part of the general registry, and can be installed via the package manager -as: +MPSKit.jl is a part of the general registry. +Together with the packages used throughout this documentation, it can be installed via the +package manager as: ``` -pkg> add MPSKit +pkg> add MPSKit TensorKit MPSKitModels TensorKitTensors Plots ``` +- `MPSKit` provides the matrix product state and operator types, together with the + ground-state, time-evolution, and bond-dimension algorithms. +- `TensorKit` supplies the tensor backend (`TensorMap`s and vector spaces) that MPSKit is + built on; it also re-exports the `@tensor` macro for contracting tensors by hand, along + with truncation-scheme constructors such as `truncrank` (from MatrixAlgebraKit). +- `MPSKitModels` collects pre-defined Hamiltonians and local operators for common physical + models. +- `TensorKitTensors` provides ready-made local operators, such as the Pauli operators used throughout the documentation. +- `Plots` is used to visualize results in several of the how-to guides and examples. -## Usage - -To get started with MPSKit, we recommend also including -[TensorKit.jl](https://github.com/Jutho/TensorKit.jl) and -[MPSKitModels.jl](https://github.com/QuantumKitHub/MPSKitModels.jl). The former defines the -tensor backend which is used throughout MPSKit, while the latter includes some common -operators and models. +For a step-by-step walkthrough that sets up a dedicated environment and verifies the installation, see [Installation](@ref tutorial_installation). -```julia -using TensorOperations -using TensorKit -using MPSKit -using LinearAlgebra: norm -``` +## A first calculation -### Finite Matrix Product States +Almost every MPSKit calculation follows the same three steps: build a Hamiltonian, optimize a state, and read off observables. +The transverse-field Ising chain (TFIM) makes each step concrete in a few lines. -```@setup finitemps -using LinearAlgebra -using TensorOperations -using TensorKit -using MPSKit +```@raw html +A matrix product state: a chain of tensors joined by virtual bonds, each with a physical leg ``` -Finite MPS are characterised by a set of tensors, one for each site, which each have 3 legs. -They can be constructed by specifying the virtual spaces and the physical spaces, i.e. the -dimensions of each of the legs. These are then contracted to form the MPS. In MPSKit, they -are represented by `FiniteMPS`, which can be constructed either by passing in the tensors -directly, or by specifying the dimensions of the legs. +A matrix product state is a chain of tensors: the horizontal bonds carry the virtual indices, and the leg hanging off each site is its physical index. -```@example finitemps -d = 2 # physical dimension -D = 5 # virtual dimension -L = 10 # number of sites +### 1. Build a Hamiltonian -mps = FiniteMPS(L, ComplexSpace(d), ComplexSpace(D)) # random MPS with maximal bond dimension D -``` +MPO Hamiltonians are assembled directly from local operators, so an arbitrary model — not just the built-in ones — takes only a couple of lines. +Here the single-site Pauli operators come from TensorKitTensors, and the TFIM is a nearest-neighbour `σᶻσᶻ` coupling plus a transverse `σˣ` field: -The `FiniteMPS` object then handles the gauging of the MPS, which is necessary for many of -the algorithms. This is done automatically when needed, and the user can access the gauged -tensors by getting and setting the `AL`, `AR`, `CR`/`CL` and `AC` fields, which each -represent a vector of these tensors. +```@example index +using MPSKit, TensorKit +using TensorKitTensors.SpinOperators: σˣ, σᶻ -```@example finitemps -al = mps.AL[3] # left gauged tensor of the third site -@tensor E[a; b] := al[c, d, b] * conj(al[c, d, a]) -@show isapprox(E, id(right_virtualspace(mps, 3))) -``` -```@example finitemps -ar = mps.AR[3] # right gauged tensor of the third site -@tensor E[a; b] := ar[a, d, c] * conj(ar[b, d, c]) -@show isapprox(E, id(left_virtualspace(mps, 3))) +L = 16 +g = 0.5 +lattice = fill(ℂ^2, L) +H = FiniteMPOHamiltonian(lattice, (i, i + 1) => -(σᶻ() ⊗ σᶻ()) for i in 1:(L - 1)) + + FiniteMPOHamiltonian(lattice, (i,) => -g * σˣ() for i in 1:L) ``` -As the mps will be kept in a gauged form, updating a tensor will also update the gauged -tensors. For example, we can set the tensor of the third site to the identity, and the -gauged tensors will be updated accordingly. +See [Building Hamiltonians](@ref howto_hamiltonians) for infinite lattices, longer-range terms, and boundary conditions. -```@example finitemps -mps.C[3] = id(domain(mps.C[3])) -mps -``` +### 2. Optimize a state -These objects can then be used to compute observables and expectation values. For example, -the expectation value of the identity operator at the third site, which is equal to the norm -of the MPS, can be computed as: +Start from an initial [`FiniteMPS`](@ref) of bond dimension 16 and pass it, together with the Hamiltonian, to [`find_groundstate`](@ref). +The algorithm — here [`DMRG`](@ref) — is an ordinary argument, and its keywords (tolerance, iteration count, verbosity) tune the optimization: -```@example finitemps -N1 = LinearAlgebra.norm(mps) -N2 = expectation_value(mps, 3 => id(physicalspace(mps, 3))) -println("‖mps‖ = $N1") -println(" = $N2") +```@example index +ψ₀ = FiniteMPS(L, ℂ^2, ℂ^16) +ψ, envs, ϵ = find_groundstate(ψ₀, H, DMRG(; tol = 1e-10, verbosity = 0)) +ϵ # final convergence error ``` -Finally, the MPS can be optimized in order to determine groundstates of given Hamiltonians. -Using the pre-defined models in `MPSKitModels`, we can construct the ground state for the -transverse field Ising model: +Choosing a different optimizer such as [`VUMPS`](@ref), or raising the bond dimension, is a one-line change; see [Ground-state algorithms](@ref howto_groundstate_algorithms). -```@example finitemps -J = 1.0 -g = 0.5 -lattice = fill(ComplexSpace(2), 10) -X = TensorMap(ComplexF64[0 1; 1 0], ComplexSpace(2), ComplexSpace(2)) -Z = TensorMap(ComplexF64[1 0; 0 -1], space(X)) -H = FiniteMPOHamiltonian(lattice, (i, i+1) => -J * X ⊗ X for i in 1:length(lattice)-1) + - FiniteMPOHamiltonian(lattice, (i,) => - g * Z for i in 1:length(lattice)) -find_groundstate!(mps, H, DMRG(; maxiter=10)) -E0 = expectation_value(mps, H) -println(" = $real(E0)") -``` +### 3. Read off observables -### Infinite Matrix Product States +Expectation values are a single call. +The ground-state energy is just the Hamiltonian evaluated on the state: -```@setup infinitemps -using LinearAlgebra -using TensorOperations -using TensorKit -using MPSKit +```@example index +E = expectation_value(ψ, H) ``` -Similarly, an infinite MPS can be constructed by specifying the tensors for the unit cell, -characterised by the spaces (dimensions) thereof. +Local operators, correlators, and entanglement measures work the same way. +For instance, the von Neumann [`entropy`](@ref) across each bond traces out the entanglement profile of the chain: -```@example infinitemps -d = 2 # physical dimension -D = 5 # virtual dimension -mps = InfiniteMPS(d, D) # random MPS +```@example index +using Plots +S = [real(entropy(ψ, i)) for i in 1:(L - 1)] +plot( + 1:(L - 1), S; xlabel = "cut position", ylabel = "entanglement entropy", + marker = :circle, legend = false, title = "Entanglement across the chain" +) ``` -The `InfiniteMPS` object then handles the gauging of the MPS, which is necessary for many of -the algorithms. This is done automatically upon creation of the object, and the user can -access the gauged tensors by getting and setting the `AL`, `AR`, `C` and `AC` fields, -which each represent a (periodic) vector of these tensors. +See [Computing observables](@ref howto_observables) and [Entanglement entropy and spectrum](@ref howto_entanglement) for the full set, and [Your first ground state](@ref tutorial_first_groundstate) for a guided walkthrough of this calculation. -```@example infinitemps -al = mps.AL[1] # left gauged tensor of the first site -@tensor E[a; b] := al[c, d, b] * conj(al[c, d, a]) -@show isapprox(E, id(left_virtualspace(mps, 1))) -``` -```@example infinitemps -ar = mps.AR[1] # right gauged tensor of the first site -@tensor E[a; b] := ar[a, d, c] * conj(ar[b, d, c]) -@show isapprox(E, id(right_virtualspace(mps, 2))) -``` +## Beyond this example -As regauging the MPS is not possible without recomputing all the tensors, setting a single -tensor is not supported. Instead, the user should construct a new mps object with the -desired tensor, which will then be gauged upon construction. +The same three steps carry over to harder problems, usually by changing only the vector spaces or the state type: -```@example infinitemps -als = 3 .* mps.AL -mps = InfiniteMPS(als) -``` +- [**The thermodynamic limit**](@ref tutorial_thermodynamic_limit) works at infinite system size: replace `FiniteMPS` with an [`InfiniteMPS`](@ref) and `DMRG` with [`VUMPS`](@ref), and the rest of the code is unchanged. +- [**Using symmetries**](@ref tutorial_using_symmetries) imposes abelian or non-abelian symmetries by swapping the plain `ℂ^2` spaces for symmetric ones (for example an `SU2Space`), which also shrinks the bond dimension; see also the [Haldane gap](examples/excitations/0.haldane/index.md) example. +- [**The Hubbard model**](examples/groundstates/2.hubbard/index.md) treats fermions with the same machinery, through TensorKit's graded vector spaces. -These objects can then be used to compute observables and expectation values. For example, -the norm of the MPS, which is equal to the expectation value of the identity operator can be -computed by: +## Where next -```@example infinitemps -N1 = norm(mps) -N2 = expectation_value(mps, 1 => id(physicalspace(mps, 1))) -println("‖mps‖ = $N1") -println(" = $N2") -``` +- [**Installation**](@ref tutorial_installation) and [**Your first ground state**](@ref tutorial_first_groundstate) open the tutorial track, walking through complete calculations from scratch. +- [**How-to guides**](@ref howto_index) are focused recipes for a known task, such as [constructing states](@ref howto_states), [building Hamiltonians](@ref howto_hamiltonians), and [computing observables](@ref howto_observables). +- [**Concepts**](@ref concept_vector_spaces) explain the ideas behind the library, from [vector spaces and TensorKit](@ref concept_vector_spaces) through [matrix product states](@ref concept_matrix_product_states), [operators and Hamiltonians](@ref concept_operators_and_hamiltonians), and [the algorithm landscape](@ref concept_algorithm_landscape). +- [**The examples gallery**](examples/index.md) collects longer, fully worked case studies across symmetries, infinite systems, and less common algorithms. +- [**The public API**](@ref public_api) is the curated, stable entry point to the full library reference. -!!! note "Normalization of infinite MPS" - Because infinite MPS cannot sensibly be normalized to anything but $1$, the `norm` of - an infinite MPS is always set to be $1$ at construction. If this were not the case, any - observable computed from the MPS would either blow up to infinity or vanish to zero. +## Ecosystem -Finally, the MPS can be optimized in order to determine groundstates of given Hamiltonians. -There are plenty of pre-defined models in `MPSKitModels`, but we can also manually construct -the ground state for the transverse field Ising model: +MPSKit builds on [TensorKit.jl](https://github.com/Jutho/TensorKit.jl), which supplies the tensors and vector spaces and handles the symmetries. +Models and ready-made operators come from [MPSKitModels.jl](https://github.com/QuantumKitHub/MPSKitModels.jl) and [TensorKitTensors.jl](https://github.com/QuantumKitHub/TensorKitTensors.jl). +All of these are part of the [QuantumKitHub](https://github.com/QuantumKitHub) organization; the TensorKit documentation is available [here](https://quantumkithub.github.io/TensorKit.jl/stable/). -```@example infinitemps -J = 1.0 -g = 0.5 -lattice = PeriodicVector([ComplexSpace(2)]) -X = TensorMap(ComplexF64[0 1; 1 0], ComplexSpace(2), ComplexSpace(2)) -Z = TensorMap(ComplexF64[1 0; 0 -1], space(X)) -H = InfiniteMPOHamiltonian(lattice, (1, 2) => -J * X ⊗ X, (1,) => - g * Z) -mps, = find_groundstate(mps, H, VUMPS(; maxiter=10)) -E0 = expectation_value(mps, H) -println(" = $(sum(real(E0)) / length(mps))") -``` +## Community and support -### Additional Resources +Questions and general discussion are welcome on [GitHub Discussions](https://github.com/QuantumKitHub/MPSKit.jl/discussions); bug reports belong on the [issue tracker](https://github.com/QuantumKitHub/MPSKit.jl/issues). +If you would like to contribute, see [CONTRIBUTING.md](https://github.com/QuantumKitHub/MPSKit.jl/blob/main/CONTRIBUTING.md) on GitHub. -For more detailed information on the functionality and capabilities of MPSKit, refer to the -Manual section, or have a look at the [Examples](@ref) page. +## Citing MPSKit -Keep in mind that the documentation is still a work in progress, and that some features may -not be fully documented yet. If you encounter any issues or have questions, please check the -library's [issue tracker](https://github.com/QuantumKitHub/MPSKit.jl/issues) on the GitHub -repository and open a new issue. +If MPSKit.jl is useful for your research, please consider citing it — a citation is the most direct way to support the project and helps others find it. +The package is archived on Zenodo under the DOI [10.5281/zenodo.10654900](https://doi.org/10.5281/zenodo.10654900). +The [`CITATION.cff`](https://github.com/QuantumKitHub/MPSKit.jl/blob/main/CITATION.cff) file in the repository always holds the up-to-date metadata, or you can use the BibTeX entry below: +```bibtex +@software{mpskitjl, + author = {Devos, Lukas and Van Damme, Maarten and Haegeman, Jutho}, + title = {{MPSKit.jl}}, + version = {v0.13.13}, + doi = {10.5281/zenodo.10654900}, + url = {https://github.com/QuantumKitHub/MPSKit.jl}, + year = {2026} +} +``` diff --git a/docs/src/lib/bond_dimension.md b/docs/src/lib/bond_dimension.md new file mode 100644 index 000000000..ff6dc5bf6 --- /dev/null +++ b/docs/src/lib/bond_dimension.md @@ -0,0 +1,35 @@ +# [Bond dimension](@id lib_bond_dimension) + +Reference for changing the bond dimension of a state — expanding or truncating its virtual spaces — and for inspecting those virtual spaces directly. +For a task-oriented walkthrough see [Controlling bond dimension](@ref howto_bond_dimension); the full, canonical docstrings for the whole package live in the [Library](@ref lib_index) index. + +## Interface + +```@docs; canonical=false +changebonds +changebonds! +``` + +## Expansion and truncation algorithms + +```@docs; canonical=false +OptimalExpand +RandExpand +SvdCut +VUMPSSvdCut +SketchedExpand +``` + +!!! note + `SketchedExpand` is experimental: it uses randomized controlled bond expansion (CBE), so its reported error estimate is itself randomized, and it is only defined for `FiniteMPS`. + +## Inspecting the virtual spaces + +The bond dimension of an MPS or MPO is the dimension of the virtual space living on a given bond. +The accessors below return that `VectorSpace`, whose `dim` gives the numeric bond dimension. + +```@docs; canonical=false +left_virtualspace +right_virtualspace +physicalspace +``` diff --git a/docs/src/lib/environments.md b/docs/src/lib/environments.md new file mode 100644 index 000000000..98ccf7720 --- /dev/null +++ b/docs/src/lib/environments.md @@ -0,0 +1,34 @@ +# [Environments](@id lib_environments) + +Reference for MPSKit's environment machinery — the caches that store the partially contracted tensor networks reused throughout the algorithms. +For an explanation of what environments are and why they exist see the concept page on [Environments](@ref concept_environments); this page only lists the API. +The full, canonical docstrings for the whole package live in the [Library](@ref lib_index) index. + +```@meta +CurrentModule = MPSKit +``` + +## Constructing environments + +```@docs; canonical=false +environments +``` + +## Querying environments + +```@docs; canonical=false +leftenv +rightenv +``` + +## Environment types + +The concrete environment types below are returned by [`environments`](@ref) and are managed automatically by the algorithms. +They are implementation details — you normally obtain them from `environments` rather than constructing them directly — and are not part of the public API. + +```@docs; canonical=false +AbstractMPSEnvironments +FiniteEnvironments +InfiniteEnvironments +InfiniteQPEnvironments +``` diff --git a/docs/src/lib/excitations.md b/docs/src/lib/excitations.md new file mode 100644 index 000000000..ce17ec23e --- /dev/null +++ b/docs/src/lib/excitations.md @@ -0,0 +1,30 @@ +# [Excitations](@id lib_excitations) + +Reference for the excitation interface, its algorithms, and the quasiparticle state types it produces. +For a task-oriented walkthrough see the how-to guides. +The full, canonical docstrings for the whole package live in the [Library](@ref lib_index) index. + +## Interface + +```@docs; canonical=false +excitations +``` + +## Algorithms + +```@docs; canonical=false +QuasiparticleAnsatz +FiniteExcited +ChepigaAnsatz +ChepigaAnsatz2 +``` + +## Quasiparticle states + +These are the ansatz states produced by, and passed to, `excitations` on top of a ground state. + +```@docs; canonical=false +QP +LeftGaugedQP +RightGaugedQP +``` diff --git a/docs/src/lib/groundstate.md b/docs/src/lib/groundstate.md new file mode 100644 index 000000000..7ec0b3425 --- /dev/null +++ b/docs/src/lib/groundstate.md @@ -0,0 +1,21 @@ +# [Ground-state algorithms](@id lib_groundstate) + +Reference for the ground-state search interface and its algorithms. +For a task-oriented walkthrough see the how-to guides; the full, canonical docstrings for the whole package live in the [Library](@ref lib_index) index. + +## Interface + +```@docs; canonical=false +find_groundstate +``` + +## Algorithms + +```@docs; canonical=false +DMRG +DMRG2 +VUMPS +IDMRG +IDMRG2 +GradientGrassmann +``` diff --git a/docs/src/lib/internals.md b/docs/src/lib/internals.md new file mode 100644 index 000000000..ba9402633 --- /dev/null +++ b/docs/src/lib/internals.md @@ -0,0 +1,53 @@ +# [Internals](@id lib_internals) + +!!! warning "Non-public API" + The symbols documented on this page are **internal**: they are unexported, not + part of the public API, and may change or be removed in any release without notice + or a deprecation cycle. They are collected here as a reference for contributors and + advanced users reading the source, not as a stable interface to build on. For the + supported surface see the [Public API](@ref public_api). Experimental features (for + example the current GPU support, discussed in [Parallelism and GPU support](@ref howto_parallelism_gpu)) are + likewise unstable and subject to change. + +```@meta +CurrentModule = MPSKit +``` + +## Effective (derivative) operators + +The local eigenvalue and time-evolution problems solved by DMRG, VUMPS, TDVP and friends are phrased in terms of effective "derivative" operators acting on a single gauge tensor. +These are built internally from the Hamiltonian and the surrounding [environments](@ref lib_environments). + +```@docs; canonical=false +DerivativeOperator +C_hamiltonian +AC_hamiltonian +AC2_hamiltonian +``` + +## Transfer matrices + +Low-level application of (regularized) transfer matrices to boundary vectors, used when building infinite-MPS environments. + +```@docs; canonical=false +transfer_left +transfer_right +``` + +## Environment algorithm resolution + +Helpers that pick and instantiate the iterative solver used to compute environments for a given bra/operator/ket combination. + +```@docs; canonical=false +environment_alg +resolve_environment_solver +``` + +## Defaults and scheduling + +Global configuration lives in the `MPSKit.Defaults` submodule, including the multi-threading scheduler used across the package. + +```@docs +Defaults +Defaults.set_scheduler! +``` diff --git a/docs/src/lib/lib.md b/docs/src/lib/lib.md index b1645c273..7f7b23eb0 100644 --- a/docs/src/lib/lib.md +++ b/docs/src/lib/lib.md @@ -1,4 +1,4 @@ -# Library documentation +# [Library documentation](@id lib_index) ```@autodocs Modules = [MPSKit] diff --git a/docs/src/lib/observables.md b/docs/src/lib/observables.md new file mode 100644 index 000000000..1431a7cec --- /dev/null +++ b/docs/src/lib/observables.md @@ -0,0 +1,48 @@ +# [Observables and analysis](@id lib_observables) + +Reference for extracting physical quantities and analysis diagnostics from an MPS. +For a task-oriented walkthrough see the how-to guide [Computing observables](@ref howto_observables). +The full, canonical docstrings for the whole package live in the [Library](@ref lib_index) index. + +## Expectation values + +```@docs; canonical=false +expectation_value +``` + +!!! note "Environments are ignored in the multiline varargs method" + `expectation_value(::MultilineMPS, ::MultilineMPO, envs...)` accepts environments but does + not use them: it evaluates the expectation value line by line, and each line recomputes its + own. Passing environments here therefore saves no work — the result is correct either way. + +## Correlators + +```@docs; canonical=false +correlator +``` + +## Convergence diagnostics + +```@docs; canonical=false +variance +``` + +## Transfer matrix and correlation length + +```@docs; canonical=false +correlation_length +marek_gap +transfer_spectrum +transferplot +``` + +## Entanglement + +Entropy and entanglement spectrum are computed from the state's bond/gauge tensors. +See the how-to [Entanglement entropy and spectrum](@ref howto_entanglement) for worked recipes. + +```@docs; canonical=false +entropy +entanglement_spectrum +entanglementplot +``` diff --git a/docs/src/lib/operators.md b/docs/src/lib/operators.md new file mode 100644 index 000000000..a6fbe02e3 --- /dev/null +++ b/docs/src/lib/operators.md @@ -0,0 +1,37 @@ +# [Operators](@id lib_operators) + +Reference for matrix product operators and Hamiltonians. +The full, canonical docstrings for the whole package live in the [Library](@ref lib_index) index. + +## Matrix product operators + +```@docs; canonical=false +AbstractMPO +MPO +FiniteMPO +InfiniteMPO +MultilineMPO +``` + +## Hamiltonians + +```@docs; canonical=false +MPOHamiltonian +FiniteMPOHamiltonian +InfiniteMPOHamiltonian +``` + +## Jordan-block MPO tensors + +```@docs; canonical=false +JordanMPOTensor +``` + +## Operator algebra + +```@docs; canonical=false +MultipliedOperator +TimedOperator +UntimedOperator +LazySum +``` diff --git a/docs/src/lib/public.md b/docs/src/lib/public.md new file mode 100644 index 000000000..788c269d1 --- /dev/null +++ b/docs/src/lib/public.md @@ -0,0 +1,75 @@ +# [Public API](@id public_api) + +This page is the curated, stable public API surface of MPSKit — the symbols that are exported and intended for direct use. +Each entry links to its full docstring in the [Library](@ref lib_index) index. +The category reference pages ([States](@ref lib_states), [Operators](@ref lib_operators), [Ground-state algorithms](@ref lib_groundstate)) group the same docstrings by topic. + +!!! note + Anything not listed here (or marked internal in the [Library](@ref lib_index) index) is + not part of the public API and may change without notice. + +## States + +The matrix product state types — finite, infinite, windowed, and multi-line. + +[`FiniteMPS`](@ref), [`InfiniteMPS`](@ref), [`WindowMPS`](@ref), [`MultilineMPS`](@ref) + +## Operators and Hamiltonians + +Matrix product operators and Hamiltonians, finite and infinite, plus the wrappers used to build time-dependent and summed operators. + +[`AbstractMPO`](@ref), [`MPO`](@ref), [`FiniteMPO`](@ref), [`InfiniteMPO`](@ref), [`MultilineMPO`](@ref), [`MPOHamiltonian`](@ref), [`FiniteMPOHamiltonian`](@ref), [`InfiniteMPOHamiltonian`](@ref), [`JordanMPOTensor`](@ref), [`MultipliedOperator`](@ref), [`TimedOperator`](@ref), [`UntimedOperator`](@ref), [`LazySum`](@ref) + +## Environments + +The caches that store partially contracted tensor networks and are reused throughout the algorithms; see the concept page on [Environments](@ref concept_environments) for why they exist. + +[`environments`](@ref) + +## Ground states and boundaries + +The ground-state search and 2D leading-boundary interface, and the DMRG/VUMPS/IDMRG family of algorithms that implement it. + +[`find_groundstate`](@ref), [`leading_boundary`](@ref), [`approximate`](@ref), [`VUMPS`](@ref), [`VOMPS`](@ref), [`DMRG`](@ref), [`DMRG2`](@ref), [`IDMRG`](@ref), [`IDMRG2`](@ref), [`GradientGrassmann`](@ref) + +## Bond dimension + +Expanding or truncating a state's virtual spaces, and the algorithms that drive it. + +[`changebonds`](@ref), [`OptimalExpand`](@ref), [`RandExpand`](@ref), [`SvdCut`](@ref), [`VUMPSSvdCut`](@ref) + +## Time evolution + +Real- and imaginary-time evolution drivers and the algorithms and MPO approximations that implement them. + +[`time_evolve`](@ref), [`timestep`](@ref), [`make_time_mpo`](@ref), [`TDVP`](@ref), [`TDVP2`](@ref), [`WI`](@ref), [`WII`](@ref), [`TaylorCluster`](@ref) + +## Excitations + +The excitation interface and the quasiparticle-ansatz and finite-excited-state algorithms that produce excited states on top of a ground state. + +[`excitations`](@ref), [`FiniteExcited`](@ref), [`QuasiparticleAnsatz`](@ref), [`ChepigaAnsatz`](@ref), [`ChepigaAnsatz2`](@ref) + +## Linear problems and spectral functions + +Solving the MPS linear problems behind dynamical/spectral quantities, such as propagators and susceptibilities. + +[`propagator`](@ref), [`DynamicalDMRG`](@ref), [`NaiveInvert`](@ref), [`Jeckelmann`](@ref), [`exact_diagonalization`](@ref), [`fidelity_susceptibility`](@ref) + +## Observables and analysis + +Extracting physical quantities and analysis diagnostics from an MPS — expectation values, correlators, spectra, and entanglement. + +[`expectation_value`](@ref), [`correlator`](@ref), [`variance`](@ref), [`correlation_length`](@ref), [`marek_gap`](@ref), [`transfer_spectrum`](@ref), [`entropy`](@ref), [`entanglement_spectrum`](@ref) + +## Boundary conditions + +Converting an infinite MPO into a finite one of a given length, either wrapping it (periodic) or truncating it (open). + +[`open_boundary_conditions`](@ref), [`periodic_boundary_conditions`](@ref) + +## Utility + +Periodic and windowed array containers, virtual/physical space accessors, and a compact "braille" visualization of an MPO's sparsity structure. + +[`PeriodicArray`](@ref), [`PeriodicVector`](@ref), [`PeriodicMatrix`](@ref), [`WindowArray`](@ref), [`left_virtualspace`](@ref), [`right_virtualspace`](@ref), [`physicalspace`](@ref), [`braille`](@ref) diff --git a/docs/src/lib/states.md b/docs/src/lib/states.md new file mode 100644 index 000000000..dc7f14587 --- /dev/null +++ b/docs/src/lib/states.md @@ -0,0 +1,25 @@ +# [States](@id lib_states) + +Reference for the matrix product state types. +The full, canonical docstrings for the whole package live in the [Library](@ref lib_index) index. + +## Matrix product states + +```@docs; canonical=false +FiniteMPS +InfiniteMPS +WindowMPS +MultilineMPS +``` + +## Quasiparticle states + +Excitation ansätze produced by [`excitations`](@ref). +These behave as vectors and are normally obtained from `excitations` rather than constructed directly. + +```@docs; canonical=false +QP +LeftGaugedQP +RightGaugedQP +``` + diff --git a/docs/src/lib/time_evolution.md b/docs/src/lib/time_evolution.md new file mode 100644 index 000000000..1bc598db6 --- /dev/null +++ b/docs/src/lib/time_evolution.md @@ -0,0 +1,43 @@ +# [Time evolution](@id lib_time_evolution) + +Reference for the time-evolution drivers and algorithms. +For a task-oriented walkthrough see the how-to guides. +The full, canonical docstrings for the whole package live in the [Library](@ref lib_index) index. + +## Drivers + +```@docs; canonical=false +time_evolve +timestep +timestep! +``` + +## MPS time-evolution algorithms + +```@docs; canonical=false +TDVP +TDVP2 +BUG +``` + +## Time-evolution MPOs + +For evolving with an explicitly constructed propagator MPO, e.g. for an [`InfiniteMPS`](@ref), use [`make_time_mpo`](@ref) with one of the expansion algorithms below. + +```@docs; canonical=false +make_time_mpo +TaylorCluster +WI +WII +``` + +## MPO–MPS products + +Applying an MPO to a state — a propagator MPO among others — goes through [`approximate`](@ref). +The variational algorithms ([`DMRG2`](@ref) and friends) treat the destination as an initial guess, whereas [`Zipup`](@ref) sweeps the product out in one pass and needs none. + +```@docs; canonical=false +approximate +approximate! +Zipup +``` diff --git a/docs/src/man/D_100_strided.png b/docs/src/man/D_100_strided.png deleted file mode 100644 index 585beb543..000000000 Binary files a/docs/src/man/D_100_strided.png and /dev/null differ diff --git a/docs/src/man/D_500_blas.png b/docs/src/man/D_500_blas.png deleted file mode 100644 index 6e2938376..000000000 Binary files a/docs/src/man/D_500_blas.png and /dev/null differ diff --git a/docs/src/man/D_500_strided.png b/docs/src/man/D_500_strided.png deleted file mode 100644 index d7a896e8c..000000000 Binary files a/docs/src/man/D_500_strided.png and /dev/null differ diff --git a/docs/src/man/algorithms.md b/docs/src/man/algorithms.md deleted file mode 100644 index 02b078b61..000000000 --- a/docs/src/man/algorithms.md +++ /dev/null @@ -1,390 +0,0 @@ -```@meta -DocTestSetup = :(using MPSKit, TensorKit, MPSKitModels) -``` - -# [Algorithms](@id um_algorithms) - -Here is a collection of the algorithms that have been added to MPSKit.jl. -If a particular algorithm is missing, feel free to let us know via an issue, or contribute via a PR. - -## Groundstates - -One of the most prominent use-cases of MPS is to obtain the ground state of a given (quasi-) one-dimensional quantum Hamiltonian. -In MPSKit.jl, this can be achieved through `find_groundstate`: - -```@docs; canonical=false -find_groundstate -``` - -There are a variety of algorithms that have been developed over the years, and many of them have been implemented in MPSKit. -Keep in mind that some of them are exclusive to finite or infinite systems, while others may work for both. -Many of these algorithms have different advantages and disadvantages, and figuring out the optimal algorithm is not always straightforward, since this may strongly depend on the model. -Here, we enumerate some of their properties in hopes of pointing you in the right direction. For convenience, the full list of algorithms is: - -- [DMRG](@ref) -- [DMRG2](@ref) -- [VUMPS](@ref) -- [Gradient descent](@ref) -- [TDVP](@ref) -- [Time evolution MPO](@ref) -- [Quasiparticle Ansatz](@ref) -- [Finite excitations](@ref) -- ["Chepiga Ansatz"](@ref) - -### DMRG - -Probably the most widely used algorithm for optimizing groundstates with MPS is [`DMRG`](@ref) and its variants. -This algorithm sweeps through the system, optimizing a single site or pair of sites while keeping all others fixed. -Since this local problem can be solved efficiently, the global optimal state follows by alternating through the system. -However, because of the single-site nature of this algorithm, this can never alter the bond dimension of the state, such that there is no way of dynamically increasing the precision. -This can become particularly relevant in the cases where symmetries are involved, since then finding a good distribution of charges is also required. -To circumvent this, it is also possible to optimize over two sites at the same time with [`DMRG2`](@ref), followed by a truncation back to the single site states. -This can dynamically change the bond dimension but comes at an increase in cost. - -```@docs; canonical=false -DMRG -DMRG2 -``` - -For infinite systems, a similar approach can be used by dynamically adding new sites to the middle of the system and optimizing over them. -This gradually increases the system size until the boundary effects are no longer felt. -However, because of this approach, for critical systems this algorithm can be quite slow to converge, since the number of steps needs to be larger than the correlation length of the system. -Again, both a single-site and a two-site version are implemented, to have the option to dynamically increase the bonddimension at a higher cost. - -```@docs; canonical=false -IDMRG -IDMRG2 -``` - -### VUMPS - -[`VUMPS`](@ref) is an (I)DMRG inspired algorithm that can be used to variationally find the ground state as a Uniform (infinite) Matrix Product State. -In particular, a local update is followed by a re-gauging procedure that effectively replaces the entire network with the newly updated tensor. -Compared to IDMRG, this often achieves a higher rate of convergence, since updates are felt throughout the system immediately. -Nevertheless, this algorithm only works whenever the state is injective, i.e. there is a unique ground state. -Since VUMPS is a single-site algorithm, it cannot alter the bond dimension. - -```@docs; canonical=false -VUMPS -``` - -### Gradient descent - -Both finite and infinite matrix product states can be parametrized by a set of isometric tensors, -which we can optimize over. -Making use of the geometry of the manifold (a Grassmann manifold), we can greatly outperform naive optimization strategies. -Compared to the other algorithms, quite often the convergence rate in the tail of the optimization procedure is higher, such that often the fastest method combines a different algorithm far from convergence with this algorithm close to convergence. -Since this is again a single-site algorithm, there is no way to alter the bond dimension. - -```@docs; canonical=false -GradientGrassmann -``` - -## Time evolution - -Given a particular state, it can also often be useful to examine the evolution of certain properties over time. -To that end, there are two main approaches to solving the Schrödinger equation in MPSKit. - -```math -i \hbar \frac{d}{dt} \Psi = H \Psi \implies \Psi(t) = \exp{\left(-iH(t - t_0)\right)} \Psi(t_0) -``` - -```@docs; canonical=false -timestep -time_evolve -make_time_mpo -``` - -### TDVP - -The first is focused around approximately solving the equation for a small timestep, and repeating this until the desired evolution is achieved. -This can be achieved by projecting the equation onto the tangent space of the MPS, and then solving the results. -This procedure is commonly referred to as the [`TDVP`](@ref) algorithm, which again has a two-site variant to allow for dynamically altering the bond dimension. - -```@docs; canonical=false -TDVP -TDVP2 -BUG -``` - -### Time evolution MPO - -The other approach instead tries to first approximately represent the evolution operator, and only then attempts to apply this operator to the initial state. -Typically the first step happens through [`make_time_mpo`](@ref), while the second can be achieved through [`approximate`](@ref). -Here, there are several algorithms available - -```@docs; canonical=false -WI -WII -TaylorCluster -``` - -## Excitations - -It might also be desirable to obtain information beyond the lowest energy state of a given system, and study the dispersion relation. -While it is typically not feasible to resolve states in the middle of the energy spectrum, there are several ways to target a few of the lowest-lying energy states. - -```@docs; canonical=false -excitations -``` - -```@setup excitations -using TensorKit, MPSKit, MPSKitModels -``` - -### Quasiparticle Ansatz - -The Quasiparticle Ansatz offers an approach to compute low-energy eigenstates in quantum -systems, playing a key role in both finite and infinite systems. It leverages localized -perturbations for approximations, as detailed in [haegeman2013](@cite). - -#### Finite Systems: - -In finite systems, we approximate low-energy states by altering a single tensor in the -Matrix Product State (MPS) for each site, and summing these across all sites. This method -introduces additional gauge freedoms, utilized to ensure orthogonality to the ground state. -Optimizing within this framework translates to solving an eigenvalue problem. For example, -in the transverse field Ising model, we calculate the first excited state as shown in the -provided code snippet, and check the accuracy against theoretical values. Some deviations -are expected, both due to finite-bond-dimension and finite-size effects. - -```@example excitations -# Model parameters -g = 10.0 -L = 16 -H = transverse_field_ising(FiniteChain(L); g) - -# Finding the ground state -ψ₀ = FiniteMPS(L, ℂ^2, ℂ^32) -ψ, = find_groundstate(ψ₀, H; verbosity=0) - -# Computing excitations using the Quasiparticle Ansatz -Es, ϕs = excitations(H, QuasiparticleAnsatz(), ψ; num=1) -isapprox(Es[1], 2(g - 1); rtol=1e-2) -``` - -#### Infinite Systems: - -The ansatz in infinite systems maintains translational invariance by perturbing every site -in the unit cell in a plane-wave superposition, requiring momentum specification. The -[Haldane gap](https://iopscience.iop.org/article/10.1088/0953-8984/1/19/001) computation in -the Heisenberg model illustrates this approach. - -```@example excitations -# Setting up the model and momentum -momentum = π -H = heisenberg_XXX() - -# Ground state computation -ψ₀ = InfiniteMPS(ℂ^3, ℂ^48) -ψ, = find_groundstate(ψ₀, H; verbosity=0) - -# Excitation calculations -Es, ϕs = excitations(H, QuasiparticleAnsatz(), momentum, ψ) -isapprox(Es[1], 0.41047925; atol=1e-4) -``` - -#### Charged excitations: - -When dealing with symmetric systems, the default optimization is for eigenvectors with -trivial total charge. However, quasiparticles with different charges can be obtained using -the sector keyword. For instance, in the transverse field Ising model, we consider an -excitation built up of flipping a single spin, aligning with `Z2Irrep(1)`. - -```@example excitations -g = 10.0 -L = 16 -H = transverse_field_ising(Z2Irrep, FiniteChain(L); g) -ψ₀ = FiniteMPS(L, Z2Space(0 => 1, 1 => 1), Z2Space(0 => 16, 1 => 16)) -ψ, = find_groundstate(ψ₀, H; verbosity=0) -Es, ϕs = excitations(H, QuasiparticleAnsatz(), ψ; num=1, sector=Z2Irrep(1)) -isapprox(Es[1], 2(g - 1); rtol=1e-2) # infinite analytical result -``` - -```@docs; canonical=false -QuasiparticleAnsatz -``` - -### Finite excitations - -For finite systems we can also do something else - find the ground state of the Hamiltonian + -``\\text{weight} \sum_i | \\psi_i ⟩ ⟨ \\psi_i ``. This is also supported by calling - -```@example excitations -# Model parameters -g = 10.0 -L = 16 -H = transverse_field_ising(FiniteChain(L); g) - -# Finding the ground state -ψ₀ = FiniteMPS(L, ℂ^2, ℂ^32) -ψ, = find_groundstate(ψ₀, H; verbosity=0) - -Es, ϕs = excitations(H, FiniteExcited(), ψ; num=1) -isapprox(Es[1], 2(g - 1); rtol=1e-2) -``` - -```@docs; canonical=false -FiniteExcited -``` - -### "Chepiga Ansatz" - -Computing excitations in critical systems poses a significant challenge due to the diverging -correlation length, which requires very large bond dimensions. However, we can leverage this -long-range correlation to effectively identify excitations. In this context, the left/right -gauged MPS, serving as isometries, are effectively projecting the Hamiltonian into the -low-energy sector. This projection method is particularly effective in long-range systems, -where excitations are distributed throughout the entire system. Consequently, the low-lying -energy spectrum can be extracted by diagonalizing the effective Hamiltonian (without any -additional DMRG costs!). The states of these excitations are then represented by the ground -state MPS, with one site substituted by the corresponding eigenvector. This approach is -often referred to as the 'Chepiga ansatz', named after one of the authors of this paper -[chepiga2017](@cite). - -This is supported via the following syntax: - -```@example excitations -g = 10.0 -L = 16 -H = transverse_field_ising(FiniteChain(L); g) -ψ₀ = FiniteMPS(L, ComplexSpace(2), ComplexSpace(32)) -ψ, envs, = find_groundstate(ψ₀, H; verbosity=0) -E₀ = real(sum(expectation_value(ψ, H, envs))) -Es, ϕs = excitations(H, ChepigaAnsatz(), ψ, envs; num=1) -isapprox(Es[1] - E₀, 2(g - 1); rtol=1e-2) # infinite analytical result -``` - -In order to improve the accuracy, a two-site version also exists, which varies two -neighbouring sites: - -```@example excitations -Es, ϕs = excitations(H, ChepigaAnsatz2(), ψ, envs; num=1) -isapprox(Es[1] - E₀, 2(g - 1); rtol=1e-2) # infinite analytical result -``` - -## `changebonds` - -Many of the previously mentioned algorithms do not possess a way to dynamically change to -bond dimension. This is often a problem, as the optimal bond dimension is often not a priori -known, or needs to increase because of entanglement growth throughout the course of a -simulation. [`changebonds`](@ref) exposes a way to change the bond dimension of a given -state. - -```@docs; canonical=false -changebonds -``` - -There are several different algorithms implemented, each having their own advantages and -disadvantages: - -* [`SvdCut`](@ref): The simplest method for changing the bonddimension is found by simply - locally truncating the state using an SVD decomposition. This yields a (locally) optimal - truncation, but clearly cannot be used to increase the bond dimension. Note that a - globally optimal truncation can be obtained by using the [`SvdCut`](@ref) algorithm in - combination with [`approximate`](@ref). Since the output of this method might have a - truncated bonddimension, the new state might not be identical to the input state. - The truncation is controlled through `trunc`, which dictates how the singular values of - the original state are truncated. - - -* [`OptimalExpand`](@ref): This algorithm is based on the idea of expanding the bond - dimension by investigating the two-site derivative, and adding the most important blocks - which are orthogonal to the current state. From the point of view of a local two-site - update, this procedure is *optimal*, but it requires to evaluate a two-site derivative, - which can be costly when the physical space is large. The state will remain unchanged, but - a one-site scheme will now be able to push the optimization further. The subspace used for - expansion can be truncated through `trunc`, which dictates how many singular values will - be added. - -* [`RandExpand`](@ref): This algorithm similarly adds blocks orthogonal to the current - state, but does not attempt to select the most important ones, and rather just selects - them at random. The advantage here is that this is much cheaper than the optimal expand, - and if the bond dimension is grown slow enough, this still obtains a very good expansion - scheme. Again, The state will remain unchanged and a one-site scheme will now be able to - push the optimization further. The subspace used for expansion can be truncated through - `trunc`, which dictates how many orthogonal vectors will be added. - -* [`VUMPSSvdCut`](@ref): This algorithm is based on the [`VUMPS`](@ref) algorithm, and - consists of performing a two-site update, and then truncating the state back down. Because - of the two-site update, this can again become expensive, but the algorithm has the option - of both expanding as well as truncating the bond dimension. Here, `trunc` controls the - truncation of the full state after the two-site update. - -## Leading boundary - -For statistical mechanics partition functions we want to find the approximate leading -boundary MPS. Again this can be done with VUMPS: - -```julia -th = nonsym_ising_mpo() -ts = InfiniteMPS([ℂ^2],[ℂ^20]); -(ts,envs,_) = leading_boundary(ts,th,VUMPS(maxiter=400,verbosity=false)); -``` - -If the mpo satisfies certain properties (positive and hermitian), it may also be possible to -use GradientGrassmann. - -```@docs; canonical=false -leading_boundary -``` - -## `approximate` - -Often, it is useful to approximate a given MPS by another, typically by one of a different -bond dimension. This is achieved by approximating an application of an MPO to the initial -state, by a new state. - -```@docs; canonical=false -approximate -``` - -## Varia - -What follows is a medley of lesser known (or used) algorithms and don't entirely fit under -one of the above categories. - -### Dynamical DMRG - -Dynamical DMRG has been described in other papers and is a way to find the propagator. The -basic idea is that to calculate ``G(z) = ⟨ V | (H-z)^{-1} | V ⟩ `` , one can variationally -find ``(H-z) |W ⟩ = | V ⟩ `` and then the propagator simply equals ``G(z) = ⟨ V | W ⟩``. - -```@docs; canonical=false -propagator -DynamicalDMRG -NaiveInvert -Jeckelmann -``` - -### fidelity susceptibility - -The fidelity susceptibility measures how much the ground state changes when tuning a -parameter in your Hamiltonian. Divergences occur at phase transitions, making it a valuable -measure when no order parameter is known. - -```@docs; canonical=false -fidelity_susceptibility -``` - -### Boundary conditions - -You can impose periodic or open boundary conditions on an infinite Hamiltonian, to generate a finite counterpart. -In particular, for periodic boundary conditions we still return an MPO that does not form a closed loop, such that it can be used with regular matrix product states. -This is straightforward to implement but, and while this effectively squares the bond dimension, it is still competitive with more advanced periodic MPS algorithms. - -```@docs; canonical=false -open_boundary_conditions -periodic_boundary_conditions -``` - -### Exact diagonalization - -As a side effect, our code supports exact diagonalization. The idea is to construct a finite -matrix product state with maximal bond dimension, and then optimize the middle site. Because -we never truncate the bond dimension, this single site effectively parametrizes the entire -Hilbert space. - -```@docs; canonical=false -exact_diagonalization -``` diff --git a/docs/src/man/environments.md b/docs/src/man/environments.md deleted file mode 100644 index 6d431e05e..000000000 --- a/docs/src/man/environments.md +++ /dev/null @@ -1,66 +0,0 @@ -# [Environments](@id um_environments) - -In many tensor network algorithms we encounter partially contracted tensor networks. -In DMRG for example, one needs to know the sum of all the Hamiltonian contributions left and right of the site that we want to optimize. -If you then optimize the neighboring site to the right, you only need to add one new contribution to the previous sum of Hamiltonian contributions. - -This kind of information is stored in the environment objects. -The goal is that the user should preferably never have to deal with these objects, but being aware of the inner workings may allow you to write more efficient code. -That is why they are nonetheless included in the manual. - -## Finite Environments - -When you create a state and a Hamiltonian: - -```julia -state = FiniteMPS(rand, ComplexF64, 20, ℂ^2, ℂ^10); -operator = nonsym_ising_ham(); -``` - -an environment object can be created by calling -```julia -envs = environments(state, operator, state) -``` - -The partially contracted mpohamiltonian left of site i can then be queried using: - -```julia -@time leftenv(envs, i, state) -``` - -This may take some time, but a subsequent call should be a lot quicker - -```julia -@time leftenv(envs, i - 1, state) -``` - -Behind the scenes the `envs` stored all tensors it used to calculate leftenv (state.AL[1 .. i]) and when queried again, it checks if the tensors it previously used are identical (using ===). If so, it can simply return the previously stored results. If not, it will recalculate again. If you update a tensor in-place, the caches cannot know using === that the actual tensors have changed. If you do this, you have to call poison!(state,i). - -As an optional argument, many algorithms allow you to pass in an environment object, and they also return an updated one. Therefore, for time evolution code, it is more efficient to give it the updated caches every time step, instead of letting it recalculate. - -## Infinite Environments - -Infinite Environments are very similar : -```julia -state = InfiniteMPS(ℂ^2, ℂ^10) -operator = transverse_field_ising() -envs = environments(state, operator, state) -``` - -There are also some notable differences. Infinite environments typically require solving linear problems or eigenvalue problems iteratively with finite precision. To find out what precision we used we can type: -```julia -(cache.tol,cache.maxiter) -``` - -To recalculate with a different precision : -```julia -cache.tol=1e-8; -recalculate!(cache,state) -``` - -Unlike their finite counterparts, recalculating is not done automatically. To get the environment for a different state one has to recalculate explicitly! -```julia -different_state = InfiniteMPS([ℂ^2],[ℂ^10]); -recalculate!(cache,different_state) -leftenv(cache,3,different_state) -``` diff --git a/docs/src/man/intro.md b/docs/src/man/intro.md deleted file mode 100644 index ee6c3ddeb..000000000 --- a/docs/src/man/intro.md +++ /dev/null @@ -1,93 +0,0 @@ -# Prerequisites - -The following sections describe the prerequisites for using MPSKit. If you are already -familiar with the concepts of MPSKit and TensorKit, you can skip to the [Conventions](@ref) -sections. - -## TensorKit - -```@example tensorkit -using TensorKit -``` - -MPSKit uses the tensors defined in [TensorKit.jl](https://github.com/Jutho/TensorKit.jl) as -its underlying data structure. This is what allows the library to be generic with respect to -the symmetry of the tensors. The main difference with regular multi-dimensional arrays is -the notion of a partition of the dimensions in **incoming** and **outgoing**, which are -respectively called **domain** and **codomain**. In other words, a `TensorMap` can be -interpreted as a linear map from its domain to its codomain. Additionally, as generic -symmetries are supported, in general the structure of the indices are not just integers, but -are given by spaces. - -The general syntax for creating a tensor is similar to the creation of arrays, where the -`axes` or `size` specifiers are replaced with `VectorSpace` objects: -```julia -zeros(scalartype, codomain, domain) -rand(scalartype, codomain ← domain) # ← is the `\leftarrow` operator -``` - -For example, the following creates a random tensor with three legs, each of which has -dimension two, however with different partitions. - -```@example tensorkit -V1 = ℂ^2 # ℂ is the `\bbC` operator, equivalent to ComplexSpace(10) -t1 = rand(Float64, V1 ⊗ V1 ⊗ V1) # all spaces in codomain -t2 = rand(Float64, V1, V1 ⊗ V1) # one space in codomain, two in domain -``` - -We can now no longer trivially add them together: - -```@example tensorkit -try #hide -t1 + t2 # incompatible partition -catch err; Base.showerror(stderr, err); end #hide -``` -But this can be resolved by permutation: - -```@example tensorkit -try #hide -t1 + permute(t2, (1, 2, 3), ()) # incompatible arrows -catch err; Base.showerror(stderr, err); end #hide -``` - -These abstract objects can represent not only plain arrays but also symmetric tensors. The -following creates a symmetric tensor with ℤ₂ symmetry, again with three legs of dimension -two. However, now the dimension two is now split over even and odd sectors of ℤ₂. - -```@example tensorkit -V2 = Z2Space(0 => 1, 1 => 1) -t3 = rand(Float64, V2 ⊗ V2, V2) -``` - -For more information, check out the [TensorKit documentation](https://quantumkithub.github.io/TensorKit.jl/stable/)! - -## Conventions - -The general definition of an MPS tensor is as follows: - -```@raw html -convention MPSTensor -``` - -These tensors are allowed to have an arbitrary number of physical legs, and both `FiniteMPS` -as well as `InfiniteMPS` will be able to handle the resulting objects. This allows for -example for the definition of boundary tensors in PEPS code, which have two physical legs. - -Similarly, the definition of a bond tensor, appearing in between two MPS tensors, is as -follows: - -```@raw html -convention BondTensor -``` - -Finally, the definition of a MPO tensor, which is used to represent statistical mechanics -problems as well as quantum Hamiltonians, is represented as: - -```@raw html -convention MPOTensor -``` - -While this results at first glance in the not very intuitive ordering of spaces as $V_l -\otimes P \leftarrow P \otimes V_r$, this is actually the most natural ordering for keeping -the algorithms planar. In particular, this is relevant for dealing with fermionic systems, -where additional crossings would lead to sign problems. diff --git a/docs/src/man/lattices.md b/docs/src/man/lattices.md deleted file mode 100644 index 7ec0c4ad0..000000000 --- a/docs/src/man/lattices.md +++ /dev/null @@ -1,4 +0,0 @@ -# [Lattices](@id lattices) - -!!! warning - This section is still under construction. Coming soon! \ No newline at end of file diff --git a/docs/src/man/operators.md b/docs/src/man/operators.md deleted file mode 100644 index 05525add2..000000000 --- a/docs/src/man/operators.md +++ /dev/null @@ -1,276 +0,0 @@ -# [Operators](@id um_operators) - -In analogy to how we can define matrix product states as a contraction of local tensors, a -similar construction exist for operators. To that end, a Matrix Product Operator (MPO) is -nothing more than a collection of local [`MPOTensor`](@ref MPSKit.MPOTensor) objects, contracted along a -line. Again, we can distinguish between finite and infinite operators, with the latter being -represented by a periodic array of MPO tensors. - -## FiniteMPO - -Starting off with the simplest case, a basic [`FiniteMPO`](@ref) is a vector of `MPOTensor` objects. -These objects can be created either directly from a vector of `MPOTensor`s, or starting from -a dense operator (a subtype of `AbstractTensorMap`), which is then decomposed into a -product of local tensors. - -```@raw html -MPO -``` - -```@setup operators -using TensorKit, MPSKit, MPSKitModels -``` - -```@example operators -S_x = TensorMap(ComplexF64[0 1; 1 0], ℂ^2 ← ℂ^2) -S_z = TensorMap(ComplexF64[1 0; 0 -1], ℂ^2 ← ℂ^2) -O_xzx = FiniteMPO(S_x ⊗ S_z ⊗ S_x); -``` - -The individual tensors are accessible via regular indexing. Note that the tensors are -internally converted to the `MPOTensor` objects, thus having four indices. In this specific -case, the left- and right virtual spaces are trivial, but this is not a requirement. - -```@example operators -O_xzx[1] -``` - -!!! warning - The local tensors are defined only up to a gauge transformation of the virtual spaces. - This means that the tensors are not uniquely defined, and special care must be taken - when comparing MPOs on an element-wise basis. - -For convenience, a number of utility functions are defined for probing the structure of the -constructed MPO. For example, the spaces can be queried as follows: - -```@example operators -left_virtualspace(O_xzx, 2) -right_virtualspace(O_xzx, 2) -physicalspace(O_xzx, 2) -``` - -MPOs also support a range of linear algebra operations, such as addition, subtraction and -multiplication, either among themselves or with a finite MPS. Here, it is important to note -that these operations will increase the virtual dimension of the resulting MPO or MPS, and -this naive application is thus typically not optimal. For approximate operations that do not -increase the virtual dimension, the more advanced algorithms in the [um_algorithms](@ref) -sections should be used. - -```@example operators -O_xzx² = O_xzx * O_xzx -println("Virtual dimension of O_xzx²: ", left_virtualspace(O_xzx², 2)) -O_xzx_sum = 0.1 * O_xzx + O_xzx² -println("Virtual dimension of O_xzx_sum: ", left_virtualspace(O_xzx_sum, 2)) -``` - -```@example operators -O_xzx_sum * FiniteMPS(3, ℂ^2, ℂ^4) -``` - -!!! note - The virtual spaces of the resulting MPOs typically grow exponentially with the - number of multiplications. Nevertheless, a number of optimizations are in place that - make sure that the virtual spaces do not increase past the maximal virtual space that - is dictated by the requirement of being full-rank tensors. - -## InfiniteMPO - -This construction can again be extended to the infinite case, where the tensors are repeated periodically. -Therefore, an [`InfiniteMPO`](@ref) is simply a `PeriodicVector` of `MPOTensor` objects. -These can only be constructed from vectors of `MPOTensor`s, since it is impossible to create the infinite operators directly. - -```@example operators -mpo = InfiniteMPO(O_xzx[1:2]) -``` - -Otherwise, their behavior is mostly similar to that of their finite counterparts. - -## FiniteMPOHamiltonian - -We can also represent quantum Hamiltonians in the same form. This is done by converting a -sum of local operators into a single MPO operator. The resulting operator has a very -specific structure, and is often referred to as a *Jordan block MPO*. - -This object can be constructed as an MPO by using the [`FiniteMPOHamiltonian`](@ref) constructor, -which takes two crucial pieces of information: - -1. An array of `VectorSpace` objects, which determines the local Hilbert spaces of the - system. The resulting MPO will snake through the array in linear indexing order. - -2. A set of local operators, which are characterised by a number of indices that specify on - which sites the operator acts, along with an operator to define the action. These are - specified as a `inds => operator` pairs, or any other iterable collection thereof. The - `inds` should be tuples of valid indices for the array of `VectorSpace` objects, or a - single integer for single-site operators. - -As a concrete example, we consider the -[Transverse-field Ising model](https://en.wikipedia.org/wiki/Transverse-field_Ising_model) -defined by the Hamiltonian - -```math -H = -J \sum_{\langle i, j \rangle} X_i X_j - h \sum_j Z_j -``` - -```@example operators -J = 1.0 -h = 0.5 -chain = fill(ℂ^2, 3) # a finite chain of 4 sites, each with a 2-dimensional Hilbert space -single_site_operators = [1 => -h * S_z, 2 => -h * S_z, 3 => -h * S_z] -two_site_operators = [(1, 2) => -J * S_x ⊗ S_x, (2, 3) => -J * S_x ⊗ S_x] -H_ising = FiniteMPOHamiltonian(chain, single_site_operators..., two_site_operators...) -``` - -Various alternative constructions are possible, such as using a `Dict` with key-value pairs -that specify the operators, or using generator expressions to simplify the construction. - -```@example operators -H_ising′ = -J * FiniteMPOHamiltonian(chain, - (i, i + 1) => S_x ⊗ S_x for i in 1:(length(chain) - 1)) - - h * FiniteMPOHamiltonian(chain, i => S_z for i in 1:length(chain)) -isapprox(H_ising, H_ising′; atol=1e-6) -``` - -Note that this construction is not limited to nearest-neighbour interactions, or 1D systems. -In particular, it is possible to construct quasi-1D realisations of 2D systems, by using -different arrays of [`VectorSpace`](@extref TensorKit.VectorSpace) objects. -For example, the 2D Ising model on a square lattice can be constructed as follows: - -```@example operators -square = fill(ℂ^2, 3, 3) # a 3x3 square lattice -operators = Dict() - -local_operators = Dict() -for I in eachindex(square) - local_operators[(I,)] = -h * S_z # single site operators still require tuples of indices -end - -# horizontal and vertical interactions are easier using Cartesian indices -horizontal_operators = Dict() -I_horizontal = CartesianIndex(0, 1) -for I in eachindex(IndexCartesian(), square) - if I[2] < size(square, 2) - horizontal_operators[(I, I + I_horizontal)] = -J * S_x ⊗ S_x - end -end - -vertical_operators = Dict() -I_vertical = CartesianIndex(1, 0) -for I in eachindex(IndexCartesian(), square) - if I[1] < size(square, 1) - vertical_operators[(I, I + I_vertical)] = -J * S_x ⊗ S_x - end -end - -H_ising_2d = FiniteMPOHamiltonian(square, local_operators) + - FiniteMPOHamiltonian(square, horizontal_operators) + - FiniteMPOHamiltonian(square, vertical_operators); -``` - -There are various utility functions available for constructing more advanced lattices, for -which the [lattices](@ref) section should be consulted. - -## InfiniteMPOHamiltonian - -Again, this construction can be extended straightforwardly to the infinite case. -To that end, we simply need to specify all interactions per unit cell. -In particular, an [`InfiniteMPOHamiltonian`](@ref) for the Ising model is obtained via - -```@example operators -J = 1.0 -h = 0.5 -infinite_chain = PeriodicVector([ℂ^2]) # an infinite chain of a local 2-dimensional Hilbert space -H_ising_infinite = InfiniteMPOHamiltonian(infinite_chain, 1 => -h * S_z, (1, 2) => -J * S_x ⊗ S_x) -``` - -### Expert mode - -The `MPOHamiltonian` constructor is in fact an automated way of constructing the -aforementioned *Jordan block MPO*. In its most general form, the matrix $W$ takes on the -form of the following block matrix: - -```math -\begin{pmatrix} -1 & C & D \\ -0 & A & B \\ -0 & 0 & 1 -\end{pmatrix} -``` - -which generates all single-site local operators $D$, all two-site operators $CB$, three-site -operators $CAB$, and so on. Additionally, this machinery can also be used to construct -interaction that are of (exponentially decaying) infinite range, and to approximate -power-law interactions. - -In order to illustrate this, consider the following explicit example of the Transverse-field -Ising model: - -```math -W = \begin{pmatrix} -1 & X & -hZ \\ -0 & 0 & -JX \\ -0 & 0 & 1 -\end{pmatrix} -``` - -If we add in the left and right boundary vectors - -```math -v_L = \begin{pmatrix} -1 & 0 & 0 -\end{pmatrix} -, \qquad -v_R = \begin{pmatrix} -0 \\ 0 \\ 1 -\end{pmatrix} -``` - -One can easily check that the Hamiltonian on $N$ sites is given by the contraction - -```math -H = V_L W^{\otimes N} V_R -``` - -We can even verify this symbolically: - -```@example operators -using Symbolics -L = 4 -# generate W matrices -@variables A[1:L] B[1:L] C[1:L] D[1:L] -Ws = map(1:L) do l - return [1 C[l] D[l] - 0 A[l] B[l] - 0 0 1] -end - -# generate boundary vectors -Vₗ = [1, 0, 0]' -Vᵣ = [0, 0, 1] - -# expand the MPO -expand(Vₗ * prod(Ws) * Vᵣ) -``` - -The [`FiniteMPOHamiltonian`](@ref) constructor can also be used to construct the operator from this most -general form, by supplying a vector of [`BlockTensorMap`](@extref BlockTensorKit.BlockTensorMap) objects -to the constructor. Here, the vector specifies the sites in the unit cell, while the blocktensors contain -the rows and columns of the matrix. We can verify this explicitly: - -```@example operators -H_ising[2] # print the blocktensor -``` - -### Working with `MPOHamiltonian` objects - -!!! warning - This part is still a work in progress - -Because of the discussion above, the `FiniteMPOHamiltonian` object is in fact just an `AbstractMPO`, -with some additional structure. This means that similar operations and properties are -available, such as the virtual spaces, or the individual tensors. However, the block -structure of the operator means that now the virtual spaces are not just a single space, but -a collection (direct sum) of spaces, one for each row/column. - -```@example operators -left_virtualspace(H_ising, 1), right_virtualspace(H_ising, 1), physicalspace(H_ising, 1) -``` diff --git a/docs/src/man/parallelism.md b/docs/src/man/parallelism.md deleted file mode 100644 index 683be061c..000000000 --- a/docs/src/man/parallelism.md +++ /dev/null @@ -1,116 +0,0 @@ -# Parallelism in julia - -Julia has great -[parallelism infrastructure](https://julialang.org/blog/2019/07/multithreading/), but there -is a caveat that is relevant for all algorithms implemented in MPSKit. The Julia threads do -not play nicely together with the BLAS threads, which are the threads used for many of the -linear algebra routines, and in particular for `gemm` (general matrix-matrix -multiplication). As this is a core routine in MPSKit, this has a significant impact on the -overall performance. - -## Julia threads vs BLAS threads - -A lot of the confusion stems from the fact that the BLAS threading behaviour is not -consistent between different vendors. Additionally, performance behaviour is severely -dependent on hardware, the specifics of the problem, and the availability of other resources -such as total memory, or memory bandwidth. This means that there is no one size fits all -solution, and that you will have to experiment with the settings to get optimal performance. -Nevertheless, there are some general guidelines that can be followed, which seem to at least -work well in most cases. - -The number of threads that are set by `BLAS.set_num_threads()`, in the case of OpenBLAS (the -default vendor), is equal to the **total number** of BLAS threads that is kept in a pool, -which is then shared by all Julia threads. This means that if you have 4 julia threads and 4 -BLAS threads, then all julia threads will share the same 4 BLAS threads. On the other hand, -using `BLAS.set_num_threads(1)`, OpenBLAS will now utilize the julia threads to run the BLAS -jobs. Thus, for OpenBLAS, very often setting the number of BLAS threads to 1 is the best -option, which will then maximally utilize the julia threading infrastructure of MPSKit. - -In the case of [MKL.jl](), which often outperforms OpenBLAS, the situation is a bit -different. Here, the number of BLAS threads corresponds to the number of threads that are -spawned by **each** julia thread. Thus, if you have 4 julia threads and 4 BLAS threads, then -each julia thread will spawn 4 BLAS threads, for a total of 16 BLAS threads. As such, it -might become necessary to adapt the settings to avoid oversubscription of the cores. - -A careful analysis of the different cases and benefits can be inspected by making use of -[`ThreadPinning.jl`](https://github.com/carstenbauer/ThreadPinning.jl)'s tool -`threadinfo(; blas=true, info=true)`. In particular, the following might demonstrate the -difference between OpenBLAS and MKL: - -```julia-repl -julia> Threads.nthreads() -4 - -julia> using ThreadPinning; threadinfo(; blas=true, hints=true) - -System: 8 cores (2-way SMT), 1 sockets, 1 NUMA domains - -| 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15 | - -# = Julia thread, # = HT, # = Julia thread on HT, | = Socket separator - -Julia threads: 4 -├ Occupied CPU-threads: 4 -└ Mapping (Thread => CPUID): 1 => 8, 2 => 5, 3 => 9, 4 => 2, - -BLAS: libopenblas64_.so -└ openblas_get_num_threads: 8 - -[ Info: jlthreads != 1 && blasthreads < cputhreads. You should either set BLAS.set_num_threads(1) (recommended!) or at least BLAS.set_num_threads(16). -[ Info: jlthreads < cputhreads. Perhaps increase number of Julia threads to 16? -julia> using MKL; threadinfo(; blas=true, hints=true) - -System: 8 cores (2-way SMT), 1 sockets, 1 NUMA domains - -| 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15 | - -# = Julia thread, # = HT, # = Julia thread on HT, | = Socket separator - -Julia threads: 4 -├ Occupied CPU-threads: 4 -└ Mapping (Thread => CPUID): 1 => 11, 2 => 12, 3 => 1, 4 => 2, - -BLAS: libmkl_rt.so -├ mkl_get_num_threads: 8 -└ mkl_get_dynamic: true - -┌ Warning: blasthreads_per_jlthread > cputhreads_per_jlthread. You should decrease the number of MKL threads, i.e. BLAS.set_num_threads(4). -└ @ ThreadPinning ~/.julia/packages/ThreadPinning/qV2Cd/src/threadinfo.jl:256 -[ Info: jlthreads < cputhreads. Perhaps increase number of Julia threads to 16? -``` - -## MPSKit multithreading - -Within MPSKit, when Julia is started with multiple threads, by default the `OhMyThreads.jl` -machinery will be used to parallelize the code as much as possible. In particular, this mostly -occurs whenever there is a unitcell and local updates can take place at each site in parallel. - -The multithreading behaviour can be controlled through a global `scheduler`, which can be set -using the `MPSKit.Defaults.set_scheduler!(arg; kwargs...)` function. This function accepts -either a `Symbol`, an `OhMyThreads.Scheduler` or keywords to determine a scheduler automatically. - -```julia -MPSKit.Defaults.set_scheduler!(:serial) # disable multithreading -MPSKit.Defaults.set_scheduler!(:greedy) # multithreading with greedy load-balancing -MPSKit.Defaults.set_scheduler!(:dynamic) # default: multithreading with some load-balancing -``` - -For further reference on the available schedulers and finer control, please refer to the -[`OhMyThreads.jl` documentation](https://juliafolds2.github.io/OhMyThreads.jl/stable/) - -## TensorKit multithreading - -Finally, when dealing with tensors that have some internal symmetry, it is also possible to -parallelize over the symmetry sectors. This is handled by TensorKit, and more information -can be found in its documentation (Soon TM). - -## Memory management - -Because of the way julia threads work, it is possible that the total memory usage of your -program becomes rather high. This seems to be because of the fact that MPSKit spawns several -tasks (in a nested way), which each allocate and deallocate quite a bit of memory in a tight -loop. This seems to lead to a situation where the garbage collector is not able to keep up, -and can even fail to clear the garbage before an `OutOfMemory` error occurs. In this case, -often the best thing to do is disable the multithreading of MPSKit, specifically for the -`derivatives`, as this seems to be the most memory intensive part. This is something that is -under investigation, and hopefully will be fixed in the future. diff --git a/docs/src/man/states.md b/docs/src/man/states.md deleted file mode 100644 index 168b2117c..000000000 --- a/docs/src/man/states.md +++ /dev/null @@ -1,174 +0,0 @@ -# [States](@id um_states) - -```@setup states -using MPSKit -using TensorKit -using LinearAlgebra: dot -``` - -## FiniteMPS - -A [`FiniteMPS`](@ref) is - at its core - a chain of mps tensors. - -```@raw html -finite MPS -``` - -### Usage - -A `FiniteMPS` can be created by passing in a vector of tensormaps: - -```@example states -L = 10 -data = [rand(ComplexF64, ℂ^1 ⊗ ℂ^2 ← ℂ^1) for _ in 1:L]; -state = FiniteMPS(data) -``` - -Or alternatively by specifying its structure - -```@example states -max_bond_dimension = ℂ^4 -physical_space = ℂ^2 -state = FiniteMPS(rand, ComplexF64, L, physical_space, max_bond_dimension) -``` - -You can take dot products, renormalize!, expectation values,.... - -### Gauging and canonical forms - -An MPS representation is not unique: for every virtual bond we can insert $C \cdot C^{-1}$ without altering the state. -Then, by redefining the tensors on both sides of the bond to include one factor each, we can change the representation. - -```@raw html -MPS gauge freedom -``` - -There are two particularly convenient choices for the gauge at a site, the so-called left and right canonical form. -For the left canonical form, all tensors to the left of a site are gauged such that they become left-isometries. -By convention, we call these tensors `AL`. - -```@example states -al = state.AL[3] -al' * al ≈ id(right_virtualspace(al)) -``` - -Similarly, the right canonical form turns the tensors into right-isometries. -By convention, these are called `AR`. - -```@example states -ar = state.AR[3] -repartition(ar, 1, 2) * repartition(ar, 1, 2)' ≈ id(left_virtualspace(ar)) -``` - -It is also possible to mix and match these two forms, where all tensors to the left of a given site are in the left gauge, while all tensors to the right are in the right gauge. -In this case, the final gauge transformation tensor can no longer be absorbed, since that would spoil the gauge either to the left or the right. -This center-gauged tensor is called `C`, which is also the gauge transformation to relate left- and right-gauged tensors. -Finally, for convenience it is also possible to leave a single MPS tensor in the center gauge, which we call `AC = AL * C` - -```@example states -c = state.C[3] # to the right of site 3 -c′ = state.C[2] # to the left of site 3 -al * c ≈ state.AC[3] ≈ repartition(c′ * repartition(ar, 1, 2), 2, 1) -``` - -These forms are often used throughout MPS algorithms, and the [`FiniteMPS`](@ref) object acts as an automatic manager for this. -It will automatically compute and cache the different forms, and detect when to recompute whenever needed. -For example, in order to compute the overlap of an MPS with itself, we can choose any site and bring that into the center gauge. -Since then both the left and right side simplify to the identity, this simply becomes the overlap of the gauge tensors: - -```@example states -d = dot(state, state) -all(c -> dot(c, c) ≈ d, state.C) -``` - -### Implementation details - -Behind the scenes, a `FiniteMPS` has 4 fields - -```julia -ALs::Vector{Union{Missing,A}} -ARs::Vector{Union{Missing,A}} -ACs::Vector{Union{Missing,A}} -Cs::Vector{Union{Missing,B}} -``` - -and calling `AL`, `AR`, `C` or `AC` returns lazy views over these vectors that instantiate the tensors whenever they are requested. -Similarly, changing a tensor will poison the `ARs` to the left of that tensor, and the `ALs` to the right. -The idea behind this construction is that one never has to worry about how the state is gauged, as this gets handled automagically. - -!!! warning - While a `FiniteMPS` can automatically detect when to recompute the different gauges, this requires that one of the tensors is set using an indexing operation. - In particular, in-place changes to the different tensors will not trigger the recomputation. - -## InfiniteMPS - -An [`InfiniteMPS`](@ref) can be thought of as being very similar to a finite mps, where the set of tensors is repeated periodically. - -It can also be created by passing in a vector of `TensorMap`s: - -```@example states -data = [rand(ComplexF64, ℂ^4 ⊗ ℂ^2 ← ℂ^4) for _ in 1:2] -state = InfiniteMPS(data) -``` - -or by initializing it from given spaces - -```@example states -phys_spaces = fill(ℂ^2, 2) -virt_spaces = [ℂ^4, ℂ^5] # by convention to the right of a site -state = InfiniteMPS(phys_spaces, virt_spaces) -``` - -Note that the code above creates an `InfiniteMPS` with a two-site unit cell, where the given virtual spaces are located to the right of their respective sites. - -### Gauging and canonical forms - -Much like for `FiniteMPS`, we can again query the gauged tensors `AL`, `AR`, `C` and `AC`. -Here however, the implementation is much easier, since they all have to be recomputed whenever a single tensor changes. -This is a result of periodically repeating the tensors, every `AL` is to the right of the changed site, and every `AR` is to the left. -As a result, the fields are simply - -```julia -AL::PeriodicArray{A,1} -AR::PeriodicArray{A,1} -C::PeriodicArray{B,1} -AC::PeriodicArray{A,1} -``` - -## WindowMPS - -A [`WindowMPS`](@ref) or segment MPS can be seen as a mix between an [`InfiniteMPS`](@ref) and a [`FiniteMPS`](@ref). -It represents a window of mutable tensors (a finite MPS), embedded in an infinite environment (two infinite MPSs). -It can therefore be created accordingly, ensuring that the edges match: - -```@example states -infinite_state = InfiniteMPS(ℂ^2, ℂ^4) -finite_state = FiniteMPS(5, ℂ^2, ℂ^4; left=ℂ^4, right=ℂ^4) -window = WindowMPS(infinite_state, finite_state, infinite_state) -``` - -Algorithms will then act on this window of tensors, while leaving the left and right infinite states invariant. - -## MultilineMPS - -A two-dimensional classical partition function can often be represented by an infinite tensor network. -There are many ways to evaluate such a network, but here we focus on the so-called boundary MPS methods. -These first reduce the problem from contracting a two-dimensional network to the contraction of a one-dimensional MPS, by finding the fixed point of the row-to-row (or column-to-column) transfer matrix. -In these cases however, there might be a non-trivial periodicity in both the horizontal as well as vertical direction. -Therefore, in MPSKit they are represented by [`MultilineMPS`](@ref), which are simply a repeating set of [`InfiniteMPS`](@ref). - -```@example states -state = MultilineMPS(fill(infinite_state, 2)) -``` - -They offer some convenience functionality for using cartesian indexing (row - column): - -You can access properties by calling -```@example states -row = 2 -col = 2 -al = state.AL[row, col]; -``` - -These objects are also used extensively in the context of [PEPSKit.jl](https://github.com/QuantumKitHub/PEPSKit.jl). - diff --git a/docs/src/tutorials/excitations.md b/docs/src/tutorials/excitations.md new file mode 100644 index 000000000..a34346485 --- /dev/null +++ b/docs/src/tutorials/excitations.md @@ -0,0 +1,105 @@ +# [Quasiparticle excitations](@id tutorial_excitations) + +The previous tutorials ended with a ground state: the lowest-energy state of the transverse-field Ising model, first on a finite chain and then directly in [the thermodynamic limit](@ref tutorial_thermodynamic_limit). +The natural next question is what lies *above* it: how much energy does it cost to excite the system? +For a translation-invariant chain the answer is organized by momentum — for each momentum ``k`` there is a lowest excitation energy ``\Delta E(k)``, and the resulting curve is the **dispersion relation** of the model. +Its minimum over all momenta is the **energy gap**, one of the most basic characterizations of a quantum phase. + +In this tutorial we compute the dispersion relation of the infinite transverse-field Ising chain with MPSKit's quasiparticle ansatz, and finish with a plot of ``\Delta E(k)`` across the Brillouin zone — compared against the exact solution. + +## Loading the packages + +As in the previous tutorials, every code block on this page shares one Julia session, so we load the packages once. + +```@example excitations +using MPSKit, MPSKitModels, TensorKit +using Plots +``` + +## 1. Find the ground state + +Excitations are computed *on top of* a ground state, so the first step is the calculation you already know from [The thermodynamic limit](@ref tutorial_thermodynamic_limit): build the infinite Hamiltonian, make a random `InfiniteMPS`, and converge it with `VUMPS`. + +This time we set the field to `g = 2.0`, deep in the paramagnetic phase, where the model is **gapped**: the lowest excitation costs a finite amount of energy, which is exactly what we want to measure. + +```@example excitations +g = 2.0 +H = transverse_field_ising(; g) +ψ₀ = InfiniteMPS(ℂ^2, ℂ^12) +ψ, envs, ϵ = find_groundstate(ψ₀, H, VUMPS(; verbosity = 0)) +``` + +We keep all three return values this time: the optimized state `ψ` and the environments `envs` both feed directly into the excitation calculation below, so nothing has to be recomputed. + +## 2. One excitation at one momentum + +The **quasiparticle ansatz** builds an excited state directly on top of the uniform ground state. +The idea is simple to picture: take the converged ground state and perturb it locally, replacing the tensor at one site with a new one that we get to optimize. +Because the chain is infinite and translation invariant, we do not place this perturbation at any particular site; instead we superpose it across *all* sites with a plane-wave phase, which gives the excitation a definite momentum ``k``. +Optimizing the perturbation then yields the lowest excited state at that momentum. + +The call is [`excitations`](@ref) with the [`QuasiparticleAnsatz`](@ref) algorithm, a momentum (a real number, in radians per site), and the ground state with its environments. +Let us ask for the excitation at the edge of the Brillouin zone, ``k = \pi``: + +```@example excitations +E, ϕ = excitations(H, QuasiparticleAnsatz(), π, ψ, envs) +E +``` + +Two things to note about the return values: + +- `E` is a *vector* of excitation energies, of length `num` — the keyword controlling how many excitations to compute at this momentum, which defaults to `num = 1`, so here it has a single entry. +- The entries of `E` are energies **above the ground state** — gaps at this momentum, not total energies. The ground-state energy is subtracted internally, so you can read them off directly. + +The second return value `ϕ` holds the corresponding quasiparticle states, which can be used for further post-processing; we will not need them in this tutorial. + +## 3. The full dispersion + +To trace out the whole dispersion relation we simply pass a *range* of momenta instead of a single number. +By symmetry it is enough to scan from ``0`` to ``\pi``, and we use 16 points to keep the runtime modest. + +```@example excitations +momenta = range(0, π, 16) +Es, ϕs = excitations(H, QuasiparticleAnsatz(), momenta, ψ, envs; verbosity = 0) +size(Es) +``` + +With a range of momenta the energies come back as a matrix of size `(length(momenta), num)` — here `(16, 1)`, one row per momentum and one column because we kept the default `num = 1`. +We pass `verbosity = 0` to silence the progress line this method otherwise prints for every momentum. +The momenta are independent of one another, so MPSKit works on them in parallel by default. + +## 4. Plot the dispersion + +Now for the payoff. +This particular model is exactly solvable, so we can plot our numerical dispersion right on top of the known answer: + +```math +\Delta E(k) = 2\sqrt{1 + g^2 - 2 g \cos k}. +``` + +For this Hermitian problem the computed energies come back as real numbers; the `real.(...)` below is a harmless safeguard for the general case, where the eigenvalue solver may return a complex number type with numerically vanishing imaginary parts. + +```@example excitations +k_exact = range(0, π, 200) +ΔE_exact = @. 2 * sqrt(1 + g^2 - 2g * cos(k_exact)) +plot(k_exact, ΔE_exact; label = "exact", xlabel = "momentum k", ylabel = "ΔE(k)", title = "TFIM dispersion (g = $g)") +scatter!(momenta, real.(Es); label = "quasiparticle ansatz (D = 12)") +``` + +The 16 computed points fall right on the exact curve. +The dispersion rises monotonically from ``k = 0`` to ``k = \pi``, so its minimum — the gap — sits at zero momentum, where the exact value is ``\Delta E(0) = 2(g - 1)``. +Our first matrix entry is precisely that point, so we can close with a numerical check: + +```@example excitations +real(Es[1, 1]), 2 * (g - 1) +``` + +A ground state at bond dimension 12 plus a variational quasiparticle on top reproduces the exact gap of the model — that is the quasiparticle ansatz working as intended. + +## Where to go next + +You have computed a full dispersion relation on top of an infinite ground state and read off the energy gap. + +The [`excitations`](@ref) entry point can do considerably more than what we used here: it can target excitations carrying a nontrivial symmetry charge, build topological (domain-wall) excitations that interpolate between two different ground states, and compute excited states of *finite* chains, where momentum is no longer a good quantum number and different algorithms take over. +All of these are recipes in [Excited states](@ref howto_excitations). +For what each excitation algorithm actually does and when to choose it, see the library reference [Excitations](@ref lib_excitations). diff --git a/docs/src/tutorials/first_groundstate.md b/docs/src/tutorials/first_groundstate.md new file mode 100644 index 000000000..3b2473b74 --- /dev/null +++ b/docs/src/tutorials/first_groundstate.md @@ -0,0 +1,150 @@ +# [Your first ground state](@id tutorial_first_groundstate) + +This tutorial walks you through a complete MPSKit.jl calculation from start to finish: we build the transverse-field Ising model, find its ground state with DMRG, measure a few physical quantities, and finish with a plot of the magnetization across the model's phase transition. +It assumes only that you are comfortable with basic quantum mechanics and that you have finished [Installation](@ref tutorial_installation), so the packages used below are already available in your environment. + +The transverse-field Ising model (TFIM) is the "hello world" of quantum many-body physics: it is the simplest model that still shows a genuine quantum phase transition, so it is the natural place to learn the tools. +On a chain of ``L`` spin-1/2 sites it is + +```math +H = -J\left(\sum_{\langle i,j\rangle} \sigma^z_i\,\sigma^z_j + g\sum_i \sigma^x_i\right), +``` + +where the first sum runs over neighbouring pairs. +The coupling ``J`` sets the overall energy scale, and the dimensionless field ``g`` tunes the competition between the ferromagnetic ``\sigma^z\sigma^z`` interaction and the transverse ``\sigma^x`` field. + +The ground state of ``H`` lives in a Hilbert space of dimension ``2^L``, which is far too large to store as a plain vector for any interesting ``L``. +A *matrix product state* (MPS) sidesteps this by storing the state as a chain of small tensors, one per site, whose sizes we control directly; this is what makes the calculation below tractable. +The details of that compression are the subject of the concept pages — here we simply use it. + +## Loading the packages + +Every code block on this page shares one Julia session, so we only need to load packages once. +We take the model and lattice from MPSKitModels, the local spin operators from TensorKitTensors, and `Plots` for the final figure. + +```@example first-groundstate +using MPSKit, MPSKitModels, TensorKit +using TensorKitTensors.SpinOperators: σˣ, σᶻ +using Plots +``` + +## 1. Build the Hamiltonian + +We work with a chain of `L = 16` sites and fix the field to `g = 0.5` for now. +`transverse_field_ising` assembles the Hamiltonian above; passing `FiniteChain(L)` asks for a finite open chain of `L` sites. + +```@example first-groundstate +L = 16 +H = transverse_field_ising(FiniteChain(L); g = 0.5) +``` + +The returned object is an `MPOHamiltonian`: the Hamiltonian written in matrix-product-operator form, i.e. as a chain of small tensors just like the state it acts on. +You do not need to know its internals to use it — MPSKit's algorithms consume it directly. +For other ways to build Hamiltonians see [Building Hamiltonians](@ref howto_hamiltonians). + +## 2. Build the initial state + +DMRG is an optimization: it needs a starting state to improve. +We create a random `FiniteMPS` with the right structure. + +```@example first-groundstate +D = 4 +ψ₀ = FiniteMPS(L, ℂ^2, ℂ^D) +``` + +The two space arguments describe the two kinds of index every MPS tensor carries: + +- `ℂ^2` is the **physical space** — the local Hilbert space of a single spin-1/2 site, which has dimension 2. +- `ℂ^D` is the **virtual (bond) space** — the internal index linking neighbouring tensors, whose dimension `D` is the *bond dimension*. + +The bond dimension `D` is the accuracy knob of the whole method: a larger `D` lets the MPS capture more entanglement and represent the true ground state more faithfully, at the cost of more computation. +`D = 4` is deliberately small so this tutorial runs quickly; [Controlling bond dimension](@ref howto_bond_dimension) covers how to choose and grow it. + +!!! warning "Pass spaces, not integers" + The physical and virtual arguments must be *vector spaces* (`ℂ^2`, `ℂ^D`, or equivalently `ComplexSpace(2)`), never bare integers. + Writing `FiniteMPS(16, 2, 4)` throws a `MethodError` — this is the single most common beginner mistake. + +## 3. Find the ground state + +Now we run the calculation. +`find_groundstate` takes the starting state, the Hamiltonian, and an algorithm; we pass [`DMRG`](@ref) explicitly so the algorithm is visible. + +```@example first-groundstate +ψ, envs, ϵ = find_groundstate(ψ₀, H, DMRG()) +``` + +DMRG (the density-matrix renormalization group) sweeps back and forth along the chain, locally optimizing each tensor while holding the others fixed, and repeats until the state stops changing. +The lines printed above are the per-iteration convergence log (shown at the default `verbosity`); each reports the sweep number, the current energy, and a convergence measure (the same Galerkin residual returned as `ϵ` below). + +!!! note "The algorithm is optional" + Calling `find_groundstate(ψ₀, H)` with no algorithm argument selects DMRG automatically for a finite input, so the explicit `DMRG()` above is only for clarity. + `DMRG` accepts keywords such as `tol` (default `1e-10`), `maxiter` (default `200`), and `verbosity` (default `3`); we use `verbosity = 0` later to silence the log inside a loop. + +`find_groundstate` returns a triple: + +- `ψ` — the optimized ground-state MPS (a *new* state; `ψ₀` is left untouched, so we can reuse it below). A mutating variant `find_groundstate!` also exists. +- `envs` — the *environments*, cached partial contractions that later measurements can reuse to save work. +- `ϵ` — a convergence-error measure (the Galerkin residual). It quantifies how well the sweeps converged; note that it is **not** the error in the energy. + +## 4. Measure observables + +With a ground state in hand we can extract physical quantities. +The energy is the expectation value of the Hamiltonian itself — pass `H` directly, with no site index: + +```@example first-groundstate +E = expectation_value(ψ, H) +``` + +For a Hermitian `H` and a normalized state this is real up to floating-point noise. + +The order parameter of the TFIM is the local magnetization ``\langle\sigma^z_i\rangle``. +We measure it at every site by pairing each site index with the single-site operator `σᶻ()`: + +```@example first-groundstate +[expectation_value(ψ, i => σᶻ()) for i in 1:L] +``` + +Finally, a good "how converged am I really?" check is the energy variance ``\langle H^2\rangle - \langle H\rangle^2``, which vanishes exactly when `ψ` is a true eigenstate: + +```@example first-groundstate +variance(ψ, H) +``` + +A small variance indicates the state is close to an eigenstate of `H`. +More recipes for observables live in [Computing observables](@ref howto_observables). + +## 5. Magnetization across the transition + +The payoff: we sweep the field `g` from 0 to 2 and, for each value, find the ground state and record its average magnetization. +This traces out the phase transition. + +Each step of the sweep repeats the workflow of Sections 1–4 on the same open chain — only the value of `g` changes. + +```@example first-groundstate +g_values = 0:0.1:2 +M = map(g_values) do g + Hg = transverse_field_ising(FiniteChain(L); g = g) + ψg, = find_groundstate(ψ₀, Hg; verbosity = 0) + return abs(sum(expectation_value(ψg, i => σᶻ()) for i in 1:L)) / L +end +scatter(g_values, M; xlabel = "g", ylabel = "M", label = "D = $D", title = "TFIM magnetization") +``` + +Here we take the **absolute value** of the mean magnetization. +At finite `L` the exact ground state does not break the symmetry: it is the symmetric combination of the two oppositely magnetized states, and its raw magnetization ``\sum_i\langle\sigma^z_i\rangle`` is exactly zero. +DMRG at finite bond dimension, however, converges to one of the two symmetry-broken states instead, because either one carries far less entanglement than their symmetric superposition. +Which sign it lands on is arbitrary — it can differ from run to run and between values of `g` — so taking `abs` makes the order-parameter curve well-defined regardless of the branch. + +The plot shows the magnetization close to 1 deep on the ordered side, then dropping to zero — noticeably *below* the thermodynamic critical point `g = 1` (around `g ≈ 0.6` at these parameters). +Both features follow from how the state is computed rather than from the physics of the transition: +on the ordered side DMRG sits on one symmetry-broken branch, and past the drop it recovers the exactly symmetric ground state, whose magnetization vanishes. +Exactly where the drop lands depends on `L` and `D`, so its location by itself is *not yet* a measurement of the critical point. +The honest way to locate the transition is by performing a scaling analysis, taking the limit of infinite size and bond dimension. + +## Where to go next + +You have run a full MPSKit workflow: build a model, optimize an MPS ground state, measure observables, and scan a parameter. +A natural next step is [The thermodynamic limit](@ref tutorial_thermodynamic_limit): +the same calculation performed directly at infinite system size with an `InfiniteMPS`, which removes the finite-size effects seen above and lets you locate the critical point more cleanly. + +To go deeper on the individual steps, see [Constructing states](@ref howto_states), [Building Hamiltonians](@ref howto_hamiltonians), [Computing observables](@ref howto_observables), [Controlling bond dimension](@ref howto_bond_dimension), and [Entanglement entropy and spectrum](@ref howto_entanglement); the algorithm reference is [Ground-state algorithms](@ref lib_groundstate). diff --git a/docs/src/tutorials/installation.md b/docs/src/tutorials/installation.md new file mode 100644 index 000000000..78b67dc4b --- /dev/null +++ b/docs/src/tutorials/installation.md @@ -0,0 +1,63 @@ +# [Installation](@id tutorial_installation) + +This page walks you through setting up a Julia environment for working with MPSKit.jl, and ends with a small snippet you can run to check that everything works. + +## Prerequisites + +You need a working installation of Julia, version 1.10 or later. +If you don't have Julia yet, install it via [juliaup](https://github.com/JuliaLang/juliaup) or download it directly from [julialang.org](https://julialang.org/downloads/). +This tutorial assumes you are comfortable starting the Julia REPL and typing commands into it, but does not assume any prior experience with Julia's package manager. + +## Set up a project environment + +Before installing any packages, create a dedicated environment for this tutorial. +Working in a fresh, named environment (rather than the global default environment) keeps the exact package versions you use here reproducible, and avoids clashes with other projects on your machine. + +Start Julia, enter the package manager by pressing `]`, and activate a new environment: + +``` +pkg> activate mpskit-tutorial +``` + +Julia will create the environment the first time you add a package to it. + +## Install the packages + +With the environment activated, install MPSKit.jl and the packages used throughout this documentation: + +``` +pkg> add MPSKit TensorKit TensorOperations MPSKitModels TensorKitTensors Plots +``` + +- `MPSKit` provides the matrix product state and operator types, together with the ground-state, time-evolution, and bond-dimension algorithms. +- `TensorKit` supplies the tensor backend (`TensorMap`s and vector spaces) that MPSKit is built on; installing it alongside MPSKit also gives access to truncation-scheme constructors such as `truncrank`, which TensorKit re-exports from MatrixAlgebraKit. +- `TensorOperations` provides the `@tensor` macro used to contract tensors by hand. +- `MPSKitModels` collects pre-defined Hamiltonians (such as the transverse-field Ising model) and lattices for common physical models. +- `TensorKitTensors` provides ready-made local operators, such as the Pauli operators. +- `Plots` is used to visualize results in several of the how-to guides and examples; it is optional if you only intend to run computations without plotting. + +MPSKit.jl is registered in Julia's General registry, so `pkg> add` fetches it directly; you do not need to add any custom registries. + +!!! note "First `using` is slow" + The first time you load these packages with `using`, Julia precompiles them, which can take a minute or two. + Subsequent loads in the same environment are much faster. + +## Verify your setup + +Once the packages have finished installing, exit the package manager (backspace) and run the following in the same environment to check that MPSKit, TensorKit, and MPSKitModels work together. + +```@example verify-install +using MPSKit, TensorKit, MPSKitModels + +H = transverse_field_ising(FiniteChain(8); J = 1.0, g = 0.5) +ψ = FiniteMPS(8, ℂ^2, ℂ^8) +``` + +If this runs without error and prints a `FiniteMPS`, your environment is ready. + +From here, continue with [Your first ground state](@ref tutorial_first_groundstate), which uses this same Hamiltonian and initial state to find the ground state of the transverse-field Ising model with DMRG. + +## Troubleshooting + +- **Long precompilation on first use:** this is expected the first time you `using` a package (or after updating one), especially for a large dependency stack; it is not a sign that anything is wrong. +- **Version resolver conflicts:** if `pkg> add` reports that it cannot find a compatible set of versions, try creating a fresh environment (as above) rather than adding these packages to an existing environment that already has other constraints. diff --git a/docs/src/tutorials/thermodynamic_limit.md b/docs/src/tutorials/thermodynamic_limit.md new file mode 100644 index 000000000..0be68f69e --- /dev/null +++ b/docs/src/tutorials/thermodynamic_limit.md @@ -0,0 +1,134 @@ +# [The thermodynamic limit](@id tutorial_thermodynamic_limit) + +In [Your first ground state](@ref tutorial_first_groundstate) we put the transverse-field Ising model on a finite chain of `L = 16` sites. +That is a perfectly good calculation, but it carries two prices: the open ends of the chain are physically different from its middle (boundary effects), and every quantity we measured still depends on the length `L` (finite-size effects). +To read off the true physics of the model we would have to repeat the calculation at several lengths and extrapolate `L → ∞`. + +MPSKit lets you skip that extrapolation and work *directly* at `L = ∞`. +The trick is translation invariance: instead of storing one tensor per site, we store a single tensor and imagine it repeated forever along the chain — an [`InfiniteMPS`](@ref). +There are no ends, so there are no boundary effects, and there is no `L` to extrapolate. +Best of all, as you are about to see, the code barely changes: the same model, the same workflow, two edits. + +!!! note "Infinite states are always normalized" + An `InfiniteMPS` is normalized to 1 by construction, and you cannot choose otherwise. + Any other normalization would make expectation values either blow up or vanish as the (infinite) chain length is taken to infinity, so per-site quantities are the only ones that make sense here. + +## Loading the packages + +As before, every code block on this page shares one Julia session, so we load the packages once. + +```@example thermodynamic-limit +using MPSKit, MPSKitModels, TensorKit +using TensorKitTensors.SpinOperators: σˣ, σᶻ +using Plots +``` + +## 1. Build the Hamiltonian and initial state + +Here are the only two lines that differ from the finite tutorial. + +For the Hamiltonian, we drop the lattice argument. +Where the finite version wrote `transverse_field_ising(FiniteChain(L); g = 0.5)`, we simply omit `FiniteChain(L)`: with no lattice, `transverse_field_ising` builds the Hamiltonian for the infinite chain. + +```@example thermodynamic-limit +H = transverse_field_ising(; g = 0.5) +``` + +For the state, we swap `FiniteMPS` for `InfiniteMPS`. +There is no length to pass, so the constructor takes just the physical and virtual spaces — the physical space `ℂ^2` of a spin-1/2 site and the bond space `ℂ^D` whose dimension `D` is again the accuracy knob. + +```@example thermodynamic-limit +D = 4 +ψ₀ = InfiniteMPS(ℂ^2, ℂ^D) +``` + +That is the whole difference. +The bond dimension means exactly what it did on the finite chain (see [Controlling bond dimension](@ref howto_bond_dimension)), and `ℂ^2`/`ℂ^D` are the same physical/virtual spaces. + +!!! note "`InfiniteMPS` also accepts bare integers" + Unlike `FiniteMPS`, the infinite constructor happily takes plain integers: `InfiniteMPS(2, D)` is equivalent to `InfiniteMPS(ℂ^2, ℂ^D)`. + We stick with the explicit spaces to match the rest of the documentation. + +## 2. Find the ground state + +We optimize with [`VUMPS`](@ref), the infinite-chain workhorse, passing it explicitly so it is visible. + +```@example thermodynamic-limit +ψ, envs, ϵ = find_groundstate(ψ₀, H, VUMPS()) +``` + +The lines printed above are VUMPS's per-iteration convergence log, shown at the default `verbosity`. +VUMPS (the variational uniform matrix product state algorithm) optimizes the single repeated tensor directly in the thermodynamic limit, iterating until it reaches a fixed point. + +The return value has the same shape as on the finite chain: the optimized state `ψ`, the reusable `envs`, and a convergence-error measure `ϵ`. + +!!! note "The algorithm is optional here too" + Just as `find_groundstate(ψ₀, H)` selected DMRG for a finite input, calling it with no algorithm on an *infinite* input selects VUMPS automatically. + `VUMPS` accepts the familiar keywords `tol` (default `1e-10`), `maxiter` (default `200`), and `verbosity` (default `3`); we use `verbosity = 0` later to silence the log inside a loop. + Note there is no `find_groundstate!` for infinite states — VUMPS returns a fresh state and leaves `ψ₀` untouched. + +## 3. Measure observables + +For the default single-site unit cell used here, `expectation_value(ψ, H)` returns the energy of that one-site unit cell, which is exactly the **energy per site**: + +```@example thermodynamic-limit +E = expectation_value(ψ, H) +``` + +The magnetization is the local order parameter ``\langle\sigma^z\rangle``. +Because the state is translation-invariant, every site is identical, so we measure it at site 1 of the unit cell: + +```@example thermodynamic-limit +expectation_value(ψ, 1 => σᶻ()) +``` + +So far these are the same quantities we computed on the finite chain. +The infinite setting also unlocks an observable with no finite-chain analogue: the [`correlation_length`](@ref), extracted from the transfer-matrix spectrum of the uniform state. + +```@example thermodynamic-limit +correlation_length(ψ) +``` + +The correlation length tells us how far apart two spins can still "feel" each other; it is measured in units of the lattice spacing. +It grows as we approach the critical point `g = 1`, where correlations become long-ranged. +We can see this by optimizing a second state right at criticality and comparing: + +```@example thermodynamic-limit +H_crit = transverse_field_ising(; g = 1.0) +ψ_crit, = find_groundstate(ψ₀, H_crit, VUMPS(; verbosity = 0)) +correlation_length(ψ_crit) +``` + +At a genuine critical point the correlation length diverges, but a finite bond dimension `D` can only capture correlations out to a finite range, so what we measure is large but capped rather than infinite. + +## 4. Magnetization across the transition + +As on the finite chain, we finish by sweeping the field `g` and recording the magnetization. +The structure mirrors the finite sweep exactly — only `InfiniteMPS` and `VUMPS` have changed. + +```@example thermodynamic-limit +g_values = 0.1:0.1:2 +M = map(g_values) do g + Hg = transverse_field_ising(; g = g) + ψg, = find_groundstate(ψ₀, Hg, VUMPS(; verbosity = 0)) + return abs(expectation_value(ψg, 1 => σᶻ())) +end +scatter(g_values, M; xlabel = "g", ylabel = "M", label = "D = $D", title = "TFIM magnetization (L = ∞)") +``` + +Compare this with the finite-chain sweep of the previous tutorial, where the magnetization dropped to zero well before `g = 1`, at a point set by the algorithm rather than by the physics. +The infinite curve instead tracks the transition itself: the magnetization stays on its ordered branch all the way up to the critical point and collapses to zero right at `g = 1`. +What little smearing remains around the critical point is a finite-bond-dimension effect, and it shrinks as `D` grows. + +We still take the **absolute value** of the magnetization, but for a subtly different reason than on the finite chain. +On the finite chain the nonzero magnetization was an artifact of the algorithm: the exact ground state there is symmetric, and DMRG landed on a symmetry-broken state only because it carries less entanglement. +In the thermodynamic limit the symmetry breaking is genuine — the two oppositely magnetized states become true ground states — and an infinite MPS at finite bond dimension settles into one of them on the ordered side, landing on a definite nonzero magnetization of either sign; `abs` again puts both branches onto a single order-parameter curve. + +## Where to go next + +You have now run the same TFIM calculation twice — once at finite size, once directly at `L = ∞` — and seen how little the code had to change. + +From here you can go beyond ground states. +A natural next step is to [compute the excitations above this infinite ground state](@ref tutorial_excitations) (the model's quasiparticle spectrum), or to [exploit the symmetries of the model](@ref tutorial_using_symmetries) to make the calculation cheaper and more accurate. + +To go deeper on the individual steps used here, see [Constructing states](@ref howto_states), [Controlling bond dimension](@ref howto_bond_dimension), and [Entanglement entropy and spectrum](@ref howto_entanglement); the algorithm reference is [Ground-state algorithms](@ref lib_groundstate). diff --git a/docs/src/tutorials/time_evolution.md b/docs/src/tutorials/time_evolution.md new file mode 100644 index 000000000..21e8474e4 --- /dev/null +++ b/docs/src/tutorials/time_evolution.md @@ -0,0 +1,134 @@ +# [A quantum quench](@id tutorial_time_evolution) + +The previous tutorials computed ground states — static snapshots of a model at its lowest energy. +This tutorial adds the time axis: we take a state that is *not* an eigenstate of its Hamiltonian and watch it evolve under the Schrödinger equation, + +```math +|\psi(t)\rangle = e^{-iHt}\,|\psi(0)\rangle , +``` + +tracking one local observable as a function of time. +The protocol we use is the simplest and most common one in the field, a *global quench*, and the workhorse algorithm is [`TDVP`](@ref), the time-dependent variational principle, driven one step at a time through [`timestep`](@ref). + +We stay with the transverse-field Ising model from [Your first ground state](@ref tutorial_first_groundstate), so the model-building and ground-state steps below should look familiar. + +## Loading the packages + +Every code block on this page shares one Julia session, so we load the packages once. +As before, the model comes from MPSKitModels, the local spin operators from TensorKitTensors, and `Plots` draws the final figure. + +```@example time-evolution +using MPSKit, MPSKitModels, TensorKit +using TensorKitTensors.SpinOperators: σˣ, σᶻ +using Plots +``` + +## 1. Prepare the initial state + +Time evolution needs a definite starting state, and the standard choice is the ground state of some Hamiltonian. +We take a chain of `L = 12` sites with a transverse field `g₀ = 0.5` — the ordered side of the model — and find its ground state exactly as in [the first tutorial](@ref tutorial_first_groundstate), silencing the convergence log with `verbosity = 0`. +The bond dimension `D = 16` is comfortably large for a ground state of this size; we will see below why time evolution wants more headroom than a ground-state calculation. + +```@example time-evolution +L = 12 +D = 16 +g₀ = 0.5 +H₀ = transverse_field_ising(FiniteChain(L); g = g₀) +ψ₀ = FiniteMPS(L, ℂ^2, ℂ^D) +ψ, = find_groundstate(ψ₀, H₀, DMRG(; verbosity = 0)) +nothing # hide +``` + +The observable we will track through the evolution is the transverse magnetization ``\langle\sigma^x\rangle`` at the middle of the chain, away from the open ends. +We measure its baseline value in the pre-quench ground state: + +```@example time-evolution +i_mid = L ÷ 2 +real(expectation_value(ψ, i_mid => σˣ())) +``` + +## 2. The quench + +A *global quench* is the sudden change of a parameter of the Hamiltonian, everywhere at once: we prepare the ground state of `H₀`, then at `t = 0` switch the Hamiltonian to a different `H₁` and let the state evolve under it. +Because `ψ` is an eigenstate of `H₀` but not of `H₁`, it is no longer stationary — the quench injects energy into the system, and nontrivial dynamics follows. + +Our quench takes the transverse field from `g₀ = 0.5` all the way across the phase transition to `g₁ = 2.0`: + +```@example time-evolution +g₁ = 2.0 +H₁ = transverse_field_ising(FiniteChain(L); g = g₁) +``` + + +## 3. A single time step + +The elementary move of real-time evolution in MPSKit is [`timestep`](@ref), which advances a state by one small increment `dt`. +Its arguments are, in order: the state, the Hamiltonian to evolve under, the current time, the step size, and the algorithm. +Here we take the very first step, from `t = 0.0` to `t = dt`, with single-site [`TDVP`](@ref): + +```@example time-evolution +dt = 0.05 +ψ_t, envs = timestep(ψ, H₁, 0.0, dt, TDVP()) +real(expectation_value(ψ_t, i_mid => σˣ())) +``` + +TDVP integrates the Schrödinger equation projected onto the space of MPS with the current bond dimension, which is why it slots so naturally into an MPS workflow. + +`timestep` returns two things: + +- `ψ_t` — the evolved state at time `dt` (a new state; `ψ` is left untouched). +- `envs` — the environments, cached partial contractions belonging to the new state and `H₁`. + +The `envs` are the reason evolution loops are cheap to keep running: passing them back into the next `timestep` call lets it start from the cached contractions instead of recomputing them from scratch. + +The transverse magnetization has already moved slightly away from its `t = 0` value — the state is on its way. + +## 4. Evolving in a loop + +Real-time evolution is nothing more than this single step, repeated. +We already took step 1 above, so the loop below performs the remaining steps, up to `n_steps = 40` in total (a final time of `t = 2.0`), recording the transverse magnetization at the middle of the chain after every step. +Note how each iteration feeds the previous `envs` back in as the optional last argument, and how the current time `(n - 1) * dt` advances with the loop. + +```@example time-evolution +n_steps = 40 +times = (0:n_steps) .* dt +m = zeros(n_steps + 1) +m[1] = real(expectation_value(ψ, i_mid => σˣ())) # t = 0, before the quench dynamics +m[2] = real(expectation_value(ψ_t, i_mid => σˣ())) # t = dt, from the single step above +for n in 2:n_steps + global ψ_t, envs + ψ_t, envs = timestep(ψ_t, H₁, (n - 1) * dt, dt, TDVP(), envs) + m[n + 1] = real(expectation_value(ψ_t, i_mid => σˣ())) +end +m[end] +``` + +(The `global` keyword is needed because the loop rebinds `ψ_t` and `envs`, which live outside it; inside a function you would not need it.) + +Writing the loop by hand like this keeps every moving part visible, which is the point of a tutorial. +For production use, [`time_evolve`](@ref) wraps exactly this loop and steps through a whole vector of time points in one call — see [Time evolution](@ref howto_time_evolution). + +## 5. Magnetization over time + +The payoff: the transverse magnetization at the middle of the chain, as a function of time after the quench. + +```@example time-evolution +plot(times, m; + xlabel = "t", ylabel = "⟨σˣ⟩ at site $i_mid", + label = "TDVP, D = $D", title = "TFIM transverse magnetization after a quench") +``` + +The curve shows how the observable responds to the sudden change in the field: starting from its pre-quench value, ``\langle\sigma^x\rangle`` relaxes towards a new value set by `H₁`, with oscillations along the way. + +!!! warning "Fixed bond dimension means finite reach in time" + Single-site TDVP keeps the bond dimension fixed at whatever the initial state has. + After a quench, however, the entanglement of the evolving state grows with time, so a fixed bond dimension can only follow the true dynamics faithfully up to some finite time — beyond it, the simulation quietly loses accuracy rather than failing loudly. + The practical checks and remedies — two-site [`TDVP2`](@ref), which grows the bond dimension as it truncates, and bond-expansion options for single-site TDVP — are collected in [Time evolution](@ref howto_time_evolution). + +## Where to go next + +You have run your first dynamics simulation: prepare a ground state, quench the Hamiltonian, step the state forward in time, and read off an observable at every step. + +The natural reference for everything this page glossed over is the [Time evolution](@ref howto_time_evolution) how-to: evolving over a time span in one call, growing the bond dimension during evolution, imaginary time, and evolving infinite states. +Speaking of which — everything here was done on a finite chain, but `timestep` works just as well on the `InfiniteMPS` states introduced in [The thermodynamic limit](@ref tutorial_thermodynamic_limit). +And for measuring more than a single local magnetization on the evolved states, see [Computing observables](@ref howto_observables). diff --git a/docs/src/tutorials/using_symmetries.md b/docs/src/tutorials/using_symmetries.md new file mode 100644 index 000000000..95716596b --- /dev/null +++ b/docs/src/tutorials/using_symmetries.md @@ -0,0 +1,169 @@ +# [Using symmetries](@id tutorial_using_symmetries) + +In [Your first ground state](@ref tutorial_first_groundstate) and [The thermodynamic limit](@ref tutorial_thermodynamic_limit) we treated the transverse-field Ising model (TFIM) as a generic spin chain. +But the TFIM is not generic: it has a symmetry, and in this tutorial we teach MPSKit about it. + +Recall the Hamiltonian, + +```math +H = -J\left(\sum_{\langle i,j\rangle} \sigma^z_i\,\sigma^z_j + g\sum_i \sigma^x_i\right), +``` + +and consider the *global spin flip* ``P = \prod_i \sigma^x_i``, which flips every spin at once. +Conjugating by ``P`` sends ``\sigma^z_i \to -\sigma^z_i``, so the interaction term ``\sigma^z_i\sigma^z_j`` picks up two minus signs and is unchanged, while the field term ``\sigma^x_i`` commutes with ``P`` trivially. +Hence ``H`` commutes with ``P``. +Since ``P^2 = 1``, this is a ``\mathbb{Z}_2`` symmetry, and every eigenstate of ``H`` can be labelled by a parity quantum number: *even* (``P = +1``) or *odd* (``P = -1``). + +MPSKit, through the TensorKit tensor backend, can bake this symmetry directly into the tensors of the MPS. +Doing so buys you two things. +First, the tensors become **block-sparse**: at the same total bond dimension the computer multiplies smaller dense blocks, which is faster. +Second, every state you compute carries an explicit **sector label**, so "the lowest odd-parity excitation" becomes something you can ask for directly. +This tutorial demonstrates both, by redoing the finite-chain TFIM calculation once without and once with the symmetry. + +!!! note "Why ``\\mathbb{Z}_2`` and not U(1)?" + ``\\mathbb{Z}_2`` is the symmetry the TFIM actually has, and it is what `MPSKitModels.transverse_field_ising` supports: its `symmetry` argument accepts `Trivial`, `Z2Irrep` or `FermionParity`, and anything else — `U1Irrep` included — throws an `ArgumentError`. + Larger groups pay off more, but need a model that has them: for U(1) see [Constructing states](@ref howto_states), and for SU(2) the Heisenberg pages in the [examples gallery](@ref examples_index). + +## Loading the packages + +Every code block on this page shares one Julia session, so we load the packages once. +`Z2Irrep` and `Z2Space`, the symmetry-aware building blocks used below, come from TensorKit. + +```@example using-symmetries +using MPSKit, MPSKitModels, TensorKit +``` + +## 1. Recap: the ground state without symmetry + +We start from the workflow of the first tutorial: a chain of `L = 16` sites, a random `FiniteMPS`, and a DMRG ground-state search. +Two small changes from before: we set the field to `g = 2.0`, and we use a total bond dimension of 16. + +Why `g = 2.0`? +This puts us deep in the paramagnetic phase, where the ground state respects the spin-flip symmetry. +That matters for what comes next: an MPS built from symmetric tensors lives in exactly one parity sector and *cannot* spontaneously break the symmetry, so a fair comparison needs a point where the true ground state is symmetric to begin with. + +```@example using-symmetries +L = 16 +H = transverse_field_ising(FiniteChain(L); g = 2.0) +ψ₀ = FiniteMPS(L, ℂ^2, ℂ^16) +ψ, envs, ϵ = find_groundstate(ψ₀, H, DMRG(; verbosity = 0)) +E = expectation_value(ψ, H) +``` + +This is our reference number: the ground-state energy computed with plain, symmetry-oblivious tensors. + +## 2. The same model, with the symmetry made explicit + +To exploit the symmetry we change two lines: the Hamiltonian and the initial state. + +For the Hamiltonian, we pass the symmetry as an extra first argument. +`transverse_field_ising(Z2Irrep, ...)` builds the *same* Hamiltonian as before, but out of tensors that manifestly commute with the spin flip: + +```@example using-symmetries +H_Z2 = transverse_field_ising(Z2Irrep, FiniteChain(L); g = 2.0) +``` + +For the state, the plain spaces `ℂ^2` and `ℂ^16` are replaced by *graded* spaces that keep track of parity: + +```@example using-symmetries +ψ₀_Z2 = FiniteMPS(L, Z2Space(0 => 1, 1 => 1), Z2Space(0 => 8, 1 => 8)) +``` + +The syntax reads as a list of `sector => dimension` pairs, where sector `0` is the even (``P = +1``) irrep of ``\mathbb{Z}_2`` and sector `1` is the odd (``P = -1``) one: + +- The physical space `Z2Space(0 => 1, 1 => 1)` is the familiar two-dimensional spin-1/2 site, now split into its symmetry content: one even state and one odd state. +- The virtual space `Z2Space(0 => 8, 1 => 8)` says the bond carries 8 states of even parity and 8 of odd parity — 16 in total, matching the `ℂ^16` of the plain run, so the two calculations have exactly the same variational power. + +From here the workflow is unchanged: + +```@example using-symmetries +ψ_Z2, envs_Z2, ϵ_Z2 = find_groundstate(ψ₀_Z2, H_Z2, DMRG(; verbosity = 0)) +E_Z2 = expectation_value(ψ_Z2, H_Z2) +``` + +Both runs found the same ground state, and the two energies agree to numerical precision: + +```@example using-symmetries +E, E_Z2 +``` + +!!! note "Same physics, different bookkeeping" + Nothing about the model changed — only the way its tensors are stored. + The symmetric calculation restricts the search to states of definite (here: even) parity, and stores only the tensor blocks the symmetry allows to be nonzero. + +## 3. The payoff, part 1: block-sparse tensors + +Where did the symmetry go? +Into the *structure* of the state. +Ask for the virtual space at the central bond and you no longer get an anonymous `ℂ^16`, but a space that knows its sector decomposition: + +```@example using-symmetries +V = left_virtualspace(ψ_Z2, L ÷ 2) +``` + +Its total dimension is still 16: + +```@example using-symmetries +dim(V) +``` + +The same sector labels show up in every quantity derived from the state. +The entanglement spectrum at the central cut, for instance, now comes back resolved by sector — compare [Entanglement entropy and spectrum](@ref howto_entanglement), where the same call on an unsymmetric state produced a single `Trivial()` block: + +```@example using-symmetries +spectrum = entanglement_spectrum(ψ_Z2, L ÷ 2) +collect(keys(spectrum)) +``` + +Iterating `pairs` gives each sector together with its singular values: + +```@example using-symmetries +collect(pairs(spectrum)) +``` + +This block structure is where the speedup comes from: instead of multiplying one dense 16-dimensional bond index, the computer multiplies two independent blocks of roughly half that size, and the forbidden matrix elements between the sectors are never stored or touched at all. +At bond dimension 16 the difference is negligible, but the saving grows with the bond dimension and with the size of the symmetry group. + +## 4. The payoff, part 2: sectors label the physics + +The sector labels are not just an implementation detail — they classify the eigenstates of ``H``, and MPSKit lets you target a sector directly. + +In the paramagnetic phase the lowest excitation of the TFIM is, roughly speaking, a single flipped spin. +Flipping one spin changes the parity of the state, so this excitation lives in the *odd* sector — a different sector than the (even) ground state. + +The [`excitations`](@ref) function computes excited states on top of a converged ground state; on a finite chain it takes the Hamiltonian, an algorithm, the ground state, and its environments, and returns energies measured *above* the ground state. +By default it searches the trivial (even) sector: + +```@example using-symmetries +Es_even, ϕs_even = excitations(H_Z2, QuasiparticleAnsatz(), ψ_Z2, envs_Z2; num = 1) +Es_even[1] +``` + +The `sector` keyword redirects the search to the odd sector: + +```@example using-symmetries +Es_odd, ϕs_odd = excitations( + H_Z2, QuasiparticleAnsatz(), ψ_Z2, envs_Z2; + num = 1, sector = Z2Irrep(1) +) +Es_odd[1] +``` + +The odd-sector excitation is indeed the lower one: + +```@example using-symmetries +Es_odd[1] < Es_even[1] +``` + +Without the symmetry built into the tensors, this question could not even be posed: the plain calculation of Section 1 has no notion of parity to select on. +More ways to use `excitations` — dispersion relations, other algorithms, infinite chains — are collected in [Excited states](@ref howto_excitations). + +## Where to go next + +You have run the flagship TFIM calculation with its ``\mathbb{Z}_2`` symmetry made explicit: the same physics at the same total bond dimension, but with block-sparse tensors and sector labels on everything the calculation produces. + +The same syntax scales up to larger symmetry groups, where the payoff grows. +For a U(1) symmetry (particle number, magnetization) the graded spaces list integer or half-integer charges instead of parities — worked constructions are in [Constructing states](@ref howto_states), Section 9. +For non-abelian symmetries such as SU(2) the gains are more dramatic still, because each symmetric block then represents an entire multiplet of states; the spin-1 Haldane chain and XXZ Heisenberg pages in the [examples gallery](@ref examples_index) show this in action. + +To continue the tutorial track, [Quasiparticle excitations](@ref tutorial_excitations) develops the excitation calculation of Section 4 into a full dispersion relation; the recipe collection for excited states is [Excited states](@ref howto_excitations), and the one for building symmetric states is [Constructing states](@ref howto_states).