#!/usr/bin/env python3
"""Precompute mpmath high-precision O3 vectors for engineering.uncertainty.propagate.

Independent of Node/V8 binary64 and of the IUT. Linear GUM
u_c = √(Σ (cᵢ uᵢ)² + 2 Σ ρᵢⱼ cᵢ cⱼ uᵢ uⱼ) at 80 dps; round-to-nearest binary64.
Does not pin a gmpy2 backend. Monte Carlo is not tabulated here.

Re-run: python3 scripts/lib/cvp/oracles/generate-uncertainty-propagate-o3.py
Public copies: /developers/cvp/reproduce/generate-uncertainty-propagate-o3.py
and uncertainty-propagate-o3-tables.json
"""
from __future__ import annotations

import json
import math
import shutil
from pathlib import Path

import mpmath as mp

DPS = 80
GENERATOR_ID = "uncertainty-propagate-mpmath-o3"
GENERATOR_VERSION = "1.0.0"
SEED = "20260916.uncertainty-o3"


def f64(x: mp.mpf) -> float:
    return float(x)


def combine(cs: list, us: list, rho_pairs: list | None = None) -> mp.mpf:
    s = mp.mpf(0)
    for c, u in zip(cs, us):
        s += (mp.mpf(c) * mp.mpf(u)) ** 2
    for i, j, rho in rho_pairs or []:
        s += 2 * mp.mpf(rho) * mp.mpf(cs[i]) * mp.mpf(cs[j]) * mp.mpf(us[i]) * mp.mpf(us[j])
    if s < 0 and s > -mp.mpf("1e-40"):
        s = mp.mpf(0)
    return mp.sqrt(s)


def row(
    vid: str,
    inputs: dict,
    *,
    kind: str,
    y: mp.mpf | None,
    u_c: mp.mpf,
) -> dict:
    out = {
        "id": vid,
        "kind": kind,
        "inputs": inputs,
        "u_c_f64": f64(u_c),
        "u_c_decimal": mp.nstr(u_c, 40, strip_zeros=False),
    }
    if y is not None:
        out["y_f64"] = f64(y)
        out["y_decimal"] = mp.nstr(y, 40, strip_zeros=False)
    return out


def vectors() -> list[dict]:
    a, ua, b, ub = mp.mpf(100), mp.mpf("0.1"), mp.mpf(50), mp.mpf("0.2")
    out = []
    out.append(
        row(
            "o3-sum",
            {"mode": "sum", "a": 100, "ua": 0.1, "b": 50, "ub": 0.2},
            kind="binary-sum",
            y=a + b,
            u_c=combine([1, 1], [ua, ub]),
        )
    )
    out.append(
        row(
            "o3-difference",
            {"mode": "difference", "a": 100, "ua": 0.1, "b": 50, "ub": 0.2},
            kind="binary-difference",
            y=a - b,
            u_c=combine([1, -1], [ua, ub]),
        )
    )
    out.append(
        row(
            "o3-difference-rho1",
            {"mode": "difference", "a": 100, "ua": 0.1, "b": 50, "ub": 0.2, "rho": 1},
            kind="binary-difference-rho1",
            y=a - b,
            u_c=combine([1, -1], [ua, ub], [(0, 1, 1)]),
        )
    )
    pa, pua, pb, pub = mp.mpf(10), mp.mpf("0.1"), mp.mpf(5), mp.mpf("0.05")
    out.append(
        row(
            "o3-product",
            {"mode": "product", "a": 10, "ua": 0.1, "b": 5, "ub": 0.05},
            kind="binary-product",
            y=pa * pb,
            u_c=combine([pb, pa], [pua, pub]),
        )
    )
    out.append(
        row(
            "o3-quotient",
            {"mode": "quotient", "a": 10, "ua": 0.1, "b": 5, "ub": 0.05},
            kind="binary-quotient",
            y=pa / pb,
            u_c=combine([1 / pb, -pa / (pb * pb)], [pua, pub]),
        )
    )
    u0, u1 = mp.mpf("0.3"), mp.mpf("0.4")
    out.append(
        row(
            "o3-rss",
            {"mode": "rss", "uncertainties": [0.3, 0.4]},
            kind="rss",
            y=None,
            u_c=combine([1, 1], [u0, u1]),
        )
    )
    out.append(
        row(
            "o3-sens",
            {"mode": "sensitivity", "coefficients": [2, 3], "uncertainties": [0.1, 0.2], "y": 10},
            kind="sensitivity",
            y=mp.mpf(10),
            u_c=combine([2, 3], [mp.mpf("0.1"), mp.mpf("0.2")]),
        )
    )
    cs = [mp.mpf(1), mp.mpf(1), mp.mpf(1)]
    us = [mp.mpf("0.1"), mp.mpf("0.1"), mp.mpf("0.1")]
    out.append(
        row(
            "o3-sens-psd-3x3",
            {
                "mode": "sensitivity",
                "coefficients": [1, 1, 1],
                "uncertainties": [0.1, 0.1, 0.1],
                "correlations": [
                    {"i": 0, "j": 1, "rho": 0.3},
                    {"i": 0, "j": 2, "rho": 0.2},
                    {"i": 1, "j": 2, "rho": 0.1},
                ],
            },
            kind="sensitivity-psd",
            y=None,
            u_c=combine(cs, us, [(0, 1, "0.3"), (0, 2, "0.2"), (1, 2, "0.1")]),
        )
    )
    out.append(
        row(
            "o3-sens-singular-psd",
            {
                "mode": "sensitivity",
                "coefficients": [1, 1, 1],
                "uncertainties": [0.1, 0.1, 0.1],
                "correlations": [
                    {"i": 0, "j": 1, "rho": 1},
                    {"i": 0, "j": 2, "rho": 1},
                    {"i": 1, "j": 2, "rho": 1},
                ],
            },
            kind="sensitivity-singular-psd",
            y=None,
            u_c=combine(cs, us, [(0, 1, 1), (0, 2, 1), (1, 2, 1)]),
        )
    )
    c0, u_v, c1, u_d = mp.mpf(1), mp.mpf("0.002"), mp.mpf(1), mp.mpf("0.001")
    x0, x1 = mp.mpf(5), mp.mpf(0)
    out.append(
        row(
            "o3-budget",
            {
                "mode": "budget",
                "components": [
                    {"name": "Vref", "x": 5, "u": 0.002, "c": 1},
                    {"name": "drift", "x": 0, "u": 0.001, "c": 1},
                ],
            },
            kind="budget",
            y=c0 * x0 + c1 * x1,
            u_c=combine([c0, c1], [u_v, u_d]),
        )
    )
    return out


def main() -> None:
    mp.mp.dps = DPS
    here = Path(__file__).resolve().parent
    table = {
        "family": "uncertainty_propagate",
        "generator_id": GENERATOR_ID,
        "generator_version": GENERATOR_VERSION,
        "seed": SEED,
        "mpmath_dps": DPS,
        "precision_bits": int(DPS * math.log2(10)),
        "library": f"mpmath {mp.__version__}",
        "notes": "Linear GUM u_c from high-precision hypot/RSS; independent of Node Math and the IUT. Not Monte Carlo.",
        "vectors": vectors(),
    }
    dest = here / "uncertainty-propagate-o3-tables.json"
    dest.write_text(json.dumps(table, indent=2) + "\n", encoding="utf-8")
    public = here.parents[3] / "public/developers/cvp/reproduce"
    public.mkdir(parents=True, exist_ok=True)
    shutil.copy2(dest, public / "uncertainty-propagate-o3-tables.json")
    shutil.copy2(Path(__file__), public / "generate-uncertainty-propagate-o3.py")
    print(f"wrote {dest} ({len(table['vectors'])} vectors)")


if __name__ == "__main__":
    main()
