Expected behavior
BoseWord documents that it "can be constructed from a standard dictionary", where the
first integer of each key is the position of the operator in the Bose word. The position is
encoded in the key, so the insertion order of the dictionary is not meant to be significant: two
dictionaries with the same key/value pairs represent the same operator.
Indeed, BoseWord canonicalises on construction (it stores a sorted_dic), and two such words
compare equal:
w1 = BoseWord({(0, 0): '+', (1, 1): '-'}) # b⁺(0) b(1)
w2 = BoseWord({(1, 1): '-', (0, 0): '+'}) # same operator, different insertion order
assert w1 == w2 # True
assert str(w1) == str(w2) # both print "b⁺(0) b(1)"
Because w1 and w2 are equal operators, multiplying either of them by w1 must give the same
result:
w1 * w1 == w1 * w2 # both should be b⁺(0) b(1) b⁺(0) b(1)
Actual behavior
Multiplying by w2 (built from a non-sorted dict) produces a different operator, even though
w1 == w2:
w1 == w2 : True
w1 * w2 = b⁺(0) b(1) b(0) b⁺(1) # wrong
w1 * w1 = b⁺(0) b(1) b⁺(0) b(1) # correct
(w1*w2) == (w1*w1) : False
The creation/annihilation symbols at the last two positions are swapped: the second factor
silently becomes b(0) b⁺(1) instead of b⁺(0) b(1). Physically this turns a
particle-redistributing operator (b⁺(0) b(1) b⁺(0) b(1), which moves two excitations from mode 1
to mode 0) into a number-operator-like product (b⁺(0) b(1) b(0) b⁺(1)), so it has a different
matrix, spectrum, and commutation structure. The corruption is stored in the returned object's
sorted_dic, not merely in its printed form, so it propagates to normal ordering,
qml.matrix, Hamiltonian construction, etc.
Additional information
Root cause is in pennylane/bose/bosonic.py, BoseWord.__mul__. When building the product it
pairs the sorted keys of the right factor with that factor's insertion-order values:
order_final = [i[0] + len(self) for i in other.sorted_dic.keys()] # positions from sorted keys
other_wires = [i[1] for i in other.sorted_dic.keys()] # modes from sorted keys
dict_other = dict(
zip(
[(order_idx, other_wires[i]) for i, order_idx in enumerate(order_final)],
other.values(), # <-- values in native (insertion) order, not sorted order
strict=True,
)
)
other.sorted_dic.keys() iterates in sorted-by-position order, while other.values() (the
dict subclass itself) iterates in the original insertion order. When these two orders differ,
each key is paired with the wrong '+'/'-' value. The fix is to take the values consistently
from the sorted view, e.g. list(other.sorted_dic.values()) instead of other.values().
Source code
from pennylane.bose.bosonic import BoseWord
w1 = BoseWord({(0, 0): '+', (1, 1): '-'})
w2 = BoseWord({(1, 1): '-', (0, 0): '+'})
print("w1 =", w1, "| w2 =", w2, "| w1 == w2 :", w1 == w2)
print("w1 * w2 =", w1 * w2)
print("w1 * w1 =", w1 * w1, "(correct)")
print("(w1*w2) == (w1*w1) :", (w1 * w2) == (w1 * w1))
Tracebacks
w1 = b⁺(0) b(1) | w2 = b⁺(0) b(1) | w1 == w2 : True
w1 * w2 = b⁺(0) b(1) b(0) b⁺(1)
w1 * w1 = b⁺(0) b(1) b⁺(0) b(1) (correct)
(w1*w2) == (w1*w1) : False
System information
Version: 0.45.0
Platform info: macOS
Python version: 3.13.13
Existing GitHub issues
Expected behavior
BoseWorddocuments that it "can be constructed from a standard dictionary", where thefirst integer of each key is the position of the operator in the Bose word. The position is
encoded in the key, so the insertion order of the dictionary is not meant to be significant: two
dictionaries with the same key/value pairs represent the same operator.
Indeed,
BoseWordcanonicalises on construction (it stores asorted_dic), and two such wordscompare equal:
Because
w1andw2are equal operators, multiplying either of them byw1must give the sameresult:
Actual behavior
Multiplying by
w2(built from a non-sorted dict) produces a different operator, even thoughw1 == w2:The creation/annihilation symbols at the last two positions are swapped: the second factor
silently becomes
b(0) b⁺(1)instead ofb⁺(0) b(1). Physically this turns aparticle-redistributing operator (
b⁺(0) b(1) b⁺(0) b(1), which moves two excitations from mode 1to mode 0) into a number-operator-like product (
b⁺(0) b(1) b(0) b⁺(1)), so it has a differentmatrix, spectrum, and commutation structure. The corruption is stored in the returned object's
sorted_dic, not merely in its printed form, so it propagates to normal ordering,qml.matrix, Hamiltonian construction, etc.Additional information
Root cause is in
pennylane/bose/bosonic.py,BoseWord.__mul__. When building the product itpairs the sorted keys of the right factor with that factor's insertion-order values:
other.sorted_dic.keys()iterates in sorted-by-position order, whileother.values()(thedictsubclass itself) iterates in the original insertion order. When these two orders differ,each key is paired with the wrong
'+'/'-'value. The fix is to take the values consistentlyfrom the sorted view, e.g.
list(other.sorted_dic.values())instead ofother.values().Source code
from pennylane.bose.bosonic import BoseWord w1 = BoseWord({(0, 0): '+', (1, 1): '-'}) w2 = BoseWord({(1, 1): '-', (0, 0): '+'}) print("w1 =", w1, "| w2 =", w2, "| w1 == w2 :", w1 == w2) print("w1 * w2 =", w1 * w2) print("w1 * w1 =", w1 * w1, "(correct)") print("(w1*w2) == (w1*w1) :", (w1 * w2) == (w1 * w1))Tracebacks
System information
Existing GitHub issues