fix: reactor controller — vacuum/condenser pumps, drop ineffective sweep

- Start condenser vacuum pump at init; turn it off while the retention
  tank return valve is open (ejector has no suction during drain) and
  restart when the drain completes
- Start condenser circulation pump at 25% (was never running); prevents
  excessive cooling of return water per manual §Stabilization
- Drop primary pump hill-climb sweep: effect is negligible vs rod control
  and was masked by xenon transients; set fixed 65% for better heat transfer
- Raise auto temp-setpoint ceiling from 360 °C to 375 °C for more power headroom
- Raise condenser fill upper threshold from 50 % to 60 % (more reserve for secondary pumps)
- Add CONDENSER_VACUUM to state reads and TUI display (with colour alarm)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Dominik Moritz Roth 2026-03-15 11:18:31 +01:00
parent 646399dcc7
commit 2bb4207a98

View File

@ -5,14 +5,16 @@ Architecture:
- Rod PID: keeps CORE_TEMP at setpoint via ROD_BANK_POS_0_ORDERED - Rod PID: keeps CORE_TEMP at setpoint via ROD_BANK_POS_0_ORDERED
Per-train control (trains 1/2/3, 0-indexed as 0/1/2 in param names): Per-train control (trains 1/2/3, 0-indexed as 0/1/2 in param names):
- Primary pump: fixed 50%; hill-climbs only when deficit > 5 MW - Primary pump: fixed 65% (higher than 50% improves heat transfer per manual)
- MSCV PI: drives train power output, gated on steam availability - MSCV PI: drives train power output, gated on steam availability
- Secondary pump feedforward: half of steam outlet + level PID - Secondary pump feedforward: half of steam outlet + level PID
- Bypass: hold near 0 - Bypass: hold at 0
Auxiliary: Auxiliary:
- Vacuum pump: on continuously; turned off only during retention tank drain
- Condenser circulation pump: fixed 25% (prevents overcooling of return water)
- Retention tank: drain via ejector return valve when > 75%, stop at 50% - Retention tank: drain via ejector return valve when > 75%, stop at 50%
- Condenser fill: run FREIGHT_PUMP_CONDENSER below 33%, stop at 50% - Condenser fill: run FREIGHT_PUMP_CONDENSER below 45%, stop at 60%
Usage: Usage:
python3.14 scripts/reactor_control.py --trains 3 --target 50000 python3.14 scripts/reactor_control.py --trains 3 --target 50000
@ -126,16 +128,11 @@ class TrainController:
out_min=-2.0, out_max=2.0, integral_max=3.0) out_min=-2.0, out_max=2.0, integral_max=3.0)
self.sec_level_target = 25_000.0 self.sec_level_target = 25_000.0
self.prim_pump = 50.0 self.prim_pump = 65.0
self.mscv = 9.0 self.mscv = 9.0
self.sec_pump = 40.0 self.sec_pump = 40.0
self._pump_hill_cycle = 0 set_param(f'COOLANT_CORE_CIRCULATION_PUMP_{self.i}_ORDERED_SPEED', self.prim_pump)
self._pump_hill_dir = +1.0
self._pump_hill_steam = None
self.PUMP_SWEEP_THRESHOLD = 5_000.0
self.sweeping = False
set_param(f'STEAM_TURBINE_{self.i}_BYPASS_ORDERED', 0.0) set_param(f'STEAM_TURBINE_{self.i}_BYPASS_ORDERED', 0.0)
self._params = [ self._params = [
@ -175,24 +172,6 @@ class TrainController:
self.mscv = float(np.clip(new_mscv, 0.5, 100.0)) self.mscv = float(np.clip(new_mscv, 0.5, 100.0))
set_param(f'MSCV_{self.i}_OPENING_ORDERED', self.mscv) set_param(f'MSCV_{self.i}_OPENING_ORDERED', self.mscv)
self.sweeping = power_error > self.PUMP_SWEEP_THRESHOLD
if self.sweeping:
self._pump_hill_cycle += 1
if self._pump_hill_cycle >= 12:
self._pump_hill_cycle = 0
if self._pump_hill_steam is not None:
if steam_out < self._pump_hill_steam - 0.2:
self._pump_hill_dir = -self._pump_hill_dir
self._pump_hill_steam = steam_out
self.prim_pump = float(np.clip(self.prim_pump + self._pump_hill_dir, 15.0, 90.0))
set_param(f'COOLANT_CORE_CIRCULATION_PUMP_{self.i}_ORDERED_SPEED', self.prim_pump)
else:
if self.prim_pump != 50.0:
self.prim_pump = 50.0
set_param(f'COOLANT_CORE_CIRCULATION_PUMP_{self.i}_ORDERED_SPEED', self.prim_pump)
self._pump_hill_cycle = 0
self._pump_hill_steam = None
sec_ff = steam_out / 2.0 sec_ff = steam_out / 2.0
level = s[f'COOLANT_SEC_{self.i}_LIQUID_VOLUME'] level = s[f'COOLANT_SEC_{self.i}_LIQUID_VOLUME']
level_error = self.sec_level_target - level level_error = self.sec_level_target - level
@ -228,6 +207,7 @@ core_params = [
'CORE_STATE_CRITICALITY', 'CORE_STATE_CRITICALITY',
'VACUUM_RETENTION_TANK_VOLUME', 'VACUUM_RETENTION_TANK_VOLUME',
'CONDENSER_VOLUME', 'CONDENSER_VAPOR_VOLUME', 'CONDENSER_VOLUME', 'CONDENSER_VAPOR_VOLUME',
'CONDENSER_VACUUM', # vacuum level % — monitor for pump health
'POWER_DEMAND_MW', 'POWER_DEMAND_MW',
'CORE_PRIMARY_CIRCUIT_COOLING_TANK_VOLUME', # pressurizer water volume 'CORE_PRIMARY_CIRCUIT_COOLING_TANK_VOLUME', # pressurizer water volume
'COOLANT_CORE_PRIMARY_LOOP_LEVEL', # overall primary loop fill % 'COOLANT_CORE_PRIMARY_LOOP_LEVEL', # overall primary loop fill %
@ -257,6 +237,8 @@ _init = read_state([
'STEAM_EJECTOR_CONDENSER_RETURN_VALVE_ACTUAL', 'STEAM_EJECTOR_CONDENSER_RETURN_VALVE_ACTUAL',
'CONDENSER_VOLUME', 'CONDENSER_VAPOR_VOLUME', 'CONDENSER_VOLUME', 'CONDENSER_VAPOR_VOLUME',
'FREIGHT_PUMP_CONDENSER_ACTIVE', 'FREIGHT_PUMP_CONDENSER_ACTIVE',
'CONDENSER_VACUUM_PUMP_ACTIVE',
'CONDENSER_CIRCULATION_PUMP_ACTIVE',
]) ])
_ret_vol_init = _init.get('VACUUM_RETENTION_TANK_VOLUME', 0.0) _ret_vol_init = _init.get('VACUUM_RETENTION_TANK_VOLUME', 0.0)
_ret_valve_init = _init.get('STEAM_EJECTOR_CONDENSER_RETURN_VALVE_ACTUAL', 0.0) _ret_valve_init = _init.get('STEAM_EJECTOR_CONDENSER_RETURN_VALVE_ACTUAL', 0.0)
@ -272,7 +254,7 @@ _cond_vap_init = _init.get('CONDENSER_VAPOR_VOLUME', 0.0)
_cond_tot_init = _cond_vol_init + _cond_vap_init _cond_tot_init = _cond_vol_init + _cond_vap_init
_cond_pct_init = (_cond_vol_init / _cond_tot_init * 100.0) if _cond_tot_init > 0 else 0.0 _cond_pct_init = (_cond_vol_init / _cond_tot_init * 100.0) if _cond_tot_init > 0 else 0.0
_cond_pump_init = bool(_init.get('FREIGHT_PUMP_CONDENSER_ACTIVE', False)) _cond_pump_init = bool(_init.get('FREIGHT_PUMP_CONDENSER_ACTIVE', False))
if _cond_pump_init and _cond_pct_init >= 50.0: if _cond_pump_init and _cond_pct_init >= 60.0:
nucon.set(nucon._parameters['FREIGHT_PUMP_CONDENSER_SWITCH'], False) nucon.set(nucon._parameters['FREIGHT_PUMP_CONDENSER_SWITCH'], False)
cond_pump_on = False cond_pump_on = False
elif not _cond_pump_init and _cond_pct_init < 45.0: elif not _cond_pump_init and _cond_pct_init < 45.0:
@ -281,6 +263,20 @@ elif not _cond_pump_init and _cond_pct_init < 45.0:
else: else:
cond_pump_on = _cond_pump_init cond_pump_on = _cond_pump_init
# Vacuum pump — keep on continuously; turn off only during retention tank drain.
# (Opening the return valve breaks the suction path so the pump has no effect.)
vac_pump_on = bool(_init.get('CONDENSER_VACUUM_PUMP_ACTIVE', False))
if not vac_pump_on:
nucon.set(nucon._parameters['CONDENSER_VACUUM_PUMP_START_STOP'], True)
vac_pump_on = True
# Condenser circulation pump — run at moderate speed to prevent overcooling
# (manual §Stabilization: "prevent excessive cooling of the coolant returning to the evaporator").
_cond_circ_on = bool(_init.get('CONDENSER_CIRCULATION_PUMP_ACTIVE', False))
if not _cond_circ_on:
nucon.set(nucon._parameters['CONDENSER_CIRCULATION_PUMP_SWITCH'], True)
set_param('CONDENSER_CIRCULATION_PUMP_ORDERED_SPEED', 25.0)
# Pressurizer spray valve — init from live state # Pressurizer spray valve — init from live state
_prsr_live = read_state(['CORE_PRIMARY_CIRCUIT_COOLING_TANK_VOLUME', 'COOLANT_CORE_PRIMARY_LOOP_LEVEL', 'FREIGHT_PUMP_FEEDWATER_ACTIVE']) _prsr_live = read_state(['CORE_PRIMARY_CIRCUIT_COOLING_TANK_VOLUME', 'COOLANT_CORE_PRIMARY_LOOP_LEVEL', 'FREIGHT_PUMP_FEEDWATER_ACTIVE'])
_prsr_level = _prsr_live.get('CORE_PRIMARY_CIRCUIT_COOLING_TANK_VOLUME', PRSR_VOL_MAX * 0.6) / PRSR_VOL_MAX * 100.0 _prsr_level = _prsr_live.get('CORE_PRIMARY_CIRCUIT_COOLING_TANK_VOLUME', PRSR_VOL_MAX * 0.6) / PRSR_VOL_MAX * 100.0
@ -327,6 +323,7 @@ def run_controller(stdscr):
global rod_pos, rod_cycle, rod_integral global rod_pos, rod_cycle, rod_integral
global ret_valve, ret_draining, ret_prev_vol, cond_pump_on global ret_valve, ret_draining, ret_prev_vol, cond_pump_on
global prsr_spraying, feedwater_on global prsr_spraying, feedwater_on
global vac_pump_on
global train_controllers, targets global train_controllers, targets
curses.curs_set(0) curses.curs_set(0)
@ -360,6 +357,7 @@ def run_controller(stdscr):
disp = dict(s={}, dynamic_setpoint=temp_setpoint, temp_auto=temp_auto, criticality=0.0, disp = dict(s={}, dynamic_setpoint=temp_setpoint, temp_auto=temp_auto, criticality=0.0,
ret_pct=0.0, ret_draining=False, ret_valve=0.0, ret_pct=0.0, ret_draining=False, ret_valve=0.0,
cond_pct=0.0, cond_pump_on=False, cond_pct=0.0, cond_pump_on=False,
vac_pump_on=vac_pump_on,
prsr_level=_prsr_level, prsr_spraying=prsr_spraying, prsr_level=_prsr_level, prsr_spraying=prsr_spraying,
prim_level=_prsr_live.get('COOLANT_CORE_PRIMARY_LOOP_LEVEL', 100.0), prim_level=_prsr_live.get('COOLANT_CORE_PRIMARY_LOOP_LEVEL', 100.0),
feedwater_on=feedwater_on, feedwater_on=feedwater_on,
@ -392,7 +390,7 @@ def run_controller(stdscr):
# Right/+ increase Left/- decrease # Right/+ increase Left/- decrease
elif key in (ord('+'), ord('='), curses.KEY_RIGHT): elif key in (ord('+'), ord('='), curses.KEY_RIGHT):
if selected_train == 0 and not temp_auto: if selected_train == 0 and not temp_auto:
temp_setpoint = min(round(temp_setpoint / 5.0) * 5.0 + 5.0, 410.0) temp_setpoint = min(round(temp_setpoint / 5.0) * 5.0 + 5.0, 375.0)
elif selected_train == 4: elif selected_train == 4:
args.grid_buffer = min(args.grid_buffer + 1.0, 100.0) args.grid_buffer = min(args.grid_buffer + 1.0, 100.0)
elif selected_train in train_controllers: elif selected_train in train_controllers:
@ -471,7 +469,7 @@ def run_controller(stdscr):
_safe_addstr(stdscr, row, 10, f'[{auto_str}]', auto_color | BOLD) _safe_addstr(stdscr, row, 10, f'[{auto_str}]', auto_color | BOLD)
_safe_addstr(stdscr, row, 16, 'Temp: ', BOLD) _safe_addstr(stdscr, row, 16, 'Temp: ', BOLD)
_safe_addstr(stdscr, row, 22, f'{core_temp:6.1f}°C', temp_color | BOLD) _safe_addstr(stdscr, row, 22, f'{core_temp:6.1f}°C', temp_color | BOLD)
sp_color = RED if dynamic_setpoint < 306 or dynamic_setpoint > 360 else 0 sp_color = RED if dynamic_setpoint < 306 or dynamic_setpoint > 375 else 0
_safe_addstr(stdscr, row, 32, f'sp=', 0) _safe_addstr(stdscr, row, 32, f'sp=', 0)
_safe_addstr(stdscr, row, 35, f'{dynamic_setpoint:.0f}°C', sp_color | BOLD) _safe_addstr(stdscr, row, 35, f'{dynamic_setpoint:.0f}°C', sp_color | BOLD)
_safe_addstr(stdscr, row, 40, _safe_addstr(stdscr, row, 40,
@ -501,10 +499,9 @@ def run_controller(stdscr):
_safe_addstr(stdscr, row, 32, _safe_addstr(stdscr, row, 32,
f'[{_bar(pwr_pct, 14)}] {power_error/1000:+5.1f}MW {tgt_str}') f'[{_bar(pwr_pct, 14)}] {power_error/1000:+5.1f}MW {tgt_str}')
row += 1 row += 1
sweep_ind = '~' if tc.sweeping else ' '
_safe_addstr(stdscr, row, 16, _safe_addstr(stdscr, row, 16,
f'Steam: {steam_out:5.1f} MSCV: {tc.mscv:4.1f} ' f'Steam: {steam_out:5.1f} MSCV: {tc.mscv:4.1f} '
f'Prim: {tc.prim_pump:3.0f}%{sweep_ind} Sec: {tc.sec_pump:3.0f}% ' f'Prim: {tc.prim_pump:3.0f}% Sec: {tc.sec_pump:3.0f}% '
f'Lvl: {level:.0f}{level_error:+.0f})') f'Lvl: {level:.0f}{level_error:+.0f})')
elif not is_active: elif not is_active:
hint = ' (+/Up to add)' if is_sel else '' hint = ' (+/Up to add)' if is_sel else ''
@ -540,12 +537,18 @@ def run_controller(stdscr):
f' DRAINING valve={ret_valve:.0f}%' if ret_draining else ' OK', f' DRAINING valve={ret_valve:.0f}%' if ret_draining else ' OK',
YELLOW if ret_draining else GREEN) YELLOW if ret_draining else GREEN)
row += 1 row += 1
cond_vac_ = s.get('CONDENSER_VACUUM', 0.0)
cond_color = RED if cond_pct < 25 else YELLOW if cond_pct < 40 else GREEN cond_color = RED if cond_pct < 25 else YELLOW if cond_pct < 40 else GREEN
vac_on_ = disp.get('vac_pump_on', True)
vac_color = (RED if cond_vac_ < 50 else YELLOW if cond_vac_ < 80 else GREEN) if vac_on_ else YELLOW
_safe_addstr(stdscr, row, 2, '◆ CONDENSER FILL ', BOLD) _safe_addstr(stdscr, row, 2, '◆ CONDENSER FILL ', BOLD)
_safe_addstr(stdscr, row, 20, f'[{_bar(cond_pct, 20)}]', cond_color) _safe_addstr(stdscr, row, 20, f'[{_bar(cond_pct, 20)}]', cond_color)
_safe_addstr(stdscr, row, 43, f' {cond_pct:4.0f}%') _safe_addstr(stdscr, row, 43, f' {cond_pct:4.0f}%')
_safe_addstr(stdscr, row, 49, ' PUMP ON' if cond_pump_on else ' OK', _safe_addstr(stdscr, row, 49, ' PUMP ON' if cond_pump_on else ' OK',
YELLOW if cond_pump_on else GREEN) YELLOW if cond_pump_on else GREEN)
_safe_addstr(stdscr, row, 60,
f' VAC:{"OFF" if not vac_on_ else f"{cond_vac_:.0f}%"}',
vac_color)
row += 1 row += 1
prsr_level_ = disp['prsr_level'] prsr_level_ = disp['prsr_level']
prsr_spray_ = disp['prsr_spraying'] prsr_spray_ = disp['prsr_spraying']
@ -636,7 +639,7 @@ def run_controller(stdscr):
if temp_auto and train_data: if temp_auto and train_data:
total_error = sum(train_data[t][1] for t in train_data) # sum of power_errors total_error = sum(train_data[t][1] for t in train_data) # sum of power_errors
sp_delta = float(np.clip(total_error * 0.00002, -0.5, 0.5)) sp_delta = float(np.clip(total_error * 0.00002, -0.5, 0.5))
temp_setpoint = float(np.clip(temp_setpoint + sp_delta, 306.0, 360.0)) temp_setpoint = float(np.clip(temp_setpoint + sp_delta, 306.0, 375.0))
# ---- Aux: retention tank ---- # ---- Aux: retention tank ----
ret_vol = s.get('VACUUM_RETENTION_TANK_VOLUME', 0.0) ret_vol = s.get('VACUUM_RETENTION_TANK_VOLUME', 0.0)
@ -645,7 +648,17 @@ def run_controller(stdscr):
ret_draining = False ret_draining = False
ret_valve = 0.0 ret_valve = 0.0
set_param('STEAM_EJECTOR_CONDENSER_RETURN_VALVE', 0.0) set_param('STEAM_EJECTOR_CONDENSER_RETURN_VALVE', 0.0)
# Drain complete — restart vacuum pump
if not vac_pump_on:
nucon.set(nucon._parameters['CONDENSER_VACUUM_PUMP_START_STOP'], True)
vac_pump_on = True
elif ret_vol > RETENTION_HI: elif ret_vol > RETENTION_HI:
if not ret_draining:
# Starting drain — stop vacuum pump.
# The ejector return valve bypasses the suction path so the pump has no effect
# and wastes power; turn it off for the duration of the drain.
nucon.set(nucon._parameters['CONDENSER_VACUUM_PUMP_START_STOP'], False)
vac_pump_on = False
ret_draining = True ret_draining = True
if ret_prev_vol is not None and ret_vol >= ret_prev_vol - 50.0: if ret_prev_vol is not None and ret_vol >= ret_prev_vol - 50.0:
ret_valve = min(ret_valve + 1.0, 50.0) ret_valve = min(ret_valve + 1.0, 50.0)
@ -662,7 +675,7 @@ def run_controller(stdscr):
if not cond_pump_on and cond_pct < 45.0: if not cond_pump_on and cond_pct < 45.0:
cond_pump_on = True cond_pump_on = True
nucon.set(nucon._parameters['FREIGHT_PUMP_CONDENSER_SWITCH'], True) nucon.set(nucon._parameters['FREIGHT_PUMP_CONDENSER_SWITCH'], True)
elif cond_pump_on and cond_pct >= 50.0: elif cond_pump_on and cond_pct >= 60.0:
cond_pump_on = False cond_pump_on = False
nucon.set(nucon._parameters['FREIGHT_PUMP_CONDENSER_SWITCH'], False) nucon.set(nucon._parameters['FREIGHT_PUMP_CONDENSER_SWITCH'], False)
@ -694,6 +707,7 @@ def run_controller(stdscr):
criticality=criticality, criticality=criticality,
ret_pct=ret_pct, ret_draining=ret_draining, ret_valve=ret_valve, ret_pct=ret_pct, ret_draining=ret_draining, ret_valve=ret_valve,
cond_pct=cond_pct, cond_pump_on=cond_pump_on, cond_pct=cond_pct, cond_pump_on=cond_pump_on,
vac_pump_on=vac_pump_on,
prsr_level=prsr_level, prsr_spraying=prsr_spraying, prsr_level=prsr_level, prsr_spraying=prsr_spraying,
prim_level=prim_level, feedwater_on=feedwater_on, prim_level=prim_level, feedwater_on=feedwater_on,
grid_follow=grid_follow, grid_demand_kw=grid_demand_kw) grid_follow=grid_follow, grid_demand_kw=grid_demand_kw)