Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,11 @@ venv/
ENV/
env.bak/
venv.bak/
.asv/

# Downloaded benchmark files (cached by tests/benchmarks/qasm/benchmark_downloader.py)
tests/benchmarks/qasm/*.qasm
!tests/benchmarks/qasm/neutral_atom_gate.qasm

# Spyder project settings
.spyderproject
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Types of changes:
## Unreleased

### Added
- Added `asv` benchmarking support in `pyqasm`. ([#258](https://github.com/qBraid/pyqasm/pull/258))

### Improved / Modified

Expand Down
33 changes: 33 additions & 0 deletions asv.conf.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"version": 1,
"project": "pyqasm",
"project_url": "https://sdk.qbraid.com/pyqasm/",
"repo": ".",
"install_command": [
"in-dir={env_dir} python -m pip install {wheel_file}[visualization,pulse]"
],
"uninstall_command": [
"return-code=any python -m pip uninstall -y pyqasm"
],
"build_command": [
"python -m pip install -U build",
"python -m build --outdir {build_cache_dir} --wheel {build_dir}"
],
"branches": [
"main"
],
"dvcs": "git",
"environment_type": "virtualenv",
"show_commit_url": "https://github.com/qBraid/pyqasm/commit/",
"pythons": [
"3.10",
"3.11",
"3.12",
"3.13",
"3.14"
],
"benchmark_dir": "tests/benchmarks",
"env_dir": ".asv/env",
Comment thread
vinayswamik marked this conversation as resolved.
"results_dir": ".asv/results",
"html_dir": ".asv/html"
}
13 changes: 13 additions & 0 deletions tests/benchmarks/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# 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.
28 changes: 28 additions & 0 deletions tests/benchmarks/import.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# 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.

"""
This module is used to test the import time of pyqasm.
"""

from subprocess import check_call
from sys import executable


class PyqasmImport:
"""Test the import time of pyqasm."""

def time_pyqasm_import(self):
# check_call, not call: a failed import would otherwise be timed as a fast success
check_call((executable, "-c", "import pyqasm"))
33 changes: 33 additions & 0 deletions tests/benchmarks/openpulse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# 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.

"""
This module is used to test the openpulse of pyqasm.
"""

from pyqasm import load

from .qasm.benchmark_downloader import get_benchmark_file


class Openpulse:
"""Test the pyqasm openpulse functionality."""

def setup(self):
# Get benchmark file, downloading if necessary
# pylint: disable-next=attribute-defined-outside-init
self.qasm_file = get_benchmark_file("neutral_atom_gate.qasm")

def time_openpulse(self):
_ = load(self.qasm_file).unroll()
90 changes: 90 additions & 0 deletions tests/benchmarks/pyqasm_functions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# 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=attribute-defined-outside-init

"""
This module is used to test the pyqasm functions.
"""

import os
from pathlib import Path

from pyqasm import dump, dumps, load, printer

from .qasm.benchmark_downloader import get_benchmark_file


class PyqasmFunctions:
"""Test the pyqasm functions."""

# Define parameters for asv
params = [["small (224 lines)", "mid (2335 lines)", "large (17460 lines)"]]
param_names = ["qasm_file"]
timeout = 600

def setup(self, file_size):
# Extract the original file size name from the parameter value
if "(224 lines)" in file_size:
file_size_key = "small"
elif "(2335 lines)" in file_size:
file_size_key = "mid"
elif "(17460 lines)" in file_size:
file_size_key = "large"
else:
file_size_key = file_size

# Define files for each size category
self.files = {
"small": "vqe_uccsd_n4.qasm", # 224 lines
"mid": "dnn_n16.qasm", # 2335 lines
"large": "qv_N029_12345.qasm", # 17460 lines
}

# Get benchmark file for the specified size
self.qasm_file = get_benchmark_file(self.files[file_size_key])
self.pyqasm_obj = load(self.qasm_file)

# mpl_draw unrolls in place, so give it its own module, unrolled up front:
# sharing pyqasm_obj would make only the first draw pay for the unroll
self.draw_obj = load(self.qasm_file)
self.draw_obj.unroll()

# Create output file path for dump operations
input_path = Path(self.qasm_file)
self.output_file = str(input_path.parent / f"{file_size_key}_unrolled.qasm")

def teardown(self, _):
# Clean up the output file if it was created
if hasattr(self, "output_file") and os.path.exists(self.output_file):
try:
os.remove(self.output_file)
except OSError:
pass

def time_load(self, _):
"""Load QASM file of specified size."""
_ = load(self.qasm_file)

def time_dumps(self, _):
"""Serialize QASM object of specified size to string."""
_ = dumps(self.pyqasm_obj)

def time_dump(self, _):
"""Dump QASM object of specified size to file."""
dump(self.pyqasm_obj, self.output_file)

def time_draw(self, _):
"""Draw QASM object of specified size."""
_ = printer.mpl_draw(self.draw_obj, idle_wires=True, external_draw=False)
13 changes: 13 additions & 0 deletions tests/benchmarks/qasm/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# 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.
80 changes: 80 additions & 0 deletions tests/benchmarks/qasm/benchmark_downloader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# 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.

"""
Benchmark file downloader utility.

This module handles downloading benchmark QASM files from the Qiskit repository
and caching them locally to avoid storing large files in version control.
"""

import json
import urllib.request
from pathlib import Path
from typing import Dict, Optional

_DOWNLOAD_TIMEOUT = 30 # seconds


class BenchmarkDownloader:
"""Handles downloading and caching of benchmark files."""

def __init__(self, cache_dir: Optional[str] = None):
"""Initialize the downloader."""
self.cache_dir = Path(cache_dir) if cache_dir else Path(__file__).parent
self.cache_dir.mkdir(exist_ok=True)

# Load metadata
with open(self.cache_dir / "benchmark_metadata.json", "r", encoding="utf-8") as f:
self.metadata = json.load(f)

def get_file_path(self, filename: str) -> Path:
"""Get the path for a benchmark file, fetching from remote repository if needed."""
# Check local_files first
if filename in self.metadata["local_files"]:
file_path = self.cache_dir / filename
if not file_path.exists():
raise FileNotFoundError(f"Local file {filename} not found in {self.cache_dir}")
return file_path

# Check benchmark_files
if filename in self.metadata["benchmark_files"]:
file_info = self.metadata["benchmark_files"][filename]
# Fetch remote files
return self._fetch_remote_file(filename, file_info)

raise ValueError(f"Unknown benchmark file: {filename}")

def _fetch_remote_file(self, filename: str, file_info: Dict) -> Path:
"""Fetch a remote benchmark file into the cache and return its path."""
cached_path = self.cache_dir / filename
if cached_path.exists():
return cached_path

url = file_info["url"]
try:
# without a timeout a stalled connection blocks the whole benchmark run
with urllib.request.urlopen(url, timeout=_DOWNLOAD_TIMEOUT) as response:
content = response.read()
except Exception as e:
raise RuntimeError(f"Failed to fetch {filename} from {url}: {e}") from e
Comment on lines +66 to +71

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files:"
git ls-files | rg '(^|/)benchmark_downloader\.py$|qasm' || true

echo
echo "Target file outline:"
ast-grep outline tests/benchmarks/qasm/benchmark_downloader.py || true

echo
echo "Target lines 1-140:"
cat -n tests/benchmarks/qasm/benchmark_downloader.py | sed -n '1,140p'

echo
echo "Imports/usages of benchmark_downloader:"
rg -n "benchmark_downloader|download.*qasm|fetch .*qasm|urlopen|URLError|timeout" tests/benchmarks || true

echo
echo "Python urllib.timeout behavior probe:"
python3 - <<'PY'
import inspect, urllib.request
sig = inspect.signature(urllib.request.urlopen)
print("urlopen signature:", sig)
PY

Repository: qBraid/pyqasm

Length of output: 9437


Bound the download and preserve connection failures.

urllib.request.urlopen(url) uses no timeout, so a stalled download can block ASV indefinitely. Add an explicit timeout. Catch urllib.error.URLError and timeout-related errors explicitly, then raise a connection-specific exception; let unrelated programming errors propagate.

🧰 Tools
🪛 ast-grep (0.45.0)

[warning] 64-64: Request-controlled URL passed to urlopen; validate against an allowlist to prevent SSRF.
Context: urllib.request.urlopen(url)
Note: [CWE-918] Server-Side Request Forgery (SSRF).

(urlopen-unsanitized-data)

🪛 Ruff (0.16.1)

[error] 65-65: Audit URL open for permitted schemes. Allowing use of file: or custom schemes is often unexpected.

(S310)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/benchmarks/qasm/benchmark_downloader.py` around lines 64 - 68, Update
the download logic around urllib.request.urlopen in the benchmark downloader to
pass an explicit timeout and prevent stalled requests from blocking
indefinitely. Catch urllib.error.URLError and timeout-related exceptions
explicitly, convert them to the existing connection-specific exception with the
filename and URL context, and allow unrelated programming errors to propagate.

Source: Coding guidelines


cached_path.write_bytes(content)
return cached_path


def get_benchmark_file(filename: str) -> str:
"""Get the path to a benchmark file, downloading if necessary."""
downloader = BenchmarkDownloader()
return str(downloader.get_file_path(filename))
37 changes: 37 additions & 0 deletions tests/benchmarks/qasm/benchmark_metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"benchmark_files": {
Comment thread
vinayswamik marked this conversation as resolved.
"vqe_uccsd_n4.qasm": {
"source": "Qiskit-benchpress",
"url": "https://raw.githubusercontent.com/Qiskit/benchpress/573c9ce7a1ffebd4ca6d1b0971eba7aca2c9c9a4/benchpress/qasm/qasmbench-small/vqe_uccsd_n4/vqe_uccsd_n4.qasm",
"description": "VQE UCCSD benchmark with 4 qubits (small)",
"size_bytes": 15000
},
"dnn_n16.qasm": {
"source": "Qiskit-benchpress",
"url": "https://raw.githubusercontent.com/Qiskit/benchpress/573c9ce7a1ffebd4ca6d1b0971eba7aca2c9c9a4/benchpress/qasm/qasmbench-medium/dnn_n16/dnn_n16.qasm",
"description": "Deep Neural Network benchmark with 16 qubits (medium)",
"size_bytes": 120000
},
"qv_N029_12345.qasm": {
"source": "Qiskit-benchpress",
"url": "https://raw.githubusercontent.com/Qiskit/benchpress/573c9ce7a1ffebd4ca6d1b0971eba7aca2c9c9a4/benchpress/qasm/qv/qv_N029_12345.qasm",
"description": "Quantum Volume benchmark with 29 qubits (large, 17,460 lines)",
"size_bytes": 470000
}
},
"repository_info": {
"name": "Qiskit-benchpress",
"url": "https://github.com/Qiskit/benchpress",
"description": "QASM benchmark files from Qiskit benchpress repository",
"license": "Apache-2.0",
"commit_hash": "573c9ce7a1ffebd4ca6d1b0971eba7aca2c9c9a4"
},
"local_files": {
"neutral_atom_gate.qasm": {
"source": "local",
"description": "Neutral atom gate benchmark (local file)",
"size_bytes": 3584,
"local_only": true
}
}
}
Loading
Loading