function u = ctrl_heatup(t, x, plant, ref) % CTRL_HEATUP Ramp T_avg toward a target at a bounded rate. % % Structure: % u_ff = -alpha_f*(T_f - T_f0) - alpha_c*(T_c - T_c0) % cancel feedback % T_ref = min(ref.T_start + ref.ramp_rate * t, ref.T_target) % ramped reference % u_unsat = u_ff + Kp * (T_ref - T_avg) % P on error % u = sat(u_unsat, ref.u_min, ref.u_max) % bounded rod worth % % Why saturation: % Without it the P gain can push u toward prompt-supercritical as the % cold-hot feedback bias unwinds late in the ramp. Capping u at % +0.5*beta guarantees rho_total < beta (below prompt), which in % turn bounds the neutron-kinetics excursion rate for reachability. % % Why no integrator: % Ramp tracking has a structural lag proportional to ramp_rate / Kp_eff. % Acceptable because the DRC exits heatup on a predicate window % (t_avg_in_range & p_above_crit), not on zero steady-state error. % Adding PI would double-count the intrinsic plant integrator % (thermal mass) and make anti-windup a hybrid transition. % % Inputs: % t - time [s] % x - state vector (10 x 1) % plant - parameter struct (alpha_f, alpha_c, T_f0, T_c0 used) % ref - struct with fields: % .T_start starting T_avg [C] % .T_target final T_avg [C] % .ramp_rate desired dT_avg/dt [C/s] % .u_min (optional) lower saturation [dk/k]; default -5*beta % .u_max (optional) upper saturation [dk/k]; default +0.5*beta Kp = 1e-4; % [dk/k per K] T_f = x(8); T_c = x(9); T_avg = T_c; % Feedforward: cancel intrinsic temperature feedback so rho_total = Kp*e % (before saturation). u_ff = -plant.alpha_f * (T_f - plant.T_f0) ... -plant.alpha_c * (T_avg - plant.T_c0); % Ramped reference, clamped at target. T_ref = min(ref.T_start + ref.ramp_rate * t, ref.T_target); e = T_ref - T_avg; u_unsat = u_ff + Kp * e; % Saturation bounds (defaults keep rod worth subcritical-prompt). if isfield(ref, 'u_min'), u_min = ref.u_min; else, u_min = -5 * plant.beta; end if isfield(ref, 'u_max'), u_max = ref.u_max; else, u_max = 0.5 * plant.beta; end u = min(max(u_unsat, u_min), u_max); end