Skip to content

Commit 4c49481

Browse files
committed
docs + hugr
1 parent 002a443 commit 4c49481

File tree

12 files changed

+1905
-196
lines changed

12 files changed

+1905
-196
lines changed

crates/pecos-hugr/src/engine.rs

Lines changed: 361 additions & 39 deletions
Large diffs are not rendered by default.

crates/pecos-qis-ffi/src/ffi.rs

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,19 @@
66
77
use crate::{Operation, QuantumOp, with_interface};
88
use log::debug;
9+
use std::cell::Cell;
10+
11+
// Thread-local counter to prevent infinite loops in collection mode.
12+
// After MAX_COLLECTION_READS, `___read_future_bool` returns true to break out of
13+
// loops like "repeat_until_one" (while not result: ... result = measure(q)).
14+
thread_local! {
15+
static COLLECTION_MODE_READ_COUNT: Cell<u32> = const { Cell::new(0) };
16+
}
17+
18+
/// Maximum number of measurement reads in collection mode before returning true.
19+
/// This prevents infinite loops when collecting operations for programs with
20+
/// "repeat until success" patterns.
21+
const MAX_COLLECTION_READS: u32 = 100;
922

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

685-
// Default: return false (for first pass or if no dynamic mode)
686-
false
698+
// Collection mode (non-dynamic): track read count to prevent infinite loops.
699+
// For programs with "repeat until success" loops like:
700+
// while not result:
701+
// q = qubit()
702+
// result = measure(q)
703+
// Each iteration creates a new result_id, so we track total reads.
704+
// After MAX_COLLECTION_READS, we return true to break the loop.
705+
let read_count = COLLECTION_MODE_READ_COUNT.with(|c| {
706+
let count = c.get() + 1;
707+
c.set(count);
708+
count
709+
});
710+
711+
if read_count >= MAX_COLLECTION_READS {
712+
log::debug!(
713+
"___read_future_bool: collection mode read count ({read_count}) >= threshold, returning true to break loop"
714+
);
715+
true
716+
} else {
717+
// Default: return false (allows first iterations of loops to proceed)
718+
false
719+
}
720+
}
721+
722+
/// Reset the collection mode read counter.
723+
///
724+
/// This should be called at the start of each new execution to reset the loop
725+
/// termination counter used in `___read_future_bool`.
726+
pub fn reset_collection_read_count() {
727+
COLLECTION_MODE_READ_COUNT.with(|c| c.set(0));
687728
}
688729

689730
/// Increment the reference count of a future (Guppy/HUGR-LLVM style)

crates/pecos-qis-ffi/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,8 @@ where
239239
/// Reset the thread-local operation collector
240240
pub fn reset_interface() {
241241
with_interface(OperationCollector::reset);
242+
// Also reset the collection mode read counter for loop termination
243+
ffi::reset_collection_read_count();
242244
}
243245

244246
/// Get a clone of the thread-local operation collector

crates/pecos-qis/src/ccengine.rs

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -768,25 +768,14 @@ impl ClassicalEngine for QisEngine {
768768
// Convert stored measurement results to PECOS shot format
769769
let mut shot = Shot::default();
770770

771-
// Add measurements from stored results (numeric IDs)
772-
for (result_id, value) in &self.measurement_results {
773-
shot.data.insert(
774-
format!("measurement_{result_id}"),
775-
Data::U32(u32::from(*value)),
776-
);
777-
debug!(
778-
"QisEngine: Added to shot: measurement_{} = {}",
779-
result_id,
780-
i32::from(*value)
781-
);
782-
}
783-
784-
// Add named results from print_bool/print_bool_arr calls
771+
// First, try to get named results from print_bool/print_bool_arr calls
772+
let mut has_named_results = false;
785773
if let Some(state) = &self.dynamic_state
786774
&& let Some(handle) = &state.sync_handle
787775
{
788776
match handle.get_named_results() {
789777
Ok(named_results) => {
778+
has_named_results = !named_results.is_empty();
790779
for (name, values) in named_results {
791780
// Convert Vec<bool> to Data
792781
// For single values, store as U32; for arrays, store as Vec<U32>
@@ -807,10 +796,29 @@ impl ClassicalEngine for QisEngine {
807796
}
808797
}
809798

799+
// Only add raw measurements if there are no named results.
800+
// This handles circuits with variable loop iterations where each shot
801+
// may produce a different number of raw measurements, but the named
802+
// results (from result() calls) are consistent.
803+
if !has_named_results {
804+
for (result_id, value) in &self.measurement_results {
805+
shot.data.insert(
806+
format!("measurement_{result_id}"),
807+
Data::U32(u32::from(*value)),
808+
);
809+
debug!(
810+
"QisEngine: Added to shot: measurement_{} = {}",
811+
result_id,
812+
i32::from(*value)
813+
);
814+
}
815+
}
816+
810817
debug!("QisEngine: Final shot data: {:?}", shot.data);
811818
debug!(
812-
"Returning shot with {} measurement results",
813-
self.measurement_results.len()
819+
"Returning shot with {} measurement results (has_named_results={})",
820+
self.measurement_results.len(),
821+
has_named_results
814822
);
815823
Ok(shot)
816824
}

crates/pecos-quantum/src/hugr_convert.rs

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,10 @@ struct QuantumOp {
191191
/// Check if a gate type is a rotation gate that takes angle parameters.
192192
#[must_use]
193193
pub fn is_rotation_gate(gate_type: GateType) -> bool {
194-
matches!(gate_type, GateType::RX | GateType::RY | GateType::RZ)
194+
matches!(
195+
gate_type,
196+
GateType::RX | GateType::RY | GateType::RZ | GateType::CRZ
197+
)
195198
}
196199

197200
/// Try to extract a constant numeric value from a HUGR Const node.
@@ -437,6 +440,16 @@ fn trace_back_for_const(hugr: &Hugr, node: Node, depth: usize) -> Option<(f64, b
437440
return trace_back_for_const(hugr, src_node, depth + 1);
438441
}
439442
}
443+
444+
// Handle float negation (fneg) - used for negative rotation angles
445+
if op_name == "fneg" {
446+
let input_port = IncomingPort::from(0);
447+
if let Some((src_node, _)) = hugr.single_linked_output(node, input_port)
448+
&& let Some((val, is_half_turns)) = trace_back_for_const(hugr, src_node, depth + 1)
449+
{
450+
return Some((-val, is_half_turns));
451+
}
452+
}
440453
}
441454

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

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

481498
// Try to get a numeric value from this input
482-
if let Some((val, _)) = trace_back_for_const(hugr, src_node, depth + 1) {
483-
numeric_values.push((port_idx, val));
499+
if let Some((val, is_half_turns)) = trace_back_for_const(hugr, src_node, depth + 1)
500+
{
501+
numeric_values.push((port_idx, val, is_half_turns));
484502
}
485503
}
486504
}
487505

506+
// If this is a negation call and we have a numeric value, negate it
507+
if is_negation && !numeric_values.is_empty() {
508+
let (_, val, is_half_turns) = numeric_values[0];
509+
return Some((-val, is_half_turns));
510+
}
511+
488512
// If this is a division call and we have two numeric values, compute the result
489513
if is_division && numeric_values.len() >= 2 {
490514
// Sort by port index to get correct order (numerator first, denominator second)
491-
numeric_values.sort_by_key(|(idx, _)| *idx);
515+
numeric_values.sort_by_key(|(idx, _, _)| *idx);
492516
let numerator = numeric_values[0].1;
493517
let denominator = numeric_values[1].1;
494518
if denominator != 0.0 {
@@ -498,14 +522,14 @@ fn trace_back_for_const(hugr: &Hugr, node: Node, depth: usize) -> Option<(f64, b
498522

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

507531
// For other calls, try to return the first numeric value found
508-
if let Some((_, val)) = numeric_values.first() {
532+
if let Some((_, val, _)) = numeric_values.first() {
509533
return Some((*val, false));
510534
}
511535
}

docs/user-guide/hugr-simulation.md

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ Let's create a Bell state using Guppy. First, define a quantum function:
3232

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

35+
```hidden-python
36+
import os
37+
# Create shared directory for HUGR files that Rust tests can use
38+
os.makedirs("/tmp/pecos-doc-tests", exist_ok=True)
39+
```
40+
3541
```python
3642
from guppylang import guppy
3743
from guppylang.std.quantum import h, cx, measure, qubit
@@ -61,11 +67,16 @@ Let's create a Bell state using Guppy. First, define a quantum function:
6167

6268
print(results.to_dict())
6369
# Results: always correlated (00 or 11)
70+
71+
# Save compiled HUGR for other examples
72+
_hugr = bell_state.compile()
73+
with open("/tmp/pecos-doc-tests/bell_state.hugr", "w") as f:
74+
f.write(_hugr.to_str())
6475
```
6576

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

68-
<!--skip: requires pre-compiled bell_state.hugr file-->
79+
<!--test-data: bell_state.hugr-->
6980
```rust
7081
use pecos_hugr::{hugr_engine, hugr_sim};
7182
use pecos_engines::{ClassicalControlEngineBuilder, ClassicalEngine};
@@ -119,6 +130,13 @@ If you have HUGR files (compiled from Guppy or other tools), you can run them di
119130

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

133+
```hidden-python
134+
import os
135+
# Ensure circuit.hugr doesn't exist so we get FileNotFoundError
136+
if os.path.exists("circuit.hugr"):
137+
os.remove("circuit.hugr")
138+
```
139+
122140
<!--expect-error: FileNotFoundError.*circuit\.hugr-->
123141
```python
124142
from pecos import sim, Hugr
@@ -129,17 +147,26 @@ If you have HUGR files (compiled from Guppy or other tools), you can run them di
129147

130148
With an actual HUGR file:
131149

132-
<!--skip: requires pre-compiled .hugr file-->
150+
```hidden-python
151+
import shutil
152+
from pecos import sim, Hugr
153+
from pecos_rslib import state_vector
154+
# Use the bell_state.hugr generated earlier
155+
shutil.copy("/tmp/pecos-doc-tests/bell_state.hugr", "circuit.hugr")
156+
# Also save as circuit.hugr for Rust tests
157+
shutil.copy("/tmp/pecos-doc-tests/bell_state.hugr", "/tmp/pecos-doc-tests/circuit.hugr")
158+
```
159+
133160
```python
134161
# From bytes
135162
with open("circuit.hugr", "rb") as f:
136163
hugr_bytes = f.read()
137-
results = sim(Hugr(hugr_bytes)).run(1000)
164+
results = sim(Hugr(hugr_bytes)).qubits(2).quantum(state_vector()).run(1000)
138165
```
139166

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

142-
<!--skip: requires pre-compiled circuit.hugr file-->
169+
<!--test-data: circuit.hugr-->
143170
```rust
144171
use pecos_hugr::{hugr_engine, hugr_sim};
145172
use pecos_engines::{ClassicalControlEngineBuilder, ClassicalEngine};
@@ -239,7 +266,7 @@ One of HUGR's key advantages is native support for control flow based on measure
239266

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

242-
<!--skip: while loop may exceed 60s timeout-->
269+
<!--skip: HUGR while loop interpreter has known performance issues-->
243270
```python
244271
from guppylang import guppy
245272
from guppylang.std.quantum import h, measure, qubit

0 commit comments

Comments
 (0)