-
Notifications
You must be signed in to change notification settings - Fork 1
Refactor mps.py: extract model-agnostic MPS class from WaveFunction #203
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -6,9 +6,9 @@ | |||||||||||||||
| from ..utility.bitspack import pack_int, unpack_int | ||||||||||||||||
|
|
||||||||||||||||
|
|
||||||||||||||||
| class WaveFunction(torch.nn.Module): | ||||||||||||||||
| class MPS(torch.nn.Module): | ||||||||||||||||
| """ | ||||||||||||||||
| A trivial Matrix Product State (MPS) wave function. | ||||||||||||||||
| Model-agnostic Matrix Product State (MPS) core. | ||||||||||||||||
|
|
||||||||||||||||
| The MPS tensors have shape (physical_dim, virtual_dim, virtual_dim) for each site, | ||||||||||||||||
| with open boundary conditions: the left boundary vector is e_0 = [1, 0, ..., 0] | ||||||||||||||||
|
|
@@ -17,14 +17,23 @@ class WaveFunction(torch.nn.Module): | |||||||||||||||
| The amplitude is computed as: | ||||||||||||||||
| Ψ(s) = e_L^T @ A[0][s_0] @ A[1][s_1] @ ... @ A[L-1][s_{L-1}] @ e_R | ||||||||||||||||
| where e_L = e_R = [1, 0, ..., 0] (first standard basis vector). | ||||||||||||||||
|
|
||||||||||||||||
| Parameters | ||||||||||||||||
| ---------- | ||||||||||||||||
| sites : int | ||||||||||||||||
| Number of sites in the MPS. | ||||||||||||||||
| physical_dim : int | ||||||||||||||||
| Dimension of the physical (local) Hilbert space at each site. | ||||||||||||||||
| virtual_dim : int | ||||||||||||||||
| Bond dimension (virtual dimension) of the MPS. | ||||||||||||||||
| """ | ||||||||||||||||
|
|
||||||||||||||||
| def __init__( | ||||||||||||||||
| self, | ||||||||||||||||
| *, | ||||||||||||||||
| sites: int, # Number of sites in the MPS | ||||||||||||||||
| physical_dim: int, # Dimension of the physical (local) Hilbert space at each site | ||||||||||||||||
| virtual_dim: int, # Bond dimension (virtual dimension) of the MPS | ||||||||||||||||
| sites: int, | ||||||||||||||||
| physical_dim: int, | ||||||||||||||||
| virtual_dim: int, | ||||||||||||||||
| ) -> None: | ||||||||||||||||
| super().__init__() | ||||||||||||||||
| self.sites: int = sites | ||||||||||||||||
|
|
@@ -38,6 +47,192 @@ def __init__( | |||||||||||||||
| torch.randn(sites, physical_dim, virtual_dim, virtual_dim) | ||||||||||||||||
| ) | ||||||||||||||||
|
|
||||||||||||||||
| @torch.jit.export | ||||||||||||||||
| def amplitude(self, configs: torch.Tensor) -> torch.Tensor: | ||||||||||||||||
| """ | ||||||||||||||||
| Compute MPS amplitudes for a batch of configurations. | ||||||||||||||||
|
|
||||||||||||||||
| Parameters | ||||||||||||||||
| ---------- | ||||||||||||||||
| configs : torch.Tensor | ||||||||||||||||
| Integer configurations of shape (batch_size, sites) with values in [0, physical_dim). | ||||||||||||||||
|
|
||||||||||||||||
| Returns | ||||||||||||||||
| ------- | ||||||||||||||||
| torch.Tensor | ||||||||||||||||
| Real amplitudes of shape (batch_size,), using the same dtype as the MPS tensors. | ||||||||||||||||
| """ | ||||||||||||||||
| batch_size: int = configs.shape[0] | ||||||||||||||||
| tensors: torch.Tensor = self.tensors | ||||||||||||||||
|
|
||||||||||||||||
| # v: (batch_size, virtual_dim) -- left environment vector, starting from e_L = [1, 0, ..., 0] | ||||||||||||||||
| v: torch.Tensor = torch.zeros([batch_size, self.virtual_dim], device=tensors.device, dtype=tensors.dtype) | ||||||||||||||||
| v[:, 0] = 1.0 | ||||||||||||||||
|
|
||||||||||||||||
| for i in range(self.sites): | ||||||||||||||||
| # A_i: (physical_dim, virtual_dim, virtual_dim) | ||||||||||||||||
| A_i: torch.Tensor = tensors[i] | ||||||||||||||||
| # s_i: (batch_size,) integer indices for the physical state at site i | ||||||||||||||||
| s_i: torch.Tensor = configs[:, i].to(dtype=torch.int64) | ||||||||||||||||
|
Comment on lines
+72
to
+76
|
||||||||||||||||
| # A_s_i: (batch_size, virtual_dim, virtual_dim) | ||||||||||||||||
| A_s_i: torch.Tensor = A_i[s_i] | ||||||||||||||||
| # v = v @ A_s_i: (batch_size, virtual_dim) | ||||||||||||||||
| v = torch.bmm(v.unsqueeze(1), A_s_i).squeeze(1) | ||||||||||||||||
|
|
||||||||||||||||
| # Project onto right boundary e_R = [1, 0, ..., 0]: extract the first element | ||||||||||||||||
| return v[:, 0] | ||||||||||||||||
|
|
||||||||||||||||
| @torch.jit.export | ||||||||||||||||
| def build_right_envs(self) -> list[torch.Tensor]: | ||||||||||||||||
|
||||||||||||||||
| """ | ||||||||||||||||
| Pre-compute right-environment density matrices from right to left. | ||||||||||||||||
|
|
||||||||||||||||
| Used as auxiliary variables for autoregressive sampling. Define M[i] as the | ||||||||||||||||
| right-environment density matrix that captures all sites from ``i`` to the right | ||||||||||||||||
| boundary (exclusive): | ||||||||||||||||
|
|
||||||||||||||||
| - M[sites] = e_R @ e_R^T (right-boundary density matrix, only [0,0] entry is 1) | ||||||||||||||||
| - M[i] = sum_s A[i][s] @ M[i+1] @ A[i][s]^T for i = sites-1, ..., 1 | ||||||||||||||||
|
|
||||||||||||||||
| The returned list has length ``sites``; entry ``renv[k] = M[sites - k]``: | ||||||||||||||||
|
|
||||||||||||||||
| - ``renv[0]`` = M[sites] (right boundary; used when sampling the last site) | ||||||||||||||||
| - ``renv[k]`` = M[sites - k] (right env to the right of site sites-k-1) | ||||||||||||||||
| - ``renv[sites-1]`` = M[1] (right env to the right of site 0) | ||||||||||||||||
|
|
||||||||||||||||
| Note: M[0] (looking right from before site 0) is never needed and is not computed. | ||||||||||||||||
|
|
||||||||||||||||
| When sampling at site ``i``, pass ``renv[sites - i - 1]`` as the right environment. | ||||||||||||||||
|
|
||||||||||||||||
| Returns | ||||||||||||||||
| ------- | ||||||||||||||||
| list[torch.Tensor] | ||||||||||||||||
| List of ``sites`` matrices, each of shape (virtual_dim, virtual_dim). | ||||||||||||||||
| """ | ||||||||||||||||
| tensors: torch.Tensor = self.tensors | ||||||||||||||||
| device: torch.device = tensors.device | ||||||||||||||||
| dtype: torch.dtype = tensors.dtype | ||||||||||||||||
|
|
||||||||||||||||
| renv: list[torch.Tensor] = [] | ||||||||||||||||
| M: torch.Tensor = torch.zeros([self.virtual_dim, self.virtual_dim], device=device, dtype=dtype) | ||||||||||||||||
| M[0, 0] = 1.0 # e_R @ e_R^T (only the [0,0] entry is non-zero) | ||||||||||||||||
| renv.append(M) | ||||||||||||||||
|
|
||||||||||||||||
| for i in range(self.sites - 1, 0, -1): | ||||||||||||||||
| A_i: torch.Tensor = tensors[i] # (physical_dim, virtual_dim, virtual_dim) | ||||||||||||||||
| # M_new[a,d] = sum_{s,b,c} A_i[s,a,b] * M[b,c] * A_i[s,d,c] | ||||||||||||||||
| # = sum_s (A_i[s] @ M @ A_i[s]^T)[a,d] | ||||||||||||||||
| M = torch.einsum("sab,bc,sdc->ad", A_i, M, A_i) | ||||||||||||||||
| renv.append(M) | ||||||||||||||||
|
|
||||||||||||||||
| # After the loop renv has length `sites`: | ||||||||||||||||
| # renv[0] = M[sites], renv[1] = M[sites-1], ..., renv[sites-1] = M[1] | ||||||||||||||||
| return renv | ||||||||||||||||
|
|
||||||||||||||||
| @torch.jit.export | ||||||||||||||||
| def init_left_vec(self, batch_size: int) -> torch.Tensor: | ||||||||||||||||
| """ | ||||||||||||||||
| Initialize the left boundary vector for a batch. | ||||||||||||||||
|
|
||||||||||||||||
| Parameters | ||||||||||||||||
| ---------- | ||||||||||||||||
| batch_size : int | ||||||||||||||||
| Number of samples in the batch. | ||||||||||||||||
|
|
||||||||||||||||
| Returns | ||||||||||||||||
| ------- | ||||||||||||||||
| torch.Tensor | ||||||||||||||||
| Left boundary vector of shape (batch_size, virtual_dim) with | ||||||||||||||||
| v[:, 0] = 1 and all other entries 0 (e_L = [1, 0, ..., 0]). | ||||||||||||||||
| """ | ||||||||||||||||
| tensors: torch.Tensor = self.tensors | ||||||||||||||||
| v: torch.Tensor = torch.zeros([batch_size, self.virtual_dim], device=tensors.device, dtype=tensors.dtype) | ||||||||||||||||
| v[:, 0] = 1.0 | ||||||||||||||||
| return v | ||||||||||||||||
|
|
||||||||||||||||
| @torch.jit.export | ||||||||||||||||
| def site_probs(self, v: torch.Tensor, site: int, right_env: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: | ||||||||||||||||
|
||||||||||||||||
| """ | ||||||||||||||||
| Compute unnormalized conditional probabilities at a site for autoregressive sampling. | ||||||||||||||||
|
|
||||||||||||||||
| Parameters | ||||||||||||||||
| ---------- | ||||||||||||||||
| v : torch.Tensor | ||||||||||||||||
| Current left boundary vector of shape (batch_size, virtual_dim). | ||||||||||||||||
| site : int | ||||||||||||||||
| The site index to compute probabilities for. | ||||||||||||||||
| right_env : torch.Tensor | ||||||||||||||||
| Right-environment density matrix of shape (virtual_dim, virtual_dim) for the | ||||||||||||||||
| sites to the right of ``site``. | ||||||||||||||||
|
|
||||||||||||||||
| Returns | ||||||||||||||||
| ------- | ||||||||||||||||
| prob : torch.Tensor | ||||||||||||||||
| Unnormalized probabilities of shape (batch_size, physical_dim), clamped to [0, ∞). | ||||||||||||||||
| v_new_all : torch.Tensor | ||||||||||||||||
| Candidate left vectors of shape (batch_size, physical_dim, virtual_dim), | ||||||||||||||||
| one for each possible physical state. | ||||||||||||||||
| """ | ||||||||||||||||
| A_i: torch.Tensor = self.tensors[site] # (physical_dim, virtual_dim, virtual_dim) | ||||||||||||||||
|
|
||||||||||||||||
| # v_new_all[b, s, j] = sum_k v[b, k] * A_i[s, k, j] (batch, phys, virt) | ||||||||||||||||
| v_new_all: torch.Tensor = torch.einsum("bi,sij->bsj", v, A_i) | ||||||||||||||||
|
|
||||||||||||||||
| # prob[b, s] = v_new_all[b, s] @ right_env @ v_new_all[b, s]^T (non-negative) | ||||||||||||||||
| Mv: torch.Tensor = torch.einsum("jk,bsk->bsj", right_env, v_new_all) | ||||||||||||||||
| prob: torch.Tensor = (v_new_all * Mv).sum(dim=-1).clamp(min=0.0) | ||||||||||||||||
|
Comment on lines
+181
to
+183
|
||||||||||||||||
| # prob[b, s] = v_new_all[b, s] @ right_env @ v_new_all[b, s]^T (non-negative) | |
| Mv: torch.Tensor = torch.einsum("jk,bsk->bsj", right_env, v_new_all) | |
| prob: torch.Tensor = (v_new_all * Mv).sum(dim=-1).clamp(min=0.0) | |
| # prob[b, s] = v_new_all[b, s]^† @ right_env @ v_new_all[b, s] (non-negative real scalar) | |
| Mv: torch.Tensor = torch.einsum("jk,bsk->bsj", right_env, v_new_all) | |
| prob_raw: torch.Tensor = (v_new_all.conj() * Mv).sum(dim=-1) | |
| prob: torch.Tensor = prob_raw.real.clamp(min=0.0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
amplitude()duplicates the left-boundary initialization logic that already exists ininit_left_vec(). Consider callingself.init_left_vec(batch_size)here so the boundary convention is defined in one place (and changes won’t risk diverging implementations).