From da3dae86273380b79610dbf522991a104cdcff99 Mon Sep 17 00:00:00 2001 From: MIWdlB Date: Thu, 9 Oct 2025 16:06:50 +0100 Subject: [PATCH] allow spinless jw --- python/ffsim/qiskit/jordan_wigner.py | 27 ++++++++++--- tests/python/qiskit/jordan_wigner_test.py | 48 ++++++++++++++++++++++- 2 files changed, 67 insertions(+), 8 deletions(-) diff --git a/python/ffsim/qiskit/jordan_wigner.py b/python/ffsim/qiskit/jordan_wigner.py index 6239c9dbd..39d91df46 100644 --- a/python/ffsim/qiskit/jordan_wigner.py +++ b/python/ffsim/qiskit/jordan_wigner.py @@ -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 @@ -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. @@ -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, ) diff --git a/tests/python/qiskit/jordan_wigner_test.py b/tests/python/qiskit/jordan_wigner_test.py index 95830d0a9..3e7102ab2 100644 --- a/tests/python/qiskit/jordan_wigner_test.py +++ b/tests/python/qiskit/jordan_wigner_test.py @@ -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.""" @@ -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