Repository: PECOS-packages/PECOS (language: python)

Issue:
Optimize qops buffer generation in HybridEngine
Related to PR #27. Right now for iterating over block executing PyPMIR will yield the qops buffer regardless if it can be combine with latter qops. It might be good to keep on building the buffer so there is a larger chunk of operations for the ErrorModel and qsim to work with... presumably it would be faster to do so.


Discussion / hints:

Fix patch:
diff --git a/pyproject.toml b/pyproject.toml
index 07debada..573a03e2 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -15,7 +15,7 @@ build-backend = "setuptools.build_meta"
 
 [project]
 name = "quantum-pecos"
-version = "0.5.0.dev4"
+version = "0.5.0.dev5"
 authors = [
     {name = "The PECOS Developers"},
 ]
@@ -99,9 +99,6 @@ where = ["python"]
 include = ["pecos*"]
 namespaces = true
 
-[tool.pytest.ini_options]
-filterwarnings = ["ignore:::dateutil.tz.tz.*"]
-
 # Linting and autorefactoring tools
 # ---------------------------------
 
diff --git a/python/pecos/classical_interpreters/phir_classical_interpreter.py b/python/pecos/classical_interpreters/phir_classical_interpreter.py
index fb8244da..6d2ced0c 100644
--- a/python/pecos/classical_interpreters/phir_classical_interpreter.py
+++ b/python/pecos/classical_interpreters/phir_classical_interpreter.py
@@ -125,12 +125,29 @@ def initialize_cenv(self) -> None:
                 self.cenv.append(dtype(0))
                 self.cid2dtype.append(dtype)
 
-    def execute(self, sequence: Sequence) -> Generator[list, Any, None]:
+    def _flatten_blocks(self, seq: Sequence):
+        """Flattens the ops of blocks to be processed by the execute() method."""
+        for op in seq:
+            if isinstance(op, pt.block.SeqBlock):
+                yield from self._flatten_blocks(op.ops)
+
+            elif isinstance(op, pt.block.IfBlock):
+                if self.eval_expr(op.condition):
+                    yield from self._flatten_blocks(op.true_branch)
+                elif op.false_branch:
+                    yield from self._flatten_blocks(op.false_branch)
+                else:  # For case of no false_branch (no else)
+                    pass
+
+            else:
+                yield op
+
+    def execute(self, seq: Sequence) -> Generator[list, Any, None]:
         """A generator that runs through and executes classical logic and yields other operations via a buffer."""
 
         op_buffer = []
 
-        for op in sequence:
+        for op in self._flatten_blocks(seq):
             if isinstance(op, pt.opt.QOp):
                 op_buffer.append(op)
 
@@ -141,14 +158,11 @@ def execute(self, sequence: Sequence) -> Generator[list, Any, None]:
             elif isinstance(op, pt.opt.COp):
                 self.handle_cops(op)
 
-            elif isinstance(op, pt.block.Block):
-                yield from self.execute_block(op)
-
             elif isinstance(op, pt.opt.MOp):
                 op_buffer.append(op)
 
             else:
-                msg = f"Statement not recognized: {op}"
+                msg = f"Statement not recognized: {op} of type: {type(op)}"
                 raise TypeError(msg)
 
         if op_buffer:
@@ -279,25 +293,6 @@ def handle_cops(self, op):
             msg = f"Unsupported COp: {op}"
             raise Exception(msg)
 
-    def execute_block(self, op):
-        """Execute a block of ops."""
-        if isinstance(op, pt.block.IfBlock):
-            if self.eval_expr(op.condition):
-                yield from self.execute(op.true_branch)
-
-            elif op.false_branch:
-                yield from self.execute(op.false_branch)
-
-            else:
-                yield from self.execute([])
-
-        elif isinstance(op, pt.block.SeqBlock):
-            yield from self.execute(op.ops)
-
-        else:
-            msg = f"block not implemented! {op}"
-            raise NotImplementedError(msg)
-
     def receive_results(self, qsim_results: list[dict]):
         """Receive measurement results and assign as needed."""
         for meas in qsim_results:
diff --git a/python/pecos/engines/hybrid_engine.py b/python/pecos/engines/hybrid_engine.py
index db5f3408..b80beb6a 100644
--- a/python/pecos/engines/hybrid_engine.py
+++ b/python/pecos/engines/hybrid_engine.py
@@ -32,6 +32,8 @@
 
 
 class HybridEngine:
+    """Engine that runs hybrid quantum/classical programs."""
+
     def __init__(
         self,
         cinterp: ClassicalInterpreter | None = None,
diff --git a/python/pecos/machines/generic_machine.py b/python/pecos/machines/generic_machine.py
index 5137ab9e..f36dfa5f 100644
--- a/python/pecos/machines/generic_machine.py
+++ b/python/pecos/machines/generic_machine.py
@@ -49,7 +49,7 @@ def process(self, op_buffer: list[QOp | MOp]) -> list:
     def leak(self, qubits: set) -> list[QOp]:
         """Starts tracking qubits as leaked qubits and calls the quantum simulation appropriately to trigger leakage."""
         self.leaked_qubits |= qubits
-        return [QOp(name="Init", args=list(qubits), metadata={})]
+        return [QOp(name="Init", args=list(qubits))]
 
     def unleak(self, qubits: set) -> None:
         """Untrack qubits as leaked qubits and calls the quantum simulation appropriately to trigger leakage."""
@@ -58,5 +58,5 @@ def unleak(self, qubits: set) -> None:
     def meas_leaked(self, qubits: set) -> list[QOp]:
         self.leaked_qubits -= qubits
         return [
-            QOp(name="Init -Z", args=list(qubits), metadata={}),
+            QOp(name="Init -Z", args=list(qubits)),
         ]
diff --git a/python/pecos/reps/pypmir/op_types.py b/python/pecos/reps/pypmir/op_types.py
index dcae250d..45cbc1a0 100644
--- a/python/pecos/reps/pypmir/op_types.py
+++ b/python/pecos/reps/pypmir/op_types.py
@@ -13,6 +13,8 @@
 
 
 class Op:
+    """Parent class of operations."""
+
     def __init__(
         self,
         name: str,
@@ -39,7 +41,7 @@ def __init__(
                     raise TypeError(msg)
 
     def __str__(self) -> str:
-        return f"{self.name}, {self.args}, {self.returns}, {self.metadata}, {self.angles}"
+        return f"{self.name}, {self.args}, {self.returns}, {self.metadata}"
 
 
 class QOp(Op):
@@ -61,6 +63,9 @@ def __init__(
         )
         self.angles = angles
 
+    def __str__(self):
+        return self.__repr__()
+
     def __repr__(self):
         return (
             f"<QOP: {self.name} angles: {self.angles} args: {self.args} returns: {self.returns} "
diff --git a/requirements.txt b/requirements.txt
index d2c16a29..5e5c3bd1 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -8,23 +8,23 @@ annotated-types==0.6.0
 colorama==0.4.6
 contourpy==1.2.0
 cycler==0.12.1
-fonttools==4.46.0
+fonttools==4.47.0
 iniconfig==2.0.0
 kiwisolver==1.4.5
 markdown-it-py==3.0.0
 matplotlib==3.8.2
 mdurl==0.1.2
 networkx==2.8.8
-numpy==1.26.2
+numpy==1.26.3
 packaging==23.2
 phir==0.2.1
-pillow==10.1.0
+pillow==10.2.0
 pluggy==1.3.0
-pydantic==2.5.2
-pydantic-core==2.14.5
+pydantic==2.5.3
+pydantic-core==2.14.6
 pygments==2.17.2
 pyparsing==3.1.1
-pytest==7.4.3
+pytest==7.4.4
 python-dateutil==2.8.2
 rich==13.7.0
 scipy==1.11.4


Test patch:
diff --git a/pytest.ini b/pytest.ini
index 37c8f463..7a24d31d 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -19,3 +19,4 @@ markers =
 # TODO: comment this to deal with ProjectQ gate warnings
 filterwarnings =
     ignore::PendingDeprecationWarning:projectq.ops._gates
+    ignore::DeprecationWarning:dateutil.tz.tz.*
diff --git a/tests/end2end/__init__.py b/tests/end2end/__init__.py
new file mode 100644
index 00000000..d383878a
--- /dev/null
+++ b/tests/end2end/__init__.py
@@ -0,0 +1,11 @@
+# Copyright 2024 The PECOS developers
+#
+# 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
+#
+#     https://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.
+"""End-to-end tests"""
diff --git a/tests/integration/phir/bell_qparallel.json b/tests/integration/phir/bell_qparallel.json
new file mode 100644
index 00000000..0bc1b7c8
--- /dev/null
+++ b/tests/integration/phir/bell_qparallel.json
@@ -0,0 +1,22 @@
+{
+ "format": "PHIR/JSON",
+ "version": "0.1.0",
+ "metadata": {"source": "pytket-phir v0.2.0", "strict_parallelism": "true"},
+ "ops": [
+  {"data": "qvar_define", "data_type": "qubits", "variable": "q", "size": 2},
+  {"data": "cvar_define", "data_type": "u32", "variable": "m", "size": 2},
+  {"qop": "RZ", "angles": [[1.0], "pi"], "args": [["q", 0], ["q", 1]]},
+  {"block": "qparallel", "ops": [
+   {"qop": "R1XY", "angles": [[0.5, 0.5], "pi"], "args": [["q", 0]]},
+   {"qop": "R1XY", "angles": [[1.5, 0.5], "pi"], "args": [["q", 1]]}
+  ]},
+  {"qop": "RZ", "angles": [[1.0], "pi"], "args": [["q", 0]]},
+  {"qop": "RZZ", "angles": [[0.5], "pi"], "args": [[["q", 0], ["q", 1]]]},
+  {"block": "qparallel", "ops": [
+   {"qop": "RZ", "angles": [[1.5], "pi"], "args": [["q", 0]]},
+   {"qop": "RZ", "angles": [[0.5], "pi"], "args": [["q", 1]]}
+  ]},
+  {"qop": "R1XY", "angles": [[1.5, 0.5], "pi"], "args": [["q", 1]]},
+  {"qop": "Measure", "args": [["q", 0], ["q", 1]], "returns": [["m", 0], ["m", 1]]}
+ ]
+}
diff --git a/tests/integration/phir/bell_qparallel_cliff.json b/tests/integration/phir/bell_qparallel_cliff.json
new file mode 100644
index 00000000..c0e2386a
--- /dev/null
+++ b/tests/integration/phir/bell_qparallel_cliff.json
@@ -0,0 +1,22 @@
+{
+ "format": "PHIR/JSON",
+ "version": "0.1.0",
+ "metadata": {"source": "pytket-phir v0.2.0", "strict_parallelism": "true"},
+ "ops": [
+  {"data": "qvar_define", "data_type": "qubits", "variable": "q", "size": 2},
+  {"data": "cvar_define", "data_type": "u32", "variable": "m", "size": 2},
+  {"qop": "Z", "args": [["q", 0], ["q", 1]]},
+  {"block": "qparallel", "ops": [
+   {"qop": "SY", "args": [["q", 0]]},
+   {"qop": "SYdg", "args": [["q", 1]]}
+  ]},
+  {"qop": "Z", "args": [["q", 0]]},
+  {"qop": "SZZ", "args": [[["q", 0], ["q", 1]]]},
+  {"block": "qparallel", "ops": [
+   {"qop": "SZdg", "args": [["q", 0]]},
+   {"qop": "SZ", "args": [["q", 1]]}
+  ]},
+  {"qop": "SYdg", "args": [["q", 1]]},
+  {"qop": "Measure", "args": [["q", 0], ["q", 1]], "returns": [["m", 0], ["m", 1]]}
+ ]
+}
diff --git a/tests/integration/test_phir.py b/tests/integration/test_phir.py
index aae33b5f..07eba624 100644
--- a/tests/integration/test_phir.py
+++ b/tests/integration/test_phir.py
@@ -260,3 +260,29 @@ def test_qparallel():
 
     m = results["m"]
     assert m.count("1111") == len(m)
+
+
+@pytest.mark.optional_dependency()  # uses projectq / state-vector
+def test_bell_qparallel():
+    """Testing a program creating and measuring a Bell state and using qparallel blocks returns expected results."""
+
+    results = HybridEngine(qsim="state-vector").run(
+        program=json.load(Path.open(this_dir / "phir" / "bell_qparallel.json")),
+        shots=20,
+    )
+
+    m = results["m"]
+    assert m.count("00") + m.count("11") == len(m)
+
+
+def test_bell_qparallel_cliff():
+    """Testing a program creating and measuring a Bell state and using qparallel blocks returns expected results (with
+    Clifford circuits and stabilizer sim)."""
+
+    results = HybridEngine(qsim="stabilizer").run(
+        program=json.load(Path.open(this_dir / "phir" / "bell_qparallel_cliff.json")),
+        shots=20,
+    )
+
+    m = results["m"]
+    assert m.count("00") + m.count("11") == len(m)
diff --git a/tests/unit/test_blocks.py b/tests/unit/test_blocks.py
new file mode 100644
index 00000000..d1fad48d
--- /dev/null
+++ b/tests/unit/test_blocks.py
@@ -0,0 +1,241 @@
+# Copyright 2024 The PECOS developers
+#
+# 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
+#
+#     https://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.
+"""Tests to ensure the sequence of operations are as expected."""
+from pecos.classical_interpreters.phir_classical_interpreter import PHIRClassicalInterpreter
+
+
+def get_seq(program):
+    """Get the sequences of operations produced by using the PHIR interpreter."""
+    interp = PHIRClassicalInterpreter()
+    interp.init(program)
+
+    ops_seq = []
+    for buffered_ops in interp.execute(interp.program.ops):
+        subseq = []
+
+        # Create a simple identifier for operations
+        for op in buffered_ops:
+            op_ident = [op.name]
+            if hasattr(op, "metadata") and op.metadata.get("angles") is not None:
+                op_ident.append(op.metadata["angles"])
+            if hasattr(op, "args") and op.args is not None:
+                op_ident.append(op.args)
+            if hasattr(op, "returns") and op.returns is not None:
+                op_ident.append(op.returns)
+            subseq.append(tuple(op_ident))
+        ops_seq.append(subseq)
+    return ops_seq
+
+
+def test_seq():
+    """Test that the expected sequence of operations is returned with a program using Sequence Blocks."""
+    program = {
+        "format": "PHIR/JSON",
+        "version": "0.1.0",
+        "metadata": {"source": "pytket-phir v0.2.0", "strict_parallelism": "true"},
+        "ops": [
+            {"data": "qvar_define", "data_type": "qubits", "variable": "q", "size": 2},
+            {"data": "cvar_define", "data_type": "u32", "variable": "m", "size": 2},
+            {"qop": "RZ", "angles": [[1.0], "pi"], "args": [["q", 0], ["q", 1]]},
+            {
+                "block": "sequence",
+                "ops": [
+                    {"qop": "R1XY", "angles": [[0.5, 0.5], "pi"], "args": [["q", 0]]},
+                    {"qop": "Measure", "args": [["q", 0], ["q", 1]], "returns": [["m", 0], ["m", 1]]},
+                    {"qop": "R1XY", "angles": [[0.5, 0.5], "pi"], "args": [["q", 0]]},
+                ],
+            },
+            {"qop": "Measure", "args": [["q", 0], ["q", 1]], "returns": [["m", 0], ["m", 1]]},
+            {"qop": "X", "args": [["q", 0]]},
+        ],
+    }
+
+    assert get_seq(program) == [
+        [
+            ("RZ", [3.141592653589793], [0, 1]),
+            ("R1XY", [1.5707963267948966, 1.5707963267948966], [0]),
+            ("Measure", [0, 1], [["m", 0], ["m", 1]]),
+        ],
+        [
+            ("R1XY", [1.5707963267948966, 1.5707963267948966], [0]),
+            ("Measure", [0, 1], [["m", 0], ["m", 1]]),
+        ],
+        [
+            ("X", [0]),
+        ],
+    ]
+
+
+def test_qparallel():
+    """Test that the expected sequence of operations is returned with a program using Sequence Blocks."""
+    program = {
+        "format": "PHIR/JSON",
+        "version": "0.1.0",
+        "metadata": {"source": "pytket-phir v0.2.0", "strict_parallelism": "true"},
+        "ops": [
+            {"data": "qvar_define", "data_type": "qubits", "variable": "q", "size": 2},
+            {"data": "cvar_define", "data_type": "u32", "variable": "m", "size": 2},
+            {"qop": "RZ", "angles": [[1.0], "pi"], "args": [["q", 0], ["q", 1]]},
+            {
+                "block": "qparallel",
+                "ops": [
+                    {"qop": "R1XY", "angles": [[0.5, 0.5], "pi"], "args": [["q", 0]]},
+                    {"qop": "Measure", "args": [["q", 0], ["q", 1]], "returns": [["m", 0], ["m", 1]]},
+                    {"qop": "R1XY", "angles": [[0.5, 0.5], "pi"], "args": [["q", 0]]},
+                ],
+            },
+            {"qop": "Measure", "args": [["q", 0], ["q", 1]], "returns": [["m", 0], ["m", 1]]},
+            {"qop": "X", "args": [["q", 0]]},
+        ],
+    }
+
+    assert get_seq(program) == [
+        [
+            ("RZ", [3.141592653589793], [0, 1]),
+            ("R1XY", [1.5707963267948966, 1.5707963267948966], [0]),
+            ("Measure", [0, 1], [["m", 0], ["m", 1]]),
+        ],
+        [
+            ("R1XY", [1.5707963267948966, 1.5707963267948966], [0]),
+            ("Measure", [0, 1], [["m", 0], ["m", 1]]),
+        ],
+        [
+            ("X", [0]),
+        ],
+    ]
+
+
+def test_if_true():
+    program = {
+        "format": "PHIR/JSON",
+        "version": "0.1.0",
+        "metadata": {"source": "pytket-phir v0.2.0", "strict_parallelism": "true"},
+        "ops": [
+            {"data": "qvar_define", "data_type": "qubits", "variable": "q", "size": 2},
+            {"data": "cvar_define", "data_type": "u32", "variable": "v", "size": 1},
+            {"data": "cvar_define", "data_type": "u32", "variable": "m", "size": 2},
+            {"cop": "=", "returns": [["v", 0]], "args": [1]},
+            {"qop": "H", "angles": None, "args": [["q", 0]]},
+            {"qop": "CX", "angles": None, "args": [[["q", 0], ["q", 1]]]},
+            {"qop": "Measure", "returns": [["m", 1]], "args": [["q", 1]]},
+            {
+                "block": "if",
+                "condition": {"cop": "==", "args": [["v", 0], 1]},
+                "true_branch": [
+                    {"qop": "X", "angles": None, "args": [["q", 0]]},
+                ],
+                "false_branch": [
+                    {"qop": "Y", "angles": None, "args": [["q", 0]]},
+                ],
+            },
+            {"qop": "Measure", "returns": [["m", 0]], "args": [["q", 0]]},
+        ],
+    }
+
+    assert get_seq(program) == [
+        [
+            ("H", [0]),
+            ("CX", [[0, 1]]),
+            ("Measure", [1], [["m", 1]]),
+        ],
+        [
+            ("X", [0]),
+            (
+                "Measure",
+                [0],
+                [["m", 0]],
+            ),
+        ],
+    ]
+
+
+def test_if_false():
+    program = {
+        "format": "PHIR/JSON",
+        "version": "0.1.0",
+        "metadata": {"source": "pytket-phir v0.2.0", "strict_parallelism": "true"},
+        "ops": [
+            {"data": "qvar_define", "data_type": "qubits", "variable": "q", "size": 2},
+            {"data": "cvar_define", "data_type": "u32", "variable": "v", "size": 1},
+            {"data": "cvar_define", "data_type": "u32", "variable": "m", "size": 2},
+            {"cop": "=", "returns": [["v", 0]], "args": [1]},
+            {"qop": "H", "angles": None, "args": [["q", 0]]},
+            {"qop": "CX", "angles": None, "args": [[["q", 0], ["q", 1]]]},
+            {"qop": "Measure", "returns": [["m", 1]], "args": [["q", 1]]},
+            {
+                "block": "if",
+                "condition": {"cop": "==", "args": [["v", 0], 0]},
+                "true_branch": [
+                    {"qop": "X", "angles": None, "args": [["q", 0]]},
+                ],
+                "false_branch": [
+                    {"qop": "Y", "angles": None, "args": [["q", 0]]},
+                ],
+            },
+            {"qop": "Measure", "returns": [["m", 0]], "args": [["q", 0]]},
+        ],
+    }
+
+    assert get_seq(program) == [
+        [
+            ("H", [0]),
+            ("CX", [[0, 1]]),
+            ("Measure", [1], [["m", 1]]),
+        ],
+        [
+            ("Y", [0]),
+            (
+                "Measure",
+                [0],
+                [["m", 0]],
+            ),
+        ],
+    ]
+
+
+def test_if_no_false():
+    program = {
+        "format": "PHIR/JSON",
+        "version": "0.1.0",
+        "metadata": {"source": "pytket-phir v0.2.0", "strict_parallelism": "true"},
+        "ops": [
+            {"data": "qvar_define", "data_type": "qubits", "variable": "q", "size": 2},
+            {"data": "cvar_define", "data_type": "u32", "variable": "v", "size": 1},
+            {"data": "cvar_define", "data_type": "u32", "variable": "m", "size": 2},
+            {"cop": "=", "returns": [["v", 0]], "args": [1]},
+            {"qop": "H", "angles": None, "args": [["q", 0]]},
+            {"qop": "CX", "angles": None, "args": [[["q", 0], ["q", 1]]]},
+            {"qop": "Measure", "returns": [["m", 1]], "args": [["q", 1]]},
+            {
+                "block": "if",
+                "condition": {"cop": "==", "args": [["v", 0], 0]},
+                "true_branch": [
+                    {"qop": "X", "angles": None, "args": [["q", 0]]},
+                ],
+            },
+            {"qop": "Measure", "returns": [["m", 0]], "args": [["q", 0]]},
+        ],
+    }
+
+    assert get_seq(program) == [
+        [
+            ("H", [0]),
+            ("CX", [[0, 1]]),
+            ("Measure", [1], [["m", 1]]),
+        ],
+        [
+            (
+                "Measure",
+                [0],
+                [["m", 0]],
+            ),
+        ],
+    ]