From 6638f3b43ff9423fa7d8260520a0c2814b392c4a Mon Sep 17 00:00:00 2001 From: Robert Jovanov Date: Thu, 15 Jan 2026 21:07:03 +0100 Subject: [PATCH 1/6] Add Gaussian OpenPulse waveform generator with PyQASM module export --- .gitignore | 1 + pyproject.toml | 3 + qbraid_algorithms/openpulse/__init__.py | 18 ++++ qbraid_algorithms/openpulse/gaussian.py | 105 ++++++++++++++++++++++++ tox.ini | 2 +- 5 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 qbraid_algorithms/openpulse/__init__.py create mode 100644 qbraid_algorithms/openpulse/gaussian.py diff --git a/.gitignore b/.gitignore index 6b5ae9e..7eae4e3 100644 --- a/.gitignore +++ b/.gitignore @@ -164,3 +164,4 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ +qbraid_algorithms/openpulse/scripts/ diff --git a/pyproject.toml b/pyproject.toml index ce0a431..9f83d58 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,9 @@ cli = [ "rich>=10.11.0", "typing-extensions" ] +openpulse = [ + "openpulse" +] [project.scripts] qbraid-algorithms = "qbraid_algorithms.cli.main:app" diff --git a/qbraid_algorithms/openpulse/__init__.py b/qbraid_algorithms/openpulse/__init__.py new file mode 100644 index 0000000..e6d650d --- /dev/null +++ b/qbraid_algorithms/openpulse/__init__.py @@ -0,0 +1,18 @@ +# Copyright 2026 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. + +"""OpenPulse generators for qbraid-algorithms.""" +from .gaussian import GaussianPulse, generate_program + +__all__ = ["GaussianPulse", "generate_program"] diff --git a/qbraid_algorithms/openpulse/gaussian.py b/qbraid_algorithms/openpulse/gaussian.py new file mode 100644 index 0000000..f49a1d3 --- /dev/null +++ b/qbraid_algorithms/openpulse/gaussian.py @@ -0,0 +1,105 @@ +# 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. + +""" +Gaussian OpenPulse Waveform Generator + +This module provides a Gaussian pulse waveform generator using OpenQASM 3 OpenPulse +calibration syntax. +""" + +from __future__ import annotations + +from dataclasses import dataclass + +import pyqasm + + +def _complex_to_qasm(z: complex) -> str: + """Convert a Python complex number to an OpenQASM complex literal.""" + a = float(z.real) + b = float(z.imag) + sign = "+" if b >= 0 else "-" + return f"{a} {sign} {abs(b)}im" + + +@dataclass(frozen=True) +class GaussianPulse: + """A minimal Gaussian waveform spec for OpenPulse. + + Notes: + - duration/sigma are strings like "16ns", "100e-6s", etc. + - amplitude is complex (OpenQASM complex literal). + """ + + amplitude: complex + duration: str + sigma: str + + def to_waveform_qasm(self, var_name: str = "wf") -> str: + """Generate the OpenQASM waveform definition for this Gaussian pulse.""" + amp = _complex_to_qasm(self.amplitude) + return f"waveform {var_name} = gaussian({amp}, {self.duration}, {self.sigma});" + + +def generate_program( + *, + amplitude: complex = 1.0 + 0.0j, + duration: str = "16ns", + sigma: str = "4ns", + frame_frequency: float = 5.0e9, + frame_phase: float = 0.0, + port_name: str = "d0", + frame_name: str = "driveframe", + waveform_name: str = "wf", + defcal_name: str = "play_gaussian", + qubit: int = 0, +) -> "pyqasm.QasmModule": + """ + Load a Gaussian OpenPulse waveform program as a pyqasm module. + + Args: + amplitude (complex): Complex amplitude of the Gaussian pulse. + duration (str): Total pulse duration (e.g. "16ns"). + sigma (str): Standard deviation of the Gaussian envelope. + frame_frequency (float): Initial frequency of the drive frame in Hz. + frame_phase (float): Initial phase of the drive frame in radians. + port_name (str): Name of the OpenPulse port. + frame_name (str): Name of the OpenPulse frame. + waveform_name (str): Identifier for the Gaussian waveform. + defcal_name (str): Name of the generated calibration routine. + qubit (int): Target qubit index for the calibration routine. + + Returns: + (PyQasm Module) pyqasm module containing the Gaussian OpenPulse program + """ + pulse = GaussianPulse(amplitude=amplitude, duration=duration, sigma=sigma) + wf_line = pulse.to_waveform_qasm(var_name=waveform_name) + + qasm = f"""OPENQASM 3.0; +defcalgrammar "openpulse"; + +cal {{ + port {port_name}; + frame {frame_name} = newframe({port_name}, {frame_frequency}, {frame_phase}); + {wf_line} +}} + +defcal {defcal_name} ${qubit} {{ + play({frame_name}, {waveform_name}); +}} +""" + + module = pyqasm.loads(qasm) + return module diff --git a/tox.ini b/tox.ini index 2ceee78..1505326 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,7 @@ envlist = skip_missing_interpreter = true [testenv] -commands_pre = python -m pip install .[cli] +commands_pre = python -m pip install .[cli,openpulse] basepython = python3 [testenv:unit-tests] From 63786065e9659ffb9f5403c537b9e59286dcb022 Mon Sep 17 00:00:00 2001 From: Robert Jovanov Date: Tue, 27 Jan 2026 17:27:39 +0100 Subject: [PATCH 2/6] Resolving to the correct dependencies --- pyproject.toml | 4 ++-- tox.ini | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9f83d58..c70bb2e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,8 +37,8 @@ cli = [ "rich>=10.11.0", "typing-extensions" ] -openpulse = [ - "openpulse" +pulse = [ + "pyqasm[pulse]" ] [project.scripts] diff --git a/tox.ini b/tox.ini index 1505326..bccbd44 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,7 @@ envlist = skip_missing_interpreter = true [testenv] -commands_pre = python -m pip install .[cli,openpulse] +commands_pre = python -m pip install .[cli,pulse] basepython = python3 [testenv:unit-tests] From 37a82a2f0b398c2d9f2263e566adb62c45ca8bb9 Mon Sep 17 00:00:00 2001 From: Robert Jovanov Date: Tue, 27 Jan 2026 21:45:24 +0100 Subject: [PATCH 3/6] Made the implementation more modular and futureproof for other types of pulses --- qbraid_algorithms/openpulse/__init__.py | 4 +- qbraid_algorithms/openpulse/gaussian.py | 68 +++++++++++++++---------- 2 files changed, 42 insertions(+), 30 deletions(-) diff --git a/qbraid_algorithms/openpulse/__init__.py b/qbraid_algorithms/openpulse/__init__.py index e6d650d..5301c33 100644 --- a/qbraid_algorithms/openpulse/__init__.py +++ b/qbraid_algorithms/openpulse/__init__.py @@ -13,6 +13,6 @@ # limitations under the License. """OpenPulse generators for qbraid-algorithms.""" -from .gaussian import GaussianPulse, generate_program +from .gaussian import GaussianPulse, PulseParams, generate_program -__all__ = ["GaussianPulse", "generate_program"] +__all__ = ["GaussianPulse", "PulseParams", "generate_program"] diff --git a/qbraid_algorithms/openpulse/gaussian.py b/qbraid_algorithms/openpulse/gaussian.py index f49a1d3..9cc4494 100644 --- a/qbraid_algorithms/openpulse/gaussian.py +++ b/qbraid_algorithms/openpulse/gaussian.py @@ -21,7 +21,8 @@ from __future__ import annotations -from dataclasses import dataclass +from dataclasses import asdict, dataclass +from typing import Any import pyqasm @@ -33,11 +34,35 @@ def _complex_to_qasm(z: complex) -> str: sign = "+" if b >= 0 else "-" return f"{a} {sign} {abs(b)}im" +@dataclass(frozen=True) +class PulseParams: + """Common parameters for building an OpenPulse calibration program. + + frame_frequency (float): Initial frequency of the drive frame in Hz. + frame_phase (float): Initial phase of the drive frame in radians. + port_name (str): Name of the OpenPulse port. + frame_name (str): Name of the OpenPulse frame. + waveform_name (str): Identifier for the Gaussian waveform. + defcal_name (str): Name of the generated calibration routine. + qubit (int): Target qubit index for the calibration routine. + """ + + frame_frequency: float = 5.0e9 + frame_phase: float = 0.0 + port_name: str = "d0" + frame_name: str = "driveframe" + waveform_name: str = "wf" + defcal_name: str = "play_pulse" + qubit: int = 0 @dataclass(frozen=True) class GaussianPulse: """A minimal Gaussian waveform spec for OpenPulse. + amplitude (complex): Complex amplitude of the Gaussian pulse. + duration (str): Total pulse duration (e.g. "16ns"). + sigma (str): Standard deviation of the Gaussian envelope. + Notes: - duration/sigma are strings like "16ns", "100e-6s", etc. - amplitude is complex (OpenQASM complex literal). @@ -54,50 +79,37 @@ def to_waveform_qasm(self, var_name: str = "wf") -> str: def generate_program( - *, - amplitude: complex = 1.0 + 0.0j, - duration: str = "16ns", - sigma: str = "4ns", - frame_frequency: float = 5.0e9, - frame_phase: float = 0.0, - port_name: str = "d0", - frame_name: str = "driveframe", - waveform_name: str = "wf", - defcal_name: str = "play_gaussian", - qubit: int = 0, + pulse: GaussianPulse, + params: PulseParams | None = None, + **kwargs: Any, ) -> "pyqasm.QasmModule": """ Load a Gaussian OpenPulse waveform program as a pyqasm module. Args: - amplitude (complex): Complex amplitude of the Gaussian pulse. - duration (str): Total pulse duration (e.g. "16ns"). - sigma (str): Standard deviation of the Gaussian envelope. - frame_frequency (float): Initial frequency of the drive frame in Hz. - frame_phase (float): Initial phase of the drive frame in radians. - port_name (str): Name of the OpenPulse port. - frame_name (str): Name of the OpenPulse frame. - waveform_name (str): Identifier for the Gaussian waveform. - defcal_name (str): Name of the generated calibration routine. - qubit (int): Target qubit index for the calibration routine. + pulse (GaussianPulse): Pulse specification (amplitude, duration, sigma). + params (PulseParams | None): Common OpenPulse/program parameters. + **kwargs: Overrides for fields in PulseParams (e.g. frame_name="...", qubit=1). Returns: (PyQasm Module) pyqasm module containing the Gaussian OpenPulse program """ - pulse = GaussianPulse(amplitude=amplitude, duration=duration, sigma=sigma) - wf_line = pulse.to_waveform_qasm(var_name=waveform_name) + params = params or PulseParams() + p = {**asdict(params), **kwargs} + + wf_line = pulse.to_waveform_qasm(var_name=p["waveform_name"]) qasm = f"""OPENQASM 3.0; defcalgrammar "openpulse"; cal {{ - port {port_name}; - frame {frame_name} = newframe({port_name}, {frame_frequency}, {frame_phase}); + port {p["port_name"]}; + frame {p["frame_name"]} = newframe({p["port_name"]}, {p["frame_frequency"]}, {p["frame_phase"]}); {wf_line} }} -defcal {defcal_name} ${qubit} {{ - play({frame_name}, {waveform_name}); +defcal {p["defcal_name"]} ${p["qubit"]} {{ + play({p["frame_name"]}, {p["waveform_name"]}); }} """ From 4a0ba2e3af5d7d3dbb70e33c722da92a51cf2a0d Mon Sep 17 00:00:00 2001 From: Robert Jovanov Date: Tue, 27 Jan 2026 22:04:57 +0100 Subject: [PATCH 4/6] Added unit tests for Gaussian OpenPulse generator. The tests cover generation, overrides, and unrolling. --- tests/test_gaussian.py | 79 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 tests/test_gaussian.py diff --git a/tests/test_gaussian.py b/tests/test_gaussian.py new file mode 100644 index 0000000..0e2ad37 --- /dev/null +++ b/tests/test_gaussian.py @@ -0,0 +1,79 @@ +# 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 the Gaussian OpenPulse waveform generator.""" + +from __future__ import annotations + +import pyqasm +import pytest + +from qbraid_algorithms.openpulse import GaussianPulse, PulseParams, generate_program + + +def test_generate_program_dumps_contains_expected_sections() -> None: + """Verify generated OpenPulse program contains expected QASM sections.""" + pulse = GaussianPulse(amplitude=1.0 + 2.0j, duration="16ns", sigma="4ns") + params = PulseParams(frame_frequency=5.0e9, frame_phase=0.0, defcal_name="play_gaussian", qubit=0) + + module = generate_program(pulse, params=params) + qasm = pyqasm.dumps(module) + + assert 'defcalgrammar "openpulse";' in qasm + assert "cal {" in qasm + assert "port d0;" in qasm + assert "frame driveframe = newframe(d0, 5000000000.0, 0.0);" in qasm + assert "waveform wf = gaussian(1.0 + 2.0im, 16ns, 4ns);" in qasm + assert "defcal play_gaussian" in qasm + assert "play(driveframe, wf);" in qasm + + +def test_generate_program_kwargs_override_names() -> None: + """Verify kwargs correctly override parameters.""" + pulse = GaussianPulse(amplitude=0.5 + 0.0j, duration="8ns", sigma="2ns") + params = PulseParams(frame_frequency=5.0e9, frame_phase=0.0, defcal_name="play_gaussian", qubit=0) + + module = generate_program( + pulse, + params=params, + frame_name="driveframe2", + waveform_name="wf2", + port_name="d1", + ) + qasm = pyqasm.dumps(module) + + assert "port d1;" in qasm + assert "frame driveframe2 = newframe(d1, 5000000000.0, 0.0);" in qasm + assert "waveform wf2 = gaussian(0.5 + 0.0im, 8ns, 2ns);" in qasm + assert "play(driveframe2, wf2);" in qasm + + +def test_unroll_succeeds_when_pulse_dependencies_present() -> None: + """ + Unrolling requires the OpenPulse parser dependency. + + In CI this should be available via the `pulse` extra (pyqasm[pulse]). + """ + pulse = GaussianPulse(amplitude=1.0 + 2.0j, duration="16ns", sigma="4ns") + params = PulseParams(frame_frequency=5.0e9, frame_phase=0.0, defcal_name="play_gaussian", qubit=0) + + module = generate_program(pulse, params=params) + + try: + module.unroll() + except ModuleNotFoundError as exc: + pytest.skip(f"OpenPulse parser dependency not installed: {exc}") + + qasm = pyqasm.dumps(module) + assert "qubit[1] __PYQASM_QUBITS__;" in qasm From b9557c40362a45aaf12a59984a316abc3bbecf1a Mon Sep 17 00:00:00 2001 From: Robert Jovanov Date: Wed, 28 Jan 2026 22:07:20 +0100 Subject: [PATCH 5/6] Added an example notebook of how to use the generator on its own, and as a building block for larger experiments, such as Qubit Spectroscopy --- examples/openpulse/gaussian_pulse.ipynb | 316 ++++++++++++++++++++++++ 1 file changed, 316 insertions(+) create mode 100644 examples/openpulse/gaussian_pulse.ipynb diff --git a/examples/openpulse/gaussian_pulse.ipynb b/examples/openpulse/gaussian_pulse.ipynb new file mode 100644 index 0000000..4b5a51b --- /dev/null +++ b/examples/openpulse/gaussian_pulse.ipynb @@ -0,0 +1,316 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "8818805c", + "metadata": {}, + "source": [ + "# Gaussian OpenPulse waveform generator\n", + "\n", + "This notebook shows how to use `qbraid_algorithms.openpulse` to generate an **OpenQASM 3 + OpenPulse** program that defines a **Gaussian** waveform and a `defcal` that plays it on a target qubit." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "b14ce19a", + "metadata": {}, + "outputs": [], + "source": [ + "import pyqasm\n", + "\n", + "from qbraid_algorithms.openpulse import GaussianPulse, PulseParams, generate_program\n" + ] + }, + { + "cell_type": "markdown", + "id": "0eec9271", + "metadata": {}, + "source": [ + "## Generate a standalone Gaussian pulse QASM program" + ] + }, + { + "cell_type": "markdown", + "id": "08e0e059", + "metadata": {}, + "source": [ + "First we define the parameters of the pulse we want to play on a target qubit." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "30727e52", + "metadata": {}, + "outputs": [], + "source": [ + "pulse = GaussianPulse(amplitude=1.0 + 2.0j, duration=\"16ns\", sigma=\"4ns\")\n", + "params = PulseParams(\n", + " frame_frequency=5.0e9,\n", + " frame_phase=0.0,\n", + " port_name=\"d0\",\n", + " frame_name=\"driveframe\",\n", + " waveform_name=\"wf\",\n", + " defcal_name=\"play_gaussian\",\n", + " qubit=0,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "276571ed", + "metadata": {}, + "source": [ + "Then we pass the parameters to a `generate_program()` call to load a PyQASM module that defines and plays the pulse." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "d6f8d58a", + "metadata": {}, + "outputs": [], + "source": [ + "module = generate_program(pulse=pulse, params=params)" + ] + }, + { + "cell_type": "markdown", + "id": "7df56c25", + "metadata": {}, + "source": [ + "After loading the program as a PyQASM module, we can run all the standard PyQASM operations on it, such as displaying it:" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "16944412", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "OPENQASM 3.0;\n", + "defcalgrammar \"openpulse\";\n", + "cal {\n", + " port d0;\n", + " frame driveframe = newframe(d0, 5000000000.0, 0.0);\n", + " waveform wf = gaussian(1.0 + 2.0im, 16ns, 4ns);\n", + "}\n", + "defcal play_gaussian() $0 {\n", + " play(driveframe, wf);\n", + "}\n", + "\n" + ] + } + ], + "source": [ + "print(pyqasm.dumps(module))" + ] + }, + { + "cell_type": "markdown", + "id": "4535603f", + "metadata": {}, + "source": [ + "or unrolling it:" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "7f3c924f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "OPENQASM 3.0;\n", + "qubit[1] __PYQASM_QUBITS__;\n", + "defcalgrammar \"openpulse\";\n", + "cal {\n", + " port d0;\n", + " frame driveframe = newframe(d0, 5000000000.0, 0.0, 0ns);\n", + " waveform wf = gaussian(1.0 + 2.0im, 16.0ns, 4.0ns);\n", + "}\n", + "defcal play_gaussian() $0 {\n", + " play(driveframe, wf);\n", + "}\n", + "\n" + ] + } + ], + "source": [ + "unrolled = module.copy()\n", + "unrolled.unroll()\n", + "\n", + "print(pyqasm.dumps(unrolled))" + ] + }, + { + "cell_type": "markdown", + "id": "a76f9284", + "metadata": {}, + "source": [ + "## Example: Qubit Spectroscopy" + ] + }, + { + "cell_type": "markdown", + "id": "52d3f2e8", + "metadata": {}, + "source": [ + "Qubit spectroscopy is a common pulse-level experiment where the qubit is driven with the same pulse\n", + "while the drive frequency is swept. By observing the qubit’s response as a function of frequency,\n", + "its transition frequency can be identified.\n", + "\n", + "In this example, we use the Gaussian OpenPulse generator to define a reusable pulse waveform and a\n", + "`defcal` routine that plays it on a drive frame. This calibration is then called inside a frequency\n", + "sweep loop, showing how the generator can be used as a building block for larger pulse-level\n", + "experiments." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "3362ad37", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "OPENQASM 3.0;\n", + "defcalgrammar \"openpulse\";\n", + "cal {\n", + " port d0;\n", + " frame driveframe = newframe(d0, 5000000000.0, 0.0);\n", + " waveform wf = gaussian(1.0 + 2.0im, 16ns, 4ns);\n", + "}\n", + "defcal play_gaussian() $0 {\n", + " play(driveframe, wf);\n", + "}\n", + "\n", + "const float frequency_start = 4.5e9;\n", + "const float frequency_step = 1e6;\n", + "const int frequency_num_steps = 3;\n", + "\n", + "cal {\n", + " set_frequency(driveframe, frequency_start);\n", + "}\n", + "\n", + "for int i in [1:frequency_num_steps] {\n", + " cal {\n", + " shift_frequency(driveframe, frequency_step);\n", + " }\n", + " play_gaussian $0;\n", + " measure $0;\n", + "}\n", + "\n" + ] + } + ], + "source": [ + "import pyqasm\n", + "\n", + "base_module = generate_program(pulse=pulse, params=params)\n", + "base_qasm = pyqasm.dumps(base_module).rstrip()\n", + "\n", + "qubit_spectroscopy_qasm_code = base_qasm + f\"\"\"\n", + "\n", + "const float frequency_start = 4.5e9;\n", + "const float frequency_step = 1e6;\n", + "const int frequency_num_steps = 3;\n", + "\n", + "cal {{\n", + " set_frequency({params.frame_name}, frequency_start);\n", + "}}\n", + "\n", + "for int i in [1:frequency_num_steps] {{\n", + " cal {{\n", + " shift_frequency({params.frame_name}, frequency_step);\n", + " }}\n", + " {params.defcal_name} ${params.qubit};\n", + " measure ${params.qubit};\n", + "}}\n", + "\"\"\"\n", + "\n", + "print(qubit_spectroscopy_qasm_code)" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "deb69867", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "OPENQASM 3.0;\n", + "qubit[1] __PYQASM_QUBITS__;\n", + "defcalgrammar \"openpulse\";\n", + "cal {\n", + " port d0;\n", + " frame driveframe = newframe(d0, 5000000000.0, 0.0, 0ns);\n", + " waveform wf = gaussian(1.0 + 2.0im, 16.0ns, 4.0ns);\n", + "}\n", + "defcal play_gaussian() $0 {\n", + " play(driveframe, wf);\n", + "}\n", + "cal {\n", + " set_frequency(driveframe, 4500000000.0);\n", + "}\n", + "cal {\n", + " shift_frequency(driveframe, 4501000000.0);\n", + "}\n", + "play_gaussian __PYQASM_QUBITS__[0];\n", + "measure __PYQASM_QUBITS__[0];\n", + "cal {\n", + " shift_frequency(driveframe, 4502000000.0);\n", + "}\n", + "play_gaussian __PYQASM_QUBITS__[0];\n", + "measure __PYQASM_QUBITS__[0];\n", + "cal {\n", + " shift_frequency(driveframe, 4503000000.0);\n", + "}\n", + "play_gaussian __PYQASM_QUBITS__[0];\n", + "measure __PYQASM_QUBITS__[0];\n", + "\n" + ] + } + ], + "source": [ + "qubit_spectroscopy_module = pyqasm.loads(qubit_spectroscopy_qasm_code)\n", + "qubit_spectroscopy_module.unroll()\n", + "print(pyqasm.dumps(qubit_spectroscopy_module))" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "qbraid-algos", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 2423540e373a9d3f88eb0a05d327e6c571843630 Mon Sep 17 00:00:00 2001 From: Robert Jovanov Date: Thu, 29 Jan 2026 14:12:07 +0100 Subject: [PATCH 6/6] Fixed tests to address both structure and content of the generated QASM --- tests/test_gaussian.py | 65 ++++++++++++++++++++++++++++-------------- 1 file changed, 44 insertions(+), 21 deletions(-) diff --git a/tests/test_gaussian.py b/tests/test_gaussian.py index 0e2ad37..ef7b1f5 100644 --- a/tests/test_gaussian.py +++ b/tests/test_gaussian.py @@ -16,31 +16,42 @@ from __future__ import annotations +import textwrap + import pyqasm import pytest from qbraid_algorithms.openpulse import GaussianPulse, PulseParams, generate_program -def test_generate_program_dumps_contains_expected_sections() -> None: - """Verify generated OpenPulse program contains expected QASM sections.""" +def test_generate_program_emits_expected_qasm() -> None: + """Generated OpenPulse QASM should match the expected structure and order.""" pulse = GaussianPulse(amplitude=1.0 + 2.0j, duration="16ns", sigma="4ns") params = PulseParams(frame_frequency=5.0e9, frame_phase=0.0, defcal_name="play_gaussian", qubit=0) module = generate_program(pulse, params=params) - qasm = pyqasm.dumps(module) - - assert 'defcalgrammar "openpulse";' in qasm - assert "cal {" in qasm - assert "port d0;" in qasm - assert "frame driveframe = newframe(d0, 5000000000.0, 0.0);" in qasm - assert "waveform wf = gaussian(1.0 + 2.0im, 16ns, 4ns);" in qasm - assert "defcal play_gaussian" in qasm - assert "play(driveframe, wf);" in qasm - - -def test_generate_program_kwargs_override_names() -> None: - """Verify kwargs correctly override parameters.""" + actual = pyqasm.dumps(module).strip() + + expected = textwrap.dedent( + """\ + OPENQASM 3.0; + defcalgrammar "openpulse"; + cal { + port d0; + frame driveframe = newframe(d0, 5000000000.0, 0.0); + waveform wf = gaussian(1.0 + 2.0im, 16ns, 4ns); + } + defcal play_gaussian() $0 { + play(driveframe, wf); + } + """ + ).strip() + + assert actual == expected + + +def test_generate_program_kwargs_override_names_emits_expected_qasm() -> None: + """kwargs overrides should be reflected in the emitted QASM (structure + order).""" pulse = GaussianPulse(amplitude=0.5 + 0.0j, duration="8ns", sigma="2ns") params = PulseParams(frame_frequency=5.0e9, frame_phase=0.0, defcal_name="play_gaussian", qubit=0) @@ -51,12 +62,24 @@ def test_generate_program_kwargs_override_names() -> None: waveform_name="wf2", port_name="d1", ) - qasm = pyqasm.dumps(module) - - assert "port d1;" in qasm - assert "frame driveframe2 = newframe(d1, 5000000000.0, 0.0);" in qasm - assert "waveform wf2 = gaussian(0.5 + 0.0im, 8ns, 2ns);" in qasm - assert "play(driveframe2, wf2);" in qasm + actual = pyqasm.dumps(module).strip() + + expected = textwrap.dedent( + """\ + OPENQASM 3.0; + defcalgrammar "openpulse"; + cal { + port d1; + frame driveframe2 = newframe(d1, 5000000000.0, 0.0); + waveform wf2 = gaussian(0.5 + 0.0im, 8ns, 2ns); + } + defcal play_gaussian() $0 { + play(driveframe2, wf2); + } + """ + ).strip() + + assert actual == expected def test_unroll_succeeds_when_pulse_dependencies_present() -> None: