Simulation Logs

Circuit Extraction

Circuit extraction records the per-shot circuit instantiated during simulation, including branch-dependent operations introduced by runtime control flow.

The CircuitExtractor is used as an event_hook in run_shots(...) to capture instruction-level execution traces for each shot. After execution, extracted circuits are available through event_hook.shots.

For a given shot, get_user_circuit() returns a user-facing circuit representation, while dump() provides a detailed structural log suitable for debugging and audit.

This mechanism is useful for validating dynamic circuit behavior, comparing shot-specific execution paths, and diagnosing mismatches between intended and observed control-flow outcomes.

from guppylang import guppy
from guppylang.std.quantum import qubit, h, measure
from guppylang.std.builtins import result

@guppy
def main() -> None:
  q0: qubit = qubit()
  h(q0)
  c0: bool = measure(q0)
  result("c0", c0)
  if c0:
    q1: qubit = qubit()
    h(q1)
    c1: bool = measure(q1)
    result("c1", c1)
    if c1:
      q2: qubit = qubit()
      h(q2)
      c2: bool = measure(q2)
      result("c2", c2)

hugr = main.compile()

Running the simulation with CoinFlip shows there are multiple execution paths dependent on mid-circuit measurement outcomes.

from selene_sim.event_hooks import CircuitExtractor
from selene_sim import Coinflip, build

from hugr.qsystem.result import QsysResult

event_hook = CircuitExtractor() 
runner = build(hugr)

shots = QsysResult(runner.run_shots(
    simulator=Coinflip(),
    n_qubits=3,
    n_shots=100,
    event_hook=event_hook
))
shots.collated_counts()
Counter({(('c0', '0'),): 43,
         (('c0', '1'), ('c1', '0')): 26,
         (('c0', '1'), ('c1', '1'), ('c2', '1')): 17,
         (('c0', '1'), ('c1', '1'), ('c2', '0')): 14})

The CircuitExtractor event hook enables access to the quantum operations streamed to the runtime from the user program. This is returned to the user as a pytket circuit.

Note

The pytket representation of the user program does not capture conditional elements, since the control flow and conditional branching is reolved before user requested quantum operations are streamed to the runtime.

event_hook.shots[1].get_user_circuit()
[Reset q[0]; PhasedX(0.5, 1.5) q[0]; Rz(1) q[0]; Measure q[0] --> c[0]; Reset q[0]; PhasedX(0.5, 1.5) q[0]; Rz(1) q[0]; Measure q[0] --> c[0]; Reset q[0]; PhasedX(0.5, 1.5) q[0]; Rz(1) q[0]; Measure q[0] --> c[0]; ]

Additionally, the CircuitExtractor event hook enables access to a dialogue of post-runtime quantum operations. This is the stream of operations that runs on the quantum simulator.

event_hook.shots[1].dump()
Source.USER: QAlloc(qubit=0)
Source.USER: Reset(qubit=0)
Source.OPTIMISER: BatchStart(start_time_ns=0, duration_ns=0)
Source.OPTIMISER: Reset(qubit=0)
Source.USER: Rxy(qubit=0, theta=1.5707963267948966, phi=-1.5707963267948966)
Source.OPTIMISER: BatchStart(start_time_ns=0, duration_ns=0)
Source.OPTIMISER: Rxy(qubit=0, theta=1.5707963267948966, phi=-1.5707963267948966)
Source.USER: Rz(qubit=0, theta=3.141592653589793)
Source.OPTIMISER: BatchStart(start_time_ns=0, duration_ns=0)
Source.OPTIMISER: Rz(qubit=0, theta=3.141592653589793)
Source.USER: MeasureRequest(qubit=0)
Source.OPTIMISER: BatchStart(start_time_ns=0, duration_ns=0)
Source.OPTIMISER: FutureRead(qubit=0)
Source.USER: QFree(qubit=0)
Source.USER: FutureRead(qubit=0)
Source.USER: FutureRead(qubit=0)
Source.USER: QAlloc(qubit=0)
Source.USER: Reset(qubit=0)
Source.OPTIMISER: BatchStart(start_time_ns=0, duration_ns=0)
Source.OPTIMISER: Reset(qubit=0)
Source.USER: Rxy(qubit=0, theta=1.5707963267948966, phi=-1.5707963267948966)
Source.OPTIMISER: BatchStart(start_time_ns=0, duration_ns=0)
Source.OPTIMISER: Rxy(qubit=0, theta=1.5707963267948966, phi=-1.5707963267948966)
Source.USER: Rz(qubit=0, theta=3.141592653589793)
Source.OPTIMISER: BatchStart(start_time_ns=0, duration_ns=0)
Source.OPTIMISER: Rz(qubit=0, theta=3.141592653589793)
Source.USER: MeasureRequest(qubit=0)
Source.OPTIMISER: BatchStart(start_time_ns=0, duration_ns=0)
Source.OPTIMISER: FutureRead(qubit=0)
Source.USER: QFree(qubit=0)
Source.USER: FutureRead(qubit=1)
Source.USER: FutureRead(qubit=1)
Source.USER: QAlloc(qubit=0)
Source.USER: Reset(qubit=0)
Source.OPTIMISER: BatchStart(start_time_ns=0, duration_ns=0)
Source.OPTIMISER: Reset(qubit=0)
Source.USER: Rxy(qubit=0, theta=1.5707963267948966, phi=-1.5707963267948966)
Source.OPTIMISER: BatchStart(start_time_ns=0, duration_ns=0)
Source.OPTIMISER: Rxy(qubit=0, theta=1.5707963267948966, phi=-1.5707963267948966)
Source.USER: Rz(qubit=0, theta=3.141592653589793)
Source.OPTIMISER: BatchStart(start_time_ns=0, duration_ns=0)
Source.OPTIMISER: Rz(qubit=0, theta=3.141592653589793)
Source.USER: MeasureRequest(qubit=0)
Source.OPTIMISER: BatchStart(start_time_ns=0, duration_ns=0)
Source.OPTIMISER: FutureRead(qubit=0)
Source.USER: QFree(qubit=0)
Source.USER: FutureRead(qubit=2)

Metric Store

Analogous to the circuit extractor, the MetricStore event hook provides statistics on the user program and post-runtime dialogue. The circuit extractor is designed to retrieve a representation of the user program before and after runtime compilation. The Metrics Store instead captures statistics on gate count, and is intended to enable exploration of runtime efficacy.

Both the Circuit Extractor and the Metric Store can be used together, via the MultiEventHook object.

from selene_sim import MetricStore, MultiEventHook, SoftRZRuntime


event_hook = MultiEventHook(
   event_hooks=[
      CircuitExtractor(), 
      MetricStore()
])

shots = QsysResult(runner.run_shots(
   Coinflip(),
   runtime=SoftRZRuntime(),
   n_qubits=2,
   n_shots=2,
   event_hook=event_hook,
))

shots.collated_counts()
Counter({(('c0', '0'),): 1, (('c0', '1'), ('c1', '0')): 1})

Statistics can be retrieved on a per shot basis by indexing to the required shot. As a consequence of using the SoftRZRuntime runtime, the post-runtime program has elided rz operations.

event_hook.event_hooks[1].shots[0]["user_program"]
{'qalloc_count': 1,
 'qfree_count': 1,
 'reset_count': 1,
 'measure_request_count': 1,
 'measure_leaked_request_count': 0,
 'measure_read_count': 2,
 'rxy_count': 1,
 'rzz_count': 0,
 'rz_count': 1,
 'global_barrier_count': 0,
 'local_barrier_count': 0,
 'max_allocated': 1,
 'currently_allocated': 0}
event_hook.event_hooks[1].shots[0]["post_runtime"]
{'custom_op_batch_count': 0,
 'custom_op_individual_count': 0,
 'measure_batch_count': 1,
 'measure_individual_count': 1,
 'measure_leaked_batch_count': 0,
 'measure_leaked_individual_count': 0,
 'reset_batch_count': 1,
 'reset_individual_count': 1,
 'rxy_batch_count': 1,
 'rxy_individual_count': 1,
 'rz_batch_count': 0,
 'rz_individual_count': 0,
 'rzz_batch_count': 0,
 'rzz_individual_count': 0,
 'total_duration_ns': 0}