Developer Architecture Concepts
This page captures implementation details that are important for developers working in the navigate codebase. Each section is intentionally short so teams can keep, merge, or remove entries as this architecture evolves.
Hardware Integration Deep Dives
For hardware-focused implementation guidance, use these companion pages:
Process and Inter-Process Communication
Controller-Model Process Boundary
The main GUI process owns Controller (src/navigate/controller/controller.py) and runs Tk, while Model (src/navigate/model/model.py) is created in a separate process. This split keeps hardware I/O and acquisition work away from Tk event handling, which is required for responsive GUI behavior. New developer features should preserve this boundary unless there is a strong reason to change it.
ObjectInSubprocess Remote-Call Contract
ObjectInSubprocess in src/navigate/model/concurrency/concurrency_tools.py makes subprocess method calls look local, but each call is still a pipe round-trip. It uses a non-blocking pipe lock and raises a RuntimeError when two threads try to talk to the same proxy at once, which is an intentional safety behavior. This contract is central to understanding many stop-path and concurrency edge cases.
Forced spawn Multiprocessing Start Method
The concurrency layer forces multiprocessing start method spawn in src/navigate/model/concurrency/concurrency_tools.py. That keeps behavior consistent across platforms and avoids fork-specific issues with GUI state, hardware handles, and child-process initialization. Any future multiprocessing work should assume spawn semantics.
Event Queue Contract (event_queue)
The model emits typed events to the controller through event_queue (for example warning, update_stage, frame_rate, waveform, and plugin-defined events). The controller drains this queue in its event pump and routes payloads to UI updates or registered listeners in src/navigate/controller/controller.py. When adding new events, define payload shape clearly and keep handler work lightweight.
Image Pipe Contract (show_img_pipe)
Image display uses a dedicated pipe where the model sends frame IDs and a "stop" sentinel, while the image data itself stays in shared memory. This keeps transfer overhead small and avoids queueing full arrays across processes. Additional virtual microscopes follow the same pattern with per-microscope pipes.
GUI Threading and Latency Control
Main-Thread Dispatcher and Event Pump
Controller method _run_on_main_thread plus _schedule_event_pump in src/navigate/controller/controller.py form the core Tk-thread dispatch system. Background threads enqueue callables, and the Tk loop executes them predictably at short intervals. GUI updates that bypass this pathway can produce race conditions or hard-to-reproduce Tk errors.
Tk Off-Main-Thread Guard
install_tk_thread_guard in src/navigate/tools/tk_thread_guard.py patches core Tk calls to log off-main-thread access with sampled stack traces. It is enabled for runtime safety and can be disabled in specific environments (for example, test contexts) via environment variables. This guard is a diagnostic tool, not a substitute for proper dispatch design.
Resource-Scoped Thread Scheduling in Controller
The controller uses SynchronizedThreadPool (src/navigate/controller/thread_pool.py) to serialize thread execution per named resource such as model or camera. This reduces cross-command interference and provides a predictable command queue model at the controller layer. New long-running controller actions should be assigned to the right resource to avoid contention.
sloppy_stop and Stop-Path Contention
sloppy_stop in src/navigate/controller/controller.py is explicitly marked as a temporary workaround that repeatedly tries to send stop until it wins contention. It relies on ObjectInSubprocess lock behavior rather than explicit queue preemption, which is effective but brittle. Any stop-path refactor should replace this with a first-class interruption strategy.
Live-Mode Frame Dropping to Prevent Display Lag
In live mode, the controller drains queued frame IDs and keeps only the most recent frame before rendering. This intentionally drops stale frames to bound display latency, which is usually preferable to showing every frame late. Developers should treat this as a real-time UX decision, not a data-loss bug.
Coalesced Camera Display Updates
Camera view updates use after_idle coalescing in src/navigate/controller/sub_controllers/camera_view.py, where “latest frame wins” between idle cycles. Together with max-FPS throttling, this prevents render backlog from growing unbounded under high acquisition rates. This pattern is a key reason the GUI remains responsive when cameras outpace display.
Persistent PhotoImage and In-Place Paste
The camera view keeps a persistent PhotoImage and updates it with in-place paste instead of recreating Tk image objects per frame. This minimizes Tk object churn and significantly reduces frame-display overhead. Developers changing display code should preserve this memory and object-reuse strategy.
Histogram Coalescing and Statistical Downsampling
Histogram updates in src/navigate/controller/sub_controllers/histogram.py are also coalesced with after_idle and operate on downsampled pixel subsets. The downsampling target is chosen from bin count and desired accuracy, which limits compute cost while preserving useful distribution shape. This is a deliberate latency-vs-fidelity tradeoff that should remain explicit.
Performance Telemetry and Diagnostics
The codebase defines a custom PERFORMANCE log level and uses performance_monitor decorators for hot paths like image display and histogram rendering. The diagnostics popup loads and summarizes these logs into histograms for acquisition, rendering, stage, DAQ, and serial timings. This creates an internal performance feedback loop that developers can use before profiling with external tools.
Stage Position Caching Policy for Latency
Microscope.set_stage_position_cache_policy enables cached stage positions for z-stack and customized modes in src/navigate/model/microscope.py. This avoids repeated hardware position queries in tight loops, which the code comments identify as expensive. The tradeoff is potentially stale positions between explicit refresh points, so cache policy must match acquisition semantics.
Known High-Impact Bottlenecks
Current architecture and telemetry point to a few recurring bottlenecks: image rendering, histogram generation, stage-position polling, and serial communication overhead. These areas are already instrumented and have dedicated mitigation patterns (coalescing, throttling, caching, batching). Performance changes should start by measuring these paths first instead of broad refactors.
Feature Execution Model
Feature Container as Signal/Data Trees
The feature system builds parallel signal and data trees from feature definitions in src/navigate/model/features/feature_container.py. Signal nodes coordinate hardware-side behavior, while data nodes process frame IDs from acquisition. This split lets one feature list express both device orchestration and image/data logic.
config_table Interface Contract
Each feature object exposes a config_table with signal, data, and optional node metadata. Default handlers are filled in by get_registered_funcs when entries are omitted, which keeps feature declarations concise but still structured. Developers should treat config_table as the canonical feature API surface.
Tree Control Flow: Child, Sibling, Loop, Break/Continue
Feature lists are compiled into child/sibling graphs, with tuple-based structures representing loop-like behavior and explicit handling for break/continue. Conditional branches are expressed through true and false feature entries. This is more expressive than a simple linear list and is central to advanced workflows.
Cleanup and Failure Semantics
Containers maintain cleanup lists and run cleanup handlers on shutdown or unrecoverable exceptions. Data-container behavior distinguishes recoverable one-step node errors from fatal synchronization failures, which is important for avoiding stuck signal/data states. Developers adding complex features should always provide meaningful cleanup handlers for device-safe exits.
Acquisition Mode to Feature-List Mapping
The model maps acquisition modes (for example single, live, z-stack, customized) to specific feature lists in src/navigate/model/model.py. Plugin acquisition modes can register their own feature lists and lifecycle hooks, extending this map without changing core mode logic. This mapping is the first place to inspect when behavior differs by mode.
Hardware and Device Abstractions
Microscope Composition and Device Startup
Each microscope object is assembled from configuration by Microscope in src/navigate/model/microscope.py, then connected to started devices from src/navigate/model/device_startup_functions.py. The composition layer supports shared physical devices, per-microscope state, and plugin-provided hardware categories. Understanding this assembly path is critical for any new hardware integration work.
Abstract Base Types and Interface Markers
Device and controller layers use abstract contracts and markers, including DeviceBase and device-type marker classes in src/navigate/model/devices/device_types.py, plus abstract view-controller interfaces in camera view code. Data I/O similarly uses base abstractions like DataSource and DataReader. These contracts define what new implementations must provide to stay compatible with core orchestration code.
Connection Factories and Reuse Policy
Device startup uses connection factories (serial, integrated, sequence) that cache and reuse device connections, with retry logic via auto_redial. Serial paths are wrapped with performance logging for read/write timing, helping diagnose command latency. This shared-connection strategy reduces duplicate connections and startup instability when multiple devices reference the same hardware endpoint.
Virtual Microscopes and Additional Display Pipes
The model can launch virtual microscopes with synthetic or shared components and independent shared buffers. The controller then creates per-microscope image pipes and camera-view popups to display those streams. This pattern is useful for multi-view workflows and as a template for future multiplexed displays.
Waveform Generation and Waveform UI Updates
Waveform generation combines camera, DAQ, remote-focus, and galvo timing calculations in Microscope, then posts waveform events to the controller. The waveform tab subscribes through the controller event-listener mechanism and redraws plots when these events arrive. Exposure-time adjustments in some modes also feed back into UI state through events.
ASI Model Divergence
ASIModel in src/navigate/model/model.py overrides key acquisition paths for Tiger-controller workflows, including z-stack position bookkeeping and stack-specific behavior. It preallocates larger position buffers and advances frame indices differently than the default model. Developers working on stack logic should verify both model paths before considering a change complete.
Persistence, Metadata, and Configuration
ImageWriter Save Pipeline and Disk-Safety Guards
ImageWriter in src/navigate/model/features/image_writer.py is responsible for frame saving, MIP generation, periodic disk-space checks, and acquisition stop signaling on save failures. It also ties saved frames to stage-position metadata via data_buffer_positions indexing. This class is a high-risk area because save-path bugs can silently corrupt data or metadata alignment.
Data Source and Metadata Abstraction Layer
File formats are implemented behind DataSource abstractions (TIFF/OME-TIFF, OME-Zarr, BDV-related paths), while metadata conversion is handled by metadata-source classes. This separation allows format-specific storage logic and shared acquisition metadata logic to evolve independently. New formats should follow this pattern rather than embedding file-format logic directly into acquisition threads.
Controller/Sub-Controller Boundaries and Event Bus
The main controller owns process orchestration, lifecycle control, and global routing, while sub-controllers own specific UI domains (camera view, histogram, channels, stages, menus, plugins, and more). Sub-controllers publish custom_events that are registered into the controller’s event-listener map. This keeps feature ownership local while still allowing model events to reach the right UI handlers.
Plugin Loading Architecture (Model and Controller Sides)
Plugin loading is split across model-side and controller-side managers, with support for both filesystem plugins and Python package entry points in src/navigate/plugins/plugin_manager.py. Plugins can contribute features, feature lists, acquisition modes, devices, tabs/popups, and custom event handlers. The split design keeps hardware/data extensions near the model and UI extensions near sub-controllers.
Configuration Verification and Normalization Pipeline
At startup, controller code calls verification routines in src/navigate/config/config.py to normalize and repair experiment, waveform, and configuration trees. These functions fill defaults, coerce types, prune invalid entries, and synchronize microscope-dependent structures. Treat these functions as the schema safety layer that allows older configs and partial configs to keep running.