feat: improve manual rod control and reactor power
This commit is contained in:
@@ -9,7 +9,7 @@ All source code lives under `src/reactor_sim`. Submodules map to plant systems:
|
|||||||
- `python -m pip install -e .[dev]` — install editable package with optional tooling when dev dependencies are defined.
|
- `python -m pip install -e .[dev]` — install editable package with optional tooling when dev dependencies are defined.
|
||||||
|
|
||||||
## Operations & Control Hooks
|
## Operations & Control Hooks
|
||||||
Manual commands live in `reactor_sim.commands.ReactorCommand`. Pass a `command_provider` callable to `ReactorSimulation` to adjust rods, pumps, turbine, coolant demand, or the attached `ElectricalConsumer`. Use `ReactorCommand.scram_all()` for full shutdown, `ReactorCommand(consumer_online=True, consumer_demand=600)` to hook the grid, or toggle pumps (`primary_pump_on=False`) to simulate faults. Control helpers in `control.py` expose `set_rods`, `increment_rods`, and `scram`. For hands-on runs, launch the curses dashboard (`FISSION_DASHBOARD=1 FISSION_REALTIME=1 python run_simulation.py`) and use the on-screen shortcuts (q quit/save, space SCRAM, p/o pumps, t turbine, +/- rods, [/ ] consumer demand, s/d setpoint).
|
Manual commands live in `reactor_sim.commands.ReactorCommand`. Pass a `command_provider` callable to `ReactorSimulation` to adjust rods, pumps, turbine, coolant demand, or the attached `ElectricalConsumer`. Use `ReactorCommand.scram_all()` for full shutdown, `ReactorCommand(consumer_online=True, consumer_demand=600)` to hook the grid, or toggle pumps (`primary_pump_on=False`) to simulate faults. Control helpers in `control.py` expose `set_rods`, `increment_rods`, and `scram`, and you can switch `set_manual_mode(True)` to pause the automatic rod controller. For hands-on runs, launch the curses dashboard (`FISSION_DASHBOARD=1 FISSION_REALTIME=1 python run_simulation.py`) and use the on-screen shortcuts (q quit/save, space SCRAM, p/o pumps, t turbine, +/- rods in 0.05 steps, [/ ] consumer demand, s/d setpoint, `a` toggles auto/manual rods). Recommended startup: enable manual rods (`a`), withdraw to ~0.3 before ramping the turbine/consumer, then re-enable auto control when you want closed-loop operation.
|
||||||
The plant now boots cold (ambient core temperature, idle pumps); scripts must sequence startup: enable pumps, gradually withdraw rods, connect the consumer after turbine spin-up, and use `ControlSystem.set_power_setpoint` to chase desired output. Set `FISSION_REALTIME=1` to run continuously with real-time pacing; optionally set `FISSION_SIM_DURATION=infinite` for indefinite runs and send SIGINT/Ctrl+C to stop. Use `FISSION_SIM_DURATION=600` (default) for bounded offline batches.
|
The plant now boots cold (ambient core temperature, idle pumps); scripts must sequence startup: enable pumps, gradually withdraw rods, connect the consumer after turbine spin-up, and use `ControlSystem.set_power_setpoint` to chase desired output. Set `FISSION_REALTIME=1` to run continuously with real-time pacing; optionally set `FISSION_SIM_DURATION=infinite` for indefinite runs and send SIGINT/Ctrl+C to stop. Use `FISSION_SIM_DURATION=600` (default) for bounded offline batches.
|
||||||
|
|
||||||
## Coding Style & Naming Conventions
|
## Coding Style & Naming Conventions
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class ReactorCommand:
|
|||||||
power_setpoint: float | None = None
|
power_setpoint: float | None = None
|
||||||
consumer_online: bool | None = None
|
consumer_online: bool | None = None
|
||||||
consumer_demand: float | None = None
|
consumer_demand: float | None = None
|
||||||
|
rod_manual: bool | None = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def scram_all(cls) -> "ReactorCommand":
|
def scram_all(cls) -> "ReactorCommand":
|
||||||
|
|||||||
@@ -16,4 +16,4 @@ GENERATOR_EFFICIENCY = 0.96
|
|||||||
ENVIRONMENT_TEMPERATURE = 295.0 # K
|
ENVIRONMENT_TEMPERATURE = 295.0 # K
|
||||||
AMU_TO_KG = 1.660_539_066_60e-27
|
AMU_TO_KG = 1.660_539_066_60e-27
|
||||||
MEV_TO_J = 1.602_176_634e-13
|
MEV_TO_J = 1.602_176_634e-13
|
||||||
ELECTRON_FISSION_CROSS_SECTION = 5e-23 # cm^2, tuned for simulation scale
|
ELECTRON_FISSION_CROSS_SECTION = 5e-16 # cm^2, tuned for simulation scale
|
||||||
|
|||||||
@@ -21,8 +21,11 @@ def clamp(value: float, lo: float, hi: float) -> float:
|
|||||||
class ControlSystem:
|
class ControlSystem:
|
||||||
setpoint_mw: float = 3_000.0
|
setpoint_mw: float = 3_000.0
|
||||||
rod_fraction: float = 0.5
|
rod_fraction: float = 0.5
|
||||||
|
manual_control: bool = False
|
||||||
|
|
||||||
def update_rods(self, state: CoreState, dt: float) -> float:
|
def update_rods(self, state: CoreState, dt: float) -> float:
|
||||||
|
if self.manual_control:
|
||||||
|
return self.rod_fraction
|
||||||
error = (state.power_output_mw - self.setpoint_mw) / self.setpoint_mw
|
error = (state.power_output_mw - self.setpoint_mw) / self.setpoint_mw
|
||||||
adjustment = -error * 0.3
|
adjustment = -error * 0.3
|
||||||
adjustment = clamp(adjustment, -constants.CONTROL_ROD_SPEED * dt, constants.CONTROL_ROD_SPEED * dt)
|
adjustment = clamp(adjustment, -constants.CONTROL_ROD_SPEED * dt, constants.CONTROL_ROD_SPEED * dt)
|
||||||
@@ -50,6 +53,11 @@ class ControlSystem:
|
|||||||
self.setpoint_mw = clamp(megawatts, 100.0, 4_000.0)
|
self.setpoint_mw = clamp(megawatts, 100.0, 4_000.0)
|
||||||
LOGGER.info("Power setpoint %.0f -> %.0f MW", previous, self.setpoint_mw)
|
LOGGER.info("Power setpoint %.0f -> %.0f MW", previous, self.setpoint_mw)
|
||||||
|
|
||||||
|
def set_manual_mode(self, manual: bool) -> None:
|
||||||
|
if self.manual_control != manual:
|
||||||
|
self.manual_control = manual
|
||||||
|
LOGGER.info("Rod control %s", "manual" if manual else "automatic")
|
||||||
|
|
||||||
def coolant_demand(self, primary: CoolantLoopState) -> float:
|
def coolant_demand(self, primary: CoolantLoopState) -> float:
|
||||||
desired_temp = 580.0
|
desired_temp = 580.0
|
||||||
error = (primary.temperature_out - desired_temp) / 100.0
|
error = (primary.temperature_out - desired_temp) / 100.0
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ class ReactorDashboard:
|
|||||||
DashboardKey("[/]", "Adjust consumer demand −/+50 MW"),
|
DashboardKey("[/]", "Adjust consumer demand −/+50 MW"),
|
||||||
DashboardKey("s", "Setpoint −250 MW"),
|
DashboardKey("s", "Setpoint −250 MW"),
|
||||||
DashboardKey("d", "Setpoint +250 MW"),
|
DashboardKey("d", "Setpoint +250 MW"),
|
||||||
|
DashboardKey("a", "Toggle auto rod control"),
|
||||||
]
|
]
|
||||||
|
|
||||||
def run(self) -> None:
|
def run(self) -> None:
|
||||||
@@ -112,23 +113,28 @@ class ReactorDashboard:
|
|||||||
self._queue_command(ReactorCommand(power_setpoint=self.reactor.control.setpoint_mw - 250.0))
|
self._queue_command(ReactorCommand(power_setpoint=self.reactor.control.setpoint_mw - 250.0))
|
||||||
elif ch in (ord("d"), ord("D")):
|
elif ch in (ord("d"), ord("D")):
|
||||||
self._queue_command(ReactorCommand(power_setpoint=self.reactor.control.setpoint_mw + 250.0))
|
self._queue_command(ReactorCommand(power_setpoint=self.reactor.control.setpoint_mw + 250.0))
|
||||||
|
elif ch in (ord("a"), ord("A")):
|
||||||
|
self._queue_command(ReactorCommand(rod_manual=not self.reactor.control.manual_control))
|
||||||
|
|
||||||
def _queue_command(self, command: ReactorCommand) -> None:
|
def _queue_command(self, command: ReactorCommand) -> None:
|
||||||
if self.pending_command is None:
|
if self.pending_command is None:
|
||||||
self.pending_command = command
|
self.pending_command = command
|
||||||
return
|
else:
|
||||||
for field in command.__dataclass_fields__: # type: ignore[attr-defined]
|
for field in command.__dataclass_fields__: # type: ignore[attr-defined]
|
||||||
value = getattr(command, field)
|
value = getattr(command, field)
|
||||||
if value is None or value is False:
|
if value is None or value is False:
|
||||||
continue
|
continue
|
||||||
if field == "rod_step" and getattr(self.pending_command, field) is not None:
|
if field == "rod_step" and getattr(self.pending_command, field) is not None:
|
||||||
setattr(
|
setattr(
|
||||||
self.pending_command,
|
self.pending_command,
|
||||||
field,
|
field,
|
||||||
getattr(self.pending_command, field) + value,
|
getattr(self.pending_command, field) + value,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
setattr(self.pending_command, field, value)
|
setattr(self.pending_command, field, value)
|
||||||
|
if self.pending_command.rod_position is not None and self.pending_command.rod_manual is None:
|
||||||
|
self.pending_command.rod_manual = True
|
||||||
|
|
||||||
|
|
||||||
def _next_command(self, _: float, __: PlantState) -> Optional[ReactorCommand]:
|
def _next_command(self, _: float, __: PlantState) -> Optional[ReactorCommand]:
|
||||||
cmd = self.pending_command
|
cmd = self.pending_command
|
||||||
|
|||||||
@@ -182,7 +182,10 @@ class Reactor:
|
|||||||
self._set_turbine_state(False)
|
self._set_turbine_state(False)
|
||||||
if command.power_setpoint is not None:
|
if command.power_setpoint is not None:
|
||||||
self.control.set_power_setpoint(command.power_setpoint)
|
self.control.set_power_setpoint(command.power_setpoint)
|
||||||
|
if command.rod_manual is not None:
|
||||||
|
self.control.set_manual_mode(command.rod_manual)
|
||||||
if command.rod_position is not None:
|
if command.rod_position is not None:
|
||||||
|
self.control.set_manual_mode(True)
|
||||||
overrides["rod_fraction"] = self.control.set_rods(command.rod_position)
|
overrides["rod_fraction"] = self.control.set_rods(command.rod_position)
|
||||||
self.shutdown = self.shutdown or command.rod_position >= 0.95
|
self.shutdown = self.shutdown or command.rod_position >= 0.95
|
||||||
elif command.rod_step is not None:
|
elif command.rod_step is not None:
|
||||||
|
|||||||
Reference in New Issue
Block a user