feat: add hamiltonians plugin for wavefunction operations - #227
Conversation
a1171f9 to
6e30193
Compare
There was a problem hiding this comment.
Pull request overview
Adds a new qmp.plugins.hamiltonians module providing a WaveFunction SHCI text I/O helper (2-bit packed configs) and a Hamiltonian wrapper around qmp.models.fcidump.Model for common wavefunction-space operations, plus an H2 example dataset/script.
Changes:
- Introduce
qmp/plugins/hamiltonians.pywith SHCI load/dump andapply_within/find_relative/list_relative/diagonal_termwrappers. - Export the new plugin module from
qmp/plugins/__init__.py. - Add an
examples/hamiltonians/H2 FCIDUMP + wavefunction inputs/outputs and a runnable example script.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| qmp/plugins/hamiltonians.py | New WaveFunction I/O + Hamiltonian wrapper for SHCI-format workflows. |
| qmp/plugins/init.py | Exposes hamiltonians (and pyscf) via package imports/__all__. |
| examples/hamiltonians/h2_example.py | Demonstrates loading SHCI wavefunction, Hamiltonian ops, and dumping results. |
| examples/hamiltonians/H2.FCIDUMP | Sample integrals for H2 demonstration. |
| examples/hamiltonians/H2.wavefunction | Sample input wavefunction for H2. |
| examples/hamiltonians/found.wavefunction | Example “found” configuration output. |
| examples/hamiltonians/rel.wavefunction | Example “relative configs” output. |
| examples/hamiltonians/result.wavefunction | Example apply_within output. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| config = [CHAR_TO_VAL[c] for c in parts[2:]] | ||
| config_list.append(config) |
There was a problem hiding this comment.
WaveFunction.load will raise a raw KeyError if the SHCI file contains an unexpected occupation token. Consider validating tokens and raising a ValueError that includes the offending token and line content/number to make debugging bad inputs easier.
| chars = [VAL_TO_CHAR[int(v)] for v in chars_vals] | ||
| # Tab-separated format: index, coefficient, config characters | ||
| line = f"{i}\t{coef.real.item():.8e}\t" + " ".join(chars) + "\n" | ||
| f.write(line) |
There was a problem hiding this comment.
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.
| psi = self.model.apply_within(source.config, source.psi, destination.config) | ||
| return WaveFunction(config=destination.config, psi=psi, orbit_number=destination.orbit_number) |
There was a problem hiding this comment.
apply_within() sets orbit_number from destination.orbit_number. If destination was created without orbit_number (None), the returned WaveFunction cannot be dumped/unpacked correctly. Consider validating that source/destination orbit_number match and falling back to source.orbit_number when destination.orbit_number is None.
| psi = self.model.apply_within(source.config, source.psi, destination.config) | |
| return WaveFunction(config=destination.config, psi=psi, orbit_number=destination.orbit_number) | |
| if ( | |
| source.orbit_number is not None | |
| and destination.orbit_number is not None | |
| and source.orbit_number != destination.orbit_number | |
| ): | |
| raise ValueError( | |
| "source and destination must have matching orbit_number values" | |
| ) | |
| orbit_number = ( | |
| destination.orbit_number | |
| if destination.orbit_number is not None | |
| else source.orbit_number | |
| ) | |
| psi = self.model.apply_within(source.config, source.psi, destination.config) | |
| return WaveFunction(config=destination.config, psi=psi, orbit_number=orbit_number) |
| """ | ||
| configs_exclude = exclude.config if exclude is not None else None | ||
| configs_j = self.model.find_relative(source.config, source.psi, count_selected, configs_exclude) | ||
| return WaveFunction(config=configs_j, psi=torch.ones([len(configs_j)], dtype=torch.complex128), orbit_number=source.orbit_number) |
There was a problem hiding this comment.
find_relative() creates psi as torch.ones(...) without specifying device, so if source.config is on CUDA the returned WaveFunction will have config on GPU and psi on CPU, which will break subsequent Hamiltonian operations unless the caller manually moves psi. Construct psi on configs_j.device (and typically matching source.psi.dtype) to keep tensors consistent.
| return WaveFunction(config=configs_j, psi=torch.ones([len(configs_j)], dtype=torch.complex128), orbit_number=source.orbit_number) | |
| psi_dtype = source.psi.dtype if source.psi is not None else torch.complex128 | |
| psi_j = torch.ones([len(configs_j)], dtype=psi_dtype, device=configs_j.device) | |
| return WaveFunction(config=configs_j, psi=psi_j, orbit_number=source.orbit_number) |
| result = hamiltonian.apply_within(wave.to("cuda"), found.to("cuda")) | ||
| result.dump(data_dir / "result.wavefunction") |
There was a problem hiding this comment.
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.
| # Infer orbital count from first configuration | ||
| self.orbit_number = len(config_list[0]) if config_list else 0 |
There was a problem hiding this comment.
If the input file has no valid configuration lines, orbit_number becomes 0 and subsequent packing will produce an incorrectly shaped config tensor (pack_int on an empty 1D tensor yields 1D output, not the documented 2D (batch, packed_bits)). Consider raising an explicit error for empty inputs (or initializing config as an empty 2D uint8 tensor).
| # 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]) |
|
|
||
| # Unpack to recover 2-bit values per orbital | ||
| config_vals = unpack_int(self.config, size=2, last_dim=self.orbit_number) | ||
|
|
There was a problem hiding this comment.
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" | |
| ) |
| Wavefunction with found configurations. psi is None since only | ||
| configs are computed, not amplitudes. |
There was a problem hiding this comment.
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. |
efed936 to
5da5d4b
Compare
Add qmp.plugins.hamiltonians subpackage with WaveFunction and Hamiltonian classes for quantum chemistry calculations. WaveFunction handles SHCI-format text file I/O with packed configurations (2-bit encoding). Hamiltonian wraps qmp's Model class for operations like apply_within, find_relative, and diagonal_term. Example included for H2 molecule.
5da5d4b to
bcb0cb8
Compare
Add qmp.plugins.hamiltonians subpackage with WaveFunction and Hamiltonian classes for quantum chemistry calculations. WaveFunction handles SHCI-format text file I/O with packed configurations (2-bit encoding). Hamiltonian wraps qmp's Model class for operations like apply_within, find_relative, and diagonal_term. Example included for H2 molecule.