From 536c8dd79edaa759583d64bbe17cc3679cb65865 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Tue, 16 Jun 2026 10:52:23 -0500 Subject: [PATCH 001/118] Added EG_ATTEMPT and EG_SUCCESS record instances to SingleHeralded --- .../entanglement_management/generation/single_heralded.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sequence/entanglement_management/generation/single_heralded.py b/sequence/entanglement_management/generation/single_heralded.py index e25067da4..f0ed2f48e 100644 --- a/sequence/entanglement_management/generation/single_heralded.py +++ b/sequence/entanglement_management/generation/single_heralded.py @@ -11,7 +11,7 @@ from ...kernel.process import Process from ...kernel.quantum_manager import QuantumManager from ...resource_management.memory_manager import MemoryInfo -from ...utils import log +from ...utils import log, metrics if TYPE_CHECKING: from ...components.memory import Memory @@ -120,6 +120,8 @@ def emit_event(self) -> None: log.logger.info(f'{self} is not valid, emit_event() failed.') return + metrics.record(metrics.EG_ATTEMPT, self.owner.name, round=self.ent_round) + if self.ent_round == 1: self.memory.update_state(QuantumCircuitMixin._plus_state) self.memory.excite(self.middle, protocol='sh') @@ -228,6 +230,7 @@ def received_message(self, src: str, msg: EntanglementGenerationMessage) -> None def _entanglement_succeed(self): log.logger.info(f'{self.owner.name} successful entanglement of memory {self.memory}') + metrics.record(metrics.EG_SUCCESS, self.owner.name, fidelity=self.raw_fidelity) self.memory.entangled_memory['node_id'] = self.remote_node_name self.memory.entangled_memory['memo_id'] = self.remote_memo_id self.memory.fidelity = self.raw_fidelity From c5d7e458ddf6d69bb933be608bde150e2a760219 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Tue, 16 Jun 2026 10:52:57 -0500 Subject: [PATCH 002/118] MVP of metrics.py --- sequence/utils/__init__.py | 2 +- sequence/utils/metrics.py | 100 +++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 sequence/utils/metrics.py diff --git a/sequence/utils/__init__.py b/sequence/utils/__init__.py index 207106039..87a5c439b 100644 --- a/sequence/utils/__init__.py +++ b/sequence/utils/__init__.py @@ -1,3 +1,3 @@ -__all__ = ['encoding', 'log', 'config_generator_cli', 'graphs', 'nx_converter'] +__all__ = ['encoding', 'log', 'config_generator_cli', 'graphs', 'nx_converter', 'metrics'] def __dir__(): return sorted(__all__) diff --git a/sequence/utils/metrics.py b/sequence/utils/metrics.py new file mode 100644 index 000000000..737e2d270 --- /dev/null +++ b/sequence/utils/metrics.py @@ -0,0 +1,100 @@ +"""Centralized metrics tracking for SeQUeNCe simulations. + +This module provides a global registry for recording simulation events. +Metrics are disabled by default; call ``enable()`` to opt in to recording. +""" + +from __future__ import annotations + +from enum import Enum, auto +from time import time_ns +from typing import Any, Protocol + + +class EventType(Enum): + """Event types that can be recorded by the metrics module.""" + + EG_ATTEMPT = auto() + EG_SUCCESS = auto() + +EG_ATTEMPT = EventType.EG_ATTEMPT +EG_SUCCESS = EventType.EG_SUCCESS + +class TimeProvider(Protocol): + """Protocol for objects that supply a timestamp for recorded events.""" + + def now(self) -> int: + ... + + +class SystemTimeProvider: + """Fallback time source using the current system clock.""" + + def now(self) -> int: + return time_ns() + + +class InMemoryStorage: + """Store metric records in memory for the lifetime of the simulation.""" + + def __init__(self) -> None: + self._records: list[dict[str, Any]] = [] + + def append(self, record: dict[str, Any]) -> None: + self._records.append(record) + + def get_all(self) -> list[dict[str, Any]]: + return list(self._records) + + def get_by_event(self, event_type: EventType) -> list[dict[str, Any]]: + return [record for record in self._records if record["event_type"] is event_type] + + def get_by_owner(self, owner_name: str) -> list[dict[str, Any]]: + return [record for record in self._records if record["owner_name"] == owner_name] + + def clear(self) -> None: + self._records.clear() + + +_enabled = False +_enabled_events: set[EventType] = set() +storage = InMemoryStorage() +_system_time_provider = SystemTimeProvider() +time_provider: TimeProvider = _system_time_provider + + +def register_time_provider(provider: TimeProvider) -> None: + """Register the active time source for recorded events.""" + global time_provider + time_provider = provider + + +def enable(event_types: list[EventType]) -> None: + """Enable metrics recording for the given event types.""" + global _enabled, _enabled_events + _enabled = True + _enabled_events = set(event_types) + + +def configure(storage_type: str = "in_memory", time_provider: TimeProvider | None = None) -> None: + """Configure metrics storage and optional time source.""" + global storage + if storage_type != "in_memory": + raise ValueError("Only in_memory storage is supported") + + storage = InMemoryStorage() + if time_provider is not None: + register_time_provider(time_provider) + + +def record(event_type: EventType, owner_name: str, **kwargs: Any) -> None: + """Record a metrics event if metrics are enabled for this event type.""" + if not _enabled or event_type not in _enabled_events: + return + + storage.append({ + "event_type": event_type, + "owner_name": owner_name, + "sim_time": time_provider.now(), + **kwargs, + }) From b8d3f6d5a6c110eac7a529a50e02121748c78bc6 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Tue, 16 Jun 2026 10:55:39 -0500 Subject: [PATCH 003/118] Timeline will register itself as the metrics time provider I don't know if 2 Timelines will ever exist at once, but if so then this might be a problem. For now it seems to be an okay solution --- sequence/kernel/timeline.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sequence/kernel/timeline.py b/sequence/kernel/timeline.py index 6345c7ba5..55565e623 100644 --- a/sequence/kernel/timeline.py +++ b/sequence/kernel/timeline.py @@ -17,7 +17,7 @@ from .eventlist import EventList from .quantum_manager import QuantumManager -from ..utils import log +from ..utils import log, metrics # for timeline formatting NANOSECONDS_PER_MILLISECOND = 10**6 @@ -76,6 +76,7 @@ def __init__(self, stop_time: int = 10 ** 23, formalism: str = None, manager_kwa if manager_kwargs is None: manager_kwargs = {} self.quantum_manager: QuantumManager = QuantumManager.create(**manager_kwargs) + metrics.register_time_provider(self) def now(self) -> int: """Returns current simulation time.""" From 4e8163c7826d059ecf08ca9fdfe8d1e43d1b25c6 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Tue, 16 Jun 2026 11:07:50 -0500 Subject: [PATCH 004/118] Added tests for metrics (passed) --- tests/utils/test_metrics.py | 85 +++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 tests/utils/test_metrics.py diff --git a/tests/utils/test_metrics.py b/tests/utils/test_metrics.py new file mode 100644 index 000000000..279c361f0 --- /dev/null +++ b/tests/utils/test_metrics.py @@ -0,0 +1,85 @@ +import pytest + +from sequence.utils import metrics + + +@pytest.fixture(autouse=True) +def reset_metrics(): + metrics._enabled = False + metrics._enabled_events.clear() + metrics.storage.clear() + metrics.register_time_provider(metrics._system_time_provider) + + +# Tests that record doesn't do anything if the user has not enabled metrics +def test_record_before_enable_is_noop(): + metrics.record(metrics.EG_SUCCESS, "e0", fidelity=0.9) + assert metrics.storage.get_all() == [] + + +def test_enable_filters_event_types(): + metrics.enable([metrics.EG_SUCCESS]) + + metrics.record(metrics.EG_ATTEMPT, "e0", round=1) + metrics.record(metrics.EG_SUCCESS, "e0", fidelity=0.9) + + records = metrics.storage.get_all() + assert len(records) == 1 + assert records[0]["event_type"] is metrics.EG_SUCCESS + assert records[0]["owner_name"] == "e0" + assert records[0]["fidelity"] == 0.9 + + +def test_record_stores_arbitrary_kwargs(): + metrics.enable([metrics.EG_ATTEMPT]) + + metrics.record(metrics.EG_ATTEMPT, "e1", round=2, custom_metric=42) + + record = metrics.storage.get_all()[0] + assert record["round"] == 2 + assert record["custom_metric"] == 42 + + +def test_configure_uses_in_memory_storage(): + metrics.enable([metrics.EG_SUCCESS]) + metrics.record(metrics.EG_SUCCESS, "e0", fidelity=0.8) + assert len(metrics.storage.get_all()) == 1 + + metrics.configure(storage_type="in_memory") + assert metrics.storage.get_all() == [] + + +# In case a timeline is never created (rare but could happen?) +def test_default_time_provider_uses_system_time(monkeypatch): + monkeypatch.setattr(metrics._system_time_provider, "now", lambda: 42) + metrics.register_time_provider(metrics._system_time_provider) + metrics.enable([metrics.EG_SUCCESS]) + + metrics.record(metrics.EG_SUCCESS, "e0", fidelity=0.9) + + assert metrics.storage.get_all()[0]["sim_time"] == 42 + + +def test_register_time_provider_uses_registered_source(): + class StubTimeProvider: + def now(self) -> int: + return 12345 + + metrics.register_time_provider(StubTimeProvider()) + metrics.enable([metrics.EG_SUCCESS]) + + metrics.record(metrics.EG_SUCCESS, "e0", fidelity=0.9) + + assert metrics.storage.get_all()[0]["sim_time"] == 12345 + + +def test_storage_query_helpers(): + metrics.enable([metrics.EG_ATTEMPT, metrics.EG_SUCCESS]) + + metrics.record(metrics.EG_ATTEMPT, "e0", round=1) + metrics.record(metrics.EG_SUCCESS, "e0", fidelity=0.9) + metrics.record(metrics.EG_ATTEMPT, "e1", round=1) + + assert len(metrics.storage.get_by_event(metrics.EG_ATTEMPT)) == 2 + assert len(metrics.storage.get_by_owner("e0")) == 2 + assert len(metrics.storage.get_by_owner("e1")) == 1 From 1ffd8b1928edad944a34b2d8371d882c847ff3d3 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Tue, 16 Jun 2026 16:06:01 -0500 Subject: [PATCH 005/118] Changed EG_ATTEMPT to EG_FAILURE --- .../generation/single_heralded.py | 6 +- sequence/utils/metrics.py | 99 ++++++++++++++----- 2 files changed, 81 insertions(+), 24 deletions(-) diff --git a/sequence/entanglement_management/generation/single_heralded.py b/sequence/entanglement_management/generation/single_heralded.py index f0ed2f48e..65630e59f 100644 --- a/sequence/entanglement_management/generation/single_heralded.py +++ b/sequence/entanglement_management/generation/single_heralded.py @@ -120,8 +120,6 @@ def emit_event(self) -> None: log.logger.info(f'{self} is not valid, emit_event() failed.') return - metrics.record(metrics.EG_ATTEMPT, self.owner.name, round=self.ent_round) - if self.ent_round == 1: self.memory.update_state(QuantumCircuitMixin._plus_state) self.memory.excite(self.middle, protocol='sh') @@ -228,6 +226,10 @@ def received_message(self, src: str, msg: EntanglementGenerationMessage) -> None else: raise Exception(f"Invalid message {msg_type} received by EG on node {self.owner.name}") + def _entanglement_fail(self): + metrics.record(metrics.EG_FAILURE, self.owner.name, initial_fidelity=self.raw_fidelity) + super()._entanglement_fail() + def _entanglement_succeed(self): log.logger.info(f'{self.owner.name} successful entanglement of memory {self.memory}') metrics.record(metrics.EG_SUCCESS, self.owner.name, fidelity=self.raw_fidelity) diff --git a/sequence/utils/metrics.py b/sequence/utils/metrics.py index 737e2d270..9674be6f9 100644 --- a/sequence/utils/metrics.py +++ b/sequence/utils/metrics.py @@ -14,17 +14,18 @@ class EventType(Enum): """Event types that can be recorded by the metrics module.""" - EG_ATTEMPT = auto() + EG_FAILURE = auto() EG_SUCCESS = auto() -EG_ATTEMPT = EventType.EG_ATTEMPT + +EG_FAILURE = EventType.EG_FAILURE EG_SUCCESS = EventType.EG_SUCCESS + class TimeProvider(Protocol): """Protocol for objects that supply a timestamp for recorded events.""" - def now(self) -> int: - ... + def now(self) -> int: ... class SystemTimeProvider: @@ -47,10 +48,14 @@ def get_all(self) -> list[dict[str, Any]]: return list(self._records) def get_by_event(self, event_type: EventType) -> list[dict[str, Any]]: - return [record for record in self._records if record["event_type"] is event_type] + return [ + record for record in self._records if record["event_type"] is event_type + ] def get_by_owner(self, owner_name: str) -> list[dict[str, Any]]: - return [record for record in self._records if record["owner_name"] == owner_name] + return [ + record for record in self._records if record["owner_name"] == owner_name + ] def clear(self) -> None: self._records.clear() @@ -62,6 +67,10 @@ def clear(self) -> None: _system_time_provider = SystemTimeProvider() time_provider: TimeProvider = _system_time_provider +# Counter dicts that contain the failure and success counters for each node +_failures: dict[str, int] = {} +_successes: dict[str, int] = {} + def register_time_provider(provider: TimeProvider) -> None: """Register the active time source for recorded events.""" @@ -69,32 +78,78 @@ def register_time_provider(provider: TimeProvider) -> None: time_provider = provider +def reset_counters() -> None: + """Reset per-node attempt and success counters.""" + _failures.clear() + _successes.clear() + + +def get_failures(owner_name: str) -> int: + return _failures.get(owner_name, 0) + + +def get_successes(owner_name: str) -> int: + return _successes.get(owner_name, 0) + + +def get_success_rate(owner_name: str) -> float: + failures = get_failures(owner_name) + successes = get_successes(owner_name) + attempts = failures + successes + if attempts == 0: + return 0.0 + return successes / attempts + + def enable(event_types: list[EventType]) -> None: - """Enable metrics recording for the given event types.""" + """Enable metrics recording for the given event types. + + Available metrics types are in the metrics.EventType enum. + """ global _enabled, _enabled_events _enabled = True _enabled_events = set(event_types) -def configure(storage_type: str = "in_memory", time_provider: TimeProvider | None = None) -> None: - """Configure metrics storage and optional time source.""" - global storage - if storage_type != "in_memory": - raise ValueError("Only in_memory storage is supported") +def configure(storage_type: str = "in_memory") -> None: + """Configure metrics storage - storage = InMemoryStorage() - if time_provider is not None: - register_time_provider(time_provider) + Available storage options: + - "in_memory": The default, uses `InMemoryStorage`. Keeps all records + available stored as a dictionary object in memory. + """ + global storage + if storage_type == "in_memory": + storage = InMemoryStorage() def record(event_type: EventType, owner_name: str, **kwargs: Any) -> None: - """Record a metrics event if metrics are enabled for this event type.""" + """Record a metrics event if metrics are enabled for this event type. + + Will increment counter if counter is available for metric. + Metrics with counters: + - EG_SUCCESS + - EG_FAILURE + + Will automatically record success rate for: + - EG_SUCCESS + - EG_FAILURE + """ if not _enabled or event_type not in _enabled_events: return - storage.append({ - "event_type": event_type, - "owner_name": owner_name, - "sim_time": time_provider.now(), - **kwargs, - }) + if event_type is EG_FAILURE: + _failures[owner_name] = _failures.get(owner_name, 0) + 1 + kwargs["success_rate"] = get_success_rate(owner_name) + elif event_type is EG_SUCCESS: + _successes[owner_name] = _successes.get(owner_name, 0) + 1 + kwargs["success_rate"] = get_success_rate(owner_name) + + storage.append( + { + "event_type": event_type, + "owner_name": owner_name, + "sim_time": time_provider.now(), + **kwargs, + } + ) From 9118746a269138d7956cc9733ffcd04e61be5f55 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Tue, 16 Jun 2026 16:12:17 -0500 Subject: [PATCH 006/118] Edited metric tests to reflect change to EG_FAILURE --- tests/utils/test_metrics.py | 87 ++++++++++++++++++++++++++++--------- 1 file changed, 66 insertions(+), 21 deletions(-) diff --git a/tests/utils/test_metrics.py b/tests/utils/test_metrics.py index 279c361f0..16439751f 100644 --- a/tests/utils/test_metrics.py +++ b/tests/utils/test_metrics.py @@ -8,6 +8,7 @@ def reset_metrics(): metrics._enabled = False metrics._enabled_events.clear() metrics.storage.clear() + metrics.reset_counters() metrics.register_time_provider(metrics._system_time_provider) @@ -20,7 +21,7 @@ def test_record_before_enable_is_noop(): def test_enable_filters_event_types(): metrics.enable([metrics.EG_SUCCESS]) - metrics.record(metrics.EG_ATTEMPT, "e0", round=1) + metrics.record(metrics.EG_FAILURE, "e0", initial_fidelity=0.9) metrics.record(metrics.EG_SUCCESS, "e0", fidelity=0.9) records = metrics.storage.get_all() @@ -31,25 +32,81 @@ def test_enable_filters_event_types(): def test_record_stores_arbitrary_kwargs(): - metrics.enable([metrics.EG_ATTEMPT]) + metrics.enable([metrics.EG_FAILURE]) - metrics.record(metrics.EG_ATTEMPT, "e1", round=2, custom_metric=42) + metrics.record(metrics.EG_FAILURE, "e1", initial_fidelity=0.8, custom_metric=42) record = metrics.storage.get_all()[0] - assert record["round"] == 2 + assert record["initial_fidelity"] == 0.8 assert record["custom_metric"] == 42 -def test_configure_uses_in_memory_storage(): - metrics.enable([metrics.EG_SUCCESS]) - metrics.record(metrics.EG_SUCCESS, "e0", fidelity=0.8) - assert len(metrics.storage.get_all()) == 1 +def test_configure_replaces_storage(): + metrics.enable([metrics.EG_FAILURE, metrics.EG_SUCCESS]) + metrics.record(metrics.EG_FAILURE, "e0") + metrics.record(metrics.EG_SUCCESS, "e0") + assert len(metrics.storage.get_all()) == 2 metrics.configure(storage_type="in_memory") + assert metrics.storage.get_all() == [] -# In case a timeline is never created (rare but could happen?) +def test_reset_counters_clears_per_node_counts(): + metrics.enable([metrics.EG_FAILURE, metrics.EG_SUCCESS]) + metrics.record(metrics.EG_FAILURE, "e0") + metrics.record(metrics.EG_SUCCESS, "e0") + + metrics.reset_counters() + + assert metrics.get_failures("e0") == 0 + assert metrics.get_successes("e0") == 0 + assert metrics.get_success_rate("e0") == 0.0 + + +def test_per_node_counters_are_independent(): + metrics.enable([metrics.EG_FAILURE, metrics.EG_SUCCESS]) + + metrics.record(metrics.EG_FAILURE, "e0") + metrics.record(metrics.EG_FAILURE, "e0") + metrics.record(metrics.EG_SUCCESS, "e0") + metrics.record(metrics.EG_FAILURE, "e1") + + assert metrics.get_failures("e0") == 2 + assert metrics.get_successes("e0") == 1 + assert metrics.get_success_rate("e0") == pytest.approx(1 / 3) + assert metrics.get_failures("e1") == 1 + assert metrics.get_successes("e1") == 0 + assert metrics.get_success_rate("e1") == 0.0 + + +def test_completion_events_record_running_success_rate(): + metrics.enable([metrics.EG_FAILURE, metrics.EG_SUCCESS]) + + metrics.record(metrics.EG_FAILURE, "e0") + metrics.record(metrics.EG_SUCCESS, "e0") + metrics.record(metrics.EG_FAILURE, "e0") + + failure_records = metrics.storage.get_by_event(metrics.EG_FAILURE) + success_records = metrics.storage.get_by_event(metrics.EG_SUCCESS) + + assert failure_records[0]["success_rate"] == 0.0 + assert success_records[0]["success_rate"] == 0.5 + assert failure_records[1]["success_rate"] == pytest.approx(1 / 3) + + +def test_storage_query_helpers(): + metrics.enable([metrics.EG_FAILURE, metrics.EG_SUCCESS]) + + metrics.record(metrics.EG_FAILURE, "e0", initial_fidelity=0.9) + metrics.record(metrics.EG_SUCCESS, "e0", fidelity=0.9) + metrics.record(metrics.EG_FAILURE, "e1", initial_fidelity=0.9) + + assert len(metrics.storage.get_by_event(metrics.EG_FAILURE)) == 2 + assert len(metrics.storage.get_by_owner("e0")) == 2 + assert len(metrics.storage.get_by_owner("e1")) == 1 + + def test_default_time_provider_uses_system_time(monkeypatch): monkeypatch.setattr(metrics._system_time_provider, "now", lambda: 42) metrics.register_time_provider(metrics._system_time_provider) @@ -71,15 +128,3 @@ def now(self) -> int: metrics.record(metrics.EG_SUCCESS, "e0", fidelity=0.9) assert metrics.storage.get_all()[0]["sim_time"] == 12345 - - -def test_storage_query_helpers(): - metrics.enable([metrics.EG_ATTEMPT, metrics.EG_SUCCESS]) - - metrics.record(metrics.EG_ATTEMPT, "e0", round=1) - metrics.record(metrics.EG_SUCCESS, "e0", fidelity=0.9) - metrics.record(metrics.EG_ATTEMPT, "e1", round=1) - - assert len(metrics.storage.get_by_event(metrics.EG_ATTEMPT)) == 2 - assert len(metrics.storage.get_by_owner("e0")) == 2 - assert len(metrics.storage.get_by_owner("e1")) == 1 From 4f30c4f70233120a04d7c272483e87e84c3bdf7e Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Tue, 16 Jun 2026 16:12:55 -0500 Subject: [PATCH 007/118] Initial entanglement generation experiment to test metrics Takes heavily from https://github.com/sequence-toolbox/entanglement_purification_experiment/blob/6c49f649bdacfeebefcd85c91e9902151d775368/eg_symmetric_experiment.py --- example/eg_experiment/eg_experiment.py | 229 +++++++++++++++++++++++++ example/eg_experiment/two_node.json | 67 ++++++++ 2 files changed, 296 insertions(+) create mode 100644 example/eg_experiment/eg_experiment.py create mode 100644 example/eg_experiment/two_node.json diff --git a/example/eg_experiment/eg_experiment.py b/example/eg_experiment/eg_experiment.py new file mode 100644 index 000000000..62f965266 --- /dev/null +++ b/example/eg_experiment/eg_experiment.py @@ -0,0 +1,229 @@ +"""Entanglement generation experiment using the built-in metrics module. + +Runs SingleHeralded entanglement generation on a two-node linear topology and +produces plots of success rate versus initial fidelity and success rate over time. +""" + +from __future__ import annotations + +import json +import os +import time +from pathlib import Path + +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd + +from sequence.app.request_app import RequestApp +from sequence.constants import BELL_DIAGONAL_STATE_FORMALISM, SINGLE_HERALDED +from sequence.entanglement_management.generation import ( + EntanglementGenerationA, + EntanglementGenerationB, +) +from sequence.kernel.quantum_manager import QuantumManager as qm +from sequence.topology.router_net_topo import RouterNetTopo +from sequence.utils import metrics + +EXPERIMENT_DIR = Path(__file__).resolve().parent +CONFIG_FILE = EXPERIMENT_DIR / "two_node.json" + + +def modify_config(config_file: Path, fidelity: float) -> None: + """Set initial memory fidelity in the topology template.""" + with config_file.open("r", encoding="utf-8") as file: + config = json.load(file) + + config["templates"]["perfect_router"]["MemoryArray"]["fidelity"] = fidelity + + with config_file.open("w", encoding="utf-8") as file: + json.dump(config, file, indent=2) + + +def collect_trial_metrics(app_start: RequestApp) -> dict: + """Collect per-trial metrics from the metrics module for the app node.""" + node = app_start.node.name + return { + "eg_failures": metrics.get_failures(node), + "eg_success": metrics.get_successes(node), + "eg_success_rate": metrics.get_success_rate(node), + "app_throughput": app_start.get_throughput(), + "event_records": metrics.storage.get_by_owner(node), + } + + +def aggregate_trial_metrics(trial_metrics: list[dict]) -> dict: + """Aggregate scalar metrics across trials (mirrors eg_symmetric_experiment.py).""" + scalar_metrics = [key for key in trial_metrics[0] if key != "event_records"] + + aggregated = {} + for metric in scalar_metrics: + values = [trial[metric] for trial in trial_metrics] + aggregated[f"avg_{metric}"] = np.mean(values) + if len(values) > 0: + aggregated[f"std_{metric}"] = np.std(values) + + return aggregated + + +def run_trial( + config_file: Path, + prep_time: int, + collect_time: int, + qc_freq: float, + app_node_name: str, + other_node_name: str, + num_memories: int, + fidelity: float, + trial: int, +) -> dict: + """Run a single trial and return metrics collected from the metrics module.""" + EntanglementGenerationA.set_global_type(SINGLE_HERALDED) + EntanglementGenerationB.set_global_type(SINGLE_HERALDED) + + metrics.configure(storage_type="in_memory") + metrics.enable([metrics.EG_FAILURE, metrics.EG_SUCCESS]) + + net_topo = RouterNetTopo(str(config_file)) + qm.set_global_manager_formalism(BELL_DIAGONAL_STATE_FORMALISM) + + tl = net_topo.get_timeline() + tl.stop_time = prep_time + collect_time + + routers = net_topo.get_nodes_by_type(RouterNetTopo.QUANTUM_ROUTER) + bsm_nodes = net_topo.get_nodes_by_type(RouterNetTopo.BSM_NODE) + + base_seed = int(time.time_ns()) % (2**20) + os.getpid() + for j, node in enumerate(routers + bsm_nodes): + node.set_seed(base_seed + trial * 1000 + j) + + for qc in net_topo.get_qchannels(): + qc.frequency = qc_freq + + start_node = None + end_node = None + for node in routers: + if node.name == app_node_name: + start_node = node + elif node.name == other_node_name: + end_node = node + if not start_node or not end_node: + raise ValueError("Could not find required nodes") + + app_start = RequestApp(start_node) + RequestApp(end_node) + + tl.init() + app_start.start( + other_node_name, + prep_time, + prep_time + collect_time, + num_memories, + float(fidelity), + ) + tl.run() + + return collect_trial_metrics(app_start) + + +def plot_success_rate_vs_fidelity(results: list[dict], output_path: Path) -> None: + fidelities = [result["initial_fidelity"] for result in results] + success_rates = [result["avg_eg_success_rate"] for result in results] + errors = [result["std_eg_success_rate"] for result in results] + + plt.figure(figsize=(8, 6)) + plt.errorbar(fidelities, success_rates, yerr=errors, marker="o", capsize=3) + plt.xlabel("Initial fidelity") + plt.ylabel("Entanglement generation success rate") + plt.title("EG success rate vs initial memory fidelity") + plt.grid(True, alpha=0.3) + plt.tight_layout() + plt.savefig(output_path) + plt.close() + + +def plot_success_rate_vs_time(event_records: list[dict], output_path: Path) -> None: + completion_events = [ + record + for record in event_records + if record["event_type"] in (metrics.EG_FAILURE, metrics.EG_SUCCESS) + ] + completion_events.sort(key=lambda record: record["sim_time"]) + times = [record["sim_time"] * 1e-12 for record in completion_events] + rates = [record["success_rate"] for record in completion_events] + + plt.figure(figsize=(8, 6)) + plt.plot(times, rates, marker=".", linestyle="-", markersize=4) + plt.xlabel("Simulation time (s)") + plt.ylabel("Running EG success rate") + plt.title("EG success rate vs simulation time") + plt.grid(True, alpha=0.3) + plt.tight_layout() + plt.savefig(output_path) + plt.close() + + +def main() -> None: + num_trials = 3 + prep_time = int(1e12) + collect_time = int(50e12) + qc_freq = 1e11 + app_node_name = "left" + other_node_name = "right" + num_memories = 2 + initial_fidelities = np.round(np.arange(0.6, 1.0, 0.05), 3) + time_series_fidelity = 0.9 + + results = [] + time_series_records: list[dict] = [] + + for fidelity in initial_fidelities: + print(f"Running {num_trials} trial(s) for initial fidelity={fidelity}") + modify_config(CONFIG_FILE, float(fidelity)) + + trial_metrics = [ + run_trial( + CONFIG_FILE, + prep_time, + collect_time, + qc_freq, + app_node_name, + other_node_name, + num_memories, + float(fidelity), + trial, + ) + for trial in range(num_trials) + ] + + aggregated_metrics = aggregate_trial_metrics(trial_metrics) + + if fidelity == time_series_fidelity: + time_series_records = trial_metrics[0]["event_records"] + + result = { + "initial_fidelity": float(fidelity), + "num_trials": num_trials, + **aggregated_metrics, + } + results.append(result) + + print( + f" avg success rate: {result['avg_eg_success_rate']:.3f} " + f"(std: {result['std_eg_success_rate']:.3f})" + ) + + df = pd.DataFrame(results) + df.to_csv(EXPERIMENT_DIR / "output_eg_exp.csv", index=False) + + plot_success_rate_vs_fidelity( + results, EXPERIMENT_DIR / "success_rate_vs_fidelity.png" + ) + plot_success_rate_vs_time( + time_series_records, EXPERIMENT_DIR / "success_rate_vs_time.png" + ) + print(f"Wrote results to {EXPERIMENT_DIR}") + + +if __name__ == "__main__": + main() diff --git a/example/eg_experiment/two_node.json b/example/eg_experiment/two_node.json new file mode 100644 index 000000000..453a519eb --- /dev/null +++ b/example/eg_experiment/two_node.json @@ -0,0 +1,67 @@ +{ + "nodes": [ + { + "name": "left", + "type": "QuantumRouter", + "seed": 0, + "memo_size": 2, + "template": "perfect_router" + }, + { + "name": "right", + "type": "QuantumRouter", + "seed": 1, + "memo_size": 2, + "template": "perfect_router" + } + ], + "qconnections": [ + { + "node1": "left", + "node2": "right", + "attenuation": 0.0002, + "distance": 40000, + "type": "meet_in_the_middle", + "template": "perfect_bsm" + } + ], + "cconnections": [ + { + "node1": "left", + "node2": "right", + "delay": 200000000.0 + } + ], + "templates": { + "perfect_router": { + "MemoryArray": { + "frequency": 200000000.0, + "coherence_time": -1, + "efficiency": 1, + "fidelity": 0.95, + "cutoff_flag": false + } + }, + "perfect_bsm": { + "encoding_type": "single_heralded", + "SingleHeraldedBSM": { + "detectors": [ + { + "efficiency": 1, + "dark_count": 0, + "time_resolution": 6, + "count_rate": 100000000000.0 + }, + { + "efficiency": 1, + "dark_count": 0, + "time_resolution": 6, + "count_rate": 100000000000.0 + } + ] + } + } + }, + "is_parallel": false, + "formalism": "bell_diagonal" +} \ No newline at end of file From 9566e015ae11f1c04173fb269a3b839ceb2bf05e Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Tue, 16 Jun 2026 16:16:45 -0500 Subject: [PATCH 008/118] Helpful experiment instructions --- example/eg_experiment/README.md | 39 +++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 example/eg_experiment/README.md diff --git a/example/eg_experiment/README.md b/example/eg_experiment/README.md new file mode 100644 index 000000000..10a1852e8 --- /dev/null +++ b/example/eg_experiment/README.md @@ -0,0 +1,39 @@ +# Entanglement Generation Metrics Experiment + +This quick experiment demonstrates the built-in `metrics` module by running +a SingleHeralded entanglement generation experiment on a two-node linear +topology and plotting: + +- **Success rate vs initial fidelity** (`success_rate_vs_fidelity.png`) +- **Running success rate vs simulation time** (`success_rate_vs_time.png`) + +## Generate Topology + +Generate the network config: + +A `two_node.json` configuration is already provided in the directory, but +this command is what is used to generate it. + +```bash +generate-topology linear 2 \ + --memory-size 64 \ + --formalism bell_diagonal \ + --output two_node.json \ + --directory example/eg_experiment +``` + +## Run Experiment + +```bash +uv run python example/eg_experiment/eg_experiment.py +``` + +Results are written to this directory: + +- `output_eg_exp.csv` — aggregated trial metrics per initial fidelity +- `success_rate_vs_fidelity.png` +- `success_rate_vs_time.png` + +The experiment structure follows +[eg_symmetric_experiment.py](https://github.com/sequence-toolbox/entanglement_purification_experiment/blob/6c49f649bdacfeebefcd85c91e9902151d775368/eg_symmetric_experiment.py), +but uses `sequence.utils.metrics` instead of monkey-patching `QuantumRouter`. From a22b1d32e646750f346a3891ff7be309c58ec739 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Tue, 16 Jun 2026 16:55:21 -0500 Subject: [PATCH 009/118] Added support for directly recording throughput from RequestApp --- sequence/app/request_app.py | 13 +++++++++++++ sequence/utils/metrics.py | 15 +++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/sequence/app/request_app.py b/sequence/app/request_app.py index e8d0bc04d..613dcffd0 100644 --- a/sequence/app/request_app.py +++ b/sequence/app/request_app.py @@ -167,6 +167,19 @@ def schedule_reservation(self, reservation: Reservation) -> None: event = Event(reservation.end_time, process) self.node.timeline.schedule(event) + process = Process(self, "record_throughput_metric", []) + event = Event(reservation.end_time, process) + self.node.timeline.schedule(event) + + def record_throughput_metric(self) -> None: + from ..utils import metrics + + metrics.record( + metrics.THROUGHPUT, + self.node.name, + throughput=self.get_throughput(), + ) + def set_name(self, name: str): self.name = name diff --git a/sequence/utils/metrics.py b/sequence/utils/metrics.py index 9674be6f9..9f24811e1 100644 --- a/sequence/utils/metrics.py +++ b/sequence/utils/metrics.py @@ -16,10 +16,12 @@ class EventType(Enum): EG_FAILURE = auto() EG_SUCCESS = auto() + THROUGHPUT = auto() EG_FAILURE = EventType.EG_FAILURE EG_SUCCESS = EventType.EG_SUCCESS +THROUGHPUT = EventType.THROUGHPUT class TimeProvider(Protocol): @@ -153,3 +155,16 @@ def record(event_type: EventType, owner_name: str, **kwargs: Any) -> None: **kwargs, } ) + + +def _get_throughput(owner_name: str) -> float: + throughput_records = [ + record + for record in storage.get_by_owner(owner_name) + if record["event_type"] is THROUGHPUT + ] + if not throughput_records: + return float("nan") + return throughput_records[-1]["throughput"] + + From abb435ba1152c6b8bc0af5b31c8a832017dfe673 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Tue, 16 Jun 2026 16:57:28 -0500 Subject: [PATCH 010/118] Implemented collect and aggregate trial metrics methods --- sequence/utils/metrics.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/sequence/utils/metrics.py b/sequence/utils/metrics.py index 9f24811e1..bf700ff1e 100644 --- a/sequence/utils/metrics.py +++ b/sequence/utils/metrics.py @@ -7,6 +7,7 @@ from __future__ import annotations from enum import Enum, auto +from statistics import mean, stdev from time import time_ns from typing import Any, Protocol @@ -168,3 +169,32 @@ def _get_throughput(owner_name: str) -> float: return throughput_records[-1]["throughput"] +def collect_trial_metrics(owner_name: str) -> dict[str, Any]: + """Collect per-trial metrics for a node from the metrics module.""" + return { + "eg_failures": get_failures(owner_name), + "eg_success": get_successes(owner_name), + "eg_success_rate": get_success_rate(owner_name), + "app_throughput": _get_throughput(owner_name), + "event_records": storage.get_by_owner(owner_name), + } + + +def aggregate_trial_metrics(trials: list[dict[str, Any]]) -> dict[str, float]: + """Aggregate scalar trial metrics across multiple trials.""" + if not trials: + raise ValueError("Cannot aggregate an empty list of trials") + + aggregated: dict[str, float] = {} + scalar_metrics = [ + key + for key, value in trials[0].items() + if not isinstance(value, (list, dict)) + ] + + for metric in scalar_metrics: + values = [trial[metric] for trial in trials] + aggregated[f"avg_{metric}"] = mean(values) + aggregated[f"std_{metric}"] = stdev(values) if len(values) > 1 else 0.0 + + return aggregated From 0b35df588057d17407135d55aa39202659afda6f Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Tue, 16 Jun 2026 16:55:43 -0500 Subject: [PATCH 011/118] Metrics tests for throughput --- tests/utils/test_metrics.py | 101 ++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/tests/utils/test_metrics.py b/tests/utils/test_metrics.py index 16439751f..588da2f74 100644 --- a/tests/utils/test_metrics.py +++ b/tests/utils/test_metrics.py @@ -1,5 +1,10 @@ +import math + import pytest +from sequence.app.request_app import RequestApp +from sequence.kernel.timeline import Timeline +from sequence.topology.node import QuantumRouter from sequence.utils import metrics @@ -128,3 +133,99 @@ def now(self) -> int: metrics.record(metrics.EG_SUCCESS, "e0", fidelity=0.9) assert metrics.storage.get_all()[0]["sim_time"] == 12345 + + +def test_throughput_does_not_affect_eg_counters(): + metrics.enable([metrics.EG_FAILURE, metrics.EG_SUCCESS, metrics.THROUGHPUT]) + + metrics.record(metrics.EG_FAILURE, "e0") + metrics.record(metrics.EG_SUCCESS, "e0") + metrics.record(metrics.THROUGHPUT, "e0", throughput=42.0) + + assert metrics.get_failures("e0") == 1 + assert metrics.get_successes("e0") == 1 + assert metrics.get_success_rate("e0") == 0.5 + + +def test_collect_trial_metrics_returns_node_snapshot(): + metrics.enable([metrics.EG_FAILURE, metrics.EG_SUCCESS, metrics.THROUGHPUT]) + + metrics.record(metrics.EG_FAILURE, "e0", initial_fidelity=0.8) + metrics.record(metrics.EG_SUCCESS, "e0", fidelity=0.8) + metrics.record(metrics.THROUGHPUT, "e0", throughput=12.5) + + trial = metrics.collect_trial_metrics("e0") + + assert trial["eg_failures"] == 1 + assert trial["eg_success"] == 1 + assert trial["eg_success_rate"] == 0.5 + assert trial["app_throughput"] == 12.5 + assert len(trial["event_records"]) == 3 + + +def test_collect_trial_metrics_without_throughput_is_nan(): + metrics.enable([metrics.EG_SUCCESS]) + metrics.record(metrics.EG_SUCCESS, "e0", fidelity=0.9) + + trial = metrics.collect_trial_metrics("e0") + + assert math.isnan(trial["app_throughput"]) + + +def test_aggregate_trial_metrics_computes_avg_and_std(): + trials = [ + {"eg_failures": 10, "eg_success": 5, "eg_success_rate": 0.5, "app_throughput": 1.0}, + {"eg_failures": 12, "eg_success": 6, "eg_success_rate": 0.5, "app_throughput": 2.0}, + ] + + aggregated = metrics.aggregate_trial_metrics(trials) + + assert aggregated["avg_eg_failures"] == 11 + assert aggregated["avg_eg_success"] == 5.5 + assert aggregated["avg_app_throughput"] == 1.5 + assert aggregated["std_eg_failures"] == pytest.approx(1.4142135623730951) + + +def test_aggregate_trial_metrics_single_trial_has_zero_std(): + trials = [{"eg_failures": 3, "eg_success": 1, "eg_success_rate": 0.25, "app_throughput": 4.0}] + + aggregated = metrics.aggregate_trial_metrics(trials) + + assert aggregated["avg_eg_failures"] == 3 + assert aggregated["std_eg_failures"] == 0.0 + + +def test_aggregate_trial_metrics_skips_event_records(): + trials = [ + { + "eg_failures": 1, + "eg_success": 1, + "eg_success_rate": 0.5, + "app_throughput": 1.0, + "event_records": [{"event_type": metrics.EG_SUCCESS}], + } + ] + + aggregated = metrics.aggregate_trial_metrics(trials) + + assert "avg_event_records" not in aggregated + + +def test_aggregate_trial_metrics_empty_list_raises(): + with pytest.raises(ValueError, match="empty list"): + metrics.aggregate_trial_metrics([]) + + +def test_request_app_record_throughput_metric(): + tl = Timeline() + node = QuantumRouter("router_0", tl) + app = RequestApp(node) + app.start_t = int(1e12) + app.end_t = int(2e12) + app.memory_counter = 10 + + metrics.enable([metrics.THROUGHPUT]) + app.record_throughput_metric() + + trial = metrics.collect_trial_metrics("router_0") + assert trial["app_throughput"] == pytest.approx(10.0) From a5b446663b0512cf6b538f5b0c3fa23ba5e6c80e Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Tue, 16 Jun 2026 16:56:12 -0500 Subject: [PATCH 012/118] Updated experiment to use new collect and aggregate trial metrics methods --- example/eg_experiment/eg_experiment.py | 32 +++----------------------- 1 file changed, 3 insertions(+), 29 deletions(-) diff --git a/example/eg_experiment/eg_experiment.py b/example/eg_experiment/eg_experiment.py index 62f965266..efe0e56f6 100644 --- a/example/eg_experiment/eg_experiment.py +++ b/example/eg_experiment/eg_experiment.py @@ -40,32 +40,6 @@ def modify_config(config_file: Path, fidelity: float) -> None: json.dump(config, file, indent=2) -def collect_trial_metrics(app_start: RequestApp) -> dict: - """Collect per-trial metrics from the metrics module for the app node.""" - node = app_start.node.name - return { - "eg_failures": metrics.get_failures(node), - "eg_success": metrics.get_successes(node), - "eg_success_rate": metrics.get_success_rate(node), - "app_throughput": app_start.get_throughput(), - "event_records": metrics.storage.get_by_owner(node), - } - - -def aggregate_trial_metrics(trial_metrics: list[dict]) -> dict: - """Aggregate scalar metrics across trials (mirrors eg_symmetric_experiment.py).""" - scalar_metrics = [key for key in trial_metrics[0] if key != "event_records"] - - aggregated = {} - for metric in scalar_metrics: - values = [trial[metric] for trial in trial_metrics] - aggregated[f"avg_{metric}"] = np.mean(values) - if len(values) > 0: - aggregated[f"std_{metric}"] = np.std(values) - - return aggregated - - def run_trial( config_file: Path, prep_time: int, @@ -82,7 +56,7 @@ def run_trial( EntanglementGenerationB.set_global_type(SINGLE_HERALDED) metrics.configure(storage_type="in_memory") - metrics.enable([metrics.EG_FAILURE, metrics.EG_SUCCESS]) + metrics.enable([metrics.EG_FAILURE, metrics.EG_SUCCESS, metrics.THROUGHPUT]) net_topo = RouterNetTopo(str(config_file)) qm.set_global_manager_formalism(BELL_DIAGONAL_STATE_FORMALISM) @@ -123,7 +97,7 @@ def run_trial( ) tl.run() - return collect_trial_metrics(app_start) + return metrics.collect_trial_metrics(app_start.node.name) def plot_success_rate_vs_fidelity(results: list[dict], output_path: Path) -> None: @@ -196,7 +170,7 @@ def main() -> None: for trial in range(num_trials) ] - aggregated_metrics = aggregate_trial_metrics(trial_metrics) + aggregated_metrics = metrics.aggregate_trial_metrics(trial_metrics) if fidelity == time_series_fidelity: time_series_records = trial_metrics[0]["event_records"] From 2a9d19c9c103507e370aa6800598ca7681694dde Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Wed, 17 Jun 2026 14:25:58 -0500 Subject: [PATCH 013/118] Adjust throughput to only be recorded by responder --- example/eg_experiment/eg_experiment.py | 2 +- sequence/app/request_app.py | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/example/eg_experiment/eg_experiment.py b/example/eg_experiment/eg_experiment.py index efe0e56f6..9e77b7597 100644 --- a/example/eg_experiment/eg_experiment.py +++ b/example/eg_experiment/eg_experiment.py @@ -97,7 +97,7 @@ def run_trial( ) tl.run() - return metrics.collect_trial_metrics(app_start.node.name) + return metrics.collect_trial_metrics(other_node_name) def plot_success_rate_vs_fidelity(results: list[dict], output_path: Path) -> None: diff --git a/sequence/app/request_app.py b/sequence/app/request_app.py index 613dcffd0..457894515 100644 --- a/sequence/app/request_app.py +++ b/sequence/app/request_app.py @@ -167,9 +167,12 @@ def schedule_reservation(self, reservation: Reservation) -> None: event = Event(reservation.end_time, process) self.node.timeline.schedule(event) - process = Process(self, "record_throughput_metric", []) - event = Event(reservation.end_time, process) - self.node.timeline.schedule(event) + if self.node.name == reservation.responder: + self.start_t = reservation.start_time + self.end_t = reservation.end_time + process = Process(self, "record_throughput_metric", []) + event = Event(reservation.end_time, process) + self.node.timeline.schedule(event) def record_throughput_metric(self) -> None: from ..utils import metrics From d7ede60654ed7e3aeaadfcbc71d3c573195bc8a4 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Wed, 17 Jun 2026 14:26:09 -0500 Subject: [PATCH 014/118] Avoid NaN values getting returned as throughput --- sequence/app/request_app.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sequence/app/request_app.py b/sequence/app/request_app.py index 457894515..b272214b4 100644 --- a/sequence/app/request_app.py +++ b/sequence/app/request_app.py @@ -143,7 +143,10 @@ def get_memory(self, info: MemoryInfo) -> None: self.node.resource_manager.update(None, info.memory, "RAW") def get_throughput(self) -> float: - return self.memory_counter / (self.end_t - self.start_t) * 1e12 + duration = self.end_t - self.start_t + if duration <= 0: + return float("nan") + return self.memory_counter / duration * 1e12 def schedule_reservation(self, reservation: Reservation) -> None: From dfbf1b562f2226e2b22d37d414186c5b3e204e4a Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Wed, 17 Jun 2026 14:26:49 -0500 Subject: [PATCH 015/118] Metrics throughput tests --- tests/utils/test_metrics.py | 38 +++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/utils/test_metrics.py b/tests/utils/test_metrics.py index 588da2f74..b1dce0468 100644 --- a/tests/utils/test_metrics.py +++ b/tests/utils/test_metrics.py @@ -216,6 +216,20 @@ def test_aggregate_trial_metrics_empty_list_raises(): metrics.aggregate_trial_metrics([]) +def test_aggregate_trial_metrics_ignores_non_finite_values(): + trials = [ + {"eg_success_rate": 0.5, "app_throughput": float("nan")}, + {"eg_success_rate": 0.6, "app_throughput": 2.0}, + {"eg_success_rate": 0.4, "app_throughput": 4.0}, + ] + + aggregated = metrics.aggregate_trial_metrics(trials) + + assert aggregated["avg_eg_success_rate"] == pytest.approx(0.5) + assert aggregated["avg_app_throughput"] == 3.0 + assert aggregated["std_app_throughput"] == pytest.approx(1.4142135623730951) + + def test_request_app_record_throughput_metric(): tl = Timeline() node = QuantumRouter("router_0", tl) @@ -229,3 +243,27 @@ def test_request_app_record_throughput_metric(): trial = metrics.collect_trial_metrics("router_0") assert trial["app_throughput"] == pytest.approx(10.0) + + +def test_request_app_schedules_throughput_on_responder_only(): + from sequence.network_management.reservation import Reservation + + tl = Timeline(stop_time=int(3e12)) + responder = QuantumRouter("right", tl) + initiator = QuantumRouter("left", tl) + responder_app = RequestApp(responder) + RequestApp(initiator) + + reservation = Reservation( + "left", "right", int(1e12), int(2e12), 2, 0.9, + ) + metrics.enable([metrics.THROUGHPUT]) + responder_app.memory_counter = 4 + responder_app.schedule_reservation(reservation) + + tl.init() + tl.run() + + trial = metrics.collect_trial_metrics("right") + assert trial["app_throughput"] == pytest.approx(4.0) + assert math.isnan(metrics.collect_trial_metrics("left")["app_throughput"]) From 01b26c43980302a5fd828b79fc23eac8676904d0 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Wed, 17 Jun 2026 14:36:01 -0500 Subject: [PATCH 016/118] Use isfinite to filter out non-finite values such as NaN --- sequence/utils/metrics.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/sequence/utils/metrics.py b/sequence/utils/metrics.py index bf700ff1e..2c9ee12a4 100644 --- a/sequence/utils/metrics.py +++ b/sequence/utils/metrics.py @@ -6,6 +6,7 @@ from __future__ import annotations +import math from enum import Enum, auto from statistics import mean, stdev from time import time_ns @@ -187,14 +188,23 @@ def aggregate_trial_metrics(trials: list[dict[str, Any]]) -> dict[str, float]: aggregated: dict[str, float] = {} scalar_metrics = [ - key - for key, value in trials[0].items() - if not isinstance(value, (list, dict)) + key for key, value in trials[0].items() if not isinstance(value, (list, dict)) ] for metric in scalar_metrics: values = [trial[metric] for trial in trials] - aggregated[f"avg_{metric}"] = mean(values) - aggregated[f"std_{metric}"] = stdev(values) if len(values) > 1 else 0.0 + finite_values = [ + value + for value in values + if isinstance(value, (int, float)) and math.isfinite(value) + ] + if finite_values: + aggregated[f"avg_{metric}"] = mean(finite_values) + aggregated[f"std_{metric}"] = ( + stdev(finite_values) if len(finite_values) > 1 else 0.0 + ) + else: + aggregated[f"avg_{metric}"] = float("nan") + aggregated[f"std_{metric}"] = float("nan") return aggregated From 93853c5d11e50fb1270298fbf785bff464de9311 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Wed, 17 Jun 2026 14:37:23 -0500 Subject: [PATCH 017/118] Fix for events that are exactly at prep_time + collect_time --- example/eg_experiment/eg_experiment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/eg_experiment/eg_experiment.py b/example/eg_experiment/eg_experiment.py index 9e77b7597..7badfa3d6 100644 --- a/example/eg_experiment/eg_experiment.py +++ b/example/eg_experiment/eg_experiment.py @@ -62,7 +62,7 @@ def run_trial( qm.set_global_manager_formalism(BELL_DIAGONAL_STATE_FORMALISM) tl = net_topo.get_timeline() - tl.stop_time = prep_time + collect_time + tl.stop_time = prep_time + collect_time + 1 routers = net_topo.get_nodes_by_type(RouterNetTopo.QUANTUM_ROUTER) bsm_nodes = net_topo.get_nodes_by_type(RouterNetTopo.BSM_NODE) From e8fcb120f65454086bb1340f9c78a2528842867f Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Wed, 17 Jun 2026 15:15:12 -0500 Subject: [PATCH 018/118] Extra comments for counters --- sequence/utils/metrics.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/sequence/utils/metrics.py b/sequence/utils/metrics.py index 2c9ee12a4..bac7c2800 100644 --- a/sequence/utils/metrics.py +++ b/sequence/utils/metrics.py @@ -71,10 +71,6 @@ def clear(self) -> None: _system_time_provider = SystemTimeProvider() time_provider: TimeProvider = _system_time_provider -# Counter dicts that contain the failure and success counters for each node -_failures: dict[str, int] = {} -_successes: dict[str, int] = {} - def register_time_provider(provider: TimeProvider) -> None: """Register the active time source for recorded events.""" @@ -82,6 +78,14 @@ def register_time_provider(provider: TimeProvider) -> None: time_provider = provider +# TODO: Hopefully the counters and the 4 functions associated with them +# can be generalized for purification and swapping as well + +# Counter dicts that contain the failure and success counters for each node +_failures: dict[str, int] = {} +_successes: dict[str, int] = {} + + def reset_counters() -> None: """Reset per-node attempt and success counters.""" _failures.clear() @@ -170,6 +174,7 @@ def _get_throughput(owner_name: str) -> float: return throughput_records[-1]["throughput"] +# Not super generalized but works in the MVP def collect_trial_metrics(owner_name: str) -> dict[str, Any]: """Collect per-trial metrics for a node from the metrics module.""" return { From 9c9d90daed573f4c1cffdce65409736ba99578ee Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Thu, 18 Jun 2026 11:05:36 -0500 Subject: [PATCH 019/118] Added EP successes and failures --- sequence/utils/metrics.py | 83 ++++++++++++++++++++++++++------------- 1 file changed, 56 insertions(+), 27 deletions(-) diff --git a/sequence/utils/metrics.py b/sequence/utils/metrics.py index bac7c2800..50378ab8c 100644 --- a/sequence/utils/metrics.py +++ b/sequence/utils/metrics.py @@ -19,11 +19,15 @@ class EventType(Enum): EG_FAILURE = auto() EG_SUCCESS = auto() THROUGHPUT = auto() + EP_FAILURE = auto() + EP_SUCCESS = auto() EG_FAILURE = EventType.EG_FAILURE EG_SUCCESS = EventType.EG_SUCCESS THROUGHPUT = EventType.THROUGHPUT +EP_FAILURE = EventType.EP_FAILURE +EP_SUCCESS = EventType.EP_SUCCESS class TimeProvider(Protocol): @@ -71,6 +75,11 @@ def clear(self) -> None: _system_time_provider = SystemTimeProvider() time_provider: TimeProvider = _system_time_provider +_eg_failures: dict[str, int] = {} +_eg_successes: dict[str, int] = {} +_ep_failures: dict[str, int] = {} +_ep_successes: dict[str, int] = {} + def register_time_provider(provider: TimeProvider) -> None: """Register the active time source for recorded events.""" @@ -78,31 +87,42 @@ def register_time_provider(provider: TimeProvider) -> None: time_provider = provider -# TODO: Hopefully the counters and the 4 functions associated with them -# can be generalized for purification and swapping as well +def reset_counters() -> None: + """Reset per-node attempt and success counters.""" + _eg_failures.clear() + _eg_successes.clear() + _ep_failures.clear() + _ep_successes.clear() + -# Counter dicts that contain the failure and success counters for each node -_failures: dict[str, int] = {} -_successes: dict[str, int] = {} +def get_eg_failures(owner_name: str) -> int: + return _eg_failures.get(owner_name, 0) -def reset_counters() -> None: - """Reset per-node attempt and success counters.""" - _failures.clear() - _successes.clear() +def get_eg_successes(owner_name: str) -> int: + return _eg_successes.get(owner_name, 0) + + +def get_eg_success_rate(owner_name: str) -> float: + failures = get_eg_failures(owner_name) + successes = get_eg_successes(owner_name) + attempts = failures + successes + if attempts == 0: + return 0.0 + return successes / attempts -def get_failures(owner_name: str) -> int: - return _failures.get(owner_name, 0) +def get_ep_failures(owner_name: str) -> int: + return _ep_failures.get(owner_name, 0) -def get_successes(owner_name: str) -> int: - return _successes.get(owner_name, 0) +def get_ep_successes(owner_name: str) -> int: + return _ep_successes.get(owner_name, 0) -def get_success_rate(owner_name: str) -> float: - failures = get_failures(owner_name) - successes = get_successes(owner_name) +def get_ep_success_rate(owner_name: str) -> float: + failures = get_ep_failures(owner_name) + successes = get_ep_successes(owner_name) attempts = failures + successes if attempts == 0: return 0.0 @@ -136,22 +156,28 @@ def record(event_type: EventType, owner_name: str, **kwargs: Any) -> None: Will increment counter if counter is available for metric. Metrics with counters: - - EG_SUCCESS - - EG_FAILURE + - EG_SUCCESS, EG_FAILURE + - EP_SUCCESS, EP_FAILURE Will automatically record success rate for: - - EG_SUCCESS - - EG_FAILURE + - EG_SUCCESS, EG_FAILURE (as ``success_rate``) + - EP_SUCCESS, EP_FAILURE (as ``ep_success_rate``) """ if not _enabled or event_type not in _enabled_events: return if event_type is EG_FAILURE: - _failures[owner_name] = _failures.get(owner_name, 0) + 1 - kwargs["success_rate"] = get_success_rate(owner_name) + _eg_failures[owner_name] = _eg_failures.get(owner_name, 0) + 1 + kwargs["success_rate"] = get_eg_success_rate(owner_name) elif event_type is EG_SUCCESS: - _successes[owner_name] = _successes.get(owner_name, 0) + 1 - kwargs["success_rate"] = get_success_rate(owner_name) + _eg_successes[owner_name] = _eg_successes.get(owner_name, 0) + 1 + kwargs["success_rate"] = get_eg_success_rate(owner_name) + elif event_type is EP_FAILURE: + _ep_failures[owner_name] = _ep_failures.get(owner_name, 0) + 1 + kwargs["ep_success_rate"] = get_ep_success_rate(owner_name) + elif event_type is EP_SUCCESS: + _ep_successes[owner_name] = _ep_successes.get(owner_name, 0) + 1 + kwargs["ep_success_rate"] = get_ep_success_rate(owner_name) storage.append( { @@ -178,9 +204,12 @@ def _get_throughput(owner_name: str) -> float: def collect_trial_metrics(owner_name: str) -> dict[str, Any]: """Collect per-trial metrics for a node from the metrics module.""" return { - "eg_failures": get_failures(owner_name), - "eg_success": get_successes(owner_name), - "eg_success_rate": get_success_rate(owner_name), + "eg_failures": get_eg_failures(owner_name), + "eg_success": get_eg_successes(owner_name), + "eg_success_rate": get_eg_success_rate(owner_name), + "ep_failures": get_ep_failures(owner_name), + "ep_success": get_ep_successes(owner_name), + "ep_success_rate": get_ep_success_rate(owner_name), "app_throughput": _get_throughput(owner_name), "event_records": storage.get_by_owner(owner_name), } From 118247e3df9ef2b1444ace632d9debade3caa08a Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Thu, 18 Jun 2026 11:15:17 -0500 Subject: [PATCH 020/118] Adjusted test functions for EP --- tests/utils/test_metrics.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/tests/utils/test_metrics.py b/tests/utils/test_metrics.py index b1dce0468..bffd6ce4a 100644 --- a/tests/utils/test_metrics.py +++ b/tests/utils/test_metrics.py @@ -58,15 +58,20 @@ def test_configure_replaces_storage(): def test_reset_counters_clears_per_node_counts(): - metrics.enable([metrics.EG_FAILURE, metrics.EG_SUCCESS]) + metrics.enable([metrics.EG_FAILURE, metrics.EG_SUCCESS, metrics.EP_FAILURE, metrics.EP_SUCCESS]) metrics.record(metrics.EG_FAILURE, "e0") metrics.record(metrics.EG_SUCCESS, "e0") + metrics.record(metrics.EP_FAILURE, "e0") + metrics.record(metrics.EP_SUCCESS, "e0") metrics.reset_counters() - assert metrics.get_failures("e0") == 0 - assert metrics.get_successes("e0") == 0 - assert metrics.get_success_rate("e0") == 0.0 + assert metrics.get_eg_failures("e0") == 0 + assert metrics.get_eg_successes("e0") == 0 + assert metrics.get_eg_success_rate("e0") == 0.0 + assert metrics.get_ep_failures("e0") == 0 + assert metrics.get_ep_successes("e0") == 0 + assert metrics.get_ep_success_rate("e0") == 0.0 def test_per_node_counters_are_independent(): @@ -77,12 +82,12 @@ def test_per_node_counters_are_independent(): metrics.record(metrics.EG_SUCCESS, "e0") metrics.record(metrics.EG_FAILURE, "e1") - assert metrics.get_failures("e0") == 2 - assert metrics.get_successes("e0") == 1 - assert metrics.get_success_rate("e0") == pytest.approx(1 / 3) - assert metrics.get_failures("e1") == 1 - assert metrics.get_successes("e1") == 0 - assert metrics.get_success_rate("e1") == 0.0 + assert metrics.get_eg_failures("e0") == 2 + assert metrics.get_eg_successes("e0") == 1 + assert metrics.get_eg_success_rate("e0") == pytest.approx(1 / 3) + assert metrics.get_eg_failures("e1") == 1 + assert metrics.get_eg_successes("e1") == 0 + assert metrics.get_eg_success_rate("e1") == 0.0 def test_completion_events_record_running_success_rate(): @@ -142,9 +147,9 @@ def test_throughput_does_not_affect_eg_counters(): metrics.record(metrics.EG_SUCCESS, "e0") metrics.record(metrics.THROUGHPUT, "e0", throughput=42.0) - assert metrics.get_failures("e0") == 1 - assert metrics.get_successes("e0") == 1 - assert metrics.get_success_rate("e0") == 0.5 + assert metrics.get_eg_failures("e0") == 1 + assert metrics.get_eg_successes("e0") == 1 + assert metrics.get_eg_success_rate("e0") == 0.5 def test_collect_trial_metrics_returns_node_snapshot(): From 9a42caf9f87f65c4115ae906d5299bc0872f1a37 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Thu, 18 Jun 2026 11:15:50 -0500 Subject: [PATCH 021/118] Metrics logging for BBPSSW --- .../entanglement_management/purification/bbpssw_bds.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/sequence/entanglement_management/purification/bbpssw_bds.py b/sequence/entanglement_management/purification/bbpssw_bds.py index 1796abae6..b85a2571a 100644 --- a/sequence/entanglement_management/purification/bbpssw_bds.py +++ b/sequence/entanglement_management/purification/bbpssw_bds.py @@ -16,7 +16,7 @@ from ...topology.node import Node from ...constants import BELL_DIAGONAL_STATE_FORMALISM -from ...utils import log +from ...utils import log, metrics from .bbpssw_protocol import BBPSSWProtocol, BBPSSWMessage, BBPSSWMsgType @BBPSSWProtocol.register(BELL_DIAGONAL_STATE_FORMALISM) @@ -137,9 +137,15 @@ def received_message(self, src: str, msg: BBPSSWMessage) -> None: remote_kept_memory.bds_decohere() self.kept_memo.bds_decohere() self.kept_memo.fidelity = self.kept_memo.get_bds_fidelity() + metrics.record( + metrics.EP_SUCCESS, + self.owner.name, + fidelity=self.kept_memo.fidelity, + ) self.update_resource_manager(self.kept_memo, state="PURIFIED") else: log.logger.info(f'Purification failed because measure results: {self.meas_res}, {msg.meas_res}') + metrics.record(metrics.EP_FAILURE, self.owner.name) self.update_resource_manager(self.kept_memo, state="RAW") else: From 05089c8b0f37d1f7acd0645d60a488efa36519a2 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Thu, 18 Jun 2026 11:55:00 -0500 Subject: [PATCH 022/118] Multiprocessing for eg_experiment --- example/eg_experiment/eg_experiment.py | 46 ++++++++++++++++++-------- 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/example/eg_experiment/eg_experiment.py b/example/eg_experiment/eg_experiment.py index 7badfa3d6..d97f11609 100644 --- a/example/eg_experiment/eg_experiment.py +++ b/example/eg_experiment/eg_experiment.py @@ -7,6 +7,7 @@ from __future__ import annotations import json +import multiprocessing as mp import os import time from pathlib import Path @@ -155,20 +156,37 @@ def main() -> None: print(f"Running {num_trials} trial(s) for initial fidelity={fidelity}") modify_config(CONFIG_FILE, float(fidelity)) - trial_metrics = [ - run_trial( - CONFIG_FILE, - prep_time, - collect_time, - qc_freq, - app_node_name, - other_node_name, - num_memories, - float(fidelity), - trial, - ) - for trial in range(num_trials) - ] + if num_trials == 1: + trial_metrics = [ + run_trial( + CONFIG_FILE, + prep_time, + collect_time, + qc_freq, + app_node_name, + other_node_name, + num_memories, + float(fidelity), + 0, + ) + ] + else: + params = [ + ( + CONFIG_FILE, + prep_time, + collect_time, + qc_freq, + app_node_name, + other_node_name, + num_memories, + float(fidelity), + trial, + ) + for trial in range(num_trials) + ] + with mp.Pool(processes=mp.cpu_count()) as pool: + trial_metrics = pool.starmap(run_trial, params) aggregated_metrics = metrics.aggregate_trial_metrics(trial_metrics) From f74f7bf20c9f3c175054ad76b156f2d1c6006f2a Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Thu, 18 Jun 2026 15:14:29 -0500 Subject: [PATCH 023/118] Purified delivery metrics --- sequence/utils/metrics.py | 75 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 70 insertions(+), 5 deletions(-) diff --git a/sequence/utils/metrics.py b/sequence/utils/metrics.py index 50378ab8c..6b9772a63 100644 --- a/sequence/utils/metrics.py +++ b/sequence/utils/metrics.py @@ -6,7 +6,9 @@ from __future__ import annotations +import json import math +import time from enum import Enum, auto from statistics import mean, stdev from time import time_ns @@ -21,6 +23,7 @@ class EventType(Enum): THROUGHPUT = auto() EP_FAILURE = auto() EP_SUCCESS = auto() + PURIFIED_DELIVERY = auto() EG_FAILURE = EventType.EG_FAILURE @@ -28,6 +31,7 @@ class EventType(Enum): THROUGHPUT = EventType.THROUGHPUT EP_FAILURE = EventType.EP_FAILURE EP_SUCCESS = EventType.EP_SUCCESS +PURIFIED_DELIVERY = EventType.PURIFIED_DELIVERY class TimeProvider(Protocol): @@ -200,23 +204,65 @@ def _get_throughput(owner_name: str) -> float: return throughput_records[-1]["throughput"] -# Not super generalized but works in the MVP -def collect_trial_metrics(owner_name: str) -> dict[str, Any]: +def _get_purified_fidelities(owner_name: str) -> list[float]: + return [ + record["fidelity"] + for record in storage.get_by_owner(owner_name) + if record["event_type"] is EP_SUCCESS and "fidelity" in record + ] + + +def _get_app_ep_time( + delivery_owner: str, + target_pairs: int, + reservation_start_time: int | None, +) -> float: + delivery_records = [ + record + for record in storage.get_by_owner(delivery_owner) + if record["event_type"] is PURIFIED_DELIVERY + ] + delivery_records.sort(key=lambda record: record["sim_time"]) + if len(delivery_records) < target_pairs: + return float("nan") + if reservation_start_time is None: + return float("nan") + target_time = delivery_records[target_pairs - 1]["sim_time"] + return (target_time - reservation_start_time) * 1e-12 + + +def collect_trial_metrics( + owner_name: str, + *, + delivery_owner: str | None = None, + target_pairs: int = 500, + reservation_start_time: int | None = None, +) -> dict[str, Any]: """Collect per-trial metrics for a node from the metrics module.""" - return { + delivery_owner = delivery_owner or owner_name + result = { "eg_failures": get_eg_failures(owner_name), "eg_success": get_eg_successes(owner_name), "eg_success_rate": get_eg_success_rate(owner_name), "ep_failures": get_ep_failures(owner_name), "ep_success": get_ep_successes(owner_name), "ep_success_rate": get_ep_success_rate(owner_name), + "purified_fidelities": _get_purified_fidelities(owner_name), "app_throughput": _get_throughput(owner_name), + "app_ep_time": _get_app_ep_time( + delivery_owner, target_pairs, reservation_start_time + ), "event_records": storage.get_by_owner(owner_name), } + return result -def aggregate_trial_metrics(trials: list[dict[str, Any]]) -> dict[str, float]: - """Aggregate scalar trial metrics across multiple trials.""" +def aggregate_trial_metrics( + trials: list[dict[str, Any]], + *, + list_metric_cap: int | None = 500, +) -> dict[str, float]: + """Aggregate trial metrics across multiple trials.""" if not trials: raise ValueError("Cannot aggregate an empty list of trials") @@ -224,6 +270,7 @@ def aggregate_trial_metrics(trials: list[dict[str, Any]]) -> dict[str, float]: scalar_metrics = [ key for key, value in trials[0].items() if not isinstance(value, (list, dict)) ] + list_metrics = [key for key, value in trials[0].items() if isinstance(value, list)] for metric in scalar_metrics: values = [trial[metric] for trial in trials] @@ -241,4 +288,22 @@ def aggregate_trial_metrics(trials: list[dict[str, Any]]) -> dict[str, float]: aggregated[f"avg_{metric}"] = float("nan") aggregated[f"std_{metric}"] = float("nan") + for metric in list_metrics: + if metric == "event_records": + continue + all_values: list[float] = [] + for trial in trials: + trial_values = trial[metric] + if list_metric_cap is not None: + trial_values = trial_values[:list_metric_cap] + all_values.extend(trial_values) + if all_values: + aggregated[f"avg_{metric}"] = mean(all_values) + aggregated[f"std_{metric}"] = ( + stdev(all_values) if len(all_values) > 1 else 0.0 + ) + else: + aggregated[f"avg_{metric}"] = float("nan") + aggregated[f"std_{metric}"] = float("nan") + return aggregated From de2282e766eefee5ccb8e4d12becfa7a33d22537 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Thu, 18 Jun 2026 15:14:34 -0500 Subject: [PATCH 024/118] Tests for purified delivery metrics --- tests/utils/test_metrics.py | 122 ++++++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/tests/utils/test_metrics.py b/tests/utils/test_metrics.py index bffd6ce4a..8167a83f2 100644 --- a/tests/utils/test_metrics.py +++ b/tests/utils/test_metrics.py @@ -272,3 +272,125 @@ def test_request_app_schedules_throughput_on_responder_only(): trial = metrics.collect_trial_metrics("right") assert trial["app_throughput"] == pytest.approx(4.0) assert math.isnan(metrics.collect_trial_metrics("left")["app_throughput"]) + + +def test_ep_counters_and_success_rate(): + metrics.enable([metrics.EP_FAILURE, metrics.EP_SUCCESS]) + + metrics.record(metrics.EP_FAILURE, "left") + metrics.record(metrics.EP_SUCCESS, "left", fidelity=0.75) + metrics.record(metrics.EP_FAILURE, "left") + + assert metrics.get_ep_failures("left") == 2 + assert metrics.get_ep_successes("left") == 1 + assert metrics.get_ep_success_rate("left") == pytest.approx(1 / 3) + + success_records = metrics.storage.get_by_event(metrics.EP_SUCCESS) + assert success_records[0]["ep_success_rate"] == pytest.approx(0.5) + + +def test_purified_delivery_does_not_affect_ep_counters(): + metrics.enable([metrics.EP_FAILURE, metrics.EP_SUCCESS, metrics.PURIFIED_DELIVERY]) + + metrics.record(metrics.EP_SUCCESS, "left", fidelity=0.8) + metrics.record(metrics.PURIFIED_DELIVERY, "right", fidelity=0.8, pair_number=1) + + assert metrics.get_ep_successes("left") == 1 + assert metrics.get_ep_failures("right") == 0 + + +def test_collect_trial_metrics_ep_fields_and_delivery_time(): + class StubTimeProvider: + def __init__(self) -> None: + self._time = int(1e12) + + def now(self) -> int: + current = self._time + self._time += int(1e11) + return current + + provider = StubTimeProvider() + metrics.register_time_provider(provider) + metrics.enable([metrics.EP_SUCCESS, metrics.PURIFIED_DELIVERY]) + + metrics.record(metrics.EP_SUCCESS, "left", fidelity=0.7) + metrics.record(metrics.EP_SUCCESS, "left", fidelity=0.75) + + for pair_number in range(1, 4): + metrics.record( + metrics.PURIFIED_DELIVERY, + "right", + fidelity=0.7 + pair_number * 0.01, + pair_number=pair_number, + ) + + trial = metrics.collect_trial_metrics( + "left", + delivery_owner="right", + target_pairs=3, + reservation_start_time=int(1e12), + ) + + assert trial["ep_success"] == 2 + assert trial["purified_fidelities"] == [0.7, 0.75] + assert trial["app_ep_time"] == pytest.approx(0.4) + + +def test_collect_trial_metrics_app_ep_time_nan_when_target_not_reached(): + metrics.enable([metrics.PURIFIED_DELIVERY]) + metrics.record(metrics.PURIFIED_DELIVERY, "right", fidelity=0.9, pair_number=1) + + trial = metrics.collect_trial_metrics( + "left", + delivery_owner="right", + target_pairs=500, + reservation_start_time=int(1e12), + ) + + assert math.isnan(trial["app_ep_time"]) + + +def test_collect_trial_metrics_delivery_owner_defaults_to_owner(): + metrics.enable([metrics.PURIFIED_DELIVERY]) + metrics.record(metrics.PURIFIED_DELIVERY, "right", fidelity=0.9, pair_number=1) + + trial = metrics.collect_trial_metrics( + "right", + target_pairs=1, + reservation_start_time=0, + ) + + assert not math.isnan(trial["app_ep_time"]) + + +def test_aggregate_trial_metrics_flattens_purified_fidelities(): + trials = [ + { + "ep_success_rate": 0.6, + "purified_fidelities": [0.7, 0.75], + "app_ep_time": 10.0, + }, + { + "ep_success_rate": 0.7, + "purified_fidelities": [0.8], + "app_ep_time": 9.0, + }, + ] + + aggregated = metrics.aggregate_trial_metrics(trials) + + assert aggregated["avg_purified_fidelities"] == pytest.approx(0.75) + assert aggregated["std_purified_fidelities"] == pytest.approx(0.05) + assert aggregated["avg_app_ep_time"] == 9.5 + + +def test_aggregate_trial_metrics_handles_nan_app_ep_time(): + trials = [ + {"app_ep_time": float("nan"), "purified_fidelities": [0.7]}, + {"app_ep_time": 12.0, "purified_fidelities": [0.8]}, + ] + + aggregated = metrics.aggregate_trial_metrics(trials) + + assert aggregated["avg_app_ep_time"] == 12.0 + assert aggregated["std_app_ep_time"] == 0.0 From efee5840307c1b4b337aaaa8f490416e29f5008f Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Thu, 18 Jun 2026 15:24:28 -0500 Subject: [PATCH 025/118] Remove eg_experiment from examples --- example/eg_experiment/README.md | 39 ----- example/eg_experiment/eg_experiment.py | 221 ------------------------- example/eg_experiment/two_node.json | 67 -------- 3 files changed, 327 deletions(-) delete mode 100644 example/eg_experiment/README.md delete mode 100644 example/eg_experiment/eg_experiment.py delete mode 100644 example/eg_experiment/two_node.json diff --git a/example/eg_experiment/README.md b/example/eg_experiment/README.md deleted file mode 100644 index 10a1852e8..000000000 --- a/example/eg_experiment/README.md +++ /dev/null @@ -1,39 +0,0 @@ -# Entanglement Generation Metrics Experiment - -This quick experiment demonstrates the built-in `metrics` module by running -a SingleHeralded entanglement generation experiment on a two-node linear -topology and plotting: - -- **Success rate vs initial fidelity** (`success_rate_vs_fidelity.png`) -- **Running success rate vs simulation time** (`success_rate_vs_time.png`) - -## Generate Topology - -Generate the network config: - -A `two_node.json` configuration is already provided in the directory, but -this command is what is used to generate it. - -```bash -generate-topology linear 2 \ - --memory-size 64 \ - --formalism bell_diagonal \ - --output two_node.json \ - --directory example/eg_experiment -``` - -## Run Experiment - -```bash -uv run python example/eg_experiment/eg_experiment.py -``` - -Results are written to this directory: - -- `output_eg_exp.csv` — aggregated trial metrics per initial fidelity -- `success_rate_vs_fidelity.png` -- `success_rate_vs_time.png` - -The experiment structure follows -[eg_symmetric_experiment.py](https://github.com/sequence-toolbox/entanglement_purification_experiment/blob/6c49f649bdacfeebefcd85c91e9902151d775368/eg_symmetric_experiment.py), -but uses `sequence.utils.metrics` instead of monkey-patching `QuantumRouter`. diff --git a/example/eg_experiment/eg_experiment.py b/example/eg_experiment/eg_experiment.py deleted file mode 100644 index d97f11609..000000000 --- a/example/eg_experiment/eg_experiment.py +++ /dev/null @@ -1,221 +0,0 @@ -"""Entanglement generation experiment using the built-in metrics module. - -Runs SingleHeralded entanglement generation on a two-node linear topology and -produces plots of success rate versus initial fidelity and success rate over time. -""" - -from __future__ import annotations - -import json -import multiprocessing as mp -import os -import time -from pathlib import Path - -import matplotlib.pyplot as plt -import numpy as np -import pandas as pd - -from sequence.app.request_app import RequestApp -from sequence.constants import BELL_DIAGONAL_STATE_FORMALISM, SINGLE_HERALDED -from sequence.entanglement_management.generation import ( - EntanglementGenerationA, - EntanglementGenerationB, -) -from sequence.kernel.quantum_manager import QuantumManager as qm -from sequence.topology.router_net_topo import RouterNetTopo -from sequence.utils import metrics - -EXPERIMENT_DIR = Path(__file__).resolve().parent -CONFIG_FILE = EXPERIMENT_DIR / "two_node.json" - - -def modify_config(config_file: Path, fidelity: float) -> None: - """Set initial memory fidelity in the topology template.""" - with config_file.open("r", encoding="utf-8") as file: - config = json.load(file) - - config["templates"]["perfect_router"]["MemoryArray"]["fidelity"] = fidelity - - with config_file.open("w", encoding="utf-8") as file: - json.dump(config, file, indent=2) - - -def run_trial( - config_file: Path, - prep_time: int, - collect_time: int, - qc_freq: float, - app_node_name: str, - other_node_name: str, - num_memories: int, - fidelity: float, - trial: int, -) -> dict: - """Run a single trial and return metrics collected from the metrics module.""" - EntanglementGenerationA.set_global_type(SINGLE_HERALDED) - EntanglementGenerationB.set_global_type(SINGLE_HERALDED) - - metrics.configure(storage_type="in_memory") - metrics.enable([metrics.EG_FAILURE, metrics.EG_SUCCESS, metrics.THROUGHPUT]) - - net_topo = RouterNetTopo(str(config_file)) - qm.set_global_manager_formalism(BELL_DIAGONAL_STATE_FORMALISM) - - tl = net_topo.get_timeline() - tl.stop_time = prep_time + collect_time + 1 - - routers = net_topo.get_nodes_by_type(RouterNetTopo.QUANTUM_ROUTER) - bsm_nodes = net_topo.get_nodes_by_type(RouterNetTopo.BSM_NODE) - - base_seed = int(time.time_ns()) % (2**20) + os.getpid() - for j, node in enumerate(routers + bsm_nodes): - node.set_seed(base_seed + trial * 1000 + j) - - for qc in net_topo.get_qchannels(): - qc.frequency = qc_freq - - start_node = None - end_node = None - for node in routers: - if node.name == app_node_name: - start_node = node - elif node.name == other_node_name: - end_node = node - if not start_node or not end_node: - raise ValueError("Could not find required nodes") - - app_start = RequestApp(start_node) - RequestApp(end_node) - - tl.init() - app_start.start( - other_node_name, - prep_time, - prep_time + collect_time, - num_memories, - float(fidelity), - ) - tl.run() - - return metrics.collect_trial_metrics(other_node_name) - - -def plot_success_rate_vs_fidelity(results: list[dict], output_path: Path) -> None: - fidelities = [result["initial_fidelity"] for result in results] - success_rates = [result["avg_eg_success_rate"] for result in results] - errors = [result["std_eg_success_rate"] for result in results] - - plt.figure(figsize=(8, 6)) - plt.errorbar(fidelities, success_rates, yerr=errors, marker="o", capsize=3) - plt.xlabel("Initial fidelity") - plt.ylabel("Entanglement generation success rate") - plt.title("EG success rate vs initial memory fidelity") - plt.grid(True, alpha=0.3) - plt.tight_layout() - plt.savefig(output_path) - plt.close() - - -def plot_success_rate_vs_time(event_records: list[dict], output_path: Path) -> None: - completion_events = [ - record - for record in event_records - if record["event_type"] in (metrics.EG_FAILURE, metrics.EG_SUCCESS) - ] - completion_events.sort(key=lambda record: record["sim_time"]) - times = [record["sim_time"] * 1e-12 for record in completion_events] - rates = [record["success_rate"] for record in completion_events] - - plt.figure(figsize=(8, 6)) - plt.plot(times, rates, marker=".", linestyle="-", markersize=4) - plt.xlabel("Simulation time (s)") - plt.ylabel("Running EG success rate") - plt.title("EG success rate vs simulation time") - plt.grid(True, alpha=0.3) - plt.tight_layout() - plt.savefig(output_path) - plt.close() - - -def main() -> None: - num_trials = 3 - prep_time = int(1e12) - collect_time = int(50e12) - qc_freq = 1e11 - app_node_name = "left" - other_node_name = "right" - num_memories = 2 - initial_fidelities = np.round(np.arange(0.6, 1.0, 0.05), 3) - time_series_fidelity = 0.9 - - results = [] - time_series_records: list[dict] = [] - - for fidelity in initial_fidelities: - print(f"Running {num_trials} trial(s) for initial fidelity={fidelity}") - modify_config(CONFIG_FILE, float(fidelity)) - - if num_trials == 1: - trial_metrics = [ - run_trial( - CONFIG_FILE, - prep_time, - collect_time, - qc_freq, - app_node_name, - other_node_name, - num_memories, - float(fidelity), - 0, - ) - ] - else: - params = [ - ( - CONFIG_FILE, - prep_time, - collect_time, - qc_freq, - app_node_name, - other_node_name, - num_memories, - float(fidelity), - trial, - ) - for trial in range(num_trials) - ] - with mp.Pool(processes=mp.cpu_count()) as pool: - trial_metrics = pool.starmap(run_trial, params) - - aggregated_metrics = metrics.aggregate_trial_metrics(trial_metrics) - - if fidelity == time_series_fidelity: - time_series_records = trial_metrics[0]["event_records"] - - result = { - "initial_fidelity": float(fidelity), - "num_trials": num_trials, - **aggregated_metrics, - } - results.append(result) - - print( - f" avg success rate: {result['avg_eg_success_rate']:.3f} " - f"(std: {result['std_eg_success_rate']:.3f})" - ) - - df = pd.DataFrame(results) - df.to_csv(EXPERIMENT_DIR / "output_eg_exp.csv", index=False) - - plot_success_rate_vs_fidelity( - results, EXPERIMENT_DIR / "success_rate_vs_fidelity.png" - ) - plot_success_rate_vs_time( - time_series_records, EXPERIMENT_DIR / "success_rate_vs_time.png" - ) - print(f"Wrote results to {EXPERIMENT_DIR}") - - -if __name__ == "__main__": - main() diff --git a/example/eg_experiment/two_node.json b/example/eg_experiment/two_node.json deleted file mode 100644 index 453a519eb..000000000 --- a/example/eg_experiment/two_node.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "nodes": [ - { - "name": "left", - "type": "QuantumRouter", - "seed": 0, - "memo_size": 2, - "template": "perfect_router" - }, - { - "name": "right", - "type": "QuantumRouter", - "seed": 1, - "memo_size": 2, - "template": "perfect_router" - } - ], - "qconnections": [ - { - "node1": "left", - "node2": "right", - "attenuation": 0.0002, - "distance": 40000, - "type": "meet_in_the_middle", - "template": "perfect_bsm" - } - ], - "cconnections": [ - { - "node1": "left", - "node2": "right", - "delay": 200000000.0 - } - ], - "templates": { - "perfect_router": { - "MemoryArray": { - "frequency": 200000000.0, - "coherence_time": -1, - "efficiency": 1, - "fidelity": 0.95, - "cutoff_flag": false - } - }, - "perfect_bsm": { - "encoding_type": "single_heralded", - "SingleHeraldedBSM": { - "detectors": [ - { - "efficiency": 1, - "dark_count": 0, - "time_resolution": 6, - "count_rate": 100000000000.0 - }, - { - "efficiency": 1, - "dark_count": 0, - "time_resolution": 6, - "count_rate": 100000000000.0 - } - ] - } - } - }, - "is_parallel": false, - "formalism": "bell_diagonal" -} \ No newline at end of file From 773bb1f1b8bea7e95cb70ae30116e67ddda4000c Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Thu, 18 Jun 2026 15:27:23 -0500 Subject: [PATCH 026/118] Revert "Avoid NaN values getting returned as throughput" This reverts commit d7ede60654ed7e3aeaadfcbc71d3c573195bc8a4. --- sequence/app/request_app.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/sequence/app/request_app.py b/sequence/app/request_app.py index b272214b4..457894515 100644 --- a/sequence/app/request_app.py +++ b/sequence/app/request_app.py @@ -143,10 +143,7 @@ def get_memory(self, info: MemoryInfo) -> None: self.node.resource_manager.update(None, info.memory, "RAW") def get_throughput(self) -> float: - duration = self.end_t - self.start_t - if duration <= 0: - return float("nan") - return self.memory_counter / duration * 1e12 + return self.memory_counter / (self.end_t - self.start_t) * 1e12 def schedule_reservation(self, reservation: Reservation) -> None: From 7093f4b3920f982b73621699b9fae08890c637b2 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Fri, 19 Jun 2026 16:43:27 -0500 Subject: [PATCH 027/118] Reworked Event Types, new file for interface --- sequence/utils/metrics/__init__.py | 206 +++++++++++++++++++++++ sequence/utils/metrics/builtins.py | 63 +++++++ sequence/utils/metrics/event_types.py | 36 ++++ sequence/utils/metrics/metric_types.py | 220 +++++++++++++++++++++++++ sequence/utils/metrics/storage.py | 47 ++++++ tests/utils/test_metrics.py | 40 +++++ 6 files changed, 612 insertions(+) create mode 100644 sequence/utils/metrics/__init__.py create mode 100644 sequence/utils/metrics/builtins.py create mode 100644 sequence/utils/metrics/event_types.py create mode 100644 sequence/utils/metrics/metric_types.py create mode 100644 sequence/utils/metrics/storage.py diff --git a/sequence/utils/metrics/__init__.py b/sequence/utils/metrics/__init__.py new file mode 100644 index 000000000..0c95711e5 --- /dev/null +++ b/sequence/utils/metrics/__init__.py @@ -0,0 +1,206 @@ +"""Centralized metrics tracking for SeQUeNCe simulations. + +This module provides a global registry for recording simulation events. +Metrics are disabled by default; call ``enable()`` to opt in to recording. +""" + +from __future__ import annotations + +import math +from statistics import mean, stdev +from typing import Any + +from . import builtins +from .event_types import EventType, get_event_type, list_event_types, register_event_type +from .metric_types import ( + AppEpTimeMetric, + CollectContext, + CounterPairMetric, + EventFieldListMetric, + EventRecordsMetric, + LastValueMetric, + Metric, +) +from .registry import ( + clear_registry, + get_counter_pair, + list_metrics, + register_metric, + reset_metrics, + unregister_metric, +) +from .storage import InMemoryStorage, SystemTimeProvider, TimeProvider + +# Built-in event type aliases +EG_FAILURE = builtins.EG_FAILURE +EG_SUCCESS = builtins.EG_SUCCESS +THROUGHPUT = builtins.THROUGHPUT +EP_FAILURE = builtins.EP_FAILURE +EP_SUCCESS = builtins.EP_SUCCESS +PURIFIED_DELIVERY = builtins.PURIFIED_DELIVERY + +_enabled = False +_enabled_events: set[EventType] = set() +storage = InMemoryStorage() +_system_time_provider = SystemTimeProvider() +time_provider: TimeProvider = _system_time_provider + + +def register_time_provider(provider: TimeProvider) -> None: + """Register the active time source for recorded events.""" + global time_provider + time_provider = provider + + +def enable(event_types: list[EventType]) -> None: + """Enable metrics recording for the given event types.""" + global _enabled, _enabled_events + _enabled = True + _enabled_events = set(event_types) + + +def configure(storage_type: str = "in_memory") -> None: + """Configure metrics storage. + + Available storage options: + - "in_memory": The default, uses ``InMemoryStorage``. + """ + global storage + if storage_type == "in_memory": + storage = InMemoryStorage() + reset_metrics() + + +def record(event_type: EventType, owner_name: str, **kwargs: Any) -> None: + """Record a metrics event if metrics are enabled for this event type.""" + if not _enabled or event_type not in _enabled_events: + return + + record_kwargs = dict(kwargs) + for metric in list_metrics(): + if event_type in metric.event_types: + metric.on_record(event_type, owner_name, record_kwargs) + + storage.append( + { + "event_type": event_type, + "owner_name": owner_name, + "sim_time": time_provider.now(), + **record_kwargs, + } + ) + + +def collect_trial_metrics( + owner_name: str, + *, + delivery_owner: str | None = None, + target_pairs: int = 500, + reservation_start_time: int | None = None, +) -> dict[str, Any]: + """Collect per-trial metrics for a node from the metrics module.""" + ctx = CollectContext( + owner_name=owner_name, + storage=storage, + delivery_owner=delivery_owner or owner_name, + target_pairs=target_pairs, + reservation_start_time=reservation_start_time, + ) + result: dict[str, Any] = {} + for metric in list_metrics(): + result.update(metric.collect(ctx)) + return result + + +def aggregate_trial_metrics( + trials: list[dict[str, Any]], + *, + list_metric_cap: int | None = 500, +) -> dict[str, float]: + """Aggregate trial metrics across multiple trials.""" + if not trials: + raise ValueError("Cannot aggregate an empty list of trials") + + aggregated: dict[str, float] = {} + scalar_metrics = [ + key for key, value in trials[0].items() if not isinstance(value, (list, dict)) + ] + list_metrics_keys = [ + key for key, value in trials[0].items() if isinstance(value, list) + ] + + for metric in scalar_metrics: + values = [trial[metric] for trial in trials] + finite_values = [ + value + for value in values + if isinstance(value, (int, float)) and math.isfinite(value) + ] + if finite_values: + aggregated[f"avg_{metric}"] = mean(finite_values) + aggregated[f"std_{metric}"] = ( + stdev(finite_values) if len(finite_values) > 1 else 0.0 + ) + else: + aggregated[f"avg_{metric}"] = float("nan") + aggregated[f"std_{metric}"] = float("nan") + + for metric in list_metrics_keys: + if metric == "event_records": + continue + all_values: list[float] = [] + for trial in trials: + trial_values = trial[metric] + if list_metric_cap is not None: + trial_values = trial_values[:list_metric_cap] + all_values.extend(trial_values) + if all_values: + aggregated[f"avg_{metric}"] = mean(all_values) + aggregated[f"std_{metric}"] = ( + stdev(all_values) if len(all_values) > 1 else 0.0 + ) + else: + aggregated[f"avg_{metric}"] = float("nan") + aggregated[f"std_{metric}"] = float("nan") + + return aggregated + + +builtins.register_builtin_metrics() + +__all__ = [ + "AppEpTimeMetric", + "CollectContext", + "CounterPairMetric", + "EG_FAILURE", + "EG_SUCCESS", + "EP_FAILURE", + "EP_SUCCESS", + "EventFieldListMetric", + "EventType", + "EventRecordsMetric", + "InMemoryStorage", + "LastValueMetric", + "Metric", + "PURIFIED_DELIVERY", + "SystemTimeProvider", + "THROUGHPUT", + "TimeProvider", + "aggregate_trial_metrics", + "clear_registry", + "collect_trial_metrics", + "configure", + "enable", + "get_counter_pair", + "get_event_type", + "list_event_types", + "list_metrics", + "record", + "register_event_type", + "register_metric", + "register_time_provider", + "reset_metrics", + "storage", + "time_provider", + "unregister_metric", +] diff --git a/sequence/utils/metrics/builtins.py b/sequence/utils/metrics/builtins.py new file mode 100644 index 000000000..9e8f44520 --- /dev/null +++ b/sequence/utils/metrics/builtins.py @@ -0,0 +1,63 @@ +"""Built-in event types and metrics.""" + +from __future__ import annotations + +from .event_types import register_event_type +from .metric_types import ( + AppEpTimeMetric, + CounterPairMetric, + EventFieldListMetric, + EventRecordsMetric, + LastValueMetric, +) +from .registry import register_metric + +EG_FAILURE = register_event_type("EG_FAILURE") +EG_SUCCESS = register_event_type("EG_SUCCESS") +THROUGHPUT = register_event_type("THROUGHPUT") +EP_FAILURE = register_event_type("EP_FAILURE") +EP_SUCCESS = register_event_type("EP_SUCCESS") +PURIFIED_DELIVERY = register_event_type("PURIFIED_DELIVERY") + +EG_METRIC = CounterPairMetric( + prefix="eg", + failure_event=EG_FAILURE, + success_event=EG_SUCCESS, + rate_field="success_rate", +) +EP_METRIC = CounterPairMetric( + prefix="ep", + failure_event=EP_FAILURE, + success_event=EP_SUCCESS, + rate_field="ep_success_rate", +) +THROUGHPUT_METRIC = LastValueMetric( + key="app_throughput", + event=THROUGHPUT, + field="throughput", +) +PURIFIED_FIDELITIES_METRIC = EventFieldListMetric( + key="purified_fidelities", + event=EP_SUCCESS, + field="fidelity", +) +APP_EP_TIME_METRIC = AppEpTimeMetric( + key="app_ep_time", + delivery_event=PURIFIED_DELIVERY, +) +EVENT_RECORDS_METRIC = EventRecordsMetric() + +_BUILTIN_METRICS = ( + EG_METRIC, + EP_METRIC, + THROUGHPUT_METRIC, + PURIFIED_FIDELITIES_METRIC, + APP_EP_TIME_METRIC, + EVENT_RECORDS_METRIC, +) + + +def register_builtin_metrics() -> None: + """Register all built-in metrics.""" + for metric in _BUILTIN_METRICS: + register_metric(metric) diff --git a/sequence/utils/metrics/event_types.py b/sequence/utils/metrics/event_types.py new file mode 100644 index 000000000..d22dd1c3f --- /dev/null +++ b/sequence/utils/metrics/event_types.py @@ -0,0 +1,36 @@ +"""Registrable event types for the metrics module.""" + +from __future__ import annotations + +from dataclasses import dataclass + +_registry: dict[str, EventType] = {} + + +@dataclass(frozen=True, slots=True) +class EventType: + """Identifies a type of recordable simulation event.""" + + name: str + + +def register_event_type(name: str) -> EventType: + """Register an event type. Returns the existing instance if already registered.""" + if name in _registry: + return _registry[name] + type = EventType(name) + _registry[name] = type + return type + + +def get_event_type(name: str) -> EventType: + """Return a registered event type by name.""" + try: + return _registry[name] + except KeyError as exc: + raise KeyError(f"Event type '{name}' is not registered.") from exc + + +def list_event_types() -> list[EventType]: + """Return all registered event types.""" + return list(_registry.values()) diff --git a/sequence/utils/metrics/metric_types.py b/sequence/utils/metrics/metric_types.py new file mode 100644 index 000000000..9b38e7755 --- /dev/null +++ b/sequence/utils/metrics/metric_types.py @@ -0,0 +1,220 @@ +"""Metric type hierarchy for the metrics module.""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from dataclasses import dataclass, field +from typing import Any + +from .event_types import EventType +from .storage import InMemoryStorage + + +@dataclass +class CollectContext: + """Context passed to metrics when collecting trial results.""" + + owner_name: str + storage: InMemoryStorage + delivery_owner: str | None = None + target_pairs: int = 500 + reservation_start_time: int | None = None + + +class Metric(ABC): + """Base class for metrics that aggregate recorded events.""" + + @property + @abstractmethod + def event_types(self) -> frozenset[EventType]: + """Event types this metric reacts to during recording.""" + + @property + @abstractmethod + def output_keys(self) -> frozenset[str]: + """Keys produced by collect().""" + + def on_record( + self, + event_type: EventType, + owner_name: str, + kwargs: dict[str, Any], + ) -> None: + """Update metric state when a matching event is recorded.""" + + @abstractmethod + def collect(self, ctx: CollectContext) -> dict[str, Any]: + """Return trial result keys and values for this metric.""" + + def reset(self) -> None: + """Clear per-trial metric state.""" + + +@dataclass +class CounterPairMetric(Metric): + """Tracks failure/success counts and a running success rate.""" + + prefix: str + failure_event: EventType + success_event: EventType + rate_field: str + _failures: dict[str, int] = field(default_factory=dict, init=False, repr=False) + _successes: dict[str, int] = field(default_factory=dict, init=False, repr=False) + + @property + def event_types(self) -> frozenset[EventType]: + return frozenset({self.failure_event, self.success_event}) + + @property + def output_keys(self) -> frozenset[str]: + return frozenset( + { + f"{self.prefix}_failures", + f"{self.prefix}_success", + f"{self.prefix}_success_rate", + } + ) + + def failures(self, owner_name: str) -> int: + return self._failures.get(owner_name, 0) + + def successes(self, owner_name: str) -> int: + return self._successes.get(owner_name, 0) + + def success_rate(self, owner_name: str) -> float: + failures = self.failures(owner_name) + successes = self.successes(owner_name) + attempts = failures + successes + if attempts == 0: + return 0.0 + return successes / attempts + + def on_record( + self, + event_type: EventType, + owner_name: str, + kwargs: dict[str, Any], + ) -> None: + if event_type is self.failure_event: + self._failures[owner_name] = self._failures.get(owner_name, 0) + 1 + kwargs[self.rate_field] = self.success_rate(owner_name) + elif event_type is self.success_event: + self._successes[owner_name] = self._successes.get(owner_name, 0) + 1 + kwargs[self.rate_field] = self.success_rate(owner_name) + + def collect(self, ctx: CollectContext) -> dict[str, Any]: + return { + f"{self.prefix}_failures": self.failures(ctx.owner_name), + f"{self.prefix}_success": self.successes(ctx.owner_name), + f"{self.prefix}_success_rate": self.success_rate(ctx.owner_name), + } + + def reset(self) -> None: + self._failures.clear() + self._successes.clear() + + +@dataclass +class LastValueMetric(Metric): + """Collects the last scalar field value from matching events.""" + + key: str + event: EventType + field: str + + @property + def event_types(self) -> frozenset[EventType]: + return frozenset({self.event}) + + @property + def output_keys(self) -> frozenset[str]: + return frozenset({self.key}) + + def collect(self, ctx: CollectContext) -> dict[str, Any]: + records = [ + record + for record in ctx.storage.get_by_owner(ctx.owner_name) + if record["event_type"] is self.event + ] + if not records: + return {self.key: float("nan")} + return {self.key: records[-1][self.field]} + + +@dataclass +class EventFieldListMetric(Metric): + """Collects a list of field values from matching events.""" + + key: str + event: EventType + field: str + + @property + def event_types(self) -> frozenset[EventType]: + return frozenset({self.event}) + + @property + def output_keys(self) -> frozenset[str]: + return frozenset({self.key}) + + def collect(self, ctx: CollectContext) -> dict[str, Any]: + values = [ + record[self.field] + for record in ctx.storage.get_by_owner(ctx.owner_name) + if record["event_type"] is self.event and self.field in record + ] + return {self.key: values} + + +@dataclass +class EventRecordsMetric(Metric): + """Collects raw event records for an owner.""" + + key: str = "event_records" + + @property + def event_types(self) -> frozenset[EventType]: + return frozenset() + + @property + def output_keys(self) -> frozenset[str]: + return frozenset({self.key}) + + def collect(self, ctx: CollectContext) -> dict[str, Any]: + return {self.key: ctx.storage.get_by_owner(ctx.owner_name)} + + +@dataclass +class AppEpTimeMetric(Metric): + """Time to deliver N purified pairs relative to reservation start.""" + + key: str = "app_ep_time" + delivery_event: EventType | None = None + + @property + def event_types(self) -> frozenset[EventType]: + if self.delivery_event is None: + return frozenset() + return frozenset({self.delivery_event}) + + @property + def output_keys(self) -> frozenset[str]: + return frozenset({self.key}) + + def collect(self, ctx: CollectContext) -> dict[str, Any]: + delivery_owner = ctx.delivery_owner or ctx.owner_name + delivery_records = [ + record + for record in ctx.storage.get_by_owner(delivery_owner) + if self.delivery_event is not None + and record["event_type"] is self.delivery_event + ] + delivery_records.sort(key=lambda record: record["sim_time"]) + if len(delivery_records) < ctx.target_pairs: + return {self.key: float("nan")} + if ctx.reservation_start_time is None: + return {self.key: float("nan")} + target_time = delivery_records[ctx.target_pairs - 1]["sim_time"] + return { + self.key: (target_time - ctx.reservation_start_time) * 1e-12, + } diff --git a/sequence/utils/metrics/storage.py b/sequence/utils/metrics/storage.py new file mode 100644 index 000000000..e4c8ed7d9 --- /dev/null +++ b/sequence/utils/metrics/storage.py @@ -0,0 +1,47 @@ +"""Storage and time providers for metrics.""" + +from __future__ import annotations + +from time import time_ns +from typing import Any, Protocol + +from .event_types import EventType + + +class TimeProvider(Protocol): + """Protocol for objects that supply a timestamp for recorded events.""" + + def now(self) -> int: ... + + +class SystemTimeProvider: + """Fallback time source using the current system clock.""" + + def now(self) -> int: + return time_ns() + + +class InMemoryStorage: + """Store metric records in memory for the lifetime of the simulation.""" + + def __init__(self) -> None: + self._records: list[dict[str, Any]] = [] + + def append(self, record: dict[str, Any]) -> None: + self._records.append(record) + + def get_all(self) -> list[dict[str, Any]]: + return list(self._records) + + def get_by_event(self, event_type: EventType) -> list[dict[str, Any]]: + return [ + record for record in self._records if record["event_type"] is event_type + ] + + def get_by_owner(self, owner_name: str) -> list[dict[str, Any]]: + return [ + record for record in self._records if record["owner_name"] == owner_name + ] + + def clear(self) -> None: + self._records.clear() diff --git a/tests/utils/test_metrics.py b/tests/utils/test_metrics.py index 8167a83f2..f9ab0ccb4 100644 --- a/tests/utils/test_metrics.py +++ b/tests/utils/test_metrics.py @@ -394,3 +394,43 @@ def test_aggregate_trial_metrics_handles_nan_app_ep_time(): assert aggregated["avg_app_ep_time"] == 12.0 assert aggregated["std_app_ep_time"] == 0.0 + + +def test_register_event_type_is_idempotent(): + type_a = metrics.register_event_type("CUSTOM_EVENT") + type_b = metrics.register_event_type("CUSTOM_EVENT") + assert type_a is type_b + + +def test_register_metric_adds_to_collect_trial_metrics(): + swap_failure = metrics.register_event_type("SWAP_FAILURE") + swap_success = metrics.register_event_type("SWAP_SUCCESS") + swap_metric = CounterPairMetric( + prefix="swap", + failure_event=swap_failure, + success_event=swap_success, + rate_field="swap_success_rate", + ) + metrics.register_metric(swap_metric) + + metrics.enable([swap_failure, swap_success]) + metrics.record(swap_failure, "n0") + metrics.record(swap_success, "n0") + + trial = metrics.collect_trial_metrics("n0") + assert trial["swap_failures"] == 1 + assert trial["swap_success"] == 1 + assert trial["swap_success_rate"] == 0.5 + + metrics.unregister_metric(swap_metric) + + +def test_register_metric_rejects_duplicate_output_keys(): + duplicate = CounterPairMetric( + prefix="eg", + failure_event=metrics.register_event_type("DUP_FAIL"), + success_event=metrics.register_event_type("DUP_SUCCESS"), + rate_field="dup_success_rate", + ) + with pytest.raises(ValueError, match="already registered"): + metrics.register_metric(duplicate) From 3945fb42754587acd72a07054b4544cf306e5b88 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Fri, 19 Jun 2026 16:43:36 -0500 Subject: [PATCH 028/118] Metric registry to make public facing interface for adding new metrics easier --- sequence/utils/metrics/registry.py | 55 ++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 sequence/utils/metrics/registry.py diff --git a/sequence/utils/metrics/registry.py b/sequence/utils/metrics/registry.py new file mode 100644 index 000000000..644ce884a --- /dev/null +++ b/sequence/utils/metrics/registry.py @@ -0,0 +1,55 @@ +"""Metric registry and lookup helpers.""" + +from __future__ import annotations + +from .metric_types import CounterPairMetric, Metric + +_metrics: list[Metric] = [] +_counter_pairs: dict[str, CounterPairMetric] = {} + + +def register_metric(metric: Metric) -> None: + """Register a metric. Raises if output keys collide with existing metrics.""" + new_keys = metric.output_keys + for existing in _metrics: + overlap = existing.output_keys & new_keys + if overlap: + raise ValueError( + f"Metric output keys {sorted(overlap)} already registered." + ) + _metrics.append(metric) + if isinstance(metric, CounterPairMetric): + _counter_pairs[metric.prefix] = metric + + +def unregister_metric(metric: Metric) -> None: + """Remove a metric from the registry.""" + if metric in _metrics: + _metrics.remove(metric) + if isinstance(metric, CounterPairMetric): + _counter_pairs.pop(metric.prefix, None) + + +def list_metrics() -> list[Metric]: + """Return registered metrics in registration order.""" + return list(_metrics) + + +def get_counter_pair(prefix: str) -> CounterPairMetric: + """Return a registered counter-pair metric by prefix.""" + try: + return _counter_pairs[prefix] + except KeyError as exc: + raise KeyError(f"No CounterPairMetric registered with prefix '{prefix}'.") from exc + + +def reset_metrics() -> None: + """Reset per-trial state for all registered metrics.""" + for metric in _metrics: + metric.reset() + + +def clear_registry() -> None: + """Remove all metrics from the registry (for tests).""" + _metrics.clear() + _counter_pairs.clear() From bdb9676e370703d733e06f9255358c7f53ee82bc Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Fri, 19 Jun 2026 16:43:55 -0500 Subject: [PATCH 029/118] Public interface now in __init__ of package --- sequence/utils/metrics.py | 309 -------------------------------------- 1 file changed, 309 deletions(-) delete mode 100644 sequence/utils/metrics.py diff --git a/sequence/utils/metrics.py b/sequence/utils/metrics.py deleted file mode 100644 index 6b9772a63..000000000 --- a/sequence/utils/metrics.py +++ /dev/null @@ -1,309 +0,0 @@ -"""Centralized metrics tracking for SeQUeNCe simulations. - -This module provides a global registry for recording simulation events. -Metrics are disabled by default; call ``enable()`` to opt in to recording. -""" - -from __future__ import annotations - -import json -import math -import time -from enum import Enum, auto -from statistics import mean, stdev -from time import time_ns -from typing import Any, Protocol - - -class EventType(Enum): - """Event types that can be recorded by the metrics module.""" - - EG_FAILURE = auto() - EG_SUCCESS = auto() - THROUGHPUT = auto() - EP_FAILURE = auto() - EP_SUCCESS = auto() - PURIFIED_DELIVERY = auto() - - -EG_FAILURE = EventType.EG_FAILURE -EG_SUCCESS = EventType.EG_SUCCESS -THROUGHPUT = EventType.THROUGHPUT -EP_FAILURE = EventType.EP_FAILURE -EP_SUCCESS = EventType.EP_SUCCESS -PURIFIED_DELIVERY = EventType.PURIFIED_DELIVERY - - -class TimeProvider(Protocol): - """Protocol for objects that supply a timestamp for recorded events.""" - - def now(self) -> int: ... - - -class SystemTimeProvider: - """Fallback time source using the current system clock.""" - - def now(self) -> int: - return time_ns() - - -class InMemoryStorage: - """Store metric records in memory for the lifetime of the simulation.""" - - def __init__(self) -> None: - self._records: list[dict[str, Any]] = [] - - def append(self, record: dict[str, Any]) -> None: - self._records.append(record) - - def get_all(self) -> list[dict[str, Any]]: - return list(self._records) - - def get_by_event(self, event_type: EventType) -> list[dict[str, Any]]: - return [ - record for record in self._records if record["event_type"] is event_type - ] - - def get_by_owner(self, owner_name: str) -> list[dict[str, Any]]: - return [ - record for record in self._records if record["owner_name"] == owner_name - ] - - def clear(self) -> None: - self._records.clear() - - -_enabled = False -_enabled_events: set[EventType] = set() -storage = InMemoryStorage() -_system_time_provider = SystemTimeProvider() -time_provider: TimeProvider = _system_time_provider - -_eg_failures: dict[str, int] = {} -_eg_successes: dict[str, int] = {} -_ep_failures: dict[str, int] = {} -_ep_successes: dict[str, int] = {} - - -def register_time_provider(provider: TimeProvider) -> None: - """Register the active time source for recorded events.""" - global time_provider - time_provider = provider - - -def reset_counters() -> None: - """Reset per-node attempt and success counters.""" - _eg_failures.clear() - _eg_successes.clear() - _ep_failures.clear() - _ep_successes.clear() - - -def get_eg_failures(owner_name: str) -> int: - return _eg_failures.get(owner_name, 0) - - -def get_eg_successes(owner_name: str) -> int: - return _eg_successes.get(owner_name, 0) - - -def get_eg_success_rate(owner_name: str) -> float: - failures = get_eg_failures(owner_name) - successes = get_eg_successes(owner_name) - attempts = failures + successes - if attempts == 0: - return 0.0 - return successes / attempts - - -def get_ep_failures(owner_name: str) -> int: - return _ep_failures.get(owner_name, 0) - - -def get_ep_successes(owner_name: str) -> int: - return _ep_successes.get(owner_name, 0) - - -def get_ep_success_rate(owner_name: str) -> float: - failures = get_ep_failures(owner_name) - successes = get_ep_successes(owner_name) - attempts = failures + successes - if attempts == 0: - return 0.0 - return successes / attempts - - -def enable(event_types: list[EventType]) -> None: - """Enable metrics recording for the given event types. - - Available metrics types are in the metrics.EventType enum. - """ - global _enabled, _enabled_events - _enabled = True - _enabled_events = set(event_types) - - -def configure(storage_type: str = "in_memory") -> None: - """Configure metrics storage - - Available storage options: - - "in_memory": The default, uses `InMemoryStorage`. Keeps all records - available stored as a dictionary object in memory. - """ - global storage - if storage_type == "in_memory": - storage = InMemoryStorage() - - -def record(event_type: EventType, owner_name: str, **kwargs: Any) -> None: - """Record a metrics event if metrics are enabled for this event type. - - Will increment counter if counter is available for metric. - Metrics with counters: - - EG_SUCCESS, EG_FAILURE - - EP_SUCCESS, EP_FAILURE - - Will automatically record success rate for: - - EG_SUCCESS, EG_FAILURE (as ``success_rate``) - - EP_SUCCESS, EP_FAILURE (as ``ep_success_rate``) - """ - if not _enabled or event_type not in _enabled_events: - return - - if event_type is EG_FAILURE: - _eg_failures[owner_name] = _eg_failures.get(owner_name, 0) + 1 - kwargs["success_rate"] = get_eg_success_rate(owner_name) - elif event_type is EG_SUCCESS: - _eg_successes[owner_name] = _eg_successes.get(owner_name, 0) + 1 - kwargs["success_rate"] = get_eg_success_rate(owner_name) - elif event_type is EP_FAILURE: - _ep_failures[owner_name] = _ep_failures.get(owner_name, 0) + 1 - kwargs["ep_success_rate"] = get_ep_success_rate(owner_name) - elif event_type is EP_SUCCESS: - _ep_successes[owner_name] = _ep_successes.get(owner_name, 0) + 1 - kwargs["ep_success_rate"] = get_ep_success_rate(owner_name) - - storage.append( - { - "event_type": event_type, - "owner_name": owner_name, - "sim_time": time_provider.now(), - **kwargs, - } - ) - - -def _get_throughput(owner_name: str) -> float: - throughput_records = [ - record - for record in storage.get_by_owner(owner_name) - if record["event_type"] is THROUGHPUT - ] - if not throughput_records: - return float("nan") - return throughput_records[-1]["throughput"] - - -def _get_purified_fidelities(owner_name: str) -> list[float]: - return [ - record["fidelity"] - for record in storage.get_by_owner(owner_name) - if record["event_type"] is EP_SUCCESS and "fidelity" in record - ] - - -def _get_app_ep_time( - delivery_owner: str, - target_pairs: int, - reservation_start_time: int | None, -) -> float: - delivery_records = [ - record - for record in storage.get_by_owner(delivery_owner) - if record["event_type"] is PURIFIED_DELIVERY - ] - delivery_records.sort(key=lambda record: record["sim_time"]) - if len(delivery_records) < target_pairs: - return float("nan") - if reservation_start_time is None: - return float("nan") - target_time = delivery_records[target_pairs - 1]["sim_time"] - return (target_time - reservation_start_time) * 1e-12 - - -def collect_trial_metrics( - owner_name: str, - *, - delivery_owner: str | None = None, - target_pairs: int = 500, - reservation_start_time: int | None = None, -) -> dict[str, Any]: - """Collect per-trial metrics for a node from the metrics module.""" - delivery_owner = delivery_owner or owner_name - result = { - "eg_failures": get_eg_failures(owner_name), - "eg_success": get_eg_successes(owner_name), - "eg_success_rate": get_eg_success_rate(owner_name), - "ep_failures": get_ep_failures(owner_name), - "ep_success": get_ep_successes(owner_name), - "ep_success_rate": get_ep_success_rate(owner_name), - "purified_fidelities": _get_purified_fidelities(owner_name), - "app_throughput": _get_throughput(owner_name), - "app_ep_time": _get_app_ep_time( - delivery_owner, target_pairs, reservation_start_time - ), - "event_records": storage.get_by_owner(owner_name), - } - return result - - -def aggregate_trial_metrics( - trials: list[dict[str, Any]], - *, - list_metric_cap: int | None = 500, -) -> dict[str, float]: - """Aggregate trial metrics across multiple trials.""" - if not trials: - raise ValueError("Cannot aggregate an empty list of trials") - - aggregated: dict[str, float] = {} - scalar_metrics = [ - key for key, value in trials[0].items() if not isinstance(value, (list, dict)) - ] - list_metrics = [key for key, value in trials[0].items() if isinstance(value, list)] - - for metric in scalar_metrics: - values = [trial[metric] for trial in trials] - finite_values = [ - value - for value in values - if isinstance(value, (int, float)) and math.isfinite(value) - ] - if finite_values: - aggregated[f"avg_{metric}"] = mean(finite_values) - aggregated[f"std_{metric}"] = ( - stdev(finite_values) if len(finite_values) > 1 else 0.0 - ) - else: - aggregated[f"avg_{metric}"] = float("nan") - aggregated[f"std_{metric}"] = float("nan") - - for metric in list_metrics: - if metric == "event_records": - continue - all_values: list[float] = [] - for trial in trials: - trial_values = trial[metric] - if list_metric_cap is not None: - trial_values = trial_values[:list_metric_cap] - all_values.extend(trial_values) - if all_values: - aggregated[f"avg_{metric}"] = mean(all_values) - aggregated[f"std_{metric}"] = ( - stdev(all_values) if len(all_values) > 1 else 0.0 - ) - else: - aggregated[f"avg_{metric}"] = float("nan") - aggregated[f"std_{metric}"] = float("nan") - - return aggregated From d52bf433a188b0b66d0ca1941504f357394f5dc1 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Fri, 19 Jun 2026 16:44:12 -0500 Subject: [PATCH 030/118] Update tests for new metrics features --- tests/utils/test_metrics.py | 56 ++++++++++++++++++++----------------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/tests/utils/test_metrics.py b/tests/utils/test_metrics.py index f9ab0ccb4..5595e660f 100644 --- a/tests/utils/test_metrics.py +++ b/tests/utils/test_metrics.py @@ -6,18 +6,18 @@ from sequence.kernel.timeline import Timeline from sequence.topology.node import QuantumRouter from sequence.utils import metrics +from sequence.utils.metrics import CounterPairMetric @pytest.fixture(autouse=True) -def reset_metrics(): +def reset_metrics_state(): metrics._enabled = False metrics._enabled_events.clear() metrics.storage.clear() - metrics.reset_counters() + metrics.reset_metrics() metrics.register_time_provider(metrics._system_time_provider) -# Tests that record doesn't do anything if the user has not enabled metrics def test_record_before_enable_is_noop(): metrics.record(metrics.EG_SUCCESS, "e0", fidelity=0.9) assert metrics.storage.get_all() == [] @@ -57,21 +57,23 @@ def test_configure_replaces_storage(): assert metrics.storage.get_all() == [] -def test_reset_counters_clears_per_node_counts(): +def test_reset_metrics_clears_per_node_counts(): metrics.enable([metrics.EG_FAILURE, metrics.EG_SUCCESS, metrics.EP_FAILURE, metrics.EP_SUCCESS]) metrics.record(metrics.EG_FAILURE, "e0") metrics.record(metrics.EG_SUCCESS, "e0") metrics.record(metrics.EP_FAILURE, "e0") metrics.record(metrics.EP_SUCCESS, "e0") - metrics.reset_counters() + metrics.reset_metrics() - assert metrics.get_eg_failures("e0") == 0 - assert metrics.get_eg_successes("e0") == 0 - assert metrics.get_eg_success_rate("e0") == 0.0 - assert metrics.get_ep_failures("e0") == 0 - assert metrics.get_ep_successes("e0") == 0 - assert metrics.get_ep_success_rate("e0") == 0.0 + eg = metrics.get_counter_pair("eg") + ep = metrics.get_counter_pair("ep") + assert eg.failures("e0") == 0 + assert eg.successes("e0") == 0 + assert eg.success_rate("e0") == 0.0 + assert ep.failures("e0") == 0 + assert ep.successes("e0") == 0 + assert ep.success_rate("e0") == 0.0 def test_per_node_counters_are_independent(): @@ -82,12 +84,13 @@ def test_per_node_counters_are_independent(): metrics.record(metrics.EG_SUCCESS, "e0") metrics.record(metrics.EG_FAILURE, "e1") - assert metrics.get_eg_failures("e0") == 2 - assert metrics.get_eg_successes("e0") == 1 - assert metrics.get_eg_success_rate("e0") == pytest.approx(1 / 3) - assert metrics.get_eg_failures("e1") == 1 - assert metrics.get_eg_successes("e1") == 0 - assert metrics.get_eg_success_rate("e1") == 0.0 + eg = metrics.get_counter_pair("eg") + assert eg.failures("e0") == 2 + assert eg.successes("e0") == 1 + assert eg.success_rate("e0") == pytest.approx(1 / 3) + assert eg.failures("e1") == 1 + assert eg.successes("e1") == 0 + assert eg.success_rate("e1") == 0.0 def test_completion_events_record_running_success_rate(): @@ -147,9 +150,10 @@ def test_throughput_does_not_affect_eg_counters(): metrics.record(metrics.EG_SUCCESS, "e0") metrics.record(metrics.THROUGHPUT, "e0", throughput=42.0) - assert metrics.get_eg_failures("e0") == 1 - assert metrics.get_eg_successes("e0") == 1 - assert metrics.get_eg_success_rate("e0") == 0.5 + eg = metrics.get_counter_pair("eg") + assert eg.failures("e0") == 1 + assert eg.successes("e0") == 1 + assert eg.success_rate("e0") == 0.5 def test_collect_trial_metrics_returns_node_snapshot(): @@ -281,9 +285,10 @@ def test_ep_counters_and_success_rate(): metrics.record(metrics.EP_SUCCESS, "left", fidelity=0.75) metrics.record(metrics.EP_FAILURE, "left") - assert metrics.get_ep_failures("left") == 2 - assert metrics.get_ep_successes("left") == 1 - assert metrics.get_ep_success_rate("left") == pytest.approx(1 / 3) + ep = metrics.get_counter_pair("ep") + assert ep.failures("left") == 2 + assert ep.successes("left") == 1 + assert ep.success_rate("left") == pytest.approx(1 / 3) success_records = metrics.storage.get_by_event(metrics.EP_SUCCESS) assert success_records[0]["ep_success_rate"] == pytest.approx(0.5) @@ -295,8 +300,9 @@ def test_purified_delivery_does_not_affect_ep_counters(): metrics.record(metrics.EP_SUCCESS, "left", fidelity=0.8) metrics.record(metrics.PURIFIED_DELIVERY, "right", fidelity=0.8, pair_number=1) - assert metrics.get_ep_successes("left") == 1 - assert metrics.get_ep_failures("right") == 0 + ep = metrics.get_counter_pair("ep") + assert ep.successes("left") == 1 + assert ep.failures("right") == 0 def test_collect_trial_metrics_ep_fields_and_delivery_time(): From 414707fc8e791c0b7d5a0a6e6e8a19d86fdfd7bd Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Mon, 22 Jun 2026 11:24:46 -0500 Subject: [PATCH 031/118] Removed EventRecordsMetric --- sequence/utils/metrics/__init__.py | 11 ++++--- sequence/utils/metrics/builtins.py | 3 -- sequence/utils/metrics/metric_types.py | 18 ----------- tests/utils/test_metrics.py | 45 +++++++++++++++----------- 4 files changed, 32 insertions(+), 45 deletions(-) diff --git a/sequence/utils/metrics/__init__.py b/sequence/utils/metrics/__init__.py index 0c95711e5..5ca31a2ee 100644 --- a/sequence/utils/metrics/__init__.py +++ b/sequence/utils/metrics/__init__.py @@ -11,13 +11,17 @@ from typing import Any from . import builtins -from .event_types import EventType, get_event_type, list_event_types, register_event_type +from .event_types import ( + EventType, + get_event_type, + list_event_types, + register_event_type, +) from .metric_types import ( AppEpTimeMetric, CollectContext, CounterPairMetric, EventFieldListMetric, - EventRecordsMetric, LastValueMetric, Metric, ) @@ -146,8 +150,6 @@ def aggregate_trial_metrics( aggregated[f"std_{metric}"] = float("nan") for metric in list_metrics_keys: - if metric == "event_records": - continue all_values: list[float] = [] for trial in trials: trial_values = trial[metric] @@ -178,7 +180,6 @@ def aggregate_trial_metrics( "EP_SUCCESS", "EventFieldListMetric", "EventType", - "EventRecordsMetric", "InMemoryStorage", "LastValueMetric", "Metric", diff --git a/sequence/utils/metrics/builtins.py b/sequence/utils/metrics/builtins.py index 9e8f44520..1cbcbda3b 100644 --- a/sequence/utils/metrics/builtins.py +++ b/sequence/utils/metrics/builtins.py @@ -7,7 +7,6 @@ AppEpTimeMetric, CounterPairMetric, EventFieldListMetric, - EventRecordsMetric, LastValueMetric, ) from .registry import register_metric @@ -45,7 +44,6 @@ key="app_ep_time", delivery_event=PURIFIED_DELIVERY, ) -EVENT_RECORDS_METRIC = EventRecordsMetric() _BUILTIN_METRICS = ( EG_METRIC, @@ -53,7 +51,6 @@ THROUGHPUT_METRIC, PURIFIED_FIDELITIES_METRIC, APP_EP_TIME_METRIC, - EVENT_RECORDS_METRIC, ) diff --git a/sequence/utils/metrics/metric_types.py b/sequence/utils/metrics/metric_types.py index 9b38e7755..5c56fa2b1 100644 --- a/sequence/utils/metrics/metric_types.py +++ b/sequence/utils/metrics/metric_types.py @@ -166,24 +166,6 @@ def collect(self, ctx: CollectContext) -> dict[str, Any]: return {self.key: values} -@dataclass -class EventRecordsMetric(Metric): - """Collects raw event records for an owner.""" - - key: str = "event_records" - - @property - def event_types(self) -> frozenset[EventType]: - return frozenset() - - @property - def output_keys(self) -> frozenset[str]: - return frozenset({self.key}) - - def collect(self, ctx: CollectContext) -> dict[str, Any]: - return {self.key: ctx.storage.get_by_owner(ctx.owner_name)} - - @dataclass class AppEpTimeMetric(Metric): """Time to deliver N purified pairs relative to reservation start.""" diff --git a/tests/utils/test_metrics.py b/tests/utils/test_metrics.py index 5595e660f..babe231b4 100644 --- a/tests/utils/test_metrics.py +++ b/tests/utils/test_metrics.py @@ -58,7 +58,9 @@ def test_configure_replaces_storage(): def test_reset_metrics_clears_per_node_counts(): - metrics.enable([metrics.EG_FAILURE, metrics.EG_SUCCESS, metrics.EP_FAILURE, metrics.EP_SUCCESS]) + metrics.enable( + [metrics.EG_FAILURE, metrics.EG_SUCCESS, metrics.EP_FAILURE, metrics.EP_SUCCESS] + ) metrics.record(metrics.EG_FAILURE, "e0") metrics.record(metrics.EG_SUCCESS, "e0") metrics.record(metrics.EP_FAILURE, "e0") @@ -169,7 +171,6 @@ def test_collect_trial_metrics_returns_node_snapshot(): assert trial["eg_success"] == 1 assert trial["eg_success_rate"] == 0.5 assert trial["app_throughput"] == 12.5 - assert len(trial["event_records"]) == 3 def test_collect_trial_metrics_without_throughput_is_nan(): @@ -183,8 +184,18 @@ def test_collect_trial_metrics_without_throughput_is_nan(): def test_aggregate_trial_metrics_computes_avg_and_std(): trials = [ - {"eg_failures": 10, "eg_success": 5, "eg_success_rate": 0.5, "app_throughput": 1.0}, - {"eg_failures": 12, "eg_success": 6, "eg_success_rate": 0.5, "app_throughput": 2.0}, + { + "eg_failures": 10, + "eg_success": 5, + "eg_success_rate": 0.5, + "app_throughput": 1.0, + }, + { + "eg_failures": 12, + "eg_success": 6, + "eg_success_rate": 0.5, + "app_throughput": 2.0, + }, ] aggregated = metrics.aggregate_trial_metrics(trials) @@ -196,28 +207,19 @@ def test_aggregate_trial_metrics_computes_avg_and_std(): def test_aggregate_trial_metrics_single_trial_has_zero_std(): - trials = [{"eg_failures": 3, "eg_success": 1, "eg_success_rate": 0.25, "app_throughput": 4.0}] - - aggregated = metrics.aggregate_trial_metrics(trials) - - assert aggregated["avg_eg_failures"] == 3 - assert aggregated["std_eg_failures"] == 0.0 - - -def test_aggregate_trial_metrics_skips_event_records(): trials = [ { - "eg_failures": 1, + "eg_failures": 3, "eg_success": 1, - "eg_success_rate": 0.5, - "app_throughput": 1.0, - "event_records": [{"event_type": metrics.EG_SUCCESS}], + "eg_success_rate": 0.25, + "app_throughput": 4.0, } ] aggregated = metrics.aggregate_trial_metrics(trials) - assert "avg_event_records" not in aggregated + assert aggregated["avg_eg_failures"] == 3 + assert aggregated["std_eg_failures"] == 0.0 def test_aggregate_trial_metrics_empty_list_raises(): @@ -264,7 +266,12 @@ def test_request_app_schedules_throughput_on_responder_only(): RequestApp(initiator) reservation = Reservation( - "left", "right", int(1e12), int(2e12), 2, 0.9, + "left", + "right", + int(1e12), + int(2e12), + 2, + 0.9, ) metrics.enable([metrics.THROUGHPUT]) responder_app.memory_counter = 4 From 928fc1932ad137a708c01e041ac3374327740ea9 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Mon, 22 Jun 2026 11:40:44 -0500 Subject: [PATCH 032/118] Moved entanglement fail to superclass --- .../entanglement_management/generation/generation_base.py | 3 ++- .../entanglement_management/generation/single_heralded.py | 4 ---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/sequence/entanglement_management/generation/generation_base.py b/sequence/entanglement_management/generation/generation_base.py index 4771b8487..2ba529e02 100644 --- a/sequence/entanglement_management/generation/generation_base.py +++ b/sequence/entanglement_management/generation/generation_base.py @@ -12,7 +12,7 @@ from ..entanglement_protocol import EntanglementProtocol from ...components.circuit import Circuit -from ...utils import log +from ...utils import log, metrics class QuantumCircuitMixin: @@ -180,6 +180,7 @@ def _entanglement_succeed(self): def _entanglement_fail(self): for event in self.scheduled_events: self.owner.timeline.remove_event(event) + metrics.record(metrics.EG_FAILURE, self.owner.name, initial_fidelity=self.fidelity) log.logger.info(f'{self.owner.name} failed entanglement of memory {self.memory}') self.update_resource_manager(self.memory, MemoryInfo.RAW) diff --git a/sequence/entanglement_management/generation/single_heralded.py b/sequence/entanglement_management/generation/single_heralded.py index 65630e59f..56b80e5a2 100644 --- a/sequence/entanglement_management/generation/single_heralded.py +++ b/sequence/entanglement_management/generation/single_heralded.py @@ -226,10 +226,6 @@ def received_message(self, src: str, msg: EntanglementGenerationMessage) -> None else: raise Exception(f"Invalid message {msg_type} received by EG on node {self.owner.name}") - def _entanglement_fail(self): - metrics.record(metrics.EG_FAILURE, self.owner.name, initial_fidelity=self.raw_fidelity) - super()._entanglement_fail() - def _entanglement_succeed(self): log.logger.info(f'{self.owner.name} successful entanglement of memory {self.memory}') metrics.record(metrics.EG_SUCCESS, self.owner.name, fidelity=self.raw_fidelity) From ae7588626e483a9fe9007185c4788193a5f8afbf Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Mon, 22 Jun 2026 11:53:43 -0500 Subject: [PATCH 033/118] Support for BarretKok --- sequence/entanglement_management/generation/barret_kok.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sequence/entanglement_management/generation/barret_kok.py b/sequence/entanglement_management/generation/barret_kok.py index e9e4e3879..22b7210dc 100644 --- a/sequence/entanglement_management/generation/barret_kok.py +++ b/sequence/entanglement_management/generation/barret_kok.py @@ -13,7 +13,7 @@ from ...kernel.event import Event from ...kernel.process import Process -from ...utils import log +from ...utils import log, metrics @EntanglementGenerationA.register(BARRET_KOK) @@ -225,6 +225,7 @@ def _entanglement_succeed(self): self.memory.entangled_memory['node_id'] = self.remote_node_name self.memory.entangled_memory['memo_id'] = self.remote_memo_id self.memory.fidelity = self.memory.raw_fidelity + metrics.record(metrics.EG_SUCCESS, self.owner.name, fidelity=self.fidelity) self.update_resource_manager(self.memory, MemoryInfo.ENTANGLED) From cbd5b0010677fddf3973b7525b92e2e258846895 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Mon, 22 Jun 2026 14:58:30 -0500 Subject: [PATCH 034/118] Revert changes to request_app --- sequence/app/request_app.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/sequence/app/request_app.py b/sequence/app/request_app.py index 457894515..e8d0bc04d 100644 --- a/sequence/app/request_app.py +++ b/sequence/app/request_app.py @@ -167,22 +167,6 @@ def schedule_reservation(self, reservation: Reservation) -> None: event = Event(reservation.end_time, process) self.node.timeline.schedule(event) - if self.node.name == reservation.responder: - self.start_t = reservation.start_time - self.end_t = reservation.end_time - process = Process(self, "record_throughput_metric", []) - event = Event(reservation.end_time, process) - self.node.timeline.schedule(event) - - def record_throughput_metric(self) -> None: - from ..utils import metrics - - metrics.record( - metrics.THROUGHPUT, - self.node.name, - throughput=self.get_throughput(), - ) - def set_name(self, name: str): self.name = name From 84e5a18f9f8503bfb03c07c215747bbb4fdb2b1a Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Mon, 22 Jun 2026 15:04:27 -0500 Subject: [PATCH 035/118] Removed __all__ --- sequence/utils/metrics/__init__.py | 36 ------------------------------ 1 file changed, 36 deletions(-) diff --git a/sequence/utils/metrics/__init__.py b/sequence/utils/metrics/__init__.py index 5ca31a2ee..104ececc0 100644 --- a/sequence/utils/metrics/__init__.py +++ b/sequence/utils/metrics/__init__.py @@ -169,39 +169,3 @@ def aggregate_trial_metrics( builtins.register_builtin_metrics() - -__all__ = [ - "AppEpTimeMetric", - "CollectContext", - "CounterPairMetric", - "EG_FAILURE", - "EG_SUCCESS", - "EP_FAILURE", - "EP_SUCCESS", - "EventFieldListMetric", - "EventType", - "InMemoryStorage", - "LastValueMetric", - "Metric", - "PURIFIED_DELIVERY", - "SystemTimeProvider", - "THROUGHPUT", - "TimeProvider", - "aggregate_trial_metrics", - "clear_registry", - "collect_trial_metrics", - "configure", - "enable", - "get_counter_pair", - "get_event_type", - "list_event_types", - "list_metrics", - "record", - "register_event_type", - "register_metric", - "register_time_provider", - "reset_metrics", - "storage", - "time_provider", - "unregister_metric", -] From 3fab5bc7449c11b55897db74549588cd93f56cee Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Mon, 22 Jun 2026 15:04:42 -0500 Subject: [PATCH 036/118] Renamed app ep time to delivery time --- sequence/utils/metrics/__init__.py | 2 +- sequence/utils/metrics/builtins.py | 8 ++++---- sequence/utils/metrics/metric_types.py | 4 ++-- tests/utils/test_metrics.py | 24 ++++++++++++------------ 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/sequence/utils/metrics/__init__.py b/sequence/utils/metrics/__init__.py index 104ececc0..22b8e4092 100644 --- a/sequence/utils/metrics/__init__.py +++ b/sequence/utils/metrics/__init__.py @@ -18,7 +18,7 @@ register_event_type, ) from .metric_types import ( - AppEpTimeMetric, + DeliveryTimeMetric, CollectContext, CounterPairMetric, EventFieldListMetric, diff --git a/sequence/utils/metrics/builtins.py b/sequence/utils/metrics/builtins.py index 1cbcbda3b..18bd75242 100644 --- a/sequence/utils/metrics/builtins.py +++ b/sequence/utils/metrics/builtins.py @@ -4,7 +4,7 @@ from .event_types import register_event_type from .metric_types import ( - AppEpTimeMetric, + DeliveryTimeMetric, CounterPairMetric, EventFieldListMetric, LastValueMetric, @@ -40,8 +40,8 @@ event=EP_SUCCESS, field="fidelity", ) -APP_EP_TIME_METRIC = AppEpTimeMetric( - key="app_ep_time", +DELIVERY_TIME_METRIC = DeliveryTimeMetric( + key="delivery_time", delivery_event=PURIFIED_DELIVERY, ) @@ -50,7 +50,7 @@ EP_METRIC, THROUGHPUT_METRIC, PURIFIED_FIDELITIES_METRIC, - APP_EP_TIME_METRIC, + DELIVERY_TIME_METRIC, ) diff --git a/sequence/utils/metrics/metric_types.py b/sequence/utils/metrics/metric_types.py index 5c56fa2b1..3ee72a3d8 100644 --- a/sequence/utils/metrics/metric_types.py +++ b/sequence/utils/metrics/metric_types.py @@ -167,10 +167,10 @@ def collect(self, ctx: CollectContext) -> dict[str, Any]: @dataclass -class AppEpTimeMetric(Metric): +class DeliveryTimeMetric(Metric): """Time to deliver N purified pairs relative to reservation start.""" - key: str = "app_ep_time" + key: str = "delivery_time" delivery_event: EventType | None = None @property diff --git a/tests/utils/test_metrics.py b/tests/utils/test_metrics.py index babe231b4..42d6b6a3c 100644 --- a/tests/utils/test_metrics.py +++ b/tests/utils/test_metrics.py @@ -346,10 +346,10 @@ def now(self) -> int: assert trial["ep_success"] == 2 assert trial["purified_fidelities"] == [0.7, 0.75] - assert trial["app_ep_time"] == pytest.approx(0.4) + assert trial["delivery_time"] == pytest.approx(0.4) -def test_collect_trial_metrics_app_ep_time_nan_when_target_not_reached(): +def test_collect_trial_metrics_delivery_time_nan_when_target_not_reached(): metrics.enable([metrics.PURIFIED_DELIVERY]) metrics.record(metrics.PURIFIED_DELIVERY, "right", fidelity=0.9, pair_number=1) @@ -360,7 +360,7 @@ def test_collect_trial_metrics_app_ep_time_nan_when_target_not_reached(): reservation_start_time=int(1e12), ) - assert math.isnan(trial["app_ep_time"]) + assert math.isnan(trial["delivery_time"]) def test_collect_trial_metrics_delivery_owner_defaults_to_owner(): @@ -373,7 +373,7 @@ def test_collect_trial_metrics_delivery_owner_defaults_to_owner(): reservation_start_time=0, ) - assert not math.isnan(trial["app_ep_time"]) + assert not math.isnan(trial["delivery_time"]) def test_aggregate_trial_metrics_flattens_purified_fidelities(): @@ -381,12 +381,12 @@ def test_aggregate_trial_metrics_flattens_purified_fidelities(): { "ep_success_rate": 0.6, "purified_fidelities": [0.7, 0.75], - "app_ep_time": 10.0, + "delivery_time": 10.0, }, { "ep_success_rate": 0.7, "purified_fidelities": [0.8], - "app_ep_time": 9.0, + "delivery_time": 9.0, }, ] @@ -394,19 +394,19 @@ def test_aggregate_trial_metrics_flattens_purified_fidelities(): assert aggregated["avg_purified_fidelities"] == pytest.approx(0.75) assert aggregated["std_purified_fidelities"] == pytest.approx(0.05) - assert aggregated["avg_app_ep_time"] == 9.5 + assert aggregated["avg_delivery_time"] == 9.5 -def test_aggregate_trial_metrics_handles_nan_app_ep_time(): +def test_aggregate_trial_metrics_handles_nan_delivery_time(): trials = [ - {"app_ep_time": float("nan"), "purified_fidelities": [0.7]}, - {"app_ep_time": 12.0, "purified_fidelities": [0.8]}, + {"delivery_time": float("nan"), "purified_fidelities": [0.7]}, + {"delivery_time": 12.0, "purified_fidelities": [0.8]}, ] aggregated = metrics.aggregate_trial_metrics(trials) - assert aggregated["avg_app_ep_time"] == 12.0 - assert aggregated["std_app_ep_time"] == 0.0 + assert aggregated["avg_delivery_time"] == 12.0 + assert aggregated["std_delivery_time"] == 0.0 def test_register_event_type_is_idempotent(): From b841a60e2bc665a60dddcdbb63604c3c3ae14407 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Mon, 22 Jun 2026 15:16:21 -0500 Subject: [PATCH 037/118] Remove throughput related tests --- tests/utils/test_metrics.py | 44 ------------------------------------- 1 file changed, 44 deletions(-) diff --git a/tests/utils/test_metrics.py b/tests/utils/test_metrics.py index 42d6b6a3c..d807bafda 100644 --- a/tests/utils/test_metrics.py +++ b/tests/utils/test_metrics.py @@ -241,50 +241,6 @@ def test_aggregate_trial_metrics_ignores_non_finite_values(): assert aggregated["std_app_throughput"] == pytest.approx(1.4142135623730951) -def test_request_app_record_throughput_metric(): - tl = Timeline() - node = QuantumRouter("router_0", tl) - app = RequestApp(node) - app.start_t = int(1e12) - app.end_t = int(2e12) - app.memory_counter = 10 - - metrics.enable([metrics.THROUGHPUT]) - app.record_throughput_metric() - - trial = metrics.collect_trial_metrics("router_0") - assert trial["app_throughput"] == pytest.approx(10.0) - - -def test_request_app_schedules_throughput_on_responder_only(): - from sequence.network_management.reservation import Reservation - - tl = Timeline(stop_time=int(3e12)) - responder = QuantumRouter("right", tl) - initiator = QuantumRouter("left", tl) - responder_app = RequestApp(responder) - RequestApp(initiator) - - reservation = Reservation( - "left", - "right", - int(1e12), - int(2e12), - 2, - 0.9, - ) - metrics.enable([metrics.THROUGHPUT]) - responder_app.memory_counter = 4 - responder_app.schedule_reservation(reservation) - - tl.init() - tl.run() - - trial = metrics.collect_trial_metrics("right") - assert trial["app_throughput"] == pytest.approx(4.0) - assert math.isnan(metrics.collect_trial_metrics("left")["app_throughput"]) - - def test_ep_counters_and_success_rate(): metrics.enable([metrics.EP_FAILURE, metrics.EP_SUCCESS]) From 297bd9e10ad4c22b0c5b38a2137eb1675d8d0b1d Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Mon, 22 Jun 2026 15:17:02 -0500 Subject: [PATCH 038/118] Added TODO comment to DeliveryTimeMetric --- sequence/utils/metrics/metric_types.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/sequence/utils/metrics/metric_types.py b/sequence/utils/metrics/metric_types.py index 3ee72a3d8..50b8c4699 100644 --- a/sequence/utils/metrics/metric_types.py +++ b/sequence/utils/metrics/metric_types.py @@ -4,7 +4,7 @@ from abc import ABC, abstractmethod from dataclasses import dataclass, field -from typing import Any +from typing import Any, override from .event_types import EventType from .storage import InMemoryStorage @@ -183,6 +183,11 @@ def event_types(self) -> frozenset[EventType]: def output_keys(self) -> frozenset[str]: return frozenset({self.key}) + # TODO: Implement delivery time metric using better methods to collect data + # CollectContext was previously used - however this implementation is not + # very good and needs to be reworked + # We need to get reservation_start_time and target_pairs from somewhere else + @override def collect(self, ctx: CollectContext) -> dict[str, Any]: delivery_owner = ctx.delivery_owner or ctx.owner_name delivery_records = [ From 9f311871e6f89c6249279e6deb878ffc16562267 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Mon, 22 Jun 2026 15:41:43 -0500 Subject: [PATCH 039/118] Potential fix for pull request finding 'Unused import' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- tests/utils/test_metrics.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/utils/test_metrics.py b/tests/utils/test_metrics.py index d807bafda..adabdbc7c 100644 --- a/tests/utils/test_metrics.py +++ b/tests/utils/test_metrics.py @@ -2,7 +2,6 @@ import pytest -from sequence.app.request_app import RequestApp from sequence.kernel.timeline import Timeline from sequence.topology.node import QuantumRouter from sequence.utils import metrics From 864e297c82db15dca1d65f71ee5f55838d08803a Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Mon, 22 Jun 2026 15:42:51 -0500 Subject: [PATCH 040/118] More unused imports --- tests/utils/test_metrics.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/utils/test_metrics.py b/tests/utils/test_metrics.py index adabdbc7c..cf6b1948b 100644 --- a/tests/utils/test_metrics.py +++ b/tests/utils/test_metrics.py @@ -2,8 +2,6 @@ import pytest -from sequence.kernel.timeline import Timeline -from sequence.topology.node import QuantumRouter from sequence.utils import metrics from sequence.utils.metrics import CounterPairMetric From 6ef03a6ab361d73ed70cbe337f9184505025b8c4 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Wed, 24 Jun 2026 16:19:47 -0500 Subject: [PATCH 041/118] Added Swapping equivalents of failures, successes, fidelities metrics --- sequence/utils/metrics/__init__.py | 2 ++ sequence/utils/metrics/builtins.py | 15 +++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/sequence/utils/metrics/__init__.py b/sequence/utils/metrics/__init__.py index 22b8e4092..574173ba4 100644 --- a/sequence/utils/metrics/__init__.py +++ b/sequence/utils/metrics/__init__.py @@ -42,6 +42,8 @@ EP_FAILURE = builtins.EP_FAILURE EP_SUCCESS = builtins.EP_SUCCESS PURIFIED_DELIVERY = builtins.PURIFIED_DELIVERY +ES_FAILURE = builtins.ES_FAILURE +ES_SUCCESS = builtins.ES_SUCCESS _enabled = False _enabled_events: set[EventType] = set() diff --git a/sequence/utils/metrics/builtins.py b/sequence/utils/metrics/builtins.py index 18bd75242..7609c098c 100644 --- a/sequence/utils/metrics/builtins.py +++ b/sequence/utils/metrics/builtins.py @@ -17,6 +17,8 @@ EP_FAILURE = register_event_type("EP_FAILURE") EP_SUCCESS = register_event_type("EP_SUCCESS") PURIFIED_DELIVERY = register_event_type("PURIFIED_DELIVERY") +ES_FAILURE = register_event_type("ES_FAILURE") +ES_SUCCESS = register_event_type("ES_SUCCESS") EG_METRIC = CounterPairMetric( prefix="eg", @@ -44,6 +46,17 @@ key="delivery_time", delivery_event=PURIFIED_DELIVERY, ) +ES_METRIC = CounterPairMetric( + prefix="es", + failure_event=ES_FAILURE, + success_event=ES_SUCCESS, + rate_field="es_success_rate", +) +SWAPPED_FIDELITIES_METRIC = EventFieldListMetric( + key="swapped_fidelities", + event=ES_SUCCESS, + field="fidelity", +) _BUILTIN_METRICS = ( EG_METRIC, @@ -51,6 +64,8 @@ THROUGHPUT_METRIC, PURIFIED_FIDELITIES_METRIC, DELIVERY_TIME_METRIC, + ES_METRIC, + SWAPPED_FIDELITIES_METRIC, ) From b11ffb731bcb5fb115f6c235f5670c218a5178c3 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Wed, 24 Jun 2026 16:20:26 -0500 Subject: [PATCH 042/118] Equivalent test functions for new swapping metrics --- tests/utils/test_metrics.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/utils/test_metrics.py b/tests/utils/test_metrics.py index cf6b1948b..70f2eac32 100644 --- a/tests/utils/test_metrics.py +++ b/tests/utils/test_metrics.py @@ -254,6 +254,33 @@ def test_ep_counters_and_success_rate(): assert success_records[0]["ep_success_rate"] == pytest.approx(0.5) +def test_es_counters_and_success_rate(): + metrics.enable([metrics.ES_FAILURE, metrics.ES_SUCCESS]) + + metrics.record(metrics.ES_FAILURE, "middle") + metrics.record(metrics.ES_SUCCESS, "middle", fidelity=0.75) + metrics.record(metrics.ES_FAILURE, "middle") + + es = metrics.get_counter_pair("es") + assert es.failures("middle") == 2 + assert es.successes("middle") == 1 + assert es.success_rate("middle") == pytest.approx(1 / 3) + + success_records = metrics.storage.get_by_event(metrics.ES_SUCCESS) + assert success_records[0]["es_success_rate"] == pytest.approx(0.5) + + +def test_collect_trial_metrics_swapped_fidelities(): + metrics.enable([metrics.ES_SUCCESS]) + + metrics.record(metrics.ES_SUCCESS, "middle", fidelity=0.7) + metrics.record(metrics.ES_SUCCESS, "middle", fidelity=0.75) + + trial = metrics.collect_trial_metrics("middle") + assert trial["es_success"] == 2 + assert trial["swapped_fidelities"] == [0.7, 0.75] + + def test_purified_delivery_does_not_affect_ep_counters(): metrics.enable([metrics.EP_FAILURE, metrics.EP_SUCCESS, metrics.PURIFIED_DELIVERY]) From 2c2bebbe89077555c9be0f06e77443d4e80bc41f Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Wed, 24 Jun 2026 16:21:03 -0500 Subject: [PATCH 043/118] Finish metrics recording for purification --- .../purification/bbpssw_circuit.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/sequence/entanglement_management/purification/bbpssw_circuit.py b/sequence/entanglement_management/purification/bbpssw_circuit.py index c51bb3213..120f3661f 100644 --- a/sequence/entanglement_management/purification/bbpssw_circuit.py +++ b/sequence/entanglement_management/purification/bbpssw_circuit.py @@ -14,7 +14,7 @@ from ...components.memory import Memory from ...topology.node import Node -from ...utils import log +from ...utils import log, metrics from ...constants import KET_VECTOR_FORMALISM, DENSITY_MATRIX_FORMALISM from .bbpssw_protocol import BBPSSWProtocol, BBPSSWMessage, BBPSSWMsgType @@ -117,8 +117,14 @@ def received_message(self, src: str, msg: BBPSSWMessage) -> None: self.update_resource_manager(self.meas_memo, "RAW") if self.meas_res == msg.meas_res: self.kept_memo.fidelity = self.improved_fidelity(self.kept_memo.fidelity) + metrics.record( + metrics.EP_SUCCESS, + self.owner.name, + fidelity=self.kept_memo.fidelity, + ) self.update_resource_manager(self.kept_memo, state="PURIFIED") else: + metrics.record(metrics.EP_FAILURE, self.owner.name) self.update_resource_manager(self.kept_memo, state="RAW") @staticmethod From 22b1b28ebc2aed19924feb18846a75322ffd0a00 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Wed, 24 Jun 2026 16:21:23 -0500 Subject: [PATCH 044/118] Implemented metrics recording for swapping --- sequence/entanglement_management/swapping/swapping_bds.py | 4 +++- sequence/entanglement_management/swapping/swapping_circuit.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/sequence/entanglement_management/swapping/swapping_bds.py b/sequence/entanglement_management/swapping/swapping_bds.py index b285ae54e..ac00ffb49 100644 --- a/sequence/entanglement_management/swapping/swapping_bds.py +++ b/sequence/entanglement_management/swapping/swapping_bds.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING from .swapping_base import EntanglementSwappingA, EntanglementSwappingB, SwappingMsgType, EntanglementSwappingMessage -from ...utils import log +from ...utils import log, metrics from ...constants import BELL_DIAGONAL_STATE_FORMALISM from ...resource_management.memory_manager import MemoryInfo @@ -88,6 +88,7 @@ def start(self) -> None: # get BDS conditioned on success, fidelity is the first diagonal element new_bds = self.swapping_res() fidelity = new_bds[0] + metrics.record(metrics.ES_SUCCESS, self.owner.name, fidelity=fidelity) keys = [left_remote_memory.qstate_key, right_remote_memory.qstate_key] self.owner.timeline.quantum_manager.set(keys, new_bds) @@ -107,6 +108,7 @@ def start(self) -> None: meas_res=[]) else: log.logger.info(f'swapping failed!') + metrics.record(metrics.ES_FAILURE, self.owner.name) msg_l = EntanglementSwappingMessage(SwappingMsgType.SWAP_RES, self.left_protocol_name, fidelity=0) msg_r = EntanglementSwappingMessage(SwappingMsgType.SWAP_RES, self.right_protocol_name, fidelity=0) diff --git a/sequence/entanglement_management/swapping/swapping_circuit.py b/sequence/entanglement_management/swapping/swapping_circuit.py index 018655a5c..c41f349f3 100644 --- a/sequence/entanglement_management/swapping/swapping_circuit.py +++ b/sequence/entanglement_management/swapping/swapping_circuit.py @@ -13,7 +13,7 @@ from ...components.circuit import Circuit from ...constants import KET_VECTOR_FORMALISM, DENSITY_MATRIX_FORMALISM from ...resource_management.memory_manager import MemoryInfo -from ...utils import log +from ...utils import log, metrics @EntanglementSwappingA.register(KET_VECTOR_FORMALISM) @@ -84,6 +84,7 @@ def start(self) -> None: if self.owner.get_generator().random() < self.success_probability(): # swapping succeeded fidelity = self.updated_fidelity(self.left_memo.fidelity, self.right_memo.fidelity) + metrics.record(metrics.ES_SUCCESS, self.owner.name, fidelity=fidelity) self.is_success = True expire_time = min(self.left_memo.get_expire_time(), self.right_memo.get_expire_time()) @@ -108,6 +109,7 @@ def start(self) -> None: else: # swapping failed log.logger.info(f"{self.name} swapping failed") + metrics.record(metrics.ES_FAILURE, self.owner.name) msg_l = EntanglementSwappingMessage(SwappingMsgType.SWAP_RES,self.left_protocol_name, fidelity=0) msg_r = EntanglementSwappingMessage(SwappingMsgType.SWAP_RES, self.right_protocol_name, fidelity=0) From 79c3916822769520ae0a11871700daafac83178e Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Thu, 25 Jun 2026 14:48:23 -0500 Subject: [PATCH 045/118] Improved way of dealing with reservation_start_time --- sequence/utils/metrics/__init__.py | 2 -- sequence/utils/metrics/metric_types.py | 15 ++++++--------- tests/utils/test_metrics.py | 3 --- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/sequence/utils/metrics/__init__.py b/sequence/utils/metrics/__init__.py index 574173ba4..b3f0d6298 100644 --- a/sequence/utils/metrics/__init__.py +++ b/sequence/utils/metrics/__init__.py @@ -102,7 +102,6 @@ def collect_trial_metrics( *, delivery_owner: str | None = None, target_pairs: int = 500, - reservation_start_time: int | None = None, ) -> dict[str, Any]: """Collect per-trial metrics for a node from the metrics module.""" ctx = CollectContext( @@ -110,7 +109,6 @@ def collect_trial_metrics( storage=storage, delivery_owner=delivery_owner or owner_name, target_pairs=target_pairs, - reservation_start_time=reservation_start_time, ) result: dict[str, Any] = {} for metric in list_metrics(): diff --git a/sequence/utils/metrics/metric_types.py b/sequence/utils/metrics/metric_types.py index 50b8c4699..02251a90f 100644 --- a/sequence/utils/metrics/metric_types.py +++ b/sequence/utils/metrics/metric_types.py @@ -18,7 +18,6 @@ class CollectContext: storage: InMemoryStorage delivery_owner: str | None = None target_pairs: int = 500 - reservation_start_time: int | None = None class Metric(ABC): @@ -183,10 +182,6 @@ def event_types(self) -> frozenset[EventType]: def output_keys(self) -> frozenset[str]: return frozenset({self.key}) - # TODO: Implement delivery time metric using better methods to collect data - # CollectContext was previously used - however this implementation is not - # very good and needs to be reworked - # We need to get reservation_start_time and target_pairs from somewhere else @override def collect(self, ctx: CollectContext) -> dict[str, Any]: delivery_owner = ctx.delivery_owner or ctx.owner_name @@ -199,9 +194,11 @@ def collect(self, ctx: CollectContext) -> dict[str, Any]: delivery_records.sort(key=lambda record: record["sim_time"]) if len(delivery_records) < ctx.target_pairs: return {self.key: float("nan")} - if ctx.reservation_start_time is None: - return {self.key: float("nan")} - target_time = delivery_records[ctx.target_pairs - 1]["sim_time"] + start_time: float = float("nan") + target_time: float = float("nan") + if delivery_records: + start_time = delivery_records[0].get("start_time") + target_time = delivery_records[ctx.target_pairs - 1]["sim_time"] return { - self.key: (target_time - ctx.reservation_start_time) * 1e-12, + self.key: (target_time - start_time) * 1e-12, } diff --git a/tests/utils/test_metrics.py b/tests/utils/test_metrics.py index 70f2eac32..d97612fe6 100644 --- a/tests/utils/test_metrics.py +++ b/tests/utils/test_metrics.py @@ -321,7 +321,6 @@ def now(self) -> int: "left", delivery_owner="right", target_pairs=3, - reservation_start_time=int(1e12), ) assert trial["ep_success"] == 2 @@ -337,7 +336,6 @@ def test_collect_trial_metrics_delivery_time_nan_when_target_not_reached(): "left", delivery_owner="right", target_pairs=500, - reservation_start_time=int(1e12), ) assert math.isnan(trial["delivery_time"]) @@ -350,7 +348,6 @@ def test_collect_trial_metrics_delivery_owner_defaults_to_owner(): trial = metrics.collect_trial_metrics( "right", target_pairs=1, - reservation_start_time=0, ) assert not math.isnan(trial["delivery_time"]) From da2f52c6c65f6fc7b2867b92b07470229a9ed33b Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Thu, 25 Jun 2026 14:50:58 -0500 Subject: [PATCH 046/118] New ReservationDeliveryMetric metric type --- sequence/utils/metrics/metric_types.py | 40 ++++++++++++++++++++++++++ sequence/utils/metrics/registry.py | 19 +++++++++++- 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/sequence/utils/metrics/metric_types.py b/sequence/utils/metrics/metric_types.py index 02251a90f..3d9a8df10 100644 --- a/sequence/utils/metrics/metric_types.py +++ b/sequence/utils/metrics/metric_types.py @@ -165,6 +165,46 @@ def collect(self, ctx: CollectContext) -> dict[str, Any]: return {self.key: values} +@dataclass +class ReservationDeliveryMetric(Metric): + """Tracks per-reservation pair delivery counts and assigns pair_index on record.""" + + delivery_event: EventType + _pair_counters: dict[tuple[str, int], int] = field( + default_factory=dict, init=False, repr=False + ) + + @property + def event_types(self) -> frozenset[EventType]: + return frozenset({self.delivery_event}) + + @property + def output_keys(self) -> frozenset[str]: + return frozenset() + + def served_pairs(self, owner_name: str, identity: int) -> int: + return self._pair_counters.get((owner_name, identity), 0) + + def on_record( + self, + event_type: EventType, + owner_name: str, + kwargs: dict[str, Any], + ) -> None: + if event_type is not self.delivery_event: + return + identity = kwargs.get("identity", 0) + key = (owner_name, identity) + self._pair_counters[key] = self._pair_counters.get(key, 0) + 1 + kwargs["pair_index"] = self._pair_counters[key] + + def collect(self, ctx: CollectContext) -> dict[str, Any]: + return {} + + def reset(self) -> None: + self._pair_counters.clear() + + @dataclass class DeliveryTimeMetric(Metric): """Time to deliver N purified pairs relative to reservation start.""" diff --git a/sequence/utils/metrics/registry.py b/sequence/utils/metrics/registry.py index 644ce884a..7861b7fd7 100644 --- a/sequence/utils/metrics/registry.py +++ b/sequence/utils/metrics/registry.py @@ -2,10 +2,11 @@ from __future__ import annotations -from .metric_types import CounterPairMetric, Metric +from .metric_types import CounterPairMetric, Metric, ReservationDeliveryMetric _metrics: list[Metric] = [] _counter_pairs: dict[str, CounterPairMetric] = {} +_reservation_delivery_metric: ReservationDeliveryMetric | None = None def register_metric(metric: Metric) -> None: @@ -20,6 +21,9 @@ def register_metric(metric: Metric) -> None: _metrics.append(metric) if isinstance(metric, CounterPairMetric): _counter_pairs[metric.prefix] = metric + if isinstance(metric, ReservationDeliveryMetric): + global _reservation_delivery_metric + _reservation_delivery_metric = metric def unregister_metric(metric: Metric) -> None: @@ -28,6 +32,10 @@ def unregister_metric(metric: Metric) -> None: _metrics.remove(metric) if isinstance(metric, CounterPairMetric): _counter_pairs.pop(metric.prefix, None) + if isinstance(metric, ReservationDeliveryMetric): + global _reservation_delivery_metric + if _reservation_delivery_metric is metric: + _reservation_delivery_metric = None def list_metrics() -> list[Metric]: @@ -43,6 +51,13 @@ def get_counter_pair(prefix: str) -> CounterPairMetric: raise KeyError(f"No CounterPairMetric registered with prefix '{prefix}'.") from exc +def get_reservation_delivery_metric() -> ReservationDeliveryMetric: + """Return the registered reservation delivery metric.""" + if _reservation_delivery_metric is None: + raise KeyError("No ReservationDeliveryMetric registered.") + return _reservation_delivery_metric + + def reset_metrics() -> None: """Reset per-trial state for all registered metrics.""" for metric in _metrics: @@ -51,5 +66,7 @@ def reset_metrics() -> None: def clear_registry() -> None: """Remove all metrics from the registry (for tests).""" + global _reservation_delivery_metric _metrics.clear() _counter_pairs.clear() + _reservation_delivery_metric = None From e12055bab4daace06ef463c2aaeac25fa3bf9809 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Thu, 25 Jun 2026 14:52:53 -0500 Subject: [PATCH 047/118] Added reservation APPROVED, REJECTED, and COMPLETED metrics --- sequence/utils/metrics/__init__.py | 5 +++++ sequence/utils/metrics/builtins.py | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/sequence/utils/metrics/__init__.py b/sequence/utils/metrics/__init__.py index b3f0d6298..9c3c65731 100644 --- a/sequence/utils/metrics/__init__.py +++ b/sequence/utils/metrics/__init__.py @@ -24,10 +24,12 @@ EventFieldListMetric, LastValueMetric, Metric, + ReservationDeliveryMetric, ) from .registry import ( clear_registry, get_counter_pair, + get_reservation_delivery_metric, list_metrics, register_metric, reset_metrics, @@ -44,6 +46,9 @@ PURIFIED_DELIVERY = builtins.PURIFIED_DELIVERY ES_FAILURE = builtins.ES_FAILURE ES_SUCCESS = builtins.ES_SUCCESS +RESERVATION_APPROVED = builtins.RESERVATION_APPROVED +RESERVATION_REJECTED = builtins.RESERVATION_REJECTED +RESERVATION_COMPLETE = builtins.RESERVATION_COMPLETE _enabled = False _enabled_events: set[EventType] = set() diff --git a/sequence/utils/metrics/builtins.py b/sequence/utils/metrics/builtins.py index 7609c098c..0bf825f48 100644 --- a/sequence/utils/metrics/builtins.py +++ b/sequence/utils/metrics/builtins.py @@ -8,6 +8,7 @@ CounterPairMetric, EventFieldListMetric, LastValueMetric, + ReservationDeliveryMetric, ) from .registry import register_metric @@ -19,6 +20,9 @@ PURIFIED_DELIVERY = register_event_type("PURIFIED_DELIVERY") ES_FAILURE = register_event_type("ES_FAILURE") ES_SUCCESS = register_event_type("ES_SUCCESS") +RESERVATION_APPROVED = register_event_type("RESERVATION_APPROVED") +RESERVATION_REJECTED = register_event_type("RESERVATION_REJECTED") +RESERVATION_COMPLETE = register_event_type("RESERVATION_COMPLETE") EG_METRIC = CounterPairMetric( prefix="eg", @@ -57,6 +61,9 @@ event=ES_SUCCESS, field="fidelity", ) +RESERVATION_DELIVERY_METRIC = ReservationDeliveryMetric( + delivery_event=PURIFIED_DELIVERY, +) _BUILTIN_METRICS = ( EG_METRIC, @@ -66,6 +73,7 @@ DELIVERY_TIME_METRIC, ES_METRIC, SWAPPED_FIDELITIES_METRIC, + RESERVATION_DELIVERY_METRIC, ) From f3f85fbba88b53e2ddf12c2e967fc63f4be82615 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Thu, 25 Jun 2026 14:53:30 -0500 Subject: [PATCH 048/118] Reservation approved and rejected metrics in network management --- .../network_management/network_manager.py | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/sequence/network_management/network_manager.py b/sequence/network_management/network_manager.py index 9c5d38c2d..7da0b076f 100644 --- a/sequence/network_management/network_manager.py +++ b/sequence/network_management/network_manager.py @@ -14,7 +14,7 @@ from ..components.memory import MemoryArray from ..message import Message -from ..utils import log +from ..utils import log, metrics from .forwarding import ForwardingProtocol from .memory_timecard import MemoryTimeCard from .reservation import Reservation @@ -226,12 +226,38 @@ def pop(self, msg: RSVPMessage): """ reservation: Reservation = msg.reservation if msg.msg_type == RSVPMsgType.APPROVE: + metrics.record( + metrics.RESERVATION_APPROVED, + self.owner.name, + identity=reservation.identity, + initiator=reservation.initiator, + responder=reservation.responder, + start_time=reservation.start_time, + end_time=reservation.end_time, + memory_size=reservation.memory_size, + entanglement_number=reservation.entanglement_number, + target_fidelity=reservation.fidelity, + path=list(reservation.path), + ) self.generate_rules(reservation) if reservation.initiator == self.owner.name: self.owner.get_reservation_result(reservation, True) # Deliver the result to the Node elif reservation.responder == self.owner.name: self.owner.get_other_reservation(reservation) elif msg.msg_type == RSVPMsgType.REJECT: + metrics.record( + metrics.RESERVATION_REJECTED, + self.owner.name, + identity=reservation.identity, + initiator=reservation.initiator, + responder=reservation.responder, + start_time=reservation.start_time, + end_time=reservation.end_time, + memory_size=reservation.memory_size, + entanglement_number=reservation.entanglement_number, + target_fidelity=reservation.fidelity, + path=[], + ) if reservation.initiator == self.owner.name: self.owner.get_reservation_result(reservation, False) From 41ef5377589149cb5511e35f72bef9ee61f0b28f Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Thu, 25 Jun 2026 14:55:19 -0500 Subject: [PATCH 049/118] A built-in reservation data collection function --- sequence/utils/metrics/__init__.py | 63 ++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/sequence/utils/metrics/__init__.py b/sequence/utils/metrics/__init__.py index 9c3c65731..67cc49e27 100644 --- a/sequence/utils/metrics/__init__.py +++ b/sequence/utils/metrics/__init__.py @@ -121,6 +121,69 @@ def collect_trial_metrics( return result +def collect_reservation_data(owner_name: str | None = None) -> list[list]: + """Collect per-reservation tabular metrics from recorded delivery events.""" + from collections import defaultdict + + records = storage.get_by_event(PURIFIED_DELIVERY) + if owner_name is not None: + records = [record for record in records if record["owner_name"] == owner_name] + + groups: dict[tuple[str, int], list[dict[str, Any]]] = defaultdict(list) + for record in records: + groups[(record["owner_name"], record["identity"])].append(record) + + data: list[list] = [] + for (node, _), deliveries in groups.items(): + if not deliveries: + continue + deliveries.sort(key=lambda record: record["sim_time"]) + timestamps = [record["sim_time"] for record in deliveries] + fidelities = [record["fidelity"] for record in deliveries] + + first = deliveries[0] + start_time = first["start_time"] + end_time = first["end_time"] + reserved_time = end_time - start_time + served_pairs = len(deliveries) + throughput = served_pairs / reserved_time * 1e12 if reserved_time > 0 else 0.0 + completion_time = timestamps[-1] + entanglement_number = first["entanglement_number"] + fulfilled = served_pairs == entanglement_number + path = first.get("path", []) + path_length = len(path) + first_pair = timestamps[0] + avg_fidelity = mean(fidelities) if fidelities else 0.0 + std_fidelity = stdev(fidelities) if len(fidelities) > 1 else 0.0 + durations = [n - c for c, n in zip(timestamps, timestamps[1:])] + avg_duration = mean(durations) if durations else 0.0 + std_duration = stdev(durations) if len(durations) > 1 else 0.0 + + data.append( + [ + node, + first["identity"], + first["initiator"], + first["responder"], + start_time, + end_time, + reserved_time, + entanglement_number, + served_pairs, + throughput, + completion_time, + fulfilled, + path_length, + first_pair, + avg_fidelity, + std_fidelity, + avg_duration, + std_duration, + ] + ) + return data + + def aggregate_trial_metrics( trials: list[dict[str, Any]], *, From 2dd7286eff6de81aa21426260f087ce113ec393d Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Thu, 25 Jun 2026 14:57:11 -0500 Subject: [PATCH 050/118] Collection reservation data test method Looks like it works properly similar to the central scheduler experiment --- tests/utils/test_metrics.py | 48 +++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/tests/utils/test_metrics.py b/tests/utils/test_metrics.py index d97612fe6..5fa39762e 100644 --- a/tests/utils/test_metrics.py +++ b/tests/utils/test_metrics.py @@ -424,3 +424,51 @@ def test_register_metric_rejects_duplicate_output_keys(): ) with pytest.raises(ValueError, match="already registered"): metrics.register_metric(duplicate) + + +def test_collect_reservation_data_produces_expected_row(): + metrics.enable([metrics.PURIFIED_DELIVERY]) + start_time = int(1e12) + end_time = int(2e12) + base = { + "identity": 1, + "initiator": "n1", + "responder": "n2", + "start_time": start_time, + "end_time": end_time, + "memory_size": 5, + "entanglement_number": 2, + "target_fidelity": 0.9, + "path": ["n1", "m1", "n2"], + } + + class StubTimeProvider: + def __init__(self) -> None: + self._time = start_time + int(1e11) + + def now(self) -> int: + current = self._time + self._time += int(2e11) + return current + + metrics.register_time_provider(StubTimeProvider()) + metrics.record(metrics.PURIFIED_DELIVERY, "n1", fidelity=0.91, **base) + metrics.record(metrics.PURIFIED_DELIVERY, "n1", fidelity=0.92, **base) + + rows = metrics.collect_reservation_data("n1") + assert len(rows) == 1 + row = rows[0] + assert row[0] == "n1" + assert row[1] == 1 + assert row[2] == "n1" + assert row[3] == "n2" + assert row[4] == start_time + assert row[5] == end_time + assert row[6] == end_time - start_time + assert row[7] == 2 + assert row[8] == 2 + assert row[9] == pytest.approx(2 / (end_time - start_time) * 1e12) + assert row[11] is True + assert row[12] == 3 + assert row[14] == pytest.approx(0.915) + assert row[16] == pytest.approx(int(2e11)) From 0a5bff7137406c467bd069712b875b4c6c6fc783 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Thu, 25 Jun 2026 15:05:57 -0500 Subject: [PATCH 051/118] Fixed start time not being recorded --- tests/utils/test_metrics.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/utils/test_metrics.py b/tests/utils/test_metrics.py index 5fa39762e..d5c409f5e 100644 --- a/tests/utils/test_metrics.py +++ b/tests/utils/test_metrics.py @@ -315,6 +315,7 @@ def now(self) -> int: "right", fidelity=0.7 + pair_number * 0.01, pair_number=pair_number, + start_time=int(1e12), ) trial = metrics.collect_trial_metrics( @@ -343,7 +344,13 @@ def test_collect_trial_metrics_delivery_time_nan_when_target_not_reached(): def test_collect_trial_metrics_delivery_owner_defaults_to_owner(): metrics.enable([metrics.PURIFIED_DELIVERY]) - metrics.record(metrics.PURIFIED_DELIVERY, "right", fidelity=0.9, pair_number=1) + metrics.record( + metrics.PURIFIED_DELIVERY, + "right", + fidelity=0.9, + pair_number=1, + start_time=0, + ) trial = metrics.collect_trial_metrics( "right", From 8e27162059e141a51144db82bd5fb01d9d6530d0 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Thu, 25 Jun 2026 15:06:47 -0500 Subject: [PATCH 052/118] Reservation APPROVED and REJECTED tests --- tests/utils/test_metrics.py | 41 +++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/tests/utils/test_metrics.py b/tests/utils/test_metrics.py index d5c409f5e..e569ec7f2 100644 --- a/tests/utils/test_metrics.py +++ b/tests/utils/test_metrics.py @@ -433,6 +433,47 @@ def test_register_metric_rejects_duplicate_output_keys(): metrics.register_metric(duplicate) +def test_reservation_approved_records_metadata(): + metrics.enable([metrics.RESERVATION_APPROVED]) + metrics.record( + metrics.RESERVATION_APPROVED, + "n1", + identity=1, + initiator="n1", + responder="n2", + start_time=10, + end_time=20, + memory_size=5, + entanglement_number=3, + target_fidelity=0.9, + path=["n1", "m1", "n2"], + ) + record = metrics.storage.get_all()[0] + assert record["identity"] == 1 + assert record["path"] == ["n1", "m1", "n2"] + assert record["entanglement_number"] == 3 + + +def test_reservation_rejected_records_metadata(): + metrics.enable([metrics.RESERVATION_REJECTED]) + metrics.record( + metrics.RESERVATION_REJECTED, + "n1", + identity=2, + initiator="n1", + responder="n2", + start_time=10, + end_time=20, + memory_size=5, + entanglement_number=1, + target_fidelity=0.9, + path=[], + ) + record = metrics.storage.get_all()[0] + assert record["event_type"] is metrics.RESERVATION_REJECTED + assert record["path"] == [] + + def test_collect_reservation_data_produces_expected_row(): metrics.enable([metrics.PURIFIED_DELIVERY]) start_time = int(1e12) From fcf0bc850716d7893444e9a0255776ebb18f99d1 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Thu, 25 Jun 2026 15:07:29 -0500 Subject: [PATCH 053/118] New Purified Delivery test method for pair index --- tests/utils/test_metrics.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/utils/test_metrics.py b/tests/utils/test_metrics.py index e569ec7f2..09d5481f2 100644 --- a/tests/utils/test_metrics.py +++ b/tests/utils/test_metrics.py @@ -474,6 +474,31 @@ def test_reservation_rejected_records_metadata(): assert record["path"] == [] +def test_purified_delivery_assigns_pair_index(): + metrics.enable([metrics.PURIFIED_DELIVERY]) + kwargs = { + "identity": 7, + "initiator": "n1", + "responder": "n2", + "start_time": 0, + "end_time": int(1e12), + "memory_size": 5, + "entanglement_number": 2, + "target_fidelity": 0.9, + "path": ["n1", "n2"], + "fidelity": 0.91, + } + metrics.record(metrics.PURIFIED_DELIVERY, "n1", **kwargs) + metrics.record(metrics.PURIFIED_DELIVERY, "n1", **{**kwargs, "fidelity": 0.92}) + metrics.record(metrics.PURIFIED_DELIVERY, "n2", **{**kwargs, "fidelity": 0.93}) + + n1_records = metrics.storage.get_by_owner("n1") + n2_records = metrics.storage.get_by_owner("n2") + assert n1_records[0]["pair_index"] == 1 + assert n1_records[1]["pair_index"] == 2 + assert n2_records[0]["pair_index"] == 1 + + def test_collect_reservation_data_produces_expected_row(): metrics.enable([metrics.PURIFIED_DELIVERY]) start_time = int(1e12) From ea6e172e0d5172df2120bb708e7c55d5d0fd63a0 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Mon, 29 Jun 2026 15:31:11 -0500 Subject: [PATCH 054/118] Changed ruff formatter line length to 120 --- pyproject.toml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 2a4aea396..883b5c315 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,3 +76,10 @@ Homepage = "https://github.com/sequence-toolbox/SeQUeNCe" Documentation = "https://sequence-rtd-tutorial.readthedocs.io/" Issues = "https://github.com/sequence-toolbox/SeQUeNCe/issues" Changelog = "https://github.com/sequence-toolbox/SeQUeNCe/blob/master/CHANGELOG.md" + +[tool.ruff] +line-length = 120 + +[tool.ruff.lint.pycodestyle] +max-line-length = 120 + From 25a87a8aef8df8746071942c7a22fc4131d8690b Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Mon, 29 Jun 2026 15:31:32 -0500 Subject: [PATCH 055/118] Reformatted metrics files with line length 120 --- sequence/utils/metrics/__init__.py | 22 +++++----------------- sequence/utils/metrics/metric_types.py | 9 ++------- sequence/utils/metrics/registry.py | 4 +--- sequence/utils/metrics/storage.py | 8 ++------ 4 files changed, 10 insertions(+), 33 deletions(-) diff --git a/sequence/utils/metrics/__init__.py b/sequence/utils/metrics/__init__.py index 574173ba4..03029218d 100644 --- a/sequence/utils/metrics/__init__.py +++ b/sequence/utils/metrics/__init__.py @@ -128,25 +128,15 @@ def aggregate_trial_metrics( raise ValueError("Cannot aggregate an empty list of trials") aggregated: dict[str, float] = {} - scalar_metrics = [ - key for key, value in trials[0].items() if not isinstance(value, (list, dict)) - ] - list_metrics_keys = [ - key for key, value in trials[0].items() if isinstance(value, list) - ] + scalar_metrics = [key for key, value in trials[0].items() if not isinstance(value, (list, dict))] + list_metrics_keys = [key for key, value in trials[0].items() if isinstance(value, list)] for metric in scalar_metrics: values = [trial[metric] for trial in trials] - finite_values = [ - value - for value in values - if isinstance(value, (int, float)) and math.isfinite(value) - ] + finite_values = [value for value in values if isinstance(value, (int, float)) and math.isfinite(value)] if finite_values: aggregated[f"avg_{metric}"] = mean(finite_values) - aggregated[f"std_{metric}"] = ( - stdev(finite_values) if len(finite_values) > 1 else 0.0 - ) + aggregated[f"std_{metric}"] = stdev(finite_values) if len(finite_values) > 1 else 0.0 else: aggregated[f"avg_{metric}"] = float("nan") aggregated[f"std_{metric}"] = float("nan") @@ -160,9 +150,7 @@ def aggregate_trial_metrics( all_values.extend(trial_values) if all_values: aggregated[f"avg_{metric}"] = mean(all_values) - aggregated[f"std_{metric}"] = ( - stdev(all_values) if len(all_values) > 1 else 0.0 - ) + aggregated[f"std_{metric}"] = stdev(all_values) if len(all_values) > 1 else 0.0 else: aggregated[f"avg_{metric}"] = float("nan") aggregated[f"std_{metric}"] = float("nan") diff --git a/sequence/utils/metrics/metric_types.py b/sequence/utils/metrics/metric_types.py index 50b8c4699..bec49b6f6 100644 --- a/sequence/utils/metrics/metric_types.py +++ b/sequence/utils/metrics/metric_types.py @@ -131,11 +131,7 @@ def output_keys(self) -> frozenset[str]: return frozenset({self.key}) def collect(self, ctx: CollectContext) -> dict[str, Any]: - records = [ - record - for record in ctx.storage.get_by_owner(ctx.owner_name) - if record["event_type"] is self.event - ] + records = [record for record in ctx.storage.get_by_owner(ctx.owner_name) if record["event_type"] is self.event] if not records: return {self.key: float("nan")} return {self.key: records[-1][self.field]} @@ -193,8 +189,7 @@ def collect(self, ctx: CollectContext) -> dict[str, Any]: delivery_records = [ record for record in ctx.storage.get_by_owner(delivery_owner) - if self.delivery_event is not None - and record["event_type"] is self.delivery_event + if self.delivery_event is not None and record["event_type"] is self.delivery_event ] delivery_records.sort(key=lambda record: record["sim_time"]) if len(delivery_records) < ctx.target_pairs: diff --git a/sequence/utils/metrics/registry.py b/sequence/utils/metrics/registry.py index 644ce884a..ccaaefc4b 100644 --- a/sequence/utils/metrics/registry.py +++ b/sequence/utils/metrics/registry.py @@ -14,9 +14,7 @@ def register_metric(metric: Metric) -> None: for existing in _metrics: overlap = existing.output_keys & new_keys if overlap: - raise ValueError( - f"Metric output keys {sorted(overlap)} already registered." - ) + raise ValueError(f"Metric output keys {sorted(overlap)} already registered.") _metrics.append(metric) if isinstance(metric, CounterPairMetric): _counter_pairs[metric.prefix] = metric diff --git a/sequence/utils/metrics/storage.py b/sequence/utils/metrics/storage.py index e4c8ed7d9..ea1d92846 100644 --- a/sequence/utils/metrics/storage.py +++ b/sequence/utils/metrics/storage.py @@ -34,14 +34,10 @@ def get_all(self) -> list[dict[str, Any]]: return list(self._records) def get_by_event(self, event_type: EventType) -> list[dict[str, Any]]: - return [ - record for record in self._records if record["event_type"] is event_type - ] + return [record for record in self._records if record["event_type"] is event_type] def get_by_owner(self, owner_name: str) -> list[dict[str, Any]]: - return [ - record for record in self._records if record["owner_name"] == owner_name - ] + return [record for record in self._records if record["owner_name"] == owner_name] def clear(self) -> None: self._records.clear() From ac36b98ec62658b5c24cece94ba697de4aa3e840 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Mon, 29 Jun 2026 15:39:26 -0500 Subject: [PATCH 056/118] Removed some trailing commas and reformatted so args fit on one line --- sequence/utils/metrics/builtins.py | 7 +------ sequence/utils/metrics/metric_types.py | 14 ++------------ 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/sequence/utils/metrics/builtins.py b/sequence/utils/metrics/builtins.py index 7609c098c..2ac5512d3 100644 --- a/sequence/utils/metrics/builtins.py +++ b/sequence/utils/metrics/builtins.py @@ -3,12 +3,7 @@ from __future__ import annotations from .event_types import register_event_type -from .metric_types import ( - DeliveryTimeMetric, - CounterPairMetric, - EventFieldListMetric, - LastValueMetric, -) +from .metric_types import DeliveryTimeMetric, CounterPairMetric, EventFieldListMetric, LastValueMetric from .registry import register_metric EG_FAILURE = register_event_type("EG_FAILURE") diff --git a/sequence/utils/metrics/metric_types.py b/sequence/utils/metrics/metric_types.py index bec49b6f6..4fe821b8b 100644 --- a/sequence/utils/metrics/metric_types.py +++ b/sequence/utils/metrics/metric_types.py @@ -34,12 +34,7 @@ def event_types(self) -> frozenset[EventType]: def output_keys(self) -> frozenset[str]: """Keys produced by collect().""" - def on_record( - self, - event_type: EventType, - owner_name: str, - kwargs: dict[str, Any], - ) -> None: + def on_record(self, event_type: EventType, owner_name: str, kwargs: dict[str, Any]) -> None: """Update metric state when a matching event is recorded.""" @abstractmethod @@ -89,12 +84,7 @@ def success_rate(self, owner_name: str) -> float: return 0.0 return successes / attempts - def on_record( - self, - event_type: EventType, - owner_name: str, - kwargs: dict[str, Any], - ) -> None: + def on_record(self, event_type: EventType, owner_name: str, kwargs: dict[str, Any]) -> None: if event_type is self.failure_event: self._failures[owner_name] = self._failures.get(owner_name, 0) + 1 kwargs[self.rate_field] = self.success_rate(owner_name) From f58c12f6d1fb46f8b295b99f15ebbad5eb17e48e Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Mon, 29 Jun 2026 16:40:16 -0500 Subject: [PATCH 057/118] Renamed most of the metrics to better names --- sequence/utils/metrics/__init__.py | 10 +++++----- sequence/utils/metrics/builtins.py | 20 ++++++++------------ sequence/utils/metrics/metric_types.py | 12 ++++++------ sequence/utils/metrics/registry.py | 22 +++++++++++----------- 4 files changed, 30 insertions(+), 34 deletions(-) diff --git a/sequence/utils/metrics/__init__.py b/sequence/utils/metrics/__init__.py index 03029218d..d4df10e32 100644 --- a/sequence/utils/metrics/__init__.py +++ b/sequence/utils/metrics/__init__.py @@ -18,16 +18,16 @@ register_event_type, ) from .metric_types import ( - DeliveryTimeMetric, CollectContext, - CounterPairMetric, - EventFieldListMetric, - LastValueMetric, + CounterMetric, + DeliveryTimeMetric, + FidelityMetric, Metric, + RateMetric, ) from .registry import ( clear_registry, - get_counter_pair, + get_counter, list_metrics, register_metric, reset_metrics, diff --git a/sequence/utils/metrics/builtins.py b/sequence/utils/metrics/builtins.py index 2ac5512d3..319232f95 100644 --- a/sequence/utils/metrics/builtins.py +++ b/sequence/utils/metrics/builtins.py @@ -2,10 +2,11 @@ from __future__ import annotations -from .event_types import register_event_type -from .metric_types import DeliveryTimeMetric, CounterPairMetric, EventFieldListMetric, LastValueMetric +from .event_types import EventTypes +from .metric_types import CounterMetric, DeliveryTimeMetric, FidelityMetric, RateMetric from .registry import register_metric +EG_METRIC = CounterMetric( EG_FAILURE = register_event_type("EG_FAILURE") EG_SUCCESS = register_event_type("EG_SUCCESS") THROUGHPUT = register_event_type("THROUGHPUT") @@ -15,24 +16,19 @@ ES_FAILURE = register_event_type("ES_FAILURE") ES_SUCCESS = register_event_type("ES_SUCCESS") -EG_METRIC = CounterPairMetric( prefix="eg", failure_event=EG_FAILURE, success_event=EG_SUCCESS, rate_field="success_rate", ) -EP_METRIC = CounterPairMetric( +EP_METRIC = CounterMetric( prefix="ep", failure_event=EP_FAILURE, success_event=EP_SUCCESS, rate_field="ep_success_rate", ) -THROUGHPUT_METRIC = LastValueMetric( - key="app_throughput", - event=THROUGHPUT, - field="throughput", -) -PURIFIED_FIDELITIES_METRIC = EventFieldListMetric( +THROUGHPUT_METRIC = RateMetric(key="app_throughput") +PURIFIED_FIDELITIES_METRIC = FidelityMetric( key="purified_fidelities", event=EP_SUCCESS, field="fidelity", @@ -41,13 +37,13 @@ key="delivery_time", delivery_event=PURIFIED_DELIVERY, ) -ES_METRIC = CounterPairMetric( +ES_METRIC = CounterMetric( prefix="es", failure_event=ES_FAILURE, success_event=ES_SUCCESS, rate_field="es_success_rate", ) -SWAPPED_FIDELITIES_METRIC = EventFieldListMetric( +SWAPPED_FIDELITIES_METRIC = FidelityMetric( key="swapped_fidelities", event=ES_SUCCESS, field="fidelity", diff --git a/sequence/utils/metrics/metric_types.py b/sequence/utils/metrics/metric_types.py index 4fe821b8b..d4877a54a 100644 --- a/sequence/utils/metrics/metric_types.py +++ b/sequence/utils/metrics/metric_types.py @@ -46,7 +46,7 @@ def reset(self) -> None: @dataclass -class CounterPairMetric(Metric): +class CounterMetric(Metric): """Tracks failure/success counts and a running success rate.""" prefix: str @@ -105,8 +105,8 @@ def reset(self) -> None: @dataclass -class LastValueMetric(Metric): - """Collects the last scalar field value from matching events.""" +class RateMetric(Metric): + """Collects a rate value supplied at trial collection time (e.g. throughput).""" key: str event: EventType @@ -128,8 +128,8 @@ def collect(self, ctx: CollectContext) -> dict[str, Any]: @dataclass -class EventFieldListMetric(Metric): - """Collects a list of field values from matching events.""" +class FidelityMetric(Metric): + """Collects fidelity values from matching success events.""" key: str event: EventType @@ -154,7 +154,7 @@ def collect(self, ctx: CollectContext) -> dict[str, Any]: @dataclass class DeliveryTimeMetric(Metric): - """Time to deliver N purified pairs relative to reservation start.""" + """Time to deliver N pairs relative to reservation start.""" key: str = "delivery_time" delivery_event: EventType | None = None diff --git a/sequence/utils/metrics/registry.py b/sequence/utils/metrics/registry.py index ccaaefc4b..8c351d13a 100644 --- a/sequence/utils/metrics/registry.py +++ b/sequence/utils/metrics/registry.py @@ -2,10 +2,10 @@ from __future__ import annotations -from .metric_types import CounterPairMetric, Metric +from .metric_types import CounterMetric, Metric _metrics: list[Metric] = [] -_counter_pairs: dict[str, CounterPairMetric] = {} +_counters: dict[str, CounterMetric] = {} def register_metric(metric: Metric) -> None: @@ -16,16 +16,16 @@ def register_metric(metric: Metric) -> None: if overlap: raise ValueError(f"Metric output keys {sorted(overlap)} already registered.") _metrics.append(metric) - if isinstance(metric, CounterPairMetric): - _counter_pairs[metric.prefix] = metric + if isinstance(metric, CounterMetric): + _counters[metric.prefix] = metric def unregister_metric(metric: Metric) -> None: """Remove a metric from the registry.""" if metric in _metrics: _metrics.remove(metric) - if isinstance(metric, CounterPairMetric): - _counter_pairs.pop(metric.prefix, None) + if isinstance(metric, CounterMetric): + _counters.pop(metric.prefix, None) def list_metrics() -> list[Metric]: @@ -33,12 +33,12 @@ def list_metrics() -> list[Metric]: return list(_metrics) -def get_counter_pair(prefix: str) -> CounterPairMetric: - """Return a registered counter-pair metric by prefix.""" +def get_counter(prefix: str) -> CounterMetric: + """Return a registered counter metric by prefix.""" try: - return _counter_pairs[prefix] + return _counters[prefix] except KeyError as exc: - raise KeyError(f"No CounterPairMetric registered with prefix '{prefix}'.") from exc + raise KeyError(f"No CounterMetric registered with prefix '{prefix}'.") from exc def reset_metrics() -> None: @@ -50,4 +50,4 @@ def reset_metrics() -> None: def clear_registry() -> None: """Remove all metrics from the registry (for tests).""" _metrics.clear() - _counter_pairs.clear() + _counters.clear() From 1a075a716e1e80bf622f33b14aeffdc1416efdcf Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Mon, 29 Jun 2026 16:41:08 -0500 Subject: [PATCH 058/118] Throughput is now collected only on collect_trial_metrics --- sequence/utils/metrics/__init__.py | 2 ++ sequence/utils/metrics/metric_types.py | 12 +++++------- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/sequence/utils/metrics/__init__.py b/sequence/utils/metrics/__init__.py index d4df10e32..5f30a0f57 100644 --- a/sequence/utils/metrics/__init__.py +++ b/sequence/utils/metrics/__init__.py @@ -103,6 +103,7 @@ def collect_trial_metrics( delivery_owner: str | None = None, target_pairs: int = 500, reservation_start_time: int | None = None, + throughput: float | None = None, ) -> dict[str, Any]: """Collect per-trial metrics for a node from the metrics module.""" ctx = CollectContext( @@ -111,6 +112,7 @@ def collect_trial_metrics( delivery_owner=delivery_owner or owner_name, target_pairs=target_pairs, reservation_start_time=reservation_start_time, + throughput=throughput, ) result: dict[str, Any] = {} for metric in list_metrics(): diff --git a/sequence/utils/metrics/metric_types.py b/sequence/utils/metrics/metric_types.py index d4877a54a..ad7a7db34 100644 --- a/sequence/utils/metrics/metric_types.py +++ b/sequence/utils/metrics/metric_types.py @@ -19,6 +19,7 @@ class CollectContext: delivery_owner: str | None = None target_pairs: int = 500 reservation_start_time: int | None = None + throughput: float | None = None class Metric(ABC): @@ -108,23 +109,20 @@ def reset(self) -> None: class RateMetric(Metric): """Collects a rate value supplied at trial collection time (e.g. throughput).""" - key: str - event: EventType - field: str + key: str = "app_throughput" @property def event_types(self) -> frozenset[EventType]: - return frozenset({self.event}) + return frozenset() @property def output_keys(self) -> frozenset[str]: return frozenset({self.key}) def collect(self, ctx: CollectContext) -> dict[str, Any]: - records = [record for record in ctx.storage.get_by_owner(ctx.owner_name) if record["event_type"] is self.event] - if not records: + if ctx.throughput is None: return {self.key: float("nan")} - return {self.key: records[-1][self.field]} + return {self.key: ctx.throughput} @dataclass From c4dac30650a6bea04c5b97134277e30f6fe7de91 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Mon, 29 Jun 2026 16:42:06 -0500 Subject: [PATCH 059/118] Moved and updated EventTypes to its own namespaces --- sequence/utils/metrics/__init__.py | 10 +-------- sequence/utils/metrics/builtins.py | 29 +++++++++------------------ sequence/utils/metrics/event_types.py | 20 ++++++++++++++---- 3 files changed, 27 insertions(+), 32 deletions(-) diff --git a/sequence/utils/metrics/__init__.py b/sequence/utils/metrics/__init__.py index 5f30a0f57..2e884137a 100644 --- a/sequence/utils/metrics/__init__.py +++ b/sequence/utils/metrics/__init__.py @@ -13,6 +13,7 @@ from . import builtins from .event_types import ( EventType, + EventTypes, get_event_type, list_event_types, register_event_type, @@ -35,15 +36,6 @@ ) from .storage import InMemoryStorage, SystemTimeProvider, TimeProvider -# Built-in event type aliases -EG_FAILURE = builtins.EG_FAILURE -EG_SUCCESS = builtins.EG_SUCCESS -THROUGHPUT = builtins.THROUGHPUT -EP_FAILURE = builtins.EP_FAILURE -EP_SUCCESS = builtins.EP_SUCCESS -PURIFIED_DELIVERY = builtins.PURIFIED_DELIVERY -ES_FAILURE = builtins.ES_FAILURE -ES_SUCCESS = builtins.ES_SUCCESS _enabled = False _enabled_events: set[EventType] = set() diff --git a/sequence/utils/metrics/builtins.py b/sequence/utils/metrics/builtins.py index 319232f95..8689e66ef 100644 --- a/sequence/utils/metrics/builtins.py +++ b/sequence/utils/metrics/builtins.py @@ -1,4 +1,4 @@ -"""Built-in event types and metrics.""" +"""Built-in metrics for the metrics module.""" from __future__ import annotations @@ -7,45 +7,36 @@ from .registry import register_metric EG_METRIC = CounterMetric( -EG_FAILURE = register_event_type("EG_FAILURE") -EG_SUCCESS = register_event_type("EG_SUCCESS") -THROUGHPUT = register_event_type("THROUGHPUT") -EP_FAILURE = register_event_type("EP_FAILURE") -EP_SUCCESS = register_event_type("EP_SUCCESS") -PURIFIED_DELIVERY = register_event_type("PURIFIED_DELIVERY") -ES_FAILURE = register_event_type("ES_FAILURE") -ES_SUCCESS = register_event_type("ES_SUCCESS") - prefix="eg", - failure_event=EG_FAILURE, - success_event=EG_SUCCESS, + failure_event=EventTypes.EG_FAILURE, + success_event=EventTypes.EG_SUCCESS, rate_field="success_rate", ) EP_METRIC = CounterMetric( prefix="ep", - failure_event=EP_FAILURE, - success_event=EP_SUCCESS, + failure_event=EventTypes.EP_FAILURE, + success_event=EventTypes.EP_SUCCESS, rate_field="ep_success_rate", ) THROUGHPUT_METRIC = RateMetric(key="app_throughput") PURIFIED_FIDELITIES_METRIC = FidelityMetric( key="purified_fidelities", - event=EP_SUCCESS, + event=EventTypes.EP_SUCCESS, field="fidelity", ) DELIVERY_TIME_METRIC = DeliveryTimeMetric( key="delivery_time", - delivery_event=PURIFIED_DELIVERY, + delivery_event=EventTypes.DELIVERY, ) ES_METRIC = CounterMetric( prefix="es", - failure_event=ES_FAILURE, - success_event=ES_SUCCESS, + failure_event=EventTypes.ES_FAILURE, + success_event=EventTypes.ES_SUCCESS, rate_field="es_success_rate", ) SWAPPED_FIDELITIES_METRIC = FidelityMetric( key="swapped_fidelities", - event=ES_SUCCESS, + event=EventTypes.ES_SUCCESS, field="fidelity", ) diff --git a/sequence/utils/metrics/event_types.py b/sequence/utils/metrics/event_types.py index d22dd1c3f..bac9ac22e 100644 --- a/sequence/utils/metrics/event_types.py +++ b/sequence/utils/metrics/event_types.py @@ -1,4 +1,4 @@ -"""Registrable event types for the metrics module.""" +"""Event types for the metrics module.""" from __future__ import annotations @@ -18,9 +18,9 @@ def register_event_type(name: str) -> EventType: """Register an event type. Returns the existing instance if already registered.""" if name in _registry: return _registry[name] - type = EventType(name) - _registry[name] = type - return type + event_type = EventType(name) + _registry[name] = event_type + return event_type def get_event_type(name: str) -> EventType: @@ -34,3 +34,15 @@ def get_event_type(name: str) -> EventType: def list_event_types() -> list[EventType]: """Return all registered event types.""" return list(_registry.values()) + + +class EventTypes: + """Namespace for built-in simulation event types.""" + + EG_FAILURE = register_event_type("EG_FAILURE") + EG_SUCCESS = register_event_type("EG_SUCCESS") + EP_FAILURE = register_event_type("EP_FAILURE") + EP_SUCCESS = register_event_type("EP_SUCCESS") + DELIVERY = register_event_type("DELIVERY") + ES_FAILURE = register_event_type("ES_FAILURE") + ES_SUCCESS = register_event_type("ES_SUCCESS") From e635e7a6949643ec1362d3ae0d365d66f554e589 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Mon, 29 Jun 2026 16:42:54 -0500 Subject: [PATCH 060/118] Updated tests with new names and events --- tests/utils/test_metrics.py | 165 ++++++++++++++++++------------------ 1 file changed, 81 insertions(+), 84 deletions(-) diff --git a/tests/utils/test_metrics.py b/tests/utils/test_metrics.py index 70f2eac32..f5502741b 100644 --- a/tests/utils/test_metrics.py +++ b/tests/utils/test_metrics.py @@ -3,7 +3,8 @@ import pytest from sequence.utils import metrics -from sequence.utils.metrics import CounterPairMetric +from sequence.utils.metrics import CounterMetric +from sequence.utils.metrics.event_types import EventTypes @pytest.fixture(autouse=True) @@ -16,37 +17,37 @@ def reset_metrics_state(): def test_record_before_enable_is_noop(): - metrics.record(metrics.EG_SUCCESS, "e0", fidelity=0.9) + metrics.record(EventTypes.EG_SUCCESS, "e0", fidelity=0.9) assert metrics.storage.get_all() == [] def test_enable_filters_event_types(): - metrics.enable([metrics.EG_SUCCESS]) + metrics.enable([EventTypes.EG_SUCCESS]) - metrics.record(metrics.EG_FAILURE, "e0", initial_fidelity=0.9) - metrics.record(metrics.EG_SUCCESS, "e0", fidelity=0.9) + metrics.record(EventTypes.EG_FAILURE, "e0", fidelity=0.9) + metrics.record(EventTypes.EG_SUCCESS, "e0", fidelity=0.9) records = metrics.storage.get_all() assert len(records) == 1 - assert records[0]["event_type"] is metrics.EG_SUCCESS + assert records[0]["event_type"] is EventTypes.EG_SUCCESS assert records[0]["owner_name"] == "e0" assert records[0]["fidelity"] == 0.9 def test_record_stores_arbitrary_kwargs(): - metrics.enable([metrics.EG_FAILURE]) + metrics.enable([EventTypes.EG_FAILURE]) - metrics.record(metrics.EG_FAILURE, "e1", initial_fidelity=0.8, custom_metric=42) + metrics.record(EventTypes.EG_FAILURE, "e1", fidelity=0.8, custom_metric=42) record = metrics.storage.get_all()[0] - assert record["initial_fidelity"] == 0.8 + assert record["fidelity"] == 0.8 assert record["custom_metric"] == 42 def test_configure_replaces_storage(): - metrics.enable([metrics.EG_FAILURE, metrics.EG_SUCCESS]) - metrics.record(metrics.EG_FAILURE, "e0") - metrics.record(metrics.EG_SUCCESS, "e0") + metrics.enable([EventTypes.EG_FAILURE, EventTypes.EG_SUCCESS]) + metrics.record(EventTypes.EG_FAILURE, "e0") + metrics.record(EventTypes.EG_SUCCESS, "e0") assert len(metrics.storage.get_all()) == 2 metrics.configure(storage_type="in_memory") @@ -55,18 +56,16 @@ def test_configure_replaces_storage(): def test_reset_metrics_clears_per_node_counts(): - metrics.enable( - [metrics.EG_FAILURE, metrics.EG_SUCCESS, metrics.EP_FAILURE, metrics.EP_SUCCESS] - ) - metrics.record(metrics.EG_FAILURE, "e0") - metrics.record(metrics.EG_SUCCESS, "e0") - metrics.record(metrics.EP_FAILURE, "e0") - metrics.record(metrics.EP_SUCCESS, "e0") + metrics.enable([EventTypes.EG_FAILURE, EventTypes.EG_SUCCESS, EventTypes.EP_FAILURE, EventTypes.EP_SUCCESS]) + metrics.record(EventTypes.EG_FAILURE, "e0") + metrics.record(EventTypes.EG_SUCCESS, "e0") + metrics.record(EventTypes.EP_FAILURE, "e0") + metrics.record(EventTypes.EP_SUCCESS, "e0") metrics.reset_metrics() - eg = metrics.get_counter_pair("eg") - ep = metrics.get_counter_pair("ep") + eg = metrics.get_counter("eg") + ep = metrics.get_counter("ep") assert eg.failures("e0") == 0 assert eg.successes("e0") == 0 assert eg.success_rate("e0") == 0.0 @@ -76,14 +75,14 @@ def test_reset_metrics_clears_per_node_counts(): def test_per_node_counters_are_independent(): - metrics.enable([metrics.EG_FAILURE, metrics.EG_SUCCESS]) + metrics.enable([EventTypes.EG_FAILURE, EventTypes.EG_SUCCESS]) - metrics.record(metrics.EG_FAILURE, "e0") - metrics.record(metrics.EG_FAILURE, "e0") - metrics.record(metrics.EG_SUCCESS, "e0") - metrics.record(metrics.EG_FAILURE, "e1") + metrics.record(EventTypes.EG_FAILURE, "e0") + metrics.record(EventTypes.EG_FAILURE, "e0") + metrics.record(EventTypes.EG_SUCCESS, "e0") + metrics.record(EventTypes.EG_FAILURE, "e1") - eg = metrics.get_counter_pair("eg") + eg = metrics.get_counter("eg") assert eg.failures("e0") == 2 assert eg.successes("e0") == 1 assert eg.success_rate("e0") == pytest.approx(1 / 3) @@ -93,14 +92,14 @@ def test_per_node_counters_are_independent(): def test_completion_events_record_running_success_rate(): - metrics.enable([metrics.EG_FAILURE, metrics.EG_SUCCESS]) + metrics.enable([EventTypes.EG_FAILURE, EventTypes.EG_SUCCESS]) - metrics.record(metrics.EG_FAILURE, "e0") - metrics.record(metrics.EG_SUCCESS, "e0") - metrics.record(metrics.EG_FAILURE, "e0") + metrics.record(EventTypes.EG_FAILURE, "e0") + metrics.record(EventTypes.EG_SUCCESS, "e0") + metrics.record(EventTypes.EG_FAILURE, "e0") - failure_records = metrics.storage.get_by_event(metrics.EG_FAILURE) - success_records = metrics.storage.get_by_event(metrics.EG_SUCCESS) + failure_records = metrics.storage.get_by_event(EventTypes.EG_FAILURE) + success_records = metrics.storage.get_by_event(EventTypes.EG_SUCCESS) assert failure_records[0]["success_rate"] == 0.0 assert success_records[0]["success_rate"] == 0.5 @@ -108,13 +107,13 @@ def test_completion_events_record_running_success_rate(): def test_storage_query_helpers(): - metrics.enable([metrics.EG_FAILURE, metrics.EG_SUCCESS]) + metrics.enable([EventTypes.EG_FAILURE, EventTypes.EG_SUCCESS]) - metrics.record(metrics.EG_FAILURE, "e0", initial_fidelity=0.9) - metrics.record(metrics.EG_SUCCESS, "e0", fidelity=0.9) - metrics.record(metrics.EG_FAILURE, "e1", initial_fidelity=0.9) + metrics.record(EventTypes.EG_FAILURE, "e0", fidelity=0.9) + metrics.record(EventTypes.EG_SUCCESS, "e0", fidelity=0.9) + metrics.record(EventTypes.EG_FAILURE, "e1", fidelity=0.9) - assert len(metrics.storage.get_by_event(metrics.EG_FAILURE)) == 2 + assert len(metrics.storage.get_by_event(EventTypes.EG_FAILURE)) == 2 assert len(metrics.storage.get_by_owner("e0")) == 2 assert len(metrics.storage.get_by_owner("e1")) == 1 @@ -122,9 +121,9 @@ def test_storage_query_helpers(): def test_default_time_provider_uses_system_time(monkeypatch): monkeypatch.setattr(metrics._system_time_provider, "now", lambda: 42) metrics.register_time_provider(metrics._system_time_provider) - metrics.enable([metrics.EG_SUCCESS]) + metrics.enable([EventTypes.EG_SUCCESS]) - metrics.record(metrics.EG_SUCCESS, "e0", fidelity=0.9) + metrics.record(EventTypes.EG_SUCCESS, "e0", fidelity=0.9) assert metrics.storage.get_all()[0]["sim_time"] == 42 @@ -135,34 +134,32 @@ def now(self) -> int: return 12345 metrics.register_time_provider(StubTimeProvider()) - metrics.enable([metrics.EG_SUCCESS]) + metrics.enable([EventTypes.EG_SUCCESS]) - metrics.record(metrics.EG_SUCCESS, "e0", fidelity=0.9) + metrics.record(EventTypes.EG_SUCCESS, "e0", fidelity=0.9) assert metrics.storage.get_all()[0]["sim_time"] == 12345 def test_throughput_does_not_affect_eg_counters(): - metrics.enable([metrics.EG_FAILURE, metrics.EG_SUCCESS, metrics.THROUGHPUT]) + metrics.enable([EventTypes.EG_FAILURE, EventTypes.EG_SUCCESS]) - metrics.record(metrics.EG_FAILURE, "e0") - metrics.record(metrics.EG_SUCCESS, "e0") - metrics.record(metrics.THROUGHPUT, "e0", throughput=42.0) + metrics.record(EventTypes.EG_FAILURE, "e0") + metrics.record(EventTypes.EG_SUCCESS, "e0") - eg = metrics.get_counter_pair("eg") + eg = metrics.get_counter("eg") assert eg.failures("e0") == 1 assert eg.successes("e0") == 1 assert eg.success_rate("e0") == 0.5 def test_collect_trial_metrics_returns_node_snapshot(): - metrics.enable([metrics.EG_FAILURE, metrics.EG_SUCCESS, metrics.THROUGHPUT]) + metrics.enable([EventTypes.EG_FAILURE, EventTypes.EG_SUCCESS]) - metrics.record(metrics.EG_FAILURE, "e0", initial_fidelity=0.8) - metrics.record(metrics.EG_SUCCESS, "e0", fidelity=0.8) - metrics.record(metrics.THROUGHPUT, "e0", throughput=12.5) + metrics.record(EventTypes.EG_FAILURE, "e0", fidelity=0.8) + metrics.record(EventTypes.EG_SUCCESS, "e0", fidelity=0.8) - trial = metrics.collect_trial_metrics("e0") + trial = metrics.collect_trial_metrics("e0", throughput=12.5) assert trial["eg_failures"] == 1 assert trial["eg_success"] == 1 @@ -171,8 +168,8 @@ def test_collect_trial_metrics_returns_node_snapshot(): def test_collect_trial_metrics_without_throughput_is_nan(): - metrics.enable([metrics.EG_SUCCESS]) - metrics.record(metrics.EG_SUCCESS, "e0", fidelity=0.9) + metrics.enable([EventTypes.EG_SUCCESS]) + metrics.record(EventTypes.EG_SUCCESS, "e0", fidelity=0.9) trial = metrics.collect_trial_metrics("e0") @@ -239,55 +236,55 @@ def test_aggregate_trial_metrics_ignores_non_finite_values(): def test_ep_counters_and_success_rate(): - metrics.enable([metrics.EP_FAILURE, metrics.EP_SUCCESS]) + metrics.enable([EventTypes.EP_FAILURE, EventTypes.EP_SUCCESS]) - metrics.record(metrics.EP_FAILURE, "left") - metrics.record(metrics.EP_SUCCESS, "left", fidelity=0.75) - metrics.record(metrics.EP_FAILURE, "left") + metrics.record(EventTypes.EP_FAILURE, "left") + metrics.record(EventTypes.EP_SUCCESS, "left", fidelity=0.75) + metrics.record(EventTypes.EP_FAILURE, "left") - ep = metrics.get_counter_pair("ep") + ep = metrics.get_counter("ep") assert ep.failures("left") == 2 assert ep.successes("left") == 1 assert ep.success_rate("left") == pytest.approx(1 / 3) - success_records = metrics.storage.get_by_event(metrics.EP_SUCCESS) + success_records = metrics.storage.get_by_event(EventTypes.EP_SUCCESS) assert success_records[0]["ep_success_rate"] == pytest.approx(0.5) def test_es_counters_and_success_rate(): - metrics.enable([metrics.ES_FAILURE, metrics.ES_SUCCESS]) + metrics.enable([EventTypes.ES_FAILURE, EventTypes.ES_SUCCESS]) - metrics.record(metrics.ES_FAILURE, "middle") - metrics.record(metrics.ES_SUCCESS, "middle", fidelity=0.75) - metrics.record(metrics.ES_FAILURE, "middle") + metrics.record(EventTypes.ES_FAILURE, "middle") + metrics.record(EventTypes.ES_SUCCESS, "middle", fidelity=0.75) + metrics.record(EventTypes.ES_FAILURE, "middle") - es = metrics.get_counter_pair("es") + es = metrics.get_counter("es") assert es.failures("middle") == 2 assert es.successes("middle") == 1 assert es.success_rate("middle") == pytest.approx(1 / 3) - success_records = metrics.storage.get_by_event(metrics.ES_SUCCESS) + success_records = metrics.storage.get_by_event(EventTypes.ES_SUCCESS) assert success_records[0]["es_success_rate"] == pytest.approx(0.5) def test_collect_trial_metrics_swapped_fidelities(): - metrics.enable([metrics.ES_SUCCESS]) + metrics.enable([EventTypes.ES_SUCCESS]) - metrics.record(metrics.ES_SUCCESS, "middle", fidelity=0.7) - metrics.record(metrics.ES_SUCCESS, "middle", fidelity=0.75) + metrics.record(EventTypes.ES_SUCCESS, "middle", fidelity=0.7) + metrics.record(EventTypes.ES_SUCCESS, "middle", fidelity=0.75) trial = metrics.collect_trial_metrics("middle") assert trial["es_success"] == 2 assert trial["swapped_fidelities"] == [0.7, 0.75] -def test_purified_delivery_does_not_affect_ep_counters(): - metrics.enable([metrics.EP_FAILURE, metrics.EP_SUCCESS, metrics.PURIFIED_DELIVERY]) +def test_delivery_does_not_affect_ep_counters(): + metrics.enable([EventTypes.EP_FAILURE, EventTypes.EP_SUCCESS, EventTypes.DELIVERY]) - metrics.record(metrics.EP_SUCCESS, "left", fidelity=0.8) - metrics.record(metrics.PURIFIED_DELIVERY, "right", fidelity=0.8, pair_number=1) + metrics.record(EventTypes.EP_SUCCESS, "left", fidelity=0.8) + metrics.record(EventTypes.DELIVERY, "right", fidelity=0.8, pair_number=1) - ep = metrics.get_counter_pair("ep") + ep = metrics.get_counter("ep") assert ep.successes("left") == 1 assert ep.failures("right") == 0 @@ -304,14 +301,14 @@ def now(self) -> int: provider = StubTimeProvider() metrics.register_time_provider(provider) - metrics.enable([metrics.EP_SUCCESS, metrics.PURIFIED_DELIVERY]) + metrics.enable([EventTypes.EP_SUCCESS, EventTypes.DELIVERY]) - metrics.record(metrics.EP_SUCCESS, "left", fidelity=0.7) - metrics.record(metrics.EP_SUCCESS, "left", fidelity=0.75) + metrics.record(EventTypes.EP_SUCCESS, "left", fidelity=0.7) + metrics.record(EventTypes.EP_SUCCESS, "left", fidelity=0.75) for pair_number in range(1, 4): metrics.record( - metrics.PURIFIED_DELIVERY, + EventTypes.DELIVERY, "right", fidelity=0.7 + pair_number * 0.01, pair_number=pair_number, @@ -330,8 +327,8 @@ def now(self) -> int: def test_collect_trial_metrics_delivery_time_nan_when_target_not_reached(): - metrics.enable([metrics.PURIFIED_DELIVERY]) - metrics.record(metrics.PURIFIED_DELIVERY, "right", fidelity=0.9, pair_number=1) + metrics.enable([EventTypes.DELIVERY]) + metrics.record(EventTypes.DELIVERY, "right", fidelity=0.9, pair_number=1) trial = metrics.collect_trial_metrics( "left", @@ -344,8 +341,8 @@ def test_collect_trial_metrics_delivery_time_nan_when_target_not_reached(): def test_collect_trial_metrics_delivery_owner_defaults_to_owner(): - metrics.enable([metrics.PURIFIED_DELIVERY]) - metrics.record(metrics.PURIFIED_DELIVERY, "right", fidelity=0.9, pair_number=1) + metrics.enable([EventTypes.DELIVERY]) + metrics.record(EventTypes.DELIVERY, "right", fidelity=0.9, pair_number=1) trial = metrics.collect_trial_metrics( "right", @@ -398,7 +395,7 @@ def test_register_event_type_is_idempotent(): def test_register_metric_adds_to_collect_trial_metrics(): swap_failure = metrics.register_event_type("SWAP_FAILURE") swap_success = metrics.register_event_type("SWAP_SUCCESS") - swap_metric = CounterPairMetric( + swap_metric = CounterMetric( prefix="swap", failure_event=swap_failure, success_event=swap_success, @@ -419,7 +416,7 @@ def test_register_metric_adds_to_collect_trial_metrics(): def test_register_metric_rejects_duplicate_output_keys(): - duplicate = CounterPairMetric( + duplicate = CounterMetric( prefix="eg", failure_event=metrics.register_event_type("DUP_FAIL"), success_event=metrics.register_event_type("DUP_SUCCESS"), From 638e4d845f0cd67f2b9b1b016c3b1bef4c1df7b2 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Mon, 29 Jun 2026 16:44:46 -0500 Subject: [PATCH 061/118] Circuit swapping updated to include more node information --- .../swapping/swapping_circuit.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/sequence/entanglement_management/swapping/swapping_circuit.py b/sequence/entanglement_management/swapping/swapping_circuit.py index c41f349f3..4fae74e3c 100644 --- a/sequence/entanglement_management/swapping/swapping_circuit.py +++ b/sequence/entanglement_management/swapping/swapping_circuit.py @@ -14,6 +14,7 @@ from ...constants import KET_VECTOR_FORMALISM, DENSITY_MATRIX_FORMALISM from ...resource_management.memory_manager import MemoryInfo from ...utils import log, metrics +from ...utils.metrics.event_types import EventTypes @EntanglementSwappingA.register(KET_VECTOR_FORMALISM) @@ -84,7 +85,13 @@ def start(self) -> None: if self.owner.get_generator().random() < self.success_probability(): # swapping succeeded fidelity = self.updated_fidelity(self.left_memo.fidelity, self.right_memo.fidelity) - metrics.record(metrics.ES_SUCCESS, self.owner.name, fidelity=fidelity) + metrics.record( + EventTypes.ES_SUCCESS, + self.owner.name, + left_node=self.left_node, + right_node=self.right_node, + fidelity=fidelity, + ) self.is_success = True expire_time = min(self.left_memo.get_expire_time(), self.right_memo.get_expire_time()) @@ -109,7 +116,12 @@ def start(self) -> None: else: # swapping failed log.logger.info(f"{self.name} swapping failed") - metrics.record(metrics.ES_FAILURE, self.owner.name) + metrics.record( + EventTypes.ES_FAILURE, + self.owner.name, + left_node=self.left_node, + right_node=self.right_node, + ) msg_l = EntanglementSwappingMessage(SwappingMsgType.SWAP_RES,self.left_protocol_name, fidelity=0) msg_r = EntanglementSwappingMessage(SwappingMsgType.SWAP_RES, self.right_protocol_name, fidelity=0) From af2ac51af101d428572afd3328c2ea4240a0117d Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Mon, 29 Jun 2026 16:45:40 -0500 Subject: [PATCH 062/118] BDS swapping updated with more information --- .../swapping/swapping_bds.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/sequence/entanglement_management/swapping/swapping_bds.py b/sequence/entanglement_management/swapping/swapping_bds.py index ac00ffb49..7892cb7df 100644 --- a/sequence/entanglement_management/swapping/swapping_bds.py +++ b/sequence/entanglement_management/swapping/swapping_bds.py @@ -5,6 +5,7 @@ from .swapping_base import EntanglementSwappingA, EntanglementSwappingB, SwappingMsgType, EntanglementSwappingMessage from ...utils import log, metrics +from ...utils.metrics.event_types import EventTypes from ...constants import BELL_DIAGONAL_STATE_FORMALISM from ...resource_management.memory_manager import MemoryInfo @@ -88,7 +89,13 @@ def start(self) -> None: # get BDS conditioned on success, fidelity is the first diagonal element new_bds = self.swapping_res() fidelity = new_bds[0] - metrics.record(metrics.ES_SUCCESS, self.owner.name, fidelity=fidelity) + metrics.record( + EventTypes.ES_SUCCESS, + self.owner.name, + left_node=self.left_node, + right_node=self.right_node, + fidelity=fidelity, + ) keys = [left_remote_memory.qstate_key, right_remote_memory.qstate_key] self.owner.timeline.quantum_manager.set(keys, new_bds) @@ -108,7 +115,12 @@ def start(self) -> None: meas_res=[]) else: log.logger.info(f'swapping failed!') - metrics.record(metrics.ES_FAILURE, self.owner.name) + metrics.record( + EventTypes.ES_FAILURE, + self.owner.name, + left_node=self.left_node, + right_node=self.right_node, + ) msg_l = EntanglementSwappingMessage(SwappingMsgType.SWAP_RES, self.left_protocol_name, fidelity=0) msg_r = EntanglementSwappingMessage(SwappingMsgType.SWAP_RES, self.right_protocol_name, fidelity=0) From 1afcc4499b3a84a6e4ef1cbbc9609b27d520730c Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Mon, 29 Jun 2026 16:46:10 -0500 Subject: [PATCH 063/118] Circuit BBPSSW updated with more information and new names --- .../entanglement_management/purification/bbpssw_circuit.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sequence/entanglement_management/purification/bbpssw_circuit.py b/sequence/entanglement_management/purification/bbpssw_circuit.py index 120f3661f..3eb7a965a 100644 --- a/sequence/entanglement_management/purification/bbpssw_circuit.py +++ b/sequence/entanglement_management/purification/bbpssw_circuit.py @@ -15,6 +15,7 @@ from ...topology.node import Node from ...utils import log, metrics +from ...utils.metrics.event_types import EventTypes from ...constants import KET_VECTOR_FORMALISM, DENSITY_MATRIX_FORMALISM from .bbpssw_protocol import BBPSSWProtocol, BBPSSWMessage, BBPSSWMsgType @@ -118,13 +119,14 @@ def received_message(self, src: str, msg: BBPSSWMessage) -> None: if self.meas_res == msg.meas_res: self.kept_memo.fidelity = self.improved_fidelity(self.kept_memo.fidelity) metrics.record( - metrics.EP_SUCCESS, + EventTypes.EP_SUCCESS, self.owner.name, + remote_node=self.remote_node_name, fidelity=self.kept_memo.fidelity, ) self.update_resource_manager(self.kept_memo, state="PURIFIED") else: - metrics.record(metrics.EP_FAILURE, self.owner.name) + metrics.record(EventTypes.EP_FAILURE, self.owner.name, remote_node=self.remote_node_name) self.update_resource_manager(self.kept_memo, state="RAW") @staticmethod From 07402e55dc28bbea8e4e3c4147967aa5b2fc054b Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Mon, 29 Jun 2026 16:47:30 -0500 Subject: [PATCH 064/118] BDS BBPSSW updated with more information --- sequence/entanglement_management/purification/bbpssw_bds.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sequence/entanglement_management/purification/bbpssw_bds.py b/sequence/entanglement_management/purification/bbpssw_bds.py index b85a2571a..5c670e529 100644 --- a/sequence/entanglement_management/purification/bbpssw_bds.py +++ b/sequence/entanglement_management/purification/bbpssw_bds.py @@ -17,6 +17,7 @@ from ...constants import BELL_DIAGONAL_STATE_FORMALISM from ...utils import log, metrics +from ...utils.metrics.event_types import EventTypes from .bbpssw_protocol import BBPSSWProtocol, BBPSSWMessage, BBPSSWMsgType @BBPSSWProtocol.register(BELL_DIAGONAL_STATE_FORMALISM) @@ -138,14 +139,15 @@ def received_message(self, src: str, msg: BBPSSWMessage) -> None: self.kept_memo.bds_decohere() self.kept_memo.fidelity = self.kept_memo.get_bds_fidelity() metrics.record( - metrics.EP_SUCCESS, + EventTypes.EP_SUCCESS, self.owner.name, + remote_node=self.remote_node_name, fidelity=self.kept_memo.fidelity, ) self.update_resource_manager(self.kept_memo, state="PURIFIED") else: log.logger.info(f'Purification failed because measure results: {self.meas_res}, {msg.meas_res}') - metrics.record(metrics.EP_FAILURE, self.owner.name) + metrics.record(EventTypes.EP_FAILURE, self.owner.name, remote_node=self.remote_node_name) self.update_resource_manager(self.kept_memo, state="RAW") else: From 316207b29dbc3e0b7135dd19aca4dc5394da7903 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Mon, 29 Jun 2026 16:48:57 -0500 Subject: [PATCH 065/118] Generation updated to include more information and new names --- sequence/entanglement_management/generation/barret_kok.py | 8 +++++++- .../entanglement_management/generation/generation_base.py | 8 +++++++- .../entanglement_management/generation/single_heralded.py | 8 +++++++- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/sequence/entanglement_management/generation/barret_kok.py b/sequence/entanglement_management/generation/barret_kok.py index 22b7210dc..95b8c4409 100644 --- a/sequence/entanglement_management/generation/barret_kok.py +++ b/sequence/entanglement_management/generation/barret_kok.py @@ -14,6 +14,7 @@ from ...kernel.event import Event from ...kernel.process import Process from ...utils import log, metrics +from ...utils.metrics.event_types import EventTypes @EntanglementGenerationA.register(BARRET_KOK) @@ -225,7 +226,12 @@ def _entanglement_succeed(self): self.memory.entangled_memory['node_id'] = self.remote_node_name self.memory.entangled_memory['memo_id'] = self.remote_memo_id self.memory.fidelity = self.memory.raw_fidelity - metrics.record(metrics.EG_SUCCESS, self.owner.name, fidelity=self.fidelity) + metrics.record( + EventTypes.EG_SUCCESS, + self.owner.name, + remote_node=self.remote_node_name, + fidelity=self.fidelity, + ) self.update_resource_manager(self.memory, MemoryInfo.ENTANGLED) diff --git a/sequence/entanglement_management/generation/generation_base.py b/sequence/entanglement_management/generation/generation_base.py index 2ba529e02..759681b75 100644 --- a/sequence/entanglement_management/generation/generation_base.py +++ b/sequence/entanglement_management/generation/generation_base.py @@ -13,6 +13,7 @@ from ..entanglement_protocol import EntanglementProtocol from ...components.circuit import Circuit from ...utils import log, metrics +from ...utils.metrics.event_types import EventTypes class QuantumCircuitMixin: @@ -180,7 +181,12 @@ def _entanglement_succeed(self): def _entanglement_fail(self): for event in self.scheduled_events: self.owner.timeline.remove_event(event) - metrics.record(metrics.EG_FAILURE, self.owner.name, initial_fidelity=self.fidelity) + metrics.record( + EventTypes.EG_FAILURE, + self.owner.name, + remote_node=self.remote_node_name, + fidelity=self.fidelity, + ) log.logger.info(f'{self.owner.name} failed entanglement of memory {self.memory}') self.update_resource_manager(self.memory, MemoryInfo.RAW) diff --git a/sequence/entanglement_management/generation/single_heralded.py b/sequence/entanglement_management/generation/single_heralded.py index 56b80e5a2..074fbfb22 100644 --- a/sequence/entanglement_management/generation/single_heralded.py +++ b/sequence/entanglement_management/generation/single_heralded.py @@ -12,6 +12,7 @@ from ...kernel.quantum_manager import QuantumManager from ...resource_management.memory_manager import MemoryInfo from ...utils import log, metrics +from ...utils.metrics.event_types import EventTypes if TYPE_CHECKING: from ...components.memory import Memory @@ -228,9 +229,14 @@ def received_message(self, src: str, msg: EntanglementGenerationMessage) -> None def _entanglement_succeed(self): log.logger.info(f'{self.owner.name} successful entanglement of memory {self.memory}') - metrics.record(metrics.EG_SUCCESS, self.owner.name, fidelity=self.raw_fidelity) self.memory.entangled_memory['node_id'] = self.remote_node_name self.memory.entangled_memory['memo_id'] = self.remote_memo_id + metrics.record( + EventTypes.EG_SUCCESS, + self.owner.name, + remote_node=self.remote_node_name, + fidelity=self.raw_fidelity, + ) self.memory.fidelity = self.raw_fidelity self.update_resource_manager(self.memory, MemoryInfo.ENTANGLED) From 4fe3d68c4941e27c87a7fae9fd18b4976c917ddc Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Tue, 30 Jun 2026 16:03:28 -0500 Subject: [PATCH 066/118] Added doc strings for storage methods --- sequence/utils/metrics/storage.py | 37 ++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/sequence/utils/metrics/storage.py b/sequence/utils/metrics/storage.py index ea1d92846..47629a8ae 100644 --- a/sequence/utils/metrics/storage.py +++ b/sequence/utils/metrics/storage.py @@ -11,13 +11,20 @@ class TimeProvider(Protocol): """Protocol for objects that supply a timestamp for recorded events.""" - def now(self) -> int: ... + def now(self) -> int: + """Return the current timestamp in picoseconds.""" + ... class SystemTimeProvider: """Fallback time source using the current system clock.""" def now(self) -> int: + """Return the current system time in picoseconds. + + Returns: + Current wall-clock time from ``time_ns()``. + """ return time_ns() @@ -25,19 +32,47 @@ class InMemoryStorage: """Store metric records in memory for the lifetime of the simulation.""" def __init__(self) -> None: + """Initialize an empty in-memory record store.""" self._records: list[dict[str, Any]] = [] def append(self, record: dict[str, Any]) -> None: + """Append a metric record to storage. + + Args: + record: Event record with event type, owner, simulation time, and fields. + """ self._records.append(record) def get_all(self) -> list[dict[str, Any]]: + """Return all stored metric records. + + Returns: + Copy of every record appended since construction or last ``clear()``. + """ return list(self._records) def get_by_event(self, event_type: EventType) -> list[dict[str, Any]]: + """Return records matching an event type. + + Args: + event_type: Event type to filter on. + + Returns: + Records whose ``event_type`` matches the given value. + """ return [record for record in self._records if record["event_type"] is event_type] def get_by_owner(self, owner_name: str) -> list[dict[str, Any]]: + """Return records for a given owner. + + Args: + owner_name: Owner name to filter on. + + Returns: + Records whose ``owner_name`` matches the given value. + """ return [record for record in self._records if record["owner_name"] == owner_name] def clear(self) -> None: + """Remove all stored records.""" self._records.clear() From ef41a9c7aba393aa7fe51070b0f61c65fc50fdd3 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Tue, 30 Jun 2026 16:03:37 -0500 Subject: [PATCH 067/118] Added docstrings for registry methods --- sequence/utils/metrics/registry.py | 40 +++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/sequence/utils/metrics/registry.py b/sequence/utils/metrics/registry.py index 8c351d13a..738ff3f25 100644 --- a/sequence/utils/metrics/registry.py +++ b/sequence/utils/metrics/registry.py @@ -9,7 +9,14 @@ def register_metric(metric: Metric) -> None: - """Register a metric. Raises if output keys collide with existing metrics.""" + """Register a metric. + + Args: + metric: Metric instance to add to the registry. + + Raises: + ValueError: If output keys collide with an existing metric. + """ new_keys = metric.output_keys for existing in _metrics: overlap = existing.output_keys & new_keys @@ -21,7 +28,11 @@ def register_metric(metric: Metric) -> None: def unregister_metric(metric: Metric) -> None: - """Remove a metric from the registry.""" + """Remove a metric from the registry. + + Args: + metric: Metric instance to remove. + """ if metric in _metrics: _metrics.remove(metric) if isinstance(metric, CounterMetric): @@ -29,12 +40,23 @@ def unregister_metric(metric: Metric) -> None: def list_metrics() -> list[Metric]: - """Return registered metrics in registration order.""" + """Return registered metrics in registration order. + + Returns: + Registered metrics in the order they were added. + """ return list(_metrics) def get_counter(prefix: str) -> CounterMetric: - """Return a registered counter metric by prefix.""" + """Return a registered counter metric by prefix. + + Args: + prefix: Counter metric prefix used at registration time. + + Returns: + The matching counter metric. + """ try: return _counters[prefix] except KeyError as exc: @@ -42,12 +64,18 @@ def get_counter(prefix: str) -> CounterMetric: def reset_metrics() -> None: - """Reset per-trial state for all registered metrics.""" + """Reset per-trial state for all registered metrics. + + Clears accumulated counts and other state held by each metric between trials. + """ for metric in _metrics: metric.reset() def clear_registry() -> None: - """Remove all metrics from the registry (for tests).""" + """Remove all metrics from the registry. + + Intended for test isolation; also clears the counter lookup table. + """ _metrics.clear() _counters.clear() From b1585ffcff0de805f6e815cb919d232e76ea729e Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Tue, 30 Jun 2026 16:03:44 -0500 Subject: [PATCH 068/118] Added docstrings for metric types methods --- sequence/utils/metrics/metric_types.py | 109 +++++++++++++++++++++++-- 1 file changed, 100 insertions(+), 9 deletions(-) diff --git a/sequence/utils/metrics/metric_types.py b/sequence/utils/metrics/metric_types.py index ad7a7db34..d3d54e8fb 100644 --- a/sequence/utils/metrics/metric_types.py +++ b/sequence/utils/metrics/metric_types.py @@ -12,7 +12,16 @@ @dataclass class CollectContext: - """Context passed to metrics when collecting trial results.""" + """Context passed to metrics when collecting trial results. + + Attributes: + owner_name: Node name for counter and fidelity metrics. + storage: In-memory store of recorded events for the trial. + delivery_owner: Node name for delivery-time metrics; defaults to ``owner_name``. + target_pairs: Number of delivered pairs required to compute delivery time. + reservation_start_time: Simulation time when the reservation started (ps). + throughput: Application throughput supplied at collection time. + """ owner_name: str storage: InMemoryStorage @@ -28,19 +37,40 @@ class Metric(ABC): @property @abstractmethod def event_types(self) -> frozenset[EventType]: - """Event types this metric reacts to during recording.""" + """Event types this metric reacts to during recording. + + Returns: + Frozen set of event types handled by ``on_record``. + """ @property @abstractmethod def output_keys(self) -> frozenset[str]: - """Keys produced by collect().""" + """Keys produced by ``collect()``. + + Returns: + Frozen set of keys written into per-trial result dictionaries. + """ def on_record(self, event_type: EventType, owner_name: str, kwargs: dict[str, Any]) -> None: - """Update metric state when a matching event is recorded.""" + """Update metric state when a matching event is recorded. + + Args: + event_type: Type of the recorded event. + owner_name: Name of the node or component that owns the event. + kwargs: Mutable event payload; metrics may add derived fields. + """ @abstractmethod def collect(self, ctx: CollectContext) -> dict[str, Any]: - """Return trial result keys and values for this metric.""" + """Return trial result keys and values for this metric. + + Args: + ctx: Collection context with owner, storage, and trial parameters. + + Returns: + Mapping of output keys to per-trial values. + """ def reset(self) -> None: """Clear per-trial metric state.""" @@ -72,12 +102,36 @@ def output_keys(self) -> frozenset[str]: ) def failures(self, owner_name: str) -> int: + """Return the failure count for an owner. + + Args: + owner_name: Name of the node or component to query. + + Returns: + Number of recorded failure events for the owner. + """ return self._failures.get(owner_name, 0) def successes(self, owner_name: str) -> int: + """Return the success count for an owner. + + Args: + owner_name: Name of the node or component to query. + + Returns: + Number of recorded success events for the owner. + """ return self._successes.get(owner_name, 0) def success_rate(self, owner_name: str) -> float: + """Return the success rate for an owner. + + Args: + owner_name: Name of the node or component to query. + + Returns: + Ratio of successes to total attempts, or 0.0 if there are no attempts. + """ failures = self.failures(owner_name) successes = self.successes(owner_name) attempts = failures + successes @@ -86,6 +140,13 @@ def success_rate(self, owner_name: str) -> float: return successes / attempts def on_record(self, event_type: EventType, owner_name: str, kwargs: dict[str, Any]) -> None: + """Increment failure or success counts and update the rate field in kwargs. + + Args: + event_type: Recorded event type; must match failure or success event. + owner_name: Name of the node or component that owns the event. + kwargs: Event payload; updated with the current success rate. + """ if event_type is self.failure_event: self._failures[owner_name] = self._failures.get(owner_name, 0) + 1 kwargs[self.rate_field] = self.success_rate(owner_name) @@ -94,6 +155,14 @@ def on_record(self, event_type: EventType, owner_name: str, kwargs: dict[str, An kwargs[self.rate_field] = self.success_rate(owner_name) def collect(self, ctx: CollectContext) -> dict[str, Any]: + """Return failure, success, and success-rate counts for the trial owner. + + Args: + ctx: Collection context with the owner name to report. + + Returns: + Mapping of prefixed failure, success, and success-rate keys. + """ return { f"{self.prefix}_failures": self.failures(ctx.owner_name), f"{self.prefix}_success": self.successes(ctx.owner_name), @@ -101,6 +170,7 @@ def collect(self, ctx: CollectContext) -> dict[str, Any]: } def reset(self) -> None: + """Clear per-owner failure and success counts.""" self._failures.clear() self._successes.clear() @@ -120,6 +190,14 @@ def output_keys(self) -> frozenset[str]: return frozenset({self.key}) def collect(self, ctx: CollectContext) -> dict[str, Any]: + """Return the throughput value supplied at collection time. + + Args: + ctx: Collection context; uses ``throughput`` when set. + + Returns: + Mapping with the configured rate key and throughput or NaN. + """ if ctx.throughput is None: return {self.key: float("nan")} return {self.key: ctx.throughput} @@ -142,6 +220,14 @@ def output_keys(self) -> frozenset[str]: return frozenset({self.key}) def collect(self, ctx: CollectContext) -> dict[str, Any]: + """Collect fidelity values from matching success events for the owner. + + Args: + ctx: Collection context with owner name and event storage. + + Returns: + Mapping of the configured key to a list of fidelity values. + """ values = [ record[self.field] for record in ctx.storage.get_by_owner(ctx.owner_name) @@ -167,12 +253,17 @@ def event_types(self) -> frozenset[EventType]: def output_keys(self) -> frozenset[str]: return frozenset({self.key}) - # TODO: Implement delivery time metric using better methods to collect data - # CollectContext was previously used - however this implementation is not - # very good and needs to be reworked - # We need to get reservation_start_time and target_pairs from somewhere else @override def collect(self, ctx: CollectContext) -> dict[str, Any]: + """Compute elapsed time to deliver the target number of pairs. + + Args: + ctx: Collection context with delivery owner, target pair count, + reservation start time, and stored delivery events. + + Returns: + Mapping with delivery time in seconds, or NaN if data is insufficient. + """ delivery_owner = ctx.delivery_owner or ctx.owner_name delivery_records = [ record From f53194b2a5f7993e2095869e1ccbf03fae4f4019 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Tue, 30 Jun 2026 16:03:48 -0500 Subject: [PATCH 069/118] Added docstrings for event types methods --- sequence/utils/metrics/event_types.py | 30 +++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/sequence/utils/metrics/event_types.py b/sequence/utils/metrics/event_types.py index bac9ac22e..89ca6c6c8 100644 --- a/sequence/utils/metrics/event_types.py +++ b/sequence/utils/metrics/event_types.py @@ -9,13 +9,24 @@ @dataclass(frozen=True, slots=True) class EventType: - """Identifies a type of recordable simulation event.""" + """Identifies a type of recordable simulation event. + + Attributes: + name: Unique string identifier for the event type. + """ name: str def register_event_type(name: str) -> EventType: - """Register an event type. Returns the existing instance if already registered.""" + """Register an event type. + + Args: + name: Unique name for the event type. + + Returns: + The registered event type, or the existing instance if already registered. + """ if name in _registry: return _registry[name] event_type = EventType(name) @@ -24,7 +35,14 @@ def register_event_type(name: str) -> EventType: def get_event_type(name: str) -> EventType: - """Return a registered event type by name.""" + """Return a registered event type by name. + + Args: + name: Name of the event type to look up. + + Returns: + The registered event type. + """ try: return _registry[name] except KeyError as exc: @@ -32,7 +50,11 @@ def get_event_type(name: str) -> EventType: def list_event_types() -> list[EventType]: - """Return all registered event types.""" + """Return all registered event types. + + Returns: + All event types registered so far. + """ return list(_registry.values()) From 954b87f5d7008a72348f5b5a82f0e614ae442d9d Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Tue, 30 Jun 2026 16:03:57 -0500 Subject: [PATCH 070/118] Added doc strings for builtins methods --- sequence/utils/metrics/builtins.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sequence/utils/metrics/builtins.py b/sequence/utils/metrics/builtins.py index 8689e66ef..c870e1039 100644 --- a/sequence/utils/metrics/builtins.py +++ b/sequence/utils/metrics/builtins.py @@ -52,6 +52,10 @@ def register_builtin_metrics() -> None: - """Register all built-in metrics.""" + """Register all built-in metrics with the global registry. + + Registers entanglement generation, purification, swapping, throughput, + fidelity, and delivery-time metrics defined in this module. + """ for metric in _BUILTIN_METRICS: register_metric(metric) From 9697bd1df722e191c965f90ab2a9f324df913246 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Tue, 30 Jun 2026 16:04:04 -0500 Subject: [PATCH 071/118] Added docstrings for init methods --- sequence/utils/metrics/__init__.py | 46 ++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/sequence/utils/metrics/__init__.py b/sequence/utils/metrics/__init__.py index 2e884137a..84a3dd821 100644 --- a/sequence/utils/metrics/__init__.py +++ b/sequence/utils/metrics/__init__.py @@ -45,13 +45,21 @@ def register_time_provider(provider: TimeProvider) -> None: - """Register the active time source for recorded events.""" + """Register the active time source for recorded events. + + Args: + provider: Object supplying timestamps via ``now()``. + """ global time_provider time_provider = provider def enable(event_types: list[EventType]) -> None: - """Enable metrics recording for the given event types.""" + """Enable metrics recording for the given event types. + + Args: + event_types: Event types to record when ``record()`` is called. + """ global _enabled, _enabled_events _enabled = True _enabled_events = set(event_types) @@ -62,6 +70,9 @@ def configure(storage_type: str = "in_memory") -> None: Available storage options: - "in_memory": The default, uses ``InMemoryStorage``. + + Args: + storage_type: Storage backend identifier. """ global storage if storage_type == "in_memory": @@ -70,7 +81,13 @@ def configure(storage_type: str = "in_memory") -> None: def record(event_type: EventType, owner_name: str, **kwargs: Any) -> None: - """Record a metrics event if metrics are enabled for this event type.""" + """Record a metrics event if metrics are enabled for this event type. + + Args: + event_type: Type of simulation event to record. + owner_name: Name of the node or component that owns the event. + **kwargs: Additional event-specific fields stored with the record. + """ if not _enabled or event_type not in _enabled_events: return @@ -97,7 +114,18 @@ def collect_trial_metrics( reservation_start_time: int | None = None, throughput: float | None = None, ) -> dict[str, Any]: - """Collect per-trial metrics for a node from the metrics module.""" + """Collect per-trial metrics for a node from the metrics module. + + Args: + owner_name: Node name to collect counter and fidelity metrics for. + delivery_owner: Node name used for delivery-time metrics; defaults to ``owner_name``. + target_pairs: Number of delivered pairs required to compute delivery time. + reservation_start_time: Simulation time when the reservation started (ps). + throughput: Application throughput to include in collected metrics. + + Returns: + Mapping of metric output keys to per-trial values. + """ ctx = CollectContext( owner_name=owner_name, storage=storage, @@ -117,7 +145,15 @@ def aggregate_trial_metrics( *, list_metric_cap: int | None = 500, ) -> dict[str, float]: - """Aggregate trial metrics across multiple trials.""" + """Aggregate trial metrics across multiple trials. + + Args: + trials: Per-trial metric dictionaries from ``collect_trial_metrics``. + list_metric_cap: Maximum list elements per trial to include when aggregating list metrics. + + Returns: + Mapping of ``avg_*`` and ``std_*`` keys to aggregated statistics. + """ if not trials: raise ValueError("Cannot aggregate an empty list of trials") From 552ae768260d05bb28a11d86d72d7565e4f426e5 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Wed, 1 Jul 2026 12:17:07 -0500 Subject: [PATCH 072/118] Improve system time provider (now returns picoseconds properly) --- sequence/utils/metrics/storage.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sequence/utils/metrics/storage.py b/sequence/utils/metrics/storage.py index 47629a8ae..ac6e85048 100644 --- a/sequence/utils/metrics/storage.py +++ b/sequence/utils/metrics/storage.py @@ -2,7 +2,7 @@ from __future__ import annotations -from time import time_ns +from time import monotonic_ns from typing import Any, Protocol from .event_types import EventType @@ -25,7 +25,7 @@ def now(self) -> int: Returns: Current wall-clock time from ``time_ns()``. """ - return time_ns() + return monotonic_ns() * 1000 class InMemoryStorage: From b89ffb9945ae09cc836c83b204cbc8edbfa9f039 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Wed, 1 Jul 2026 12:23:58 -0500 Subject: [PATCH 073/118] Replace ellipsis with exception --- sequence/utils/metrics/storage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sequence/utils/metrics/storage.py b/sequence/utils/metrics/storage.py index ac6e85048..8edf16fd4 100644 --- a/sequence/utils/metrics/storage.py +++ b/sequence/utils/metrics/storage.py @@ -13,7 +13,7 @@ class TimeProvider(Protocol): def now(self) -> int: """Return the current timestamp in picoseconds.""" - ... + raise NotImplementedError class SystemTimeProvider: From 0e98bf72fe6197f068526acb8de74209f05ff783 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Wed, 1 Jul 2026 13:11:05 -0500 Subject: [PATCH 074/118] Remove TimeProvider abstraction, provide Timeline directly --- sequence/utils/metrics/__init__.py | 8 ++++---- sequence/utils/metrics/storage.py | 23 +---------------------- 2 files changed, 5 insertions(+), 26 deletions(-) diff --git a/sequence/utils/metrics/__init__.py b/sequence/utils/metrics/__init__.py index 84a3dd821..7598577d9 100644 --- a/sequence/utils/metrics/__init__.py +++ b/sequence/utils/metrics/__init__.py @@ -9,6 +9,7 @@ import math from statistics import mean, stdev from typing import Any +from ...kernel.timeline import Timeline from . import builtins from .event_types import ( @@ -34,17 +35,16 @@ reset_metrics, unregister_metric, ) -from .storage import InMemoryStorage, SystemTimeProvider, TimeProvider +from .storage import InMemoryStorage _enabled = False _enabled_events: set[EventType] = set() storage = InMemoryStorage() -_system_time_provider = SystemTimeProvider() -time_provider: TimeProvider = _system_time_provider +time_provider: Timeline | None = None -def register_time_provider(provider: TimeProvider) -> None: +def register_time_provider(provider: Timeline) -> None: """Register the active time source for recorded events. Args: diff --git a/sequence/utils/metrics/storage.py b/sequence/utils/metrics/storage.py index 8edf16fd4..7b321b2fb 100644 --- a/sequence/utils/metrics/storage.py +++ b/sequence/utils/metrics/storage.py @@ -2,32 +2,11 @@ from __future__ import annotations -from time import monotonic_ns -from typing import Any, Protocol +from typing import Any from .event_types import EventType -class TimeProvider(Protocol): - """Protocol for objects that supply a timestamp for recorded events.""" - - def now(self) -> int: - """Return the current timestamp in picoseconds.""" - raise NotImplementedError - - -class SystemTimeProvider: - """Fallback time source using the current system clock.""" - - def now(self) -> int: - """Return the current system time in picoseconds. - - Returns: - Current wall-clock time from ``time_ns()``. - """ - return monotonic_ns() * 1000 - - class InMemoryStorage: """Store metric records in memory for the lifetime of the simulation.""" From 91b3e63da0600257aefd48756076d97ed97efa5c Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Wed, 1 Jul 2026 13:33:48 -0500 Subject: [PATCH 075/118] Fix test metrics to use timeline time provider --- tests/utils/test_metrics.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/tests/utils/test_metrics.py b/tests/utils/test_metrics.py index f5502741b..1365522e2 100644 --- a/tests/utils/test_metrics.py +++ b/tests/utils/test_metrics.py @@ -2,6 +2,7 @@ import pytest +from sequence.kernel.timeline import Timeline from sequence.utils import metrics from sequence.utils.metrics import CounterMetric from sequence.utils.metrics.event_types import EventTypes @@ -13,7 +14,7 @@ def reset_metrics_state(): metrics._enabled_events.clear() metrics.storage.clear() metrics.reset_metrics() - metrics.register_time_provider(metrics._system_time_provider) + tl = Timeline(int(1e12)) def test_record_before_enable_is_noop(): @@ -118,16 +119,6 @@ def test_storage_query_helpers(): assert len(metrics.storage.get_by_owner("e1")) == 1 -def test_default_time_provider_uses_system_time(monkeypatch): - monkeypatch.setattr(metrics._system_time_provider, "now", lambda: 42) - metrics.register_time_provider(metrics._system_time_provider) - metrics.enable([EventTypes.EG_SUCCESS]) - - metrics.record(EventTypes.EG_SUCCESS, "e0", fidelity=0.9) - - assert metrics.storage.get_all()[0]["sim_time"] == 42 - - def test_register_time_provider_uses_registered_source(): class StubTimeProvider: def now(self) -> int: From 79c0fbca64d0faa4f769a31c1de3e40d24c2f605 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Wed, 1 Jul 2026 13:36:41 -0500 Subject: [PATCH 076/118] Potential fix for pull request finding 'Unused local variable' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- tests/utils/test_metrics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/utils/test_metrics.py b/tests/utils/test_metrics.py index 1365522e2..f0063dd23 100644 --- a/tests/utils/test_metrics.py +++ b/tests/utils/test_metrics.py @@ -14,7 +14,7 @@ def reset_metrics_state(): metrics._enabled_events.clear() metrics.storage.clear() metrics.reset_metrics() - tl = Timeline(int(1e12)) + Timeline(int(1e12)) def test_record_before_enable_is_noop(): From c3830c13d98d6665c0a6fd8ea1e8d8c93f338e22 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Wed, 1 Jul 2026 13:40:44 -0500 Subject: [PATCH 077/118] Type checking guard --- sequence/utils/metrics/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sequence/utils/metrics/__init__.py b/sequence/utils/metrics/__init__.py index 7598577d9..ddaac7d19 100644 --- a/sequence/utils/metrics/__init__.py +++ b/sequence/utils/metrics/__init__.py @@ -8,8 +8,10 @@ import math from statistics import mean, stdev -from typing import Any -from ...kernel.timeline import Timeline +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from ...kernel.timeline import Timeline from . import builtins from .event_types import ( From 9ccfa8dbd006817253ea6a4db47b3b7bfb906224 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Wed, 1 Jul 2026 15:03:29 -0500 Subject: [PATCH 078/118] Revert "Changed ruff formatter line length to 120" This reverts commit ea6e172e0d5172df2120bb708e7c55d5d0fd63a0. --- pyproject.toml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 883b5c315..2a4aea396 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,10 +76,3 @@ Homepage = "https://github.com/sequence-toolbox/SeQUeNCe" Documentation = "https://sequence-rtd-tutorial.readthedocs.io/" Issues = "https://github.com/sequence-toolbox/SeQUeNCe/issues" Changelog = "https://github.com/sequence-toolbox/SeQUeNCe/blob/master/CHANGELOG.md" - -[tool.ruff] -line-length = 120 - -[tool.ruff.lint.pycodestyle] -max-line-length = 120 - From 7169459d0e7c9f6c99e92e4966084a63ea3c26c1 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Wed, 1 Jul 2026 15:05:34 -0500 Subject: [PATCH 079/118] Target pairs defaults to None --- sequence/utils/metrics/metric_types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sequence/utils/metrics/metric_types.py b/sequence/utils/metrics/metric_types.py index d3d54e8fb..6bf8c63cc 100644 --- a/sequence/utils/metrics/metric_types.py +++ b/sequence/utils/metrics/metric_types.py @@ -26,7 +26,7 @@ class CollectContext: owner_name: str storage: InMemoryStorage delivery_owner: str | None = None - target_pairs: int = 500 + target_pairs: int | None = None reservation_start_time: int | None = None throughput: float | None = None From 93c420a69d701614d24465a315a27cf980ff79e3 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Wed, 1 Jul 2026 15:07:02 -0500 Subject: [PATCH 080/118] Target pairs defaults to None --- sequence/utils/metrics/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sequence/utils/metrics/__init__.py b/sequence/utils/metrics/__init__.py index ddaac7d19..868429c15 100644 --- a/sequence/utils/metrics/__init__.py +++ b/sequence/utils/metrics/__init__.py @@ -112,7 +112,7 @@ def collect_trial_metrics( owner_name: str, *, delivery_owner: str | None = None, - target_pairs: int = 500, + target_pairs: int | None = None, reservation_start_time: int | None = None, throughput: float | None = None, ) -> dict[str, Any]: From 861502f312612081c7cc190cfb6fafc375d7d29c Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Wed, 1 Jul 2026 15:08:07 -0500 Subject: [PATCH 081/118] Create a better dummy timeline for the tests --- tests/utils/test_metrics.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/tests/utils/test_metrics.py b/tests/utils/test_metrics.py index f0063dd23..ad631bfa8 100644 --- a/tests/utils/test_metrics.py +++ b/tests/utils/test_metrics.py @@ -120,11 +120,9 @@ def test_storage_query_helpers(): def test_register_time_provider_uses_registered_source(): - class StubTimeProvider: - def now(self) -> int: - return 12345 - - metrics.register_time_provider(StubTimeProvider()) + timeline = Timeline(int(1e12)) + timeline.time = 12345 + metrics.register_time_provider(timeline) metrics.enable([EventTypes.EG_SUCCESS]) metrics.record(EventTypes.EG_SUCCESS, "e0", fidelity=0.9) @@ -281,17 +279,17 @@ def test_delivery_does_not_affect_ep_counters(): def test_collect_trial_metrics_ep_fields_and_delivery_time(): - class StubTimeProvider: + class AdvancingTimeline(Timeline): def __init__(self) -> None: - self._time = int(1e12) + super().__init__(int(1e12)) + self.time = int(1e12) def now(self) -> int: - current = self._time - self._time += int(1e11) + current = self.time + self.time += int(1e11) return current - provider = StubTimeProvider() - metrics.register_time_provider(provider) + metrics.register_time_provider(AdvancingTimeline()) metrics.enable([EventTypes.EP_SUCCESS, EventTypes.DELIVERY]) metrics.record(EventTypes.EP_SUCCESS, "left", fidelity=0.7) From 695b96045b77c9358099afc769ddb0e307f39592 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Wed, 1 Jul 2026 15:11:46 -0500 Subject: [PATCH 082/118] Fixed target pairs logic check in DeliveryTimeMetric --- sequence/utils/metrics/metric_types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sequence/utils/metrics/metric_types.py b/sequence/utils/metrics/metric_types.py index 6bf8c63cc..2dded654d 100644 --- a/sequence/utils/metrics/metric_types.py +++ b/sequence/utils/metrics/metric_types.py @@ -271,7 +271,7 @@ def collect(self, ctx: CollectContext) -> dict[str, Any]: if self.delivery_event is not None and record["event_type"] is self.delivery_event ] delivery_records.sort(key=lambda record: record["sim_time"]) - if len(delivery_records) < ctx.target_pairs: + if ctx.target_pairs is None or len(delivery_records) < ctx.target_pairs: return {self.key: float("nan")} if ctx.reservation_start_time is None: return {self.key: float("nan")} From a9fe29a5ce01c8d47ebefe59b2d6b416323f03d8 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Wed, 1 Jul 2026 15:18:08 -0500 Subject: [PATCH 083/118] Better descriptions for optional metric methods (+pass) --- sequence/utils/metrics/metric_types.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/sequence/utils/metrics/metric_types.py b/sequence/utils/metrics/metric_types.py index 2dded654d..4110991c5 100644 --- a/sequence/utils/metrics/metric_types.py +++ b/sequence/utils/metrics/metric_types.py @@ -55,11 +55,17 @@ def output_keys(self) -> frozenset[str]: def on_record(self, event_type: EventType, owner_name: str, kwargs: dict[str, Any]) -> None: """Update metric state when a matching event is recorded. + This method is called for every metric when it is recorded. + Default implementation is a no-op. + This method is completely optional for any metric subclasses. + Should only be implemented if you need a hook when a metric is recorded. + Args: event_type: Type of the recorded event. owner_name: Name of the node or component that owns the event. kwargs: Mutable event payload; metrics may add derived fields. """ + pass @abstractmethod def collect(self, ctx: CollectContext) -> dict[str, Any]: @@ -73,7 +79,14 @@ def collect(self, ctx: CollectContext) -> dict[str, Any]: """ def reset(self) -> None: - """Clear per-trial metric state.""" + """Clear per-trial metric state. + + This method is called for every metric by default when all metrics are reset. + Default implementation is a no-op. + This method is completely optional for any metric subclasses. + Should only be implemented if you need a hook when the metric is reset. + """ + pass @dataclass From ad15cd68c15f0adac71c320c269be5352a7f0796 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Wed, 1 Jul 2026 15:20:09 -0500 Subject: [PATCH 084/118] Mean and STD method provided to clean up aggregation code --- sequence/utils/metrics/__init__.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/sequence/utils/metrics/__init__.py b/sequence/utils/metrics/__init__.py index 868429c15..6be4c5747 100644 --- a/sequence/utils/metrics/__init__.py +++ b/sequence/utils/metrics/__init__.py @@ -142,6 +142,13 @@ def collect_trial_metrics( return result +def _mean_and_std(values: list[float]) -> tuple[float, float]: + finite_values = [value for value in values if math.isfinite(value)] + if not finite_values: + return float("nan"), float("nan") + return mean(finite_values), stdev(finite_values) if len(finite_values) > 1 else 0.0 + + def aggregate_trial_metrics( trials: list[dict[str, Any]], *, @@ -164,14 +171,10 @@ def aggregate_trial_metrics( list_metrics_keys = [key for key, value in trials[0].items() if isinstance(value, list)] for metric in scalar_metrics: - values = [trial[metric] for trial in trials] - finite_values = [value for value in values if isinstance(value, (int, float)) and math.isfinite(value)] - if finite_values: - aggregated[f"avg_{metric}"] = mean(finite_values) - aggregated[f"std_{metric}"] = stdev(finite_values) if len(finite_values) > 1 else 0.0 - else: - aggregated[f"avg_{metric}"] = float("nan") - aggregated[f"std_{metric}"] = float("nan") + values = [float(trial[metric]) for trial in trials if isinstance(trial[metric], (int, float))] + avg, std = _mean_and_std(values) + aggregated[f"avg_{metric}"] = avg + aggregated[f"std_{metric}"] = std for metric in list_metrics_keys: all_values: list[float] = [] @@ -180,12 +183,9 @@ def aggregate_trial_metrics( if list_metric_cap is not None: trial_values = trial_values[:list_metric_cap] all_values.extend(trial_values) - if all_values: - aggregated[f"avg_{metric}"] = mean(all_values) - aggregated[f"std_{metric}"] = stdev(all_values) if len(all_values) > 1 else 0.0 - else: - aggregated[f"avg_{metric}"] = float("nan") - aggregated[f"std_{metric}"] = float("nan") + avg, std = _mean_and_std(all_values) + aggregated[f"avg_{metric}"] = avg + aggregated[f"std_{metric}"] = std return aggregated From 28eb0628df4763fab141a1b651e1e3e75ef76ed5 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Wed, 1 Jul 2026 15:52:54 -0500 Subject: [PATCH 085/118] Moved BUILTIN_METRICS scope --- sequence/utils/metrics/builtins.py | 31 ++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/sequence/utils/metrics/builtins.py b/sequence/utils/metrics/builtins.py index c870e1039..59361c2e7 100644 --- a/sequence/utils/metrics/builtins.py +++ b/sequence/utils/metrics/builtins.py @@ -3,7 +3,13 @@ from __future__ import annotations from .event_types import EventTypes -from .metric_types import CounterMetric, DeliveryTimeMetric, FidelityMetric, RateMetric +from .metric_types import ( + CounterMetric, + DeliveryTimeMetric, + FidelityMetric, + Metric, + RateMetric, +) from .registry import register_metric EG_METRIC = CounterMetric( @@ -40,16 +46,6 @@ field="fidelity", ) -_BUILTIN_METRICS = ( - EG_METRIC, - EP_METRIC, - THROUGHPUT_METRIC, - PURIFIED_FIDELITIES_METRIC, - DELIVERY_TIME_METRIC, - ES_METRIC, - SWAPPED_FIDELITIES_METRIC, -) - def register_builtin_metrics() -> None: """Register all built-in metrics with the global registry. @@ -57,5 +53,16 @@ def register_builtin_metrics() -> None: Registers entanglement generation, purification, swapping, throughput, fidelity, and delivery-time metrics defined in this module. """ - for metric in _BUILTIN_METRICS: + + BUILTIN_METRICS: list[Metric] = [ + EG_METRIC, + EP_METRIC, + THROUGHPUT_METRIC, + PURIFIED_FIDELITIES_METRIC, + DELIVERY_TIME_METRIC, + ES_METRIC, + SWAPPED_FIDELITIES_METRIC, + ] + + for metric in BUILTIN_METRICS: register_metric(metric) From e08ef9d834823389bf182615ab8d419bced93f9e Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Wed, 1 Jul 2026 15:56:32 -0500 Subject: [PATCH 086/118] Converted `is` keyword to == to prevent missed matches Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- sequence/utils/metrics/metric_types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sequence/utils/metrics/metric_types.py b/sequence/utils/metrics/metric_types.py index 4110991c5..cbd097969 100644 --- a/sequence/utils/metrics/metric_types.py +++ b/sequence/utils/metrics/metric_types.py @@ -281,7 +281,7 @@ def collect(self, ctx: CollectContext) -> dict[str, Any]: delivery_records = [ record for record in ctx.storage.get_by_owner(delivery_owner) - if self.delivery_event is not None and record["event_type"] is self.delivery_event + if self.delivery_event is not None and record["event_type"] == self.delivery_event ] delivery_records.sort(key=lambda record: record["sim_time"]) if ctx.target_pairs is None or len(delivery_records) < ctx.target_pairs: From 2a0af52c715c7744e08b4d5d35651187a54f2efb Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Wed, 1 Jul 2026 15:56:53 -0500 Subject: [PATCH 087/118] Converted `is` keyword to == to prevent missed matches Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- sequence/utils/metrics/metric_types.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sequence/utils/metrics/metric_types.py b/sequence/utils/metrics/metric_types.py index cbd097969..640f951a9 100644 --- a/sequence/utils/metrics/metric_types.py +++ b/sequence/utils/metrics/metric_types.py @@ -160,10 +160,10 @@ def on_record(self, event_type: EventType, owner_name: str, kwargs: dict[str, An owner_name: Name of the node or component that owns the event. kwargs: Event payload; updated with the current success rate. """ - if event_type is self.failure_event: + if event_type == self.failure_event: self._failures[owner_name] = self._failures.get(owner_name, 0) + 1 kwargs[self.rate_field] = self.success_rate(owner_name) - elif event_type is self.success_event: + elif event_type == self.success_event: self._successes[owner_name] = self._successes.get(owner_name, 0) + 1 kwargs[self.rate_field] = self.success_rate(owner_name) From c9b791339e9e1649974d0208ebe29f694a10f97f Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Wed, 1 Jul 2026 15:57:12 -0500 Subject: [PATCH 088/118] Converted `is` keyword to == to prevent missed matches Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- sequence/utils/metrics/storage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sequence/utils/metrics/storage.py b/sequence/utils/metrics/storage.py index 7b321b2fb..16b6bfee6 100644 --- a/sequence/utils/metrics/storage.py +++ b/sequence/utils/metrics/storage.py @@ -39,7 +39,7 @@ def get_by_event(self, event_type: EventType) -> list[dict[str, Any]]: Returns: Records whose ``event_type`` matches the given value. """ - return [record for record in self._records if record["event_type"] is event_type] + return [record for record in self._records if record["event_type"] == event_type] def get_by_owner(self, owner_name: str) -> list[dict[str, Any]]: """Return records for a given owner. From 34f40ba91d8e42c92101f66469bc1706ed3dab64 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Wed, 1 Jul 2026 15:58:02 -0500 Subject: [PATCH 089/118] Converted `is` keyword to == to prevent missed matches Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- sequence/utils/metrics/metric_types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sequence/utils/metrics/metric_types.py b/sequence/utils/metrics/metric_types.py index 640f951a9..6d36df04e 100644 --- a/sequence/utils/metrics/metric_types.py +++ b/sequence/utils/metrics/metric_types.py @@ -244,7 +244,7 @@ def collect(self, ctx: CollectContext) -> dict[str, Any]: values = [ record[self.field] for record in ctx.storage.get_by_owner(ctx.owner_name) - if record["event_type"] is self.event and self.field in record + if record["event_type"] == self.event and self.field in record ] return {self.key: values} From 53daeec2f9f3878eff71368e7f6523f7e57b400d Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Wed, 1 Jul 2026 15:58:52 -0500 Subject: [PATCH 090/118] Added error for missing storage types Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- sequence/utils/metrics/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sequence/utils/metrics/__init__.py b/sequence/utils/metrics/__init__.py index 6be4c5747..5a65724ed 100644 --- a/sequence/utils/metrics/__init__.py +++ b/sequence/utils/metrics/__init__.py @@ -80,7 +80,8 @@ def configure(storage_type: str = "in_memory") -> None: if storage_type == "in_memory": storage = InMemoryStorage() reset_metrics() - + return + raise ValueError(f"Unknown storage_type '{storage_type}'. Supported: 'in_memory'.") def record(event_type: EventType, owner_name: str, **kwargs: Any) -> None: """Record a metrics event if metrics are enabled for this event type. From a93ce16d3067c2e6fb6205f5540c4029644294d9 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Wed, 1 Jul 2026 16:05:56 -0500 Subject: [PATCH 091/118] Fixed typing for storage attribute --- sequence/utils/metrics/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sequence/utils/metrics/__init__.py b/sequence/utils/metrics/__init__.py index 5a65724ed..ca971b6b0 100644 --- a/sequence/utils/metrics/__init__.py +++ b/sequence/utils/metrics/__init__.py @@ -42,7 +42,7 @@ _enabled = False _enabled_events: set[EventType] = set() -storage = InMemoryStorage() +storage: InMemoryStorage = InMemoryStorage() time_provider: Timeline | None = None From eed31a117da9ab423c3f1ffe3335ea577fbff6fd Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Thu, 2 Jul 2026 12:40:16 -0500 Subject: [PATCH 092/118] Return of the __all__ list of exported symbols --- sequence/utils/metrics/__init__.py | 34 ++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/sequence/utils/metrics/__init__.py b/sequence/utils/metrics/__init__.py index ca971b6b0..49b86d919 100644 --- a/sequence/utils/metrics/__init__.py +++ b/sequence/utils/metrics/__init__.py @@ -83,6 +83,7 @@ def configure(storage_type: str = "in_memory") -> None: return raise ValueError(f"Unknown storage_type '{storage_type}'. Supported: 'in_memory'.") + def record(event_type: EventType, owner_name: str, **kwargs: Any) -> None: """Record a metrics event if metrics are enabled for this event type. @@ -192,3 +193,36 @@ def aggregate_trial_metrics( builtins.register_builtin_metrics() + +# Exported symbols +__all__ = [ + # From event_types + "EventType", + "EventTypes", + "get_event_type", + "list_event_types", + "register_event_type", + # From metric_types + "CollectContext", + "CounterMetric", + "DeliveryTimeMetric", + "FidelityMetric", + "Metric", + "RateMetric", + # From registry + "clear_registry", + "get_counter", + "list_metrics", + "register_metric", + "reset_metrics", + "unregister_metric", + # From storage + "InMemoryStorage", + # Top-level symbols in this module + "register_time_provider", + "enable", + "configure", + "record", + "collect_trial_metrics", + "aggregate_trial_metrics", +] From 5507e57ae97865d83af6842c458fdd7150fd9167 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Mon, 6 Jul 2026 11:48:46 -0500 Subject: [PATCH 093/118] Replace double backtick to single backtick in docstrings --- sequence/utils/metrics/__init__.py | 6 +++--- sequence/utils/metrics/metric_types.py | 12 ++++++------ sequence/utils/metrics/storage.py | 6 +++--- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/sequence/utils/metrics/__init__.py b/sequence/utils/metrics/__init__.py index 49b86d919..d784ed9eb 100644 --- a/sequence/utils/metrics/__init__.py +++ b/sequence/utils/metrics/__init__.py @@ -71,7 +71,7 @@ def configure(storage_type: str = "in_memory") -> None: """Configure metrics storage. Available storage options: - - "in_memory": The default, uses ``InMemoryStorage``. + - "in_memory": The default, uses `InMemoryStorage`. Args: storage_type: Storage backend identifier. @@ -122,7 +122,7 @@ def collect_trial_metrics( Args: owner_name: Node name to collect counter and fidelity metrics for. - delivery_owner: Node name used for delivery-time metrics; defaults to ``owner_name``. + delivery_owner: Node name used for delivery-time metrics; defaults to `owner_name`. target_pairs: Number of delivered pairs required to compute delivery time. reservation_start_time: Simulation time when the reservation started (ps). throughput: Application throughput to include in collected metrics. @@ -159,7 +159,7 @@ def aggregate_trial_metrics( """Aggregate trial metrics across multiple trials. Args: - trials: Per-trial metric dictionaries from ``collect_trial_metrics``. + trials: Per-trial metric dictionaries from `collect_trial_metrics`. list_metric_cap: Maximum list elements per trial to include when aggregating list metrics. Returns: diff --git a/sequence/utils/metrics/metric_types.py b/sequence/utils/metrics/metric_types.py index 6d36df04e..803a08669 100644 --- a/sequence/utils/metrics/metric_types.py +++ b/sequence/utils/metrics/metric_types.py @@ -17,7 +17,7 @@ class CollectContext: Attributes: owner_name: Node name for counter and fidelity metrics. storage: In-memory store of recorded events for the trial. - delivery_owner: Node name for delivery-time metrics; defaults to ``owner_name``. + delivery_owner: Node name for delivery-time metrics; defaults to `owner_name`. target_pairs: Number of delivered pairs required to compute delivery time. reservation_start_time: Simulation time when the reservation started (ps). throughput: Application throughput supplied at collection time. @@ -40,7 +40,7 @@ def event_types(self) -> frozenset[EventType]: """Event types this metric reacts to during recording. Returns: - Frozen set of event types handled by ``on_record``. + Frozen set of event types handled by `on_record`. """ @property @@ -56,7 +56,7 @@ def on_record(self, event_type: EventType, owner_name: str, kwargs: dict[str, An """Update metric state when a matching event is recorded. This method is called for every metric when it is recorded. - Default implementation is a no-op. + Default implementation is a no-op. This method is completely optional for any metric subclasses. Should only be implemented if you need a hook when a metric is recorded. @@ -80,9 +80,9 @@ def collect(self, ctx: CollectContext) -> dict[str, Any]: def reset(self) -> None: """Clear per-trial metric state. - + This method is called for every metric by default when all metrics are reset. - Default implementation is a no-op. + Default implementation is a no-op. This method is completely optional for any metric subclasses. Should only be implemented if you need a hook when the metric is reset. """ @@ -206,7 +206,7 @@ def collect(self, ctx: CollectContext) -> dict[str, Any]: """Return the throughput value supplied at collection time. Args: - ctx: Collection context; uses ``throughput`` when set. + ctx: Collection context; uses `throughput` when set. Returns: Mapping with the configured rate key and throughput or NaN. diff --git a/sequence/utils/metrics/storage.py b/sequence/utils/metrics/storage.py index 16b6bfee6..01fa7a830 100644 --- a/sequence/utils/metrics/storage.py +++ b/sequence/utils/metrics/storage.py @@ -26,7 +26,7 @@ def get_all(self) -> list[dict[str, Any]]: """Return all stored metric records. Returns: - Copy of every record appended since construction or last ``clear()``. + Copy of every record appended since construction or last `clear()`. """ return list(self._records) @@ -37,7 +37,7 @@ def get_by_event(self, event_type: EventType) -> list[dict[str, Any]]: event_type: Event type to filter on. Returns: - Records whose ``event_type`` matches the given value. + Records whose `event_type` matches the given value. """ return [record for record in self._records if record["event_type"] == event_type] @@ -48,7 +48,7 @@ def get_by_owner(self, owner_name: str) -> list[dict[str, Any]]: owner_name: Owner name to filter on. Returns: - Records whose ``owner_name`` matches the given value. + Records whose `owner_name` matches the given value. """ return [record for record in self._records if record["owner_name"] == owner_name] From e1852f779941a19f04cbf998d0148e6b35186ea5 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Mon, 6 Jul 2026 11:49:41 -0500 Subject: [PATCH 094/118] Update CollectContext docstring for incorrect default --- sequence/utils/metrics/metric_types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sequence/utils/metrics/metric_types.py b/sequence/utils/metrics/metric_types.py index 803a08669..94056c30d 100644 --- a/sequence/utils/metrics/metric_types.py +++ b/sequence/utils/metrics/metric_types.py @@ -17,7 +17,7 @@ class CollectContext: Attributes: owner_name: Node name for counter and fidelity metrics. storage: In-memory store of recorded events for the trial. - delivery_owner: Node name for delivery-time metrics; defaults to `owner_name`. + delivery_owner: Node name for delivery-time metrics. target_pairs: Number of delivered pairs required to compute delivery time. reservation_start_time: Simulation time when the reservation started (ps). throughput: Application throughput supplied at collection time. From b209dfc5072274aeac431e5ef48aee19d57cd657 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Fri, 26 Jun 2026 14:11:15 -0500 Subject: [PATCH 095/118] Test methods for the network manager --- .../test_network_manager.py | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/network_management/test_network_manager.py b/tests/network_management/test_network_manager.py index e10941795..841cca71d 100644 --- a/tests/network_management/test_network_manager.py +++ b/tests/network_management/test_network_manager.py @@ -11,6 +11,7 @@ from sequence.network_management.memory_timecard import MemoryTimeCard from sequence.protocol import StackProtocol from sequence.topology.node import QuantumRouter, BSMNode +from sequence.utils import metrics import pytest class DistributedQuantumRouter(QuantumRouter): @@ -64,6 +65,7 @@ def test_node(tl): @pytest.fixture def mock_reservation(test_node): reservation = Mock(spec=Reservation) + reservation.identity = 0 reservation.initiator = 'n1' reservation.responder = 'n2' reservation.path = ['n1', 'n2'] @@ -71,9 +73,18 @@ def mock_reservation(test_node): reservation.end_time = 3e12 reservation.memory_size = 50 reservation.fidelity = 1 + reservation.entanglement_number = 1 return reservation +@pytest.fixture(autouse=True) +def reset_metrics_state(): + metrics._enabled = False + metrics._enabled_events.clear() + metrics.storage.clear() + metrics.reset_metrics() + + # Test the Network Manager basics class TestNetworkManager: @@ -162,6 +173,37 @@ def test_pop_approve_intermediate(self, test_node, mock_reservation): test_node.get_reservation_result.assert_not_called() test_node.get_other_reservation.assert_not_called() + @pytest.mark.unit + def test_pop_approve_records_reservation_approved(self, test_node, mock_reservation): + metrics.enable([metrics.RESERVATION_APPROVED]) + mock_reservation.initiator = test_node.name + inbound_msg = Mock() + inbound_msg.msg_type = RSVPMsgType.APPROVE + inbound_msg.reservation = mock_reservation + + test_node.network_manager.pop(msg=inbound_msg) + + records = metrics.storage.get_by_event(metrics.RESERVATION_APPROVED) + assert len(records) == 1 + assert records[0]["owner_name"] == test_node.name + assert records[0]["identity"] == mock_reservation.identity + assert records[0]["path"] == mock_reservation.path + + @pytest.mark.unit + def test_pop_reject_records_reservation_rejected(self, test_node, mock_reservation): + metrics.enable([metrics.RESERVATION_REJECTED]) + mock_reservation.initiator = test_node.name + inbound_msg = Mock() + inbound_msg.msg_type = RSVPMsgType.REJECT + inbound_msg.reservation = mock_reservation + + test_node.network_manager.pop(msg=inbound_msg) + + records = metrics.storage.get_by_event(metrics.RESERVATION_REJECTED) + assert len(records) == 1 + assert records[0]["owner_name"] == test_node.name + assert records[0]["path"] == [] + def test_NetworkManager_push(self, test_node, mock_reservation): outbound_msg = Mock() test_node.send_out = False From b6fb0e3edcf6d43369ec73ab83c36959ab66109e Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Tue, 30 Jun 2026 12:55:25 -0500 Subject: [PATCH 096/118] Renaming a lot of the new variables after changes to upstream metrics branch --- .../network_management/network_manager.py | 4 ++-- sequence/utils/metrics/__init__.py | 2 +- sequence/utils/metrics/builtins.py | 2 +- .../test_network_manager.py | 8 +++---- tests/utils/test_metrics.py | 24 +++++++++---------- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/sequence/network_management/network_manager.py b/sequence/network_management/network_manager.py index 7da0b076f..f884db23c 100644 --- a/sequence/network_management/network_manager.py +++ b/sequence/network_management/network_manager.py @@ -227,7 +227,7 @@ def pop(self, msg: RSVPMessage): reservation: Reservation = msg.reservation if msg.msg_type == RSVPMsgType.APPROVE: metrics.record( - metrics.RESERVATION_APPROVED, + EventTypes.RESERVATION_APPROVED, self.owner.name, identity=reservation.identity, initiator=reservation.initiator, @@ -246,7 +246,7 @@ def pop(self, msg: RSVPMessage): self.owner.get_other_reservation(reservation) elif msg.msg_type == RSVPMsgType.REJECT: metrics.record( - metrics.RESERVATION_REJECTED, + EventTypes.RESERVATION_REJECTED, self.owner.name, identity=reservation.identity, initiator=reservation.initiator, diff --git a/sequence/utils/metrics/__init__.py b/sequence/utils/metrics/__init__.py index 27a046bb6..dd3b9d397 100644 --- a/sequence/utils/metrics/__init__.py +++ b/sequence/utils/metrics/__init__.py @@ -114,7 +114,7 @@ def collect_reservation_data(owner_name: str | None = None) -> list[list]: """Collect per-reservation tabular metrics from recorded delivery events.""" from collections import defaultdict - records = storage.get_by_event(PURIFIED_DELIVERY) + records = storage.get_by_event(DELIVERY) if owner_name is not None: records = [record for record in records if record["owner_name"] == owner_name] diff --git a/sequence/utils/metrics/builtins.py b/sequence/utils/metrics/builtins.py index dc69ff385..bde732cba 100644 --- a/sequence/utils/metrics/builtins.py +++ b/sequence/utils/metrics/builtins.py @@ -59,7 +59,7 @@ field="fidelity", ) RESERVATION_DELIVERY_METRIC = ReservationDeliveryMetric( - delivery_event=PURIFIED_DELIVERY, + delivery_event=EventTypes.DELIVERY, ) _BUILTIN_METRICS = ( diff --git a/tests/network_management/test_network_manager.py b/tests/network_management/test_network_manager.py index 841cca71d..f9685814a 100644 --- a/tests/network_management/test_network_manager.py +++ b/tests/network_management/test_network_manager.py @@ -175,7 +175,7 @@ def test_pop_approve_intermediate(self, test_node, mock_reservation): @pytest.mark.unit def test_pop_approve_records_reservation_approved(self, test_node, mock_reservation): - metrics.enable([metrics.RESERVATION_APPROVED]) + metrics.enable([EventTypes.RESERVATION_APPROVED]) mock_reservation.initiator = test_node.name inbound_msg = Mock() inbound_msg.msg_type = RSVPMsgType.APPROVE @@ -183,7 +183,7 @@ def test_pop_approve_records_reservation_approved(self, test_node, mock_reservat test_node.network_manager.pop(msg=inbound_msg) - records = metrics.storage.get_by_event(metrics.RESERVATION_APPROVED) + records = metrics.storage.get_by_event(EventTypes.RESERVATION_APPROVED) assert len(records) == 1 assert records[0]["owner_name"] == test_node.name assert records[0]["identity"] == mock_reservation.identity @@ -191,7 +191,7 @@ def test_pop_approve_records_reservation_approved(self, test_node, mock_reservat @pytest.mark.unit def test_pop_reject_records_reservation_rejected(self, test_node, mock_reservation): - metrics.enable([metrics.RESERVATION_REJECTED]) + metrics.enable([EventTypes.RESERVATION_REJECTED]) mock_reservation.initiator = test_node.name inbound_msg = Mock() inbound_msg.msg_type = RSVPMsgType.REJECT @@ -199,7 +199,7 @@ def test_pop_reject_records_reservation_rejected(self, test_node, mock_reservati test_node.network_manager.pop(msg=inbound_msg) - records = metrics.storage.get_by_event(metrics.RESERVATION_REJECTED) + records = metrics.storage.get_by_event(EventTypes.RESERVATION_REJECTED) assert len(records) == 1 assert records[0]["owner_name"] == test_node.name assert records[0]["path"] == [] diff --git a/tests/utils/test_metrics.py b/tests/utils/test_metrics.py index c1a2190be..bfff500b3 100644 --- a/tests/utils/test_metrics.py +++ b/tests/utils/test_metrics.py @@ -425,9 +425,9 @@ def test_register_metric_rejects_duplicate_output_keys(): def test_reservation_approved_records_metadata(): - metrics.enable([metrics.RESERVATION_APPROVED]) + metrics.enable([EventTypes.RESERVATION_APPROVED]) metrics.record( - metrics.RESERVATION_APPROVED, + EventTypes.RESERVATION_APPROVED, "n1", identity=1, initiator="n1", @@ -446,9 +446,9 @@ def test_reservation_approved_records_metadata(): def test_reservation_rejected_records_metadata(): - metrics.enable([metrics.RESERVATION_REJECTED]) + metrics.enable([EventTypes.RESERVATION_REJECTED]) metrics.record( - metrics.RESERVATION_REJECTED, + EventTypes.RESERVATION_REJECTED, "n1", identity=2, initiator="n1", @@ -461,12 +461,12 @@ def test_reservation_rejected_records_metadata(): path=[], ) record = metrics.storage.get_all()[0] - assert record["event_type"] is metrics.RESERVATION_REJECTED + assert record["event_type"] is EventTypes.RESERVATION_REJECTED assert record["path"] == [] def test_purified_delivery_assigns_pair_index(): - metrics.enable([metrics.PURIFIED_DELIVERY]) + metrics.enable([EventTypes.DELIVERY]) kwargs = { "identity": 7, "initiator": "n1", @@ -479,9 +479,9 @@ def test_purified_delivery_assigns_pair_index(): "path": ["n1", "n2"], "fidelity": 0.91, } - metrics.record(metrics.PURIFIED_DELIVERY, "n1", **kwargs) - metrics.record(metrics.PURIFIED_DELIVERY, "n1", **{**kwargs, "fidelity": 0.92}) - metrics.record(metrics.PURIFIED_DELIVERY, "n2", **{**kwargs, "fidelity": 0.93}) + metrics.record(EventTypes.DELIVERY, "n1", **kwargs) + metrics.record(EventTypes.DELIVERY, "n1", **{**kwargs, "fidelity": 0.92}) + metrics.record(EventTypes.DELIVERY, "n2", **{**kwargs, "fidelity": 0.93}) n1_records = metrics.storage.get_by_owner("n1") n2_records = metrics.storage.get_by_owner("n2") @@ -491,7 +491,7 @@ def test_purified_delivery_assigns_pair_index(): def test_collect_reservation_data_produces_expected_row(): - metrics.enable([metrics.PURIFIED_DELIVERY]) + metrics.enable([EventTypes.DELIVERY]) start_time = int(1e12) end_time = int(2e12) base = { @@ -516,8 +516,8 @@ def now(self) -> int: return current metrics.register_time_provider(StubTimeProvider()) - metrics.record(metrics.PURIFIED_DELIVERY, "n1", fidelity=0.91, **base) - metrics.record(metrics.PURIFIED_DELIVERY, "n1", fidelity=0.92, **base) + metrics.record(EventTypes.DELIVERY, "n1", fidelity=0.91, **base) + metrics.record(EventTypes.DELIVERY, "n1", fidelity=0.92, **base) rows = metrics.collect_reservation_data("n1") assert len(rows) == 1 From d870c6f60f11609776fd8877f83a7dd3503d33d7 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Tue, 30 Jun 2026 15:41:01 -0500 Subject: [PATCH 097/118] Renamed variable missed --- sequence/utils/metrics/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sequence/utils/metrics/__init__.py b/sequence/utils/metrics/__init__.py index dd3b9d397..65b36ced7 100644 --- a/sequence/utils/metrics/__init__.py +++ b/sequence/utils/metrics/__init__.py @@ -114,7 +114,7 @@ def collect_reservation_data(owner_name: str | None = None) -> list[list]: """Collect per-reservation tabular metrics from recorded delivery events.""" from collections import defaultdict - records = storage.get_by_event(DELIVERY) + records = storage.get_by_event(EventTypes.DELIVERY) if owner_name is not None: records = [record for record in records if record["owner_name"] == owner_name] From 3e6def66bae652550864c9dce8c233b11124f002 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Tue, 30 Jun 2026 15:47:21 -0500 Subject: [PATCH 098/118] Re-organized start and target time logic for DeliveryTimeMetric --- sequence/utils/metrics/metric_types.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/sequence/utils/metrics/metric_types.py b/sequence/utils/metrics/metric_types.py index a0a575843..bbac86ff7 100644 --- a/sequence/utils/metrics/metric_types.py +++ b/sequence/utils/metrics/metric_types.py @@ -216,11 +216,12 @@ def collect(self, ctx: CollectContext) -> dict[str, Any]: delivery_records.sort(key=lambda record: record["sim_time"]) if len(delivery_records) < ctx.target_pairs: return {self.key: float("nan")} - start_time: float = float("nan") - target_time: float = float("nan") - if delivery_records: - start_time = delivery_records[0].get("start_time") - target_time = delivery_records[ctx.target_pairs - 1]["sim_time"] + + start_time = delivery_records[0].get("start_time") + if start_time is None: + return {self.key: float("nan")} + + target_time = delivery_records[ctx.target_pairs - 1]["sim_time"] return { self.key: (target_time - start_time) * 1e-12, } From 84577301e2f17938515c1b6bf4bc4d28268bb295 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Tue, 30 Jun 2026 15:47:38 -0500 Subject: [PATCH 099/118] Reformatted _pair_counters --- sequence/utils/metrics/metric_types.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/sequence/utils/metrics/metric_types.py b/sequence/utils/metrics/metric_types.py index bbac86ff7..246a22141 100644 --- a/sequence/utils/metrics/metric_types.py +++ b/sequence/utils/metrics/metric_types.py @@ -153,9 +153,7 @@ class ReservationDeliveryMetric(Metric): """Tracks per-reservation pair delivery counts and assigns pair_index on record.""" delivery_event: EventType - _pair_counters: dict[tuple[str, int], int] = field( - default_factory=dict, init=False, repr=False - ) + _pair_counters: dict[tuple[str, int], int] = field(default_factory=dict, init=False, repr=False) @property def event_types(self) -> frozenset[EventType]: From 35fbdd898ce6bddd87b07209a505b2486ac77328 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Tue, 7 Jul 2026 12:49:18 -0500 Subject: [PATCH 100/118] Reformatted init, metric_types, registry --- sequence/utils/metrics/__init__.py | 14 +++----------- sequence/utils/metrics/metric_types.py | 15 ++++----------- sequence/utils/metrics/registry.py | 4 +--- 3 files changed, 8 insertions(+), 25 deletions(-) diff --git a/sequence/utils/metrics/__init__.py b/sequence/utils/metrics/__init__.py index a999fd344..1b044694e 100644 --- a/sequence/utils/metrics/__init__.py +++ b/sequence/utils/metrics/__init__.py @@ -246,19 +246,11 @@ def aggregate_trial_metrics( raise ValueError("Cannot aggregate an empty list of trials") aggregated: dict[str, float] = {} - scalar_metrics = [ - key for key, value in trials[0].items() if not isinstance(value, (list, dict)) - ] - list_metrics_keys = [ - key for key, value in trials[0].items() if isinstance(value, list) - ] + scalar_metrics = [key for key, value in trials[0].items() if not isinstance(value, (list, dict))] + list_metrics_keys = [key for key, value in trials[0].items() if isinstance(value, list)] for metric in scalar_metrics: - values = [ - float(trial[metric]) - for trial in trials - if isinstance(trial[metric], (int, float)) - ] + values = [float(trial[metric]) for trial in trials if isinstance(trial[metric], (int, float))] avg, std = _mean_and_std(values) aggregated[f"avg_{metric}"] = avg aggregated[f"std_{metric}"] = std diff --git a/sequence/utils/metrics/metric_types.py b/sequence/utils/metrics/metric_types.py index 8e60ddaa7..1c43d3b0e 100644 --- a/sequence/utils/metrics/metric_types.py +++ b/sequence/utils/metrics/metric_types.py @@ -50,9 +50,7 @@ def output_keys(self) -> frozenset[str]: Frozen set of keys written into per-trial result dictionaries. """ - def on_record( - self, event_type: EventType, owner_name: str, kwargs: dict[str, Any] - ) -> None: + def on_record(self, event_type: EventType, owner_name: str, kwargs: dict[str, Any]) -> None: """Update metric state when a matching event is recorded. This method is called for every metric when it is recorded. @@ -152,9 +150,7 @@ def success_rate(self, owner_name: str) -> float: return 0.0 return successes / attempts - def on_record( - self, event_type: EventType, owner_name: str, kwargs: dict[str, Any] - ) -> None: + def on_record(self, event_type: EventType, owner_name: str, kwargs: dict[str, Any]) -> None: """Increment failure or success counts and update the rate field in kwargs. Args: @@ -256,9 +252,7 @@ class ReservationDeliveryMetric(Metric): """Tracks per-reservation pair delivery counts and assigns pair_index on record.""" delivery_event: EventType - _pair_counters: dict[tuple[str, int], int] = field( - default_factory=dict, init=False, repr=False - ) + _pair_counters: dict[tuple[str, int], int] = field(default_factory=dict, init=False, repr=False) @property def event_types(self) -> frozenset[EventType]: @@ -323,8 +317,7 @@ def collect(self, ctx: CollectContext) -> dict[str, Any]: delivery_records = [ record for record in ctx.storage.get_by_owner(delivery_owner) - if self.delivery_event is not None - and record["event_type"] == self.delivery_event + if self.delivery_event is not None and record["event_type"] == self.delivery_event ] delivery_records.sort(key=lambda record: record["sim_time"]) if ctx.target_pairs is None or len(delivery_records) < ctx.target_pairs: diff --git a/sequence/utils/metrics/registry.py b/sequence/utils/metrics/registry.py index 3a3438860..dba18244f 100644 --- a/sequence/utils/metrics/registry.py +++ b/sequence/utils/metrics/registry.py @@ -22,9 +22,7 @@ def register_metric(metric: Metric) -> None: for existing in _metrics: overlap = existing.output_keys & new_keys if overlap: - raise ValueError( - f"Metric output keys {sorted(overlap)} already registered." - ) + raise ValueError(f"Metric output keys {sorted(overlap)} already registered.") _metrics.append(metric) if isinstance(metric, CounterMetric): _counters[metric.prefix] = metric From 9aea30796cd90743fd5ce294c1bf4a6fec29423c Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Tue, 7 Jul 2026 12:52:57 -0500 Subject: [PATCH 101/118] Fixed these metrics types imports --- sequence/utils/metrics/builtins.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sequence/utils/metrics/builtins.py b/sequence/utils/metrics/builtins.py index de378c3f1..7f11c2452 100644 --- a/sequence/utils/metrics/builtins.py +++ b/sequence/utils/metrics/builtins.py @@ -4,11 +4,12 @@ from .event_types import EventTypes, register_event_type from .metric_types import ( - DeliveryTimeMetric, CounterMetric, + DeliveryTimeMetric, FidelityMetric, Metric, RateMetric, + ReservationDeliveryMetric, ) from .registry import register_metric From 96f2931268563e4119e8f076049cba6ff46468cd Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Tue, 7 Jul 2026 12:54:47 -0500 Subject: [PATCH 102/118] These were in the wrong location --- sequence/utils/metrics/builtins.py | 12 ------------ sequence/utils/metrics/event_types.py | 4 ++++ 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/sequence/utils/metrics/builtins.py b/sequence/utils/metrics/builtins.py index 7f11c2452..7c5de7a39 100644 --- a/sequence/utils/metrics/builtins.py +++ b/sequence/utils/metrics/builtins.py @@ -13,18 +13,6 @@ ) from .registry import register_metric -EG_FAILURE = register_event_type("EG_FAILURE") -EG_SUCCESS = register_event_type("EG_SUCCESS") -THROUGHPUT = register_event_type("THROUGHPUT") -EP_FAILURE = register_event_type("EP_FAILURE") -EP_SUCCESS = register_event_type("EP_SUCCESS") -PURIFIED_DELIVERY = register_event_type("PURIFIED_DELIVERY") -ES_FAILURE = register_event_type("ES_FAILURE") -ES_SUCCESS = register_event_type("ES_SUCCESS") -RESERVATION_APPROVED = register_event_type("RESERVATION_APPROVED") -RESERVATION_REJECTED = register_event_type("RESERVATION_REJECTED") -RESERVATION_COMPLETE = register_event_type("RESERVATION_COMPLETE") - EG_METRIC = CounterMetric( prefix="eg", diff --git a/sequence/utils/metrics/event_types.py b/sequence/utils/metrics/event_types.py index 89ca6c6c8..f7fe39ab9 100644 --- a/sequence/utils/metrics/event_types.py +++ b/sequence/utils/metrics/event_types.py @@ -68,3 +68,7 @@ class EventTypes: DELIVERY = register_event_type("DELIVERY") ES_FAILURE = register_event_type("ES_FAILURE") ES_SUCCESS = register_event_type("ES_SUCCESS") + THROUGHPUT = register_event_type("THROUGHPUT") + RESERVATION_APPROVED = register_event_type("RESERVATION_APPROVED") + RESERVATION_REJECTED = register_event_type("RESERVATION_REJECTED") + RESERVATION_COMPLETE = register_event_type("RESERVATION_COMPLETE") From 09eebf8a9c6bfa92fd7c4efb6afc55554430f18f Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Tue, 7 Jul 2026 16:53:51 -0500 Subject: [PATCH 103/118] Update docstrings --- sequence/utils/metrics/__init__.py | 2 -- sequence/utils/metrics/metric_types.py | 6 ++---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/sequence/utils/metrics/__init__.py b/sequence/utils/metrics/__init__.py index 1b044694e..e5488469d 100644 --- a/sequence/utils/metrics/__init__.py +++ b/sequence/utils/metrics/__init__.py @@ -140,8 +140,6 @@ def collect_trial_metrics( owner_name: Node name to collect counter and fidelity metrics for. delivery_owner: Node name used for delivery-time metrics; defaults to `owner_name`. target_pairs: Number of delivered pairs required to compute delivery time. - reservation_start_time: Simulation time when the reservation started (ps). - throughput: Application throughput to include in collected metrics. Returns: Mapping of metric output keys to per-trial values. diff --git a/sequence/utils/metrics/metric_types.py b/sequence/utils/metrics/metric_types.py index 1c43d3b0e..9d01ba8d6 100644 --- a/sequence/utils/metrics/metric_types.py +++ b/sequence/utils/metrics/metric_types.py @@ -19,8 +19,6 @@ class CollectContext: storage: In-memory store of recorded events for the trial. delivery_owner: Node name for delivery-time metrics. target_pairs: Number of delivered pairs required to compute delivery time. - reservation_start_time: Simulation time when the reservation started (ps). - throughput: Application throughput supplied at collection time. """ owner_name: str @@ -188,7 +186,7 @@ def reset(self) -> None: @dataclass class RateMetric(Metric): - """Collects a rate value supplied at trial collection time (e.g. throughput).""" + """Computes application throughput from recorded deliveries at collection time.""" key: str = "app_throughput" @@ -201,7 +199,7 @@ def output_keys(self) -> frozenset[str]: return frozenset({self.key}) def collect(self, ctx: CollectContext) -> dict[str, Any]: - """Return the throughput value supplied at collection time. + """Compute throughput as delivered pairs per second over the reservation window. Args: ctx: Collection context; uses `throughput` when set. From 48dcd886e40cfe50c8e58dca09fa1c9691003250 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Tue, 7 Jul 2026 16:54:10 -0500 Subject: [PATCH 104/118] Adjusted collect of RateMetric to copy get_throughput --- sequence/utils/metrics/builtins.py | 5 ++++- sequence/utils/metrics/metric_types.py | 28 ++++++++++++++++++++++---- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/sequence/utils/metrics/builtins.py b/sequence/utils/metrics/builtins.py index 7c5de7a39..e13fb2076 100644 --- a/sequence/utils/metrics/builtins.py +++ b/sequence/utils/metrics/builtins.py @@ -26,7 +26,10 @@ success_event=EventTypes.EP_SUCCESS, rate_field="ep_success_rate", ) -THROUGHPUT_METRIC = RateMetric(key="app_throughput") +THROUGHPUT_METRIC = RateMetric( + key="app_throughput", + delivery_event=EventTypes.DELIVERY, +) PURIFIED_FIDELITIES_METRIC = FidelityMetric( key="purified_fidelities", event=EventTypes.EP_SUCCESS, diff --git a/sequence/utils/metrics/metric_types.py b/sequence/utils/metrics/metric_types.py index 9d01ba8d6..3fcea41ad 100644 --- a/sequence/utils/metrics/metric_types.py +++ b/sequence/utils/metrics/metric_types.py @@ -189,6 +189,7 @@ class RateMetric(Metric): """Computes application throughput from recorded deliveries at collection time.""" key: str = "app_throughput" + delivery_event: EventType | None = None @property def event_types(self) -> frozenset[EventType]: @@ -202,14 +203,33 @@ def collect(self, ctx: CollectContext) -> dict[str, Any]: """Compute throughput as delivered pairs per second over the reservation window. Args: - ctx: Collection context; uses `throughput` when set. + ctx: Collection context with delivery owner and stored delivery events. Returns: - Mapping with the configured rate key and throughput or NaN. + Mapping with the configured rate key in pairs per second, or NaN if data is insufficient. """ - if ctx.throughput is None: + if self.delivery_event is None: + return {self.key: float("nan")} + + delivery_owner = ctx.delivery_owner or ctx.owner_name + delivery_records = [ + record + for record in ctx.storage.get_by_owner(delivery_owner) + if record["event_type"] == self.delivery_event + ] + if not delivery_records: return {self.key: float("nan")} - return {self.key: ctx.throughput} + + delivery_records.sort(key=lambda record: record["sim_time"]) + start_time = delivery_records[0].get("start_time") + if start_time is None: + return {self.key: float("nan")} + + elapsed_ps = delivery_records[-1]["sim_time"] - start_time + if elapsed_ps <= 0: + return {self.key: float("nan")} + + return {self.key: len(delivery_records) / elapsed_ps * 1e12} @dataclass From 09b7a25bbd0442987e276ed4bedde3083898285e Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Tue, 7 Jul 2026 16:55:40 -0500 Subject: [PATCH 105/118] Adjust tests for new throughput changes --- tests/utils/test_metrics.py | 40 ++++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/tests/utils/test_metrics.py b/tests/utils/test_metrics.py index 36b9b86e8..e26bcca56 100644 --- a/tests/utils/test_metrics.py +++ b/tests/utils/test_metrics.py @@ -150,12 +150,39 @@ def test_collect_trial_metrics_returns_node_snapshot(): metrics.record(EventTypes.EG_FAILURE, "e0", fidelity=0.8) metrics.record(EventTypes.EG_SUCCESS, "e0", fidelity=0.8) - trial = metrics.collect_trial_metrics("e0", throughput=12.5) + trial = metrics.collect_trial_metrics("e0") assert trial["eg_failures"] == 1 assert trial["eg_success"] == 1 assert trial["eg_success_rate"] == 0.5 - assert trial["app_throughput"] == 12.5 + assert math.isnan(trial["app_throughput"]) + + +def test_collect_trial_metrics_computes_throughput_from_deliveries(): + timeline = Timeline(int(1e12)) + timeline.time = int(1e12) + metrics.register_time_provider(timeline) + metrics.enable([EventTypes.DELIVERY]) + + metrics.record( + EventTypes.DELIVERY, + "right", + fidelity=0.9, + pair_number=1, + start_time=int(1e12), + ) + timeline.time = int(2e12) + metrics.record( + EventTypes.DELIVERY, + "right", + fidelity=0.9, + pair_number=2, + start_time=int(1e12), + ) + + trial = metrics.collect_trial_metrics("left", delivery_owner="right") + + assert trial["app_throughput"] == pytest.approx(2.0) def test_collect_trial_metrics_without_throughput_is_nan(): @@ -315,6 +342,7 @@ def now(self) -> int: assert trial["ep_success"] == 2 assert trial["purified_fidelities"] == [0.7, 0.75] assert trial["delivery_time"] == pytest.approx(0.4) + assert trial["app_throughput"] == pytest.approx(7.5) def test_collect_trial_metrics_delivery_time_nan_when_target_not_reached(): @@ -332,7 +360,13 @@ def test_collect_trial_metrics_delivery_time_nan_when_target_not_reached(): def test_collect_trial_metrics_delivery_owner_defaults_to_owner(): metrics.enable([metrics.DELIVERY_TIME_METRIC]) - metrics.record(EventTypes.DELIVERY, "right", fidelity=0.9, pair_number=1) + metrics.record( + EventTypes.DELIVERY, + "right", + fidelity=0.9, + pair_number=1, + start_time=0, + ) trial = metrics.collect_trial_metrics( "right", From 9512f715b7da5b5a5591b26a86e4ff5dc784d4f8 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Tue, 7 Jul 2026 18:01:23 -0400 Subject: [PATCH 106/118] Potential fix for pull request finding 'Unused import' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- sequence/utils/metrics/builtins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sequence/utils/metrics/builtins.py b/sequence/utils/metrics/builtins.py index e13fb2076..2a7021c8d 100644 --- a/sequence/utils/metrics/builtins.py +++ b/sequence/utils/metrics/builtins.py @@ -2,7 +2,7 @@ from __future__ import annotations -from .event_types import EventTypes, register_event_type +from .event_types import EventTypes from .metric_types import ( CounterMetric, DeliveryTimeMetric, From 91ed005e31ccc19967df7dc04362f7e3e011334a Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Tue, 7 Jul 2026 17:12:11 -0500 Subject: [PATCH 107/118] Forgot to import these --- sequence/network_management/network_manager.py | 1 + tests/network_management/test_network_manager.py | 1 + 2 files changed, 2 insertions(+) diff --git a/sequence/network_management/network_manager.py b/sequence/network_management/network_manager.py index f884db23c..1b1097c5e 100644 --- a/sequence/network_management/network_manager.py +++ b/sequence/network_management/network_manager.py @@ -15,6 +15,7 @@ from ..components.memory import MemoryArray from ..message import Message from ..utils import log, metrics +from ..utils.metrics.event_types import EventTypes from .forwarding import ForwardingProtocol from .memory_timecard import MemoryTimeCard from .reservation import Reservation diff --git a/tests/network_management/test_network_manager.py b/tests/network_management/test_network_manager.py index f9685814a..850ddaa83 100644 --- a/tests/network_management/test_network_manager.py +++ b/tests/network_management/test_network_manager.py @@ -12,6 +12,7 @@ from sequence.protocol import StackProtocol from sequence.topology.node import QuantumRouter, BSMNode from sequence.utils import metrics +from sequence.utils.metrics.event_types import EventTypes import pytest class DistributedQuantumRouter(QuantumRouter): From c06ba919b79c5cbcf98f580fdabad7ec11f795f5 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Wed, 8 Jul 2026 16:13:29 -0500 Subject: [PATCH 108/118] This method is quite useless --- sequence/utils/metrics/__init__.py | 2 -- sequence/utils/metrics/event_types.py | 15 --------------- 2 files changed, 17 deletions(-) diff --git a/sequence/utils/metrics/__init__.py b/sequence/utils/metrics/__init__.py index e5488469d..f191974f8 100644 --- a/sequence/utils/metrics/__init__.py +++ b/sequence/utils/metrics/__init__.py @@ -26,7 +26,6 @@ from .event_types import ( EventType, EventTypes, - get_event_type, list_event_types, register_event_type, ) @@ -282,7 +281,6 @@ def aggregate_trial_metrics( # From event_types "EventType", "EventTypes", - "get_event_type", "list_event_types", "register_event_type", # From metric_types diff --git a/sequence/utils/metrics/event_types.py b/sequence/utils/metrics/event_types.py index f7fe39ab9..ba46d52af 100644 --- a/sequence/utils/metrics/event_types.py +++ b/sequence/utils/metrics/event_types.py @@ -34,21 +34,6 @@ def register_event_type(name: str) -> EventType: return event_type -def get_event_type(name: str) -> EventType: - """Return a registered event type by name. - - Args: - name: Name of the event type to look up. - - Returns: - The registered event type. - """ - try: - return _registry[name] - except KeyError as exc: - raise KeyError(f"Event type '{name}' is not registered.") from exc - - def list_event_types() -> list[EventType]: """Return all registered event types. From 781c948194e5ab9183b525153339188ce36a741f Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Fri, 10 Jul 2026 11:44:04 -0500 Subject: [PATCH 109/118] Reformat to line length 120 --- sequence/utils/metrics/metric_types.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/sequence/utils/metrics/metric_types.py b/sequence/utils/metrics/metric_types.py index 3fcea41ad..cc0a441b1 100644 --- a/sequence/utils/metrics/metric_types.py +++ b/sequence/utils/metrics/metric_types.py @@ -213,9 +213,7 @@ def collect(self, ctx: CollectContext) -> dict[str, Any]: delivery_owner = ctx.delivery_owner or ctx.owner_name delivery_records = [ - record - for record in ctx.storage.get_by_owner(delivery_owner) - if record["event_type"] == self.delivery_event + record for record in ctx.storage.get_by_owner(delivery_owner) if record["event_type"] == self.delivery_event ] if not delivery_records: return {self.key: float("nan")} @@ -283,12 +281,7 @@ def output_keys(self) -> frozenset[str]: def served_pairs(self, owner_name: str, identity: int) -> int: return self._pair_counters.get((owner_name, identity), 0) - def on_record( - self, - event_type: EventType, - owner_name: str, - kwargs: dict[str, Any], - ) -> None: + def on_record(self, event_type: EventType, owner_name: str, kwargs: dict[str, Any]) -> None: if event_type is not self.delivery_event: return identity = kwargs.get("identity", 0) From da3e4ac718aab16cdd7069d4b126599cd3350f10 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Fri, 17 Jul 2026 16:14:18 -0500 Subject: [PATCH 110/118] Remove registry stuff --- sequence/utils/metrics/__init__.py | 2 +- sequence/utils/metrics/registry.py | 17 +---------------- 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/sequence/utils/metrics/__init__.py b/sequence/utils/metrics/__init__.py index f191974f8..bd06d2148 100644 --- a/sequence/utils/metrics/__init__.py +++ b/sequence/utils/metrics/__init__.py @@ -40,7 +40,6 @@ ) from .registry import ( clear_registry, - get_reservation_delivery_metric, get_counter, list_metrics, register_metric, @@ -290,6 +289,7 @@ def aggregate_trial_metrics( "FidelityMetric", "Metric", "RateMetric", + "ReservationDeliveryMetric" # From registry "clear_registry", "get_counter", diff --git a/sequence/utils/metrics/registry.py b/sequence/utils/metrics/registry.py index dba18244f..738ff3f25 100644 --- a/sequence/utils/metrics/registry.py +++ b/sequence/utils/metrics/registry.py @@ -2,10 +2,9 @@ from __future__ import annotations -from .metric_types import CounterMetric, Metric, ReservationDeliveryMetric +from .metric_types import CounterMetric, Metric _metrics: list[Metric] = [] -_reservation_delivery_metric: ReservationDeliveryMetric | None = None _counters: dict[str, CounterMetric] = {} @@ -26,9 +25,6 @@ def register_metric(metric: Metric) -> None: _metrics.append(metric) if isinstance(metric, CounterMetric): _counters[metric.prefix] = metric - if isinstance(metric, ReservationDeliveryMetric): - global _reservation_delivery_metric - _reservation_delivery_metric = metric def unregister_metric(metric: Metric) -> None: @@ -41,10 +37,6 @@ def unregister_metric(metric: Metric) -> None: _metrics.remove(metric) if isinstance(metric, CounterMetric): _counters.pop(metric.prefix, None) - if isinstance(metric, ReservationDeliveryMetric): - global _reservation_delivery_metric - if _reservation_delivery_metric is metric: - _reservation_delivery_metric = None def list_metrics() -> list[Metric]: @@ -71,13 +63,6 @@ def get_counter(prefix: str) -> CounterMetric: raise KeyError(f"No CounterMetric registered with prefix '{prefix}'.") from exc -def get_reservation_delivery_metric() -> ReservationDeliveryMetric: - """Return the registered reservation delivery metric.""" - if _reservation_delivery_metric is None: - raise KeyError("No ReservationDeliveryMetric registered.") - return _reservation_delivery_metric - - def reset_metrics() -> None: """Reset per-trial state for all registered metrics. From 79b1ec720af0beac5d94265067831d01676b664c Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Fri, 17 Jul 2026 16:17:45 -0500 Subject: [PATCH 111/118] Due to change in collect, RateMetric should now be ThroughputMetric --- sequence/utils/metrics/__init__.py | 4 ++-- sequence/utils/metrics/builtins.py | 4 ++-- sequence/utils/metrics/metric_types.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/sequence/utils/metrics/__init__.py b/sequence/utils/metrics/__init__.py index bd06d2148..9b7e9d8dd 100644 --- a/sequence/utils/metrics/__init__.py +++ b/sequence/utils/metrics/__init__.py @@ -35,7 +35,7 @@ DeliveryTimeMetric, FidelityMetric, Metric, - RateMetric, + ThroughputMetric, ReservationDeliveryMetric, ) from .registry import ( @@ -288,7 +288,7 @@ def aggregate_trial_metrics( "DeliveryTimeMetric", "FidelityMetric", "Metric", - "RateMetric", + "ThroughputMetric", "ReservationDeliveryMetric" # From registry "clear_registry", diff --git a/sequence/utils/metrics/builtins.py b/sequence/utils/metrics/builtins.py index 2a7021c8d..43f0785de 100644 --- a/sequence/utils/metrics/builtins.py +++ b/sequence/utils/metrics/builtins.py @@ -8,7 +8,7 @@ DeliveryTimeMetric, FidelityMetric, Metric, - RateMetric, + ThroughputMetric, ReservationDeliveryMetric, ) from .registry import register_metric @@ -26,7 +26,7 @@ success_event=EventTypes.EP_SUCCESS, rate_field="ep_success_rate", ) -THROUGHPUT_METRIC = RateMetric( +THROUGHPUT_METRIC = ThroughputMetric( key="app_throughput", delivery_event=EventTypes.DELIVERY, ) diff --git a/sequence/utils/metrics/metric_types.py b/sequence/utils/metrics/metric_types.py index cc0a441b1..6ca2c77c3 100644 --- a/sequence/utils/metrics/metric_types.py +++ b/sequence/utils/metrics/metric_types.py @@ -185,7 +185,7 @@ def reset(self) -> None: @dataclass -class RateMetric(Metric): +class ThroughputMetric(Metric): """Computes application throughput from recorded deliveries at collection time.""" key: str = "app_throughput" From 33cffac1a1aefff36d6a2e9dd361799bb380cbb2 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Fri, 17 Jul 2026 16:20:04 -0500 Subject: [PATCH 112/118] There is no "throughput" event anymore --- sequence/utils/metrics/event_types.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sequence/utils/metrics/event_types.py b/sequence/utils/metrics/event_types.py index ba46d52af..5b4d54476 100644 --- a/sequence/utils/metrics/event_types.py +++ b/sequence/utils/metrics/event_types.py @@ -53,7 +53,6 @@ class EventTypes: DELIVERY = register_event_type("DELIVERY") ES_FAILURE = register_event_type("ES_FAILURE") ES_SUCCESS = register_event_type("ES_SUCCESS") - THROUGHPUT = register_event_type("THROUGHPUT") RESERVATION_APPROVED = register_event_type("RESERVATION_APPROVED") RESERVATION_REJECTED = register_event_type("RESERVATION_REJECTED") RESERVATION_COMPLETE = register_event_type("RESERVATION_COMPLETE") From 9fe972d9f5e6cfd5383001abd8855c9b20bc60d5 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Fri, 17 Jul 2026 16:22:25 -0500 Subject: [PATCH 113/118] There is no reason for this metric to exist --- sequence/utils/metrics/__init__.py | 11 +-------- sequence/utils/metrics/builtins.py | 13 +--------- sequence/utils/metrics/metric_types.py | 33 -------------------------- 3 files changed, 2 insertions(+), 55 deletions(-) diff --git a/sequence/utils/metrics/__init__.py b/sequence/utils/metrics/__init__.py index 9b7e9d8dd..5c4a40379 100644 --- a/sequence/utils/metrics/__init__.py +++ b/sequence/utils/metrics/__init__.py @@ -29,15 +29,7 @@ list_event_types, register_event_type, ) -from .metric_types import ( - CollectContext, - CounterMetric, - DeliveryTimeMetric, - FidelityMetric, - Metric, - ThroughputMetric, - ReservationDeliveryMetric, -) +from .metric_types import CollectContext, CounterMetric, DeliveryTimeMetric, FidelityMetric, Metric, ThroughputMetric from .registry import ( clear_registry, get_counter, @@ -289,7 +281,6 @@ def aggregate_trial_metrics( "FidelityMetric", "Metric", "ThroughputMetric", - "ReservationDeliveryMetric" # From registry "clear_registry", "get_counter", diff --git a/sequence/utils/metrics/builtins.py b/sequence/utils/metrics/builtins.py index 43f0785de..7f2093c22 100644 --- a/sequence/utils/metrics/builtins.py +++ b/sequence/utils/metrics/builtins.py @@ -3,14 +3,7 @@ from __future__ import annotations from .event_types import EventTypes -from .metric_types import ( - CounterMetric, - DeliveryTimeMetric, - FidelityMetric, - Metric, - ThroughputMetric, - ReservationDeliveryMetric, -) +from .metric_types import CounterMetric, DeliveryTimeMetric, FidelityMetric, Metric, ThroughputMetric from .registry import register_metric @@ -50,9 +43,6 @@ event=EventTypes.ES_SUCCESS, field="fidelity", ) -RESERVATION_DELIVERY_METRIC = ReservationDeliveryMetric( - delivery_event=EventTypes.DELIVERY, -) def register_builtin_metrics() -> None: @@ -70,7 +60,6 @@ def register_builtin_metrics() -> None: DELIVERY_TIME_METRIC, ES_METRIC, SWAPPED_FIDELITIES_METRIC, - RESERVATION_DELIVERY_METRIC, ] for metric in BUILTIN_METRICS: diff --git a/sequence/utils/metrics/metric_types.py b/sequence/utils/metrics/metric_types.py index 6ca2c77c3..5037c6ccd 100644 --- a/sequence/utils/metrics/metric_types.py +++ b/sequence/utils/metrics/metric_types.py @@ -263,39 +263,6 @@ def collect(self, ctx: CollectContext) -> dict[str, Any]: return {self.key: values} -@dataclass -class ReservationDeliveryMetric(Metric): - """Tracks per-reservation pair delivery counts and assigns pair_index on record.""" - - delivery_event: EventType - _pair_counters: dict[tuple[str, int], int] = field(default_factory=dict, init=False, repr=False) - - @property - def event_types(self) -> frozenset[EventType]: - return frozenset({self.delivery_event}) - - @property - def output_keys(self) -> frozenset[str]: - return frozenset() - - def served_pairs(self, owner_name: str, identity: int) -> int: - return self._pair_counters.get((owner_name, identity), 0) - - def on_record(self, event_type: EventType, owner_name: str, kwargs: dict[str, Any]) -> None: - if event_type is not self.delivery_event: - return - identity = kwargs.get("identity", 0) - key = (owner_name, identity) - self._pair_counters[key] = self._pair_counters.get(key, 0) + 1 - kwargs["pair_index"] = self._pair_counters[key] - - def collect(self, ctx: CollectContext) -> dict[str, Any]: - return {} - - def reset(self) -> None: - self._pair_counters.clear() - - @dataclass class DeliveryTimeMetric(Metric): """Time to deliver N pairs relative to reservation start.""" From 6ed87b497c72f1fbac09739c864c2b19a065b4d9 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Fri, 17 Jul 2026 16:25:30 -0500 Subject: [PATCH 114/118] Fix these to use metrics instead of events --- .../test_network_manager.py | 30 ------------------- tests/utils/test_metrics.py | 6 ++-- 2 files changed, 3 insertions(+), 33 deletions(-) diff --git a/tests/network_management/test_network_manager.py b/tests/network_management/test_network_manager.py index 850ddaa83..983e4f028 100644 --- a/tests/network_management/test_network_manager.py +++ b/tests/network_management/test_network_manager.py @@ -174,36 +174,6 @@ def test_pop_approve_intermediate(self, test_node, mock_reservation): test_node.get_reservation_result.assert_not_called() test_node.get_other_reservation.assert_not_called() - @pytest.mark.unit - def test_pop_approve_records_reservation_approved(self, test_node, mock_reservation): - metrics.enable([EventTypes.RESERVATION_APPROVED]) - mock_reservation.initiator = test_node.name - inbound_msg = Mock() - inbound_msg.msg_type = RSVPMsgType.APPROVE - inbound_msg.reservation = mock_reservation - - test_node.network_manager.pop(msg=inbound_msg) - - records = metrics.storage.get_by_event(EventTypes.RESERVATION_APPROVED) - assert len(records) == 1 - assert records[0]["owner_name"] == test_node.name - assert records[0]["identity"] == mock_reservation.identity - assert records[0]["path"] == mock_reservation.path - - @pytest.mark.unit - def test_pop_reject_records_reservation_rejected(self, test_node, mock_reservation): - metrics.enable([EventTypes.RESERVATION_REJECTED]) - mock_reservation.initiator = test_node.name - inbound_msg = Mock() - inbound_msg.msg_type = RSVPMsgType.REJECT - inbound_msg.reservation = mock_reservation - - test_node.network_manager.pop(msg=inbound_msg) - - records = metrics.storage.get_by_event(EventTypes.RESERVATION_REJECTED) - assert len(records) == 1 - assert records[0]["owner_name"] == test_node.name - assert records[0]["path"] == [] def test_NetworkManager_push(self, test_node, mock_reservation): outbound_msg = Mock() diff --git a/tests/utils/test_metrics.py b/tests/utils/test_metrics.py index e26bcca56..da4d85afd 100644 --- a/tests/utils/test_metrics.py +++ b/tests/utils/test_metrics.py @@ -162,7 +162,7 @@ def test_collect_trial_metrics_computes_throughput_from_deliveries(): timeline = Timeline(int(1e12)) timeline.time = int(1e12) metrics.register_time_provider(timeline) - metrics.enable([EventTypes.DELIVERY]) + metrics.enable([metrics.DELIVERY_TIME_METRIC]) metrics.record( EventTypes.DELIVERY, @@ -491,7 +491,7 @@ def test_reservation_rejected_records_metadata(): def test_purified_delivery_assigns_pair_index(): - metrics.enable([EventTypes.DELIVERY]) + metrics.enable([metrics.DELIVERY_TIME_METRIC]) kwargs = { "identity": 7, "initiator": "n1", @@ -516,7 +516,7 @@ def test_purified_delivery_assigns_pair_index(): def test_collect_reservation_data_produces_expected_row(): - metrics.enable([EventTypes.DELIVERY]) + metrics.enable([metrics.DELIVERY_TIME_METRIC]) start_time = int(1e12) end_time = int(2e12) base = { From efddeadb73f8a5398139e6bbed146d81e970b1e0 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Fri, 17 Jul 2026 16:25:40 -0500 Subject: [PATCH 115/118] These tests dont need to exist --- tests/utils/test_metrics.py | 41 ------------------------------------- 1 file changed, 41 deletions(-) diff --git a/tests/utils/test_metrics.py b/tests/utils/test_metrics.py index da4d85afd..eeb1da2ac 100644 --- a/tests/utils/test_metrics.py +++ b/tests/utils/test_metrics.py @@ -449,47 +449,6 @@ def test_register_metric_rejects_duplicate_output_keys(): metrics.register_metric(duplicate) -def test_reservation_approved_records_metadata(): - metrics.enable([EventTypes.RESERVATION_APPROVED]) - metrics.record( - EventTypes.RESERVATION_APPROVED, - "n1", - identity=1, - initiator="n1", - responder="n2", - start_time=10, - end_time=20, - memory_size=5, - entanglement_number=3, - target_fidelity=0.9, - path=["n1", "m1", "n2"], - ) - record = metrics.storage.get_all()[0] - assert record["identity"] == 1 - assert record["path"] == ["n1", "m1", "n2"] - assert record["entanglement_number"] == 3 - - -def test_reservation_rejected_records_metadata(): - metrics.enable([EventTypes.RESERVATION_REJECTED]) - metrics.record( - EventTypes.RESERVATION_REJECTED, - "n1", - identity=2, - initiator="n1", - responder="n2", - start_time=10, - end_time=20, - memory_size=5, - entanglement_number=1, - target_fidelity=0.9, - path=[], - ) - record = metrics.storage.get_all()[0] - assert record["event_type"] is EventTypes.RESERVATION_REJECTED - assert record["path"] == [] - - def test_purified_delivery_assigns_pair_index(): metrics.enable([metrics.DELIVERY_TIME_METRIC]) kwargs = { From ccdc9ab6807f86f13274b1eab6ef9b53860d5009 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Fri, 17 Jul 2026 16:26:21 -0500 Subject: [PATCH 116/118] Undo changes to network manager tests --- tests/network_management/test_network_manager.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/tests/network_management/test_network_manager.py b/tests/network_management/test_network_manager.py index 983e4f028..e10941795 100644 --- a/tests/network_management/test_network_manager.py +++ b/tests/network_management/test_network_manager.py @@ -11,8 +11,6 @@ from sequence.network_management.memory_timecard import MemoryTimeCard from sequence.protocol import StackProtocol from sequence.topology.node import QuantumRouter, BSMNode -from sequence.utils import metrics -from sequence.utils.metrics.event_types import EventTypes import pytest class DistributedQuantumRouter(QuantumRouter): @@ -66,7 +64,6 @@ def test_node(tl): @pytest.fixture def mock_reservation(test_node): reservation = Mock(spec=Reservation) - reservation.identity = 0 reservation.initiator = 'n1' reservation.responder = 'n2' reservation.path = ['n1', 'n2'] @@ -74,18 +71,9 @@ def mock_reservation(test_node): reservation.end_time = 3e12 reservation.memory_size = 50 reservation.fidelity = 1 - reservation.entanglement_number = 1 return reservation -@pytest.fixture(autouse=True) -def reset_metrics_state(): - metrics._enabled = False - metrics._enabled_events.clear() - metrics.storage.clear() - metrics.reset_metrics() - - # Test the Network Manager basics class TestNetworkManager: @@ -174,7 +162,6 @@ def test_pop_approve_intermediate(self, test_node, mock_reservation): test_node.get_reservation_result.assert_not_called() test_node.get_other_reservation.assert_not_called() - def test_NetworkManager_push(self, test_node, mock_reservation): outbound_msg = Mock() test_node.send_out = False From a101a75f260d6ac1d1213c67ff84f7d6f1a954d5 Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Fri, 17 Jul 2026 17:32:03 -0500 Subject: [PATCH 117/118] Added a bunch of new events and organized them --- sequence/utils/metrics/event_types.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/sequence/utils/metrics/event_types.py b/sequence/utils/metrics/event_types.py index 5b4d54476..a3644e8ab 100644 --- a/sequence/utils/metrics/event_types.py +++ b/sequence/utils/metrics/event_types.py @@ -46,13 +46,30 @@ def list_event_types() -> list[EventType]: class EventTypes: """Namespace for built-in simulation event types.""" + # Entanglement Management Events EG_FAILURE = register_event_type("EG_FAILURE") EG_SUCCESS = register_event_type("EG_SUCCESS") EP_FAILURE = register_event_type("EP_FAILURE") EP_SUCCESS = register_event_type("EP_SUCCESS") - DELIVERY = register_event_type("DELIVERY") ES_FAILURE = register_event_type("ES_FAILURE") ES_SUCCESS = register_event_type("ES_SUCCESS") + + # Network Management Events RESERVATION_APPROVED = register_event_type("RESERVATION_APPROVED") RESERVATION_REJECTED = register_event_type("RESERVATION_REJECTED") - RESERVATION_COMPLETE = register_event_type("RESERVATION_COMPLETE") + RESERVATION_REQUESTED = register_event_type("RESERVATION_REQUESTED") + RESERVATION_HOP_REJECT = register_event_type("RESERVATION_HOP_REJECT") + RESERVATION_REACHED_RESPONDER = register_event_type("RESERVATION_REACHED_RESPONDER") + + # Forwarding + FORWARDING_TABLE_MISS = register_event_type("FORWARDING_TABLE_MISS") + + # Routing + NEIGHBOR_DOWN = register_event_type("NEIGHBOR_DOWN") + NEIGHBOR_FULL = register_event_type("NEIGHBOR_FULL") + ROUTE_RECOMPUTED = register_event_type("ROUTE_RECOMPUTED") + LSA_ORIGINATED = register_event_type("LSA_ORIGINATED") + LSDB_UPDATED = register_event_type("LSDB_UPDATED") + + # Application Events + DELIVERY = register_event_type("DELIVERY") From e3a91e6152c5d856ecb26ba1e8a8ff1a819ec7fc Mon Sep 17 00:00:00 2001 From: Corbin <63320116+Tw1ZZLER@users.noreply.github.com> Date: Fri, 17 Jul 2026 17:32:06 -0500 Subject: [PATCH 118/118] Organized builtin metrics --- sequence/utils/metrics/builtins.py | 42 +++++++++++++++++++----------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/sequence/utils/metrics/builtins.py b/sequence/utils/metrics/builtins.py index 7f2093c22..1077ba538 100644 --- a/sequence/utils/metrics/builtins.py +++ b/sequence/utils/metrics/builtins.py @@ -6,7 +6,7 @@ from .metric_types import CounterMetric, DeliveryTimeMetric, FidelityMetric, Metric, ThroughputMetric from .registry import register_metric - +# Entanglement Management Metrics EG_METRIC = CounterMetric( prefix="eg", failure_event=EventTypes.EG_FAILURE, @@ -19,31 +19,43 @@ success_event=EventTypes.EP_SUCCESS, rate_field="ep_success_rate", ) -THROUGHPUT_METRIC = ThroughputMetric( - key="app_throughput", - delivery_event=EventTypes.DELIVERY, -) -PURIFIED_FIDELITIES_METRIC = FidelityMetric( - key="purified_fidelities", - event=EventTypes.EP_SUCCESS, - field="fidelity", -) -DELIVERY_TIME_METRIC = DeliveryTimeMetric( - key="delivery_time", - delivery_event=EventTypes.DELIVERY, -) ES_METRIC = CounterMetric( prefix="es", failure_event=EventTypes.ES_FAILURE, success_event=EventTypes.ES_SUCCESS, rate_field="es_success_rate", ) +PURIFIED_FIDELITIES_METRIC = FidelityMetric( + key="purified_fidelities", + event=EventTypes.EP_SUCCESS, + field="fidelity", +) SWAPPED_FIDELITIES_METRIC = FidelityMetric( key="swapped_fidelities", event=EventTypes.ES_SUCCESS, field="fidelity", ) +# Network Management Metrics +RESERVATION_APPROVAL_RATE = CounterMetric( + prefix="reservation_approval_rate", + failure_event=EventTypes.RESERVATION_REJECTED, + success_event=EventTypes.RESERVATION_APPROVED, + rate_field="reservation_approval_rate", +) + +# Resource Management Metrics + +# Application Metrics +THROUGHPUT_METRIC = ThroughputMetric( + key="app_throughput", + delivery_event=EventTypes.DELIVERY, +) +DELIVERY_TIME_METRIC = DeliveryTimeMetric( + key="delivery_time", + delivery_event=EventTypes.DELIVERY, +) + def register_builtin_metrics() -> None: """Register all built-in metrics with the global registry. @@ -55,10 +67,10 @@ def register_builtin_metrics() -> None: BUILTIN_METRICS: list[Metric] = [ EG_METRIC, EP_METRIC, + ES_METRIC, THROUGHPUT_METRIC, PURIFIED_FIDELITIES_METRIC, DELIVERY_TIME_METRIC, - ES_METRIC, SWAPPED_FIDELITIES_METRIC, ]