Application Architecture¶
This guide covers the technical architecture, code organization, and development patterns of SPECTROview. It is intended for developers who want to understand, maintain, or extend the application.
MVVM Pattern¶
SPECTROview enforces a strict Model-View-ViewModel architecture.
Every workspace follows the same three-layer separation of concerns:
- Model — Pure data containers and domain logic. Models hold no references to Qt widgets and can be tested independently.
- ViewModel — Business-logic orchestrator. The ViewModel reads and mutates Models, then notifies the View through Qt signals. It never imports or references View classes.
- View — Qt widget layer. Views connect to ViewModel signals and call ViewModel methods in response to user actions.
graph LR
A["User Action"] --> B["View (v_*)"]
B -->|"method call"| C["ViewModel (vm_*)"]
C -->|"method call"| D["Model (m_*)"]
D -->|"return data"| C
C -->|"emit Signal"| B
B -->|"update UI"| A
File Naming Convention¶
| Layer | Prefix | Example |
|---|---|---|
| View | v_ |
v_workspace_spectra.py |
| ViewModel | vm_ |
vm_workspace_spectra.py |
| Model | m_ |
spectra_store.py |
Import Rules¶
| Layer | Can Import | Cannot Import |
|---|---|---|
| View | ViewModel, Components | — |
| ViewModel | Model, fit_engine |
View |
| Model | Standard libs only | View, ViewModel |
Signal/Slot Communication¶
Views and ViewModels communicate exclusively via Qt signals and slots. A ViewModel must never call View methods directly, and a View must never modify Model state without going through its ViewModel.
# ── ViewModel defines signals ──
class VMWorkspaceSpectra(QObject):
spectra_list_changed = Signal(list) # ViewModel → View
fit_progress_updated = Signal(int, int, int, float, int)
# ── View connects in __init__ ──
class VWorkspaceSpectra(QWidget):
def __init__(self):
self.vm.spectra_list_changed.connect(self._update_list)
Project Structure¶
spectroview/
├── __init__.py # Constants, peak models, resource paths
├── main.py # Entry point, QMainWindow, cross-workspace wiring
│
├── model/ # Data models (no Qt deps)
│ ├── spectra_store.py # Tensor-centric SpectraStore & MapData structures
│ ├── workspace_io.py # Unified serialization for Workspaces
│ ├── peak_model.py # Helper functions for peak parameters
│ ├── m_graph.py # Plot configuration model
│ ├── m_settings.py # Persistent app settings (QSettings wrapper)
│ ├── m_io.py # File loaders (TXT, CSV, WDF, SPC, TRPL, DAT)
│ ├── m_mva.py # PCA + NMF engine
│ ├── m_fit_model_manager.py # Saved fit model file management
│ ├── m_file_converter.py # Batch file format converter
│ ├── m_quick_calculators.py # Pure math logic for scientific calculators (Spot Size, Depth, Unit conversion)
│ ├── m_update_checker.py # Background GitHub release checker (QThread)
│ └── m_spc.py # Galactic SPC binary reader
│
├── viewmodel/ # Business logic and data orchestration
│ ├── vm_workspace_spectra.py # Spectra workspace logic (base class)
│ ├── vm_workspace_maps.py # Maps workspace (extends Spectra VM)
│ ├── vm_workspace_graphs.py # Graphs workspace logic
│ ├── vm_fit_model_builder.py # Fit model file management orchestration
│ ├── vm_mva.py # MVA orchestration
│ ├── vm_settings.py # Settings persistence
│ └── utils.py # Helpers, toast notifications
│
├── ai_agent/ # AI Data Chat module (multi-provider; runtime-optional)
│ ├── __init__.py # Module docstring, keeps import guarded
│ ├── m_llm_client.py # Multi-provider LLM clients (Ollama/OpenAI/Anthropic) + QThread workers
│ ├── m_conversation.py # Conversation model (+ m_conversation_store.py: history)
│ ├── m_prompt_manager.py # System-prompt / rules / knowledge assembly
│ ├── mcp/server.py # FastMCP server exposing the AI tools (plot_graph, query_dataframe, …)
│ ├── config/, prompts/, rules/, knowledge/, examples/, utils/ # Prompt assets + helpers (see ai_agent.md)
│ ├── vm_chat.py # Chat ViewModel (prompt-tier selection, agentic loop)
│ └── v_chat_panel.py # Floating chat dialog (+ v_history_dialog.py)
│
├── view/ # Qt widgets and UI layout
│ ├── v_workspace_spectra.py # Spectra workspace View
│ ├── v_workspace_maps.py # Maps workspace View (extends Spectra)
│ ├── v_workspace_graphs.py # Graphs workspace View
│ ├── v_quick_calculators.py # Scientific calculators GUI
│ └── components/ # Shared / reusable widgets
│ ├── v_spectra_viewer.py # Matplotlib spectra canvas
│ ├── v_fit_model_builder.py # Baseline + Peak + Fit controls
│ ├── v_peak_table.py # Interactive peak parameter table
│ ├── v_map_viewer.py # Heatmap / wafer canvas
│ ├── v_map_viewer_dialog.py # Detachable map viewer window
│ ├── v_map_list.py # Loaded maps list panel
│ ├── v_graph.py # Single graph widget (matplotlib)
│ ├── v_mva.py # PCA/NMF controls and plots
│ ├── v_fit_results.py # Fit results DataFrame table
│ ├── v_data_filter.py # Dynamic query filter panel
│ ├── v_dataframe_table.py # Generic DataFrame viewer
│ ├── v_spectra_list.py # Spectrum list with checkboxes
│ ├── v_moretab.py # Metadata / tab panel
│ ├── v_settings.py # Fit/view settings dialog
│ ├── v_menubar.py # Main menu bar
│ ├── v_about.py # About dialog
│ ├── v_update_banner.py # Update notification banner widget
│ ├── v_user_manual.py # Built-in user manual viewer
│ ├── customized_widgets.py # Palette combobox, custom toolbar
│ └── customize_graph/ # Graph customization dialog (package)
│ ├── customize_graph_dialog.py # CustomizeGraphDialog (singleton, tab host)
│ ├── customize_legend.py # Legend/Color tab
│ ├── customize_axis.py # Axis (scale/limits/breaks) tab
│ ├── customize_annotations.py # Annotations tab
│ ├── customize_more_options.py # More Options tab (plot options, theme, fonts, sorting, trendline/histogram/colormap)
│ └── customize_annotation_dialogs.py # EditLineDialog/EditTextDialog/ColorDelegate
│
├── fit_engine/ # High-performance batch fitting
│ ├── vbf_engine.py # Orchestrator (VBFengine)
│ ├── evaluator.py # Parameter mapping (VBFevaluator)
│ ├── optimizer.py # Batched Levenberg-Marquardt
│ ├── models.py # Batched peak functions + Jacobians
│ ├── scalar_models.py # Fallback scalar functions + PEAK_MODEL_REGISTRY
│ ├── vbf_thread.py # QThread wrapper
│ ├── baseline.py # Baseline algorithms (arPLS, airPLS, etc)
│ └── noise.py # Noise estimation functions
│
└── resources/ # Icons, stylesheets, user manual assets
├── icons/
├── styles/
└── user_manual/
Application Entry Point¶
spectroview/main.py creates the QMainWindow and instantiates all workspaces as tabs in a QTabWidget:
graph LR
Main["main.py"] --> Tabs["QTabWidget"]
Tabs --> S["Spectra"]
Tabs --> M["Maps"]
Tabs --> G["Graphs"]
Main --> W["setup_connections()"]
W -->|"inject ref"| M
W -->|"signal"| S
W -->|"signal"| Tabs
The setup_connections() method in main.py wires cross-workspace dependencies:
- Maps → Graphs:
VMWorkspaceMaps.set_graphs_workspace(v_graphs)injects a reference soMapscan send profiles and DataFrames directly to theGraphsworkspace. - Maps → Spectra:
main.pyconnectssend_spectra_to_workspaceto the Spectra ViewModel'sreceive_spectra(), which ingests deep copies of the selected map spectra into theSpectratab'sSpectraStore. - Fit Results → Graphs: Both
SpectraandMapsemitfit_results_updatedwith apd.DataFramethat can be forwarded to theGraphsworkspace for statistical plotting.
Keeping the startup path lean¶
Importing main.py transitively imports every module reachable from it, and that import graph dominates startup time. Two families of packages are deliberately kept off that graph and imported at their point of use instead:
| Package | Import cost | Where it is now imported |
|---|---|---|
anthropic, openai, ollama, mcp |
~4.5 s combined | Main.open_ai_chat(), _load_*() in ai_agent/m_llm_client.py, MCPHub._open_session() |
scipy.stats, scipy.interpolate, scipy.linalg |
~1.3 s combined | PlotRenderer._plot_histogram() / WaferPlot.plot(), SpectraStore.batch_preprocess(), VMMVA._build_from_all_maps(), build_heatmap_grid(), optimizer._batched_solve() |
scipy carries an extra constraint: a purely lazy import just moves the stall to the first histogram or wafer render, which made opening a saved .graphs workspace visibly slower. Main.showEvent() therefore starts a daemon thread (_prewarm_heavy_imports) that imports the three scipy submodules right after the first paint — the window is up at ~2 s and scipy is warm ~1 s later, so every function-local import scipy... is a sys.modules hit by the time a user can trigger one. The LLM SDKs are deliberately not prewarmed: most sessions never open the chat, and the panel already shows a loading state.
Verify with python -X importtime -c "import spectroview.main" before adding a new module-level import of a heavy third-party package — if it isn't needed to paint the first window, defer it. tests/performance/test_startup_imports.py fails if one of these reappears on the startup path.
Data Lifecycle¶
Spectra: Loading → Processing → Fitting → Results¶
sequenceDiagram
participant View
participant VM as ViewModel
participant Engine as FitEngine
View->>VM: load_spectra(paths)
VM-->>View: spectra_list_changed
View->>VM: subtract_baseline(), add_peak()
View->>VM: fit(apply_all)
VM->>Engine: fit_spectra()
Engine-->>VM: results
VM-->>View: fit_results_updated
Maps: Loading → Extraction → Heatmap → Profile¶
sequenceDiagram
participant View
participant VM as ViewModel
participant Viewer as MapViewer
View->>VM: load_map_files(paths)
VM-->>View: maps_list_changed
View->>VM: select_map(name)
VM-->>View: map_data_updated
View->>Viewer: plot_heatmap()
View->>VM: extract_profile()
VM-->>View: switch_to_graphs_tab
Workspace Inheritance¶
VMWorkspaceMaps extends VMWorkspaceSpectra. This means every fitting, baseline, peak, and serialization feature available in the Spectra workspace is automatically available in the Maps workspace, with additional map-specific overrides:
classDiagram
class VMWorkspaceSpectra {
+store
+fit()
+save_work()
}
class VMWorkspaceMaps {
+maps
+load_map_files()
+select_map()
}
class VMWorkspaceGraphs {
+dataframes
+graphs
+create_graph()
}
VMWorkspaceSpectra <|-- VMWorkspaceMaps
Reusable Component System¶
Workspaces are composed from shared components that follow the same signal-based pattern:
| Component | File | Purpose |
|---|---|---|
VSpectraViewer |
v_spectra_viewer.py |
Matplotlib canvas for spectrum display, zoom/pan, peak/baseline interaction |
VFitModelBuilder |
v_fit_model_builder.py |
X-correction, spectral range, baseline, peaks, fit controls |
VPeakTable |
v_peak_table.py |
Editable table of peak parameters (center, FWHM, amplitude, bounds) |
VMapViewer |
v_map_viewer.py |
Heatmap/wafer canvas with Z/X range sliders, mask, profile extraction |
VMapViewerDialog |
v_map_viewer_dialog.py |
Detachable always-on-top window wrapping VMapViewer |
VGraph |
v_graph.py |
Matplotlib graph widget supporting 10+ plot styles |
VDataFilter |
v_data_filter.py |
Dynamic pandas .query() filter builder |
VFitResults |
v_fit_results.py |
Color-coded fit results table |
VMVA |
v_mva.py |
PCA/NMF controls and embedded plotting |
CustomizeGraphDialog |
customize_graph/customize_graph_dialog.py |
Singleton dialog for graph annotation, legends, and axis customization |
Persistence & Serialization¶
SPECTROview delegates all loading and saving operations to the unified WorkspaceIO class (workspace_io.py), which isolates IO logic from the ViewModels. Each workspace uses its own save/load format via WorkspaceIO:
| Workspace | File Extension | Key Strategy |
|---|---|---|
Spectra |
.spectra |
ZIP archive (format_version: 2): metadata.json (store_meta per map) + per-map NPZ arrays. Written by the unified WorkspaceIO.save_workspace(). |
Maps |
.maps |
Same unified ZIP format as .spectra (WorkspaceIO.save_workspace()), whose metadata additionally carries maps_metadata, map_type, and the fit-results DataFrame. |
Graphs |
.graphs |
ZIP archive (format_version: 3) via WorkspaceIO.save_workspace(): metadata.json (plots + DataFrame sources) + compressed DataFrames. |
Spectrum Serialization Flow¶
# Save (format_version 2): SpectraStore → ZIP archive (metadata.json + NPZ arrays)
metadata = {
"format_version": 2,
"store_meta": { # keyed by map name (== fname for single spectra)
"sample_001": {
"fnames": ["sample_001"],
"is_active": [True],
"baseline_config": {...},
"peak_params": [...],
"range_min": None, "range_max": None,
# ...
}
}
}
# arrays: per-map NPZ blocks produced by SpectraStore.to_npz_dict(map_name)
# (raw x0 float64 axis + Y0 float32 intensities, plus any processed/fit arrays)
Threading Model¶
Long-running operations run on QThread subclasses to prevent UI freezing.
All threads emit progress signals that the ViewModel relays to the View's progress bar:
| Thread Class | Location | Purpose |
|---|---|---|
VBFthread |
fit_engine/vbf_thread.py |
Batched fitting (primary engine) |
UpdateCheckerWorker |
model/m_update_checker.py |
Background GitHub release check at startup |
LLMWorker / APIWorker / AnthropicWorker |
ai_agent/m_llm_client.py |
Streaming chat requests for AI Data Chat — one worker per backend (Ollama / OpenAI-compatible / Anthropic) |
Thread lifecycle:
- ViewModel instantiates the thread and connects
finished/progresssignals. - Thread
.start()— runsrun()on a separate OS thread. - On completion, the
finishedsignal triggers_on_fit_finished()in the ViewModel. - ViewModel emits result signals → View updates UI.
Cross-Workspace Communication¶
SPECTROview avoids a global event bus. Instead, main.py uses dependency injection and direct signal connections:
# main.py → setup_connections()
# 1. Inject reference: Maps VM can call Graphs workspace methods
self.v_maps_workspace.vm.set_graphs_workspace(self.v_graphs_workspace)
# 2. Maps → Spectra: ingest spectra sent from the Maps workspace
self.v_maps_workspace.vm.send_spectra_to_workspace.connect(
self.v_spectra_workspace.vm.receive_spectra
)
# 3. Signal: Maps requests tab switch after sending profile
self.v_maps_workspace.vm.switch_to_graphs_tab.connect(
lambda: self.tabWidget.setCurrentWidget(self.v_graphs_workspace)
)
Global Constants (__init__.py)¶
spectroview/__init__.py defines application-wide constants:
| Constant | Purpose |
|---|---|
PEAK_MODELS |
Registered peak shapes (Gaussian, Lorentzian, PseudoVoigt, Fano, ...) |
FIT_PARAMS |
Default fitting parameters (max_ite, xtol, ftol, bounds) |
PLOT_STYLES |
Available graph types (point, scatter, box, bar, line, wafer, ...) |
X_AXIS_UNIT, Y_AXIS_UNIT |
Axis label registries |
AXIS_LABELS |
Autocomplete suggestions for graph labels |
ICON_DIR |
Resolved path to resources/icons/ |
PLOT_POLICY_LIGHT, PLOT_POLICY_DARK |
Matplotlib stylesheet paths |
Update Checker¶
Overview¶
SPECTROview ships a lightweight, opt-out update notification system that queries the GitHub Releases API in the background and displays a dismissable banner when a newer version is found.
No extra dependency is required — only Python's built-in urllib.
Key Files¶
| File | Role |
|---|---|
model/m_update_checker.py |
QThread worker — performs the HTTP request and emits update_available |
view/components/v_update_banner.py |
Slim 36 px banner widget inserted at position 0 of the central layout |
model/m_settings.py |
Stores enabled, skipped_version, and last_check_date in QSettings |
main.py |
Starts the thread via QTimer.singleShot(2000, ...) from showEvent |
Flow¶
sequenceDiagram
participant Main
participant Timer as QTimer (2 s)
participant Worker as UpdateCheckerWorker
participant GitHub as api.github.com
participant Banner as VUpdateBanner
Main->>Timer: showEvent → singleShot(2000)
Timer->>Worker: _start_update_check() → worker.start()
Worker->>GitHub: GET /repos/CEA-MetroCarac/SPECTROview/releases/latest
GitHub-->>Worker: JSON {tag_name, html_url, body}
Worker->>Worker: compare versions
alt newer version found
Worker-->>Main: update_available(tag, notes, url)
Main->>Banner: insertWidget(0, VUpdateBanner(...))
end
Worker-->>Main: check_finished → set_last_check_date(today)
Design Decisions¶
| Decision | Rationale |
|---|---|
QThread instead of QNetworkAccessManager |
Pure Python urllib avoids Qt networking module complexity; thread is simpler to test |
| 2-second startup delay | Ensures the UI is fully painted before the network request starts |
| Once-per-day throttle | Avoids redundant requests; the date is persisted via QSettings |
| Silent failure | URLError, OSError, json.JSONDecodeError are all caught — offline machines see no error |
| Version comparison via tuples | _parse_version('v26.29.0') → (26, 29, 0) handles v-prefixed tags and non-numeric parts gracefully |
| Skip vs Dismiss | Skip persists the exact tag — the banner re-appears for the next release. Dismiss hides only for the session |
Adding / Modifying the Checker¶
To change the API endpoint (e.g., to query PyPI instead), edit GITHUB_API_URL in m_update_checker.py and adjust the JSON key extraction in UpdateCheckerWorker.run().
To add a "disable updates" toggle to the Settings dialog, bind MSettings.set_check_for_updates() to a QCheckBox in v_settings.py — the _start_update_check() method in main.py already reads this flag before starting the thread.
Deep-Dive Documentation¶
| Topic | Page | Summary |
|---|---|---|
| Data Architecture: SpectraStore | spectra_store.md | MapData, MapInfo, SpectrumProxy, data hierarchy, preprocessing pipeline, persistence |
| Spectra Workspace | spectra.md | VMWorkspaceSpectra, spectrum lifecycle, baseline/peak pipeline, fit model management |
| Maps Workspace | maps.md | VMWorkspaceMaps, hyperspectral data loading, heatmap rendering, coordinate handling |
| Graphs Workspace | graphs.md | VMWorkspaceGraphs, DataFrame management, plot creation, VGraph rendering |
Vectorized Batch Fit Engine (VBF Engine) |
vbf_engine.md | Batched LM optimizer, analytical Jacobians, adding new peak models |
| Multivariate Analysis | mva.md | PCA/NMF implementation, data pipeline, export to Graphs |
| AI Data Chat | ai_agent.md | Multi-provider LLM chat (Ollama/OpenAI/Anthropic/…), MCP tool calling, system prompt, Graphs workspace integration |
Running & Testing¶
# Run from source
python -m spectroview.main
# Install in editable mode
pip install -e .
# Run tests
pytest
# Build documentation
mkdocs serve
Dependencies¶
| Package | Constraint | Purpose |
|---|---|---|
PySide6 |
— | Qt 6 bindings (not PyQt) |
matplotlib |
< 3.10.9 |
Plotting backend for spectra and maps |
numpy |
< 2.0.0 |
Numerical array operations |
scipy |
— | Interpolation, KDTree, SVD |
pandas |
— | DataFrame management |
renishawWiRE |
— | Renishaw .wdf file reader |
superqt |
— | Enhanced Qt widgets (range sliders) |
ollama / openai / anthropic / mcp |
ollama ≥ 0.4, mcp ≥ 1.28 |
LLM backends + Model Context Protocol for AI Data Chat. Core dependencies (no [ai] extras group) — a plain pip install includes them; the feature is only optional to use at runtime. |
truststore |
≥ 0.9 (Python ≥ 3.10) |
Routes TLS verification through the OS trust store (corporate/internal CAs) for cloud AI providers; import is guarded |