Compare commits

..

3 Commits

15 changed files with 399 additions and 79 deletions
+14 -2
View File
@@ -7,11 +7,16 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "pygui" name = "pygui"
version = "0.1.0" version = "0.1.0"
requires-python = ">=3.11"
dependencies = [ dependencies = [
"pyyaml>=6.0.3", "PySide6>=6.6.0",
"scipy>=1.17.1", "pyqtgraph>=0.13.0",
"numpy>=1.24.0",
"pyserial>=3.5",
"pandas>=2.0.0", "pandas>=2.0.0",
"matplotlib>=3.7.0", "matplotlib>=3.7.0",
"scipy>=1.17.1",
"pyyaml>=6.0.3",
] ]
[project.optional-dependencies] [project.optional-dependencies]
@@ -84,3 +89,10 @@ files = [
dev = [ dev = [
"pytest>=9.0.3", "pytest>=9.0.3",
] ]
[tool.uv]
environments = [
"sys_platform == 'darwin'",
"sys_platform == 'win32'",
"sys_platform == 'linux'",
]
+34 -1
View File
@@ -24,8 +24,15 @@ Rectangle {
property int selectedTabIndex: 0 property int selectedTabIndex: 0
property int selectedSideActionIndex: -1 // nothing active on startup property int selectedSideActionIndex: -1 // nothing active on startup
property bool memoryRead: false
property bool waferDetected: false
Connections { Connections {
target: deviceController target: deviceController
function onDetectResult(result){
//result is a waferInfo dict on success, None on failure
root.waferDetected = (result != null && result!= undefined)
}
function onParsedDataReady(result) { function onParsedDataReady(result) {
if (result.success && result.csv_path) { if (result.success && result.csv_path) {
// Load the freshly read CSV into the player // Load the freshly read CSV into the player
@@ -36,8 +43,23 @@ Rectangle {
} }
} }
} }
Connections {
target: deviceController
function onReadResult(result) {
// result has "success" key on success, "error" key on failure
root.memoryRead = (result !== null &&
result !== undefined &&
result.success === true)
// ===== Main Two-Column Layout ===== if (root.memoryRead) {
console.log(`[P1.1] Memory read complete: ${result.bytes} bytes`)
} else {
console.log(`[P1.1] Read failed: ${result.error || "unknown"}`)
}
}
}
// ===== Main Two-Column Layout ======
RowLayout { RowLayout {
anchors.fill: parent anchors.fill: parent
spacing: 0 spacing: 0
@@ -66,6 +88,16 @@ Rectangle {
Button { Button {
id: control id: control
text: modelData text: modelData
enabled: {
switch (index) {
case 0: return true // DETECT WAFER
case 1: return root.waferDetected // READ MEMORY
case 2: return root.memoryRead // OPEN CSV IN EXCEL
case 3: return root.waferDetected // ERASE MEMORY
case 4: return root.memoryRead // IMPORT DATA
default: return true // STORED DATA
}
}
property bool isActive: index === root.selectedSideActionIndex property bool isActive: index === root.selectedSideActionIndex
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: Math.max(Theme.sideButtonMinHeight, sideRail.computedButtonHeight) Layout.preferredHeight: Math.max(Theme.sideButtonMinHeight, sideRail.computedButtonHeight)
@@ -74,6 +106,7 @@ Rectangle {
root.selectedSideActionIndex = index root.selectedSideActionIndex = index
root.selectedTabIndex = 0 // always jump to Status tab root.selectedTabIndex = 0 // always jump to Status tab
if (index === 0) { if (index === 0) {
root.memoryRead = false
streamController.setMode("review") streamController.setMode("review")
streamController.stopStream() streamController.stopStream()
deviceController.detectWafer() deviceController.detectWafer()
+26
View File
@@ -1,6 +1,7 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Dialogs
import ISC import ISC
// ===== Status Tab ===== // ===== Status Tab =====
@@ -29,6 +30,28 @@ ColumnLayout {
property string csvPath: "" property string csvPath: ""
property bool dataParsed: false property bool dataParsed: false
function cleanFolderUrl(url) {
return decodeURIComponent(String(url).replace(/^file:\/\//, ""))
}
function currentFamilyCode() {
var info = deviceController.lastWaferInfo
return info && info.length > 0 ? (info[0] || "") : ""
}
function parseAndSavePendingRead() {
deviceController.parseAndSaveData(root.currentFamilyCode(), deviceController.selectedPort || "")
}
FolderDialog {
id: saveDirDialog
title: "Choose CSV Save Folder"
onAccepted: {
deviceController.setSaveDataDir(root.cleanFolderUrl(selectedFolder))
root.parseAndSavePendingRead()
}
}
// ===== Empty state — nothing shown until detect fires ===== // ===== Empty state — nothing shown until detect fires =====
Item { Item {
Layout.fillWidth: true Layout.fillWidth: true
@@ -345,6 +368,9 @@ ColumnLayout {
root.dataRows = 0 root.dataRows = 0
root.dataCols = 0 root.dataCols = 0
root.csvPath = "" root.csvPath = ""
if (result && result.success === true)
saveDirDialog.open()
} }
} }
@@ -182,6 +182,14 @@ ColumnLayout {
font.pixelSize: 11 font.pixelSize: 11
} }
PanelCheckBox {
id: clusterAverageToggle
text: "Average Clusters"
checked: streamController.clusterAveragingEnabled
font.pixelSize: 11
onCheckedChanged: streamController.clusterAveragingEnabled = checked
}
RowLayout { RowLayout {
Layout.fillWidth: true Layout.fillWidth: true
spacing: 6 spacing: 6
@@ -29,8 +29,9 @@ Dialog {
background: Rectangle { background: Rectangle {
color: Theme.cardBackground color: Theme.cardBackground
radius: 8 radius: Theme.radiusLg
border.color: Theme.cardBorder border.color: Theme.cardBorder
border.width: Theme.borderThin
} }
ColumnLayout { ColumnLayout {
+60
View File
@@ -0,0 +1,60 @@
"""Cluster Average utility for wafer sensor values."""
from __future__ import annotations
import math
from pygui.backend.wafer.zwafer_models import Sensor
def average_clusters(values: list[float], clusters: list[list[int]]) -> list[float]:
"""Return a new list of values where members of each cluster are replaced by their mean.
NaN values are filtered out before computing the mean.
If all values in a cluster are NaN, they remain NaN.
"""
result = list(values)
for cluster in clusters:
valid_vals = []
for idx in cluster:
if idx < len(values):
val = values[idx]
if not math.isnan(val):
valid_vals.append(val)
if valid_vals:
mean_val = sum(valid_vals) / len(valid_vals)
for idx in cluster:
if idx < len(result):
result[idx] = mean_val
return result
def group_sensors_by_radius(sensors: list[Sensor], tolerance: float = 2.0) -> list[list[int]]:
"""Group sensor indices into clusters if their radial distances are within tolerance."""
# Compute radii for all sensors
radii = [(i, math.hypot(s.x, s.y)) for i, s in enumerate(sensors)]
# Filter out center sensors (r < 1.0)
non_center = [item for item in radii if item[1] >= 1.0]
if not non_center:
return []
# Sort by radius
non_center.sort(key=lambda x: x[1])
clusters: list[list[int]] = []
current_cluster: list[int] = [non_center[0][0]]
last_r = non_center[0][1]
for idx, r in non_center[1:]:
if r - last_r <= tolerance:
current_cluster.append(idx)
else:
if len(current_cluster) > 1:
clusters.append(current_cluster)
current_cluster = [idx]
last_r = r
if len(current_cluster) > 1:
clusters.append(current_cluster)
return clusters
@@ -303,7 +303,8 @@ class DeviceController(QObject):
"""Read wafer memory in a background thread. """Read wafer memory in a background thread.
Uses the family code and port from the last detect. Uses the family code and port from the last detect.
Emits readResult on completion, then auto-chains to parseAndSaveData. Emits readResult on completion; QML prompts for the save directory
before calling parseAndSaveData.
""" """
if self._operation_in_progress: if self._operation_in_progress:
self._append_log("Already busy — ignoring read request") self._append_log("Already busy — ignoring read request")
@@ -350,9 +351,7 @@ class DeviceController(QObject):
self._raw_bytes = data self._raw_bytes = data
self.readResult.emit({"success": True, "bytes": len(data)}) self.readResult.emit({"success": True, "bytes": len(data)})
self._set_operation_progress(False) self._set_operation_progress(False)
# Auto-chain: read → parse → save (matches C# behavior). self._append_log("Memory read complete - choose save directory to save CSV")
# Parse runs on main thread — it's CPU-bound but bounded (~1s).
self.parseAndSaveData(family_code, port)
self._save_status() self._save_status()
return return
self._connection_status = "Disconnected" self._connection_status = "Disconnected"
@@ -489,6 +488,7 @@ class DeviceController(QObject):
return return
self._append_log(f"Saved CSV: {csv_path}") self._append_log(f"Saved CSV: {csv_path}")
self._last_csv_path = csv_path
# Load data into the QAbstractTableModel # Load data into the QAbstractTableModel
self._data_model.load_data(temp_data, cols) self._data_model.load_data(temp_data, cols)
@@ -15,7 +15,7 @@ from pygui.backend.wafer.zwafer_models import Sensor
from pygui.backend.wafer.zwafer_parser import ZWaferParser from pygui.backend.wafer.zwafer_parser import ZWaferParser
from pygui.serialcomm.stream_reader import StreamReader from pygui.serialcomm.stream_reader import StreamReader
from pygui.backend.models.sensor_editor import SensorEditor from pygui.backend.models.sensor_editor import SensorEditor
from pygui.backend.cluster_average import average_clusters, group_sensors_by_radius
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
MODE_LIVE = "live" MODE_LIVE = "live"
@@ -24,12 +24,13 @@ MODE_REVIEW = "review"
class SessionController(QObject): class SessionController(QObject):
# public signals # public signals
frameUpdated = Signal() # any property below may have changed frameUpdated = Signal()
modeChanged = Signal() modeChanged = Signal()
stateChanged = Signal() stateChanged = Signal()
recordingChanged = Signal() recordingChanged = Signal()
sensorsChanged = Signal() sensorsChanged = Signal()
loadedFileChanged = Signal() loadedFileChanged = Signal()
clusterAveragingEnabledChanged = Signal()
# private: marshal a worker-thread frame onto the main thread # private: marshal a worker-thread frame onto the main thread
_liveFrame = Signal(object) # Frame _liveFrame = Signal(object) # Frame
@@ -59,6 +60,8 @@ class SessionController(QObject):
self._sensor_editor = SensorEditor() self._sensor_editor = SensorEditor()
self._last_raw_frame: Frame | None = None self._last_raw_frame: Frame | None = None
self._loaded_file: str = "" self._loaded_file: str = ""
self._cluster_averaging_enabled = False
self._active_clusters: list[list[int]] = []
# ---- properties QML binds to ---- # ---- properties QML binds to ----
@Property(str, notify=modeChanged) @Property(str, notify=modeChanged)
@@ -163,6 +166,18 @@ class SessionController(QObject):
"""Indices of sensors that currently have a replacement or offset.""" """Indices of sensors that currently have a replacement or offset."""
return self._sensor_editor.active_indices() return self._sensor_editor.active_indices()
@Property(bool, notify=clusterAveragingEnabledChanged)
def clusterAveragingEnabled(self) -> bool:
return self._cluster_averaging_enabled
@clusterAveragingEnabled.setter
def clusterAveragingEnabled(self, value: bool) -> None:
if self._cluster_averaging_enabled == value:
return
self._cluster_averaging_enabled = value
self.clusterAveragingEnabledChanged.emit()
self._reprocess_current()
# ---- mode + thresholds ---- # ---- mode + thresholds ----
@Slot(str) @Slot(str)
def setMode(self, mode: str) -> None: def setMode(self, mode: str) -> None:
@@ -186,7 +201,6 @@ class SessionController(QObject):
from pygui.backend.data.data_records import is_official_csv, read_data_records, read_official_csv from pygui.backend.data.data_records import is_official_csv, read_data_records, read_official_csv
from pygui.backend.wafer.wafer_layouts import WaferLayout, resolve_shape_and_size from pygui.backend.wafer.wafer_layouts import WaferLayout, resolve_shape_and_size
sensors: list[Sensor] = []
frames = [] frames = []
if is_official_csv(file_path): if is_official_csv(file_path):
@@ -218,6 +232,9 @@ class SessionController(QObject):
shape, size = resolve_shape_and_size(sensors, wafer_id) shape, size = resolve_shape_and_size(sensors, wafer_id)
self._sensors = WaferLayout(sensors, shape=shape, size=size) self._sensors = WaferLayout(sensors, shape=shape, size=size)
self._active_clusters = getattr(self._sensors, 'clusters', [])
if not self._active_clusters:
self._active_clusters = group_sensors_by_radius(self._sensors)
self._player.load(frames) self._player.load(frames)
self._model.reset() self._model.reset()
self._loaded_file = file_path self._loaded_file = file_path
@@ -270,13 +287,20 @@ class SessionController(QObject):
self._emit_current() self._emit_current()
self._play_timer.start(self._next_interval_ms()) self._play_timer.start(self._next_interval_ms())
def _apply_pipeline(self, raw_values: list[float]) -> list[float]:
"""Apply override first, then optional cluster averaging."""
edited = self._sensor_editor.apply(raw_values)
if getattr(self, "_cluster_averaging_enabled", False):
return average_clusters(edited, getattr(self, "_active_clusters", []))
return edited
def _emit_current(self) -> None: def _emit_current(self) -> None:
frame = self._player.current() frame = self._player.current()
if frame is None: if frame is None:
return return
self._last_raw_frame = frame self._last_raw_frame = frame
edited = Frame(seq=frame.seq, time=frame.time, edited = Frame(seq=frame.seq, time=frame.time,
values=self._sensor_editor.apply(frame.values)) values=self._apply_pipeline(frame.values))
self._last = self._model.process(edited) self._last = self._model.process(edited)
self.frameUpdated.emit() self.frameUpdated.emit()
self.stateChanged.emit() self.stateChanged.emit()
@@ -287,7 +311,7 @@ class SessionController(QObject):
if frame is None: if frame is None:
return return
edited = Frame(seq=frame.seq, time=frame.time, edited = Frame(seq=frame.seq, time=frame.time,
values=self._sensor_editor.apply(frame.values)) values=self._apply_pipeline(frame.values))
self._last = self._model.process(edited) self._last = self._model.process(edited)
self.frameUpdated.emit() self.frameUpdated.emit()
self.stateChanged.emit() self.stateChanged.emit()
@@ -307,11 +331,17 @@ class SessionController(QObject):
except Exception as e: except Exception as e:
log.warning("Could not load layout for %s: %s", family_code, e) log.warning("Could not load layout for %s: %s", family_code, e)
self._active_clusters = getattr(self._sensors, 'clusters', [])
if not self._active_clusters:
self._active_clusters = group_sensors_by_radius(self._sensors)
# The new binary protocol sends payload of (sensorCount * 2) bytes # The new binary protocol sends payload of (sensorCount * 2) bytes
# Each sensor is a big-endian 16-bit value (Sign-Magnitude). # Each sensor is a big-endian 16-bit value (Sign-Magnitude).
expected_sensors = 80 if (family_code or "") == "X" else 244
def parse_binary_frame(payload: bytes, seq: int) -> Frame: def parse_binary_frame(payload: bytes, seq: int) -> Frame:
values = [] values = []
num_sensors = min(80, len(payload) // 2) num_sensors = min(expected_sensors, len(payload) // 2)
for i in range(num_sensors): for i in range(num_sensors):
high_byte = payload[i * 2] high_byte = payload[i * 2]
low_byte = payload[i * 2 + 1] low_byte = payload[i * 2 + 1]
@@ -333,13 +363,9 @@ class SessionController(QObject):
self._last = None self._last = None
self._last_raw_frame = None self._last_raw_frame = None
try: transport = SerialPort.open_port(port, timeout=1)
transport = pyserial.Serial(port, SerialPort.BAUDRATE, timeout=1)
except Exception as exc:
log.warning("Baud rate %d failed on %s, falling back to 115200: %s", SerialPort.BAUDRATE, port, exc)
transport = pyserial.Serial(port, 115200, timeout=1)
# Send 'D2' command padded to 512 bytes to start the stream # Send 'D2' command padded to 512 bytes to start the stream
cmd = b"D2" + (b"F" * 510) cmd = b"D2" + SerialPort.COMMAND_PAD.encode()
transport.write(cmd) transport.write(cmd)
def on_error(exc: Exception): def on_error(exc: Exception):
@@ -381,7 +407,7 @@ class SessionController(QObject):
# Main thread (queued). Record raw, process edited; mark dirty for repaint. # Main thread (queued). Record raw, process edited; mark dirty for repaint.
self._last_raw_frame = frame self._last_raw_frame = frame
edited = Frame(seq=frame.seq, time=frame.time, edited = Frame(seq=frame.seq, time=frame.time,
values=self._sensor_editor.apply(frame.values)) values=self._apply_pipeline(frame.values))
self._last = self._model.process(edited) self._last = self._model.process(edited)
if self._recorder.is_recording: if self._recorder.is_recording:
self._recorder.write(frame) # always record raw values, never edited self._recorder.write(frame) # always record raw values, never edited
+3
View File
@@ -0,0 +1,3 @@
# ===== Backend Data Constants =====
DEFAULT_DATA_DIR_NAME = "isc_data"
+2 -1
View File
@@ -10,6 +10,7 @@ from PySide6.QtWidgets import QFileDialog, QMessageBox
from pygui.backend.data.csv_file_metadata import CSVFileMetadata from pygui.backend.data.csv_file_metadata import CSVFileMetadata
from pygui.backend.wafer.zwafer_parser import ZWaferParser from pygui.backend.wafer.zwafer_parser import ZWaferParser
from pygui.backend.data.constants import DEFAULT_DATA_DIR_NAME
# ===== File Browser Model ===== # ===== File Browser Model =====
@@ -232,7 +233,7 @@ class FileBrowser(QObject):
QStandardPaths.DocumentsLocation QStandardPaths.DocumentsLocation
) )
base_dir = Path(documents_dir) if documents_dir else (Path.home() / "Documents") base_dir = Path(documents_dir) if documents_dir else (Path.home() / "Documents")
return base_dir / "ISC_DATA" return base_dir / DEFAULT_DATA_DIR_NAME
def _set_current_directory(self, directory: Path) -> None: def _set_current_directory(self, directory: Path) -> None:
normalized = Path(directory) normalized = Path(directory)
@@ -7,6 +7,7 @@ from typing import Any
from PySide6.QtCore import QObject, Property, QDateTime, QStandardPaths, Signal, Slot from PySide6.QtCore import QObject, Property, QDateTime, QStandardPaths, Signal, Slot
from pygui.backend.data.local_settings import LocalSettings from pygui.backend.data.local_settings import LocalSettings
from pygui.backend.data.constants import DEFAULT_DATA_DIR_NAME
MASTER_FAMILIES = ("A", "B", "C", "D", "E", "F", "P", "X", "Z") MASTER_FAMILIES = ("A", "B", "C", "D", "E", "F", "P", "X", "Z")
@@ -52,7 +53,7 @@ class LocalSettingsModel(QObject):
QStandardPaths.DocumentsLocation QStandardPaths.DocumentsLocation
) )
base_dir = Path(documents_dir) if documents_dir else (Path.home() / "Documents") base_dir = Path(documents_dir) if documents_dir else (Path.home() / "Documents")
return base_dir / "ISC_DATA" return base_dir / DEFAULT_DATA_DIR_NAME
def _new_defaults(self) -> dict[str, Any]: def _new_defaults(self) -> dict[str, Any]:
return { return {
@@ -475,11 +475,11 @@ class WaferMapItem(QQuickPaintedItem):
# Calculate box top-left (lx, ly) relative to dot center (px, py) # Calculate box top-left (lx, ly) relative to dot center (px, py)
gap = 3 gap = 3
if side == "left": if side == "left":
lx = px - r - gap - text_w + ox lx = px - r - gap - text_w - ox
ly = py - text_h // 2 + oy ly = py - text_h // 2 + oy
elif side == "top": elif side == "top":
lx = px - text_w // 2 + ox lx = px - text_w // 2 + ox
ly = py - r - gap - text_h + oy ly = py - r - gap - text_h - oy
elif side == "bottom": elif side == "bottom":
lx = px - text_w // 2 + ox lx = px - text_w // 2 + ox
ly = py + r + gap + oy ly = py + r + gap + oy
+28 -25
View File
@@ -45,6 +45,33 @@ class SerialPort:
def __init__(self, port_name: str) -> None: def __init__(self, port_name: str) -> None:
self._port_name = port_name self._port_name = port_name
# ------------------------------------------------------------------
# Port opening (shared between scoped and long-lived use)
# ------------------------------------------------------------------
@staticmethod
def open_port(port_name: str, timeout: float | None = None) -> pyserial.Serial:
"""Open a serial port with baud-rate fallback.
Tries SerialPort.BAUDRATE first, falls back to 115200 on failure.
The caller owns the lifecycle of the returned Serial object."""
kwargs = dict(
parity=pyserial.PARITY_NONE,
bytesize=8,
stopbits=pyserial.STOPBITS_ONE,
xonxoff=False,
rtscts=False,
dsrdtr=False,
)
if timeout is not None:
kwargs["timeout"] = timeout
try:
return pyserial.Serial(port_name, SerialPort.BAUDRATE, **kwargs)
except Exception as exc:
log.warning("Baud rate %d failed on %s, falling back to 115200: %s", SerialPort.BAUDRATE, port_name, exc)
return pyserial.Serial(port_name, 115200, **kwargs)
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Context manager for scoped port access # Context manager for scoped port access
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@@ -52,31 +79,7 @@ class SerialPort:
@contextmanager @contextmanager
def _open(self, timeout: float | None = None) -> Iterator[pyserial.Serial]: def _open(self, timeout: float | None = None) -> Iterator[pyserial.Serial]:
"""Open the serial port, yield it, then close on exit.""" """Open the serial port, yield it, then close on exit."""
try: port = self.open_port(self._port_name, timeout=timeout)
port = pyserial.Serial(
self._port_name,
self.BAUDRATE,
parity=pyserial.PARITY_NONE,
bytesize=8,
stopbits=pyserial.STOPBITS_ONE,
xonxoff=False,
rtscts=False,
dsrdtr=False,
)
except Exception as exc:
log.warning("Baud rate %d failed on %s, falling back to 115200: %s", self.BAUDRATE, self._port_name, exc)
port = pyserial.Serial(
self._port_name,
115200,
parity=pyserial.PARITY_NONE,
bytesize=8,
stopbits=pyserial.STOPBITS_ONE,
xonxoff=False,
rtscts=False,
dsrdtr=False,
)
if timeout is not None:
port.timeout = timeout
try: try:
yield port yield port
finally: finally:
+52
View File
@@ -0,0 +1,52 @@
"""Tests for src/pygui/backend/cluster_average.py."""
import math
import pytest
from pygui.backend.cluster_average import average_clusters, group_sensors_by_radius
from pygui.backend.wafer.zwafer_models import Sensor
def test_members_take_cluster_mean():
vals = [10.0, 20.0, 100.0, 0.0]
clusters = [[0, 1], [2, 3]]
assert average_clusters(vals, clusters) == [15.0, 15.0, 50.0, 50.0]
def test_unclustered_sensors_unchanged():
vals = [10.0, 20.0, 30.0]
assert average_clusters(vals, [[0, 1]]) == [15.0, 15.0, 30.0]
def test_empty_clusters_is_identity():
assert average_clusters([1.0, 2.0], []) == [1.0, 2.0]
def test_nan_handling_in_averaging():
vals = [10.0, float('nan'), 100.0, float('nan')]
clusters = [[0, 1], [2, 3]]
res = average_clusters(vals, clusters)
assert res[0] == 10.0
assert res[1] == 10.0
assert res[2] == 100.0
assert res[3] == 100.0
def test_all_nan_cluster():
vals = [float('nan'), float('nan'), 5.0]
clusters = [[0, 1]]
res = average_clusters(vals, clusters)
assert math.isnan(res[0])
assert math.isnan(res[1])
assert res[2] == 5.0
def test_group_by_radius():
sensors = [
Sensor(label="1", x=0.0, y=0.0), # center (r=0)
Sensor(label="2", x=10.0, y=0.0), # r=10
Sensor(label="3", x=0.0, y=10.5), # r=10.5 (within 2mm)
Sensor(label="4", x=20.0, y=0.0), # r=20
]
# Center should be excluded or not grouped; other sensors grouped
clusters = group_sensors_by_radius(sensors, tolerance=2.0)
# Expected: 2 and 3 are in the same cluster because 10.5 - 10.0 <= 2.0.
assert [1, 2] in clusters
Generated
+123 -29
View File
@@ -2,12 +2,17 @@ version = 1
revision = 3 revision = 3
requires-python = ">=3.11" requires-python = ">=3.11"
resolution-markers = [ resolution-markers = [
"python_full_version >= '3.14' and sys_platform == 'darwin'",
"python_full_version < '3.14' and sys_platform == 'darwin'",
"python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform == 'win32'",
"python_full_version >= '3.14' and sys_platform == 'emscripten'",
"python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
"python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version < '3.14' and sys_platform == 'win32'",
"python_full_version < '3.14' and sys_platform == 'emscripten'", "python_full_version >= '3.14' and sys_platform == 'linux'",
"python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version < '3.14' and sys_platform == 'linux'",
]
supported-markers = [
"sys_platform == 'darwin'",
"sys_platform == 'win32'",
"sys_platform == 'linux'",
] ]
[[package]] [[package]]
@@ -24,7 +29,7 @@ name = "contourpy"
version = "1.3.3" version = "1.3.3"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "numpy" }, { name = "numpy", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" }
wheels = [ wheels = [
@@ -279,15 +284,15 @@ name = "matplotlib"
version = "3.11.0" version = "3.11.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "contourpy" }, { name = "contourpy", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "cycler" }, { name = "cycler", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "fonttools" }, { name = "fonttools", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "kiwisolver" }, { name = "kiwisolver", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "numpy" }, { name = "numpy", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "packaging" }, { name = "packaging", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "pillow" }, { name = "pillow", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "pyparsing" }, { name = "pyparsing", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "python-dateutil" }, { name = "python-dateutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/1f/24/080c99d223d158d3a8902769269ab6da5b50f7a0e6e072513907e02b7a6c/matplotlib-3.11.0.tar.gz", hash = "sha256:68c0c7be01b30dcca3638934f7f591df73401235cbdbf0d1ab1c71e7db7f8b57", size = 33251176, upload-time = "2026-06-12T02:29:15.508Z" } sdist = { url = "https://files.pythonhosted.org/packages/1f/24/080c99d223d158d3a8902769269ab6da5b50f7a0e6e072513907e02b7a6c/matplotlib-3.11.0.tar.gz", hash = "sha256:68c0c7be01b30dcca3638934f7f591df73401235cbdbf0d1ab1c71e7db7f8b57", size = 33251176, upload-time = "2026-06-12T02:29:15.508Z" }
wheels = [ wheels = [
@@ -431,9 +436,9 @@ name = "pandas"
version = "3.0.3" version = "3.0.3"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "numpy" }, { name = "numpy", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "python-dateutil" }, { name = "python-dateutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, { name = "tzdata", marker = "sys_platform == 'win32'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/f8/87/4341c6252d1c47b08768c3d25ac487362bf403f0313ddae4a2a26c9b1b4c/pandas-3.0.3.tar.gz", hash = "sha256:696a4a00a2a2a35d4e5deb3fc946641b96c944f02230e4f76137fe35d806c4fc", size = 4651414, upload-time = "2026-05-11T18:54:29.21Z" } sdist = { url = "https://files.pythonhosted.org/packages/f8/87/4341c6252d1c47b08768c3d25ac487362bf403f0313ddae4a2a26c9b1b4c/pandas-3.0.3.tar.gz", hash = "sha256:696a4a00a2a2a35d4e5deb3fc946641b96c944f02230e4f76137fe35d806c4fc", size = 4651414, upload-time = "2026-05-11T18:54:29.21Z" }
wheels = [ wheels = [
@@ -596,26 +601,34 @@ name = "pygui"
version = "0.1.0" version = "0.1.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "matplotlib" }, { name = "matplotlib", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "pandas" }, { name = "numpy", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "pyyaml" }, { name = "pandas", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "scipy" }, { name = "pyqtgraph", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "pyserial", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "pyside6", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "pyyaml", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "scipy", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
] ]
[package.optional-dependencies] [package.optional-dependencies]
dev = [ dev = [
{ name = "pytest" }, { name = "pytest", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
] ]
[package.dev-dependencies] [package.dev-dependencies]
dev = [ dev = [
{ name = "pytest" }, { name = "pytest", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
] ]
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "matplotlib", specifier = ">=3.7.0" }, { name = "matplotlib", specifier = ">=3.7.0" },
{ name = "numpy", specifier = ">=1.24.0" },
{ name = "pandas", specifier = ">=2.0.0" }, { name = "pandas", specifier = ">=2.0.0" },
{ name = "pyqtgraph", specifier = ">=0.13.0" },
{ name = "pyserial", specifier = ">=3.5" },
{ name = "pyside6", specifier = ">=6.6.0" },
{ name = "pytest", marker = "extra == 'dev'" }, { name = "pytest", marker = "extra == 'dev'" },
{ name = "pyyaml", specifier = ">=6.0.3" }, { name = "pyyaml", specifier = ">=6.0.3" },
{ name = "scipy", specifier = ">=1.17.1" }, { name = "scipy", specifier = ">=1.17.1" },
@@ -634,16 +647,85 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" },
] ]
[[package]]
name = "pyqtgraph"
version = "0.14.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "numpy", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/32/36/4c242f81fdcbfa4fb62a5645f6af79191f4097a0577bd5460c24f19cc4ef/pyqtgraph-0.14.0-py3-none-any.whl", hash = "sha256:7abb7c3e17362add64f8711b474dffac5e7b0e9245abdf992e9a44119b7aa4f5", size = 1924755, upload-time = "2025-11-16T19:43:22.251Z" },
]
[[package]]
name = "pyserial"
version = "3.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1e/7d/ae3f0a63f41e4d2f6cb66a5b57197850f919f59e558159a4dd3a818f5082/pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb", size = 159125, upload-time = "2020-11-23T03:59:15.045Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/07/bc/587a445451b253b285629263eb51c2d8e9bcea4fc97826266d186f96f558/pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0", size = 90585, upload-time = "2020-11-23T03:59:13.41Z" },
]
[[package]]
name = "pyside6"
version = "6.11.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyside6-addons", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "pyside6-essentials", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "shiboken6", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/da/a6/27ba5947ed48918f7b74b7c43a1e280aac069e36f25adeb4c9adfac835c4/pyside6-6.11.1-cp310-abi3-macosx_13_0_universal2.whl", hash = "sha256:537682c3b7530817203e667c1f5a2f00486b37bf52c52eeab438544c7a0917f6", size = 571921, upload-time = "2026-05-13T09:47:36.402Z" },
{ url = "https://files.pythonhosted.org/packages/d8/de/af89d71410c83b10654d86ff9aff2a4f87c30163658f1cc145242e222526/pyside6-6.11.1-cp310-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b1fc521ba2bb5109425ab8add06bddbdd524abcad06cfa012cc39a22a189feb2", size = 572102, upload-time = "2026-05-13T09:47:38.249Z" },
{ url = "https://files.pythonhosted.org/packages/b6/0e/d583bd3f7bf5046a4497b36f3902cfb64aa29554489a5a25c18e6b4ac0ac/pyside6-6.11.1-cp310-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:75f0005c3eb95c07cfb65522ec50d0815ac007a96482c21dc3cb4b4c04895d84", size = 572098, upload-time = "2026-05-13T09:47:39.44Z" },
{ url = "https://files.pythonhosted.org/packages/57/f2/d9d8ce1373dabb37e5919f63cd18446556079631d3f2eea3ada03c29f6b8/pyside6-6.11.1-cp310-abi3-win_amd64.whl", hash = "sha256:0968877ab1fb4ef3587a284da6fe05e8647ada56a6a3750b6395188e01f4aba6", size = 578377, upload-time = "2026-05-13T09:47:40.76Z" },
{ url = "https://files.pythonhosted.org/packages/96/02/a6057d8bd2bdb1940820fff2d627fdf4013148c9c57adf69fa40d3452ac3/pyside6-6.11.1-cp310-abi3-win_arm64.whl", hash = "sha256:acee467cb5f256cc47ebb9d815a054c1d8416da380c191b247a76d164aa3f805", size = 561765, upload-time = "2026-05-13T09:47:41.9Z" },
]
[[package]]
name = "pyside6-addons"
version = "6.11.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyside6-essentials", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "shiboken6", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/3f/6b/8bc94aff48b63f788f2d84e5467c12362d68906ba742c0942f46cb04c879/pyside6_addons-6.11.1-cp310-abi3-macosx_13_0_universal2.whl", hash = "sha256:54733c77f789bef5f03c6aff4ad3bec8b2eff021f0cfcbc53d5e6c250ded24f9", size = 331714589, upload-time = "2026-05-13T09:39:12.36Z" },
{ url = "https://files.pythonhosted.org/packages/dd/62/fb1428a523b2a4541e232aab50d9e789e6b4526f37fd9593452a7ea5b6b3/pyside6_addons-6.11.1-cp310-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:8e6c65fbd73a512d6f72cda8d8277444a85a34dc99dd1dae9c21d35b8671bb1f", size = 175063224, upload-time = "2026-05-13T09:39:34.185Z" },
{ url = "https://files.pythonhosted.org/packages/ee/9b/2ccd52f66db55c06de65d0501170a1935d04d64d0a230c0d892284a02ce3/pyside6_addons-6.11.1-cp310-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:bf1c6c4e954e5eba3d2a7c661ad4b9689e8f09c7f4a16bdf29713371d11af993", size = 170553429, upload-time = "2026-05-13T09:39:54.424Z" },
{ url = "https://files.pythonhosted.org/packages/9a/bd/8adc4d350b3b363f3dfc8fccdcf5bfed25f7e36c2fff30c64e106f4f1572/pyside6_addons-6.11.1-cp310-abi3-win_amd64.whl", hash = "sha256:0d13c4dfd671b050a48e4f8d8ddc724b7248f9c0437e7fc47fdf316278572923", size = 168816308, upload-time = "2026-05-13T09:40:13.541Z" },
{ url = "https://files.pythonhosted.org/packages/65/b7/9a840d97f0f0f04e372a87e205dd30ee285b4e3b021b188459a917c9dc76/pyside6_addons-6.11.1-cp310-abi3-win_arm64.whl", hash = "sha256:3494f480dee92f415be2f2d989c0b3f4755ac332b28045cbf4ba0f5c5a22ba37", size = 35759347, upload-time = "2026-05-13T09:40:21.199Z" },
]
[[package]]
name = "pyside6-essentials"
version = "6.11.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "shiboken6", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/da/10d9197e7370eb4fed8df5fc547b7548dec88e5c5949e2d450db4ae96feb/pyside6_essentials-6.11.1-cp310-abi3-macosx_13_0_universal2.whl", hash = "sha256:228de53c2bc26b07e5021fbe3614fc44ca08e4dab9999af08c2b389d2c239957", size = 110352945, upload-time = "2026-05-13T09:43:08.006Z" },
{ url = "https://files.pythonhosted.org/packages/5c/49/0e1237c4400bec7e335d2c4eeb49bc40d9fd88a9ac44ca9083ce1abdc308/pyside6_essentials-6.11.1-cp310-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:e3ef7027b41e4e55fadb56e3b3257dc8ee92154b639fe67fc4c8e05e9d976c60", size = 79908535, upload-time = "2026-05-13T09:43:24.836Z" },
{ url = "https://files.pythonhosted.org/packages/4c/c5/da4c5f23c6540ac5211a1f60177c8dee84b1bf40f2719479587ab8c60731/pyside6_essentials-6.11.1-cp310-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:a039b6da68a3a4b9d243217b2b98d475eed3f617159ef6be925badab53c11b0d", size = 78960051, upload-time = "2026-05-13T09:43:35.423Z" },
{ url = "https://files.pythonhosted.org/packages/64/0e/b663ecc96ca57b5c91b83b6615d6b174380b0faf30338125c26e053d6aa7/pyside6_essentials-6.11.1-cp310-abi3-win_amd64.whl", hash = "sha256:63311bd48e32c584599ab04b9ef7c324082374cd2c9fa533f978fb893bb47e40", size = 77549267, upload-time = "2026-05-13T09:43:44.92Z" },
{ url = "https://files.pythonhosted.org/packages/f1/12/eb6723faf5cb7fa581145da1c15f40d641b96e080f0491af2f1859fdeedb/pyside6_essentials-6.11.1-cp310-abi3-win_arm64.whl", hash = "sha256:11253ea52aabecefe9febddbbe78b43a824129e3af1cec98431028fba7fa954f", size = 57964512, upload-time = "2026-05-13T09:43:52.968Z" },
]
[[package]] [[package]]
name = "pytest" name = "pytest"
version = "9.0.3" version = "9.0.3"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" }, { name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "iniconfig" }, { name = "iniconfig", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "packaging" }, { name = "packaging", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "pluggy" }, { name = "pluggy", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "pygments" }, { name = "pygments", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
wheels = [ wheels = [
@@ -655,7 +737,7 @@ name = "python-dateutil"
version = "2.9.0.post0" version = "2.9.0.post0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "six" }, { name = "six", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
wheels = [ wheels = [
@@ -722,7 +804,7 @@ name = "scipy"
version = "1.17.1" version = "1.17.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "numpy" }, { name = "numpy", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" }
wheels = [ wheels = [
@@ -788,6 +870,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" }, { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" },
] ]
[[package]]
name = "shiboken6"
version = "6.11.1"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/17/f3/f2b63df0251e7cd3172ea28e32ede52739de9566bcefcd0178681538ac81/shiboken6-6.11.1-cp310-abi3-macosx_13_0_universal2.whl", hash = "sha256:1a16867f103ef1c662a5f09dfed03273a9f81688b174555162c58e83650a3f02", size = 476874, upload-time = "2026-05-13T09:47:01.091Z" },
{ url = "https://files.pythonhosted.org/packages/c7/9b/e0355d8897b5c150770f1d95718aad17d432fcc9c035c04f3f58427d4693/shiboken6-6.11.1-cp310-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9a8bccfafc8805254cabcfa1edfaf55cd52889f4998c91ad0d9a4433fb1bcdbe", size = 272222, upload-time = "2026-05-13T09:47:02.653Z" },
{ url = "https://files.pythonhosted.org/packages/57/d5/dd4f1defed400be03340f2ede34b61f846776650b4e7ed9ebaf4c71979a2/shiboken6-6.11.1-cp310-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:1bd2f4314414df2d122d9f646e03b731bc6d6b5f77a5f53f99a4fe4e97d84e6f", size = 270350, upload-time = "2026-05-13T09:47:04.02Z" },
{ url = "https://files.pythonhosted.org/packages/52/b5/3f6fb2ee65b534193fb4ef713dd619dc31dadff5d12c16979a7699ad58be/shiboken6-6.11.1-cp310-abi3-win_amd64.whl", hash = "sha256:c2c6863aa80ec18c0f82cea3417837b279cdc60024ac17123461dc9042577df7", size = 1223647, upload-time = "2026-05-13T09:47:05.924Z" },
{ url = "https://files.pythonhosted.org/packages/98/d1/f15ca0e1666faae02c945f48e745ea35f8fcd8243b176109b4e2c4251f47/shiboken6-6.11.1-cp310-abi3-win_arm64.whl", hash = "sha256:7c8d9af17db4495d4fa5b1c393f218311c4855546b9dfa6a0bd21bcd66b55e9d", size = 1784170, upload-time = "2026-05-13T09:47:07.617Z" },
]
[[package]] [[package]]
name = "six" name = "six"
version = "1.17.0" version = "1.17.0"