Full Hypercausal System Demo (PennyLane + SPSA + KL)#
Overview#
This example demonstrates a full hypercausal system on a PennyLane backend, combining causal feedback with SPSA derivative-free optimization and a KL-constrained trust-region guard. Dynamic depth scheduling and multi-metric telemetry track how the internal feedback parameter \(\alpha\) co-evolves with coherence and consistency under non-stationary drift.
The model evaluates the stability of quantum-inspired feedback systems under drift, using adaptive control of internal causal parameters to maintain temporal coherence and counterfactual consistency.
Core Objectives#
Hypercausal Feedback Modeling: multi-directional causal propagation.
Quantum-Inspired Efficiency: simulated superposition for reduced cost.
Deterministic–Stochastic Integration: deterministic and probabilistic backends.
Causal Stability Measurement: tracking coherence, consistency, and feedback drift.
Scientific Transparency: reproducible, open experimentation.
Experimental Setup#
We simulate a minimal quantum-like node structure using PennyLane circuits. The internal state \(S_t\) evolves via recursive updates modulated by a causal feedback coefficient \(\alpha\), representing the degree of self-consistent causal correction.
The environment introduces a small non-stationary drift (sinusoidal) to evaluate robustness of the feedback mechanism.
System Architecture & Modules#
This demo wires the high-level components of QML-HCS as follows:
Backend:
PennyLaneBackendwithBackendConfig(output_dim=qubits, shots=shots).Policy:
MeanPolicy(branch aggregation by mean).Node / Model: a single
HCNodewrapped byHCModel.Losses:
MSELoss(task),ConsistencyLoss(temporal),CoherenceLoss(branch coherence).Callbacks:
MemoryLogger(telemetry) andDepthScheduler(progressive circuit depth).Optimizers: base SPSA (derivative-free) wrapped by a KL-bounded trust-region controller.
Data & Non-Stationary Drift#
We use a fixed seed input vector x0 ∈ ℝ^{qubits} and inject a sinusoidal drift
with amplitude drift_amp and phase coupled to the epoch:
where \(A=\texttt{drift\_amp}\) and \(T\) is the number of epochs. The effective input is the feedback-scaled signal
A simple linear target ramp \(y_t\) is used to evaluate task loss, while consistency/coherence are computed from the model’s present state \(S_t\) and one-step future projection \(\mu_{\mathrm{fut}}\).
Alternative Drift Mode: Physical (Hardware-Style)#
A complementary version of this demo extends the drift model from a single
sinusoidal component to hardware-inspired multi-source drift.
Instead of a global additive term drift_amp * sin(phase), the physical mode
introduces three concurrent perturbations:
Phase Drift (additive): slow offset mimicking accumulated phase error.
Frequency Detuning (multiplicative): gradual amplitude scaling in ppm.
Readout Bias (post-measurement): asymmetric bias reflecting device readout imbalance.
The same SPSA + KL trust-region control loop operates unchanged, yet now compensates for these realistic distortions, testing hypercausal resilience under hardware noise.
Mathematical formulation of hardware-style drift
In the physical mode, three concurrent perturbations model hardware-level noise:
The perturbed input to the model is
and a post-measurement correction is applied as
Code mapping.
\(\Phi_{\max} \rightarrow \texttt{phase\_max}\),
\(\epsilon_{\mathrm{ppm}} \rightarrow \texttt{freq\_ppm}\),
\(B_{\max} \rightarrow \texttt{readout\_bias\_max}\).
\(\lambda_t\) is an epoch-wise scalar applied element-wise to the input vector,
and \(\mathbf{1}\) denotes a vector of ones of size qubits.
The logged drift_real corresponds to the first component of \(\Delta\phi_t\).
Note
The legacy parameter drift_amp remains for API compatibility but has no
effect in this mode.
Backend, Node & Depth Scheduling#
PennyLaneBackendinstantiates the quantum-inspired circuit with the given number of qubits and shots.HCNode+MeanPolicyproduce a vector state and per-branch statistics.DepthScheduler(start=1, end=5, epochs=E)gradually increases circuit depth across the full training (E = epochs), enabling a mild curriculum: shallow circuits during early exploration and deeper circuits once feedback stabilizes.
Mathematical Formulation#
Let \(x_t\) be the input at epoch \(t\). The internal state and one–step future projection are
where \(\theta\) denotes circuit parameters and \(\alpha_t\) is the feedback coefficient.
Task loss (MSE).
Temporal consistency loss.
Coherence loss (branch coherence). Let \(p_t(b)\) be the normalized branch activation:
Total objective (minimized).
Optimizers & Trust-Region (SPSA + KL)#
SPSA updates the feedback parameter \(\alpha\) using two antithetic cost evaluations, while a KL-bounded trust-region ensures distributional stability of the state branches between epochs.
SPSA (antithetic) step:
\[\hat{g} = \frac{\mathcal{L}(\alpha+\varepsilon \Delta) - \mathcal{L}(\alpha-\varepsilon \Delta)}{2\varepsilon}\,\Delta,\quad \alpha_{\text{new}} \leftarrow \alpha - \eta\,\hat{g}\]with \(\Delta \in \{-1,+1\}^{d}\). This derivative-free estimate is robust to measurement noise and stochastic loss surfaces.
KL trust-region guard: accept the proposal only if the symmetric KL proxy between old and new branch statistics satisfies
\[D_{\mathrm{KL}}^{\mathrm{sym}}(p_{\text{old}}\parallel p_{\text{new}}) \le \delta_{\mathrm{KL}},\]otherwise perform backtracking line-search (factor 0.7, up to 8 attempts).
This separation lets SPSA explore, while the trust-region preserves temporal coherence, which is key to hypercausal stability.
Trust-Region Stability Metric (KL)#
To prevent disruptive jumps between epochs, we bound the symmetric KL on branch statistics:
A step is accepted only if \(D_{\mathrm{KL}}^{\mathrm{sym}} \le \delta_{\mathrm{KL}}\). Otherwise a backtracking line-search (factor 0.7, up to 8 trials) scales the proposal until the constraint is met.
Choosing and Swapping Optimizers#
This framework allows you to easily switch between multiple optimizers implemented
in qmlhc.optim.numpy_optim using create_optimizer_numpy(name, **kwargs).
Each optimizer explores a different adaptation strategy for the feedback coefficient \(\alpha_t\), offering varied robustness and convergence properties depending on the noise level, drift intensity, or causal stability required.
Below is a concise summary of each available optimizer:
SPSA (Simultaneous Perturbation Stochastic Approximation) - Derivative-free stochastic optimization with antithetic perturbations; highly robust to shot noise and measurement errors, ideal for quantum-like experiments.
Finite Differences (FD) - Deterministic gradient approximation via finite perturbations; more precise in low-noise regimes but computationally heavier.
Adam (ADAM) - Adaptive-momentum update rule that accelerates convergence on smooth loss surfaces when analytical or approximated gradients are available.
Natural Gradient (NGD) - Rescales parameter updates according to the local information geometry (Fisher metric), improving step stability.
K-FAC (Kronecker-Factored Approximation of Curvature) - Efficient block-wise approximation of the natural gradient; suited for structured or layered models.
Dual Ascent (DA) - Alternating primal–dual updates following a Lagrangian scheme, enforcing constraints on feedback or coherence terms.
Model Predictive Control (MPC) - Forward-planning optimizer that predicts a short-horizon trajectory for \(\alpha_t\) to maintain causal stability under strong non-stationary drift.
Trust-Region (KL) - A stability wrapper that bounds the inter-epoch distributional shift via a symmetric KL-divergence criterion; it can wrap any base optimizer to enforce temporal coherence.
Example: selecting an optimizer#
The demo uses SPSA wrapped by a Trust-Region (KL) controller.
To experiment with others, replace the optimizer initialization in the script:
# === Choose your optimizer ===
base_opt = create_optimizer_numpy("spsa", lr0=0.05, eps0=0.10, antithetic=True, clip=4.0)
# base_opt = create_optimizer_numpy("finite-diff", lr=0.02, eps=1e-2, clip=4.0)
# base_opt = create_optimizer_numpy("adam", lr=0.02)
# base_opt = create_optimizer_numpy("natural-grad", lr=0.05)
# base_opt = create_optimizer_numpy("kfac", lr=0.05)
# base_opt = create_optimizer_numpy("dual-ascent", lr=0.05)
# base_opt = create_optimizer_numpy("mpc", horizon=5, lr=0.03)
# Optional: wrap with Trust-Region (KL) for stability
opt = create_optimizer_numpy("trust-kl",
base_opt=base_opt,
delta_kl=0.02, backtrack=0.7, max_backtracks=8)
Each optimizer modifies how \(\alpha_t\) adapts to environmental drift and causal feedback, providing a platform to study robustness, adaptation speed, and stability across hypercausal configurations.
Summary and Further Exploration#
The optimization architecture of QML-HCS is modular and extensible. Beyond the examples above, the following components form the complete Optimization Suite, allowing researchers to customize, register, and explore adaptive strategies in detail:
Optimizer API - Defines the abstract interface and parameter flow used by all optimizers.
Registry: NumPy Optimizers - Factory/registry (create_optimizer_numpy) for dynamic construction from string identifiers.
NumPy Optimizers - Concrete implementations (SPSA, Finite-Diff, Adam, Natural-Grad, K-FAC, Dual-Ascent, MPC, Trust-Region).
Together, these modules enable a flexible experimental workflow and form the backbone of adaptive dynamics within the QML-HCS Optimization Suite.
Telemetry & Training Loop#
MemoryLogger stores per-epoch scalars (losses, α, means), enabling downstream plots and CSV export.
CallbackList orchestrates hooks:
on_epoch_begin→ optimizer step → loss eval →on_step_end→on_epoch_end.
Pseudo-flow per epoch:
for t in range(E):
callbacks.on_epoch_begin(t)
build context (x0, drift_t, target_t, losses, branches, info)
info_old = refresh_info(model, α_t)
α_{t+1} = optimizer.step(model, α_t | info_old, KL_guard)
forward pass → S_t, μ_fut, branches
compute losses → task, cons, coh, total
log metrics & means; callbacks.on_step_end(t)
callbacks.on_epoch_end(t)
Saving & Numbered Figures#
All results are saved under runs/hc_full_demo/:
CSV:
results.csvwith columns[epoch, alpha, task_loss, cons_loss, coh_loss, total_loss, mean_s, mean_mu].Numbered plots: helper
_savefig_numbered(dir, base)produces files like<base>_001.png,<base>_002.png, preserving previous runs.
Hyperparameters (Adaptive Drift Mode)#
Parameter |
Value / Description |
qubits |
7 |
branches |
20 |
epochs |
350 |
drift_amp |
0.30 (sinusoidal amplitude) |
DepthScheduler |
start=1, end=5, epochs=E |
SPSA |
lr0=0.05, eps0=0.10, antithetic=True, clip=4.0 |
Trust-KL |
δ_KL=0.02, backtrack=0.7, max_backtracks=8 |
shots |
1024 |
How to Run#
python ex_hypercausal_drift_adaptation.py
Hyperparameters (Physical Drift Mode)#
Parameter |
Value / Description |
qubits |
7 (hardware-style demo) |
branches |
20 |
epochs |
300 |
freq_ppm |
12e-6 (frequency detuning rate) |
phase_max |
0.03 rad (maximum phase drift) |
readout_bias_max |
0.12 (max readout asymmetry) |
DepthScheduler |
start=1, end=5, epochs=E |
SPSA |
lr0=0.05, eps0=0.10, antithetic=True, clip=4.0 |
Trust-KL |
δ_KL=0.02, backtrack=0.7, max_backtracks=8 |
shots |
1024 |
How to Run (Physical Drift Mode)#
python examples/ex_hypercausal_physical_drift_resilience.py
Typical Console Output (Summarized)#
Below is a condensed example of the console output produced by a 101-epoch run.
User/host paths are omitted for portability; figures and CSV are saved to
runs/hc_full_demo/.
/.../pennylane/devices/device_api.py:193: PennyLaneDeprecationWarning: Setting shots on device is deprecated. Please use the `set_shots` transform on the respective QNode instead.
warnings.warn(
Epoch 00 | α=1.0000 | Task=0.14699 Cons=0.97379 Coh=0.00133 Tot=0.63454
Epoch 01 | α=1.0000 | Task=0.14109 Cons=0.95652 Coh=0.00136 Tot=0.62003
...
Epoch 99 | α=1.0503 | Task=0.14837 Cons=0.98211 Coh=0.00131 Tot=0.64008
Epoch 100 | α=1.0503 | Task=0.14188 Cons=0.94497 Coh=0.00139 Tot=0.61506
[OK] Saved CSV: runs/hc_full_demo/results.csv
[FIG] Saved: runs/hc_full_demo/loss_coherence_001.png
[FIG] Saved: runs/hc_full_demo/consistency_vs_coherence_001.png
[FIG] Saved: runs/hc_full_demo/alpha_over_epochs_001.png
[FIG] Saved: runs/hc_full_demo/state_alignment_001.png
[FIG] Saved: runs/hc_full_demo/causal_space_3d_001.png
[FIG] Saved: runs/hc_full_demo/causal_parallel_001.png
[FIG] Saved: runs/hc_full_demo/alpha_sensitivity_001.png
[FIG] Saved: runs/hc_full_demo/causal_phase_portrait_001.png
[FIG] Saved: runs/hc_full_demo/drift_vs_coherence_001.png
Comparative Figures: Hypercausal Drift Adaptation vs Physical Drift Resilience#
The following figures present side-by-side results from the two experimental regimes:
(A) ex_hypercausal_drift_adaptation – adaptive stabilization under simulated drift,
(B) ex_hypercausal_physical_drift_resilience – resilience under real physical perturbations.
Each subsection describes the common structure of the plot and the specific phenomena observed in both regimes.
—
Figure 1 – Loss vs Coherence Dynamics
General meaning: This dual-axis plot depicts the joint evolution of total loss (left, red) and coherence (right, blue). It reveals how causal regulation maintains internal consistency when exposed to drift energy.
A – Adaptation: In ex_hypercausal_drift_adaptation, loss and coherence exhibit phase-locked oscillations that gradually stabilize. This reflects the damping effect of the KL-bounded SPSA controller, which maintains coherent adaptation under artificial non-stationary drift.
B – Resilience: In ex_hypercausal_physical_drift_resilience, the same relationship persists under physically injected perturbations. Coherence shows micro-oscillations but no collapse, demonstrating robust compensation capacity and physical drift absorption through hypercausal regulation.
—
Figure 2 – Consistency vs Coherence
General meaning: Scatter plots illustrating the interdependence between coherence and consistency. Each dot corresponds to one epoch; color indicates temporal progression.
A – Adaptation: The negative correlation between both quantities shows a causal compensation mechanism: when coherence fluctuates, consistency increases to preserve total information integrity.
B – Resilience: The pattern tightens into a near-perfect line. The system evolves along a hypercausal compensation axis, reflecting greater determinism and causal symmetry under physical drift.
—
Figure 3 – Alpha (Feedback) Over Epochs
General meaning: This time-series shows the temporal evolution of the feedback coefficient \(\alpha\), which modulates causal strength in the hypercausal loop.
A – Adaptation: Incremental rises followed by plateaus indicate gradual convergence of the adaptive feedback, with smooth exploration bounded by the KL trust region.
B – Resilience: The steps become discrete and quantized, suggesting feedback stabilization via causal quantization— a strategy to prevent overshoot when real-world perturbations affect drift amplitude.
—
Figure 4 – State Alignment (mean(S_t) vs mean(μ_fut))
General meaning: These plots compare average current and predictive states, capturing temporal-causal coherence through alignment.
A – Adaptation: Both trajectories stay closely synchronized, confirming temporal-causal stability under non-stationary drift.
B – Resilience: Under real drift perturbation, small phase slips occur, followed by rapid re-alignment. This demonstrates elastic causal recovery, a resilience property that maintains coherence even under continuous disturbance.
—
Figure 5 – Alpha Sensitivity (Δα per Epoch)
General meaning: Plots of \(Δα\) highlight how feedback sensitivity fluctuates per epoch, indicating reaction speed and causal flexibility.
A – Adaptation: Distributed moderate spikes mark regular recalibration of feedback within the KL constraint.
B – Resilience: Sharper yet shorter bursts appear, reflecting high-frequency response bursts triggered by real drift events and rapidly neutralized by the KL guard.
—
Figure 6 – Drift vs Coherence Dynamics
General meaning: Comparison of the coherence curve with a drift proxy (Δα) to reveal phase coupling and absorption behavior.
A – Adaptation: Coherence slightly lags behind drift oscillations but remains bounded, illustrating reactive stabilization.
B – Resilience: Coherence remains nearly invariant despite high-amplitude drift spikes—proof of hypercausal energy absorption through feedback adaptation.
—
Figure 7 – Causal Phase Portrait (Temporal Path)
General meaning: Phase portraits map coherence vs consistency evolution, revealing the causal trajectory of the system.
A – Adaptation: A smooth manifold indicates a self-correcting hypercausal attractor regulating the drift-compensated trajectory.
B – Resilience: The manifold becomes denser and more aligned, reflecting phase-locking under physical drift and a more rigid hypercausal attractor geometry.
—
Figure 8 – Parallel Causal Dimensions
General meaning: Parallel coordinate plots visualize multi-metric causal coherence across epochs.
A – Adaptation: The smooth gradient reveals coherent evolution across causal axes under synthetic drift.
B – Resilience: Gradients compress and overlap more tightly, signifying causal dimensional coupling and invariant coherence despite physical perturbations.
—
Figure 9 – 3D Causal Space (Coherence, Consistency, Alpha)
General meaning: Three-dimensional trajectories show joint evolution in (coherence, consistency, α) space, revealing equilibrium geometry.
A – Adaptation: The path spirals smoothly into a coherence basin, characteristic of controlled adaptation.
B – Resilience: The structure contracts into a compact funnel-shaped attractor, manifesting hypercausal equilibrium resilience under physically modulated drift.
—
Figure 10 – Drift Signals Only (Real vs Proxy)
General meaning: Comparison between the real drift (or synthetic analogue) and its feedback-derived proxyΔαto assess tracking fidelity.
A – Adaptation: The proxy reproduces the sinusoidal synthetic drift with slight phase lag, validating proxy fidelity under controlled conditions.
B – Resilience: TheΔαsignal tracks the real physical drift almost perfectly, evidencing phase-resilient feedback synchronization and causal coherence preservation under continuous perturbation.
—
Discussion#
The results from Drift_adaptation and Physical_drift_resilience reveal
how the feedback architecture behaves under two complementary drift conditions.
In the adaptive drift experiment, the system maintains coherence and consistency despite the injected sinusoidal perturbation. Figures 1–10 illustrate how the SPSA + KL-bounded control gradually reduces fluctuations in total loss, keeps coherence bounded, and stabilizes the feedback parameter α. The trajectory in coherence–consistency space remains narrow, indicating that the feedback mechanism effectively compensates for smooth, low-frequency drift without destabilizing the internal state.
In the physical drift experiment, the perturbation source becomes more complex, combining phase offsets, frequency detuning, and readout bias. The same feedback loop remains functional but now operates under conditions resembling hardware-level fluctuations. Figures 1–10 show that coherence and consistency are still preserved, although α exhibits small adaptive oscillations that reflect continuous micro-compensation. The 3D and parallel plots highlight that the system reorganizes its causal balance rather than collapsing under the higher noise regime.
Overall, both sets of figures demonstrate that the hypercausal feedback model maintains internal coherence and stability across different drift sources. The adaptive case confirms robustness under controlled drift, while the physical resilience case shows that the same principles extend to more realistic, hardware-like disturbances.
Reproducibility Notes#
Results may vary slightly due to stochasticity in SPSA noise, device shots, and random seeding.
Fixing a global random seed and using stable shot allocation improves reproducibility.
The KL trust-region remains essential to maintain distributional stability across epochs.
Both experiments use numbered output directories to prevent overwriting figures between runs.
The resilience version may show higher-frequency jitter, which reflects expected variability under physical drift injection rather than numerical error.
Source Code Reference#
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3"""
4Hypercausal Drift Core Demo
5
6"""
7
8import os
9import sys
10import re
11import math
12import numpy as np
13import pandas as pd
14import matplotlib.pyplot as plt
15from mpl_toolkits.mplot3d import Axes3D # noqa: F401
16from typing import List
17from pandas.plotting import parallel_coordinates
18
19
20# ======================================================================
21# Ensure src/ path
22# ======================================================================
23def ensure_src_path():
24 script_dir = os.path.dirname(os.path.abspath(__file__))
25 repo_root = os.path.abspath(os.path.join(script_dir, ".."))
26 src_path = os.path.join(repo_root, "src")
27 if src_path not in sys.path:
28 sys.path.insert(0, src_path)
29 return repo_root, src_path
30
31# ======================================================================
32# Import modules
33# ======================================================================
34repo_root, src_path = ensure_src_path()
35
36from qmlhc.backends.pennylane_backend import PennyLaneBackend
37from qmlhc.hc.node import HCNode
38from qmlhc.hc.policy import MeanPolicy
39from qmlhc.loss.task import MSELoss
40from qmlhc.loss.consistency import ConsistencyLoss
41from qmlhc.loss.coherence import CoherenceLoss
42from qmlhc.callbacks.telemetry import MemoryLogger
43from qmlhc.callbacks.depth_control import DepthScheduler
44from qmlhc.callbacks.base import CallbackList
45from qmlhc.core.model import HCModel
46from qmlhc.core.backend import BackendConfig
47from qmlhc.optim.registry_numpy import create_optimizer_numpy
48
49
50# ======================================================================
51# Utility
52# ======================================================================
53def ensure_dirs(*dirs):
54 for d in dirs:
55 os.makedirs(d, exist_ok=True)
56
57def _next_numbered_path(output_dir: str, base: str) -> str:
58 """
59 Return a path like <output_dir>/<base>_NNN.png with the next free index.
60 """
61 os.makedirs(output_dir, exist_ok=True)
62 pat = re.compile(rf"^{re.escape(base)}_(\d+)\.png$")
63 max_n = 0
64 for name in os.listdir(output_dir):
65 m = pat.match(name)
66 if m:
67 max_n = max(max_n, int(m.group(1)))
68 return os.path.join(output_dir, f"{base}_{max_n+1:03d}.png")
69
70def _savefig_numbered(output_dir: str, base: str, fig=None, dpi: int = 160):
71 """
72 Save the current (or provided) figure numbered as <base>_NNN.png with tight bbox.
73 """
74 path = _next_numbered_path(output_dir, base)
75 (fig or plt.gcf()).savefig(path, dpi=dpi, bbox_inches="tight")
76 print(f"[FIG] Saved: {path}")
77
78# ======================================================================
79# Build system
80# ======================================================================
81def build_system(qubits: int = 7, shots: int = 1024, branches: int = 20,
82 sched_epochs: int = 350): # <-- CHANGE: added sched_epochs param (default 10)
83 cfg = BackendConfig(output_dim=qubits, shots=shots)
84 backend = PennyLaneBackend(cfg, num_qubits=qubits, shots=shots)
85
86 policy = MeanPolicy()
87 node = HCNode(backend=backend, policy=policy)
88 model = HCModel(nodes=[node])
89
90 task_loss = MSELoss()
91 cons_loss = ConsistencyLoss(alpha=1.0, beta=0.8)
92 coh_loss = CoherenceLoss(mode="variance")
93
94 telemetry = MemoryLogger()
95 # <-- CHANGE: use sched_epochs to spread the depth schedule across the full training length
96 depth_sched = DepthScheduler(target_attr="depth", start=1, end=5, epochs=sched_epochs)
97
98 callbacks = CallbackList([telemetry, depth_sched])
99 return model, task_loss, cons_loss, coh_loss, callbacks
100
101# === Optimizer wiring helpers (SPSA / Trust-KL / MPC ready) ===
102def evaluate_stats(model, params, ctx):
103 """Compute task/cons/coh/total and return model info (branches)."""
104 x0 = np.asarray(ctx["x0"], dtype=float)
105 drift = np.asarray(ctx["drift"], dtype=float)
106 target = np.asarray(ctx["target"], dtype=float)
107 branches = int(ctx["branches"])
108 task_loss, cons_loss, coh_loss = ctx["losses"]
109
110 alpha = float(params["alpha"])
111 x = alpha * x0
112 s_tm1 = np.zeros_like(x)
113 s_t, s_hat, info = model.forward(x + drift, s_tm1, branches)
114
115 lt = float(task_loss(s_t, target))
116 lc = float(cons_loss(s_tm1, s_t, s_hat))
117 lq = float(coh_loss(info.get("branches", np.vstack([s_t, s_hat]))))
118 total = lt + 0.5 * (lc + lq)
119 return {"task": lt, "cons": lc, "coh": lq, "total": total, "info": info}
120
121def refresh_info(model, params, ctx):
122 """Recompute info(dict) for trust-region KL guard."""
123 return evaluate_stats(model, params, ctx)["info"]
124
125def kl_fn(old_info, new_info):
126 """Symmetric KL-like proxy using branch statistics."""
127 from qmlhc.optim.numpy_optim.utils import kl_proxy
128 return float(kl_proxy(old_info, new_info))
129
130# ======================================================================
131# Run experiment
132# ======================================================================
133def run_experiment(epochs: int = 350, qubits: int = 7, branches: int = 20, drift_amp: float = 0.30, shots: int = 1024):
134 # <-- CHANGE: pass sched_epochs=epochs so the DepthScheduler runs across the full training
135 model, task_loss, cons_loss, coh_loss, callbacks = build_system(
136 qubits=qubits,
137 shots=shots,
138 branches=branches,
139 sched_epochs=epochs # <-- CHANGE
140 )
141
142 x0 = np.linspace(0.1, 0.3, qubits)
143 alpha = 1.0
144 # === Initialize SPSA + Trust-KL Optimizer ===
145 base_opt = create_optimizer_numpy("spsa", lr0=0.05, eps0=0.10, antithetic=True, clip=4.0)
146 opt = create_optimizer_numpy("trust-kl", base_opt=base_opt, delta_kl=0.02, backtrack=0.7, max_backtracks=8)
147 params = {"alpha": 1.0}
148 opt.initialize(params)
149
150 rows = []
151 header = [
152 "epoch", "alpha",
153 "task_loss", "cons_loss", "coh_loss", "total_loss",
154 "mean_s", "mean_mu", "drift_real"
155 ]
156
157
158 for epoch in range(epochs):
159 context = {"epoch": epoch, "model": model}
160 callbacks.on_epoch_begin(epoch, context)
161
162 phase = 2 * math.pi * epoch / max(1, epochs - 1)
163 target = np.linspace(0.4, 0.9, qubits)
164 drift = drift_amp * np.sin(phase) * np.ones(qubits)
165
166 # --- Build optimizer context for this epoch ---
167 context = {
168 "epoch": epoch,
169 "epochs": epochs,
170 "model": model,
171 "x0": x0,
172 "drift": drift,
173 "target": target,
174 "losses": (task_loss, cons_loss, coh_loss),
175 "branches": branches,
176 "info": {},
177 "kl_fn": kl_fn,
178 "refresh_info": refresh_info,
179 }
180 context["info"] = refresh_info(model, {"alpha": float(params["alpha"])}, context)
181
182 # === Optimizer-driven update of alpha ===
183 params, opt_state = opt.step_params(model, params, context)
184 alpha = float(params["alpha"])
185
186 x = alpha * x0
187 s_tm1 = np.zeros_like(x)
188 s_t, s_hat, info = model.forward(x + drift, s_tm1, branches)
189
190 l_task = task_loss(s_t, target)
191 l_cons = cons_loss(s_tm1, s_t, s_hat)
192 l_coh = coh_loss(info["branches"])
193 total_loss = l_task + 0.5 * (l_cons + l_coh)
194
195 mean_s = float(np.mean(s_t))
196 mean_mu = float(np.mean(s_hat))
197
198 step_context = {"epoch": epoch, "loss": total_loss, "alpha": alpha}
199 callbacks.on_step_end(epoch, step_context)
200
201 rows.append([epoch, alpha, l_task, l_cons, l_coh, total_loss, mean_s, mean_mu, drift[0]])
202
203 print(f"Epoch {epoch:02d} | α={alpha:.4f} | "
204 f"Task={l_task:.5f} Cons={l_cons:.5f} Coh={l_coh:.5f} Tot={total_loss:.5f}")
205
206 callbacks.on_epoch_end(epoch, {"epoch": epoch, "loss": total_loss})
207
208 return rows, header
209
210# ======================================================================
211# Save CSV
212# ======================================================================
213def save_and_plot_quantum_style(rows, header, output_dir="runs/hc_full_demo"):
214 ensure_dirs(output_dir)
215 df = pd.DataFrame(rows, columns=header)
216 csv_path = os.path.join(output_dir, "results.csv")
217 df.to_csv(csv_path, index=False)
218 print(f"[OK] Saved CSV: {csv_path}")
219
220 # 1) Loss & Coherence (dual y-axes)
221 fig, ax1 = plt.subplots(figsize=(12, 6))
222 ax2 = ax1.twinx()
223 ax1.plot(df["epoch"], df["total_loss"], color="tab:red", linewidth=2)
224 ax2.plot(df["epoch"], df["coh_loss"], color="tab:blue", linewidth=2)
225 ax1.set_xlabel("Epoch")
226 ax1.set_ylabel("Loss", color="tab:red")
227 ax2.set_ylabel("Coherence", color="tab:blue")
228 fig.tight_layout()
229 _savefig_numbered(output_dir, "loss_coherence", fig)
230
231 # 2) Consistency vs Coherence (scatter), viridis + colorbar
232 plt.figure(figsize=(6, 6))
233 sc = plt.scatter(df["coh_loss"], df["cons_loss"], c=df["epoch"], cmap="viridis", s=40)
234 plt.xlabel("Coherence")
235 plt.ylabel("Consistency")
236 plt.title("Consistency vs Coherence")
237 cbar = plt.colorbar(sc)
238 cbar.set_label("Epoch")
239 plt.grid(True)
240 _savefig_numbered(output_dir, "consistency_vs_coherence")
241
242 # 3) Alpha (Feedback) Over Epochs — green
243 plt.figure(figsize=(10, 4))
244 plt.plot(df["epoch"], df["alpha"], color="tab:green", linewidth=2)
245 plt.title("Alpha (Feedback) Over Epochs")
246 plt.xlabel("Epoch")
247 plt.ylabel("Alpha")
248 plt.grid(True)
249 _savefig_numbered(output_dir, "alpha_over_epochs")
250
251 # 4) State Alignment — purple vs orange
252 plt.figure(figsize=(12, 5))
253 plt.plot(df["epoch"], df["mean_s"], color="tab:purple", linewidth=2, label="mean(S_t)")
254 plt.plot(df["epoch"], df["mean_mu"], color="tab:orange", linewidth=2, label="mean(mu_fut)")
255 plt.title("State Alignment")
256 plt.xlabel("Epoch")
257 plt.ylabel("Mean Values")
258 plt.legend()
259 plt.grid(True)
260 _savefig_numbered(output_dir, "state_alignment")
261
262 # 5) 3D Causal Space — plasma, colorbar 'Epoch'
263 fig = plt.figure(figsize=(10, 7))
264 ax = fig.add_subplot(111, projection="3d")
265 sc3 = ax.scatter(df["coh_loss"], df["cons_loss"], df["alpha"],
266 c=df["epoch"], cmap="plasma", s=60, alpha=0.9)
267 ax.set_xlabel("Coherence")
268 ax.set_ylabel("Consistency")
269 ax.set_zlabel("Alpha")
270 ax.set_title("3D Causal Space")
271 cb3 = fig.colorbar(sc3, ax=ax, shrink=0.65)
272 cb3.set_label("Epoch")
273 plt.tight_layout()
274 _savefig_numbered(output_dir, "causal_space_3d")
275
276 # 6) Parallel Causal Dimensions (epoch grouped)
277 plt.figure(figsize=(8,6))
278 subset = df[["coh_loss", "cons_loss", "alpha", "epoch"]].copy()
279 subset["epoch_group"] = pd.cut(subset["epoch"], bins=5, labels=False)
280 parallel_coordinates(subset, "epoch_group", color=plt.cm.plasma(np.linspace(0,1,5)), alpha=0.6)
281 plt.title("Parallel Causal Dimensions")
282 plt.xlabel("Metrics")
283 plt.ylabel("Value")
284 plt.grid(True, alpha=0.3)
285 plt.tight_layout()
286 _savefig_numbered(output_dir, "causal_parallel")
287
288 # 7) Alpha Sensitivity (Δα per Epoch)
289 plt.figure(figsize=(10, 5))
290 d_alpha = df["alpha"].diff().fillna(0.0)
291 plt.plot(df["epoch"], d_alpha, color="tab:gray", linewidth=2)
292 plt.title("Alpha Sensitivity (Δα per Epoch)")
293 plt.xlabel("Epoch")
294 plt.ylabel("ΔAlpha")
295 plt.grid(True, alpha=0.3)
296 plt.tight_layout()
297 _savefig_numbered(output_dir, "alpha_sensitivity")
298
299 # 8) Causal Phase Portrait (Temporal Path)
300 plt.figure(figsize=(7, 6))
301 plt.plot(df["coh_loss"], df["cons_loss"], color="tab:purple", linewidth=1.5, alpha=0.8)
302 sc_path = plt.scatter(df["coh_loss"], df["cons_loss"],
303 c=df["epoch"], cmap="plasma", s=28, alpha=0.9)
304 plt.xlabel("Coherence")
305 plt.ylabel("Consistency")
306 plt.title("Causal Phase Portrait (Temporal Path)")
307 cbp = plt.colorbar(sc_path)
308 cbp.set_label("Epoch")
309 plt.grid(True, alpha=0.3)
310 plt.tight_layout()
311 _savefig_numbered(output_dir, "causal_phase_portrait")
312
313 # 9) Drift vs Coherence Dynamics
314 plt.figure(figsize=(9, 5))
315 drift_proxy = np.abs(df["alpha"].diff().fillna(0.0))
316 plt.plot(df["epoch"], df["coh_loss"], color="tab:blue", linewidth=2, label="Coherence")
317 plt.plot(df["epoch"], drift_proxy, color="tab:red", linewidth=2, label="|ΔAlpha| (Drift Proxy)")
318 plt.title("Drift vs Coherence Dynamics")
319 plt.xlabel("Epoch")
320 plt.ylabel("Value")
321 plt.legend(frameon=False)
322 plt.grid(True, alpha=0.3)
323 plt.tight_layout()
324 _savefig_numbered(output_dir, "drift_vs_coherence")
325
326 # 10) Drift Signals Only (Real vs Proxy)
327 fig, ax1 = plt.subplots(figsize=(10, 4))
328 ax2 = ax1.twinx()
329
330 drift_proxy = np.abs(df["alpha"].diff().fillna(0.0))
331 drift_proxy_s = drift_proxy.rolling(5, min_periods=1).mean()
332
333 ax1.plot(df["epoch"], df["drift_real"], color="tab:purple", linewidth=2, label="Drift Real (phase-like)")
334 ax2.plot(df["epoch"], drift_proxy_s, color="tab:red", linestyle="--", linewidth=1.8, label="|ΔAlpha| Proxy (roll=5)")
335
336 ax1.set_xlabel("Epoch")
337 ax1.set_ylabel("Drift Real")
338 ax2.set_ylabel("|ΔAlpha| Proxy")
339 ax1.set_title("Drift Signals Only")
340
341 l1, lab1 = ax1.get_legend_handles_labels()
342 l2, lab2 = ax2.get_legend_handles_labels()
343 ax1.legend(l1 + l2, lab1 + lab2, loc="upper right", frameon=False)
344
345 ax1.grid(True, alpha=0.3)
346 plt.tight_layout()
347 _savefig_numbered(output_dir, "drift_signals_only")
348
349
350 return df
351
352# ======================================================================
353# Main
354# ======================================================================
355if __name__ == "__main__":
356 rows, header = run_experiment(epochs=350, qubits=7, branches=20, drift_amp=0.30, shots=1024)
357 save_and_plot_quantum_style(rows, header, output_dir="runs/hc_full_demo")
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3"""
4Hypercausal Core Demo — physical drift (phase + detuning + readout bias)
5"""
6
7import os
8import sys
9import re
10import math
11import numpy as np
12import pandas as pd
13import matplotlib.pyplot as plt
14from mpl_toolkits.mplot3d import Axes3D # noqa: F401
15from typing import List
16from pandas.plotting import parallel_coordinates
17
18
19# ======================================================================
20# Ensure src/ path
21# ======================================================================
22def ensure_src_path():
23 script_dir = os.path.dirname(os.path.abspath(__file__))
24 repo_root = os.path.abspath(os.path.join(script_dir, ".."))
25 src_path = os.path.join(repo_root, "src")
26 if src_path not in sys.path:
27 sys.path.insert(0, src_path)
28 return repo_root, src_path
29
30# ======================================================================
31# Import modules
32# ======================================================================
33repo_root, src_path = ensure_src_path()
34
35from qmlhc.backends.pennylane_backend import PennyLaneBackend
36from qmlhc.hc.node import HCNode
37from qmlhc.hc.policy import MeanPolicy
38from qmlhc.loss.task import MSELoss
39from qmlhc.loss.consistency import ConsistencyLoss
40from qmlhc.loss.coherence import CoherenceLoss
41from qmlhc.callbacks.telemetry import MemoryLogger
42from qmlhc.callbacks.depth_control import DepthScheduler
43from qmlhc.callbacks.base import CallbackList
44from qmlhc.core.model import HCModel
45from qmlhc.core.backend import BackendConfig
46from qmlhc.optim.registry_numpy import create_optimizer_numpy
47
48
49# =====================================================================
50# Utility
51# =====================================================================
52def ensure_dirs(*dirs):
53 for d in dirs:
54 os.makedirs(d, exist_ok=True)
55
56def _next_numbered_path(output_dir: str, base: str) -> str:
57 """
58 Return a path like <output_dir>/<base>_NNN.png using the next available index.
59 """
60 os.makedirs(output_dir, exist_ok=True)
61 pat = re.compile(rf"^{re.escape(base)}_(\d+)\.png$")
62 max_n = 0
63 for name in os.listdir(output_dir):
64 m = pat.match(name)
65 if m:
66 max_n = max(max_n, int(m.group(1)))
67 return os.path.join(output_dir, f"{base}_{max_n+1:03d}.png")
68
69def _savefig_numbered(output_dir: str, base: str, fig=None, dpi: int = 160):
70 """
71 Save the current (or provided) figure as <base>_NNN.png with a tight bounding box.
72 """
73 path = _next_numbered_path(output_dir, base)
74 (fig or plt.gcf()).savefig(path, dpi=dpi, bbox_inches="tight")
75 print(f"[FIG] Saved: {path}")
76
77# --- drift (hardware-style) helpers --------------------------------
78def hardware_drift_emulation (epoch, total_epochs, qubits,
79 freq_ppm=12e-6, # “slow” drift ~ tens of ppm
80 phase_max=0.03, # maximum accumulated phase in radians (1 sinusoidal cycle over the full run)
81 readout_bias_max=0.12): # readout bias (e.g., up to ~12%)
82 """
83 Emulate hardware-level drift commonly observed in QPUs:
84 - Accumulated phase (due to frequency drift) -> additive offset in parameters
85 - Detuning/frequency (ppm) -> slow multiplicative scaling
86 - Readout bias -> bias applied post-measurement
87 """
88 # 1) Slow per-qubit phase (simulates frequency drift -> phase)
89 phase = 2.0 * np.pi * (epoch / max(1, total_epochs - 1))
90 phase_drift = phase_max * np.sin(phase) * np.ones(qubits)
91
92 # 2) Micro detuning/frequency interpreted as a mild amplitude scale (accumulated in ppm)
93 detuning_scale = 1.0 + freq_ppm * epoch
94 amp_drift_scale = np.full(qubits, detuning_scale, dtype=float)
95
96 # 3) Small oscillatory readout bias
97 readout_bias = readout_bias_max * (0.5 + 0.5 * np.sin(phase + np.pi/3.0))
98
99 return phase_drift, amp_drift_scale, float(readout_bias)
100
101# =====================================================================
102# Build system
103# =====================================================================
104def build_system(qubits: int = 7, shots: int = 1024, branches: int = 20,
105 sched_epochs: int = 350): # <-- spread depth schedule across full training
106 cfg = BackendConfig(output_dim=qubits, shots=shots)
107 backend = PennyLaneBackend(cfg, num_qubits=qubits, shots=shots)
108
109 policy = MeanPolicy()
110 node = HCNode(backend=backend, policy=policy)
111 model = HCModel(nodes=[node])
112
113 task_loss = MSELoss()
114 cons_loss = ConsistencyLoss(alpha=1.0, beta=0.8)
115 coh_loss = CoherenceLoss(mode="variance")
116
117 telemetry = MemoryLogger()
118 depth_sched = DepthScheduler(target_attr="depth", start=1, end=5, epochs=sched_epochs)
119 callbacks = CallbackList([telemetry, depth_sched])
120 return model, task_loss, cons_loss, coh_loss, callbacks
121
122# === Optimizer wiring helpers (SPSA / Trust-KL / MPC ready) ===
123def evaluate_stats(model, params, ctx):
124 """Compute task/consistency/coherence/total losses and return model info (branches)."""
125 x0 = np.asarray(ctx["x0"], dtype=float)
126 drift = np.asarray(ctx["drift"], dtype=float) # phase (additive)
127 target = np.asarray(ctx["target"], dtype=float)
128 branches = int(ctx["branches"])
129 task_loss, cons_loss, coh_loss = ctx["losses"]
130
131 # Additional signals
132 amp_scale = np.asarray(ctx.get("amp_scale", 1.0), dtype=float) # detuning (multiplicative)
133 readout_bias = float(ctx.get("readout_bias", 0.0)) # readout bias
134
135 alpha = float(params["alpha"])
136 x = alpha * x0
137 s_tm1 = np.zeros_like(x)
138
139 # Forward pass with hardware-style physics: detuning + phase
140 x_in = amp_scale * (x + drift)
141 s_t, s_hat, info = model.forward(x_in, s_tm1, branches)
142
143 # Readout bias (post-measurement)
144 s_t = (1.0 - readout_bias) * s_t + readout_bias * np.sign(s_t)
145 s_hat = (1.0 - readout_bias) * s_hat + readout_bias * np.sign(s_hat)
146
147 lt = float(task_loss(s_t, target))
148 lc = float(cons_loss(s_tm1, s_t, s_hat))
149 lq = float(coh_loss(info.get("branches", np.vstack([s_t, s_hat]))))
150 total = lt + 0.5 * (lc + lq)
151 return {"task": lt, "cons": lc, "coh": lq, "total": total, "info": info}
152
153def refresh_info(model, params, ctx):
154 """Recompute info(dict) used by the trust-region KL guard."""
155 return evaluate_stats(model, params, ctx)["info"]
156
157def kl_fn(old_info, new_info):
158 """Symmetric KL-like proxy based on branch statistics."""
159 from qmlhc.optim.numpy_optim.utils import kl_proxy
160 return float(kl_proxy(old_info, new_info))
161
162# =====================================================================
163# Run experiment
164# =====================================================================
165def run_experiment(epochs: int = 350, qubits: int = 7, branches: int = 20, drift_amp: float = 0.30, shots: int = 1024):
166 # Note: drift_amp remains as a “legacy” parameter (unused in hardware-style mode) to preserve the call signature.
167 model, task_loss, cons_loss, coh_loss, callbacks = build_system(
168 qubits=qubits,
169 shots=shots,
170 branches=branches,
171 sched_epochs=epochs
172 )
173
174 x0 = np.linspace(0.1, 0.3, qubits)
175 alpha = 1.0
176
177 # === Initialize SPSA + Trust-KL Optimizer ===
178 base_opt = create_optimizer_numpy("spsa", lr0=0.05, eps0=0.10, antithetic=True, clip=4.0)
179 opt = create_optimizer_numpy("trust-kl", base_opt=base_opt, delta_kl=0.02, backtrack=0.7, max_backtracks=8)
180 params = {"alpha": 1.0}
181 opt.initialize(params)
182
183 rows = []
184 header = [
185 "epoch", "alpha",
186 "task_loss", "cons_loss", "coh_loss", "total_loss",
187 "mean_s", "mean_mu", "drift_real"
188 ]
189
190 for epoch in range(epochs):
191 ctx0 = {"epoch": epoch, "model": model}
192 callbacks.on_epoch_begin(epoch, ctx0)
193
194 # --- drift (hardware-style) ---
195 phase_drift, amp_scale, readout_bias = hardware_drift_emulation (epoch, epochs, qubits)
196
197 # Nominal target
198 target = np.linspace(0.4, 0.9, qubits)
199
200 # Physical signals for this epoch
201 drift = phase_drift # “drift_real” to log and pass to the optimizer
202
203 # --- Build optimizer context for this epoch (with physics) ---
204 context = {
205 "epoch": epoch,
206 "epochs": epochs,
207 "model": model,
208 "x0": x0,
209 "drift": drift, # phase (additive)
210 "amp_scale": amp_scale, # detuning (multiplicative)
211 "readout_bias": readout_bias, # readout bias
212 "target": target,
213 "losses": (task_loss, cons_loss, coh_loss),
214 "branches": branches,
215 "info": {},
216 "kl_fn": kl_fn,
217 "refresh_info": refresh_info,
218 }
219 context["info"] = refresh_info(model, {"alpha": float(params["alpha"])}, context)
220
221 # === Optimizer-driven update of alpha ===
222 params, opt_state = opt.step_params(model, params, context)
223 alpha = float(params["alpha"])
224
225 # === Official forward pass with the same physics ===
226 x = alpha * x0
227 s_tm1 = np.zeros_like(x)
228 x_in = amp_scale * (x + drift)
229 s_t, s_hat, info = model.forward(x_in, s_tm1, branches)
230
231 # Readout bias (post-measurement)
232 s_t = (1.0 - readout_bias) * s_t + readout_bias * np.sign(s_t)
233 s_hat = (1.0 - readout_bias) * s_hat + readout_bias * np.sign(s_hat)
234
235 # Losses and metrics
236 l_task = task_loss(s_t, target)
237 l_cons = cons_loss(s_tm1, s_t, s_hat)
238 l_coh = coh_loss(info["branches"])
239 total_loss = l_task + 0.5 * (l_cons + l_coh)
240
241 mean_s = float(np.mean(s_t))
242 mean_mu = float(np.mean(s_hat))
243
244 step_context = {"epoch": epoch, "loss": total_loss, "alpha": alpha}
245 callbacks.on_step_end(epoch, step_context)
246
247 rows.append([
248 epoch, alpha,
249 l_task, l_cons, l_coh, total_loss,
250 mean_s, mean_mu,
251 float(drift[0]) # “drift_real” used in plots
252 ])
253
254 print(f"Epoch {epoch:04d} | α={alpha:.4f} | "
255 f"Task={l_task:.5f} Cons={l_cons:.5f} Coh={l_coh:.5f} Tot={total_loss:.5f}")
256
257 callbacks.on_epoch_end(epoch, {"epoch": epoch, "loss": total_loss})
258
259 return rows, header
260
261# ======================================================================
262# Save CSV & Plots
263# ======================================================================
264def save_and_plot_quantum_style(rows, header, output_dir="runs/hc_full_demo"):
265 ensure_dirs(output_dir)
266 df = pd.DataFrame(rows, columns=header)
267 csv_path = os.path.join(output_dir, "results.csv")
268 df.to_csv(csv_path, index=False)
269 print(f"[OK] Saved CSV: {csv_path}")
270
271 # 1) Loss & Coherence (dual y-axes)
272 fig, ax1 = plt.subplots(figsize=(12, 6))
273 ax2 = ax1.twinx()
274 ax1.plot(df["epoch"], df["total_loss"], color="tab:red", linewidth=2)
275 ax2.plot(df["epoch"], df["coh_loss"], color="tab:blue", linewidth=2)
276 ax1.set_xlabel("Epoch")
277 ax1.set_ylabel("Loss", color="tab:red")
278 ax2.set_ylabel("Coherence", color="tab:blue")
279 fig.tight_layout()
280 _savefig_numbered(output_dir, "loss_coherence", fig)
281
282 # 2) Consistency vs Coherence (scatter), viridis + colorbar
283 plt.figure(figsize=(6, 6))
284 sc = plt.scatter(df["coh_loss"], df["cons_loss"], c=df["epoch"], cmap="viridis", s=40)
285 plt.xlabel("Coherence")
286 plt.ylabel("Consistency")
287 plt.title("Consistency vs Coherence")
288 cbar = plt.colorbar(sc)
289 cbar.set_label("Epoch")
290 plt.grid(True)
291 _savefig_numbered(output_dir, "consistency_vs_coherence")
292
293 # 3) Alpha (Feedback) Over Epochs — green
294 plt.figure(figsize=(10, 4))
295 plt.plot(df["epoch"], df["alpha"], color="tab:green", linewidth=2)
296 plt.title("Alpha (Feedback) Over Epochs")
297 plt.xlabel("Epoch")
298 plt.ylabel("Alpha")
299 plt.grid(True)
300 _savefig_numbered(output_dir, "alpha_over_epochs")
301
302 # 4) State Alignment — purple vs orange
303 plt.figure(figsize=(12, 5))
304 plt.plot(df["epoch"], df["mean_s"], color="tab:purple", linewidth=2, label="mean(S_t)")
305 plt.plot(df["epoch"], df["mean_mu"], color="tab:orange", linewidth=2, label="mean(mu_fut)")
306 plt.title("State Alignment")
307 plt.xlabel("Epoch")
308 plt.ylabel("Mean Values")
309 plt.legend()
310 plt.grid(True)
311 _savefig_numbered(output_dir, "state_alignment")
312
313 # 5) 3D Causal Space — plasma, colorbar 'Epoch'
314 fig = plt.figure(figsize=(10, 7))
315 ax = fig.add_subplot(111, projection="3d")
316 sc3 = ax.scatter(df["coh_loss"], df["cons_loss"], df["alpha"],
317 c=df["epoch"], cmap="plasma", s=60, alpha=0.9)
318 ax.set_xlabel("Coherence")
319 ax.set_ylabel("Consistency")
320 ax.set_zlabel("Alpha")
321 ax.set_title("3D Causal Space")
322 cb3 = fig.colorbar(sc3, ax=ax, shrink=0.65)
323 cb3.set_label("Epoch")
324 plt.tight_layout()
325 _savefig_numbered(output_dir, "causal_space_3d")
326
327 # 6) Drift Signals Only (Real vs Proxy)
328 fig, ax1 = plt.subplots(figsize=(10, 4))
329 ax2 = ax1.twinx()
330
331 drift_proxy = np.abs(df["alpha"].diff().fillna(0.0))
332 drift_proxy_s = drift_proxy.rolling(5, min_periods=1).mean()
333
334 ax1.plot(df["epoch"], df["drift_real"], color="tab:purple", linewidth=2, label="Drift Real (phase-like)")
335 ax2.plot(df["epoch"], drift_proxy_s, color="tab:red", linestyle="--", linewidth=1.8, label="|ΔAlpha| Proxy (roll=5)")
336
337 ax1.set_xlabel("Epoch")
338 ax1.set_ylabel("Drift Real")
339 ax2.set_ylabel("|ΔAlpha| Proxy")
340 ax1.set_title("Drift Signals Only")
341
342 l1, lab1 = ax1.get_legend_handles_labels()
343 l2, lab2 = ax2.get_legend_handles_labels()
344 ax1.legend(l1 + l2, lab1 + lab2, loc="upper right", frameon=False)
345
346 ax1.grid(True, alpha=0.3)
347 plt.tight_layout()
348 _savefig_numbered(output_dir, "drift_signals_only")
349
350 # 7) Alpha Sensitivity (Δα per Epoch)
351 plt.figure(figsize=(10, 5))
352 d_alpha = df["alpha"].diff().fillna(0.0)
353 plt.plot(df["epoch"], d_alpha, color="tab:gray", linewidth=2)
354 plt.title("Alpha Sensitivity (Δα per Epoch)")
355 plt.xlabel("Epoch")
356 plt.ylabel("ΔAlpha")
357 plt.grid(True, alpha=0.3)
358 plt.tight_layout()
359 _savefig_numbered(output_dir, "alpha_sensitivity")
360
361 # 8) Causal Phase Portrait (Temporal Path)
362 plt.figure(figsize=(7, 6))
363 plt.plot(df["coh_loss"], df["cons_loss"], color="tab:purple", linewidth=1.5, alpha=0.8)
364 sc_path = plt.scatter(df["coh_loss"], df["cons_loss"],
365 c=df["epoch"], cmap="plasma", s=28, alpha=0.9)
366 plt.xlabel("Coherence")
367 plt.ylabel("Consistency")
368 plt.title("Causal Phase Portrait (Temporal Path)")
369 cbp = plt.colorbar(sc_path)
370 cbp.set_label("Epoch")
371 plt.grid(True, alpha=0.3)
372 plt.tight_layout()
373 _savefig_numbered(output_dir, "causal_phase_portrait")
374
375 # 9) Drift vs Coherence Dynamics
376 plt.figure(figsize=(9, 5))
377 drift_proxy = np.abs(df["alpha"].diff().fillna(0.0))
378 plt.plot(df["epoch"], df["coh_loss"], color="tab:blue", linewidth=2, label="Coherence")
379 plt.plot(df["epoch"], drift_proxy, color="tab:red", linewidth=2, label="|ΔAlpha| (Drift Proxy)")
380 plt.title("Drift vs Coherence Dynamics")
381 plt.xlabel("Epoch")
382 plt.ylabel("Value")
383 plt.legend(frameon=False)
384 plt.grid(True, alpha=0.3)
385 plt.tight_layout()
386 _savefig_numbered(output_dir, "drift_vs_coherence")
387
388 # 10) Parallel Causal Dimensions (epoch grouped)
389 plt.figure(figsize=(8,6))
390 subset = df[["coh_loss", "cons_loss", "alpha", "epoch"]].copy()
391 subset["epoch_group"] = pd.cut(subset["epoch"], bins=5, labels=False)
392 parallel_coordinates(subset, "epoch_group", color=plt.cm.plasma(np.linspace(0,1,5)), alpha=0.6)
393 plt.title("Parallel Causal Dimensions")
394 plt.xlabel("Metrics")
395 plt.ylabel("Value")
396 plt.grid(True, alpha=0.3)
397 plt.tight_layout()
398 _savefig_numbered(output_dir, "causal_parallel")
399
400
401 return df
402
403# ======================================================================
404# Main
405# ======================================================================
406if __name__ == "__main__":
407 # You may adjust epochs/qubits/branches according to your test profile
408 rows, header = run_experiment(epochs=300, qubits=7, branches=20, drift_amp=0.30, shots=1024)
409 save_and_plot_quantum_style(rows, header, output_dir="runs/hypercausal_physical_drift_resilience")