thesean 0.1.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- thesean-0.1.4/LICENSE +21 -0
- thesean-0.1.4/PKG-INFO +98 -0
- thesean-0.1.4/README.md +59 -0
- thesean-0.1.4/pyproject.toml +99 -0
- thesean-0.1.4/setup.cfg +4 -0
- thesean-0.1.4/src/thesean/__init__.py +3 -0
- thesean-0.1.4/src/thesean/__main__.py +5 -0
- thesean-0.1.4/src/thesean/adapters/__init__.py +0 -0
- thesean-0.1.4/src/thesean/adapters/f1/__init__.py +0 -0
- thesean-0.1.4/src/thesean/adapters/f1/controllers.py +56 -0
- thesean-0.1.4/src/thesean/adapters/f1/degrade.py +31 -0
- thesean-0.1.4/src/thesean/adapters/f1/env.py +55 -0
- thesean-0.1.4/src/thesean/adapters/f1/factory.py +186 -0
- thesean-0.1.4/src/thesean/adapters/f1/live_viewer.py +143 -0
- thesean-0.1.4/src/thesean/adapters/f1/planner.py +60 -0
- thesean-0.1.4/src/thesean/adapters/f1/signals.py +352 -0
- thesean-0.1.4/src/thesean/adapters/f1/world_model.py +117 -0
- thesean-0.1.4/src/thesean/adapters/registry.py +35 -0
- thesean-0.1.4/src/thesean/cli/__init__.py +0 -0
- thesean-0.1.4/src/thesean/cli/app.py +21 -0
- thesean-0.1.4/src/thesean/cli/version_check.py +32 -0
- thesean-0.1.4/src/thesean/cli/wizard/__init__.py +3 -0
- thesean-0.1.4/src/thesean/cli/wizard/discovery.py +65 -0
- thesean-0.1.4/src/thesean/cli/wizard/models.py +38 -0
- thesean-0.1.4/src/thesean/cli/wizard/questions.py +174 -0
- thesean-0.1.4/src/thesean/cli/wizard/review.py +54 -0
- thesean-0.1.4/src/thesean/cli/wizard/service.py +181 -0
- thesean-0.1.4/src/thesean/core/__init__.py +0 -0
- thesean-0.1.4/src/thesean/core/config.py +77 -0
- thesean-0.1.4/src/thesean/core/contracts.py +129 -0
- thesean-0.1.4/src/thesean/core/signal_schema.py +54 -0
- thesean-0.1.4/src/thesean/evaluation/__init__.py +0 -0
- thesean-0.1.4/src/thesean/evaluation/metrics/__init__.py +24 -0
- thesean-0.1.4/src/thesean/evaluation/metrics/offtrack.py +30 -0
- thesean-0.1.4/src/thesean/evaluation/metrics/prediction_error.py +71 -0
- thesean-0.1.4/src/thesean/evaluation/metrics/progress.py +22 -0
- thesean-0.1.4/src/thesean/evaluation/metrics/reward.py +22 -0
- thesean-0.1.4/src/thesean/evaluation/metrics/survival.py +22 -0
- thesean-0.1.4/src/thesean/models/__init__.py +42 -0
- thesean-0.1.4/src/thesean/models/case.py +30 -0
- thesean-0.1.4/src/thesean/models/comparison.py +30 -0
- thesean-0.1.4/src/thesean/models/episode.py +81 -0
- thesean-0.1.4/src/thesean/models/evaluation_result.py +51 -0
- thesean-0.1.4/src/thesean/models/event.py +40 -0
- thesean-0.1.4/src/thesean/models/evidence.py +18 -0
- thesean-0.1.4/src/thesean/models/explanation.py +23 -0
- thesean-0.1.4/src/thesean/models/isolation.py +35 -0
- thesean-0.1.4/src/thesean/models/manifest.py +24 -0
- thesean-0.1.4/src/thesean/models/metric.py +14 -0
- thesean-0.1.4/src/thesean/models/project.py +25 -0
- thesean-0.1.4/src/thesean/models/run.py +50 -0
- thesean-0.1.4/src/thesean/models/signal.py +14 -0
- thesean-0.1.4/src/thesean/models/swap.py +25 -0
- thesean-0.1.4/src/thesean/pipeline/__init__.py +0 -0
- thesean-0.1.4/src/thesean/pipeline/case_io.py +22 -0
- thesean-0.1.4/src/thesean/pipeline/compare/__init__.py +4 -0
- thesean-0.1.4/src/thesean/pipeline/compare/decision.py +20 -0
- thesean-0.1.4/src/thesean/pipeline/compare/execution.py +50 -0
- thesean-0.1.4/src/thesean/pipeline/compare/service.py +95 -0
- thesean-0.1.4/src/thesean/pipeline/compare/stats.py +60 -0
- thesean-0.1.4/src/thesean/pipeline/compare_module.py +258 -0
- thesean-0.1.4/src/thesean/pipeline/context.py +204 -0
- thesean-0.1.4/src/thesean/pipeline/episodes.py +109 -0
- thesean-0.1.4/src/thesean/pipeline/event_extraction.py +254 -0
- thesean-0.1.4/src/thesean/pipeline/events/__init__.py +6 -0
- thesean-0.1.4/src/thesean/pipeline/events/config.py +41 -0
- thesean-0.1.4/src/thesean/pipeline/events/detection.py +231 -0
- thesean-0.1.4/src/thesean/pipeline/events/divergence.py +106 -0
- thesean-0.1.4/src/thesean/pipeline/isolation/__init__.py +4 -0
- thesean-0.1.4/src/thesean/pipeline/isolation/attribution.py +187 -0
- thesean-0.1.4/src/thesean/pipeline/isolation/designs.py +35 -0
- thesean-0.1.4/src/thesean/pipeline/isolation/executor.py +60 -0
- thesean-0.1.4/src/thesean/pipeline/isolation/planner.py +10 -0
- thesean-0.1.4/src/thesean/pipeline/live_update.py +59 -0
- thesean-0.1.4/src/thesean/pipeline/metrics_util.py +65 -0
- thesean-0.1.4/src/thesean/pipeline/paired_runner.py +185 -0
- thesean-0.1.4/src/thesean/pipeline/runner.py +109 -0
- thesean-0.1.4/src/thesean/pipeline/stages/__init__.py +22 -0
- thesean-0.1.4/src/thesean/pipeline/stages/attribute.py +78 -0
- thesean-0.1.4/src/thesean/pipeline/stages/base.py +18 -0
- thesean-0.1.4/src/thesean/pipeline/stages/compare.py +37 -0
- thesean-0.1.4/src/thesean/pipeline/stages/events.py +53 -0
- thesean-0.1.4/src/thesean/pipeline/stages/isolate.py +88 -0
- thesean-0.1.4/src/thesean/pipeline/stages/report.py +59 -0
- thesean-0.1.4/src/thesean/pipeline/staleness.py +33 -0
- thesean-0.1.4/src/thesean/pipeline/state.py +31 -0
- thesean-0.1.4/src/thesean/pipeline/workspace.py +176 -0
- thesean-0.1.4/src/thesean/reporting/__init__.py +0 -0
- thesean-0.1.4/src/thesean/reporting/bundle.py +74 -0
- thesean-0.1.4/src/thesean/reporting/evidence.py +28 -0
- thesean-0.1.4/src/thesean/reporting/renderers/__init__.py +0 -0
- thesean-0.1.4/src/thesean/reporting/renderers/console.py +27 -0
- thesean-0.1.4/src/thesean/reporting/renderers/html.py +28 -0
- thesean-0.1.4/src/thesean/reporting/renderers/json.py +13 -0
- thesean-0.1.4/src/thesean/reporting/templates/investigation_report.html.j2 +85 -0
- thesean-0.1.4/src/thesean/reporting/templates/report.html.j2 +99 -0
- thesean-0.1.4/src/thesean/reporting/types.py +33 -0
- thesean-0.1.4/src/thesean/tui/__init__.py +0 -0
- thesean-0.1.4/src/thesean/tui/actions.py +78 -0
- thesean-0.1.4/src/thesean/tui/app.py +1218 -0
- thesean-0.1.4/src/thesean/tui/detection.py +241 -0
- thesean-0.1.4/src/thesean/tui/screens/__init__.py +1 -0
- thesean-0.1.4/src/thesean/tui/screens/attribution.py +252 -0
- thesean-0.1.4/src/thesean/tui/screens/case_history.py +132 -0
- thesean-0.1.4/src/thesean/tui/screens/case_verdict.py +663 -0
- thesean-0.1.4/src/thesean/tui/screens/command_palette.py +70 -0
- thesean-0.1.4/src/thesean/tui/screens/drawers/__init__.py +1 -0
- thesean-0.1.4/src/thesean/tui/screens/drawers/context_drawer.py +90 -0
- thesean-0.1.4/src/thesean/tui/screens/drawers/evidence_drawer.py +113 -0
- thesean-0.1.4/src/thesean/tui/screens/error_modal.py +54 -0
- thesean-0.1.4/src/thesean/tui/screens/investigation.py +690 -0
- thesean-0.1.4/src/thesean/tui/screens/run_builder.py +492 -0
- thesean-0.1.4/src/thesean/tui/screens/workspace_picker.py +69 -0
- thesean-0.1.4/src/thesean/tui/services.py +765 -0
- thesean-0.1.4/src/thesean/tui/state.py +137 -0
- thesean-0.1.4/src/thesean/tui/styles/app.tcss +224 -0
- thesean-0.1.4/src/thesean/tui/views/__init__.py +0 -0
- thesean-0.1.4/src/thesean/tui/widgets/__init__.py +0 -0
- thesean-0.1.4/src/thesean/tui/widgets/action_bar.py +44 -0
- thesean-0.1.4/src/thesean/tui/widgets/artifact_list.py +38 -0
- thesean-0.1.4/src/thesean/tui/widgets/artifact_preview.py +34 -0
- thesean-0.1.4/src/thesean/tui/widgets/attribution_decision_card.py +55 -0
- thesean-0.1.4/src/thesean/tui/widgets/case_bar.py +108 -0
- thesean-0.1.4/src/thesean/tui/widgets/causal_sequence.py +72 -0
- thesean-0.1.4/src/thesean/tui/widgets/comparability_summary.py +48 -0
- thesean-0.1.4/src/thesean/tui/widgets/context_rail.py +69 -0
- thesean-0.1.4/src/thesean/tui/widgets/effect_table.py +32 -0
- thesean-0.1.4/src/thesean/tui/widgets/event_navigator.py +176 -0
- thesean-0.1.4/src/thesean/tui/widgets/explanation_card.py +67 -0
- thesean-0.1.4/src/thesean/tui/widgets/falsifier_list.py +72 -0
- thesean-0.1.4/src/thesean/tui/widgets/focus_signals_strip.py +22 -0
- thesean-0.1.4/src/thesean/tui/widgets/help_overlay.py +84 -0
- thesean-0.1.4/src/thesean/tui/widgets/isolation_case_detail.py +67 -0
- thesean-0.1.4/src/thesean/tui/widgets/isolation_case_table.py +50 -0
- thesean-0.1.4/src/thesean/tui/widgets/live_run_monitor.py +353 -0
- thesean-0.1.4/src/thesean/tui/widgets/metric_detail.py +95 -0
- thesean-0.1.4/src/thesean/tui/widgets/metric_table.py +73 -0
- thesean-0.1.4/src/thesean/tui/widgets/progress_summary.py +299 -0
- thesean-0.1.4/src/thesean/tui/widgets/run_config_panel.py +163 -0
- thesean-0.1.4/src/thesean/tui/widgets/run_monitor.py +91 -0
- thesean-0.1.4/src/thesean/tui/widgets/section_title.py +15 -0
- thesean-0.1.4/src/thesean/tui/widgets/signal_timeline.py +206 -0
- thesean-0.1.4/src/thesean/tui/widgets/status_badge.py +54 -0
- thesean-0.1.4/src/thesean/tui/widgets/step_inspector.py +105 -0
- thesean-0.1.4/src/thesean/tui/widgets/tier_indicator.py +43 -0
- thesean-0.1.4/src/thesean/tui/widgets/track_map.py +136 -0
- thesean-0.1.4/src/thesean/tui/widgets/transport_bar.py +152 -0
- thesean-0.1.4/src/thesean/tui/widgets/verdict_strip.py +103 -0
- thesean-0.1.4/src/thesean.egg-info/PKG-INFO +98 -0
- thesean-0.1.4/src/thesean.egg-info/SOURCES.txt +153 -0
- thesean-0.1.4/src/thesean.egg-info/dependency_links.txt +1 -0
- thesean-0.1.4/src/thesean.egg-info/entry_points.txt +5 -0
- thesean-0.1.4/src/thesean.egg-info/requires.txt +23 -0
- thesean-0.1.4/src/thesean.egg-info/top_level.txt +1 -0
- thesean-0.1.4/tests/test_raster_size_inference.py +93 -0
thesean-0.1.4/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 TheSean
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
thesean-0.1.4/PKG-INFO
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: thesean
|
|
3
|
+
Version: 0.1.4
|
|
4
|
+
Summary: Regression investigation harness for ML pipelines
|
|
5
|
+
Author: Danny Nguyen
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/theseanai/thesean
|
|
8
|
+
Project-URL: Issues, https://github.com/theseanai/thesean/issues
|
|
9
|
+
Keywords: regression,investigation,ml,machine-learning,debugging,pipeline
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: typer<1,>=0.9.0
|
|
19
|
+
Requires-Dist: pydantic<3,>=2.0.0
|
|
20
|
+
Requires-Dist: pydantic-settings>=2.0.0
|
|
21
|
+
Requires-Dist: jinja2>=3.1.0
|
|
22
|
+
Requires-Dist: tomli-w>=1.0.0
|
|
23
|
+
Requires-Dist: InquirerPy>=0.3.4
|
|
24
|
+
Requires-Dist: rich>=13.0.0
|
|
25
|
+
Requires-Dist: torch<3,>=2.0.0
|
|
26
|
+
Requires-Dist: numpy<3,>=1.24.0
|
|
27
|
+
Requires-Dist: scipy>=1.11.0
|
|
28
|
+
Requires-Dist: statsmodels>=0.14.0
|
|
29
|
+
Requires-Dist: textual>=0.80.0
|
|
30
|
+
Requires-Dist: tomli>=2.0.0; python_version < "3.11"
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
33
|
+
Requires-Dist: pytest-textual-snapshot; extra == "dev"
|
|
34
|
+
Requires-Dist: hypothesis; extra == "dev"
|
|
35
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
|
|
36
|
+
Requires-Dist: ruff>=0.4.0; extra == "dev"
|
|
37
|
+
Requires-Dist: mypy>=1.8.0; extra == "dev"
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
|
|
40
|
+
# TheSean
|
|
41
|
+
|
|
42
|
+
A regression investigation harness for ML pipelines, latent action systems, and world models.
|
|
43
|
+
|
|
44
|
+
TheSean runs paired A/B evaluations across ML/world model experiments, detects regressions, and provides an interactive investigation workbench for debugging episode-level divergences.
|
|
45
|
+
|
|
46
|
+
## What it does
|
|
47
|
+
|
|
48
|
+
- Configure A/B experiment pairs with different weights, planners, or configs
|
|
49
|
+
- Run paired evaluations with live telemetry
|
|
50
|
+
- Detect regressions via bootstrap significance testing across 5 metrics
|
|
51
|
+
- Drill into individual episodes with signal timelines and event detection
|
|
52
|
+
|
|
53
|
+
## Quickstart
|
|
54
|
+
|
|
55
|
+
Python 3.10+ required.
|
|
56
|
+
|
|
57
|
+
### Install
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install thesean
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Prerequisites: TheSean requires an ML repo with a compatible adapter.
|
|
64
|
+
|
|
65
|
+
The only available adapter is for [f1worldmodel](https://github.com/justinsiek/f1worldmodel).
|
|
66
|
+
|
|
67
|
+
Clone the repo and follow its README to install dependencies.
|
|
68
|
+
|
|
69
|
+
### Run
|
|
70
|
+
|
|
71
|
+
cd into the ML repo project root:
|
|
72
|
+
|
|
73
|
+
Example:
|
|
74
|
+
```bash
|
|
75
|
+
cd f1worldmodel
|
|
76
|
+
thesean
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
This launches the TUI. From there:
|
|
80
|
+
|
|
81
|
+
1. Select or create a case.
|
|
82
|
+
2. Pick a track, configure Run A (baseline) and Run B (candidate) with different checkpoints or planner settings
|
|
83
|
+
3. Run the evaluation
|
|
84
|
+
4. View the verdict and drill into episodes to investigate divergences
|
|
85
|
+
|
|
86
|
+
## Available Adapters
|
|
87
|
+
|
|
88
|
+
| Adapter | Repo | Status |
|
|
89
|
+
|---------|------|--------|
|
|
90
|
+
| `f1` | [justinsiek/f1worldmodel](https://github.com/justinsiek/f1worldmodel) | Beta |
|
|
91
|
+
|
|
92
|
+
## Status
|
|
93
|
+
|
|
94
|
+
Beta. APIs may change.
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
|
|
98
|
+
MIT
|
thesean-0.1.4/README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# TheSean
|
|
2
|
+
|
|
3
|
+
A regression investigation harness for ML pipelines, latent action systems, and world models.
|
|
4
|
+
|
|
5
|
+
TheSean runs paired A/B evaluations across ML/world model experiments, detects regressions, and provides an interactive investigation workbench for debugging episode-level divergences.
|
|
6
|
+
|
|
7
|
+
## What it does
|
|
8
|
+
|
|
9
|
+
- Configure A/B experiment pairs with different weights, planners, or configs
|
|
10
|
+
- Run paired evaluations with live telemetry
|
|
11
|
+
- Detect regressions via bootstrap significance testing across 5 metrics
|
|
12
|
+
- Drill into individual episodes with signal timelines and event detection
|
|
13
|
+
|
|
14
|
+
## Quickstart
|
|
15
|
+
|
|
16
|
+
Python 3.10+ required.
|
|
17
|
+
|
|
18
|
+
### Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install thesean
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Prerequisites: TheSean requires an ML repo with a compatible adapter.
|
|
25
|
+
|
|
26
|
+
The only available adapter is for [f1worldmodel](https://github.com/justinsiek/f1worldmodel).
|
|
27
|
+
|
|
28
|
+
Clone the repo and follow its README to install dependencies.
|
|
29
|
+
|
|
30
|
+
### Run
|
|
31
|
+
|
|
32
|
+
cd into the ML repo project root:
|
|
33
|
+
|
|
34
|
+
Example:
|
|
35
|
+
```bash
|
|
36
|
+
cd f1worldmodel
|
|
37
|
+
thesean
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
This launches the TUI. From there:
|
|
41
|
+
|
|
42
|
+
1. Select or create a case.
|
|
43
|
+
2. Pick a track, configure Run A (baseline) and Run B (candidate) with different checkpoints or planner settings
|
|
44
|
+
3. Run the evaluation
|
|
45
|
+
4. View the verdict and drill into episodes to investigate divergences
|
|
46
|
+
|
|
47
|
+
## Available Adapters
|
|
48
|
+
|
|
49
|
+
| Adapter | Repo | Status |
|
|
50
|
+
|---------|------|--------|
|
|
51
|
+
| `f1` | [justinsiek/f1worldmodel](https://github.com/justinsiek/f1worldmodel) | Beta |
|
|
52
|
+
|
|
53
|
+
## Status
|
|
54
|
+
|
|
55
|
+
Beta. APIs may change.
|
|
56
|
+
|
|
57
|
+
## License
|
|
58
|
+
|
|
59
|
+
MIT
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "thesean"
|
|
7
|
+
version = "0.1.4"
|
|
8
|
+
description = "Regression investigation harness for ML pipelines"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
authors = [{name = "Danny Nguyen"}]
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
keywords = ["regression", "investigation", "ml", "machine-learning", "debugging", "pipeline"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.10",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
dependencies = [
|
|
23
|
+
"typer>=0.9.0,<1",
|
|
24
|
+
"pydantic>=2.0.0,<3",
|
|
25
|
+
"pydantic-settings>=2.0.0",
|
|
26
|
+
"jinja2>=3.1.0",
|
|
27
|
+
"tomli-w>=1.0.0",
|
|
28
|
+
"InquirerPy>=0.3.4",
|
|
29
|
+
"rich>=13.0.0",
|
|
30
|
+
"torch>=2.0.0,<3",
|
|
31
|
+
"numpy>=1.24.0,<3",
|
|
32
|
+
"scipy>=1.11.0",
|
|
33
|
+
"statsmodels>=0.14.0",
|
|
34
|
+
"textual>=0.80.0",
|
|
35
|
+
"tomli>=2.0.0; python_version < '3.11'",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://github.com/theseanai/thesean"
|
|
40
|
+
Issues = "https://github.com/theseanai/thesean/issues"
|
|
41
|
+
|
|
42
|
+
[project.optional-dependencies]
|
|
43
|
+
dev = [
|
|
44
|
+
"pytest>=7.0",
|
|
45
|
+
"pytest-textual-snapshot",
|
|
46
|
+
"hypothesis",
|
|
47
|
+
"pytest-asyncio>=0.23.0",
|
|
48
|
+
"ruff>=0.4.0",
|
|
49
|
+
"mypy>=1.8.0",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
[project.entry-points."thesean.adapters"]
|
|
53
|
+
f1 = "thesean.adapters.f1.factory:F1AdapterFactory"
|
|
54
|
+
|
|
55
|
+
[project.scripts]
|
|
56
|
+
thesean = "thesean.cli.app:app"
|
|
57
|
+
|
|
58
|
+
[tool.setuptools.packages.find]
|
|
59
|
+
where = ["src"]
|
|
60
|
+
|
|
61
|
+
[tool.setuptools.package-data]
|
|
62
|
+
thesean = ["tui/styles/*.tcss", "reporting/templates/*.j2"]
|
|
63
|
+
|
|
64
|
+
[tool.ruff]
|
|
65
|
+
target-version = "py310"
|
|
66
|
+
line-length = 120
|
|
67
|
+
|
|
68
|
+
[tool.ruff.lint]
|
|
69
|
+
select = ["E", "F", "I", "UP", "B", "SIM"]
|
|
70
|
+
ignore = ["B008", "B904", "SIM102", "SIM105", "SIM108", "SIM117", "UP037", "UP038"]
|
|
71
|
+
|
|
72
|
+
[tool.mypy]
|
|
73
|
+
python_version = "3.10"
|
|
74
|
+
warn_return_any = true
|
|
75
|
+
warn_unused_configs = true
|
|
76
|
+
ignore_missing_imports = true
|
|
77
|
+
|
|
78
|
+
[[tool.mypy.overrides]]
|
|
79
|
+
module = ["thesean.models.*", "thesean.reporting.*", "thesean.pipeline.context", "thesean.pipeline.stages.*", "thesean.core.contracts"]
|
|
80
|
+
disallow_untyped_defs = true
|
|
81
|
+
|
|
82
|
+
[[tool.mypy.overrides]]
|
|
83
|
+
module = ["thesean.pipeline.compare.*", "thesean.pipeline.isolation.attribution"]
|
|
84
|
+
disallow_untyped_defs = false
|
|
85
|
+
warn_return_any = false
|
|
86
|
+
|
|
87
|
+
[tool.pytest.ini_options]
|
|
88
|
+
testpaths = ["tests"]
|
|
89
|
+
markers = [
|
|
90
|
+
"unit: fast isolated tests",
|
|
91
|
+
"integration: tests spanning multiple components",
|
|
92
|
+
"smoke: CLI entry-point smoke tests",
|
|
93
|
+
"property: hypothesis property-based tests",
|
|
94
|
+
"snapshot: textual snapshot tests",
|
|
95
|
+
"tui: tests requiring Textual runtime",
|
|
96
|
+
"f1: tests that require the real F1 adapter/repo",
|
|
97
|
+
"framework: hermetic framework tests",
|
|
98
|
+
]
|
|
99
|
+
asyncio_default_fixture_loop_scope = "function"
|
thesean-0.1.4/setup.cfg
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Scripted controller adapter — wraps F1 scripted policies as PlannerPlugin."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ScriptedControllerAdapter:
|
|
12
|
+
"""Wraps a scripted controller to satisfy PlannerPlugin protocol.
|
|
13
|
+
|
|
14
|
+
This enables the existing episode runner (run_episodes) to work with
|
|
15
|
+
scripted controllers without a separate code path.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, controller_cls: type, track: Any | None = None) -> None:
|
|
19
|
+
self._controller_cls = controller_cls
|
|
20
|
+
self._track = track
|
|
21
|
+
self._accepts_car_state = self._check_car_state(controller_cls)
|
|
22
|
+
self._controller = self._make_controller()
|
|
23
|
+
|
|
24
|
+
@staticmethod
|
|
25
|
+
def _check_car_state(cls: type) -> bool:
|
|
26
|
+
"""Check if the controller's __call__ accepts a car_state kwarg."""
|
|
27
|
+
sig = inspect.signature(cls.__call__)
|
|
28
|
+
return "car_state" in sig.parameters
|
|
29
|
+
|
|
30
|
+
def _make_controller(self) -> Any:
|
|
31
|
+
"""Instantiate the controller, passing track if the constructor accepts it."""
|
|
32
|
+
sig = inspect.signature(self._controller_cls)
|
|
33
|
+
if "track" in sig.parameters and self._track is not None:
|
|
34
|
+
return self._controller_cls(self._track)
|
|
35
|
+
return self._controller_cls()
|
|
36
|
+
|
|
37
|
+
def planner_id(self) -> str:
|
|
38
|
+
return f"scripted_{self._controller_cls.__name__}"
|
|
39
|
+
|
|
40
|
+
def configure(self, config: dict[str, Any], world_model: Any = None) -> None:
|
|
41
|
+
"""No-op for scripted controllers."""
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
def reset(self) -> None:
|
|
45
|
+
"""Re-instantiate the controller to reset all internal state.
|
|
46
|
+
|
|
47
|
+
This is safer than manually resetting step_counter/panic_counter etc
|
|
48
|
+
because it handles any future stateful controllers automatically.
|
|
49
|
+
"""
|
|
50
|
+
self._controller = self._make_controller()
|
|
51
|
+
|
|
52
|
+
def act(self, obs: dict[str, Any], car_state: dict[str, Any] | None = None) -> np.ndarray:
|
|
53
|
+
"""Delegate to the underlying controller's __call__."""
|
|
54
|
+
if self._accepts_car_state:
|
|
55
|
+
return self._controller(obs, car_state=car_state) # type: ignore[no-any-return]
|
|
56
|
+
return self._controller(obs) # type: ignore[no-any-return]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import torch
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def degrade_model(
|
|
7
|
+
src_path: str,
|
|
8
|
+
dst_path: str,
|
|
9
|
+
noise_scale: float = 0.3,
|
|
10
|
+
) -> None:
|
|
11
|
+
"""Load a world model checkpoint, add noise to predictor/head weights, save."""
|
|
12
|
+
state_dict = torch.load(src_path, map_location="cpu", weights_only=True)
|
|
13
|
+
|
|
14
|
+
degraded = {}
|
|
15
|
+
for key, tensor in state_dict.items():
|
|
16
|
+
if key.startswith("predictor.") or "_head." in key:
|
|
17
|
+
noise = torch.randn_like(tensor) * noise_scale * tensor.abs().mean()
|
|
18
|
+
degraded[key] = tensor + noise
|
|
19
|
+
else:
|
|
20
|
+
degraded[key] = tensor.clone()
|
|
21
|
+
|
|
22
|
+
torch.save(degraded, dst_path)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def load_model(weights_path: str, device: str = "cpu"):
|
|
26
|
+
"""Convenience: load a WorldModel with weights."""
|
|
27
|
+
from thesean.adapters.f1.world_model import F1WorldModelAdapter
|
|
28
|
+
|
|
29
|
+
adapter = F1WorldModelAdapter()
|
|
30
|
+
adapter.load(weights_path, device=device)
|
|
31
|
+
return adapter
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class F1EnvAdapter:
|
|
9
|
+
"""Wraps env.f1_env.F1Env to satisfy EnvPlugin protocol."""
|
|
10
|
+
|
|
11
|
+
def __init__(self) -> None:
|
|
12
|
+
self._env = None
|
|
13
|
+
|
|
14
|
+
def env_id(self) -> str:
|
|
15
|
+
return "f1_env"
|
|
16
|
+
|
|
17
|
+
def configure(self, config: dict[str, Any]) -> None:
|
|
18
|
+
import os
|
|
19
|
+
import threading
|
|
20
|
+
|
|
21
|
+
# Prevent pygame/SDL2 from initializing Cocoa display when imported
|
|
22
|
+
# from a non-main thread (causes SIGABRT on macOS).
|
|
23
|
+
if threading.current_thread() is not threading.main_thread():
|
|
24
|
+
os.environ.setdefault("SDL_VIDEODRIVER", "dummy")
|
|
25
|
+
|
|
26
|
+
from configs.default import Config
|
|
27
|
+
from env.f1_env import F1Env
|
|
28
|
+
|
|
29
|
+
accepted = {k: v for k, v in config.items() if hasattr(Config, k)}
|
|
30
|
+
dropped = set(config.keys()) - set(accepted.keys())
|
|
31
|
+
if dropped:
|
|
32
|
+
import logging
|
|
33
|
+
logging.getLogger(__name__).warning(
|
|
34
|
+
"F1EnvAdapter: config keys dropped (not in F1 Config): %s", sorted(dropped)
|
|
35
|
+
)
|
|
36
|
+
cfg = Config(**accepted)
|
|
37
|
+
self._env = F1Env.from_config(cfg)
|
|
38
|
+
|
|
39
|
+
def reset(self, seed: int | None = None) -> dict[str, Any]:
|
|
40
|
+
if self._env is None:
|
|
41
|
+
raise RuntimeError("configure() must be called before reset()")
|
|
42
|
+
# F1Env.reset() does not accept a seed parameter. Global RNG seeding
|
|
43
|
+
# is handled by the runner. If F1Env adds seed support, forward it here.
|
|
44
|
+
return self._env.reset()
|
|
45
|
+
|
|
46
|
+
def step(self, action: np.ndarray) -> tuple[dict, float, bool, dict]:
|
|
47
|
+
if self._env is None:
|
|
48
|
+
raise RuntimeError("configure() must be called before step()")
|
|
49
|
+
return self._env.step(action)
|
|
50
|
+
|
|
51
|
+
def get_car_state(self) -> dict[str, Any]:
|
|
52
|
+
return self._env.get_car_state() # type: ignore[attr-defined, no-any-return]
|
|
53
|
+
|
|
54
|
+
def get_progress(self) -> float:
|
|
55
|
+
return self._env.get_progress() # type: ignore[attr-defined, no-any-return]
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""F1-specific adapter factory — the single place that knows about F1."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from thesean.adapters.f1.env import F1EnvAdapter
|
|
11
|
+
from thesean.adapters.f1.planner import F1PlannerAdapter
|
|
12
|
+
from thesean.adapters.f1.signals import F1SignalTranslator
|
|
13
|
+
from thesean.adapters.f1.world_model import F1WorldModelAdapter
|
|
14
|
+
from thesean.core.contracts import (
|
|
15
|
+
EnvPlugin,
|
|
16
|
+
MetricPlugin,
|
|
17
|
+
PanelProvider,
|
|
18
|
+
PlannerPlugin,
|
|
19
|
+
SignalTranslator,
|
|
20
|
+
WorldModelPlugin,
|
|
21
|
+
)
|
|
22
|
+
from thesean.evaluation.metrics import ALL_METRICS
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class F1AdapterFactory:
|
|
26
|
+
"""Implements AdapterFactory using F1 adapters."""
|
|
27
|
+
|
|
28
|
+
def __init__(self) -> None:
|
|
29
|
+
self._repo: Path | None = None
|
|
30
|
+
|
|
31
|
+
def bind_repo(self, repo: Path) -> None:
|
|
32
|
+
"""Prepare the F1 repo for use: validate and add to sys.path."""
|
|
33
|
+
repo = repo.expanduser().resolve()
|
|
34
|
+
if not repo.is_dir():
|
|
35
|
+
raise ValueError(f"Adapter repo path does not exist: {repo}")
|
|
36
|
+
self._repo = repo
|
|
37
|
+
repo_str = str(repo)
|
|
38
|
+
if repo_str not in sys.path:
|
|
39
|
+
sys.path.insert(0, repo_str)
|
|
40
|
+
|
|
41
|
+
def create_env(self, config: dict[str, Any]) -> EnvPlugin:
|
|
42
|
+
env = F1EnvAdapter()
|
|
43
|
+
env.configure(config)
|
|
44
|
+
return env
|
|
45
|
+
|
|
46
|
+
def create_world_model(self, weights_path: str, device: str = "cpu") -> WorldModelPlugin:
|
|
47
|
+
wm = F1WorldModelAdapter()
|
|
48
|
+
wm.load(weights_path, device=device)
|
|
49
|
+
return wm
|
|
50
|
+
|
|
51
|
+
def create_planner(self, config: dict[str, Any], world_model: WorldModelPlugin) -> PlannerPlugin:
|
|
52
|
+
planner = F1PlannerAdapter()
|
|
53
|
+
planner.configure(config, world_model)
|
|
54
|
+
return planner
|
|
55
|
+
|
|
56
|
+
def get_metrics(self) -> list[MetricPlugin]:
|
|
57
|
+
return list(ALL_METRICS)
|
|
58
|
+
|
|
59
|
+
def discover_weights(self, repo: Path) -> list[dict[str, Any]]:
|
|
60
|
+
"""Scan checkpoints/*.pth, return [{name, path, size_mb, mtime}, ...] newest-first."""
|
|
61
|
+
ckpt_dir = repo / "checkpoints"
|
|
62
|
+
if not ckpt_dir.is_dir():
|
|
63
|
+
return []
|
|
64
|
+
results = []
|
|
65
|
+
for p in ckpt_dir.glob("*.pth"):
|
|
66
|
+
st = p.stat()
|
|
67
|
+
results.append({
|
|
68
|
+
"name": p.name,
|
|
69
|
+
"path": str(p.resolve()),
|
|
70
|
+
"size_mb": round(st.st_size / 1_048_576, 1),
|
|
71
|
+
"mtime": datetime.fromtimestamp(st.st_mtime, tz=timezone.utc)
|
|
72
|
+
.strftime("%Y-%m-%d %H:%M"),
|
|
73
|
+
})
|
|
74
|
+
return sorted(results, key=lambda w: w["mtime"], reverse=True)
|
|
75
|
+
|
|
76
|
+
def discover_envs(self, repo: Path) -> list[str]:
|
|
77
|
+
"""Scan tracks/*.csv, return sorted track names (stems)."""
|
|
78
|
+
tracks_dir = repo / "tracks"
|
|
79
|
+
if not tracks_dir.is_dir():
|
|
80
|
+
return []
|
|
81
|
+
return sorted(p.stem for p in tracks_dir.glob("*.csv"))
|
|
82
|
+
|
|
83
|
+
def discover_controllers(self) -> list[dict[str, Any]]:
|
|
84
|
+
"""Discover all scripted controller classes from data.controllers.
|
|
85
|
+
|
|
86
|
+
Returns list of dicts: [{"name": "ScriptedPolicy", "requires_track": True}, ...]
|
|
87
|
+
Requires bind_repo() to have been called first (sys.path must include F1 repo).
|
|
88
|
+
"""
|
|
89
|
+
import importlib
|
|
90
|
+
import inspect as _inspect
|
|
91
|
+
|
|
92
|
+
mod = importlib.import_module("data.controllers")
|
|
93
|
+
controllers: list[dict[str, Any]] = []
|
|
94
|
+
for name, cls in _inspect.getmembers(mod, _inspect.isclass):
|
|
95
|
+
if name.endswith("Policy"):
|
|
96
|
+
sig = _inspect.signature(cls.__init__)
|
|
97
|
+
requires_track = "track" in sig.parameters
|
|
98
|
+
controllers.append({
|
|
99
|
+
"name": name,
|
|
100
|
+
"requires_track": requires_track,
|
|
101
|
+
})
|
|
102
|
+
return sorted(controllers, key=lambda c: c["name"])
|
|
103
|
+
|
|
104
|
+
def create_scripted_controller(self, controller_name: str, track: Any = None) -> Any:
|
|
105
|
+
"""Create a ScriptedControllerAdapter for the named controller.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
controller_name: Class name from data.controllers (e.g. "ScriptedPolicy")
|
|
109
|
+
track: Track object (required for track-dependent controllers)
|
|
110
|
+
"""
|
|
111
|
+
import importlib
|
|
112
|
+
|
|
113
|
+
mod = importlib.import_module("data.controllers")
|
|
114
|
+
cls = getattr(mod, controller_name)
|
|
115
|
+
from thesean.adapters.f1.controllers import ScriptedControllerAdapter
|
|
116
|
+
return ScriptedControllerAdapter(cls, track=track)
|
|
117
|
+
|
|
118
|
+
def default_planner_config(self) -> dict[str, Any]:
|
|
119
|
+
return {"num_candidates": 400, "horizon": 25, "iterations": 4, "num_elites": 40}
|
|
120
|
+
|
|
121
|
+
def default_env_config(self, env_id: str, world_model: WorldModelPlugin | None = None) -> dict[str, Any]:
|
|
122
|
+
"""Build full env config for given track name. Paths are absolute if repo is bound.
|
|
123
|
+
|
|
124
|
+
If world_model is provided, raster_size is inferred from checkpoint weights.
|
|
125
|
+
"""
|
|
126
|
+
raster_size = world_model.raster_size if world_model is not None else 64
|
|
127
|
+
track_csv = f"tracks/{env_id}.csv"
|
|
128
|
+
if self._repo is not None:
|
|
129
|
+
track_csv = str(self._repo / track_csv)
|
|
130
|
+
return {
|
|
131
|
+
"track_csv": track_csv,
|
|
132
|
+
"max_speed": 50.0, "dt": 0.1, "max_steps": 1000,
|
|
133
|
+
"max_steer_rate": 3.5, "off_track_tolerance": 10,
|
|
134
|
+
"raster_size": raster_size, "pixels_per_meter": 3.0,
|
|
135
|
+
"progress_reward": 0.02, "off_track_penalty": 0.5,
|
|
136
|
+
"step_penalty": 0.005, "lap_bonus": 1.0,
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
# ── Phase 3 optional methods ──────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
def detect_project(self, repo: Path) -> dict[str, Any]:
|
|
142
|
+
"""Scan F1 repo for available assets."""
|
|
143
|
+
repo = repo.expanduser().resolve()
|
|
144
|
+
assets: dict[str, Any] = {
|
|
145
|
+
"weights": self.discover_weights(repo),
|
|
146
|
+
"envs": self.discover_envs(repo),
|
|
147
|
+
"scenarios": [],
|
|
148
|
+
"configs": [],
|
|
149
|
+
}
|
|
150
|
+
# Scan for config files
|
|
151
|
+
config_dir = repo / "configs"
|
|
152
|
+
if config_dir.is_dir():
|
|
153
|
+
assets["configs"] = sorted(p.name for p in config_dir.glob("*.py"))
|
|
154
|
+
return assets
|
|
155
|
+
|
|
156
|
+
def get_signal_translator(self) -> SignalTranslator | None:
|
|
157
|
+
return F1SignalTranslator()
|
|
158
|
+
|
|
159
|
+
def get_panel_providers(self) -> list[PanelProvider]:
|
|
160
|
+
return [F1TrackPanel()]
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class F1TrackPanel:
|
|
164
|
+
"""PanelProvider for F1 track visualization."""
|
|
165
|
+
|
|
166
|
+
def panel_id(self) -> str:
|
|
167
|
+
return "f1_track"
|
|
168
|
+
|
|
169
|
+
def panel_label(self) -> str:
|
|
170
|
+
return "F1 Track View"
|
|
171
|
+
|
|
172
|
+
def load_track_geometry(self, track_csv: str) -> list[tuple[float, float]]:
|
|
173
|
+
"""Load (x, y) centerline points from a track CSV file."""
|
|
174
|
+
import csv
|
|
175
|
+
|
|
176
|
+
points: list[tuple[float, float]] = []
|
|
177
|
+
with open(track_csv) as f:
|
|
178
|
+
reader = csv.reader(f)
|
|
179
|
+
for row in reader:
|
|
180
|
+
if not row or row[0].strip().startswith("#"):
|
|
181
|
+
continue
|
|
182
|
+
try:
|
|
183
|
+
points.append((float(row[0].strip()), float(row[1].strip())))
|
|
184
|
+
except (ValueError, IndexError):
|
|
185
|
+
continue
|
|
186
|
+
return points
|