256 """Replays a Q2NS simulation event stream and exposes the quantum state.
260 sm = SimulationStateManager()
261 sm.load_events(parsed_events)
262 snap = sm.snapshot_at(t_ns)
263 groups = snap.entangled_states
267 - ``snapshot_at(t_ns)`` returns an immutable ``Snapshot`` carrying all
268 per-instant state; prefer its fields over the query helpers. The
269 helpers reflect the state positioned by the most recent
270 ``snapshot_at()`` call; calling them on a fresh instance returns
271 empty results silently.
272 - ``ent_graph`` is restored or advanced to the requested time on every
273 ``snapshot_at()`` call. Do not cache pointers to its contents.
274 - The class is **not** thread-safe; all calls must originate from the
277 JSON event types handled
278 -------------------------
279 ``createNode``, ``createChannel``,
280 ``createQubit``, ``sendQubit``, ``removeQubit``,
281 ``createCbit``, ``sendCbit``, ``removeCbit``,
282 ``entangle``, ``measure``, ``graphMeasure``,
283 ``sendPacket``, ``traceText``.
284 @ingroup q2nsviz_engine
289 _CHECKPOINT_INTERVAL = 256
292 """Initialize all tracking collections to empty state.
294 Call ``reset()`` to return to this state after loading events,
295 or ``load_events()`` to populate from a parsed trace file.
297 self.nodes: dict[str, Node] = {}
298 self.channels: list[Channel] = []
299 self.
qubits: dict[str, Qubit] = {}
300 self.
cbits: dict[str, ClassicalBit] = {}
301 self.
ent_graph: dict[str, set[str]] = defaultdict(set)
306 self.
events: list[dict[str, Any]] = []
314 self.
_actions: list[tuple[int, int, int, str, dict[str, Any]]] = []
315 self._checkpoints: list[tuple[int, _StateSnapshot]] = []
316 self._checkpoint_times: list[int] = []
317 self.
_cursor: tuple[int, int] |
None =
None
325 """Clear all loaded state."""
327 self.channels.clear()
335 self._checkpoints.clear()
336 self._checkpoint_times.clear()
343 """Ingest a trace and build the simulation timeline.
345 Accepts either an already-parsed event list or a path to a trace
346 file; a path is parsed with ``EventFileParser`` first, with any
347 parse errors logged as warnings. Processes ``createNode`` and
348 ``createChannel`` events immediately; all other event types are
349 deferred to ``snapshot_at()``. Runs ``_validate_events()`` and logs
350 any detected issues as warnings.
352 @param source List of raw event dictionaries from
353 ``EventFileParser``, or the path of a JSON/NDJSON
356 Calls ``reset()`` before loading, so any previously loaded state is
357 discarded. Each call to ``load_events()`` always starts from a clean
360 if isinstance(source, str | Path):
361 events, errors = EventFileParser.load_from_file(str(source))
363 logger.warning(
"Trace parse: %s", error)
371 logger.warning(
"Trace validation: %s", issue)
373 seen_channels: set[tuple] = set()
375 event_type = event.get(
"type")
377 if event_type ==
"createNode":
378 label = event.get(
"label")
380 logger.warning(
"createNode event missing 'label': %s", event)
384 x_pct=event.get(
"x", 0.0),
385 y_pct=event.get(
"y", 0.0),
386 has_explicit_position=(
"x" in event
and "y" in event),
388 self.nodes[label] = node
390 elif event_type ==
"createChannel":
391 from_node = event.get(
"from")
392 to_node = event.get(
"to")
393 if not from_node
or not to_node:
394 logger.warning(
"createChannel missing 'from'/'to': %s", event)
396 kind = event.get(
"kind",
"quantum")
399 key = (*sorted((from_node, to_node)), kind)
400 if key
in seen_channels:
402 seen_channels.add(key)
403 self.channels.append(
Channel(from_node=from_node, to_node=to_node, kind=kind))
410 def _build_index(self):
411 """Group the events by type, and collect the ``traceText`` events in time order.
413 Both indexes are built once per load so that the per-frame consumers --
414 the canvas animation passes and ``get_log_events()`` -- never rescan the
417 by_type: dict[str, list[dict[str, Any]]] = defaultdict(list)
419 by_type[event.get(
"type",
"")].append(event)
426 def _build_timeline(self):
427 """Build a sorted array of keyframe times from the event list."""
430 if "t_ns" in event
and event[
"t_ns"]
is not None:
432 if "t1_ns" in event
and event[
"t1_ns"]
is not None:
435 time_set: set[int] = set()
437 if "t_ns" in event
and event[
"t_ns"]
is not None:
438 time_set.add(event[
"t_ns"])
439 if "t0_ns" in event
and "t1_ns" in event
and event[
"t0_ns"]
is not None and event[
"t1_ns"]
is not None:
440 t0, t1 = event[
"t0_ns"], event[
"t1_ns"]
445 for k
in range(1, k_frames + 1):
446 time_set.add(int(t0 + (k / (k_frames + 1)) * (t1 - t0)))
447 if "duration_ns" in event
and event.get(
"t_ns")
is not None:
448 duration = event[
"duration_ns"]
449 if duration
and duration > 0:
450 t_end = event[
"t_ns"]
451 t_start = max(0, t_end - duration)
452 time_set.add(t_start)
454 time_set.add((t_start + t_end) // 2)
460 def _validate_events(self, events: list[dict[str, Any]]) -> list[str]:
461 """Validate the event stream and return a list of issue descriptions.
464 - Timestamps are non-negative and non-decreasing.
465 - t0_ns <= t1_ns for movement events (sendQubit, sendCbit, sendPacket).
466 - Qubit and node labels referenced before createQubit / createNode.
467 - duration_ns windows (``[t_ns - duration_ns, t_ns)``) that reach back
468 before t=0 or before the referenced qubit existed.
470 issues: list[str] = []
471 known_bits: dict[str, int] = {}
472 known_nodes: set[str] = set()
475 for idx, ev
in enumerate(events):
476 ev_type = ev.get(
"type",
"")
481 issues.append(f
"Event #{idx} ({ev_type}): negative t_ns={t}")
484 f
"Event #{idx} ({ev_type}): t_ns={t} is before previous "
485 f
"t_ns={last_t} \u2014 events are out of chronological order"
487 last_t = max(last_t, t)
489 if ev_type
in {
"sendQubit",
"sendCbit",
"sendPacket"}:
490 t0, t1 = ev.get(
"t0_ns"), ev.get(
"t1_ns")
491 if t0
is not None and t1
is not None and t0 > t1:
492 issues.append(f
"Event #{idx} ({ev_type}): t0_ns={t0} > t1_ns={t1}")
494 if ev_type ==
"createNode":
495 label = ev.get(
"label")
497 known_nodes.add(label)
499 elif ev_type ==
"createChannel":
500 for field
in (
"from",
"to"):
502 if ref
and ref
not in known_nodes:
503 issues.append(f
"Event #{idx} (createChannel): '{field}' references unknown node '{ref}'")
505 elif ev_type ==
"createQubit":
506 label = ev.get(
"label")
508 issues.append(f
"Event #{idx} (createQubit): missing 'label' field")
510 known_bits[label] = t
if t
is not None else 0
512 elif ev_type ==
"createCbit":
513 label = ev.get(
"label")
515 issues.append(f
"Event #{idx} (createCbit): missing 'label' field")
517 known_bits[label] = t
if t
is not None else 0
519 elif ev_type
in {
"measure",
"graphMeasure",
"removeQubit"}:
520 qubit_label = ev.get(
"bit")
521 if qubit_label
and qubit_label
not in known_bits:
522 issues.append(f
"Event #{idx} ({ev_type}): qubit '{qubit_label}' referenced before createQubit")
523 if ev_type !=
"removeQubit" and qubit_label:
525 self.
_window_issues(idx, ev_type, t, ev.get(
"duration_ns"), [qubit_label], known_bits)
528 elif ev_type
in {
"removeCbit"}:
529 cbit_label = ev.get(
"bit")
530 if cbit_label
and cbit_label
not in known_bits:
531 issues.append(f
"Event #{idx} ({ev_type}): cbit '{cbit_label}' referenced before createCbit")
533 elif ev_type ==
"sendQubit":
534 qubit_label = ev.get(
"bit")
535 if qubit_label
and qubit_label
not in known_bits:
536 issues.append(f
"Event #{idx} (sendQubit): qubit '{qubit_label}' referenced before createQubit")
537 for field
in (
"from",
"to"):
539 if ref
and ref
not in known_nodes:
540 issues.append(f
"Event #{idx} (sendQubit): '{field}' references unknown node '{ref}'")
542 elif ev_type ==
"sendCbit":
543 cbit_label = ev.get(
"bit")
544 if cbit_label
and cbit_label
not in known_bits:
545 issues.append(f
"Event #{idx} (sendCbit): cbit '{cbit_label}' referenced before createCbit")
546 for field
in (
"from",
"to"):
548 if ref
and ref
not in known_nodes:
549 issues.append(f
"Event #{idx} (sendCbit): '{field}' references unknown node '{ref}'")
551 elif ev_type ==
"sendPacket":
552 for field
in (
"from",
"to"):
554 if ref
and ref
not in known_nodes:
555 issues.append(f
"Event #{idx} (sendPacket): '{field}' references unknown node '{ref}'")
557 elif ev_type ==
"entangle":
558 for qubit_label
in ev.get(
"bits", []):
559 if qubit_label
not in known_bits:
561 f
"Event #{idx} (entangle): qubit '{qubit_label}' referenced before createQubit"
564 self.
_window_issues(idx, ev_type, t, ev.get(
"duration_ns"), ev.get(
"bits", []), known_bits)
571 idx: int, ev_type: str, t: int |
None, duration: int |
None, bits: list[str], known_bits: dict[str, int]
573 """Check one durationed event's back-window ``[t - duration, t)`` for acausality."""
574 issues: list[str] = []
575 if t
is None or not duration
or duration <= 0:
577 t_start = t - duration
580 f
"Event #{idx} ({ev_type}): duration_ns={duration} exceeds t_ns={t} "
581 f
"— processing window clipped at t=0"
584 created = known_bits.get(bit)
585 if created
is not None and t_start < created:
587 f
"Event #{idx} ({ev_type}): window [{t_start}, {t}) starts before "
588 f
"createQubit of '{bit}' at t_ns={created} — acausal duration_ns"
594 def _build_actions(self) -> None:
595 """Compile ``self.events`` into a time-sorted list of atomic state actions.
597 Each action is ``(t_ns, event_idx, phase, kind, event)``. Sorting by
598 ``(t_ns, event_idx, phase)`` preserves the file order of same-time
599 events, so order-sensitive semantics (``entangle`` toggling, measured
600 guards) replay exactly as in ``_snapshot_at_uncached()`` for
601 chronologically consistent traces. Events with a ``duration_ns``
602 window expand into start/end actions that maintain the operation-ring
603 sets; transfers expand into departure/arrival actions. Malformed
604 events are warned about once here and skipped.
605 @ingroup q2nsviz_engine
607 actions: list[tuple[int, int, int, str, dict[str, Any]]] = []
608 for idx, event
in enumerate(self.
events):
609 event_type = event.get(
"type")
610 t = event.get(
"t_ns", 0)
612 if event_type ==
"createQubit":
613 if not event.get(
"label"):
614 logger.warning(
"createQubit event missing 'label': %s", event)
616 actions.append((t, idx, 0,
"createQubit", event))
618 elif event_type ==
"createCbit":
619 if not event.get(
"label"):
620 logger.warning(
"createCbit event missing 'label': %s", event)
622 actions.append((t, idx, 0,
"createCbit", event))
624 elif event_type ==
"sendQubit":
625 if not event.get(
"bit"):
626 logger.warning(
"sendQubit event missing 'bit': %s", event)
628 actions.append((event.get(
"t0_ns", 0), idx, 0,
"transitStartQubit", event))
629 t1 = event.get(
"t1_ns")
631 actions.append((t1, idx, 1,
"arriveEndQubit", event))
635 actions.append((0, idx, 1,
"arriveQubit", event))
637 elif event_type ==
"sendCbit":
638 if not event.get(
"bit"):
639 logger.warning(
"sendCbit event missing 'bit': %s", event)
641 actions.append((event.get(
"t0_ns", 0), idx, 0,
"transitStartCbit", event))
642 t1 = event.get(
"t1_ns")
644 actions.append((t1, idx, 1,
"arriveEndCbit", event))
646 actions.append((0, idx, 1,
"arriveCbit", event))
648 elif event_type ==
"entangle":
649 duration = event.get(
"duration_ns", 0)
or 0
651 actions.append((max(0, t - duration), idx, 0,
"gateStart", event))
652 actions.append((t, idx, 1,
"gateEnd", event))
653 actions.append((t, idx, 2,
"entangleCommit", event))
655 elif event_type ==
"measure":
656 duration = event.get(
"duration_ns", 0)
or 0
658 actions.append((max(0, t - duration), idx, 0,
"measureStart", event))
659 actions.append((t, idx, 1,
"measureEnd", event))
660 actions.append((t, idx, 2,
"measureCommit", event))
662 elif event_type ==
"graphMeasure":
663 duration = event.get(
"duration_ns", 0)
or 0
665 actions.append((max(0, t - duration), idx, 0,
"graphMeasureStart", event))
666 actions.append((t, idx, 1,
"graphMeasureEnd", event))
667 actions.append((t, idx, 2,
"graphMeasureCommit", event))
669 elif event_type ==
"removeQubit":
670 if not event.get(
"bit"):
671 logger.warning(
"removeQubit event missing 'bit': %s", event)
673 actions.append((t, idx, 0,
"removeQubit", event))
675 elif event_type ==
"removeCbit":
676 if not event.get(
"bit"):
677 logger.warning(
"removeCbit event missing 'bit': %s", event)
679 actions.append((t, idx, 0,
"removeCbit", event))
681 actions.sort(key=
lambda a: (a[0], a[1], a[2]))
685 def _counter_inc(counter: dict[str, int], key: str) ->
None:
686 counter[key] = counter.get(key, 0) + 1
689 def _counter_dec(counter: dict[str, int], key: str) ->
None:
690 n = counter.get(key, 0)
692 counter.pop(key,
None)
696 def _apply_action(self, kind: str, event: dict[str, Any]) ->
None:
697 """Apply one atomic action to the live replay state."""
698 if kind ==
"createQubit":
699 label = event[
"label"]
700 self.
qubits[label] =
Qubit(label=label, node=event.get(
"node"))
701 elif kind ==
"createCbit":
702 label = event[
"label"]
704 elif kind ==
"transitStartQubit":
706 elif kind ==
"arriveEndQubit":
709 self.
qubits[bit].node = event.get(
"to")
711 elif kind ==
"arriveQubit":
714 self.
qubits[bit].node = event.get(
"to")
715 elif kind ==
"transitStartCbit":
717 elif kind ==
"arriveEndCbit":
719 if bit
in self.
cbits:
720 self.
cbits[bit].node = event.get(
"to")
722 elif kind ==
"arriveCbit":
724 if bit
in self.
cbits:
725 self.
cbits[bit].node = event.get(
"to")
726 elif kind ==
"gateStart":
727 for label
in event.get(
"bits", []):
729 elif kind ==
"gateEnd":
730 for label
in event.get(
"bits", []):
732 elif kind ==
"entangleCommit":
733 labels = event.get(
"bits", [])
734 for i
in range(len(labels)):
735 for j
in range(i + 1, len(labels)):
737 elif kind ==
"measureStart":
738 bit = event.get(
"bit")
741 elif kind ==
"measureEnd":
742 bit = event.get(
"bit")
745 elif kind ==
"measureCommit":
746 bit = event.get(
"bit")
750 elif kind ==
"graphMeasureStart":
751 bit = event.get(
"bit")
754 elif kind ==
"graphMeasureEnd":
755 bit = event.get(
"bit")
758 elif kind ==
"graphMeasureCommit":
759 target = event.get(
"bit")
763 elif kind ==
"removeQubit":
766 if event.get(
"reason") ==
"discarded":
769 elif kind ==
"removeCbit":
772 def _clear_replay_state(self) -> None:
773 """Reset the live replay state to the pre-simulation (t < 0) state."""
787 def _capture_snapshot(self) -> _StateSnapshot:
788 """Copy the live replay state into an immutable checkpoint."""
789 return _StateSnapshot(
790 qubits={k:
Qubit(label=v.label, node=v.node)
for k, v
in self.
qubits.items()},
791 cbits={k:
ClassicalBit(label=v.label, node=v.node)
for k, v
in self.
cbits.items()},
792 ent_graph={k: frozenset(v)
for k, v
in self.
ent_graph.items()},
804 def _restore_snapshot(self, snap: _StateSnapshot) ->
None:
805 """Replace the live replay state with fresh mutable copies of *snap*."""
806 self.
qubits = {k:
Qubit(label=v.label, node=v.node)
for k, v
in snap.qubits.items()}
808 self.
ent_graph = defaultdict(set, {k: set(v)
for k, v
in snap.ent_graph.items()})
819 def _build_checkpoints(self) -> None:
820 """Replay the action stream once, storing sparse checkpoints.
822 A checkpoint is stored after roughly every ``_CHECKPOINT_INTERVAL``
823 applied actions, always on a boundary between distinct timestamps so
824 that a restored checkpoint reflects every action at its time. Memory
825 is O(actions / interval) snapshots; traces shorter than the interval
826 store none and replay from the empty state on demand.
828 Called automatically at the end of ``load_events()``. Leaves the
829 live state cleared; ``snapshot_at()`` positions it on first use.
830 @ingroup q2nsviz_engine
832 self._checkpoints.clear()
833 self._checkpoint_times.clear()
840 t_block = actions[i][0]
841 while i < n
and actions[i][0] == t_block:
844 since_checkpoint += 1
846 self._checkpoint_times.append(t_block)
858 inflight_qubits: Iterable[str],
859 gate_qubits: Iterable[str],
860 measuring_qubits: Iterable[str],
861 graph_measuring_qubits: Iterable[str],
862 inflight_cbits: Iterable[str],
864 """Package the current live replay state as an immutable ``Snapshot``.
866 A pure function of the live state: both the incremental engine
867 (``snapshot_at()``) and the from-scratch reference
868 (``_snapshot_at_uncached()``) position the state independently and
869 then delegate here, so shared packaging cannot mask a divergence
870 between the two replay paths. The operation-window sets are computed
871 by each caller and passed in. Empty adjacency entries are dropped
872 from ``ent_graph`` and entangled components are keyed by their
873 smallest member, so snapshots from the two paths compare equal
874 whenever the underlying state matches.
875 @ingroup q2nsviz_engine
884 channels=self.channels,
885 qubits={k:
Qubit(label=v.label, node=v.node)
for k, v
in self.
qubits.items()},
886 cbits={k:
ClassicalBit(label=v.label, node=v.node)
for k, v
in self.
cbits.items()},
887 ent_graph={q: frozenset(nbrs)
for q, nbrs
in self.
ent_graph.items()
if nbrs},
888 entangled_states={min(members): tuple(sorted(members))
for members
in groups.values()},
889 measured_qubits=measured,
890 removed_qubits=removed,
891 discarded_qubits=discarded,
892 lost_qubits=removed - measured - discarded,
893 live_qubit_labels=frozenset(q
for q
in self.
qubits if q
not in removed
and q
not in measured),
894 inflight_qubits=frozenset(inflight_qubits),
895 inflight_cbits=frozenset(inflight_cbits),
896 gate_qubits=frozenset(gate_qubits),
897 measuring_qubits=frozenset(measuring_qubits),
898 graph_measuring_qubits=frozenset(graph_measuring_qubits),
902 def seek(self, t_ns: int) ->
None:
903 """Position the replay state at *t_ns* without packaging a ``Snapshot``.
905 Moving forward in time continues incrementally from the last queried
906 position; moving backward restores the nearest checkpoint at or before
907 *t_ns* and replays the gap (at most ``_CHECKPOINT_INTERVAL`` actions
908 after a restore). Valid for arbitrary timestamps, not only keyframes
911 This is the O(advanced-actions) positioning primitive shared by
912 ``snapshot_at()``. Use it directly for bulk timeline scans where the
913 O(#qubits) packaging cost of a full ``Snapshot`` per step would
914 dominate, then read the query helpers (``get_entangled_states()``
915 and friends) on the positioned state.
917 @param t_ns Simulation timestamp in nanoseconds.
918 @ingroup q2nsviz_engine
924 pos = bisect.bisect_right(self._checkpoint_times, t_ns) - 1
926 i, snap = self._checkpoints[pos]
932 while i < n
and actions[i][0] <= t_ns:
938 """Advance or restore the replay state to *t_ns* and return a ``Snapshot``.
940 Positions the state via ``seek()`` and packages it as an immutable
941 ``Snapshot``. This is the per-instant entry point used by the
942 ``QuantumVisualizerWindow`` controller and by scripts.
944 ``_snapshot_at_uncached()`` is retained as the from-scratch reference
945 implementation and produces identical results for chronologically
948 @param t_ns Simulation timestamp in nanoseconds.
949 @returns ``Snapshot`` of the reconstructed state at *t_ns*.
950 @ingroup q2nsviz_engine
962 def _snapshot_at_uncached(self, t_ns: int) -> Snapshot:
963 """Replay all events up to *t_ns* from scratch and return a ``Snapshot``.
965 The from-scratch reference implementation used to verify the incremental
966 replay engine; normal playback uses ``snapshot_at()``. Durationed events
967 (``entangle``, ``measure``, ``graphMeasure``) are stamped at operation
968 completion: the state transition commits at ``t_ns``, with the operation
969 ring shown during ``[t_ns - duration_ns, t_ns)``. O(N) in the number of
972 @param t_ns Simulation timestamp in nanoseconds.
973 @returns ``Snapshot`` of the reconstructed state at *t_ns*.
985 inflight_qubits: set[str] = set()
986 inflight_cbits: set[str] = set()
987 gate_qubits: set[str] = set()
988 measuring_qubits: set[str] = set()
989 graph_measuring_qubits: set[str] = set()
992 event_type = event.get(
"type")
993 event_t = event.get(
"t_ns", 0)
995 if event_type ==
"createQubit" and event_t <= t_ns:
996 label = event.get(
"label")
998 logger.warning(
"createQubit event missing 'label': %s", event)
1000 qubit =
Qubit(label=label, node=event.get(
"node"))
1001 self.
qubits[label] = qubit
1003 elif event_type ==
"sendQubit":
1004 qubit_label = event.get(
"bit")
1006 logger.warning(
"sendQubit event missing 'bit': %s", event)
1008 if event.get(
"t1_ns", 0) <= t_ns
and qubit_label
in self.
qubits:
1009 self.
qubits[qubit_label].node = event.get(
"to")
1010 if event.get(
"t0_ns", 0) <= t_ns < event.get(
"t1_ns", float(
"inf")):
1011 inflight_qubits.add(qubit_label)
1013 elif event_type ==
"createCbit" and event_t <= t_ns:
1014 label = event.get(
"label")
1016 logger.warning(
"createCbit event missing 'label': %s", event)
1018 cbit =
ClassicalBit(label=label, node=event.get(
"node"))
1019 self.
cbits[label] = cbit
1021 elif event_type ==
"sendCbit":
1022 cbit_label = event.get(
"bit")
1024 logger.warning(
"sendCbit event missing 'bit': %s", event)
1026 if event.get(
"t1_ns", 0) <= t_ns
and cbit_label
in self.
cbits:
1027 self.
cbits[cbit_label].node = event.get(
"to")
1028 if event.get(
"t0_ns", 0) <= t_ns < event.get(
"t1_ns", float(
"inf")):
1029 inflight_cbits.add(cbit_label)
1031 elif event_type ==
"entangle":
1032 duration_ns = event.get(
"duration_ns", 0)
or 0
1033 t_start = max(0, event_t - duration_ns)
1036 if t_start <= t_ns < event_t:
1037 for qubit_label
in event.get(
"bits", []):
1038 gate_qubits.add(qubit_label)
1040 qubit_labels = event.get(
"bits", [])
1041 for i
in range(len(qubit_labels)):
1042 for j
in range(i + 1, len(qubit_labels)):
1045 elif event_type ==
"measure":
1046 duration_ns = event.get(
"duration_ns", 0)
or 0
1047 t_start = max(0, event_t - duration_ns)
1048 if t_start <= t_ns < event_t:
1049 qubit_label = event.get(
"bit")
1051 measuring_qubits.add(qubit_label)
1053 qubit_label = event.get(
"bit")
1058 elif event_type ==
"graphMeasure":
1059 duration_ns = event.get(
"duration_ns", 0)
or 0
1060 t_start = max(0, event_t - duration_ns)
1061 if t_start <= t_ns < event_t:
1062 target = event.get(
"bit")
1064 graph_measuring_qubits.add(target)
1066 target = event.get(
"bit")
1071 elif event_type ==
"removeQubit" and event_t <= t_ns:
1072 qubit_label = event.get(
"bit")
1074 logger.warning(
"removeQubit event missing 'bit': %s", event)
1077 if event.get(
"reason") ==
"discarded":
1081 elif event_type ==
"removeCbit" and event_t <= t_ns:
1082 cbit_label = event.get(
"bit")
1084 logger.warning(
"removeCbit event missing 'bit': %s", event)
1090 inflight_qubits=inflight_qubits,
1091 gate_qubits=gate_qubits,
1092 measuring_qubits=measuring_qubits,
1093 graph_measuring_qubits=graph_measuring_qubits,
1094 inflight_cbits=inflight_cbits,
1099 def _ent_graph_add_edge(self, qubit1: str, qubit2: str):
1100 """Toggle the entanglement edge between *qubit1* and *qubit2*.
1102 Models a generic entangling operation: applying it twice is the
1103 identity, so a second ``entangle`` event on the same pair
1107 logger.warning(
"Ignoring entangle event: %s or %s is already measured", qubit1, qubit2)
1110 logger.warning(
"Ignoring entangle event: %s or %s is already removed/lost", qubit1, qubit2)
1112 if qubit2
in self.
ent_graph.get(qubit1, set()):
1119 def _ent_graph_remove_vertex(self, qubit: str):
1120 """Remove *qubit* from the entanglement graph and clean up its edges."""
1124 self.
ent_graph[connected].discard(qubit)
1127 def _ent_graph_local_complement(self, target: str):
1128 """Toggle all edges between neighbors of *target* (graph-state LC op)."""
1130 logger.warning(
"Qubit %s not found in entanglement graph", target)
1133 neighbors = list(self.
ent_graph[target])
1134 logger.debug(
"Local complementation on %s, neighbors: %s", target, neighbors)
1136 for i
in range(len(neighbors)):
1137 for j
in range(i + 1, len(neighbors)):
1138 n1, n2 = neighbors[i], neighbors[j]
1142 logger.debug(
" Removed edge: [%s, %s]", n1, n2)
1146 logger.debug(
" Added edge: [%s, %s]", n1, n2)
1148 def _perform_graph_measurement(self, target: str, basis: str, support_node: str |
None =
None):
1149 """Execute a graph-state measurement in the given Pauli basis."""
1150 basis = basis.upper()
1153 logger.info(
"Graph-state Z measurement on %s", target)
1157 logger.info(
"Graph-state Y measurement on %s", target)
1162 logger.info(
"Graph-state X measurement on %s", target)
1164 logger.warning(
"Qubit %s not found in entanglement graph", target)
1166 neighbors = sorted(self.
ent_graph[target])
1168 if support_node
is None or support_node
not in neighbors:
1170 "graphMeasure on '%s' (X-basis): no valid supportNode in trace "
1171 "(got %r, neighbors: %s). Falling back to alphabetical ('%s'). "
1172 "Displayed topology may differ from the simulator's "
1173 "internal representation.",
1179 support_node = neighbors[0]
1180 logger.debug(
"Adjacent: %s, support node: %s", neighbors, support_node)
1187 logger.debug(
"Qubit %s had no neighbors; removed", target)
1190 logger.warning(
"Unknown basis '%s', defaulting to Z measurement", basis)
1196 """Return all entanglement components as ``{root: [members]}``.
1198 Must be called *after* ``snapshot_at(t_ns)`` so that ``ent_graph``
1199 and ``qubits`` reflect the desired simulation time.
1201 Computes connected components of ``ent_graph`` via
1202 ``UnionFind``, excluding measured and removed qubits. The result
1203 therefore correctly reflects graph-state measurements (X/Y/Z) and
1204 any bridge qubits whose removal splits the graph.
1206 @returns Dictionary mapping each component root label to the
1207 sorted list of qubit labels in that component.
1214 for q, neighbors
in self.
ent_graph.items():
1217 for nbr
in neighbors:
1220 return dict(uf.groups())
1223 """Return entanglement components with two or more qubits.
1225 Traverses only the entanglement graph, so the cost scales with the
1226 number of live entanglement edges rather than with every qubit
1227 created so far (``get_entanglement_groups()`` also enrolls singleton
1228 components and therefore scans the full qubit registry).
1230 @returns ``{root: members}`` for every component with two or more
1231 qubits; equal, as member sets, to filtering
1232 ``get_entanglement_groups()`` by ``len(members) >= 2``.
1237 for q, neighbors
in self.
ent_graph.items():
1240 for nbr
in neighbors:
1243 return {root: members
for root, members
in uf.groups().items()
if len(members) > 1}
1246 """Return how many ``traceText`` events have a timestamp <= *t_ns*.
1248 The log grows monotonically with time, so the GUI uses this count to
1249 append only the new lines instead of rebuilding the whole log view.
1251 @param t_ns Cut-off time in nanoseconds.
1252 @returns Index into ``log_events`` one past the last visible entry.
1254 return bisect.bisect_right(self.
_log_times, t_ns)
1257 """Return all ``traceText`` events with timestamp <= *t_ns*, in time order.
1259 @param t_ns Cut-off time in nanoseconds.
1260 @returns List of event dicts with ``type == "traceText"``.