"""Concrete hardware experiments for IBM Quantum.

Each experiment function takes a HardwareRunner and returns results + classical
reference for comparison. Designed to fit within the 10-min/month free tier.

20 experiments total. See docs/EXPERIMENT_ROADMAP.md for budget allocation.
"""

from __future__ import annotations

import numpy as np
from qiskit import QuantumCircuit
from qiskit.circuit.library import PauliEvolutionGate
from qiskit.quantum_info import SparsePauliOp, Statevector
from qiskit.synthesis import LieTrotter, SuzukiTrotter
from scipy.optimize import minimize

from ..bridge.knm_hamiltonian import (
    OMEGA_N_16,
    build_knm_paper27,
    knm_to_ansatz,
    knm_to_hamiltonian,
)
from ..crypto.entanglement_qkd import (
    bell_inequality_test,
    correlator_matrix,
    scpn_qkd_protocol,
)
from ..crypto.knm_key import estimate_qber, extract_raw_key, prepare_key_state
from ..crypto.noise_analysis import devetak_winter_rate
from .classical import (
    classical_brute_mpc,
    classical_exact_diag,
    classical_exact_evolution,
)


def _build_evo_base(n, K, omega, t, trotter_reps, trotter_order=1):
    """Build evolution circuit without measurement gates."""
    qc = QuantumCircuit(n)
    for i in range(n):
        qc.ry(float(omega[i]) % (2 * np.pi), i)
    H = knm_to_hamiltonian(K, omega)
    if trotter_order >= 2:
        synthesis = SuzukiTrotter(order=trotter_order, reps=trotter_reps)
    else:
        synthesis = LieTrotter(reps=trotter_reps)
    evo = PauliEvolutionGate(H, time=t, synthesis=synthesis)
    qc.append(evo, range(n))
    return qc


def _build_xyz_circuits(base_circuit, n):
    """Build 3 copies of base_circuit measuring in Z, X, Y bases.

    X-basis: H before measurement.
    Y-basis: Sdg then H before measurement.
    Returns (z_circuit, x_circuit, y_circuit).
    """
    qc_z = base_circuit.copy()
    qc_z.measure_all()

    qc_x = base_circuit.copy()
    for q in range(n):
        qc_x.h(q)
    qc_x.measure_all()

    qc_y = base_circuit.copy()
    for q in range(n):
        qc_y.sdg(q)
        qc_y.h(q)
    qc_y.measure_all()

    return qc_z, qc_x, qc_y


def _expectation_per_qubit(counts, n_qubits):
    """Compute per-qubit <Z> and shot-noise standard deviation.

    Returns:
        (exp_vals, std_vals) where std = sqrt((1 - exp^2) / N_shots).
    """
    total = sum(counts.values())
    exp_vals = np.zeros(n_qubits)
    for bitstring, count in counts.items():
        bits = bitstring.replace(" ", "")
        for q in range(min(n_qubits, len(bits))):
            bit = int(bits[-(q + 1)])
            exp_vals[q] += (1 - 2 * bit) * count
    exp_vals /= total
    std_vals: np.ndarray = np.sqrt(np.maximum(1.0 - exp_vals**2, 0.0) / total)
    return exp_vals, std_vals


def _R_from_xyz(z_counts, x_counts, y_counts, n_qubits):
    """Compute Kuramoto order parameter R from X, Y, Z basis measurements.

    Returns:
        (R, R_std, exp_x, exp_y, exp_z, std_x, std_y, std_z)
    """
    exp_x, std_x = _expectation_per_qubit(x_counts, n_qubits)
    exp_y, std_y = _expectation_per_qubit(y_counts, n_qubits)
    exp_z, std_z = _expectation_per_qubit(z_counts, n_qubits)
    z_complex = np.mean(exp_x + 1j * exp_y)
    R = float(abs(z_complex))
    # Propagated uncertainty: delta_R ≈ sqrt(sum(std_x^2 + std_y^2)) / N
    R_std = float(np.sqrt(np.mean(std_x**2 + std_y**2)) / np.sqrt(n_qubits))
    return R, R_std, exp_x, exp_y, exp_z, std_x, std_y, std_z


def kuramoto_4osc_experiment(
    runner, shots: int = 10000, n_time_steps: int = 8, dt: float = 0.1
) -> dict:
    """4-oscillator Kuramoto XY dynamics on hardware.

    Measures order parameter R(t) via X, Y, Z basis shots at each time step.
    Compares against exact matrix-exponential evolution.

    Returns:
        dict with keys:
            experiment (str): Experiment name identifier.
            n_oscillators (int): Number of oscillators.
            dt (float): Time step size.
            hw_times (list[float]): Hardware measurement times.
            hw_R (list[float]): Hardware order parameter per step.
            hw_R_std (list[float]): Shot-noise std of R per step.
            classical_times (list[float]): Exact evolution times.
            classical_R (list[float]): Exact order parameter per step.
            classical_R_std (float): Always 0.0 (exact).
            hw_expectations (list[dict]): Per-step exp_x, exp_y, exp_z.
    """
    n = 4
    K = build_knm_paper27(L=n)
    omega = OMEGA_N_16[:n]

    print(f"\n=== Kuramoto 4-oscillator, {n_time_steps} steps, dt={dt} ===")

    all_circuits: list[QuantumCircuit] = []
    step_indices: list[int] = []
    for step in range(1, n_time_steps + 1):
        t = step * dt
        base = _build_evo_base(n, K, omega, t, trotter_reps=step * 2)
        qc_z, qc_x, qc_y = _build_xyz_circuits(base, n)
        idx = len(all_circuits)
        all_circuits.extend([qc_z, qc_x, qc_y])
        step_indices.append(idx)

    hw_results = runner.run_sampler(all_circuits, shots=shots, name="kuramoto_4osc")

    hw_R = []
    hw_R_std = []
    hw_exp = []
    for idx in step_indices:
        R, R_std, ex, ey, ez, sx, sy, sz = _R_from_xyz(
            hw_results[idx].counts,
            hw_results[idx + 1].counts,
            hw_results[idx + 2].counts,
            n,
        )
        hw_R.append(R)
        hw_R_std.append(R_std)
        hw_exp.append({"exp_x": ex.tolist(), "exp_y": ey.tolist(), "exp_z": ez.tolist()})

    hw_times = [i * dt for i in range(1, n_time_steps + 1)]
    classical = classical_exact_evolution(n, n_time_steps * dt, dt, K, omega)

    result = {
        "experiment": "kuramoto_4osc",
        "n_oscillators": n,
        "dt": dt,
        "hw_times": hw_times,
        "hw_R": hw_R,
        "hw_R_std": hw_R_std,
        "classical_times": classical["times"].tolist(),
        "classical_R": classical["R"].tolist(),
        "classical_R_std": 0.0,
        "hw_expectations": hw_exp,
    }
    runner.save_result(hw_results[0], "kuramoto_4osc.json")
    return result


def kuramoto_8osc_experiment(
    runner, shots: int = 10000, n_time_steps: int = 6, dt: float = 0.1
) -> dict:
    """8-oscillator Kuramoto XY dynamics.

    Returns:
        dict with keys:
            experiment (str): Experiment name identifier.
            n_oscillators (int): Number of oscillators.
            dt (float): Time step size.
            hw_times (list[float]): Hardware measurement times.
            hw_R (list[float]): Hardware order parameter per step.
            hw_R_std (list[float]): Shot-noise std of R per step.
            classical_times (list[float]): Exact evolution times.
            classical_R (list[float]): Exact order parameter per step.
            classical_R_std (float): Always 0.0 (exact).
    """
    n = 8
    K = build_knm_paper27(L=n)
    omega = OMEGA_N_16[:n]

    print(f"\n=== Kuramoto 8-oscillator, {n_time_steps} steps, dt={dt} ===")

    all_circuits: list[QuantumCircuit] = []
    step_indices: list[int] = []
    for step in range(1, n_time_steps + 1):
        t = step * dt
        base = _build_evo_base(n, K, omega, t, trotter_reps=step)
        qc_z, qc_x, qc_y = _build_xyz_circuits(base, n)
        idx = len(all_circuits)
        all_circuits.extend([qc_z, qc_x, qc_y])
        step_indices.append(idx)

    hw_results = runner.run_sampler(all_circuits, shots=shots, name="kuramoto_8osc")

    hw_R = []
    hw_R_std = []
    for idx in step_indices:
        R, R_std, *_ = _R_from_xyz(
            hw_results[idx].counts,
            hw_results[idx + 1].counts,
            hw_results[idx + 2].counts,
            n,
        )
        hw_R.append(R)
        hw_R_std.append(R_std)

    hw_times = [i * dt for i in range(1, n_time_steps + 1)]
    classical = classical_exact_evolution(n, n_time_steps * dt, dt, K, omega)

    result = {
        "experiment": "kuramoto_8osc",
        "n_oscillators": n,
        "dt": dt,
        "hw_times": hw_times,
        "hw_R": hw_R,
        "hw_R_std": hw_R_std,
        "classical_times": classical["times"].tolist(),
        "classical_R": classical["R"].tolist(),
        "classical_R_std": 0.0,
    }
    runner.save_result(hw_results[0], "kuramoto_8osc.json")
    return result


def _run_vqe(n: int, maxiter: int = 200) -> dict:
    """Statevector VQE on the n-oscillator XY Hamiltonian."""
    K = build_knm_paper27(L=n)
    omega = OMEGA_N_16[:n]
    H = knm_to_hamiltonian(K, omega)
    ansatz = knm_to_ansatz(K, reps=2)
    n_params = ansatz.num_parameters

    print(f"\n=== VQE {n}-qubit, {n_params} params, maxiter={maxiter} ===")

    exact = classical_exact_diag(n, K, omega)
    print(f"  Exact ground energy: {exact['ground_energy']:.6f}")

    energy_history: list[float] = []

    def cost_fn(params):
        bound = ansatz.assign_parameters(params)
        sv = Statevector.from_instruction(bound)
        energy = float(sv.expectation_value(H).real)
        energy_history.append(energy)
        return energy

    x0 = np.random.default_rng(42).uniform(-np.pi, np.pi, n_params)
    opt_result = minimize(cost_fn, x0, method="COBYLA", options={"maxiter": maxiter})

    print(f"  VQE converged: {opt_result.success}, iterations: {opt_result.nfev}")
    print(f"  VQE energy: {opt_result.fun:.6f}")

    tail = energy_history[-10:] if len(energy_history) >= 10 else energy_history
    energy_std = float(np.std(tail))

    return {
        "experiment": f"vqe_{n}q",
        "n_qubits": n,
        "vqe_energy": float(opt_result.fun),
        "exact_ground_energy": exact["ground_energy"],
        "energy_gap": float(opt_result.fun - exact["ground_energy"]),
        "energy_std": energy_std,
        "n_iterations": opt_result.nfev,
        "converged": bool(opt_result.success),
        "energy_history": energy_history,
    }


def vqe_4q_experiment(runner, shots: int = 10000, maxiter: int = 200) -> dict:
    """VQE ground state of 4-oscillator XY Hamiltonian.

    Returns:
        dict with keys:
            experiment (str): Experiment name identifier.
            n_qubits (int): Number of qubits.
            vqe_energy (float): Optimized VQE energy.
            exact_ground_energy (float): Exact diagonalization ground energy.
            energy_gap (float): vqe_energy - exact_ground_energy.
            n_iterations (int): COBYLA function evaluations.
            converged (bool): Whether optimizer converged.
            energy_history (list[float]): Energy at each iteration.
    """
    return _run_vqe(4, maxiter)


def vqe_8q_experiment(runner, shots: int = 10000, maxiter: int = 150) -> dict:
    """VQE ground state of 8-oscillator XY Hamiltonian.

    Returns:
        dict with keys:
            experiment (str): Experiment name identifier.
            n_qubits (int): Number of qubits.
            vqe_energy (float): Optimized VQE energy.
            exact_ground_energy (float): Exact diagonalization ground energy.
            energy_gap (float): vqe_energy - exact_ground_energy.
            n_iterations (int): COBYLA function evaluations.
            converged (bool): Whether optimizer converged.
            energy_history (list[float]): Energy at each iteration.
    """
    return _run_vqe(8, maxiter)


def qaoa_mpc_4_experiment(runner, shots: int = 10000) -> dict:
    """QAOA-MPC binary control, horizon=4, p=1 and p=2.

    Cost Hamiltonian is diagonal in Z, so Z-basis measurement is exact.
    Compares QAOA solution quality vs brute-force optimal.

    Returns:
        dict with keys:
            experiment (str): Experiment name identifier.
            horizon (int): MPC planning horizon.
            brute_force_cost (float): Optimal cost from exhaustive search.
            brute_force_actions (list[int]): Optimal binary actions.
            qaoa_p1 (dict): p=1 results (qaoa_cost, qaoa_actions, n_iterations).
            qaoa_p2 (dict): p=2 results (qaoa_cost, qaoa_actions, n_iterations).
    """
    B = np.eye(2)
    target = np.array([0.8, 0.6])
    horizon = 4

    print(f"\n=== QAOA-MPC h={horizon}, target={target} ===")

    brute = classical_brute_mpc(B, target, horizon)
    print(f"  Brute-force optimal cost: {brute['optimal_cost']:.6f}")
    print(f"  Brute-force optimal actions: {brute['optimal_actions']}")

    from ..control.qaoa_mpc import QAOA_MPC

    results_by_p = {}
    for p in [1, 2]:
        mpc = QAOA_MPC(B, target, horizon=horizon, p_layers=p)
        mpc.build_cost_hamiltonian()

        def cost_fn(params, _p=p, _mpc=mpc):
            gamma = params[:_p]
            beta = params[_p:]
            qc = _mpc._build_qaoa_circuit(gamma, beta)
            qc.measure_all()
            hw = runner.run_sampler(qc, shots=shots, name=f"qaoa_p{_p}")
            counts = hw[0].counts
            return _qaoa_cost_from_counts(counts, _mpc._cost_ham, _mpc.n_qubits)

        x0 = np.random.default_rng(42).uniform(0, np.pi, 2 * p)
        opt = minimize(cost_fn, x0, method="COBYLA", options={"maxiter": 100})

        gamma_opt = opt.x[:p]
        beta_opt = opt.x[p:]
        final_qc = mpc._build_qaoa_circuit(gamma_opt, beta_opt)
        final_qc.measure_all()
        final_hw = runner.run_sampler(final_qc, shots=shots, name=f"qaoa_p{p}_final")
        counts = final_hw[0].counts
        best_bitstring = max(counts, key=counts.get)
        actions = np.array([int(b) for b in reversed(best_bitstring)])

        print(f"  QAOA p={p}: cost={opt.fun:.6f}, actions={actions}, iters={opt.nfev}")

        results_by_p[p] = {
            "qaoa_cost": float(opt.fun),
            "qaoa_actions": actions.tolist(),
            "n_iterations": opt.nfev,
        }

    result = {
        "experiment": "qaoa_mpc_4",
        "horizon": horizon,
        "brute_force_cost": brute["optimal_cost"],
        "brute_force_actions": brute["optimal_actions"].tolist(),
        "qaoa_p1": results_by_p[1],
        "qaoa_p2": results_by_p[2],
    }
    return result


def upde_16_snapshot_experiment(runner, shots: int = 20000, trotter_steps: int = 1) -> dict:
    """Full 16-layer UPDE single Trotter snapshot.

    Measures in X, Y, Z bases. Compares R against exact evolution.
    ~240 ECR gates. On real hardware, needs error mitigation.

    Returns:
        dict with keys:
            experiment (str): Experiment name identifier.
            n_layers (int): Number of UPDE layers (16).
            dt (float): Time step size.
            trotter_steps (int): Number of Trotter repetitions.
            hw_R (float): Hardware order parameter.
            hw_R_std (float): Shot-noise std of R.
            classical_R (float): Exact order parameter.
            classical_R_std (float): Always 0.0 (exact).
            hw_exp_x (list[float]): Per-qubit X expectations.
            hw_exp_y (list[float]): Per-qubit Y expectations.
            hw_exp_z (list[float]): Per-qubit Z expectations.
    """
    n = 16
    K = build_knm_paper27(L=n)
    omega = OMEGA_N_16.copy()
    dt = 0.05

    print(f"\n=== UPDE 16-layer snapshot, dt={dt}, {trotter_steps} Trotter steps ===")

    base = _build_evo_base(n, K, omega, dt, trotter_reps=trotter_steps)
    qc_z, qc_x, qc_y = _build_xyz_circuits(base, n)

    hw_results = runner.run_sampler([qc_z, qc_x, qc_y], shots=shots, name="upde_16")

    R, R_std, exp_x, exp_y, exp_z, *_ = _R_from_xyz(
        hw_results[0].counts,
        hw_results[1].counts,
        hw_results[2].counts,
        n,
    )

    classical = classical_exact_evolution(n, dt, dt, K, omega)

    result = {
        "experiment": "upde_16_snapshot",
        "n_layers": n,
        "dt": dt,
        "trotter_steps": trotter_steps,
        "hw_R": R,
        "hw_R_std": R_std,
        "classical_R": float(classical["R"][-1]),
        "classical_R_std": 0.0,
        "hw_exp_x": exp_x.tolist(),
        "hw_exp_y": exp_y.tolist(),
        "hw_exp_z": exp_z.tolist(),
    }
    runner.save_result(hw_results[0], "upde_16_snapshot.json")
    return result


def _qaoa_cost_from_counts(counts: dict, cost_ham: SparsePauliOp, n_qubits: int) -> float:
    """Evaluate QAOA cost Hamiltonian (diagonal in Z) from counts."""
    total = sum(counts.values())
    energy = 0.0
    for pauli, coeff in zip(cost_ham.paulis, cost_ham.coeffs):
        label = str(pauli)
        exp_val = 0.0
        for bitstring, count in counts.items():
            bits = bitstring.replace(" ", "")
            sign = 1
            for q in range(n_qubits):
                p = label[-(q + 1)]
                if p == "Z":
                    bit = int(bits[-(q + 1)])
                    sign *= (-1) ** bit
                elif p in ("X", "Y"):
                    sign = 0
                    break
            exp_val += sign * count
        exp_val /= total
        energy += float(coeff.real) * exp_val
    return energy


def kuramoto_4osc_zne_experiment(
    runner, shots: int = 10000, dt: float = 0.1, scales: list[int] | None = None
) -> dict:
    """4-oscillator Kuramoto with ZNE error mitigation.

    Runs the evolution at multiple noise scales via unitary folding,
    then extrapolates to zero noise.

    Returns:
        dict with keys:
            experiment (str): Experiment name identifier.
            scales (list[int]): Noise scale factors used.
            R_per_scale (list[float]): Measured R at each scale.
            zne_R (float): Zero-noise extrapolated R.
            classical_R (float): Exact order parameter.
            fit_residual (float): Polynomial fit residual.
    """
    from ..mitigation.zne import gate_fold_circuit, zne_extrapolate

    if scales is None:
        scales = [1, 3, 5]

    n = 4
    K = build_knm_paper27(L=n)
    omega = OMEGA_N_16[:n]
    t = dt

    print(f"\n=== Kuramoto 4-osc ZNE, dt={dt}, scales={scales} ===")

    base = _build_evo_base(n, K, omega, t, trotter_reps=2)

    R_per_scale = []
    R_std_per_scale = []
    for s in scales:
        folded = gate_fold_circuit(base, s)
        qc_z, qc_x, qc_y = _build_xyz_circuits(folded, n)
        hw = runner.run_sampler([qc_z, qc_x, qc_y], shots=shots, name=f"zne_s{s}")
        R, R_std, *_ = _R_from_xyz(hw[0].counts, hw[1].counts, hw[2].counts, n)
        R_per_scale.append(R)
        R_std_per_scale.append(R_std)
        print(f"  scale={s}: R={R:.4f} ± {R_std:.4f}")

    zne = zne_extrapolate(scales, R_per_scale, order=1)
    classical = classical_exact_evolution(n, dt, dt, K, omega)

    print(f"  ZNE R(0) = {zne.zero_noise_estimate:.4f}")
    print(f"  Exact R  = {classical['R'][-1]:.4f}")

    return {
        "experiment": "kuramoto_4osc_zne",
        "scales": scales,
        "R_per_scale": R_per_scale,
        "R_std_per_scale": R_std_per_scale,
        "zne_R": zne.zero_noise_estimate,
        "classical_R": float(classical["R"][-1]),
        "fit_residual": zne.fit_residual,
    }


def noise_baseline_experiment(runner, shots: int = 10000) -> dict:
    """4-qubit near-identity circuit for calibration drift detection.

    Single Trotter step at dt=0.01 (near-identity). Measures R + per-qubit
    expectations. Compare Feb->Mar to detect backend drift.

    Returns:
        dict with keys:
            experiment (str): Experiment name identifier.
            n_qubits (int): Number of qubits.
            dt (float): Time step size (0.01, near-identity).
            hw_R (float): Hardware order parameter.
            hw_R_std (float): Shot-noise std of R.
            classical_R (float): Exact order parameter.
            classical_R_std (float): Always 0.0 (exact).
            hw_exp_x (list[float]): Per-qubit X expectations.
            hw_exp_y (list[float]): Per-qubit Y expectations.
            hw_exp_z (list[float]): Per-qubit Z expectations.
    """
    n = 4
    K = build_knm_paper27(L=n)
    omega = OMEGA_N_16[:n]
    dt = 0.01

    print(f"\n=== Noise baseline, {n}q, dt={dt} ===")

    base = _build_evo_base(n, K, omega, dt, trotter_reps=1)
    qc_z, qc_x, qc_y = _build_xyz_circuits(base, n)

    hw_results = runner.run_sampler([qc_z, qc_x, qc_y], shots=shots, name="noise_baseline")
    R, R_std, exp_x, exp_y, exp_z, *_ = _R_from_xyz(
        hw_results[0].counts, hw_results[1].counts, hw_results[2].counts, n
    )

    classical = classical_exact_evolution(n, dt, dt, K, omega)

    result = {
        "experiment": "noise_baseline",
        "n_qubits": n,
        "dt": dt,
        "hw_R": R,
        "hw_R_std": R_std,
        "classical_R": float(classical["R"][-1]),
        "classical_R_std": 0.0,
        "hw_exp_x": exp_x.tolist(),
        "hw_exp_y": exp_y.tolist(),
        "hw_exp_z": exp_z.tolist(),
    }
    runner.save_result(hw_results[0], "noise_baseline.json")
    return result


def kuramoto_8osc_zne_experiment(
    runner, shots: int = 10000, dt: float = 0.1, scales: list[int] | None = None
) -> dict:
    """8-oscillator Kuramoto with ZNE error mitigation.

    Gate-fold at each noise scale, Richardson extrapolation to zero noise.
    Extends the 4-osc ZNE result to depth-233 territory.

    Returns:
        dict with keys:
            experiment (str): Experiment name identifier.
            n_oscillators (int): Number of oscillators.
            scales (list[int]): Noise scale factors used.
            R_per_scale (list[float]): Measured R at each scale.
            zne_R (float): Zero-noise extrapolated R.
            classical_R (float): Exact order parameter.
            fit_residual (float): Polynomial fit residual.
    """
    from ..mitigation.zne import gate_fold_circuit, zne_extrapolate

    if scales is None:
        scales = [1, 3, 5]

    n = 8
    K = build_knm_paper27(L=n)
    omega = OMEGA_N_16[:n]
    t = dt

    print(f"\n=== Kuramoto 8-osc ZNE, dt={dt}, scales={scales} ===")

    base = _build_evo_base(n, K, omega, t, trotter_reps=2)

    R_per_scale = []
    R_std_per_scale = []
    for s in scales:
        folded = gate_fold_circuit(base, s)
        qc_z, qc_x, qc_y = _build_xyz_circuits(folded, n)
        hw = runner.run_sampler([qc_z, qc_x, qc_y], shots=shots, name=f"zne8_s{s}")
        R, R_std, *_ = _R_from_xyz(hw[0].counts, hw[1].counts, hw[2].counts, n)
        R_per_scale.append(R)
        R_std_per_scale.append(R_std)
        print(f"  scale={s}: R={R:.4f} ± {R_std:.4f}")

    zne = zne_extrapolate(scales, R_per_scale, order=1)
    classical = classical_exact_evolution(n, dt, dt, K, omega)

    print(f"  ZNE R(0) = {zne.zero_noise_estimate:.4f}")
    print(f"  Exact R  = {classical['R'][-1]:.4f}")

    return {
        "experiment": "kuramoto_8osc_zne",
        "n_oscillators": n,
        "scales": scales,
        "R_per_scale": R_per_scale,
        "R_std_per_scale": R_std_per_scale,
        "zne_R": zne.zero_noise_estimate,
        "classical_R": float(classical["R"][-1]),
        "fit_residual": zne.fit_residual,
    }


def vqe_8q_hardware_experiment(runner, shots: int = 10000, maxiter: int = 150) -> dict:
    """VQE 8-qubit: Statevector optimization -> hardware energy evaluation.

    1. COBYLA on Statevector -> optimal params
    2. Bind optimal params into Knm-informed ansatz
    3. Send bound circuit to hardware via run_estimator with H observable
    4. Return sim energy, hw energy, exact ground energy

    Returns:
        dict with keys:
            experiment (str): Experiment name identifier.
            n_qubits (int): Number of qubits.
            sim_energy (float): Statevector-optimized VQE energy.
            hw_energy (float): Hardware-evaluated energy.
            exact_energy (float): Exact diagonalization ground energy.
            sim_gap (float): sim_energy - exact_energy.
            hw_gap (float): hw_energy - exact_energy.
            n_iterations (int): COBYLA function evaluations.
    """
    n = 8
    K = build_knm_paper27(L=n)
    omega = OMEGA_N_16[:n]
    H = knm_to_hamiltonian(K, omega)
    ansatz = knm_to_ansatz(K, reps=2)

    print(f"\n=== VQE 8q hardware, {ansatz.num_parameters} params ===")

    exact = classical_exact_diag(n, K, omega)
    print(f"  Exact ground energy: {exact['ground_energy']:.6f}")

    # Step 1: classical VQE on Statevector
    def cost_fn(params):
        bound = ansatz.assign_parameters(params)
        sv = Statevector.from_instruction(bound)
        return float(sv.expectation_value(H).real)

    x0 = np.random.default_rng(42).uniform(-np.pi, np.pi, ansatz.num_parameters)
    opt = minimize(cost_fn, x0, method="COBYLA", options={"maxiter": maxiter})
    sim_energy = float(opt.fun)
    print(f"  Sim VQE energy: {sim_energy:.6f}")

    # Step 2: bind optimal params and evaluate on hardware
    bound_circuit = ansatz.assign_parameters(opt.x)
    hw_result = runner.run_estimator(bound_circuit, [H], name="vqe_8q_hw")
    hw_energy = float(hw_result.expectation_values[0])
    print(f"  HW energy: {hw_energy:.6f}")

    return {
        "experiment": "vqe_8q_hardware",
        "n_qubits": n,
        "sim_energy": sim_energy,
        "hw_energy": hw_energy,
        "exact_energy": exact["ground_energy"],
        "sim_gap": sim_energy - exact["ground_energy"],
        "hw_gap": hw_energy - exact["ground_energy"],
        "n_iterations": opt.nfev,
    }


def upde_16_dd_experiment(runner, shots: int = 20000, trotter_steps: int = 1) -> dict:
    """16-layer UPDE with dynamical decoupling.

    Same structure as upde_16_snapshot but applies DD (XY4) to each
    basis circuit before submission. Compares R(DD) vs R(no-DD) vs classical.

    Returns:
        dict with keys:
            experiment (str): Experiment name identifier.
            n_layers (int): Number of UPDE layers (16).
            dt (float): Time step size.
            trotter_steps (int): Number of Trotter repetitions.
            hw_R_raw (float): Hardware R without DD.
            hw_R_dd (float): Hardware R with dynamical decoupling.
            classical_R (float): Exact order parameter.
            hw_exp_x_dd (list[float]): Per-qubit X expectations (DD).
            hw_exp_y_dd (list[float]): Per-qubit Y expectations (DD).
            hw_exp_z_dd (list[float]): Per-qubit Z expectations (DD).
    """
    n = 16
    K = build_knm_paper27(L=n)
    omega = OMEGA_N_16.copy()
    dt = 0.05

    print(f"\n=== UPDE 16-layer DD, dt={dt}, {trotter_steps} Trotter steps ===")

    base = _build_evo_base(n, K, omega, dt, trotter_reps=trotter_steps)
    qc_z, qc_x, qc_y = _build_xyz_circuits(base, n)

    # No-DD run
    hw_raw = runner.run_sampler([qc_z, qc_x, qc_y], shots=shots, name="upde16_raw")
    R_raw, R_raw_std, exp_x_raw, exp_y_raw, exp_z_raw, *_ = _R_from_xyz(
        hw_raw[0].counts, hw_raw[1].counts, hw_raw[2].counts, n
    )

    # DD run
    dd_z = runner.transpile_with_dd(qc_z)
    dd_x = runner.transpile_with_dd(qc_x)
    dd_y = runner.transpile_with_dd(qc_y)
    hw_dd = runner.run_sampler([dd_z, dd_x, dd_y], shots=shots, name="upde16_dd")
    R_dd, R_dd_std, exp_x_dd, exp_y_dd, exp_z_dd, *_ = _R_from_xyz(
        hw_dd[0].counts, hw_dd[1].counts, hw_dd[2].counts, n
    )

    classical = classical_exact_evolution(n, dt, dt, K, omega)

    print(f"  R(raw)={R_raw:.4f}, R(DD)={R_dd:.4f}, R(exact)={classical['R'][-1]:.4f}")

    result = {
        "experiment": "upde_16_dd",
        "n_layers": n,
        "dt": dt,
        "trotter_steps": trotter_steps,
        "hw_R_raw": R_raw,
        "hw_R_dd": R_dd,
        "classical_R": float(classical["R"][-1]),
        "hw_exp_x_dd": exp_x_dd.tolist(),
        "hw_exp_y_dd": exp_y_dd.tolist(),
        "hw_exp_z_dd": exp_z_dd.tolist(),
    }
    runner.save_result(hw_raw[0], "upde_16_dd.json")
    return result


def kuramoto_4osc_trotter2_experiment(
    runner, shots: int = 10000, n_time_steps: int = 8, dt: float = 0.1
) -> dict:
    """4-oscillator Kuramoto with second-order Suzuki-Trotter.

    Same structure as kuramoto_4osc_experiment but uses SuzukiTrotter(order=2).
    Produces order-1 vs order-2 comparison data.

    Returns:
        dict with keys:
            experiment (str): Experiment name identifier.
            n_oscillators (int): Number of oscillators.
            trotter_order (int): Suzuki-Trotter order (2).
            dt (float): Time step size.
            hw_times (list[float]): Hardware measurement times.
            hw_R (list[float]): Hardware order parameter per step.
            hw_R_std (list[float]): Shot-noise std of R per step.
            classical_times (list[float]): Exact evolution times.
            classical_R (list[float]): Exact order parameter per step.
            classical_R_std (float): Always 0.0 (exact).
            hw_expectations (list[dict]): Per-step exp_x, exp_y, exp_z.
    """
    n = 4
    K = build_knm_paper27(L=n)
    omega = OMEGA_N_16[:n]

    print(f"\n=== Kuramoto 4-osc Trotter-2, {n_time_steps} steps, dt={dt} ===")

    all_circuits: list[QuantumCircuit] = []
    step_indices: list[int] = []
    for step in range(1, n_time_steps + 1):
        t = step * dt
        base = _build_evo_base(n, K, omega, t, trotter_reps=step * 2, trotter_order=2)
        qc_z, qc_x, qc_y = _build_xyz_circuits(base, n)
        idx = len(all_circuits)
        all_circuits.extend([qc_z, qc_x, qc_y])
        step_indices.append(idx)

    hw_results = runner.run_sampler(all_circuits, shots=shots, name="kuramoto_4osc_trotter2")

    hw_R = []
    hw_R_std = []
    hw_exp = []
    for idx in step_indices:
        R, R_std, ex, ey, ez, *_ = _R_from_xyz(
            hw_results[idx].counts,
            hw_results[idx + 1].counts,
            hw_results[idx + 2].counts,
            n,
        )
        hw_R.append(R)
        hw_R_std.append(R_std)
        hw_exp.append({"exp_x": ex.tolist(), "exp_y": ey.tolist(), "exp_z": ez.tolist()})

    hw_times = [i * dt for i in range(1, n_time_steps + 1)]
    classical = classical_exact_evolution(n, n_time_steps * dt, dt, K, omega)

    return {
        "experiment": "kuramoto_4osc_trotter2",
        "n_oscillators": n,
        "trotter_order": 2,
        "dt": dt,
        "hw_times": hw_times,
        "hw_R": hw_R,
        "hw_R_std": hw_R_std,
        "classical_times": classical["times"].tolist(),
        "classical_R": classical["R"].tolist(),
        "classical_R_std": 0.0,
        "hw_expectations": hw_exp,
    }


def sync_threshold_experiment(
    runner,
    shots: int = 10000,
    k_values: list[float] | None = None,
) -> dict:
    """Kuramoto synchronization phase transition on quantum hardware.

    Sweeps coupling strength K_base and measures R at fixed t=0.1.
    Maps the bifurcation: below K_c, R~0 (incoherent); above K_c,
    R grows (synchronized). K_c depends on frequency spread.

    Science: first measurement of Kuramoto phase transition on
    superconducting qubits. Validates quantum XY <-> classical Kuramoto
    correspondence at the critical point.

    Returns:
        dict with keys:
            experiment (str): Experiment name identifier.
            n_oscillators (int): Number of oscillators.
            dt (float): Time step size.
            k_values (list[float]): Coupling strengths swept.
            results (list[dict]): Per-K dicts with K_base, hw_R, classical_R.
    """
    if k_values is None:
        k_values = [0.05, 0.15, 0.30, 0.45, 0.60, 0.80]

    n = 4
    omega = OMEGA_N_16[:n]
    dt = 0.1

    print(f"\n=== Sync threshold sweep, {len(k_values)} K values ===")

    alpha = 0.3  # Paper 27, Eq. 3

    results_per_k = []
    for k_base in k_values:
        # Pure exponential-decay K (no anchors) for clean bifurcation sweep
        idx = np.arange(n)
        K = k_base * np.exp(-alpha * np.abs(idx[:, None] - idx[None, :]))
        base = _build_evo_base(n, K, omega, dt, trotter_reps=2)
        qc_z, qc_x, qc_y = _build_xyz_circuits(base, n)
        hw = runner.run_sampler([qc_z, qc_x, qc_y], shots=shots, name=f"sync_K{k_base:.2f}")
        R, R_std, ex, ey, ez, *_ = _R_from_xyz(hw[0].counts, hw[1].counts, hw[2].counts, n)

        classical = classical_exact_evolution(n, dt, dt, K, omega)
        cl_R = float(classical["R"][-1])

        results_per_k.append(
            {
                "K_base": k_base,
                "hw_R": R,
                "classical_R": cl_R,
            }
        )
        print(f"  K={k_base:.2f}: hw_R={R:.4f}, exact_R={cl_R:.4f}")

    return {
        "experiment": "sync_threshold",
        "n_oscillators": n,
        "dt": dt,
        "k_values": k_values,
        "results": results_per_k,
    }


def ansatz_comparison_hw_experiment(runner, shots: int = 10000, maxiter: int = 100) -> dict:
    """VQE ansatz comparison on hardware: Knm-informed vs TwoLocal vs EfficientSU2.

    For each ansatz:
    1. COBYLA on Statevector → optimal params
    2. Bind optimal params, evaluate energy on hardware via Estimator
    3. Compare: sim energy, hw energy, exact ground energy, param count

    Science: proves physics-informed (Knm) ansatz advantage is real
    on noisy hardware, not just an artifact of noiseless simulation.

    Returns:
        dict with keys:
            experiment (str): Experiment name identifier.
            n_qubits (int): Number of qubits.
            exact_energy (float): Exact diagonalization ground energy.
            comparison (list[dict]): Per-ansatz dicts with ansatz, n_params,
                sim_energy, hw_energy, exact_energy, sim_gap, hw_gap,
                n_iterations.
    """
    from qiskit.circuit.library import efficient_su2, n_local

    n = 4
    K = build_knm_paper27(L=n)
    omega = OMEGA_N_16[:n]
    H = knm_to_hamiltonian(K, omega)
    exact = classical_exact_diag(n, K, omega)

    print(f"\n=== Ansatz comparison (hw), exact E={exact['ground_energy']:.6f} ===")

    ansatz_builders = {
        "knm_informed": lambda: knm_to_ansatz(K, reps=2),
        "two_local": lambda: n_local(
            n, rotation_blocks=["ry", "rz"], entanglement_blocks="cz", reps=2
        ),
        "efficient_su2": lambda: efficient_su2(n, reps=2),
    }

    comparison = []
    for name, build_fn in ansatz_builders.items():
        ansatz = build_fn()

        def cost_fn(params, _a=ansatz, _H=H):
            bound = _a.assign_parameters(params)
            sv = Statevector.from_instruction(bound)
            return float(sv.expectation_value(_H).real)

        x0 = np.random.default_rng(42).uniform(-np.pi, np.pi, ansatz.num_parameters)
        opt = minimize(cost_fn, x0, method="COBYLA", options={"maxiter": maxiter})
        sim_energy = float(opt.fun)

        bound = ansatz.assign_parameters(opt.x)
        hw_result = runner.run_estimator(bound, [H], name=f"ansatz_{name}")
        hw_energy = float(hw_result.expectation_values[0])

        entry = {
            "ansatz": name,
            "n_params": ansatz.num_parameters,
            "sim_energy": sim_energy,
            "hw_energy": hw_energy,
            "exact_energy": exact["ground_energy"],
            "sim_gap": sim_energy - exact["ground_energy"],
            "hw_gap": hw_energy - exact["ground_energy"],
            "n_iterations": opt.nfev,
        }
        comparison.append(entry)
        print(f"  {name}: sim={sim_energy:.4f}, hw={hw_energy:.4f}, gap={entry['hw_gap']:.4f}")

    return {
        "experiment": "ansatz_comparison_hw",
        "n_qubits": n,
        "exact_energy": exact["ground_energy"],
        "comparison": comparison,
    }


def zne_higher_order_experiment(
    runner,
    shots: int = 10000,
    dt: float = 0.1,
    scales: list[int] | None = None,
    poly_order: int = 2,
) -> dict:
    """ZNE with extended noise scales and higher-order polynomial extrapolation.

    Default: scales=[1,3,5,7,9], quadratic fit. Tests whether 5-point
    polynomial extrapolation recovers more signal than the 3-point linear
    version (kuramoto_4osc_zne).

    Science: systematic ZNE study -- linear vs quadratic vs cubic on the
    same data. Determines optimal extrapolation order for XY evolution on
    Heron r2.

    Returns:
        dict with keys:
            experiment (str): Experiment name identifier.
            scales (list[int]): Noise scale factors used.
            R_per_scale (list[float]): Measured R at each scale.
            extrapolations (dict): Per-order dicts with zne_R, fit_residual.
            classical_R (float): Exact order parameter.
    """
    from ..mitigation.zne import gate_fold_circuit, zne_extrapolate

    if scales is None:
        scales = [1, 3, 5, 7, 9]

    n = 4
    K = build_knm_paper27(L=n)
    omega = OMEGA_N_16[:n]
    t = dt

    print(f"\n=== ZNE higher-order, scales={scales}, poly_order={poly_order} ===")

    base = _build_evo_base(n, K, omega, t, trotter_reps=2)

    R_per_scale = []
    R_std_per_scale = []
    for s in scales:
        folded = gate_fold_circuit(base, s)
        qc_z, qc_x, qc_y = _build_xyz_circuits(folded, n)
        hw = runner.run_sampler([qc_z, qc_x, qc_y], shots=shots, name=f"zne_ho_s{s}")
        R, R_std, *_ = _R_from_xyz(hw[0].counts, hw[1].counts, hw[2].counts, n)
        R_per_scale.append(R)
        R_std_per_scale.append(R_std)
        print(f"  scale={s}: R={R:.4f} ± {R_std:.4f}")

    zne_results = {}
    for order in range(1, poly_order + 1):
        zne = zne_extrapolate(scales, R_per_scale, order=order)
        zne_results[f"order_{order}"] = {
            "zne_R": zne.zero_noise_estimate,
            "fit_residual": zne.fit_residual,
        }
        print(
            f"  order-{order} ZNE R(0) = {zne.zero_noise_estimate:.4f} (residual={zne.fit_residual:.4f})"
        )

    classical = classical_exact_evolution(n, dt, dt, K, omega)

    return {
        "experiment": "zne_higher_order",
        "scales": scales,
        "R_per_scale": R_per_scale,
        "R_std_per_scale": R_std_per_scale,
        "extrapolations": zne_results,
        "classical_R": float(classical["R"][-1]),
    }


def decoherence_scaling_experiment(
    runner,
    shots: int = 10000,
    qubit_counts: list[int] | None = None,
) -> dict:
    """Systematic decoherence scaling: R vs circuit depth across qubit counts.

    Runs 1-Trotter-step evolution at fixed dt=0.1 for each qubit count,
    records depth, R, and exact R. Provides data for fitting
    R_hw = R_exact * exp(-gamma * depth).

    Science: extracts per-gate depolarization rate gamma from a single
    calibration run. Enables predictive modeling of experiment fidelity.

    Returns:
        dict with keys:
            experiment (str): Experiment name identifier.
            dt (float): Time step size.
            data_points (list[dict]): Per-qubit-count dicts with n_qubits,
                depth, hw_R, classical_R.
            fit_gamma (float): Fitted per-gate depolarization rate.
            fit_r_squared (float): R-squared of exponential fit.
    """
    if qubit_counts is None:
        qubit_counts = [2, 4, 6, 8, 10, 12]

    dt = 0.1

    print(f"\n=== Decoherence scaling, qubits={qubit_counts} ===")

    data_points = []
    for n in qubit_counts:
        K = build_knm_paper27(L=n)
        omega = OMEGA_N_16[:n]
        base = _build_evo_base(n, K, omega, dt, trotter_reps=1)
        qc_z, qc_x, qc_y = _build_xyz_circuits(base, n)
        hw = runner.run_sampler([qc_z, qc_x, qc_y], shots=shots, name=f"decoherence_{n}q")
        R, R_std, *_ = _R_from_xyz(hw[0].counts, hw[1].counts, hw[2].counts, n)

        isa = runner.transpile(base)
        depth = isa.depth()

        classical = classical_exact_evolution(n, dt, dt, K, omega)
        cl_R = float(classical["R"][-1])

        data_points.append(
            {
                "n_qubits": n,
                "depth": depth,
                "hw_R": R,
                "classical_R": cl_R,
            }
        )
        print(f"  {n}q: depth={depth}, hw_R={R:.4f}, exact_R={cl_R:.4f}")

    # Fit exponential decay: R_hw = R_exact * exp(-gamma * depth)
    depths = np.array([d["depth"] for d in data_points], dtype=float)
    ratios = np.array([d["hw_R"] / max(d["classical_R"], 1e-10) for d in data_points])
    valid = ratios > 0
    if np.sum(valid) >= 2:
        log_ratios = np.log(np.clip(ratios[valid], 1e-10, None))
        coeffs = np.polyfit(depths[valid], log_ratios, 1)
        gamma = -coeffs[0]
        r_squared = 1.0 - np.var(log_ratios - np.polyval(coeffs, depths[valid])) / max(
            np.var(log_ratios), 1e-10
        )
    else:
        gamma = float("nan")
        r_squared = float("nan")

    print(f"  Fit: gamma={gamma:.6f} per gate, R²={r_squared:.4f}")

    return {
        "experiment": "decoherence_scaling",
        "dt": dt,
        "data_points": data_points,
        "fit_gamma": gamma,
        "fit_r_squared": r_squared,
    }


def vqe_landscape_experiment(runner, shots: int = 10000, n_samples: int = 50) -> dict:
    """Sample VQE cost landscape to detect barren plateaus.

    Evaluates the 4-qubit Hamiltonian at random parameter vectors and
    computes the variance of the energy. Low variance = barren plateau
    (exponentially flat landscape). Compares Knm-informed vs TwoLocal.

    Science: barren plateaus are the #1 obstacle to scaling VQE.
    Showing Knm-informed ansatz avoids them (higher variance) is a
    publishable result on its own.

    Reference: McClean et al., "Barren plateaus in quantum neural network
    training landscapes", Nature Comm. 9, 4812 (2018).

    Returns:
        dict with keys:
            experiment (str): Experiment name identifier.
            n_qubits (int): Number of qubits.
            n_samples (int): Random parameter vectors sampled.
            exact_ground_energy (float): Exact diagonalization ground energy.
            landscapes (dict): Per-ansatz dicts with n_params, mean_energy,
                std_energy, min_energy, max_energy.
    """
    from qiskit.circuit.library import n_local

    n = 4
    K = build_knm_paper27(L=n)
    omega = OMEGA_N_16[:n]
    H = knm_to_hamiltonian(K, omega)
    rng = np.random.default_rng(42)

    print(f"\n=== VQE landscape, {n_samples} samples ===")

    results = {}
    for name, ansatz in [
        ("knm_informed", knm_to_ansatz(K, reps=2)),
        ("two_local", n_local(n, rotation_blocks=["ry", "rz"], entanglement_blocks="cz", reps=2)),
    ]:
        energies = []
        for _ in range(n_samples):
            params = rng.uniform(-np.pi, np.pi, ansatz.num_parameters)
            bound = ansatz.assign_parameters(params)
            sv = Statevector.from_instruction(bound)
            e = float(sv.expectation_value(H).real)
            energies.append(e)

        energies_arr = np.array(energies)
        results[name] = {
            "n_params": ansatz.num_parameters,
            "mean_energy": float(energies_arr.mean()),
            "std_energy": float(energies_arr.std()),
            "min_energy": float(energies_arr.min()),
            "max_energy": float(energies_arr.max()),
        }
        print(f"  {name}: mean={energies_arr.mean():.4f}, std={energies_arr.std():.4f}")

    exact = classical_exact_diag(n, K, omega)

    return {
        "experiment": "vqe_landscape",
        "n_qubits": n,
        "n_samples": n_samples,
        "exact_ground_energy": exact["ground_energy"],
        "landscapes": results,
    }


def _correlator_from_counts(counts: dict, qubit_a: int, qubit_b: int) -> float:
    """Compute <A B> from 2-qubit marginal of multi-qubit counts.

    E(A,B) = (N_same - N_diff) / N_total where same/diff refers to
    the measurement outcomes of qubits a and b.
    """
    n_same = 0
    n_diff = 0
    for bitstring, count in counts.items():
        bits = bitstring.replace(" ", "")
        bit_a = int(bits[-(qubit_a + 1)])
        bit_b = int(bits[-(qubit_b + 1)])
        if bit_a == bit_b:
            n_same += count
        else:
            n_diff += count
    total = n_same + n_diff
    if total == 0:
        return 0.0
    return (n_same - n_diff) / total


def bell_test_4q_experiment(runner, shots: int = 10000, maxiter: int = 100) -> dict:
    """CHSH Bell test on 4-qubit K_nm ground state.

    Certifies entanglement between qubits 0 and 1 via CHSH inequality
    violation. S > 2 proves non-classical correlations on hardware.
    ~20s QPU budget (4 circuits x ~5s each).

    Returns:
        dict with keys:
            experiment (str): Experiment name identifier.
            S_hw (float): CHSH S value from hardware.
            S_sim (float): CHSH S value from simulation.
            violates_classical_hw (bool): S_hw > 2.0.
            violates_classical_sim (bool): S_sim > 2.0.
            correlators_hw (dict): ZZ, ZX, XZ, XX correlators (hardware).
            correlators_sim (dict): Correlators from simulation.
    """
    n = 4
    K = build_knm_paper27(L=n)
    omega = OMEGA_N_16[:n]

    print(f"\n=== Bell test 4q, maxiter={maxiter} ===")

    key_state = prepare_key_state(K, omega, ansatz_reps=2, maxiter=maxiter)
    base_circuit = key_state["circuit"]
    sv = key_state["statevector"]

    # 4 measurement circuits: ZZ, ZX, XZ, XX on qubits 0,1
    qc_zz = base_circuit.copy()
    qc_zz.measure_all()

    qc_zx = base_circuit.copy()
    qc_zx.h(1)
    qc_zx.measure_all()

    qc_xz = base_circuit.copy()
    qc_xz.h(0)
    qc_xz.measure_all()

    qc_xx = base_circuit.copy()
    qc_xx.h(0)
    qc_xx.h(1)
    qc_xx.measure_all()

    hw_results = runner.run_sampler([qc_zz, qc_zx, qc_xz, qc_xx], shots=shots, name="bell_test_4q")

    e_zz = _correlator_from_counts(hw_results[0].counts, 0, 1)
    e_zx = _correlator_from_counts(hw_results[1].counts, 0, 1)
    e_xz = _correlator_from_counts(hw_results[2].counts, 0, 1)
    e_xx = _correlator_from_counts(hw_results[3].counts, 0, 1)

    s_hw = abs(e_zz - e_zx + e_xz + e_xx)

    sim_bell = bell_inequality_test(sv, 0, 1, n)
    s_sim = sim_bell["S"]

    print(f"  S_hw={s_hw:.4f}, S_sim={s_sim:.4f}")

    return {
        "experiment": "bell_test_4q",
        "S_hw": s_hw,
        "S_sim": s_sim,
        "violates_classical_hw": s_hw > 2.0,
        "violates_classical_sim": sim_bell["violates_classical"],
        "correlators_hw": {"ZZ": e_zz, "ZX": e_zx, "XZ": e_xz, "XX": e_xx},
        "correlators_sim": sim_bell["correlators"],
    }


def correlator_4q_experiment(runner, shots: int = 10000, maxiter: int = 100) -> dict:
    """ZZ cross-correlation of 4-qubit K_nm ground state on hardware.

    Validates that the K_ij coupling topology maps to measurable quantum
    correlations. Connected correlation C[i,j] = <Z_i Z_j> - <Z_i><Z_j>.
    ~25s QPU budget (1 circuit).

    Returns:
        dict with keys:
            experiment (str): Experiment name identifier.
            corr_hw (list[list[float]]): 4x4 connected correlation matrix (hw).
            corr_sim (list[list[float]]): 4x4 connected correlation matrix (sim).
            frobenius_error (float): ||corr_hw - corr_sim||_F.
            max_correlation_hw (float): Max absolute correlation on hardware.
    """
    n = 4
    K = build_knm_paper27(L=n)
    omega = OMEGA_N_16[:n]

    print(f"\n=== Correlator 4q, maxiter={maxiter} ===")

    key_state = prepare_key_state(K, omega, ansatz_reps=2, maxiter=maxiter)
    base_circuit = key_state["circuit"]
    sv = key_state["statevector"]

    qc_z = base_circuit.copy()
    qc_z.measure_all()
    hw_results = runner.run_sampler(qc_z, shots=shots, name="correlator_4q")
    counts = hw_results[0].counts

    # Per-qubit <Z_i> from counts
    exp_z, _ = _expectation_per_qubit(counts, n)

    # <Z_i Z_j> for all pairs
    corr_hw = np.zeros((n, n))
    for i in range(n):
        for j in range(n):
            if i == j:
                corr_hw[i, j] = 1.0 - exp_z[i] ** 2
            else:
                zz = _correlator_from_counts(counts, i, j)
                corr_hw[i, j] = zz - exp_z[i] * exp_z[j]

    # Simulator reference: correlator_matrix gives alice×bob block
    corr_sim_block = correlator_matrix(sv, [0, 1], [2, 3])
    corr_sim = np.zeros((n, n))
    corr_sim[:2, 2:] = corr_sim_block
    corr_sim[2:, :2] = corr_sim_block.T

    frob_error = float(np.linalg.norm(corr_hw - corr_sim))
    max_corr = float(np.max(np.abs(corr_hw)))

    print(f"  frobenius_error={frob_error:.4f}, max_correlation={max_corr:.4f}")

    return {
        "experiment": "correlator_4q",
        "corr_hw": corr_hw.tolist(),
        "corr_sim": corr_sim.tolist(),
        "frobenius_error": frob_error,
        "max_correlation_hw": max_corr,
    }


def qkd_qber_4q_experiment(runner, shots: int = 10000, maxiter: int = 100) -> dict:
    """QBER measurement from hardware for BB84-family security validation.

    Measures in Z and X bases, extracts Alice (qubits 0,1) and Bob (qubits 2,3)
    raw keys, computes QBER. Secure if QBER < 0.11 (BB84 threshold).
    ~15s QPU budget (2 circuits).

    Returns:
        dict with keys:
            experiment (str): Experiment name identifier.
            qber_z_hw (float): Z-basis QBER from hardware.
            qber_x_hw (float): X-basis QBER from hardware.
            qber_sim (float): QBER from simulator.
            secure_hw (bool): Both QBERs < 0.11.
            secure_sim (bool): Simulator QBER < 0.11.
            key_rate_hw (float): Devetak-Winter secret key rate.
    """
    n = 4
    K = build_knm_paper27(L=n)
    omega = OMEGA_N_16[:n]

    print(f"\n=== QKD QBER 4q, maxiter={maxiter} ===")

    key_state = prepare_key_state(K, omega, ansatz_reps=2, maxiter=maxiter)
    base_circuit = key_state["circuit"]

    # Z-basis and X-basis measurement circuits
    qc_z = base_circuit.copy()
    qc_z.measure_all()

    qc_x = base_circuit.copy()
    for q in range(n):
        qc_x.h(q)
    qc_x.measure_all()

    hw_results = runner.run_sampler([qc_z, qc_x], shots=shots, name="qkd_qber_4q")

    alice_z = extract_raw_key(hw_results[0].counts, "Z", [0, 1])
    bob_z = extract_raw_key(hw_results[0].counts, "Z", [2, 3])
    alice_x = extract_raw_key(hw_results[1].counts, "X", [0, 1])
    bob_x = extract_raw_key(hw_results[1].counts, "X", [2, 3])

    qber_z_hw = estimate_qber(alice_z, bob_z)
    qber_x_hw = estimate_qber(alice_x, bob_x)

    # Simulator reference
    sim_result = scpn_qkd_protocol(K, omega, [0, 1], [2, 3], shots=shots)
    qber_sim = sim_result["qber"]

    rate_z = devetak_winter_rate(qber_z_hw)
    rate_x = devetak_winter_rate(qber_x_hw)

    print(f"  QBER_Z={qber_z_hw:.4f}, QBER_X={qber_x_hw:.4f}, QBER_sim={qber_sim:.4f}")
    print(f"  key_rate_z={rate_z:.4f}, key_rate_x={rate_x:.4f}")

    return {
        "experiment": "qkd_qber_4q",
        "qber_z_hw": qber_z_hw,
        "qber_x_hw": qber_x_hw,
        "qber_sim": qber_sim,
        "secure_hw": qber_z_hw < 0.11 and qber_x_hw < 0.11,
        "secure_sim": qber_sim < 0.11,
        "key_rate_hw": max(rate_z, rate_x),
    }


ALL_EXPERIMENTS = {
    "kuramoto_4osc": kuramoto_4osc_experiment,
    "kuramoto_8osc": kuramoto_8osc_experiment,
    "vqe_4q": vqe_4q_experiment,
    "vqe_8q": vqe_8q_experiment,
    "qaoa_mpc_4": qaoa_mpc_4_experiment,
    "upde_16_snapshot": upde_16_snapshot_experiment,
    "kuramoto_4osc_zne": kuramoto_4osc_zne_experiment,
    "noise_baseline": noise_baseline_experiment,
    "kuramoto_8osc_zne": kuramoto_8osc_zne_experiment,
    "vqe_8q_hardware": vqe_8q_hardware_experiment,
    "upde_16_dd": upde_16_dd_experiment,
    "kuramoto_4osc_trotter2": kuramoto_4osc_trotter2_experiment,
    "sync_threshold": sync_threshold_experiment,
    "ansatz_comparison_hw": ansatz_comparison_hw_experiment,
    "zne_higher_order": zne_higher_order_experiment,
    "decoherence_scaling": decoherence_scaling_experiment,
    "vqe_landscape": vqe_landscape_experiment,
    "bell_test_4q": bell_test_4q_experiment,
    "correlator_4q": correlator_4q_experiment,
    "qkd_qber_4q": qkd_qber_4q_experiment,
}
