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.
Files changed (155) hide show
  1. thesean-0.1.4/LICENSE +21 -0
  2. thesean-0.1.4/PKG-INFO +98 -0
  3. thesean-0.1.4/README.md +59 -0
  4. thesean-0.1.4/pyproject.toml +99 -0
  5. thesean-0.1.4/setup.cfg +4 -0
  6. thesean-0.1.4/src/thesean/__init__.py +3 -0
  7. thesean-0.1.4/src/thesean/__main__.py +5 -0
  8. thesean-0.1.4/src/thesean/adapters/__init__.py +0 -0
  9. thesean-0.1.4/src/thesean/adapters/f1/__init__.py +0 -0
  10. thesean-0.1.4/src/thesean/adapters/f1/controllers.py +56 -0
  11. thesean-0.1.4/src/thesean/adapters/f1/degrade.py +31 -0
  12. thesean-0.1.4/src/thesean/adapters/f1/env.py +55 -0
  13. thesean-0.1.4/src/thesean/adapters/f1/factory.py +186 -0
  14. thesean-0.1.4/src/thesean/adapters/f1/live_viewer.py +143 -0
  15. thesean-0.1.4/src/thesean/adapters/f1/planner.py +60 -0
  16. thesean-0.1.4/src/thesean/adapters/f1/signals.py +352 -0
  17. thesean-0.1.4/src/thesean/adapters/f1/world_model.py +117 -0
  18. thesean-0.1.4/src/thesean/adapters/registry.py +35 -0
  19. thesean-0.1.4/src/thesean/cli/__init__.py +0 -0
  20. thesean-0.1.4/src/thesean/cli/app.py +21 -0
  21. thesean-0.1.4/src/thesean/cli/version_check.py +32 -0
  22. thesean-0.1.4/src/thesean/cli/wizard/__init__.py +3 -0
  23. thesean-0.1.4/src/thesean/cli/wizard/discovery.py +65 -0
  24. thesean-0.1.4/src/thesean/cli/wizard/models.py +38 -0
  25. thesean-0.1.4/src/thesean/cli/wizard/questions.py +174 -0
  26. thesean-0.1.4/src/thesean/cli/wizard/review.py +54 -0
  27. thesean-0.1.4/src/thesean/cli/wizard/service.py +181 -0
  28. thesean-0.1.4/src/thesean/core/__init__.py +0 -0
  29. thesean-0.1.4/src/thesean/core/config.py +77 -0
  30. thesean-0.1.4/src/thesean/core/contracts.py +129 -0
  31. thesean-0.1.4/src/thesean/core/signal_schema.py +54 -0
  32. thesean-0.1.4/src/thesean/evaluation/__init__.py +0 -0
  33. thesean-0.1.4/src/thesean/evaluation/metrics/__init__.py +24 -0
  34. thesean-0.1.4/src/thesean/evaluation/metrics/offtrack.py +30 -0
  35. thesean-0.1.4/src/thesean/evaluation/metrics/prediction_error.py +71 -0
  36. thesean-0.1.4/src/thesean/evaluation/metrics/progress.py +22 -0
  37. thesean-0.1.4/src/thesean/evaluation/metrics/reward.py +22 -0
  38. thesean-0.1.4/src/thesean/evaluation/metrics/survival.py +22 -0
  39. thesean-0.1.4/src/thesean/models/__init__.py +42 -0
  40. thesean-0.1.4/src/thesean/models/case.py +30 -0
  41. thesean-0.1.4/src/thesean/models/comparison.py +30 -0
  42. thesean-0.1.4/src/thesean/models/episode.py +81 -0
  43. thesean-0.1.4/src/thesean/models/evaluation_result.py +51 -0
  44. thesean-0.1.4/src/thesean/models/event.py +40 -0
  45. thesean-0.1.4/src/thesean/models/evidence.py +18 -0
  46. thesean-0.1.4/src/thesean/models/explanation.py +23 -0
  47. thesean-0.1.4/src/thesean/models/isolation.py +35 -0
  48. thesean-0.1.4/src/thesean/models/manifest.py +24 -0
  49. thesean-0.1.4/src/thesean/models/metric.py +14 -0
  50. thesean-0.1.4/src/thesean/models/project.py +25 -0
  51. thesean-0.1.4/src/thesean/models/run.py +50 -0
  52. thesean-0.1.4/src/thesean/models/signal.py +14 -0
  53. thesean-0.1.4/src/thesean/models/swap.py +25 -0
  54. thesean-0.1.4/src/thesean/pipeline/__init__.py +0 -0
  55. thesean-0.1.4/src/thesean/pipeline/case_io.py +22 -0
  56. thesean-0.1.4/src/thesean/pipeline/compare/__init__.py +4 -0
  57. thesean-0.1.4/src/thesean/pipeline/compare/decision.py +20 -0
  58. thesean-0.1.4/src/thesean/pipeline/compare/execution.py +50 -0
  59. thesean-0.1.4/src/thesean/pipeline/compare/service.py +95 -0
  60. thesean-0.1.4/src/thesean/pipeline/compare/stats.py +60 -0
  61. thesean-0.1.4/src/thesean/pipeline/compare_module.py +258 -0
  62. thesean-0.1.4/src/thesean/pipeline/context.py +204 -0
  63. thesean-0.1.4/src/thesean/pipeline/episodes.py +109 -0
  64. thesean-0.1.4/src/thesean/pipeline/event_extraction.py +254 -0
  65. thesean-0.1.4/src/thesean/pipeline/events/__init__.py +6 -0
  66. thesean-0.1.4/src/thesean/pipeline/events/config.py +41 -0
  67. thesean-0.1.4/src/thesean/pipeline/events/detection.py +231 -0
  68. thesean-0.1.4/src/thesean/pipeline/events/divergence.py +106 -0
  69. thesean-0.1.4/src/thesean/pipeline/isolation/__init__.py +4 -0
  70. thesean-0.1.4/src/thesean/pipeline/isolation/attribution.py +187 -0
  71. thesean-0.1.4/src/thesean/pipeline/isolation/designs.py +35 -0
  72. thesean-0.1.4/src/thesean/pipeline/isolation/executor.py +60 -0
  73. thesean-0.1.4/src/thesean/pipeline/isolation/planner.py +10 -0
  74. thesean-0.1.4/src/thesean/pipeline/live_update.py +59 -0
  75. thesean-0.1.4/src/thesean/pipeline/metrics_util.py +65 -0
  76. thesean-0.1.4/src/thesean/pipeline/paired_runner.py +185 -0
  77. thesean-0.1.4/src/thesean/pipeline/runner.py +109 -0
  78. thesean-0.1.4/src/thesean/pipeline/stages/__init__.py +22 -0
  79. thesean-0.1.4/src/thesean/pipeline/stages/attribute.py +78 -0
  80. thesean-0.1.4/src/thesean/pipeline/stages/base.py +18 -0
  81. thesean-0.1.4/src/thesean/pipeline/stages/compare.py +37 -0
  82. thesean-0.1.4/src/thesean/pipeline/stages/events.py +53 -0
  83. thesean-0.1.4/src/thesean/pipeline/stages/isolate.py +88 -0
  84. thesean-0.1.4/src/thesean/pipeline/stages/report.py +59 -0
  85. thesean-0.1.4/src/thesean/pipeline/staleness.py +33 -0
  86. thesean-0.1.4/src/thesean/pipeline/state.py +31 -0
  87. thesean-0.1.4/src/thesean/pipeline/workspace.py +176 -0
  88. thesean-0.1.4/src/thesean/reporting/__init__.py +0 -0
  89. thesean-0.1.4/src/thesean/reporting/bundle.py +74 -0
  90. thesean-0.1.4/src/thesean/reporting/evidence.py +28 -0
  91. thesean-0.1.4/src/thesean/reporting/renderers/__init__.py +0 -0
  92. thesean-0.1.4/src/thesean/reporting/renderers/console.py +27 -0
  93. thesean-0.1.4/src/thesean/reporting/renderers/html.py +28 -0
  94. thesean-0.1.4/src/thesean/reporting/renderers/json.py +13 -0
  95. thesean-0.1.4/src/thesean/reporting/templates/investigation_report.html.j2 +85 -0
  96. thesean-0.1.4/src/thesean/reporting/templates/report.html.j2 +99 -0
  97. thesean-0.1.4/src/thesean/reporting/types.py +33 -0
  98. thesean-0.1.4/src/thesean/tui/__init__.py +0 -0
  99. thesean-0.1.4/src/thesean/tui/actions.py +78 -0
  100. thesean-0.1.4/src/thesean/tui/app.py +1218 -0
  101. thesean-0.1.4/src/thesean/tui/detection.py +241 -0
  102. thesean-0.1.4/src/thesean/tui/screens/__init__.py +1 -0
  103. thesean-0.1.4/src/thesean/tui/screens/attribution.py +252 -0
  104. thesean-0.1.4/src/thesean/tui/screens/case_history.py +132 -0
  105. thesean-0.1.4/src/thesean/tui/screens/case_verdict.py +663 -0
  106. thesean-0.1.4/src/thesean/tui/screens/command_palette.py +70 -0
  107. thesean-0.1.4/src/thesean/tui/screens/drawers/__init__.py +1 -0
  108. thesean-0.1.4/src/thesean/tui/screens/drawers/context_drawer.py +90 -0
  109. thesean-0.1.4/src/thesean/tui/screens/drawers/evidence_drawer.py +113 -0
  110. thesean-0.1.4/src/thesean/tui/screens/error_modal.py +54 -0
  111. thesean-0.1.4/src/thesean/tui/screens/investigation.py +690 -0
  112. thesean-0.1.4/src/thesean/tui/screens/run_builder.py +492 -0
  113. thesean-0.1.4/src/thesean/tui/screens/workspace_picker.py +69 -0
  114. thesean-0.1.4/src/thesean/tui/services.py +765 -0
  115. thesean-0.1.4/src/thesean/tui/state.py +137 -0
  116. thesean-0.1.4/src/thesean/tui/styles/app.tcss +224 -0
  117. thesean-0.1.4/src/thesean/tui/views/__init__.py +0 -0
  118. thesean-0.1.4/src/thesean/tui/widgets/__init__.py +0 -0
  119. thesean-0.1.4/src/thesean/tui/widgets/action_bar.py +44 -0
  120. thesean-0.1.4/src/thesean/tui/widgets/artifact_list.py +38 -0
  121. thesean-0.1.4/src/thesean/tui/widgets/artifact_preview.py +34 -0
  122. thesean-0.1.4/src/thesean/tui/widgets/attribution_decision_card.py +55 -0
  123. thesean-0.1.4/src/thesean/tui/widgets/case_bar.py +108 -0
  124. thesean-0.1.4/src/thesean/tui/widgets/causal_sequence.py +72 -0
  125. thesean-0.1.4/src/thesean/tui/widgets/comparability_summary.py +48 -0
  126. thesean-0.1.4/src/thesean/tui/widgets/context_rail.py +69 -0
  127. thesean-0.1.4/src/thesean/tui/widgets/effect_table.py +32 -0
  128. thesean-0.1.4/src/thesean/tui/widgets/event_navigator.py +176 -0
  129. thesean-0.1.4/src/thesean/tui/widgets/explanation_card.py +67 -0
  130. thesean-0.1.4/src/thesean/tui/widgets/falsifier_list.py +72 -0
  131. thesean-0.1.4/src/thesean/tui/widgets/focus_signals_strip.py +22 -0
  132. thesean-0.1.4/src/thesean/tui/widgets/help_overlay.py +84 -0
  133. thesean-0.1.4/src/thesean/tui/widgets/isolation_case_detail.py +67 -0
  134. thesean-0.1.4/src/thesean/tui/widgets/isolation_case_table.py +50 -0
  135. thesean-0.1.4/src/thesean/tui/widgets/live_run_monitor.py +353 -0
  136. thesean-0.1.4/src/thesean/tui/widgets/metric_detail.py +95 -0
  137. thesean-0.1.4/src/thesean/tui/widgets/metric_table.py +73 -0
  138. thesean-0.1.4/src/thesean/tui/widgets/progress_summary.py +299 -0
  139. thesean-0.1.4/src/thesean/tui/widgets/run_config_panel.py +163 -0
  140. thesean-0.1.4/src/thesean/tui/widgets/run_monitor.py +91 -0
  141. thesean-0.1.4/src/thesean/tui/widgets/section_title.py +15 -0
  142. thesean-0.1.4/src/thesean/tui/widgets/signal_timeline.py +206 -0
  143. thesean-0.1.4/src/thesean/tui/widgets/status_badge.py +54 -0
  144. thesean-0.1.4/src/thesean/tui/widgets/step_inspector.py +105 -0
  145. thesean-0.1.4/src/thesean/tui/widgets/tier_indicator.py +43 -0
  146. thesean-0.1.4/src/thesean/tui/widgets/track_map.py +136 -0
  147. thesean-0.1.4/src/thesean/tui/widgets/transport_bar.py +152 -0
  148. thesean-0.1.4/src/thesean/tui/widgets/verdict_strip.py +103 -0
  149. thesean-0.1.4/src/thesean.egg-info/PKG-INFO +98 -0
  150. thesean-0.1.4/src/thesean.egg-info/SOURCES.txt +153 -0
  151. thesean-0.1.4/src/thesean.egg-info/dependency_links.txt +1 -0
  152. thesean-0.1.4/src/thesean.egg-info/entry_points.txt +5 -0
  153. thesean-0.1.4/src/thesean.egg-info/requires.txt +23 -0
  154. thesean-0.1.4/src/thesean.egg-info/top_level.txt +1 -0
  155. 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
@@ -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"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """Regression thesean harness for ML pipelines."""
2
+
3
+ __version__ = "0.1.4"
@@ -0,0 +1,5 @@
1
+ """Allow running as `python -m thesean`."""
2
+
3
+ from thesean.cli.app import app
4
+
5
+ app()
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