Skip to content

Commit

Permalink
Update tapering.py to support operator arithmetic (#4252)
Browse files Browse the repository at this point in the history
* save changes to tapering

* add tapering update + tests

* more testing

* lint and tests

* more tests

* Apply suggestions from code review

Co-authored-by: soranjh <40344468+soranjh@users.noreply.github.com>

* added more tests

* fix bug

* lint

* lint

* lint

* lint + codecov

* more tests

* Added test and warning message for coverage

* Update pennylane/ops/functions/generator.py

Co-authored-by: Utkarsh <utkarshazad98@gmail.com>

* lint

* lint and codefactor

* lint + changelog

* fix tests + lint

* code review

---------

Co-authored-by: soranjh <40344468+soranjh@users.noreply.github.com>
Co-authored-by: Utkarsh <utkarshazad98@gmail.com>
  • Loading branch information
3 people authored Jun 16, 2023
1 parent ed8ada7 commit dbadf45
Show file tree
Hide file tree
Showing 12 changed files with 445 additions and 160 deletions.
4 changes: 4 additions & 0 deletions doc/releases/changelog-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,9 @@
* The new device interface is integrated with `qml.execute` for Tensorflow.
[(#4169)](https://github.com/PennyLaneAI/pennylane/pull/4169)

* Updated various qubit tapering methods to support operator arithmetic.
[(#4252)](https://github.com/PennyLaneAI/pennylane/pull/4252)

<h3>Breaking changes 💔</h3>

* All drawing methods changed their default value for the keyword argument `show_matrices` to `True`.
Expand Down Expand Up @@ -434,6 +437,7 @@
This release contains contributions from (in alphabetical order):
Venkatakrishnan AnushKrishna
Utkarsh Azad
Isaac De Vlugt,
Lillian M. A. Frederiksen,
Emiliano Godinez Ramirez
Expand Down
18 changes: 15 additions & 3 deletions pennylane/ops/functions/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ def generator(op, format="prefactor"):
``'observable'``, or ``'hamiltonian'``. See below for more details.
Returns:
.Observable or tuple[.Observable, float]: The returned generator, with format/type
.Operator or tuple[.Observable, float]: The returned generator, with format/type
dependent on the ``format`` argument.
* ``"prefactor"``: Return the generator as ``(obs, prefactor)`` (representing
Expand All @@ -132,6 +132,11 @@ def generator(op, format="prefactor"):
will always be converted into :class:`~.Hamiltonian` regardless of how ``op``
encodes the generator.
* ``"arithmetic"``: Similar to ``"hamiltonian"``, however the returned observable
will always be converted into an arithmetic operator. The returned generator may be
any type, including:
:class:`~.ops.op_math.SProd`, :class:`~.ops.op_math.Prod`, :class:`~.ops.op_math.Sum`, or the operator itself.
**Example**
Given an operation, ``qml.generator`` returns the generator representation:
Expand Down Expand Up @@ -159,7 +164,8 @@ def generator(op, format="prefactor"):
<Hamiltonian: terms=1, wires=[0]>
>>> qml.generator(qml.PhaseShift(0.1, wires=0), format="observable") # ouput will be a simplified obs where possible
Projector([1], wires=[0])
>>> qml.generator(op, format="arithmetic") # output is an instance of `SProd`
-0.5*(PauliX(wires=[0]))
"""
if op.num_params != 1:
raise ValueError(f"Operation {op.name} is not written in terms of a single parameter")
Expand All @@ -182,7 +188,13 @@ def generator(op, format="prefactor"):
if format == "hamiltonian":
return _generator_hamiltonian(gen, op)

if format == "arithmetic":
h = _generator_hamiltonian(gen, op)
return qml.dot(h.coeffs, h.ops)

if format == "observable":
return gen

raise ValueError("format must be one of ('prefactor', 'hamiltonian', 'observable')")
raise ValueError(
"format must be one of ('prefactor', 'hamiltonian', 'observable', 'arithmetic')"
)
6 changes: 3 additions & 3 deletions pennylane/ops/qubit/qchem_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -995,9 +995,9 @@ def generator(self):
w0, w1, w2, w3 = self.wires
return 0.25 * (
qml.PauliX(w0) @ qml.PauliZ(w1) @ qml.PauliY(w2)
- qml.PauliY(w0) @ qml.PauliZ(w1) @ qml.PauliX(w2)
+ qml.PauliX(w1) @ qml.PauliZ(w2) @ qml.PauliY(w3)
- qml.PauliY(w1) @ qml.PauliZ(w2) @ qml.PauliX(w3)
- (qml.PauliY(w0) @ qml.PauliZ(w1) @ qml.PauliX(w2))
+ (qml.PauliX(w1) @ qml.PauliZ(w2) @ qml.PauliY(w3))
- (qml.PauliY(w1) @ qml.PauliZ(w2) @ qml.PauliX(w3))
)

def __init__(self, phi, wires, do_queue=None, id=None):
Expand Down
38 changes: 23 additions & 15 deletions pennylane/pauli/conversion.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,6 @@ def pauli_decompose(
return Hamiltonian(coeffs, obs)


@singledispatch
def pauli_sentence(op):
"""Return the PauliSentence representation of an arithmetic operator or Hamiltonian.
Expand All @@ -156,68 +155,77 @@ def pauli_sentence(op):
Returns:
.PauliSentence: the PauliSentence representation of an arithmetic operator or Hamiltonian
"""
if (ps := op._pauli_rep) is not None: # pylint: disable=protected-access
return ps

return _pauli_sentence(op)


@singledispatch
def _pauli_sentence(op):
"""Private function to dispatch"""
raise ValueError(f"Op must be a linear combination of Pauli operators only, got: {op}")


@pauli_sentence.register
@_pauli_sentence.register
def _(op: PauliX):
return PauliSentence({PauliWord({op.wires[0]: X}): 1.0})


@pauli_sentence.register
@_pauli_sentence.register
def _(op: PauliY):
return PauliSentence({PauliWord({op.wires[0]: Y}): 1.0})


@pauli_sentence.register
@_pauli_sentence.register
def _(op: PauliZ):
return PauliSentence({PauliWord({op.wires[0]: Z}): 1.0})


@pauli_sentence.register
@_pauli_sentence.register
def _(op: Identity): # pylint:disable=unused-argument
return PauliSentence({PauliWord({}): 1.0})


@pauli_sentence.register
@_pauli_sentence.register
def _(op: Tensor):
if not is_pauli_word(op):
raise ValueError(f"Op must be a linear combination of Pauli operators only, got: {op}")

factors = (pauli_sentence(factor) for factor in op.obs)
factors = (_pauli_sentence(factor) for factor in op.obs)
return reduce(lambda a, b: a * b, factors)


@pauli_sentence.register
@_pauli_sentence.register
def _(op: Prod):
factors = (pauli_sentence(factor) for factor in op)
factors = (_pauli_sentence(factor) for factor in op)
return reduce(lambda a, b: a * b, factors)


@pauli_sentence.register
@_pauli_sentence.register
def _(op: SProd):
ps = pauli_sentence(op.base)
ps = _pauli_sentence(op.base)
for pw, coeff in ps.items():
ps[pw] = coeff * op.scalar
return ps


@pauli_sentence.register
@_pauli_sentence.register
def _(op: Hamiltonian):
if not all(is_pauli_word(o) for o in op.ops):
raise ValueError(f"Op must be a linear combination of Pauli operators only, got: {op}")

summands = []
for coeff, term in zip(*op.terms()):
ps = pauli_sentence(term)
ps = _pauli_sentence(term)
for pw, sub_coeff in ps.items():
ps[pw] = coeff * sub_coeff
summands.append(ps)

return reduce(lambda a, b: a + b, summands) if len(summands) > 0 else PauliSentence()


@pauli_sentence.register
@_pauli_sentence.register
def _(op: Sum):
summands = (pauli_sentence(summand) for summand in op)
summands = (_pauli_sentence(summand) for summand in op)
return reduce(lambda a, b: a + b, summands)
41 changes: 41 additions & 0 deletions pennylane/pauli/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1504,6 +1504,47 @@ def _binary_matrix(terms, num_qubits, wire_map=None):
return binary_matrix


def _binary_matrix_from_pws(terms, num_qubits, wire_map=None):
r"""Get a binary matrix representation from a list of PauliWords where each row corresponds to a
Pauli term, which is represented by a concatenation of Z and X vectors.
Args:
terms (Iterable[~.PauliWord]): operators defining the Hamiltonian
num_qubits (int): number of wires required to define the Hamiltonian
wire_map (dict): dictionary containing all wire labels used in the Pauli words as keys, and
unique integer labels as their values
Returns:
array[int]: binary matrix representation of the Hamiltonian of shape
:math:`len(terms) * 2*num_qubits`
**Example**
>>> from pennylane.pauli import PauliWord
>>> wire_map = {'a':0, 'b':1, 'c':2, 'd':3}
>>> terms = [PauliWord({'a': 'Z', 'b': 'X'}),
... PauliWord({'a': 'Z', 'c': 'Y'}),
... PauliWord({'a': 'X', 'd': 'Y'})]
>>> _binary_matrix_from_pws(terms, 4, wire_map=wire_map)
array([[1, 0, 0, 0, 0, 1, 0, 0],
[1, 0, 1, 0, 0, 0, 1, 0],
[0, 0, 0, 1, 1, 0, 0, 1]])
"""
if wire_map is None:
all_wires = qml.wires.Wires.all_wires([term.wires for term in terms], sort=True)
wire_map = {i: c for c, i in enumerate(all_wires)}

binary_matrix = np.zeros((len(terms), 2 * num_qubits), dtype=int)
for idx, pw in enumerate(terms):
for wire, pauli_op in pw.items():
if pauli_op in ["X", "Y"]:
binary_matrix[idx][wire_map[wire] + num_qubits] = 1
if pauli_op in ["Z", "Y"]:
binary_matrix[idx][wire_map[wire]] = 1

return binary_matrix


@lru_cache
def _get_pauli_map(n):
r"""Return a list of Pauli operator objects acting on wires `0` up to `n`.
Expand Down
2 changes: 1 addition & 1 deletion pennylane/qchem/observable_hf.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def qubit_observable(o_ferm, cutoff=1.0e-12):
cutoff (float): cutoff value for discarding the negligible terms
Returns:
Union[~.Hamiltonian, ~.Operator]: Simplified PennyLane Hamiltonian
(~.Operator): Simplified PennyLane Hamiltonian
**Example**
Expand Down
Loading

0 comments on commit dbadf45

Please sign in to comment.