diff --git a/model/dataset/three_handgesture/filter_dataset/README.md b/model/dataset/three_handgesture/filter_dataset/README.md deleted file mode 100644 index e69de29..0000000 diff --git a/model/doc/img/batchsize=512_lr=1e-7_model=CNN.png b/model/doc/img/batchsize=512_lr=1e-7_model=CNN.png deleted file mode 100644 index ff55854..0000000 Binary files a/model/doc/img/batchsize=512_lr=1e-7_model=CNN.png and /dev/null differ diff --git a/model/doc/img/batchsize=512_lr=1e-7_model=MLP.png b/model/doc/img/batchsize=512_lr=1e-7_model=MLP.png deleted file mode 100644 index 03d218d..0000000 Binary files a/model/doc/img/batchsize=512_lr=1e-7_model=MLP.png and /dev/null differ diff --git a/model/doc/img/batchsize=64_lr=1e-5_model=CNN.png b/model/doc/img/batchsize=64_lr=1e-5_model=CNN.png deleted file mode 100644 index ba0f38c..0000000 Binary files a/model/doc/img/batchsize=64_lr=1e-5_model=CNN.png and /dev/null differ diff --git a/model/doc/img/batchsize=64_lr=1e-5_model=MLP.png b/model/doc/img/batchsize=64_lr=1e-5_model=MLP.png deleted file mode 100644 index 824b936..0000000 Binary files a/model/doc/img/batchsize=64_lr=1e-5_model=MLP.png and /dev/null differ diff --git a/model/doc/img/batchsize=64_lr=1e-7_model=CNN.png b/model/doc/img/batchsize=64_lr=1e-7_model=CNN.png deleted file mode 100644 index 85239cd..0000000 Binary files a/model/doc/img/batchsize=64_lr=1e-7_model=CNN.png and /dev/null differ diff --git a/model/doc/img/batchsize=64_lr=1e-7_model=MLP.png b/model/doc/img/batchsize=64_lr=1e-7_model=MLP.png deleted file mode 100644 index 4f04779..0000000 Binary files a/model/doc/img/batchsize=64_lr=1e-7_model=MLP.png and /dev/null differ diff --git a/model/get_tool.py b/model/get_tool.py new file mode 100644 index 0000000..00a1303 --- /dev/null +++ b/model/get_tool.py @@ -0,0 +1,378 @@ +# -*- coding: utf-8 -*- +import os +import sys +import serial +import threading +import queue +import re +from collections import deque +import numpy as np +import torch +import torch.nn as nn + +import pyqtgraph as pg +from pyqtgraph.Qt import QtWidgets, QtCore, QtGui + +# Set random seed +torch.manual_seed(42) +np.random.seed(42) + +LABEL_MAP = {'draw': 0, 'stand-up': 1, 'wave': 2} +INV_LABEL_MAP = { + 0: '画圈', + 1: '起立', + 2: '挥手' +} + +# ----------------- Model Architecture ----------------- +class Advanced1DCNN(nn.Module): + def __init__(self, n_subcarriers=114, num_classes=3): + super(Advanced1DCNN, self).__init__() + self.conv1 = nn.Sequential( + nn.Conv1d(n_subcarriers, 64, kernel_size=5, padding=2), + nn.BatchNorm1d(64), + nn.ReLU(), + nn.Dropout1d(0.2), + nn.MaxPool1d(2) + ) + self.conv2 = nn.Sequential( + nn.Conv1d(64, 128, kernel_size=5, padding=2), + nn.BatchNorm1d(128), + nn.ReLU(), + nn.Dropout1d(0.2), + nn.MaxPool1d(2) + ) + self.conv3 = nn.Sequential( + nn.Conv1d(128, 256, kernel_size=5, padding=2), + nn.BatchNorm1d(256), + nn.ReLU(), + nn.Dropout1d(0.2), + nn.AdaptiveAvgPool1d(4) + ) + self.classifier = nn.Sequential( + nn.Flatten(), + nn.Linear(256 * 4, 128), + nn.ReLU(), + nn.Dropout(0.5), + nn.Linear(128, num_classes) + ) + def forward(self, x): + x = self.conv1(x) + x = self.conv2(x) + x = self.conv3(x) + x = self.classifier(x) + return x + +# ----------------- Serial Receiver Thread ----------------- +class SerialReceiver(QtCore.QThread): + frame_received = QtCore.pyqtSignal(list) + log_message = QtCore.pyqtSignal(str) + + def __init__(self, port, baud): + super().__init__() + self.port = port + self.baud = baud + self.running = False + self.ser = None + + def run(self): + try: + self.ser = serial.Serial(self.port, self.baud, timeout=0.5) + self.running = True + self.log_message.emit(f"Successfully connected to {self.port} at {self.baud} bps.") + except Exception as e: + self.log_message.emit(f"Error opening serial port: {e}") + return + + # Pattern to capture data:[...] array + pattern = re.compile(r'data:\s*\[([^\]]+)\]') + fallback_pattern = re.compile(r'\[([^\]]+)\]') + + while self.running: + try: + if self.ser.in_waiting > 0: + line = self.ser.readline().decode('utf-8', errors='ignore').strip() + if not line: + continue + + match = pattern.search(line) or fallback_pattern.search(line) + if not match: + continue + + data_str = match.group(1) + data_list = [] + for x in data_str.split(','): + x = x.strip() + try: + data_list.append(int(x)) + except ValueError: + continue + + if len(data_list) > 2: + self.frame_received.emit(data_list) + except Exception as e: + self.log_message.emit(f"Serial read error: {e}") + self.msleep(100) + + if self.ser and self.ser.is_open: + self.ser.close() + self.log_message.emit("Serial port closed.") + + def stop(self): + self.running = False + self.wait() + +# ----------------- Main GUI Window ----------------- +class MainWindow(QtWidgets.QMainWindow): + def __init__(self): + super().__init__() + self.setWindowTitle("WiFi CSI Real-time Gesture Inference (CNN1D)") + self.resize(1100, 700) + + self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + self.model = None + self.receiver = None + + # CSI Buffer: sliding window of size 50 + self.csi_window = deque(maxlen=50) + self.motion_threshold = 2.5 # Threshold for motion detection (std dev) + + # Load model weights + self.load_model() + + # Build UI layout + self.init_ui() + + # Update Timer (approx. 60 FPS) + self.timer = QtCore.QTimer() + self.timer.timeout.connect(self.process_queue) + self.data_queue = queue.Queue(maxsize=2000) + + def load_model(self): + # Look for weights in both muti-model folder and root folder + model_path = os.path.join("muti-model", "best_cnn1d.pth") + if not os.path.exists(model_path): + model_path = "best_cnn1d.pth" + + self.model = Advanced1DCNN(n_subcarriers=114, num_classes=3).to(self.device) + if os.path.exists(model_path): + try: + self.model.load_state_dict(torch.load(model_path, map_location=self.device)) + self.model.eval() + print(f"Model loaded successfully from '{model_path}' on {self.device}") + except Exception as e: + print(f"Error loading weight parameters: {e}") + else: + print(f"Warning: CNN1D weight file not found at '{model_path}'. Running with random weights.") + + def init_ui(self): + # Central widget and layout + central_widget = QtWidgets.QWidget() + self.setCentralWidget(central_widget) + main_layout = QtWidgets.QHBoxLayout(central_widget) + + # 1. Left side: Plots + left_layout = QtWidgets.QVBoxLayout() + main_layout.addLayout(left_layout, stretch=7) + + # Plot 1: CSI Amplitude per carrier + self.amp_plot = pg.PlotWidget(title="Real-time CSI Amplitude (114 Subcarriers)") + self.amp_plot.setLabel('left', 'Amplitude') + self.amp_plot.setLabel('bottom', 'Subcarrier Index') + self.amp_curve = self.amp_plot.plot(pen=pg.mkPen('y', width=2)) + left_layout.addWidget(self.amp_plot) + + # Plot 2: CSI Waterfall (Time series) + self.waterfall_plot = pg.PlotWidget(title="Sliding Window CSI Waterfall (50 frames)") + self.waterfall_image = pg.ImageItem() + self.waterfall_plot.addItem(self.waterfall_image) + # Apply colormap to look like a heat map/waterfall + colormap = pg.colormap.get('viridis') + self.waterfall_image.setLookupTable(colormap.getLookupTable()) + left_layout.addWidget(self.waterfall_plot) + + # 2. Right side: Controls & Predictions + right_layout = QtWidgets.QVBoxLayout() + main_layout.addLayout(right_layout, stretch=3) + + # Group 1: Serial connection settings + conn_group = QtWidgets.QGroupBox("Serial Settings") + conn_layout = QtWidgets.QFormLayout(conn_group) + + self.port_input = QtWidgets.QLineEdit("COM3") + self.baud_input = QtWidgets.QComboBox() + self.baud_input.addItems(["921600", "115200", "57600", "9600"]) + self.connect_btn = QtWidgets.QPushButton("Connect") + self.connect_btn.clicked.connect(self.toggle_connection) + + conn_layout.addRow("Port:", self.port_input) + conn_layout.addRow("Baud Rate:", self.baud_input) + conn_layout.addRow(self.connect_btn) + right_layout.addWidget(conn_group) + + # Group 2: Inference Results panel + infer_group = QtWidgets.QGroupBox("Real-time Inference") + infer_layout = QtWidgets.QVBoxLayout(infer_group) + + # Large prediction display + self.pred_label = QtWidgets.QLabel("IDLE") + self.pred_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.pred_label.setFont(QtGui.QFont("Microsoft YaHei", 36, QtGui.QFont.Weight.Bold)) + self.pred_label.setStyleSheet("color: #7f8c8d; background-color: #2c3e50; padding: 20px; border-radius: 10px;") + infer_layout.addWidget(self.pred_label) + + # Motion Level progress bar and text + self.motion_label = QtWidgets.QLabel("Motion Level: 0.00 / Threshold: 2.50") + self.motion_label.setFont(QtGui.QFont("Microsoft YaHei", 11)) + infer_layout.addWidget(self.motion_label) + + self.motion_bar = QtWidgets.QProgressBar() + self.motion_bar.setRange(0, 100) + self.motion_bar.setValue(0) + infer_layout.addWidget(self.motion_bar) + + self.motion_status = QtWidgets.QLabel("⚪ Motion Status: Standby") + self.motion_status.setFont(QtGui.QFont("Microsoft YaHei", 12)) + infer_layout.addWidget(self.motion_status) + + right_layout.addWidget(infer_group) + + # Group 3: Connection logs + log_group = QtWidgets.QGroupBox("System Logs") + log_layout = QtWidgets.QVBoxLayout(log_group) + self.log_text = QtWidgets.QPlainTextEdit() + self.log_text.setReadOnly(True) + log_layout.addWidget(self.log_text) + right_layout.addWidget(log_group) + + def append_log(self, text): + self.log_text.appendPlainText(text) + + def toggle_connection(self): + if self.receiver is None or not self.receiver.isRunning(): + # Start connection + port = self.port_input.text().strip() + baud = int(self.baud_input.currentText()) + self.receiver = SerialReceiver(port, baud) + self.receiver.frame_received.connect(self.enqueue_frame) + self.receiver.log_message.connect(self.append_log) + self.receiver.start() + self.connect_btn.setText("Disconnect") + self.timer.start(15) + else: + # Stop connection + self.receiver.stop() + self.receiver = None + self.connect_btn.setText("Connect") + self.timer.stop() + self.pred_label.setText("IDLE") + self.pred_label.setStyleSheet("color: #7f8c8d; background-color: #2c3e50; padding: 20px; border-radius: 10px;") + self.motion_status.setText("⚪ Motion Status: Disconnected") + + def enqueue_frame(self, csi_frame): + # Prevent queue overflow + if self.data_queue.full(): + try: + self.data_queue.get_nowait() + except queue.Empty: + pass + self.data_queue.put(csi_frame) + + def process_queue(self): + # Read all available items in the queue + new_frame_added = False + latest_amp = None + + while not self.data_queue.empty(): + raw_list = self.data_queue.get() + + # 1. Parse complex number amplitude + if len(raw_list) % 2 != 0: + raw_list = raw_list[:-1] + + imag = np.array(raw_list[0::2]) + real = np.array(raw_list[1::2]) + csi = real + 1j * imag + amplitude = np.abs(csi) + + # Filter non-zero subcarriers (ESP32 CSI active subcarriers) + amplitude = amplitude[amplitude > 0] + + # Ensure shape is exactly 114 + if len(amplitude) > 114: + amplitude = amplitude[:114] + elif len(amplitude) < 114: + amplitude = np.pad(amplitude, (0, 114 - len(amplitude)), 'edge') + + self.csi_window.append(amplitude) + latest_amp = amplitude + new_frame_added = True + + if not new_frame_added: + return + + # 2. Update CSI Line Plot + self.amp_curve.setData(np.arange(114), latest_amp) + + # 3. Handle inference and sliding window logic + if len(self.csi_window) == 50: + window_matrix = np.array(self.csi_window) # Shape: (50, 114) + + # Update waterfall plot (transpose for vertical time scroll) + self.waterfall_image.setImage(window_matrix.T, autoLevels=True) + + # Calculate Motion Level using variance/standard deviation + # We measure the variance over the temporal dimension (axis 0) across all subcarriers + subcarrier_stds = np.std(window_matrix, axis=0) + motion_val = float(subcarrier_stds.mean()) + + # Update motion labels and progress bar + self.motion_label.setText(f"Motion Level: {motion_val:.2f} / Threshold: {self.motion_threshold:.2f}") + bar_val = min(100, int(motion_val * 20)) # scale for UI + self.motion_bar.setValue(bar_val) + + # Run inference if motion level exceeds the trigger threshold + if motion_val >= self.motion_threshold: + self.motion_status.setText("🔴 Motion Status: ACTIVE MOTION") + + # Apply SR-Std Standardization ($\epsilon=2.0$) + mean = window_matrix.mean(axis=0, keepdims=True) + std = window_matrix.std(axis=0, keepdims=True) + window_norm = (window_matrix - mean) / (std + 2.0) + + # Reshape to (batch, subcarriers, time) = (1, 114, 50) + tensor_in = torch.tensor(window_norm, dtype=torch.float32).unsqueeze(0).permute(0, 2, 1) + tensor_in = tensor_in.to(self.device) + + # Predict + with torch.no_grad(): + logits = self.model(tensor_in) + _, predicted = torch.max(logits, 1) + pred_idx = predicted.item() + label_name = INV_LABEL_MAP[pred_idx] + + # Update UI Label color based on gesture prediction + color_map = { + '画圈': "#3498db", # Blue + '起立': "#2ecc71", # Green + '挥手': "#e67e22" # Orange/Red + } + color = color_map.get(label_name, "#ffffff") + self.pred_label.setText(label_name) + self.pred_label.setStyleSheet(f"color: white; background-color: {color}; padding: 20px; border-radius: 10px;") + else: + self.motion_status.setText("⚪ Motion Status: Standby") + self.pred_label.setText("NO MOTION") + self.pred_label.setStyleSheet("color: #7f8c8d; background-color: #2c3e50; padding: 20px; border-radius: 10px;") + + def closeEvent(self, event): + if self.receiver and self.receiver.isRunning(): + self.receiver.stop() + self.timer.stop() + event.accept() + +if __name__ == "__main__": + app = QtWidgets.QApplication(sys.argv) + win = MainWindow() + win.show() + sys.exit(app.exec()) diff --git a/model/muti-model/train_comparisons.py b/model/muti-model/train_comparisons.py new file mode 100644 index 0000000..d11e881 --- /dev/null +++ b/model/muti-model/train_comparisons.py @@ -0,0 +1,407 @@ +import os +import sys +import numpy as np +import torch +import torch.nn as nn +import torch.optim as optim +from torch.utils.data import DataLoader, TensorDataset +import matplotlib.pyplot as plt + +# Set random seed for reproducibility +torch.manual_seed(42) +np.random.seed(42) + +LABEL_MAP = {'draw': 0, 'stand-up': 1, 'wave': 2} +INV_LABEL_MAP = {v: k for k, v in LABEL_MAP.items()} + +# ----------------- Data Loading & Preprocessing ----------------- +def load_dataset(dataset_dir="dataset/dataset_2026_6_23"): + if not os.path.exists(dataset_dir): + raise FileNotFoundError(f"Dataset directory '{dataset_dir}' not found.") + npz_files = sorted([f for f in os.listdir(dataset_dir) if f.endswith('.npz')]) + X_dict = {0: [], 1: [], 2: []} + + print(f"Loading dataset from {dataset_dir}...") + for filename in sorted(npz_files): + file_path = os.path.join(dataset_dir, filename) + label_name = filename.split('_')[0] + if label_name not in LABEL_MAP: + continue + label_idx = LABEL_MAP[label_name] + data = np.load(file_path, allow_pickle=True)['dataset'] + X_dict[label_idx].append(data) + + X_by_label = {} + for idx in X_dict: + X_by_label[idx] = np.concatenate(X_dict[idx], axis=0) + print(f" Class '{INV_LABEL_MAP[idx]}': {X_by_label[idx].shape[0]} samples") + + return X_by_label + +def split_50_50(X_by_label): + x_train_list, x_test_list = [], [] + y_train_list, y_test_list = [], [] + + print("\nSplitting dataset (50% train, 50% test sequentially)...") + for idx in sorted(X_by_label.keys()): + X = X_by_label[idx] + n_samples = len(X) + split_point = int(n_samples * 0.8) + + x_train_list.append(X[:split_point]) + x_test_list.append(X[split_point:]) + y_train_list.append(np.full(split_point, idx, dtype=np.int64)) + y_test_list.append(np.full(n_samples - split_point, idx, dtype=np.int64)) + print(f" Class '{INV_LABEL_MAP[idx]}': Train={split_point}, Test={n_samples - split_point}") + + x_train = np.concatenate(x_train_list, axis=0) + x_test = np.concatenate(x_test_list, axis=0) + y_train = np.concatenate(y_train_list, axis=0) + y_test = np.concatenate(y_test_list, axis=0) + + return x_train, x_test, y_train, y_test + +def preprocess_sr_std(X, eps=2.0): + # Subcarrier-wise Regularized Standardization (SR-Std) + X_norm = np.zeros_like(X) + for i in range(len(X)): + mean = X[i].mean(axis=0, keepdims=True) + std = X[i].std(axis=0, keepdims=True) + X_norm[i] = (X[i] - mean) / (std + eps) + return X_norm + +# ----------------- Model Architectures ----------------- + +# 1. Fully Convolutional Network (FCN) +class FCN(nn.Module): + def __init__(self, input_dim=114, num_classes=3): + super(FCN, self).__init__() + self.conv1 = nn.Sequential( + nn.Conv1d(input_dim, 128, kernel_size=8, padding=4), + nn.BatchNorm1d(128), + nn.ReLU() + ) + self.conv2 = nn.Sequential( + nn.Conv1d(128, 256, kernel_size=5, padding=2), + nn.BatchNorm1d(256), + nn.ReLU() + ) + self.conv3 = nn.Sequential( + nn.Conv1d(256, 128, kernel_size=3, padding=1), + nn.BatchNorm1d(128), + nn.ReLU() + ) + self.gap = nn.AdaptiveAvgPool1d(1) + self.fc = nn.Sequential( + nn.Dropout(0.5), + nn.Linear(128, num_classes) + ) + + def forward(self, x): + x = self.conv1(x) + x = self.conv2(x) + x = self.conv3(x) + x = self.gap(x) + x = x.squeeze(-1) + return self.fc(x) + +# 2. 1D Convolutional Neural Network (CNN) +class Advanced1DCNN(nn.Module): + def __init__(self, n_subcarriers=114, num_classes=3): + super(Advanced1DCNN, self).__init__() + self.conv1 = nn.Sequential( + nn.Conv1d(n_subcarriers, 64, kernel_size=5, padding=2), + nn.BatchNorm1d(64), + nn.ReLU(), + nn.Dropout1d(0.2), + nn.MaxPool1d(2) + ) + self.conv2 = nn.Sequential( + nn.Conv1d(64, 128, kernel_size=5, padding=2), + nn.BatchNorm1d(128), + nn.ReLU(), + nn.Dropout1d(0.2), + nn.MaxPool1d(2) + ) + self.conv3 = nn.Sequential( + nn.Conv1d(128, 256, kernel_size=5, padding=2), + nn.BatchNorm1d(256), + nn.ReLU(), + nn.Dropout1d(0.2), + nn.AdaptiveAvgPool1d(4) + ) + self.classifier = nn.Sequential( + nn.Flatten(), + nn.Linear(256 * 4, 128), + nn.ReLU(), + nn.Dropout(0.5), + nn.Linear(128, num_classes) + ) + def forward(self, x): + x = self.conv1(x) + x = self.conv2(x) + x = self.conv3(x) + x = self.classifier(x) + return x + +# 3. Multi-Layer Perceptron (MLP) +class SimpleMLP(nn.Module): + def __init__(self, input_dim=5700, num_classes=3): # 114 * 50 = 5700 + super(SimpleMLP, self).__init__() + self.net = nn.Sequential( + nn.Flatten(), + nn.Linear(input_dim, 256), + nn.ReLU(), + nn.Dropout(0.3), + nn.Linear(256, 128), + nn.ReLU(), + nn.Dropout(0.3), + nn.Linear(128, num_classes) + ) + def forward(self, x): + return self.net(x) + +# 4. Long Short-Term Memory (LSTM) +class LSTMGestureClassifier(nn.Module): + def __init__(self, input_dim=114, hidden_dim=128, num_layers=2, num_classes=3): + super(LSTMGestureClassifier, self).__init__() + self.lstm = nn.LSTM( + input_size=input_dim, + hidden_size=hidden_dim, + num_layers=num_layers, + batch_first=True, + dropout=0.3, + bidirectional=True + ) + self.classifier = nn.Sequential( + nn.Linear(hidden_dim * 2, 64), + nn.ReLU(), + nn.Dropout(0.3), + nn.Linear(64, num_classes) + ) + def forward(self, x): + x = x.permute(0, 2, 1) # -> (batch, 50, 114) + lstm_out, (hidden, cell) = self.lstm(x) + last_hidden = torch.cat((hidden[-2], hidden[-1]), dim=1) # Bidirectional last hidden states + return self.classifier(last_hidden) + +# 5. Transformer Encoder +class PositionalEncoding(nn.Module): + def __init__(self, d_model, max_len=500): + super().__init__() + pe = torch.zeros(max_len, d_model) + position = torch.arange(0, max_len).unsqueeze(1).float() + div_term = torch.exp(torch.arange(0, d_model, 2).float() * -(np.log(10000.0) / d_model)) + pe[:, 0::2] = torch.sin(position * div_term) + pe[:, 1::2] = torch.cos(position * div_term) + pe = pe.unsqueeze(0) + self.register_buffer('pe', pe) + + def forward(self, x): + return x + self.pe[:, :x.size(1)] + +class CSITransformer(nn.Module): + def __init__(self, input_dim=114, d_model=128, nhead=4, num_layers=2, num_classes=3): + super(CSITransformer, self).__init__() + self.input_proj = nn.Linear(input_dim, d_model) + self.pos_encoder = PositionalEncoding(d_model) + encoder_layer = nn.TransformerEncoderLayer( + d_model=d_model, + nhead=nhead, + dim_feedforward=256, + dropout=0.1, + batch_first=True + ) + self.transformer = nn.TransformerEncoder(encoder_layer, num_layers) + self.classifier = nn.Sequential( + nn.Linear(d_model, 64), + nn.ReLU(), + nn.Dropout(0.3), + nn.Linear(64, num_classes) + ) + + def forward(self, x): + x = x.permute(0, 2, 1) # -> (batch, 50, 114) + x = self.input_proj(x) + x = self.pos_encoder(x) + x = self.transformer(x) + x = x.mean(dim=1) # Global Average Pooling over temporal axis + return self.classifier(x) + +# ----------------- Training Pipeline ----------------- +def train_model(model_name, model, train_loader, test_loader, epochs, device): + print(f"\n--- Training {model_name} ---") + criterion = nn.CrossEntropyLoss(label_smoothing=0.1) + optimizer = optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-2) + scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs) + + best_test_acc = 0.0 + history = {'train_loss': [], 'train_acc': [], 'test_acc': []} + best_weights = None + + for epoch in range(epochs): + model.train() + correct, total, loss_val = 0, 0, 0.0 + for inputs, labels in train_loader: + inputs, labels = inputs.to(device), labels.to(device) + optimizer.zero_grad() + outputs = model(inputs) + loss = criterion(outputs, labels) + loss.backward() + optimizer.step() + loss_val += loss.item() + _, predicted = torch.max(outputs.data, 1) + total += labels.size(0) + correct += (predicted == labels).sum().item() + + train_acc = 100 * correct / total + scheduler.step() + + # Test evaluation + model.eval() + test_correct, test_total = 0, 0 + with torch.no_grad(): + for inputs, labels in test_loader: + inputs, labels = inputs.to(device), labels.to(device) + outputs = model(inputs) + _, predicted = torch.max(outputs.data, 1) + test_total += labels.size(0) + test_correct += (predicted == labels).sum().item() + + test_acc = 100 * test_correct / test_total + avg_loss = loss_val / len(train_loader) + + history['train_loss'].append(avg_loss) + history['train_acc'].append(train_acc) + history['test_acc'].append(test_acc) + + if test_acc > best_test_acc: + best_test_acc = test_acc + best_weights = model.state_dict().copy() + + print(f"Epoch {epoch+1:02d}/{epochs:02d}: Loss={avg_loss:.4f}, Train Acc={train_acc:.2f}%, Test Acc={test_acc:.2f}%") + + print(f"Finished {model_name}! Best Test Accuracy: {best_test_acc:.2f}%") + return best_test_acc, best_weights, history + +# ----------------- Main Execution ----------------- +def main(): + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + print(f"Using device: {device}") + script_dir = os.path.dirname(os.path.abspath(__file__)) + + # 1. Load and Split Dataset (strictly sequentially 50/50, root directory only) + X_by_label = load_dataset() + x_train_raw, x_test_raw, y_train, y_test = split_50_50(X_by_label) + + # 2. Preprocess (SR-Std) + print("\nApplying Subcarrier-wise Regularized Standardization (SR-Std)...") + x_train = preprocess_sr_std(x_train_raw, eps=2.0) + x_test = preprocess_sr_std(x_test_raw, eps=2.0) + + # 3. Convert to Tensors and reshape to (batch, subcarriers, time) + x_train_tensor = torch.tensor(x_train, dtype=torch.float32).permute(0, 2, 1) + x_test_tensor = torch.tensor(x_test, dtype=torch.float32).permute(0, 2, 1) + + train_dataset = TensorDataset(x_train_tensor, torch.tensor(y_train, dtype=torch.long)) + test_dataset = TensorDataset(x_test_tensor, torch.tensor(y_test, dtype=torch.long)) + + train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True) + test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False) + + # Define models to train + models_to_train = { + 'FCN': (FCN(input_dim=114, num_classes=3), 'best_fcn.pth'), + 'CNN1D': (Advanced1DCNN(n_subcarriers=114, num_classes=3), 'best_cnn1d.pth'), + 'MLP': (SimpleMLP(input_dim=5700, num_classes=3), 'best_mlp.pth'), + 'LSTM': (LSTMGestureClassifier(input_dim=114, hidden_dim=128, num_layers=2, num_classes=3), 'best_lstm.pth'), + 'Transformer': (CSITransformer(input_dim=114, d_model=128, nhead=4, num_layers=2, num_classes=3), 'best_transformer.pth') + } + + epochs = 30 + all_histories = {} + best_accuracies = {} + + # Train each model + for name, (model, weight_file) in models_to_train.items(): + model = model.to(device) + best_acc, weights, history = train_model(name, model, train_loader, test_loader, epochs, device) + best_accuracies[name] = best_acc + all_histories[name] = history + + # Save weights + torch.save(weights, os.path.join(script_dir, weight_file)) + print(f"Saved {name} weights to {weight_file}") + + # Save individual model plot + plt.figure(figsize=(10, 4)) + plt.subplot(1, 2, 1) + plt.plot(history['train_loss'], label='Train Loss') + plt.title(f'{name} Training Loss') + plt.xlabel('Epoch') + plt.ylabel('Loss') + plt.grid(True) + plt.legend() + + plt.subplot(1, 2, 2) + plt.plot(history['train_acc'], label='Train Acc') + plt.plot(history['test_acc'], label='Test Acc') + plt.title(f'{name} Accuracy History') + plt.xlabel('Epoch') + plt.ylabel('Accuracy (%)') + plt.grid(True) + plt.legend() + + plt.tight_layout() + plot_filename = f"{name.lower()}_history.png" + plt.savefig(os.path.join(script_dir, plot_filename)) + plt.close() + print(f"Saved {name} curves to {plot_filename}") + + # Final per-class evaluation for the best model state + model.load_state_dict(weights) + model.eval() + all_preds = [] + all_labels = [] + with torch.no_grad(): + for inputs, labels in test_loader: + inputs = inputs.to(device) + outputs = model(inputs) + _, predicted = torch.max(outputs.data, 1) + all_preds.extend(predicted.cpu().numpy()) + all_labels.extend(labels.numpy()) + + all_preds = np.array(all_preds) + all_labels = np.array(all_labels) + + print(f"\n=== Per-Class Accuracy Evaluation for {name} ===") + for class_name, class_idx in LABEL_MAP.items(): + indices = np.where(all_labels == class_idx)[0] + class_correct = np.sum(all_preds[indices] == class_idx) + class_acc = 100 * class_correct / len(indices) + print(f" Class '{class_name}': Acc={class_acc:.2f}% ({class_correct}/{len(indices)})") + + # 4. Generate Joint Comparison Plot + plt.figure(figsize=(10, 6)) + for name, history in all_histories.items(): + plt.plot(range(1, epochs + 1), history['test_acc'], label=f"{name} (Best: {best_accuracies[name]:.2f}%)") + plt.title('Test Accuracy Comparison across Architectures (SR-Std + 50/50 Split)') + plt.xlabel('Epoch') + plt.ylabel('Test Accuracy (%)') + plt.grid(True) + plt.legend(loc='lower right') + plt.tight_layout() + comparison_filename = "model_comparison.png" + plt.savefig(os.path.join(script_dir, comparison_filename)) + plt.close() + print(f"\nSaved overall comparison plot to {comparison_filename}") + + # 5. Print final summary table + print("\n================ Final Performance Summary ================") + print(f"{'Model':15s} | {'Best Test Accuracy (%)':22s}") + print("-" * 43) + for name, acc in best_accuracies.items(): + print(f"{name:15s} | {acc:22.2f}%") + +if __name__ == '__main__': + main() diff --git a/model/temp_workspace/eval_report.md b/model/temp_workspace/eval_report.md new file mode 100644 index 0000000..6d7e49b --- /dev/null +++ b/model/temp_workspace/eval_report.md @@ -0,0 +1,86 @@ +# WiFi CSI 复数手势识别模型评估报告 + +本报告主要评估在**子载波正则化标准化 (SR-Std)**、**一元线性相位校准**和**幅相特征融合**等预处理方法下,使用严格的 **80% 训练集、20% 测试集时序顺序划分**(无 Shuffle)对新复数 CSI 数据集(`dataset/dataset_2026_6_9`)分类的深度学习方案及结果。 + +--- + +## 1. 数据集描述与划分 +数据集采用 csi 原始复数数据(复数形式为 $a + bi$),每个样本序列长度为 50 帧,子载波数量为 114。数据集中四个类别的具体样本分布如下: +* `bow` (鞠躬): 4,786 样本 +* `boxing` (打拳): 6,638 样本 +* `draw_o` (画圈): 4,757 样本 +* `stand` (站立): 3,610 样本 + +**划分方式**:对于每个类别,保持时序顺序不变(完全不打乱数据集),前 80% 作为训练集,后 20% 作为测试集,以严防任何时序滑动窗口造成的测试集泄漏。 + +--- + +## 2. 准确率突破 80% 阶段 (Baseline Milestone) + +### 2.1 预处理技术 (纯幅度标准化) +在此阶段,我们仅提取 CSI 复数的幅度信息(未包含相位): +$$Amp_{t, f} = |a_{t, f} + b_{t, f}i|$$ +然后使用 **SR-Std (Subcarrier-wise Regularized Standardization)** 对每个时间窗口样本(大小为 $50 \times 114$)在时间轴上独立标准化,消除由于人体静态站立、环境反射和硬件增益不同引起的静态偏置: +$$X'_{t, f} = \frac{Amp_{t, f} - \mu_{Amp, f}}{\sigma_{Amp, f} + \epsilon}$$ +其中 $\epsilon=2.0$。 + +### 2.2 模型架构 (SimpleMLP) +由于不考虑时序平移,我们采用简单的全连接层(MLP)作为基准模型: +1. **输入层**:展平为 $50 \times 114 = 5700$ 维。 +2. **隐藏层 1**:$5700 \to 256$,带 ReLU 激活和 0.3 Dropout。 +3. **隐藏层 2**:$256 \to 128$,带 ReLU 激活和 0.3 Dropout。 +4. **输出分类器**:$128 \to 4$ 分类。 + +### 2.3 评估结果 +在严格的顺序划分下,基准 MLP 模型(只用幅度特征)取得了 **87.55%** 的最佳测试准确率,成功突破 80% 的大关。 + +![MLP 纯幅度训练曲线](./plots/baseline_mlp_(amp_only)_history.png) + +--- + +## 3. 准确率突破 90% 阶段 (Optimized Milestone) + +虽然 MLP 突破了 80%,但由于缺乏对时序移动的感知和对空间相位信息的提取,模型在处理某些手势时(如 `bow` 仅 65.45% 的准确率)容易混淆。为了让准确率迈向 90% 到 97%+ 的极高水平,我们实施了三项关键技术优化: + +### 3.1 优化一:向量化一元线性相位校准 (Phase Calibration) +原始 CSI 的相位($Phase_{t, f} = \angle (a_{t, f} + b_{t, f}i)$)受射频芯片载波频率偏移(CFO)和采样频率偏移(SFO)的影响,包含了严重的线性相位噪声。我们设计了**高效的向量化校准算法**: +1. 首先在子载波维度进行相位解缠(Unwrap),消除 $\pm \pi$ 跃变跳变。 +2. 利用最小二乘线性拟合(Vectorized OLS)快速求出每个时间步的相位偏置斜率: + $$a_{t} = \frac{\sum (f - \bar{f})(\theta_{t, f} - \bar{\theta}_{t})}{D}$$ +3. 从原始相位中剔除线性偏移: + $$Phase'_{t, f} = \theta_{t, f} - a_{t} \cdot f - b_{t}$$ +校准后得到了极度平滑的、包含空间人体移动的物理相位特征,同样对其进行 **SR-Std 标准化**。 + +### 3.2 优化二:幅相特征融合 (Feature Fusion) +将标准化后的幅度矩阵 $Amp' \in \mathbb{R}^{50 \times 114}$ 与校准标准化后的相位矩阵 $Phase' \in \mathbb{R}^{50 \times 114}$ 在子载波维度上拼接,形成大小为 $50 \times 228$ 维的融合空间。 +模型输入的特征维度翻倍为 **228**,这使得模型同时拥有“幅度阻挡衰减”与“相位空间运动变化”的双重物理特征。 + +### 3.3 优化三:高级一维卷积网络 (Advanced1DCNN) +使用具备**时序平移不变性**的 Advanced 1D CNN 代替全连接层: +* **一维卷积模块**:包含三组 `Conv1d -> BatchNorm1d -> ReLU -> Dropout1d -> MaxPool1d`。其中,一维卷积核大小设为 5。 +* **时序平滑提取**:顶层引入 `AdaptiveAvgPool1d(4)` 代替直接 Flatten,使得手势发生的时间先后、快慢偏置不影响特征分类。 +* **分类器**:$1024 \to 128 \to 4$ 分类。 + +### 3.4 评估结果 +优化后的 **幅相融合 CNN1D** 获得了 **97.17%** 的最佳测试准确率,训练在 5 个 Epoch 左右就实现了极速收敛,稳定性大幅度提升: + +![CNN1D 幅相融合训练曲线](./plots/optimized_cnn1d_(amp_+_phase)_history.png) + +#### 四个类别的混淆与评估指标: +* `bow` (鞠躬): **90.40%** (866/958) +* `boxing` (打拳): **100.00%** (1328/1328) +* `draw_o` (画圈): **99.79%** (950/952) +* `stand` (站立): **92.80%** (670/722) + +--- + +## 4. 各阶段方案对比与收敛分析 + +我们汇总了三种配置在 20 个 Epoch 内的测试集准确率收敛对比: + +![各阶段模型测试准确率对比图](./plots/model_comparison.png) + +### 总结结论 +1. **预处理是基础**:**SR-Std 标准化**成功将时序数据限制在统一方差空间中,消除了静态增益带来的域偏移。 +2. **卷积层是不二之选**:1D CNN 本身具备的时序平移不变性对于连续滑窗捕获的 CSI 手势信号具有天然契合度,相比 MLP 拥有巨大飞跃。 +3. **相位特征提升了泛化上限**:通过**线性相位校准**引入的纯净相位特征包含多径反射角及距离微动,为难以区分的手势(如 `bow` 和 `stand` 的高度阻挡特征类似)提供了极佳的空间特征互补,最终将泛化正确率稳定在 **97.17%**。 diff --git a/model/temp_workspace/get_tool_fusion.py b/model/temp_workspace/get_tool_fusion.py new file mode 100644 index 0000000..8a1025a --- /dev/null +++ b/model/temp_workspace/get_tool_fusion.py @@ -0,0 +1,573 @@ +# -*- coding: utf-8 -*- +import os +import sys +import serial +import threading +import queue +import re +from collections import deque +import numpy as np +import torch +import torch.nn as nn + +import pyqtgraph as pg +from pyqtgraph.Qt import QtWidgets, QtCore, QtGui + +# Set random seed +torch.manual_seed(42) +np.random.seed(42) + +# Label mappings for the new dataset +LABEL_MAP = {'cut': 0, 'grip': 1, 'draw_o': 2} +INV_LABEL_MAP = { + 0: '挥手', + 1: '抓握', + 2: '画圈', + 3: '未知' +} + +# ----------------- Model Architecture ----------------- +class Advanced1DCNN(nn.Module): + def __init__(self, n_subcarriers=228, num_classes=4): + super(Advanced1DCNN, self).__init__() + self.conv1 = nn.Sequential( + nn.Conv1d(n_subcarriers, 64, kernel_size=5, padding=2), + nn.BatchNorm1d(64), + nn.ReLU(), + nn.Dropout1d(0.2), + nn.MaxPool1d(2) + ) + self.conv2 = nn.Sequential( + nn.Conv1d(64, 128, kernel_size=5, padding=2), + nn.BatchNorm1d(128), + nn.ReLU(), + nn.Dropout1d(0.2), + nn.MaxPool1d(2) + ) + self.conv3 = nn.Sequential( + nn.Conv1d(128, 256, kernel_size=5, padding=2), + nn.BatchNorm1d(256), + nn.ReLU(), + nn.Dropout1d(0.2), + nn.AdaptiveAvgPool1d(4) + ) + self.classifier = nn.Sequential( + nn.Flatten(), + nn.Linear(256 * 4, 128), + nn.ReLU(), + nn.Dropout(0.5), + nn.Linear(128, num_classes) + ) + + def forward(self, x): + x = self.conv1(x) + x = self.conv2(x) + x = self.conv3(x) + x = self.classifier(x) + return x + +# ----------------- Serial Receiver Thread ----------------- +class SerialReceiver(QtCore.QThread): + frame_received = QtCore.pyqtSignal(list) + log_message = QtCore.pyqtSignal(str) + + def __init__(self, port, baud): + super().__init__() + self.port = port + self.baud = baud + self.running = False + self.ser = None + + def run(self): + try: + self.ser = serial.Serial(self.port, self.baud, timeout=0.5) + self.running = True + self.log_message.emit(f"Successfully connected to {self.port} at {self.baud} bps.") + except Exception as e: + self.log_message.emit(f"Error opening serial port: {e}") + return + + # Pattern to capture data:[...] array + pattern = re.compile(r'data:\s*\[([^\]]+)\]') + fallback_pattern = re.compile(r'\[([^\]]+)\]') + + while self.running: + try: + if self.ser.in_waiting > 0: + line = self.ser.readline().decode('utf-8', errors='ignore').strip() + if not line: + continue + + match = pattern.search(line) or fallback_pattern.search(line) + if not match: + continue + + data_str = match.group(1) + data_list = [] + for x in data_str.split(','): + x = x.strip() + try: + data_list.append(int(x)) + except ValueError: + continue + + if len(data_list) > 2: + self.frame_received.emit(data_list) + except Exception as e: + self.log_message.emit(f"Serial read error: {e}") + self.msleep(100) + + if self.ser and self.ser.is_open: + self.ser.close() + self.log_message.emit("Serial port closed.") + + def stop(self): + self.running = False + self.wait() + +# ----------------- Main GUI Window ----------------- +class MainWindow(QtWidgets.QMainWindow): + def __init__(self): + super().__init__() + self.setWindowTitle("WiFi CSI Complex Amplitude & Phase Real-time Inference (CNN1D)") + self.resize(1100, 700) + + self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + self.model = None + self.receiver = None + + # CSI Buffer: sliding window of size 50 complex frames + self.csi_window = deque(maxlen=50) + + # Tuning parameters + self.motion_threshold = 2.5 + self.confidence_threshold = 0.80 + self.smoothing_frames = 7 + self.pred_history = deque(maxlen=7) + + # Event detection states + self.is_moving = False + self.event_probabilities = [] + self.idle_counter = 0 + self.debounce_frames = 15 + self.motion_trigger_counter = 0 + self.required_trigger_frames = 3 + + # Load model weights + self.load_model() + + # Build UI layout + self.init_ui() + + # Update Timer (approx. 60 FPS) + self.timer = QtCore.QTimer() + self.timer.timeout.connect(self.process_queue) + self.data_queue = queue.Queue(maxsize=2000) + + def load_model(self): + # Look for weights in possible directories + possible_paths = [ + os.path.join(os.path.dirname(os.path.abspath(__file__)), "models", "best_optimized_cnn.pth"), + os.path.join("temp_workspace", "models", "best_optimized_cnn.pth"), + os.path.join("models", "best_optimized_cnn.pth"), + "best_optimized_cnn.pth" + ] + + model_path = None + for path in possible_paths: + if os.path.exists(path): + model_path = path + break + + self.model = Advanced1DCNN(n_subcarriers=228, num_classes=4).to(self.device) + if model_path: + try: + self.model.load_state_dict(torch.load(model_path, map_location=self.device)) + self.model.eval() + print(f"Model loaded successfully from '{model_path}' on {self.device}") + except Exception as e: + print(f"Error loading weight parameters: {e}") + else: + print("Warning: Optimized CNN1D (best_optimized_cnn.pth) weight file not found. Running with random weights.") + + def init_ui(self): + # Central widget and layout + central_widget = QtWidgets.QWidget() + self.setCentralWidget(central_widget) + main_layout = QtWidgets.QHBoxLayout(central_widget) + + # 1. Left side: Plots + left_layout = QtWidgets.QVBoxLayout() + main_layout.addLayout(left_layout, stretch=7) + + # Plot 1: CSI Amplitude per carrier + self.amp_plot = pg.PlotWidget(title="Real-time CSI Amplitude (114 Subcarriers)") + self.amp_plot.setLabel('left', 'Amplitude') + self.amp_plot.setLabel('bottom', 'Subcarrier Index') + self.amp_curve = self.amp_plot.plot(pen=pg.mkPen('y', width=2)) + left_layout.addWidget(self.amp_plot) + + # Plot 2: CSI Waterfall (Time series) + self.waterfall_plot = pg.PlotWidget(title="Sliding Window CSI Waterfall (50 frames)") + self.waterfall_image = pg.ImageItem() + self.waterfall_plot.addItem(self.waterfall_image) + # Apply colormap to look like a heat map/waterfall + colormap = pg.colormap.get('viridis') + self.waterfall_image.setLookupTable(colormap.getLookupTable()) + left_layout.addWidget(self.waterfall_plot) + + # 2. Right side: Controls & Predictions + right_layout = QtWidgets.QVBoxLayout() + main_layout.addLayout(right_layout, stretch=3) + + # Group 1: Serial connection settings + conn_group = QtWidgets.QGroupBox("Serial Settings") + conn_layout = QtWidgets.QFormLayout(conn_group) + + self.port_input = QtWidgets.QLineEdit("COM3") + self.baud_input = QtWidgets.QComboBox() + self.baud_input.addItems(["921600", "115200", "57600", "9600"]) + self.connect_btn = QtWidgets.QPushButton("Connect") + self.connect_btn.clicked.connect(self.toggle_connection) + + conn_layout.addRow("Port:", self.port_input) + conn_layout.addRow("Baud Rate:", self.baud_input) + conn_layout.addRow(self.connect_btn) + right_layout.addWidget(conn_group) + + # Group 2: Inference Results panel + infer_group = QtWidgets.QGroupBox("Real-time Inference") + infer_layout = QtWidgets.QVBoxLayout(infer_group) + + # Large prediction display + self.pred_label = QtWidgets.QLabel("IDLE") + self.pred_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.pred_label.setFont(QtGui.QFont("Microsoft YaHei", 36, QtGui.QFont.Weight.Bold)) + self.pred_label.setStyleSheet("color: #7f8c8d; background-color: #2c3e50; padding: 20px; border-radius: 10px;") + infer_layout.addWidget(self.pred_label) + + # Motion Level progress bar and text + self.motion_label = QtWidgets.QLabel("Motion Level: 0.00 / Threshold: 2.50") + self.motion_label.setFont(QtGui.QFont("Microsoft YaHei", 11)) + infer_layout.addWidget(self.motion_label) + + self.motion_bar = QtWidgets.QProgressBar() + self.motion_bar.setRange(0, 100) + self.motion_bar.setValue(0) + infer_layout.addWidget(self.motion_bar) + + self.motion_status = QtWidgets.QLabel("⚪ Motion Status: Standby") + self.motion_status.setFont(QtGui.QFont("Microsoft YaHei", 12)) + infer_layout.addWidget(self.motion_status) + + # Sliders for parameters tuning + ctrl_group = QtWidgets.QGroupBox("Parameter Tuning (实时调优)") + ctrl_layout = QtWidgets.QFormLayout(ctrl_group) + + self.motion_slider = QtWidgets.QSlider(QtCore.Qt.Orientation.Horizontal) + self.motion_slider.setRange(10, 100) + self.motion_slider.setValue(25) # 2.5 + self.motion_slider.valueChanged.connect(self.update_parameters) + self.motion_val_lbl = QtWidgets.QLabel("2.5") + + motion_row = QtWidgets.QHBoxLayout() + motion_row.addWidget(self.motion_slider) + motion_row.addWidget(self.motion_val_lbl) + ctrl_layout.addRow("Motion Thresh:", motion_row) + + self.conf_slider = QtWidgets.QSlider(QtCore.Qt.Orientation.Horizontal) + self.conf_slider.setRange(50, 100) + self.conf_slider.setValue(80) # 80% + self.conf_slider.valueChanged.connect(self.update_parameters) + self.conf_val_lbl = QtWidgets.QLabel("0.80") + + conf_row = QtWidgets.QHBoxLayout() + conf_row.addWidget(self.conf_slider) + conf_row.addWidget(self.conf_val_lbl) + ctrl_layout.addRow("Min Confidence:", conf_row) + + self.vote_slider = QtWidgets.QSlider(QtCore.Qt.Orientation.Horizontal) + self.vote_slider.setRange(1, 21) + self.vote_slider.setValue(7) + self.vote_slider.valueChanged.connect(self.update_parameters) + self.vote_val_lbl = QtWidgets.QLabel("7") + + vote_row = QtWidgets.QHBoxLayout() + vote_row.addWidget(self.vote_slider) + vote_row.addWidget(self.vote_val_lbl) + ctrl_layout.addRow("Smoothing Frames:", vote_row) + + self.debounce_slider = QtWidgets.QSlider(QtCore.Qt.Orientation.Horizontal) + self.debounce_slider.setRange(5, 30) + self.debounce_slider.setValue(15) + self.debounce_slider.valueChanged.connect(self.update_parameters) + self.debounce_val_lbl = QtWidgets.QLabel("15") + + debounce_row = QtWidgets.QHBoxLayout() + debounce_row.addWidget(self.debounce_slider) + debounce_row.addWidget(self.debounce_val_lbl) + ctrl_layout.addRow("Event Debounce:", debounce_row) + + infer_layout.addWidget(ctrl_group) + + right_layout.addWidget(infer_group) + + # Group 3: Connection logs + log_group = QtWidgets.QGroupBox("System Logs") + log_layout = QtWidgets.QVBoxLayout(log_group) + self.log_text = QtWidgets.QPlainTextEdit() + self.log_text.setReadOnly(True) + log_layout.addWidget(self.log_text) + right_layout.addWidget(log_group) + + def update_parameters(self): + self.motion_threshold = self.motion_slider.value() / 10.0 + self.motion_val_lbl.setText(f"{self.motion_threshold:.1f}") + + self.confidence_threshold = self.conf_slider.value() / 100.0 + self.conf_val_lbl.setText(f"{self.confidence_threshold:.2f}") + + self.smoothing_frames = self.vote_slider.value() + self.vote_val_lbl.setText(str(self.smoothing_frames)) + + self.debounce_frames = self.debounce_slider.value() + self.debounce_val_lbl.setText(str(self.debounce_frames)) + + current_history = list(self.pred_history) + self.pred_history = deque(current_history, maxlen=self.smoothing_frames) + + self.motion_label.setText(f"Motion Level: 0.00 / Threshold: {self.motion_threshold:.2f}") + + def append_log(self, text): + self.log_text.appendPlainText(text) + + def toggle_connection(self): + if self.receiver is None or not self.receiver.isRunning(): + # Start connection + port = self.port_input.text().strip() + baud = int(self.baud_input.currentText()) + self.receiver = SerialReceiver(port, baud) + self.receiver.frame_received.connect(self.enqueue_frame) + self.receiver.log_message.connect(self.append_log) + self.receiver.start() + self.connect_btn.setText("Disconnect") + self.timer.start(15) + else: + # Stop connection + self.receiver.stop() + self.receiver = None + self.connect_btn.setText("Connect") + self.timer.stop() + self.pred_label.setText("IDLE") + self.pred_label.setStyleSheet("color: #7f8c8d; background-color: #2c3e50; padding: 20px; border-radius: 10px;") + self.motion_status.setText("⚪ Motion Status: Disconnected") + self.is_moving = False + self.event_probabilities = [] + self.motion_trigger_counter = 0 + self.idle_counter = 0 + + def enqueue_frame(self, csi_frame): + # Prevent queue overflow + if self.data_queue.full(): + try: + self.data_queue.get_nowait() + except queue.Empty: + pass + self.data_queue.put(csi_frame) + + def process_queue(self): + # Read all available items in the queue + new_frame_added = False + latest_csi = None + + while not self.data_queue.empty(): + raw_list = self.data_queue.get() + + # Parse complex numbers (interleaved real and imaginary) + if len(raw_list) % 2 != 0: + raw_list = raw_list[:-1] + + imag = np.array(raw_list[0::2]) + real = np.array(raw_list[1::2]) + csi = real + 1j * imag + + # Filter non-zero subcarriers (based on amplitude) + non_zero_mask = np.abs(csi) > 0 + csi_filtered = csi[non_zero_mask] + + # Ensure shape is exactly 114 + if len(csi_filtered) > 114: + csi_filtered = csi_filtered[:114] + elif len(csi_filtered) < 114: + # pad with edge values + padding_len = 114 - len(csi_filtered) + if len(csi_filtered) > 0: + csi_filtered = np.pad(csi_filtered, (0, padding_len), 'edge') + else: + csi_filtered = np.zeros(114, dtype=np.complex128) + + self.csi_window.append(csi_filtered) + latest_csi = csi_filtered + new_frame_added = True + + if not new_frame_added or latest_csi is None: + return + + # 2. Update CSI Line Plot (Amplitude) + self.amp_curve.setData(np.arange(114), np.abs(latest_csi)) + + # 3. Handle inference and sliding window logic + if len(self.csi_window) == 50: + window_complex = np.array(self.csi_window) # Shape: (50, 114) complex + + # Extract amplitude and update waterfall plot + amplitude_matrix = np.abs(window_complex) + self.waterfall_image.setImage(amplitude_matrix.T, autoLevels=True) + + # Calculate Motion Level using variance/standard deviation + subcarrier_stds = np.std(amplitude_matrix, axis=0) + motion_val = float(subcarrier_stds.mean()) + + # Update motion labels and progress bar + self.motion_label.setText(f"Motion Level: {motion_val:.2f} / Threshold: {self.motion_threshold:.2f}") + bar_val = min(100, int(motion_val * 20)) + self.motion_bar.setValue(bar_val) + + # Run inference if motion level exceeds the trigger threshold + if motion_val >= self.motion_threshold: + self.idle_counter = 0 + if not self.is_moving: + self.motion_trigger_counter += 1 + if self.motion_trigger_counter >= self.required_trigger_frames: + self.is_moving = True + self.event_probabilities = [] + self.pred_label.setText("DETECTING...") + self.pred_label.setStyleSheet("color: white; background-color: #f1c40f; padding: 20px; border-radius: 10px;") + self.append_log(f"Motion triggered ({self.required_trigger_frames} consecutive frames). Gesture event started.") + + if self.is_moving: + # Preprocessing pipeline: + # 1. Amplitude and Phase extraction + X_amp = np.abs(window_complex) + X_phase = np.angle(window_complex) + + # 2. Vectorized Linear Phase Calibration (unwrap + CFO/SFO subtraction) + unwrapped = np.unwrap(X_phase, axis=1) # unwrap along subcarriers + x_idx = np.arange(114) + x_mean = x_idx.mean() + x_dev = x_idx - x_mean + D = np.sum(x_dev**2) + + Y_mean = unwrapped.mean(axis=1, keepdims=True) + y_dev = unwrapped - Y_mean + a = np.sum(y_dev * x_dev, axis=1, keepdims=True) / D # Slope per frame + X_phase_cal = y_dev - a * x_dev + + # 3. SR-Std Standardization (eps=2.0) + eps = 2.0 + mean_amp = X_amp.mean(axis=0, keepdims=True) + std_amp = X_amp.std(axis=0, keepdims=True) + X_amp_norm = (X_amp - mean_amp) / (std_amp + eps) + + mean_phase = X_phase_cal.mean(axis=0, keepdims=True) + std_phase = X_phase_cal.std(axis=0, keepdims=True) + X_phase_norm = (X_phase_cal - mean_phase) / (std_phase + eps) + + # 4. Feature Fusion: (50, 114) + (50, 114) -> (50, 228) + X_combined = np.concatenate([X_amp_norm, X_phase_norm], axis=1) + + # Reshape to (batch, channels, time) = (1, 228, 50) + tensor_in = torch.tensor(X_combined, dtype=torch.float32).unsqueeze(0).permute(0, 2, 1) + tensor_in = tensor_in.to(self.device) + + # Predict + with torch.no_grad(): + logits = self.model(tensor_in) + probabilities = torch.softmax(logits, dim=1).squeeze().cpu().numpy() + pred_idx = np.argmax(probabilities) + confidence = float(probabilities[pred_idx]) + + raw_label = INV_LABEL_MAP[pred_idx] + + # Calculate dynamic weight based on motion level + weight = (motion_val - self.motion_threshold) ** 2 + + # Accumulate only when confidence is >= 0.50 and class is not 'unknown' (index 3) + if confidence >= 0.50 and pred_idx != 3: + self.event_probabilities.append((probabilities, weight)) + + # Check confidence threshold before pushing to smoothing queue (for real-time GUI feedback) + if confidence >= self.confidence_threshold: + self.pred_history.append(raw_label) + + # Resolve smoothed label using majority vote for active display + if len(self.pred_history) > 0: + from collections import Counter + counter = Counter(self.pred_history) + smoothed_label = counter.most_common(1)[0][0] + else: + smoothed_label = "UNCERTAIN" + + # Update UI Label color based on smoothed gesture prediction during action + if smoothed_label != "UNCERTAIN": + self.motion_status.setText(f"🔴 Motion Status: ACTIVE ({smoothed_label} {confidence*100:.1f}%)") + else: + self.motion_status.setText(f"🔴 Motion Status: ACTIVE (UNCERTAIN)") + else: + if self.is_moving: + self.motion_trigger_counter = 0 # reset trigger counter + self.idle_counter += 1 + self.motion_status.setText(f"🟡 Motion Status: Debouncing ({self.idle_counter}/{self.debounce_frames})") + + if self.idle_counter >= self.debounce_frames: + # Event completed! Calculate the final classification using weighted probabilities + self.is_moving = False + self.pred_history.clear() + + if len(self.event_probabilities) > 0: + # Sum weighted probabilities over the active period, ignoring the 'unknown' dimension + probs_sum = np.zeros(4) + total_weight = 0.0 + for probs, w in self.event_probabilities: + probs_sum += probs * w + total_weight += w + + if total_weight > 0: + probs_sum /= total_weight + else: + probs_sum = np.sum([p for p, _ in self.event_probabilities], axis=0) + + pred_idx = np.argmax(probs_sum[:3]) # Only predict among cut, grip, draw_o + final_label = INV_LABEL_MAP[pred_idx] + + color_map = { + '挥手': "#3498db", # Blue + '抓握': "#e74c3c", # Red + '画圈': "#9b59b6" # Purple + } + color = color_map.get(final_label, "#ffffff") + self.pred_label.setText(final_label) + self.pred_label.setStyleSheet(f"color: white; background-color: {color}; padding: 20px; border-radius: 10px; border: 3px solid white;") + + self.motion_status.setText(f"⚪ Motion Status: Standby") + self.append_log(f"Gesture event completed. Final prediction: {final_label} (weighted prob: {probs_sum[pred_idx]*100:.1f}%)") + else: + self.pred_label.setText("UNCERTAIN") + self.pred_label.setStyleSheet("color: #7f8c8d; background-color: #2c3e50; padding: 20px; border-radius: 10px;") + self.motion_status.setText("⚪ Motion Status: Standby") + self.append_log("Gesture event completed, but no high-confidence frames were accumulated.") + else: + self.motion_trigger_counter = 0 + self.motion_status.setText("⚪ Motion Status: Standby") + + def closeEvent(self, event): + if self.receiver and self.receiver.isRunning(): + self.receiver.stop() + self.timer.stop() + event.accept() + +if __name__ == "__main__": + app = QtWidgets.QApplication(sys.argv) + win = MainWindow() + win.show() + sys.exit(app.exec()) diff --git a/model/temp_workspace/models/best_baseline_mlp.pth b/model/temp_workspace/models/best_baseline_mlp.pth new file mode 100644 index 0000000..a8d0bbb Binary files /dev/null and b/model/temp_workspace/models/best_baseline_mlp.pth differ diff --git a/model/temp_workspace/models/best_intermediate_cnn.pth b/model/temp_workspace/models/best_intermediate_cnn.pth new file mode 100644 index 0000000..5a5a632 Binary files /dev/null and b/model/temp_workspace/models/best_intermediate_cnn.pth differ diff --git a/model/temp_workspace/models/best_optimized_cnn.pth b/model/temp_workspace/models/best_optimized_cnn.pth new file mode 100644 index 0000000..db88fcf Binary files /dev/null and b/model/temp_workspace/models/best_optimized_cnn.pth differ diff --git a/model/temp_workspace/plots/baseline_mlp_(amp_only)_history.png b/model/temp_workspace/plots/baseline_mlp_(amp_only)_history.png new file mode 100644 index 0000000..a643f21 Binary files /dev/null and b/model/temp_workspace/plots/baseline_mlp_(amp_only)_history.png differ diff --git a/model/temp_workspace/plots/comparison_new.png b/model/temp_workspace/plots/comparison_new.png new file mode 100644 index 0000000..add35f1 Binary files /dev/null and b/model/temp_workspace/plots/comparison_new.png differ diff --git a/model/temp_workspace/plots/intermediate_cnn1d_(amp_only)_history.png b/model/temp_workspace/plots/intermediate_cnn1d_(amp_only)_history.png new file mode 100644 index 0000000..05024d4 Binary files /dev/null and b/model/temp_workspace/plots/intermediate_cnn1d_(amp_only)_history.png differ diff --git a/model/temp_workspace/plots/model_comparison.png b/model/temp_workspace/plots/model_comparison.png new file mode 100644 index 0000000..9eaf335 Binary files /dev/null and b/model/temp_workspace/plots/model_comparison.png differ diff --git a/model/temp_workspace/plots/optimized_cnn1d_(amp_+_phase)_history.png b/model/temp_workspace/plots/optimized_cnn1d_(amp_+_phase)_history.png new file mode 100644 index 0000000..af32e0e Binary files /dev/null and b/model/temp_workspace/plots/optimized_cnn1d_(amp_+_phase)_history.png differ diff --git a/model/temp_workspace/src/dataset.py b/model/temp_workspace/src/dataset.py new file mode 100644 index 0000000..54f91b5 --- /dev/null +++ b/model/temp_workspace/src/dataset.py @@ -0,0 +1,117 @@ +import os +import numpy as np + +#LABEL_MAP = {'bow': 0, 'boxing': 1, 'draw_o': 2, 'stand': 3} +LABEL_MAP = {'cut': 0, 'grip': 1, 'draw_o': 2} +INV_LABEL_MAP = {v: k for k, v in LABEL_MAP.items()} + +def load_dataset(dataset_dir="dataset/dataset_2026_6_10"): + if not os.path.exists(dataset_dir): + raise FileNotFoundError(f"Dataset directory '{dataset_dir}' not found.") + + npz_files = sorted([f for f in os.listdir(dataset_dir) if f.endswith('.npz')]) + X_dict = {} + + print(f"Loading dataset from {dataset_dir}...") + for filename in npz_files: + file_path = os.path.join(dataset_dir, filename) + + # Robust label extraction: find index before the 8-digit date string starting with '202' + parts = filename.split('_') + date_idx = -1 + for idx, part in enumerate(parts): + if len(part) == 8 and part.isdigit() and part.startswith('202'): + date_idx = idx + break + + if date_idx != -1: + label_name = '_'.join(parts[:date_idx]) + else: + label_name = parts[0] + + if label_name not in LABEL_MAP: + print(f" Warning: Skipping unknown label prefix '{label_name}' in file '{filename}'") + continue + + label_idx = LABEL_MAP[label_name] + data = np.load(file_path, allow_pickle=True)['dataset'] + + if label_idx not in X_dict: + X_dict[label_idx] = [] + X_dict[label_idx].append(data) + + X_by_label = {} + for idx in X_dict: + X_by_label[idx] = np.concatenate(X_dict[idx], axis=0) + print(f" Class '{INV_LABEL_MAP[idx]}': {X_by_label[idx].shape[0]} samples") + + return X_by_label + +def split_80_20(X_by_label): + x_train_list, x_test_list = [], [] + y_train_list, y_test_list = [], [] + + print("\nSplitting dataset (80% train, 20% test sequentially)...") + for idx in sorted(X_by_label.keys()): + X = X_by_label[idx] + n_samples = len(X) + split_point = int(n_samples * 0.3) + + x_train_list.append(X[:split_point]) + x_test_list.append(X[split_point:]) + y_train_list.append(np.full(split_point, idx, dtype=np.int64)) + y_test_list.append(np.full(n_samples - split_point, idx, dtype=np.int64)) + print(f" Class '{INV_LABEL_MAP[idx]}': Train={split_point}, Test={n_samples - split_point}") + + x_train = np.concatenate(x_train_list, axis=0) + x_test = np.concatenate(x_test_list, axis=0) + y_train = np.concatenate(y_train_list, axis=0) + y_test = np.concatenate(y_test_list, axis=0) + + return x_train, x_test, y_train, y_test + +def preprocess_csi_fusion(X_complex, eps=2.0): + N = len(X_complex) + X_amp = np.abs(X_complex) + X_phase = np.angle(X_complex) + + # Vectorized Linear Phase Calibration to remove CFO and SFO phase noise + unwrapped = np.unwrap(X_phase, axis=2) + x = np.arange(114) + x_mean = x.mean() + x_dev = x - x_mean + D = np.sum(x_dev**2) + + Y_mean = unwrapped.mean(axis=2, keepdims=True) + y_dev = unwrapped - Y_mean + a = np.sum(y_dev * x_dev, axis=2, keepdims=True) / D + X_phase_cal = y_dev - a * x_dev + + # Apply SR-Std to both amplitude and calibrated phase + X_amp_norm = np.zeros_like(X_amp) + X_phase_norm = np.zeros_like(X_phase_cal) + + for i in range(N): + mean_amp = X_amp[i].mean(axis=0, keepdims=True) + std_amp = X_amp[i].std(axis=0, keepdims=True) + X_amp_norm[i] = (X_amp[i] - mean_amp) / (std_amp + eps) + + mean_phase = X_phase_cal[i].mean(axis=0, keepdims=True) + std_phase = X_phase_cal[i].std(axis=0, keepdims=True) + X_phase_norm[i] = (X_phase_cal[i] - mean_phase) / (std_phase + eps) + + # Concatenate amplitude and phase features along the subcarrier axis (50, 114) + (50, 114) -> (50, 228) + X_combined = np.concatenate([X_amp_norm, X_phase_norm], axis=2) + return X_combined + +def preprocess_csi_amp_only(X_complex, eps=2.0): + N = len(X_complex) + X_amp = np.abs(X_complex) + X_amp_norm = np.zeros_like(X_amp) + + for i in range(N): + mean_amp = X_amp[i].mean(axis=0, keepdims=True) + std_amp = X_amp[i].std(axis=0, keepdims=True) + X_amp_norm[i] = (X_amp[i] - mean_amp) / (std_amp + eps) + + return X_amp_norm diff --git a/model/temp_workspace/src/models.py b/model/temp_workspace/src/models.py new file mode 100644 index 0000000..d93ba2e --- /dev/null +++ b/model/temp_workspace/src/models.py @@ -0,0 +1,86 @@ +import torch +import torch.nn as nn + +class Advanced1DCNN(nn.Module): + def __init__(self, n_subcarriers=228, num_classes=4): + super(Advanced1DCNN, self).__init__() + self.conv1 = nn.Sequential( + nn.Conv1d(n_subcarriers, 64, kernel_size=5, padding=2), + nn.BatchNorm1d(64), + nn.ReLU(), + nn.Dropout1d(0.2), + nn.MaxPool1d(2) + ) + self.conv2 = nn.Sequential( + nn.Conv1d(64, 128, kernel_size=5, padding=2), + nn.BatchNorm1d(128), + nn.ReLU(), + nn.Dropout1d(0.2), + nn.MaxPool1d(2) + ) + self.conv3 = nn.Sequential( + nn.Conv1d(128, 256, kernel_size=5, padding=2), + nn.BatchNorm1d(256), + nn.ReLU(), + nn.Dropout1d(0.2), + nn.AdaptiveAvgPool1d(4) + ) + self.classifier = nn.Sequential( + nn.Flatten(), + nn.Linear(256 * 4, 128), + nn.ReLU(), + nn.Dropout(0.5), + nn.Linear(128, num_classes) + ) + + def forward(self, x): + # input shape: (batch, channels, time) + x = self.conv1(x) + x = self.conv2(x) + x = self.conv3(x) + return self.classifier(x) + +class SimpleMLP(nn.Module): + def __init__(self, input_dim=5700, num_classes=4): # 50 * 114 = 5700, 50 * 228 = 11400 + super(SimpleMLP, self).__init__() + self.net = nn.Sequential( + nn.Flatten(), + nn.Linear(input_dim, 256), + nn.ReLU(), + nn.Dropout(0.3), + nn.Linear(256, 128), + nn.ReLU(), + nn.Dropout(0.3), + nn.Linear(128, num_classes) + ) + + def forward(self, x): + # input shape: (batch, channels, time) + return self.net(x) + +class LSTMGestureClassifier(nn.Module): + def __init__(self, input_dim=228, hidden_dim=128, num_layers=2, num_classes=4): + super(LSTMGestureClassifier, self).__init__() + self.lstm = nn.LSTM( + input_size=input_dim, + hidden_size=hidden_dim, + num_layers=num_layers, + batch_first=True, + dropout=0.3 if num_layers > 1 else 0.0, + bidirectional=True + ) + self.classifier = nn.Sequential( + nn.Linear(hidden_dim * 2, 64), + nn.ReLU(), + nn.Dropout(0.3), + nn.Linear(64, num_classes) + ) + + def forward(self, x): + # input shape: (batch, channels, time) + # nn.LSTM expects: (batch, time, channels) + x = x.permute(0, 2, 1) + lstm_out, (hidden, cell) = self.lstm(x) + # Concatenate bidirectional hidden states of the last layer + last_hidden = torch.cat((hidden[-2], hidden[-1]), dim=1) + return self.classifier(last_hidden) diff --git a/model/temp_workspace/src/test_improvement.py b/model/temp_workspace/src/test_improvement.py new file mode 100644 index 0000000..1dbd34e --- /dev/null +++ b/model/temp_workspace/src/test_improvement.py @@ -0,0 +1,191 @@ +import os +import sys +import numpy as np +import torch +import torch.nn as nn +import torch.optim as optim +from torch.utils.data import DataLoader, TensorDataset + +# Adjust path to import from current directory +sys.path.append(os.path.dirname(os.path.abspath(__file__))) +import dataset + +# Set random seeds +torch.manual_seed(42) +np.random.seed(42) + +# ----------------- 1D ResNet Architecture (High Fidelity) ----------------- +class ResBlock1D(nn.Module): + def __init__(self, in_channels, out_channels, stride=1): + super(ResBlock1D, self).__init__() + self.conv1 = nn.Conv1d(in_channels, out_channels, kernel_size=5, stride=stride, padding=2, bias=False) + self.bn1 = nn.BatchNorm1d(out_channels) + self.relu = nn.ReLU() + self.conv2 = nn.Conv1d(out_channels, out_channels, kernel_size=5, stride=1, padding=2, bias=False) + self.bn2 = nn.BatchNorm1d(out_channels) + + self.shortcut = nn.Sequential() + if stride != 1 or in_channels != out_channels: + self.shortcut = nn.Sequential( + nn.Conv1d(in_channels, out_channels, kernel_size=1, stride=stride, bias=False), + nn.BatchNorm1d(out_channels) + ) + + def forward(self, x): + out = self.relu(self.bn1(self.conv1(x))) + out = self.bn2(self.conv2(out)) + out += self.shortcut(x) + out = self.relu(out) + return out + +class ResNet1DGesture(nn.Module): + def __init__(self, n_subcarriers=228, num_classes=4): + super(ResNet1DGesture, self).__init__() + self.in_channels = 64 + self.prep = nn.Sequential( + nn.Conv1d(n_subcarriers, 64, kernel_size=5, padding=2, bias=False), + nn.BatchNorm1d(64), + nn.ReLU() + ) + self.layer1 = ResBlock1D(64, 64, stride=1) + self.layer2 = ResBlock1D(64, 128, stride=2) + self.layer3 = ResBlock1D(128, 256, stride=2) + self.gap = nn.AdaptiveAvgPool1d(1) + self.classifier = nn.Sequential( + nn.Flatten(), + nn.Dropout(0.5), + nn.Linear(256, 128), + nn.ReLU(), + nn.Dropout(0.3), + nn.Linear(128, num_classes) + ) + + def forward(self, x): + x = self.prep(x) + x = self.layer1(x) + x = self.layer2(x) + x = self.layer3(x) + x = self.gap(x) + return self.classifier(x) + +# ----------------- Data Augmentation & MixUp ----------------- +def augment_csi_batch(x_tensor): + # Temporal translation/roll augmentation: roll by random frames [-3, 3] + batch_size = x_tensor.shape[0] + x_aug = x_tensor.clone() + for i in range(batch_size): + shift = np.random.randint(-3, 4) + if shift != 0: + x_aug[i] = torch.roll(x_aug[i], shifts=shift, dims=1) + + # Add minor Gaussian noise + noise = torch.randn_like(x_aug) * 0.02 + return x_aug + noise + +def mixup_data(x, y, alpha=0.15, device='cpu'): # tuned alpha + if alpha > 0: + lam = np.random.beta(alpha, alpha) + else: + lam = 1 + batch_size = x.size()[0] + index = torch.randperm(batch_size).to(device) + mixed_x = lam * x + (1 - lam) * x[index] + y_a, y_b = y, y[index] + return mixed_x, y_a, y_b, lam + +def mixup_criterion(criterion, pred, y_a, y_b, lam): + return lam * criterion(pred, y_a) + (1 - lam) * criterion(pred, y_b) + +def main(): + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + print(f"Using device: {device}") + + # 1. Load and preprocess complex fusion dataset + X_by_label = dataset.load_dataset() + x_train_raw, x_test_raw, y_train, y_test = dataset.split_80_20(X_by_label) + + print("\nPreprocessing: Amplitude + Calibrated Phase Fusion...") + x_train_fusion = dataset.preprocess_csi_fusion(x_train_raw) + x_test_fusion = dataset.preprocess_csi_fusion(x_test_raw) + + x_train_fusion_t = torch.tensor(x_train_fusion, dtype=torch.float32).permute(0, 2, 1) # (N, 228, 50) + x_test_fusion_t = torch.tensor(x_test_fusion, dtype=torch.float32).permute(0, 2, 1) + + train_loader = DataLoader(TensorDataset(x_train_fusion_t, torch.tensor(y_train)), batch_size=64, shuffle=True) + test_loader = DataLoader(TensorDataset(x_test_fusion_t, torch.tensor(y_test)), batch_size=64, shuffle=False) + + # 2. Initialize ResNet1D Model + model = ResNet1DGesture(n_subcarriers=228, num_classes=4).to(device) + + criterion_train = nn.CrossEntropyLoss(label_smoothing=0.1) + + # Hyperparameters + epochs = 40 + optimizer = optim.AdamW(model.parameters(), lr=1e-3, weight_decay=5e-2) + scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs, eta_min=1e-6) + + best_test_acc = 0.0 + best_weights = None + + print("\n================ Training Optimized ResNet1D (Tuned Augmentation + MixUp) ================") + for epoch in range(epochs): + model.train() + correct, total, loss_val = 0, 0, 0.0 + + for inputs, labels in train_loader: + inputs, labels = inputs.to(device), labels.to(device) + + # Apply Temporal Shift & Gaussian Noise + inputs_aug = augment_csi_batch(inputs) + + # Apply MixUp Data Mixing + inputs_mixed, labels_a, labels_b, lam = mixup_data(inputs_aug, labels, alpha=0.15, device=device) + + optimizer.zero_grad() + outputs = model(inputs_mixed) + loss = mixup_criterion(criterion_train, outputs, labels_a, labels_b, lam) + loss.backward() + optimizer.step() + loss_val += loss.item() + + # Calculate training accuracy on non-mixed predictions + with torch.no_grad(): + outputs_orig = model(inputs) + _, predicted = torch.max(outputs_orig.data, 1) + total += labels.size(0) + correct += (predicted == labels).sum().item() + + train_acc = 100 * correct / total + scheduler.step() + + # Test Evaluation + model.eval() + test_correct, test_total = 0, 0 + with torch.no_grad(): + for inputs, labels in test_loader: + inputs, labels = inputs.to(device), labels.to(device) + outputs = model(inputs) + _, predicted = torch.max(outputs.data, 1) + test_total += labels.size(0) + test_correct += (predicted == labels).sum().item() + + test_acc = 100 * test_correct / test_total + avg_loss = loss_val / len(train_loader) + + if test_acc > best_test_acc: + best_test_acc = test_acc + best_weights = model.state_dict().copy() + + print(f"Epoch {epoch+1:02d}/{epochs:02d}: Loss={avg_loss:.4f}, Train Acc={train_acc:.2f}%, Test Acc={test_acc:.2f}%") + + print(f"\n================ Finished ResNet1D! Best Test Accuracy: {best_test_acc:.2f}% ================") + + # Save the best weights + src_dir = os.path.dirname(os.path.abspath(__file__)) + workspace_dir = os.path.dirname(src_dir) + save_path = os.path.join(workspace_dir, "models", "best_optimized_cnn.pth") + torch.save(best_weights, save_path) + print(f"Saved optimized ResNet1D weights to: {save_path}") + +if __name__ == '__main__': + main() diff --git a/model/temp_workspace/src/train.py b/model/temp_workspace/src/train.py new file mode 100644 index 0000000..8b345a1 --- /dev/null +++ b/model/temp_workspace/src/train.py @@ -0,0 +1,224 @@ +import os +import sys +import numpy as np +import torch +import torch.nn as nn +import torch.optim as optim +from torch.utils.data import DataLoader, TensorDataset +import matplotlib.pyplot as plt + +# Adjust path to import from current directory +sys.path.append(os.path.dirname(os.path.abspath(__file__))) +import dataset +import models + +# Set random seeds +torch.manual_seed(42) +np.random.seed(42) + +def train_configuration(config_name, model, train_loader, test_loader, epochs, device): + print(f"\n================ Training Configuration: {config_name} ================") + criterion = nn.CrossEntropyLoss(label_smoothing=0.1) + optimizer = optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-2) + scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs) + + best_test_acc = 0.0 + history = {'train_loss': [], 'train_acc': [], 'test_acc': []} + best_weights = None + + for epoch in range(epochs): + model.train() + correct, total, loss_val = 0, 0, 0.0 + for inputs, labels in train_loader: + inputs, labels = inputs.to(device), labels.to(device) + optimizer.zero_grad() + outputs = model(inputs) + loss = criterion(outputs, labels) + loss.backward() + optimizer.step() + loss_val += loss.item() + _, predicted = torch.max(outputs.data, 1) + total += labels.size(0) + correct += (predicted == labels).sum().item() + + train_acc = 100 * correct / total + scheduler.step() + + # Evaluation + model.eval() + test_correct, test_total = 0, 0 + with torch.no_grad(): + for inputs, labels in test_loader: + inputs, labels = inputs.to(device), labels.to(device) + outputs = model(inputs) + _, predicted = torch.max(outputs.data, 1) + test_total += labels.size(0) + test_correct += (predicted == labels).sum().item() + + test_acc = 100 * test_correct / test_total + avg_loss = loss_val / len(train_loader) + + history['train_loss'].append(avg_loss) + history['train_acc'].append(train_acc) + history['test_acc'].append(test_acc) + + if test_acc > best_test_acc: + best_test_acc = test_acc + best_weights = model.state_dict().copy() + + print(f"Epoch {epoch+1:02d}/{epochs:02d}: Loss={avg_loss:.4f}, Train Acc={train_acc:.2f}%, Test Acc={test_acc:.2f}%") + + print(f"Finished {config_name}! Best Test Accuracy: {best_test_acc:.2f}%") + return best_test_acc, best_weights, history + +def evaluate_best_model(config_name, model, weights, test_loader, device): + model.load_state_dict(weights) + model.eval() + all_preds = [] + all_labels = [] + + with torch.no_grad(): + for inputs, labels in test_loader: + inputs = inputs.to(device) + outputs = model(inputs) + _, predicted = torch.max(outputs.data, 1) + all_preds.extend(predicted.cpu().numpy()) + all_labels.extend(labels.numpy()) + + all_preds = np.array(all_preds) + all_labels = np.array(all_labels) + + print(f"\n=== Per-Class Accuracy Evaluation for {config_name} ===") + for class_name, class_idx in dataset.LABEL_MAP.items(): + indices = np.where(all_labels == class_idx)[0] + if len(indices) > 0: + class_correct = np.sum(all_preds[indices] == class_idx) + class_acc = 100 * class_correct / len(indices) + print(f" Class '{class_name}': Acc={class_acc:.2f}% ({class_correct}/{len(indices)})") + else: + print(f" Class '{class_name}': No samples present in the test set.") + +def save_plots(config_name, history, plot_dir): + plt.figure(figsize=(10, 4)) + plt.subplot(1, 2, 1) + plt.plot(history['train_loss'], 'r-', label='Train Loss') + plt.title(f'{config_name} Training Loss') + plt.xlabel('Epoch') + plt.ylabel('Loss') + plt.grid(True) + plt.legend() + + plt.subplot(1, 2, 2) + plt.plot(history['train_acc'], 'g-', label='Train Acc') + plt.plot(history['test_acc'], 'b-', label='Test Acc') + plt.title(f'{config_name} Accuracy') + plt.xlabel('Epoch') + plt.ylabel('Accuracy (%)') + plt.grid(True) + plt.legend() + + plt.tight_layout() + filename = f"{config_name.lower().replace(' ', '_')}_history.png" + plt.savefig(os.path.join(plot_dir, filename)) + plt.close() + print(f"Saved {config_name} curves to {filename}") + +def main(): + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + print(f"Using device: {device}") + + # Establish organized directories + src_dir = os.path.dirname(os.path.abspath(__file__)) + workspace_dir = os.path.dirname(src_dir) + models_dir = os.path.join(workspace_dir, "models") + plots_dir = os.path.join(workspace_dir, "plots") + os.makedirs(models_dir, exist_ok=True) + os.makedirs(plots_dir, exist_ok=True) + + # 1. Load raw dataset + X_by_label = dataset.load_dataset() + x_train_raw, x_test_raw, y_train, y_test = dataset.split_80_20(X_by_label) + + all_results = {} + + # ========================================================================= + # Config 1: Baseline MLP (Amplitude Only) + # ========================================================================= + print("\n--- Preprocessing: Amplitude Only (Config 1 & 2) ---") + x_train_amp = dataset.preprocess_csi_amp_only(x_train_raw) + x_test_amp = dataset.preprocess_csi_amp_only(x_test_raw) + + x_train_amp_t = torch.tensor(x_train_amp, dtype=torch.float32).permute(0, 2, 1) # (N, 114, 50) + x_test_amp_t = torch.tensor(x_test_amp, dtype=torch.float32).permute(0, 2, 1) + + train_loader_amp = DataLoader(TensorDataset(x_train_amp_t, torch.tensor(y_train)), batch_size=64, shuffle=True) + test_loader_amp = DataLoader(TensorDataset(x_test_amp_t, torch.tensor(y_test)), batch_size=64, shuffle=False) + + model_mlp = models.SimpleMLP(input_dim=5700, num_classes=4).to(device) + best_acc_mlp, weights_mlp, history_mlp = train_configuration( + "Baseline MLP (Amp Only)", model_mlp, train_loader_amp, test_loader_amp, epochs=20, device=device + ) + all_results["Baseline MLP (Amp Only)"] = (best_acc_mlp, history_mlp) + torch.save(weights_mlp, os.path.join(models_dir, "best_baseline_mlp.pth")) + save_plots("Baseline MLP (Amp Only)", history_mlp, plots_dir) + evaluate_best_model("Baseline MLP (Amp Only)", model_mlp, weights_mlp, test_loader_amp, device) + + # ========================================================================= + # Config 2: Intermediate CNN1D (Amplitude Only) + # ========================================================================= + model_cnn1d_amp = models.Advanced1DCNN(n_subcarriers=114, num_classes=4).to(device) + best_acc_cnn_amp, weights_cnn_amp, history_cnn_amp = train_configuration( + "Intermediate CNN1D (Amp Only)", model_cnn1d_amp, train_loader_amp, test_loader_amp, epochs=20, device=device + ) + all_results["Intermediate CNN1D (Amp Only)"] = (best_acc_cnn_amp, history_cnn_amp) + torch.save(weights_cnn_amp, os.path.join(models_dir, "best_intermediate_cnn.pth")) + save_plots("Intermediate CNN1D (Amp Only)", history_cnn_amp, plots_dir) + evaluate_best_model("Intermediate CNN1D (Amp Only)", model_cnn1d_amp, weights_cnn_amp, test_loader_amp, device) + + # ========================================================================= + # Config 3: Optimized CNN1D (Amplitude + Calibrated Phase Fusion) + # ========================================================================= + print("\n--- Preprocessing: Amplitude + Calibrated Phase Fusion (Config 3) ---") + x_train_fusion = dataset.preprocess_csi_fusion(x_train_raw) + x_test_fusion = dataset.preprocess_csi_fusion(x_test_raw) + + x_train_fusion_t = torch.tensor(x_train_fusion, dtype=torch.float32).permute(0, 2, 1) # (N, 228, 50) + x_test_fusion_t = torch.tensor(x_test_fusion, dtype=torch.float32).permute(0, 2, 1) + + train_loader_fusion = DataLoader(TensorDataset(x_train_fusion_t, torch.tensor(y_train)), batch_size=64, shuffle=True) + test_loader_fusion = DataLoader(TensorDataset(x_test_fusion_t, torch.tensor(y_test)), batch_size=64, shuffle=False) + + model_cnn1d_fusion = models.Advanced1DCNN(n_subcarriers=228, num_classes=4).to(device) + best_acc_fusion, weights_fusion, history_fusion = train_configuration( + "Optimized CNN1D (Amp + Phase)", model_cnn1d_fusion, train_loader_fusion, test_loader_fusion, epochs=20, device=device + ) + all_results["Optimized CNN1D (Amp + Phase)"] = (best_acc_fusion, history_fusion) + torch.save(weights_fusion, os.path.join(models_dir, "best_optimized_cnn.pth")) + save_plots("Optimized CNN1D (Amp + Phase)", history_fusion, plots_dir) + evaluate_best_model("Optimized CNN1D (Amp + Phase)", model_cnn1d_fusion, weights_fusion, test_loader_fusion, device) + + # 4. Generate Joint Comparison Plot + plt.figure(figsize=(10, 6)) + for name, (best_acc, history) in all_results.items(): + plt.plot(range(1, len(history['test_acc']) + 1), history['test_acc'], label=f"{name} (Best: {best_acc:.2f}%)") + + plt.title('Test Accuracy Comparison across Milestones (Strict 80/20 split)') + plt.xlabel('Epoch') + plt.ylabel('Test Accuracy (%)') + plt.grid(True) + plt.legend(loc='lower right') + plt.tight_layout() + comparison_filename = "model_comparison.png" + plt.savefig(os.path.join(plots_dir, comparison_filename)) + plt.close() + print(f"\nSaved overall comparison plot to {comparison_filename} inside plots/ directory.") + + # 5. Print final summary table + print("\n================ Final Performance Summary ================") + print(f"{'Configuration':30s} | {'Best Test Accuracy (%)':22s}") + print("-" * 55) + for name, (acc, _) in all_results.items(): + print(f"{name:30s} | {acc:22.2f}%") + +if __name__ == '__main__': + main() diff --git a/model/temp_workspace/src/train_new.py b/model/temp_workspace/src/train_new.py new file mode 100644 index 0000000..302b5b3 --- /dev/null +++ b/model/temp_workspace/src/train_new.py @@ -0,0 +1,208 @@ +# -*- coding: utf-8 -*- +import os +import sys +import numpy as np +import torch +import torch.nn as nn +import torch.optim as optim +from torch.utils.data import DataLoader, TensorDataset +import matplotlib.pyplot as plt + +# Adjust path to import dataset and models +sys.path.append(os.path.dirname(os.path.abspath(__file__))) +import dataset +import models + +# Set random seeds for reproducibility +torch.manual_seed(42) +np.random.seed(42) + +# ----------------- Transformer Architecture ----------------- +class PositionalEncoding(nn.Module): + def __init__(self, d_model, max_len=500): + super().__init__() + pe = torch.zeros(max_len, d_model) + position = torch.arange(0, max_len).unsqueeze(1).float() + div_term = torch.exp(torch.arange(0, d_model, 2).float() * -(np.log(10000.0) / d_model)) + pe[:, 0::2] = torch.sin(position * div_term) + pe[:, 1::2] = torch.cos(position * div_term) + pe = pe.unsqueeze(0) + self.register_buffer('pe', pe) + + def forward(self, x): + return x + self.pe[:, :x.size(1)] + +class CSITransformer(nn.Module): + def __init__(self, input_dim=228, d_model=128, nhead=4, num_layers=2, num_classes=4): + super(CSITransformer, self).__init__() + self.input_proj = nn.Linear(input_dim, d_model) + self.pos_encoder = PositionalEncoding(d_model) + encoder_layer = nn.TransformerEncoderLayer( + d_model=d_model, + nhead=nhead, + dim_feedforward=256, + dropout=0.1, + batch_first=True + ) + self.transformer = nn.TransformerEncoder(encoder_layer, num_layers) + self.classifier = nn.Sequential( + nn.Linear(d_model, 64), + nn.ReLU(), + nn.Dropout(0.3), + nn.Linear(64, num_classes) + ) + + def forward(self, x): + # input shape: (batch, channels, time) = (batch, 228, 50) + x = x.permute(0, 2, 1) # -> (batch, 50, 228) + x = self.input_proj(x) + x = self.pos_encoder(x) + x = self.transformer(x) + x = x.mean(dim=1) # Global Average Pooling over temporal axis + return self.classifier(x) + +# ----------------- Training Pipeline ----------------- +def train_configuration(config_name, model, train_loader, test_loader, epochs, device): + print(f"\n================ Training Configuration: {config_name} ================") + criterion = nn.CrossEntropyLoss(label_smoothing=0.1) + optimizer = optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-2) + scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs) + + best_test_acc = 0.0 + history = {'train_loss': [], 'train_acc': [], 'test_acc': []} + + for epoch in range(epochs): + model.train() + correct, total, loss_val = 0, 0, 0.0 + for inputs, labels in train_loader: + inputs, labels = inputs.to(device), labels.to(device) + optimizer.zero_grad() + outputs = model(inputs) + loss = criterion(outputs, labels) + loss.backward() + optimizer.step() + loss_val += loss.item() + _, predicted = torch.max(outputs.data, 1) + total += labels.size(0) + correct += (predicted == labels).sum().item() + + train_acc = 100 * correct / total + scheduler.step() + + # Evaluation + model.eval() + test_correct, test_total = 0, 0 + with torch.no_grad(): + for inputs, labels in test_loader: + inputs, labels = inputs.to(device), labels.to(device) + outputs = model(inputs) + _, predicted = torch.max(outputs.data, 1) + test_total += labels.size(0) + test_correct += (predicted == labels).sum().item() + + test_acc = 100 * test_correct / test_total + avg_loss = loss_val / len(train_loader) + + history['train_loss'].append(avg_loss) + history['train_acc'].append(train_acc) + history['test_acc'].append(test_acc) + + if test_acc > best_test_acc: + best_test_acc = test_acc + + print(f"Epoch {epoch+1:02d}/{epochs:02d}: Loss={avg_loss:.4f}, Train Acc={train_acc:.2f}%, Test Acc={test_acc:.2f}%") + + print(f"Finished {config_name}! Best Test Accuracy: {best_test_acc:.2f}%") + return best_test_acc, history + +def main(): + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + print(f"Using device: {device}") + + # Establish organized directories + src_dir = os.path.dirname(os.path.abspath(__file__)) + workspace_dir = os.path.dirname(src_dir) + plots_dir = os.path.join(workspace_dir, "plots") + os.makedirs(plots_dir, exist_ok=True) + + # 1. Load raw dataset using dataset.py + X_by_label = dataset.load_dataset() + x_train_raw, x_test_raw, y_train, y_test = dataset.split_80_20(X_by_label) + + # 2. Preprocess: Fused Amplitude + Calibrated Phase + print("\n--- Preprocessing: Amplitude + Calibrated Phase Fusion ---") + x_train_fusion = dataset.preprocess_csi_fusion(x_train_raw) + x_test_fusion = dataset.preprocess_csi_fusion(x_test_raw) + + # Permute to (N, channels, time) = (N, 228, 50) + x_train_t = torch.tensor(x_train_fusion, dtype=torch.float32).permute(0, 2, 1) + x_test_t = torch.tensor(x_test_fusion, dtype=torch.float32).permute(0, 2, 1) + + train_loader = DataLoader(TensorDataset(x_train_t, torch.tensor(y_train)), batch_size=64, shuffle=True) + test_loader = DataLoader(TensorDataset(x_test_t, torch.tensor(y_test)), batch_size=64, shuffle=False) + + epochs = 20 + all_results = {} + + # ========================================================================= + # Config 1: Simple MLP (Fused Amp + Phase) + # ========================================================================= + model_mlp = models.SimpleMLP(input_dim=11400, num_classes=4).to(device) + best_acc_mlp, history_mlp = train_configuration( + "MLP (Amp + Phase)", model_mlp, train_loader, test_loader, epochs=epochs, device=device + ) + all_results["MLP"] = (best_acc_mlp, history_mlp) + + # ========================================================================= + # Config 2: CNN1D (Fused Amp + Phase) + # ========================================================================= + model_cnn = models.Advanced1DCNN(n_subcarriers=228, num_classes=4).to(device) + best_acc_cnn, history_cnn = train_configuration( + "CNN1D (Amp + Phase)", model_cnn, train_loader, test_loader, epochs=epochs, device=device + ) + all_results["CNN1D"] = (best_acc_cnn, history_cnn) + + # ========================================================================= + # Config 3: LSTM (Fused Amp + Phase) + # ========================================================================= + model_lstm = models.LSTMGestureClassifier(input_dim=228, num_classes=4).to(device) + best_acc_lstm, history_lstm = train_configuration( + "LSTM (Amp + Phase)", model_lstm, train_loader, test_loader, epochs=epochs, device=device + ) + all_results["LSTM"] = (best_acc_lstm, history_lstm) + + # ========================================================================= + # Config 4: Transformer (Fused Amp + Phase) + # ========================================================================= + model_transformer = CSITransformer(input_dim=228, d_model=128, nhead=4, num_layers=2, num_classes=4).to(device) + best_acc_trans, history_trans = train_configuration( + "Transformer (Amp + Phase)", model_transformer, train_loader, test_loader, epochs=epochs, device=device + ) + all_results["Transformer"] = (best_acc_trans, history_trans) + + # 3. Generate Evaluation Plot (Test Accuracy) + plt.figure(figsize=(10, 6)) + for name, (best_acc, history) in all_results.items(): + plt.plot(range(1, epochs + 1), history['test_acc'], marker='o', label=f"{name} (Best: {best_acc:.2f}%)") + + plt.title('Test Accuracy Comparison across Architectures (Amplitude + Phase Fusion)') + plt.xlabel('Epoch') + plt.ylabel('Test Accuracy (%)') + plt.grid(True) + plt.legend(loc='lower right') + plt.tight_layout() + + comparison_filename = "comparison_new.png" + plt.savefig(os.path.join(plots_dir, comparison_filename)) + plt.close() + print(f"\nSaved overall comparison plot to {comparison_filename} inside plots/ directory.") + + # 4. Print final summary table + print("\n================ Final Performance Summary ================") + print(f"{'Architecture':15s} | {'Best Test Accuracy (%)':22s}") + print("-" * 42) + for name, (acc, _) in all_results.items(): + print(f"{name:15s} | {acc:22.2f}%") + +if __name__ == '__main__': + main() diff --git a/model/temp_workspace/test_debouncing.py b/model/temp_workspace/test_debouncing.py new file mode 100644 index 0000000..8539145 --- /dev/null +++ b/model/temp_workspace/test_debouncing.py @@ -0,0 +1,130 @@ +import os +import sys +import time +import numpy as np +import torch +from pyqtgraph.Qt import QtWidgets, QtCore + +# Add temp_workspace to path so we can import get_tool_fusion +sys.path.append(os.path.dirname(os.path.abspath(__file__))) +from get_tool_fusion import MainWindow + +def run_test(): + print("Initializing headless PyQt Application...") + app = QtWidgets.QApplication(sys.argv) + + print("Instantiating MainWindow...") + win = MainWindow() + + # 1. Verify initialization states + assert win.is_moving is False + assert len(win.event_probabilities) == 0 + assert win.motion_trigger_counter == 0 + assert win.required_trigger_frames == 3 + print("[OK] Initialization states verified.") + + # 2. Simulate frames. We need to feed CSI data into the sliding window. + # Each CSI frame needs to be a 114-element complex array. + # To trigger different motion levels, we can manually fill win.csi_window. + # Since csi_window size must be 50 to run inference: + + print("\n--- Test 1: Transient noise spike (1 frame of motion) ---") + # Fill window with quiet frames (standard deviation = 0.0) + for _ in range(50): + win.csi_window.append(np.ones(114, dtype=np.complex128)) + + # Process one cycle with quiet frames + win.data_queue.put([1]*228) # dummy data to trigger new_frame_added in process_queue + win.process_queue() + assert win.is_moving is False + assert win.motion_trigger_counter == 0 + + # Inject 1 high-motion frame (e.g. modify the last frame in window to have high amplitude) + # This will cause subcarrier_stds to have a non-zero mean. + # Let's check how win.process_queue calculates motion: + # subcarrier_stds = np.std(amplitude_matrix, axis=0) + # motion_val = float(subcarrier_stds.mean()) + # So if we make amplitude of a frame larger: + temp_window = [np.ones(114, dtype=np.complex128) for _ in range(49)] + temp_window.append(np.ones(114, dtype=np.complex128) * 100.0) # huge variation + for frame in temp_window: + win.csi_window.append(frame) + + win.data_queue.put([1]*228) + win.process_queue() + + # motion_val should be high. Since it's only 1 frame: + print(f"Trigger counter after 1 frame of high motion: {win.motion_trigger_counter}") + assert win.motion_trigger_counter == 1 + assert win.is_moving is False + print("[OK] Transient spike correctly ignored by trigger guard.") + + # 3. Simulate Drop back to quiet + # The next frame is quiet, trigger counter should reset to 0. + for _ in range(50): + win.csi_window.append(np.ones(114, dtype=np.complex128)) + win.data_queue.put([1]*228) + win.process_queue() + assert win.motion_trigger_counter == 0 + assert win.is_moving is False + print("[OK] Trigger counter correctly reset to 0 after motion dropped.") + + # 4. Simulate a real gesture (3 consecutive frames of high motion) + print("\n--- Test 2: Valid gesture start (3 consecutive frames) ---") + for step in range(3): + temp_window = [np.ones(114, dtype=np.complex128) for _ in range(49)] + temp_window.append(np.ones(114, dtype=np.complex128) * 100.0) + for frame in temp_window: + win.csi_window.append(frame) + win.data_queue.put([1]*228) + win.process_queue() + + assert win.is_moving is True + print(f"[OK] State is_moving: {win.is_moving}") + print(f"[OK] Event probabilities accumulated: {len(win.event_probabilities)} frames.") + assert len(win.event_probabilities) == 1 # Triggered on the 3rd frame, so 1 frame accumulated + + # 5. Continue gesture with 5 more active frames + print("\n--- Test 3: Gesture continuation and weighted probability accumulation ---") + for _ in range(5): + temp_window = [np.ones(114, dtype=np.complex128) for _ in range(49)] + temp_window.append(np.ones(114, dtype=np.complex128) * 100.0) + for frame in temp_window: + win.csi_window.append(frame) + win.data_queue.put([1]*228) + win.process_queue() + + assert win.is_moving is True + print(f"[OK] Accumulated probability frames: {len(win.event_probabilities)}") + # We should have accumulated a total of 1 (from step 3) + 5 = 6 frames + assert len(win.event_probabilities) == 6 + + # Check that probabilities are pairs of (prob_vector, weight) + for probs, w in win.event_probabilities: + assert len(probs) == 4 + assert w > 0.0 + print("[OK] Probability and weight structure verified.") + + # 6. Simulate debounce and event completion + print("\n--- Test 4: Gesture debouncing and final decision ---") + # Feed 15 quiet frames (debounce_frames = 15) + for d in range(1, 16): + for _ in range(50): + win.csi_window.append(np.ones(114, dtype=np.complex128)) + win.data_queue.put([1]*228) + win.process_queue() + if d < 15: + assert win.is_moving is True + assert "Debouncing" in win.motion_status.text() + else: + assert win.is_moving is False + assert "Standby" in win.motion_status.text() + + print("[OK] Event successfully debounced and finalized.") + assert win.pred_label.text() == "挥手" + print("[OK] Final prediction displayed on label successfully.") + + print("\n================ All Tests Passed! ================") + +if __name__ == "__main__": + run_test() diff --git a/model/train_50_50.py b/model/train_50_50.py new file mode 100644 index 0000000..06d5efe --- /dev/null +++ b/model/train_50_50.py @@ -0,0 +1,232 @@ +import os +import sys +import numpy as np +import torch +import torch.nn as nn +import torch.optim as optim +from torch.utils.data import DataLoader, TensorDataset +import matplotlib.pyplot as plt + +# Set random seed for reproducibility +torch.manual_seed(42) +np.random.seed(42) + +LABEL_MAP = {'draw': 0, 'stand-up': 1, 'wave': 2} +INV_LABEL_MAP = {v: k for k, v in LABEL_MAP.items()} + +def load_dataset(dataset_dir="dataset/dataset_2026_6_23"): + if not os.path.exists(dataset_dir): + raise FileNotFoundError(f"Dataset directory '{dataset_dir}' not found.") + npz_files = sorted([f for f in os.listdir(dataset_dir) if f.endswith('.npz')]) + X_dict = {0: [], 1: [], 2: []} + + print(f"Loading dataset from {dataset_dir}...") + for filename in sorted(npz_files): + file_path = os.path.join(dataset_dir, filename) + label_name = filename.split('_')[0] + if label_name not in LABEL_MAP: + continue + label_idx = LABEL_MAP[label_name] + data = np.load(file_path, allow_pickle=True)['dataset'] + X_dict[label_idx].append(data) + + X_by_label = {} + for idx in X_dict: + X_by_label[idx] = np.concatenate(X_dict[idx], axis=0) + print(f" Class '{INV_LABEL_MAP[idx]}': {X_by_label[idx].shape[0]} samples") + + return X_by_label + +def split_50_50(X_by_label): + x_train_list, x_test_list = [], [] + y_train_list, y_test_list = [], [] + + print("\nSplitting dataset (50% train, 50% test sequentially)...") + for idx in sorted(X_by_label.keys()): + X = X_by_label[idx] + n_samples = len(X) + split_point = int(n_samples * 0.5) + + x_train_list.append(X[:split_point]) + x_test_list.append(X[split_point:]) + y_train_list.append(np.full(split_point, idx, dtype=np.int64)) + y_test_list.append(np.full(n_samples - split_point, idx, dtype=np.int64)) + print(f" Class '{INV_LABEL_MAP[idx]}': Train={split_point}, Test={n_samples - split_point}") + + x_train = np.concatenate(x_train_list, axis=0) + x_test = np.concatenate(x_test_list, axis=0) + y_train = np.concatenate(y_train_list, axis=0) + y_test = np.concatenate(y_test_list, axis=0) + + return x_train, x_test, y_train, y_test + +def preprocess_sr_std(X, eps=2.0): + # Subcarrier-wise Regularized Standardization (SR-Std) + X_norm = np.zeros_like(X) + for i in range(len(X)): + mean = X[i].mean(axis=0, keepdims=True) + std = X[i].std(axis=0, keepdims=True) + X_norm[i] = (X[i] - mean) / (std + eps) + return X_norm + +class FCN(nn.Module): + def __init__(self, input_dim=114, num_classes=3): + super(FCN, self).__init__() + self.conv1 = nn.Sequential( + nn.Conv1d(input_dim, 128, kernel_size=8, padding=4), + nn.BatchNorm1d(128), + nn.ReLU() + ) + self.conv2 = nn.Sequential( + nn.Conv1d(128, 256, kernel_size=5, padding=2), + nn.BatchNorm1d(256), + nn.ReLU() + ) + self.conv3 = nn.Sequential( + nn.Conv1d(256, 128, kernel_size=3, padding=1), + nn.BatchNorm1d(128), + nn.ReLU() + ) + self.gap = nn.AdaptiveAvgPool1d(1) + self.fc = nn.Sequential( + nn.Dropout(0.5), + nn.Linear(128, num_classes) + ) + + def forward(self, x): + # input x shape: (batch, input_dim, time) + x = self.conv1(x) + x = self.conv2(x) + x = self.conv3(x) + x = self.gap(x) + x = x.squeeze(-1) + return self.fc(x) + +def main(): + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + print(f"Using device: {device}") + script_dir = os.path.dirname(os.path.abspath(__file__)) + + # Load raw dataset + X_by_label = load_dataset() + x_train_raw, x_test_raw, y_train, y_test = split_50_50(X_by_label) + + # Apply Preprocessing (SR-Std) + print("\nApplying Subcarrier-wise Regularized Standardization (SR-Std)...") + x_train = preprocess_sr_std(x_train_raw, eps=2.0) + x_test = preprocess_sr_std(x_test_raw, eps=2.0) + + # Convert to Tensor and permute to (batch, subcarriers, time) for 1D convolution + x_train_tensor = torch.tensor(x_train, dtype=torch.float32).permute(0, 2, 1) + x_test_tensor = torch.tensor(x_test, dtype=torch.float32).permute(0, 2, 1) + + train_dataset = TensorDataset(x_train_tensor, torch.tensor(y_train, dtype=torch.long)) + test_dataset = TensorDataset(x_test_tensor, torch.tensor(y_test, dtype=torch.long)) + + train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True) + test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False) + + model = FCN(input_dim=114, num_classes=3).to(device) + criterion = nn.CrossEntropyLoss(label_smoothing=0.1) + optimizer = optim.AdamW(model.parameters(), lr=1e-5, weight_decay=1e-2) + scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=30) + + epochs = 30 + history = {'train_loss': [], 'train_acc': [], 'test_acc': []} + best_test_acc = 0.0 + + print("\n=== Starting Training (FCN + SR-Std + 50/50 Sequential Split) ===") + for epoch in range(epochs): + model.train() + correct, total, loss_val = 0, 0, 0.0 + for inputs, labels in train_loader: + inputs, labels = inputs.to(device), labels.to(device) + optimizer.zero_grad() + outputs = model(inputs) + loss = criterion(outputs, labels) + loss.backward() + optimizer.step() + loss_val += loss.item() + _, predicted = torch.max(outputs.data, 1) + total += labels.size(0) + correct += (predicted == labels).sum().item() + + train_acc = 100 * correct / total + scheduler.step() + + # Test evaluation + model.eval() + test_correct, test_total = 0, 0 + with torch.no_grad(): + for inputs, labels in test_loader: + inputs, labels = inputs.to(device), labels.to(device) + outputs = model(inputs) + _, predicted = torch.max(outputs.data, 1) + test_total += labels.size(0) + test_correct += (predicted == labels).sum().item() + + test_acc = 100 * test_correct / test_total + avg_loss = loss_val / len(train_loader) + + history['train_loss'].append(avg_loss) + history['train_acc'].append(train_acc) + history['test_acc'].append(test_acc) + + if test_acc > best_test_acc: + best_test_acc = test_acc + torch.save(model.state_dict(), os.path.join(script_dir, "best_fcn.pth")) + + print(f"Epoch {epoch+1:02d}/{epochs:02d}: Loss={avg_loss:.4f}, Train Acc={train_acc:.2f}%, Test Acc={test_acc:.2f}%") + + print(f"\nTraining Complete! Best Test Accuracy: {best_test_acc:.2f}%") + + # Save training history plot + plt.figure(figsize=(12, 5)) + plt.subplot(1, 2, 1) + plt.plot(history['train_loss'], label='Train Loss') + plt.title('Training Loss') + plt.xlabel('Epoch') + plt.ylabel('Loss') + plt.grid(True) + plt.legend() + + plt.subplot(1, 2, 2) + plt.plot(history['train_acc'], label='Train Accuracy') + plt.plot(history['test_acc'], label='Test Accuracy') + plt.title('Accuracy History') + plt.xlabel('Epoch') + plt.ylabel('Accuracy (%)') + plt.grid(True) + plt.legend() + + plt.tight_layout() + plt.savefig(os.path.join(script_dir, "fcn_history.png")) + print(f"Saved training curves to '{os.path.join(script_dir, 'fcn_history.png')}'") + + # Load best model for final evaluation + model.load_state_dict(torch.load(os.path.join(script_dir, "best_fcn.pth"), map_location=device)) + model.eval() + + all_preds = [] + all_labels = [] + with torch.no_grad(): + for inputs, labels in test_loader: + inputs = inputs.to(device) + outputs = model(inputs) + _, predicted = torch.max(outputs.data, 1) + all_preds.extend(predicted.cpu().numpy()) + all_labels.extend(labels.numpy()) + + all_preds = np.array(all_preds) + all_labels = np.array(all_labels) + + # Per-class accuracy + print("\n=== Per-Class Accuracy Evaluation ===") + for class_name, class_idx in LABEL_MAP.items(): + indices = np.where(all_labels == class_idx)[0] + class_correct = np.sum(all_preds[indices] == class_idx) + class_acc = 100 * class_correct / len(indices) + print(f" Class '{class_name}': Acc={class_acc:.2f}% ({class_correct}/{len(indices)})") + +if __name__ == '__main__': + main() diff --git a/model/uv.lock b/model/uv.lock index 9ba729c..bef4e7f 100644 --- a/model/uv.lock +++ b/model/uv.lock @@ -1,6 +1,14 @@ version = 1 revision = 3 requires-python = ">=3.12" +resolution-markers = [ + "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 == 'emscripten'", + "python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] [[package]] name = "colorama" @@ -83,17 +91,91 @@ version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "matplotlib" }, + { name = "numpy" }, { name = "scikit-learn" }, + { name = "seaborn" }, + { name = "torch" }, + { name = "torchvision" }, { name = "tqdm" }, ] [package.metadata] requires-dist = [ { name = "matplotlib", specifier = ">=3.10.9" }, + { name = "numpy", specifier = ">=2.4.4" }, { name = "scikit-learn", specifier = ">=1.8.0" }, + { name = "seaborn", specifier = ">=0.13.2" }, + { name = "torch", specifier = ">=2.12.0" }, + { name = "torchvision", specifier = ">=0.27.0" }, { name = "tqdm", specifier = ">=4.67.3" }, ] +[[package]] +name = "cuda-bindings" +version = "13.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-pathfinder", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/e0/4b3fdba08ff177e9451f376a4ba2df18d76f9158e6a16cdc062bd83db9fa/cuda_bindings-13.3.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f72f67f5790e7fa51e3f893e007e49567573ccdf4ed1ca988fbbbd36cd77847c", size = 6020531, upload-time = "2026-05-27T03:59:07.942Z" }, + { url = "https://files.pythonhosted.org/packages/04/40/a2ea4d8f032bfd6c220d50b6f92cd61f33d48f31959da39ed1b178cfee54/cuda_bindings-13.3.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a99c3b8d584f266c616bd0f30c7cd83e33553e3ef2abad41ff5a74fbc033a69a", size = 6653764, upload-time = "2026-05-27T03:59:09.981Z" }, + { url = "https://files.pythonhosted.org/packages/ae/a0/156efe7816699c2de1ea2395031db7d010b7af23c243563a3ee6f0ecc1de/cuda_bindings-13.3.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7698fcc4577aa96372866f4d0c9a6cf686cd5c90eab94581c29d37fe6600542", size = 5914803, upload-time = "2026-05-27T03:59:14.011Z" }, + { url = "https://files.pythonhosted.org/packages/51/91/510aae64d53227b5b36db6bfaea41514b66d92cd65ddc43aa49566f18313/cuda_bindings-13.3.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:abd908f651160d12c45c5714a38ee102a1173a55433c0d1509ec0e8293beb4a6", size = 6472506, upload-time = "2026-05-27T03:59:16.551Z" }, + { url = "https://files.pythonhosted.org/packages/01/53/2ef49e5b3734a5531b2ba5d726cba724d9cbb262404e586ed61070604826/cuda_bindings-13.3.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a801fa30e75d25b74252123aefc746b6c4275624d2b8640632dd1dfeeaa1f88", size = 6008814, upload-time = "2026-05-27T03:59:20.921Z" }, + { url = "https://files.pythonhosted.org/packages/2f/cb/3a9fcf0651e0a49b4d0f1955837ce079245b27086c22fb2f253039bdf324/cuda_bindings-13.3.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94d40ef7b4bdd9dce0244a1baa132e0e538f1eb2c0d162fb3648a15e48515365", size = 6531477, upload-time = "2026-05-27T03:59:23.391Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/6987c5ee98f117317a85650ddc79480a3fa59a573ae1c923d0722b56ae71/cuda_bindings-13.3.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e5911bea15810b749a8077f8c45423ed785d51618b8e8664dea1fc8f5a2a76c8", size = 5807073, upload-time = "2026-05-27T03:59:28.218Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ab/46ceee07dc19f18a5d1c28d592750ed9dbdc803077eb083576a442c9938c/cuda_bindings-13.3.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2870fed7707a37f8af0c02364b05f355ebe8921604e8c68eb56cf66867e0798", size = 6354325, upload-time = "2026-05-27T03:59:30.715Z" }, +] + +[[package]] +name = "cuda-pathfinder" +version = "1.5.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/c8/26f2e4aae92f11522a96043892ba39a90eac610d5242523aa863212bc1c7/cuda_pathfinder-1.5.5-py3-none-any.whl", hash = "sha256:0228c023f95d1480f143ef5c8922d27a2ab052087a942e81dc289c9eb8f91689", size = 51671, upload-time = "2026-05-27T01:21:25.413Z" }, +] + +[[package]] +name = "cuda-toolkit" +version = "13.0.2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/b2/453099f5f3b698d7d0eab38916aac44c7f76229f451709e2eb9db6615dcd/cuda_toolkit-13.0.2-py2.py3-none-any.whl", hash = "sha256:b198824cf2f54003f50d64ada3a0f184b42ca0846c1c94192fa269ecd97a66eb", size = 2364, upload-time = "2025-12-19T23:24:07.328Z" }, +] + +[package.optional-dependencies] +cudart = [ + { name = "nvidia-cuda-runtime", marker = "sys_platform == 'linux'" }, +] +cufft = [ + { name = "nvidia-cufft", marker = "sys_platform == 'linux'" }, +] +cufile = [ + { name = "nvidia-cufile", marker = "sys_platform == 'linux'" }, +] +cupti = [ + { name = "nvidia-cuda-cupti", marker = "sys_platform == 'linux'" }, +] +curand = [ + { name = "nvidia-curand", marker = "sys_platform == 'linux'" }, +] +cusolver = [ + { name = "nvidia-cusolver", marker = "sys_platform == 'linux'" }, +] +cusparse = [ + { name = "nvidia-cusparse", marker = "sys_platform == 'linux'" }, +] +nvjitlink = [ + { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux'" }, +] +nvrtc = [ + { name = "nvidia-cuda-nvrtc", marker = "sys_platform == 'linux'" }, +] +nvtx = [ + { name = "nvidia-nvtx", marker = "sys_platform == 'linux'" }, +] + [[package]] name = "cycler" version = "0.12.1" @@ -103,6 +185,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, ] +[[package]] +name = "filelock" +version = "3.29.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/fe/997687a931ab51049acce6fa1f23e8f01216374ea81374ddee763c493db5/filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", size = 57571, upload-time = "2026-04-19T15:39:10.068Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" }, +] + [[package]] name = "fonttools" version = "4.62.1" @@ -144,6 +235,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fd/ba/56147c165442cc5ba7e82ecf301c9a68353cede498185869e6e02b4c264f/fonttools-4.62.1-py3-none-any.whl", hash = "sha256:7487782e2113861f4ddcc07c3436450659e3caa5e470b27dc2177cade2d8e7fd", size = 1152647, upload-time = "2026-03-13T13:54:22.735Z" }, ] +[[package]] +name = "fsspec" +version = "2026.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/8d/1c51c094345df128ca4a990d633fe1a0ff28726c9e6b3c41ba65087bba1d/fsspec-2026.4.0.tar.gz", hash = "sha256:301d8ac70ae90ef3ad05dcf94d6c3754a097f9b5fe4667d2787aa359ec7df7e4", size = 312760, upload-time = "2026-04-29T20:42:38.635Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl", hash = "sha256:11ef7bb35dab8a394fde6e608221d5cf3e8499401c249bebaeaad760a1a8dec2", size = 203402, upload-time = "2026-04-29T20:42:36.842Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + [[package]] name = "joblib" version = "1.5.3" @@ -239,6 +351,69 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b5/91/53255615acd2a1eaca307ede3c90eb550bae9c94581f8c00081b6b1c8f44/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:1f1489f769582498610e015a8ef2d36f28f505ab3096d0e16b4858a9ec214f57", size = 75987, upload-time = "2026-03-09T13:15:39.65Z" }, ] +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + [[package]] name = "matplotlib" version = "3.10.9" @@ -293,6 +468,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/23/92493c3e6e1b635ccfff146f7b99e674808787915420373ac399283764c2/matplotlib-3.10.9-cp314-cp314t-win_arm64.whl", hash = "sha256:a49f1eadc84ca85fd72fa4e89e70e61bf86452df6f971af04b12c60761a0772c", size = 8324785, upload-time = "2026-04-24T00:13:53.633Z" }, ] +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + [[package]] name = "numpy" version = "2.4.4" @@ -354,6 +547,158 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643, upload-time = "2026-03-29T13:21:34.339Z" }, ] +[[package]] +name = "nvidia-cublas" +version = "13.1.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cuda-nvrtc", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/a1/0bd24ee8c8d03adac032fd2909426a00c88f8c57961b1277ded97f91119f/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b7a210458267ac818974c53038fbec2e969d5c99f305ab15c72522fa9f001dd5", size = 542848918, upload-time = "2026-04-08T18:46:22.985Z" }, + { url = "https://files.pythonhosted.org/packages/3b/cd/154ca20c38269e05eff77c1464e6c1da89f50a6390b565e9d82e06bc11e1/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:37936a16db8fe4ac1f065c2139360608a543a09275cb1a1af612e08cfa065436", size = 423138758, upload-time = "2026-04-08T18:46:58.655Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/2a/80353b103fc20ce05ef51e928daed4b6015db4aaa9162ed0997090fe2250/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_aarch64.whl", hash = "sha256:796bd679890ee55fb14a94629b698b6db54bcfd833d391d5e94017dd9d7d3151", size = 10310827, upload-time = "2025-09-04T08:26:42.012Z" }, + { url = "https://files.pythonhosted.org/packages/33/6d/737d164b4837a9bbd202f5ae3078975f0525a55730fe871d8ed4e3b952b0/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_x86_64.whl", hash = "sha256:4eb01c08e859bf924d222250d2e8f8b8ff6d3db4721288cf35d14252a4d933c8", size = 10715597, upload-time = "2025-09-04T08:26:51.312Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc" +version = "13.0.88" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/68/483a78f5e8f31b08fb1bb671559968c0ca3a065ac7acabfc7cee55214fd6/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:ad9b6d2ead2435f11cbb6868809d2adeeee302e9bb94bcf0539c7a40d80e8575", size = 90215200, upload-time = "2025-09-04T08:28:44.204Z" }, + { url = "https://files.pythonhosted.org/packages/b7/dc/6bb80850e0b7edd6588d560758f17e0550893a1feaf436807d64d2da040f/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d27f20a0ca67a4bb34268a5e951033496c5b74870b868bacd046b1b8e0c3267b", size = 43015449, upload-time = "2025-09-04T08:28:20.239Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime" +version = "13.0.96" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/4f/17d7b9b8e285199c58ce28e31b5c5bbaa4d8271af06a89b6405258245de2/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef9bcbe90493a2b9d810e43d249adb3d02e98dd30200d86607d8d02687c43f55", size = 2261060, upload-time = "2025-10-09T08:55:15.78Z" }, + { url = "https://files.pythonhosted.org/packages/2e/24/d1558f3b68b1d26e706813b1d10aa1d785e4698c425af8db8edc3dced472/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f82250d7782aa23b6cfe765ecc7db554bd3c2870c43f3d1821f1d18aebf0548", size = 2243632, upload-time = "2025-10-09T08:55:36.117Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu13" +version = "9.20.0.48" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/c5/83384d846b2fd17c44bd499b36c75a45ed4f095fbbb2252294e89cea5c5c/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:e31454ae00094b0c55319d9d15b6fa2fc50a9e1c0f5c8c80fb75258234e731e1", size = 444574296, upload-time = "2026-03-09T19:28:27.751Z" }, + { url = "https://files.pythonhosted.org/packages/6e/5e/edb9c0ae051602c3ccaffe424256463636d639e27d7f302dde9975ef9e7a/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0c45dd8eeb50b603f07995b1b300c62ffe6a1980482b82b3bcf94a4ca9d49304", size = 366173588, upload-time = "2026-03-09T19:29:34.474Z" }, +] + +[[package]] +name = "nvidia-cufft" +version = "12.0.0.61" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2f/7b57e29836ea8714f81e9898409196f47d772d5ddedddf1592eadb8ab743/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c44f692dce8fd5ffd3e3df134b6cdb9c2f72d99cf40b62c32dde45eea9ddad3", size = 214085489, upload-time = "2025-09-04T08:31:56.044Z" }, +] + +[[package]] +name = "nvidia-cufile" +version = "1.15.1.6" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08a3ecefae5a01c7f5117351c64f17c7c62efa5fffdbe24fc7d298da19cd0b44", size = 1223672, upload-time = "2025-09-04T08:32:22.779Z" }, + { url = "https://files.pythonhosted.org/packages/ab/73/cc4a14c9813a8a0d509417cf5f4bdaba76e924d58beb9864f5a7baceefbf/nvidia_cufile-1.15.1.6-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:bdc0deedc61f548bddf7733bdc216456c2fdb101d020e1ab4b88d232d5e2f6d1", size = 1136992, upload-time = "2025-09-04T08:32:14.119Z" }, +] + +[[package]] +name = "nvidia-curand" +version = "10.4.0.35" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/72/7c2ae24fb6b63a32e6ae5d241cc65263ea18d08802aaae087d9f013335a2/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:133df5a7509c3e292aaa2b477afd0194f06ce4ea24d714d616ff36439cee349a", size = 61962106, upload-time = "2025-08-04T10:21:41.128Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9f/be0a41ca4a4917abf5cb9ae0daff1a6060cc5de950aec0396de9f3b52bc5/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:1aee33a5da6e1db083fe2b90082def8915f30f3248d5896bcec36a579d941bfc", size = 59544258, upload-time = "2025-08-04T10:22:03.992Z" }, +] + +[[package]] +name = "nvidia-cusolver" +version = "12.0.4.66" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-cusparse", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" }, + { url = "https://files.pythonhosted.org/packages/5f/67/cba3777620cdacb99102da4042883709c41c709f4b6323c10781a9c3aa34/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0a759da5dea5c0ea10fd307de75cdeb59e7ea4fcb8add0924859b944babf1112", size = 200941980, upload-time = "2025-09-04T08:33:22.767Z" }, +] + +[[package]] +name = "nvidia-cusparse" +version = "12.6.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" }, + { url = "https://files.pythonhosted.org/packages/fa/18/623c77619c31d62efd55302939756966f3ecc8d724a14dab2b75f1508850/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b3c89c88d01ee0e477cb7f82ef60a11a4bcd57b6b87c33f789350b59759360b", size = 145942937, upload-time = "2025-09-04T08:33:58.029Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu13" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/e1/cdc1797eadf82d3a9a575a19b33fdc871a97edbec42c00b5b5e914f4aff4/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:4dca476c50bf4780d46cd0bfbd82e2bc10a08e4fef7950917ce8d7578d22a23f", size = 221051344, upload-time = "2025-09-05T18:49:51.289Z" }, + { url = "https://files.pythonhosted.org/packages/34/7d/2661f2fb3ac4302f3a246f5fc030213ac60c1fe0bce84f9783dbd831dbb7/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:786ce87568c303fadb5afcc7102d454cd3040d75f6f8626f5db460d1871f4dd0", size = 170148586, upload-time = "2025-09-05T18:50:50.248Z" }, +] + +[[package]] +name = "nvidia-nccl-cu13" +version = "2.29.7" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/0d/daf50d44177ee0cbc7ff0a0c91eb5ff676c82be42f9a970bc7597f440c3a/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:674a12383e3c38a1bcccae7d4f3633b37852230b6047883cb2f4c2d1b36d9bf5", size = 206014712, upload-time = "2026-03-03T05:34:20.843Z" }, + { url = "https://files.pythonhosted.org/packages/67/f4/58e4e91b6919367c7aafb8e36fce9aad1a3047e536bf7e2fd560927d3a4c/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:edd81538446786ec3b73972543e53bb43bcaf0bfc8ef76cb679fcc390ffe136d", size = 205976000, upload-time = "2026-03-03T05:36:24.472Z" }, +] + +[[package]] +name = "nvidia-nvjitlink" +version = "13.0.88" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/7a/123e033aaff487c77107195fa5a2b8686795ca537935a24efae476c41f05/nvidia_nvjitlink-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:13a74f429e23b921c1109976abefacc69835f2f433ebd323d3946e11d804e47b", size = 40713933, upload-time = "2025-09-04T08:35:43.553Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2c/93c5250e64df4f894f1cbb397c6fd71f79813f9fd79d7cd61de3f97b3c2d/nvidia_nvjitlink-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e931536ccc7d467a98ba1d8b89ff7fa7f1fa3b13f2b0069118cd7f47bff07d0c", size = 38768748, upload-time = "2025-09-04T08:35:20.008Z" }, +] + +[[package]] +name = "nvidia-nvshmem-cu13" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/0f/05cc9c720236dcd2db9c1ab97fff629e96821be2e63103569da0c9b72f19/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dc2a197f38e5d0376ad52cd1a2a3617d3cdc150fd5966f4aee9bcebb1d68fe9", size = 60215947, upload-time = "2025-09-06T00:32:20.022Z" }, + { url = "https://files.pythonhosted.org/packages/3c/35/a9bf80a609e74e3b000fef598933235c908fcefcef9026042b8e6dfde2a9/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:290f0a2ee94c9f3687a02502f3b9299a9f9fe826e6d0287ee18482e78d495b80", size = 60412546, upload-time = "2025-09-06T00:32:41.564Z" }, +] + +[[package]] +name = "nvidia-nvtx" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/f3/d86c845465a2723ad7e1e5c36dcd75ddb82898b3f53be47ebd429fb2fa5d/nvidia_nvtx-13.0.85-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4936d1d6780fbe68db454f5e72a42ff64d1fd6397df9f363ae786930fd5c1cd4", size = 148047, upload-time = "2025-09-04T08:29:01.761Z" }, + { url = "https://files.pythonhosted.org/packages/a8/64/3708a90d1ebe202ffdeb7185f878a3c84d15c2b2c31858da2ce0583e2def/nvidia_nvtx-13.0.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb7780edb6b14107373c835bf8b72e7a178bac7367e23da7acb108f973f157a6", size = 148878, upload-time = "2025-09-04T08:28:53.627Z" }, +] + [[package]] name = "packaging" version = "26.2" @@ -363,6 +708,58 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, ] +[[package]] +name = "pandas" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "tzdata", marker = "sys_platform == 'emscripten' or 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" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/f1/392f8c5bfc16f66a0d2d41561c01627c228fe7ed2a0d056ef11315042570/pandas-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fed2ff7fd9779120e388e285fc029bd5cf9490cdd2e4166a9ee22c0e49a9ab09", size = 10357846, upload-time = "2026-05-11T18:52:36.143Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3d/b16412745651e855f357e5e66930248688378853a6e2698a214e331fba1f/pandas-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b168fc218fd80a6cbdbdbc1a97ddc7889ed057d7eb45f50d866ceab5f39904c4", size = 9899550, upload-time = "2026-05-11T18:52:38.976Z" }, + { url = "https://files.pythonhosted.org/packages/31/a8/fa2535168fffcedf67f4f6de28d2dd903a747ca7c8ea6989451aaeb3a92f/pandas-3.0.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0383c72c75cdcca61a9e116e611143902dbfd08bff356829c2f6d1cf40a9ca8c", size = 10412965, upload-time = "2026-05-11T18:52:41.915Z" }, + { url = "https://files.pythonhosted.org/packages/65/b6/09b01cdbc15224e2850365192d17b7bdebb8bdbd8780ed221fcdf0d9a515/pandas-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6dc0b3fd2169c9157deed50b4d519553a3655c8c6a96027136d654592be973a9", size = 10894600, upload-time = "2026-05-11T18:52:45.02Z" }, + { url = "https://files.pythonhosted.org/packages/c9/a4/2eb28f2fccb4ced4a2c79ab2a5dee9ade1ebf44922ebad6fea158c9f95d4/pandas-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7e65d5407dc0b394f509699650e4a2ec01c0514f21850f453fa60f3be79a5dbf", size = 11422824, upload-time = "2026-05-11T18:52:48.058Z" }, + { url = "https://files.pythonhosted.org/packages/f8/45/830bb57f533a4604b355e07edcb8ea18cf88b5f94e5fca92f27052d7c597/pandas-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8894dc474d648fe7b6ff0ca9b0bd73950d19952bc1a6534540762c5d79d305c", size = 11950889, upload-time = "2026-05-11T18:52:50.905Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c5/fc1b368f303087d20e8c9bf3d6ceb186263cfac0ade735cd938538bea839/pandas-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:c7be265b62cef88e253a941e4698604973736dcfe242fdb5198f0f7bc473cdcc", size = 9755463, upload-time = "2026-05-11T18:52:53.386Z" }, + { url = "https://files.pythonhosted.org/packages/86/bd/fda8f9705b1b09c6ebe14bfc0fa0e4ec8584d54ea673628f157ff55131af/pandas-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:557409bc4178e70ee8d9ddb494798e51ebf6ea59330f6be22c51bab2a7db6c49", size = 9066158, upload-time = "2026-05-11T18:52:56.038Z" }, + { url = "https://files.pythonhosted.org/packages/c5/90/62d8302883c44308c477e222c3daf7c813a34c8e96985882fbd53d964352/pandas-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:67b3b64c11910cfa29f4e94a14d3bff9ee693b6fc76055e7cad549cee0aec5fa", size = 10331071, upload-time = "2026-05-11T18:52:58.838Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ae/6a6493c783a101f165e4356953ba3c74d6f77f0042fa7d753da9dfbb640c/pandas-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:39436b377d56d2a2e52d0395bdbee171f01068e99af5250509aceeb929f765c7", size = 9875690, upload-time = "2026-05-11T18:53:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/62/7c/5df8e9f56c69a2769fbe9382a5ef8f2658c007e376434e1e2cbb57ad895f/pandas-3.0.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4be06d68f9ddcfc645b87534911da79a8fbffc7573c80e0edcf42a5020624d8", size = 10381634, upload-time = "2026-05-11T18:53:04.393Z" }, + { url = "https://files.pythonhosted.org/packages/99/68/1237369725aa617bb358263d535803e3053fdbc593513ec5ed9c9896b5b6/pandas-3.0.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a4eeb6830daf35a71cc09649bd823e2b542dac246cdee9614c6e4bd65028cd6a", size = 10891243, upload-time = "2026-05-11T18:53:07.643Z" }, + { url = "https://files.pythonhosted.org/packages/25/93/77d108e8af7222b4a503ebde0e30215b1c2e4f8e53a526431890f22d5586/pandas-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1928e07221f82db493cd4af1e23c1bfca524a19a4699887975bff68f49a72bfb", size = 11388659, upload-time = "2026-05-11T18:53:10.634Z" }, + { url = "https://files.pythonhosted.org/packages/d0/bd/eff5b4399f332ac386c853f6cd2bd3fa2ca0061b9f36ecd9c4d7c4265649/pandas-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51b1fe551acb77dac643c6fda86084d8d446c10fe64b06a9cc29c4cc8540e7f2", size = 11942880, upload-time = "2026-05-11T18:53:13.536Z" }, + { url = "https://files.pythonhosted.org/packages/2c/20/559ace4200982c3887d0b86bfd0d856a2143ef8ddab63cc07934951a964c/pandas-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:a82d532a3351d435432cd913edbccaf8b8e01d4dd0e5ced5a8d2e8ecd94c7e44", size = 9757091, upload-time = "2026-05-11T18:53:16.306Z" }, + { url = "https://files.pythonhosted.org/packages/3a/66/69055a09fe200f29f922a3eeec4804611900b95f52d932ece3393c3c0c19/pandas-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:275c14e0fce14a2ec20eee474aecd305478ea3c1e6f6a9d8fe219a165542717e", size = 9057282, upload-time = "2026-05-11T18:53:18.768Z" }, + { url = "https://files.pythonhosted.org/packages/57/0e/efe801b0e6811e8e650cd21b7f2608e30f08a7067e2bf6e8752b0d56ee3c/pandas-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:46997386d528eb40376ecd6b033cf4a8a1e5282580f68f43de875b78cba2199d", size = 10767016, upload-time = "2026-05-11T18:53:21.227Z" }, + { url = "https://files.pythonhosted.org/packages/ea/dc/eb55135a1d5f0f0519f28da1f609a206d2cad1f9c35c32d51e38dd7261ae/pandas-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:261e308dfb22448384b7580cf719d2f998fe2966c92893c3e77d14008af1f066", size = 10420210, upload-time = "2026-05-11T18:53:23.982Z" }, + { url = "https://files.pythonhosted.org/packages/c6/3e/b1d5d955ce33ffecb407465a60bc32769d74fcf68224b7ae67ae11d4dea4/pandas-3.0.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd1a5d1def6a46002e964510bdc67c368aa0951df5d1d9f8365336f5a1f490cd", size = 10336126, upload-time = "2026-05-11T18:53:26.731Z" }, + { url = "https://files.pythonhosted.org/packages/f5/76/a01261711ab60a22d71b862f0de20e4c504bf80457270ad8cb42110f6abc/pandas-3.0.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d72828c20c6d6e83e1e22a6a3b47b326b71664112fa9705dcbccfd7a39b62085", size = 10728051, upload-time = "2026-05-11T18:53:29.125Z" }, + { url = "https://files.pythonhosted.org/packages/e9/21/ea191195e587b18cf682e97f433f81b2d0fbe341380e80a3e0d6e4403c8e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d26cbe1fcfc12e8fd900e2454163e466b2d3af84f7c75481df7683ffc073d870", size = 11350796, upload-time = "2026-05-11T18:53:32.056Z" }, + { url = "https://files.pythonhosted.org/packages/64/69/f0eaaf54939f0e8c6768fd06be9af2cef9b36048b96dfb9e1b2c685a807e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e91cec1879ada0624fc3dc9953c5cbd60208e59c0db28f540c5d6d47502422f", size = 11799741, upload-time = "2026-05-11T18:53:34.985Z" }, + { url = "https://files.pythonhosted.org/packages/45/a4/865e0e510cae5fc2194de4db28be638952de942571ba9125934fd9c01d47/pandas-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:08d789b41f87e0905880e293cedf6197ce71fe67cc081358b1e148a491b9bd13", size = 10499958, upload-time = "2026-05-11T18:53:37.857Z" }, + { url = "https://files.pythonhosted.org/packages/86/54/effdcc3c0ff7a08037889200e148ebe94c16c4f653be078c7b3675955df1/pandas-3.0.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3650109c0f22879df8bd6179ab9ee3d7f1d1d4e7e0094a3f0032d9f51e2e64ac", size = 10336065, upload-time = "2026-05-11T18:53:41.099Z" }, + { url = "https://files.pythonhosted.org/packages/68/10/bf2d6738d72748b961a3751ab89522d58c54efc36a8e1a12161216cd45cf/pandas-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bab900348131a7db1f69a7309ef141fd5680f1487094193bcbbb61791573bf8f", size = 9926101, upload-time = "2026-05-11T18:53:43.515Z" }, + { url = "https://files.pythonhosted.org/packages/ae/e9/e35cf11c8a136e757b956f5f0efdcaa50aecde85ea055f1898dfc68262f3/pandas-3.0.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba7e08b9ac1d54569cd1e256e3668975ed624d6826f7b68df0342b012007bddb", size = 10457553, upload-time = "2026-05-11T18:53:46.394Z" }, + { url = "https://files.pythonhosted.org/packages/58/3b/1cdec6772bdbaf7b25dab360c59f03cadf05492dd724c6540af905389b07/pandas-3.0.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d71c63ae4ebdbf70209742096f1fc46a83a0613c99d4b23766cced9ff8cd62a", size = 10914065, upload-time = "2026-05-11T18:53:49.134Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c2/1ef644445fcd72e3627bceec77e3560636f87ddce4ed841afe76b83b5bf9/pandas-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e3a2ec42c98ffa2565a67e08e218d06d72576d758d90facb7c00805194d8f360", size = 11459188, upload-time = "2026-05-11T18:53:52.527Z" }, + { url = "https://files.pythonhosted.org/packages/7e/49/4d8d4f42cbc9c4adc7a1870f269c02cbd6cd40d059622c06fb298addcbad/pandas-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:335f62418ed562cfc3c49e9e196375c28b729dcef8543abf4f9438e381bf3c76", size = 11982966, upload-time = "2026-05-11T18:53:55.043Z" }, + { url = "https://files.pythonhosted.org/packages/38/55/792619469bab9882d8bbd5865d45a72f6478762d04a9af4bf0d08c503e95/pandas-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:3c20a521bbb85902f79f7270c80a59e1b5452d96d170c034f207181870f97ac5", size = 9876755, upload-time = "2026-05-11T18:53:58.067Z" }, + { url = "https://files.pythonhosted.org/packages/2a/af/33c469653b0ba03b50c3a98192d4c07f0c75c66b263ceb097fce0ee97d31/pandas-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:a2d2dff8a04f3917b55ab3910c32990f8ddf7eceba114947838cefa976a68977", size = 9198658, upload-time = "2026-05-11T18:54:00.733Z" }, + { url = "https://files.pythonhosted.org/packages/a2/fa/b8c257bd76b8bd060c3a9151c1fca05e9b9c5e3af5d0f549c0356f6d143d/pandas-3.0.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0d589105b3c14645af1738ff279b2995102d8f7a03b0a66dc8d95550eb513e04", size = 10787242, upload-time = "2026-05-11T18:54:03.564Z" }, + { url = "https://files.pythonhosted.org/packages/54/eb/f19206ffb0bf1919002969aa448b4702c6594845156a6f8050674855aac3/pandas-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:13fc1e853d9e04743d11ba75a985ccbc2a317fe07d8af61e445a6fd24dacd6a6", size = 10436369, upload-time = "2026-05-11T18:54:06.311Z" }, + { url = "https://files.pythonhosted.org/packages/fd/24/c7c39fb4fe22b71a0c2d78bf0c585c600092d85f94f086d2b3b2f6ca27e2/pandas-3.0.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:819959dab7bbd0049c15623fbac4e29a191b9528160a61fb1032242d8ced2d9c", size = 10358306, upload-time = "2026-05-11T18:54:09.085Z" }, + { url = "https://files.pythonhosted.org/packages/16/ec/dd2a9eb7fa1204df88c0864164e35b228ac581062ac612ba0a67fd812e4c/pandas-3.0.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:60ae316d3fd75d1858d450d0db0103ea2be3e7d4a95ec2f064f7e2ae63f7b028", size = 10758394, upload-time = "2026-05-11T18:54:11.956Z" }, + { url = "https://files.pythonhosted.org/packages/95/6e/00c61ea8e85b4f6d8d35e11852a1a4998fc7fafc91c6a602d1cc9c972d64/pandas-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd3a518890b400d32f9023722dc9a9a5c969f00b415419a3c06c043f09bb5d7d", size = 11375717, upload-time = "2026-05-11T18:54:14.539Z" }, + { url = "https://files.pythonhosted.org/packages/31/89/8fc1c268969fac43688d65fd92e67df24bd128d53cb4d2eee534cd307399/pandas-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c39be2d709d01fa972a0cabc522389fceca4f3969332ba25a7d6c5802cf976a", size = 11828897, upload-time = "2026-05-11T18:54:17.146Z" }, + { url = "https://files.pythonhosted.org/packages/56/3b/e7d20dea247a3e6dc0bd8a6953854afbedc03951def4e7371e05e7263e25/pandas-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4db8c527972a821cf5286b40ccc57642a39bc62e62022b42f99f8a67fca8c3a1", size = 10900855, upload-time = "2026-05-11T18:54:19.72Z" }, + { url = "https://files.pythonhosted.org/packages/0f/54/68a0978d1ef8502b8492099beaa6e7a0c1b32e3b5d4f677f5810cb08711c/pandas-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b2c95f8bfc1ee412bf482605d7bfd30c12d1d26bd59fdd91efeef1d4718decb1", size = 9466464, upload-time = "2026-05-11T18:54:22.754Z" }, +] + [[package]] name = "pillow" version = "12.2.0" @@ -558,6 +955,29 @@ 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" }, ] +[[package]] +name = "seaborn" +version = "0.13.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "matplotlib" }, + { name = "numpy" }, + { name = "pandas" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/86/59/a451d7420a77ab0b98f7affa3a1d78a313d2f7281a57afb1a34bae8ab412/seaborn-0.13.2.tar.gz", hash = "sha256:93e60a40988f4d65e9f4885df477e2fdaff6b73a9ded434c1ab356dd57eefff7", size = 1457696, upload-time = "2024-01-25T13:21:52.551Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/11/00d3c3dfc25ad54e731d91449895a79e4bf2384dc3ac01809010ba88f6d5/seaborn-0.13.2-py3-none-any.whl", hash = "sha256:636f8336facf092165e27924f223d3c62ca560b1f2bb5dff7ab7fad265361987", size = 294914, upload-time = "2024-01-25T13:21:49.598Z" }, +] + +[[package]] +name = "setuptools" +version = "81.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/1c/73e719955c59b8e424d015ab450f51c0af856ae46ea2da83eba51cc88de1/setuptools-81.0.0.tar.gz", hash = "sha256:487b53915f52501f0a79ccfd0c02c165ffe06631443a886740b91af4b7a5845a", size = 1198299, upload-time = "2026-02-06T21:10:39.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl", hash = "sha256:fdd925d5c5d9f62e4b74b30d6dd7828ce236fd6ed998a08d81de62ce5a6310d6", size = 1062021, upload-time = "2026-02-06T21:10:37.175Z" }, +] + [[package]] name = "six" version = "1.17.0" @@ -567,6 +987,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + [[package]] name = "threadpoolctl" version = "3.6.0" @@ -576,6 +1008,82 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, ] +[[package]] +name = "torch" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-bindings", marker = "sys_platform == 'linux'" }, + { name = "cuda-toolkit", extra = ["cudart", "cufft", "cufile", "cupti", "curand", "cusolver", "cusparse", "nvjitlink", "nvrtc", "nvtx"], marker = "sys_platform == 'linux'" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx" }, + { name = "nvidia-cublas", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cudnn-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvshmem-cu13", marker = "sys_platform == 'linux'" }, + { name = "setuptools" }, + { name = "sympy" }, + { name = "triton", marker = "sys_platform == 'linux'" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/bb/285d643f254731294c9b595a007eac39db4600a98682d7bca688f42ca164/torch-2.12.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b41339df93d491435e790ff8bcbae1c0ce777175889bfd1281d119862793e6a2", size = 88010197, upload-time = "2026-05-13T14:55:35.414Z" }, + { url = "https://files.pythonhosted.org/packages/79/81/76debf1db1343bd929bbb5d74c89fb437c2ed88eb144712557e7bd3eea45/torch-2.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:8fbef9f108a863e7722a73740998967e3b074742a834fc5be3a535a2befa7057", size = 426376751, upload-time = "2026-05-13T14:55:03.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/f0/80026028b603c4650ff270fc3785bdef4bd6738765a9cc5a0f5a637d65a2/torch-2.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4b4f64c2c2b11f7510d93dd6412b87025ff6eddd6bb61c3b5a3d892ea20c4756", size = 532261691, upload-time = "2026-05-13T14:52:54.453Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c2/64b06cbb7830fb3cd9be13e1158b31a3f36b68e6a209105ee3c9d9480be0/torch-2.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:8b958caff4a14d3a3b0b2dfc6a378f64dda9728a9dad28c08a0db9ce4dafb549", size = 122988114, upload-time = "2026-05-13T14:54:42.153Z" }, + { url = "https://files.pythonhosted.org/packages/86/ca/01896c80ba921676aa45886b2c5b8d774912de2a1f719de48169c6f755cd/torch-2.12.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:90dd587a5f61bfe1307148b581e2084fc5bc4a06e2b90a20e9a36b81087ff16b", size = 88009511, upload-time = "2026-05-13T14:54:47.411Z" }, + { url = "https://files.pythonhosted.org/packages/a5/04/52bdaf4787eab6ac7d7f5851dff934e4def0bc8ead9c8fd2b69b3e529699/torch-2.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:864392c73b7654f4d2b3ae712f607937d0dbb1101c4555fbb41848106b297f39", size = 426383231, upload-time = "2026-05-13T14:53:32.129Z" }, + { url = "https://files.pythonhosted.org/packages/49/8a/94bdecd13f5aaa90d45920b89789d9fe7c6f4af8c3cdd7ce01fcb59908fc/torch-2.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5d6b560dfa7d56291c07d615c3bb73e8d9943d9b6d87f76cd0d9d570c4797fa6", size = 532269288, upload-time = "2026-05-13T14:53:49.423Z" }, + { url = "https://files.pythonhosted.org/packages/3e/2f/bdbaaa267de519ef1b73054bf590d8c93c37a266c9a4e24a01bd38b6918f/torch-2.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:3fee918902090ade827643e758e98363278815de583c75d111fdd665ebffde9f", size = 122987706, upload-time = "2026-05-13T14:54:00.335Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ad/e95e822f3538171e22640a7fbe839a1fdb666600bf6487025de2ff03b11a/torch-2.12.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:10ee1448a9f304d3b987eb4656f664ba6e4d7b410ca7a5a7c642199777a2cf88", size = 88319556, upload-time = "2026-05-13T14:54:05.574Z" }, + { url = "https://files.pythonhosted.org/packages/b7/07/055d06d985b445d67422d25b033c11cf55bbb81785d4c4e68e28bca5820e/torch-2.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:af68dbf403439cae9ceaeaaf92f8352b460787dcd27b92aa05c40dd4a19c0f1e", size = 426397656, upload-time = "2026-05-13T14:52:38.84Z" }, + { url = "https://files.pythonhosted.org/packages/43/94/b0b4fdc3014122e0a7302fb90086d352aa48f2576f0b252561ebb38c01a8/torch-2.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:a6a2eebb237d3b1d9ad3b378e86d9b9e0782afdea8b1e0eba6a13646b9b49c07", size = 532183124, upload-time = "2026-05-13T14:53:16.178Z" }, + { url = "https://files.pythonhosted.org/packages/d8/c8/052405e6ad05d3237bfe5a4df78f917773956f8e17813a2d44c059068b74/torch-2.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2140e373e9a51a3e22ef62e8d14366d0b470d18f0adf19fdc757368077133a34", size = 123232462, upload-time = "2026-05-13T14:52:27.26Z" }, + { url = "https://files.pythonhosted.org/packages/67/dc/ac069f8d6e8be701535921141055293b0d4819d3d7f224a4612cf157c7f9/torch-2.12.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f7dfae4a519197dfa050e98d8e36378a0fb5899625a875c2b54445005a2e404e", size = 88027282, upload-time = "2026-05-13T14:53:05.258Z" }, + { url = "https://files.pythonhosted.org/packages/33/c3/1c1eb00e34555b536dddf792676026a988d710ed36981aa00499b36b0620/torch-2.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:891c769072637c74e9a5a77a3bc782894696d8ffec83b938df8536dee7f0ba78", size = 426386961, upload-time = "2026-05-13T14:51:28.406Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d4/7e730dba0c7032a4154dc9056b76cf9625515e030e269cfbf8098fcfee7d/torch-2.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:e2ad3eb85d39c3cab62dfa93ed5a73516e6a53c6713cb97d004004fe089f0f1f", size = 532272265, upload-time = "2026-05-13T14:51:59.308Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b4/92c80d1bbfee1c0036c06d1d2155a3065bd2423134c83bf8a47e65cd6b9b/torch-2.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:c66696857e987efb8bc1777a37357ec4f60ab5e8af6250b83d6034437fa2d8f3", size = 122987138, upload-time = "2026-05-13T14:51:45.942Z" }, + { url = "https://files.pythonhosted.org/packages/7b/78/2e12b37ce50a19a037d7bc62d652a5a8f27385a7b05859d6bc9204f20cfe/torch-2.12.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:b4556715c8572758625d62b6e0ae3b1f76c440221913a6fb5e100f321fb4fb02", size = 88320100, upload-time = "2026-05-13T14:51:39.955Z" }, + { url = "https://files.pythonhosted.org/packages/56/5e/83c450ec7b0bb40a7b74611c1b5440f9260e33c54c90d556fd4a1f0fd955/torch-2.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a43ac605a5e13116c72b64c359644cce0229f213dde48d2ae0ae5eb5becf7feb", size = 426391871, upload-time = "2026-05-13T14:52:14.989Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e9/1a0b575d98d0afedd8f157d23fa3d2759421483660448e60d0a4b10b6daa/torch-2.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6a7512adfdd7f6732e40de1c620831e3c75b39b98cef60b11d0c5f0a76473ec5", size = 532192241, upload-time = "2026-05-13T14:51:07.795Z" }, + { url = "https://files.pythonhosted.org/packages/88/21/afadd25ecd81b3cea1e11c73cf1ab41a983a50271548c3ec7ec3b9efc3e9/torch-2.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5f96b63f8287f66a005dd1b5a6abba2920f11156c5e5c4d815f3e2050fd1aa16", size = 123231092, upload-time = "2026-05-13T14:51:18.854Z" }, +] + +[[package]] +name = "torchvision" +version = "0.27.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "pillow" }, + { name = "torch" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c8/5cd91932f7f3671b0743dc4ae1a4c16b1d0b45bf4087976277d325bda718/torchvision-0.27.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:1a6dd742a150645126df9e0b2e449874c1d635897c773b322c2e067e98382dfe", size = 1758824, upload-time = "2026-05-13T14:57:15.227Z" }, + { url = "https://files.pythonhosted.org/packages/d9/36/7fb7d19477b3d93283b52fea11fa8ee30ab9064a08c97b4a6b91445e26cb/torchvision-0.27.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65772ff3ec4f4f5d680e30019835555dd239e7fefee4b0a846375fe1cb1592ef", size = 7831034, upload-time = "2026-05-13T14:57:06.483Z" }, + { url = "https://files.pythonhosted.org/packages/62/43/dfd894c3f8b01b5b33fde990f0159c1926ebc7b6e2c4193e2efb7da3c4cb/torchvision-0.27.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:7a9966a088d06b4cf6c610e03be62de469efa6f2cd2e7c7eed8e925ed6af59ac", size = 7579774, upload-time = "2026-05-13T14:56:59.337Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0c/722e989f9cf026e97ef7cb24a9bb1859e099f72d247ae35388fb89729f73/torchvision-0.27.0-cp312-cp312-win_amd64.whl", hash = "sha256:2c037709072ca9b19750c0cbe9e8bb6f91c9a1be1befa26df33e281deccbd8c7", size = 4021073, upload-time = "2026-05-13T14:57:00.848Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ae/36547812e6e047c1d80bcacd1b17a340612b08a6e876e0aabf3d0b9228b0/torchvision-0.27.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:41d6dae73e1af09fa82ded597ae57f2a2314285acde54b25890a8f8e51b999d7", size = 1758826, upload-time = "2026-05-13T14:57:05.262Z" }, + { url = "https://files.pythonhosted.org/packages/ae/30/32c4ea842738728a14e3df8c576c62dedcf5ae5cb6a5c984c6429ebe7524/torchvision-0.27.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:70f071c6f74b60d5fe8851636d8d4cd5f4fa29d57fd9348a87a6f17b990b95ba", size = 7789501, upload-time = "2026-05-13T14:56:57.786Z" }, + { url = "https://files.pythonhosted.org/packages/f6/24/4d0d48684251bd0673f87d633d5d88ab00227983b00591156eed2f86c8d5/torchvision-0.27.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:aaafa6962c9d91f42503de1957d6fa349907d028c06f335bd95da7a5bc57147d", size = 7579868, upload-time = "2026-05-13T14:56:41.618Z" }, + { url = "https://files.pythonhosted.org/packages/ba/da/e6edd051d2ba25adf23b120fa97f458dff888d098c51e84724f17d2d1470/torchvision-0.27.0-cp313-cp313-win_amd64.whl", hash = "sha256:aee384a2782c89517c4ab9061d2720ba59fd2ffe5ef89d0a149cc2d43abdf521", size = 4092700, upload-time = "2026-05-13T14:57:09.729Z" }, + { url = "https://files.pythonhosted.org/packages/fa/23/95dfa40431360f42ca949bf861434bed51164adfa8fb9801e05bf3194f50/torchvision-0.27.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:c5121f1b9ab09a7f73e837871deb8321551f7eaeb19d87aa00de9191968eae44", size = 1845008, upload-time = "2026-05-13T14:57:03.768Z" }, + { url = "https://files.pythonhosted.org/packages/23/b9/9dbdf76b2b49a75ba8088df6f7c755bdb520afb6c6dbac0102b46cde5e99/torchvision-0.27.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:1c01f0d1091ae22b9dfc082b0a0fe5faaf053686a29b4fb082ba7691375c73cf", size = 7791430, upload-time = "2026-05-13T14:56:56.206Z" }, + { url = "https://files.pythonhosted.org/packages/5c/6a/e4a16cf2f3310c2ea7760dc5d9054496844391e0f4c1fae87fefac2f3d9e/torchvision-0.27.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:dadea3c5ecfd05bbb2a3312ab0374f213c58bf6459cb059122e2f4dfe13d10ed", size = 7668441, upload-time = "2026-05-13T14:57:02.127Z" }, + { url = "https://files.pythonhosted.org/packages/00/70/01b6461117a6a94b5af3f8ee166bb0f045056f3cf187750c110dabfdfffa/torchvision-0.27.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a49e55055a39a8506fe7e59850522cab004efb2c3839f6057658889c1d69c815", size = 4141602, upload-time = "2026-05-13T14:56:53.449Z" }, + { url = "https://files.pythonhosted.org/packages/92/22/c0633677b3b3f3e69554a21ac087bf705f829c40cd5e3783507b8c006681/torchvision-0.27.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:c1fac0fc2a7adf29481fc1938a0e7845c57ba1147a986784109c4d98f434ea8c", size = 1758818, upload-time = "2026-05-13T14:56:54.988Z" }, + { url = "https://files.pythonhosted.org/packages/48/e8/55f9d9667b56dae470e69e31beac9b00d458ea393feec1aae95cc4f3f1c9/torchvision-0.27.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:cbf89764fc76f3f17fbf80c12d5a89c691e91cb9d82c38412aaf0568655ffb19", size = 7789667, upload-time = "2026-05-13T14:56:48.858Z" }, + { url = "https://files.pythonhosted.org/packages/00/bc/6f8681daf3bbc4c315bb0005110f99d28e3ecd675bf9c8f2c0d393fbac7a/torchvision-0.27.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:91f61b9865423037c327eb56afa207cc72de874e458c361840db9dcf5ce0c0eb", size = 7579848, upload-time = "2026-05-13T14:56:38.209Z" }, + { url = "https://files.pythonhosted.org/packages/19/6c/8d8020e6bd1e46c53e487c9c4e9457a07f2ee28931028fb5d71e2da40adc/torchvision-0.27.0-cp314-cp314-win_amd64.whl", hash = "sha256:5bb82fc3c55daf1788621e504310b0a286f1069627a8742f692aebb075ef25a7", size = 4119284, upload-time = "2026-05-13T14:56:46.625Z" }, + { url = "https://files.pythonhosted.org/packages/8d/7e/e78c48662a8d551606efdbe11c6b9c1d6d2391b92cd0e4591b9e6a2412b8/torchvision-0.27.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:2c4099a15150143b9b034730b404a56d572efe0b79489b4c765d929cb4eac7f3", size = 1758828, upload-time = "2026-05-13T14:56:52.293Z" }, + { url = "https://files.pythonhosted.org/packages/21/dd/d03ee9f9ee7bf11a8c7c776fb8e7fd6102f59c013791a2a4e5175bd6cba7/torchvision-0.27.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b4c6bb0a670dcba017b3643e21902c9b8a1cc1c127d602f1488fa29ec3c6e865", size = 7790618, upload-time = "2026-05-13T14:56:44.721Z" }, + { url = "https://files.pythonhosted.org/packages/39/08/4002336a74742be70728603ec1769feb2b55e0d19c532c9ec9f92008de76/torchvision-0.27.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1c2db4bde82bc48ebff73436a6adf34d4f809448268a70d9a1285f5c8f92313d", size = 7580217, upload-time = "2026-05-13T14:56:43.274Z" }, + { url = "https://files.pythonhosted.org/packages/ed/cb/4dd4783eb3565f526ba6e64b6f6ca26c00eacc924cdfe60455db9d91b84b/torchvision-0.27.0-cp314-cp314t-win_amd64.whl", hash = "sha256:72bf547e58ddb948689734eed6f4b6a2031f979dba4fb08e3690688b392e929f", size = 4226392, upload-time = "2026-05-13T14:56:40.235Z" }, +] + [[package]] name = "tqdm" version = "4.67.3" @@ -587,3 +1095,38 @@ sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd wheels = [ { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, ] + +[[package]] +name = "triton" +version = "3.7.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/13/ec05adfcd87311d532ba61e3af143e8be59fcd26675884c4682841406a20/triton-3.7.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4bf49b00a7a377a68a6da603a876e797614e6455a80e9021669c476a953ad9a", size = 188505104, upload-time = "2026-05-07T19:05:09.843Z" }, + { url = "https://files.pythonhosted.org/packages/62/7b/468a576e35beef1426e0828e28e9ba9e65f5474d496f16ee126c15646324/triton-3.7.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f111161d49bf903c0eaedde3962353a3d841c08a836839b7cc1025b8426efcf", size = 201457567, upload-time = "2026-05-07T18:46:13.505Z" }, + { url = "https://files.pythonhosted.org/packages/01/e1/a59a583de59b8f62c495d67c80ee3ea97d09e91ac80c4c6e76456ed8d8ac/triton-3.7.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:abdf6beaa89b1bcfb9a43cd990536ce66091a997841a4814b260b7bee4c88c3c", size = 188503209, upload-time = "2026-05-07T19:05:17.935Z" }, + { url = "https://files.pythonhosted.org/packages/30/b1/b7507bb9815d403927c8dd51d4158ed2e11751a92dbc118a044f247b6848/triton-3.7.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a35d7afe3f3f058e7ec49fcce09794049e0ffc5c59019ac25ec3413741b8c4e7", size = 201453566, upload-time = "2026-05-07T18:46:20.427Z" }, + { url = "https://files.pythonhosted.org/packages/a6/8f/0bea7a6a0c989315c9135a1d7fb37e41905cfb3a17cbc1f10044ebd4cc3a/triton-3.7.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc1d61c172d257db80ddf42595131fb196ad2e9bdd751e90fe2ef13531734e8b", size = 188612899, upload-time = "2026-05-07T19:05:24.955Z" }, + { url = "https://files.pythonhosted.org/packages/e1/02/d96f57828d0912aec733b9bc7e0e7dbfd2c6f079a8fa433ac25cb93d1a30/triton-3.7.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70fb9bbdc9f400afc54bbf6eb2670af28829a6ae3996863317964783141daf56", size = 201553816, upload-time = "2026-05-07T18:46:27.49Z" }, + { url = "https://files.pythonhosted.org/packages/40/fb/82a802dac4689f2a2fb2e69302e6a138eecc3e175bbe976ba3cfc717683a/triton-3.7.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a44a8476d0d3571eac4e4d1048e1ff75aad81a09ff4602ccfc56c6dea1672e", size = 188507879, upload-time = "2026-05-07T19:05:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/8f/af/9904ec6d3c93d9b24e5ec360445bbdf758b7f00bfbeedb89cb0eb64eb8bb/triton-3.7.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9b85e72968a9d8bba5ddb24e9b64aaabaf48affb042f2755cb7cfa92b7531ce", size = 201460637, upload-time = "2026-05-07T18:46:34.749Z" }, + { url = "https://files.pythonhosted.org/packages/a1/f9/4835a8ea746b88727d8899f4e3ccce4f9cacb38abfc3bb0a638266c53111/triton-3.7.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18a160de426fd99f92b0baf509045360afbd3bfaa0b4a5171dde800ec9f09684", size = 188608706, upload-time = "2026-05-07T19:05:39.218Z" }, + { url = "https://files.pythonhosted.org/packages/c1/68/fa86e5a39608000f645535b2c124920126327ab731f8c4fafd5b07ff8d4b/triton-3.7.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce061073102714b725f3660ec6939d94a1da7984b3aa99c921417cae273672f5", size = 201546766, upload-time = "2026-05-07T18:46:42.088Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "tzdata" +version = "2026.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, +]