Skip to content
Draft
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
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ tmp/
# pytest results
junit/

# Generated doc tests (from scripts/docs/generate_doc_tests.py)
python/quantum-pecos/tests/docs/generated/

# Generated WASM binaries (compile from .wat source)
*.wasm

# Cython
*.pyc
*.pyd
Expand Down
7 changes: 7 additions & 0 deletions .typos.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,10 @@ arange = "arange"
nd = "nd"
# delocate is a macOS wheel repair tool (delocate-wheel command)
delocate = "delocate"
# Gate name breakdown notation uses partial words like C(ontrolled), R(otation)
ontrolled = "ontrolled"
otation = "otation"
repare = "repare"
easure = "easure"
egative = "egative"
agger = "agger"
19 changes: 14 additions & 5 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -138,13 +138,22 @@ docs-build:
docs port="8000":
cargo run -p pecos --features cli -- docs --port {{port}}

# Test all code examples in documentation
# Test all code examples in documentation (generates pytest files and runs them)
docs-test:
uv run python scripts/docs/test_code_examples.py
uv run python scripts/docs/generate_doc_tests.py
uv run pytest python/quantum-pecos/tests/docs/generated -v

# Generate doc tests without running them
docs-test-generate:
uv run python scripts/docs/generate_doc_tests.py

# Test only working code examples in documentation
docs-test-working:
uv run python scripts/docs/test_working_examples.py
# Run doc tests with pytest options (e.g., just docs-test-run "-k bell_state")
docs-test-run *args:
uv run pytest python/quantum-pecos/tests/docs/generated {{args}}

# Legacy: test code examples with old script
docs-test-legacy:
uv run python scripts/docs/test_code_examples.py

# =============================================================================
# Linting / Formatting
Expand Down
400 changes: 361 additions & 39 deletions crates/pecos-hugr/src/engine.rs

Large diffs are not rendered by default.

45 changes: 43 additions & 2 deletions crates/pecos-qis-ffi/src/ffi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@

use crate::{Operation, QuantumOp, with_interface};
use log::debug;
use std::cell::Cell;

// Thread-local counter to prevent infinite loops in collection mode.
// After MAX_COLLECTION_READS, `___read_future_bool` returns true to break out of
// loops like "repeat_until_one" (while not result: ... result = measure(q)).
thread_local! {
static COLLECTION_MODE_READ_COUNT: Cell<u32> = const { Cell::new(0) };
}

/// Maximum number of measurement reads in collection mode before returning true.
/// This prevents infinite loops when collecting operations for programs with
/// "repeat until success" patterns.
const MAX_COLLECTION_READS: u32 = 100;

/// Helper to convert i64 to usize
#[inline]
Expand Down Expand Up @@ -682,8 +695,36 @@ pub unsafe extern "C" fn ___read_future_bool(future_id: i64) -> bool {
log::debug!("___read_future_bool: timeout waiting for result");
}

// Default: return false (for first pass or if no dynamic mode)
false
// Collection mode (non-dynamic): track read count to prevent infinite loops.
// For programs with "repeat until success" loops like:
// while not result:
// q = qubit()
// result = measure(q)
// Each iteration creates a new result_id, so we track total reads.
// After MAX_COLLECTION_READS, we return true to break the loop.
let read_count = COLLECTION_MODE_READ_COUNT.with(|c| {
let count = c.get() + 1;
c.set(count);
count
});

if read_count >= MAX_COLLECTION_READS {
log::debug!(
"___read_future_bool: collection mode read count ({read_count}) >= threshold, returning true to break loop"
);
true
} else {
// Default: return false (allows first iterations of loops to proceed)
false
}
}

/// Reset the collection mode read counter.
///
/// This should be called at the start of each new execution to reset the loop
/// termination counter used in `___read_future_bool`.
pub fn reset_collection_read_count() {
COLLECTION_MODE_READ_COUNT.with(|c| c.set(0));
}

/// Increment the reference count of a future (Guppy/HUGR-LLVM style)
Expand Down
2 changes: 2 additions & 0 deletions crates/pecos-qis-ffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,8 @@ where
/// Reset the thread-local operation collector
pub fn reset_interface() {
with_interface(OperationCollector::reset);
// Also reset the collection mode read counter for loop termination
ffi::reset_collection_read_count();
}

/// Get a clone of the thread-local operation collector
Expand Down
40 changes: 24 additions & 16 deletions crates/pecos-qis/src/ccengine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -768,25 +768,14 @@ impl ClassicalEngine for QisEngine {
// Convert stored measurement results to PECOS shot format
let mut shot = Shot::default();

// Add measurements from stored results (numeric IDs)
for (result_id, value) in &self.measurement_results {
shot.data.insert(
format!("measurement_{result_id}"),
Data::U32(u32::from(*value)),
);
debug!(
"QisEngine: Added to shot: measurement_{} = {}",
result_id,
i32::from(*value)
);
}

// Add named results from print_bool/print_bool_arr calls
// First, try to get named results from print_bool/print_bool_arr calls
let mut has_named_results = false;
if let Some(state) = &self.dynamic_state
&& let Some(handle) = &state.sync_handle
{
match handle.get_named_results() {
Ok(named_results) => {
has_named_results = !named_results.is_empty();
for (name, values) in named_results {
// Convert Vec<bool> to Data
// For single values, store as U32; for arrays, store as Vec<U32>
Expand All @@ -807,10 +796,29 @@ impl ClassicalEngine for QisEngine {
}
}

// Only add raw measurements if there are no named results.
// This handles circuits with variable loop iterations where each shot
// may produce a different number of raw measurements, but the named
// results (from result() calls) are consistent.
if !has_named_results {
for (result_id, value) in &self.measurement_results {
shot.data.insert(
format!("measurement_{result_id}"),
Data::U32(u32::from(*value)),
);
debug!(
"QisEngine: Added to shot: measurement_{} = {}",
result_id,
i32::from(*value)
);
}
}

debug!("QisEngine: Final shot data: {:?}", shot.data);
debug!(
"Returning shot with {} measurement results",
self.measurement_results.len()
"Returning shot with {} measurement results (has_named_results={})",
self.measurement_results.len(),
has_named_results
);
Ok(shot)
}
Expand Down
38 changes: 31 additions & 7 deletions crates/pecos-quantum/src/hugr_convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,10 @@ struct QuantumOp {
/// Check if a gate type is a rotation gate that takes angle parameters.
#[must_use]
pub fn is_rotation_gate(gate_type: GateType) -> bool {
matches!(gate_type, GateType::RX | GateType::RY | GateType::RZ)
matches!(
gate_type,
GateType::RX | GateType::RY | GateType::RZ | GateType::CRZ
)
}

/// Try to extract a constant numeric value from a HUGR Const node.
Expand Down Expand Up @@ -437,6 +440,16 @@ fn trace_back_for_const(hugr: &Hugr, node: Node, depth: usize) -> Option<(f64, b
return trace_back_for_const(hugr, src_node, depth + 1);
}
}

// Handle float negation (fneg) - used for negative rotation angles
if op_name == "fneg" {
let input_port = IncomingPort::from(0);
if let Some((src_node, _)) = hugr.single_linked_output(node, input_port)
&& let Some((val, is_half_turns)) = trace_back_for_const(hugr, src_node, depth + 1)
{
return Some((-val, is_half_turns));
}
}
}

// For UnpackTuple, trace through
Expand All @@ -456,7 +469,8 @@ fn trace_back_for_const(hugr: &Hugr, node: Node, depth: usize) -> Option<(f64, b
// 2. Numeric argument values
let mut is_division = false;
let mut is_multiplication = false;
let mut numeric_values: Vec<(usize, f64)> = Vec::new();
let mut is_negation = false;
let mut numeric_values: Vec<(usize, f64, bool)> = Vec::new();

// Get the number of input ports for this node
let num_inputs = hugr.num_inputs(node);
Expand All @@ -476,19 +490,29 @@ fn trace_back_for_const(hugr: &Hugr, node: Node, depth: usize) -> Option<(f64, b
if func_name.contains("__mul__") || func_name.contains("__rmul__") {
is_multiplication = true;
}
if func_name.contains("__neg__") {
is_negation = true;
}
}

// Try to get a numeric value from this input
if let Some((val, _)) = trace_back_for_const(hugr, src_node, depth + 1) {
numeric_values.push((port_idx, val));
if let Some((val, is_half_turns)) = trace_back_for_const(hugr, src_node, depth + 1)
{
numeric_values.push((port_idx, val, is_half_turns));
}
}
}

// If this is a negation call and we have a numeric value, negate it
if is_negation && !numeric_values.is_empty() {
let (_, val, is_half_turns) = numeric_values[0];
return Some((-val, is_half_turns));
}

// If this is a division call and we have two numeric values, compute the result
if is_division && numeric_values.len() >= 2 {
// Sort by port index to get correct order (numerator first, denominator second)
numeric_values.sort_by_key(|(idx, _)| *idx);
numeric_values.sort_by_key(|(idx, _, _)| *idx);
let numerator = numeric_values[0].1;
let denominator = numeric_values[1].1;
if denominator != 0.0 {
Expand All @@ -498,14 +522,14 @@ fn trace_back_for_const(hugr: &Hugr, node: Node, depth: usize) -> Option<(f64, b

// If this is a multiplication call and we have two numeric values, compute the result
if is_multiplication && numeric_values.len() >= 2 {
numeric_values.sort_by_key(|(idx, _)| *idx);
numeric_values.sort_by_key(|(idx, _, _)| *idx);
let factor1 = numeric_values[0].1;
let factor2 = numeric_values[1].1;
return Some((factor1 * factor2, false));
}

// For other calls, try to return the first numeric value found
if let Some((_, val)) = numeric_values.first() {
if let Some((_, val, _)) = numeric_values.first() {
return Some((*val, false));
}
}
Expand Down
66 changes: 45 additions & 21 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,35 +8,59 @@ quantum/classical compute execution models.

## Quick Start

=== "Python"
Simulate a distance-3 repetition code with syndrome extraction using [Guppy](https://github.com/CQCL/guppylang), a pythonic quantum programming language:

=== ":fontawesome-brands-python: Python"

```bash
pip install quantum-pecos
```

```python
from pecos import sim, Qasm

# Define a Bell state circuit
circuit = Qasm(
"""
OPENQASM 2.0;
include "qelib1.inc";
qreg q[2];
creg c[2];
h q[0];
cx q[0], q[1];
measure q -> c;
"""
from pecos import Guppy, sim, state_vector, depolarizing_noise
from guppylang import guppy
from guppylang.std.quantum import qubit, cx, measure
from guppylang.std.builtins import array, result


@guppy
def repetition_code() -> None:
# 3 data qubits encode logical |0⟩ = |000⟩
d0, d1, d2 = qubit(), qubit(), qubit()

# 2 ancillas for syndrome extraction
s0, s1 = qubit(), qubit()

# Measure parity between adjacent data qubits
cx(d0, s0)
cx(d1, s0)
cx(d1, s1)
cx(d2, s1)

# Extract syndromes as an array
result("syndrome", array(measure(s0), measure(s1)))

# Measure data qubits (required by Guppy)
_ = measure(d0), measure(d1), measure(d2)


# Run 10 shots with 10% depolarizing noise
noise = depolarizing_noise().with_uniform_probability(0.1)
results = (
sim(Guppy(repetition_code))
.qubits(5)
.quantum(state_vector())
.noise(noise)
.seed(42)
.run(10)
)

# Run 10 shots
results = sim(circuit).seed(42).run(10)
print(results.to_dict()) # {'c': [0, 0, 0, 3, 3, ...]}
# 0 = both |0⟩, 3 = both |1⟩ (always correlated!)
print(results["syndrome"])
# [[1, 1], [0, 1], [0, 0], [1, 1], [0, 0], [0, 1], [1, 1], [0, 0], [0, 1], [0, 1]]
```

=== "Rust"
Non-trivial syndromes like `[1, 0]`, `[0, 1]`, `[1, 1]` indicate detected errors that a decoder would use to identify and correct faults.

=== ":fontawesome-brands-rust: Rust"

```toml
# Cargo.toml
Expand Down Expand Up @@ -67,7 +91,7 @@ quantum/classical compute execution models.
}
```

For more examples and detailed usage, see the [User Guide](user-guide/getting-started.md).
For OpenQASM, PHIR, or other formats, see the [User Guide](user-guide/getting-started.md).

## Features

Expand Down
32 changes: 32 additions & 0 deletions docs/assets/test-data/README.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Test Data Directory

This directory contains test data files (HUGR programs, WASM modules, etc.) used by documentation code examples.

## Usage

When documentation code examples need external files, place them here. The doc test generator (`scripts/docs/generate_doc_tests.py`) can copy these files to the test environment.

## Example Files

- `repetition_code.hugr` - A compiled repetition code circuit (TODO: generate)

## Generating HUGR Files

HUGR files can be generated from Guppy programs:

```python
from guppylang import GuppyModule
from guppylang.std.quantum import qubit, cx, measure

@guppy
def my_circuit() -> None:
q = qubit()
# ... circuit logic ...

# Export to HUGR
my_circuit.compile().save("my_circuit.hugr")
```

## Note

If a documentation example requires a file that doesn't exist here, mark it with `<!--skip: Requires filename.hugr-->` in the documentation.
Loading
Loading