dreamforge 0.2.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.
Files changed (41) hide show
  1. dreamforge/__init__.py +8 -0
  2. dreamforge/core/__init__.py +6 -0
  3. dreamforge/core/config.py +142 -0
  4. dreamforge/core/models/__init__.py +1 -0
  5. dreamforge/core/models/dream_context.py +323 -0
  6. dreamforge/core/models/events.py +310 -0
  7. dreamforge/core/models/memory_graph.py +385 -0
  8. dreamforge/core/models/neurochemistry.py +124 -0
  9. dreamforge/core/models/sleep_cycle.py +394 -0
  10. dreamforge/core/provenance/__init__.py +1 -0
  11. dreamforge/core/provenance/clock.py +38 -0
  12. dreamforge/core/providers/__init__.py +1 -0
  13. dreamforge/core/providers/narrative.py +230 -0
  14. dreamforge/core/scoring/__init__.py +1 -0
  15. dreamforge/core/scoring/bizarreness.py +87 -0
  16. dreamforge/core/serialization/__init__.py +1 -0
  17. dreamforge/core/serialization/dqcj.py +283 -0
  18. dreamforge/demo.py +120 -0
  19. dreamforge/integrations/__init__.py +5 -0
  20. dreamforge/integrations/anthropic_compat.py +174 -0
  21. dreamforge/integrations/errors.py +33 -0
  22. dreamforge/integrations/openai_compat.py +177 -0
  23. dreamforge/integrations/retry.py +72 -0
  24. dreamforge/integrations/transport.py +69 -0
  25. dreamforge/simulation/__init__.py +1 -0
  26. dreamforge/simulation/counterfactual.py +178 -0
  27. dreamforge/simulation/engine.py +310 -0
  28. dreamforge/simulation/ensemble.py +135 -0
  29. dreamforge/simulation/export_import.py +471 -0
  30. dreamforge/simulation/report.py +153 -0
  31. dreamforge/simulation/run_repository.py +176 -0
  32. dreamforge/simulation/sweeps.py +133 -0
  33. dreamforge/visualization/__init__.py +1 -0
  34. dreamforge/visualization/dashboard.py +183 -0
  35. dreamforge/visualization/loader.py +68 -0
  36. dreamforge-0.2.0.dist-info/METADATA +156 -0
  37. dreamforge-0.2.0.dist-info/RECORD +41 -0
  38. dreamforge-0.2.0.dist-info/WHEEL +5 -0
  39. dreamforge-0.2.0.dist-info/entry_points.txt +3 -0
  40. dreamforge-0.2.0.dist-info/licenses/LICENSE +21 -0
  41. dreamforge-0.2.0.dist-info/top_level.txt +1 -0
dreamforge/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ """DreamForge — deterministic research and visualization simulator.
2
+
3
+ DreamForge is a research and visualization simulator. It does not measure
4
+ brains, diagnose conditions, predict dreams, infer psychological meaning, or
5
+ provide medical advice.
6
+ """
7
+
8
+ __version__ = "0.2.0"
@@ -0,0 +1,6 @@
1
+ """DreamForge deterministic core.
2
+
3
+ Nothing in this package may import FastAPI, Streamlit, LangGraph, provider
4
+ SDKs, environment variables, clocks, filesystem/network clients, or runtime
5
+ configuration loaders (MASTER_PROMPT.md section 3).
6
+ """
@@ -0,0 +1,142 @@
1
+ """Strict simulation configuration loading (fail closed).
2
+
3
+ The configuration is the only external input to a run. It is validated into
4
+ frozen Pydantic models before any engine object is constructed; any violation
5
+ raises :class:`ConfigError` with a machine-readable ``code``. No environment
6
+ variables, clocks, or network access participate.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
15
+
16
+ from dreamforge.core.models.events import ALL_STAGES, StageName
17
+ from dreamforge.core.models.memory_graph import GraphSpecConfig, ReplaySelectorConfig
18
+ from dreamforge.core.models.neurochemistry import NeurochemistryConfig
19
+ from dreamforge.core.models.sleep_cycle import (
20
+ CircadianConfig,
21
+ DwellDistribution,
22
+ ProcessSConfig,
23
+ TransitionMatrixConfig,
24
+ )
25
+
26
+ #: Hard limits guarding against oversized/hostile configs (section 6.3).
27
+ MAX_EPOCH_SECONDS = 3600.0
28
+ MIN_EPOCH_SECONDS = 1.0
29
+ MAX_TOTAL_TICKS = 100_000
30
+
31
+
32
+ class ConfigError(ValueError):
33
+ """Raised when configuration fails validation; ``code`` is stable."""
34
+
35
+ def __init__(self, code: str, message: str) -> None:
36
+ super().__init__(message)
37
+ self.code = code
38
+
39
+
40
+ class ReplayPolicyConfig(BaseModel):
41
+ """Where replay selection gets its graph and its policy."""
42
+
43
+ model_config = ConfigDict(frozen=True, extra="forbid")
44
+
45
+ synthetic_graph: GraphSpecConfig = Field(default_factory=GraphSpecConfig)
46
+ selector: ReplaySelectorConfig = Field(default_factory=ReplaySelectorConfig)
47
+ replay_every_n_epochs: int = Field(default=8, ge=1)
48
+
49
+
50
+ class SimulationConfig(BaseModel):
51
+ """Fully validated run configuration (immutable after load)."""
52
+
53
+ model_config = ConfigDict(frozen=True, extra="forbid")
54
+
55
+ schema_version: str = Field(pattern=r"^\d+\.\d+$")
56
+ run_id: str = Field(min_length=8, max_length=64, pattern=r"^[A-Za-z0-9._-]+$")
57
+ run_seed: int = Field(ge=0, le=2**64 - 1)
58
+ epoch_seconds: float = Field(default=30.0)
59
+ total_ticks: int = Field(ge=1)
60
+ initial_stage: StageName = "Wake"
61
+ process_s: ProcessSConfig = Field(default_factory=ProcessSConfig)
62
+ circadian: CircadianConfig = Field(default_factory=CircadianConfig)
63
+ transitions: TransitionMatrixConfig
64
+ dwells: dict[StageName, DwellDistribution]
65
+ chemistry: NeurochemistryConfig
66
+ replay_policy: ReplayPolicyConfig = Field(default_factory=ReplayPolicyConfig)
67
+
68
+ @field_validator("epoch_seconds")
69
+ @classmethod
70
+ def _epoch_bounds(cls, value: float) -> float:
71
+ if not MIN_EPOCH_SECONDS <= value <= MAX_EPOCH_SECONDS:
72
+ msg = f"epoch_seconds must be within " f"[{MIN_EPOCH_SECONDS}, {MAX_EPOCH_SECONDS}]"
73
+ raise ValueError(msg)
74
+ return value
75
+
76
+ @field_validator("total_ticks")
77
+ @classmethod
78
+ def _ticks_cap(cls, value: int) -> int:
79
+ if value > MAX_TOTAL_TICKS:
80
+ msg = f"total_ticks exceeds MAX_TOTAL_TICKS={MAX_TOTAL_TICKS}"
81
+ raise ValueError(msg)
82
+ return value
83
+
84
+ @model_validator(mode="after")
85
+ def _dwell_support(self) -> SimulationConfig:
86
+ missing = [s for s in ALL_STAGES if s not in self.dwells]
87
+ if missing:
88
+ msg = f"dwells missing stages: {missing}"
89
+ raise ValueError(msg)
90
+ for stage, dwell in self.dwells.items():
91
+ longest = dwell.min_epochs + len(dwell.weights) - 1
92
+ if longest > self.total_ticks:
93
+ msg = f"dwell support for {stage} exceeds total_ticks"
94
+ raise ValueError(msg)
95
+ return self
96
+
97
+ def simulated_time_minutes(self, tick: int) -> float:
98
+ """Documented conversion: tick × epoch_seconds / 60."""
99
+ return tick * self.epoch_seconds / 60.0
100
+
101
+
102
+ def dumps_config_canonical(config: SimulationConfig) -> bytes:
103
+ """Canonical configuration snapshot bytes (embedded into exports).
104
+
105
+ Lives here rather than in the simulation package so that export/import
106
+ verification never needs to load the engine module at all.
107
+ """
108
+ from dreamforge.core.serialization.dqcj import dumps_canonical
109
+
110
+ return dumps_canonical(config.model_dump(mode="json"))
111
+
112
+
113
+ def load_config(source: str | Path | dict[str, Any]) -> SimulationConfig:
114
+ """Load and strictly validate a run configuration.
115
+
116
+ ``source`` may be an already-parsed dict or a path to UTF-8 JSON. Raises
117
+ :class:`ConfigError` on any violation (schema, ranges, structure).
118
+ """
119
+ if isinstance(source, dict):
120
+ raw = source
121
+ else:
122
+ path = Path(source)
123
+ try:
124
+ text = path.read_text(encoding="utf-8")
125
+ except OSError as exc:
126
+ msg = f"config file unreadable: {exc.__class__.__name__}"
127
+ raise ConfigError("config_unreadable", msg) from exc
128
+ from dreamforge.core.serialization.dqcj import loads_strict
129
+
130
+ try:
131
+ raw = loads_strict(text)
132
+ except ValueError as exc:
133
+ raise ConfigError("config_invalid_json", str(exc)) from exc
134
+ if not isinstance(raw, dict):
135
+ raise ConfigError(
136
+ "config_invalid_json",
137
+ "top-level configuration must be a JSON object",
138
+ )
139
+ try:
140
+ return SimulationConfig.model_validate(raw)
141
+ except Exception as exc: # noqa: BLE001 - re-raised typed below
142
+ raise ConfigError("config_validation_failed", str(exc)) from exc
@@ -0,0 +1 @@
1
+ """Domain models: sleep cycle, neurochemistry, memory graph, events."""
@@ -0,0 +1,323 @@
1
+ """Deterministic dream context and segments (MASTER_PROMPT.md §5.4, §6.1).
2
+
3
+ ``DreamContext``/``DreamSegment`` are immutable and built ONLY from structured
4
+ state and selected synthetic node IDs — never prose. They are pure post-run
5
+ projections of emitted events; constructing one never mutates core state and
6
+ providers can never write back (ADR 0003).
7
+
8
+ Every feature documents its evidence variables, missing-data behaviour
9
+ (missing evidence ⇒ value 0.0), category distribution source, normalization,
10
+ and bounds. Values are structured numbers in [0, 1], never prose scores.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from typing import Any
16
+
17
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
18
+
19
+ #: DQCJ-1 quantization registry additions for context payloads. The score is
20
+ #: quantized to 0.01 (two-decimal percentage points) at the canonical boundary;
21
+ #: features keep 0.000001 like other engine floats.
22
+ CONTEXT_QUANTIZATIONS: dict[str, str] = {
23
+ "features.scene_discontinuity": "0.000001",
24
+ "features.entity_incongruity": "0.000001",
25
+ "features.causal_implausibility": "0.000001",
26
+ "features.temporal_distortion": "0.000001",
27
+ "features.identity_instability": "0.000001",
28
+ "features.memory_blending_entropy": "0.000001",
29
+ "score_bizarreness_normalized": "0.000001",
30
+ }
31
+
32
+
33
+ class DreamSegment(BaseModel):
34
+ """One contiguous stage episode with its replay selections."""
35
+
36
+ model_config = ConfigDict(frozen=True, extra="forbid")
37
+
38
+ segment_index: int = Field(ge=0)
39
+ stage: str
40
+ start_tick: int = Field(ge=0)
41
+ end_tick: int = Field(ge=0) # inclusive epoch of the episode's last tick
42
+ selected_node_ids: tuple[str, ...] = ()
43
+
44
+ @model_validator(mode="after")
45
+ def _order(self) -> DreamSegment:
46
+ if self.end_tick < self.start_tick:
47
+ msg = "end_tick must be >= start_tick"
48
+ raise ValueError(msg)
49
+ return self
50
+
51
+
52
+ class ContextFeatures(BaseModel):
53
+ """The six bounded [0, 1] structured features (§5.4)."""
54
+
55
+ model_config = ConfigDict(frozen=True, extra="forbid")
56
+
57
+ scene_discontinuity: float = Field(ge=0.0, le=1.0)
58
+ entity_incongruity: float = Field(ge=0.0, le=1.0)
59
+ causal_implausibility: float = Field(ge=0.0, le=1.0)
60
+ temporal_distortion: float = Field(ge=0.0, le=1.0)
61
+ identity_instability: float = Field(ge=0.0, le=1.0)
62
+ memory_blending_entropy: float = Field(ge=0.0, le=1.0)
63
+
64
+
65
+ class DreamContext(BaseModel):
66
+ """Immutable structured context over one completed run."""
67
+
68
+ model_config = ConfigDict(frozen=True, extra="forbid")
69
+
70
+ run_id: str
71
+ schema_version: str
72
+ total_ticks: int = Field(ge=1)
73
+ segments: tuple[DreamSegment, ...]
74
+ features: ContextFeatures
75
+ score_bizarreness_0_100: float = Field(ge=0.0, le=100.0)
76
+ scorer_version: str
77
+ output_class: str = "mechanistic_proxy"
78
+ visible_label: str = "Simulated model proxy — not a biological measurement"
79
+
80
+ @model_validator(mode="after")
81
+ def _segments_ordered(self) -> DreamContext:
82
+ previous_end = -1
83
+ for seg in self.segments:
84
+ if seg.start_tick <= previous_end:
85
+ msg = "segments must be strictly ordered, non-overlapping"
86
+ raise ValueError(msg)
87
+ previous_end = seg.end_tick
88
+ return self
89
+
90
+
91
+ def build_segments(events: list[Any], *, min_segment_ticks: int = 2) -> tuple[DreamSegment, ...]:
92
+ """Group consecutive same-stage epochs into segments.
93
+
94
+ Evidence: ``sleep_state.tick`` + ``stage`` per epoch; other event types
95
+ carry no stage evidence and are ignored. Every stage episode — including
96
+ short ones — is preserved as its own segment so the context never
97
+ mislabels stages (``min_segment_ticks`` is retained for API stability and
98
+ currently does not merge, documented honestly here).
99
+ """
100
+ if not events:
101
+ return ()
102
+ # Only sleep_state events carry stage evidence.
103
+ state_events = [e for e in events if e.event_type == "sleep_state"]
104
+ if not state_events:
105
+ return ()
106
+ episodes: list[tuple[str, int, int]] = []
107
+ stage = str(state_events[0].payload.stage)
108
+ start = int(state_events[0].tick)
109
+ current = start
110
+ for event in state_events[1:]:
111
+ tick = int(event.tick)
112
+ event_stage = str(event.payload.stage)
113
+ if event_stage != stage or tick != current + 1:
114
+ episodes.append((stage, start, current))
115
+ stage, start = event_stage, tick
116
+ current = tick
117
+ episodes.append((stage, start, current))
118
+
119
+ merged: list[list[int]] = []
120
+ labels: list[str] = []
121
+ for stage_name, ep_start, ep_end in episodes:
122
+ merged.append([ep_start, ep_end])
123
+ labels.append(stage_name)
124
+
125
+ return tuple(
126
+ DreamSegment(
127
+ segment_index=index,
128
+ stage=label,
129
+ start_tick=span[0],
130
+ end_tick=span[1],
131
+ )
132
+ for index, (label, span) in enumerate(zip(labels, merged, strict=True))
133
+ )
134
+
135
+
136
+ # --- structured features (§5.4) --------------------------------------------
137
+ #
138
+ # Each feature documents: evidence variables, missing-data behaviour (missing
139
+ # evidence ⇒ 0.0), normalization (explicit denominator), and bounds. All are
140
+ # deterministic functions over emitted events/segments only.
141
+
142
+
143
+ def _replays_in(events: list[Any]) -> list[Any]:
144
+ return [e for e in events if e.event_type == "memory_replay"]
145
+
146
+
147
+ def feature_scene_discontinuity(segments: tuple[DreamSegment, ...], total_ticks: int) -> float:
148
+ """Stage-transition density per epoch, normalized by observed maximum.
149
+
150
+ Evidence: segment boundaries. Missing data (no segments) ⇒ 0.0.
151
+ Normalizer: 1 transition per 10 epochs is mapped to the full value — a
152
+ declared calibration constant of this scorer version, not an empirical fit.
153
+ """
154
+ if len(segments) < 2 or total_ticks <= 0:
155
+ return 0.0
156
+ transitions = len(segments) - 1
157
+ raw = transitions / max(total_ticks - 1, 1)
158
+ return min(raw / 0.1, 1.0)
159
+
160
+
161
+ def feature_entity_incongruity(
162
+ type_sets: list[set[str]],
163
+ ) -> float:
164
+ """Mean type-mix breadth across replay selections.
165
+
166
+ Evidence: the set of distinct node types in each replay's selected IDs
167
+ (types joined from the graph snapshot). Normalization: selections drawing
168
+ from all 6 node types map to 1.0 (declared constant of this scorer
169
+ version); missing selections ⇒ 0.0.
170
+ """
171
+ if not type_sets:
172
+ return 0.0
173
+ mixes = [min(len(types) / 6.0, 1.0) for types in type_sets]
174
+ return sum(mixes) / len(mixes)
175
+
176
+
177
+ def feature_causal_implausibility(events: list[Any]) -> float:
178
+ """Rate of N3→REM direct transitions among all stage transitions.
179
+
180
+ Declared hypothesis-tagged proxy pattern: deep-slowed cortex followed
181
+ immediately by REM-like activity is the configured 'implausible' pair for
182
+ this toy grammar. Missing transitions ⇒ 0.0.
183
+ """
184
+ transitions = [e for e in events if e.event_type == "stage_transition"]
185
+ if not transitions:
186
+ return 0.0
187
+ implausible = sum(
188
+ 1 for e in transitions if e.payload.from_stage == "N3" and e.payload.to_stage == "REM"
189
+ )
190
+ return min(implausible / len(transitions), 1.0)
191
+
192
+
193
+ def feature_temporal_distortion(segments: tuple[DreamSegment, ...]) -> float:
194
+ """Coefficient of variation of segment lengths, squashed to [0, 1].
195
+
196
+ Evidence: epoch spans of segments. A perfectly regular structure maps
197
+ toward 0; wildly irregular lengths map toward 1 (bounded via
198
+ CV/(1+CV)). Fewer than two segments ⇒ missing evidence ⇒ 0.0.
199
+ """
200
+ if len(segments) < 2:
201
+ return 0.0
202
+ lengths = [s.end_tick - s.start_tick + 1 for s in segments]
203
+ mean_length = sum(lengths) / len(lengths)
204
+ if mean_length <= 0:
205
+ return 0.0
206
+ variance = sum((n - mean_length) ** 2 for n in lengths) / len(lengths)
207
+ cv = (variance**0.5) / mean_length
208
+ return float(min(cv / (1.0 + cv), 1.0))
209
+
210
+
211
+ def feature_identity_instability(replays: list[Any]) -> float:
212
+ """Mean fraction of newly-introduced nodes across consecutive selections.
213
+
214
+ Evidence: consecutive ``selected_node_ids`` sets. One selection or none
215
+ ⇒ missing evidence ⇒ 0.0. Identical successive selections ⇒ 0.0.
216
+ """
217
+ if len(replays) < 2:
218
+ return 0.0
219
+ fractions: list[float] = []
220
+ previous: frozenset[str] | None = None
221
+ for event in replays:
222
+ current = frozenset(event.payload.selected_node_ids)
223
+ if previous is not None and previous:
224
+ fresh = len(current - previous)
225
+ fractions.append(fresh / len(previous))
226
+ previous = current
227
+ if not fractions:
228
+ return 0.0
229
+ return min(sum(fractions) / len(fractions), 1.0)
230
+
231
+
232
+ def feature_memory_blending_entropy(replays: list[Any]) -> float:
233
+ """Shannon entropy of node-selection frequencies over the run.
234
+
235
+ Evidence: every selected ID across replays. Normalized by ln(k) where k =
236
+ number of DISTINCT nodes observed (documented denominator); k < 2 ⇒ 0.0.
237
+ Uniform coverage of all observed nodes approaches 1.0.
238
+ """
239
+ import math
240
+
241
+ counts: dict[str, int] = {}
242
+ for event in replays:
243
+ for node_id in event.payload.selected_node_ids:
244
+ counts[node_id] = counts.get(node_id, 0) + 1
245
+ total_selections = sum(counts.values())
246
+ if total_selections == 0 or len(counts) < 2:
247
+ return 0.0
248
+ entropy = -sum(
249
+ (count / total_selections) * math.log(count / total_selections, math.e)
250
+ for count in counts.values()
251
+ )
252
+ return min(entropy / math.log(len(counts), math.e), 1.0)
253
+
254
+
255
+ def build_dream_context(
256
+ *,
257
+ run_id: str,
258
+ schema_version: str,
259
+ total_ticks: int,
260
+ events: list[Any],
261
+ node_type_lookup: dict[str, str] | None = None,
262
+ weights: dict[str, float] | None = None,
263
+ ) -> DreamContext:
264
+ """Construct the immutable context from emitted events (pure function)."""
265
+ from dreamforge.core.scoring.bizarreness import score_bizarreness
266
+
267
+ segments = build_segments(events)
268
+ replays = _replays_in(events)
269
+
270
+ # Attach each epoch's replay selections to the segment containing it so
271
+ # downstream consumers (provider projection) see tokens per episode.
272
+ if segments and replays:
273
+ selections_by_tick: dict[int, tuple[str, ...]] = {
274
+ int(event.tick): tuple(event.payload.selected_node_ids) for event in replays
275
+ }
276
+ attached: list[DreamSegment] = []
277
+ for segment in segments:
278
+ token_ids: list[str] = []
279
+ for tick in range(segment.start_tick, segment.end_tick + 1):
280
+ for node_id in selections_by_tick.get(tick, ()):
281
+ if node_id not in token_ids:
282
+ token_ids.append(node_id)
283
+ if token_ids:
284
+ attached.append(
285
+ segment.model_copy(update={"selected_node_ids": tuple(token_ids)}),
286
+ )
287
+ else:
288
+ attached.append(segment)
289
+ segments = tuple(attached)
290
+
291
+ # Entity incongruity joins node types from the caller-supplied lookup
292
+ # (built from the graph snapshot); without it, documented missing-data
293
+ # behaviour applies (0.0).
294
+ type_sets: list[set[str]] = []
295
+ if node_type_lookup:
296
+ type_sets = [
297
+ {
298
+ node_type_lookup.get(node_id, "unknown")
299
+ for node_id in event.payload.selected_node_ids
300
+ }
301
+ for event in replays
302
+ ]
303
+ entity_value = feature_entity_incongruity(type_sets)
304
+
305
+ features = ContextFeatures(
306
+ scene_discontinuity=feature_scene_discontinuity(segments, total_ticks),
307
+ entity_incongruity=entity_value,
308
+ causal_implausibility=feature_causal_implausibility(events),
309
+ temporal_distortion=feature_temporal_distortion(segments),
310
+ identity_instability=feature_identity_instability(replays),
311
+ memory_blending_entropy=feature_memory_blending_entropy(replays),
312
+ )
313
+
314
+ normalized, absolute = score_bizarreness(features, weights=weights)
315
+ return DreamContext(
316
+ run_id=run_id,
317
+ schema_version=schema_version,
318
+ total_ticks=max(total_ticks, 1),
319
+ segments=segments,
320
+ features=features,
321
+ score_bizarreness_0_100=absolute,
322
+ scorer_version="bizarreness-v1",
323
+ )