Source code for pennylane.bose.bosonic
# Copyright 2024 Xanadu Quantum Technologies Inc.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The bosonic representation classes and functions."""
from copy import copy
import pennylane as qml
from pennylane.typing import TensorLike
# pylint: disable= too-many-nested-blocks, too-many-branches, invalid-name
[docs]class BoseWord(dict):
r"""Dictionary used to represent a Bose word, a product of bosonic creation and
annihilation operators, that can be constructed from a standard dictionary.
The keys of the dictionary are tuples of two integers. The first integer represents the
position of the creation/annihilation operator in the Bose word and the second integer
represents the mode it acts on. The values of the dictionary are one of ``'+'`` or ``'-'``
symbols that denote creation and annihilation operators, respectively. The operator
:math:`b^{\dagger}_0 b_1` can then be constructed as
>>> w = qml.BoseWord({(0, 0) : '+', (1, 1) : '-'})
>>> print(w)
b⁺(0) b(1)
"""
# override the arithmetic dunder methods for numpy arrays so that the
# methods defined on this class are used instead
# (i.e. ensure `np.array + BoseWord` uses `BoseWord.__radd__` instead of `np.array.__add__`)
__numpy_ufunc__ = None
__array_ufunc__ = None
def __init__(self, operator):
self.sorted_dic = dict(sorted(operator.items()))
indices = [i[0] for i in self.sorted_dic.keys()]
if indices:
if list(range(max(indices) + 1)) != indices:
raise ValueError(
"The operator indices must belong to the set {0, ..., len(operator)-1}."
)
super().__init__(operator)
[docs] def adjoint(self):
r"""Return the adjoint of BoseWord."""
n = len(self.items())
adjoint_dict = {}
for key, value in reversed(self.items()):
position = n - key[0] - 1
orbital = key[1]
bose = "+" if value == "-" else "-"
adjoint_dict[(position, orbital)] = bose
return BoseWord(adjoint_dict)
[docs] def items(self):
"""Returns the dictionary items in sorted order."""
return self.sorted_dic.items()
@property
def wires(self):
r"""Return wires in a BoseWord."""
return set(i[1] for i in self.sorted_dic.keys())
def __missing__(self, key):
r"""Return empty string for a missing key in BoseWord."""
return ""
[docs] def update(self, item):
r"""Restrict updating BoseWord after instantiation."""
raise TypeError("BoseWord object does not support assignment")
def __setitem__(self, key, item):
r"""Restrict setting items after instantiation."""
raise TypeError("BoseWord object does not support assignment")
def __reduce__(self):
r"""Defines how to pickle and unpickle a BoseWord. Otherwise, un-pickling
would cause __setitem__ to be called, which is forbidden on PauliWord.
For more information, see: https://docs.python.org/3/library/pickle.html#object.__reduce__
"""
return BoseWord, (dict(self),)
def __copy__(self):
r"""Copy the BoseWord instance."""
return BoseWord(dict(self.items()))
def __deepcopy__(self, memo):
r"""Deep copy the BoseWord instance."""
res = self.__copy__()
memo[id(self)] = res
return res
def __hash__(self):
r"""Hash value of a BoseWord."""
return hash(frozenset(self.items()))
[docs] def to_string(self):
r"""Return a compact string representation of a BoseWord. Each operator in the word is
represented by the number of the wire it operates on, and a `+` or `-` to indicate either
a creation or annihilation operator.
>>> w = qml.BoseWord({(0, 0) : '+', (1, 1) : '-'})
>>> w.to_string()
'b⁺(0) b(1)'
"""
if len(self) == 0:
return "I"
symbol_map = {"+": "\u207a", "-": ""}
string = " ".join(
[
"b" + symbol_map[j] + "(" + i + ")"
for i, j in zip(
[str(i[1]) for i in self.sorted_dic.keys()], self.sorted_dic.values()
)
]
)
return string
def __str__(self):
r"""String representation of a BoseWord."""
return f"{self.to_string()}"
def __repr__(self):
r"""Terminal representation of a BoseWord"""
return f"BoseWord({self.sorted_dic})"
def __add__(self, other):
"""Add a BoseSentence, BoseWord or constant to a BoseWord. Converts both
elements into BoseSentences, and uses the BoseSentence __add__
method"""
self_bs = BoseSentence({self: 1.0})
if isinstance(other, BoseSentence):
return self_bs + other
if isinstance(other, BoseWord):
return self_bs + BoseSentence({other: 1.0})
if not isinstance(other, TensorLike):
raise TypeError(f"Cannot add {type(other)} to a BoseWord.")
if qml.math.size(other) > 1:
raise ValueError(
f"Arithmetic Bose operations can only accept an array of length 1, "
f"but received {other} of length {len(other)}"
)
return self_bs + BoseSentence({BoseWord({}): other})
def __radd__(self, other):
"""Add a BoseWord to a constant, i.e. `2 + BoseWord({...})`"""
return self.__add__(other)
def __sub__(self, other):
"""Subtract a BoseSentence, BoseWord or constant from a BoseWord. Converts both
elements into BoseSentences (with negative coefficient for `other`), and
uses the BoseSentence __add__ method"""
self_bs = BoseSentence({self: 1.0})
if isinstance(other, BoseWord):
return self_bs + BoseSentence({other: -1.0})
if isinstance(other, BoseSentence):
other_bs = BoseSentence(dict(zip(other.keys(), [-v for v in other.values()])))
return self_bs + other_bs
if not isinstance(other, TensorLike):
raise TypeError(f"Cannot subtract {type(other)} from a BoseWord.")
if qml.math.size(other) > 1:
raise ValueError(
f"Arithmetic Bose operations can only accept an array of length 1, "
f"but received {other} of length {len(other)}"
)
return self_bs + BoseSentence({BoseWord({}): -1 * other}) # -constant * I
def __rsub__(self, other):
"""Subtract a BoseWord to a constant, i.e. `2 - BoseWord({...})`"""
if not isinstance(other, TensorLike):
raise TypeError(f"Cannot subtract a BoseWord from {type(other)}.")
if qml.math.size(other) > 1:
raise ValueError(
f"Arithmetic Bose operations can only accept an array of length 1, "
f"but received {other} of length {len(other)}"
)
self_bs = BoseSentence({self: -1.0})
other_bs = BoseSentence({BoseWord({}): other})
return self_bs + other_bs
def __mul__(self, other):
r"""Multiply a BoseWord with another BoseWord, a BoseSentence, or a constant.
>>> w = qml.BoseWord({(0, 0) : '+', (1, 1) : '-'})
>>> print(w * w)
b⁺(0) b(1) b⁺(0) b(1)
"""
if isinstance(other, BoseWord):
if len(self) == 0:
return copy(other)
if len(other) == 0:
return copy(self)
order_final = [i[0] + len(self) for i in other.sorted_dic.keys()]
other_wires = [i[1] for i in other.sorted_dic.keys()]
dict_other = dict(
zip(
[(order_idx, other_wires[i]) for i, order_idx in enumerate(order_final)],
other.values(),
)
)
dict_self = dict(zip(self.keys(), self.values()))
dict_self.update(dict_other)
return BoseWord(dict_self)
if isinstance(other, BoseSentence):
return BoseSentence({self: 1}) * other
if not isinstance(other, TensorLike):
raise TypeError(f"Cannot multiply BoseWord by {type(other)}.")
if qml.math.size(other) > 1:
raise ValueError(
f"Arithmetic Bose operations can only accept an array of length 1, "
f"but received {other} of length {len(other)}"
)
return BoseSentence({self: other})
def __rmul__(self, other):
r"""Reverse multiply a BoseWord
Multiplies a BoseWord "from the left" with an object that can't be modified
to support __mul__ for BoseWord. Will be defaulted in for example
``2 * BoseWord({(0, 0): "+"})``, where the ``__mul__`` operator on an integer
will fail to multiply with a BoseWord"""
return self.__mul__(other)
def __pow__(self, value):
r"""Exponentiate a Bose word to an integer power.
>>> w = qml.BoseWord({(0, 0) : '+', (1, 1) : '-'})
>>> print(w**3)
b⁺(0) b(1) b⁺(0) b(1) b⁺(0) b(1)
"""
if value < 0 or not isinstance(value, int):
raise ValueError("The exponent must be a positive integer.")
operator = BoseWord({})
for _ in range(value):
operator *= self
return operator
[docs] def normal_order(self):
r"""Convert a BoseWord to its normal-ordered form.
>>> bw = qml.BoseWord({(0, 0): "-", (1, 0): "-", (2, 0): "+", (3, 0): "+"})
>>> print(bw.normal_order())
4.0 * b⁺(0) b(0)
+ 2.0 * I
+ 1.0 * b⁺(0) b⁺(0) b(0) b(0)
"""
bw_terms = sorted(self)
len_op = len(bw_terms)
bw_comm = BoseSentence({BoseWord({}): 0.0})
if len_op == 0:
return 1 * BoseWord({})
bw = self
left_pointer = 0
# The right pointer iterates through all operators in the BoseWord
for right_pointer in range(len_op):
# The right pointer finds the leftmost creation operator
if self[bw_terms[right_pointer]] == "+":
# This ensures that the left pointer starts at the leftmost annihilation term
if left_pointer == right_pointer:
left_pointer += 1
continue
# We shift the leftmost creation operator to the position of the left pointer
bs = bw.shift_operator(right_pointer, left_pointer)
bs_as_list = sorted(list(bs.items()), key=lambda x: len(x[0].keys()), reverse=True)
bw = bs_as_list[0][0]
for i in range(1, len(bs_as_list)):
bw_comm += bs_as_list[i][0] * bs_as_list[i][1]
# Left pointer now points to the new leftmost annihilation term
left_pointer += 1
# Sort BoseWord by indice
plus_terms = list(bw.items())[:left_pointer]
minus_terms = list(bw.items())[left_pointer:]
sorted_plus_terms = dict(sorted(plus_terms, key=lambda x: (x[0][1], x[0][0])))
sorted_minus_terms = dict(sorted(minus_terms, key=lambda x: (x[0][1], x[0][0])))
sorted_dict = {**sorted_plus_terms, **sorted_minus_terms}
bw_sorted_by_index = {}
for i, (k, v) in enumerate(sorted_dict.items()):
bw_sorted_by_index[(i, k[1])] = v
ordered_op = BoseWord(bw_sorted_by_index) + bw_comm.normal_order()
ordered_op.simplify(tol=1e-8)
return ordered_op
[docs] def shift_operator(self, initial_position, final_position):
r"""Shifts an operator in the BoseWord from ``initial_position`` to ``final_position`` by applying the bosonic commutation relations.
Args:
initial_position (int): the position of the operator to be shifted
final_position (int): the desired position of the operator
Returns:
BoseSentence: The ``BoseSentence`` obtained after applying the commutator relations.
Raises:
TypeError: if ``initial_position`` or ``final_position`` is not an integer
ValueError: if ``initial_position`` or ``final_position`` are outside the range ``[0, len(BoseWord) - 1]``
where ``len(BoseWord)`` is the number of operators in the BoseWord.
"""
if not isinstance(initial_position, int) or not isinstance(final_position, int):
raise TypeError("Positions must be integers.")
if initial_position < 0 or final_position < 0:
raise ValueError("Positions must be positive integers.")
if initial_position > len(self.sorted_dic) - 1 or final_position > len(self.sorted_dic) - 1:
raise ValueError("Positions are out of range.")
if initial_position == final_position:
return BoseSentence({self: 1})
bw = self
bs = BoseSentence({bw: 1})
delta = 1 if initial_position < final_position else -1
current = initial_position
while current != final_position:
indices = list(bw.sorted_dic.keys())
next = current + delta
curr_idx, curr_val = indices[current], bw[indices[current]]
next_idx, next_val = indices[next], bw[indices[next]]
# commuting identical terms
if curr_idx[1] == next_idx[1] and curr_val == next_val:
current += delta
continue
coeff = bs.pop(bw)
bw = dict(bw)
bw[(current, next_idx[1])] = next_val
bw[(next, curr_idx[1])] = curr_val
if curr_idx[1] != next_idx[1]:
del bw[curr_idx], bw[next_idx]
bw = BoseWord(bw)
# commutator is 0
if curr_val == next_val or curr_idx[1] != next_idx[1]:
current += delta
bs += coeff * bw
continue
# commutator is 1
_min = min(current, next)
_max = max(current, next)
items = list(bw.sorted_dic.items())
left = BoseWord({(i, key[1]): value for i, (key, value) in enumerate(items[:_min])})
middle = BoseWord(
{(i, key[1]): value for i, (key, value) in enumerate(items[_min : _max + 1])}
)
right = BoseWord(
{(i, key[1]): value for i, (key, value) in enumerate(items[_max + 1 :])}
)
terms = left * (1 + middle) * right
bs += coeff * terms
current += delta
return bs
# pylint: disable=useless-super-delegation
[docs]class BoseSentence(dict):
r"""Dictionary used to represent a Bose sentence, a linear combination of Bose words,
with the keys as BoseWord instances and the values correspond to coefficients.
>>> w1 = qml.BoseWord({(0, 0) : '+', (1, 1) : '-'})
>>> w2 = qml.BoseWord({(0, 1) : '+', (1, 2) : '-'})
>>> s = qml.BoseSentence({w1 : 1.2, w2: 3.1})
>>> print(s)
1.2 * b⁺(0) b(1)
+ 3.1 * b⁺(1) b(2)
"""
# override the arithmetic dunder methods for numpy arrays so that the
# methods defined on this class are used instead
# (i.e. ensure `np.array + BoseSentence` uses `BoseSentence.__radd__`
# instead of `np.array.__add__`)
__numpy_ufunc__ = None
__array_ufunc__ = None
def __init__(self, operator):
super().__init__(operator)
[docs] def adjoint(self):
r"""Return the adjoint of BoseSentence."""
adjoint_dict = {}
for key, value in self.items():
word = key.adjoint()
scalar = qml.math.conj(value)
adjoint_dict[word] = scalar
return BoseSentence(adjoint_dict)
@property
def wires(self):
r"""Return wires of the BoseSentence."""
return set().union(*(bw.wires for bw in self.keys()))
def __str__(self):
r"""String representation of a BoseSentence."""
if len(self) == 0:
return "0 * I"
return "\n+ ".join(f"{coeff} * {bw.to_string()}" for bw, coeff in self.items())
def __repr__(self):
r"""Terminal representation for BoseSentence."""
return f"BoseSentence({dict(self)})"
def __missing__(self, key):
r"""If the BoseSentence does not contain a BoseWord then the associated value will be 0."""
return 0.0
def __add__(self, other):
r"""Add a BoseSentence, BoseWord or constant to a BoseSentence by iterating over the
smaller one and adding its terms to the larger one."""
if not isinstance(other, (TensorLike, BoseWord, BoseSentence)):
raise TypeError(f"Cannot add {type(other)} to a BoseSentence.")
if qml.math.size(other) > 1:
raise ValueError(
f"Arithmetic Bose operations can only accept an array of length 1, "
f"but received {other} of length {len(other)}"
)
if isinstance(other, BoseWord):
other = BoseSentence({other: 1})
if isinstance(other, TensorLike):
other = BoseSentence({BoseWord({}): other})
smaller_bs, larger_bs = (
(self, copy(other)) if len(self) < len(other) else (other, copy(self))
)
for key in smaller_bs:
larger_bs[key] += smaller_bs[key]
return larger_bs
def __radd__(self, other):
"""Add a BoseSentence to a constant, i.e. `2 + BoseSentence({...})`"""
return self.__add__(other)
def __sub__(self, other):
r"""Subtract a BoseSentence, BoseWord or constant from a BoseSentence"""
if isinstance(other, BoseWord):
other = BoseSentence({other: -1})
return self.__add__(other)
if isinstance(other, BoseSentence):
other = BoseSentence(dict(zip(other.keys(), [-1 * v for v in other.values()])))
return self.__add__(other)
if not isinstance(other, TensorLike):
raise TypeError(f"Cannot subtract {type(other)} from a BoseSentence.")
if qml.math.size(other) > 1:
raise ValueError(
f"Arithmetic Bose operations can only accept an array of length 1, "
f"but received {other} of length {len(other)}"
)
other = BoseSentence({BoseWord({}): -1 * other}) # -constant * I
return self.__add__(other)
def __rsub__(self, other):
"""Subtract a BoseSentence to a constant, i.e. 2 - BoseSentence({...})"""
if not isinstance(other, TensorLike):
raise TypeError(f"Cannot subtract a BoseSentence from {type(other)}.")
if qml.math.size(other) > 1:
raise ValueError(
f"Arithmetic Bose operations can only accept an array of length 1, "
f"but received {other} of length {len(other)}"
)
self_bs = BoseSentence(dict(zip(self.keys(), [-1 * v for v in self.values()])))
other_bs = BoseSentence({BoseWord({}): other}) # constant * I
return self_bs + other_bs
def __mul__(self, other):
r"""Multiply two Bose sentences by iterating over each sentence and multiplying the Bose
words pair-wise"""
if isinstance(other, BoseWord):
other = BoseSentence({other: 1})
if isinstance(other, BoseSentence):
if (len(self) == 0) or (len(other) == 0):
return BoseSentence({BoseWord({}): 0})
product = BoseSentence({})
for bw1, coeff1 in self.items():
for bw2, coeff2 in other.items():
product[bw1 * bw2] += coeff1 * coeff2
return product
if not isinstance(other, TensorLike):
raise TypeError(f"Cannot multiply BoseSentence by {type(other)}.")
if qml.math.size(other) > 1:
raise ValueError(
f"Arithmetic Bose operations can only accept an array of length 1, "
f"but received {other} of length {len(other)}"
)
vals = [i * other for i in self.values()]
return BoseSentence(dict(zip(self.keys(), vals)))
def __rmul__(self, other):
r"""Reverse multiply a BoseSentence
Multiplies a BoseSentence "from the left" with an object that can't be modified
to support __mul__ for BoseSentence. Will be defaulted in for example when
multiplying ``2 * bose_sentence``, since the ``__mul__`` operator on an integer
will fail to multiply with a BoseSentence"""
if not isinstance(other, TensorLike):
raise TypeError(f"Cannot multiply {type(other)} by BoseSentence.")
if qml.math.size(other) > 1:
raise ValueError(
f"Arithmetic Bose operations can only accept an array of length 1, "
f"but received {other} of length {len(other)}"
)
vals = [i * other for i in self.values()]
return BoseSentence(dict(zip(self.keys(), vals)))
def __pow__(self, value):
r"""Exponentiate a Bose sentence to an integer power."""
if value < 0 or not isinstance(value, int):
raise ValueError("The exponent must be a positive integer.")
operator = BoseSentence({BoseWord({}): 1}) # 1 times Identity
for _ in range(value):
operator *= self
return operator
[docs] def simplify(self, tol=1e-8):
r"""Remove any BoseWords in the BoseSentence with coefficients less than the threshold
tolerance."""
items = list(self.items())
for bw, coeff in items:
if abs(coeff) <= tol:
del self[bw]
[docs] def normal_order(self):
r"""Convert a BoseSentence to its normal-ordered form.
>>> bw = qml.BoseWord({(0, 0): "-", (1, 0): "-", (2, 0): "+", (3, 0): "+"})
>>> bs = qml.BoseSentence({bw: 1})
>>> print(bw.normal_order())
4.0 * b⁺(0) b(0)
+ 2.0 * I
+ 1.0 * b⁺(0) b⁺(0) b(0) b(0)
"""
bose_sen_ordered = BoseSentence({})
for bw, coeff in self.items():
bose_word_ordered = bw.normal_order()
for bw_ord, coeff_ord in bose_word_ordered.items():
bose_sen_ordered += coeff_ord * coeff * bw_ord
return bose_sen_ordered
_modules/pennylane/bose/bosonic
Download Python script
Download Notebook
View on GitHub