Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implements a reverse_map() method for JordanWignerMapper(). #1344

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
e162f47
Implement reverse_map() method.
MarcoBarroca Feb 25, 2024
8d7486b
Created unit test for reverse_map() and added release note.
MarcoBarroca Feb 25, 2024
b7fe618
Implement reverse_map() method in jordan_wigner_mapper.py
MarcoBarroca Feb 25, 2024
3efd4ca
Update jordan_wigner_mapper.py to fix automated checks
MarcoBarroca Feb 27, 2024
0da3ec8
Add Pyscf check in test_reverse_map() for automated checks.
MarcoBarroca Feb 27, 2024
4c15ca7
Complex inside FermionicOP to fix mypy error
MarcoBarroca Feb 27, 2024
98c6451
Merge branch 'main' into main
MarcoBarroca Mar 1, 2024
9a48d53
Update releasenotes/notes/reverse-jordan-wigner-mapping-be0e0ab217967…
MarcoBarroca Mar 2, 2024
b41f04f
Update releasenotes/notes/reverse-jordan-wigner-mapping-be0e0ab217967…
MarcoBarroca Mar 2, 2024
bc52fb5
Update reverse-jordan-wigner-mapping-be0e0ab217967f61.yaml
MarcoBarroca Mar 2, 2024
657cc12
Update releasenotes/notes/reverse-jordan-wigner-mapping-be0e0ab217967…
MarcoBarroca Mar 2, 2024
fb3ad15
Make invert_pauli_terms() private.
MarcoBarroca Mar 2, 2024
aa53bfd
Add note to use reverse_map() ony on JW operators.
MarcoBarroca Mar 2, 2024
0ca421d
Changed wording on the note.
MarcoBarroca Mar 2, 2024
9a7223a
Update jordan_wigner_mapper.py
MarcoBarroca Mar 2, 2024
79a739a
FIx style formatting.
MarcoBarroca Mar 2, 2024
8bd7562
Update qiskit_nature/second_q/mappers/jordan_wigner_mapper.py
MarcoBarroca Mar 6, 2024
bfe70ea
Merge branch 'main' into main
MarcoBarroca Apr 12, 2024
904c889
Merge branch 'main' into main
MarcoBarroca May 9, 2024
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
64 changes: 62 additions & 2 deletions qiskit_nature/second_q/mappers/jordan_wigner_mapper.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This code is part of a Qiskit project.
#
# (C) Copyright IBM 2021, 2023.
# (C) Copyright IBM 2021, 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
Expand All @@ -19,7 +19,9 @@
import numpy as np

from qiskit.quantum_info.operators import Pauli
from qiskit.quantum_info import SparsePauliOp

from qiskit_nature.second_q.operators import FermionicOp
from .fermionic_mapper import FermionicMapper


Expand All @@ -41,5 +43,63 @@ def pauli_table(cls, register_length: int) -> list[tuple[Pauli, Pauli]]:
# c_x = np.asarray([0] * register_length, dtype=bool)
pauli_table.append((Pauli((a_z, a_x)), Pauli((b_z, b_x))))
# TODO add Pauli 3-tuple to lookup table

return pauli_table

@classmethod
def reverse_map(cls, qubit_op: SparsePauliOp) -> FermionicOp:
"""Maps a qubit operator ``SparsePauliOp`` back into the second
Copy link
Member

Choose a reason for hiding this comment

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

I have a question around this. This seems to take qubit operator and maps it back regardless of what it really is. I get if I this is given a JW mapped Fermionic Op as a qubit operator it will reverse that. What happens to some other operator - does it always work and produce something even if its meaningless? Is there any check possible? Should we add any note here about this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It will work for any operator that is a SparsePauliOp and write that in terms of fermionic operators. But if a user tries to reverse an operator that was mapped with a different mapper(I.e. ParityMapper()) they will just get different ones.

It can also take any qubit operator and turn into a fermionic Op even if it doesn’t make sense, so it will always give a result.

Maybe a note making it clear that this works for JW? I wouldn’t know how to check if an qubit operator was transformed via Jordan Wigner or something else.

Copy link
Member

Choose a reason for hiding this comment

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

A note sounds like the best way then - that says it users responsibility to provide an operator that was mapped with JW to get a meaningful outcome as this does the reverse mapping for any operator on the given assumption it was done with a JW mapping in the first place - or however you want to phrase it,.

quantized operator ``FermionicOp``.

Args:
qubit_op: The qubit operator ``SparsePauliOp`` to be mapped.

Returns:
The second quantized operator ``FermionicOp`` corresponding to
the Hamiltonian in the Fermionic space.
"""
num_qubits = (
qubit_op.num_qubits
) # get number of qubits from input second quantized operator
qubit_op = cls.invert_pauli_terms(qubit_op)
total_fermionic_op = FermionicOp.zero()
for term in qubit_op:
coef_term = term.coeffs[0]
target_pauli_op = term.paulis[0]
ferm_term_ops = []
for i in range(num_qubits):
one_pauli = target_pauli_op[num_qubits - 1 - i]
pauli_char = one_pauli.to_label()
if pauli_char == "Z": # dealing Pauli Z op
ferm_op_pauli = FermionicOp({"": 1, f"+_{i} -_{i}": -2})
elif pauli_char == "X": # dealing Pauli X op
ferm_op_pauli = FermionicOp({f"+_{i}": 1, f"-_{i}": 1})
target_pauli_op = Pauli("I" * (i + 1) + "Z" * (num_qubits - i - 1)).compose(
target_pauli_op
)
elif one_pauli.to_label() == "Y": # dealing Pauli Y op
ferm_op_pauli = FermionicOp({f"+_{i}": -1j, f"-_{i}": 1j})
target_pauli_op = Pauli("I" * (i + 1) + "Z" * (num_qubits - i - 1)).compose(
target_pauli_op
)
else:
ferm_op_pauli = FermionicOp.one()
ferm_term_ops.append(ferm_op_pauli)
term_fermionic_op = FermionicOp.one()
for op in ferm_term_ops:
term_fermionic_op = term_fermionic_op @ op
if target_pauli_op.phase == 1:
coef_term *= -1j
elif target_pauli_op.phase == 2:
coef_term *= -1
elif target_pauli_op.phase == 3:
coef_term *= 1j
total_fermionic_op += coef_term * term_fermionic_op
return total_fermionic_op.normal_order()

@staticmethod
def invert_pauli_terms(sparse_pauli_op: SparsePauliOp) -> SparsePauliOp:
Copy link
Member

Choose a reason for hiding this comment

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

I am thinking this should be private method - ie something we do not want as part of supported public API. (If it was it would havte to have full docstring stating args, return etc. I think this is more intended as some private utility right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right now I think I’m the only one asking for this. I don’t see why we shouldn’t have this on the public API though. If Open Fermion can do this and thinks it’s relevant why shouldn’t Qiskit Nature?

Copy link
Member

Choose a reason for hiding this comment

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

We are talking just about the invert_pauli_terms method right - that was all I was meaning as being private not the whole reverse map method

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh! my bad. Sure, I can turn that into a private method. It's just a utility for the reverse_map().

"""Utility to invert the order of Pauli operators in each term of a SparsePauliOp."""
inverted_labels = [label[::-1] for label in sparse_pauli_op.paulis.to_labels()]
# Create a new SparsePauliOp with the inverted labels but same coefficients
inverted_sparse_pauli_op = SparsePauliOp(inverted_labels, sparse_pauli_op.coeffs)
return inverted_sparse_pauli_op
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

MarcoBarroca marked this conversation as resolved.
Show resolved Hide resolved
features:
-
MarcoBarroca marked this conversation as resolved.
Show resolved Hide resolved
Created a new method reverse_map() for JordanWignerMapper() that allows users to recover the fermionic operator from a qubit operator.
MarcoBarroca marked this conversation as resolved.
Show resolved Hide resolved
15 changes: 14 additions & 1 deletion test/second_q/mappers/test_jordan_wigner_mapper.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This code is part of a Qiskit project.
#
# (C) Copyright IBM 2021, 2023.
# (C) Copyright IBM 2021, 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
Expand Down Expand Up @@ -142,6 +142,19 @@ def test_mapping_overwrite_reg_len(self):
mapper = JordanWignerMapper()
self.assertEqual(mapper.map(op, register_length=3), mapper.map(expected))

@unittest.skipIf(not _optionals.HAS_PYSCF, "pyscf not available.")
def test_reverse_map(self):
"""Test reverse mapping from qubit operator back to fermionic operator."""
driver = PySCFDriver()
driver_result = driver.run()
fermionic_op, _ = driver_result.second_q_ops()
mapper = JordanWignerMapper()
qubit_op = mapper.map(fermionic_op)
recovered_fermionic_op = mapper.reverse_map(qubit_op)
fermionic_op = fermionic_op.normal_order()
recovered_fermionic_op = recovered_fermionic_op.normal_order()
self.assertTrue(fermionic_op.equiv(recovered_fermionic_op))


if __name__ == "__main__":
unittest.main()