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 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