For fine-grained debugging of label placement logic in the trajectory panel, use the DEBUG_LABELS_ASSIGNMENT flag in tools/editor/debug_config.py. This enables only the deep debug output for the label assignment block (not all label-related debug output). Use this when you want to trace label mapping and assignment logic without cluttering logs with unrelated label debug info.
How to use:
- Set
DEBUG_LABELS_ASSIGNMENT = Trueindebug_config.pyto enable only the label assignment block debug output. - Set
DEBUG_LABELS = Trueto enable all label-related debug output (including assignment and synchronization).
This allows you to focus debugging on the most critical or noisy section as needed.
To prevent silent failures from type mismatches in dictionary lookups (e.g., using float keys on int-keyed dicts), add debug assertions in critical paths:
# In controller.py, before key lookups:
assert isinstance(event_id, int), f"event_id must be int, got {type(event_id)}: {event_id}"
assert event_id in modified_prims, f"Missing event_id {event_id} in modified_prims keys: {list(modified_prims.keys())}"This provides immediate visibility into key type issues with minimal overhead (assertions disabled in production). Use DEBUG_STATE = True to enable related logging.
This document describes the overall architecture, object model, and design principles for the WhenMathPrays interactive scenario editor. It is intended to guide development, debugging, and future enhancements.
The WhenMathPrays project maintains comprehensive documentation across 60+ files organized by purpose and development phase. For a complete index of all documentation including user guides, research papers, testing procedures, and legacy specifications, see CONTENTS.md.
Quick Navigation:
- SOFTWARE_MODULES.md - Detailed module reference with I/O variables
- interactive_editor_user_guide.md - User guide for the interactive editor
- DEBUG.md - Debugging and logging configuration
- INTERACTIVE_EDITOR_TESTING.md - Testing methodology and procedures
All features and enhancements in this project follow the MVT standard:
- M = Modeled: Clean architecture, follows established patterns (MVC, observer, command), maintainable code structure
- V = Verifiable: Observable behavior with clear success criteria, user-visible feedback, deterministic outcomes
- T = Testable: Manual test checklists and/or automated tests, regression-proof, repeatable validation
MVT is the minimum bar for any code contribution. Features that are not Modeled, Verifiable, and Testable should not be merged.
See INTERACTIVE_EDITOR_TESTING.md for testing methodology that supports the MVT standard.
Coding Standards: All code contributions must follow the Coding Guidelines for maintainability, clarity, and consistency. These guidelines cover naming, state management, debugging, and best practices.
December 17, 2025: v2.2.3 Double-Click Reset Implementation:
- ✅ Double-Click Reset Functionality: Implemented complete double-click reset for primitive markers
- Added
sigPointDoubleClickedsignal toDraggableScatterItemclass for mouse event emission - Added
_on_point_double_clickedmethod toPrimitivePanelPyQtGraphfor signal handling and primitive reset requests - Fixed critical undo stack push in
controller.py_do_primitive_reset_logicmethod (ResetPrimitiveCommand was created but never executed)
- Added
- ✅ Signal Chain Completion: Established complete event flow from mouse double-click to primitive value reset
- Mouse event → signal emission → controller processing → undo command execution → model update → view refresh
- Integrated with existing debug infrastructure for comprehensive event correlation tracking
- Impact on User Experience:
- Double-clicking primitive markers now properly resets values to baseline CSV values
- Undo/redo functionality works correctly for reset operations
- Consistent with user expectations from interactive editor design
- Debugging Infrastructure: Enhanced with correlation tracking for double-click reset events
- Event correlation IDs track complete signal chains
- Comprehensive logging for mouse events, hit detection, signal emission, and command execution
- See GUI_DEBUGGING.md for debugging workflows
December 15, 2025: v2.2.2 Architecture Cleanup - A+ Grade (9.3/10):
- ✅ Dead Code Elimination: Removed 1,542 lines of unused matplotlib code
- Deleted obsolete panels: primitive_panel.py, trajectory_panel.py, draggable_point.py
- Phase 2 PyQtGraph panels are active, old matplotlib code was unused
- Eliminated global
_double_click_armedvariable (violated P2: Controller as Mediator)
- ✅ Deprecated Fields Removed: Cleaned Model of all backward compatibility cruft
- Removed Model.name (use name_m1/name_m2 per-perspective)
- Removed Model.gamma_self_0 and related fields (use gamma_self_0_m1/_m2)
- Removed Model.gamma_self_0_modified (track per-perspective)
- Removed marker_positions dicts (stored in Marker objects)
- Updated all callers to use perspective-specific fields/methods
- ✅ Central Registries Established: Single source of truth for primitives and perspectives
- Added PERSPECTIVES = ['M1', 'M2'] constant in constants.py
- Added validate_perspective(), validate_primitive(), validate_primitive_value()
- Added PRIMITIVE_MIN/MAX constants (-10.0 to 10.0)
- Enables extensibility: Add M3/M4 by updating single list instead of 50+ files
- Impact on Architecture Scores:
- Debugging: 9/10 → 10/10 (no globals, clean state)
- Understandability: 8/10 → 9/10 (no deprecated clutter)
- Visibility: 9.5/10 → 10/10 (all state explicit)
- Extensibility: 7.5/10 → 8.5/10 (central registries)
- Overall: A- (8.5/10) → A+ (9.3/10)
- Principle Applied: Clean architecture compounds - each cleanup makes the next easier
- See: Coding guidelines in docs/architecture/05_CODING_GUIDELINES.md
December 15, 2025: v2.1.4 Spinbox State Management Fixes & Planned Refactoring:
- ✅ Spinbox Bug Fixes (v2.1.4): Three critical bugs resolved in Primitive Value Editor
- Fixed time label refresh on event insertions (Ctrl+Shift+Click)
- Implemented per-perspective spinbox state tracking (M1 and M2 independent)
- Fixed time label preservation on perspective switching (M1→M2→M1)
- 🔄 Spinbox Refactoring Planned: Single controller ownership pattern to prevent future bugs
- Problem: Multiple code paths accessing spinbox widget led to race conditions and silent failures
- Root Cause: Both controller.py and interactive_editor.py directly manipulate widget
- Solution: 5-phase incremental refactoring to single controller ownership
- See: docs/architecture/spinbox_refactor_2025_12.md
- Benefits:
- Clear single ownership model (controller only)
- Race conditions impossible with one owner
- Easier to debug and test
- Prevents entire class of bugs
December 13, 2025: Entry Point Consolidation & Observability Refactor (PLANNED):
- 🔄 Phase 1-3 Planned: Consolidate dual entry points and add comprehensive observability
- Problem:
interactive_editor.py(1120 lines) andapplication.py(572 lines) implement duplicate logic - Bug Example: Event insertion undo failed because interactive_editor.py bypassed command system
- Root Cause: Two parallel code paths with different implementations (legacy vs modern)
- Solution: Migrate all logic to
application.py+ add observer pattern for full visibility - Timeline: 4 weeks (1 week per phase + cleanup)
- See: docs/architecture/entry_point_consolidation_plan.md
- Benefits:
- Single code path eliminates duplicate implementations
- Observer provides complete visibility: widget → application → controller → model
- Debugging time reduced from 30+ minutes to <5 minutes
- Future features only need one implementation
- <1% performance overhead from observability
- Problem:
December 13, 2025: v2.2.0 Marker-Centric State Architecture (Debugging-First Design):
- ✅ Marker as Single Entry Point (v2.2.0): Marker objects now own all perspective-aware state
- Why This Change: Debugging complexity with scattered state across Model/Controller/View
- Bug example: "Label persists after undo" - where's the bug?
- Had to check: Controller label commands, Model tracking dicts, View rendering, all in different files
- No single observation point to understand marker lifecycle
- User mental model: "marker didn't do what I expected" → needs single place to inspect marker state
- Problem Pattern: Scattered state = scattered debugging
modified_primitivesdict in Model → is marker modified?marker_positionsdict in Model → where's gamma_self marker?modified_labelsdict in View → is label shown?- Controller coordinates all three → debugging requires tracing through 4+ files
- Solution: Marker objects own their state, expose via accessor methods
marker.is_modified[perspective]- modification statusmarker.gamma_self_position[perspective]- position on gamma_self plotmarker.label_visible[perspective]- label should be shown- Single entry point methods:
set_is_modified(),set_gamma_position(),set_label_visible()
- Debugging Architecture: "Event tree terminus pattern"
- All state changes flow through Marker accessor methods
- One breakpoint in accessor = catch ALL changes from any code path
- Call stack reveals: who (controller method), what (action), why (user intent), when (sequence)
- Compare marker state vs view rendering to isolate bugs immediately
- View Pattern: Declarative/reactive (pull from marker state)
- Controller sets marker state:
marker.set_label_visible(perspective, True) - View reads marker state:
if marker.get_label_visible(perspective): show_label() - View can't drift out of sync - always reads fresh marker state
- Self-correcting on every
update_from_model()call
- Controller sets marker state:
- Benefits:
- Single breakpoint catches all marker state changes
- Call stack shows complete context (who, what, why, how)
- Marker inspection shows complete state (not scattered across 3+ dicts)
- View synchronization bugs immediately visible (compare marker state vs rendering)
- Extensible to arbitrary perspectives (dict keyed by perspective name)
- Event Source Tracking: Events now track origin via
event.source = 'csv' | 'inserted' - See detailed rationale in Marker State Architecture section below
- Why This Change: Debugging complexity with scattered state across Model/Controller/View
December 13, 2025: v2.1.3 Event ID-based tracking system:
- ✅ Event Identity Architecture (v2.1.3): Migrated from time-based to ID-based event tracking
-
Why This Change: Discovered cascade deletion bug during undo operations
- When undoing insertions, baseline shift logic deleted keys it just created
- Example: Shift (56.0, 'v')→(49.0, 'v') creates (49.0, 'v')=8.0
- Then shift (49.0, 'v')→(42.0, 'v') immediately deletes (49.0, 'v') that was just created!
- Root cause: Time-based keys change during insertions, requiring fragile shift operations
- Fix required collect-then-apply pattern to prevent self-destruction
-
Problem Pattern: Time-based keys = mutable identity
- Insert event → all subsequent times shift → all tracking dictionaries must shift
- Baseline, modified_primitives, marker_positions, labels all shift in parallel
- Each shift operation had cascade deletion risk
- Undo/redo complexity exponential with each new tracking dictionary added
-
Solution: Immutable event IDs assigned at creation, independent of timeline position
- Event identity never changes regardless of insertions/deletions before it
- No shifting logic required anywhere in codebase
-
Benefits:
- Zero baseline shifting during insert/undo operations
- Simplified state management (no cascade deletion bugs possible)
- Enhanced debuggability (stable event identity through history)
- Object-oriented design (event carries its own identity as attribute)
- Extensibility: New tracking dictionaries don't need shift logic
-
Implementation: Events assigned monotonically increasing IDs at creation, never reused
-
Impact: All tracking uses
(event_id, primitive)keys instead of(time, primitive) -
Further Reading: See ID-Based Event Tracking Refactor for detailed rationale and implementation notes on the ID-based event system.
-
December 12, 2024: Phase 3.6 Perspective Management Architecture Refactor (Design Phase):
- 🔄 Phase 3.6 Planned: Event-driven perspective management to resolve state synchronization bugs
- Problem: Manual coordination of perspective switches across 3+ components causes bugs
- Example Bug: Labels from M1 perspective persist in M2 despite cleanup attempts
- Root Cause: No centralized coordination, scattered state, no atomic transactions
- Solution: Qt signal-based architecture + built-in observability + perspective-aware model
- See: docs/architecture/perspective_management_refactor.md
- Goals:
- Centralized perspective state management
- Components self-manage via Qt signals (observer pattern)
- Built-in observability for debugging (toggle-able via EDITOR_DEBUG env var)
- 3rd-party extensibility (documented extension points)
- Status: Design complete, awaiting implementation
December 11, 2025: v2.1.2 baseline storage refactoring complete:
- ✅ Baseline Storage Refactoring (v2.1.2): Migrated from index-based arrays to time-keyed dictionary
- Replaced fragile
baseline_primitives['r'][6]with stablebaseline_by_time[(42.0, 'r')] - Removed complex array synchronization (12 operations → 5 dict operations per insert/delete)
- Eliminated corruption risk (baseline completely independent of model)
- All tests passing, undo/redo working correctly
- See docs/architecture/baseline_storage_refactoring.md
- Replaced fragile
December 11, 2025: v2.1.1 undo system bug fixes and baseline storage architecture identified:
- ✅ Undo System Fixes (v2.1.1): Critical bugs in undo/redo system resolved
- Fixed insertion bypass: Text field insertions now create proper undo commands
- Fixed command index instability: Commands store event time (stable) instead of index (shifts on insert/delete)
- Fixed baseline corruption: Insert/delete operations now maintain baseline arrays correctly
- Fixed label cleanup: Modified labels properly removed when values return to baseline
⚠️ Baseline Storage Architecture Issue: Identified and documented fragility in index-based storage- Status: ✅ RESOLVED in v2.1.2 - migrated to time-keyed dictionary
- See refactoring details above
December 11, 2025: Phase 3.4 state management refactoring completed, Phase 3.5 architecture refactoring in progress:
- ✅ Phase 3.4 Complete: Centralized state management with
editor_state.pymodule (see STATE_MANAGEMENT_REFACTORING.md)- Eliminated ~40 scattered state variables with explicit state enums
- Observer pattern for state change notifications
- Validated state transitions with operation guards
- 34 comprehensive tests, all passing
- Backward compatible integration into controller and commands
- 🔄 Phase 3.5 In Progress: Architecture refactoring to eliminate "god class" pattern
- ✅ Created
file_manager.py(240 lines) - Centralized M1/M2 path resolution and file management - ✅ Created
ui_builder.py(220 lines) - Extracted widget creation logic from interactive_editor.py - ⏳ Remaining: Application module, qt_window→main_window refactor, interactive_editor.py slimming
- Target: Reduce interactive_editor.py from 1094 lines to ~100 line entry point
- Benefit: Clean separation of concerns, Phase 4 readiness (multi-window architecture)
- ✅ Created
December 7, 2025: Phase 2.1 diagnostic markers completed - Known architectural debt documented:
- Mixed Event System: ✅ Being addressed in Phase 3.5 - signal-based architecture
- Coordinate System Documentation: PyQtGraph coordinate mapping differences documented in working code
- Diagnostic Handler Placement: ✅ Being addressed in Phase 3.5 - proper controller separation
- GUI Importing Core Math: ✅ Being addressed in Phase 3.5 - clean controller boundary
- Status: Phase 3.5 refactoring resolves most architectural debt identified in Phase 2.1
December 6, 2025: Architecture improvements for Phase 2 readiness:
- Primitives Module (
tools/editor/primitives.py) - Single source of truth for primitive metadata - Configuration System (
tools/editor/config.py) - User preferences with JSON config - Primitive Name Updates - Corrected UI labels: Ego→Visibility, Vulnerability→Altruism
This section provides comprehensive documentation of the core objects in the interactive editor system, their structure, purpose, and usage patterns. Each object is documented with its attributes, control patterns, debugging methods, and importance to the overall architecture.
Structure:
@dataclass
class EventPoint:
time: float # Event timestamp (step number)
v: float # Visibility primitive (-10.0 to +10.0)
r: float # Resonance primitive (-10.0 to +10.0)
f: float # Freedom primitive (-10.0 to +10.0)
a: float # Altruism primitive (-10.0 to +10.0)
S: float # Shared Breath primitive (-10.0 to +10.0)
notes: str = "" # User annotation text
marker: str = "" # Visual marker type ("circle", "star", etc.)
locked: bool = False # Prevents editing when TrueReason for Structure:
- Immutable data container using dataclass for thread-safety and validation
- Primitive values constrained to [-10, +10] range for mathematical consistency
- Single source of truth for event data across all perspectives
- CSV-compatible export format with
to_dict()method
How to View/Debug:
- State Viewer:
print(model.events_m1[0])shows full object state - CSV export:
event.to_dict()for human-readable format - Validation:
__post_init__()raises ValueError for invalid primitive ranges
When/Where/Why/How Used:
- When: Created during CSV loading, event insertion, or primitive editing
- Where: Stored in
EditorModel.events_m1andEditorModel.events_m2lists - Why: Represents single point in scenario timeline with all primitive values
- How: Accessed by index in event lists, modified through Command pattern
Who Controls It:
- Owner: EditorModel (creation, storage, validation)
- Modifier: Command objects (InsertEventCommand, EditPrimitiveCommand)
- Reader: All views and controllers for display/editing
Usage and Importance:
- Core data unit for entire scenario system
- Enables dual-perspective editing (M1/M2) with shared event identity
- Critical for trajectory computation and visualization
- Foundation for all primitive-based calculations
Attribute Details:
time: Timeline position; used for sorting and trajectory interpolationv,r,f,a,S: Primitive values; core mathematical inputs to gamma_self computationnotes: User context; preserved in CSV but not used in calculationsmarker: Visual indicator; affects display style but not computationlocked: Edit protection; prevents accidental changes to baseline events
Structure:
class Marker:
# Core attributes
time: float # Event time
value: float # Current primitive value
state: str # 'original', 'modified', 'preview'
style: dict # Visual properties (color, shape)
gamma_self_value: float # Computed gamma_self at this point
# Perspective-aware state
gamma_self_position: dict # {perspective: complex} - trajectory position
is_modified: dict # {perspective: bool} - edit status
label_visible: dict # {perspective: bool} - UI visibilityReason for Structure:
- Encapsulates marker state separate from EventPoint for UI flexibility
- Perspective-aware design supports M1/M2 independent editing
- Observable state changes enable reactive UI updates
- Single responsibility: visual representation of event state
How to View/Debug:
- State Viewer:
print(marker.__dict__)shows all attributes - Perspective accessors:
marker.get_gamma_position('M1')for per-perspective state - Debug logging: Enable
DEBUG_MARKERS = Truein debug_config.py
When/Where/Why/How Used:
- When: Created during panel initialization or event modification
- Where: Stored in panel-specific collections (trajectory panel markers)
- Why: Provides visual feedback for event state and user interactions
- How: Updated by controller on primitive changes, read by views for rendering
Who Controls It:
- Owner: TrajectoryPanelPyQtGraph (creation, positioning)
- Modifier: EditorController (state updates, position changes)
- Reader: Label managers and rendering code
Usage and Importance:
- Critical for user interaction feedback on trajectory plots
- Enables visual distinction between original/modified/preview states
- Supports label display for modified primitives only
- Foundation for drag-and-drop editing workflows
Attribute Details:
time/value: Links to EventPoint data; used for positioningstate: Controls visual appearance; 'modified' triggers label displaystyle: Dict of visual properties; enables customizationgamma_self_value: Computed result; displayed in trajectory coordinatesgamma_self_position: Per-perspective storage; prevents cross-contaminationis_modified: Edit tracking; determines label visibilitylabel_visible: UI state; managed by label synchronization logic
Structure:
class EditorModel:
# Identity
name_m1: str # Scenario name for M1 perspective
name_m2: str # Scenario name for M2 perspective
filepath: Path # Source CSV file path
time_unit: str # Time unit ("days")
# Event data
events: list # Legacy event list (deprecated)
events_m1: List[EventPoint] # M1 perspective events
events_m2: List[EventPoint] # M2 perspective events
next_event_id: int # Monotonic ID counter
# Trajectory state
gamma_self_0_m1: complex # M1 initial position
gamma_self_0_m2: complex # M2 initial position
gamma_self_0_m1_original: complex # Original M1 value from CSV
gamma_self_0_m2_original: complex # Original M2 value from CSV
# Modification tracking
modified_primitives_m1: ObservableDict # {event_id: set of primitives}
modified_primitives_m2: ObservableDict # {event_id: set of primitives}
modified_indices: set # Legacy tracking (deprecated)
# UI state
preview_changes: dict # Uncommitted drag changes {event_idx: {primitive: value}}
dirty: bool # Unsaved changes flagReason for Structure:
- Central state container following MVC pattern
- Perspective separation enables independent M1/M2 editing
- ObservableDict for modification tracking enables reactive updates
- Single source of truth prevents state synchronization bugs
- Legacy fields maintained for backward compatibility during refactoring
How to View/Debug:
- State Viewer:
model.state_viewer.log_state()for complete snapshot - Observable logging: Enable
DEBUG_OBSERVABLE = Truefor change tracking - Validation:
model.validate_state()checks internal consistency - Perspective access:
model.get_gamma_self_0('M1')for trajectory state
When/Where/Why/How Used:
- When: Created at application startup, persists throughout session
- Where: Passed to controller and views as shared state reference
- Why: Manages all scenario data and modification state
- How: Modified through Command pattern, read by all components
Who Controls It:
- Owner: Application layer (creation and lifecycle)
- Modifier: Command objects execute changes via model methods
- Reader: Controller and views for current state
Usage and Importance:
- Heart of the application state management
- Enables undo/redo through Command pattern integration
- Supports dual-perspective editing with shared event identity
- Critical for data persistence and CSV I/O
Attribute Details:
name_m1/m2: User-facing scenario identifiers; used in UI and filenamesevents: Deprecated list; maintained for compatibility during transitionevents_m1/m2: Core data lists; indexed access for event operationsnext_event_id: Ensures unique event identities across perspectivesgamma_self_0_m1/m2: Trajectory starting points; computed from initial eventsgamma_self_0_m1/m2_original: Baseline values from CSV; used for reset operationsmodified_primitives_m1/m2: Tracks edits; keys are event_ids (int), values are primitive setsmodified_indices: Legacy set; deprecated in favor of ID-based trackingpreview_changes: Temporary state; cleared on commit/canceldirty: Save state indicator; prevents data loss
Structure:
class EditorController:
# Core components
model: EditorModel # Data model reference
primitive_panel: PrimitivePanelPyQtGraph # Primitive editing view
trajectory_panel: TrajectoryPanelPyQtGraph # Trajectory display view
# Command system
undo_stack_m1: QUndoStack # M1 perspective undo/redo
undo_stack_m2: QUndoStack # M2 perspective undo/redo
undo_stack: QUndoStack # Active stack (points to current perspective)
# Perspective state
perspective: str # 'M1' or 'M2'
active_primitive_state_m1: dict # {'primitive': str, 'event_id': int, 'event_time': float}
active_primitive_state_m2: dict # {'primitive': str, 'event_id': int, 'event_time': float}
# Computation parameters
weights: dict # GRP computation weights
delta_t: float # Time step for trajectory
entropy_real_target: float # Entropy computation parameters
entropy_imag_target: float
entropy_delS_real: float
entropy_delS_imag: float
# Baseline tracking
baseline_by_id_m1: dict # {(event_id, primitive): baseline_value}
baseline_by_id_m2: dict # {(event_id, primitive): baseline_value}
baseline_comm_m1: BaselineCommunicator # M1 baseline sync
baseline_comm_m2: BaselineCommunicator # M2 baseline sync
# Mapping and state
primitive_to_gamma_self: dict # {(event_id, primitive): trajectory_mapping}
committed_gamma_trajectory: list # Last committed trajectory for comparison
debounce_timer: threading.Timer # Debounced trajectory updatesReason for Structure:
- Mediator pattern implementation controls all inter-component communication
- Separate undo stacks per perspective enable independent editing
- Active primitive state tracking supports spinbox editing workflow
- Baseline communication ensures primitive↔trajectory synchronization
- Debounced updates prevent excessive recomputation during rapid changes
How to View/Debug:
- State inspection:
controller.__dict__shows current state - Event logging: Enable
DEBUG_CONTROLLER = Truefor action tracing - Command stack:
controller.undo_stack.count()for pending operations - Active state:
controller.active_primitive_statefor current editing context
When/Where/Why/How Used:
- When: Created after model initialization, handles all user interactions
- Where: Connects model to views, processes UI events
- Why: Centralizes business logic and state transitions
- How: Receives events from views, executes commands on model
Who Controls It:
- Owner: Application layer (instantiation and wiring)
- Modifier: Self (processes events and updates state)
- Reader: Views (query current state and capabilities)
Usage and Importance:
- Prevents views from directly modifying model (maintains invariants)
- Enables complex operations like event insertion with proper undo support
- Manages perspective switching and primitive selection
- Critical for maintaining UI consistency across panels
Attribute Details:
model/primitive_panel/trajectory_panel: Core MVC references; never null after initializationundo_stack_m1/m2: Separate command histories; enables perspective-independent undoundo_stack: Active reference; switches when perspective changesactive_primitive_state_m1/m2: Current editing focus per perspective; preserves state on switchingweights/delta_t: Mathematical computation parameters; affect trajectory calculationentropy_*: Entropy field parameters; configurable via UIbaseline_by_id_m1/m2: Original values for reset operations; keyed by (event_id, primitive)baseline_comm_m1/m2: Synchronization objects; maintain primitive↔trajectory consistencyprimitive_to_gamma_self: Mapping for UI feedback; updated on trajectory recomputecommitted_gamma_trajectory: Reference trajectory; used for change detectiondebounce_timer: Performance optimization; delays expensive operations
Structure:
class PrimitivePanelPyQtGraph(QWidget):
# PyQtGraph components
graphics_widget: pg.GraphicsLayoutWidget # Container for all plots
plot_items: dict # {primitive: PlotItem} - main plot surfaces
scatter_items: dict # {primitive: DraggableScatterItem} - interactive markers
baseline_scatter_items: dict # {primitive: ScatterPlotItem} - baseline markers
line_items: dict # {primitive: PlotDataItem} - connecting lines
overlay_line_items: dict # {primitive: PlotDataItem} - inactive perspective lines
overlay_scatter_items: dict # {primitive: ScatterPlotItem} - inactive perspective markers
# Diagnostic features
diagnostic_markers: dict # {primitive: DraggableScatterItem} - debug markers
diagnostic_event_idx: int # Current diagnostic event index
diagnostic_primitive: str # Which primitive has diagnostic marker
# State management
ready: bool # Initialization complete flag
trajectory_label_manager: TrajectoryLabelManager # Label synchronization
# Legacy compatibility
on_primitive_preview: None # Deprecated callback (signal-based now)
on_primitive_reset: None # Deprecated callback (signal-based now)
# Constants
PRIMITIVE_NAMES: dict # {'v': 'Visibility', ...}
PRIMITIVE_LABELS: dict # UI display labels
PRIMITIVE_COLORS: dict # Color scheme per primitiveReason for Structure:
- View component in MVC pattern handles visualization only
- Extensive plot item management for multi-primitive display
- Diagnostic features enable debugging of primitive interactions
- Overlay system supports dual-perspective visualization
- Signal/slot pattern connects to controller actions
How to View/Debug:
- Widget inspection:
panel.plot_widget.items()shows plot contents (but it's graphics_widget) - Marker state:
panel.scatter_itemsdict shows interactive markers - Diagnostic:
panel.diagnostic_markersfor debug marker state - Signal debugging: Enable
DEBUG_PANEL = Truefor interaction logging
When/Where/Why/How Used:
- When: Created during UI initialization, updated on model changes
- Where: Embedded in main window layout
- Why: Provides visual editing interface for primitive values
- How: Renders markers, handles drag events, updates on model changes
Who Controls It:
- Owner: Main window (layout and lifecycle)
- Modifier: Controller (receives update signals)
- Reader: User (visual feedback and interaction)
Usage and Importance:
- Primary user interface for primitive editing
- Enables intuitive drag-and-drop value changes
- Provides real-time visual feedback during editing
- Critical for user workflow efficiency
Attribute Details:
graphics_widget: Top-level container; holds all plot_itemsplot_items: Per-primitive plot surfaces; one subplot per primitive (v,r,f,a,S)scatter_items: Interactive markers; handle mouse events for editingbaseline_scatter_items: Reference markers; show original valuesline_items: Connecting lines; show value progression over timeoverlay_line/scatter_items: Inactive perspective visualization; shows other perspective's datadiagnostic_markers: Debug markers; enable testing specific interactionsdiagnostic_event_idx/primitive: Current debug focus; controls diagnostic marker placementready: Initialization flag; prevents premature updatestrajectory_label_manager: Label coordination; manages marker labels across panelsPRIMITIVE_*: Constants; define primitive metadata and appearance
Structure:
class TrajectoryPanelPyQtGraph(QWidget):
# PyQtGraph components
plot_widget: pg.PlotWidget # Trajectory canvas
trajectory_line: pg.PlotCurveItem # Active perspective trajectory
overlay_line: pg.PlotCurveItem # Inactive perspective trajectory
# Label management
marker_labels: List[pg.TextItem] # Value labels on trajectory
trajectory_label_manager: TrajectoryLabelManager # Label synchronization
# Signals
panel_ready: Signal() # Initialization complete
gamma_clicked: Signal(object) # Trajectory click eventsReason for Structure:
- Specialized view for trajectory visualization
- Simple curve rendering with marker overlays
- Label management system prevents UI clutter
- Signal-based communication with controller
How to View/Debug:
- Plot inspection:
panel.plot_widget.items()shows all plot elements - Label state:
panel.marker_labelslist shows current text items - Trajectory data: Enable
DEBUG_TRAJECTORY = Truefor computation logging
When/Where/Why/How Used:
- When: Created at startup, updated on primitive changes
- Where: Main window layout alongside primitive panel
- Why: Shows computed gamma_self trajectory with modification indicators
- How: Computes trajectory from events, renders curve and markers
Who Controls It:
- Owner: Main window (layout management)
- Modifier: Controller (perspective switches, model updates)
- Reader: User (trajectory visualization and marker interaction)
Usage and Importance:
- Provides mathematical feedback for primitive changes
- Enables visual validation of editing results
- Critical for understanding scenario dynamics
- Supports marker-based navigation and editing
Attribute Details:
plot_widget: Rendering surface; contains curve, markers, labelstrajectory_line: Active perspective path; computed from current eventsoverlay_line: Inactive perspective path; shows other perspective's trajectorymarker_labels: Text overlays; show modified primitive values at event locationstrajectory_label_manager: Synchronization logic; coordinates with primitive panel labelspanel_ready/gamma_clicked: Signals; communicate initialization and user interactions
Structure:
class InsertEventCommand(QUndoCommand):
# Operation data
controller: EditorController # Target controller
insert_time: float # Time for new event
# Undo state
# (State stored implicitly through controller operations)
def redo(self): # Execute operation
def undo(self): # Reverse operationReason for Structure:
- QUndoCommand subclass enables Qt's undo framework
- Encapsulates operation data and reversal logic
- Atomic operations prevent partial state corruption
- Extensible base class for new editing operations
How to View/Debug:
- Stack inspection:
controller.undo_stack.command(index)for command details - Execution logging: Enable
DEBUG_COMMANDS = Truefor operation tracing - State validation: Commands validate pre/post conditions
When/Where/Why/How Used:
- When: Created on user actions (insert, edit, delete)
- Where: Pushed to controller's undo_stack
- Why: Enables reliable undo/redo with state integrity
- How: Executed by controller, stored for reversal
Who Controls It:
- Owner: EditorController (creation and execution)
- Modifier: Self (modifies model state)
- Reader: Undo framework (for reversal operations)
Usage and Importance:
- Prevents data loss through undo/redo capability
- Enables complex multi-step operations with rollback
- Critical for user confidence in editing workflow
- Foundation for advanced features like macro recording
Attribute Details:
controller: Modification target; contains operation logicinsert_time/event_time/primitive/old_value/new_value: Operation parameters; define what to changeredo/undo: Operation methods; implement change and reversal
Structure:
@dataclass
class EditorState:
# Core states
perspective: PerspectiveState # 'M1' or 'M2'
edit_state: EditState # IDLE, PREVIEW, COMMITTED
compute_state: TrajectoryComputeState # CURRENT, SCHEDULED, COMPUTING
undo_state: UndoRedoState # CLEAN, DIRTY, IN_OPERATION
file_load_state: FileLoadState # DUAL_PERSPECTIVE, SINGLE_M1, etc.
# Flags
initial_load_complete: bool # First load finished
dirty: bool # Unsaved changes exist
# Observers
_observers: dict # {state_name: [callbacks]}Reason for Structure:
- Centralized state container replaces scattered boolean flags
- Enum-based states with validation prevent invalid transitions
- Observer pattern enables reactive UI updates
- Single source of truth for complex state interactions
How to View/Debug:
- State inspection:
state.__dict__shows all current values - Transition logging: Enable
DEBUG_STATE = Truefor state changes - Observer debugging:
state._observersshows registered callbacks
When/Where/Why/How Used:
- When: Created at controller initialization, persists throughout session
- Where: Owned by controller, accessed by all components
- Why: Manages complex state interactions and prevents race conditions
- How: Updated through validated transition methods, observed by UI components
Who Controls It:
- Owner: EditorController (creation and primary updates)
- Modifier: Self (through transition methods with validation)
- Reader: All components (query current state and register observers)
Usage and Importance:
- Prevents invalid state combinations through enum validation
- Enables complex workflows like drag-preview-commit cycles
- Critical for trajectory computation scheduling and UI consistency
- Foundation for multi-perspective editing state management
Attribute Details:
perspective: Active editing context; affects all operationsedit_state: Current operation phase; controls UI behavior during editscompute_state: Trajectory calculation status; prevents redundant computationsundo_state: Save/dirty status; affects UI indicators and warningsfile_load_state: File loading configuration; determines perspective availabilityinitial_load_complete/dirty: Status flags; control initialization and save prompts_observers: Callback registry; enables reactive updates across components
The architecture provides comprehensive observability through State Viewer, enabling debugging of all key objects and state transitions. However, current implementation has gaps that could be addressed through refactoring.
✅ Full Coverage (37 operations tracked):
- Command Classes: All redo/undo operations (
redo_insert_event,undo_delete_event, etc.) - EditorModel: Core operations (
update_primitive,reset_primitive,save_csv,switch_perspective) - EditorController: Key orchestrations (
switch_perspective,update_primitive_to_gamma_self_mapping) - PrimitivePanelPyQtGraph: Lifecycle events (
panel_ready,update_skipped_not_ready,primitive_changed) - TrajectoryPanelPyQtGraph: Label operations via
TrajectoryLabelManager(add_label,remove_label,show_label,hide_label)
❌ Limited/No Coverage:
- EventPoint: Individual field modifications (106+ call sites)
- Marker: Internal state changes (
set_gamma_position,set_is_modified,set_label_visible) - EditorState: State transitions (
perspective,edit_state,compute_statechanges) - EditorController: Internal state updates (
active_primitive_statechanges) - EditorModel: ObservableDict changes (automatic via observer callbacks)
Effort Required: 1-2 weeks total development time
Phase 1 (Easy - 2-3 days):
- EditorState: Add State Viewer calls to existing observer callbacks
- Marker: Add State Viewer calls to existing setter methods (11 call sites)
- EditorModel ObservableDict: Add State Viewer calls to existing observer callbacks
Phase 2 (Medium - 3-4 days):
- EditorController: Add State Viewer calls to key internal state changes
- PrimitivePanelPyQtGraph: Add State Viewer calls to plot state updates
Phase 3 (Hard - 3-5 days):
- EventPoint: Add property setters with State Viewer instrumentation (106+ call sites)
Debugging Power: See every state change across all objects with timestamps and call stacks
Root Cause Analysis: Trace complex bugs through complete state transition history
Performance: Minimal overhead (~100ns per call, controllable via STATE_VIEWER env var)
Maintenance: Centralized logging infrastructure, easy to extend for new state changes
The codebase is well-designed for this refactoring:
- Observer patterns already implemented (EditorState, ObservableDict)
- Single entry points designed for Marker setters ("Single entry point for debugging")
- Existing infrastructure supports the changes with minimal disruption
See: DEBUG.md for detailed debugging methodology and State Viewer usage patterns.
- Interactive Editor Changelog - Version history and feature additions
- Interactive Editor Testing - Testing strategy and quality assurance
- Interactive Editor User Guide - Complete usage guide
- Architecture Deep Dive - Detailed component docs, refactoring timeline, bug analysis
- Architecture Principles - Core design principles
- Information Flow - Component interaction patterns
- Module Directory - Complete module/directory reference with locations
- API Contracts - Method specifications with pre/postconditions
- Coding Guidelines - Do's and don'ts for maintainable code
- Spinbox Refactoring - Single controller ownership (v2.2.0)
- State Management Refactoring - Phase 3.4 centralized state + State Viewer logging (v2.1.0)
- ID-Based Tracking - Event identity system (v2.1.3)
- DEBUG.md - Systematic debugging methodology, State Viewer usage, common issues
- Observability Guide - Toggle-able logging for debugging
- Baseline Communication Protocol - Primitive↔gamma_self protocol spec
Debugging and maintainability are core to this architecture. The following principles are enforced throughout the codebase:
- Architecture First:
- All communication flows are explicit and documented.
- States are named, visible, and centralized; control operations are clearly defined and scoped.
- Visibility and Control:
- No silent controls: all control functions are purposefully visible and their scope, timing, and rationale are documented.
- Debugging is built into the architecture, not bolted on.
- State Richness:
- States carry enough information to trace their origin and intended use, supporting root-cause analysis.
- Code Cleanliness:
- Clean code and comments are enforced; documentation and cleanup are part of every change.
- Debug Built-In:
- Debug logging and State Viewer integration are standard; print/logging at the function level is used for isolating issues.
- Iterative Improvement:
- Fix root causes, document as you go, and refactor without fear to maintain long-term code health.
Canonical Reference:
- For systematic debugging methodology, logging patterns, and State Viewer usage, see DEBUG.md.
- For architectural context and three-layer debugging, see the "Marker State Architecture" and "Spinbox Primitive Editor" sections above.
This section is the single source of truth for debugability principles. All contributors should review and follow these guidelines, and refer to DEBUG.md for detailed methodology and examples.
The GRP framework distinguishes two orthogonal dimensions:
Real Axis (Ego ↔ We): Identity Boundary
- Ego-space (negative): Separate, distinct identities—M1 experiences self as "I" (distinct from M2)
- We-space (positive): Merged, shared identity—M1 experiences self as "We", self-concept includes M2
- Primitive: Visibility (v) affects identity boundary through presence/absence
Imaginary Axis (Hate ↔ Love): Affective Quality
- Hate (negative): Negative emotional states—resentment, bitterness, discord
- Love (positive): Positive emotional states—warmth, care, resonance
- Primitives: Resonance (r), Fidelity (f), Altruism (a) affect emotional experience
Identity Statement Test for primitive classification:
- Imaginary axis primitives (r, f, a): Use action/feeling language
- "I helped them" (action, identity remains distinct)
- "I care about them" (feeling, identity remains distinct)
- "I resonate with them" (emotional experience)
- Real axis primitives (v): Support identity language
- "I am married to them" (defines WHO M1 is)
- "We are partners" (shared identity statement)
- "We are buying a house" (joint identity action)
Key distinction: Imaginary effects describe what M1 DOES or FEELS—identity remains distinct. Real effects define WHO M1 IS—self-concept incorporates M2.
Examples validating orthogonality:
- Fan relationship: High r/a (Love) + Ego-space (separate identity)
- Toxic enmeshment: Low r/a (Hate) + We-space (merged identity)
- Marriage: Variable affect (Love or Hate) + We-space ("I am married to them")
For detailed empirical foundation, see gamma_self_defense.md.
Problem: Complex controller orchestrates Model ↔ View synchronization across multiple state dimensions:
- Modification tracking (hollow vs filled markers)
- Gamma_self marker positions (trajectory plot)
- Label visibility (primitive panel)
- Perspective switching (M1 vs M2)
Traditional MVC scatter this state across Model dictionaries, Controller logic, and View rendering, making debugging difficult.
Solution: Marker as event tree terminus - single observable point where all state changes converge.
User Action (drag, undo, insert, etc.)
↓
Controller logic (complex, multiple paths)
↓
Marker.set_is_modified(perspective, True) ← SINGLE BREAKPOINT
Marker.set_gamma_position(perspective, pos) ← SINGLE BREAKPOINT
Marker.set_label_visible(perspective, True) ← SINGLE BREAKPOINT
↓
View.update_from_model()
↓
View reads marker state (pull pattern)
if marker.get_label_visible(perspective):
show_label()
↓
Rendering synchronized to marker state
Three-Layer Checkpoint System:
# Layer 1: Model/Marker state (should it be modified?)
marker.is_modified['M1'] # True
# Layer 2: Controller intention (did controller set visibility?)
marker.label_visible['M1'] # True
# Layer 3: View rendering (did view actually render it?)
(event.time, 'v') in view.modified_labels_m1 # Check actual TextItemBug Isolation:
- If layers 1-2 match but layer 3 differs → View refresh bug
- If layer 1 differs from layer 2 → Controller logic bug
- If all layers match but still wrong → Baseline/computation bug
Call Stack Analysis:
Breakpoint in Marker.set_label_visible() reveals:
- Who: Which controller method (
_apply_primitive_change,_insert_event, etc.) - What: User action (drag release, undo, perspective switch)
- Why: Intent (modification, baseline reset, cleanup)
- When: Sequence in operation timeline
State stored in dicts keyed by perspective name:
marker.is_modified = {'M1': True, 'M2': False}
marker.gamma_self_position = {'M1': 3+2j, 'M2': None}
marker.label_visible = {'M1': True, 'M2': False}Adding M3 perspective requires no Marker changes - automatically supported.
Old (imperative - bug-prone):
# Controller directly manipulates view (4+ call sites)
self.view._add_marker_label(time, prim, value)
# Can forget to call, call twice, call at wrong timeNew (declarative - self-correcting):
# Controller sets marker state
marker.set_label_visible(perspective, True)
# View reads marker state on every refresh
def _sync_labels_from_markers(self, events):
for event in events:
for prim in ['v', 'r', 'f', 'a', 'S']:
if event.markers[prim].get_label_visible(perspective):
self._ensure_label_shown(event.time, prim)View can't drift out of sync - always reads fresh marker state.
- Single breakpoint debugging: Catch ALL state changes from any code path
- Complete context: Call stack shows who/what/why/when
- Isolated bug detection: Three-layer checkpoint system pinpoints exact failure layer
Problem: Current gauge-based editing is imprecise (hard to set exact values like 7.2 by dragging). Multiple gauges increase UI complexity and don't support keyboard-driven workflows.
Solution: Single shared spinbox widget for precise numeric input. Controller tracks "active primitive" state (which primitive + which event is being edited). Spinbox displays/edits the currently active primitive.
Controller owns the active primitive state:
class EditorController:
def __init__(self):
self.active_primitive_state = {
'primitive': None, # 'v', 'r', 'f', 'a', 'S', or None
'event_id': None, # Which event is being edited
'event_time': None # Time of the event (for logging)
}Widget displays controller state:
class PrimitiveSpinboxWidget(QWidget):
value_changed = Signal(float) # Emitted when user commits new value
def __init__(self):
self.spinbox = QDoubleSpinBox() # -10.0 to +10.0
self.label = QLabel("Editing: (none)") # Shows active primitive
def set_active_primitive(self, primitive_name, event_id, current_value):
"""Called by controller when primitive selected"""
self.label.setText(f"Editing: {primitive_name}")
self.spinbox.setValue(current_value)
self.spinbox.setEnabled(True)
def clear_active(self):
"""Called when no primitive/event active"""
self.label.setText("Editing: (none)")
self.spinbox.setEnabled(False)Selection flow:
User clicks primitive label/marker
↓
PrimitiveLabel.clicked signal
↓
Controller.on_primitive_selected(primitive, event_id)
↓
Controller updates active_primitive_state
↓
Controller calls SpinboxWidget.set_active_primitive()
↓
Spinbox displays current value
Edit flow:
User types value + Enter
↓
SpinboxWidget.value_changed signal
↓
Controller.on_spinbox_value_changed(new_value)
↓
Controller creates EditPrimitiveCommand (undo support)
↓
Command updates Model
↓
Model.data_changed signal
↓
Controller.update_views()
↓
Spinbox.setValue() [if still active primitive]
No circular dependencies - flow is strictly unidirectional through signals.
All state transitions logged for debugability:
# Primitive selection
[PRIMITIVE_SELECT] perspective=M1, event_id=2, day=2.0, primitive=v, value=5.5
# Value change
[PRIMITIVE_EDIT] perspective=M1, event_id=2, day=2.0, primitive=v, old=5.5, new=7.2
# Clear selection
[PRIMITIVE_DESELECT] perspective=M1Debugging workflow:
- User reports: "Spinbox didn't update when I switched events"
- Export State Viewer log (Ctrl+Shift+L)
- Search for
PRIMITIVE_SELECTentries - Verify selection state matches user expectation
- Check if
set_active_primitive()was called - Isolate bug: selection logic vs widget update vs event mechanism
Event Selection (existing mechanism):
- User clicks event marker → sets active event
- Spinbox queries controller for active event
- Works with normal events, Event Insertion Points, Ctrl+Shift+Click markers
Perspective Switching:
- User switches M1 ↔ M2
- Controller preserves
active_primitive_state.primitive(name stays same) - Controller updates
active_primitive_state.event_id(new perspective's event) - Spinbox value updates to new perspective's value for that primitive
Undo/Redo:
- EditPrimitiveCommand stores event_time (not index) for stability
- Undo reverts primitive value
- If undone primitive is still active, spinbox value refreshed automatically
- State Viewer logs undo operations
Follows existing gamma_self plot label system:
# Same pattern as gamma_self labels
class SpinboxLabel:
def __init__(self):
self.text = QLabel()
self.text.setStyleSheet("font-weight: bold; color: #333;")
def update_for_primitive(self, primitive_name):
"""Update label when primitive selected"""
self.text.setText(f"Editing: {primitive_name}")
def clear(self):
"""Clear when no primitive active"""
self.text.setText("Editing: (none)")
# Label persists until next primitive selected (user requirement)
# Matches behavior of gamma_self plot labelsDesign consistency:
- Same font/styling as gamma_self labels
- Same persistence behavior (stays visible)
- Same clear separation: label management vs value display
- Traceable through same logging infrastructure
Three-layer checkpoint system (same pattern as Marker State):
# Layer 1: Controller state (what should be active?)
controller.active_primitive_state['primitive'] # 'v'
controller.active_primitive_state['event_id'] # 2
# Layer 2: Widget state (what is widget showing?)
spinbox_widget.label.text() # "Editing: v"
spinbox_widget.spinbox.value() # 5.5
# Layer 3: State Viewer log (what happened?)
grep "PRIMITIVE_SELECT.*event_id=2" state_log.txt
# [PRIMITIVE_SELECT] perspective=M1, event_id=2, primitive=v, value=5.5Bug isolation:
- Layers 1-2 mismatch → Controller didn't call widget update method
- Layer 2-3 mismatch → Logging incomplete (instrumentation bug)
- All layers match but still wrong → Event selection logic bug
No event selected:
if controller.active_event_id is None:
spinbox_widget.clear_active()
spinbox_widget.spinbox.setEnabled(False)No primitive selected (initial state):
if controller.active_primitive_state['primitive'] is None:
spinbox_widget.clear_active()Event deleted while editing:
def on_event_deleted(self, event_id):
if self.active_primitive_state['event_id'] == event_id:
self.active_primitive_state = {'primitive': None, 'event_id': None, 'event_time': None}
self.spinbox_widget.clear_active()Perspective switch during edit:
def on_perspective_switched(self, new_perspective):
if self.active_primitive_state['primitive'] is not None:
# Preserve primitive name, update event and value
new_event_id = self.get_event_id_for_time(
self.active_primitive_state['event_time'],
new_perspective
)
if new_event_id is not None:
new_value = self.model.get_primitive_value(
new_event_id,
self.active_primitive_state['primitive']
)
self.active_primitive_state['event_id'] = new_event_id
self.spinbox_widget.set_active_primitive(
self.active_primitive_state['primitive'],
new_event_id,
new_value
)
else:
# Event doesn't exist in new perspective
self.active_primitive_state = {'primitive': None, 'event_id': None, 'event_time': None}
self.spinbox_widget.clear_active()Invalid value entry:
# Spinbox automatically clamps to range
self.spinbox.setRange(-10.0, 10.0) # Human-scale authoring values
self.spinbox.setDecimals(1)
self.spinbox.setSingleStep(0.1)
# Non-numeric input rejected by QDoubleSpinBox automatically
# Empty field reverts to current value on focus lossAdding new state dimensions:
# Current: track primitive + event
self.active_primitive_state = {
'primitive': 'v',
'event_id': 2,
'event_time': 2.0
}
# Future: add confidence/certainty dimension
self.active_primitive_state = {
'primitive': 'v',
'event_id': 2,
'event_time': 2.0,
'confidence': 0.8, # NEW: uncertainty tracking
'source': 'manual' # NEW: manual vs inferred
}
# Spinbox widget unchanged - only displays value
# Additional widgets can show confidence, source, etc.Adding new primitive types:
# Current primitives: v, r, f, a, S
# Future: Add new primitive 'c' (commitment)
# No changes needed in spinbox widget
# No changes needed in state tracking
# Only changes: primitive metadata in PRIMITIVES.py- Precise editing: Type exact values (-10.0 to +10.0) instead of dragging
- Clean state ownership: Controller owns state, widget displays it
- Unidirectional flow: No circular dependencies, all communication via Qt signals
- Debuggability: Three-layer checkpoint system + State Viewer logging
- Testable in isolation: Mock signals, verify state transitions
- Consistent pattern: Follows marker state architecture and gamma_self label pattern
- Extensible: New primitives/dimensions require no widget changes
- Accessible: Keyboard-driven workflow, cleaner UI
Status: Specification complete, awaiting implementation
Open design questions:
- Keep read-only primitive value labels or remove them?
- Replace gauge double-click reset - Reset button? Ctrl+double-click? Right-click menu?
- Visual feedback for active primitive - Bold label? Color? Highlight?
- Spinbox placement - Above primitives? Below? Inline?
See:
- INTERACTIVE_EDITOR_CHANGELOG.md v2.4 - Feature specification and timeline
- interactive_editor_user_guide.md - User workflow and usage
- UI Architecture Cleanup - Detailed implementation examples
- Self-correcting view: Pull pattern prevents synchronization drift
- Perspective scalability: Dict-based storage extends to arbitrary perspectives
- Maintainability: Adding new tracking doesn't scatter code across multiple files
This pattern trades slight redundancy (state stored on marker + briefly in view dict) for dramatic debugging improvements.
The complete software architecture consists of 20+ modules organized in a clean MVC pattern with supporting utilities. For detailed documentation of each module including location, purpose, key I/O variables, and interdependencies, see SOFTWARE_MODULES.md.
- Entry Point:
tools/interactive_editor.pyorchestrates the entire application - State Management:
tools/editor/editor_state.pyprovides centralized state with observer pattern - MVC Core: Model (
model.py), View (views/), Controller (controller.py) with command pattern (commands.py) - UI Framework: PyQtGraph-based high-performance visualization with PySide6 widgets
- Utilities: File management, configuration, debugging, and testing infrastructure
The architecture follows strict separation of concerns with comprehensive logging, testing, and documentation coverage.
Centralized state management for the entire editor application.
- State Enums:
PerspectiveState,EditState,TrajectoryComputeState,UndoRedoState,FileLoadState - Transition Methods:
switch_perspective(),start_edit(),mark_dirty(),enter_undo_operation(), etc. - Validation Methods:
can_edit_primitive(),can_delete_event(),can_insert_event() - Observer Pattern:
add_observer(),_notify_observers()for UI updates - Singleton Access:
get_editor_state(),reset_editor_state() - See STATE_MANAGEMENT_REFACTORING.md for detailed documentation
Centralized file path management and M1/M2 resolution.
- Methods:
validate_and_resolve(),get_save_path(),get_png_path(),has_dual_perspective() - FileLoadResult: Dataclass containing validation results, resolved paths, and perspective info
- Handles automatic M1↔M2 file discovery and perspective-based naming
Widget creation and layout configuration.
- Methods:
build_panels(),build_dock_widgets(),build_editor_widgets(),build_gauges(),configure_layout() - Pure UI construction, no business logic
- Supports dual-perspective M1/M2 layouts
Represents a visual and logical marker for an event/primitive, now owning all perspective-specific state.
Core Properties:
time: Event time (synchronized with parent Event)value: Primitive value at this eventstate: Display state ('original', 'modified', 'preview')style: Visual styling information
Perspective-Aware State (v2.2.0):
gamma_self_position: Dict{perspective: complex}- where gamma_self was when this primitive was modifiedis_modified: Dict{perspective: bool}- whether modified from baseline in each perspectivelabel_visible: Dict{perspective: bool}- whether label should be shown in each perspective
Accessor Methods (Single Entry Points for Debugging):
get_gamma_position(perspective),set_gamma_position(perspective, pos),clear_gamma_position(perspective)get_is_modified(perspective),set_is_modified(perspective, modified),clear_is_modified(perspective)get_label_visible(perspective),set_label_visible(perspective, visible)
Debugging Pattern:
Breakpoint in any set_*() method catches ALL changes to that state from any code path. Call stack reveals complete context (controller method → user action → event sequence).
Design Rationale:
- Single observation point: One breakpoint per state type vs. scattered across Model/Controller/View
- Event tree terminus: All state changes flow through Marker, making it the debugging choke point
- Self-documenting: Inspecting marker shows complete state, not scattered across 3+ dictionaries
- Perspective extensibility: Dict-based storage scales naturally to arbitrary perspectives (M1, M2, M3...)
Represents a single scenario event with immutable identity and origin tracking.
Properties:
id: Permanent, immutable identifier (integer) - assigned at creation, never changestime: Current position in timeline (mutable - changes on insertion/deletion)source: Event origin ('csv' = from file, 'inserted' = user-created via Ctrl+Shift+Click)markers: Dict of Marker objects for primitives{v, r, f, a, S}notes,marker,locked: Metadata fields- References to marker objects
Event Identity System:
- ID Assignment: Events receive monotonically increasing IDs at creation (0, 1, 2, 3, ...)
- ID Permanence: IDs are NEVER reused, even after deletion
- Similar to database primary keys or Social Security Numbers
- Once ID=5 is assigned, that ID is "retired forever" if deleted
- Counter Management:
next_event_idonly increments, never decrements- After deleting event id=5, next new event gets id=6, NOT id=5
- Prevents collision bugs when undoing deletions
Deletion Strategy:
- Hard Delete: Deleted events removed from
eventslist - Undo Storage: DeleteEventCommand stores complete Event object
- Redo Cycle: Events toggle between:
- Active: Present in
eventslist - Dormant: Stored in undo command, can be resurrected
- Active: Present in
- No Soft Delete: No
deletedflag - events either exist in list or don't- Simpler code (no filtering required)
- Better performance (no flag checks)
- Undo stack is the "graveyard" for deleted events
Why ID-Based Over Time-Based:
- Time changes during insertions (event at time=49.0 shifts to time=56.0)
- IDs remain constant regardless of timeline position
- Eliminates complex baseline/label shifting logic
- Simplifies undo/redo (just reference stable ID)
- Enhanced debuggability ("trace event id=7" vs "trace whatever is at time=49.0")
Methods: update, reset, audit
Encapsulates logic/state for a single primitive (v, r, f, a, S).
- Properties:
name,value, constraints, metadata
Represents a computed gamma_self value at a specific time.
- Properties:
time,value,pinned(bool), debug info
MVC controller managing business logic and trajectory computation.
State Management:
- Uses
EditorStatefor centralized state management - Coordinates between model, views, and core mathematics
- Handles primitive edits, perspective switching, undo/redo
Event Tracking (v2.1.3):
baseline_by_id[(event_id, primitive)]- Baseline values indexed by immutable event IDmodified_primitives[(event_id, primitive)]- Modified state trackingnext_event_id- Monotonically increasing counter for new events- ID Assignment Rules:
- New events:
event.id = next_event_id++ - Deleted events: ID permanently retired, never reused
- CSV load: IDs assigned sequentially (0, 1, 2, ...) based on file order
- New events:
- No Shifting Required: When inserting events, baseline/modified dictionaries unchanged (IDs don't shift)
Migration from v2.1.2:
- v2.1.2 used
baseline_by_time[(time, primitive)]requiring complex shift logic - v2.1.3 uses
baseline_by_id[(event_id, primitive)]with no shifts needed
- Structured Programming: Code is organized into clear classes and functions with well-defined responsibilities
- Separation of Concerns: Model, view, and controller logic are separated for maintainability
- Phase 3.5: FileManager (paths), UIBuilder (widgets), Controller (logic)
- Single Source of Truth:
- Primitive metadata centralized in
primitives.py - Application state centralized in
EditorState(Phase 3.4) - File path logic centralized in
FileManager(Phase 3.5) - Configuration values loaded from JSON with sensible defaults
- Baseline values use time-keyed dictionary (v2.1.2)
- Primitive metadata centralized in
- Immutable Event Identity: Use event IDs as stable identifiers (v2.1.3)
- ✅ Events have permanent IDs assigned at creation, independent of timeline position
- ✅ Baseline storage:
baseline_by_id[(event_id, primitive)](v2.1.3) - ✅ Modified primitives tracked by ID:
modified_primitives[(event_id, primitive)](v2.1.3) - ✅ Marker positions keyed by ID:
marker_positions[(event_id, primitive)](v2.1.3) - ✅ Undo commands reference events by ID (v2.1.3)
- Rationale: Time changes during insertions; IDs remain constant throughout event lifetime
- CRITICAL RULE: Never Store Event Indices in Persistent State
- ❌ FORBIDDEN: Storing
event_indexin any state that persists beyond current call stack - ✅ REQUIRED: Store
event.id, look up current index when needed with_find_event_index_by_id() - Why: Indices change when events are inserted/deleted before them → stale references → extremely hard to debug
- Pattern:
active_event_id = event.id(store), thenindex = _find_event_index_by_id(active_event_id)(lookup) - Code Review Rule: If you see index stored in object attribute/dict that persists, flag it immediately
- Example Bug: Store index=4, insert event at position 0, index=4 now points to wrong event
- This is the #1 source of "mysterious wrong event edited" bugs
- ❌ FORBIDDEN: Storing
- Explicit State Management: Phase 3.4 replaced ~40 scattered boolean flags with explicit state enums
- Observer Pattern: State changes trigger UI updates through registered observers (Phase 3.4)
- Configuration Over Code: User preferences externalized to JSON config file
- Validated Transitions: State changes go through validation methods preventing invalid operations
- Extensibility: Architecture designed for Phase 4 features (multi-window, analysis tools, inverse editing)
- Testability: 34 state management tests, 82 total editor tests (all passing)
- Debuggability: State is explicit and easy to inspect; marker objects centralize event state
The State Viewer is the central tool for logging, debugging, and analyzing all state transitions in the scenario editor. It provides a complete, timestamped log of primitive edits, event changes, perspective switches, and more. The State Viewer log is designed for both human and AI-assisted analysis, and is essential for diagnosing UI, synchronization, and logic bugs.
Key Features:
- Comprehensive, structured state transition logging
- Exportable log (Ctrl+Shift+L) for analysis and sharing
- Designed for extensibility and privacy
- Zero overhead when disabled
For full details, log format, and advanced usage, see:
- docs/State_Viewer.md — Detailed State Viewer specification and usage
- STATE_MANAGEMENT_REFACTORING.md — Log format and implementation notes
For user-facing instructions, see:
For debugging methodology, see:
- User provides CSV path → FileManager validates and resolves M1/M2 pair
- EditorState transitions:
FILE_NOT_LOADED→FILE_LOADING→FILE_LOADED - Events and markers created from CSV data
- Perspective set based on file availability (M1 or M2)
- User edits marker → EditorState validates operation with
can_edit_primitive() - EditorState transitions:
NO_EDIT→EDITING(marks dirty) - Marker state updates, curve redraws
- Controller triggers trajectory recomputation
- EditorState transitions:
TRAJECTORY_COMPUTING→TRAJECTORY_READY(marks clean) - Observer notifications update UI gauges and readouts
- User clicks M1/M2 button → EditorState validates with
can_switch_perspective() - EditorState transitions perspective:
M1↔M2 - Controller reloads events from alternate perspective file
- UI rebuilds panels with new data
- Observers notified to update button states
- EditorState tracks:
CAN_UNDO,CAN_REDO,IN_UNDO_REDO - Commands use
enter_undo_operation()/exit_undo_operation() - Prevents recursive undo during undo execution
- Trajectory recomputation triggered after command execution
- User clicks Save → FileManager determines correct save path based on active perspective
- Modified events written to CSV
- EditorState marks file as clean
See interactive_edit_roadmap.md for complete Phase 4 requirements:
- Analysis Window: Separate synchronized window for advanced analysis
- Inverse Editing: Drag gamma_self to suggest primitive changes
- Sensitivity Analysis: Automated ranking of primitive influence
- Batch Export: Multiple PNG exports with templating
- Interpolation Tools: Fill gaps in trajectory data
- Animation/Playback: Time-based trajectory visualization
- Constraint Validation: Multi-primitive constraint checking
- Database Integration: Large-scale scenario management
Architecture Readiness: Phase 3.5 refactoring prepares for Phase 4 by:
- Enabling multi-window architecture (shared model/controller)
- Supporting tool plugins (sensitivity, inverse solver)
- Establishing event-driven communication patterns
- Centralizing export pipeline for batch operations
For implementing ANY feature:
- This Document (ARCHITECTURE.md) - Architectural patterns, state management, signal flow requirements
- Marker State Architecture - Event tree terminus pattern, three-layer debugging
- Spinbox Primitive Editor - Active state tracking, unidirectional signals
- MVT Quality Standard - Modeled, Verifiable, Testable requirements
For understanding current system:
- INTERACTIVE_EDITOR_CHANGELOG.md - Version history, feature specifications, implementation phases
- Use for: Finding what's planned/complete, understanding feature scope, checking status
- Key sections: v2.4-spinbox (planned features), v2.2.2-state-viewer-log (debugging), version timeline
For using the editor:
- interactive_editor_user_guide.md - Complete user manual, workflows, UI reference
- Use for: Understanding user perspective, usage patterns, UI layout, keyboard shortcuts
- Key sections: Application States, UI layout, Spinbox Primitive Editing (planned)
State & Architecture:
- STATE_MANAGEMENT_REFACTORING.md - Phase 3.4 centralized state enums and observers
- Use for: EditorState usage, state transition validation, observer pattern
- Entry Point Consolidation - Planned dual entry point consolidation + observability
- Use for: Understanding observability framework, why we're consolidating, migration timeline
- Baseline Storage Refactoring - Time-keyed baseline architecture
- Use for: Baseline value lookups, reset functionality, event ID vs time keys
- Baseline Communication Protocol - Baseline value communication patterns
- Use for: How baseline values flow between model/controller/view
UI & Signal Patterns:
- UI Architecture Cleanup - Signal flow patterns, communication anti-patterns
- Use for: Code examples of spinbox signals, clean vs mixed patterns, integration examples
- Key section: Problem 4 (Gauge-Based Primitive Editing) - spinbox implementation details
- Architecture Recommendations - GUI architecture planning (QDockWidget, future features)
- Use for: Future-proofing decisions, planned Phase 3/4 features, dock system requirements
Debugging & Testing:
- DEBUG.md - Debug infrastructure, logging patterns, State Viewer usage
- Use for: Adding debug logging, State Viewer format, systematic debugging methodology
- INTERACTIVE_EDITOR_TESTING.md - Testing strategy, manual checklists, MVT validation
- Use for: Creating test plans, regression testing, quality assurance
- interactive_edit_roadmap.md - Phase 4 roadmap, planned features
- Use for: Understanding where features fit in timeline, future compatibility considerations
- Installation Guide - Setup and dependencies
- Use for: Environment setup, dependency requirements
- Main README - Project overview, getting started
I need to:
- ✅ Implement a new feature → Read ARCHITECTURE.md (patterns) + CHANGELOG.md (v2.4 spec if related to spinbox)
- ✅ Fix a bug → Read DEBUG.md (methodology) + ARCHITECTURE.md (three-layer debugging)
- ✅ Add a widget → Read UI Architecture Cleanup (signal patterns) + ARCHITECTURE.md (state ownership)
- ✅ Understand state transitions → Read STATE_MANAGEMENT_REFACTORING.md + ARCHITECTURE.md (Data Flow Overview)
- ✅ Add State Viewer logging → Read DEBUG.md (format) + ARCHITECTURE.md (logging examples in Spinbox section)
- ✅ Write tests → Read INTERACTIVE_EDITOR_TESTING.md (MVT standard) + ARCHITECTURE.md (testability patterns)
- ✅ Learn user workflow → Read interactive_editor_user_guide.md
- ✅ Check what's planned → Read INTERACTIVE_EDITOR_CHANGELOG.md (versions section)
- ✅ Understand baseline system → Read Baseline Storage Refactoring + Baseline Communication Protocol
For more details on usage and features, see the documentation links above.
For more details on usage and features, see the documentation links above.