From bf65034a1a9b8b23dbbd6a51f593c8e0c53754a9 Mon Sep 17 00:00:00 2001 From: arunjmoorthy Date: Mon, 26 May 2025 13:35:28 -0700 Subject: [PATCH 1/8] Fix #182: added pulse dependency/docs --- README.md | 8 ++++++++ pyproject.toml | 1 + 2 files changed, 9 insertions(+) diff --git a/README.md b/README.md index 4a47a245..17735776 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,14 @@ PyQASM requires Python 3.10 or greater, and can be installed with pip as follows pip install pyqasm ``` +### Optional Dependencies + +PyQASM provides an optional extra called pyqasm[pulse] that adds pulse/calibration features. + +```bash +pip install pyqasm[pulse] +``` + ### Install from source You can also install from source by cloning this repository and running a pip install command diff --git a/pyproject.toml b/pyproject.toml index f78becce..10a6f0b1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,7 @@ test = ["pytest", "pytest-cov", "pytest-mpl", "matplotlib"] lint = ["black", "isort>=6.0.0", "pylint", "mypy", "qbraid-cli>=0.10.2"] docs = ["sphinx>=7.3.7,<8.3.0", "sphinx-autodoc-typehints>=1.24,<3.2", "sphinx-rtd-theme>=2.0.0,<4.0.0", "docutils<0.22", "sphinx-copybutton"] visualization = ["matplotlib"] +pulse = ["openpulse[parser]>=1.0.1"] [tool.setuptools.package-data] pyqasm = ["py.typed", "*.pyx"] From 94498560d2cc4c25a5c12fa0682c23b0cdb3ab47 Mon Sep 17 00:00:00 2001 From: arunjmoorthy Date: Wed, 28 May 2025 00:08:30 -0700 Subject: [PATCH 2/8] readme/test_import updated --- README.md | 12 ++++++++++++ tests/pulse/test_import.py | 8 ++++++++ 2 files changed, 20 insertions(+) create mode 100644 tests/pulse/test_import.py diff --git a/README.md b/README.md index 17735776..7baa7c70 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,18 @@ PyQASM provides an optional extra called pyqasm[pulse] that adds pulse/calibrati pip install pyqasm[pulse] ``` +PyQASM also offers optional extras for command-line interface (CLI) functionality and for program visualization. + +To install the CLI tools: +```bash +pip install pyqasm[cli] +``` + +To install the visualization tools: +```bash +pip install pyqasm[visualization] +``` + ### Install from source You can also install from source by cloning this repository and running a pip install command diff --git a/tests/pulse/test_import.py b/tests/pulse/test_import.py new file mode 100644 index 00000000..4fa9b371 --- /dev/null +++ b/tests/pulse/test_import.py @@ -0,0 +1,8 @@ +import pytest + +def test_openpulse_import(): + """Tests that openpulse can be imported.""" + try: + import openpulse + except ImportError: + pytest.fail("Failed to import openpulse. Ensure that pyqasm[pulse] is installed.") \ No newline at end of file From b56be8425a5b7c7ee623aa6046175f6fdbfb18ce Mon Sep 17 00:00:00 2001 From: arunjmoorthy Date: Wed, 28 May 2025 01:09:53 -0700 Subject: [PATCH 3/8] test_import updated --- tests/pulse/test_import.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/pulse/test_import.py b/tests/pulse/test_import.py index 4fa9b371..f981a990 100644 --- a/tests/pulse/test_import.py +++ b/tests/pulse/test_import.py @@ -1,8 +1,5 @@ import pytest def test_openpulse_import(): - """Tests that openpulse can be imported.""" - try: - import openpulse - except ImportError: - pytest.fail("Failed to import openpulse. Ensure that pyqasm[pulse] is installed.") \ No newline at end of file + """Tests that openpulse can be imported if pyqasm[pulse] is installed.""" + pytest.importorskip("openpulse") \ No newline at end of file From 551bd1d838c7007166df939ceeaa9cdf1eafa345 Mon Sep 17 00:00:00 2001 From: arunjmoorthy Date: Wed, 28 May 2025 01:22:13 -0700 Subject: [PATCH 4/8] fixed import order --- tests/pulse/test_import.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/pulse/test_import.py b/tests/pulse/test_import.py index f981a990..c1d2f3b6 100644 --- a/tests/pulse/test_import.py +++ b/tests/pulse/test_import.py @@ -1,5 +1,7 @@ +"""Tests for pulse functionality.""" import pytest + def test_openpulse_import(): """Tests that openpulse can be imported if pyqasm[pulse] is installed.""" - pytest.importorskip("openpulse") \ No newline at end of file + pytest.importorskip("openpulse") From f8ef8fb258c4214cb2434d57f1c914ea5ccc0b66 Mon Sep 17 00:00:00 2001 From: arunjmoorthy Date: Wed, 28 May 2025 01:25:35 -0700 Subject: [PATCH 5/8] fixed black formatting --- tests/pulse/test_import.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/pulse/test_import.py b/tests/pulse/test_import.py index c1d2f3b6..29b5ed20 100644 --- a/tests/pulse/test_import.py +++ b/tests/pulse/test_import.py @@ -1,4 +1,5 @@ """Tests for pulse functionality.""" + import pytest From 800251c54e8eb5a426962b15dce0de1a3a1c4cd7 Mon Sep 17 00:00:00 2001 From: arunjmoorthy Date: Wed, 28 May 2025 01:30:33 -0700 Subject: [PATCH 6/8] added license header --- tests/pulse/test_import.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/pulse/test_import.py b/tests/pulse/test_import.py index 29b5ed20..f7e6abb1 100644 --- a/tests/pulse/test_import.py +++ b/tests/pulse/test_import.py @@ -1,3 +1,17 @@ +# Copyright 2025 qBraid +# +# 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. + """Tests for pulse functionality.""" import pytest From 008f8eb35b2771c401b2564d252427a82532eba3 Mon Sep 17 00:00:00 2001 From: arunjmoorthy Date: Wed, 2 Jul 2025 21:39:23 -0700 Subject: [PATCH 7/8] structure for defcal --- examples/pulse_unroll_example.py | 141 ++++++++++++++++++++++ src/pyqasm/entrypoint.py | 14 ++- src/pyqasm/modules/qasm3.py | 3 + src/pyqasm/visitor.py | 199 ++++++++++++++++++++++++++++++- 4 files changed, 351 insertions(+), 6 deletions(-) create mode 100644 examples/pulse_unroll_example.py diff --git a/examples/pulse_unroll_example.py b/examples/pulse_unroll_example.py new file mode 100644 index 00000000..76459c3f --- /dev/null +++ b/examples/pulse_unroll_example.py @@ -0,0 +1,141 @@ +# Copyright 2025 qBraid +# +# 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. + +# pylint: disable=invalid-name + +""" +Script demonstrating how to unroll a QASM 3 program using pyqasm. + +""" + +from pyqasm import dumps, loads + +complex_example = """ +OPENQASM 3; +defcalgrammar "openpulse"; + +cal { + extern port q0_drive; + extern port q0_readout; + extern frame q0_frame = newframe(q0_drive, 5.2e9, 0.0); + extern frame q0_readout_frame = newframe(q0_readout, 6.1e9, 0.0); + extern frame q0_acquire = newframe(q0_readout, 6.1e9, 0.0); +} + +const duration pulse_length = 60ns; +const duration meas_length = 800ns; +const duration buffer_time = 20ns; + +waveform gaussian_pulse = gaussian(pulse_length, 0.3, 10ns); +waveform drag_pulse = gaussian(pulse_length, 0.25, 10ns, alpha=0.5); +waveform meas_pulse = constant(meas_length, 0.2); +waveform zero_pad = constant(buffer_time, 0.0); + +defcal rb_sequence $q0 { + for int i in [0:20] { + bit[2] selector = random[2]; + + switch(selector) { + case 0: play(q0_frame, gaussian_pulse); + case 1: play(q0_frame, drag_pulse); + case 2: { + play(q0_frame, gaussian_pulse); + delay[pulse_length] q0_frame; + play(q0_frame, drag_pulse); + } + default: { + play(q0_frame, drag_pulse); + delay[pulse_length] q0_frame; + play(q0_frame, gaussian_pulse); + } + } + delay[buffer_time] q0_frame; + } + + play(q0_readout_frame, meas_pulse); + capture(q0_acquire, meas_pulse); + + box { + bit result = get_measure(q0_acquire); + stream result; + if (result) { + delay[1ms] q0_frame; + } + } +} + +qubit[1] q; +bit[1] c; + +rb_sequence q[0]; +c[0] = measure q[0]; +""" + +simple_example = """ +OPENQASM 3; +defcalgrammar "openpulse"; + +cal { + port tx_port; + frame tx_frame = newframe(tx_port2, 7883050000.0, 0); + waveform readout_waveform_wf = constant(5e-06, 0.03); + for int shot in [0:499] { + play(readout_waveform_wf, tx_frame2); + barrier tx_frame2; + } +} +""" + +# cal { +# extern drag(complex[size] amp, duration l, duration sigma, float[size] beta) -> waveform; +# extern gaussian_square(complex[size] amp, duration l, duration square_width, duration sigma) -> waveform; + +# extern port q0; +# extern port q1; + +# frame q0_frame = newframe(q0, q0_freq, 0); +# frame q1_frame = newframe(q1, q1_freq, 0); +# } + +example = """ +OPENQASM 3; +defcalgrammar "openpulse"; + +const float q0_freq = 5.0e9; +const float q1_freq = 5.1e9; + +defcal rz(angle theta, angle theta) $0 { + shift_phase(q0_frame, theta); +} + +defcal rz(angle theta) $1 { + shift_phase(q1_frame, theta); +} + +defcal sx $0 { + waveform sx_wf = drag(0.2+0.1im, 160dt, 40dt, 0.05); + play(q0_frame, sx_wf); +} + +defcal sx $1 { + waveform sx_wf = drag(0.1+0.05im, 160dt, 40dt, 0.1); + play(q1_frame, sx_wf); +} +""" + +program = loads(example) + +program.unroll() + +print(dumps(program)) diff --git a/src/pyqasm/entrypoint.py b/src/pyqasm/entrypoint.py index af083021..e44badf5 100644 --- a/src/pyqasm/entrypoint.py +++ b/src/pyqasm/entrypoint.py @@ -21,6 +21,7 @@ from typing import TYPE_CHECKING import openqasm3 +import openpulse from pyqasm.exceptions import ValidationError from pyqasm.maps import SUPPORTED_QASM_VERSIONS @@ -63,11 +64,14 @@ def loads(program: openqasm3.ast.Program | str) -> QasmModule: """ if isinstance(program, str): try: - program = openqasm3.parse(program) - except openqasm3.parser.QASM3ParsingError as err: - raise ValidationError(f"Failed to parse OpenQASM string: {err}") from err - elif not isinstance(program, openqasm3.ast.Program): - raise TypeError("Input quantum program must be of type 'str' or 'openqasm3.ast.Program'.") + if 'defcalgrammar "openpulse"' in program: + program = openpulse.parse(program) + else: + program = openqasm3.parse(program) + except (openqasm3.parser.QASM3ParsingError, openpulse.parser.OpenPulseParsingError) as err: + raise ValidationError(f"Failed to parse the OpenQASM/Openpulse string: {err}") from err + elif not isinstance(program, openqasm3.ast.Program): + raise TypeError("Input quantum program must be of type 'str' or 'openqasm3.ast.Program' or 'openpulse.ast.Program'.") if program.version not in SUPPORTED_QASM_VERSIONS: raise ValidationError( f"Unsupported OpenQASM version: {program.version}. " diff --git a/src/pyqasm/modules/qasm3.py b/src/pyqasm/modules/qasm3.py index 8ed08d51..d4348ed8 100644 --- a/src/pyqasm/modules/qasm3.py +++ b/src/pyqasm/modules/qasm3.py @@ -49,6 +49,9 @@ def accept(self, visitor): visitor (QasmVisitor): The visitor to accept """ unrolled_stmt_list = visitor.visit_basic_block(self._statements) + print("--------------------------------") + print("Unrolled stmt list: ", unrolled_stmt_list) + print("--------------------------------") final_stmt_list = visitor.finalize(unrolled_stmt_list) self._unrolled_ast.statements = final_stmt_list diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index bb24ef47..0bc84a87 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -26,6 +26,7 @@ import numpy as np import openqasm3.ast as qasm3_ast +import openpulse.ast as pulse_ast from openqasm3.printer import dumps from pyqasm.analyzer import Qasm3Analyzer @@ -84,6 +85,7 @@ def __init__( self._custom_gates: dict[str, qasm3_ast.QuantumGateDefinition] = {} self._external_gates: list[str] = [] if external_gates is None else external_gates self._subroutine_defns: dict[str, qasm3_ast.SubroutineDefinition] = {} + self._calibration_defns: dict[tuple[str, tuple], pulse_ast.CalibrationDefinition] = {} self._check_only: bool = check_only self._unroll_barriers: bool = unroll_barriers self._curr_scope: int = 0 @@ -2145,6 +2147,198 @@ def _visit_include(self, include: qasm3_ast.Include) -> list[qasm3_ast.Statement return [include] + def _visit_calibration_statement(self, statement: qasm3_ast.CalibrationStatement) -> list[qasm3_ast.Statement]: + """Validate a calibration definition. + + Args: + definition (qasm3_ast.CalibrationDefinition): The definition to validate. + + Raises: + ValidationError: If the definition is invalid. + """ + print("Calibration Statement Structure:") + print("Statement: ", statement) + + return [statement] + + def _visit_calibration_definition(self, definition: qasm3_ast.CalibrationDefinition) -> list[qasm3_ast.CalibrationDefinition]: + """Visit a calibration definition element (defcal block). + + Args: + definition (qasm3_ast.CalibrationDefinition): The calibration definition to visit. + + Returns: + list[qasm3_ast.CalibrationDefinition]: The calibration definition if not in check_only mode, empty list if in check_only mode. + """ + gate_name = definition.name.name + print("--------------------------------") + print("Gate name: ", gate_name) + print("--------------------------------") + + # 1. Parse and validate physical qubit identifiers + physical_qubits = self._parse_physical_qubits(definition.qubits, definition) + self._validate_calibration_arguments(definition.arguments, gate_name, definition) + calibration_key = (gate_name, tuple(physical_qubits)) + if calibration_key in self._calibration_defns: + raise_qasm3_error( + f"Duplicate calibration definition for gate '{gate_name}' " + f"on physical qubits {physical_qubits}", + error_node=definition, + span=definition.span, + ) + + self._validate_calibration_body(definition.body, gate_name, definition) + self._calibration_defns[calibration_key] = definition + + logger.debug("Added calibration definition for gate '%s' on qubits %s", + gate_name, physical_qubits) + + if self._check_only: + return [] + + return [definition] + + def _parse_physical_qubits(self, qubits: list, definition: qasm3_ast.CalibrationDefinition) -> list[int]: + """Parse physical qubit identifiers from calibration definition. + + Args: + qubits: List of qubit identifiers from the calibration definition + definition: The calibration definition for error reporting + + Returns: + list[int]: List of physical qubit indices + """ + physical_qubits = [] + + for qubit in qubits: + if not isinstance(qubit, qasm3_ast.Identifier): + raise_qasm3_error( + f"Expected physical qubit identifier, got {type(qubit).__name__}", + error_node=definition, + span=definition.span, + ) + + qubit_name = qubit.name + + # Physical qubits should start with '$' + if not qubit_name.startswith('$'): + raise_qasm3_error( + f"Physical qubit identifier must start with '$', got '{qubit_name}'", + error_node=definition, + span=definition.span, + ) + + try: + physical_qubit_id = int(qubit_name[1:]) # Remove '$' and convert to int + physical_qubits.append(physical_qubit_id) + except ValueError: + raise_qasm3_error( + f"Invalid physical qubit identifier '{qubit_name}'. " + "Expected format: $", + error_node=definition, + span=definition.span, + ) + + return physical_qubits + + def _validate_calibration_arguments(self, arguments: list, gate_name: str, definition: qasm3_ast.CalibrationDefinition) -> None: + """Validate the classical arguments of a calibration definition. + + Args: + arguments: List of classical arguments + gate_name: Name of the gate being calibrated + definition: The calibration definition for error reporting + """ + seen_arg_names = set() + + for arg in arguments: + if not isinstance(arg, qasm3_ast.ClassicalArgument): + raise_qasm3_error( + f"Expected classical argument in calibration definition for '{gate_name}'", + error_node=definition, + span=definition.span, + ) + + arg_name = arg.name.name + if arg_name in seen_arg_names: + raise_qasm3_error( + f"Duplicate argument name '{arg_name}' in calibration definition for '{gate_name}'", + error_node=definition, + span=definition.span, + ) + seen_arg_names.add(arg_name) + + # Validate argument types (angle, duration, etc.) + arg_type = arg.type + valid_types = (qasm3_ast.AngleType, qasm3_ast.DurationType, + qasm3_ast.FloatType, qasm3_ast.IntType) + + if not isinstance(arg_type, valid_types): + raise_qasm3_error( + f"Unsupported argument type '{type(arg_type).__name__}' " + f"in calibration definition for '{gate_name}'", + error_node=definition, + span=definition.span, + ) + + def _validate_calibration_body(self, body: list, gate_name: str, + definition: qasm3_ast.CalibrationDefinition) -> None: + """Validate the statements inside a calibration definition body. + + Args: + body: List of statements in the calibration body + gate_name: Name of the gate being calibrated + definition: The calibration definition for error reporting + """ + + # Need a full list of the allowed statement types + allowed_stmt_types = { + qasm3_ast.ExpressionStatement, # For functions like shift_phase, play + qasm3_ast.ClassicalDeclaration, # For waveform declarations + qasm3_ast.ClassicalAssignment, # For variable assignments + } + + # Need a full list of the allowed functions + allowed_functions = { + 'shift_phase', 'play', 'delay', 'set_frequency', 'set_phase', + 'drag', 'gaussian', 'newframe', 'capture_v0', 'capture_v1', + } + + for stmt in body: + stmt_type = type(stmt) + + if stmt_type not in allowed_stmt_types: + raise_qasm3_error( + f"Unsupported statement type '{stmt_type.__name__}' " + f"in calibration definition for '{gate_name}'", + error_node=definition, + span=definition.span, + ) + + if isinstance(stmt, qasm3_ast.ExpressionStatement): + if isinstance(stmt.expression, qasm3_ast.FunctionCall): + func_name = stmt.expression.name.name + if func_name not in allowed_functions: + raise_qasm3_error( + f"Unsupported function '{func_name}' " + f"in calibration definition for '{gate_name}'", + error_node=definition, + span=definition.span, + ) + + def _visit_calibration_grammar_declaration(self, declaration: qasm3_ast.CalibrationGrammarDeclaration) -> list[qasm3_ast.Statement]: + """Visit a calibration statement element. + + Args: + statement (qasm3_ast.CalibrationStatement): The calibration statement to visit. + + Returns: + list[qasm3_ast.Statement]: The list of statements generated by the calibration. + """ + print("Calibration Definition Structure:") + + return [] + def visit_statement(self, statement: qasm3_ast.Statement) -> list[qasm3_ast.Statement]: """Visit a statement element. @@ -2175,6 +2369,9 @@ def visit_statement(self, statement: qasm3_ast.Statement) -> list[qasm3_ast.Stat qasm3_ast.SubroutineDefinition: self._visit_subroutine_definition, qasm3_ast.ExpressionStatement: lambda x: self._visit_function_call(x.expression), qasm3_ast.IODeclaration: lambda x: [], + pulse_ast.CalibrationStatement: self._visit_calibration_statement, + pulse_ast.CalibrationDefinition: self._visit_calibration_definition, + pulse_ast.CalibrationGrammarDeclaration: self._visit_calibration_grammar_declaration, } visitor_function = visit_map.get(type(statement)) @@ -2227,4 +2424,4 @@ def finalize(self, unrolled_stmts): if isinstance(stmt, qasm3_ast.QuantumPhase): if len(stmt.qubits) == len(self._qubit_labels): stmt.qubits = [] - return unrolled_stmts + return unrolled_stmts \ No newline at end of file From bce09b4871bfff51a42df53883fc1adfc7337403 Mon Sep 17 00:00:00 2001 From: arunjmoorthy Date: Sun, 6 Jul 2025 13:33:51 -0700 Subject: [PATCH 8/8] defcal block parsing structure --- examples/pulse_unroll_example.py | 4 +- src/pyqasm/modules/qasm3.py | 6 +- src/pyqasm/visitor.py | 486 ++++++++++++++++++++----------- 3 files changed, 320 insertions(+), 176 deletions(-) diff --git a/examples/pulse_unroll_example.py b/examples/pulse_unroll_example.py index 76459c3f..845767a9 100644 --- a/examples/pulse_unroll_example.py +++ b/examples/pulse_unroll_example.py @@ -115,7 +115,7 @@ const float q0_freq = 5.0e9; const float q1_freq = 5.1e9; -defcal rz(angle theta, angle theta) $0 { +defcal rz(angle theta) $0 { shift_phase(q0_frame, theta); } @@ -134,7 +134,7 @@ } """ -program = loads(example) +program = loads(simple_example) program.unroll() diff --git a/src/pyqasm/modules/qasm3.py b/src/pyqasm/modules/qasm3.py index d4348ed8..3524ea00 100644 --- a/src/pyqasm/modules/qasm3.py +++ b/src/pyqasm/modules/qasm3.py @@ -49,9 +49,9 @@ def accept(self, visitor): visitor (QasmVisitor): The visitor to accept """ unrolled_stmt_list = visitor.visit_basic_block(self._statements) - print("--------------------------------") - print("Unrolled stmt list: ", unrolled_stmt_list) - print("--------------------------------") + # print("--------------------------------") + # print("Unrolled stmt list: ", unrolled_stmt_list) + # print("--------------------------------") final_stmt_list = visitor.finalize(unrolled_stmt_list) self._unrolled_ast.statements = final_stmt_list diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index 0bc84a87..0fa297e1 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -2146,198 +2146,59 @@ def _visit_include(self, include: qasm3_ast.Include) -> list[qasm3_ast.Statement return [] return [include] + + + + # You'll also need these other visitor methods for your visit map: + def _visit_calibration_statement(self, statement: qasm3_ast.CalibrationStatement) -> list[qasm3_ast.Statement]: - """Validate a calibration definition. + """Visit a calibration statement element. Args: - definition (qasm3_ast.CalibrationDefinition): The definition to validate. + statement (qasm3_ast.CalibrationStatement): The calibration statement to visit. - Raises: - ValidationError: If the definition is invalid. + Returns: + list[qasm3_ast.Statement]: The calibration statement if not in check_only mode. """ print("Calibration Statement Structure:") print("Statement: ", statement) - return [statement] - - def _visit_calibration_definition(self, definition: qasm3_ast.CalibrationDefinition) -> list[qasm3_ast.CalibrationDefinition]: - """Visit a calibration definition element (defcal block). - - Args: - definition (qasm3_ast.CalibrationDefinition): The calibration definition to visit. - - Returns: - list[qasm3_ast.CalibrationDefinition]: The calibration definition if not in check_only mode, empty list if in check_only mode. - """ - gate_name = definition.name.name - print("--------------------------------") - print("Gate name: ", gate_name) - print("--------------------------------") - - # 1. Parse and validate physical qubit identifiers - physical_qubits = self._parse_physical_qubits(definition.qubits, definition) - self._validate_calibration_arguments(definition.arguments, gate_name, definition) - calibration_key = (gate_name, tuple(physical_qubits)) - if calibration_key in self._calibration_defns: - raise_qasm3_error( - f"Duplicate calibration definition for gate '{gate_name}' " - f"on physical qubits {physical_qubits}", - error_node=definition, - span=definition.span, - ) - - self._validate_calibration_body(definition.body, gate_name, definition) - self._calibration_defns[calibration_key] = definition - - logger.debug("Added calibration definition for gate '%s' on qubits %s", - gate_name, physical_qubits) + # Add specific validation for CalibrationStatement if needed + # This might be different from CalibrationDefinition if self._check_only: return [] - return [definition] - - def _parse_physical_qubits(self, qubits: list, definition: qasm3_ast.CalibrationDefinition) -> list[int]: - """Parse physical qubit identifiers from calibration definition. + return [statement] + + def _visit_calibration_grammar_declaration(self, declaration: qasm3_ast.CalibrationGrammarDeclaration) -> list[qasm3_ast.Statement]: + """Visit a calibration grammar declaration element. Args: - qubits: List of qubit identifiers from the calibration definition - definition: The calibration definition for error reporting + declaration (qasm3_ast.CalibrationGrammarDeclaration): The calibration grammar declaration to visit. Returns: - list[int]: List of physical qubit indices + list[qasm3_ast.Statement]: The declaration if not in check_only mode. """ - physical_qubits = [] + print("Calibration Grammar Declaration Structure:") + print("Declaration: ", declaration) - for qubit in qubits: - if not isinstance(qubit, qasm3_ast.Identifier): - raise_qasm3_error( - f"Expected physical qubit identifier, got {type(qubit).__name__}", - error_node=definition, - span=definition.span, - ) - - qubit_name = qubit.name - - # Physical qubits should start with '$' - if not qubit_name.startswith('$'): - raise_qasm3_error( - f"Physical qubit identifier must start with '$', got '{qubit_name}'", - error_node=definition, - span=definition.span, - ) - - try: - physical_qubit_id = int(qubit_name[1:]) # Remove '$' and convert to int - physical_qubits.append(physical_qubit_id) - except ValueError: - raise_qasm3_error( - f"Invalid physical qubit identifier '{qubit_name}'. " - "Expected format: $", - error_node=definition, - span=definition.span, - ) + grammar_name = declaration.name - return physical_qubits - - def _validate_calibration_arguments(self, arguments: list, gate_name: str, definition: qasm3_ast.CalibrationDefinition) -> None: - """Validate the classical arguments of a calibration definition. - - Args: - arguments: List of classical arguments - gate_name: Name of the gate being calibrated - definition: The calibration definition for error reporting - """ - seen_arg_names = set() - - for arg in arguments: - if not isinstance(arg, qasm3_ast.ClassicalArgument): - raise_qasm3_error( - f"Expected classical argument in calibration definition for '{gate_name}'", - error_node=definition, - span=definition.span, - ) - - arg_name = arg.name.name - if arg_name in seen_arg_names: - raise_qasm3_error( - f"Duplicate argument name '{arg_name}' in calibration definition for '{gate_name}'", - error_node=definition, - span=definition.span, - ) - seen_arg_names.add(arg_name) - - # Validate argument types (angle, duration, etc.) - arg_type = arg.type - valid_types = (qasm3_ast.AngleType, qasm3_ast.DurationType, - qasm3_ast.FloatType, qasm3_ast.IntType) - - if not isinstance(arg_type, valid_types): - raise_qasm3_error( - f"Unsupported argument type '{type(arg_type).__name__}' " - f"in calibration definition for '{gate_name}'", - error_node=definition, - span=definition.span, - ) - - def _validate_calibration_body(self, body: list, gate_name: str, - definition: qasm3_ast.CalibrationDefinition) -> None: - """Validate the statements inside a calibration definition body. - - Args: - body: List of statements in the calibration body - gate_name: Name of the gate being calibrated - definition: The calibration definition for error reporting - """ - - # Need a full list of the allowed statement types - allowed_stmt_types = { - qasm3_ast.ExpressionStatement, # For functions like shift_phase, play - qasm3_ast.ClassicalDeclaration, # For waveform declarations - qasm3_ast.ClassicalAssignment, # For variable assignments - } - - # Need a full list of the allowed functions - allowed_functions = { - 'shift_phase', 'play', 'delay', 'set_frequency', 'set_phase', - 'drag', 'gaussian', 'newframe', 'capture_v0', 'capture_v1', - } + # Validate supported calibration grammars + supported_grammars = {"openpulse"} # Add more as needed + if grammar_name not in supported_grammars: + raise_qasm3_error( + f"Unsupported calibration grammar '{grammar_name}'", + error_node=declaration, + span=declaration.span, + ) - for stmt in body: - stmt_type = type(stmt) - - if stmt_type not in allowed_stmt_types: - raise_qasm3_error( - f"Unsupported statement type '{stmt_type.__name__}' " - f"in calibration definition for '{gate_name}'", - error_node=definition, - span=definition.span, - ) - - if isinstance(stmt, qasm3_ast.ExpressionStatement): - if isinstance(stmt.expression, qasm3_ast.FunctionCall): - func_name = stmt.expression.name.name - if func_name not in allowed_functions: - raise_qasm3_error( - f"Unsupported function '{func_name}' " - f"in calibration definition for '{gate_name}'", - error_node=definition, - span=definition.span, - ) - - def _visit_calibration_grammar_declaration(self, declaration: qasm3_ast.CalibrationGrammarDeclaration) -> list[qasm3_ast.Statement]: - """Visit a calibration statement element. + if self._check_only: + return [] - Args: - statement (qasm3_ast.CalibrationStatement): The calibration statement to visit. - - Returns: - list[qasm3_ast.Statement]: The list of statements generated by the calibration. - """ - print("Calibration Definition Structure:") - - return [] + return [declaration] def visit_statement(self, statement: qasm3_ast.Statement) -> list[qasm3_ast.Statement]: """Visit a statement element. @@ -2424,4 +2285,287 @@ def finalize(self, unrolled_stmts): if isinstance(stmt, qasm3_ast.QuantumPhase): if len(stmt.qubits) == len(self._qubit_labels): stmt.qubits = [] - return unrolled_stmts \ No newline at end of file + return unrolled_stmts + + + # ---------------------------------------------------------------------------------------------------------------- + # DEFCAL BLOCK IMPLEMENTATION + + def _visit_calibration_definition(self, definition: pulse_ast.CalibrationDefinition) -> list[pulse_ast.CalibrationDefinition]: + """Visit a calibration definition element (defcal block).""" + gate_name = definition.name.name + print("--------------------------------") + print("Gate name: ", gate_name) + print("--------------------------------") + + # Parse physical qubit identifiers + physical_qubits = self._parse_physical_qubits(definition.qubits, definition) + + # Validate calibration arguments + self._validate_calibration_arguments(definition.arguments, gate_name, definition) + + # have to check for duplicate calibration definitions + calibration_key = (gate_name, tuple(physical_qubits)) + if calibration_key in self._calibration_defns: + raise_qasm3_error( + f"Duplicate calibration definition for gate '{gate_name}' " + f"on physical qubits {physical_qubits}", + error_node=definition, + span=definition.span, + ) + + # Validate the calibration body + self._validate_calibration_body(definition.body, gate_name, definition) + + # store calibration definition + self._calibration_defns[calibration_key] = definition + + logger.debug("Added calibration definition for gate '%s' on qubits %s", gate_name, physical_qubits) + + if self._check_only: + return [] + return [definition] + + def _parse_physical_qubits(self, qubits: list, definition: pulse_ast.CalibrationDefinition) -> list[int]: + """Parse physical qubit identifiers from calibration definition.""" + print("--------------------------------") + print("Parsing physical qubits: ", qubits) + + + physical_qubits = [] + seen_qubits = set() + + for qubit in qubits: + print("--------------------------------") + print("Qubit: ", qubit) + print("--------------------------------") + qubit_name = qubit.name + + if qubit_name in seen_qubits: + raise_qasm3_error( + f"Duplicate physical qubit '{qubit_name}' in calibration definition", + error_node=definition, + span=definition.span, + ) + seen_qubits.add(qubit_name) + + if not qubit_name.startswith('$'): + raise_qasm3_error( + f"Physical qubit identifier must start with '$', got '{qubit_name}'", + error_node=definition, + span=definition.span, + ) + + try: + physical_qubit_id = int(qubit_name[1:]) + physical_qubits.append(physical_qubit_id) + except ValueError: + raise_qasm3_error( + f"Invalid physical qubit identifier '{qubit_name}'. Expected format: $", + error_node=definition, + span=definition.span, + ) + + print("--------------------------------") + return physical_qubits + + def _validate_calibration_arguments(self, arguments: list, gate_name: str, definition: pulse_ast.CalibrationDefinition) -> None: + """Validate the classical arguments of a calibration definition.""" + print("--------------------------------") + print("Validating calibration arguments: ", arguments) + print("--------------------------------") + seen_arg_names = set() + + for arg in arguments: + print("--------------------------------") + print("Argument: ", arg) + print("--------------------------------") + arg_name = arg.name.name if hasattr(arg.name, 'name') else str(arg.name) + + if arg_name in seen_arg_names: + raise_qasm3_error( + f"Duplicate argument name '{arg_name}' in calibration definition for '{gate_name}'", + error_node=definition, + span=definition.span, + ) + seen_arg_names.add(arg_name) + + # Validate argument type + type_name = type(arg.type).__name__ + valid_types = {'AngleType', 'DurationType', 'FloatType', 'IntType', 'ComplexType'} + + if type_name not in valid_types: + raise_qasm3_error( + f"Unsupported argument type '{type_name}' for argument '{arg_name}' " + f"in calibration definition for '{gate_name}'", + error_node=definition, + span=definition.span, + ) + + def _validate_calibration_body(self, body: list, gate_name: str, definition: pulse_ast.CalibrationDefinition) -> None: + """Validate calibration body using EXISTING visitor methods.""" + print("--------------------------------") + print("Validating calibration body: ", body) + print("--------------------------------") + # Set up scope for calibration arguments + self._push_scope({}) + self._add_calibration_args_to_scope(definition.arguments) + + try: + print("--------------------------------") + print("Validating calibration body: ", body) + print("--------------------------------") + for stmt in body: + self._validate_calibration_statement_dispatch(stmt, gate_name, definition) + finally: + self._pop_scope() + + def _add_calibration_args_to_scope(self, arguments: list) -> None: + """Add calibration arguments to current scope.""" + for arg in arguments: + arg_name = arg.name.name if hasattr(arg.name, 'name') else str(arg.name) + variable = Variable( + name=arg_name, + base_type=arg.type, + base_size=self._get_calibration_arg_size(arg.type), + dims=None, + value=None, + is_constant=False, + readonly=True + ) + self._add_var_in_scope(variable) + + def _validate_calibration_statement_dispatch(self, stmt, gate_name: str, definition: pulse_ast.CalibrationDefinition) -> None: + """Route calibration statements to EXISTING validators.""" + stmt_type_name = type(stmt).__name__ + + if stmt_type_name == 'ClassicalDeclaration': + # Only validate openpulse-specific types, then use existing validator + self._validate_openpulse_declaration_type(stmt, gate_name, definition) + # Try to use existing validator if compatible + try: + self._visit_classical_declaration(stmt) + except (AttributeError, TypeError): + # If pulse_ast isn't compatible, do basic validation + self._basic_pulse_declaration_validation(stmt, gate_name, definition) + + elif stmt_type_name == 'ClassicalAssignment': + # Use existing assignment validator + try: + self._visit_classical_assignment(stmt) + except Exception as err: + raise_qasm3_error( + f"Invalid assignment in calibration definition for '{gate_name}': {err}", + error_node=definition, + span=definition.span, + raised_from=err, + ) + + elif stmt_type_name in ['BranchingStatement', 'IfStatement']: + # Use existing branching validator + try: + self._visit_branching_statement(stmt) + except Exception as err: + raise_qasm3_error( + f"Invalid branching statement in calibration definition for '{gate_name}': {err}", + error_node=definition, + span=definition.span, + raised_from=err, + ) + + elif stmt_type_name in ['ForInLoop', 'ForLoop']: + # Use existing loop validator + try: + self._visit_forin_loop(stmt) + except Exception as err: + raise_qasm3_error( + f"Invalid loop in calibration definition for '{gate_name}': {err}", + error_node=definition, + span=definition.span, + raised_from=err, + ) + + elif stmt_type_name == 'ExpressionStatement': + # Validate openpulse function calls + self._validate_openpulse_function_call(stmt, gate_name, definition) + + else: + raise_qasm3_error( + f"Unsupported statement type '{stmt_type_name}' in calibration definition for '{gate_name}'", + error_node=definition, + span=definition.span, + ) + + def _get_calibration_arg_size(self, arg_type) -> int: + """Get size for calibration argument type.""" + type_name = type(arg_type).__name__ + return { + 'AngleType': 64, 'FloatType': 64, 'IntType': 32, + 'DurationType': 64, 'ComplexType': 128 + }.get(type_name, 32) + + def _validate_openpulse_declaration_type(self, stmt, gate_name: str, definition) -> None: + """Validate that declaration uses valid openpulse types.""" + type_name = type(stmt.type).__name__ + valid_types = {'WaveformType', 'FrameType', 'PortType', 'IntType', 'FloatType', 'AngleType', 'DurationType'} + + if type_name not in valid_types: + raise_qasm3_error( + f"Invalid type '{type_name}' in calibration definition for '{gate_name}'", + error_node=definition, + span=definition.span, + ) + + def _basic_pulse_declaration_validation(self, stmt, gate_name: str, definition) -> None: + """Basic validation for pulse declarations when existing validator fails.""" + var_name = stmt.identifier.name if hasattr(stmt.identifier, 'name') else str(stmt.identifier) + + if self._check_in_scope(var_name): + raise_qasm3_error( + f"Variable '{var_name}' already exists in scope", + error_node=definition, + span=definition.span, + ) + + # Add to scope manually + variable = Variable( + name=var_name, + base_type=stmt.type, + base_size=self._get_calibration_arg_size(stmt.type), + dims=None, + value=None, + is_constant=False, + readonly=False + ) + self._add_var_in_scope(variable) + + def _validate_openpulse_function_call(self, stmt, gate_name: str, definition) -> None: + """Validate openpulse function calls.""" + if hasattr(stmt, 'expression') and hasattr(stmt.expression, 'name'): + func_name = stmt.expression.name.name + + openpulse_functions = { + 'shift_phase', 'play', 'delay', 'set_frequency', 'set_phase', + 'drag', 'gaussian', 'constant', 'arbitrary', + 'newframe', 'sum', 'mix', 'capture_v0', 'capture_v1', 'capture', + 'barrier', 'reset_phase' + } + + if func_name not in openpulse_functions: + raise_qasm3_error( + f"Invalid function '{func_name}' in calibration definition for '{gate_name}'", + error_node=definition, + span=definition.span, + ) + + # Basic argument validation - check variables exist in scope + if hasattr(stmt.expression, 'arguments'): + for arg in stmt.expression.arguments: + if hasattr(arg, 'name'): + var_name = arg.name if isinstance(arg.name, str) else getattr(arg.name, 'name', str(arg.name)) + if not self._check_in_scope(var_name): + raise_qasm3_error( + f"Undefined variable '{var_name}' in function '{func_name}'", + error_node=definition, + span=definition.span, + ) \ No newline at end of file