Skip to content

Commit

Permalink
add qiskit jordan-wigner (#281)
Browse files Browse the repository at this point in the history
  • Loading branch information
kevinsung authored Jul 10, 2024
1 parent 1fc5204 commit 77b9b61
Show file tree
Hide file tree
Showing 5 changed files with 255 additions and 1 deletion.
2 changes: 2 additions & 0 deletions python/ffsim/qiskit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
UCJOpSpinlessJW,
UCJOpSpinUnbalancedJW,
)
from ffsim.qiskit.jordan_wigner import jordan_wigner
from ffsim.qiskit.sampler import FfsimSampler
from ffsim.qiskit.sim import final_state_vector
from ffsim.qiskit.transpiler_passes import DropNegligible, MergeOrbitalRotations
Expand Down Expand Up @@ -75,6 +76,7 @@
"UCJOpSpinlessJW",
"ffsim_vec_to_qiskit_vec",
"final_state_vector",
"jordan_wigner",
"pre_init_passes",
"qiskit_vec_to_ffsim_vec",
]
77 changes: 77 additions & 0 deletions python/ffsim/qiskit/jordan_wigner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# (C) Copyright IBM 2024.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

"""Jordan-Wigner transformation."""

from __future__ import annotations

import functools

from qiskit.quantum_info import SparsePauliOp

from ffsim.operators import FermionOperator


def jordan_wigner(op: FermionOperator, n_qubits: int | None = None) -> SparsePauliOp:
"""Jordan-Wigner transformation.
Transform a fermion operator to a qubit operator using the Jordan-Wigner
transformation.
Args:
op: The fermion operator to transform.
n_qubits: The number of qubits to include in the output qubit operator. If not
specified, the minimum number of qubits needed to accommodate the fermion
operator will be used. Must be non-negative.
Returns:
The qubit operator as a Qiskit SparsePauliOp.
Raises:
ValueError: Number of qubits was negative.
ValueError: Number of qubits was not enough to accommodate the fermion operator.
"""
if n_qubits and n_qubits < 0:
raise ValueError(f"Number of qubits must be non-negative. Got {n_qubits}.")
if not op:
return SparsePauliOp.from_sparse_list([("", [], 0.0)], num_qubits=n_qubits or 0)

norb = 1 + max(orb for term in op for _, _, orb in term)
if n_qubits is None:
n_qubits = 2 * norb
if n_qubits < 2 * norb:
raise ValueError(
"Number of qubits is not enough to accommodate the fermion operator. "
f"The fermion operator has {norb} spatial orbitals, so at least {2 * norb} "
f"qubits is needed, but got {n_qubits}."
)

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

return SparsePauliOp.sum(qubit_terms)


@functools.cache
def _qubit_action(action: bool, qubit: int, n_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=n_qubits,
)
4 changes: 4 additions & 0 deletions python/ffsim/random/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
from ffsim.random.random import (
random_antihermitian,
random_double_factorized_hamiltonian,
random_fermion_hamiltonian,
random_fermion_operator,
random_hermitian,
random_molecular_hamiltonian,
random_orthogonal,
Expand All @@ -32,6 +34,8 @@
__all__ = [
"random_antihermitian",
"random_double_factorized_hamiltonian",
"random_fermion_hamiltonian",
"random_fermion_operator",
"random_hermitian",
"random_molecular_hamiltonian",
"random_orthogonal",
Expand Down
96 changes: 95 additions & 1 deletion python/ffsim/random/random.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@
from __future__ import annotations

import itertools
from collections import defaultdict

import numpy as np
from typing_extensions import deprecated

from ffsim import hamiltonians, variational
from ffsim import hamiltonians, operators, variational


@deprecated(
Expand Down Expand Up @@ -508,3 +509,96 @@ def random_double_factorized_hamiltonian(
constant=constant,
z_representation=z_representation,
)


def random_fermion_operator(
norb: int, n_terms: int | None = None, max_term_length: int | None = None, seed=None
) -> operators.FermionOperator:
"""Sample a random fermion operator.
Args:
norb: The number of spatial orbitals.
n_terms: The number of terms to include in the operator. If not specified,
`norb` is used.
max_term_length: The maximum length of a term. If not specified, `norb` is used.
seed: A seed to initialize the pseudorandom number generator.
Should be a valid input to ``np.random.default_rng``.
Returns:
The sampled fermion operator.
"""
rng = np.random.default_rng(seed)
if n_terms is None:
n_terms = norb
if max_term_length is None:
max_term_length = norb
coeffs: defaultdict[tuple[tuple[bool, bool, int], ...], complex] = defaultdict(
complex
)
for _ in range(n_terms):
term_length = int(rng.integers(1, max_term_length + 1))
actions = [bool(i) for i in rng.integers(2, size=term_length)]
spins = [bool(i) for i in rng.integers(2, size=term_length)]
indices = [int(i) for i in rng.integers(norb, size=term_length)]
coeff = rng.standard_normal() + 1j * rng.standard_normal()
term = tuple(zip(actions, spins, indices))
coeffs[term] += coeff
return operators.FermionOperator(coeffs)


def random_fermion_hamiltonian(
norb: int, n_terms: int | None = None, seed=None
) -> operators.FermionOperator:
"""Sample a random fermion Hamiltonian.
A fermion Hamiltonian is hermitian and conserves particle number and spin Z.
Args:
norb: The number of spatial orbitals.
n_terms: The number of terms to include in the operator. If not specified,
`norb` is used.
seed: A seed to initialize the pseudorandom number generator.
Should be a valid input to ``np.random.default_rng``.
Returns:
The sampled fermion Hamiltonian.
"""
rng = np.random.default_rng(seed)
if n_terms is None:
n_terms = norb
coeffs: defaultdict[tuple[tuple[bool, bool, int], ...], complex] = defaultdict(
complex
)
for _ in range(n_terms):
n_excitations = int(rng.integers(1, norb + 1))
term = _random_num_and_spin_z_conserving_term(norb, n_excitations, seed=rng)
term_adjoint = _adjoint_term(term)
coeff = rng.standard_normal() + 1j * rng.standard_normal()
coeffs[term] += coeff
coeffs[term_adjoint] += coeff.conjugate()
return operators.FermionOperator(coeffs)


def _random_num_and_spin_z_conserving_term(
norb: int, n_excitations: int, seed=None
) -> tuple[tuple[bool, bool, int], ...]:
rng = np.random.default_rng(seed)
term = []
for _ in range(n_excitations):
spin = bool(rng.integers(2))
orb_1, orb_2 = [int(x) for x in rng.integers(norb, size=2)]
action_1, action_2 = [
bool(x) for x in rng.choice([True, False], size=2, replace=False)
]
term.append(operators.FermionAction(action_1, spin, orb_1))
term.append(operators.FermionAction(action_2, spin, orb_2))
return tuple(term)


def _adjoint_term(
term: tuple[tuple[bool, bool, int], ...],
) -> tuple[tuple[bool, bool, int], ...]:
return tuple(
operators.FermionAction(bool(1 - action), spin, orb)
for action, spin, orb in reversed(term)
)
77 changes: 77 additions & 0 deletions tests/python/qiskit/jordan_wigner_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# (C) Copyright IBM 2024.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

"""Tests for Jordan-Wigner transformation."""

import numpy as np
import pytest

import ffsim
import ffsim.random.random


@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."""
rng = np.random.default_rng(4482)
op = ffsim.random.random_fermion_hamiltonian(norb, seed=rng)
linop = ffsim.linear_operator(op, norb=norb, nelec=nelec)
vec = ffsim.random.random_state_vector(ffsim.dim(norb, nelec), seed=rng)

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
)
expected_result = ffsim.qiskit.ffsim_vec_to_qiskit_vec(linop @ vec, norb, nelec)
np.testing.assert_allclose(actual_result, expected_result, atol=1e-12)

qubit_op = ffsim.qiskit.jordan_wigner(op, n_qubits=2 * 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)


def test_bad_qubit_number():
"""Test passing bad number of qubits raises errors."""
op = ffsim.FermionOperator({(ffsim.cre_a(3),): 1.0})
with pytest.raises(ValueError, match="non-negative"):
_ = ffsim.qiskit.jordan_wigner(op, n_qubits=-1)
with pytest.raises(ValueError, match="enough"):
_ = ffsim.qiskit.jordan_wigner(op, n_qubits=7)


def test_hubbard():
"""Test on Hubbard model"""
rng = np.random.default_rng(7431)
norb_x = 2
norb_y = 2
norb = norb_x * norb_y
nelec = (norb // 2, norb // 2)
op = ffsim.fermi_hubbard_2d(
norb_x=norb_x,
norb_y=norb_y,
tunneling=rng.uniform(-10, 10),
interaction=rng.uniform(-10, 10),
chemical_potential=rng.uniform(-10, 10),
nearest_neighbor_interaction=rng.uniform(-10, 10),
periodic=False,
)
linop = ffsim.linear_operator(op, norb=norb, nelec=nelec)
vec = ffsim.random.random_state_vector(ffsim.dim(norb, nelec), seed=rng)
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
)
expected_result = ffsim.qiskit.ffsim_vec_to_qiskit_vec(linop @ vec, norb, nelec)
np.testing.assert_allclose(actual_result, expected_result)

0 comments on commit 77b9b61

Please sign in to comment.