Skip to content
Merged
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
7 changes: 3 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,9 @@ jobs:

- name: Run Setup Script
run: |
chmod +x scripts/setup/setup_env.sh
./scripts/setup/setup_env.sh
python3 -m scripts.setup

- name: Linting (Ruff & Pyright) for Python only (C++ later)
- name: Formatting
run: |
# Check for linting errors
uv run ruff check .
Expand All @@ -25,4 +24,4 @@ jobs:
uv run pyright

- name: Run CI Pipeline
run: uv run scripts/run.py --num-tests 10
run: uv run -m scripts.run --num-tests 10
5 changes: 2 additions & 3 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,14 @@ jobs:

- name: Run Setup Script
run: |
chmod +x scripts/setup/setup_env.sh
./scripts/setup/setup_env.sh
python3 -m scripts.setup

- name: Run Nightly Pipeline
run: |
# Use workflow inputs if provided, otherwise defaults
GRAMMARS="${{ github.event.inputs.grammars || 'pytket qiskit' }}"

uv run scripts/run.py \
uv run -m scripts.run \
--nightly \
--num-tests 1200 \
--grammars $GRAMMARS
Expand Down
17 changes: 10 additions & 7 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
__pycache__

.vscode
build
outputs
local_saved_circuits
.venv
__pycache__
.DS_Store
.env
.devcontainer
# libs/
nightly_results
.coverage

build
outputs
external
nightly_results

*.egg-info
coverage.png

local-override.toml
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ set(CMAKE_CXX_FLAGS_DEBUG "-g -fsanitize=address -O3 -DDEBUG -Wall -Wextra -Wswi
set(CMAKE_CXX_FLAGS_RELEASE "-O3 -DNDEBUG")


file(GLOB_RECURSE SRCS "src/*.cpp" "libs/linenoise/linenoise.c")
file(GLOB_RECURSE SRCS CONFIGURE_DEPENDS "src/*.cpp" "external/linenoise/linenoise.c")

add_executable(qf ${SRCS})

Expand Down
5 changes: 2 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
FROM ubuntu:24.04

# Install needed deps
RUN apt-get update && apt-get install -y sudo curl vim && rm -rf /var/lib/apt/lists/*
RUN apt-get update && apt-get install -y sudo curl vim python3 python3-venv && rm -rf /var/lib/apt/lists/*

# Prevent interactive prompts during installation
ENV DEBIAN_FRONTEND=noninteractive
Expand All @@ -14,8 +14,7 @@ WORKDIR /app
COPY . .

# Run the existing setup script (installs all dependencies and builds)
RUN chmod +x scripts/setup/setup_env.sh && \
./scripts/setup/setup_env.sh
RUN python3 -m scripts.setup

# Add Cargo and local bin to PATH
ENV PATH="/root/.cargo/bin:/root/.local/bin:${PATH}"
Expand Down
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,19 @@ qf++ is a grammar-driven fuzzing framework for quantum compilers. It generates s
| [Writing a Grammar](docs/writing-grammars.md) | Step-by-step guide to adding a new quantum framework |
| [MAP-Elites](docs/map-elites.md) | How diversity-driven generation works and where to take it next |
| [Differential Testing](docs/diff-testing.md) | How circuits are validated and bugs are classified |
| [Dev](docs/dev.md) | Notes for dev environment |

## Quick start

```sh
# 1. Install everything
./scripts/setup/setup_env.sh
# 1. Setup
python3 -m scripts.setup

# 2. Run CI pipeline (10 circuits per grammar)
uv run scripts/run.py --num-tests 10
uv run -m scripts.run --num-tests 10

# 3. Run nightly (1200 circuits, saves interesting ones)
uv run scripts/run.py --nightly --num-tests 1200 --grammars pytket qiskit
uv run -m scripts.run --nightly --num-tests 1200 --grammars pytket qiskit

# 4. Use the interactive fuzzer REPL directly
./build/qf
Expand Down Expand Up @@ -59,7 +60,7 @@ uv run scripts/run.py --nightly --num-tests 1200 --grammars pytket qiskit

## Acknowledgements

- [Linenoise](https://github.com/antirez/linenoise) library for nicities in REPL loop like command history and tab completion. Downloaded code [here](libs/linenoise/).
- [Linenoise](https://github.com/antirez/linenoise) library for nicities in REPL loop like command history and tab completion.

## Note
- *Only GCC/clang compilers due to some use of GCC pragmas*
Expand Down
124 changes: 124 additions & 0 deletions diff_testing/cudaq_qf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import traceback
from typing import Any, Dict

import cudaq

from .lib import Base


def _apply_opt_level_1(kernel) -> None:
"""
Aggressive early inlining + dead-code elimination.
Corresponds roughly to -O1 in nvq++ (fast, cheap passes only).
"""
cudaq.passes.aggressive_early_inlining(kernel) # type: ignore


def _apply_opt_level_2(kernel) -> None:
"""
Level 1 passes + unitary synthesis for small qubit blocks.
Corresponds roughly to -O2 in nvq++.
"""
cudaq.passes.aggressive_early_inlining(kernel) # type: ignore
cudaq.passes.unitary_synthesis(kernel) # type: ignore


def _apply_opt_level_3(kernel) -> None:
"""
Full optimization: level 2 + decomposition into a native gate set.
Corresponds roughly to -O3 in nvq++.
"""
cudaq.passes.aggressive_early_inlining(kernel) # type: ignore
cudaq.passes.unitary_synthesis(kernel) # type: ignore
cudaq.passes.decompose_to_basis(kernel) # type: ignore


_OPT_PASSES = {
0: None,
1: _apply_opt_level_1,
2: _apply_opt_level_2,
3: _apply_opt_level_3,
}


class cudaqTesting(Base):
def __init__(self) -> None:
super().__init__("cudaq")

def get_counts(self, kernel, opt_level: int, circuit_num: int) -> Dict[Any, int]:
"""
Compile and sample `kernel` at the requested optimisation level.

CUDA-Q does not expose optimisation levels as a single integer the way
Qiskit/pytket do. Instead we build four increasingly aggressive pass
pipelines that mirror what nvq++ applies at -O0 through -O3:

0 — no passes applied (reference baseline)
1 — aggressive early inlining + dead-code elimination
2 — level-1 passes + unitary synthesis (small blocks)
3 — level-2 passes + decomposition into native gate set

If any pass pipeline fails (e.g. the installed CUDA-Q build does not
ship that pass), we fall back to the un-optimised sample so that the
rest of the test still runs and we get a meaningful KS value.
"""
try:
pass_fn = _OPT_PASSES.get(opt_level)

if pass_fn is not None:
try:
pass_fn(kernel)
except (AttributeError, RuntimeError) as e:
# cudaq.passes may not be available on all builds — degrade
# gracefully so at least the unoptimised path still works.
print(
f"[cudaqTesting] opt_level={opt_level} pass failed "
f"({type(e).__name__}: {e}); falling back to no-pass sampling."
)

result = cudaq.sample(kernel, shots_count=self.num_shots)

raw_counts: Dict[str, int] = dict(result)

n_bits = max((len(k) for k in raw_counts), default=1)

if self.plot:
self.plot_histogram(
res=self.preprocess_counts(raw_counts, n_bits),
title=f"cudaq_opt{opt_level}",
circuit_number=circuit_num,
)

return self.preprocess_counts(raw_counts, n_bits)

except Exception:
print(f"[cudaqTesting] Exception during get_counts (opt_level={opt_level}):")
print(traceback.format_exc())
return {}

def opt_ks_test(self, kernel, circuit_number: int) -> None:
"""
Run the standard KS-test differential-testing loop.

Overrides Base.opt_ks_test so we can call cudaq.sample correctly
(the kernel is a callable, not a circuit object). The logic is
otherwise identical to the base implementation.
"""
counts0 = self.get_counts(kernel, opt_level=0, circuit_num=circuit_number)

if not counts0:
print("[cudaqTesting] Baseline (opt_level=0) returned empty counts; skipping.")
return

for i in range(3):
opt_level = i + 1
counts_i = self.get_counts(kernel, opt_level=opt_level, circuit_num=circuit_number)

if not counts_i:
print(
f"[cudaqTesting] opt_level={opt_level} returned empty counts; skipping KS test."
)
continue

ks_value = self.ks_test(counts0, counts_i)
print(f"Optimisation level {opt_level} ks-test p-value: {ks_value}")
60 changes: 0 additions & 60 deletions diff_testing/wisq.py

This file was deleted.

4 changes: 2 additions & 2 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
**`Lexer`** (`lex.h`) tokenises a `.qf` file using a single regex. Every token is classified into one of three groups:

- **Rule kinds** (`RULE_KINDS_TOP` to `RULE_KINDS_BOTTOM`): terminals that the AST builder knows how to specialise — `GATE_NAME`, `QUBIT_DEF`, `CIRCUIT`, `COMPOUND_STMT`, etc.
- **Meta functions** (`META_FUNC_TOP` to `META_FUNC_BOTTOM`): dynamic tokens resolved at AST build time — `CIRCUIT_NAME`, `GATE_QUBITS`, `NAME`, `SIZE`, `INDEX`, `FLOAT`, `CHILD_INDENT`, etc.
- **Meta functions** (`META_FUNC_TOP` to `META_FUNC_BOTTOM`): dynamic tokens resolved at AST build time — `GET_CIRCUIT_NAME`, `GATE_QUBITS`, `GET_NAME`, `GET_SIZE`, `GET_INDEX`, `MAKE_FLOAT`, `CHILD_INDENT`, etc.
- **Grammar syntax**: `=`, `|`, `;`, `(`, `)`, `[`, `]`, `{`, `}`, `*`, `+`, `?`, `<`, `>`, `::`, `+=`.

**`Grammar`** (`grammar.h/cpp`) builds a graph of `Rule` objects from tokens. Each rule has one or more `Branch`es; each branch is a sequence of `Term`s. Terms are either syntax literals or pointers to other rules (allowing recursion). A `Term_constraint` on a term controls how many times it is repeated when the AST builder expands that term.
Expand All @@ -53,7 +53,7 @@ All semantic decisions — which qubit to pick, what gate to use, whether subrou
- Nested depth (decremented each time a `CHILD_INDENT` node is created)
- The `Control` struct (grammar-level config: `MAX_REG_SIZE`, `NESTED_MAX_DEPTH`, expected rules)

**Printing** — `Node::print_program()` traverses the tree and writes text. `Print_mode` on a node controls indentation: `DEFAULT` (recurse into children), `CHILD_INDENT` (add one tab level for all children), `SELF_INDENT` (add the current tab level prefix before this node), `INDENT_LEVEL` (emit the raw integer depth for things like Qiskit's `else_N` variable names).
**Printing** — `Node::print_program()` traverses the tree and writes text. `Print_mode` on a node controls indentation: `DEFAULT` (recurse into children), `CHILD_INDENT` (add one tab level for all children), `SELF_INDENT` (add the current tab level prefix before this node), `GET_INDENT_LEVEL` (emit the raw integer depth for things like Qiskit's `else_N` variable names).

### Generator layer (`include/generator/`, `src/generator/`)

Expand Down
18 changes: 12 additions & 6 deletions docs/dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,16 @@
}
```

Run `pre-commit run --all-files` to run checks locally
- `uv run ruff format .` to format all files
- `uv run ruff check --fix .` to check formatting and fix issues

## Coverage

Pytket is built from source by the setup script. To override the prebuilt version, create a `local-override.toml` file with
```
[tool.uv.sources]
pytket = { path = "external/tket/pytket", editable = true }
```

then set the environment variable `export UV_OVERRIDE="local-override.toml"`. All `uv` commands now run in the modified env.

### Seeds to explore
1. Wildly different ks values for specific optimisation levels
- 3266767068: qiskit
2. Runtime error
- 1737423893: pytket
2 changes: 1 addition & 1 deletion docs/diff-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,6 @@ PennyLane circuits are QNodes (decorated functions), not circuit objects. The ge
2. Add a rule to your grammar's `testing_method`:
```qf
testing_method = opt_ks_test | my_custom_test;
my_custom_test = "mt.my_custom_test(" CIRCUIT_NAME COMMA circuit_id RPAREN NEWLINE;
my_custom_test = "mt.my_custom_test(" GET_CIRCUIT_NAME COMMA circuit_id RPAREN NEWLINE;
```
3. `run.py`'s regex already parses any line matching `ks-test p-value:` or `Dot product`.
Loading
Loading