Repository: PennyLaneAI/pennylane
Pull request #3586 (2023-01-02 11:37:17)

Add `dot` function
This function returns the dot product between a vector and a list of operators. It contains a `pauli` kwarg to decide wether to return a `PauliSentence` or a `Sum` operator.

Building hamiltonians using the `dot` function is **FASTER** than instantiating a `Hamiltonian`. Moreover, the hamiltonian is already simplified! See example below:

```python
import time
from numpy.random import randint, random
import pennylane as qml
from pennylane.ops import Hamiltonian

N = 100000
num_wires = 10
coeffs = (random(size=N) * 2) - 1
pauli_ops = [qml.PauliX, qml.PauliY, qml.PauliZ]
ops = [pauli_ops[randint(0, 3)](randint(0, num_wires)) for _ in range(N)]

t0 = time.time()
sum_op = qml.ops.dot(coeffs, ops)
t1 = time.time()
print(f"Sum initialization time: {t1 - t0}")

t0 = time.time()
pauli_sentence = qml.ops.dot(coeffs, ops, pauli=True)
t1 = time.time()
print(f"PauliSentence initialization time: {t1 - t0}")
h_ps = pauli_sentence.hamiltonian()
print(f"PauliSentence number of operations: {len(h_ps.ops)}")


t0 = time.time()
h = Hamiltonian(coeffs, ops)
t1 = time.time()
print(f"Hamiltonian initialization time: {t1 - t0}")
t0 = time.time()
h.simplify()
t1 = time.time()
print(f"Hamiltonian simplification time: {t1 - t0}")
print(f"Hamiltonian number of operations: {len(h.ops)}")

assert qml.equal(h_ps, h)
```
```
Sum initialization time: 3.0903141498565674
PauliSentence initialization time: 0.1672801971435547
PauliSentence number of operations: 30
Hamiltonian initialization time: 0.1728980541229248
Hamiltonian simplification time: 12.068267107009888
Hamiltonian number of operations: 30
```

Using `qml.ops.dot(coeffs, ops, pauli=True)` returns a **simplified** pauli sentence in just **0.167** seconds, while using `Hamiltonian` + `simplify` lasts **12** seconds!

github-actions[bot]: Hello. You may have forgotten to update the changelog!
Please edit [`doc/releases/changelog-dev.md`](/PennyLaneAI/pennylane/blob/master/doc/releases/changelog-dev.md) with:
* A one-to-two sentence description of the change. You may include a small working example for new features.
* A link back to this PR.
* Your name (or GitHub username) in the contributors section.
codecov[bot]: # [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3586?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
> Merging [#3586](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3586?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (5daca36) into [master](https://codecov.io/gh/PennyLaneAI/pennylane/commit/880517228462ba01af35d94204e55fc9c47a7cac?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (8805172) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

```diff
@@           Coverage Diff           @@
##           master    #3586   +/-   ##
=======================================
  Coverage   99.87%   99.87%           
=======================================
  Files         314      315    +1     
  Lines       28019    28032   +13     
=======================================
+ Hits        27983    27996   +13     
  Misses         36       36           
```


| [Impacted Files](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3586?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | Coverage Δ | |
|---|---|---|
| [pennylane/ops/op\_math/\_\_init\_\_.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3586/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wcy9vcF9tYXRoL19faW5pdF9fLnB5) | `100.00% <100.00%> (ø)` | |
| [pennylane/ops/op\_math/dot.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3586/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wcy9vcF9tYXRoL2RvdC5weQ==) | `100.00% <100.00%> (ø)` | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)

AlbertMitjans: > @AlbertMitjans don't forget to add this function to the docs in `op_math.rst` and `introduction/operators.rst`!

@josh146 thanks for the reminder! `qml_ops_op_math.rst` uses automodule, but I added the function in `introduction/operation.rst` 👍 
AlbertMitjans: > Need to add it to the list of "constructor functions" in `ops/op_math/__init__.py`. Not showing up in the documentation right now.

@albi3ro done!
AlbertMitjans: [sc-31242]

Patch:
diff --git a/doc/introduction/operations.rst b/doc/introduction/operations.rst
index b448c02b1..aaa4816a7 100644
--- a/doc/introduction/operations.rst
+++ b/doc/introduction/operations.rst
@@ -57,6 +57,8 @@ Operator to Operator functions
     ~pennylane.prod
     ~pennylane.s_prod
     ~pennylane.generator
+    ~pennylane.map_wires
+    ~pennylane.ops.dot
 
 These operator functions act on operators to produce new operators.
 
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 5a811fe06..86f73e17d 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -101,6 +101,20 @@
   library.
   [(#3363)](https://github.com/PennyLaneAI/pennylane/pull/3363)
 
+* Added `qml.ops.dot` function to compute the dot product between a vector and a list of operators.
+
+  ```pycon
+  >>> coeffs = np.array([1.1, 2.2])
+  >>> ops = [qml.PauliX(0), qml.PauliY(0)]
+  >>> qml.ops.dot(coeffs, ops)
+  (1.1*(PauliX(wires=[0]))) + (2.2*(PauliY(wires=[0])))
+  >>> qml.ops.dot(coeffs, ops, pauli=True)
+  1.1 * X(0)
+  + 2.2 * Y(0)
+  ```
+
+  [(#3586)](https://github.com/PennyLaneAI/pennylane/pull/3586)
+
 <h3>Improvements</h3>
 
 * Extended the `qml.equal` function to compare `Prod` and `Sum` operators.
diff --git a/pennylane/ops/op_math/__init__.py b/pennylane/ops/op_math/__init__.py
index 595a2c34f..e38cad799 100644
--- a/pennylane/ops/op_math/__init__.py
+++ b/pennylane/ops/op_math/__init__.py
@@ -29,6 +29,7 @@ Constructor Functions
     ~pow
     ~prod
     ~s_prod
+    ~ops.dot
 
 Symbolic Classes
 ~~~~~~~~~~~~~~~~
@@ -56,6 +57,7 @@ from .adjoint_class import Adjoint
 from .adjoint_constructor import adjoint
 from .controlled_class import Controlled, ControlledOp
 from .exp import exp, Exp, Evolution
+from .dot import dot
 
 from .prod import prod, Prod
 
diff --git a/pennylane/ops/op_math/dot.py b/pennylane/ops/op_math/dot.py
new file mode 100644
index 000000000..6395ad59d
--- /dev/null
+++ b/pennylane/ops/op_math/dot.py
@@ -0,0 +1,86 @@
+# Copyright 2018-2023 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.
+"""
+This file contains the definition of the dot function, which computes the dot product between
+a vector and a list of operators.
+"""
+from collections import defaultdict
+from typing import Sequence
+
+import pennylane as qml
+from pennylane.operation import Operator
+
+
+def dot(coeffs: Sequence[float], ops: Sequence[Operator], pauli=False):
+    r"""Returns the dot product between the ``coeffs`` vector and the ``ops`` list of operators.
+
+    This function returns the following linear combination: :math:`\sum_{k=0}^{N-1} c_k O_k`, where
+    :math:`c_k` and :math:`O_k` are the elements inside the ``coeffs`` and ``ops`` arguments respectively.
+
+    Args:
+        coeffs (Sequence[float]): sequence containing the coefficients of the linear combination
+        ops (Sequence[Operator]): sequence containing the operators of the linear combination
+        pauli (bool, optional): If True, a :class:`pennylane.pauli.pauli_arithmetic.PauliSentence`
+            operator is used to represent the linear combination. If False, a :class:`Sum` operator
+            is returned. Defaults to False.
+
+    Raises:
+        ValueError: if the number of coefficients and operators does not match or if they are empty
+
+    Returns:
+        .Sum | .Operator: operator describing the linear combination
+
+    **Example**
+
+    >>> coeffs = np.array([1.1, 2.2])
+    >>> ops = [qml.PauliX(0), qml.PauliY(0)]
+    >>> qml.ops.dot(coeffs, ops)
+    (1.1*(PauliX(wires=[0]))) + (2.2*(PauliY(wires=[0])))
+    >>> qml.ops.dot(coeffs, ops, pauli=True)
+    1.1 * X(0)
+    + 2.2 * Y(0)
+
+    ``pauli=True`` can be used to construct a more efficient, simplified version of the operator.
+    Note that it returns a :class:`~.PauliSentence`, which is not an :class:`~.Operator`. This
+    specialized representation can be converted to an operator:
+
+    >>> qml.ops.dot([1, 2], [qml.PauliX(0), qml.PauliX(0)], pauli=True).operation()
+    3.0*(PauliX(wires=[0]))
+
+    Using ``pauli=True`` and then converting the result to an :class:`~.Operator` is much faster
+    than using ``pauli=False``, but it only works for pauli words
+    (see :func:`pennylane.pauli.is_pauli_word`).
+    """
+    if qml.math.shape(coeffs)[0] != len(ops):
+        raise ValueError("Number of coefficients and operators does not match.")
+    if len(coeffs) == 0 and len(ops) == 0:
+        raise ValueError("Cannot compute the dot product of an empty sequence.")
+
+    if pauli:
+        return _pauli_dot(coeffs, ops)
+
+    operands = []
+    for coeff, op in zip(coeffs, ops):
+        operands.append(op if coeff == 1 else qml.s_prod(coeff, op))
+    return qml.op_sum(*operands) if len(operands) > 1 else operands[0]
+
+
+def _pauli_dot(coeffs: Sequence[float], ops: Sequence[Operator]):
+    pauli_words = defaultdict(lambda: 0)
+    for coeff, op in zip(coeffs, ops):
+        sentence = qml.pauli.pauli_sentence(op)
+        for pw in sentence:
+            pauli_words[pw] += sentence[pw] * coeff
+
+    return qml.pauli.PauliSentence(pauli_words)


Test patch:
diff --git a/tests/ops/op_math/test_dot.py b/tests/ops/op_math/test_dot.py
new file mode 100644
index 000000000..a38e6e119
--- /dev/null
+++ b/tests/ops/op_math/test_dot.py
@@ -0,0 +1,228 @@
+# Copyright 2018-2023 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.
+"""
+Unit tests for the dot function
+"""
+import pytest
+
+import pennylane as qml
+from pennylane.ops import Hamiltonian, SProd, Sum, dot
+from pennylane.pauli.pauli_arithmetic import PauliSentence
+
+
+class TestDotSum:
+    """Unittests for the dot function when ``pauli=False``."""
+
+    def test_dot_returns_sum(self):
+        """Test that the dot function returns a Sum operator when ``pauli=False``."""
+        c = [1.0, 2.0, 3.0]
+        o = [qml.PauliX(0), qml.PauliY(1), qml.PauliZ(2)]
+        S = dot(coeffs=c, ops=o)
+        assert isinstance(S, Sum)
+        for summand, coeff, op in zip(S.operands, c, o):
+            if coeff != 1:
+                assert isinstance(summand, SProd)
+                assert summand.scalar == coeff
+            else:
+                assert isinstance(summand, type(op))
+
+    def test_dot_returns_sprod(self):
+        """Test that the dot function returns a SProd operator when only one operator is input."""
+        O = dot(coeffs=[2.0], ops=[qml.PauliX(0)])
+        assert isinstance(O, SProd)
+        assert O.scalar == 2
+
+    def test_dot_different_number_of_coeffs_and_ops(self):
+        """Test that a ValueError is raised when the number of coefficients and operators does
+        not match."""
+        with pytest.raises(
+            ValueError,
+            match="Number of coefficients and operators does not match",
+        ):
+            dot([1.0], [qml.PauliX(0), qml.PauliY(1)])
+
+    def test_dot_empty_coeffs_or_ops(self):
+        """Test that a ValueError is raised when the number of coefficients and operators does
+        not match."""
+        with pytest.raises(
+            ValueError,
+            match="Cannot compute the dot product of an empty sequence",
+        ):
+            dot([], [])
+
+    @pytest.mark.autograd
+    def test_dot_autograd(self):
+        """Test the dot function with the autograd interface."""
+        c = qml.numpy.array([1.0, 2.0, 3.0])
+        o = [qml.PauliX(0), qml.PauliY(1), qml.PauliZ(2)]
+        op_sum = dot(c, o)
+        op_sum_2 = Sum(
+            qml.PauliX(0),
+            SProd(qml.numpy.array(2.0), qml.PauliY(1)),
+            SProd(qml.numpy.array(3.0), qml.PauliZ(2)),
+        )
+        assert qml.equal(op_sum, op_sum_2)
+
+    @pytest.mark.tf
+    def test_dot_tf(self):
+        """Test the dot function with the tensorflow interface."""
+        import tensorflow as tf
+
+        c = tf.constant([1.0, 2.0, 3.0])
+        o = [qml.PauliX(0), qml.PauliY(1), qml.PauliZ(2)]
+        op_sum = dot(c, o)
+        op_sum_2 = Sum(
+            qml.PauliX(0),
+            SProd(tf.constant(2.0), qml.PauliY(1)),
+            SProd(tf.constant(3.0), qml.PauliZ(2)),
+        )
+        assert qml.equal(op_sum, op_sum_2)
+
+    @pytest.mark.torch
+    def test_dot_torch(self):
+        """Test the dot function with the torch interface."""
+        import torch
+
+        c = torch.tensor([1.0, 2.0, 3.0])
+        o = [qml.PauliX(0), qml.PauliY(1), qml.PauliZ(2)]
+        op_sum = dot(c, o)
+        op_sum_2 = Sum(
+            qml.PauliX(0),
+            SProd(torch.tensor(2.0), qml.PauliY(1)),
+            SProd(torch.tensor(3.0), qml.PauliZ(2)),
+        )
+        assert qml.equal(op_sum, op_sum_2)
+
+    @pytest.mark.jax
+    def test_dot_jax(self):
+        """Test the dot function with the torch interface."""
+        import jax
+
+        c = jax.numpy.array([1.0, 2.0, 3.0])
+        o = [qml.PauliX(0), qml.PauliY(1), qml.PauliZ(2)]
+        op_sum = dot(c, o)
+        op_sum_2 = Sum(
+            qml.PauliX(0),
+            SProd(jax.numpy.array(2.0), qml.PauliY(1)),
+            SProd(jax.numpy.array(3.0), qml.PauliZ(2)),
+        )
+        assert qml.equal(op_sum, op_sum_2)
+
+
+coeffs = [0.12345, 1.2345, 12.345, 123.45, 1234.5, 12345]
+ops = [
+    qml.PauliX(0),
+    qml.PauliY(1),
+    qml.PauliZ(2),
+    qml.PauliX(3),
+    qml.PauliY(4),
+    qml.PauliZ(5),
+]
+
+
+class TestDotPauliSentence:
+    """Unittest for the dot function when ``pauli=True``"""
+
+    def test_dot_returns_pauli_sentence(self):
+        """Test that the dot function returns a PauliSentence class."""
+        ps = dot(coeffs, ops, pauli=True)
+        assert isinstance(ps, PauliSentence)
+
+    def test_coeffs_and_ops(self):
+        """Test that the coefficients and operators of the returned PauliSentence are correct."""
+        ps = dot(coeffs, ops, pauli=True)
+        h = ps.hamiltonian()
+        assert qml.math.allequal(h.coeffs, coeffs)
+        assert all(qml.equal(op1, op2) for op1, op2 in zip(h.ops, ops))
+
+    def test_dot_simplifies_linear_combination(self):
+        """Test that the dot function groups equal pauli words."""
+        ps = dot(
+            coeffs=[0.12, 1.2, 12], ops=[qml.PauliX(0), qml.PauliX(0), qml.PauliX(0)], pauli=True
+        )
+        assert len(ps) == 1
+        h = ps.hamiltonian()
+        assert len(h.ops) == 1
+        assert qml.equal(h.ops[0], qml.PauliX(0))
+
+    def test_dot_returns_hamiltonian_simplified(self):
+        """Test that hamiltonian computed from the PauliSentence created by the dot function is equal
+        to the simplified hamiltonian."""
+        ps = dot(coeffs, ops, pauli=True)
+        h_ps = ps.hamiltonian()
+        h = Hamiltonian(coeffs, ops)
+        h.simplify()
+        assert qml.equal(h_ps, h)
+
+    @pytest.mark.autograd
+    def test_dot_autograd(self):
+        """Test the dot function with the autograd interface."""
+        c = qml.numpy.array([1.0, 2.0, 3.0])
+        o = [qml.PauliX(0), qml.PauliY(1), qml.PauliZ(2)]
+        ps = dot(c, o, pauli=True)
+        op_sum = Sum(
+            qml.PauliX(0),
+            SProd(qml.numpy.array(2.0), qml.PauliY(1)),
+            SProd(qml.numpy.array(3.0), qml.PauliZ(2)),
+        )
+        ps_2 = qml.pauli.pauli_sentence(op_sum)
+        assert ps == ps_2
+
+    @pytest.mark.tf
+    def test_dot_tf(self):
+        """Test the dot function with the tensorflow interface."""
+        import tensorflow as tf
+
+        c = tf.constant([1.0, 2.0, 3.0])
+        o = [qml.PauliX(0), qml.PauliY(1), qml.PauliZ(2)]
+        ps = dot(c, o, pauli=True)
+        op_sum = Sum(
+            qml.PauliX(0),
+            SProd(tf.constant(2.0), qml.PauliY(1)),
+            SProd(tf.constant(3.0), qml.PauliZ(2)),
+        )
+        ps_2 = qml.pauli.pauli_sentence(op_sum)
+        assert ps == ps_2
+
+    @pytest.mark.torch
+    def test_dot_torch(self):
+        """Test the dot function with the torch interface."""
+        import torch
+
+        c = torch.tensor([1.0, 2.0, 3.0])
+        o = [qml.PauliX(0), qml.PauliY(1), qml.PauliZ(2)]
+        ps = dot(c, o, pauli=True)
+        op_sum = Sum(
+            qml.PauliX(0),
+            SProd(torch.tensor(2.0), qml.PauliY(1)),
+            SProd(torch.tensor(3.0), qml.PauliZ(2)),
+        )
+        ps_2 = qml.pauli.pauli_sentence(op_sum)
+        assert ps == ps_2
+
+    @pytest.mark.jax
+    def test_dot_jax(self):
+        """Test the dot function with the torch interface."""
+        import jax
+
+        c = jax.numpy.array([1.0, 2.0, 3.0])
+        o = [qml.PauliX(0), qml.PauliY(1), qml.PauliZ(2)]
+        ps = dot(c, o, pauli=True)
+        op_sum = Sum(
+            qml.PauliX(0),
+            SProd(jax.numpy.array(2.0), qml.PauliY(1)),
+            SProd(jax.numpy.array(3.0), qml.PauliZ(2)),
+        )
+        ps_2 = qml.pauli.pauli_sentence(op_sum)
+        assert ps == ps_2