#!/usr/bin/env julia
#
# reach_scram_full_fat.jl — FULL nonlinear PKE scram reach (no PJ).
#
# Companion to reach_scram_pj_fat.jl which uses the prompt-jump-reduced
# 9-state model.  This version keeps n as a state and uses the full
# 10-state PKE — captures the prompt jump dynamics directly (rather
# than via the algebraic n substitution).
#
# Dane wanted the "real" prompt jump visible in the tubes, even at the
# cost of stiffness-driven slowness in TMJets.  The penalty: each
# integration step is bounded by Λ ≈ 10⁻⁴ s.  60 s plant horizon means
# very many steps and a real risk of over-approximation slack growth.
#
# State (11D with augmented time):
#   x[1]    = n
#   x[2..7] = C_1..C_6
#   x[8]    = T_f
#   x[9]    = T_c
#   x[10]   = T_cold
#   x[11]   = t (augmented time)
#
# Constant control u = -8β (rod-fall scram).
# Q_sg = 3% P0 (decay-heat-level sink).
#
# Saves per-step envelopes for T_c, T_f, ρ, n at every probe horizon.

using Pkg
Pkg.activate(joinpath(@__DIR__, "..", ".."))

using LinearAlgebra
using ReachabilityAnalysis, LazySets
using JSON
using MAT
using Printf

# Plant constants — must match pke_params.
const LAMBDA  = 1e-4
const BETA_1, BETA_2, BETA_3, BETA_4, BETA_5, BETA_6 =
    0.000215, 0.001424, 0.001274, 0.002568, 0.000748, 0.000273
const BETA    = BETA_1 + BETA_2 + BETA_3 + BETA_4 + BETA_5 + BETA_6
const LAM_1, LAM_2, LAM_3, LAM_4, LAM_5, LAM_6 =
    0.0124, 0.0305, 0.111, 0.301, 1.14, 3.01

const P0   = 1e9
const M_F, C_F, M_C, C_C, HA, W_M, M_SG =
    50000.0, 300.0, 20000.0, 5450.0, 5e7, 5000.0, 30000.0
const ALPHA_F, ALPHA_C = -2.5e-5, -1e-4

const T_COLD0 = 290.0
const DT_CORE = P0 / (W_M * C_C)
const T_HOT0  = T_COLD0 + DT_CORE
const T_C0    = (T_HOT0 + T_COLD0) / 2
const T_F0    = T_C0 + P0 / HA

const U_SCRAM    = -8 * BETA
const Q_SG_DECAY = 0.03 * P0

const RHO_SDM = 0.01
const SDM_RHS = -RHO_SDM - U_SCRAM + ALPHA_F*T_F0 + ALPHA_C*T_C0

# Full-PKE Taylorized scram RHS (11 states with augmented t).
@taylorize function rhs_scram_full_taylor!(dx, x, p, t)
    rho = U_SCRAM + ALPHA_F * (x[8] - T_F0) + ALPHA_C * (x[9] - T_C0)
    sum_lam_C = LAM_1*x[2] + LAM_2*x[3] + LAM_3*x[4] +
                LAM_4*x[5] + LAM_5*x[6] + LAM_6*x[7]

    # n equation — full PKE (the source of stiffness via 1/Λ ≈ 10^4)
    dx[1] = ((rho - BETA) / LAMBDA) * x[1] + sum_lam_C

    # Precursor balance equations
    dx[2] = (BETA_1 / LAMBDA) * x[1] - LAM_1 * x[2]
    dx[3] = (BETA_2 / LAMBDA) * x[1] - LAM_2 * x[3]
    dx[4] = (BETA_3 / LAMBDA) * x[1] - LAM_3 * x[4]
    dx[5] = (BETA_4 / LAMBDA) * x[1] - LAM_4 * x[5]
    dx[6] = (BETA_5 / LAMBDA) * x[1] - LAM_5 * x[6]
    dx[7] = (BETA_6 / LAMBDA) * x[1] - LAM_6 * x[7]

    # Thermal-hydraulics
    dx[8]  = (P0 * x[1] - HA * (x[8] - x[9])) / (M_F * C_F)
    dx[9]  = (HA * (x[8] - x[9]) - 2 * W_M * C_C * (x[9] - x[10])) / (M_C * C_C)
    dx[10] = (2 * W_M * C_C * (x[9] - x[10]) - Q_SG_DECAY) / (M_SG * C_C)

    dx[11] = one(x[1])
    return nothing
end

# --- Build fat X_entry from union of reach envelopes (11D with n) ---
results_dir = joinpath(@__DIR__, "..", "..", "..", "results")
pred_raw = JSON.parsefile(joinpath(@__DIR__, "..", "..", "..", "reachability", "predicates.json"))

# Helper: compute equilibrium C_i for a given n.
C_eq(n) = [BETA_1/(LAM_1*LAMBDA)*n, BETA_2/(LAM_2*LAMBDA)*n,
           BETA_3/(LAM_3*LAMBDA)*n, BETA_4/(LAM_4*LAMBDA)*n,
           BETA_5/(LAM_5*LAMBDA)*n, BETA_6/(LAM_6*LAMBDA)*n]

# 1. Hot-standby (n ≈ 1e-7..1e-4, T near 275 °C).
sh = pred_raw["mode_boundaries"]["q_shutdown"]["X_entry_polytope"]
hs_n_lo, hs_n_hi   = sh["n_range"]
shutdown_lo = [hs_n_lo; C_eq(hs_n_lo); sh["T_f_range_C"][1];
               sh["T_c_range_C"][1]; sh["T_cold_range_C"][1]]
shutdown_hi = [hs_n_hi; C_eq(hs_n_hi); sh["T_f_range_C"][2];
               sh["T_c_range_C"][2]; sh["T_cold_range_C"][2]]

# 2. Operation (n near 1.0, full-power thermals).
op_path = joinpath(results_dir, "reach_operation_result.mat")
operation_lo = nothing; operation_hi = nothing
if isfile(op_path)
    o = matread(op_path)
    X_lo_op = o["X_lo"]; X_hi_op = o["X_hi"]   # 10 × M (n at row 1)
    operation_lo = [minimum(X_lo_op[i, :]) for i in 1:10]
    operation_hi = [maximum(X_hi_op[i, :]) for i in 1:10]
end

# X_entry construction note:
#   We tried widening to a single hyperrectangle covering shutdown
#   THROUGH high-flux pre-trip (n=1.15, T_c=320, T_f=348).  TMJets
#   refused — single hyperrectangle that contains both (n=1.15, T_c=320)
#   AND (n=1e-7, T_c=270) also contains physically-impossible
#   combinations like (n=1.15, T_c=270).  Over-approximation slack at
#   t=0 was so wide TMJets failed at the first step.
#
#   Right fix: SET SPLITTING — split entry into multiple narrower boxes,
#   run reach on each separately, take union of tubes.  Multi-hour
#   build; flagged for follow-up.
#
#   For now: union shutdown + "operation transient" box (which itself
#   spans n=0.85..1.15, the load-follow + high-flux range).  This gives
#   the widest entry that TMJets can actually solve.  ~7 orders of
#   magnitude on n; ~50 °C on temperatures.

# Operation-transient box (covers steady + load-follow + pre-trip,
# physically consistent — high n correlates with high T_f).
trans_n_lo, trans_n_hi   = 0.85, 1.15
trans_Tf_lo, trans_Tf_hi = 320.0, 332.0   # ~T_c0 + P/hA at the relevant n
trans_Tc_lo, trans_Tc_hi = 305.0, 311.0   # tight around T_c0
trans_Tco_lo, trans_Tco_hi = 285.0, 295.0
trans_lo = [trans_n_lo; C_eq(trans_n_lo); trans_Tf_lo; trans_Tc_lo; trans_Tco_lo]
trans_hi = [trans_n_hi; C_eq(trans_n_hi); trans_Tf_hi; trans_Tc_hi; trans_Tco_hi]

# Union: shutdown + transients box.
fat_lo = copy(shutdown_lo); fat_hi = copy(shutdown_hi)
for src_pair in [(trans_lo, trans_hi)]
    lo, hi = src_pair
    for i in 1:10
        fat_lo[i] = min(fat_lo[i], lo[i])
        fat_hi[i] = max(fat_hi[i], hi[i])
    end
end

# Defensive clamping (n must be > 0; precursors must be in physical range).
fat_lo[1] = max(fat_lo[1], 1e-9)
C_max = [BETA_i/(LAM_i*LAMBDA) * 1.3 for (BETA_i, LAM_i) in
         zip([BETA_1,BETA_2,BETA_3,BETA_4,BETA_5,BETA_6],
             [LAM_1,LAM_2,LAM_3,LAM_4,LAM_5,LAM_6])]
for i in 1:6
    fat_hi[1+i] = min(fat_hi[1+i], C_max[i])
    fat_lo[1+i] = max(fat_lo[1+i], 0.0)
end

# Build the 11D X0 (add augmented time = 0).
x_lo = vcat(fat_lo, 0.0)
x_hi = vcat(fat_hi, 0.0)
X0 = Hyperrectangle(low=x_lo, high=x_hi)

println("\n=== Fat-entry FULL-PKE scram reach (no PJ) ===")
println("  X_entry (n):       [", round(fat_lo[1]; sigdigits=3), ", ",
        round(fat_hi[1]; sigdigits=3), "]")
println("  X_entry (T_f, °C): [", round(fat_lo[8]; digits=2), ", ",
        round(fat_hi[8]; digits=2), "]")
println("  X_entry (T_c, °C): [", round(fat_lo[9]; digits=2), ", ",
        round(fat_hi[9]; digits=2), "]")
println("  Constant u = -8β = ", round(U_SCRAM; digits=4))
println("  Probe horizons: 10 s only (full-PKE stiffness wall + slack growth)")

results = Dict{Float64, Any}()
for T_probe in (10.0,)
    println("\n--- Probe T = $T_probe s ---")
    sys  = BlackBoxContinuousSystem(rhs_scram_full_taylor!, 11)
    prob = InitialValueProblem(sys, X0)
    try
        alg = TMJets(orderT=4, orderQ=2, abstol=1e-9, maxsteps=2_000_000)
        t_start = time()
        sol = solve(prob; T=T_probe, alg=alg)
        elapsed = time() - t_start
        flow = flowpipe(sol)
        n_sets = length(flow)
        println("  TMJets: $n_sets reach-sets in $(round(elapsed; digits=1)) s wall")

        # Per-step overapproximation can produce NaN radii deep into the
        # run when slack growth blows out (full-PKE scram is stiff and
        # this is expected after a few seconds of plant time).  Wrap
        # each step in try/catch so we save the longest contiguous
        # NaN-free prefix.
        n_steps_full = length(flow)
        t_arr_buf     = Float64[]
        n_lo_buf      = Float64[]; n_hi_buf  = Float64[]
        rho_lo_buf    = Float64[]; rho_hi_buf = Float64[]
        Tc_lo_buf     = Float64[]; Tc_hi_buf = Float64[]
        Tf_lo_buf     = Float64[]; Tf_hi_buf = Float64[]
        for R in flow
            try
                s = set(overapproximate(R, Hyperrectangle))
                # Skip if any extracted radius is NaN.
                tval = high(s, 11)
                nlo  = low(s, 1);  nhi  = high(s, 1)
                Tflo = low(s, 8);  Tfhi = high(s, 8)
                Tclo = low(s, 9);  Tchi = high(s, 9)
                if any(isnan, (tval, nlo, nhi, Tflo, Tfhi, Tclo, Tchi))
                    break
                end
                push!(t_arr_buf,  tval)
                push!(n_lo_buf,   nlo);  push!(n_hi_buf,   nhi)
                push!(Tf_lo_buf,  Tflo); push!(Tf_hi_buf,  Tfhi)
                push!(Tc_lo_buf,  Tclo); push!(Tc_hi_buf,  Tchi)
                push!(rho_lo_buf, U_SCRAM + ALPHA_F*(Tfhi - T_F0) + ALPHA_C*(Tchi - T_C0))
                push!(rho_hi_buf, U_SCRAM + ALPHA_F*(Tflo - T_F0) + ALPHA_C*(Tclo - T_C0))
            catch
                break
            end
        end
        n_steps = length(t_arr_buf)
        t_arr     = t_arr_buf
        n_lo_ts   = n_lo_buf;   n_hi_ts   = n_hi_buf
        rho_lo_ts = rho_lo_buf; rho_hi_ts = rho_hi_buf
        Tc_lo_ts  = Tc_lo_buf;  Tc_hi_ts  = Tc_hi_buf
        Tf_lo_ts  = Tf_lo_buf;  Tf_hi_ts  = Tf_hi_buf

        # Build a "last" hyperrectangle from the final NaN-free reach set.
        if n_steps == 0
            error("All reach-sets produced NaN under overapproximate.")
        end
        # Use last buffered values for the "at T_probe" report.
        last_n_lo = n_lo_ts[end];  last_n_hi = n_hi_ts[end]
        last_Tf_lo = Tf_lo_ts[end]; last_Tf_hi = Tf_hi_ts[end]
        last_Tc_lo = Tc_lo_ts[end]; last_Tc_hi = Tc_hi_ts[end]
        last_rho_lo = rho_lo_ts[end]; last_rho_hi = rho_hi_ts[end]
        println("  Extracted $n_steps NaN-free reach-sets out of $(n_steps_full) total")

        @printf "  n at last good step:   [%.4e, %.4e]\n" last_n_lo last_n_hi
        @printf "  T_f at last good step: [%.2f, %.2f] °C\n" last_Tf_lo last_Tf_hi
        @printf "  T_c at last good step: [%.2f, %.2f] °C\n" last_Tc_lo last_Tc_hi
        @printf "  ρ at last good step:   [%.5f, %.5f]\n" last_rho_lo last_rho_hi
        @printf "  last good time: %.4f s\n" t_arr[end]

        results[T_probe] = (status="OK", n_sets=n_sets, elapsed=elapsed,
                            t_arr=t_arr,
                            n_lo_ts=n_lo_ts, n_hi_ts=n_hi_ts,
                            rho_lo_ts=rho_lo_ts, rho_hi_ts=rho_hi_ts,
                            Tc_lo_ts=Tc_lo_ts, Tc_hi_ts=Tc_hi_ts,
                            Tf_lo_ts=Tf_lo_ts, Tf_hi_ts=Tf_hi_ts)
    catch err
        msg = sprint(showerror, err)
        println("  FAILED: ", first(msg, 300))
        results[T_probe] = (status="FAILED", err=first(msg, 300))
        break
    end
end

# Save .mat — same key naming convention as the PJ scram script.
mat_out = joinpath(results_dir, "reach_scram_full_fat.mat")
saved = Dict{String, Any}()
saved["fat_lo"]   = fat_lo
saved["fat_hi"]   = fat_hi
saved["sources"]  = ["shutdown", "operation", "loca"]
saved["sdm_rhs"]  = SDM_RHS
saved["rho_sdm"]  = RHO_SDM
for T_probe in (10.0, 30.0, 60.0)
    haskey(results, T_probe) || continue
    r = results[T_probe]
    r.status == "OK" || continue
    pre = "T_$(Int(T_probe))_"
    saved[pre * "t_arr"]     = r.t_arr
    saved[pre * "n_lo_ts"]   = r.n_lo_ts
    saved[pre * "n_hi_ts"]   = r.n_hi_ts
    saved[pre * "rho_lo_ts"] = r.rho_lo_ts
    saved[pre * "rho_hi_ts"] = r.rho_hi_ts
    saved[pre * "Tc_lo_ts"]  = r.Tc_lo_ts
    saved[pre * "Tc_hi_ts"]  = r.Tc_hi_ts
    saved[pre * "Tf_lo_ts"]  = r.Tf_lo_ts
    saved[pre * "Tf_hi_ts"]  = r.Tf_hi_ts
end
matwrite(mat_out, saved)
println("\nSaved fat-entry full-PKE scram reach to $mat_out")
