Skip to content

[BUG] BoseWord.__mul__ returns the wrong operator when a factor was built from a non-sorted dict #9650

Description

@YujinSong-hep

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

  • I have searched existing GitHub issues to make sure the issue does not already exist.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bug 🐛Something isn't workingcommunity-botIssue suspected to be found by a bot

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions