Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 21 additions & 6 deletions python/ffsim/qiskit/jordan_wigner.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
from ffsim.operators import FermionOperator


def jordan_wigner(op: FermionOperator, norb: int | None = None) -> SparsePauliOp:
def jordan_wigner(
op: FermionOperator, norb: int | None = None, allow_spinless: bool = True
) -> SparsePauliOp:
r"""Jordan-Wigner transformation.

Transform a fermion operator to a qubit operator using the Jordan-Wigner
Expand All @@ -41,6 +43,8 @@ def jordan_wigner(op: FermionOperator, norb: int | None = None) -> SparsePauliOp
op: The fermion operator to transform.
norb: The total number of spatial orbitals. If not specified, it is determined
by the largest-index orbital present in the operator.
allow_spinless: Flag enabling spinless FermionOperators to be
encoded with norb qubits.

Returns:
The qubit operator as a Qiskit SparsePauliOp.
Expand Down Expand Up @@ -69,25 +73,36 @@ def jordan_wigner(op: FermionOperator, norb: int | None = None) -> SparsePauliOp
f"only {norb} were specified."
)

qubit_terms = [SparsePauliOp.from_sparse_list([("", [], 0.0)], num_qubits=2 * norb)]
is_spinless = allow_spinless
if allow_spinless:
for term in op.keys():
if any(t[1] for t in term):
is_spinless = False
break

num_qubits = norb if is_spinless else 2 * norb
qubit_terms = [
SparsePauliOp.from_sparse_list([("", [], 0.0)], num_qubits=num_qubits)
]

for term, coeff in op.items():
qubit_op = SparsePauliOp.from_sparse_list(
[("", [], coeff)], num_qubits=2 * norb
[("", [], coeff)], num_qubits=num_qubits
)
for action, spin, orb in term:
qubit_op @= _qubit_action(action, orb + spin * norb, norb)
qubit_op @= _qubit_action(action, orb + spin * norb, num_qubits)
qubit_terms.append(qubit_op)

return SparsePauliOp.sum(qubit_terms).simplify()


@functools.cache
def _qubit_action(action: bool, qubit: int, norb: int):
def _qubit_action(action: bool, qubit: int, num_qubits: int):
qubits = list(range(qubit + 1))
return SparsePauliOp.from_sparse_list(
[
("Z" * qubit + "X", qubits, 0.5),
("Z" * qubit + "Y", qubits, -0.5j if action else 0.5j),
],
num_qubits=2 * norb,
num_qubits=num_qubits,
)
48 changes: 46 additions & 2 deletions tests/python/qiskit/jordan_wigner_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,50 @@
import ffsim.random.random


@pytest.mark.parametrize("spinless", [True, False])
@pytest.mark.parametrize("norb, nelec", ffsim.testing.generate_norb_nelec(range(1, 5)))
def test_random_molecular_hamiltonian(
norb: int, nelec: tuple[int, int], spinless: bool
):
"""Test on random fermion Hamiltonian."""
rng = np.random.default_rng(4482)

if spinless:
nelec = nelec[0]
mol_ham = ffsim.random.random_molecular_hamiltonian_spinless(norb, seed=rng)
else:
mol_ham = ffsim.random.random_molecular_hamiltonian(norb, seed=rng)

op = ffsim.fermion_operator(mol_ham)
linop = ffsim.linear_operator(op, norb=norb, nelec=nelec)
vec = ffsim.random.random_state_vector(ffsim.dim(norb, nelec), seed=rng)
expected_result = ffsim.qiskit.ffsim_vec_to_qiskit_vec(linop @ vec, norb, nelec)
qubit_op = ffsim.qiskit.jordan_wigner(op)
qubit_op_sparse = qubit_op.to_matrix(sparse=True)
actual_result = qubit_op_sparse @ ffsim.qiskit.ffsim_vec_to_qiskit_vec(
vec, norb, nelec
)
np.testing.assert_allclose(actual_result, expected_result, atol=1e-12)

qubit_op = ffsim.qiskit.jordan_wigner(op, norb=norb)
qubit_op_sparse = qubit_op.to_matrix(sparse=True)
actual_result = qubit_op_sparse @ ffsim.qiskit.ffsim_vec_to_qiskit_vec(
vec, norb, nelec
)
np.testing.assert_allclose(actual_result, expected_result, atol=1e-12)
actual_result = qubit_op_sparse @ ffsim.qiskit.ffsim_vec_to_qiskit_vec(
vec, norb, nelec
)
np.testing.assert_allclose(actual_result, expected_result, atol=1e-12)

qubit_op = ffsim.qiskit.jordan_wigner(op, norb=norb)
qubit_op_sparse = qubit_op.to_matrix(sparse=True)
actual_result = qubit_op_sparse @ ffsim.qiskit.ffsim_vec_to_qiskit_vec(
vec, norb, nelec
)
np.testing.assert_allclose(actual_result, expected_result, atol=1e-12)


@pytest.mark.parametrize("norb, nelec", ffsim.testing.generate_norb_nelec(range(5)))
def test_random(norb: int, nelec: tuple[int, int]):
"""Test on random fermion Hamiltonian."""
Expand All @@ -26,14 +70,14 @@ def test_random(norb: int, nelec: tuple[int, int]):
vec = ffsim.random.random_state_vector(ffsim.dim(norb, nelec), seed=rng)
expected_result = ffsim.qiskit.ffsim_vec_to_qiskit_vec(linop @ vec, norb, nelec)

qubit_op = ffsim.qiskit.jordan_wigner(op)
qubit_op = ffsim.qiskit.jordan_wigner(op, allow_spinless=False)
qubit_op_sparse = qubit_op.to_matrix(sparse=True)
actual_result = qubit_op_sparse @ ffsim.qiskit.ffsim_vec_to_qiskit_vec(
vec, norb, nelec
)
np.testing.assert_allclose(actual_result, expected_result, atol=1e-12)

qubit_op = ffsim.qiskit.jordan_wigner(op, norb=norb)
qubit_op = ffsim.qiskit.jordan_wigner(op, norb=norb, allow_spinless=False)
qubit_op_sparse = qubit_op.to_matrix(sparse=True)
actual_result = qubit_op_sparse @ ffsim.qiskit.ffsim_vec_to_qiskit_vec(
vec, norb, nelec
Expand Down