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

fix: make FermionicOp.simplify preserve the term order #893

Merged
Merged
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
37 changes: 20 additions & 17 deletions qiskit_nature/second_q/operators/fermionic_op.py
Original file line number Diff line number Diff line change
Expand Up @@ -518,50 +518,53 @@ def simplify(self, *, atol: float | None = None) -> FermionicOp:
def _simplify_label(self, label: str, coeff: complex) -> tuple[str, complex]:
bits = _BitsContainer()

# Since Python 3.7, dictionaries are guaranteed to be insert-order preserving. We use this
# to our advantage, to implement an ordered set, which allows us to preserve the label order
# and only remove canceling terms.
new_label: dict[str, None] = {}

for lbl in label.split():
char, index = lbl.split("_")
idx = int(index)
char_b = char == "+"

if idx not in bits:
bits[idx] = int(f"{char_b:b}{not char_b:b}{char_b:b}{char_b:b}", base=2)
# we store all relevant information for each register index in 4 bits:
# 1. True if a `+` has been applied on this index
# 2. True if a `-` has been applied on this index
# 3. True if a `+` was applied first, False if a `-` was applied first
# 4. True if the last added operation on this index was `+`, False if `-`
bits[idx] = int(f"{char_b:b}{not char_b:b}{char_b:b}{char_b:b}", base=2)
# and we insert the encountered label into our ordered set
new_label[lbl] = None

elif bits.get_last(idx) == char_b:
# we bail, if we apply the same operator as the last one
return "", 0

elif bits.get_plus(idx) and bits.get_minus(idx):
# if both, `+` and `-`, have already been applied, we cancel the opposite to the
# current one (i.e. `+` will cancel `-` and vice versa)
# If both, `+` and `-`, have already been applied, we cancel the opposite to the
# current one (i.e. `+` will cancel `-` and vice versa). We update the bit container
bits.set_plus_or_minus(idx, not char_b, False)
# and pop the reverse label from the ordered set
new_label.pop(f"{'-' if char_b else '+'}_{idx}")
# we also update the last bit to the current char
bits.set_last(idx, char_b)
# and finally, we need to update the coefficient to account for the swap operations
# which were necessary to place the canceling terms next to each other
if idx != self.num_spin_orbitals:
num_exchange = sum(i in bits for i in range(idx + 1, self.num_spin_orbitals))
coeff *= -1 if num_exchange % 2 else 1

else:
# else, we simply set the bit of the currently applied char
bits.set_plus_or_minus(idx, char_b, True)
# and track it in our ordered set
new_label[lbl] = None
# we also update the last bit to the current char
bits.set_last(idx, char_b)

if idx != self.num_spin_orbitals:
num_exchange = 0
for i in range(idx + 1, self.num_spin_orbitals):
if i in bits:
num_exchange += (bits.get_plus(i) + bits.get_minus(i)) % 2
coeff *= -1 if num_exchange % 2 else 1

new_label = []
for idx in sorted(bits):
plus = f"+_{idx}" if bits.get_plus(idx) else None
minus = f"-_{idx}" if bits.get_minus(idx) else None
new_label.extend([plus, minus] if bits.get_order(idx) else [minus, plus])

return " ".join(lbl for lbl in new_label if lbl is not None), coeff
return " ".join(new_label), coeff


class _BitsContainer(MutableMapping):
Expand Down
10 changes: 9 additions & 1 deletion test/second_q/operators/test_fermionic_op.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def test_compose(self):
) @ FermionicOp({"": 1, "-_0 +_1": 1}, num_spin_orbitals=2)
fer_op = fer_op.simplify()
targ = FermionicOp(
{"+_0 +_1 -_1": 1, "-_0 +_0 -_1": 1, "+_0 -_0 +_1": 1, "-_0 -_1 +_1": -1},
{"+_0 +_1 -_1": 1, "-_0 +_0 -_1": 1, "+_0 +_1 -_0": 1, "-_0 -_1 +_1": -1},
Copy link
Contributor

Choose a reason for hiding this comment

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

You swapped the order of the terms here, but you didn't add a minus sign.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks for spotting this! #900 should address this bug 👍

num_spin_orbitals=2,
)
self.assertEqual(fer_op, targ)
Expand Down Expand Up @@ -155,12 +155,20 @@ def test_simplify(self):
simplified_op = fer_op.simplify()
self.assertEqual(simplified_op, fer_op)

fer_op = FermionicOp({"-_1 +_0": 1 + 0j}, num_spin_orbitals=2)
simplified_op = fer_op.simplify()
self.assertEqual(simplified_op, fer_op)

with self.subTest("simplify zero"):
fer_op = self.op1 - self.op1
simplified_op = fer_op.simplify()
targ = FermionicOp.zero()
self.assertEqual(simplified_op, targ)

with self.subTest("simplify commutes with normal_ordered"):
fer_op = FermionicOp({"-_0 +_1": 1}, num_spin_orbitals=2)
self.assertEqual(fer_op.simplify().normal_ordered(), fer_op.normal_ordered().simplify())

def test_hermiticity(self):
"""test is_hermitian"""
with self.subTest("operator hermitian"):
Expand Down