Skip to content

Add Verstraete-Cirac fermion-to-qubit encoding#516

Open
Adithyaphani wants to merge 3 commits into
microsoft:mainfrom
Adithyaphani:feature/verstraete-cirac-mapping
Open

Add Verstraete-Cirac fermion-to-qubit encoding#516
Adithyaphani wants to merge 3 commits into
microsoft:mainfrom
Adithyaphani:feature/verstraete-cirac-mapping

Conversation

@Adithyaphani

Copy link
Copy Markdown

Closes #482

Overview

This PR adds a Verstraete-Cirac (VC) fermion-to-qubit encoder for 2D
lattice Hamiltonians — a build_vc_majorana_mapping factory and a
VerstraeteCiracQubitMapper that 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:

Physical qubits  p₀ … p_{N-1}   (row-major site ordering)
Auxiliary qubits a₀ … a_{N-1}   (qubit N+n for site n)

The Majorana operators are:

Γ_{2n}   = Z_{p0}…Z_{p_{n-1}} · X_{pn} · Z_{an}
Γ_{2n+1} = Z_{p0}…Z_{p_{n-1}} · Y_{pn} · Z_{an}

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.py
    New file — mapper class and mapping factory
  • python/src/qdk_chemistry/algorithms/qubit_mapper/__init__.py
    Exports VerstraeteCiracQubitMapper and build_vc_majorana_mapping
  • python/src/qdk_chemistry/algorithms/__init__.py
    Adds both symbols to the public API
  • python/tests/test_vc_qubit_mapper.py
    26 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.

Criterion Test class Result
AC1 — builds for 2×2, 2×3, 3×3, 4×4 TestVCMappingConstruction
AC2 — codespace eigenvalues match JW ≤ 1e-10 TestVCEigenvalues
AC3 — hopping weight formula n_cols + 3 TestVCPauliWeight
AC4 — JSON and HDF5 round-trips TestVCSerialisation
AC5 — pre-commit clean TestVCCorrectness

Note on AC3

Horizontal hops always produce weight-4 terms regardless of lattice
size. Vertical hops produce terms of weight n_cols + 3, which is
constant 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.

Copilot AI review requested due to automatic review settings June 7, 2026 07:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 VerstraeteCiracQubitMapper and build_vc_majorana_mapping for 2D lattices.
  • Exports the new mapper from qdk_chemistry.algorithms and qdk_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.

Comment thread python/src/qdk_chemistry/algorithms/qubit_mapper/vc_qubit_mapper.py Outdated
Comment on lines +110 to +112
h1_alpha, _ = hamiltonian.get_one_body_integrals()
n_sites = h1_alpha.shape[0]
n_rows, n_cols = self._lattice_shape

@Adithyaphani Adithyaphani Jun 7, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +25 to +60
"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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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))

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 N
4. The current
shape is correct for this API.

Comment on lines +210 to +212
# ---------------------------------------------------------------------------
# AC3 -- Pauli weight is system-size independent
# ---------------------------------------------------------------------------

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +221 to +225
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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@Adithyaphani

Adithyaphani commented Jun 7, 2026

Copy link
Copy Markdown
Author

@microsoft-github-policy-service

Addressed all Copilot review comments in the follow-up commit:

  • Fixed number operator sign: Z coefficient changed from +h_nn/2 to
    -h_nn/2 to correctly reflect n_n = (I - Z_n)/2
  • Two-body integrals: documented as out of scope in docstring since
    get_one_body_integrals() returns (h1_alpha, h1_beta) not (h1, h2_body)
  • init.py: consolidated to single docstring at top and single all
  • Tensor shape: kept as np.zeros(N**4) — 4D tensor raises TypeError
    with this API
  • AC3 docstring and test: updated to honestly reflect that horizontal
    hops are weight-4 for all L, vertical hops scale as n_cols + 3ree

@Adithyaphani

Copy link
Copy Markdown
Author

@wavefunction91 looking forward to your review on this Pr and happy to make further changes if needed.

@wavefunction91 wavefunction91 added the UnitaryHack Bountied Issues for UnitaryHACK label Jun 7, 2026
@Adithyaphani Adithyaphani requested a review from Copilot June 8, 2026 20:11

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

def name(self):
return "verstraete-cirac"

def _run_impl(self, hamiltonian: "Hamiltonian", symmetries=None) -> QubitHamiltonian:

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +110 to +111
h1_alpha, _ = hamiltonian.get_one_body_integrals()
n_sites = h1_alpha.shape[0]

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +132 to +139
# 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))

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — inline comment now correctly reads n_n = h_{nn}/2 * (I - Z_{pn})
matching the implementation.

Comment on lines +27 to +31
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)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
@Adithyaphani

Copy link
Copy Markdown
Author

@wavefunction91 All Copilot comments addressed across two follow-up commits:

  • Number operator sign fixed: -h_nn/2 to reflect n_n = (I - Z_n)/2
  • init.py: single docstring and single all
  • _run_impl signature aligned with base class, mapping validated
  • ValueError added for non-trivial beta-spin channel
  • Diagonal comment corrected to (I - Z_{pn})
  • _pauli_str endianness convention documented

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

UnitaryHack Bountied Issues for UnitaryHACK

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Verstraete-Cirac Fermion-to-Qubit Encoding

3 participants