#!/usr/bin/env julia # # reach_heatup_nonlinear.jl — nonlinear reach on heatup mode via TMJets. # # STATUS: partial success. 10 s horizon works; longer horizons fail. # # Full 10-state nonlinear reach of the PKE + thermal-hydraulics # closed-loop is limited to ~10 s horizons by prompt-neutron stiffness # (Lambda = 1e-4 s). TMJets' adaptive step size shrinks to ~1 ms to # resolve prompt dynamics, then hits numerical precision floor and # produces NaN around t ~ 10 s. 10,583 steps for 10 s of sim time. # # For heatup's 5-hour horizon (18 million prompt timescales), we need # singular-perturbation reduction of the neutronics: treat n as a # quasi-static algebraic function of (T, C, rho) rather than a dynamic # state. The reduced-order PKE replaces the stiff dn/dt equation with # an algebraic relation, removing the fast timescale from the reach # problem. # # The 10 s result is a validation of the Taylor-model approach, not # a usable safety proof. The machinery works; the model needs # reduction before the horizon is meaningful. # # Saturation disabled (ctrl_heatup_unsat structure), all constants # inlined so @taylorize can generate a specialized Taylor-model RHS. # Time carried as augmented state x[11]. using Pkg Pkg.activate(joinpath(@__DIR__, "..")) using LinearAlgebra using ReachabilityAnalysis, LazySets using JSON # --- Plant constants (inlined; must match pke_params.m) --- const LAMBDA = 1e-4 const BETA_1 = 0.000215 const BETA_2 = 0.001424 const BETA_3 = 0.001274 const BETA_4 = 0.002568 const BETA_5 = 0.000748 const BETA_6 = 0.000273 const BETA = BETA_1 + BETA_2 + BETA_3 + BETA_4 + BETA_5 + BETA_6 const LAM_1 = 0.0124 const LAM_2 = 0.0305 const LAM_3 = 0.111 const LAM_4 = 0.301 const LAM_5 = 1.14 const LAM_6 = 3.01 const P0 = 1e9 const M_F = 50000.0 const C_F = 300.0 const M_C = 20000.0 const C_C = 5450.0 const HA = 5e7 const W_M = 5000.0 const M_SG = 30000.0 const ALPHA_F = -2.5e-5 const ALPHA_C = -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 # --- Controller constants --- const T_STANDBY = T_C0 - 33.333333 const RAMP_RATE_CS = 28.0 / 3600 const KP_HEATUP = 1e-4 # --- Taylorized RHS with augmented time (x[11] = t, dx[11] = 1) --- # TMJets handles augmented time better than accessing the solver's t arg # directly inside the Taylor-model rewriting. @taylorize function rhs_heatup_taylor!(dx, x, p, t) # Inlined cancellation: rho_total = Kp * (T_ref - T_c) after cancellation. # T_ref uses x[11] as the time-state (no min; valid while T_ref < T_c0). rho = KP_HEATUP * (T_STANDBY + RAMP_RATE_CS * x[11] - x[9]) # Neutronics (explicit — no loops, no function calls) dx[1] = (rho - BETA) / LAMBDA * x[1] + LAM_1*x[2] + LAM_2*x[3] + LAM_3*x[4] + LAM_4*x[5] + LAM_5*x[6] + LAM_6*x[7] 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])) / (M_SG * C_C) # Time (augmented state for reach; dt/dt = 1) dx[11] = one(x[1]) return nothing end # --- Build X_entry from predicates.json --- pred_path = joinpath(@__DIR__, "..", "..", "reachability", "predicates.json") pred_raw = JSON.parsefile(pred_path) entry = pred_raw["mode_boundaries"]["q_heatup"]["X_entry_polytope"] n_lo, n_hi = entry["n_range"] T_f_lo, T_f_hi = entry["T_f_range_C"] T_c_lo, T_c_hi = entry["T_c_range_C"] T_cold_lo, T_cold_hi = entry["T_cold_range_C"] n_mid = 0.5 * (n_lo + n_hi) C_mid_1 = (BETA_1 / (LAM_1 * LAMBDA)) * n_mid C_mid_2 = (BETA_2 / (LAM_2 * LAMBDA)) * n_mid C_mid_3 = (BETA_3 / (LAM_3 * LAMBDA)) * n_mid C_mid_4 = (BETA_4 / (LAM_4 * LAMBDA)) * n_mid C_mid_5 = (BETA_5 / (LAM_5 * LAMBDA)) * n_mid C_mid_6 = (BETA_6 / (LAM_6 * LAMBDA)) * n_mid x_lo = [n_lo; C_mid_1 * (n_lo / n_mid); C_mid_2 * (n_lo / n_mid); C_mid_3 * (n_lo / n_mid); C_mid_4 * (n_lo / n_mid); C_mid_5 * (n_lo / n_mid); C_mid_6 * (n_lo / n_mid); T_f_lo; T_c_lo; T_cold_lo; 0.0] # augmented time x_hi = [n_hi; C_mid_1 * (n_hi / n_mid); C_mid_2 * (n_hi / n_mid); C_mid_3 * (n_hi / n_mid); C_mid_4 * (n_hi / n_mid); C_mid_5 * (n_hi / n_mid); C_mid_6 * (n_hi / n_mid); T_f_hi; T_c_hi; T_cold_hi; 0.0] X0 = Hyperrectangle(low=x_lo, high=x_hi) println("\n=== Nonlinear heatup reach (saturation disabled, @taylorize RHS) ===") println(" X_entry: n ∈ [$(n_lo), $(n_hi)], T_c ∈ [$(T_c_lo), $(T_c_hi)] C") for T_probe in (10.0, 60.0, 300.0) println("\n--- Probe T = $T_probe s ---") sys = BlackBoxContinuousSystem(rhs_heatup_taylor!, 11) prob = InitialValueProblem(sys, X0) try alg = TMJets(orderT=4, orderQ=2, abstol=1e-10, maxsteps=50000) sol = solve(prob; T=T_probe, alg=alg) flow = flowpipe(sol) n_sets = length(flow) println(" TMJets: $n_sets reach-sets") # Overapproximate each Taylor-model reach-set as a hyperrectangle # so we can query envelopes. This is standard LazySets usage. flow_hr = overapproximate(flow, Hyperrectangle) n_sets_hr = length(flow_hr) # Envelope per state over the whole flowpipe. n_lo_env = minimum(low(set(R), 1) for R in flow_hr) n_hi_env = maximum(high(set(R), 1) for R in flow_hr) Tc_lo_env = minimum(low(set(R), 9) for R in flow_hr) Tc_hi_env = maximum(high(set(R), 9) for R in flow_hr) Tf_lo_env = minimum(low(set(R), 8) for R in flow_hr) Tf_hi_env = maximum(high(set(R), 8) for R in flow_hr) Tcold_lo = minimum(low(set(R), 10) for R in flow_hr) Tcold_hi = maximum(high(set(R), 10) for R in flow_hr) t_final = maximum(high(set(R), 11) for R in flow_hr) println(" t reached: $(round(t_final; digits=1)) s of $T_probe") println(" n envelope: [$(round(n_lo_env; sigdigits=4)), $(round(n_hi_env; sigdigits=4))]") println(" T_f envelope: [$(round(Tf_lo_env; digits=2)), $(round(Tf_hi_env; digits=2))] C") println(" T_c envelope: [$(round(Tc_lo_env; digits=2)), $(round(Tc_hi_env; digits=2))] C") println(" T_cold env: [$(round(Tcold_lo; digits=2)), $(round(Tcold_hi; digits=2))] C") catch err msg = sprint(showerror, err) println(" FAILED: ", first(msg, 300)) end end