This document outlines systematic debugging approaches for the interactive editor, particularly for complex UI/state synchronization issues.
What: Try solutions directly, iterate until something works.
When to use:
- Simple, well-understood problems
- Time-constrained situations
- When you have a strong hypothesis
Limitations: Can miss root causes, create technical debt
What: Verify what we can and cannot control in the framework/libraries.
When to use:
- Working with external libraries (PyQtGraph, Qt)
- Unexpected behavior from framework components
- Need to understand API limitations
Example: Testing if PyQtGraph's removeItem() actually removes items from rendering cache vs just from data structures.
What: Trace how data moves through the system using logging, breakpoints, or stack traces.
When to use:
- Multi-component interactions
- Understanding execution order
- Finding where state changes occur
Tools:
- Debug print statements with timestamps
- Python debugger breakpoints
- Stack trace logging (
traceback.format_stack())
What: Snapshot complete system state at key moments and compare.
When to use:
- State corruption issues
- Data getting lost or appearing unexpectedly
- Understanding what's different between working/broken states
How:
def dump_state(label):
print(f"=== STATE DUMP: {label} ===")
print(f"M1 labels: {len(panel.modified_labels_m1)}")
print(f"M2 labels: {len(panel.modified_labels_m2)}")
print(f"Plot items: {[(k, len(v.items)) for k, v in panel.plot_items.items()]}")
print(f"Model modified_primitives: {model.modified_primitives}")
print("===========================")What: Systematically disable code sections to narrow down the problem location.
When to use:
- Unknown root cause
- Complex code paths
- Multiple suspects
Process:
- Comment out half the suspected code
- Test if bug persists
- If yes, bug is in other half; if no, bug is in commented half
- Repeat until you isolate the exact line
What: Explicitly test assumptions we're making about the system.
When to use:
- Stuck on a problem for >30 minutes
- Behavior doesn't match expectations
- Before major refactoring
Critical Questions:
- Is the data structure actually empty when we think it is?
- Is this the same object or a different instance?
- Is the code path we think is executing actually being executed?
- Are we checking the right variable/attribute?
Example:
# Don't assume, verify:
assert len(modified_labels_m2) == 0, f"Expected 0 labels, found {len(modified_labels_m2)}"
assert text_item.scene() is None, f"TextItem still has scene: {text_item.scene()}"
assert id(label1) != id(label2), "These are the same object!"What: Create smallest possible standalone script that reproduces the bug.
When to use:
- Suspecting framework bug
- Need to report issue to library maintainers
- Want to understand framework behavior in isolation
Benefits:
- Eliminates application-specific complexity
- Can test framework behavior independently
- Often reveals misunderstandings of API
What: Read library source code, documentation, or known issues.
When to use:
- Unexpected framework behavior
- Undocumented edge cases
- Before filing bug report
Resources:
- PyQtGraph GitHub: https://github.com/pyqtgraph/pyqtgraph
- Qt documentation
- Stack Overflow for known issues
When facing a complex bug:
- Start with Assumption Validation (#6) - Question what you think you know
- Add Information Flow Tracking (#3) - Understand execution order
- Do State Inspection (#4) - Compare expected vs actual state
- Try Bisection (#5) - Narrow down the problem location
- Consider Control Testing (#2) - Verify framework behavior
- Create Minimal Reproduction (#7) - If still stuck, isolate the issue
- Only then: Direct Bug Squashing (#1) - Apply targeted fix
Problem: Labels from M1 perspective appearing in M2 perspective despite removal attempts.
What we tried (Direct Bug Squashing):
removeItem()- didn't worksetVisible(False)- didn't workdeleteLater()- didn't work- "Nuclear cleanup" removing all TextItems - didn't work
What we should have done:
- Assumption Validation: Verify the label IS actually a leftover M1 label (user tested: it moves with M2 marker → NOT a leftover!)
- Information Flow: Trace where labels are created (found: only in
_add_marker_label()) - State Inspection: Check if TextItems persist in plot after removal (added debug for this)
Key insight: After 15+ removal attempts, user revealed the label moves with M2's marker, proving it's NOT a ghost - it's being actively created/attached, but our debug tracing doesn't catch it. This means there's a hidden code path.
Next steps: Instrument all addItem() calls to find the hidden creation path.
- Issue: Stray marker/label artifacts (e.g., 0.0/S, 49.0/S, 28.0/S) appeared on the trajectory plot, and label persistence was inconsistent.
- Approach: Traced label creation and filtering logic, inserted breakpoints, and iteratively patched controller and view logic.
- Solution: Filtered pinned_markers to only include actually modified markers (using event_id and primitive), and ensured label visibility is managed per user action.
- Validation: Tested moving, resetting, and undoing markers in both M1 and M2 perspectives. Confirmed only modified markers show labels, and labels disappear only when reset.
- Log state changes with timestamps and context
- Use assertions to validate assumptions during development
- Add debug modes that can be toggled without code changes
- Document what you tried to avoid repeating failed approaches
- Question your assumptions before adding more code
- Simplify before expanding - remove debug code once understood
Location: tools/editor/debug_config.py
The editor uses Python's standard logging module with configurable levels and file-only output by default. This provides professional logging with granular control and clean terminal output.
Configuration via Environment Variables:
LOG_LEVEL- Controls logging verbosity (DEBUG, INFO, WARNING, ERROR, CRITICAL)DEBUG: Detailed internal operations, state transitions, primitive changesINFO: General information, successful operations (default)WARNING: Potential issues that don't prevent operationERROR: Errors that may affect functionalityCRITICAL: Severe errors requiring immediate attention
LOG_TO_TERMINAL- Enable terminal output (default: false, logs to files only)
Log Files:
- Location:
logs/directory (auto-created in project root) - Naming:
interactive_editor_YYYYMMDD_HHMMSS.log - Example:
interactive_editor_20251217_142530.log - Format:
2025-12-17 14:25:30 [INFO] WhenMathPrays.controller: File loaded successfully
Turning Off Logging:
- Set
LOG_LEVEL=CRITICALto show only severe errors - Set
LOG_LEVEL=ERRORto show errors and critical issues only - Default
INFOlevel provides good balance of information without noise DEBUGlevel is for development/troubleshooting only
Category Flags: Enable/disable logging for specific subsystems:
DEBUG_SPINBOX- Primitive spinbox editor (click, drag, value changes)DEBUG_TRAJECTORY- Trajectory computation and gamma_self calculationsDEBUG_LABELS- Label synchronization and perspective switching (all label-related debug output except assignment block)DEBUG_LABELS_ASSIGNMENT- Label assignment block only (fine-grained):- Controls deep debug output for the label assignment logic in
trajectory_panel_pyqtgraph.py. - When enabled, logs all mapping keys, label text, and assignment steps for pinned markers.
- Use this to trace and debug the exact process of label placement and mapping, without enabling all label debug output.
- See ARCHITECTURE.md for architectural context and usage guidance.
- Controls deep debug output for the label assignment logic in
DEBUG_GAMMA- Gamma_self parameter calculationsDEBUG_STATE- State save/restore operationsDEBUG_UNDO- Undo/redo command executionDEBUG_DRAG- Drag operation trackingDEBUG_BASELINE- Baseline synchronization eventsDEBUG_SYNC- Cross-component synchronization
# In each module that needs logging:
from tools.editor.debug_config import get_debug_logger, DEBUG_SPINBOX
_logger = get_debug_logger('SPINBOX')
# In code:
if DEBUG_SPINBOX:
_logger.debug("marker clicked: index={index}, primitive={primitive}")- Centralized control: Single file controls all debug logging flags
- File output: Logs persist to timestamped files for post-mortem analysis
- Granular control: Enable only specific categories without noise
- Professional formatting: Timestamps, log levels, module names
- No console spam: Logs route to files, keeping terminal clean
- Easy toggling: Change one flag to enable/disable entire category
Completed:
- ✅ Spinbox-related logging fully migrated (interactive_editor.py, controller.py, primitive_panel_pyqtgraph.py)
- ✅ All primitive spinbox editor debug statements use logger
Future Migrations:
- Trajectory computation logging → DEBUG_TRAJECTORY
- Label sync logging → DEBUG_LABELS
- Gamma_self calculation logging → DEBUG_GAMMA
- State save/restore logging → DEBUG_STATE
- Undo/redo logging → DEBUG_UNDO
- Drag operations → DEBUG_DRAG
- Baseline tracking → DEBUG_BASELINE
Note: These are being migrated to the centralized logging system above.
Debug messages use prefixed tags for filtering/searching:
[DEBUG]- General debug information[BASELINE]- Baseline synchronization events[BASELINE_CHECK]- Baseline comparison logic[APPLY_CHANGE]- Primitive value changes[UNDO]- Undo/redo operations[CONTROLLER]- Controller state changes[PYQTGRAPH]- View update timing[TRAJECTORY]- Gamma_self trajectory computation[PANEL_REMOVE]- Label removal operations[LABEL_ADD]- Label addition with call stacks
Major operations use delimited sections:
=== INSERT EVENT ===
... operation details ...
=== END INSERT ===
The baseline communication protocol has dedicated logging (see baseline_communication_protocol.md):
- Enable:
controller.enable_baseline_protocol_logging() - Disable:
controller.disable_baseline_protocol_logging() - Dump:
controller.dump_baseline_protocol_log("path/to/file.json")
- Minimal noise: Production code has minimal debug output (critical operations only)
- Targeted logging: Add detailed logging temporarily when debugging specific issues
- Centralized control: Use debug_config.py flags instead of scattered conditionals
- File-based logs: Route to files for analysis without terminal clutter
- Tagged output: All debug messages include category and context information
For systematic debugging and regression testing, the editor includes a comprehensive state logging and verification system:
- Verification System - Automated verification scripts and baseline state logs
- State Logs: Complete operation records with timestamps, entities, and state changes
- Regression Testing: Compare current behavior against verified baselines
- Debug Data: State logs serve as debugging aids for complex state synchronization issues
The verification system uses the same logging infrastructure described above but captures complete state transitions for automated analysis and debugging.
Complex bugs often reveal architectural issues:
- The label persistence bug revealed fragmented perspective switching across multiple components without centralized coordination
- Proper fix: Refactor to event-driven architecture with Qt signals/slots for perspective changes
- The baseline cascade deletion bug revealed time-based keys creating shift complexity
- Proper fix: Migrate to ID-based event identity for immutable tracking
- Lesson: Manual state synchronization across components is error-prone; use observer pattern and immutable identities
See:
- architecture/perspective_management_refactor.md for perspective switching refactor
- ARCHITECTURE.md Event Identity section for ID-based tracking design
While the primitive spinbox editor has been validated for:
- ✅ Click activation and auto-activation during drag
- ✅ Undo/redo (Ctrl+Z/Ctrl+Y) across M1/M2 perspectives
- ✅ Event insertion (Ctrl+Shift+Click) with spinbox active
- ✅ Event deletion with spinbox active (no crashes)
Proposals for systematic testing:
- Create automated UI test suite for editor operations
- Add regression tests for known bugs (label persistence, baseline cascade deletion)
- Consider property-based testing for trajectory computation
- Add performance benchmarks for large timelines (>100 events)
Immediate opportunities:
- Verify file logging: Test that logs actually appear in
logs/debug_<timestamp>.logwhenDEBUG_TO_FILE = True - Complete migration: Migrate remaining print statements to centralized logger:
- Trajectory computation → DEBUG_TRAJECTORY
- Label synchronization → DEBUG_LABELS
- Gamma_self calculations → DEBUG_GAMMA
- State save/restore → DEBUG_STATE
- Undo/redo operations → DEBUG_UNDO
- Drag operations → DEBUG_DRAG
- Baseline tracking → DEBUG_BASELINE
Future architecture improvements:
- Log rotation: Implement max file size/age limits to prevent disk bloat
- Log levels: Use INFO/WARNING/ERROR more strategically instead of only DEBUG
- Context managers: Add context managers for operation logging:
with log_operation('PRIMITIVE_EDIT', event_id=5): # operation code # automatic logging of start/end/duration
- Structured logging: Consider JSON-formatted logs for machine parsing
- Performance profiling: Add timing decorators to identify slow operations
- Remote logging: Optional log aggregation for distributed testing
Proposals:
- Add debug_config.py usage examples to ARCHITECTURE.md
- Create visual debugging guide with screenshots of:
- Spinbox editor in action
- Log file output examples
- Common debug workflows
- Document known gotchas and their solutions
- Maintain changelog of debug infrastructure changes
Complex UI bugs often arise from hidden or implicit mapping between the primitive (time-based) domain and the gamma_self (event-based) domain. To ensure robust debugging and synchronization, the mapping must be explicit, visible, and debuggable.
- Explicit Mapping: All correspondences between primitive events and gamma_self events must be recorded in a first-class mapping structure (e.g., a dictionary).
- Bidirectional Traceability: Debugging tools must allow tracing from primitive to gamma_self and vice versa.
- State Viewer Integration: The State Viewer log should export the mapping for inspection and AI-assisted analysis.
- Logging: All mapping changes (creation, update, removal) should be logged with context (who, what, why, when).
- Observe UI Issue: Label or marker appears in the wrong position or perspective.
- Export State Viewer Log: Use Ctrl+Shift+L to export the current mapping and state.
- Inspect Mapping: Check the mapping structure in the log to verify correct correspondence between primitive and gamma_self domains.
- Trace Source: Use mapping logs to identify where synchronization failed (e.g., missing, stale, or incorrect mapping entries).
- Round-Trip Check: Given a primitive event, confirm its gamma_self representation (and vice versa) using the mapping.
primitive_to_gamma_self = {
(event_id, 'v'): {'gamma_self_pos': 3+2j, 'label': 'Visibility', 'perspective': 'M1'},
(event_id, 'r'): {'gamma_self_pos': 1+4j, 'label': 'Resonance', 'perspective': 'M2'},
# ...
}- Eliminates hidden/implicit mapping bugs
- Enables rapid diagnosis and AI-assisted debugging
- Supports future extensibility (new perspectives, primitives)
Note: The user-facing manual now provides a brief summary and refers here for full details on the explicit primitive-to-gamma_self mapping, debugging workflows, and log inspection. This section is the canonical technical reference for developers and advanced users.
This section provides detailed debugging guidance for all key objects defined in ARCHITECTURE.md. Each object includes inspection methods, State Viewer coverage, and debugging workflows.
Current State Viewer Coverage: ❌ None (106+ field modifications not tracked)
Inspection Methods:
# Direct inspection
event = model.events_m1[0]
print(f"EventPoint: time={event.time}, v={event.v}, r={event.r}, f={event.f}, a={event.a}, S={event.S}")
print(f"Metadata: notes='{event.notes}', marker='{event.marker}', locked={event.locked}")
# CSV export for verification
csv_row = event.to_dict()
print(f"CSV format: {csv_row}")
# Validation check
try:
event.__post_init__() # Raises ValueError if invalid
print("EventPoint is valid")
except ValueError as e:
print(f"EventPoint invalid: {e}")Debugging Workflows:
- Primitive value corruption: Check
event.__post_init__()validation - CSV I/O issues: Compare
event.to_dict()vs file contents - Timeline problems: Verify
event.timeordering inmodel.events_m1/m2
Future Enhancement: Add property setters with State Viewer logging for all field modifications.
Current State Viewer Coverage: ❌ None (11 setter calls not tracked)
Inspection Methods:
# Complete marker state
marker = event.markers['v'] # Get marker for specific primitive
print(f"Marker state: {marker.__dict__}")
# Perspective-aware inspection
print(f"M1 modified: {marker.get_is_modified('M1')}")
print(f"M1 gamma position: {marker.get_gamma_position('M1')}")
print(f"M1 label visible: {marker.get_label_visible('M1')}")
# Cross-perspective comparison
for perspective in ['M1', 'M2']:
print(f"{perspective}: modified={marker.get_is_modified(perspective)}, "
f"pos={marker.get_gamma_position(perspective)}, "
f"visible={marker.get_label_visible(perspective)}")Debugging Workflows:
- Label persistence bugs: Check
marker.get_label_visible(perspective)vs UI rendering - Marker positioning: Verify
marker.get_gamma_position(perspective)matches trajectory plot - Modification state: Compare
marker.get_is_modified(perspective)withmodel.modified_primitives_m1/m2
Future Enhancement: State Viewer logging in existing setter methods (set_gamma_position, set_is_modified, set_label_visible).
Current State Viewer Coverage: ✅ Partial (core operations tracked, ObservableDict changes not)
Inspection Methods:
# Core identity and events
print(f"Model: {model.name_m1}/{model.name_m2}, {len(model.events_m1)}/{len(model.events_m2)} events")
print(f"Next ID: {model.next_event_id}, Dirty: {model.dirty}")
# Perspective-specific state
for perspective in ['M1', 'M2']:
gamma_0 = model.get_gamma_self_0(perspective)
modified = getattr(model, f'modified_primitives_{perspective.lower()}')
print(f"{perspective}: gamma_0={gamma_0}, {len(modified)} modified primitives")
# ObservableDict inspection (not logged by State Viewer)
modified_m1 = model.modified_primitives_m1
print(f"M1 modified keys: {list(modified_m1.keys())}")
for event_id, primitives in modified_m1.items():
print(f" Event {event_id}: {primitives}")
# Preview state
print(f"Preview changes: {model.preview_changes}")Debugging Workflows:
- State synchronization: Compare
modified_primitives_m1/m2with marker states - Event identity: Verify
next_event_idand event ID assignments - Persistence issues: Check
dirtyflag and CSV export consistency
Future Enhancement: State Viewer logging for ObservableDict observer callbacks.
Current State Viewer Coverage:
Inspection Methods:
# Active state
print(f"Controller: perspective={controller.perspective}, "
f"active_event={controller.active_primitive_state.get('event_id')}, "
f"active_primitive={controller.active_primitive_state.get('primitive')}")
# Command system
print(f"Undo stack: {controller.undo_stack.count()} commands")
print(f"M1 stack: {controller.undo_stack_m1.count()}, M2 stack: {controller.undo_stack_m2.count()}")
# Computation state
print(f"Weights: {controller.weights}")
print(f"Trajectory: {len(controller.committed_gamma_trajectory) if controller.committed_gamma_trajectory else 0} points")
# Baseline tracking (not logged)
print(f"M1 baseline keys: {list(controller.baseline_by_id_m1.keys())[:5]}...") # First 5
print(f"M2 baseline keys: {list(controller.baseline_by_id_m2.keys())[:5]}...")
# Mapping state
print(f"Primitive→gamma_self mappings: {len(controller.primitive_to_gamma_self)}")Debugging Workflows:
- Perspective switching: Check
controller.perspectiveand active primitive state preservation - Command execution: Verify undo stack state and command ordering
- Baseline synchronization: Compare
baseline_by_id_m1/m2with current event values
Future Enhancement: State Viewer logging for internal state changes (active_primitive_state, baseline updates).
Current State Viewer Coverage: ✅ Partial (lifecycle events tracked, plot state not)
Inspection Methods:
# Panel readiness and diagnostics
print(f"Panel ready: {panel.ready}")
print(f"Diagnostic markers: {list(panel.diagnostic_markers.keys())}")
if hasattr(panel, 'diagnostic_event_idx'):
print(f"Diagnostic focus: event {panel.diagnostic_event_idx}, primitive '{panel.diagnostic_primitive}'")
# Plot structure
print(f"Plot items: {list(panel.plot_items.keys())}")
for prim, plot in panel.plot_items.items():
items = [type(item).__name__ for item in plot.items]
print(f" {prim}: {len(items)} items - {items[:3]}...") # First 3 item types
# Interactive elements
print(f"Scatter items: {list(panel.scatter_items.keys())}")
for prim, scatter in panel.scatter_items.items():
print(f" {prim}: {len(scatter.data['x'])} points")
# Overlay state (inactive perspective)
print(f"Overlay scatter: {list(panel.overlay_scatter_items.keys())}")
print(f"Overlay lines: {list(panel.overlay_line_items.keys())}")Debugging Workflows:
- Rendering issues: Check plot item counts and types
- Interaction problems: Verify scatter item data and event connections
- Perspective display: Compare active vs overlay item states
Future Enhancement: State Viewer logging for plot state changes and item management.
Current State Viewer Coverage: ✅ Good (label operations tracked via TrajectoryLabelManager)
Inspection Methods:
# Plot structure
items = panel.plot_widget.items
item_types = [type(item).__name__ for item in items]
print(f"Plot items: {len(items)} total - {item_types}")
# Trajectory state
print(f"Trajectory line: {panel.trajectory_line is not None}")
print(f"Overlay line: {panel.overlay_line is not None}")
# Label state (via manager)
if hasattr(panel, 'trajectory_label_manager'):
labels = panel.trajectory_label_manager.all_labels()
print(f"Managed labels: {len(labels)}")
for key, label in list(labels.items())[:3]: # First 3
print(f" {key}: '{label.toPlainText()}' visible={label.isVisible()}")
# Direct label inspection (legacy)
print(f"Direct labels: {len(panel.marker_labels)}")
for i, label in enumerate(panel.marker_labels[:3]): # First 3
print(f" Label {i}: '{label.toPlainText()}' visible={label.isVisible()}")Debugging Workflows:
- Label synchronization: Compare managed vs direct label states
- Trajectory rendering: Check line item existence and data
- Perspective overlays: Verify active vs overlay line states
Future Enhancement: State Viewer logging for trajectory computation and rendering updates.
Current State Viewer Coverage: ✅ Complete (all operations tracked)
Inspection Methods:
# Active command stacks
print(f"Current stack: {controller.undo_stack.count()} commands")
for i in range(min(5, controller.undo_stack.count())): # Last 5
cmd = controller.undo_stack.command(i)
print(f" Command {i}: {cmd.text()}")
# Stack separation
print(f"M1 stack: {controller.undo_stack_m1.count()} commands")
print(f"M2 stack: {controller.undo_stack_m2.count()} commands")Debugging Workflows:
- Undo/redo issues: Check command stack contents and ordering
- Command execution: Verify State Viewer logs for command operations
- Cross-perspective: Compare M1/M2 stack states
Current State Viewer Coverage: ❌ None (state transitions not tracked)
Inspection Methods:
# Current state values
print(f"EditorState: perspective={state.perspective}, "
f"edit_state={state.edit_state}, "
f"compute_state={state.compute_state}")
print(f"Undo state: {state.undo_state}, File load: {state.file_load_state}")
print(f"Flags: dirty={state.dirty}, initial_load={state.initial_load_complete}")
# Observer registry
print(f"Observers: {list(state._observers.keys())}")
for event, callbacks in state._observers.items():
print(f" {event}: {len(callbacks)} callbacks")Debugging Workflows:
- State transitions: Check current state values and validity
- Observer issues: Verify callback registration and execution
- Complex workflows: Trace state changes through drag-preview-commit cycles
Future Enhancement: State Viewer logging in existing observer callbacks (already has observer infrastructure).
For All Objects:
# Enable State Viewer
import os
os.environ['STATE_VIEWER'] = '1' # or set STATE_VIEWER=1 in environment
# Export logs during debugging
from tools.editor.state_viewer import StateViewer
StateViewer.export_to_file("debug_state.log")
# Get recent operations
recent = StateViewer.get_recent(10)
for change in recent:
print(f"{change.timestamp}: {change.operation} - {change.entity}")
# Check for warnings
warnings = StateViewer.get_warnings()
for warning in warnings:
print(f"Warning: {warning.get_warning_message()}")Debugging Workflow with State Viewer:
- Reproduce the bug with State Viewer enabled
- Export the log at the point of failure
- Search for relevant operations (e.g.,
update_primitive,add_label) - Trace the sequence of state changes leading to the bug
- Identify the root cause by comparing expected vs actual state transitions
Future development tools:
- Log viewer: GUI for browsing/filtering debug logs
- State diff tool: Visual comparison of state dumps
- Replay mode: Record user interactions, replay with different parameters
- Performance monitor: Real-time visualization of operation timing
- Test scenario generator: Record user interactions as automated tests