Refactor mps.py: extract model-agnostic MPS class from WaveFunction - #203
Conversation
Co-authored-by: hzhangxyz <11623447+hzhangxyz@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR extracts model-agnostic MPS tensor contraction logic into a standalone MPS module and rewires WaveFunction to delegate contraction/sampling primitives to it, while keeping WaveFunction’s public (packed-config) interface unchanged.
Changes:
- Added
MPSclass with core contraction + autoregressive sampling primitives (amplitude, right envs, left vec updates). - Refactored
WaveFunctionto wrapself.mps: MPSand delegate contraction/sampling steps. - Simplified/centralized right-environment and AR sampling logic by reusing
MPShelpers.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| return v[:, 0] | ||
|
|
||
| @torch.jit.export | ||
| def build_right_envs(self) -> list[torch.Tensor]: |
There was a problem hiding this comment.
These methods are marked with @torch.jit.export, but the return annotations use PEP 585 built-in generics (list[...], tuple[...]). TorchScript compatibility has historically been stricter here; using typing.List / typing.Tuple (and, if needed, torch.jit.annotate) is more reliably scriptable. Consider switching the annotations to List[torch.Tensor] and Tuple[torch.Tensor, torch.Tensor] to avoid TorchScript compilation failures.
| return v | ||
|
|
||
| @torch.jit.export | ||
| def site_probs(self, v: torch.Tensor, site: int, right_env: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: |
There was a problem hiding this comment.
These methods are marked with @torch.jit.export, but the return annotations use PEP 585 built-in generics (list[...], tuple[...]). TorchScript compatibility has historically been stricter here; using typing.List / typing.Tuple (and, if needed, torch.jit.annotate) is more reliably scriptable. Consider switching the annotations to List[torch.Tensor] and Tuple[torch.Tensor, torch.Tensor] to avoid TorchScript compilation failures.
| # 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) |
There was a problem hiding this comment.
The API/docs describe real amplitudes/probabilities, but nothing enforces a real dtype for self.tensors. If self.tensors is ever complex, prob will be complex and .clamp(min=0.0) will raise at runtime. Either (a) explicitly restrict MPS tensors to real dtypes (and raise a clear error if complex), or (b) update the probability computation to a complex-safe nonnegative scalar (e.g., using a real scalar derived from the quadratic form) and adjust docs accordingly.
| # 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) |
| 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 |
There was a problem hiding this comment.
amplitude() duplicates the left-boundary initialization logic that already exists in init_left_vec(). Consider calling self.init_left_vec(batch_size) here so the boundary convention is defined in one place (and changes won’t risk diverging implementations).
| 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 | |
| v: torch.Tensor = self.init_left_vec(batch_size) | |
| 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: (batch_size,) integer indices for the physical state at site i |
| 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) |
There was a problem hiding this comment.
Casting configs[:, i] to int64 inside the per-site loop adds repeated overhead. Consider converting configs to torch.int64 once before the loop (or ensuring unpack_int already produces int64) and then indexing without per-iteration .to().
WaveFunctionconflated MPS tensor mechanics with the framework's packed-config interface, making the contraction logic impossible to reuse independently.Changes
New
MPSclassModel-agnostic core with only
sites,physical_dim,virtual_dimparameters. Exposes the standard MPS contraction primitives:amplitude(configs)(batch, sites)build_right_envs()init_left_vec(batch_size)e_L = [1, 0, …]site_probs(v, site, right_env)update_left_vec(v_new_all, s_i)Refactored
WaveFunctionNow wraps
self.mps: MPSand delegates all tensor contractions to it. Public interface is unchanged.Original prompt
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.