From f8028f26b26ea767725d579b1a6c1ab2a7b78b30 Mon Sep 17 00:00:00 2001 From: marcosf2 Date: Mon, 20 Apr 2026 15:36:08 -0300 Subject: [PATCH 1/3] Initial deletion of code --- ...g_app_example.toml => config_example.toml} | 0 configs/config_messaging_example.toml | 23 - scripts/coincidence_gui | 140 ------ scripts/http_server | 248 ----------- src/pqnstack/app/api/routes/chsh.py | 12 +- src/pqnstack/app/core/models.py | 14 - src/pqnstack/base/__init__.py | 0 src/pqnstack/base/errors.py | 58 --- src/pqnstack/base/instrument.py | 231 ---------- src/pqnstack/cli.py | 171 +------- src/pqnstack/constants.py | 34 -- src/pqnstack/{app => }/cron_manager.py | 0 src/pqnstack/{app => }/daily_report.py | 0 src/pqnstack/network/__init__.py | 0 src/pqnstack/network/client.py | 319 -------------- src/pqnstack/network/instrument_provider.py | 402 ------------------ src/pqnstack/network/packet.py | 60 --- src/pqnstack/network/router.py | 152 ------- src/pqnstack/pqn/__init__.py | 0 src/pqnstack/pqn/drivers/__init__.py | 0 src/pqnstack/pqn/drivers/chsh_driver.py | 84 ---- src/pqnstack/pqn/drivers/dummies.py | 105 ----- src/pqnstack/pqn/drivers/polarimeter.py | 139 ------ src/pqnstack/pqn/drivers/qkd_driver.py | 120 ------ src/pqnstack/pqn/drivers/rotaryencoder.py | 47 -- src/pqnstack/pqn/drivers/rotator.py | 131 ------ src/pqnstack/pqn/drivers/timetagger.py | 95 ----- src/pqnstack/pqn/protocols/__init__.py | 0 src/pqnstack/pqn/protocols/chsh.py | 104 ----- src/pqnstack/pqn/protocols/measurement.py | 68 --- src/pqnstack/pqn/protocols/qkd.py | 98 ----- src/pqnstack/pqn/protocols/tomography.py | 91 ---- src/pqnstack/pqn/protocols/visibility.py | 91 ---- tests/messaging/__init__.py | 0 tests/messaging/blocking_client.py | 20 - tests/messaging/client.py | 58 --- tests/messaging/config_example.toml | 23 - tests/messaging/provider.py | 16 - tests/messaging/router.py | 11 - tests/pytest/test_internal_network.py | 142 ------- tests/pytest/test_network_config.toml | 23 - 41 files changed, 12 insertions(+), 3318 deletions(-) rename configs/{config_app_example.toml => config_example.toml} (100%) delete mode 100644 configs/config_messaging_example.toml delete mode 100755 scripts/coincidence_gui delete mode 100755 scripts/http_server delete mode 100644 src/pqnstack/app/core/models.py delete mode 100644 src/pqnstack/base/__init__.py delete mode 100644 src/pqnstack/base/errors.py delete mode 100644 src/pqnstack/base/instrument.py rename src/pqnstack/{app => }/cron_manager.py (100%) rename src/pqnstack/{app => }/daily_report.py (100%) delete mode 100644 src/pqnstack/network/__init__.py delete mode 100644 src/pqnstack/network/client.py delete mode 100644 src/pqnstack/network/instrument_provider.py delete mode 100644 src/pqnstack/network/packet.py delete mode 100644 src/pqnstack/network/router.py delete mode 100644 src/pqnstack/pqn/__init__.py delete mode 100644 src/pqnstack/pqn/drivers/__init__.py delete mode 100644 src/pqnstack/pqn/drivers/chsh_driver.py delete mode 100644 src/pqnstack/pqn/drivers/dummies.py delete mode 100644 src/pqnstack/pqn/drivers/polarimeter.py delete mode 100644 src/pqnstack/pqn/drivers/qkd_driver.py delete mode 100644 src/pqnstack/pqn/drivers/rotaryencoder.py delete mode 100644 src/pqnstack/pqn/drivers/rotator.py delete mode 100644 src/pqnstack/pqn/drivers/timetagger.py delete mode 100644 src/pqnstack/pqn/protocols/__init__.py delete mode 100644 src/pqnstack/pqn/protocols/chsh.py delete mode 100644 src/pqnstack/pqn/protocols/measurement.py delete mode 100644 src/pqnstack/pqn/protocols/qkd.py delete mode 100644 src/pqnstack/pqn/protocols/tomography.py delete mode 100644 src/pqnstack/pqn/protocols/visibility.py delete mode 100644 tests/messaging/__init__.py delete mode 100644 tests/messaging/blocking_client.py delete mode 100644 tests/messaging/client.py delete mode 100644 tests/messaging/config_example.toml delete mode 100644 tests/messaging/provider.py delete mode 100644 tests/messaging/router.py delete mode 100644 tests/pytest/test_internal_network.py delete mode 100644 tests/pytest/test_network_config.toml diff --git a/configs/config_app_example.toml b/configs/config_example.toml similarity index 100% rename from configs/config_app_example.toml rename to configs/config_example.toml diff --git a/configs/config_messaging_example.toml b/configs/config_messaging_example.toml deleted file mode 100644 index 53709a41..00000000 --- a/configs/config_messaging_example.toml +++ /dev/null @@ -1,23 +0,0 @@ -[router] -name = "pqnstack-router" -host = "localhost" -port = 5556 - -[provider] -name = "pqnstack-provider" -router_name = "pqnstack-router" -host = "localhost" -port = 5556 -beat_period = 2000 - -[[provider.instruments]] -name = "dummy1" -import = "pqnstack.pqn.drivers.dummies.DummyInstrument" -desc = "Dummy instrument1 for testing purposes" -hw_address = "1234" - -[[provider.instruments]] -name = "dummy2" -import = "pqnstack.pqn.drivers.dummies.DummyInstrument" -desc = "Dummy instrument2 for testing purposes" -hw_address = "1234" diff --git a/scripts/coincidence_gui b/scripts/coincidence_gui deleted file mode 100755 index 4fa4aec0..00000000 --- a/scripts/coincidence_gui +++ /dev/null @@ -1,140 +0,0 @@ -#!/usr/bin/env python -# /// script -# requires-python = ">=3.13" -# dependencies = [ -# "pqnstack", -# "pyqt5", -# "pyqtgraph", -# ] -# -# [tool.uv.sources] -# pqnstack = { path = "../" } -# /// - -import sys -from typing import Any - -import numpy as np -import pyqtgraph as pg -from PyQt5 import QtCore -from PyQt5 import QtWidgets - -from pqnstack.base.instrument import Instrument -from pqnstack.network.client import Client - - -class Worker(QtCore.QObject): - data_ready = QtCore.pyqtSignal(list, int) - - def __init__(self, device: Instrument, parent: Any = None) -> None: - super().__init__(parent) - self.device = device - self.active_channels: list[int] = [] - self._channels_for_coincidence = 2 - - @QtCore.pyqtSlot() - def measure(self) -> None: - channels = list(range(1, 11)) - singles = self.device.count_singles(channels, integration_time_s=5) - for c in range(1, 11): - if c not in self.active_channels: - singles[c - 1] = 0 - coincidence = 0 - if len(self.active_channels) == self._channels_for_coincidence: - coincidence = 100 * self.device.measure_correlation( - self.active_channels[0], self.active_channels[1], integration_time_s=5, binwidth_ps=500 - ) - self.data_ready.emit(singles, coincidence) - - -class DataViewerGUI(QtWidgets.QWidget): - def __init__(self, device: Instrument, parent: Any = None): - super().__init__(parent) - self.device = device - self.main_layout = QtWidgets.QHBoxLayout(self) - self.coin_plot = pg.PlotWidget() - self.coin_plot.showGrid(x=True, y=True) - self.coin_plot.setLabel("left", "Count Rate") - self.coin_plot.setLabel("bottom", "Time Steps (oldest on the left, newest on the right)") - self.coin_plot.addLegend() - self.main_layout.addWidget(self.coin_plot, stretch=5) - self.control_widget = QtWidgets.QWidget() - self.control_layout = QtWidgets.QVBoxLayout(self.control_widget) - self.main_layout.addWidget(self.control_widget, stretch=2) - title_label = QtWidgets.QLabel("Coincidence Mode - Time Trace") - font = title_label.font() - font.setBold(True) - title_label.setFont(font) - self.control_layout.addWidget(title_label) - self.channel_group_box = QtWidgets.QGroupBox("Channels") - self.channel_layout = QtWidgets.QVBoxLayout(self.channel_group_box) - self.channel_checkboxes = [] - for i in range(1, 11): - cb = QtWidgets.QCheckBox(f"Channel {i}") - cb.setChecked(False) - cb.stateChanged.connect(self.on_channel_toggled) - self.channel_layout.addWidget(cb) - self.channel_checkboxes.append(cb) - self.control_layout.addWidget(self.channel_group_box) - self.control_layout.addStretch() - self.max_points = 60 - self.x_data = np.arange(self.max_points) - self.lines: dict[int | str, dict[str, Any]] = {} - for ch in range(1, 11): - plot_item = self.coin_plot.plot([], [], pen=pg.mkPen(color=(ch, 11), width=3), name=f"Ch. {ch}") - y_data = np.zeros(self.max_points) - self.lines[ch] = {"plot_item": plot_item, "y_data": y_data} - coincidence_plot = self.coin_plot.plot([], [], pen=pg.mkPen(color=(10, 11), width=3), name="Coincidence") - coincidence_y = np.zeros(self.max_points) - self.lines["coinc"] = {"plot_item": coincidence_plot, "y_data": coincidence_y} - self._thread: QtCore.QThread = QtCore.QThread() - self.worker = Worker(self.device) - self.worker.moveToThread(self._thread) - self.worker.data_ready.connect(self.handle_data) - self._thread.started.connect(self.worker.measure) - self._thread.start() - - def boot_gui(self) -> None: - self.show() - - def delete_gui(self) -> None: - self.close() - self.deleteLater() - - def handle_data(self, singles: list[int], coincidence: int) -> None: - channels_for_coincidence = 2 - active_channels = [i + 1 for i, cb in enumerate(self.channel_checkboxes) if cb.isChecked()] - for ch in range(1, 11): - old_y = self.lines[ch]["y_data"] - shifted_y = np.roll(old_y, -1) - shifted_y[-1] = singles[ch - 1] - self.lines[ch]["y_data"] = shifted_y - if ch in active_channels: - self.lines[ch]["plot_item"].setData(self.x_data, shifted_y) - else: - self.lines[ch]["plot_item"].setData([], []) - old_y = self.lines["coinc"]["y_data"] - shifted_y = np.roll(old_y, -1) - shifted_y[-1] = coincidence - self.lines["coinc"]["y_data"] = shifted_y - if len(active_channels) == channels_for_coincidence: - self.lines["coinc"]["plot_item"].setData(self.x_data, shifted_y) - else: - self.lines["coinc"]["plot_item"].setData([], []) - QtCore.QTimer.singleShot(200, self.worker.measure) - - def on_channel_toggled(self) -> None: - self.worker.active_channels = [i + 1 for i, cb in enumerate(self.channel_checkboxes) if cb.isChecked()] - - -if __name__ == "__main__": - app = QtWidgets.QApplication(sys.argv) - c = Client(host="172.30.63.109", timeout=300000) - tagger = c.get_device("mini_pc", "tagger") - - device = tagger - - gui = DataViewerGUI(device=device) - gui.boot_gui() - - sys.exit(app.exec_()) diff --git a/scripts/http_server b/scripts/http_server deleted file mode 100755 index be7fe5fd..00000000 --- a/scripts/http_server +++ /dev/null @@ -1,248 +0,0 @@ -#!/usr/bin/env python - -import json -import threading -from dataclasses import dataclass -from http.server import BaseHTTPRequestHandler -from http.server import HTTPServer -from typing import TYPE_CHECKING -from typing import Any - -from pqnstack.constants import DA_BASIS -from pqnstack.constants import HV_BASIS -from pqnstack.constants import MeasurementBasis -from pqnstack.network.client import Client -from pqnstack.pqn.drivers.polarimeter import ArduinoPolarimeter -from pqnstack.pqn.drivers.polarimeter import Polarimeter -from pqnstack.pqn.protocols.measurement import MeasurementConfig -from pqnstack.pqn.protocols.qkd import Devices as QkdDevices -from pqnstack.pqn.protocols.qkd import qkd_run -from pqnstack.pqn.protocols.qrng import pq_random - -if TYPE_CHECKING: - from collections.abc import Callable - -HOST = "127.0.0.1" -PORT = 8000 -ADDRESS: tuple[str, int] = (HOST, PORT) -ALLOW_HEADER = "*" - -PARITY_THRESHOLD: float = 0.6 -MIN_COLLECTED_VALUES: int = 6 -CLIENT_TIMEOUT: int = 600000 - - -@dataclass -class PolarimeterServerState: - theta: float = 0.0 - angle1: float = 0.0 - angle2: float = 0.0 - parity_agreement: float = 0.0 - - -class PolarimeterRequestHandler(BaseHTTPRequestHandler): - def __init__( - self, - *args: Any, - server_state: PolarimeterServerState, - polarimeter: Polarimeter | None, - **kwargs: Any, - ) -> None: - self.server_state = server_state - self.polarimeter = polarimeter - super().__init__(*args, **kwargs) - - def do_OPTIONS(self) -> None: - self.send_response(200) - self.send_header("Access-Control-Allow-Origin", ALLOW_HEADER) - self.send_header("Access-Control-Allow-Methods", "GET, POST, HEAD, OPTIONS") - self.send_header( - "Access-Control-Allow-Headers", - "Content-Type, Cache-Control, Authorization", - ) - self.end_headers() - - def do_GET(self) -> None: - status: int = 200 - content_type: str = "text/plain" - response_content: str - - if self.path == "/polarimeter/theta": - if self.polarimeter: - try: - m = self.polarimeter.read() - response_content = f"{-m.theta:.2f}" - except (OSError, AttributeError): - response_content = f"{self.server_state.theta:.2f}" - else: - response_content = f"{self.server_state.theta:.2f}" - - elif self.path == "/parity/agreement": - payload = {"parity_agreement": self.server_state.parity_agreement} - response_content = json.dumps(payload) - content_type = "application/json" - - else: - status = 404 - response_content = "" - - self.send_response(status) - self.send_header("Access-Control-Allow-Origin", ALLOW_HEADER) - self.send_header("Cache-Control", "no-store") - self.send_header("Content-Type", content_type) - self.end_headers() - self.wfile.write(response_content.encode("utf-8")) - - def do_POST(self) -> None: - status: int = 200 - content_type: str = "text/plain" - response_content: str = "" - - handlers: dict[str, Callable[[], tuple[int, str, str]]] = { - "/polarimeter/start_normalizing": self._handle_start_normalizing, - "/polarimeter/stop_normalizing": self._handle_stop_normalizing, - "/chsh/set_angle1": self._handle_set_angle1, - "/chsh/set_angle2": self._handle_set_angle2, - "/rng/user_input": self._handle_rng_user_input, - "/qkd/basis": self._handle_qkd_basis, - "/parity/basis": self._handle_parity_basis, - "/parity/done": self._handle_parity_done, - } - - handler = handlers.get(self.path) - if handler: - try: - status, content_type, response_content = handler() - except json.JSONDecodeError: - status = 400 - response_content = "Invalid JSON data" - except Exception: # noqa: BLE001 - status = 500 # TODO: Log Exception - response_content = "" - else: - status = 404 - - self.send_response(status) - self.send_header("Access-Control-Allow-Origin", ALLOW_HEADER) - self.send_header("Content-Type", content_type) - self.end_headers() - self.wfile.write(response_content.encode("utf-8")) - - def _read_json(self) -> Any: - length = int(self.headers.get("Content-Length", "0")) - raw = self.rfile.read(length).decode("utf-8") - return json.loads(raw) - - def _handle_start_normalizing(self) -> tuple[int, str, str]: - if self.polarimeter: - self.polarimeter.start_normalizing() - return 200, "text/plain", "" - - def _handle_stop_normalizing(self) -> tuple[int, str, str]: - if self.polarimeter: - self.polarimeter.stop_normalizing() - return 200, "text/plain", "" - - def _handle_set_angle1(self) -> tuple[int, str, str]: - data = self._read_json() - self.server_state.angle1 = float(data.get("option", 0.0)) - return 200, "text/plain", "" - - def _handle_set_angle2(self) -> tuple[int, str, str]: - data = self._read_json() - self.server_state.angle2 = float(data.get("option", 0.0)) - client = Client(host="172.30.63.109", timeout=CLIENT_TIMEOUT) - cd = client.get_device("chsh_device", "cd") - user_basis = abs(self.server_state.angle1 - self.server_state.angle2) % 90 / 2 - result = cd.measure_chsh( - basis1=[0, 45], - basis2=[user_basis, user_basis + 45], - config=MeasurementConfig(5), - ) - payload = { - "chsh_value": f"{float(result.chsh_value):.2f}", - "chsh_error": f"{float(result.chsh_error):.2f}", - } - self.server_state.angle1 = 0.0 - self.server_state.angle2 = 0.0 - return 200, "application/json", json.dumps(payload) - - def _handle_rng_user_input(self) -> tuple[int, str, str]: - data = self._read_json() - user_str = str(data.get("option", "10101010")) - client = Client(host="172.30.63.109", timeout=CLIENT_TIMEOUT) - timetagger = client.get_device("mini_pc", "tagger") - try: - number = pq_random(6, input_str=user_str, tagger=timetagger) - except ValueError: - number = pq_random(6, input_str=user_str) - payload = {"number": number} - return 200, "application/json", json.dumps(payload) - - def _handle_qkd_basis(self) -> tuple[int, str, str]: - data = self._read_json() - option = int(data.get("option", 1)) - basis: MeasurementBasis = HV_BASIS if option == 1 else DA_BASIS - client = Client(host="172.30.63.109", timeout=CLIENT_TIMEOUT) - qd = client.get_device("qkd_device", "qd") - devices = QkdDevices(qd=qd, client=client) - config = MeasurementConfig(duration=1) - visibility, _error = qkd_run(devices, config, basis=basis) - value = 1 if visibility > PARITY_THRESHOLD else 0 - payload = {"value": value} - return 200, "application/json", json.dumps(payload) - - def _handle_parity_basis(self) -> tuple[int, str, str]: - data = self._read_json() - option = int(data.get("option", 1)) - basis: MeasurementBasis = HV_BASIS if option == 1 else DA_BASIS - client = Client(host="172.30.63.109", timeout=CLIENT_TIMEOUT) - qd = client.get_device("qkd_device", "qd") - devices = QkdDevices(qd=qd, client=client) - config = MeasurementConfig(duration=1) - visibility, _error = qkd_run(devices, config, basis=basis) - value = 1 if visibility > PARITY_THRESHOLD else 0 - payload = {"value": value} - return 200, "application/json", json.dumps(payload) - - def _handle_parity_done(self) -> tuple[int, str, str]: - self.server_state.parity_agreement = 0 - return 200, "text/plain", "" - - -def create_request_handler( - server_state: PolarimeterServerState, - polarimeter: Polarimeter | None, -) -> type: - class CustomHandler(PolarimeterRequestHandler): - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, server_state=server_state, polarimeter=polarimeter, **kwargs) - - return CustomHandler - - -def update_theta(server_state: PolarimeterServerState) -> None: - while True: - try: - new_value = input("\nEnter new theta value: ") - server_state.theta = float(new_value) - except ValueError: - continue - - -if __name__ == "__main__": - state = PolarimeterServerState() - try: - polarimeter = ArduinoPolarimeter() - except Exception: # noqa: BLE001 - polarimeter = None # TODO: Log Exception - - input_thread = threading.Thread(target=update_theta, args=(state,), daemon=True) - input_thread.start() - - handler_factory = create_request_handler(state, polarimeter) - httpd = HTTPServer(ADDRESS, handler_factory) - try: - httpd.serve_forever() - except KeyboardInterrupt: - httpd.shutdown() diff --git a/src/pqnstack/app/api/routes/chsh.py b/src/pqnstack/app/api/routes/chsh.py index 1fb48cd5..e88d66f1 100644 --- a/src/pqnstack/app/api/routes/chsh.py +++ b/src/pqnstack/app/api/routes/chsh.py @@ -15,7 +15,6 @@ from pqnstack.app.api.deps import StateDep from pqnstack.app.core.config import chsh_progress_event from pqnstack.app.core.config import settings -from pqnstack.app.core.models import calculate_chsh_expectation_error from pqnstack.network.client import Client if TYPE_CHECKING: @@ -213,3 +212,14 @@ async def request_angle_by_basis(index: int, state: StateDep, *, perp: bool = Fa hwp.move_to(angle / 2) logger.info("moving waveplate", extra={"angle": angle}) return True + + +def calculate_chsh_expectation_error(counts: list[int], dark_count: int = 0) -> float: + total_counts = sum(counts) + corrected_total = total_counts - 4 * dark_count + if corrected_total <= 0: + return 0 + first_term = (total_counts**0.5) / corrected_total + expectation = abs(counts[0] + counts[3] - counts[1] - counts[2]) + second_term = (expectation / corrected_total**2) * (total_counts + 4 * dark_count) ** 0.5 + return float(first_term + second_term) diff --git a/src/pqnstack/app/core/models.py b/src/pqnstack/app/core/models.py deleted file mode 100644 index 348411af..00000000 --- a/src/pqnstack/app/core/models.py +++ /dev/null @@ -1,14 +0,0 @@ -import logging - -logger = logging.getLogger(__name__) - - -def calculate_chsh_expectation_error(counts: list[int], dark_count: int = 0) -> float: - total_counts = sum(counts) - corrected_total = total_counts - 4 * dark_count - if corrected_total <= 0: - return 0 - first_term = (total_counts**0.5) / corrected_total - expectation = abs(counts[0] + counts[3] - counts[1] - counts[2]) - second_term = (expectation / corrected_total**2) * (total_counts + 4 * dark_count) ** 0.5 - return float(first_term + second_term) diff --git a/src/pqnstack/base/__init__.py b/src/pqnstack/base/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/pqnstack/base/errors.py b/src/pqnstack/base/errors.py deleted file mode 100644 index 3a2484a9..00000000 --- a/src/pqnstack/base/errors.py +++ /dev/null @@ -1,58 +0,0 @@ -# University of Illinois Urbana-Champaign -# Public Quantum Network -# -# NCSA/Illinois Computes - - -class DeviceNotStartedError(Exception): - def __init__(self, message: str = "Device not started") -> None: - self.message = message - super().__init__(self.message) - - -class DriverNotFoundError(Exception): - def __init__(self, message: str = "Device driver configuration not found") -> None: - self.message = message - super().__init__(self.message) - - -class DriverFunctionNotImplementedError(Exception): - def __init__(self, message: str = "One or more driver functions were not implemented") -> None: - self.message = message - super().__init__(self.message) - - -class DriverFunctionUnknownError(Exception): - def __init__(self, message: str = "Device driver function unknown") -> None: - self.message = message - super().__init__(self.message) - - -class LogDecoratorOutsideOfClassError(Exception): - def __init__(self, message: str = "Log decorator used outside of a class") -> None: - self.message = message - super().__init__(self.message) - - -class PacketError(Exception): - def __init__(self, message: str = "Packet error") -> None: - self.message = message - super().__init__(self.message) - - -class InvalidInstrumentsConfigurationError(Exception): - def __init__(self, message: str = "Invalid instruments configuration") -> None: - self.message = message - super().__init__(self.message) - - -class InvalidNetworkConfigurationError(Exception): - def __init__(self, message: str = "Invalid network configuration") -> None: - self.message = message - super().__init__(self.message) - - -class CouldNotConnectToNetworkElementError(Exception): - def __init__(self, message: str = "Could not connect to network element") -> None: - self.message = message - super().__init__(self.message) diff --git a/src/pqnstack/base/instrument.py b/src/pqnstack/base/instrument.py deleted file mode 100644 index 2b1cf2b2..00000000 --- a/src/pqnstack/base/instrument.py +++ /dev/null @@ -1,231 +0,0 @@ -# University of Illinois Urbana-Champaign -# Public Quantum Network -# -# NCSA/Illinois Computes - -import atexit -import datetime -import logging -from collections.abc import Callable -from dataclasses import dataclass -from dataclasses import field -from functools import wraps -from time import perf_counter -from typing import Any -from typing import Protocol -from typing import runtime_checkable - -from pqnstack.base.errors import LogDecoratorOutsideOfClassError - -logger = logging.getLogger(__name__) - - -@dataclass(frozen=True, slots=True) -class InstrumentInfo: - name: str = "" - desc: str = "" - hw_address: str = "" - hw_status: dict[str, Any] = field(default_factory=dict) - - -@runtime_checkable -@dataclass(slots=True) -class Instrument(Protocol): - """Base class for all instruments in the PQN stack. - - Some rules for instruments: - - * You cannot use the character `:` in the names of instruments. This is used to separate parts of requests in - proxy instruments. - - """ - - name: str - desc: str - hw_address: str - parameters: set[str] = field(default_factory=set) - operations: dict[str, Callable[..., Any]] = field(default_factory=dict) - - def __post_init__(self) -> None: - atexit.register(self.close) - - def start(self) -> None: ... - def close(self) -> None: ... - - @property - def info(self) -> InstrumentInfo: ... - - -def log_operation[T](func: Callable[..., T]) -> Callable[..., T]: - @wraps(func) - def wrapper(*args: Any, **kwargs: Any) -> T: - if len(args) == 0: - msg = "log_operation has 0 args, this usually indicates that it has been used to decorate something that is not a class method. This is not allowed." - raise LogDecoratorOutsideOfClassError(msg) - - ins = args[0] - if not isinstance(ins, Instrument): - msg = "log_operation has been used to decorate something that is not a Instrument method. This is not allowed." - raise LogDecoratorOutsideOfClassError(msg) - - start_time = perf_counter() - logger.info( - "%s| %s, %s |Starting operation '%s' with args: '%s' and kwargs '%s'", - start_time, - ins.name, - type(ins), - func.__name__, - args, - kwargs, - ) - - result = func(*args, **kwargs) - - end_time = perf_counter() - duration = end_time - start_time - logger.info( - "%s | %s, %s | Completed operation %s. Duration: %s", - end_time, - ins.name, - type(ins), - func.__name__, - duration, - ) - - return result - - return wrapper - - -def log_parameter[T](func: Callable[..., T]) -> Callable[..., T]: - @wraps(func) - def wrapper(*args: Any, **kwargs: Any) -> T: - if len(args) == 0: - msg = ( - "log_parameter has 0 args, " - "this usually indicates that it has been used to decorate something that is not a class method. " - "This is not allowed." - ) - raise LogDecoratorOutsideOfClassError(msg) - - ins = args[0] - if not isinstance(ins, Instrument): - msg = ( - "log_operation has been used to decorate something that is not a Instrument method. " - "This is not allowed." - ) - raise LogDecoratorOutsideOfClassError(msg) - - # if no args or kwargs, we are reading the value of the param, else we are setting it. - if len(args) == 1 and len(kwargs) == 0: - current_time = datetime.datetime.now(tz=datetime.UTC) - result = func(*args, **kwargs) - logger.info( - "%s | %s, %s | Parameter '%s' got read with value %s", - current_time, - ins.name, - type(ins), - func.__name__, - result, - ) - - else: - start_time = perf_counter() - result = func(*args, **kwargs) # Always return None - end_time = perf_counter() - duration = end_time - start_time - logger.info( - "%s | %s, %s | Parameter '%s' got updated to '%s', parameter update took %s long ", - end_time, - ins.name, - type(ins), - func.__name__, - args[1:], - duration, - ) - - return result - - return wrapper - - -@dataclass(frozen=True, slots=True) -class TimeTaggerInfo(InstrumentInfo): - active_channels: list[int] = field(default_factory=list) - test_signal_enabled: bool = False - test_signal_divider: int = 1 - - -@runtime_checkable -@dataclass(slots=True) -class TimeTaggerInstrument(Instrument, Protocol): - active_channels: list[int] = field(default_factory=list) - test_signal_enabled: bool = False - test_signal_divider: int = 1 - - def __post_init__(self) -> None: - self.operations["count_singles"] = self.count_singles - self.operations["measure_correlation"] = self.measure_correlation - - def count_singles(self, channels: list[int], integration_time_s: float) -> list[int]: ... - def measure_correlation(self, start_ch: int, stop_ch: int, integration_time_s: float, binwidth_ps: int) -> int: ... - - -@dataclass(frozen=True, slots=True) -class RotatorInfo(InstrumentInfo): - degrees: float = 0.0 - offset_degrees: float = 0.0 - - -@runtime_checkable -@dataclass(slots=True) -class RotatorInstrument(Instrument, Protocol): - offset_degrees: float = 0.0 - - def __post_init__(self) -> None: - self.operations["move_to"] = self.move_to - self.operations["move_by"] = self.move_by - - self.parameters.add("degrees") - - @property - @log_parameter - def degrees(self) -> float: ... - - @degrees.setter - @log_parameter - def degrees(self, degrees: float) -> None: ... - - def move_to(self, angle: float) -> None: - """Move the rotator to the specified angle.""" - self.degrees = angle - - def move_by(self, angle: float) -> None: - """Move the rotator by the specified angle.""" - self.degrees += angle - - -@dataclass(frozen=True, slots=True) -class PolarimeterInfo(InstrumentInfo): - pass - - -@runtime_checkable -@dataclass(slots=True) -class PolarimeterInstrument(Instrument, Protocol): - def __post_init__(self) -> None: - self.operations["reset"] = self.reset - self.operations["start_normalizing"] = self.start_normalizing - self.operations["stop_normalizing"] = self.stop_normalizing - - @property - def info(self) -> PolarimeterInfo: ... - - @log_operation - def reset(self) -> None: ... - - @log_operation - def start_normalizing(self) -> None: ... - - @log_operation - def stop_normalizing(self) -> None: ... diff --git a/src/pqnstack/cli.py b/src/pqnstack/cli.py index a1bad664..49b348a5 100644 --- a/src/pqnstack/cli.py +++ b/src/pqnstack/cli.py @@ -1,4 +1,3 @@ -import json import logging import tomllib from pathlib import Path @@ -13,9 +12,7 @@ from pqnstack.app.cron_manager import remove_daily_report_job from pqnstack.app.cron_manager import set_daily_report_schedule from pqnstack.app.daily_report import run_daily_report -from pqnstack.base.errors import InvalidNetworkConfigurationError -from pqnstack.network.instrument_provider import InstrumentProvider -from pqnstack.network.router import Router + # TODO: check if this way of handling logging from a command line script is ok. logging.basicConfig(level=logging.INFO) @@ -28,172 +25,6 @@ app.add_typer(daily_report_app, name="daily-report") -def _verify_instruments_config(instruments: list[dict[str, str]]) -> dict[str, dict[str, str]]: - ins = {} - for i, item in enumerate(instruments): - if "name" not in item: - msg = f"Instrument number #{i + 1} configuration is missing the field 'name'" - raise InvalidNetworkConfigurationError(msg) - if "import" not in item: - msg = f"Instrument number #{i + 1} configuration is missing the field 'import'" - raise InvalidNetworkConfigurationError(msg) - if "desc" not in item: - msg = f"Instrument number #{i + 1} configuration is missing the field 'desc'" - raise InvalidNetworkConfigurationError(msg) - if "hw_address" not in item: - msg = f"Instrument number #{i + 1} configuration is missing the field 'hw_address'" - raise InvalidNetworkConfigurationError(msg) - - name = item.pop("name") - ins[name] = item - - return ins - - -def _load_and_parse_provider_config( - config_path: Path | str, kwargs: dict[str, str | int], instruments: dict[str, dict[str, str]] -) -> tuple[dict[str, str | int], dict[str, dict[str, str]]]: - path = Path(config_path) - with path.open("rb") as f: - config = tomllib.load(f) - - if "provider" not in config: - msg = f"Config file {config_path} does not contain a provider section. Add provider configuration under '[provider]' section." - raise InvalidNetworkConfigurationError(msg) - - provider = config["provider"] - if "name" in provider: - kwargs["name"] = str(provider["name"]) - if "router_name" in provider: - kwargs["router_name"] = str(provider["router_name"]) - if "host" in provider: - kwargs["host"] = str(provider["host"]) - if "port" in provider: - kwargs["port"] = int(provider["port"]) - if "beat_period" in provider: - kwargs["beat_period"] = int(provider["beat_period"]) - - if "instruments" in provider: - instruments = _verify_instruments_config(provider["instruments"]) - - return kwargs, instruments - - -@app.command() -def start_provider( # noqa: PLR0913 - name: Annotated[str | None, typer.Option(help="Name of the InstrumentProvider.")] = None, - router_name: Annotated[ - str | None, typer.Option(help="Name of the router this provider will talk to (default: 'router1').") - ] = None, - host: Annotated[ - str | None, - typer.Option( - help="Host hw_address (IP) of the provider (default: 'localhost'). Usually the IP hw_address of the Router this provider will talk to." - ), - ] = None, - port: Annotated[ - int | None, typer.Option(help="Port of the provider (default: 5555). Has to be the same port as the Router.") - ] = None, - beat_period: Annotated[int | None, typer.Option(help="Heartbeat period in milliseconds (default: 1000)")] = None, - instruments: Annotated[ - str | None, - typer.Option( - help='JSON formatted string with necessary arguments to instantiate instruments. Example: \'{"dummy1": {"import": "pqnstack.pqn.drivers.dummies.DummyInstrument", "desc": "Dummy Instrument 1", "hw_address": "123456"}}\'' - ), - ] = None, - config: Annotated[ - str | None, typer.Option(help="Path to the config file, will get overridden by command line arguments.") - ] = None, -) -> None: - """ - Start a PQN InstrumentProvider. - - Can be configured by passing arguments directly into the command line but it is recommended to use a config file if instruments will be added. - """ - kwargs: dict[str, str | int] = {} - ins: dict[str, dict[str, str]] = {} - - if config: - kwargs, ins = _load_and_parse_provider_config(config, kwargs, ins) - - if name: - kwargs["name"] = name - if router_name: - kwargs["router_name"] = router_name - if host: - kwargs["host"] = host - if port: - kwargs["port"] = port - if beat_period: - kwargs["beat_period"] = beat_period - if instruments: - # We don't want to override instruments, instead combining them with the ones from config file is cleaner behaviour. - ins = {**ins, **json.loads(instruments)} - - if "name" not in kwargs: - msg = "InstrumentProvider name is required" - raise InvalidNetworkConfigurationError(msg) - - provider = InstrumentProvider(**kwargs, **ins) # type: ignore[arg-type] - provider.start() - - -def _load_and_parse_router_config(config_path: Path | str, kwargs: dict[str, str | int]) -> dict[str, str | int]: - path = Path(config_path) - with path.open("rb") as f: - config = tomllib.load(f) - if "router" not in config: - msg = f"Config file {config_path} does not contain a router section. Add router configuration under '[router]' section." - raise InvalidNetworkConfigurationError(msg) - router = config["router"] - if "name" in router: - kwargs["name"] = str(router["name"]) - if "host" in router: - kwargs["host"] = str(router["host"]) - if "port" in router: - kwargs["port"] = int(router["port"]) - return kwargs - - -@app.command() -def start_router( - name: Annotated[str | None, typer.Option(help="Name of the router (default 'router1')")] = None, - host: Annotated[ - str | None, - typer.Option( - help="Host hw_address (IP) of the router (default: 'localhost'). Usually the IP hw_address of the machine running the router." - ), - ] = None, - port: Annotated[str | None, typer.Option(help="Port of the router (default: 5555)")] = None, - config: Annotated[ - str | None, typer.Option(help="Path to the config file, will get overridden by command line arguments.") - ] = None, -) -> None: - """ - Start a PQN Router. - - Can be configured by passing arguments directly into the command line or through a config file. - """ - kwargs: dict[str, str | int] = {} - if config: - kwargs = _load_and_parse_router_config(config, kwargs) - - if name: - kwargs["name"] = name - if host: - kwargs["host"] = host - if port: - kwargs["port"] = int(port) - - if "name" not in kwargs: - msg = "Router name is required" - raise InvalidNetworkConfigurationError(msg) - - # mypy doesn't like **kwargs https://github.com/python/mypy/issues/5382#issuecomment-417433738 - router = Router(**kwargs) # type: ignore[arg-type] - router.start() - - @app.command() def toggle_game( games: Annotated[list[str], typer.Argument(help="Games to toggle: chsh, qf, ssm")], diff --git a/src/pqnstack/constants.py b/src/pqnstack/constants.py index f6f9b5be..d5027bd3 100644 --- a/src/pqnstack/constants.py +++ b/src/pqnstack/constants.py @@ -1,4 +1,3 @@ -from dataclasses import dataclass from enum import Enum @@ -35,36 +34,3 @@ class BellState(Enum): Phi_plus = 0 Psi_plus = 1 - -DEFAULT_SETTINGS: dict[str, tuple[float, float]] = { - "H": (0, 0), - "V": (45, 0), - "D": (22.5, 0), - "A": (-22.5, 0), - "R": (22.5, 45), - "L": (-22.5, 45), -} - - -@dataclass(frozen=True) -class MeasurementBasis: - name: str - pairs: list[tuple[str, str]] - settings: dict[str, tuple[float, float]] - - -HV_BASIS = MeasurementBasis( - name="HV", - pairs=[("H", "H"), ("H", "V"), ("V", "H"), ("V", "V")], - settings=DEFAULT_SETTINGS, -) -DA_BASIS = MeasurementBasis( - name="DA", - pairs=[("D", "D"), ("D", "A"), ("A", "D"), ("A", "A")], - settings=DEFAULT_SETTINGS, -) -RL_BASIS = MeasurementBasis( - name="RL", - pairs=[("R", "R"), ("R", "L"), ("L", "R"), ("L", "L")], - settings=DEFAULT_SETTINGS, -) diff --git a/src/pqnstack/app/cron_manager.py b/src/pqnstack/cron_manager.py similarity index 100% rename from src/pqnstack/app/cron_manager.py rename to src/pqnstack/cron_manager.py diff --git a/src/pqnstack/app/daily_report.py b/src/pqnstack/daily_report.py similarity index 100% rename from src/pqnstack/app/daily_report.py rename to src/pqnstack/daily_report.py diff --git a/src/pqnstack/network/__init__.py b/src/pqnstack/network/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/pqnstack/network/client.py b/src/pqnstack/network/client.py deleted file mode 100644 index 9cdaf368..00000000 --- a/src/pqnstack/network/client.py +++ /dev/null @@ -1,319 +0,0 @@ -import logging -import pickle -import secrets -import string -from collections.abc import Callable -from dataclasses import dataclass -from types import TracebackType -from typing import Any -from typing import NamedTuple -from typing import Self - -import zmq - -from pqnstack.base.errors import PacketError -from pqnstack.base.instrument import Instrument -from pqnstack.base.instrument import InstrumentInfo -from pqnstack.network.packet import NetworkElementClass -from pqnstack.network.packet import Packet -from pqnstack.network.packet import PacketIntent -from pqnstack.network.packet import create_registration_packet - -logger = logging.getLogger(__name__) - - -class ClientBase: - def __init__( - self, - name: str = "", - host: str = "127.0.0.1", - port: int = 5555, - router_name: str = "router1", - timeout: int = 30000, - ) -> None: - if name == "": - name = "".join( - secrets.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for _ in range(6) - ) - self.name = name - - self.host = host - self.port = port - self.address = f"tcp://{host}:{port}" - self.router_name = router_name - - self.timeout = timeout - - self.connected = False - self.context: zmq.Context[zmq.Socket[bytes]] | None = None - self.socket: zmq.Socket[bytes] | None = None - - self.connect() - - async def __aenter__(self) -> Self: - if not self.connected: - self.connect() - return self - - async def __aexit__( - self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None - ) -> None: - self.disconnect() - - def connect(self) -> None: - logger.info("Starting client '%s' Connecting to %s", self.name, self.address) - self.context = zmq.Context() - self.socket = self.context.socket(zmq.REQ) - self.socket.setsockopt(zmq.RCVTIMEO, self.timeout) - self.socket.setsockopt_string(zmq.IDENTITY, self.name) - self.socket.connect(self.address) - self.connected = True - - reg_packet = create_registration_packet( - source=self.name, destination=self.router_name, payload=NetworkElementClass.CLIENT, hops=0 - ) - ret = self.ask(reg_packet) - if ret is None: - msg = "Something went wrong with the registration." - raise RuntimeError(msg) - if ret.intent != PacketIntent.REGISTRATION_ACK: - msg = "Registration failed." - raise RuntimeError(msg) - logger.info("Acknowledged by server. Client is connected.") - - def disconnect(self) -> None: - logger.info("Disconnecting from %s", self.address) - if self.socket is None: - logger.warning("Socket is already None.") - self.connected = False - return - - self.socket.close() - self.connected = False - logger.info("Disconnected from %s", self.address) - - def ask(self, packet: Packet) -> Packet: - if not self.connected: - msg = "No connection yet." - logger.error(msg) - raise RuntimeError(msg) - - if self.socket is None: - msg = "Socket is None. Cannot ask." - logger.error(msg) - raise RuntimeError(msg) - - # try so that if timeout happens, the client remains usable - - self.socket.send(pickle.dumps(packet)) - try: - response = self.socket.recv() - except zmq.error.Again as e: - logger.exception("Timeout occurred.") - raise TimeoutError from e - - ret: Packet = pickle.loads(response) - logger.debug("Response received.") - logger.debug("Response: %s", str(ret)) - if ret.intent == PacketIntent.ERROR: - raise PacketError(str(ret)) - - return ret - - def create_control_packet( - self, destination: str, request: str, payload: tuple[tuple[Any, ...], dict[str, Any]] - ) -> Packet: - return Packet( - intent=PacketIntent.CONTROL, - request=request, - source=self.name, - destination=destination, - payload=payload, - ) - - def create_data_packet(self, destination: str, request: str, payload: Any) -> Packet: - return Packet( - intent=PacketIntent.DATA, - request=request, - source=self.name, - destination=destination, - payload=payload, - ) - - -class InstrumentClientInit(NamedTuple): - name: str - host: str - port: int - router_name: str - timeout: int - instrument_name: str - provider_name: str - - -class InstrumentClient(ClientBase): - def __init__(self, init_args: InstrumentClientInit) -> None: - super().__init__( - init_args.name, init_args.host, init_args.port, init_args.router_name, timeout=init_args.timeout - ) - - self.instrument_name = init_args.instrument_name - self.provider_name = init_args.provider_name - - def trigger_operation(self, operation: str, *args: Any, **kwargs: Any) -> Any: - packet = self.create_control_packet( - self.provider_name, self.instrument_name + ":OPERATION:" + operation, (args, kwargs) - ) - response = self.ask(packet) - - return response.payload - - def trigger_parameter(self, parameter: str, *args: Any, **kwargs: Any) -> Any: - packet = self.create_control_packet( - self.provider_name, self.instrument_name + ":PARAMETER:" + parameter, (args, kwargs) - ) - - response = self.ask(packet) - return response.payload - - def get_info(self) -> InstrumentInfo: - packet = self.create_control_packet(self.provider_name, self.instrument_name + ":INFO:", ((), {})) - - response = self.ask(packet) - if not isinstance(response.payload, InstrumentInfo): - msg = "Asking for info to proxy driver did not get a InstrumentInfo object." - raise PacketError(msg) - - return response.payload - - -class ProxyInstrumentInit(NamedTuple): - name: str - host: str - port: int - router_name: str - instrument_name: str - timeout: int - provider_name: str - desc: str - address: str - parameters: set[str] - operations: dict[str, Callable[[Any], Any]] - client_name: str - - -@dataclass -class ProxyInstrument(Instrument): - """The address here is the zmq address of the router that the InstrumentClient will talk to.""" - - name: str = "" - desc: str = "" - hw_address: str = "" - host: str = "127.0.0.1" - port: int = 5555 - timeout_ms: int = 15000 - router_name: str = "router1" - client_name: str = "" - provider_name: str = "provider1" - instrument_name: str = "instrument1" - - # Boolean used to control when new attributes are being set. - _instantiating: bool = True - - def __post_init__(self) -> None: - # The client's name is the instrument name with "_client" appended and a random 6 character string appended. - # This is to avoid any potential conflicts with other clients. - client_name = ( - self.name - + "_client_" - + "".join(secrets.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for _ in range(6)) - ) - instrument_client_init = InstrumentClientInit( - name=client_name, - host=self.host, - port=self.port, - router_name=self.router_name, - timeout=self.timeout_ms, - instrument_name=self.name, - provider_name=self.provider_name, - ) - self.client = InstrumentClient(instrument_client_init) - - self._instantiating = False - - def __getattr__(self, name: str) -> Any: - if name in self.operations: - return lambda *args, **kwargs: self.client.trigger_operation(name, *args, **kwargs) - if name in self.parameters: - return self.client.trigger_parameter(name) - msg = f"Attribute '{name}' not found." - raise AttributeError(msg) - - def __setattr__(self, name: str, value: Any) -> None: - # Catch the first iteration - if name == "_instantiating" or self._instantiating: - super().__setattr__(name, value) - return - if name in self.parameters: - self.client.trigger_parameter(name, value) - return - msg = "Cannot manually set attributes in a ProxyInstrument" - raise AttributeError(msg) - - def start(self) -> None: - pass - - def close(self) -> None: - self.client.disconnect() - - @property - def info(self) -> InstrumentInfo: - return self.client.get_info() - - -class Client(ClientBase): - def ping(self, destination: str) -> Packet | None: - ping_packet = Packet( - intent=PacketIntent.PING, request="PING", source=self.name, destination=destination, hops=0, payload=None - ) - return self.ask(ping_packet) - - def get_available_devices(self, provider_name: str) -> dict[str, str]: - packet = self.create_data_packet(provider_name, "GET_DEVICES", None) - response = self.ask(packet) - - if not isinstance(response.payload, dict): - msg = "Payload is not a dictionary." - raise PacketError(msg) - - return response.payload - - def get_device(self, provider_name: str, device_name: str, timeout_ms: int = 60_000) -> Instrument: - packet = self.create_data_packet(provider_name, "GET_DEVICE_STRUCTURE", device_name) - - response = self.ask(packet) - - if response.intent == PacketIntent.ERROR: - raise PacketError(str(response)) - - if not isinstance(response.payload, dict): - msg = "Payload is not a dictionary." - raise PacketError(msg) - - if response.payload["name"] != device_name: - msg = f"No device named {device_name}" - raise ValueError(msg) - - return ProxyInstrument( - name=response.payload["name"], - desc=response.payload["desc"], - hw_address=response.payload["hw_address"], - host=self.host, - port=self.port, - router_name=self.router_name, - timeout_ms=timeout_ms, - instrument_name=response.payload["name"], - provider_name=provider_name, - parameters=set(response.payload["parameters"]), - operations=response.payload["operations"], - ) diff --git a/src/pqnstack/network/instrument_provider.py b/src/pqnstack/network/instrument_provider.py deleted file mode 100644 index 709f2259..00000000 --- a/src/pqnstack/network/instrument_provider.py +++ /dev/null @@ -1,402 +0,0 @@ -import datetime -import importlib -import logging -import pickle -from typing import Any - -import zmq - -from pqnstack.base.errors import CouldNotConnectToNetworkElementError -from pqnstack.base.errors import InvalidInstrumentsConfigurationError -from pqnstack.base.instrument import Instrument -from pqnstack.network.packet import NetworkElementClass -from pqnstack.network.packet import Packet -from pqnstack.network.packet import PacketIntent -from pqnstack.network.packet import create_registration_packet - -logger = logging.getLogger(__name__) - - -class InstrumentProvider: - def __init__( - self, - name: str, - host: str = "localhost", - port: int = 5555, - router_name: str = "router1", - beat_period: int = 1000, - **instruments: dict[str, Any], - ) -> None: - """ - InstrumentProvider class for PQN. - - A InstrumentProvider is the class that talks with real hardware and performs experiments. It talks to a - single `Router` instance through zqm and awaits for instructions from it. Every `beat_interval` milliseconds, - sends a registration packet to the router. - This is done so if the router goes offline, the provider can reconnect to the router automatically. - - :param name: Name for the InstrumentProvider. - :param host: Hostname or IP address of the Router this provider talks to. - :param port: Port of the name of the Router this provider talks to. - :param router_name: Name of the Router this provider talks to. - :param beat_period: Interval in milliseconds to send a beat to the Router. - :param instruments: Instruments is a Dictionary holding the necessary instructions to initialize any hardware - the InstrumentProvider talks to. The keys are the names of the instruments, every key has another dictionary as its value - with all the necessary instructions to initialize the instrument. Inside of the dictionary for the specific - instrument, a key called 'import' is required holding the import path for that specific instrument. - Note that the name is not necessary since that is the key of the dictionary. - - e.g. - ``` - instruments = { - "rotator_1": { - "import": "pqnstack.pqn.drivers.rotator.Rotator", - "desc": "Rotator in optical table 1", - "hw_address": "83860213", - **extra_kwargs - } - } - """ - self.name = name - self.host = host - self.port = port - self.address = f"tcp://{host}:{port}" - self.router_name = router_name - self.beat_period = beat_period - self._beats_since_reply = 0 - self._last_received_beat: datetime.datetime | None = None - # How many sent beats with no replies do we start logging warnings for disconnected routers. - self._disconnected_threshold = 3 - - self.context: zmq.Context[zmq.Socket[bytes]] | None = None - self.socket: zmq.Socket[bytes] | None = None # Has the instance of the socket talking to the router. - - # Verify that every instrument contains the minimum required keys. - for ins_name, ins_dict in instruments.items(): - if not isinstance(ins_dict, dict): - msg = f"{ins_name} is not a dictionary, please provide the necessary instructions for this instrument" - raise InvalidInstrumentsConfigurationError(msg) - - if "import" not in ins_dict: - msg = f"{ins_name} is missing its 'import' key, please provide an import path for this instrument" - raise InvalidInstrumentsConfigurationError(msg) - - if "desc" not in ins_dict: - msg = f"{ins_name} is missing its 'desc' key, please provide a description for this instrument" - raise InvalidInstrumentsConfigurationError(msg) - - if "hw_address" not in ins_dict: - msg = f"{ins_name} is missing its 'hw_address' key, please provide an address for this instrument" - raise InvalidInstrumentsConfigurationError(msg) - - self.instruments = instruments - self.instantiated_instruments: dict[str, Instrument] = {} - - self.running = False - - def instantiate_instruments(self) -> None: - for ins_name, ins_dict in self.instruments.items(): - ins_import = ins_dict.pop("import") - ins_desc = ins_dict.pop("desc") - ins_hw_address = ins_dict.pop("hw_address") - - logger.info("Instantiating %s", ins_name) - try: - module_name, class_name = ins_import.rsplit(".", 1) - module = importlib.import_module(module_name) - class_ = getattr(module, class_name) - except (ImportError, AttributeError) as e: - msg = f"Could not import {ins_import}. Please verify the import path for this instrument." - raise InvalidInstrumentsConfigurationError(msg) from e - - try: - ins = class_(name=ins_name, desc=ins_desc, hw_address=ins_hw_address, **ins_dict) - ins.start() - # FIXME: Figure out what the exception type could be if the instrument cannot be instantiated. - except Exception as e: - msg = f"Could not instantiate {ins_import}. Please verify the parameters for this instrument." - raise InvalidInstrumentsConfigurationError(msg) from e - - self.instantiated_instruments[ins_name] = ins - logger.info("Successfully instantiated %s", ins_name) - - def start(self) -> None: - self.instantiate_instruments() - - logger.info("Starting provider %s at %s", self.name, self.address) - self.context = zmq.Context() - self.socket = self.context.socket(zmq.DEALER) - self.socket.setsockopt_string(zmq.IDENTITY, self.name) - - # Wait 10 beats for the first check before timing out - self.socket.setsockopt(zmq.RCVTIMEO, self.beat_period * 10) - try: - self._beat() - except zmq.error.Again as er: - logger.exception("Could not connect to router at %s", self.address) - msg = "Could not connect to router." - raise CouldNotConnectToNetworkElementError(msg) from er - - # Set the beat interval to the normal value. - self.socket.setsockopt(zmq.RCVTIMEO, self.beat_period) - self.running = True - - try: - while self.running: - try: - packet = self._listen() - except zmq.error.ZMQError: - logger.debug("Time interval happened, sending a beat.") - self._beat() - continue - - match packet.intent: - case PacketIntent.PING: - response = self._handle_ping(packet) - self.socket.send(pickle.dumps(response)) - - case PacketIntent.REGISTRATION_ACK: - self._handle_reg_acknowledge() - - case PacketIntent.DATA: - match packet.request: - case "GET_DEVICES": - response = self._handle_get_devices(packet) - self.socket.send(pickle.dumps(response)) - - case "GET_DEVICE_STRUCTURE": - response = self._handle_get_device_structure(packet) - self.socket.send(pickle.dumps(response)) - - case PacketIntent.CONTROL: - response = self._handle_instrument_control(packet) - self.socket.send(pickle.dumps(response)) - - finally: - self.socket.close() - - def _listen(self) -> Packet: - # This should never happen, but mypy complains if the check is not done - if self.socket is None: - msg = "Socket is None, cannot listen." - logger.error(msg) - raise RuntimeError(msg) - - _, pickled_packet = self.socket.recv_multipart() - packet = pickle.loads(pickled_packet) - if packet.destination != self.name: - # FIXME: This should return an error packet instead of just crashing - msg = f"Packet intended for {packet.destination} but received by {self.name}. Packet: {packet}" - raise RuntimeError(msg) - - logger.info("Received packet: %s", packet) - - if not isinstance(packet, Packet): - msg = f"Received packet is not a Packet object, got {type(packet)}" - raise TypeError(msg) - - return packet - - def _beat(self) -> None: - """ - Execute a single beat to the ROUTER. - - This is the same process as initial registration. If the Router cannot be reached, logs the message and keeps going. - - :return: - """ - if self._beats_since_reply > self._disconnected_threshold: - local_time = self._last_received_beat.astimezone() if self._last_received_beat else None - logger.warning( - "Router at %s did not reply to heartbeat, router seems offline. Will keep trying to reconnect. \n Beats since reply: %s. Time at last reply %s", - self.address, - self._beats_since_reply, - local_time, - ) - - try: - # This should never happen, but mypy complains if the check is not done - if self.socket is None: - msg = "Socket is None, cannot listen." - logger.error(msg) - raise RuntimeError(msg) - - self.socket.connect(self.address) - reg_packet = create_registration_packet( - source=self.name, destination=self.router_name, payload=NetworkElementClass.PROVIDER, hops=0 - ) - self.socket.send(pickle.dumps(reg_packet)) - logger.info("Sent registration packet to router at %s", self.address) - self._beats_since_reply += 1 - - except zmq.error.Again: - logger.warning("Error while sending beat to router at %s", self.address) - - def _handle_reg_acknowledge(self) -> None: - logger.info("InstrumentProvider %s is connected to router at %s", self.name, self.address) - self.running = True - self._beats_since_reply = 0 - self._last_received_beat = datetime.datetime.now(tz=datetime.UTC) - - def _handle_ping(self, packet: Packet) -> Packet: - return Packet( - intent=PacketIntent.PING, request="PONG", source=self.name, destination=packet.source, payload=None - ) - - def _handle_get_devices(self, packet: Packet) -> Packet: - ret_instruments = {name: type(ins) for name, ins in self.instantiated_instruments.items()} - return Packet( - intent=PacketIntent.DATA, - request="GET_DEVICES", - source=self.name, - destination=packet.source, - payload=ret_instruments, - ) - - def _handle_get_device_structure(self, packet: Packet) -> Packet: - if packet.payload not in self.instantiated_instruments: - return self._create_error_packet(packet.source, f"Instrument '{packet.payload}' not found.") - - ins_name = packet.payload - if not isinstance(ins_name, str): - return self._create_error_packet( - packet.source, f"Payload must be the instrument name as a string, not {type(ins_name)}" - ) - - params = self.instantiated_instruments[ins_name].parameters - operations = set(self.instantiated_instruments[ins_name].operations.keys()) - - payload = { - "name": self.instantiated_instruments[ins_name].name, - "desc": self.instantiated_instruments[ins_name].desc, - "hw_address": self.instantiated_instruments[ins_name].hw_address, - "parameters": params, - "operations": operations, - } - - return Packet( - intent=PacketIntent.DATA, - request="GET_DEVICE_STRUCTURE", - source=self.name, - destination=packet.source, - payload=payload, - ) - - def _validate_instrument_control_packet( - self, packet: Packet - ) -> tuple[str, str, str, Instrument, tuple[Any, ...], dict[str, Any]] | Packet: - request_parts = packet.request.split(":") - correct_request_len = 3 - if len(request_parts) != correct_request_len: - msg = ( - f"CONTROL packets should have a request field with 3 parts divided by a ':', " - f"not {len(request_parts)}, formatted as: " - f"::" - ) - return self._create_error_packet(packet.source, msg) - - ins_name, request_type, request_name = request_parts - if ins_name not in self.instantiated_instruments: - return self._create_error_packet(packet.source, f"Instrument '{ins_name}' not found.") - - instrument = self.instantiated_instruments[ins_name] - - if request_type not in ["OPERATION", "PARAMETER", "INFO"]: - msg = f"Request type must be either 'OPERATION', 'PARAMETER', 'INFO', not {request_type}" - return self._create_error_packet(packet.source, msg) - - if not isinstance(packet.payload, tuple): - msg = ( - f"Payload must be a tuple with the arguments and kwargs (have empty args and kwargs " - f"if not necessary) for the operation or parameter, not {type(packet.payload)}" - ) - return self._create_error_packet(packet.source, msg) - - args, kwargs = packet.payload - return ins_name, request_type, request_name, instrument, args, kwargs - - def _handle_operation_control( - self, request_name: str, instrument: Instrument, packet: Packet, args: tuple[Any, ...], kwargs: dict[str, Any] - ) -> Packet: - if request_name not in instrument.operations: - return self._create_error_packet( - packet.source, f"Operation '{request_name}' not found in '{instrument.name}'" - ) - - try: - operation_ret = instrument.operations[request_name](*args, **kwargs) - # Adding ruff exception due to not know what type of exceptions instruments can raise. - except Exception as e: # noqa:BLE001 - msg = f"Error executing operation '{request_name}' in '{instrument.name}'. Error: {e}" - return self._create_error_packet(packet.source, msg) - - return self._create_control_packet(packet.source, f"{instrument.name}:OPERATION:{request_name}", operation_ret) - - def _handle_parameter_control( - self, request_name: str, instrument: Instrument, packet: Packet, args: tuple[Any, ...], kwargs: dict[str, Any] - ) -> Packet: - if request_name not in instrument.parameters: - return self._create_error_packet( - packet.source, f"Parameter '{request_name}' not found in '{instrument.name}'" - ) - - # Check if this is just reading the parameter or setting it. - if len(args) == 0 and len(kwargs) == 0: - try: - parameter_ret = getattr(instrument, request_name) - except AttributeError as e: - msg = f"Error reading parameter '{request_name}' in '{instrument.name}'. Error: {e}" - return self._create_error_packet(packet.source, msg) - - return self._create_control_packet( - packet.source, f"{instrument.name}:PARAMETER:{request_name}", parameter_ret - ) - - try: - setattr(instrument, request_name, *args, **kwargs) - # TODO: Double check this exception type, I am not entirely sure this would work. - except AttributeError as e: - msg = f"Error setting parameter '{request_name}' in '{instrument.name}'. Error: {e}" - return self._create_error_packet(packet.source, msg) - - return self._create_control_packet(packet.source, f"{instrument.name}:PARAMETER:{request_name}", "OK") - - def _handle_instrument_control(self, packet: Packet) -> Packet: - validated_packet = self._validate_instrument_control_packet(packet) - - # If it's a packet it means that there was an error with the incoming packet so return the error packet. - if isinstance(validated_packet, Packet): - return validated_packet - - ins_name, request_type, request_name, instrument, args, kwargs = validated_packet - - if request_type == "OPERATION": - return self._handle_operation_control(request_name, instrument, packet, args, kwargs) - - if request_type == "PARAMETER": - return self._handle_parameter_control(request_name, instrument, packet, args, kwargs) - - if request_type == "INFO": - return self._create_control_packet(packet.source, f"{ins_name}:INFO", instrument.info) - - # All the possible packet options should have been handled by now, so if we get here, something went wrong. - msg = f"Something inside provider {self.name} went wrong. Check that your packet is correct and try again." - return self._create_error_packet(packet.source, msg) - - def _create_error_packet(self, destination: str, error_msg: str) -> Packet: - return Packet( - intent=PacketIntent.ERROR, - request="ERROR", - source=self.name, - destination=destination, - payload=error_msg, - ) - - def _create_control_packet(self, destination: str, request: str, payload: Any) -> Packet: - return Packet( - intent=PacketIntent.CONTROL, - request=request, - source=self.name, - destination=destination, - payload=payload, - ) diff --git a/src/pqnstack/network/packet.py b/src/pqnstack/network/packet.py deleted file mode 100644 index c6b63927..00000000 --- a/src/pqnstack/network/packet.py +++ /dev/null @@ -1,60 +0,0 @@ -# University of Illinois Urbana-Champaign -# Public Quantum Network -# -# NCSA/Illinois Computes -# -# -from dataclasses import dataclass -from enum import Enum -from enum import auto -from typing import Any - -from pqnstack.base.errors import PacketError - - -class NetworkElementClass(Enum): - ROUTER = auto() - PROVIDER = auto() - CLIENT = auto() - TELEMETRY = auto() - - -class PacketIntent(Enum): - DATA = auto() - PROTOCOL = auto() - CONTROL = auto() - REGISTRATION = auto() - REGISTRATION_ACK = auto() - ROUTING = auto() # These are used for discovering network topology automatically - PING = auto() - ERROR = auto() - - -@dataclass(kw_only=True) -class Packet: - intent: PacketIntent - request: str - source: str - destination: str - payload: object = None - hops: int = 0 - version: int = 1 - - def signature(self) -> tuple[str, str, str]: - return self.intent.name, self.request, str(self.payload) - - def routing(self) -> tuple[str, str]: - return self.source, self.destination - - -def create_registration_packet(**kwargs: Any) -> Packet: - if "payload" not in kwargs: - msg = "payload argument not present when creating registration packet." - raise PacketError(msg) - - if not isinstance(kwargs["payload"], NetworkElementClass): - msg = "payload argument must be of type NetworkElementClass." - raise PacketError(msg) - - kwargs |= {"intent": PacketIntent.REGISTRATION, "request": "REGISTER"} - return Packet(**kwargs) diff --git a/src/pqnstack/network/router.py b/src/pqnstack/network/router.py deleted file mode 100644 index dca0f810..00000000 --- a/src/pqnstack/network/router.py +++ /dev/null @@ -1,152 +0,0 @@ -import copy -import logging -import pickle - -import zmq - -from pqnstack.network.packet import NetworkElementClass -from pqnstack.network.packet import Packet -from pqnstack.network.packet import PacketIntent - -logger = logging.getLogger(__name__) - - -# FIXME: handle not finding destination and source better -class Router: - def __init__(self, name: str, host: str = "localhost", port: int = 5555) -> None: - self.name = name - self.host = host - self.port = port - - # TODO: Verify that this address is valid - self.address = f"tcp://{host}:{port}" - - # FIXME, breaking this into 3 different dictionaries is probably not the way to go. - self.routers: dict[str, bytes] = {} # Holds what other routers are in the network - self.providers: dict[str, bytes] = {} - self.clients: dict[str, bytes] = {} - - self.context: zmq.Context[zmq.Socket[bytes]] | None = None - self.socket: zmq.Socket[bytes] | None = None - self.running = False - - def start(self) -> None: - logger.info("Starting router %s at %s", self.name, self.address) - self.context = zmq.Context() - self.socket = self.context.socket(zmq.ROUTER) - self.socket.bind(self.address) - logger.info("Router %s is now listening on %s", self.name, self.address) - self.running = True - - try: - while self.running: - identity_binary, packet = self.listen() - if packet is None or identity_binary is None: - logger.error("Error listening to packets. Either the packet is None or the identity is None.") - continue - - match packet.intent: - case PacketIntent.REGISTRATION: - self.handle_registration(identity_binary, packet) - case PacketIntent.ROUTING: - logger.info("Got routing packet from %s", identity_binary) - case _: - self.handle_pass_packet(identity_binary, packet) - - finally: - self.socket.close() - - def handle_registration(self, identity_binary: bytes, packet: Packet) -> None: - if packet.destination != self.name: - self.handle_packet_error(identity_binary, f"Router {self.name} is not the destination") - return - match packet.payload: - case NetworkElementClass.PROVIDER: - self.providers[packet.source] = identity_binary - logger.info("InstrumentProvider %s registered", identity_binary) - case NetworkElementClass.CLIENT: - self.clients[packet.source] = identity_binary - logger.info("Client %s registered", identity_binary) - case NetworkElementClass.ROUTER: - self.routers[packet.source] = identity_binary - logger.info("Router %s registered", identity_binary) - - ack_packet = Packet( - intent=PacketIntent.REGISTRATION_ACK, - source=self.name, - destination=identity_binary.decode("utf-8"), - hops=0, - request="ACKNOWLEDGE", - payload=None, - ) - self._send(identity_binary, ack_packet) - - def handle_pass_packet(self, identity_binary: bytes, packet: Packet) -> None: - """Handle all the logic to get a packet from one place to another.""" - if packet.destination == self.name: - logger.info("Packet destination is self, dropping") - - elif packet.destination in self.providers or packet.destination in self.clients: - logger.info("Packet destination is a provider called %s, routing message there", packet.destination) - forward_packet = copy.copy(packet) - forward_packet.hops += 1 - dest = self.providers.get(packet.destination) or self.clients.get(packet.destination) - if dest is None: - self.handle_packet_error(identity_binary, f"Destination {packet.destination} not found.") - return - self._send(dest, forward_packet) - logger.info("Sent packet to %s", packet.destination) - - else: - logger.info("Packet destination is not a provider will ask other routers in system") - # FIXME: This is temporary and should be replaced with the routing algorithm. - self.handle_packet_error(identity_binary, "Routing not implemented yet.") - - def listen(self) -> tuple[bytes, Packet] | tuple[None, None]: - # This should never happen, but mypy complains if the check is not done - if self.socket is None: - msg = "Socket is None, cannot listen." - logger.error(msg) - raise RuntimeError(msg) - - # Depending on who is sending a request, the number of items received will be different. This is not - # DEALER sockets send 2 items, REQ sockets send an empty delimiter. - dealer_parts = 2 - req_parts = 3 - request = self.socket.recv_multipart() - if len(request) == dealer_parts: - identity_binary, pickled_packet = request - elif len(request) == req_parts: - identity_binary, _, pickled_packet = request - else: - self.handle_packet_error(request[0], f"Requests can only have 2 or 3 parts, not {len(request)}") - return None, None - - packet = pickle.loads(pickled_packet) - logger.info("Received packet from %s: %s", identity_binary, packet) - return identity_binary, packet - - def _send(self, destination: bytes, packet: Packet) -> None: - # This should never happen, but mypy complains if the check is not done - if self.socket is None: - msg = "Socket is None, cannot send message." - logger.error(msg) - raise RuntimeError(msg) - - logger.info("Sending packet to %s | Packet: %s", packet.destination, packet) - self.socket.send_multipart([destination, b"", pickle.dumps(packet)]) - logger.info("Packet sent to %s", packet.destination) - - # TODO: This should reply with a standard, error in your packet message to whoever sent the packet instead of - # just logging. - def handle_packet_error(self, destination: bytes, message: str) -> None: - logger.error(message) - error_packet = Packet( - intent=PacketIntent.ERROR, - request="ERROR", - source=self.name, - destination=destination.decode("utf-8"), - hops=0, - payload=message, - ) - self._send(destination, error_packet) diff --git a/src/pqnstack/pqn/__init__.py b/src/pqnstack/pqn/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/pqnstack/pqn/drivers/__init__.py b/src/pqnstack/pqn/drivers/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/pqnstack/pqn/drivers/chsh_driver.py b/src/pqnstack/pqn/drivers/chsh_driver.py deleted file mode 100644 index 6c0b53b8..00000000 --- a/src/pqnstack/pqn/drivers/chsh_driver.py +++ /dev/null @@ -1,84 +0,0 @@ -import logging -from dataclasses import dataclass -from dataclasses import field -from typing import cast - -from pqnstack.base.instrument import Instrument -from pqnstack.base.instrument import InstrumentInfo -from pqnstack.base.instrument import RotatorInstrument -from pqnstack.base.instrument import TimeTaggerInstrument -from pqnstack.base.instrument import log_operation -from pqnstack.network.client import Client -from pqnstack.pqn.protocols.chsh import Devices -from pqnstack.pqn.protocols.chsh import measure_chsh -from pqnstack.pqn.protocols.measurement import CHSHValue -from pqnstack.pqn.protocols.measurement import MeasurementConfig - -logger = logging.getLogger(__name__) - - -@dataclass(frozen=True, slots=True) -class CHSHInfo(InstrumentInfo): - queue_length: int = 0 - - -@dataclass(slots=True) -class CHSHDevice(Instrument): - motor_config: dict[str, dict[str, str]] = field(default_factory=dict) - tagger_config: dict[str, str] = field(default_factory=dict) - queue_length: int = field(default=0) - - _motors: dict[str, RotatorInstrument] = field(init=False, repr=False) - _tagger: TimeTaggerInstrument = field(init=False, repr=False) - _client: Client = field(init=False, repr=False) - - _players: dict[str, bool] = field(default_factory=dict, init=False, repr=False) - _submissions: dict[str, bool] = field(default_factory=dict, init=False, repr=False) - _value_gathered: dict[str, bool] = field(default_factory=dict, init=False, repr=False) - _value: int = field(default=0, init=False, repr=False) - - def __post_init__(self) -> None: - self._client = Client(host="172.30.63.109", timeout=600000) - self._motors: dict[str, RotatorInstrument] = { - motor: cast("RotatorInstrument", self._client.get_device(values["location"], values["name"])) - for motor, values in self.motor_config.items() - } - self._tagger: TimeTaggerInstrument = cast( - "TimeTaggerInstrument", self._client.get_device(self.tagger_config["location"], self.tagger_config["name"]) - ) - self.operations["measure_chsh"] = self.measure_chsh - - def start(self) -> None: - logger.info("CHSHDevice started.") - - def close(self) -> None: - logger.info("CHSHDevice closed.") - - @property - def info(self) -> CHSHInfo: - return CHSHInfo( - name=self.name, - desc=self.desc, - hw_address=self.hw_address, - queue_length=self.queue_length, - ) - - @log_operation - def measure_chsh(self, basis1: list[float], basis2: list[float], config: MeasurementConfig) -> CHSHValue: - self.queue_length += 1 - devices = Devices( - idler_hwp=self._motors["idler_hwp"], - idler_qwp=self._motors.get("idler_qwp"), - signal_hwp=self._motors["signal_hwp"], - signal_qwp=self._motors.get("signal_qwp"), - timetagger=self._tagger, - ) - - self.queue_length -= 1 - - return measure_chsh( - basis1=basis1, - basis2=basis2, - devices=devices, - config=config, - ) diff --git a/src/pqnstack/pqn/drivers/dummies.py b/src/pqnstack/pqn/drivers/dummies.py deleted file mode 100644 index 9f80d951..00000000 --- a/src/pqnstack/pqn/drivers/dummies.py +++ /dev/null @@ -1,105 +0,0 @@ -import time -from dataclasses import dataclass - -from pqnstack.base.instrument import Instrument -from pqnstack.base.instrument import InstrumentInfo -from pqnstack.base.instrument import log_operation -from pqnstack.base.instrument import log_parameter - - -@dataclass(frozen=True, slots=True) -class DummyInfo(InstrumentInfo): - param_int: int = 0 - param_str: str = "" - param_bool: bool = False - - -@dataclass(slots=True) -class DummyInstrument(Instrument): - _param_int: int = 2 - _param_str: str = "hello" - _param_bool: bool = True - connected: bool = False - - def __post_init__(self) -> None: - self.parameters = {"param_int", "param_str", "param_bool"} - self.operations = { - "double_int": self.double_int, - "lowercase_str": self.lowercase_str, - "uppercase_str": self.uppercase_str, - "toggle_bool": self.toggle_bool, - "set_half_input_int": self.set_half_input_int, - } - - @property - def info(self) -> DummyInfo: - return DummyInfo( - name=self.name, - desc=self.desc, - hw_address=self.hw_address, - param_int=self.param_int, - param_str=self.param_str, - param_bool=self.param_bool, - ) - - def start(self) -> None: - self.connected = True - - def close(self) -> None: - self.connected = False - - @property - @log_parameter - def param_int(self) -> int: - return self._param_int - - @param_int.setter - @log_parameter - def param_int(self, value: int) -> None: - self._param_int = value - - @property - @log_parameter - def param_str(self) -> str: - return self._param_str - - @param_str.setter - @log_parameter - def param_str(self, value: str) -> None: - self._param_str = value - - @property - @log_parameter - def param_bool(self) -> bool: - return self._param_bool - - @param_bool.setter - @log_parameter - def param_bool(self, value: bool) -> None: - self._param_bool = value - - @log_operation - def double_int(self) -> int: - self._param_int *= 2 - return self._param_int - - @log_operation - def set_half_input_int(self, value: int) -> int: - self._param_int = value // 2 - return self._param_int - - @log_operation - def lowercase_str(self) -> str: - self._param_str = self._param_str.lower() - return self._param_str - - @log_operation - def uppercase_str(self) -> str: - self._param_str = self._param_str.upper() - return self._param_str - - @log_operation - def toggle_bool(self) -> bool: - time.sleep(1.4) # Simulate a long operation - self._param_bool = not self._param_bool - return self._param_bool diff --git a/src/pqnstack/pqn/drivers/polarimeter.py b/src/pqnstack/pqn/drivers/polarimeter.py deleted file mode 100644 index 823809ae..00000000 --- a/src/pqnstack/pqn/drivers/polarimeter.py +++ /dev/null @@ -1,139 +0,0 @@ -import logging -import math -from collections import deque -from dataclasses import KW_ONLY -from dataclasses import dataclass -from dataclasses import field - -from pyfirmata2 import Arduino - -from pqnstack.base.instrument import PolarimeterInstrument - -logger = logging.getLogger(__name__) - - -@dataclass(slots=True) -class Buffer: - _buffer: deque[float] - normalizing: bool = field(default=False) - min: float = field(default=float("inf"), init=False) - max: float = field(default=float("-inf"), init=False) - - def __post_init__(self) -> None: - self.clear() - - def __len__(self) -> int: - return len(self._buffer) - - def clear(self) -> None: - """Clear all values in the buffer.""" - self._buffer.clear() - self.min = float("inf") - self.max = float("-inf") - - def append(self, value: float) -> None: - self._buffer.append(value) - if self.normalizing: - self.min = min(self.min, value) - self.max = max(self.max, value) - - def read(self) -> float: - if len(self._buffer) == 0: - return 0.0 - - if self.max <= self.min: - return 0.0 - - avg = sum(self._buffer) / len(self._buffer) - return (avg - self.min) / (self.max - self.min) - - -@dataclass(frozen=True, slots=True) -class PolarizationMeasurement: - h: float - v: float - d: float - a: float - _last_theta: float = field(default=0.0, repr=False, kw_only=True) # HACK: Allow reporting of full 2pi angle - - def __format__(self, spec: str, /) -> str: - if not spec: - return self.__repr__() - return f"{type(self).__name__}(h={self.h:{spec}}, v={self.v:{spec}}, d={self.d:{spec}}, a={self.a:{spec}})" - - @property - def theta(self) -> float: - """Return the calculated polarization angle in degrees.""" - if self.h + self.v == 0 or self.d + self.a == 0: - return 0.0 - - # Read polarization angle from photodiodes - h = self.h / (self.h + self.v) - radians = math.acos(math.sqrt(h)) - sign = math.copysign(1, self.a - self.d) - degrees = sign * math.degrees(radians) % 180 - - # Shift based on previous angle to allow full 0-360 range - shifted = self._last_theta // 180 - prev_wedge = self._last_theta % 180 // 60 - new_wedge = degrees // 60 - - if abs(new_wedge - prev_wedge) > 1: - shifted = not shifted - - if shifted: - degrees += 180 - - return degrees % 360 - - @property - def phi(self) -> float: - raise NotImplementedError - - -@dataclass(slots=True) -class ArduinoPolarimeter(PolarimeterInstrument): - sample_rate: int = 10 - average_width: int = 10 - _: KW_ONLY - board: Arduino = field(default_factory=lambda: Arduino(Arduino.AUTODETECT)) - pins: dict[str, int] = field(default_factory=lambda: dict(zip("hvda", range(4), strict=False))) - _buffers: list[Buffer] = field(default_factory=list, init=False) - _last_theta: float = field(default=0.0, init=False, repr=False) # HACK: Allow reporting of full 2pi angle - - def start(self) -> None: - if not self.board: - self.board = Arduino(Arduino.AUTODETECT) - self.board.samplingOn(1000 // self.sample_rate) - for pin in self.pins.values(): - buffer = Buffer(deque(maxlen=self.average_width)) - self._buffers.append(buffer) - self.board.analog[pin].register_callback(buffer.append) - self.board.analog[pin].enable_reporting() - logger.info("Polarimeter started") - - def close(self) -> None: - if self.board is not None: - logger.info("Polarimeter stopped") - self.board.exit() - - def reset(self) -> None: - self._last_theta = 0.0 - for buffer in self._buffers: - buffer.clear() - - def start_normalizing(self) -> None: - self._last_theta = 0.0 - for buffer in self._buffers: - buffer.clear() - buffer.normalizing = True - - def stop_normalizing(self) -> None: - for buffer in self._buffers: - buffer.normalizing = False - - def read(self) -> PolarizationMeasurement: - hvda = [buffer.read() for buffer in self._buffers] - pm = PolarizationMeasurement(*hvda, _last_theta=self._last_theta) - self._last_theta = pm.theta - return pm diff --git a/src/pqnstack/pqn/drivers/qkd_driver.py b/src/pqnstack/pqn/drivers/qkd_driver.py deleted file mode 100644 index cca59d2e..00000000 --- a/src/pqnstack/pqn/drivers/qkd_driver.py +++ /dev/null @@ -1,120 +0,0 @@ -from dataclasses import dataclass -from dataclasses import field -from typing import cast - -from pqnstack.base.errors import DeviceNotStartedError -from pqnstack.base.instrument import Instrument -from pqnstack.base.instrument import InstrumentInfo -from pqnstack.base.instrument import TimeTaggerInstrument -from pqnstack.base.instrument import log_operation -from pqnstack.network.client import Client - - -@dataclass(frozen=True, slots=True) -class QKDInfo(InstrumentInfo): - number_trials: int = 0 - trial_values: list[float] = field(default_factory=list) - - -@dataclass(slots=True) -class QKDDevice(Instrument): - motor_config: dict[str, dict[str, str]] = field(default_factory=dict) - tagger_config: dict[str, str] = field(default_factory=dict) - - _tagger: TimeTaggerInstrument = field(init=False, repr=False) - _client: Client = field(init=False, repr=False) - - _players: dict[str, bool] = field(default_factory=dict, init=False, repr=False) - _submissions: dict[str, bool] = field(default_factory=dict, init=False, repr=False) - _value_gathered: dict[str, bool] = field(default_factory=dict, init=False, repr=False) - _value: int = 0 - - def __post_init__(self) -> None: - self._client = Client(host="172.30.63.109", timeout=30000) - - self._players: dict[str, bool] = {"player1": False, "player2": False} - self._submissions: dict[str, bool] = {"player1": False, "player2": False} - self._value_gathered: dict[str, bool] = {"player1": False, "player2": False} - - self.operations["add_player"] = self.add_player - self.operations["remove_player"] = self.remove_player - self.operations["get_motors"] = self.get_motors - self.operations["submit"] = self.submit - self.operations["get_counts"] = self.get_counts - - def start(self) -> None: - self._set_tagger(self.tagger_config) - - def close(self) -> None: - return - - @property - def info(self) -> QKDInfo: - return QKDInfo( - name=self.name, - desc=self.desc, - hw_address=self.hw_address, - number_trials=0, - trial_values=[], - ) - - @log_operation - def _set_motors(self, **kwargs: dict[str, str]) -> None: - self.motor_config.update(kwargs) - - @log_operation - def _set_tagger(self, tagger: dict[str, str]) -> None: - self._tagger = cast("TimeTaggerInstrument", self._client.get_device(tagger["location"], tagger["name"])) - - @log_operation - def add_player(self) -> str: - for player, active in self._players.items(): - if not active: - self._players[player] = True - return player - return "" - - @log_operation - def remove_player(self, player: str) -> None: - if player in self._players: - self._players[player] = False - - @log_operation - def get_motors(self, player: str) -> dict[str, dict[str, str]]: - if player not in self._players: - return {} - key_filter = "signal" if player == "player1" else "idler" - return {name: info for name, info in self.motor_config.items() if key_filter in name} - - @log_operation - def submit(self, player: str) -> None: - if player in self._submissions: - self._submissions[player] = True - - if self._all_submitted(): - if self._tagger is None: - msg = "TimeTagger is not set" - raise DeviceNotStartedError(msg) - self._value = self._tagger.measure_correlation(1, 2, integration_time_s=5, binwidth_ps=5) - - def _all_submitted(self) -> bool: - return all(self._submissions.values()) - - def _all_measured(self) -> bool: - return all(self._value_gathered.values()) - - @log_operation - def get_counts(self, player: str) -> int: - counts = -1 - - if self._all_submitted(): - self._value_gathered[player] = True - counts = self._value - - if self._all_measured(): - self._value = 0 - for key in self._submissions: - self._submissions[key] = False - self._value_gathered[key] = False - - return counts diff --git a/src/pqnstack/pqn/drivers/rotaryencoder.py b/src/pqnstack/pqn/drivers/rotaryencoder.py deleted file mode 100644 index 04daca89..00000000 --- a/src/pqnstack/pqn/drivers/rotaryencoder.py +++ /dev/null @@ -1,47 +0,0 @@ -import atexit -from dataclasses import dataclass -from dataclasses import field -from typing import Protocol -from typing import runtime_checkable - -import serial - - -@runtime_checkable -class RotaryEncoderInstrument(Protocol): - def read(self) -> float: ... - - -@dataclass(slots=True) -class SerialRotaryEncoder: - label: str - address: str - offset_degrees: float = 0.0 - _conn: serial.Serial = field(init=False, repr=False) - - def __post_init__(self) -> None: - self._conn = serial.Serial(self.address, baudrate=115200, timeout=1) - self._conn.write(b"open_channel") - self._conn.read(100) - self._conn.write(b"ready") - self._conn.read(100) - - atexit.register(self.close) - - def close(self) -> None: - self._conn.close() - - def read(self) -> float: - self._conn.write(b"ANGLE?\n") - angle = self._conn.readline().decode().strip() - return float(angle) + self.offset_degrees - - -@dataclass(slots=True) -class MockRotaryEncoder: - """Mock rotary encoder for terminal input when hardware is not available.""" - - theta: float = 0.0 - - def read(self) -> float: - return self.theta diff --git a/src/pqnstack/pqn/drivers/rotator.py b/src/pqnstack/pqn/drivers/rotator.py deleted file mode 100644 index 6ef3ddd3..00000000 --- a/src/pqnstack/pqn/drivers/rotator.py +++ /dev/null @@ -1,131 +0,0 @@ -# University of Illinois Urbana-Champaign -# Public Quantum Network -# -# NCSA/Illinois Computes - -import logging -import time -from dataclasses import dataclass -from dataclasses import field - -import serial -from thorlabs_apt_device import KDC101 -from thorlabs_apt_device import TDC001 - -from pqnstack.base.errors import DeviceNotStartedError -from pqnstack.base.instrument import RotatorInfo -from pqnstack.base.instrument import RotatorInstrument - -logger = logging.getLogger(__name__) - - -@dataclass(slots=True) -class APTRotator(RotatorInstrument): - _degrees: float = field(default=0.0, init=False) - _device: TDC001 | KDC101 = field(init=False, repr=False) - _encoder_units_per_degree: float = field(default=86384 / 45, init=False, repr=False) - - def start(self) -> None: - # Additional setup for APT Rotator - try: - self._device = TDC001(serial_number=self.hw_address) - except RuntimeError: - self._device = KDC101(self.hw_address) - - offset_eu = round(self.offset_degrees * self._encoder_units_per_degree) - - # NOTE: Velocity units seem to not match position units - # (Device does not actually move at 1000 deg/s...) - # 500 is noticeably slower, but more than 1000 doesn't seem faster - vel = round(1000 * self._encoder_units_per_degree) - - self._device.set_home_params(velocity=vel, offset_distance=offset_eu) - self._device.set_velocity_params(vel, vel) - time.sleep(0.5) - self._wait_for_stop() - - def close(self) -> None: - if self._device is not None: - logger.info("Closing APT Rotator") - self._device.close() - - @property - def info(self) -> RotatorInfo: - return RotatorInfo( - name=self.name, - desc=self.desc, - hw_address=self.hw_address, - hw_status=self._device.status, - degrees=self.degrees, - offset_degrees=self.offset_degrees, - ) - - def _wait_for_stop(self) -> None: - if self._device is None: - msg = "Start the device before setting parameters" - raise DeviceNotStartedError(msg) - - try: - time.sleep(0.5) - while ( - self._device.status["moving_forward"] - or self._device.status["moving_reverse"] - or self._device.status["jogging_forward"] - or self._device.status["jogging_reverse"] - ): - time.sleep(0.1) - except KeyboardInterrupt: - self._device.stop(immediate=True) - - @property - def degrees(self) -> float: - return self._degrees - - @degrees.setter - def degrees(self, degrees: float) -> None: - self._set_degrees_unsafe(degrees) - self._wait_for_stop() - - def _set_degrees_unsafe(self, degrees: float) -> None: - self._degrees = degrees - self._device.move_absolute(int(degrees * self._encoder_units_per_degree)) - - -@dataclass(slots=True) -class SerialRotator(RotatorInstrument): - _degrees: float = 0.0 # The hardware doesn't support position tracking - _conn: serial.Serial = field(init=False, repr=False) - - def start(self) -> None: - self._conn = serial.Serial(self.hw_address, baudrate=115200, timeout=1) - self._conn.write(b"open_channel") - self._conn.read(100) - self._conn.write(b"motor_ready") - self._conn.read(100) - - self.degrees = self.offset_degrees - - def close(self) -> None: - self.degrees = 0 - self._conn.close() - - @property - def info(self) -> RotatorInfo: - return RotatorInfo( - name=self.name, - desc=self.desc, - hw_address=self.hw_address, - # hw_status=, - degrees=self.degrees, - offset_degrees=self.offset_degrees, - ) - - @property - def degrees(self) -> float: - return self._degrees - - @degrees.setter - def degrees(self, degrees: float) -> None: - self._conn.write(f"SRA {degrees}".encode()) - self._degrees = degrees - _ = self._conn.readline().decode() diff --git a/src/pqnstack/pqn/drivers/timetagger.py b/src/pqnstack/pqn/drivers/timetagger.py deleted file mode 100644 index e3cddd2a..00000000 --- a/src/pqnstack/pqn/drivers/timetagger.py +++ /dev/null @@ -1,95 +0,0 @@ -import logging -from dataclasses import dataclass -from dataclasses import field - -from TimeTagger import ChannelEdge -from TimeTagger import Correlation -from TimeTagger import Counter -from TimeTagger import TimeTagger -from TimeTagger import createTimeTaggerNetwork -from TimeTagger import freeTimeTagger - -from pqnstack.base.instrument import TimeTaggerInfo -from pqnstack.base.instrument import TimeTaggerInstrument - -logger = logging.getLogger(__name__) - - -@dataclass(slots=True) -class SwabianTimeTagger(TimeTaggerInstrument): - """Instantiate a SwabianTimeTagger Instrument. - - `hw_address` should be of the form "ip:port" - e.g.: hw_address = "127.0.0.1:41101". - """ - - _tagger: TimeTagger = field(init=False, repr=False) - - def start(self) -> None: - """Initialize the connection to the Swabian time tagger hardware and configures channels for potential coincidence counting.""" - logger.info("Creating Swabian Time Tagger instance.") - self._tagger = createTimeTaggerNetwork(self.hw_address) - if not self._tagger: - msg = "Failed to create time tagger. Verify hardware connection." - logger.error(msg) - raise RuntimeError(msg) - - hw_channels = self._tagger.getChannelList(ChannelEdge.Rising) - self.active_channels = [hw_channels[ch - 1] for ch in self.active_channels] - - for ch in self.active_channels: - self._tagger.setInputDelay(ch, 0) - - logger.info("Swabian Time Tagger device is now READY.") - - def close(self) -> None: - """Safely closes the connection to the Swabian time tagger hardware.""" - if self._tagger is not None: - logger.info("Closing Swabian Time Tagger connection.") - freeTimeTagger(self._tagger) - self._tagger = None - - logger.info("Swabian Time Tagger device is now OFF.") - - @property - def info(self) -> TimeTaggerInfo: - return TimeTaggerInfo( - name=self.name, - desc=self.desc, - hw_address=self.hw_address, - # hw_status=, - active_channels=self.active_channels, - test_signal_enabled=self.test_signal_enabled, - test_signal_divider=self.test_signal_divider, - ) - - def set_input_delay(self, channel: int, delay_ps: int) -> None: - self._tagger.setInputDelay(channel, delay_ps) - - def set_test_signal(self, channels: list[int], *, enable: bool = True, divider: int = 1) -> None: - self._tagger.setTestSignal(channels, enable) - if enable: - self._tagger.setTestSignalDivider(divider) - - def count_singles(self, channels: list[int], integration_time_s: float = 1.0) -> list[int]: - # TODO: use these as kwargs - _duration_ps = int(integration_time_s * 1e12) - counter = Counter(self._tagger, channels, _duration_ps, 1) - counter.startFor(_duration_ps) - counter.waitUntilFinished() - return [item[0] for item in counter.getData()] - - def measure_correlation( - self, - start_ch: int, - stop_ch: int, - integration_time_s: float = 1.0, - binwidth_ps: int = 1, - n_bins: int = int(1e5), - ) -> int: - # TODO: use these as kwargs - count_time_ps = int(integration_time_s * 1e12) - corr = Correlation(self._tagger, start_ch, stop_ch, binwidth_ps, n_bins=n_bins) - corr.startFor(count_time_ps) - corr.waitUntilFinished() - return int(max(corr.getData())) diff --git a/src/pqnstack/pqn/protocols/__init__.py b/src/pqnstack/pqn/protocols/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/pqnstack/pqn/protocols/chsh.py b/src/pqnstack/pqn/protocols/chsh.py deleted file mode 100644 index 422a3c7b..00000000 --- a/src/pqnstack/pqn/protocols/chsh.py +++ /dev/null @@ -1,104 +0,0 @@ -import datetime -import math -from dataclasses import dataclass -from time import sleep - -from pqnstack.base.instrument import RotatorInstrument -from pqnstack.base.instrument import TimeTaggerInstrument -from pqnstack.pqn.protocols.measurement import CHSHValue -from pqnstack.pqn.protocols.measurement import ExpectationValue -from pqnstack.pqn.protocols.measurement import MeasurementConfig - - -@dataclass -class Devices: - idler_hwp: RotatorInstrument - signal_hwp: RotatorInstrument - idler_qwp: RotatorInstrument | None - signal_qwp: RotatorInstrument | None - timetagger: TimeTaggerInstrument - - -def calculate_chsh_expectation_error(counts: list[int], dark_count: int = 0) -> float: - total_counts = sum(counts) - corrected_total = total_counts - 4 * dark_count - if corrected_total <= 0: - return 0 - first_term = math.sqrt(total_counts) / corrected_total - expectation = abs(counts[0] + counts[3] - counts[1] - counts[2]) - second_term = (expectation / corrected_total**2) * math.sqrt(total_counts + 4 * dark_count) - return first_term + second_term - - -def calculate_chsh_error(error_values: list[float]) -> float: - return math.sqrt(sum(x**2 for x in error_values)) - - -def basis_to_wp(basis: float) -> list[float]: - return [basis / 2, 0.0] # TODO: Make input a complex number and have the quarter waveplate angle calculated from it - - -def measure_expectation_value( - devices: Devices, config: MeasurementConfig, base1: float, base2: float -) -> ExpectationValue: - idler_wp_angles = basis_to_wp(base1) - signal_wp_angles = basis_to_wp(base2) - - angles_idler = [idler_wp_angles, [idler_wp_angles[0] + 45, idler_wp_angles[1]]] - angles_signal = [signal_wp_angles, [signal_wp_angles[0] + 45, signal_wp_angles[1]]] - - coincidence_counts = [] - for angle_idler in angles_idler: - for angle_signal in angles_signal: - devices.idler_hwp.move_to(angle_idler[0]) - devices.signal_hwp.move_to(angle_signal[0]) - if devices.idler_qwp is not None: - devices.idler_qwp.move_to(angle_idler[1]) - if devices.signal_qwp is not None: - devices.signal_qwp.move_to(angle_signal[1]) - sleep(2) - counts = devices.timetagger.measure_correlation( - config.channel1, config.channel2, int(config.integration_time_s), int(config.binwidth_ps) - ) - coincidence_counts.append(counts) - - numerator = coincidence_counts[0] - coincidence_counts[1] - coincidence_counts[2] + coincidence_counts[3] - denominator = sum(coincidence_counts) - 4 * config.dark_count - expectation_val = 0 if denominator == 0 else numerator / denominator - expectation_error = calculate_chsh_expectation_error(coincidence_counts, config.dark_count) - - return ExpectationValue( - timestamp=datetime.datetime.now(datetime.UTC).isoformat(), - input_base1=base1, - input_base2=base2, - idler_wp_angles=angles_idler, - signal_wp_angles=angles_signal, - raw_counts=coincidence_counts, - error=expectation_error, - value=expectation_val, - ) - - -def measure_chsh(basis1: list[float], basis2: list[float], devices: Devices, config: MeasurementConfig) -> CHSHValue: - expectation_values = [] - expectation_errors = [] - raw_results = [] - - for base1 in basis1: - for base2 in basis2: - raw = measure_expectation_value(devices, config, base1, base2) - expectation_values.append(raw.value) - expectation_errors.append(raw.error) - raw_results.append(raw) - - chsh_value = -1 * expectation_values[0] + expectation_values[1] + expectation_values[2] + expectation_values[3] - chsh_error = calculate_chsh_error(expectation_errors) - - return CHSHValue( - timestamp=datetime.datetime.now(datetime.UTC).isoformat(), - raw_results=raw_results, - basis1=basis1, - basis2=basis2, - chsh_value=chsh_value, - chsh_error=chsh_error, - ) diff --git a/src/pqnstack/pqn/protocols/measurement.py b/src/pqnstack/pqn/protocols/measurement.py deleted file mode 100644 index 00759f30..00000000 --- a/src/pqnstack/pqn/protocols/measurement.py +++ /dev/null @@ -1,68 +0,0 @@ -from dataclasses import dataclass - -from pydantic import BaseModel - - -class MeasurementConfig(BaseModel): - integration_time_s: float - binwidth_ps: int = 500 - channel1: int = 1 - channel2: int = 2 - dark_count: int = 0 - - -@dataclass -class ExpectationValue: - timestamp: str - input_base1: float - input_base2: float - idler_wp_angles: list[list[float]] - signal_wp_angles: list[list[float]] - raw_counts: list[int] - error: float - value: float - - -@dataclass -class CHSHValue: - timestamp: str - raw_results: list[ExpectationValue] - basis1: list[float] - basis2: list[float] - chsh_value: float - chsh_error: float - - -@dataclass(frozen=True) -class MeasurementBasis: - name: str - pairs: list[tuple[str, str]] - settings: dict[str, tuple[float, float]] - - -DEFAULT_SETTINGS: dict[str, tuple[float, float]] = { - "H": (0, 0), - "V": (45, 0), - "D": (22.5, 0), - "A": (-22.5, 0), - "R": (22.5, 45), - "L": (-22.5, 45), -} - -HV_BASIS = MeasurementBasis( - name="HV", - pairs=[("H", "H"), ("H", "V"), ("V", "H"), ("V", "V")], - settings=DEFAULT_SETTINGS, -) - -DA_BASIS = MeasurementBasis( - name="DA", - pairs=[("D", "D"), ("D", "A"), ("A", "D"), ("A", "A")], - settings=DEFAULT_SETTINGS, -) - -RL_BASIS = MeasurementBasis( - name="RL", - pairs=[("R", "R"), ("R", "L"), ("L", "R"), ("L", "L")], - settings=DEFAULT_SETTINGS, -) diff --git a/src/pqnstack/pqn/protocols/qkd.py b/src/pqnstack/pqn/protocols/qkd.py deleted file mode 100644 index 331941aa..00000000 --- a/src/pqnstack/pqn/protocols/qkd.py +++ /dev/null @@ -1,98 +0,0 @@ -from dataclasses import dataclass -from time import sleep -from typing import TYPE_CHECKING -from typing import cast - -from pqnstack.constants import DEFAULT_SETTINGS -from pqnstack.constants import HV_BASIS -from pqnstack.constants import MeasurementBasis -from pqnstack.network.client import Client -from pqnstack.network.client import ProxyInstrument -from pqnstack.pqn.protocols.measurement import MeasurementConfig -from pqnstack.pqn.protocols.visibility import calculate_visibility - -if TYPE_CHECKING: - from pqnstack.base.instrument import RotatorInstrument - - -@dataclass -class Devices: - qd: ProxyInstrument - client: Client - - -def qkd_run( - devices: Devices, - config: MeasurementConfig, - basis: MeasurementBasis = HV_BASIS, -) -> tuple[float, float]: - """ - Run a QKD protocol for a single player, independently measuring visibility. - - Parameters - ---------- - devices: Devices - basis : MeasurementBasis - Predefined measurement basis (e.g., HV_BASIS, DA_BASIS, RL_BASIS). - config : MeasurementConfig - the config for the measurement - - Returns - ------- - visibility - """ - player = devices.qd.add_player() - if not player: - msg = "No available player slots in QKD device." - raise RuntimeError(msg) - - settings = DEFAULT_SETTINGS - - key_filter = "signal" if player == "player1" else "idler" - - player_motors = devices.qd.get_motors(player) - motors: dict[str, RotatorInstrument] = { - motor_name: cast("RotatorInstrument", devices.client.get_device(info["location"], info["name"])) - for motor_name, info in player_motors.items() - } - coincidence_counts: dict[tuple[str, str], int] = {} - for index, (state1, state2) in enumerate(basis.pairs): - # Determine which state to move for this player - if player == "player1": # noqa: SIM108 - move_state = state1 if index < 2 else state2 # noqa: PLR2004 - else: - move_state = state1 if (index % 2) == 0 else state2 - - angles = settings[move_state] - hwp_angle, qwp_angle = angles - - hwp_key = f"{key_filter}_hwp" - if hwp_key in motors: - motors[hwp_key].move_to(hwp_angle) - - qwp_key = f"{key_filter}_qwp" - if qwp_key in motors: - motors[qwp_key].move_to(qwp_angle) - - sleep(config.integration_time_s) - - devices.qd.submit(player) - - counts: int | None - while (counts := devices.qd.get_counts(player)) is None: - sleep(0.5) - - coincidence_counts[(state1, state2)] = counts - - visibility, error = calculate_visibility(coincidence_counts, basis.pairs) - - devices.qd.remove_player(player) - - return visibility, error - - -if __name__ == "__main__": - from pqnstack.network.devices.client import client - - client = client(host="172.30.63.109", timeout=30000) - qd_device = client.get_device("qkd_device", "devices.qd") diff --git a/src/pqnstack/pqn/protocols/tomography.py b/src/pqnstack/pqn/protocols/tomography.py deleted file mode 100644 index a4ae070f..00000000 --- a/src/pqnstack/pqn/protocols/tomography.py +++ /dev/null @@ -1,91 +0,0 @@ -import datetime -import time -from dataclasses import dataclass - -from pqnstack.base.driver.rotator import RotatorInstrument -from pqnstack.base.instruments import TimeTaggerInstrument -from pqnstack.constants import DEFAULT_SETTINGS -from pqnstack.constants import MeasurementBasis -from pqnstack.pqn.protocols.measurement import MeasurementConfig - -_TOMOGRAPHY_STATES: list[str] = ["H", "V", "D", "A", "R", "L"] - -TOMOGRAPHY_BASIS: MeasurementBasis = MeasurementBasis( - name="TOMOGRAPHY", - pairs=[(s, i) for s in _TOMOGRAPHY_STATES for i in _TOMOGRAPHY_STATES], - settings=DEFAULT_SETTINGS, -) - - -@dataclass -class Devices: - idler_hwp: RotatorInstrument - idler_qwp: RotatorInstrument - signal_hwp: RotatorInstrument - signal_qwp: RotatorInstrument - timetagger: TimeTaggerInstrument - - -@dataclass -class TomographyValue: - timestamp: str - tomography_raw_counts: list[int] - - -def measure_tomography_raw( - devices: Devices, - config: MeasurementConfig, -) -> TomographyValue: - tomography_counts: list[int] = [] - - for signal_state, idler_state in TOMOGRAPHY_BASIS.pairs: - signal_angles: tuple[float, float] = TOMOGRAPHY_BASIS.settings[signal_state] - idler_angles: tuple[float, float] = TOMOGRAPHY_BASIS.settings[idler_state] - - devices.signal_hwp.move_to(signal_angles[0]) - devices.signal_qwp.move_to(signal_angles[1]) - devices.idler_hwp.move_to(idler_angles[0]) - devices.idler_qwp.move_to(idler_angles[1]) - - time.sleep(3) - - coincidence = devices.timetagger.measure_correlation( - config.channel1, - config.channel2, - config.integration_time_s, - config.binwidth_ps, - ) - tomography_counts.append(int(coincidence)) - - current_time: str = datetime.datetime.now(datetime.UTC).isoformat() - - return TomographyValue( - timestamp=current_time, - tomography_raw_counts=tomography_counts, - ) - - -""" -Example: -if __name__ == "__main__": - from pqnstack.network.client import Client - - client = Client(host="172.30.63.109", timeout=30000) - - idler_hwp = client.get_device("pqn_test3", "idler_hwp") - idler_qwp = client.get_device("pqn_test3", "idler_qwp") - signal_hwp = client.get_device("pqn_test3", "signal_hwp") - signal_qwp = client.get_device("pqn_test3", "signal_qwp") - timetagger = client.get_device("mini_pc", "tagger") - - devices = Devices( - idler_hwp=idler_hwp, - idler_qwp=idler_qwp, - signal_hwp=signal_hwp, - signal_qwp=signal_qwp, - timetagger=timetagger, - ) - - config = MeasurementConfig(channel1=1, channel2=2, binwidth=1_000, duration=0.5) - result = measure_tomography_raw(devices, config) -""" diff --git a/src/pqnstack/pqn/protocols/visibility.py b/src/pqnstack/pqn/protocols/visibility.py deleted file mode 100644 index ca22dbd3..00000000 --- a/src/pqnstack/pqn/protocols/visibility.py +++ /dev/null @@ -1,91 +0,0 @@ -import math -import time -from typing import Any - -from pqnstack.base.instrument import RotatorInstrument -from pqnstack.constants import MeasurementBasis -from pqnstack.pqn.protocols.measurement import MeasurementConfig - - -class Devices: - motors: dict[str, RotatorInstrument] - tagger: Any - - -def measure_visibility( - devices: Devices, - basis: MeasurementBasis, - config: MeasurementConfig, -) -> tuple[float, float]: - coincidence_counts: dict[tuple[str, str], int] = {} - - for pair in basis.pairs: - coincidence_counts[pair] = move_and_measure( - devices, - pair[0], - pair[1], - basis.settings, - config, - ) - - return calculate_visibility(coincidence_counts, basis.pairs) - - -def move_and_measure( - devices: Devices, - s_state: str, - i_state: str, - settings: dict[str, tuple[float, float]], - config: MeasurementConfig, -) -> int: - if s_state not in settings or i_state not in settings: - msg = f"State {s_state} or {i_state} is not defined in settings." - raise KeyError(msg) - - for motor_key, angle in [("signal_hwp", settings[s_state][0]), ("idler_hwp", settings[i_state][0])]: - if motor_key in devices.motors: - devices.motors[motor_key].move_to(angle) - - if "signal_qwp" in devices.motors or "idler_qwp" in devices.motors: - for motor_key, angle in [("signal_qwp", settings[s_state][1]), ("idler_qwp", settings[i_state][1])]: - if motor_key in devices.motors: - devices.motors[motor_key].move_to(angle) - - time.sleep(2) - return int( - devices.tagger.measure_correlation( - config.channel1, config.channel2, config.binwidth_ps, config.integration_time_s - ) - ) - - -def calculate_visibility( - coincidence_counts: dict[tuple[str, str], int], - pairs: list[tuple[str, str]], -) -> tuple[float, float]: - c_values = [coincidence_counts[pair] for pair in pairs] - c_max, c_min = max(c_values), min(c_values) - - denominator = (c_max + c_min) ** 2 - if denominator == 0: - return 0.0, 0.0 - - c_err = 2 * math.sqrt((c_min**2) * c_max + (c_max**2) * c_min) / denominator - return (c_max - c_min) / (c_max + c_min), c_err - - -""" -Example: - -if __name__ == "__main__": - from pqnstack.network.client import Client - - c = Client(host="172.30.63.109", timeout=300000) - idler_hwp = c.get_device("loomis_server", "signal_hwp") - signal_hwp = c.get_device("ufl_closet", "signal_hwp") - tagger = c.get_device("mini_pc", "tagger") - motors = {"idler_hwp": idler_hwp, "signal_hwp": signal_hwp} - - devices = Devices(motors=motors, tagger=tagger) - result = measure_visibility(devices, DA_BASIS) -""" diff --git a/tests/messaging/__init__.py b/tests/messaging/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/messaging/blocking_client.py b/tests/messaging/blocking_client.py deleted file mode 100644 index db89f7e3..00000000 --- a/tests/messaging/blocking_client.py +++ /dev/null @@ -1,20 +0,0 @@ -import logging - -from pqnstack.network.client import Client - -logging.basicConfig(level=logging.INFO) - -logger = logging.getLogger(__name__) - -if __name__ == "__main__": - c = Client() - - # ping provider - ping_reply = c.ping("provider1") - logger.info(ping_reply) - - instrument = c.get_device("provider1", "dummy1") - logger.info(instrument) - - # blocking operation - instrument.toggle_bool_long() diff --git a/tests/messaging/client.py b/tests/messaging/client.py deleted file mode 100644 index 0c00d935..00000000 --- a/tests/messaging/client.py +++ /dev/null @@ -1,58 +0,0 @@ -import logging - -from pqnstack.network.client import Client - -logging.basicConfig(level=logging.INFO) - -logger = logging.getLogger(__name__) - -if __name__ == "__main__": - c = Client() - - # ping provider - ping_reply = c.ping("provider1") - logger.info(ping_reply) - - devices = c.get_available_devices("provider1") - logger.info(devices) - - # Create instrument proxy - instrument = c.get_device("provider1", "dummy1") - logger.info(instrument) - logger.info("I should have the proxy object here: %s", type(instrument)) - - # Call a method on the instrument - ret = instrument.double_int() - - logger.info(ret) - - # Callable - call = instrument.double_int - logger.info(type(call)) - - # Pass argument to operation - ret = instrument.set_half_input_int(10) - logger.info(ret) - - # Passing keyword arguments - ret = instrument.set_half_input_int(value=36) - logger.info(ret) - - # Get a parameter - param = instrument.param_int - logger.info(param) - - param_str = instrument.param_str - logger.info(param_str) - - # Set a parameter - instrument.param_int = 42 - logger.info(instrument.param_int) - - # Set a parameter - try: - instrument.new_attr = 348 - except AttributeError as e: - logger.info("Caught exception: %s cannot set parameter", e) - - logger.info("Proxy instrument seems to be working correctly.") diff --git a/tests/messaging/config_example.toml b/tests/messaging/config_example.toml deleted file mode 100644 index 53709a41..00000000 --- a/tests/messaging/config_example.toml +++ /dev/null @@ -1,23 +0,0 @@ -[router] -name = "pqnstack-router" -host = "localhost" -port = 5556 - -[provider] -name = "pqnstack-provider" -router_name = "pqnstack-router" -host = "localhost" -port = 5556 -beat_period = 2000 - -[[provider.instruments]] -name = "dummy1" -import = "pqnstack.pqn.drivers.dummies.DummyInstrument" -desc = "Dummy instrument1 for testing purposes" -hw_address = "1234" - -[[provider.instruments]] -name = "dummy2" -import = "pqnstack.pqn.drivers.dummies.DummyInstrument" -desc = "Dummy instrument2 for testing purposes" -hw_address = "1234" diff --git a/tests/messaging/provider.py b/tests/messaging/provider.py deleted file mode 100644 index c2c6f96e..00000000 --- a/tests/messaging/provider.py +++ /dev/null @@ -1,16 +0,0 @@ -import logging - -from pqnstack.network.instrument_provider import InstrumentProvider - -logging.basicConfig(level=logging.INFO) - -if __name__ == "__main__": - instruments = { - "dummy1": { - "import": "pqnstack.pqn.drivers.dummies.DummyInstrument", - "desc": "Dummy Instrument 1", - "hw_address": "123456", - } - } - provider = InstrumentProvider("provider1", "127.0.0.1", 5555, **instruments) - provider.start() diff --git a/tests/messaging/router.py b/tests/messaging/router.py deleted file mode 100644 index ce8c63b3..00000000 --- a/tests/messaging/router.py +++ /dev/null @@ -1,11 +0,0 @@ -import logging - -from pqnstack.network.router import Router - -logging.basicConfig(level=logging.INFO) - -logger = logging.getLogger(__name__) - -if __name__ == "__main__": - router = Router("router1", "127.0.0.1", 5555) - router.start() diff --git a/tests/pytest/test_internal_network.py b/tests/pytest/test_internal_network.py deleted file mode 100644 index 072d5054..00000000 --- a/tests/pytest/test_internal_network.py +++ /dev/null @@ -1,142 +0,0 @@ -import logging -import shutil -import subprocess -import time -from collections.abc import Generator -from pathlib import Path -from typing import Any - -import pytest - -from pqnstack.network.client import Client -from pqnstack.network.client import ProxyInstrument -from pqnstack.network.packet import Packet -from pqnstack.network.packet import PacketIntent -from pqnstack.pqn.drivers.dummies import DummyInstrument - -logger = logging.getLogger(__name__) - - -@pytest.fixture(scope="module", autouse=True) -def messaging_services() -> Generator[None, Any, None]: - """Start router and provider services for testing.""" - logger.debug("Starting messaging services...") - # Get the path to the config file relative to this test file, not current working directory - test_dir = Path(__file__).parent - config_path = test_dir / "test_network_config.toml" - uv_path = shutil.which("uv") - if not uv_path: - msg = "Could not find 'uv' executable in PATH" - raise RuntimeError(msg) - - logger.debug("Using uv path: %s, starting router and provider with config: %s", uv_path, config_path) - - # Start router process - router_process = subprocess.Popen( # Noqa: S603 # Subprocess is used for testing purposes, not in production code. - [uv_path, "run", "pqn", "start-router", "--config", str(config_path)], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - - # Give router time to start up - time.sleep(1) - - # Start provider process - provider_process = subprocess.Popen( # Noqa: S603 # Subprocess is used for testing purposes, not in production code. - [uv_path, "run", "pqn", "start-provider", "--config", str(config_path)], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - - # Give provider time to start up and connect to router - time.sleep(1) - - # Check if provider started successfully - if provider_process.poll() is not None: - stdout, stderr = provider_process.communicate() - msg = f"Provider failed to start. Exit code: {provider_process.returncode}\nStdout: {stdout.decode()}\nStderr: {stderr.decode()}" - raise RuntimeError(msg) - - logger.debug("Services should be ready for testing") - - try: - yield - finally: - logger.debug("Cleaning up messaging services...") - - # Terminate provider first (depends on router) - if provider_process.poll() is None: - provider_process.terminate() - try: - provider_process.wait(timeout=5) - except subprocess.TimeoutExpired: - provider_process.kill() - provider_process.wait() - - # Then terminate router - if router_process.poll() is None: - router_process.terminate() - try: - router_process.wait(timeout=5) - except subprocess.TimeoutExpired: - router_process.kill() - router_process.wait() - - logger.debug("All services cleaned up") - - -def test_client_ping() -> None: - client = Client(host="localhost", port=5556, router_name="pqnstack-router", timeout=1000) - response = client.ping("pqnstack-provider") - - assert isinstance(response, Packet) - assert response.intent == PacketIntent.PING - assert response.source == "pqnstack-provider" - assert response.destination == client.name - assert response.request == "PONG" - - -def test_getting_all_instruments() -> None: - client = Client(host="localhost", port=5556, router_name="pqnstack-router", timeout=1000) - - response = client.get_available_devices("pqnstack-provider") - - instruments_names = ["dummy1", "dummy2"] - - assert instruments_names == list(response.keys()) - # Get available devices returns the __class__ of the instrument as the value. - assert isinstance(response["dummy1"], DummyInstrument.__class__) - assert isinstance(response["dummy2"], DummyInstrument.__class__) - - -def test_proxy_instrument() -> None: - client = Client(host="localhost", port=5556, router_name="pqnstack-router", timeout=1000) - - proxy_instrument = client.get_device("pqnstack-provider", "dummy1") - assert isinstance(proxy_instrument, ProxyInstrument) - - base_int = 2 - double_int = base_int * 2 - arbitrary_int = 12 - - assert proxy_instrument.name == "dummy1" - assert proxy_instrument.param_int == base_int - assert proxy_instrument.param_str == "hello" - - assert proxy_instrument.double_int() == double_int - assert proxy_instrument.param_int == double_int - - proxy_instrument.param_int = arbitrary_int - assert proxy_instrument.param_int == arbitrary_int - - assert proxy_instrument.uppercase_str() == "HELLO" - assert proxy_instrument.param_str == "HELLO" - - # Make sure you cannot add attributes to the proxy instrument - fail_flag = False - try: - proxy_instrument.new_attr = 42 - except AttributeError: - fail_flag = True - - assert fail_flag, "Should not be able to set new attributes on the ProxyInstrument" diff --git a/tests/pytest/test_network_config.toml b/tests/pytest/test_network_config.toml deleted file mode 100644 index 53709a41..00000000 --- a/tests/pytest/test_network_config.toml +++ /dev/null @@ -1,23 +0,0 @@ -[router] -name = "pqnstack-router" -host = "localhost" -port = 5556 - -[provider] -name = "pqnstack-provider" -router_name = "pqnstack-router" -host = "localhost" -port = 5556 -beat_period = 2000 - -[[provider.instruments]] -name = "dummy1" -import = "pqnstack.pqn.drivers.dummies.DummyInstrument" -desc = "Dummy instrument1 for testing purposes" -hw_address = "1234" - -[[provider.instruments]] -name = "dummy2" -import = "pqnstack.pqn.drivers.dummies.DummyInstrument" -desc = "Dummy instrument2 for testing purposes" -hw_address = "1234" From e8ac9987c93466be8407038491144b8d61c68b3f Mon Sep 17 00:00:00 2001 From: marcosf2 Date: Mon, 20 Apr 2026 15:57:17 -0300 Subject: [PATCH 2/3] Rename package to pqn_node; depend on pqn-hardware MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renames src/pqnstack → src/pqn_node, rewrites all imports that used pqnstack.{base,network,pqn} to pull from the new pqn-hardware package, and pins pqn-hardware via a git URL in pyproject.toml. - Distribution: pqnstack → pqn-node - Entry point: pqn → pqn-node - Drops hardware deps (numpy, pyfirmata2, pyzmq, thorlabs-apt-device) — they now come transitively via pqn-hardware. - CLI retains app-only commands (toggle-game, daily-report); provider and router commands live in pqn-hardware's pqn-hw CLI. Co-Authored-By: Claude Opus 4.7 --- README.md | 90 ++++++++----------- pyproject.toml | 17 ++-- src/{pqnstack => pqn_node}/__init__.py | 0 .../app => pqn_node/api}/__init__.py | 0 src/{pqnstack/app => pqn_node}/api/deps.py | 16 ++-- src/{pqnstack/app => pqn_node}/api/main.py | 18 ++-- .../api => pqn_node/api/routes}/__init__.py | 0 .../app => pqn_node}/api/routes/chsh.py | 12 +-- .../api/routes/coordination.py | 14 +-- .../app => pqn_node}/api/routes/debug.py | 8 +- .../app => pqn_node}/api/routes/games.py | 4 +- .../app => pqn_node}/api/routes/health.py | 4 +- .../app => pqn_node}/api/routes/qkd.py | 22 ++--- .../app => pqn_node}/api/routes/rng.py | 8 +- .../app => pqn_node}/api/routes/serial.py | 4 +- .../app => pqn_node}/api/routes/timetagger.py | 8 +- src/{pqnstack => pqn_node}/cli.py | 16 ++-- src/{pqnstack => pqn_node}/constants.py | 0 .../api/routes => pqn_node/core}/__init__.py | 0 src/{pqnstack/app => pqn_node}/core/config.py | 6 +- src/{pqnstack => pqn_node}/cron_manager.py | 0 src/{pqnstack => pqn_node}/daily_report.py | 10 +-- src/{pqnstack/app => pqn_node}/main.py | 2 +- src/pqnstack/app/core/__init__.py | 0 uv.lock | 29 ++++-- 25 files changed, 139 insertions(+), 149 deletions(-) rename src/{pqnstack => pqn_node}/__init__.py (100%) rename src/{pqnstack/app => pqn_node/api}/__init__.py (100%) rename src/{pqnstack/app => pqn_node}/api/deps.py (75%) rename src/{pqnstack/app => pqn_node}/api/main.py (52%) rename src/{pqnstack/app/api => pqn_node/api/routes}/__init__.py (100%) rename src/{pqnstack/app => pqn_node}/api/routes/chsh.py (96%) rename src/{pqnstack/app => pqn_node}/api/routes/coordination.py (97%) rename src/{pqnstack/app => pqn_node}/api/routes/debug.py (58%) rename src/{pqnstack/app => pqn_node}/api/routes/games.py (66%) rename src/{pqnstack/app => pqn_node}/api/routes/health.py (98%) rename src/{pqnstack/app => pqn_node}/api/routes/qkd.py (96%) rename src/{pqnstack/app => pqn_node}/api/routes/rng.py (96%) rename src/{pqnstack/app => pqn_node}/api/routes/serial.py (89%) rename src/{pqnstack/app => pqn_node}/api/routes/timetagger.py (92%) rename src/{pqnstack => pqn_node}/cli.py (92%) rename src/{pqnstack => pqn_node}/constants.py (100%) rename src/{pqnstack/app/api/routes => pqn_node/core}/__init__.py (100%) rename src/{pqnstack/app => pqn_node}/core/config.py (97%) rename src/{pqnstack => pqn_node}/cron_manager.py (100%) rename src/{pqnstack => pqn_node}/daily_report.py (98%) rename src/{pqnstack/app => pqn_node}/main.py (93%) delete mode 100644 src/pqnstack/app/core/__init__.py diff --git a/README.md b/README.md index 6dfd3762..9ac2ca7d 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,13 @@ -# PQN Stack +# pqn-node -**Software stack for Public Quantum Network (PQN) nodes** +**FastAPI node service for the Public Quantum Network (PQN)** [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/) -A distributed node based approach to quantum networks. This repository hosts all the code necessary to make the backend of nodes of the PQN function. For the frontend, see [here](https://github.com/PublicQuantumNetwork/pqn-gui). +Runs a PQN node: exposes the FastAPI routes used by the web UI, coordinates protocols between nodes, and orchestrates hardware through [`pqn-hardware`](https://github.com/PublicQuantumNetwork/pqn-hardware). Frontend lives in [pqn-gui](https://github.com/PublicQuantumNetwork/pqn-gui). +Hardware drivers, ZMQ messaging, and instrument protocols were extracted into [`pqn-hardware`](https://github.com/PublicQuantumNetwork/pqn-hardware) so that hardware and node work can evolve independently; `pqn-node` pulls it in as a git-pinned dependency.

PQN Web Interface @@ -25,12 +26,12 @@ A distributed node based approach to quantum networks. This repository hosts all PQN web interface for monitoring and controlling quantum network nodes

-Our Node is composed of multiple components. All components inside a node are part of an in internal intranet with no external world access except for quantum links to other hardware or the _Node API_. +A Node's components share an internal intranet with no external access except for quantum links to other hardware or the _Node API_. -* **Node API**: FastAPI based, handles communications with web-ui as well as Node to Node communication. Only component in a Node than can talk to other components and the outside world. Resides in [src/pqnstack/app/main.py](https://github.com/PublicQuantumNetwork/pqn-stack/blob/master/src/pqnstack/app/main.py). See the [FastAPI docs](https://fastapi.tiangolo.com/deployment/) for more options on how to run the API. -* **Lightweight Web UI**: Designed for the general public to be able to interact with quantum networks. Resides in its own repository [here](https://github.com/PublicQuantumNetwork/pqn-gui). -* **Router**: Routes messages between _Hardware Providers_, PQN developers and _Node APIs_. Uses ZMQ sockets to communicate between machines. Resides in [src/pqnstack/network/router.py](https://github.com/PublicQuantumNetwork/pqn-stack/blob/master/src/pqnstack/network/router.py). -* **Hardware Provider**: Hosts hardware resources that are provided to whoever needs them inside a Node through the use of ProxyInstruments. Resides in [src/pqnstack/network/instrument_provider.py](https://github.com/PublicQuantumNetwork/pqn-stack/blob/master/src/pqnstack/network/instrument_provider.py). +* **Node API** (this repo): FastAPI service that handles web-UI and node-to-node communication. The only component in a Node that can talk to other components and the outside world. Entry point: `src/pqn_node/main.py`. See the [FastAPI docs](https://fastapi.tiangolo.com/deployment/) for deployment options. +* **Lightweight Web UI**: For the general public to interact with quantum networks. Lives at [pqn-gui](https://github.com/PublicQuantumNetwork/pqn-gui). +* **Router** (in `pqn-hardware`): Routes ZMQ messages between _Hardware Providers_, developers, and _Node APIs_. +* **Hardware Provider** (in `pqn-hardware`): Hosts hardware resources and exposes them through ProxyInstruments. ## Quick Start @@ -45,40 +46,32 @@ Our Node is composed of multiple components. All components inside a node are pa ### Installation -1. **Clone the repository** - - ```bash - git clone https://github.com/PublicQuantumNetwork/pqn-stack.git - cd pqn-stack - ``` - -2. **Install dependencies** +```bash +git clone https://github.com/PublicQuantumNetwork/pqn-node.git +cd pqn-node +uv sync +``` - To run the fastapi backend for node operations, use: - ```bash - uv sync --extra webapp - ``` +`uv sync` fetches `pqn-hardware` at the pinned commit from its GitHub repo. ### Start a Node -To fully start a PQN Node, you need to initialize 4 different processes: +To fully start a PQN Node, four processes are typically needed: -* **PQN API** -* **Router** -* **Hardware provider** (optional) +* **PQN API** (this repo) +* **Router** (from `pqn-hardware`) +* **Hardware provider** (from `pqn-hardware`, optional) * **Web GUI** (optional) ### Set up the PQN API - - #### Config file -Before starting a Node API, you need to set up a configuration file for the Node: +Before starting a Node API, set up a configuration file: 1. **Copy the example configuration:** ```bash - cp configs/config_app_example.toml config.toml + cp configs/config_example.toml config.toml ``` > [!IMPORTANT] @@ -87,55 +80,42 @@ Before starting a Node API, you need to set up a configuration file for the Node 2. **Edit the configuration:** Open `config.toml` in your editor and replace the placeholder values with your actual settings (router addresses, instrument names, etc.). - ### Configure Router and Hardware Provider -For the first computer on the PQN, both a router and hardware provider are needed. For subsequent computers added to the same node, only a hardware provider is needed. - -Both the Router and Hardware Provider can be configured using a config file. (Alternatively you could use CLI flags for quick tests.) - -Create a TOML configuration file for the router and hardware provider (see example in [configs/config_messaging_example.toml](https://github.com/PublicQuantumNetwork/pqn-stack/blob/master/configs/config_messaging_example.toml)). The config file can contain settings for both router and provider: -- Router settings go under `[router]` -- Provider settings go under `[provider]` with instruments defined as `[[provider.instruments]]` +Router and provider live in the `pqn-hardware` package. See [pqn-hardware's README](https://github.com/PublicQuantumNetwork/pqn-hardware#quick-start) for their config format. On the first computer on the PQN, both a router and a provider are needed; subsequent computers only need a provider. Start the router: ```bash -uv run pqn start-router --config configs/config_messaging_example.toml +uv run pqn-hw start-router --config configs/router_provider.toml ``` -Start the Hardware Provider: +Start the hardware provider: ```bash -uv run pqn start-provider --config configs/config_messaging_example.toml +uv run pqn-hw start-provider --config configs/router_provider.toml ``` -**Alternative method using CLI flags:** - Pass configuration directly as command-line arguments +### Start the PQN API server -Start the Router with CLI flags: ```bash -uv run pqn start-router --name router1 --host localhost --port 5555 +uv run fastapi run src/pqn_node/main.py ``` -Start the Instrument Provider with CLI flags: -```bash -uv run pqn start-provider \ - --name provider1 \ - --router-name router1 \ - --instruments '{"dummy1": {"import": "pqnstack.pqn.drivers.dummies.DummyInstrument", "desc": "Test Instrument", "hw_address": "123456"}}' -``` +Browse protocols at http://127.0.0.1:8000/docs. -### Start the PQN API server +### Daily report + +Run or schedule the Slack health-report digest: ```bash -uv run fastapi run src/pqnstack/app/main.py +uv run pqn-node daily-report run +uv run pqn-node daily-report schedule ``` -To see the list of all the protocols that can be run, go to http://127.0.0.1:8000/docs. - ### Install the Web GUI -For instructions on how install and start the web GUI please see the repo where it lives at [https://github.com/PublicQuantumNetwork/pqn-gui](https://github.com/PublicQuantumNetwork/pqn-gui) +See [pqn-gui](https://github.com/PublicQuantumNetwork/pqn-gui) for install and start instructions. ## Acknowledgements @@ -143,4 +123,4 @@ The Public Quantum Network is supported in part by NSF Quantum Leap Challenge In ## Have questions? -Feel free to contact the PQN team at publicquantumnetwork@gmail.com. +Contact the PQN team at publicquantumnetwork@gmail.com. diff --git a/pyproject.toml b/pyproject.toml index 9219e95f..9929745b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,9 +4,9 @@ build-backend = "uv_build" [project] -name = "pqnstack" +name = "pqn-node" version = "0.2.0" -description = "Illinois Computes work on software stack for PQN." +description = "FastAPI node service for the Public Quantum Network." readme = "README.md" requires-python = ">=3.12" license = { text = "MIT" } @@ -16,19 +16,18 @@ authors = [ { name = "Soroush Hoseini", email = "soroushhoseini0@gmail.com" }, ] dependencies = [ - "numpy>=2.3.5", - "pyfirmata2>=2.5.0", - "pyzmq>=26.2.0", - "thorlabs-apt-device>=0.3.8", - "tomli-w>=1.0.0", - "typer>=0.15.1", + "pqn-hardware @ git+https://github.com/PublicQuantumNetwork/pqn-hardware.git@6440be2830fddccb09b461caea023c84b7c3bea1", "fastapi[standard]>=0.115.14", "httpx>=0.28.1", + "pydantic>=2.0", "pydantic-settings>=2.10.1", + "pyserial>=3.5", + "tomli-w>=1.0.0", + "typer>=0.15.1", ] [project.scripts] -pqn = "pqnstack.cli:app" +pqn-node = "pqn_node.cli:app" [dependency-groups] diff --git a/src/pqnstack/__init__.py b/src/pqn_node/__init__.py similarity index 100% rename from src/pqnstack/__init__.py rename to src/pqn_node/__init__.py diff --git a/src/pqnstack/app/__init__.py b/src/pqn_node/api/__init__.py similarity index 100% rename from src/pqnstack/app/__init__.py rename to src/pqn_node/api/__init__.py diff --git a/src/pqnstack/app/api/deps.py b/src/pqn_node/api/deps.py similarity index 75% rename from src/pqnstack/app/api/deps.py rename to src/pqn_node/api/deps.py index 95d33341..35ed1111 100644 --- a/src/pqnstack/app/api/deps.py +++ b/src/pqn_node/api/deps.py @@ -5,14 +5,14 @@ import httpx from fastapi import Depends -from pqnstack.app.core.config import NodeState -from pqnstack.app.core.config import get_state -from pqnstack.app.core.config import logger -from pqnstack.app.core.config import settings -from pqnstack.network.client import Client -from pqnstack.pqn.drivers.rotaryencoder import MockRotaryEncoder -from pqnstack.pqn.drivers.rotaryencoder import RotaryEncoderInstrument -from pqnstack.pqn.drivers.rotaryencoder import SerialRotaryEncoder +from pqn_node.core.config import NodeState +from pqn_node.core.config import get_state +from pqn_node.core.config import logger +from pqn_node.core.config import settings +from pqn_hardware.network.client import Client +from pqn_hardware.drivers.rotaryencoder import MockRotaryEncoder +from pqn_hardware.drivers.rotaryencoder import RotaryEncoderInstrument +from pqn_hardware.drivers.rotaryencoder import SerialRotaryEncoder async def get_http_client() -> AsyncGenerator[httpx.AsyncClient, None]: diff --git a/src/pqnstack/app/api/main.py b/src/pqn_node/api/main.py similarity index 52% rename from src/pqnstack/app/api/main.py rename to src/pqn_node/api/main.py index 2879dd07..63c2383b 100644 --- a/src/pqnstack/app/api/main.py +++ b/src/pqn_node/api/main.py @@ -1,14 +1,14 @@ from fastapi import APIRouter -from pqnstack.app.api.routes import chsh -from pqnstack.app.api.routes import coordination -from pqnstack.app.api.routes import debug -from pqnstack.app.api.routes import games -from pqnstack.app.api.routes import health -from pqnstack.app.api.routes import qkd -from pqnstack.app.api.routes import rng -from pqnstack.app.api.routes import serial -from pqnstack.app.api.routes import timetagger +from pqn_node.api.routes import chsh +from pqn_node.api.routes import coordination +from pqn_node.api.routes import debug +from pqn_node.api.routes import games +from pqn_node.api.routes import health +from pqn_node.api.routes import qkd +from pqn_node.api.routes import rng +from pqn_node.api.routes import serial +from pqn_node.api.routes import timetagger api_router = APIRouter() api_router.include_router(chsh.router) diff --git a/src/pqnstack/app/api/__init__.py b/src/pqn_node/api/routes/__init__.py similarity index 100% rename from src/pqnstack/app/api/__init__.py rename to src/pqn_node/api/routes/__init__.py diff --git a/src/pqnstack/app/api/routes/chsh.py b/src/pqn_node/api/routes/chsh.py similarity index 96% rename from src/pqnstack/app/api/routes/chsh.py rename to src/pqn_node/api/routes/chsh.py index e88d66f1..dd948fcc 100644 --- a/src/pqnstack/app/api/routes/chsh.py +++ b/src/pqn_node/api/routes/chsh.py @@ -11,14 +11,14 @@ from fastapi.responses import StreamingResponse from pydantic import BaseModel -from pqnstack.app.api.deps import ClientDep -from pqnstack.app.api.deps import StateDep -from pqnstack.app.core.config import chsh_progress_event -from pqnstack.app.core.config import settings -from pqnstack.network.client import Client +from pqn_node.api.deps import ClientDep +from pqn_node.api.deps import StateDep +from pqn_node.core.config import chsh_progress_event +from pqn_node.core.config import settings +from pqn_hardware.network.client import Client if TYPE_CHECKING: - from pqnstack.base.instrument import RotatorInstrument + from pqn_hardware.instrument import RotatorInstrument logger = logging.getLogger(__name__) diff --git a/src/pqnstack/app/api/routes/coordination.py b/src/pqn_node/api/routes/coordination.py similarity index 97% rename from src/pqnstack/app/api/routes/coordination.py rename to src/pqn_node/api/routes/coordination.py index ecd9fb8f..c7b8367a 100644 --- a/src/pqnstack/app/api/routes/coordination.py +++ b/src/pqn_node/api/routes/coordination.py @@ -12,13 +12,13 @@ from fastapi.responses import StreamingResponse from pydantic import BaseModel -from pqnstack.app.api.deps import ClientDep -from pqnstack.app.api.deps import StateDep -from pqnstack.app.core.config import NodeRole -from pqnstack.app.core.config import ask_user_for_follow_event -from pqnstack.app.core.config import protocol_cancelled_event -from pqnstack.app.core.config import settings -from pqnstack.app.core.config import user_replied_event +from pqn_node.api.deps import ClientDep +from pqn_node.api.deps import StateDep +from pqn_node.core.config import NodeRole +from pqn_node.core.config import ask_user_for_follow_event +from pqn_node.core.config import protocol_cancelled_event +from pqn_node.core.config import settings +from pqn_node.core.config import user_replied_event logger = logging.getLogger(__name__) diff --git a/src/pqnstack/app/api/routes/debug.py b/src/pqn_node/api/routes/debug.py similarity index 58% rename from src/pqnstack/app/api/routes/debug.py rename to src/pqn_node/api/routes/debug.py index 3e43d74b..d0dbfaa5 100644 --- a/src/pqnstack/app/api/routes/debug.py +++ b/src/pqn_node/api/routes/debug.py @@ -1,9 +1,9 @@ from fastapi import APIRouter -from pqnstack.app.api.deps import StateDep -from pqnstack.app.core.config import NodeState -from pqnstack.app.core.config import Settings -from pqnstack.app.core.config import settings +from pqn_node.api.deps import StateDep +from pqn_node.core.config import NodeState +from pqn_node.core.config import Settings +from pqn_node.core.config import settings router = APIRouter(prefix="/debug", tags=["debug"]) diff --git a/src/pqnstack/app/api/routes/games.py b/src/pqn_node/api/routes/games.py similarity index 66% rename from src/pqnstack/app/api/routes/games.py rename to src/pqn_node/api/routes/games.py index 99f03224..23f103b7 100644 --- a/src/pqnstack/app/api/routes/games.py +++ b/src/pqn_node/api/routes/games.py @@ -1,7 +1,7 @@ from fastapi import APIRouter -from pqnstack.app.core.config import GamesAvailability -from pqnstack.app.core.config import get_settings +from pqn_node.core.config import GamesAvailability +from pqn_node.core.config import get_settings router = APIRouter(prefix="/games", tags=["games"]) diff --git a/src/pqnstack/app/api/routes/health.py b/src/pqn_node/api/routes/health.py similarity index 98% rename from src/pqnstack/app/api/routes/health.py rename to src/pqn_node/api/routes/health.py index 607ebc47..87813ba6 100644 --- a/src/pqnstack/app/api/routes/health.py +++ b/src/pqn_node/api/routes/health.py @@ -9,8 +9,8 @@ from pydantic import BaseModel from pydantic import Field -from pqnstack.app.core.config import settings -from pqnstack.network.client import Client +from pqn_node.core.config import settings +from pqn_hardware.network.client import Client logger = logging.getLogger(__name__) diff --git a/src/pqnstack/app/api/routes/qkd.py b/src/pqn_node/api/routes/qkd.py similarity index 96% rename from src/pqnstack/app/api/routes/qkd.py rename to src/pqn_node/api/routes/qkd.py index fc090bc9..2dce92dc 100644 --- a/src/pqnstack/app/api/routes/qkd.py +++ b/src/pqn_node/api/routes/qkd.py @@ -11,19 +11,19 @@ from fastapi import status from pydantic import BaseModel -from pqnstack.app.api.deps import ClientDep -from pqnstack.app.api.deps import StateDep -from pqnstack.app.core.config import NodeRole -from pqnstack.app.core.config import NodeState -from pqnstack.app.core.config import protocol_cancelled_event -from pqnstack.app.core.config import qkd_result_received_event -from pqnstack.app.core.config import settings -from pqnstack.constants import BasisBool -from pqnstack.constants import QKDEncodingBasis -from pqnstack.network.client import Client +from pqn_node.api.deps import ClientDep +from pqn_node.api.deps import StateDep +from pqn_node.core.config import NodeRole +from pqn_node.core.config import NodeState +from pqn_node.core.config import protocol_cancelled_event +from pqn_node.core.config import qkd_result_received_event +from pqn_node.core.config import settings +from pqn_node.constants import BasisBool +from pqn_node.constants import QKDEncodingBasis +from pqn_hardware.network.client import Client if TYPE_CHECKING: - from pqnstack.base.instrument import RotatorInstrument + from pqn_hardware.instrument import RotatorInstrument logger = logging.getLogger(__name__) diff --git a/src/pqnstack/app/api/routes/rng.py b/src/pqn_node/api/routes/rng.py similarity index 96% rename from src/pqnstack/app/api/routes/rng.py rename to src/pqn_node/api/routes/rng.py index 7823de08..0ff49e80 100644 --- a/src/pqnstack/app/api/routes/rng.py +++ b/src/pqn_node/api/routes/rng.py @@ -11,10 +11,10 @@ from fastapi import status from fastapi.responses import StreamingResponse -from pqnstack.app.api.deps import ClientDep -from pqnstack.app.api.deps import StateDep -from pqnstack.app.core.config import rng_progress_event -from pqnstack.app.core.config import settings +from pqn_node.api.deps import ClientDep +from pqn_node.api.deps import StateDep +from pqn_node.core.config import rng_progress_event +from pqn_node.core.config import settings logger = logging.getLogger(__name__) diff --git a/src/pqnstack/app/api/routes/serial.py b/src/pqn_node/api/routes/serial.py similarity index 89% rename from src/pqnstack/app/api/routes/serial.py rename to src/pqn_node/api/routes/serial.py index f5811840..63d32bf4 100644 --- a/src/pqnstack/app/api/routes/serial.py +++ b/src/pqn_node/api/routes/serial.py @@ -5,10 +5,10 @@ from fastapi import APIRouter from pydantic import BaseModel -from pqnstack.app.api.deps import SERDep +from pqn_node.api.deps import SERDep if TYPE_CHECKING: - from pqnstack.pqn.drivers.rotaryencoder import MockRotaryEncoder + from pqn_hardware.drivers.rotaryencoder import MockRotaryEncoder logger = logging.getLogger(__name__) router = APIRouter(prefix="/serial", tags=["measure"]) diff --git a/src/pqnstack/app/api/routes/timetagger.py b/src/pqn_node/api/routes/timetagger.py similarity index 92% rename from src/pqnstack/app/api/routes/timetagger.py rename to src/pqn_node/api/routes/timetagger.py index 222cd2f1..c589c140 100644 --- a/src/pqnstack/app/api/routes/timetagger.py +++ b/src/pqn_node/api/routes/timetagger.py @@ -8,12 +8,12 @@ from fastapi import Query from fastapi import status -from pqnstack.app.core.config import settings -from pqnstack.network.client import Client -from pqnstack.pqn.protocols.measurement import MeasurementConfig +from pqn_node.core.config import settings +from pqn_hardware.network.client import Client +from pqn_hardware.measurement import MeasurementConfig if TYPE_CHECKING: - from pqnstack.base.instrument import TimeTaggerInstrument + from pqn_hardware.instrument import TimeTaggerInstrument logger = logging.getLogger(__name__) diff --git a/src/pqnstack/cli.py b/src/pqn_node/cli.py similarity index 92% rename from src/pqnstack/cli.py rename to src/pqn_node/cli.py index 49b348a5..130667ee 100644 --- a/src/pqnstack/cli.py +++ b/src/pqn_node/cli.py @@ -6,12 +6,12 @@ import tomli_w import typer -from pqnstack.app.core.config import get_settings -from pqnstack.app.cron_manager import describe_schedule -from pqnstack.app.cron_manager import get_daily_report_job -from pqnstack.app.cron_manager import remove_daily_report_job -from pqnstack.app.cron_manager import set_daily_report_schedule -from pqnstack.app.daily_report import run_daily_report +from pqn_node.core.config import get_settings +from pqn_node.cron_manager import describe_schedule +from pqn_node.cron_manager import get_daily_report_job +from pqn_node.cron_manager import remove_daily_report_job +from pqn_node.cron_manager import set_daily_report_schedule +from pqn_node.daily_report import run_daily_report # TODO: check if this way of handling logging from a command line script is ok. @@ -19,7 +19,7 @@ logger = logging.getLogger(__name__) -app = typer.Typer(no_args_is_help=True, help="CLI for PQN-Stack.") +app = typer.Typer(no_args_is_help=True, help="CLI for pqn-node.") daily_report_app = typer.Typer(no_args_is_help=True, help="Run and manage the daily health + Slack report.") app.add_typer(daily_report_app, name="daily-report") @@ -40,7 +40,7 @@ def toggle_game( invalid = [g for g in games if g not in valid_games] if invalid: msg = f"Game(s) must be one of: chsh, qf, ssm. Invalid: {invalid}" - raise InvalidNetworkConfigurationError(msg) + raise typer.BadParameter(msg) path = Path(config) with path.open("rb") as f: diff --git a/src/pqnstack/constants.py b/src/pqn_node/constants.py similarity index 100% rename from src/pqnstack/constants.py rename to src/pqn_node/constants.py diff --git a/src/pqnstack/app/api/routes/__init__.py b/src/pqn_node/core/__init__.py similarity index 100% rename from src/pqnstack/app/api/routes/__init__.py rename to src/pqn_node/core/__init__.py diff --git a/src/pqnstack/app/core/config.py b/src/pqn_node/core/config.py similarity index 97% rename from src/pqnstack/app/core/config.py rename to src/pqn_node/core/config.py index 21474459..9e0a1da1 100644 --- a/src/pqnstack/app/core/config.py +++ b/src/pqn_node/core/config.py @@ -10,9 +10,9 @@ from pydantic_settings import SettingsConfigDict from pydantic_settings import TomlConfigSettingsSource -from pqnstack.constants import BellState -from pqnstack.constants import QKDEncodingBasis -from pqnstack.pqn.protocols.measurement import MeasurementConfig +from pqn_node.constants import BellState +from pqn_node.constants import QKDEncodingBasis +from pqn_hardware.measurement import MeasurementConfig logger = logging.getLogger(__name__) diff --git a/src/pqnstack/cron_manager.py b/src/pqn_node/cron_manager.py similarity index 100% rename from src/pqnstack/cron_manager.py rename to src/pqn_node/cron_manager.py diff --git a/src/pqnstack/daily_report.py b/src/pqn_node/daily_report.py similarity index 98% rename from src/pqnstack/daily_report.py rename to src/pqn_node/daily_report.py index 23866dff..39ff0625 100644 --- a/src/pqnstack/daily_report.py +++ b/src/pqn_node/daily_report.py @@ -1,6 +1,6 @@ """Daily health + games report posted to Slack. -Run via `pqn daily-report` (see pqnstack.cli). Loads `DailyReportConfig`, probes +Run via `pqn-node daily-report` (see pqn_node.cli). Loads `DailyReportConfig`, probes hardware via `/health`, exercises each enabled game except SSM, and posts a single consolidated Slack digest. """ @@ -19,14 +19,14 @@ import httpx -from pqnstack.app.api.routes.chsh import ChshResult -from pqnstack.app.api.routes.health import ComponentStatus -from pqnstack.app.api.routes.health import HealthStatus +from pqn_node.api.routes.chsh import ChshResult +from pqn_node.api.routes.health import ComponentStatus +from pqn_node.api.routes.health import HealthStatus if TYPE_CHECKING: from types import FrameType - from pqnstack.app.core.config import DailyReportConfig + from pqn_node.core.config import DailyReportConfig logger = logging.getLogger(__name__) diff --git a/src/pqnstack/app/main.py b/src/pqn_node/main.py similarity index 93% rename from src/pqnstack/app/main.py rename to src/pqn_node/main.py index 840269f9..ca199420 100644 --- a/src/pqnstack/app/main.py +++ b/src/pqn_node/main.py @@ -3,7 +3,7 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from pqnstack.app.api.main import api_router +from pqn_node.api.main import api_router logging.basicConfig(level=logging.DEBUG) logger = logging.getLogger(__name__) diff --git a/src/pqnstack/app/core/__init__.py b/src/pqnstack/app/core/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/uv.lock b/uv.lock index 38f67f46..c012ee6b 100644 --- a/uv.lock +++ b/uv.lock @@ -725,17 +725,29 @@ wheels = [ ] [[package]] -name = "pqnstack" +name = "pqn-hardware" version = "0.2.0" -source = { editable = "." } +source = { git = "https://github.com/PublicQuantumNetwork/pqn-hardware.git?rev=6440be2830fddccb09b461caea023c84b7c3bea1#6440be2830fddccb09b461caea023c84b7c3bea1" } dependencies = [ - { name = "fastapi", extra = ["standard"] }, - { name = "httpx" }, { name = "numpy" }, - { name = "pydantic-settings" }, + { name = "pydantic" }, { name = "pyfirmata2" }, { name = "pyzmq" }, { name = "thorlabs-apt-device" }, + { name = "typer" }, +] + +[[package]] +name = "pqn-node" +version = "0.2.0" +source = { editable = "." } +dependencies = [ + { name = "fastapi", extra = ["standard"] }, + { name = "httpx" }, + { name = "pqn-hardware" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyserial" }, { name = "tomli-w" }, { name = "typer" }, ] @@ -753,11 +765,10 @@ dev = [ requires-dist = [ { name = "fastapi", extras = ["standard"], specifier = ">=0.115.14" }, { name = "httpx", specifier = ">=0.28.1" }, - { name = "numpy", specifier = ">=2.3.5" }, + { name = "pqn-hardware", git = "https://github.com/PublicQuantumNetwork/pqn-hardware.git?rev=6440be2830fddccb09b461caea023c84b7c3bea1" }, + { name = "pydantic", specifier = ">=2.0" }, { name = "pydantic-settings", specifier = ">=2.10.1" }, - { name = "pyfirmata2", specifier = ">=2.5.0" }, - { name = "pyzmq", specifier = ">=26.2.0" }, - { name = "thorlabs-apt-device", specifier = ">=0.3.8" }, + { name = "pyserial", specifier = ">=3.5" }, { name = "tomli-w", specifier = ">=1.0.0" }, { name = "typer", specifier = ">=0.15.1" }, ] From 04b02a1accf94de1ca26ed910eb4d1cacbd2310c Mon Sep 17 00:00:00 2001 From: marcosf2 Date: Wed, 29 Apr 2026 10:38:34 -0500 Subject: [PATCH 3/3] ruff and mypy changes --- src/pqn_node/api/deps.py | 8 ++++---- src/pqn_node/api/routes/chsh.py | 2 +- src/pqn_node/api/routes/health.py | 2 +- src/pqn_node/api/routes/qkd.py | 6 +++--- src/pqn_node/api/routes/timetagger.py | 4 ++-- src/pqn_node/cli.py | 1 - src/pqn_node/constants.py | 1 - src/pqn_node/core/config.py | 2 +- 8 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/pqn_node/api/deps.py b/src/pqn_node/api/deps.py index 35ed1111..3ef982e5 100644 --- a/src/pqn_node/api/deps.py +++ b/src/pqn_node/api/deps.py @@ -4,15 +4,15 @@ import httpx from fastapi import Depends +from pqn_hardware.drivers.rotaryencoder import MockRotaryEncoder +from pqn_hardware.drivers.rotaryencoder import RotaryEncoderInstrument +from pqn_hardware.drivers.rotaryencoder import SerialRotaryEncoder +from pqn_hardware.network.client import Client from pqn_node.core.config import NodeState from pqn_node.core.config import get_state from pqn_node.core.config import logger from pqn_node.core.config import settings -from pqn_hardware.network.client import Client -from pqn_hardware.drivers.rotaryencoder import MockRotaryEncoder -from pqn_hardware.drivers.rotaryencoder import RotaryEncoderInstrument -from pqn_hardware.drivers.rotaryencoder import SerialRotaryEncoder async def get_http_client() -> AsyncGenerator[httpx.AsyncClient, None]: diff --git a/src/pqn_node/api/routes/chsh.py b/src/pqn_node/api/routes/chsh.py index dd948fcc..3784d514 100644 --- a/src/pqn_node/api/routes/chsh.py +++ b/src/pqn_node/api/routes/chsh.py @@ -9,13 +9,13 @@ from fastapi import HTTPException from fastapi import status from fastapi.responses import StreamingResponse +from pqn_hardware.network.client import Client from pydantic import BaseModel from pqn_node.api.deps import ClientDep from pqn_node.api.deps import StateDep from pqn_node.core.config import chsh_progress_event from pqn_node.core.config import settings -from pqn_hardware.network.client import Client if TYPE_CHECKING: from pqn_hardware.instrument import RotatorInstrument diff --git a/src/pqn_node/api/routes/health.py b/src/pqn_node/api/routes/health.py index 87813ba6..b6230500 100644 --- a/src/pqn_node/api/routes/health.py +++ b/src/pqn_node/api/routes/health.py @@ -6,11 +6,11 @@ import serial from fastapi import APIRouter from fastapi import Query +from pqn_hardware.network.client import Client from pydantic import BaseModel from pydantic import Field from pqn_node.core.config import settings -from pqn_hardware.network.client import Client logger = logging.getLogger(__name__) diff --git a/src/pqn_node/api/routes/qkd.py b/src/pqn_node/api/routes/qkd.py index 2dce92dc..858c3f80 100644 --- a/src/pqn_node/api/routes/qkd.py +++ b/src/pqn_node/api/routes/qkd.py @@ -9,18 +9,18 @@ from fastapi import APIRouter from fastapi import HTTPException from fastapi import status +from pqn_hardware.network.client import Client from pydantic import BaseModel from pqn_node.api.deps import ClientDep from pqn_node.api.deps import StateDep +from pqn_node.constants import BasisBool +from pqn_node.constants import QKDEncodingBasis from pqn_node.core.config import NodeRole from pqn_node.core.config import NodeState from pqn_node.core.config import protocol_cancelled_event from pqn_node.core.config import qkd_result_received_event from pqn_node.core.config import settings -from pqn_node.constants import BasisBool -from pqn_node.constants import QKDEncodingBasis -from pqn_hardware.network.client import Client if TYPE_CHECKING: from pqn_hardware.instrument import RotatorInstrument diff --git a/src/pqn_node/api/routes/timetagger.py b/src/pqn_node/api/routes/timetagger.py index c589c140..22eb740a 100644 --- a/src/pqn_node/api/routes/timetagger.py +++ b/src/pqn_node/api/routes/timetagger.py @@ -7,10 +7,10 @@ from fastapi import HTTPException from fastapi import Query from fastapi import status +from pqn_hardware.measurement import MeasurementConfig +from pqn_hardware.network.client import Client from pqn_node.core.config import settings -from pqn_hardware.network.client import Client -from pqn_hardware.measurement import MeasurementConfig if TYPE_CHECKING: from pqn_hardware.instrument import TimeTaggerInstrument diff --git a/src/pqn_node/cli.py b/src/pqn_node/cli.py index 130667ee..7490193b 100644 --- a/src/pqn_node/cli.py +++ b/src/pqn_node/cli.py @@ -13,7 +13,6 @@ from pqn_node.cron_manager import set_daily_report_schedule from pqn_node.daily_report import run_daily_report - # TODO: check if this way of handling logging from a command line script is ok. logging.basicConfig(level=logging.INFO) diff --git a/src/pqn_node/constants.py b/src/pqn_node/constants.py index d5027bd3..0d1359fd 100644 --- a/src/pqn_node/constants.py +++ b/src/pqn_node/constants.py @@ -33,4 +33,3 @@ class BellState(Enum): Phi_plus = 0 Psi_plus = 1 - diff --git a/src/pqn_node/core/config.py b/src/pqn_node/core/config.py index 9e0a1da1..46642b50 100644 --- a/src/pqn_node/core/config.py +++ b/src/pqn_node/core/config.py @@ -3,6 +3,7 @@ from enum import Enum from functools import lru_cache +from pqn_hardware.measurement import MeasurementConfig from pydantic import BaseModel from pydantic import Field from pydantic_settings import BaseSettings @@ -12,7 +13,6 @@ from pqn_node.constants import BellState from pqn_node.constants import QKDEncodingBasis -from pqn_hardware.measurement import MeasurementConfig logger = logging.getLogger(__name__)