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: PennyLaneBackend with BackendConfig(output_dim=qubits, shots=shots).

  • Policy: MeanPolicy (branch aggregation by mean).

  • Node / Model: a single HCNode wrapped by HCModel.

  • Losses: MSELoss (task), ConsistencyLoss (temporal), CoherenceLoss (branch coherence).

  • Callbacks: MemoryLogger (telemetry) and DepthScheduler (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:

\[\text{drift}_t \;=\; A \cdot \sin\!\Big(\frac{2\pi\,t}{T-1}\Big)\,\mathbf{1},\]

where \(A=\texttt{drift\_amp}\) and \(T\) is the number of epochs. The effective input is the feedback-scaled signal

\[x_t \;=\; \alpha_t \, x_0 \;+\; \text{drift}_t.\]

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:

\[\begin{split}\begin{aligned} \text{Phase Drift:} &\quad \Delta\phi_t = \Phi_{\max} \sin\!\Big(\frac{2\pi\,t}{T-1}\Big)\,\mathbf{1}, \\[6pt] \text{Frequency Detuning:} &\quad \lambda_t = 1 + \epsilon_{\mathrm{ppm}}\,t, \\[6pt] \text{Readout Bias:} &\quad \beta_t = B_{\max} \Big(0.5 + 0.5\,\sin\!\Big(\frac{2\pi\,t}{T-1} + \tfrac{\pi}{3}\Big)\Big). \end{aligned}\end{split}\]

The perturbed input to the model is

\[x_t^{(\mathrm{in})} = \lambda_t \big(\alpha_t\,x_0 + \Delta\phi_t\big),\]

and a post-measurement correction is applied as

\[S_t' = (1 - \beta_t)\,S_t + \beta_t\,\operatorname{sign}(S_t), \qquad \mu_{\mathrm{fut}}' = (1 - \beta_t)\,\mu_{\mathrm{fut}} + \beta_t\,\operatorname{sign}(\mu_{\mathrm{fut}}).\]

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#

  • PennyLaneBackend instantiates the quantum-inspired circuit with the given number of qubits and shots.

  • HCNode + MeanPolicy produce 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

\[S_t \;=\; f_\theta(x_t, S_{t-1}, \alpha_t), \qquad \mu_{\mathrm{fut}} \;=\; g_\theta(x_t, S_t, \alpha_t),\]

where \(\theta\) denotes circuit parameters and \(\alpha_t\) is the feedback coefficient.

Task loss (MSE).

\[\mathcal{L}_{\mathrm{task}} \;=\; \tfrac{1}{N}\sum_{t=1}^{N}\,\|\,h_\theta(S_t) - y_t\,\|_2^2.\]

Temporal consistency loss.

\[\mathcal{L}_{\mathrm{consistency}} \;=\; \tfrac{1}{N}\sum_{t=1}^{N}\,\|\,S_t - \mu_{\mathrm{fut}}\,\|_2^2.\]

Coherence loss (branch coherence). Let \(p_t(b)\) be the normalized branch activation:

\[\mathcal{L}_{\mathrm{coherence}} \;=\; \tfrac{1}{N}\sum_{t=1}^{N} \Big[ - \sum_{b} p_t(b)\,\log p_t(b) \Big].\]

Total objective (minimized).

\[\mathcal{L}_{\mathrm{total}} \;=\; \mathcal{L}_{\mathrm{task}} \;+\; \tfrac12\!\left( \mathcal{L}_{\mathrm{consistency}} + \mathcal{L}_{\mathrm{coherence}} \right).\]

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:

\[D_{\mathrm{KL}}^{\mathrm{sym}} \big(p_{\text{old}} \parallel p_{\text{new}}\big) \;=\; D_{\mathrm{KL}}(p_{\text{old}} \parallel p_{\text{new}}) + D_{\mathrm{KL}}(p_{\text{new}} \parallel p_{\text{old}}).\]

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_endon_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.csv with 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

Loss vs Coherence Dynamics – Adaptation Loss vs Coherence Dynamics – Resilience

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

Consistency vs Coherence – Adaptation Consistency vs Coherence – Resilience

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

Alpha Feedback Evolution – Adaptation Alpha Feedback Evolution – Resilience

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

State Alignment – Adaptation State Alignment – Resilience

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)

Alpha Sensitivity – Adaptation Alpha Sensitivity – Resilience

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

Drift vs Coherence – Adaptation Drift vs Coherence – Resilience

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)

Causal Phase Portrait – Adaptation Causal Phase Portrait – Resilience

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

Parallel Causal Dimensions – Adaptation Parallel Causal Dimensions – Resilience

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)

3D Causal Space – Adaptation 3D Causal Space – Resilience

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)

Drift Signals Only – Adaptation Drift Signals Only – Resilience

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#

Adaptive drift compensation experiment (SPSA + Trust-KL optimization)#
  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")
Physical drift resilience experiment (hypercausal feedback under real perturbation)#
  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")