File size: 8,175 Bytes
8270f31
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1edadfb
 
8270f31
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
"""Visualize latent variable discovery: true vs inferred hidden states.

Usage:
    python -m ontology_architect.scripts.visualize_discovery \
        --config configs/dual_fluid_demo.json \
        --baseline dual_fluid_dsl \
        --output artifacts/discovery_plot.png

    python -m ontology_architect.scripts.visualize_discovery \
        --config configs/dual_fluid_demo.json \
        --theory-file path/to/generated_theory.json \
        --output artifacts/trained_discovery.png
"""

from __future__ import annotations

import argparse
import json
from pathlib import Path

try:
    from ontology_architect.baselines import get_baseline
    from ontology_architect.config import load_config
    from ontology_architect.models import OntologyArchitectAction
    from ontology_architect.sandbox import TheorySandbox
    from ontology_architect.universe import ProceduralAlienUniverse
except ImportError:  # pragma: no cover
    from baselines import get_baseline
    from config import load_config
    from models import OntologyArchitectAction
    from sandbox import TheorySandbox
    from universe import ProceduralAlienUniverse


def main() -> None:
    parser = argparse.ArgumentParser(description="Visualize latent variable discovery")
    parser.add_argument("--config", default="configs/dual_fluid_demo.json")
    parser.add_argument("--baseline", default=None, help="Name of baseline to use as theory")
    parser.add_argument("--theory-file", default=None, help="Path to theory file (DSL JSON or Python)")
    parser.add_argument("--seed", type=int, default=None, help="Override universe seed")
    parser.add_argument("--steps", type=int, default=80, help="Number of timesteps to simulate")
    parser.add_argument("--output", default="artifacts/discovery_plot.png")
    parser.add_argument("--compare-baseline", default=None, help="Second baseline for comparison")
    args = parser.parse_args()

    config = load_config(args.config)
    if args.seed is not None:
        from dataclasses import replace
        config = replace(config, universe=replace(config.universe, seed=args.seed))

    # Load theory module
    if args.theory_file:
        theory_module = Path(args.theory_file).read_text(encoding="utf-8")
    elif args.baseline:
        theory_module = get_baseline(args.baseline)
    else:
        theory_module = get_baseline("dual_fluid_dsl")

    comparison_module = get_baseline(args.compare_baseline) if args.compare_baseline else None

    # Generate ground truth with hidden latent states
    universe = ProceduralAlienUniverse(config.universe)
    records, latent_trajectory = universe.generate_with_latents(args.steps)

    # Extract true latent time series
    times = [lr.t for lr in latent_trajectory]
    latent_names = universe.latent_names
    true_latents = {name: [lr.latent_state[name] for lr in latent_trajectory] for name in latent_names}
    true_sensors = {name: [lr.sensors[name] for lr in latent_trajectory] for name in universe.sensor_names}
    noisy_sensors = {name: [r.sensors[name] for r in records] for name in universe.sensor_names}
    drift_times = [lr.t for lr in latent_trajectory if lr.drift]
    anomaly_times = [lr.t for lr in latent_trajectory if lr.anomaly]

    # Run theory through sandbox to get predictions
    sandbox = TheorySandbox(config.sandbox)
    # Use a small initial window as history so the oracle can initialize correctly
    split = 8
    history = records[:split]
    future = records[split:]

    result = sandbox.execute(theory_module, history, future, universe.sensor_names)
    predictions = result.predictions if result.ok else []

    # Also run comparison baseline if provided
    comparison_predictions = []
    if comparison_module:
        comp_result = sandbox.execute(comparison_module, history, future, universe.sensor_names)
        comparison_predictions = comp_result.predictions if comp_result.ok else []

    _plot(
        times=times,
        true_latents=true_latents,
        true_sensors=true_sensors,
        noisy_sensors=noisy_sensors,
        predictions=predictions,
        comparison_predictions=comparison_predictions,
        split_index=split,
        drift_times=drift_times,
        anomaly_times=anomaly_times,
        sensor_names=list(universe.sensor_names),
        latent_names=list(latent_names),
        output_path=args.output,
        theory_ok=result.ok,
        theory_label=args.baseline or "theory",
        compare_label=args.compare_baseline or "",
    )
    print(f"Saved discovery plot to {args.output}")
    if result.ok:
        print(f"  Theory executed OK | predictions={len(predictions)}")
    else:
        print(f"  Theory FAILED: {result.error}")


def _plot(
    times, true_latents, true_sensors, noisy_sensors,
    predictions, comparison_predictions,
    split_index, drift_times, anomaly_times,
    sensor_names, latent_names, output_path,
    theory_ok, theory_label, compare_label,
):
    try:
        import matplotlib
        matplotlib.use("Agg")
        import matplotlib.pyplot as plt
        import numpy as np
    except ImportError:
        print("matplotlib not installed — skipping plot generation")
        return

    n_latents = len(latent_names)
    n_sensors = len(sensor_names)
    fig, axes = plt.subplots(n_latents + n_sensors, 1, figsize=(14, 3 * (n_latents + n_sensors)),
                             sharex=True)
    fig.suptitle("Ontology Architect — Latent Variable Discovery", fontsize=16, fontweight="bold")

    colors = ["#3498db", "#e74c3c", "#2ecc71", "#f39c12"]
    pred_color = "#9b59b6"
    comp_color = "#95a5a6"

    # Plot hidden latent variables
    for i, name in enumerate(latent_names):
        ax = axes[i]
        ax.plot(times, true_latents[name], color=colors[i % len(colors)],
                linewidth=2, label=f"True latent {name}")
        for dt in drift_times:
            ax.axvline(dt, color="red", alpha=0.3, linestyle="--", linewidth=1)
        ax.set_ylabel(f"Latent {name}", fontsize=10)
        ax.legend(loc="upper right", fontsize=8)
        ax.grid(alpha=0.2)
        ax.set_title(f"Hidden Latent Variable: {name}", fontsize=11, fontweight="bold")

    # Plot sensor predictions vs ground truth
    pred_times = times[split_index:split_index + len(predictions)]
    for i, name in enumerate(sensor_names):
        ax = axes[n_latents + i]
        # True clean sensors
        ax.plot(times, true_sensors[name], color=colors[i % len(colors)],
                linewidth=1.5, alpha=0.7, label=f"True {name}")
        # Noisy observed sensors
        ax.scatter(times, noisy_sensors[name], color=colors[i % len(colors)],
                   s=6, alpha=0.3, label=f"Observed {name}")
        # Theory predictions
        if predictions:
            pred_values = [p.get("sensors", {}).get(name, 0.0) for p in predictions]
            ax.plot(pred_times[:len(pred_values)], pred_values,
                    color=pred_color, linewidth=2, linestyle="-",
                    label=f"{theory_label} prediction")
        # Comparison predictions
        if comparison_predictions:
            comp_values = [p.get("sensors", {}).get(name, 0.0) for p in comparison_predictions]
            ax.plot(pred_times[:len(comp_values)], comp_values,
                    color=comp_color, linewidth=1.5, linestyle=":",
                    label=f"{compare_label} prediction")
        # Mark prediction boundary
        if split_index < len(times):
            ax.axvline(times[split_index], color="black", alpha=0.4,
                       linestyle="-.", linewidth=1, label="Prediction horizon")
        # Mark drift events
        for dt in drift_times:
            ax.axvline(dt, color="red", alpha=0.2, linestyle="--", linewidth=1)
        ax.set_ylabel(name, fontsize=10)
        ax.legend(loc="upper right", fontsize=7)
        ax.grid(alpha=0.2)
        ax.set_title(f"Sensor: {name}", fontsize=11)

    axes[-1].set_xlabel("Time (t)", fontsize=11)
    plt.tight_layout()

    Path(output_path).parent.mkdir(parents=True, exist_ok=True)
    plt.savefig(output_path, dpi=150, bbox_inches="tight", facecolor="white")
    plt.close()


if __name__ == "__main__":
    main()