From 233ab27df3fa1ce444d06b959d211ec2336d8d34 Mon Sep 17 00:00:00 2001 From: Thierry Martinez Date: Fri, 28 Aug 2026 07:32:08 +0200 Subject: [PATCH 1/4] Fix #3: Support for measurements This commit adds support for measurements. Measurements of the form `bit = measure qubit;`, or the old syntax `measure qubit -> bit`, are supported. --- CHANGELOG.md | 7 +- README.md | 11 +++ graphix_qasm_parser/parser.py | 161 +++++++++++++++++++++++++--------- pyproject.toml | 1 + tests/test_parser.py | 35 +++++++- 5 files changed, 172 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2062df..de21c74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +- #12, #13: Remove incorrect parsing of `crz` gates (@clebrin) + +- #3, #14: Support for measurements + ## [0.1.1] - 2026-02-05 - #7: Parsing of `CZ` gate - #8, #9: Compatibility with the new angle convention in Graphix (https://github.com/TeamGraphix/graphix/pull/399) - ## [0.1.0] - 2025-11-24 diff --git a/README.md b/README.md index 6b9c8a4..63ae03b 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,12 @@ circuit = parser.parse_file("my_circuit.qasm") - Qubit register arrays: `qubit[n] q`, or the old syntax `qreg q[n]`. +### [Classical bits](https://openqasm.com/language/types.html#classical-bits-and-registers) + +- Single-bit registers: `bit b`, or the old syntax `creg b`. + +- Bit register arrays: `bit[n] b`, or the old syntax `creg q[n]`. + ### Supported Gates | OpenQASM gate | Graphix instruction | @@ -73,4 +79,9 @@ The constant `pi` (or `π`) is defined. [Compile-time constants](https://openqasm.com/language/types.html#compile-time-constants) can be defined and used in expressions. +### [Measurements](https://openqasm.com/language/insts.html#measurement) + +Measurements of the form `bit = measure qubit;`, or the old syntax +`measure qubit -> bit`, are supported. +Both arguments must be registers of the same size, or both must be bit/qubit types. diff --git a/graphix_qasm_parser/parser.py b/graphix_qasm_parser/parser.py index 1cdaf86..b3a952e 100644 --- a/graphix_qasm_parser/parser.py +++ b/graphix_qasm_parser/parser.py @@ -2,8 +2,10 @@ from __future__ import annotations +import enum import math from dataclasses import dataclass +from enum import Enum from typing import TYPE_CHECKING from antlr4 import ( # type: ignore[attr-defined] @@ -13,7 +15,8 @@ ParserRuleContext, ) from graphix import Circuit -from graphix.instruction import CCX, CNOT, RX, RY, RZ, SWAP, H, I, S, X, Y, Z +from graphix.fundamentals import Axis +from graphix.instruction import CCX, CNOT, RX, RY, RZ, SWAP, H, I, M, S, X, Y, Z from openqasm_parser import qasm3Lexer, qasm3Parser, qasm3ParserVisitor # override introduced in Python 3.12 @@ -22,7 +25,7 @@ if TYPE_CHECKING: from pathlib import Path - from graphix.instruction import Instruction + from graphix.instruction import InstructionType # Compatibility with graphix <= 0.3.3 # See https://github.com/TeamGraphix/graphix/pull/379 @@ -45,7 +48,8 @@ def CZ(_q0: int, _q1: int) -> None: # noqa: N802 raise NotImplementedError(msg) try: - from graphix.fundamentals import ANGLE_PI, rad_to_angle + from graphix.fundamentals import ANGLE_PI as ANGLE_PI # noqa: PLC0414 + from graphix.fundamentals import rad_to_angle except ImportError: # Compatibility with graphix <= 0.3.3 # See https://github.com/TeamGraphix/graphix/pull/399 @@ -118,13 +122,22 @@ def __rmod__(self, other: object) -> _Value: return NotImplemented def __int__(self) -> int: - msg = "Not an integer value: {ctx.getText() if isinstance(ctx, ParserRuleContext) else ctx}" + msg = f"Not an integer value: {self.report_ctx()}" raise TypeError(msg) def __float__(self) -> float: - msg = "Not a floating-point value: {ctx.getText() if isinstance(ctx, ParserRuleContext) else ctx}" + msg = f"Not a floating-point value: {self.report_ctx()}" raise TypeError(msg) + def as_qubit(self) -> _Qubit: + if not isinstance(self, _Qubit): + msg = f"Qubit expected: {self.report_ctx()}" + raise TypeError(msg) + return self + + def report_ctx(self) -> str: + return self.ctx.getText() if isinstance(self.ctx, ParserRuleContext) else self.ctx # type: ignore[union-attr,arg-type] + @dataclass class _Int(_Value): @@ -249,9 +262,14 @@ def __float__(self) -> float: return self.value +class _DeclKind(Enum): + Bit = enum.auto() + Qubit = enum.auto() + + @dataclass class _Bit(_Value): - index: int + index: int | None = None @dataclass @@ -267,12 +285,14 @@ class _Array(_Value): class _CircuitVisitor(qasm3ParserVisitor): parser: OpenQASMParser width: int - instructions: list[Instruction] + measurement_count: int + instructions: list[InstructionType] env: dict[str, _Value] def __init__(self, parser: OpenQASMParser) -> None: self.parser = parser self.width = 0 + self.measurement_count = 0 self.instructions = [] self.env = { "pi": _Float("pi", math.pi), @@ -281,24 +301,33 @@ def __init__(self, parser: OpenQASMParser) -> None: @override def visitOldStyleDeclarationStatement(self, ctx: qasm3Parser.OldStyleDeclarationStatementContext) -> None: - decl_class: type[_Bit | _Qubit] kind = ctx.getChild(0) if kind.symbol.type == qasm3Parser.QREG: - decl_class = _Qubit + decl_kind = _DeclKind.Qubit elif kind.symbol.type == qasm3Parser.CREG: - decl_class = _Bit + decl_kind = _DeclKind.Bit else: msg = f"Unknown declaration statement kind: {kind}" raise NotImplementedError(msg) identifier = ctx.Identifier().getText() # type: ignore[no-untyped-call] designator = ctx.designator() # type: ignore[no-untyped-call] - self.declare_registers(ctx, decl_class, identifier, designator) + self.declare_registers(ctx, decl_kind, identifier, designator) @override def visitQuantumDeclarationStatement(self, ctx: qasm3Parser.QuantumDeclarationStatementContext) -> None: designator = ctx.qubitType().designator() # type: ignore[no-untyped-call] identifier = ctx.Identifier().getText() # type: ignore[no-untyped-call] - self.declare_registers(ctx, _Qubit, identifier, designator) + self.declare_registers(ctx, _DeclKind.Qubit, identifier, designator) + + @override + def visitClassicalDeclarationStatement(self, ctx: qasm3Parser.ClassicalDeclarationStatementContext) -> None: + scalar_type = ctx.scalarType() # type: ignore[no-untyped-call] + if scalar_type is None or scalar_type.BIT() is None: + msg = "Only bit type is supported." + raise NotImplementedError(msg) + identifier = ctx.Identifier().getText() # type: ignore[no-untyped-call] + designator = scalar_type.designator() + self.declare_registers(ctx, _DeclKind.Bit, identifier, designator) @override def visitConstDeclarationStatement(self, ctx: qasm3Parser.ConstDeclarationStatementContext) -> None: @@ -320,7 +349,7 @@ def visitGateCallStatement(self, ctx: qasm3Parser.GateCallStatementContext) -> N ] else: exprs = [] - instruction: Instruction + instruction: InstructionType if gate == "ccx": # https://openqasm.com/language/standard_library.html#ccx instruction = CCX(target=operands[2], controls=(operands[0], operands[1])) @@ -365,10 +394,64 @@ def visitGateCallStatement(self, ctx: qasm3Parser.GateCallStatementContext) -> N raise NotImplementedError(msg) self.instructions.append(instruction) + @override + def visitAssignmentStatement(self, ctx: qasm3Parser.AssignmentStatementContext) -> None: + measure_expression = ctx.measureExpression() # type: ignore[no-untyped-call] + if measure_expression is None: + msg = "Only measure assignments are supported." + raise NotImplementedError(msg) + indexed_identifier = ctx.indexedIdentifier() # type: ignore[no-untyped-call] + self.add_measurement_statement(indexed_identifier, measure_expression) + + @override + def visitMeasureArrowAssignmentStatement(self, ctx: qasm3Parser.MeasureArrowAssignmentStatementContext) -> None: + indexed_identifier = ctx.indexedIdentifier() # type: ignore[no-untyped-call] + measure_expression = ctx.measureExpression() # type: ignore[no-untyped-call] + self.add_measurement_statement(indexed_identifier, measure_expression) + + def add_measurement_statement( + self, + indexed_identifier: qasm3Parser.IndexedIdentifierContext, + measure_expression: qasm3Parser.MeasureExpressionContext, + ) -> None: + target_bit = self.evaluate_indexed_identifier(indexed_identifier) + gate_operand = measure_expression.gateOperand() # type: ignore[no-untyped-call] + target_qubit = self.evaluate_operand(gate_operand) + if isinstance(target_bit, _Array) or isinstance(target_qubit, _Array): + if not isinstance(target_bit, _Array) or not isinstance(target_qubit, _Array): + msg = "Both arguments must be registers, or both must be bit/qubit types." + raise TypeError(msg) + if len(target_bit.values) != len(target_qubit.values): + msg = "Both registers must have the same size." + raise ValueError(msg) + for bit, qubit in zip(target_bit.values, target_qubit.values, strict=True): + self.add_measurement(bit, qubit) + return + self.add_measurement(target_bit, target_qubit) + + def add_measurement(self, target_bit: _Value, target_qubit: _Value) -> None: + if not isinstance(target_bit, _Bit): + msg = f"Only assignment to bit is supported: {target_bit} unexpected." + raise NotImplementedError(msg) + qubit_index = target_qubit.as_qubit().index + instruction = M(qubit_index, Axis.Z) + self.instructions.append(instruction) + target_bit.index = qubit_index + self.measurement_count += 1 + + def declare_register(self, ctx: ParserRuleContext, decl_kind: _DeclKind) -> _Value: # type: ignore[valid-type] + match decl_kind: + case _DeclKind.Bit: + value: _Value = _Bit(ctx) + case _DeclKind.Qubit: + value = _Qubit(ctx, self.width) + self.width += 1 + return value + def declare_registers( self, ctx: ParserRuleContext, # type: ignore[valid-type] - decl_class: type[_Bit | _Qubit], + decl_kind: _DeclKind, identifier: str, designator: qasm3Parser.DesignatorContext | None, ) -> None: @@ -376,44 +459,42 @@ def declare_registers( if designator: expression = designator.expression() # type: ignore[no-untyped-call] count = int(self.evaluate_expression(expression)) - value = _Array(ctx, [decl_class(ctx, self.width + i) for i in range(count)]) - self.width += count + value = _Array(ctx, [self.declare_register(ctx, decl_kind) for i in range(count)]) else: - value = decl_class(ctx, self.width) - self.width += 1 + value = self.declare_register(ctx, decl_kind) self.env[identifier] = value def convert_qubit_index(self, operand: qasm3Parser.GateOperandContext) -> int: value = self.evaluate_operand(operand) - if isinstance(value, _Qubit): - return value.index - msg = f"Qubit expected: {operand}" - raise ValueError(msg) + return value.as_qubit().index def evaluate_operand(self, operand: qasm3Parser.GateOperandContext) -> _Value: child = operand.getChild(0) if child.getRuleIndex() == qasm3Parser.RULE_indexedIdentifier: - identifier = child.Identifier().getText() - value = self.env.get(identifier) - if value is None: - msg = f"name {identifier} is not defined" - raise NameError(msg) - for operator in child.indexOperator(): - if not isinstance(value, _Array): - msg = f"Array expected: {identifier}" - raise TypeError(msg) - index = int(self.evaluate_expression(operator.expression(0))) - if index < 0: - msg = f"Negative index: {identifier}" - raise IndexError(msg) - if index >= len(value.values): - msg = f"Index out of bounds: {identifier} has length {len(value.values)}" - raise IndexError(msg) - value = value.values[index] - return value + return self.evaluate_indexed_identifier(child) msg = f"Unknown operand: {operand}" raise NotImplementedError(msg) + def evaluate_indexed_identifier(self, indexed_identifier: qasm3Parser.IndexedIdentifierContext) -> _Value: + identifier = indexed_identifier.Identifier().getText() # type: ignore[no-untyped-call] + value = self.env.get(identifier) + if value is None: + msg = f"name {identifier} is not defined" + raise NameError(msg) + for operator in indexed_identifier.indexOperator(): + if not isinstance(value, _Array): + msg = f"Array expected: {identifier}" + raise TypeError(msg) + index = int(self.evaluate_expression(operator.expression(0))) + if index < 0: + msg = f"Negative index: {identifier}" + raise IndexError(msg) + if index >= len(value.values): + msg = f"Index out of bounds: {identifier} has length {len(value.values)}" + raise IndexError(msg) + value = value.values[index] + return value + def evaluate_expression(self, expr: qasm3Parser.ExpressionContext) -> _Value: return _ExpressionVisitor(self).parse(expr) diff --git a/pyproject.toml b/pyproject.toml index ca1c648..614589b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ dev = { file = ["requirements-dev.txt"] } [tool.ruff] line-length = 120 +extend-exclude = ["build"] [tool.ruff.lint] select = ["ALL"] diff --git a/tests/test_parser.py b/tests/test_parser.py index 57f4266..7067aa4 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING import pytest -from graphix.instruction import CCX, CNOT, RX, RY, RZ, SWAP, H, S, X, Y, Z +from graphix.instruction import CCX, CNOT, RX, RY, RZ, SWAP, Axis, H, M, S, X, Y, Z from graphix_qasm_parser import OpenQASMParser @@ -79,7 +79,7 @@ def test_parse_simple_circuit_old_syntax() -> None: assert math.isclose(instruction.angle, 5 * ANGLE_PI / 4) -def test_parse_all_instructions() -> None: # noqa: PLR0915 +def test_parse_all_instructions() -> None: """Test parse all instructions.""" s = """ include "qelib1.inc"; @@ -248,3 +248,34 @@ def test_const_declarations() -> None: assert math.isclose(instruction.angle, ANGLE_PI / 4) with pytest.raises(StopIteration): next(iterator) + + +def test_measurement() -> None: + """Test measurement.""" + s = """ +include "stdgates.inc"; +qubit q; +bit b; +b = measure q; +qreg qo; +creg bo; +measure qo -> bo; +qubit[2] qr; +bit[2] br; +br[0] = measure qr[0]; +br[1] = measure qr[1]; +qreg qr2[2]; +creg br2[2]; +br2 = measure qr2; +""" + parser = OpenQASMParser() + circuit = parser.parse_str(s) + assert circuit.width == 6 + assert circuit.instruction == [ + M(0, Axis.Z), + M(1, Axis.Z), + M(2, Axis.Z), + M(3, Axis.Z), + M(4, Axis.Z), + M(5, Axis.Z), + ] From 9229c7b776a5496ee64de8adf3b543a5d36621ee Mon Sep 17 00:00:00 2001 From: Thierry Martinez Date: Fri, 28 Aug 2026 11:45:59 +0200 Subject: [PATCH 2/4] Fix reference in CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de21c74..19dc6c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - #12, #13: Remove incorrect parsing of `crz` gates (@clebrin) -- #3, #14: Support for measurements +- #3, #16: Support for measurements ## [0.1.1] - 2026-02-05 From 150f30682f1c38bcd575a18f7f99f2b12997e1a8 Mon Sep 17 00:00:00 2001 From: Thierry Martinez Date: Sun, 13 Sep 2026 16:35:37 +0200 Subject: [PATCH 3/4] Remove compatibility with older graphix --- graphix_qasm_parser/parser.py | 65 +++++------------- tests/test_parser.py | 125 ++++++++++------------------------ 2 files changed, 52 insertions(+), 138 deletions(-) diff --git a/graphix_qasm_parser/parser.py b/graphix_qasm_parser/parser.py index b3a952e..6157a6c 100644 --- a/graphix_qasm_parser/parser.py +++ b/graphix_qasm_parser/parser.py @@ -14,9 +14,8 @@ InputStream, ParserRuleContext, ) -from graphix import Circuit -from graphix.fundamentals import Axis -from graphix.instruction import CCX, CNOT, RX, RY, RZ, SWAP, H, I, M, S, X, Y, Z +from graphix import Circuit, Instruction +from graphix.fundamentals import Axis, rad_to_angle from openqasm_parser import qasm3Lexer, qasm3Parser, qasm3ParserVisitor # override introduced in Python 3.12 @@ -27,38 +26,6 @@ from graphix.instruction import InstructionType - # Compatibility with graphix <= 0.3.3 - # See https://github.com/TeamGraphix/graphix/pull/379 - - ANGLE_PI: float - - def rad_to_angle(angle: float) -> float: - """Prototype for rad_to_angle.""" - ... - - CZ = SWAP -else: - try: - from graphix.instruction import CZ - except ImportError: - - def CZ(_q0: int, _q1: int) -> None: # noqa: N802 - """In older versions of graphix (<= 0.3.3), CZ instructions were not supported.""" - msg = "CZ instructions are not supported by graphix <= 0.3.3" - raise NotImplementedError(msg) - - try: - from graphix.fundamentals import ANGLE_PI as ANGLE_PI # noqa: PLC0414 - from graphix.fundamentals import rad_to_angle - except ImportError: - # Compatibility with graphix <= 0.3.3 - # See https://github.com/TeamGraphix/graphix/pull/399 - ANGLE_PI = math.pi - - def rad_to_angle(angle: float) -> float: - """In older versions of graphix (<= 0.3.3), instruction angles were expressed in radians.""" - return angle - class OpenQASMParser: """Graphix OpenQASM parser.""" @@ -352,43 +319,43 @@ def visitGateCallStatement(self, ctx: qasm3Parser.GateCallStatementContext) -> N instruction: InstructionType if gate == "ccx": # https://openqasm.com/language/standard_library.html#ccx - instruction = CCX(target=operands[2], controls=(operands[0], operands[1])) + instruction = Instruction.CCX(target=operands[2], controls=(operands[0], operands[1])) elif gate == "cx": # https://openqasm.com/language/standard_library.html#cx - instruction = CNOT(target=operands[1], control=operands[0]) + instruction = Instruction.CNOT(target=operands[1], control=operands[0]) elif gate == "swap": # https://openqasm.com/language/standard_library.html#swap - instruction = SWAP(targets=(operands[0], operands[1])) + instruction = Instruction.SWAP(targets=(operands[0], operands[1])) elif gate == "cz": # https://openqasm.com/language/standard_library.html#cz - instruction = CZ(targets=(operands[0], operands[1])) + instruction = Instruction.CZ(targets=(operands[0], operands[1])) elif gate == "h": # https://openqasm.com/language/standard_library.html#h - instruction = H(target=operands[0]) + instruction = Instruction.H(target=operands[0]) elif gate == "s": # https://openqasm.com/language/standard_library.html#s - instruction = S(target=operands[0]) + instruction = Instruction.S(target=operands[0]) elif gate == "x": # https://openqasm.com/language/standard_library.html#x - instruction = X(target=operands[0]) + instruction = Instruction.X(target=operands[0]) elif gate == "y": # https://openqasm.com/language/standard_library.html#y - instruction = Y(target=operands[0]) + instruction = Instruction.Y(target=operands[0]) elif gate == "z": # https://openqasm.com/language/standard_library.html#z - instruction = Z(target=operands[0]) + instruction = Instruction.Z(target=operands[0]) elif gate == "id": # https://openqasm.com/language/standard_library.html#id - instruction = I(target=operands[0]) + instruction = Instruction.I(target=operands[0]) elif gate == "rx": # https://openqasm.com/language/standard_library.html#rx - instruction = RX(target=operands[0], angle=rad_to_angle(exprs[0])) + instruction = Instruction.RX(target=operands[0], angle=rad_to_angle(exprs[0])) elif gate == "ry": # https://openqasm.com/language/standard_library.html#ry - instruction = RY(target=operands[0], angle=rad_to_angle(exprs[0])) + instruction = Instruction.RY(target=operands[0], angle=rad_to_angle(exprs[0])) elif gate == "rz": # https://openqasm.com/language/standard_library.html#rz - instruction = RZ(target=operands[0], angle=rad_to_angle(exprs[0])) + instruction = Instruction.RZ(target=operands[0], angle=rad_to_angle(exprs[0])) else: msg = f"Unknown gate: {gate}" raise NotImplementedError(msg) @@ -434,7 +401,7 @@ def add_measurement(self, target_bit: _Value, target_qubit: _Value) -> None: msg = f"Only assignment to bit is supported: {target_bit} unexpected." raise NotImplementedError(msg) qubit_index = target_qubit.as_qubit().index - instruction = M(qubit_index, Axis.Z) + instruction = Instruction.M(qubit_index, Axis.Z) self.instructions.append(instruction) target_bit.index = qubit_index self.measurement_count += 1 diff --git a/tests/test_parser.py b/tests/test_parser.py index 7067aa4..ab65c80 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -1,49 +1,12 @@ """Tests for Graphix QASM parser.""" import math -from typing import TYPE_CHECKING import pytest -from graphix.instruction import CCX, CNOT, RX, RY, RZ, SWAP, Axis, H, M, S, X, Y, Z +from graphix import ANGLE_PI, Axis, Instruction, rad_to_angle from graphix_qasm_parser import OpenQASMParser -if TYPE_CHECKING: - # Compatibility with graphix <= 0.3.3 - # See https://github.com/TeamGraphix/graphix/pull/379 - - ANGLE_PI: float - - def rad_to_angle(angle: float) -> float: - """Prototype for rad_to_angle.""" - ... - - CZ = SWAP - HAS_CZ = True -else: - try: - from graphix.instruction import CZ - - HAS_CZ = True - except ImportError: - HAS_CZ = False - - def CZ(_q0: int, _q1: int) -> None: # noqa: N802 - """In older versions of graphix (<= 0.3.3), CZ instructions were not supported.""" - msg = "CZ instructions are not supported by graphix <= 0.3.3" - raise NotImplementedError(msg) - - try: - from graphix.fundamentals import ANGLE_PI, rad_to_angle - except ImportError: - from math import pi as ANGLE_PI # noqa: N812 - - # Compatibility with graphix <= 0.3.3 - # See https://github.com/TeamGraphix/graphix/pull/399 - def rad_to_angle(angle: float) -> float: - """In older versions of graphix (<= 0.3.3), instruction angles were expressed in radians.""" - return angle - def test_parse_simple_circuit() -> None: """Test parse simple circuit.""" @@ -57,7 +20,7 @@ def test_parse_simple_circuit() -> None: assert circuit.width == 1 assert len(circuit.instruction) == 1 instruction = circuit.instruction[0] - assert isinstance(instruction, RZ) + assert isinstance(instruction, Instruction.RZ) assert isinstance(instruction.angle, float) assert math.isclose(instruction.angle, 5 * ANGLE_PI / 4) @@ -74,12 +37,12 @@ def test_parse_simple_circuit_old_syntax() -> None: assert circuit.width == 1 assert len(circuit.instruction) == 1 instruction = circuit.instruction[0] - assert isinstance(instruction, RZ) + assert isinstance(instruction, Instruction.RZ) assert isinstance(instruction.angle, float) assert math.isclose(instruction.angle, 5 * ANGLE_PI / 4) -def test_parse_all_instructions() -> None: +def test_parse_all_instructions() -> None: # noqa: PLR0915 """Test parse all instructions.""" s = """ include "qelib1.inc"; @@ -87,7 +50,7 @@ def test_parse_all_instructions() -> None: ccx q[0], q[1], q[2]; cx q[0], q[1]; swap q[0], q[1]; -// cz q[0], q[1]; +cz q[0], q[1]; h q[0]; s q[0]; x q[0]; @@ -102,43 +65,46 @@ def test_parse_all_instructions() -> None: assert circuit.width == 3 iterator = iter(circuit.instruction) instruction = next(iterator) - assert isinstance(instruction, CCX) + assert isinstance(instruction, Instruction.CCX) assert instruction.target == 2 assert instruction.controls == (0, 1) instruction = next(iterator) - assert isinstance(instruction, CNOT) + assert isinstance(instruction, Instruction.CNOT) assert instruction.target == 1 assert instruction.control == 0 instruction = next(iterator) - assert isinstance(instruction, SWAP) + assert isinstance(instruction, Instruction.SWAP) assert instruction.targets == (0, 1) instruction = next(iterator) - assert isinstance(instruction, H) + assert isinstance(instruction, Instruction.CZ) + assert instruction.targets == (0, 1) + instruction = next(iterator) + assert isinstance(instruction, Instruction.H) assert instruction.target == 0 instruction = next(iterator) - assert isinstance(instruction, S) + assert isinstance(instruction, Instruction.S) assert instruction.target == 0 instruction = next(iterator) - assert isinstance(instruction, X) + assert isinstance(instruction, Instruction.X) assert instruction.target == 0 instruction = next(iterator) - assert isinstance(instruction, Y) + assert isinstance(instruction, Instruction.Y) assert instruction.target == 0 instruction = next(iterator) - assert isinstance(instruction, Z) + assert isinstance(instruction, Instruction.Z) assert instruction.target == 0 instruction = next(iterator) - assert isinstance(instruction, RX) + assert isinstance(instruction, Instruction.RX) assert instruction.target == 0 assert isinstance(instruction.angle, float) assert math.isclose(instruction.angle, ANGLE_PI / 4) instruction = next(iterator) - assert isinstance(instruction, RY) + assert isinstance(instruction, Instruction.RY) assert instruction.target == 0 assert isinstance(instruction.angle, float) assert math.isclose(instruction.angle, ANGLE_PI / 4) instruction = next(iterator) - assert isinstance(instruction, RZ) + assert isinstance(instruction, Instruction.RZ) assert instruction.target == 0 assert isinstance(instruction.angle, float) assert math.isclose(instruction.angle, ANGLE_PI / 4) @@ -146,25 +112,6 @@ def test_parse_all_instructions() -> None: next(iterator) -@pytest.mark.skipif(not HAS_CZ, reason="CZ instructions are not supported by graphix <= 0.3.3") -def test_parse_cz() -> None: - """Test parse CZ instructions.""" - s = """ -include "qelib1.inc"; -qubit[2] q; -cz q[0], q[1]; -""" - parser = OpenQASMParser() - circuit = parser.parse_str(s) - assert circuit.width == 2 - iterator = iter(circuit.instruction) - instruction = next(iterator) - assert isinstance(instruction, CZ) - assert instruction.targets == (0, 1) - with pytest.raises(StopIteration): - next(iterator) - - def test_parse_all_expressions() -> None: """Test parse all expressions.""" s = """ @@ -186,43 +133,43 @@ def test_parse_all_expressions() -> None: assert circuit.width == 1 iterator = iter(circuit.instruction) instruction = next(iterator) - assert isinstance(instruction, RZ) + assert isinstance(instruction, Instruction.RZ) assert isinstance(instruction.angle, float) assert math.isclose(instruction.angle, rad_to_angle(1)) instruction = next(iterator) - assert isinstance(instruction, RZ) + assert isinstance(instruction, Instruction.RZ) assert isinstance(instruction.angle, float) assert math.isclose(instruction.angle, rad_to_angle(1.5)) instruction = next(iterator) - assert isinstance(instruction, RZ) + assert isinstance(instruction, Instruction.RZ) assert isinstance(instruction.angle, float) assert math.isclose(instruction.angle, rad_to_angle(-1)) instruction = next(iterator) - assert isinstance(instruction, RZ) + assert isinstance(instruction, Instruction.RZ) assert isinstance(instruction.angle, float) assert math.isclose(instruction.angle, rad_to_angle(1 + 2)) instruction = next(iterator) - assert isinstance(instruction, RZ) + assert isinstance(instruction, Instruction.RZ) assert isinstance(instruction.angle, float) assert math.isclose(instruction.angle, rad_to_angle(1 - 2)) instruction = next(iterator) - assert isinstance(instruction, RZ) + assert isinstance(instruction, Instruction.RZ) assert isinstance(instruction.angle, float) assert math.isclose(instruction.angle, rad_to_angle(1 * 2)) instruction = next(iterator) - assert isinstance(instruction, RZ) + assert isinstance(instruction, Instruction.RZ) assert isinstance(instruction.angle, float) assert math.isclose(instruction.angle, rad_to_angle(1 / 2)) instruction = next(iterator) - assert isinstance(instruction, RZ) + assert isinstance(instruction, Instruction.RZ) assert isinstance(instruction.angle, float) assert math.isclose(instruction.angle, rad_to_angle(1 - (2 + 3))) instruction = next(iterator) - assert isinstance(instruction, RZ) + assert isinstance(instruction, Instruction.RZ) assert isinstance(instruction.angle, float) assert math.isclose(instruction.angle, ANGLE_PI) instruction = next(iterator) - assert isinstance(instruction, RZ) + assert isinstance(instruction, Instruction.RZ) assert isinstance(instruction.angle, float) assert math.isclose(instruction.angle, ANGLE_PI) with pytest.raises(StopIteration): @@ -243,7 +190,7 @@ def test_const_declarations() -> None: assert circuit.width == 1 iterator = iter(circuit.instruction) instruction = next(iterator) - assert isinstance(instruction, RZ) + assert isinstance(instruction, Instruction.RZ) assert isinstance(instruction.angle, float) assert math.isclose(instruction.angle, ANGLE_PI / 4) with pytest.raises(StopIteration): @@ -272,10 +219,10 @@ def test_measurement() -> None: circuit = parser.parse_str(s) assert circuit.width == 6 assert circuit.instruction == [ - M(0, Axis.Z), - M(1, Axis.Z), - M(2, Axis.Z), - M(3, Axis.Z), - M(4, Axis.Z), - M(5, Axis.Z), + Instruction.M(0, Axis.Z), + Instruction.M(1, Axis.Z), + Instruction.M(2, Axis.Z), + Instruction.M(3, Axis.Z), + Instruction.M(4, Axis.Z), + Instruction.M(5, Axis.Z), ] From 8c43fea9ce9a4159c91c17c50d7c261cc96abe91 Mon Sep 17 00:00:00 2001 From: Thierry Martinez Date: Sun, 13 Sep 2026 16:51:46 +0200 Subject: [PATCH 4/4] Add some documentation to `_Bit.index` --- graphix_qasm_parser/parser.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/graphix_qasm_parser/parser.py b/graphix_qasm_parser/parser.py index 6157a6c..0f21cc7 100644 --- a/graphix_qasm_parser/parser.py +++ b/graphix_qasm_parser/parser.py @@ -237,6 +237,25 @@ class _DeclKind(Enum): @dataclass class _Bit(_Value): index: int | None = None + """The index of the measurement. + + In Graphix circuits, measurement outcomes are indexed by the rank + of the measurement (the index of the outcome of the first + measurement is 0, the index of the outcome of the second + measurement is 1, etc.). Each time a measurement outcome is stored + in a bit register in the QASM file, the index of the outcome is + stored in this field, so that when the bit register is referenced + subsequently, we can retrieve the index of the corresponding + measurement. + + ``None`` means that no measurement outcome has been assigned to + the bit register yet. + + Note that bit registers cannot be referenced yet, since we do not + support conditional instructions yet. Support for conditional + instructions will be introduced in + https://github.com/TeamGraphix/graphix-qasm-parser/pull/17. + """ @dataclass