# Source code for pennylane.fermi.fermionic

# Copyright 2018-2023 Xanadu Quantum Technologies Inc.

# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at

# Unless required by applicable law or agreed to in writing, software
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
"""The fermionic representation classes and functions."""
import re
from copy import copy
from numbers import Number

from numpy import ndarray

import pennylane as qml

[docs]class FermiWord(dict):
r"""Immutable dictionary used to represent a Fermi word, a product of fermionic 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 Fermi word and the second integer
represents the orbital it acts on. The values of the dictionary are one of '+' or '-'
symbols that denote creation and annihilation operators, respectively. The operator
:math:a^{\dagger}_0 a_1 can then be constructed as

>>> w = FermiWord({(0, 0) : '+', (1, 1) : '-'})
>>> w
a⁺(0) a(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 + FermiWord uses FermiWord.__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)

@property
def wires(self):
r"""Return wires in a FermiWord."""
return set(i[1] for i in self.sorted_dic.keys())

def __missing__(self, key):
r"""Return empty string for a missing key in FermiWord."""
return ""

[docs]    def update(self, item):
r"""Restrict updating FermiWord after instantiation."""
raise TypeError("FermiWord object does not support assignment")

def __setitem__(self, key, item):
r"""Restrict setting items after instantiation."""
raise TypeError("FermiWord object does not support assignment")

def __reduce__(self):
r"""Defines how to pickle and unpickle a FermiWord. Otherwise, un-pickling
would cause __setitem__ to be called, which is forbidden on PauliWord.
"""
return FermiWord, (dict(self),)

def __copy__(self):
r"""Copy the FermiWord instance."""
return FermiWord(dict(self.items()))

def __deepcopy__(self, memo):
r"""Deep copy the FermiWord instance."""
res = self.__copy__()
memo[id(self)] = res
return res

def __hash__(self):
r"""Hash value of a FermiWord."""
return hash(frozenset(self.items()))

[docs]    def to_string(self):
r"""Return a compact string representation of a FermiWord. 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 = FermiWord({(0, 0) : '+', (1, 1) : '-'})
>>> w.to_string()
a⁺(0) a(1)
"""
if len(self) == 0:
return "I"

symbol_map = {"+": "\u207a", "-": ""}

string = " ".join(
[
"a" + 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 FermiWord."""
return f"{self.to_string()}"

def __repr__(self):
r"""Terminal representation of a FermiWord"""
return str(self)

"""Add a FermiSentence, FermiWord or constant to a FermiWord. Converts both
elements into FermiSentences, and uses the FermiSentence __add__
method"""

self_fs = FermiSentence({self: 1.0})

if isinstance(other, FermiSentence):
return self_fs + other

if isinstance(other, FermiWord):
return self_fs + FermiSentence({other: 1.0})

if isinstance(other, (Number, ndarray)):
if isinstance(other, ndarray) and qml.math.size(other) > 1:
raise ValueError(
f"Arithmetic Fermi operations can only accept an array of length 1, "
f"but received {other} of length {len(other)}"
)
return self_fs + FermiSentence({FermiWord({}): other})

raise TypeError(f"Cannot add {type(other)} to a FermiWord.")

"""Add a FermiWord to a constant, i.e. 2 + FermiWord({...})"""

if isinstance(other, (Number, ndarray)):

raise TypeError(f"Cannot add a FermiWord to {type(other)}.")

def __sub__(self, other):
"""Subtract a FermiSentence, FermiWord or constant from a FermiWord. Converts both
elements into FermiSentences (with negative coefficient for other), and

self_fs = FermiSentence({self: 1.0})

if isinstance(other, FermiWord):
return self_fs + FermiSentence({other: -1.0})

if isinstance(other, FermiSentence):
other_fs = FermiSentence(dict(zip(other.keys(), [-v for v in other.values()])))
return self_fs + other_fs

if isinstance(other, (Number, ndarray)):
if isinstance(other, ndarray) and qml.math.size(other) > 1:
raise ValueError(
f"Arithmetic Fermi operations can only accept an array of length 1, "
f"but received {other} of length {len(other)}"
)
return self_fs + FermiSentence({FermiWord({}): -1 * other})  # -constant * I

raise TypeError(f"Cannot subtract {type(other)} from a FermiWord.")

def __rsub__(self, other):
"""Subtract a FermiWord to a constant, i.e. 2 - FermiWord({...})"""
if isinstance(other, (Number, ndarray)):
if isinstance(other, ndarray) and qml.math.size(other) > 1:
raise ValueError(
f"Arithmetic Fermi operations can only accept an array of length 1, "
f"but received {other} of length {len(other)}"
)
self_fs = FermiSentence({self: -1.0})
other_fs = FermiSentence({FermiWord({}): other})
return self_fs + other_fs

raise TypeError(f"Cannot subtract a FermiWord from {type(other)}.")

def __mul__(self, other):
r"""Multiply a FermiWord with another FermiWord, a FermiSentence, or a constant.

>>> w = FermiWord({(0, 0) : '+', (1, 1) : '-'})
>>> w * w
a⁺(0) a(1) a⁺(0) a(1)
"""

if isinstance(other, FermiWord):
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 FermiWord(dict_self)

if isinstance(other, FermiSentence):
return FermiSentence({self: 1}) * other

if isinstance(other, (Number, ndarray)):
if isinstance(other, ndarray) and qml.math.size(other) > 1:
raise ValueError(
f"Arithmetic Fermi operations can only accept an array of length 1, "
f"but received {other} of length {len(other)}"
)
return FermiSentence({self: other})

raise TypeError(f"Cannot multiply FermiWord by {type(other)}.")

def __rmul__(self, other):
r"""Reverse multiply a FermiWord

Multiplies a FermiWord "from the left" with an object that can't be modified
to support __mul__ for FermiWord. Will be defaulted in for example
2 * FermiWord({(0, 0): "+"}), where the __mul__ operator on an integer
will fail to multiply with a FermiWord"""

if isinstance(other, (Number, ndarray)):
if isinstance(other, ndarray) and qml.math.size(other) > 1:
raise ValueError(
f"Arithmetic Fermi operations can only accept an array of length 1, "
f"but received {other} of length {len(other)}"
)
return FermiSentence({self: other})

raise TypeError(f"Cannot multiply FermiWord by {type(other)}.")

def __pow__(self, value):
r"""Exponentiate a Fermi word to an integer power.

>>> w = FermiWord({(0, 0) : '+', (1, 1) : '-'})
>>> w**3
a⁺(0) a(1) a⁺(0) a(1) a⁺(0) a(1)
"""

if value < 0 or not isinstance(value, int):
raise ValueError("The exponent must be a positive integer.")

operator = FermiWord({})

for _ in range(value):
operator *= self

return operator

[docs]    def to_mat(self, n_orbitals=None):
r"""Return the matrix representation.

Args:
n_orbitals (int or None): Number of orbitals. If not provided, it will be inferred from
the largest orbital index in the Fermi operator.

Returns:
NumpyArray: Matrix representation of the :class:~.FermiWord.

**Example**

>>> w = FermiWord({(0, 0): '+', (1, 1): '-'})
>>> w.to_mat()
array([0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
[0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
[0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j],
[0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j])
"""
largest_orb_id = max(key[1] for key in self.keys()) + 1
if n_orbitals and n_orbitals < largest_orb_id:
raise ValueError(
f"n_orbitals cannot be smaller than {largest_orb_id}, got: {n_orbitals}."
)

largest_order = n_orbitals or largest_orb_id
mat = qml.jordan_wigner(self, ps=True).to_mat(wire_order=list(range(largest_order)))

return mat

# pylint: disable=useless-super-delegation
[docs]class FermiSentence(dict):
r"""Immutable dictionary used to represent a Fermi sentence, a linear combination of Fermi words, with the keys
as FermiWord instances and the values correspond to coefficients.

>>> w1 = FermiWord({(0, 0) : '+', (1, 1) : '-'})
>>> w2 = FermiWord({(0, 1) : '+', (1, 2) : '-'})
>>> s = FermiSentence({w1 : 1.2, w2: 3.1})
>>> s
1.2 * a⁺(0) a(1)
+ 3.1 * a⁺(1) a(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 + FermiSentence uses FermiSentence.__radd__ instead of np.array.__add__)
__numpy_ufunc__ = None
__array_ufunc__ = None

def __init__(self, operator):
super().__init__(operator)

@property
def wires(self):
r"""Return wires of the FermiSentence."""
return set().union(*(fw.wires for fw in self.keys()))

def __str__(self):
r"""String representation of a FermiSentence."""
if len(self) == 0:
return "0 * I"
return "\n+ ".join(f"{coeff} * {fw.to_string()}" for fw, coeff in self.items())

def __repr__(self):
r"""Terminal representation for FermiSentence."""
return str(self)

def __missing__(self, key):
r"""If the FermiSentence does not contain a FermiWord then the associated value will be 0."""
return 0.0

r"""Add a FermiSentence, FermiWord or constant to a FermiSentence by iterating over the
smaller one and adding its terms to the larger one."""

# ensure other is FermiSentence
if isinstance(other, FermiWord):
other = FermiSentence({other: 1})
if isinstance(other, Number):
other = FermiSentence({FermiWord({}): other})
if isinstance(other, ndarray):
if qml.math.size(other) > 1:
raise ValueError(
f"Arithmetic Fermi operations can only accept an array of length 1, "
f"but received {other} of length {len(other)}"
)
other = FermiSentence({FermiWord({}): other})

if isinstance(other, FermiSentence):
smaller_fs, larger_fs = (
(self, copy(other)) if len(self) < len(other) else (other, copy(self))
)
for key in smaller_fs:
larger_fs[key] += smaller_fs[key]

return larger_fs

raise TypeError(f"Cannot add {type(other)} to a FermiSentence.")

"""Add a FermiSentence to a constant, i.e. 2 + FermiSentence({...})"""

if isinstance(other, (Number, ndarray)):

raise TypeError(f"Cannot add a FermiSentence to {type(other)}.")

def __sub__(self, other):
r"""Subtract a FermiSentence, FermiWord or constant from a FermiSentence"""
if isinstance(other, FermiWord):
other = FermiSentence({other: -1})

if isinstance(other, Number):
other = FermiSentence({FermiWord({}): -1 * other})  # -constant * I

if isinstance(other, ndarray):
if qml.math.size(other) > 1:
raise ValueError(
f"Arithmetic Fermi operations can only accept an array of length 1, "
f"but received {other} of length {len(other)}"
)
other = FermiSentence({FermiWord({}): -1 * other})  # -constant * I

if isinstance(other, FermiSentence):
other = FermiSentence(dict(zip(other.keys(), [-1 * v for v in other.values()])))

raise TypeError(f"Cannot subtract {type(other)} from a FermiSentence.")

def __rsub__(self, other):
"""Subtract a FermiSentence to a constant, i.e.

>>> 2 - FermiSentence({...})
"""

if isinstance(other, (Number, ndarray)):
if isinstance(other, ndarray) and qml.math.size(other) > 1:
raise ValueError(
f"Arithmetic Fermi operations can only accept an array of length 1, "
f"but received {other} of length {len(other)}"
)
self_fs = FermiSentence(dict(zip(self.keys(), [-1 * v for v in self.values()])))
other_fs = FermiSentence({FermiWord({}): other})  # constant * I
return self_fs + other_fs

raise TypeError(f"Cannot subtract a FermiSentence from {type(other)}.")

def __mul__(self, other):
r"""Multiply two Fermi sentences by iterating over each sentence and multiplying the Fermi
words pair-wise"""

if isinstance(other, FermiWord):
other = FermiSentence({other: 1})

if isinstance(other, FermiSentence):
if (len(self) == 0) or (len(other) == 0):
return FermiSentence({FermiWord({}): 0})

product = FermiSentence({})

for fw1, coeff1 in self.items():
for fw2, coeff2 in other.items():
product[fw1 * fw2] += coeff1 * coeff2

return product

if isinstance(other, (Number, ndarray)):
if isinstance(other, ndarray) and qml.math.size(other) > 1:
raise ValueError(
f"Arithmetic Fermi 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 FermiSentence(dict(zip(self.keys(), vals)))

raise TypeError(f"Cannot multiply FermiSentence by {type(other)}.")

def __rmul__(self, other):
r"""Reverse multiply a FermiSentence

Multiplies a FermiSentence "from the left" with an object that can't be modified
to support __mul__ for FermiSentence. Will be defaulted in for example when
multiplying 2 * fermi_sentence, since the __mul__ operator on an integer
will fail to multiply with a FermiSentence"""

if isinstance(other, (Number, ndarray)):
if isinstance(other, ndarray) and qml.math.size(other) > 1:
raise ValueError(
f"Arithmetic Fermi 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 FermiSentence(dict(zip(self.keys(), vals)))

raise TypeError(f"Cannot multiply {type(other)} by FermiSentence.")

def __pow__(self, value):
r"""Exponentiate a Fermi sentence to an integer power."""
if value < 0 or not isinstance(value, int):
raise ValueError("The exponent must be a positive integer.")

operator = FermiSentence({FermiWord({}): 1})  # 1 times Identity

for _ in range(value):
operator *= self

return operator

[docs]    def simplify(self, tol=1e-8):
r"""Remove any FermiWords in the FermiSentence with coefficients less than the threshold
tolerance."""
items = list(self.items())
for fw, coeff in items:
if abs(coeff) <= tol:
del self[fw]

[docs]    def to_mat(self, n_orbitals=None):
r"""Return the matrix representation.

Args:
n_orbitals (int or None): Number of orbitals. If not provided, it will be inferred from
the largest orbital index in the Fermi operator

Returns:
NumpyArray: Matrix representation of the :class:~.FermiSentence.

**Example**

>>> fs = FermiSentence({FermiWord({(0, 0): "+", (1, 1): "-"}): 1.2, FermiWord({(0, 0): "+", (1, 0): "-"}): 3.1})
>>> fs.to_mat()
array([0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j],
[0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j],
[0.0 + 0.0j, 1.2 + 0.0j, 3.1 + 0.0j, 0.0 + 0.0j],
[0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 3.1 + 0.0j])
"""
largest_orb_id = max(key[1] for fermi_word in self.keys() for key in fermi_word.keys()) + 1
if n_orbitals and n_orbitals < largest_orb_id:
raise ValueError(
f"n_orbitals cannot be smaller than {largest_orb_id}, got: {n_orbitals}."
)

largest_order = n_orbitals or largest_orb_id
mat = qml.jordan_wigner(self, ps=True).to_mat(wire_order=list(range(largest_order)))

return mat

[docs]def from_string(fermi_string):
r"""Return a fermionic operator object from its string representation.

The string representation is a compact format that uses the orbital index and '+' or '-'
symbols to indicate creation and annihilation operators, respectively. For instance, the string
representation for the operator :math:a^{\dagger}_0 a_1 a^{\dagger}_0 a_1 is
'0+ 1- 0+ 1-'. The '-' symbols can be optionally dropped such that '0+ 1 0+ 1'
represents the same operator. The format commonly used in OpenFermion to represent the same
operator, '0^ 1 0^ 1' , is also supported.

Args:
fermi_string (str): string representation of the fermionic object

Returns:
FermiWord: the fermionic operator object

**Example**

>>> from_string('0+ 1- 0+ 1-')
a⁺(0) a(1) a⁺(0) a(1)

>>> from_string('0+ 1 0+ 1')
a⁺(0) a(1) a⁺(0) a(1)

>>> from_string('0^ 1 0^ 1')
a⁺(0) a(1) a⁺(0) a(1)

>>> op1 = FermiC(0) * FermiA(1) * FermiC(2) * FermiA(3)
>>> op2 = from_string('0+ 1- 2+ 3-')
>>> op1 == op2
True
"""
if fermi_string.isspace() or not fermi_string:
return FermiWord({})

fermi_string = " ".join(fermi_string.split())

if not all(s.isdigit() or s in ["+", "-", "^", " "] for s in fermi_string):
raise ValueError(f"Invalid character encountered in string {fermi_string}.")

fermi_string = re.sub(r"\^", "+", fermi_string)

operators = [i + "-" if i[-1] not in "+-" else i for i in re.split(r"\s", fermi_string)]

return FermiWord({(i, int(s[:-1])): s[-1] for i, s in enumerate(operators)})

def _to_string(fermi_op, of=False):
r"""Return a string representation of the :class:~.FermiWord object.

Args:
fermi_op (FermiWord): the fermionic operator
of (bool): whether to return a string representation in the same style as OpenFermion using
the shorthand: 'q^' = a^\dagger_q 'q' = a_q. Each operator in the word is
represented by the number of the wire it operates on

Returns:
(str): a string representation of the :class:~.FermiWord object

**Example**

>>> w = FermiWord({(0, 0) : '+', (1, 1) : '-'})
>>> _to_string(w)
'0+ 1-'

>>> w = FermiWord({(0, 0) : '+', (1, 1) : '-'})
>>> _to_string(w, of=True)
'0^ 1'
"""
if not isinstance(fermi_op, FermiWord):
raise ValueError(f"fermi_op must be a FermiWord, got: {type(fermi_op)}")

pl_to_of_map = {"+": "^", "-": ""}

if len(fermi_op) == 0:
return "I"

op_list = ["" for _ in range(len(fermi_op))]
for loc, wire in fermi_op:
if of:
op_str = str(wire) + pl_to_of_map[fermi_op[(loc, wire)]]
else:
op_str = str(wire) + fermi_op[(loc, wire)]

op_list[loc] += op_str

return " ".join(op_list).rstrip()

# pylint: disable=too-few-public-methods
[docs]class FermiC(FermiWord):
r"""FermiC(orbital)
The fermionic creation operator :math:a^{\dagger}

For instance, the operator qml.FermiC(2) denotes :math:a^{\dagger}_2. This operator applied
to :math:\ket{0000} gives :math:\ket{0010}.

Args:
orbital(int): the non-negative integer indicating the orbital the operator acts on.

.. note:: While the FermiC class represents a mathematical operator, it is not a PennyLane qubit :class:~.Operator.

.. seealso:: :class:~pennylane.FermiA

**Example**

To construct the operator :math:a^{\dagger}_0:

>>> FermiC(0)
a⁺(0)

This can be combined with the annihilation operator :class:~pennylane.FermiA. For example,
:math:a^{\dagger}_0 a_1 a^{\dagger}_2 a_3 can be constructed as:

>>> qml.FermiC(0) * qml.FermiA(1) * qml.FermiC(2) * qml.FermiA(3)
a⁺(0) a(1) a⁺(2) a(3)
"""

def __init__(self, orbital):
if not isinstance(orbital, int) or orbital < 0:
raise ValueError(
f"FermiC: expected a single, positive integer value for orbital, but received {orbital}"
)
operator = {(0, orbital): "+"}
super().__init__(operator)

[docs]class FermiA(FermiWord):
r"""FermiA(orbital)
The fermionic annihilation operator :math:a

For instance, the operator qml.FermiA(2) denotes :math:a_2. This operator applied
to :math:\ket{0010} gives :math:\ket{0000}.

Args:
orbital(int): the non-negative integer indicating the orbital the operator acts on.

.. note:: While the FermiA class represents a mathematical operator, it is not a PennyLane qubit :class:~.Operator.

.. seealso:: :class:~pennylane.FermiC

**Example**

To construct the operator :math:a_0:

>>> FermiA(0)
a(0)

This can be combined with the creation operator :class:~pennylane.FermiC. For example,
:math:a^{\dagger}_0 a_1 a^{\dagger}_2 a_3 can be constructed as:

>>> qml.FermiC(0) * qml.FermiA(1) * qml.FermiC(2) * qml.FermiA(3)
a⁺(0) a(1) a⁺(2) a(3)
"""

def __init__(self, orbital):
if not isinstance(orbital, int) or orbital < 0:
raise ValueError(
f"FermiA: expected a single, positive integer value for orbital, but received {orbital}"
)
operator = {(0, orbital): "-"}
super().__init__(operator)


Using PennyLane

Release news

Development

API

Internals