diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index dcb051a1ef4..6b88ed4cf4f 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -40,6 +40,9 @@ `from pennylane.capture.primitives import *`. [(#6129)](https://github.com/PennyLaneAI/pennylane/pull/6129) +* `FermiWord` class now has a method to apply anti-commutator relations. + [(#6196)](https://github.com/PennyLaneAI/pennylane/pull/6196) + * `FermiWord` and `FermiSentence` classes now have methods to compute adjoints. [(#6166)](https://github.com/PennyLaneAI/pennylane/pull/6166) diff --git a/pennylane/fermi/fermionic.py b/pennylane/fermi/fermionic.py index 193cc0b4be8..b5f03020780 100644 --- a/pennylane/fermi/fermionic.py +++ b/pennylane/fermi/fermionic.py @@ -324,6 +324,110 @@ def to_mat(self, n_orbitals=None, format="dense", buffer_size=None): wire_order=list(range(largest_order)), format=format, buffer_size=buffer_size ) + def shift_operator(self, initial_position, final_position): + r"""Shifts an operator in the FermiWord from ``initial_position`` to ``final_position`` by applying the fermionic anti-commutation relations. + + There are three `anti-commutator relations `_: + + .. math:: + \left\{ a_i, a_j \right\} = 0, \quad \left\{ a^{\dagger}_i, a^{\dagger}_j \right\} = 0, \quad \left\{ a_i, a^{\dagger}_j \right\} = \delta_{ij}, + + + where + + .. math:: + \left\{a_i, a_j \right\} = a_i a_j + a_j a_i, + + and + + .. math:: + \delta_{ij} = \begin{cases} 1 & i = j \\ 0 & i \neq j \end{cases}. + + Args: + initial_position (int): The position of the operator to be shifted. + final_position (int): The desired position of the operator. + + Returns: + FermiSentence: The ``FermiSentence`` obtained after applying the anti-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(fermiword) - 1]`` + where ``len(fermiword)`` is the number of operators in the FermiWord. + + + **Example** + + >>> w = qml.fermi.FermiWord({(0, 0): '+', (1, 1): '-'}) + >>> w.shift_operator(0, 1) + -1 * a(1) a⁺(0) + """ + + 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 FermiSentence({self: 1}) + + fw = self + fs = FermiSentence({fw: 1}) + delta = 1 if initial_position < final_position else -1 + current = initial_position + + while current != final_position: + indices = list(fw.sorted_dic.keys()) + next = current + delta + curr_idx, curr_val = indices[current], fw[indices[current]] + next_idx, next_val = indices[next], fw[indices[next]] + + # commuting identical terms + if curr_idx[1] == next_idx[1] and curr_val == next_val: + current += delta + continue + + coeff = fs.pop(fw) + + fw = dict(fw) + fw[(current, next_idx[1])] = next_val + fw[(next, curr_idx[1])] = curr_val + + if curr_idx[1] != next_idx[1]: + del fw[curr_idx], fw[next_idx] + + fw = FermiWord(fw) + + # anti-commutator is 0 + if curr_val == next_val or curr_idx[1] != next_idx[1]: + current += delta + fs += -coeff * fw + continue + + # anti-commutator is 1 + _min = min(current, next) + _max = max(current, next) + items = list(fw.sorted_dic.items()) + + left = FermiWord({(i, key[1]): value for i, (key, value) in enumerate(items[:_min])}) + middle = FermiWord( + {(i, key[1]): value for i, (key, value) in enumerate(items[_min : _max + 1])} + ) + right = FermiWord( + {(i, key[1]): value for i, (key, value) in enumerate(items[_max + 1 :])} + ) + + terms = left * (1 - middle) * right + fs += coeff * terms + + current += delta + + return fs + # pylint: disable=useless-super-delegation class FermiSentence(dict): diff --git a/tests/fermi/test_fermionic.py b/tests/fermi/test_fermionic.py index dc61295115a..4e028ad9875 100644 --- a/tests/fermi/test_fermionic.py +++ b/tests/fermi/test_fermionic.py @@ -53,6 +53,62 @@ fw7 = FermiWord({(0, 10): "-", (1, 30): "+", (2, 0): "-", (3, 400): "+"}) fw7_dag = FermiWord({(0, 400): "-", (1, 0): "+", (2, 30): "-", (3, 10): "+"}) +fw8 = FermiWord({(0, 0): "-", (1, 1): "+"}) +fw8c = FermiWord({(0, 1): "+", (1, 0): "-"}) +fw8cs = FermiSentence({fw8c: -1}) + +fw9 = FermiWord({(0, 0): "-", (1, 1): "-"}) +fw9c = FermiWord({(0, 1): "-", (1, 0): "-"}) +fw9cs = FermiSentence({fw9c: -1}) + +fw10 = FermiWord({(0, 0): "+", (1, 1): "+"}) +fw10c = FermiWord({(0, 1): "+", (1, 0): "+"}) +fw10cs = FermiSentence({fw10c: -1}) + +fw11 = FermiWord({(0, 0): "-", (1, 0): "+"}) +fw11c = FermiWord({(0, 0): "+", (1, 0): "-"}) +fw11cs = 1 + FermiSentence({fw11c: -1}) + +fw12 = FermiWord({(0, 0): "+", (1, 0): "+"}) +fw12c = FermiWord({(0, 0): "+", (1, 0): "+"}) +fw12cs = FermiSentence({fw12c: 1}) + +fw13 = FermiWord({(0, 0): "-", (1, 0): "-"}) +fw13c = FermiWord({(0, 0): "-", (1, 0): "-"}) +fw13cs = FermiSentence({fw13c: 1}) + +fw14 = FermiWord({(0, 0): "+", (1, 0): "-"}) +fw14c = FermiWord({(0, 0): "-", (1, 0): "+"}) +fw14cs = 1 + FermiSentence({fw14c: -1}) + +fw15 = FermiWord({(0, 0): "-", (1, 1): "+", (2, 2): "+"}) +fw15c = FermiWord({(0, 1): "+", (1, 0): "-", (2, 2): "+"}) +fw15cs = FermiSentence({fw15c: -1}) + +fw16 = FermiWord({(0, 0): "-", (1, 1): "+", (2, 2): "-"}) +fw16c = FermiWord({(0, 0): "-", (1, 2): "-", (2, 1): "+"}) +fw16cs = FermiSentence({fw16c: -1}) + +fw17 = FermiWord({(0, 0): "-", (1, 0): "+", (2, 2): "-"}) +fw17c1 = FermiWord({(0, 2): "-"}) +fw17c2 = FermiWord({(0, 0): "+", (1, 0): "-", (2, 2): "-"}) +fw17cs = fw17c1 - fw17c2 + +fw18 = FermiWord({(0, 0): "+", (1, 1): "+", (2, 2): "-", (3, 3): "-"}) +fw18c = FermiWord({(0, 0): "+", (1, 3): "-", (2, 1): "+", (3, 2): "-"}) +fw18cs = FermiSentence({fw18c: 1}) + +fw19 = FermiWord({(0, 0): "+", (1, 1): "+", (2, 2): "-", (3, 2): "+"}) +fw19c1 = FermiWord({(0, 0): "+", (1, 1): "+"}) +fw19c2 = FermiWord({(0, 2): "+", (1, 0): "+", (2, 1): "+", (3, 2): "-"}) +fw19cs = FermiSentence({fw19c1: 1, fw19c2: -1}) + +fw20 = FermiWord({(0, 0): "-", (1, 0): "+", (2, 1): "-", (3, 0): "-", (4, 0): "+"}) +fw20c1 = FermiWord({(0, 0): "-", (1, 0): "+", (2, 1): "-"}) +fw20c2 = FermiWord({(0, 0): "+", (1, 1): "-", (2, 0): "-"}) +fw20c3 = FermiWord({(0, 0): "+", (1, 0): "-", (2, 0): "+", (3, 1): "-", (4, 0): "-"}) +fw20cs = fw20c1 + fw20c2 - fw20c3 + class TestFermiWord: def test_missing(self): @@ -167,6 +223,41 @@ def test_to_mat_error(self): with pytest.raises(ValueError, match="n_orbitals cannot be smaller than 2"): fw1.to_mat(n_orbitals=1) + tup_fw_shift = ( + (fw8, 0, 1, fw8cs), + (fw9, 0, 1, fw9cs), + (fw10, 0, 1, fw10cs), + (fw11, 0, 1, fw11cs), + (fw12, 0, 1, fw12cs), + (fw13, 0, 1, fw13cs), + (fw14, 0, 1, fw14cs), + (fw15, 0, 1, fw15cs), + (fw16, 1, 2, fw16cs), + (fw17, 0, 1, fw17cs), + (fw8, 0, 0, FermiSentence({fw8: 1})), + (fw8, 1, 0, fw8cs), + (fw11, 1, 0, fw11cs), + (fw18, 3, 1, fw18cs), + (fw19, 3, 0, fw19cs), + (fw20, 4, 0, fw20cs), + ) + + @pytest.mark.parametrize("fw, i, j, fs", tup_fw_shift) + def test_shift_operator(self, fw, i, j, fs): + """Test that the shift_operator method correctly applies the anti-commutator relations.""" + assert fw.shift_operator(i, j) == fs + + def test_shift_operator_errors(self): + """Test that the shift_operator method correctly raises exceptions.""" + with pytest.raises(TypeError, match="Positions must be integers."): + fw8.shift_operator(0.5, 1) + + with pytest.raises(ValueError, match="Positions must be positive integers."): + fw8.shift_operator(-1, 0) + + with pytest.raises(ValueError, match="Positions are out of range."): + fw8.shift_operator(1, 2) + tup_fw_dag = ( (fw1, fw1_dag), (fw2, fw2_dag), @@ -588,6 +679,15 @@ def test_array_must_not_exceed_length_1(self, method_name): } ) +fs8 = fw8 + fw9 +fs8c = fw8 + fw9cs + +fs9 = 1.3 * fw8 + (1.4 + 3.8j) * fw9 +fs9c = 1.3 * fw8 + (1.4 + 3.8j) * fw9cs + +fs10 = -1.3 * fw11 + 2.3 * fw9 +fs10c = -1.3 * fw11cs + 2.3 * fw9 + class TestFermiSentence: def test_missing(self):