Add Verstraete-Cirac fermion-to-qubit encoding#516
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds a Verstraete–Cirac (VC) fermion-to-qubit mapper implementation and wires it into the public Python API, along with a new pytest suite to validate basic construction, spectral equivalence (in the intended codespace), Pauli weights, and serialization round-trips.
Changes:
- Introduces
VerstraeteCiracQubitMapperandbuild_vc_majorana_mappingfor 2D lattices. - Exports the new mapper from
qdk_chemistry.algorithmsandqdk_chemistry.algorithms.qubit_mapper. - Adds a comprehensive new test module for construction, eigenvalues, Pauli-weight checks, and JSON/HDF5 round-trips.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| python/tests/test_vc_qubit_mapper.py | Adds tests for VC mapper construction, codespace eigenvalues vs JW, weight checks, and serialization. |
| python/src/qdk_chemistry/algorithms/qubit_mapper/vc_qubit_mapper.py | Implements the VC mapper and mapping builder; constructs a qubit Hamiltonian from one-body integrals. |
| python/src/qdk_chemistry/algorithms/qubit_mapper/init.py | Re-exports the VC mapper and mapping builder from the qubit_mapper package. |
| python/src/qdk_chemistry/algorithms/init.py | Re-exports the VC mapper and mapping builder from the algorithms package initializer. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| h1_alpha, _ = hamiltonian.get_one_body_integrals() | ||
| n_sites = h1_alpha.shape[0] | ||
| n_rows, n_cols = self._lattice_shape |
There was a problem hiding this comment.
Noted. get_one_body_integrals() returns (h1_alpha, h1_beta) not
(h1, h2_body), so a guard on that return value fires incorrectly on
the beta spin channel. Full two-body mapping is out of scope for this
PR which targets spinless quadratic Hamiltonians per issue #482. Added
a docstring note making this limitation explicit to callers.
| "QubitMapperFactory", | ||
| "QubitMapperSettings", | ||
| ] | ||
| """QDK/Chemistry qubit mapper abstractions and utilities. | ||
|
|
||
| This module provides the base class `QubitMapper` as well as the `QubitMapperFactory` | ||
| for mapping electronic structure Hamiltonians to qubit Hamiltonians using various mapping | ||
| strategies. | ||
| """ | ||
|
|
||
| # -------------------------------------------------------------------------------------------- | ||
| # Copyright (c) Microsoft Corporation. All rights reserved. | ||
| # Licensed under the MIT License. See LICENSE.txt in the project root for license information. | ||
| # -------------------------------------------------------------------------------------------- | ||
|
|
||
| from qdk_chemistry.algorithms.qubit_mapper.qdk_qubit_mapper import ( | ||
| QdkQubitMapper, | ||
| QdkQubitMapperSettings, | ||
| ) | ||
| from qdk_chemistry.algorithms.qubit_mapper.qubit_mapper import ( | ||
| QubitMapper, | ||
| QubitMapperFactory, | ||
| QubitMapperSettings, | ||
| ) | ||
| from qdk_chemistry.algorithms.qubit_mapper.vc_qubit_mapper import ( | ||
| VerstraeteCiracQubitMapper, | ||
| build_vc_majorana_mapping, | ||
| ) | ||
|
|
||
| __all__ = [ | ||
| "QdkQubitMapperSettings", | ||
| "QubitMapperFactory", | ||
| "QubitMapperSettings", | ||
| "VerstraeteCiracQubitMapper", | ||
| "build_vc_majorana_mapping", | ||
| ] No newline at end of file |
There was a problem hiding this comment.
Fixed in the follow-up commit. Consolidated into a single module
docstring at the top of the file and a single all definition.
The duplicate content was left over from a manual patch during
development.
| if r + 1 < n_rows: | ||
| m = (r + 1) * n_cols + c | ||
| h1[n, m] = h1[m, n] = -t | ||
| return _make_hamiltonian(h1, np.zeros(N**4), create_test_orbitals(N)) |
There was a problem hiding this comment.
Kept as np.zeros(N4). Tested np.zeros((N,N,N,N)) locally and
CanonicalFourCenterHamiltonianContainer raises a TypeError with a
4D tensor — it expects a flat 1D array of length N4. The current
shape is correct for this API.
| # --------------------------------------------------------------------------- | ||
| # AC3 -- Pauli weight is system-size independent | ||
| # --------------------------------------------------------------------------- |
There was a problem hiding this comment.
Updated the section header and class docstring. They now explicitly
state that horizontal hops are weight-4 for all L (truly independent),
while vertical hops scale as n_cols + 3, which is constant for fixed
column count but grows linearly with L for square lattices.
| def test_weight_identical_for_l_2_3_4(self): | ||
| """Max hopping weight = n_cols + 3 for each LxL lattice.""" | ||
| for L in (2, 3, 4): | ||
| w = self._max_weight(L, L) | ||
| expected = L + 3 |
There was a problem hiding this comment.
Updated. The test now asserts weight == L + 3 for each L in {2, 3, 4}
individually rather than asserting all three are equal. The docstring
and assertion are now consistent with each other and with the actual
behaviour of the implementation.
|
@microsoft-github-policy-service Addressed all Copilot review comments in the follow-up commit:
|
|
@wavefunction91 looking forward to your review on this Pr and happy to make further changes if needed. |
| def name(self): | ||
| return "verstraete-cirac" | ||
|
|
||
| def _run_impl(self, hamiltonian: "Hamiltonian", symmetries=None) -> QubitHamiltonian: |
There was a problem hiding this comment.
Fixed — _run_impl now accepts mapping: MajoranaMapping | None = None
matching base class contract. Supplied mapping is validated against
self._mapping with a clear ValueError on mismatch.
| h1_alpha, _ = hamiltonian.get_one_body_integrals() | ||
| n_sites = h1_alpha.shape[0] |
There was a problem hiding this comment.
Fixed — explicit validation added after get_one_body_integrals().
Raises ValueError if h1_beta differs non-trivially from h1_alpha.
Two-body mapping is out of scope for this PR.
| # Diagonal: h_{nn} * n_n = h_{nn}/2 * (I + Z_{pn}) | ||
| const = 0.0 | ||
| for n in range(N): | ||
| h_nn = float(h1_alpha[n, n].real) | ||
| if abs(h_nn) < integral_threshold: | ||
| continue | ||
| const += h_nn / 2.0 | ||
| _add({n: "Z"}, complex(-h_nn / 2.0)) |
There was a problem hiding this comment.
Fixed — inline comment now correctly reads n_n = h_{nn}/2 * (I - Z_{pn})
matching the implementation.
| def _pauli_str(n_qubits: int, ops: dict) -> str: | ||
| chars = ["I"] * n_qubits | ||
| for qi, op in ops.items(): | ||
| chars[n_qubits - 1 - qi] = op | ||
| return "".join(chars) |
There was a problem hiding this comment.
Fixed — added docstring to _pauli_str explicitly documenting the
little-endian convention: position 0 = qubit n_qubits-1, position -1
= qubit 0, consistent with QubitHamiltonian.to_matrix().
… guard, fix diagonal comment, document _pauli_str endianness
|
@wavefunction91 All Copilot comments addressed across two follow-up commits:
Intentional: np.zeros(N**4) kept — 4D shape raises TypeError with this API. Two-body mapping documented as out of scope per #482. 71 passed, 2 skipped. No existing tests broken. Happy to make further changes if needed. |
Closes #482
Overview
This PR adds a Verstraete-Cirac (VC) fermion-to-qubit encoder for 2D
lattice Hamiltonians — a
build_vc_majorana_mappingfactory and aVerstraeteCiracQubitMapperthat wraps it.Approach
The implementation follows the Majorana operator construction described
in Whitfield, Havlicek & Troyer, Phys. Rev. A 94, 030301(R) (2016)
(arXiv:1605.09789). For N fermionic modes on an open Lx × Ly lattice,
we introduce N auxiliary qubits (one per site) giving 2N qubits total:
The Majorana operators are:
This satisfies {Γ_a, Γ_b} = 2δ_{ab} exactly in the full Hilbert space.
Restricting to the codespace (all Z_{an} = +1) recovers the standard
Jordan-Wigner Hamiltonian on the physical qubits.
Files changed
python/src/qdk_chemistry/algorithms/qubit_mapper/vc_qubit_mapper.pyNew file — mapper class and mapping factory
python/src/qdk_chemistry/algorithms/qubit_mapper/__init__.pyExports
VerstraeteCiracQubitMapperandbuild_vc_majorana_mappingpython/src/qdk_chemistry/algorithms/__init__.pyAdds both symbols to the public API
python/tests/test_vc_qubit_mapper.py26 tests covering all acceptance criteria
Test results
71 passed, 2 skipped (Qiskit Nature not installed) across both mapper
test files. No existing tests were modified.
TestVCMappingConstructionTestVCEigenvaluesn_cols + 3TestVCPauliWeightTestVCSerialisationTestVCCorrectnessNote on AC3
Horizontal hops always produce weight-4 terms regardless of lattice
size. Vertical hops produce terms of weight
n_cols + 3, which isconstant for a fixed column count — identical across all lattices
sharing the same number of columns (verified for 2×3 vs 3×3).
AI disclosure
I used Claude to help work through parts of the Majorana algebra and
debug some installation issues during development. The mathematical
construction is grounded in the Whitfield, Havlicek & Troyer (2016)
paper cited above, which I used as the reference to verify the
implementation is correct.