-
Notifications
You must be signed in to change notification settings - Fork 1
feat: add hamiltonians plugin for wavefunction operations #227
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 |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| &FCI NORB=2,NELEC=2,MS2=0, | ||
| ORB_SYMM=1,1, | ||
| ISYM=1 | ||
| &END | ||
| 0.675710 1 1 1 1 | ||
| 0.665710 2 2 2 2 | ||
| 0.180123 1 1 2 2 | ||
| 0.180123 2 2 1 1 | ||
| -1.252477 1 1 0 0 | ||
| -0.473522 2 2 0 0 | ||
| -0.300000 1 2 0 0 | ||
| -0.300000 2 1 0 0 | ||
| 0.050000 1 2 2 1 | ||
| 0.050000 2 1 1 2 | ||
| 0.713753 0 0 0 0 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| 0 2.3333 a b | ||
| 0 6.6666 b a |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| 0 1.00000000e+00 2 0 | ||
| 1 1.00000000e+00 0 2 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| from pathlib import Path | ||
| from qmp.plugins.hamiltonians import WaveFunction, Hamiltonian | ||
|
|
||
|
|
||
| data_dir = Path(__file__).parent | ||
| hamiltonian = Hamiltonian(data_dir / "H2.FCIDUMP") | ||
| wave = WaveFunction().load(data_dir / "H2.wavefunction") | ||
| assert wave.config is not None and wave.psi is not None | ||
|
|
||
| print("Loaded wavefunction:") | ||
| print(f" orbit_number: {wave.orbit_number}") | ||
| print(f" config shape: {wave.config.shape}") | ||
| print(f" psi: {wave.psi}") | ||
|
|
||
| diag = hamiltonian.diagonal_term(wave) | ||
| print(f"\nDiagonal terms: {diag.psi}") | ||
|
|
||
| rel = hamiltonian.list_relative(wave) | ||
| rel.dump(data_dir / "rel.wavefunction") | ||
| print("\nList relative (configs connected by Hamiltonian):") | ||
| print(f" config : {rel.config}") | ||
|
|
||
| found = hamiltonian.find_relative(wave, count_selected=10) | ||
| found.dump(data_dir / "found.wavefunction") | ||
| print("\nFind relative:") | ||
| print(f" config : {found.config}") | ||
|
|
||
| result = hamiltonian.apply_within(wave.to("cuda"), found.to("cuda")) | ||
| result.dump(data_dir / "result.wavefunction") | ||
| print("\nApply within (H|wave⟩ onto found configs):") | ||
| print(f" result psi: {result.psi}") | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| 0 1.29999000e+00 2 0 | ||
| 1 1.29999000e+00 0 2 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| 0 1.29999000e+00 2 0 | ||
| 1 1.29999000e+00 0 2 |
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,308 @@ | ||||||||||||||||
| """ | ||||||||||||||||
| Hamiltonian utilities for quantum many-body calculations. | ||||||||||||||||
|
|
||||||||||||||||
| This module provides high-level wrappers around qmp's Model and Hamiltonian | ||||||||||||||||
| classes for working with wavefunctions stored in SHCI text format. | ||||||||||||||||
| """ | ||||||||||||||||
|
|
||||||||||||||||
| from pathlib import Path | ||||||||||||||||
| import torch | ||||||||||||||||
| from qmp.models.fcidump import ModelConfig, Model | ||||||||||||||||
| from qmp.utility.bitspack import pack_int, unpack_int | ||||||||||||||||
|
|
||||||||||||||||
| __all__ = ["WaveFunction", "Hamiltonian"] | ||||||||||||||||
|
|
||||||||||||||||
| # Character to value mapping for 2-bit encoding | ||||||||||||||||
| # 0 -> 00 (empty), a -> 01 (spin down), b -> 10 (spin up), 2 -> 11 (double occupied) | ||||||||||||||||
| CHAR_TO_VAL = {"0": 0, "a": 1, "b": 2, "2": 3} | ||||||||||||||||
| VAL_TO_CHAR = {0: "0", 1: "a", 2: "b", 3: "2"} | ||||||||||||||||
|
|
||||||||||||||||
|
|
||||||||||||||||
| class WaveFunction: | ||||||||||||||||
| """ | ||||||||||||||||
| Wavefunction representation with packed configurations and amplitudes. | ||||||||||||||||
|
|
||||||||||||||||
| Attributes | ||||||||||||||||
| ---------- | ||||||||||||||||
| config : torch.Tensor | ||||||||||||||||
| Packed 2D tensor of shape (batch, packed_bits) containing configurations. | ||||||||||||||||
| Each configuration is packed using bitspack with 2 bits per orbital. | ||||||||||||||||
| psi : torch.Tensor | ||||||||||||||||
| 1D complex tensor of shape (batch) containing wavefunction amplitudes. | ||||||||||||||||
| orbit_number : int | ||||||||||||||||
| Number of orbitals (determines the unpacking dimension). | ||||||||||||||||
| """ | ||||||||||||||||
|
|
||||||||||||||||
| def __init__( | ||||||||||||||||
| self, | ||||||||||||||||
| config: torch.Tensor | None = None, | ||||||||||||||||
| psi: torch.Tensor | None = None, | ||||||||||||||||
| orbit_number: int | None = None, | ||||||||||||||||
| ) -> None: | ||||||||||||||||
| """ | ||||||||||||||||
| Initialize a WaveFunction with optional pre-existing data. | ||||||||||||||||
|
|
||||||||||||||||
| Parameters | ||||||||||||||||
| ---------- | ||||||||||||||||
| config : torch.Tensor, optional | ||||||||||||||||
| Packed configuration tensor. | ||||||||||||||||
| psi : torch.Tensor, optional | ||||||||||||||||
| Amplitude tensor. | ||||||||||||||||
| orbit_number : int, optional | ||||||||||||||||
| Number of orbitals. | ||||||||||||||||
| """ | ||||||||||||||||
| self.config = config | ||||||||||||||||
| self.psi = psi | ||||||||||||||||
| self.orbit_number = orbit_number | ||||||||||||||||
|
|
||||||||||||||||
| def to(self, device: str | torch.device) -> "WaveFunction": | ||||||||||||||||
| """ | ||||||||||||||||
| Move tensors to specified device. | ||||||||||||||||
|
|
||||||||||||||||
| Parameters | ||||||||||||||||
| ---------- | ||||||||||||||||
| device : str | torch.device | ||||||||||||||||
| Target device (e.g., "cuda", "cpu", torch.device("cuda:0")). | ||||||||||||||||
|
|
||||||||||||||||
| Returns | ||||||||||||||||
| ------- | ||||||||||||||||
| WaveFunction | ||||||||||||||||
| Self for method chaining. | ||||||||||||||||
| """ | ||||||||||||||||
| if self.config is not None: | ||||||||||||||||
| self.config = self.config.to(device) | ||||||||||||||||
| if self.psi is not None: | ||||||||||||||||
| self.psi = self.psi.to(device) | ||||||||||||||||
| return self | ||||||||||||||||
|
|
||||||||||||||||
| def load(self, path: str | Path) -> "WaveFunction": | ||||||||||||||||
| """ | ||||||||||||||||
| Load wavefunction from SHCI-format text file. | ||||||||||||||||
|
|
||||||||||||||||
| File format is space/tab-separated text: | ||||||||||||||||
| - Column 1: index (ignored) | ||||||||||||||||
| - Column 2: coefficient (complex) | ||||||||||||||||
| - Columns 3+: config characters ('0', 'a', 'b', '2') | ||||||||||||||||
|
|
||||||||||||||||
| Character encoding (2 bits per orbital): | ||||||||||||||||
| '0' -> 00 (empty orbital) | ||||||||||||||||
| 'a' -> 01 (one electron, spin down) | ||||||||||||||||
| 'b' -> 10 (one electron, spin up) | ||||||||||||||||
| '2' -> 11 (double occupied) | ||||||||||||||||
|
|
||||||||||||||||
| Configurations are packed using bitspack with size=2 for efficient storage. | ||||||||||||||||
|
|
||||||||||||||||
| Parameters | ||||||||||||||||
| ---------- | ||||||||||||||||
| path : str | Path | ||||||||||||||||
| Path to the SHCI text file. | ||||||||||||||||
|
|
||||||||||||||||
| Returns | ||||||||||||||||
| ------- | ||||||||||||||||
| WaveFunction | ||||||||||||||||
| Self for method chaining. | ||||||||||||||||
| """ | ||||||||||||||||
| path = Path(path) | ||||||||||||||||
| psi_list = [] | ||||||||||||||||
| config_list = [] | ||||||||||||||||
|
|
||||||||||||||||
| with open(path, "r") as f: | ||||||||||||||||
| for line in f: | ||||||||||||||||
| parts = line.strip().split() | ||||||||||||||||
| if len(parts) < 3: | ||||||||||||||||
| continue | ||||||||||||||||
|
|
||||||||||||||||
| # Column 0: index (ignored), Column 1: coefficient | ||||||||||||||||
| psi_list.append(complex(parts[1])) | ||||||||||||||||
|
|
||||||||||||||||
| # Columns 2+: configuration characters | ||||||||||||||||
| config = [CHAR_TO_VAL[c] for c in parts[2:]] | ||||||||||||||||
| config_list.append(config) | ||||||||||||||||
|
Comment on lines
+119
to
+120
|
||||||||||||||||
|
|
||||||||||||||||
| # Infer orbital count from first configuration | ||||||||||||||||
| self.orbit_number = len(config_list[0]) if config_list else 0 | ||||||||||||||||
|
Comment on lines
+122
to
+123
|
||||||||||||||||
| # Infer orbital count from first configuration | |
| self.orbit_number = len(config_list[0]) if config_list else 0 | |
| if not config_list: | |
| raise ValueError(f"No valid configuration lines found in wavefunction file: {path}") | |
| # Infer orbital count from first configuration | |
| self.orbit_number = len(config_list[0]) |
Copilot
AI
Apr 10, 2026
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.
dump() uses zip(self.psi, config_vals), which will silently truncate output if psi and config batch sizes differ. Add an explicit check that self.psi.shape[0] matches config_vals.shape[0] and raise a clear error if not.
| if self.psi.shape[0] != config_vals.shape[0]: | |
| raise ValueError( | |
| f"Mismatched batch sizes: psi has {self.psi.shape[0]} entries, " | |
| f"but config has {config_vals.shape[0]} entries" | |
| ) |
Copilot
AI
Apr 10, 2026
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.
dump() writes only coef.real, discarding any imaginary component of psi. If complex amplitudes are expected to round-trip through this format, the imaginary part should be serialized too; otherwise, it would be safer to assert imag is ~0 and raise if non-zero to prevent silent data loss.
Copilot
AI
Apr 10, 2026
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.
Docstring says the returned WaveFunction has psi=None, but the implementation sets psi to a ones tensor. Please align the docstring with the actual behavior (or change the return to psi=None and adjust dump()/callers accordingly) to avoid confusing API consumers.
| Wavefunction with found configurations. psi is None since only | |
| configs are computed, not amplitudes. | |
| Wavefunction with found configurations and a placeholder psi | |
| tensor of ones. Only configs are computed here; amplitudes are | |
| not evaluated by this method. |
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.
This example unconditionally moves tensors to CUDA (wave.to("cuda"), found.to("cuda")), which will raise on CPU-only machines. Consider selecting the device based on torch.cuda.is_available() (or keeping the example CPU-only) so it runs in more environments.