Skip to content

Commit 7dead74

Browse files
committed
feat: add pre-commit configuration and update dependencies for code formatting
1 parent 6d1c224 commit 7dead74

File tree

4 files changed

+582
-0
lines changed

4 files changed

+582
-0
lines changed

pychron/canvas/README.md

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# canvas
2+
3+
2D scientific visualization and diagramming system for Pychron. Renders
4+
interactive schematic diagrams of extraction line hardware, sample positions,
5+
and laser ablation targets using the Enthought Tool Suite (Enable/Chaco).
6+
7+
## What This Package Owns
8+
9+
- **Extraction line diagrams** -- Interactive schematic diagrams showing valves,
10+
switches, pumps, lasers, spectrometers, stages, gauges, getters, and the
11+
tubing (connections) between them
12+
- **Scene system** -- Layered component model with YAML/XML persistence
13+
- **Primitives** -- Drawable items: points, circles, rectangles, lines, labels,
14+
polygons, images, valves, switches, and connection/tubing shapes
15+
- **Markup canvases** -- Annotating sample positions on irradiation trays,
16+
UV masks, laser ablation targets, and furnace stages
17+
- **Calibration canvases** -- Geometric calibration of laser ablation systems
18+
- **Data canvases** -- Chaco-based plot canvases for line plots and colormap images
19+
20+
## Entry Points
21+
22+
The `__init__.py` is empty. Consumers import directly from submodules.
23+
24+
| Import Path | Class | Role |
25+
|---|---|---|
26+
| `canvas2D.scene.scene_canvas.SceneCanvas` | `SceneCanvas` | Main canvas widget that renders scenes |
27+
| `canvas2D.scene.scene.Scene` | `Scene` | Container for layers and overlays |
28+
| `canvas2D.scene.extraction_line_scene.ExtractionLineScene` | `ExtractionLineScene` | Specialized scene for extraction line diagrams |
29+
| `canvas2D.extraction_line_canvas2D.ExtractionLineCanvas2D` | `ExtractionLineCanvas2D` | Interactive extraction line canvas with mouse handling |
30+
| `canvas2D.base_data_canvas.BaseDataCanvas` | `BaseDataCanvas` | Chaco DataView with zoom, pan, axes, coordinate mapping |
31+
| `canvas2D.scene.primitives.base.Primitive` | `Primitive`, `QPrimitive`, `Connectable` | Base classes for all drawable items |
32+
| `canvas2D.scene.layer.Layer` | `Layer` | Z-ordering container with visibility toggle |
33+
| `canvas2D.scene.yaml_scene_loader.YAMLLoader` | `YAMLLoader` | YAML scene file loader |
34+
| `canvas_editor.CanvasEditor` | `CanvasEditor` | Interactive editor for adjusting canvas items |
35+
36+
## Critical Files
37+
38+
```
39+
canvas/
40+
├── canvas_editor.py # Interactive reposition/resize editor
41+
├── scene_viewer.py # TraitsUI viewer wrappers
42+
├── utils.py # Helper functions for holder geometry
43+
├── canvas2D/
44+
│ ├── base_canvas.py # Minimal base (Enable Component)
45+
│ ├── base_data_canvas.py # Chaco DataView (zoom, pan, axes, mapping)
46+
│ ├── scene/
47+
│ │ ├── scene.py # Scene: layers, overlays, rendering dispatch
48+
│ │ ├── scene_canvas.py # Bridges BaseDataCanvas with Scene
49+
│ │ ├── layer.py # Named component list with visibility
50+
│ │ ├── extraction_line_scene.py # Specialized scene with load() and hit-testing
51+
│ │ ├── yaml_scene_loader.py # YAML file parser → primitives
52+
│ │ ├── xml_scene_loader.py # Legacy XML loader (deprecated)
53+
│ │ ├── canvas_parser.py # XML/YAML parser wrapper
54+
│ │ └── primitives/
55+
│ │ ├── base.py # Primitive, QPrimitive, Connectable
56+
│ │ ├── primitives.py # Point, Rectangle, Circle, Line, Label, etc.
57+
│ │ ├── valves.py # Switch, Valve, RoughValve, ManualSwitch
58+
│ │ ├── connections.py # Connection, Elbow, Tee, Fork, Cross
59+
│ │ ├── rounded.py # RoundedRectangle, CircleStage, Spectrometer
60+
│ │ ├── lasers.py # Laser, CircleLaser
61+
│ │ └── pumps.py # Turbo, IonPump
62+
│ ├── overlays/
63+
│ │ └── extraction_line_overlay.py # Info overlay for extraction line
64+
│ └── markup/
65+
│ ├── markup_canvas.py # Sample position annotation
66+
│ └── markup_items.py # Markup drawing items
67+
└── canvas2D/tests/
68+
└── calibration_item.py # Single test file (rotation math)
69+
```
70+
71+
## Runtime Lifecycle
72+
73+
### Creation and Loading Flow
74+
75+
1. **Canvas creation** -- Application creates a specialized canvas
76+
(e.g., `ExtractionLineCanvas2D`), which chains through
77+
`SceneCanvas``BaseDataCanvas` → Chaco `DataView`. A default
78+
`Scene` is created with two layers (`Layer("0")`, `Layer("1")`).
79+
80+
2. **Scene loading** -- `ExtractionLineScene.load()` reads a YAML file:
81+
- `_load_config()` reads view ranges, valve dimensions, colors, origin
82+
- `YAMLLoader` instantiates primitives: valves, stages, lasers, pumps
83+
- Connections are loaded last (they reference components by name)
84+
- `scene.set_canvas(canvas)` wires the canvas reference to all primitives
85+
86+
3. **Rendering** -- Triggered by `request_redraw()`:
87+
- `_draw_underlay(gc)``scene.render_components(gc, canvas)`
88+
- Culls off-screen items via `is_in_region(bounds)`
89+
- Calls `ci.render(gc)``Primitive._render(gc)`
90+
- `_draw_overlay(gc)``scene.render_overlays(gc, canvas)`
91+
92+
### Trait Event Chains
93+
94+
```
95+
Primitive property change (x, y, color, state, visible)
96+
→ _refresh_canvas() → request_redraw() → canvas redraw
97+
98+
Scene.layout_needed event
99+
→ SceneCanvas observes → request_redraw()
100+
101+
Connectable x/y change
102+
→ _update_xy() → updates Connection endpoints → request_layout() → request_redraw()
103+
```
104+
105+
### Caching
106+
107+
Primitives cache screen-space position (`_cached_xy`), size (`_cached_wh`),
108+
bounds (`_cached_bounds`), colors (`_cached_colors`), and text extents
109+
(`_cached_text_extent`). The `_layout_needed` flag invalidates caches.
110+
111+
## Test Strategy
112+
113+
**Extremely minimal.** One test file exists:
114+
115+
| Test File | Coverage |
116+
|-----------|----------|
117+
| `canvas2D/tests/calibration_item.py` | `CalibrationObject.calculate_rotation()` (8 tests) |
118+
119+
**Not tested**: canvas creation, scene loading, primitive rendering, YAML/XML
120+
parsing, extraction line interaction, connection wiring, caching invalidation.
121+
122+
## Common Failure Modes
123+
124+
| Failure | Symptom | Where |
125+
|---------|---------|-------|
126+
| `canvas` is `None` | Primitives fail silently or return wrong coordinates | `base.py` |
127+
| Malformed YAML | Incomplete scene (no error raised) | `yaml_scene_loader.py` |
128+
| Connection load order | Broken connections if loaded before components | `extraction_line_scene.py` |
129+
| Stale color cache | Wrong colors if color object mutated after conversion | `base.py` |
130+
| Overflow in coordinate mapping | Caught and ignored | `rounded.py:54` |
131+
| Unknown colormap depth | `RuntimeError` raised | `primitives.py:866` |
132+
133+
**Known fragility points:**
134+
- XML format is deprecated but both loaders still coexist
135+
- No validation on YAML structure -- malformed files silently produce incomplete scenes
136+
- Connection wiring is order-dependent: connectables must be loaded before connections
137+
- Caching invalidation relies on `_layout_needed` propagation; edge cases exist on
138+
canvas resize/zoom
139+
- `_convert_color()` caches by `id()` -- if a color object is mutated after
140+
conversion, the cache becomes stale

pychron/experiment/README.md

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
# experiment
2+
3+
Experiment orchestration engine for automated Ar/Ar geochronology analyses.
4+
Manages the full lifecycle of running samples through an extraction line and
5+
mass spectrometer -- from queue setup through data persistence.
6+
7+
## What This Package Owns
8+
9+
- **Experiment queues** -- tabular lists of analyses loaded from tab-delimited
10+
files with YAML metadata headers
11+
- **Automated runs** -- individual analysis executions with a formal state machine
12+
(NOT_RUN → EXTRACTION → MEASUREMENT → SUCCESS/FAILED/TRUNCATED/CANCELED)
13+
- **Python scripts** -- per-run PyScripts that program extraction, measurement,
14+
and post-measurement behavior
15+
- **Conditionals** -- runtime rules that can truncate, terminate, cancel, or
16+
modify the queue based on live data (isotope signals, pressures, ages, ratios)
17+
- **Overlap mode** -- concurrent execution where extraction of run N+1 starts
18+
in a thread while measurement of run N is still running
19+
- **Data persistence** -- saving results to database, DVC, and Excel
20+
- **Statistics** -- run timing, weighted means, MSWD, ETF tracking
21+
- **Scheduling** -- delayed start and scheduled stop
22+
23+
This package **orchestrates** hardware subsystems; it does **not** own hardware
24+
communication (see `extraction_line/`, `spectrometer/`, `lasers/`) or data
25+
reduction (see `processing/`).
26+
27+
## Entry Points
28+
29+
| Class | File | Role |
30+
|-------|------|------|
31+
| `Experimentor` | `experimentor.py` | Top-level facade; owns factory, queue, executor |
32+
| `ExperimentFactory` | `factory.py` | Builds queues and run specs from UI input |
33+
| `ExperimentQueue` | `queue/experiment_queue.py` | Main queue class; manages run list |
34+
| `ExperimentExecutor` | `experiment_executor.py` (~2900 lines) | Core execution engine |
35+
| `AutomatedRun` | `automated_run/automated_run.py` (~3350 lines) | Executes a single analysis |
36+
| `AutomatedRunSpec` | `automated_run/spec.py` | Data model for a queued run |
37+
| `BaseScript` | `script/script.py` | Per-run PyScript management |
38+
| `Datahub` | `datahub.py` | Central data store manager (DVC, MassSpec DB) |
39+
| `ExperimentEditorTask` | `tasks/experiment_task.py` | Envisage task (UI integration) |
40+
41+
## Critical Files
42+
43+
```
44+
experiment/
45+
├── experimentor.py # Top-level manager / facade
46+
├── experiment_executor.py # Core execution engine (~2900 lines)
47+
├── factory.py # ExperimentFactory / AutomatedRunFactory
48+
├── experiment_queue/
49+
│ ├── experiment_queue.py # Main queue class
50+
│ ├── base_queue.py # Load/dump tab-delimited files
51+
│ ├── run_block.py # Groups runs into blocks
52+
│ ├── parser.py # RunParser / UVRunParser
53+
│ └── factory.py # ExperimentQueueFactory
54+
├── automated_run/
55+
│ ├── automated_run.py # Single analysis executor (~3350 lines)
56+
│ ├── spec.py # Run data model
57+
│ ├── state_machine.py # Formal state machine
58+
│ ├── multi_collector.py # Data collector (multi-collector)
59+
│ ├── peak_hop_collector.py # Data collector (peak-hopping)
60+
│ └── persistence.py # Save to DB / DVC / Excel
61+
├── conditional/
62+
│ ├── conditional.py # All conditional types (~900 lines)
63+
│ └── utilities.py # Tokenizer for conditional expressions
64+
├── script/
65+
│ └── script.py # PyScript loading and validation
66+
├── datahub.py # Central data store manager
67+
├── experiment_status.py # UI status display
68+
├── experiment_scheduler.py # Delayed start / scheduled stop
69+
├── conflict_resolver.py # Repository identifier conflicts
70+
├── events.py # Hook system (5 event levels)
71+
├── stats.py # Run timing and statistics
72+
├── utilities/
73+
│ ├── identifier.py # Labnumber parsing and formatting
74+
│ ├── runid.py # Run ID manipulation
75+
│ ├── position_regex.py # Position expression parsing
76+
│ ├── conditionals.py # Conditional constants
77+
│ └── comment_template.py # Comment templating
78+
└── tasks/
79+
└── experiment_task.py # Envisage task (UI panes)
80+
```
81+
82+
## Runtime Lifecycle
83+
84+
### Phase 0: Setup
85+
User configures an `ExperimentQueue` via `ExperimentFactory` UI, adding
86+
`AutomatedRunSpec` entries. Each spec has: labnumber, aliquot, position,
87+
scripts (extraction, measurement, post-equilibration, post-measurement),
88+
and parameters (duration, extract value, cleanup times, etc.).
89+
90+
### Phase 1: Execute
91+
`ExperimentExecutor.execute()` runs pre-execute checks (hardware connectivity,
92+
script existence), then starts a new thread `"Execute Queues"`.
93+
94+
### Phase 2: Queue Loop
95+
Iterates over all open experiment queues. For each queue: pre-queue check,
96+
then `_execute_queue()`.
97+
98+
### Phase 3: Run Loop
99+
Gets a run generator yielding `AutomatedRunSpec` objects. For each spec:
100+
pre-run check → `_make_run()` → execute run (sync or thread for overlap mode).
101+
102+
### Phase 4: Individual Run
103+
Each run executes four steps sequentially:
104+
```
105+
_start → _extraction → _measurement → _post_measurement
106+
```
107+
- **`_start`**: Sets integration time, calls `run.start()`
108+
- **`_extraction`**: Runs extraction PyScript, monitors extraction line
109+
- **`_measurement`**: Runs measurement PyScript with data collection
110+
(multi-collector or peak-hop). Equilibration scripts run concurrently.
111+
- **`_post_measurement`**: Runs post-measurement PyScript
112+
113+
After steps: `run.save()` → post-run check → `run.finish()``run.teardown()`
114+
115+
### Phase 5: Completion
116+
`_end_runs()` stops stats timer, `END_QUEUE` event fires, `alive=False`.
117+
118+
### Overlap Mode
119+
If a run's `overlap` flag is set, extraction runs in a separate thread while
120+
the next run's measurement can begin. Critical for throughput when laser
121+
heating takes longer than measurement setup.
122+
123+
### Event System
124+
Five event levels allow external hooks:
125+
`START_QUEUE``START_RUN``SAVE_RUN``END_RUN``END_QUEUE`
126+
127+
## Test Strategy
128+
129+
Tests live in `pychron/experiment/tests/` (21 test files). Uses standard
130+
`unittest` framework.
131+
132+
| Test File | Coverage |
133+
|-----------|----------|
134+
| `state_machine.py` | State machine transitions (nominal, terminal, abort, reset) |
135+
| `identifier.py` | `get_analysis_type()` for all special identifiers |
136+
| `conditionals.py` | Conditional parsing, tokenization, evaluation |
137+
| `conditionals_actions.py` | Conditional action execution |
138+
| `pyscript_integration.py` | Script name resolution, loading validation |
139+
| `frequency_test.py` | Frequency run generation |
140+
| `position_regex_test.py` | Position regex matching |
141+
| `analysis_grouping_test.py` | Analysis grouping logic |
142+
| `editor_executor_sync.py` | Editor-executor synchronization |
143+
| `repository_identifier.py` | Repository identifier handling |
144+
145+
**Notable gaps**: No unit tests for `ExperimentExecutor`, `AutomatedRun`,
146+
`ExperimentQueue`, or `RunParser` -- too tightly coupled to hardware and Qt.
147+
148+
## Common Failure Modes
149+
150+
| Failure | Symptom | Where |
151+
|---------|---------|-------|
152+
| Pre-execute check failure | Warning dialog, `alive=False` | `experiment_executor.py` |
153+
| Pre-run check failure | Run skipped, logged | `experiment_executor.py` |
154+
| Step failure | Run transitions to FAILED, queue stops | `experiment_executor.py` |
155+
| Monitor fatal error | Run canceled and failed | `automated_run/` |
156+
| DVC save timeout (>5 min) | Run canceled, marked FAILED | `experiment_executor.py` |
157+
| Post-run check failure | `_err_message` set, queue stops | `experiment_executor.py` |
158+
| User cancel/stop/abort | Three levels of intervention | `experiment_executor.py` |
159+
| Repository conflicts | Same labnumber, different identifiers | `conflict_resolver.py` |
160+
| Database errors | `DatabaseError` caught, logged | `experiment_executor.py` |
161+
162+
### Exception Classes
163+
- `ExtractionException` -- extraction hardware failures
164+
- `PreExecuteCheckException` -- pre-execute validation failures
165+
- `PreExtractionCheckException` -- pre-extraction check failures
166+
- `CheckException` -- base for check failures with tag
167+
- `MessageException` -- generic message+error exception
168+
169+
### User Intervention Levels
170+
- **Stop**: Sets `alive=False`, waits for current run to finish
171+
- **Cancel**: Sets `_canceled=True`, cancels current run (30s confirmation timeout)
172+
- **Abort**: Sets `_aborted=True`, immediately aborts running extraction/measurement

0 commit comments

Comments
 (0)