othismos 0.4.0__py3-none-any.whl
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.
- othismos/__init__.py +156 -0
- othismos/cli.py +272 -0
- othismos/config.py +131 -0
- othismos/context_pressure.py +174 -0
- othismos/controller.py +274 -0
- othismos/diagnostics.py +201 -0
- othismos/ecology.py +307 -0
- othismos/integrations.py +329 -0
- othismos/llm.py +317 -0
- othismos/pandas_export.py +98 -0
- othismos/phases.py +295 -0
- othismos/pressure.py +340 -0
- othismos/protocols.py +59 -0
- othismos/serialization.py +144 -0
- othismos/viz.py +286 -0
- othismos-0.4.0.dist-info/METADATA +409 -0
- othismos-0.4.0.dist-info/RECORD +20 -0
- othismos-0.4.0.dist-info/WHEEL +5 -0
- othismos-0.4.0.dist-info/entry_points.txt +2 -0
- othismos-0.4.0.dist-info/top_level.txt +1 -0
othismos/__init__.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Óthismos — Core Library
|
|
2
|
+
#
|
|
3
|
+
# The force a bounded system exerts against its bounds.
|
|
4
|
+
# The push IS the knowing.
|
|
5
|
+
|
|
6
|
+
from othismos.protocols import (
|
|
7
|
+
FeasibilityFn,
|
|
8
|
+
ProjectionFn,
|
|
9
|
+
NormalFn,
|
|
10
|
+
DistanceFn,
|
|
11
|
+
ConstraintLike,
|
|
12
|
+
)
|
|
13
|
+
from othismos.config import OthismosConfig
|
|
14
|
+
from othismos.pressure import (
|
|
15
|
+
ConstraintType,
|
|
16
|
+
Constraint,
|
|
17
|
+
PressureMeasurement,
|
|
18
|
+
PressureGauge,
|
|
19
|
+
GoldilocksZone,
|
|
20
|
+
compute_othismos,
|
|
21
|
+
goldilocks_range,
|
|
22
|
+
l2_constraint,
|
|
23
|
+
box_constraint,
|
|
24
|
+
)
|
|
25
|
+
from othismos.phases import (
|
|
26
|
+
MoltPhase,
|
|
27
|
+
PhaseClassifier,
|
|
28
|
+
PhaseReading,
|
|
29
|
+
MoltCycle,
|
|
30
|
+
MoltCycleTracker,
|
|
31
|
+
)
|
|
32
|
+
from othismos.diagnostics import (
|
|
33
|
+
DiagnosticResult,
|
|
34
|
+
PopcornDiagnostic,
|
|
35
|
+
SystemHealth,
|
|
36
|
+
)
|
|
37
|
+
from othismos.ecology import (
|
|
38
|
+
Deposit,
|
|
39
|
+
Reef,
|
|
40
|
+
ReefLayer,
|
|
41
|
+
Reefquake,
|
|
42
|
+
)
|
|
43
|
+
from othismos.integrations import (
|
|
44
|
+
MetricLogger,
|
|
45
|
+
DictLogger,
|
|
46
|
+
OthismosTorchCallback,
|
|
47
|
+
OthismosTrainerCallback,
|
|
48
|
+
constraint_from_torch_model,
|
|
49
|
+
)
|
|
50
|
+
from othismos.context_pressure import (
|
|
51
|
+
ContextPressureGauge,
|
|
52
|
+
ContextPressureMeasurement,
|
|
53
|
+
cosine_distance,
|
|
54
|
+
l2_distance,
|
|
55
|
+
token_overlap,
|
|
56
|
+
)
|
|
57
|
+
from othismos.controller import (
|
|
58
|
+
PressureController,
|
|
59
|
+
ControlAction,
|
|
60
|
+
ActionType,
|
|
61
|
+
)
|
|
62
|
+
from othismos.serialization import (
|
|
63
|
+
save_history,
|
|
64
|
+
load_history,
|
|
65
|
+
save_diagnostic,
|
|
66
|
+
export_metrics_csv,
|
|
67
|
+
pressure_summary,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Optional imports (require extra deps)
|
|
71
|
+
try:
|
|
72
|
+
from othismos.viz import (
|
|
73
|
+
plot_pressure,
|
|
74
|
+
plot_molt_cycle,
|
|
75
|
+
plot_constraint_profile,
|
|
76
|
+
plot_diagnostic_timeline,
|
|
77
|
+
)
|
|
78
|
+
except ImportError:
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
from othismos.pandas_export import (
|
|
83
|
+
gauge_to_dataframe,
|
|
84
|
+
tracker_to_dataframe,
|
|
85
|
+
reef_to_dataframe,
|
|
86
|
+
)
|
|
87
|
+
except ImportError:
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
__version__ = "0.4.0"
|
|
91
|
+
__all__ = [
|
|
92
|
+
# Protocols
|
|
93
|
+
"FeasibilityFn",
|
|
94
|
+
"ProjectionFn",
|
|
95
|
+
"NormalFn",
|
|
96
|
+
"DistanceFn",
|
|
97
|
+
"ConstraintLike",
|
|
98
|
+
# Config
|
|
99
|
+
"OthismosConfig",
|
|
100
|
+
# Pressure
|
|
101
|
+
"ConstraintType",
|
|
102
|
+
"Constraint",
|
|
103
|
+
"PressureMeasurement",
|
|
104
|
+
"PressureGauge",
|
|
105
|
+
"GoldilocksZone",
|
|
106
|
+
"compute_othismos",
|
|
107
|
+
"goldilocks_range",
|
|
108
|
+
"l2_constraint",
|
|
109
|
+
"box_constraint",
|
|
110
|
+
# Phases
|
|
111
|
+
"MoltPhase",
|
|
112
|
+
"PhaseClassifier",
|
|
113
|
+
"PhaseReading",
|
|
114
|
+
"MoltCycle",
|
|
115
|
+
"MoltCycleTracker",
|
|
116
|
+
# Diagnostics
|
|
117
|
+
"DiagnosticResult",
|
|
118
|
+
"PopcornDiagnostic",
|
|
119
|
+
"SystemHealth",
|
|
120
|
+
# Ecology
|
|
121
|
+
"Deposit",
|
|
122
|
+
"Reef",
|
|
123
|
+
"ReefLayer",
|
|
124
|
+
"Reefquake",
|
|
125
|
+
# Integrations
|
|
126
|
+
"MetricLogger",
|
|
127
|
+
"DictLogger",
|
|
128
|
+
"OthismosTorchCallback",
|
|
129
|
+
"OthismosTrainerCallback",
|
|
130
|
+
"constraint_from_torch_model",
|
|
131
|
+
# Context pressure
|
|
132
|
+
"ContextPressureGauge",
|
|
133
|
+
"ContextPressureMeasurement",
|
|
134
|
+
"cosine_distance",
|
|
135
|
+
"l2_distance",
|
|
136
|
+
"token_overlap",
|
|
137
|
+
# Controller
|
|
138
|
+
"PressureController",
|
|
139
|
+
"ControlAction",
|
|
140
|
+
"ActionType",
|
|
141
|
+
# Serialization
|
|
142
|
+
"save_history",
|
|
143
|
+
"load_history",
|
|
144
|
+
"save_diagnostic",
|
|
145
|
+
"export_metrics_csv",
|
|
146
|
+
"pressure_summary",
|
|
147
|
+
# Viz (optional)
|
|
148
|
+
"plot_pressure",
|
|
149
|
+
"plot_molt_cycle",
|
|
150
|
+
"plot_constraint_profile",
|
|
151
|
+
"plot_diagnostic_timeline",
|
|
152
|
+
# Pandas (optional)
|
|
153
|
+
"gauge_to_dataframe",
|
|
154
|
+
"tracker_to_dataframe",
|
|
155
|
+
"reef_to_dataframe",
|
|
156
|
+
]
|
othismos/cli.py
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
othismos — CLI for measuring constraint pressure in bounded systems.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
othismos pressure <model_dir> [--constraints config.yaml]
|
|
7
|
+
othismos reef <action> [--db reef.json]
|
|
8
|
+
othismos reef add <id> <content> [--refs id1,id2]
|
|
9
|
+
othismos reef list [--layer surface|consolidation|foundation]
|
|
10
|
+
othismos reef fail <id>
|
|
11
|
+
othismos reef stats
|
|
12
|
+
othismos diagnose <history.json> [--heat 1.0]
|
|
13
|
+
othismos version
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import json
|
|
20
|
+
import sys
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from othismos import (
|
|
24
|
+
PressureGauge,
|
|
25
|
+
PopcornDiagnostic,
|
|
26
|
+
Reef,
|
|
27
|
+
ReefLayer,
|
|
28
|
+
Reefquake,
|
|
29
|
+
SystemHealth,
|
|
30
|
+
__version__,
|
|
31
|
+
pressure_summary,
|
|
32
|
+
save_history,
|
|
33
|
+
load_history,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ─── Pressure Commands ────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
def cmd_pressure(args):
|
|
40
|
+
"""Analyze pressure data from a saved history."""
|
|
41
|
+
if not args.history:
|
|
42
|
+
print("Error: --history required (JSON file from save_history)")
|
|
43
|
+
sys.exit(1)
|
|
44
|
+
|
|
45
|
+
gauge = load_history(args.history)
|
|
46
|
+
summary = pressure_summary(gauge)
|
|
47
|
+
|
|
48
|
+
print("\n=== Pressure Analysis ===\n")
|
|
49
|
+
for k, v in summary.items():
|
|
50
|
+
if isinstance(v, dict):
|
|
51
|
+
print(f" {k}:")
|
|
52
|
+
for sk, sv in v.items():
|
|
53
|
+
print(f" {sk}: {sv:.6f}")
|
|
54
|
+
else:
|
|
55
|
+
print(f" {k}: {v}")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ─── Reef Commands ────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
REEF_DB_DEFAULT = ".othismos-reef.json"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _load_reef(path: str) -> Reef:
|
|
64
|
+
"""Load a reef from a JSON snapshot (or create new)."""
|
|
65
|
+
reef = Reef()
|
|
66
|
+
p = Path(path)
|
|
67
|
+
if p.exists():
|
|
68
|
+
data = json.loads(p.read_text())
|
|
69
|
+
for dep_data in data.get("deposits", []):
|
|
70
|
+
reef.submit(
|
|
71
|
+
dep_data["id"],
|
|
72
|
+
dep_data["content"],
|
|
73
|
+
references=dep_data.get("references", []),
|
|
74
|
+
)
|
|
75
|
+
# Restore age and layer
|
|
76
|
+
if dep_data.get("age"):
|
|
77
|
+
dep = reef.query(dep_data["id"])
|
|
78
|
+
if dep:
|
|
79
|
+
dep.age = dep_data["age"]
|
|
80
|
+
return reef
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _save_reef(reef: Reef, path: str):
|
|
84
|
+
"""Save a reef to JSON."""
|
|
85
|
+
deposits = []
|
|
86
|
+
for dep_id in list(reef._deposits.keys()):
|
|
87
|
+
dep = reef._deposits[dep_id]
|
|
88
|
+
deposits.append({
|
|
89
|
+
"id": dep.id,
|
|
90
|
+
"content": dep.content,
|
|
91
|
+
"references": dep.references,
|
|
92
|
+
"referenced_by": list(dep.referenced_by),
|
|
93
|
+
"layer": dep.layer.name,
|
|
94
|
+
"age": dep.age,
|
|
95
|
+
"depth_score": dep.depth_score,
|
|
96
|
+
})
|
|
97
|
+
data = {
|
|
98
|
+
"version": __version__,
|
|
99
|
+
"step": reef.step,
|
|
100
|
+
"deposits": deposits,
|
|
101
|
+
}
|
|
102
|
+
Path(path).write_text(json.dumps(data, indent=2))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def cmd_reef(args):
|
|
106
|
+
"""Manage a local knowledge reef."""
|
|
107
|
+
db_path = args.db or REEF_DB_DEFAULT
|
|
108
|
+
|
|
109
|
+
if args.action == "add":
|
|
110
|
+
reef = _load_reef(db_path)
|
|
111
|
+
refs = args.refs.split(",") if args.refs else []
|
|
112
|
+
accepted, msg = reef.submit(args.id, args.content, references=refs)
|
|
113
|
+
if accepted:
|
|
114
|
+
_save_reef(reef, db_path)
|
|
115
|
+
print(f"✓ {msg}")
|
|
116
|
+
else:
|
|
117
|
+
print(f"✗ {msg}")
|
|
118
|
+
sys.exit(1)
|
|
119
|
+
|
|
120
|
+
elif args.action == "list":
|
|
121
|
+
reef = _load_reef(db_path)
|
|
122
|
+
layer_filter = args.layer.upper() if args.layer else None
|
|
123
|
+
|
|
124
|
+
deposits = []
|
|
125
|
+
for dep in reef._deposits.values():
|
|
126
|
+
if layer_filter and dep.layer.name != layer_filter:
|
|
127
|
+
continue
|
|
128
|
+
deposits.append(dep)
|
|
129
|
+
|
|
130
|
+
deposits.sort(key=lambda d: (-d.depth_score, d.id))
|
|
131
|
+
|
|
132
|
+
if not deposits:
|
|
133
|
+
print("(empty reef)")
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
print(f"\n{'ID':<20} {'Layer':<15} {'Refs':<6} {'Age':<6} {'Depth':<8} Content")
|
|
137
|
+
print("-" * 90)
|
|
138
|
+
for dep in deposits:
|
|
139
|
+
content_preview = dep.content[:40] + ("..." if len(dep.content) > 40 else "")
|
|
140
|
+
print(f"{dep.id:<20} {dep.layer.name:<15} {dep.reference_count:<6} {dep.age:<6} {dep.depth_score:<8.1f} {content_preview}")
|
|
141
|
+
|
|
142
|
+
elif args.action == "fail":
|
|
143
|
+
reef = _load_reef(db_path)
|
|
144
|
+
try:
|
|
145
|
+
reef.fail_deposit(args.id)
|
|
146
|
+
except Reefquake as rq:
|
|
147
|
+
_save_reef(reef, db_path)
|
|
148
|
+
print(f"⚠️ REEFQUAKE: deposit '{rq.failed_id}' failed!")
|
|
149
|
+
print(f" {len(rq.affected)} deposits removed:")
|
|
150
|
+
for dep_id in rq.affected:
|
|
151
|
+
print(f" - {dep_id}")
|
|
152
|
+
except KeyError:
|
|
153
|
+
print(f"✗ Unknown deposit: {args.id}")
|
|
154
|
+
sys.exit(1)
|
|
155
|
+
|
|
156
|
+
elif args.action == "stats":
|
|
157
|
+
reef = _load_reef(db_path)
|
|
158
|
+
summary = reef.summary()
|
|
159
|
+
print("\n=== Reef Statistics ===\n")
|
|
160
|
+
for k, v in summary.items():
|
|
161
|
+
if isinstance(v, dict):
|
|
162
|
+
print(f" {k}:")
|
|
163
|
+
for sk, sv in v.items():
|
|
164
|
+
print(f" {sk}: {sv}")
|
|
165
|
+
else:
|
|
166
|
+
print(f" {k}: {v}")
|
|
167
|
+
|
|
168
|
+
elif args.action == "tick":
|
|
169
|
+
reef = _load_reef(db_path)
|
|
170
|
+
result = reef.tick()
|
|
171
|
+
_save_reef(reef, db_path)
|
|
172
|
+
print(f"Step {result['step']}: {result['total_deposits']} deposits")
|
|
173
|
+
if result["eroded"]:
|
|
174
|
+
print(f" Eroded: {', '.join(result['eroded'])}")
|
|
175
|
+
if result["promoted"]:
|
|
176
|
+
for dep_id, layer in result["promoted"]:
|
|
177
|
+
print(f" Promoted: {dep_id} → {layer}")
|
|
178
|
+
|
|
179
|
+
elif args.action == "search":
|
|
180
|
+
reef = _load_reef(db_path)
|
|
181
|
+
results = reef.search(args.query)
|
|
182
|
+
if not results:
|
|
183
|
+
print("(no results)")
|
|
184
|
+
return
|
|
185
|
+
for dep in results:
|
|
186
|
+
print(f"\n {dep.id} (depth={dep.depth_score:.1f}, layer={dep.layer.name})")
|
|
187
|
+
print(f" {dep.content[:200]}")
|
|
188
|
+
|
|
189
|
+
elif args.action == "graph":
|
|
190
|
+
reef = _load_reef(db_path)
|
|
191
|
+
graph = reef.citation_graph()
|
|
192
|
+
print(json.dumps(graph, indent=2))
|
|
193
|
+
|
|
194
|
+
else:
|
|
195
|
+
print(f"Unknown reef action: {args.action}")
|
|
196
|
+
sys.exit(1)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# ─── Diagnose Commands ────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
def cmd_diagnose(args):
|
|
202
|
+
"""Run popcorn diagnostic on saved pressure data."""
|
|
203
|
+
gauge = load_history(args.history)
|
|
204
|
+
pressures = [m.pressure for m in gauge.history]
|
|
205
|
+
|
|
206
|
+
diag = PopcornDiagnostic()
|
|
207
|
+
result = diag.diagnose(pressures, heat=args.heat)
|
|
208
|
+
|
|
209
|
+
health_emoji = {
|
|
210
|
+
SystemHealth.POP: "🍿",
|
|
211
|
+
SystemHealth.BURN: "🔥",
|
|
212
|
+
SystemHealth.SEEP: "💧",
|
|
213
|
+
SystemHealth.DORMANT: "😴",
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
print(f"\n{health_emoji.get(result.health, '?')} Health: {result.health.value.upper()}")
|
|
217
|
+
print(f" Confidence: {result.confidence:.0%}")
|
|
218
|
+
print(f" Pressure: {result.pressure:.6f}")
|
|
219
|
+
print(f" Heat: {result.heat:.6f}")
|
|
220
|
+
print(f" Efficiency: {result.pressure_efficiency:.4f}")
|
|
221
|
+
print()
|
|
222
|
+
for signal in result.signals:
|
|
223
|
+
print(f" → {signal}")
|
|
224
|
+
print()
|
|
225
|
+
print(f" {result.recommendation}")
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
# ─── Main ─────────────────────────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
def main():
|
|
231
|
+
parser = argparse.ArgumentParser(
|
|
232
|
+
prog="othismos",
|
|
233
|
+
description="Measure constraint pressure in bounded systems. The push IS the knowing.",
|
|
234
|
+
)
|
|
235
|
+
sub = parser.add_subparsers(dest="command")
|
|
236
|
+
|
|
237
|
+
# Pressure
|
|
238
|
+
p_pressure = sub.add_parser("pressure", help="Analyze pressure history")
|
|
239
|
+
p_pressure.add_argument("--history", help="JSON file from save_history")
|
|
240
|
+
p_pressure.set_defaults(func=cmd_pressure)
|
|
241
|
+
|
|
242
|
+
# Reef
|
|
243
|
+
p_reef = sub.add_parser("reef", help="Manage a knowledge reef")
|
|
244
|
+
p_reef.add_argument("action", choices=["add", "list", "fail", "stats", "tick", "search", "graph"])
|
|
245
|
+
p_reef.add_argument("id", nargs="?", help="Deposit ID (for add/fail)")
|
|
246
|
+
p_reef.add_argument("content", nargs="?", help="Deposit content (for add)")
|
|
247
|
+
p_reef.add_argument("--refs", help="Comma-separated reference IDs (for add)")
|
|
248
|
+
p_reef.add_argument("--layer", choices=["surface", "consolidation", "foundation"])
|
|
249
|
+
p_reef.add_argument("--query", help="Search query (for search action)")
|
|
250
|
+
p_reef.add_argument("--db", help=f"Reef database path (default: {REEF_DB_DEFAULT})")
|
|
251
|
+
p_reef.set_defaults(func=cmd_reef)
|
|
252
|
+
|
|
253
|
+
# Diagnose
|
|
254
|
+
p_diag = sub.add_parser("diagnose", help="Run popcorn diagnostic")
|
|
255
|
+
p_diag.add_argument("history", help="JSON file from save_history")
|
|
256
|
+
p_diag.add_argument("--heat", type=float, default=1.0, help="Current external pressure")
|
|
257
|
+
p_diag.set_defaults(func=cmd_diagnose)
|
|
258
|
+
|
|
259
|
+
# Version
|
|
260
|
+
p_version = sub.add_parser("version", help="Print version")
|
|
261
|
+
p_version.set_defaults(func=lambda a: print(f"othismos {__version__}"))
|
|
262
|
+
|
|
263
|
+
args = parser.parse_args()
|
|
264
|
+
if not args.command:
|
|
265
|
+
parser.print_help()
|
|
266
|
+
sys.exit(1)
|
|
267
|
+
|
|
268
|
+
args.func(args)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
if __name__ == "__main__":
|
|
272
|
+
main()
|
othismos/config.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration objects for óthismos.
|
|
3
|
+
|
|
4
|
+
Single dataclass to configure gauge + tracker + diagnostic + controller
|
|
5
|
+
together. Supports YAML/dict construction for config-file-driven workflows.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field, asdict
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(slots=True)
|
|
15
|
+
class OthismosConfig:
|
|
16
|
+
"""Configuration for the full óthismos monitoring stack.
|
|
17
|
+
|
|
18
|
+
Pass to PressureGauge, MoltCycleTracker, PopcornDiagnostic, and
|
|
19
|
+
PressureController to ensure consistent settings.
|
|
20
|
+
|
|
21
|
+
Example:
|
|
22
|
+
>>> config = OthismosConfig(crisis_threshold=0.5, window_size=5000)
|
|
23
|
+
>>> gauge = PressureGauge(window_size=config.window_size)
|
|
24
|
+
>>> classifier = PhaseClassifier(
|
|
25
|
+
... crisis_threshold=config.crisis_threshold,
|
|
26
|
+
... expansion_floor=config.expansion_floor,
|
|
27
|
+
... )
|
|
28
|
+
|
|
29
|
+
Or from dict/YAML:
|
|
30
|
+
>>> config = OthismosConfig.from_dict({"crisis_threshold": 0.5})
|
|
31
|
+
>>> config = OthismosConfig.from_yaml("config.yaml")
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
# Pressure gauge
|
|
35
|
+
window_size: int = 1000
|
|
36
|
+
|
|
37
|
+
# Phase classifier
|
|
38
|
+
crisis_threshold: float | None = None
|
|
39
|
+
expansion_floor: float | None = None
|
|
40
|
+
settlement_decline_rate: float = -0.5
|
|
41
|
+
|
|
42
|
+
# Popcorn diagnostic
|
|
43
|
+
burn_threshold: float = 0.01
|
|
44
|
+
seep_volatility: float = 0.5
|
|
45
|
+
pop_efficiency: float = 0.3
|
|
46
|
+
|
|
47
|
+
# Controller
|
|
48
|
+
lr_bounds: tuple[float, float] = (1e-6, 1.0)
|
|
49
|
+
crisis_lr_factor: float = 0.5
|
|
50
|
+
expansion_lr_factor: float = 1.3
|
|
51
|
+
burn_patience: int = 50
|
|
52
|
+
|
|
53
|
+
# Logging
|
|
54
|
+
log_every: int = 1
|
|
55
|
+
log_backend: str = "dict" # dict | wandb | tensorboard | stdout
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def from_dict(cls, data: dict[str, Any]) -> "OthismosConfig":
|
|
59
|
+
"""Create config from a dictionary."""
|
|
60
|
+
valid = {k: v for k, v in data.items() if k in cls.__annotations__ or k in cls.__dataclass_fields__}
|
|
61
|
+
return cls(**valid)
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def from_yaml(cls, path: str) -> "OthismosConfig":
|
|
65
|
+
"""Create config from a YAML file."""
|
|
66
|
+
import yaml
|
|
67
|
+
with open(path) as f:
|
|
68
|
+
data = yaml.safe_load(f)
|
|
69
|
+
return cls.from_dict(data or {})
|
|
70
|
+
|
|
71
|
+
def to_dict(self) -> dict[str, Any]:
|
|
72
|
+
"""Serialize config to dict."""
|
|
73
|
+
return asdict(self)
|
|
74
|
+
|
|
75
|
+
def to_yaml(self, path: str) -> None:
|
|
76
|
+
"""Write config to a YAML file."""
|
|
77
|
+
import yaml
|
|
78
|
+
with open(path, "w") as f:
|
|
79
|
+
yaml.safe_dump(self.to_dict(), f, default_flow_style=False)
|
|
80
|
+
|
|
81
|
+
def build_gauge(self):
|
|
82
|
+
"""Construct a PressureGauge from this config."""
|
|
83
|
+
from othismos.pressure import PressureGauge
|
|
84
|
+
return PressureGauge(window_size=self.window_size)
|
|
85
|
+
|
|
86
|
+
def build_classifier(self):
|
|
87
|
+
"""Construct a PhaseClassifier from this config."""
|
|
88
|
+
from othismos.phases import PhaseClassifier
|
|
89
|
+
return PhaseClassifier(
|
|
90
|
+
crisis_threshold=self.crisis_threshold,
|
|
91
|
+
expansion_floor=self.expansion_floor,
|
|
92
|
+
settlement_decline_rate=self.settlement_decline_rate,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def build_diagnostic(self):
|
|
96
|
+
"""Construct a PopcornDiagnostic from this config."""
|
|
97
|
+
from othismos.diagnostics import PopcornDiagnostic
|
|
98
|
+
return PopcornDiagnostic(
|
|
99
|
+
burn_threshold=self.burn_threshold,
|
|
100
|
+
seep_volatility=self.seep_volatility,
|
|
101
|
+
pop_efficiency=self.pop_efficiency,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def build_controller(self, gauge=None, tracker=None):
|
|
105
|
+
"""Construct a PressureController from this config."""
|
|
106
|
+
from othismos.controller import PressureController
|
|
107
|
+
from othismos.phases import MoltCycleTracker
|
|
108
|
+
gauge = gauge or self.build_gauge()
|
|
109
|
+
tracker = tracker or MoltCycleTracker(classifier=self.build_classifier())
|
|
110
|
+
return PressureController(
|
|
111
|
+
gauge=gauge,
|
|
112
|
+
tracker=tracker,
|
|
113
|
+
lr_bounds=self.lr_bounds,
|
|
114
|
+
crisis_lr_factor=self.crisis_lr_factor,
|
|
115
|
+
expansion_lr_factor=self.expansion_lr_factor,
|
|
116
|
+
burn_patience=self.burn_patience,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def build_all(self):
|
|
120
|
+
"""Build the full stack: gauge, tracker, diagnostic, controller.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
(gauge, tracker, diagnostic, controller)
|
|
124
|
+
"""
|
|
125
|
+
gauge = self.build_gauge()
|
|
126
|
+
classifier = self.build_classifier()
|
|
127
|
+
from othismos.phases import MoltCycleTracker
|
|
128
|
+
tracker = MoltCycleTracker(classifier=classifier)
|
|
129
|
+
diagnostic = self.build_diagnostic()
|
|
130
|
+
controller = self.build_controller(gauge=gauge, tracker=tracker)
|
|
131
|
+
return gauge, tracker, diagnostic, controller
|