add analysis tool
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
__pycache__
|
||||
.venv
|
||||
logs
|
||||
509
evaluation/calibration/analysis.py
Normal file
509
evaluation/calibration/analysis.py
Normal file
@@ -0,0 +1,509 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Calibration Data Analysis GUI
|
||||
|
||||
This module provides a GUI for analyzing calibration data from CSV files.
|
||||
It monitors the ./logs directory for new files and displays interactive plots.
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
import time
|
||||
import threading
|
||||
from watchdog.observers import Observer
|
||||
from watchdog.events import FileSystemEventHandler
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, messagebox
|
||||
import pandas as pd
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
||||
import seaborn as sns
|
||||
import numpy as np
|
||||
|
||||
# Set seaborn style for better plots
|
||||
sns.set_style("whitegrid")
|
||||
sns.set_palette("husl")
|
||||
plt.rcParams['figure.facecolor'] = 'white'
|
||||
sns.set_theme(font_scale=0.5)
|
||||
|
||||
|
||||
class FileWatcher(FileSystemEventHandler):
|
||||
"""Watch for changes in the logs directory"""
|
||||
|
||||
def __init__(self, callback):
|
||||
self.callback = callback
|
||||
|
||||
def on_created(self, event):
|
||||
if not event.is_directory and event.src_path.endswith('.csv'):
|
||||
# Small delay to ensure file is fully written
|
||||
threading.Timer(0.5, self.callback).start()
|
||||
|
||||
def on_modified(self, event):
|
||||
if not event.is_directory and event.src_path.endswith('.csv'):
|
||||
# Small delay to ensure file is fully written
|
||||
threading.Timer(0.5, self.callback).start()
|
||||
|
||||
|
||||
class CalibrationAnalysisGUI:
|
||||
def __init__(self, root, root_dir="./logs"):
|
||||
self.root = root
|
||||
self.root.title("Calibration Data Analysis - Filter Dev App")
|
||||
self.root.geometry("1400x900")
|
||||
|
||||
# Set minimum window size
|
||||
self.root.minsize(1000, 700)
|
||||
|
||||
# Data storage
|
||||
self.csv_files = {} # filepath -> DataFrame
|
||||
self.selected_files = set()
|
||||
|
||||
# Setup directory monitoring
|
||||
self.logs_dir = Path(root_dir)
|
||||
self.logs_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Status tracking
|
||||
self.last_file_count = 0
|
||||
|
||||
self.setup_ui()
|
||||
self.setup_file_watcher()
|
||||
self.refresh_files()
|
||||
|
||||
# Auto-refresh timer
|
||||
self.root.after(1000, self.auto_refresh)
|
||||
|
||||
def setup_ui(self):
|
||||
"""Setup the main user interface"""
|
||||
|
||||
# Main container with paned windows
|
||||
main_paned = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL)
|
||||
main_paned.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||
|
||||
# Left panel for file selection and controls
|
||||
left_frame = ttk.Frame(main_paned)
|
||||
main_paned.add(left_frame, weight=0)
|
||||
|
||||
# Right panel for plots
|
||||
right_frame = ttk.Frame(main_paned)
|
||||
main_paned.add(right_frame, weight=1)
|
||||
|
||||
self.setup_control_panel(left_frame)
|
||||
self.setup_plot_panel(right_frame)
|
||||
|
||||
def setup_control_panel(self, parent):
|
||||
"""Setup the left control panel"""
|
||||
|
||||
# Title
|
||||
title_label = ttk.Label(parent, text="Calibration Analysis", font=('Arial', 14, 'bold'))
|
||||
title_label.pack(pady=(10, 20))
|
||||
|
||||
# Directory info
|
||||
dir_frame = ttk.LabelFrame(parent, text="Monitoring Directory", padding=10)
|
||||
dir_frame.pack(fill=tk.X, padx=10, pady=5)
|
||||
|
||||
self.dir_label = ttk.Label(dir_frame, text=f"Directory: {os.path.abspath(self.logs_dir)}")
|
||||
self.dir_label.pack(anchor=tk.W)
|
||||
|
||||
self.file_count_label = ttk.Label(dir_frame, text="Files found: 0")
|
||||
self.file_count_label.pack(anchor=tk.W)
|
||||
|
||||
# Refresh button
|
||||
refresh_btn = ttk.Button(dir_frame, text="Refresh Files", command=self.refresh_files)
|
||||
refresh_btn.pack(pady=(5, 0))
|
||||
|
||||
# File selection
|
||||
file_frame = ttk.LabelFrame(parent, text="Select Files to Plot", padding=10)
|
||||
file_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
|
||||
|
||||
# Select all/none buttons
|
||||
btn_frame = ttk.Frame(file_frame)
|
||||
btn_frame.pack(fill=tk.X, pady=(0, 5))
|
||||
|
||||
ttk.Button(btn_frame, text="Select All", command=self.select_all_files).pack(side=tk.LEFT, padx=(0, 5))
|
||||
ttk.Button(btn_frame, text="Clear All", command=self.clear_all_files).pack(side=tk.LEFT)
|
||||
|
||||
# File listbox with checkboxes (using Treeview)
|
||||
list_frame = ttk.Frame(file_frame)
|
||||
list_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# Scrollable treeview
|
||||
tree_scroll = ttk.Scrollbar(list_frame)
|
||||
tree_scroll.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
|
||||
self.file_tree = ttk.Treeview(list_frame, yscrollcommand=tree_scroll.set, height=15)
|
||||
self.file_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
tree_scroll.config(command=self.file_tree.yview)
|
||||
|
||||
# Configure treeview
|
||||
self.file_tree["columns"] = ("name", "samples", "date")
|
||||
self.file_tree.column("#0", width=30, minwidth=30)
|
||||
self.file_tree.column("name", width=100, minwidth=80)
|
||||
self.file_tree.column("samples", width=80, minwidth=60)
|
||||
self.file_tree.column("date", width=100, minwidth=100)
|
||||
|
||||
self.file_tree.heading("#0", text="✓")
|
||||
self.file_tree.heading("name", text="Name")
|
||||
self.file_tree.heading("samples", text="Samples")
|
||||
self.file_tree.heading("date", text="Date")
|
||||
|
||||
self.file_tree.bind("<Button-1>", self.on_file_click)
|
||||
|
||||
# Plot controls
|
||||
plot_frame = ttk.LabelFrame(parent, text="Plot Options", padding=10)
|
||||
plot_frame.pack(fill=tk.X, padx=10, pady=5)
|
||||
|
||||
self.show_raw_var = tk.BooleanVar(value=True)
|
||||
ttk.Checkbutton(plot_frame, text="Show Raw Readings", variable=self.show_raw_var,
|
||||
command=self.update_plots).pack(anchor=tk.W)
|
||||
|
||||
self.show_stats_var = tk.BooleanVar(value=True)
|
||||
ttk.Checkbutton(plot_frame, text="Show Statistics", variable=self.show_stats_var,
|
||||
command=self.update_plots).pack(anchor=tk.W)
|
||||
|
||||
# Update plots button
|
||||
ttk.Button(plot_frame, text="Update Plots", command=self.update_plots).pack(pady=(10, 0))
|
||||
|
||||
# Status and info section
|
||||
status_frame = ttk.LabelFrame(parent, text="Status", padding=10)
|
||||
status_frame.pack(fill=tk.X, padx=10, pady=5)
|
||||
|
||||
self.status_label = ttk.Label(status_frame, text="Ready", foreground="green")
|
||||
self.status_label.pack(anchor=tk.W)
|
||||
|
||||
self.last_update_label = ttk.Label(status_frame, text="Last update: Never")
|
||||
self.last_update_label.pack(anchor=tk.W)
|
||||
|
||||
def setup_plot_panel(self, parent):
|
||||
"""Setup the right plot panel"""
|
||||
|
||||
# Create matplotlib figure with custom subplot layout using seaborn style
|
||||
# One large plot in first row, two plots in second row
|
||||
self.fig = plt.figure()
|
||||
gs = self.fig.add_gridspec(2, 2, height_ratios=[1, 1], hspace=0.5, wspace=0.5,
|
||||
left=0.15, right=0.95, top=0.95, bottom=0.15)
|
||||
|
||||
# Top row - one large plot spanning both columns
|
||||
self.ax1 = self.fig.add_subplot(gs[0, :])
|
||||
|
||||
# Bottom row - two smaller plots
|
||||
self.ax2 = self.fig.add_subplot(gs[1, 0])
|
||||
self.ax3 = self.fig.add_subplot(gs[1, 1])
|
||||
|
||||
# Apply seaborn styling to axes
|
||||
for ax in [self.ax1, self.ax2, self.ax3]:
|
||||
sns.despine(ax=ax)
|
||||
|
||||
# Adjust layout
|
||||
try:
|
||||
self.fig.tight_layout(pad=2.0)
|
||||
except Exception:
|
||||
# If tight_layout fails, use subplots_adjust with more padding
|
||||
self.fig.subplots_adjust(left=0.1, right=0.95, top=0.92, bottom=0.15, hspace=0.5, wspace=0.5)
|
||||
|
||||
# Create canvas
|
||||
self.canvas = FigureCanvasTkAgg(self.fig, parent)
|
||||
self.canvas.draw()
|
||||
self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||
|
||||
# Add toolbar
|
||||
toolbar_frame = ttk.Frame(parent)
|
||||
toolbar_frame.pack(fill=tk.X)
|
||||
|
||||
from matplotlib.backends.backend_tkagg import NavigationToolbar2Tk
|
||||
toolbar = NavigationToolbar2Tk(self.canvas, toolbar_frame)
|
||||
toolbar.update()
|
||||
|
||||
def setup_file_watcher(self):
|
||||
"""Setup file system watcher for automatic updates"""
|
||||
self.observer = Observer()
|
||||
self.observer.schedule(FileWatcher(self.refresh_files), self.logs_dir, recursive=False)
|
||||
self.observer.start()
|
||||
|
||||
def refresh_files(self):
|
||||
"""Refresh the list of CSV files in the logs directory"""
|
||||
try:
|
||||
# Find all CSV files in logs directory
|
||||
csv_files = list(self.logs_dir.glob("*.csv"))
|
||||
|
||||
# Update file count
|
||||
self.file_count_label.config(text=f"Files found: {len(csv_files)}")
|
||||
|
||||
# Clear existing items
|
||||
for item in self.file_tree.get_children():
|
||||
self.file_tree.delete(item)
|
||||
|
||||
# Load and display files
|
||||
self.csv_files.clear()
|
||||
|
||||
for file_path in sorted(csv_files):
|
||||
try:
|
||||
df = pd.read_csv(file_path)
|
||||
if not df.empty:
|
||||
name = file_path.stem.replace('_readings', '')[9:]
|
||||
self.csv_files[name] = df
|
||||
|
||||
# Extract info for display
|
||||
sample_count = len(df)
|
||||
|
||||
# Try to get date from filename or file modification time
|
||||
try:
|
||||
if 'timestamp' in df.columns and not df['timestamp'].empty:
|
||||
date_str = pd.to_datetime(df['timestamp'].iloc[0]).strftime('%Y-%m-%d %H:%M')
|
||||
else:
|
||||
mod_time = os.path.getmtime(file_path)
|
||||
date_str = time.strftime('%Y-%m-%d %H:%M', time.localtime(mod_time))
|
||||
except:
|
||||
date_str = "Unknown"
|
||||
|
||||
# Add to tree
|
||||
checkbox = "☐" # Unchecked by default
|
||||
if file_path in self.selected_files:
|
||||
checkbox = "☑"
|
||||
|
||||
self.file_tree.insert("", tk.END,
|
||||
text=checkbox,
|
||||
values=(name, sample_count, date_str),
|
||||
tags=(name,))
|
||||
except Exception as e:
|
||||
print(f"Error loading {file_path}: {e}")
|
||||
|
||||
# Update plots if files are selected
|
||||
if self.selected_files:
|
||||
self.update_plots()
|
||||
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error", f"Error refreshing files: {e}")
|
||||
|
||||
def on_file_click(self, event):
|
||||
"""Handle clicking on file list items"""
|
||||
item = self.file_tree.selection()[0] if self.file_tree.selection() else None
|
||||
if item:
|
||||
file_path = self.file_tree.item(item, "tags")[0]
|
||||
if file_path:
|
||||
# Toggle selection
|
||||
if file_path in self.selected_files:
|
||||
self.selected_files.remove(file_path)
|
||||
self.file_tree.item(item, text="☐")
|
||||
else:
|
||||
self.selected_files.add(file_path)
|
||||
self.file_tree.item(item, text="☑")
|
||||
|
||||
# Update plots
|
||||
self.update_plots()
|
||||
|
||||
def select_all_files(self):
|
||||
"""Select all files"""
|
||||
self.selected_files = set(self.csv_files.keys())
|
||||
for item in self.file_tree.get_children():
|
||||
self.file_tree.item(item, text="☑")
|
||||
self.update_plots()
|
||||
|
||||
def clear_all_files(self):
|
||||
"""Clear all file selections"""
|
||||
self.selected_files.clear()
|
||||
for item in self.file_tree.get_children():
|
||||
self.file_tree.item(item, text="☐")
|
||||
self.update_plots()
|
||||
|
||||
def update_plots(self):
|
||||
"""Update all plots with selected data"""
|
||||
# Clear all axes
|
||||
for ax in [self.ax1, self.ax2, self.ax3]:
|
||||
ax.clear()
|
||||
|
||||
if not self.selected_files:
|
||||
# Show "no files selected" message with seaborn styling
|
||||
for i, ax in enumerate([self.ax1, self.ax2, self.ax3]):
|
||||
ax.text(0.5, 0.5, 'No files selected\nUse checkboxes to select files',
|
||||
ha='center', va='center', transform=ax.transAxes,
|
||||
fontsize=12, style='italic', color='gray')
|
||||
sns.despine(ax=ax, left=True, bottom=True)
|
||||
ax.set_xticks([])
|
||||
ax.set_yticks([])
|
||||
self.canvas.draw()
|
||||
return
|
||||
|
||||
try:
|
||||
self.plot_time_series()
|
||||
self.plot_boxplot()
|
||||
self.plot_histogram()
|
||||
|
||||
# Apply seaborn styling to all axes
|
||||
for ax in [self.ax1, self.ax2, self.ax3]:
|
||||
sns.despine(ax=ax)
|
||||
|
||||
try:
|
||||
self.fig.tight_layout(pad=2.0)
|
||||
except Exception:
|
||||
# If tight_layout fails, use subplots_adjust with more padding
|
||||
self.fig.subplots_adjust(left=0.1, right=0.95, top=0.92, bottom=0.15, hspace=0.5, wspace=0.5)
|
||||
|
||||
self.canvas.draw()
|
||||
|
||||
# Update status
|
||||
self.status_label.config(text=f"Plots updated - {len(self.selected_files)} files", foreground="green")
|
||||
self.last_update_label.config(text=f"Last update: {time.strftime('%H:%M:%S')}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error updating plots: {e}")
|
||||
# Show error message on plots
|
||||
for ax in [self.ax1, self.ax2, self.ax3]:
|
||||
ax.clear()
|
||||
ax.text(0.5, 0.5, f'Plot Error:\n{str(e)}',
|
||||
ha='center', va='center', transform=ax.transAxes,
|
||||
fontsize=10, color='red')
|
||||
sns.despine(ax=ax, left=True, bottom=True)
|
||||
ax.set_xticks([])
|
||||
ax.set_yticks([])
|
||||
|
||||
self.canvas.draw()
|
||||
self.status_label.config(text="Error updating plots", foreground="red")
|
||||
messagebox.showerror("Plot Error", f"Error updating plots: {e}")
|
||||
|
||||
def plot_time_series(self):
|
||||
"""Plot time series of raw readings using seaborn"""
|
||||
self.ax1.set_title('Raw Readings Over Time')
|
||||
|
||||
# Prepare data for seaborn
|
||||
plot_data = []
|
||||
for file_path in self.selected_files:
|
||||
df = self.csv_files[file_path]
|
||||
|
||||
if 'raw_reading' in df.columns:
|
||||
temp_df = df.copy()
|
||||
temp_df['file'] = file_path
|
||||
if 'time_seconds' in df.columns:
|
||||
plot_data.append(temp_df[['time_seconds', 'raw_reading', 'file']])
|
||||
else:
|
||||
temp_df['time_seconds'] = temp_df.index
|
||||
plot_data.append(temp_df[['time_seconds', 'raw_reading', 'file']])
|
||||
|
||||
if plot_data:
|
||||
combined_data = pd.concat(plot_data, ignore_index=True)
|
||||
|
||||
# Use seaborn lineplot
|
||||
sns.lineplot(data=combined_data, x='time_seconds', y='raw_reading',
|
||||
hue='file', ax=self.ax1, alpha=0.8, linewidth=2)
|
||||
|
||||
self.ax1.set_xlabel('Time (seconds)')
|
||||
self.ax1.set_ylabel('Raw Reading')
|
||||
|
||||
# Only show legend if there are multiple files
|
||||
if len(self.selected_files) > 1:
|
||||
self.ax1.legend()
|
||||
else:
|
||||
self.ax1.text(0.5, 0.5, 'No time series data available',
|
||||
ha='center', va='center', transform=self.ax1.transAxes, fontsize=12)
|
||||
|
||||
def plot_boxplot(self):
|
||||
"""Plot boxplot of raw readings using seaborn"""
|
||||
self.ax2.set_title('Distribution of Raw Readings')
|
||||
|
||||
# Prepare data for seaborn boxplot
|
||||
plot_data = []
|
||||
for file_path in self.selected_files:
|
||||
df = self.csv_files[file_path]
|
||||
|
||||
if 'raw_reading' in df.columns:
|
||||
temp_df = pd.DataFrame({
|
||||
'raw_reading': df['raw_reading'],
|
||||
'file': file_path[:15] + '...' if len(file_path) > 15 else file_path
|
||||
})
|
||||
plot_data.append(temp_df)
|
||||
|
||||
if plot_data:
|
||||
combined_data = pd.concat(plot_data, ignore_index=True)
|
||||
|
||||
# Use seaborn boxplot
|
||||
sns.boxplot(data=combined_data, x='file', y='raw_reading', ax=self.ax2)
|
||||
|
||||
self.ax2.set_xlabel('Files')
|
||||
self.ax2.set_ylabel('Raw Reading')
|
||||
self.ax2.tick_params(axis='x', rotation=45)
|
||||
|
||||
# Add padding to prevent x-label cutoff
|
||||
self.ax2.margins(x=0.1)
|
||||
else:
|
||||
self.ax2.text(0.5, 0.5, 'No raw reading data available',
|
||||
ha='center', va='center', transform=self.ax2.transAxes, fontsize=10)
|
||||
|
||||
def plot_histogram(self):
|
||||
"""Plot histogram of raw readings using seaborn"""
|
||||
self.ax3.set_title('Distribution Density')
|
||||
|
||||
# Prepare data for seaborn histogram
|
||||
plot_data = []
|
||||
for file_path in self.selected_files:
|
||||
df = self.csv_files[file_path]
|
||||
|
||||
if 'raw_reading' in df.columns:
|
||||
temp_df = pd.DataFrame({
|
||||
'raw_reading': df['raw_reading'],
|
||||
'file': file_path[:15] + '...' if len(file_path) > 15 else file_path
|
||||
})
|
||||
plot_data.append(temp_df)
|
||||
|
||||
if plot_data:
|
||||
combined_data = pd.concat(plot_data, ignore_index=True)
|
||||
|
||||
# Use seaborn histplot with kde
|
||||
sns.histplot(data=combined_data, x='raw_reading', hue='file',
|
||||
kde=True, alpha=0.6, ax=self.ax3, stat='density')
|
||||
|
||||
self.ax3.set_xlabel('Raw Reading')
|
||||
self.ax3.set_ylabel('Density')
|
||||
|
||||
# Only add legend if there are actually multiple files
|
||||
if len(self.selected_files) > 1:
|
||||
self.ax3.legend()
|
||||
else:
|
||||
self.ax3.text(0.5, 0.5, 'No raw reading data available',
|
||||
ha='center', va='center', transform=self.ax3.transAxes, fontsize=10)
|
||||
|
||||
def auto_refresh(self):
|
||||
"""Auto refresh every 5 seconds"""
|
||||
self.refresh_files()
|
||||
self.root.after(5000, self.auto_refresh) # Refresh every 5 seconds
|
||||
|
||||
def on_closing(self):
|
||||
"""Handle application closing"""
|
||||
if hasattr(self, 'observer'):
|
||||
self.observer.stop()
|
||||
self.observer.join()
|
||||
self.root.destroy()
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point"""
|
||||
try:
|
||||
# Create and configure the main window
|
||||
root = tk.Tk()
|
||||
|
||||
# Create the application
|
||||
app = CalibrationAnalysisGUI(root)
|
||||
|
||||
# Handle window closing
|
||||
root.protocol("WM_DELETE_WINDOW", app.on_closing)
|
||||
|
||||
# Center the window
|
||||
root.update_idletasks()
|
||||
width = root.winfo_width()
|
||||
height = root.winfo_height()
|
||||
x = (root.winfo_screenwidth() // 2) - (width // 2)
|
||||
y = (root.winfo_screenheight() // 2) - (height // 2)
|
||||
root.geometry(f'{width}x{height}+{x}+{y}')
|
||||
|
||||
# Start the GUI
|
||||
print("Starting Calibration Analysis GUI...")
|
||||
print(f"Monitoring directory: {os.path.abspath('./logs')}")
|
||||
root.mainloop()
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\nShutting down...")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
messagebox.showerror("Error", f"Failed to start application: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -150,7 +150,7 @@ class CalibrationFrame(ttk.Frame):
|
||||
|
||||
# Update UI
|
||||
self.status_label.config(text=f"Calibration '{calibration_name}' complete!")
|
||||
self.calibration_factor_label.config(text=f"Calibration Factor: {calibration_factor:.2f}")
|
||||
self.calibration_factor_label.config(text=f"Calibration Factor: {calibration_factor:.6f}")
|
||||
self.std_label.config(text=f"Standard Deviation: {std_reading:.2f} (raw units)")
|
||||
self.drift_label.config(text=f"Drift: {overall_drift:.2f} units ({drift_rate:.2f} units/sec)")
|
||||
|
||||
@@ -164,7 +164,7 @@ class CalibrationFrame(ttk.Frame):
|
||||
f"Mean reading: {mean_reading:.2f} (raw units)\n"
|
||||
f"Standard deviation: {std_reading:.2f} (raw units)\n"
|
||||
f"Overall drift: {overall_drift:.2f} units ({drift_rate:.2f} units/sec)\n"
|
||||
f"Calibration factor: {calibration_factor:.2f}\n\n"
|
||||
f"Calibration factor: {calibration_factor:.6f}\n\n"
|
||||
f"The scale is now calibrated for 100g.\n\n"
|
||||
f"Data saved to:\n"
|
||||
f"• Summary: calib_logs.csv\n"
|
||||
@@ -256,7 +256,7 @@ class CalibrationFrame(ttk.Frame):
|
||||
'std_deviation': round(std_reading, 4),
|
||||
'overall_drift': round(overall_drift, 4),
|
||||
'drift_rate_per_sec': round(drift_rate, 6),
|
||||
'calibration_factor': round(calibration_factor, 4),
|
||||
'calibration_factor': round(calibration_factor, 6),
|
||||
'min_reading': round(min(calibration_readings), 4),
|
||||
'max_reading': round(max(calibration_readings), 4),
|
||||
'measurement_duration_sec': 30.0
|
||||
|
||||
@@ -12,6 +12,8 @@ dependencies = [
|
||||
"pyserial>=3.5",
|
||||
"python-toolkit",
|
||||
"tqdm>=4.67.1",
|
||||
"watchdog>=4.0.0",
|
||||
"seaborn>=0.12.0",
|
||||
]
|
||||
|
||||
[tool.uv.sources]
|
||||
|
||||
39
uv.lock
generated
39
uv.lock
generated
@@ -130,7 +130,9 @@ dependencies = [
|
||||
{ name = "pandas" },
|
||||
{ name = "pyserial" },
|
||||
{ name = "python-toolkit" },
|
||||
{ name = "seaborn" },
|
||||
{ name = "tqdm" },
|
||||
{ name = "watchdog" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
@@ -141,7 +143,9 @@ requires-dist = [
|
||||
{ name = "pandas", specifier = ">=2.3.2" },
|
||||
{ name = "pyserial", specifier = ">=3.5" },
|
||||
{ name = "python-toolkit", git = "https://git.magnuss.link/JannTer/python-toolkit" },
|
||||
{ name = "seaborn", specifier = ">=0.12.0" },
|
||||
{ name = "tqdm", specifier = ">=4.67.1" },
|
||||
{ name = "watchdog", specifier = ">=4.0.0" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -545,6 +549,20 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
|
||||
]
|
||||
|
||||
[[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 = "six"
|
||||
version = "1.17.0"
|
||||
@@ -584,6 +602,27 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "watchdog"
|
||||
version = "6.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winrt-runtime"
|
||||
version = "3.2.1"
|
||||
|
||||
Reference in New Issue
Block a user