psystack 0.1.0__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.
- psystack-0.1.0/LICENSE +21 -0
- psystack-0.1.0/PKG-INFO +42 -0
- psystack-0.1.0/README.md +2 -0
- psystack-0.1.0/pyproject.toml +100 -0
- psystack-0.1.0/setup.cfg +4 -0
- psystack-0.1.0/src/psystack/__init__.py +3 -0
- psystack-0.1.0/src/psystack/__main__.py +5 -0
- psystack-0.1.0/src/psystack/adapters/__init__.py +0 -0
- psystack-0.1.0/src/psystack/adapters/f1/__init__.py +0 -0
- psystack-0.1.0/src/psystack/adapters/f1/controllers.py +56 -0
- psystack-0.1.0/src/psystack/adapters/f1/degrade.py +31 -0
- psystack-0.1.0/src/psystack/adapters/f1/env.py +48 -0
- psystack-0.1.0/src/psystack/adapters/f1/factory.py +182 -0
- psystack-0.1.0/src/psystack/adapters/f1/live_viewer.py +143 -0
- psystack-0.1.0/src/psystack/adapters/f1/planner.py +39 -0
- psystack-0.1.0/src/psystack/adapters/f1/signals.py +353 -0
- psystack-0.1.0/src/psystack/adapters/f1/world_model.py +75 -0
- psystack-0.1.0/src/psystack/adapters/registry.py +35 -0
- psystack-0.1.0/src/psystack/cli/__init__.py +0 -0
- psystack-0.1.0/src/psystack/cli/app.py +21 -0
- psystack-0.1.0/src/psystack/cli/version_check.py +32 -0
- psystack-0.1.0/src/psystack/cli/wizard/__init__.py +3 -0
- psystack-0.1.0/src/psystack/cli/wizard/discovery.py +65 -0
- psystack-0.1.0/src/psystack/cli/wizard/models.py +38 -0
- psystack-0.1.0/src/psystack/cli/wizard/questions.py +174 -0
- psystack-0.1.0/src/psystack/cli/wizard/review.py +54 -0
- psystack-0.1.0/src/psystack/cli/wizard/service.py +181 -0
- psystack-0.1.0/src/psystack/core/__init__.py +0 -0
- psystack-0.1.0/src/psystack/core/config.py +77 -0
- psystack-0.1.0/src/psystack/core/contracts.py +124 -0
- psystack-0.1.0/src/psystack/core/signal_schema.py +54 -0
- psystack-0.1.0/src/psystack/evaluation/__init__.py +0 -0
- psystack-0.1.0/src/psystack/evaluation/metrics/__init__.py +22 -0
- psystack-0.1.0/src/psystack/evaluation/metrics/offtrack.py +30 -0
- psystack-0.1.0/src/psystack/evaluation/metrics/prediction_error.py +71 -0
- psystack-0.1.0/src/psystack/evaluation/metrics/progress.py +22 -0
- psystack-0.1.0/src/psystack/evaluation/metrics/reward.py +22 -0
- psystack-0.1.0/src/psystack/evaluation/metrics/survival.py +22 -0
- psystack-0.1.0/src/psystack/models/__init__.py +42 -0
- psystack-0.1.0/src/psystack/models/case.py +30 -0
- psystack-0.1.0/src/psystack/models/comparison.py +30 -0
- psystack-0.1.0/src/psystack/models/episode.py +82 -0
- psystack-0.1.0/src/psystack/models/evaluation_result.py +51 -0
- psystack-0.1.0/src/psystack/models/event.py +40 -0
- psystack-0.1.0/src/psystack/models/evidence.py +18 -0
- psystack-0.1.0/src/psystack/models/explanation.py +23 -0
- psystack-0.1.0/src/psystack/models/isolation.py +35 -0
- psystack-0.1.0/src/psystack/models/manifest.py +24 -0
- psystack-0.1.0/src/psystack/models/metric.py +14 -0
- psystack-0.1.0/src/psystack/models/project.py +25 -0
- psystack-0.1.0/src/psystack/models/run.py +50 -0
- psystack-0.1.0/src/psystack/models/signal.py +14 -0
- psystack-0.1.0/src/psystack/models/swap.py +25 -0
- psystack-0.1.0/src/psystack/pipeline/__init__.py +0 -0
- psystack-0.1.0/src/psystack/pipeline/case_io.py +22 -0
- psystack-0.1.0/src/psystack/pipeline/compare/__init__.py +4 -0
- psystack-0.1.0/src/psystack/pipeline/compare/decision.py +20 -0
- psystack-0.1.0/src/psystack/pipeline/compare/execution.py +50 -0
- psystack-0.1.0/src/psystack/pipeline/compare/service.py +95 -0
- psystack-0.1.0/src/psystack/pipeline/compare/stats.py +60 -0
- psystack-0.1.0/src/psystack/pipeline/compare_module.py +259 -0
- psystack-0.1.0/src/psystack/pipeline/context.py +194 -0
- psystack-0.1.0/src/psystack/pipeline/episodes.py +109 -0
- psystack-0.1.0/src/psystack/pipeline/event_extraction.py +253 -0
- psystack-0.1.0/src/psystack/pipeline/events/__init__.py +6 -0
- psystack-0.1.0/src/psystack/pipeline/events/config.py +41 -0
- psystack-0.1.0/src/psystack/pipeline/events/detection.py +231 -0
- psystack-0.1.0/src/psystack/pipeline/events/divergence.py +106 -0
- psystack-0.1.0/src/psystack/pipeline/isolation/__init__.py +4 -0
- psystack-0.1.0/src/psystack/pipeline/isolation/attribution.py +187 -0
- psystack-0.1.0/src/psystack/pipeline/isolation/designs.py +35 -0
- psystack-0.1.0/src/psystack/pipeline/isolation/executor.py +60 -0
- psystack-0.1.0/src/psystack/pipeline/isolation/planner.py +10 -0
- psystack-0.1.0/src/psystack/pipeline/live_update.py +59 -0
- psystack-0.1.0/src/psystack/pipeline/metrics_util.py +65 -0
- psystack-0.1.0/src/psystack/pipeline/paired_runner.py +185 -0
- psystack-0.1.0/src/psystack/pipeline/runner.py +107 -0
- psystack-0.1.0/src/psystack/pipeline/stages/__init__.py +22 -0
- psystack-0.1.0/src/psystack/pipeline/stages/attribute.py +78 -0
- psystack-0.1.0/src/psystack/pipeline/stages/base.py +18 -0
- psystack-0.1.0/src/psystack/pipeline/stages/compare.py +37 -0
- psystack-0.1.0/src/psystack/pipeline/stages/events.py +53 -0
- psystack-0.1.0/src/psystack/pipeline/stages/isolate.py +88 -0
- psystack-0.1.0/src/psystack/pipeline/stages/report.py +59 -0
- psystack-0.1.0/src/psystack/pipeline/staleness.py +33 -0
- psystack-0.1.0/src/psystack/pipeline/state.py +31 -0
- psystack-0.1.0/src/psystack/pipeline/workspace.py +177 -0
- psystack-0.1.0/src/psystack/reporting/__init__.py +0 -0
- psystack-0.1.0/src/psystack/reporting/bundle.py +74 -0
- psystack-0.1.0/src/psystack/reporting/evidence.py +28 -0
- psystack-0.1.0/src/psystack/reporting/renderers/__init__.py +0 -0
- psystack-0.1.0/src/psystack/reporting/renderers/console.py +27 -0
- psystack-0.1.0/src/psystack/reporting/renderers/html.py +28 -0
- psystack-0.1.0/src/psystack/reporting/renderers/json.py +13 -0
- psystack-0.1.0/src/psystack/reporting/templates/investigation_report.html.j2 +85 -0
- psystack-0.1.0/src/psystack/reporting/templates/report.html.j2 +99 -0
- psystack-0.1.0/src/psystack/reporting/types.py +33 -0
- psystack-0.1.0/src/psystack/tui/__init__.py +0 -0
- psystack-0.1.0/src/psystack/tui/actions.py +78 -0
- psystack-0.1.0/src/psystack/tui/app.py +1188 -0
- psystack-0.1.0/src/psystack/tui/detection.py +241 -0
- psystack-0.1.0/src/psystack/tui/screens/__init__.py +1 -0
- psystack-0.1.0/src/psystack/tui/screens/attribution.py +252 -0
- psystack-0.1.0/src/psystack/tui/screens/case_history.py +131 -0
- psystack-0.1.0/src/psystack/tui/screens/case_verdict.py +657 -0
- psystack-0.1.0/src/psystack/tui/screens/command_palette.py +70 -0
- psystack-0.1.0/src/psystack/tui/screens/drawers/__init__.py +1 -0
- psystack-0.1.0/src/psystack/tui/screens/drawers/context_drawer.py +90 -0
- psystack-0.1.0/src/psystack/tui/screens/drawers/evidence_drawer.py +113 -0
- psystack-0.1.0/src/psystack/tui/screens/error_modal.py +54 -0
- psystack-0.1.0/src/psystack/tui/screens/investigation.py +686 -0
- psystack-0.1.0/src/psystack/tui/screens/run_builder.py +492 -0
- psystack-0.1.0/src/psystack/tui/screens/workspace_picker.py +69 -0
- psystack-0.1.0/src/psystack/tui/services.py +769 -0
- psystack-0.1.0/src/psystack/tui/state.py +137 -0
- psystack-0.1.0/src/psystack/tui/styles/app.tcss +224 -0
- psystack-0.1.0/src/psystack/tui/views/__init__.py +0 -0
- psystack-0.1.0/src/psystack/tui/widgets/__init__.py +0 -0
- psystack-0.1.0/src/psystack/tui/widgets/action_bar.py +42 -0
- psystack-0.1.0/src/psystack/tui/widgets/artifact_list.py +38 -0
- psystack-0.1.0/src/psystack/tui/widgets/artifact_preview.py +34 -0
- psystack-0.1.0/src/psystack/tui/widgets/attribution_decision_card.py +55 -0
- psystack-0.1.0/src/psystack/tui/widgets/case_bar.py +108 -0
- psystack-0.1.0/src/psystack/tui/widgets/causal_sequence.py +73 -0
- psystack-0.1.0/src/psystack/tui/widgets/comparability_summary.py +48 -0
- psystack-0.1.0/src/psystack/tui/widgets/context_rail.py +69 -0
- psystack-0.1.0/src/psystack/tui/widgets/effect_table.py +32 -0
- psystack-0.1.0/src/psystack/tui/widgets/event_navigator.py +176 -0
- psystack-0.1.0/src/psystack/tui/widgets/explanation_card.py +67 -0
- psystack-0.1.0/src/psystack/tui/widgets/falsifier_list.py +73 -0
- psystack-0.1.0/src/psystack/tui/widgets/focus_signals_strip.py +22 -0
- psystack-0.1.0/src/psystack/tui/widgets/help_overlay.py +85 -0
- psystack-0.1.0/src/psystack/tui/widgets/isolation_case_detail.py +67 -0
- psystack-0.1.0/src/psystack/tui/widgets/isolation_case_table.py +50 -0
- psystack-0.1.0/src/psystack/tui/widgets/live_run_monitor.py +337 -0
- psystack-0.1.0/src/psystack/tui/widgets/metric_detail.py +93 -0
- psystack-0.1.0/src/psystack/tui/widgets/metric_table.py +71 -0
- psystack-0.1.0/src/psystack/tui/widgets/progress_summary.py +300 -0
- psystack-0.1.0/src/psystack/tui/widgets/run_config_panel.py +163 -0
- psystack-0.1.0/src/psystack/tui/widgets/run_monitor.py +91 -0
- psystack-0.1.0/src/psystack/tui/widgets/section_title.py +15 -0
- psystack-0.1.0/src/psystack/tui/widgets/signal_timeline.py +206 -0
- psystack-0.1.0/src/psystack/tui/widgets/status_badge.py +52 -0
- psystack-0.1.0/src/psystack/tui/widgets/step_inspector.py +105 -0
- psystack-0.1.0/src/psystack/tui/widgets/tier_indicator.py +44 -0
- psystack-0.1.0/src/psystack/tui/widgets/track_map.py +137 -0
- psystack-0.1.0/src/psystack/tui/widgets/transport_bar.py +152 -0
- psystack-0.1.0/src/psystack/tui/widgets/verdict_strip.py +103 -0
- psystack-0.1.0/src/psystack.egg-info/PKG-INFO +42 -0
- psystack-0.1.0/src/psystack.egg-info/SOURCES.txt +152 -0
- psystack-0.1.0/src/psystack.egg-info/dependency_links.txt +1 -0
- psystack-0.1.0/src/psystack.egg-info/entry_points.txt +5 -0
- psystack-0.1.0/src/psystack.egg-info/requires.txt +23 -0
- psystack-0.1.0/src/psystack.egg-info/top_level.txt +1 -0
psystack-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 PsyStack
|
|
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.
|
psystack-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: psystack
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Regression investigation harness for ML pipelines
|
|
5
|
+
Author: Danny Nguyen
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/PsyStack/PsyStack
|
|
8
|
+
Project-URL: Issues, https://github.com/PsyStack/PsyStack/issues
|
|
9
|
+
Keywords: regression,investigation,ml,machine-learning,debugging,pipeline
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: typer<1,>=0.9.0
|
|
20
|
+
Requires-Dist: pydantic<3,>=2.0.0
|
|
21
|
+
Requires-Dist: pydantic-settings>=2.0.0
|
|
22
|
+
Requires-Dist: jinja2>=3.1.0
|
|
23
|
+
Requires-Dist: tomli-w>=1.0.0
|
|
24
|
+
Requires-Dist: InquirerPy>=0.3.4
|
|
25
|
+
Requires-Dist: rich>=13.0.0
|
|
26
|
+
Requires-Dist: torch<3,>=2.0.0
|
|
27
|
+
Requires-Dist: numpy<3,>=1.24.0
|
|
28
|
+
Requires-Dist: scipy>=1.11.0
|
|
29
|
+
Requires-Dist: statsmodels>=0.14.0
|
|
30
|
+
Requires-Dist: textual>=0.80.0
|
|
31
|
+
Requires-Dist: tomli>=2.0.0; python_version < "3.11"
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
34
|
+
Requires-Dist: pytest-textual-snapshot; extra == "dev"
|
|
35
|
+
Requires-Dist: hypothesis; extra == "dev"
|
|
36
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
|
|
37
|
+
Requires-Dist: ruff>=0.4.0; extra == "dev"
|
|
38
|
+
Requires-Dist: mypy>=1.8.0; extra == "dev"
|
|
39
|
+
Dynamic: license-file
|
|
40
|
+
|
|
41
|
+
# PsyStack
|
|
42
|
+
A regression investigation harness for ML pipelines, latent action systems, and world models.
|
psystack-0.1.0/README.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "psystack"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Regression investigation harness for ML pipelines"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
license = {text = "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
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.10",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
dependencies = [
|
|
24
|
+
"typer>=0.9.0,<1",
|
|
25
|
+
"pydantic>=2.0.0,<3",
|
|
26
|
+
"pydantic-settings>=2.0.0",
|
|
27
|
+
"jinja2>=3.1.0",
|
|
28
|
+
"tomli-w>=1.0.0",
|
|
29
|
+
"InquirerPy>=0.3.4",
|
|
30
|
+
"rich>=13.0.0",
|
|
31
|
+
"torch>=2.0.0,<3",
|
|
32
|
+
"numpy>=1.24.0,<3",
|
|
33
|
+
"scipy>=1.11.0",
|
|
34
|
+
"statsmodels>=0.14.0",
|
|
35
|
+
"textual>=0.80.0",
|
|
36
|
+
"tomli>=2.0.0; python_version < '3.11'",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://github.com/PsyStack/PsyStack"
|
|
41
|
+
Issues = "https://github.com/PsyStack/PsyStack/issues"
|
|
42
|
+
|
|
43
|
+
[project.optional-dependencies]
|
|
44
|
+
dev = [
|
|
45
|
+
"pytest>=7.0",
|
|
46
|
+
"pytest-textual-snapshot",
|
|
47
|
+
"hypothesis",
|
|
48
|
+
"pytest-asyncio>=0.23.0",
|
|
49
|
+
"ruff>=0.4.0",
|
|
50
|
+
"mypy>=1.8.0",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
[project.entry-points."psystack.adapters"]
|
|
54
|
+
f1 = "psystack.adapters.f1.factory:F1AdapterFactory"
|
|
55
|
+
|
|
56
|
+
[project.scripts]
|
|
57
|
+
psystack = "psystack.cli.app:app"
|
|
58
|
+
|
|
59
|
+
[tool.setuptools.packages.find]
|
|
60
|
+
where = ["src"]
|
|
61
|
+
|
|
62
|
+
[tool.setuptools.package-data]
|
|
63
|
+
psystack = ["tui/styles/*.tcss", "reporting/templates/*.j2"]
|
|
64
|
+
|
|
65
|
+
[tool.ruff]
|
|
66
|
+
target-version = "py310"
|
|
67
|
+
line-length = 120
|
|
68
|
+
|
|
69
|
+
[tool.ruff.lint]
|
|
70
|
+
select = ["E", "F", "I", "UP", "B", "SIM"]
|
|
71
|
+
ignore = ["B008", "B904"]
|
|
72
|
+
|
|
73
|
+
[tool.mypy]
|
|
74
|
+
python_version = "3.10"
|
|
75
|
+
warn_return_any = true
|
|
76
|
+
warn_unused_configs = true
|
|
77
|
+
ignore_missing_imports = true
|
|
78
|
+
|
|
79
|
+
[[tool.mypy.overrides]]
|
|
80
|
+
module = ["psystack.models.*", "psystack.reporting.*", "psystack.pipeline.context", "psystack.pipeline.stages.*", "psystack.core.contracts"]
|
|
81
|
+
disallow_untyped_defs = true
|
|
82
|
+
|
|
83
|
+
[[tool.mypy.overrides]]
|
|
84
|
+
module = ["psystack.pipeline.compare.*", "psystack.pipeline.isolation.attribution"]
|
|
85
|
+
disallow_untyped_defs = false
|
|
86
|
+
warn_return_any = false
|
|
87
|
+
|
|
88
|
+
[tool.pytest.ini_options]
|
|
89
|
+
testpaths = ["tests"]
|
|
90
|
+
markers = [
|
|
91
|
+
"unit: fast isolated tests",
|
|
92
|
+
"integration: tests spanning multiple components",
|
|
93
|
+
"smoke: CLI entry-point smoke tests",
|
|
94
|
+
"property: hypothesis property-based tests",
|
|
95
|
+
"snapshot: textual snapshot tests",
|
|
96
|
+
"tui: tests requiring Textual runtime",
|
|
97
|
+
"f1: tests that require the real F1 adapter/repo",
|
|
98
|
+
"framework: hermetic framework tests",
|
|
99
|
+
]
|
|
100
|
+
asyncio_default_fixture_loop_scope = "function"
|
psystack-0.1.0/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.__init__)
|
|
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)
|
|
56
|
+
return self._controller(obs)
|
|
@@ -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 psystack.adapters.f1.world_model import F1WorldModelAdapter
|
|
28
|
+
|
|
29
|
+
adapter = F1WorldModelAdapter()
|
|
30
|
+
adapter.load(weights_path, device=device)
|
|
31
|
+
return adapter
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
cfg = Config(**{k: v for k, v in config.items() if hasattr(Config, k)})
|
|
30
|
+
self._env = F1Env.from_config(cfg)
|
|
31
|
+
|
|
32
|
+
def reset(self, seed: int | None = None) -> dict[str, Any]:
|
|
33
|
+
if self._env is None:
|
|
34
|
+
raise RuntimeError("configure() must be called before reset()")
|
|
35
|
+
# F1Env.reset() does not accept a seed parameter. Global RNG seeding
|
|
36
|
+
# is handled by the runner. If F1Env adds seed support, forward it here.
|
|
37
|
+
return self._env.reset()
|
|
38
|
+
|
|
39
|
+
def step(self, action: np.ndarray) -> tuple[dict, float, bool, dict]:
|
|
40
|
+
if self._env is None:
|
|
41
|
+
raise RuntimeError("configure() must be called before step()")
|
|
42
|
+
return self._env.step(action)
|
|
43
|
+
|
|
44
|
+
def get_car_state(self) -> dict[str, Any]:
|
|
45
|
+
return self._env.get_car_state()
|
|
46
|
+
|
|
47
|
+
def get_progress(self) -> float:
|
|
48
|
+
return self._env.get_progress()
|
|
@@ -0,0 +1,182 @@
|
|
|
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 psystack.adapters.f1.env import F1EnvAdapter
|
|
11
|
+
from psystack.adapters.f1.planner import F1PlannerAdapter
|
|
12
|
+
from psystack.adapters.f1.signals import F1SignalTranslator
|
|
13
|
+
from psystack.adapters.f1.world_model import F1WorldModelAdapter
|
|
14
|
+
from psystack.core.contracts import (
|
|
15
|
+
EnvPlugin,
|
|
16
|
+
MetricPlugin,
|
|
17
|
+
PanelProvider,
|
|
18
|
+
PlannerPlugin,
|
|
19
|
+
SignalTranslator,
|
|
20
|
+
WorldModelPlugin,
|
|
21
|
+
)
|
|
22
|
+
from psystack.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 psystack.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) -> dict[str, Any]:
|
|
122
|
+
"""Build full env config for given track name. Paths are absolute if repo is bound."""
|
|
123
|
+
track_csv = f"tracks/{env_id}.csv"
|
|
124
|
+
if self._repo is not None:
|
|
125
|
+
track_csv = str(self._repo / track_csv)
|
|
126
|
+
return {
|
|
127
|
+
"track_csv": track_csv,
|
|
128
|
+
"max_speed": 50.0, "dt": 0.1, "max_steps": 1000,
|
|
129
|
+
"max_steer_rate": 3.5, "off_track_tolerance": 10,
|
|
130
|
+
"raster_size": 64, "pixels_per_meter": 3.0,
|
|
131
|
+
"progress_reward": 0.02, "off_track_penalty": 0.5,
|
|
132
|
+
"step_penalty": 0.005, "lap_bonus": 1.0,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
# ── Phase 3 optional methods ──────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
def detect_project(self, repo: Path) -> dict[str, Any]:
|
|
138
|
+
"""Scan F1 repo for available assets."""
|
|
139
|
+
repo = repo.expanduser().resolve()
|
|
140
|
+
assets: dict[str, Any] = {
|
|
141
|
+
"weights": self.discover_weights(repo),
|
|
142
|
+
"envs": self.discover_envs(repo),
|
|
143
|
+
"scenarios": [],
|
|
144
|
+
"configs": [],
|
|
145
|
+
}
|
|
146
|
+
# Scan for config files
|
|
147
|
+
config_dir = repo / "configs"
|
|
148
|
+
if config_dir.is_dir():
|
|
149
|
+
assets["configs"] = sorted(p.name for p in config_dir.glob("*.py"))
|
|
150
|
+
return assets
|
|
151
|
+
|
|
152
|
+
def get_signal_translator(self) -> SignalTranslator | None:
|
|
153
|
+
return F1SignalTranslator()
|
|
154
|
+
|
|
155
|
+
def get_panel_providers(self) -> list[PanelProvider]:
|
|
156
|
+
return [F1TrackPanel()]
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class F1TrackPanel:
|
|
160
|
+
"""PanelProvider for F1 track visualization."""
|
|
161
|
+
|
|
162
|
+
def panel_id(self) -> str:
|
|
163
|
+
return "f1_track"
|
|
164
|
+
|
|
165
|
+
def panel_label(self) -> str:
|
|
166
|
+
return "F1 Track View"
|
|
167
|
+
|
|
168
|
+
def load_track_geometry(self, track_csv: str) -> list[tuple[float, float]]:
|
|
169
|
+
"""Load (x, y) centerline points from a track CSV file."""
|
|
170
|
+
import csv
|
|
171
|
+
|
|
172
|
+
points: list[tuple[float, float]] = []
|
|
173
|
+
with open(track_csv) as f:
|
|
174
|
+
reader = csv.reader(f)
|
|
175
|
+
for row in reader:
|
|
176
|
+
if not row or row[0].strip().startswith("#"):
|
|
177
|
+
continue
|
|
178
|
+
try:
|
|
179
|
+
points.append((float(row[0].strip()), float(row[1].strip())))
|
|
180
|
+
except (ValueError, IndexError):
|
|
181
|
+
continue
|
|
182
|
+
return points
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""F1 Pygame sidecar — live visualization of car position during evaluation.
|
|
2
|
+
|
|
3
|
+
DISABLED: Buggy on macOS Apple Silicon — SDL2 joystick subsystem segfaults
|
|
4
|
+
during Python teardown (IOKit/CoreFoundation accesses freed memory). The
|
|
5
|
+
entire pygame sidecar is disabled until this is resolved upstream. All live
|
|
6
|
+
telemetry still flows through the TUI widgets (LiveRunMonitor, etc.).
|
|
7
|
+
|
|
8
|
+
Runs as a separate process (not thread) because Pygame needs its own
|
|
9
|
+
main thread on macOS. Reads LiveStepUpdate from a multiprocessing.Queue.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
# from __future__ import annotations
|
|
13
|
+
#
|
|
14
|
+
# import multiprocessing as mp
|
|
15
|
+
# import os
|
|
16
|
+
# import sys
|
|
17
|
+
# from typing import Any
|
|
18
|
+
#
|
|
19
|
+
#
|
|
20
|
+
# class LiveF1ViewerProcess:
|
|
21
|
+
# """Manages a subprocess running Pygame visualization of live evaluation steps."""
|
|
22
|
+
#
|
|
23
|
+
# def __init__(self, track_csv_path: str, repo_root: str) -> None:
|
|
24
|
+
# self._track_csv_path = track_csv_path
|
|
25
|
+
# self._repo_root = repo_root
|
|
26
|
+
# self._queue: mp.Queue = mp.Queue(maxsize=200)
|
|
27
|
+
# self._shutdown = mp.Event()
|
|
28
|
+
# self._process: mp.Process | None = None
|
|
29
|
+
#
|
|
30
|
+
# def start(self) -> None:
|
|
31
|
+
# """Spawn the viewer subprocess."""
|
|
32
|
+
# self._process = mp.Process(
|
|
33
|
+
# target=_viewer_main,
|
|
34
|
+
# args=(self._track_csv_path, self._repo_root, self._queue, self._shutdown),
|
|
35
|
+
# daemon=True,
|
|
36
|
+
# )
|
|
37
|
+
# self._process.start()
|
|
38
|
+
#
|
|
39
|
+
# def send_update(self, update: Any) -> None:
|
|
40
|
+
# """Send a LiveStepUpdate to the viewer. Drops oldest if queue is full."""
|
|
41
|
+
# try:
|
|
42
|
+
# self._queue.put_nowait(update)
|
|
43
|
+
# except Exception:
|
|
44
|
+
# try:
|
|
45
|
+
# self._queue.get_nowait()
|
|
46
|
+
# except Exception:
|
|
47
|
+
# pass
|
|
48
|
+
# try:
|
|
49
|
+
# self._queue.put_nowait(update)
|
|
50
|
+
# except Exception:
|
|
51
|
+
# pass
|
|
52
|
+
#
|
|
53
|
+
# def stop(self) -> None:
|
|
54
|
+
# """Signal shutdown and wait for process to exit."""
|
|
55
|
+
# self._shutdown.set()
|
|
56
|
+
# if self._process is not None:
|
|
57
|
+
# self._process.join(timeout=3)
|
|
58
|
+
# if self._process.is_alive():
|
|
59
|
+
# self._process.terminate()
|
|
60
|
+
# self._process = None
|
|
61
|
+
#
|
|
62
|
+
# def is_alive(self) -> bool:
|
|
63
|
+
# """Check if the viewer process is still running."""
|
|
64
|
+
# return self._process is not None and self._process.is_alive()
|
|
65
|
+
#
|
|
66
|
+
#
|
|
67
|
+
# def _viewer_main(
|
|
68
|
+
# track_csv_path: str,
|
|
69
|
+
# repo_root: str,
|
|
70
|
+
# queue: mp.Queue,
|
|
71
|
+
# shutdown: mp.Event,
|
|
72
|
+
# ) -> None:
|
|
73
|
+
# """Entry point for the viewer subprocess."""
|
|
74
|
+
# # Add repo root to sys.path so env/viz imports work
|
|
75
|
+
# if repo_root not in sys.path:
|
|
76
|
+
# sys.path.insert(0, repo_root)
|
|
77
|
+
#
|
|
78
|
+
# try:
|
|
79
|
+
# import pygame
|
|
80
|
+
# from env.track import Track
|
|
81
|
+
# from viz.renderer import Visualizer
|
|
82
|
+
# except ImportError:
|
|
83
|
+
# return # pygame or f1worldmodel not available
|
|
84
|
+
#
|
|
85
|
+
# try:
|
|
86
|
+
# track = Track.load(track_csv_path)
|
|
87
|
+
# except Exception:
|
|
88
|
+
# return
|
|
89
|
+
#
|
|
90
|
+
# try:
|
|
91
|
+
# viz = Visualizer(track)
|
|
92
|
+
# except Exception:
|
|
93
|
+
# return
|
|
94
|
+
#
|
|
95
|
+
# clock = pygame.time.Clock()
|
|
96
|
+
#
|
|
97
|
+
# while not shutdown.is_set():
|
|
98
|
+
# # Pump pygame events (required to keep window responsive)
|
|
99
|
+
# for event in pygame.event.get():
|
|
100
|
+
# if event.type == pygame.QUIT:
|
|
101
|
+
# shutdown.set()
|
|
102
|
+
# break
|
|
103
|
+
#
|
|
104
|
+
# if shutdown.is_set():
|
|
105
|
+
# break
|
|
106
|
+
#
|
|
107
|
+
# # Drain queue to latest update (newest-frame-wins)
|
|
108
|
+
# latest = None
|
|
109
|
+
# while True:
|
|
110
|
+
# try:
|
|
111
|
+
# latest = queue.get_nowait()
|
|
112
|
+
# except Exception:
|
|
113
|
+
# break
|
|
114
|
+
#
|
|
115
|
+
# if latest is not None:
|
|
116
|
+
# try:
|
|
117
|
+
# # Handle LivePairFrame (4C) or legacy LiveStepUpdate
|
|
118
|
+
# if hasattr(latest, "a") and hasattr(latest, "b"):
|
|
119
|
+
# # LivePairFrame — render both cars
|
|
120
|
+
# frame = latest
|
|
121
|
+
# car_state_a = frame.a.state if frame.a else {}
|
|
122
|
+
# car_state_b = frame.b.state if frame.b else {}
|
|
123
|
+
# action_a = frame.a.action if frame.a else []
|
|
124
|
+
# info_a = frame.a.info if frame.a else {}
|
|
125
|
+
# if hasattr(viz, "render_race"):
|
|
126
|
+
# viz.render_race(car_state_a, car_state_b, action_a, info_a)
|
|
127
|
+
# else:
|
|
128
|
+
# viz.render_frame(car_state_a, action_a, info_a)
|
|
129
|
+
# else:
|
|
130
|
+
# car_state = latest.state if hasattr(latest, "state") else {}
|
|
131
|
+
# action = latest.action if hasattr(latest, "action") else []
|
|
132
|
+
# info = latest.info if hasattr(latest, "info") else {}
|
|
133
|
+
# viz.render_frame(car_state, action, info)
|
|
134
|
+
# except Exception:
|
|
135
|
+
# pass # visualization errors are non-fatal
|
|
136
|
+
#
|
|
137
|
+
# clock.tick(30) # cap at 30fps
|
|
138
|
+
#
|
|
139
|
+
# try:
|
|
140
|
+
# pygame.quit()
|
|
141
|
+
# except Exception:
|
|
142
|
+
# pass
|
|
143
|
+
# os._exit(0)
|