From 5650f87fb62de95024cf85c07b6a55564cb3ddd7 Mon Sep 17 00:00:00 2001 From: mn3981 Date: Mon, 3 Aug 2026 15:00:51 +0100 Subject: [PATCH 01/11] Add PulseTimings dataclass for managing pulse timing parameters --- process/models/physics/physics.py | 35 ++++++++----------- process/models/pulse.py | 56 +++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 21 deletions(-) diff --git a/process/models/physics/physics.py b/process/models/physics/physics.py index c7edc9635d..899f3c24f6 100644 --- a/process/models/physics/physics.py +++ b/process/models/physics/physics.py @@ -28,6 +28,7 @@ DensityProfilePedestalType, PlasmaProfileShapeType, ) +from process.models.pulse import PulseTimings if TYPE_CHECKING: from process.data_structure.physics_variables import PhysicsData @@ -477,6 +478,15 @@ def run(self): self.data.times.t_plant_pulse_plasma_current_ramp_up ) + pulse_timings = PulseTimings( + t_plant_pulse_coil_precharge=self.data.times.t_plant_pulse_coil_precharge, + t_plant_pulse_plasma_current_ramp_up=self.data.times.t_plant_pulse_plasma_current_ramp_up, + t_plant_pulse_fusion_ramp=self.data.times.t_plant_pulse_fusion_ramp, + t_plant_pulse_burn=self.data.times.t_plant_pulse_burn, + t_plant_pulse_plasma_current_ramp_down=self.data.times.t_plant_pulse_plasma_current_ramp_down, + t_plant_pulse_dwell=self.data.times.t_plant_pulse_dwell, + ) + # Reset second self.data.times.t_plant_pulse_burn value # (self.data.times.t_burn_0). # This is used to ensure that the burn time is used consistently; @@ -484,29 +494,12 @@ def run(self): self.data.times.t_burn_0 = self.data.times.t_plant_pulse_burn # Time during the pulse in which a plasma is present - self.data.times.t_plant_pulse_plasma_present = ( - self.data.times.t_plant_pulse_plasma_current_ramp_up - + self.data.times.t_plant_pulse_fusion_ramp - + self.data.times.t_plant_pulse_burn - + self.data.times.t_plant_pulse_plasma_current_ramp_down - ) - self.data.times.t_plant_pulse_no_burn = ( - self.data.times.t_plant_pulse_coil_precharge - + self.data.times.t_plant_pulse_plasma_current_ramp_up - + self.data.times.t_plant_pulse_plasma_current_ramp_down - + self.data.times.t_plant_pulse_dwell - + self.data.times.t_plant_pulse_fusion_ramp - ) + self.data.times.t_plant_pulse_plasma_present = pulse_timings.plasma_present + + self.data.times.t_plant_pulse_no_burn = pulse_timings.no_burn # Total cycle time - self.data.times.t_plant_pulse_total = ( - self.data.times.t_plant_pulse_coil_precharge - + self.data.times.t_plant_pulse_plasma_current_ramp_up - + self.data.times.t_plant_pulse_fusion_ramp - + self.data.times.t_plant_pulse_burn - + self.data.times.t_plant_pulse_plasma_current_ramp_down - + self.data.times.t_plant_pulse_dwell - ) + self.data.times.t_plant_pulse_total = pulse_timings.total # ***************************** # # DIAMAGNETIC CURRENT # diff --git a/process/models/pulse.py b/process/models/pulse.py index 46741b8eb5..c1d516dcb8 100644 --- a/process/models/pulse.py +++ b/process/models/pulse.py @@ -1,6 +1,7 @@ """Module containing the Pulse class for pulsed reactor calculations.""" import logging +from dataclasses import dataclass from process.core import constants from process.core import process_output as po @@ -10,6 +11,61 @@ logger = logging.getLogger(__name__) +@dataclass(frozen=True, slots=True) +class PulseTimings: + """Class to hold the timing parameters for a pulsed reactor.""" + + t_plant_pulse_coil_precharge: float + """Time for coil precharge (s)""" + t_plant_pulse_plasma_current_ramp_up: float + """Time for plasma current ramp-up (s)""" + t_plant_pulse_fusion_ramp: float + """Time for fusion ramp (s)""" + t_plant_pulse_burn: float + """Time for burn (s)""" + t_plant_pulse_plasma_current_ramp_down: float + """Time for plasma current ramp-down (s)""" + t_plant_pulse_dwell: float + """Time for dwell (s)""" + + @property + def plasma_present(self) -> float: + """Calculate the total time during which plasma is present in the reactor.""" + return ( + self.t_plant_pulse_plasma_current_ramp_up + + self.t_plant_pulse_fusion_ramp + + self.t_plant_pulse_burn + + self.t_plant_pulse_plasma_current_ramp_down + ) + + @property + def no_burn(self) -> float: + """Calculate the total time excluding the burn phase.""" + return ( + self.t_plant_pulse_coil_precharge + + self.t_plant_pulse_plasma_current_ramp_up + + self.t_plant_pulse_plasma_current_ramp_down + + self.t_plant_pulse_dwell + + self.t_plant_pulse_fusion_ramp + ) + + @property + def total(self) -> float: + """Calculate the total time including the burn phase.""" + return self.no_burn + self.t_plant_pulse_burn + + @property + def cumulative(self) -> tuple[float, float, float, float, float, float, float]: + t0 = 0.0 + t1 = t0 + self.t_plant_pulse_coil_precharge + t2 = t1 + self.t_plant_pulse_plasma_current_ramp_up + t3 = t2 + self.t_plant_pulse_fusion_ramp + t4 = t3 + self.t_plant_pulse_burn + t5 = t4 + self.t_plant_pulse_plasma_current_ramp_down + t6 = t5 + self.t_plant_pulse_dwell + return (t0, t1, t2, t3, t4, t5, t6) + + class Pulse(Model): """Class containing pulsed reactor calculations""" From 8f51b8b7c106a78c8f25b3453c951271b70638b1 Mon Sep 17 00:00:00 2001 From: mn3981 Date: Mon, 3 Aug 2026 15:13:56 +0100 Subject: [PATCH 02/11] Enhance PulseTimings class with timing interval properties and refactor PFCoil to utilize it --- process/models/pfcoil.py | 36 ++++++++++++------------------------ process/models/pulse.py | 8 ++++++++ 2 files changed, 20 insertions(+), 24 deletions(-) diff --git a/process/models/pfcoil.py b/process/models/pfcoil.py index 686c6d6d02..dda47ce6f0 100644 --- a/process/models/pfcoil.py +++ b/process/models/pfcoil.py @@ -29,6 +29,7 @@ calculate_tresca_stress, calculate_von_mises_stress, ) +from process.models.pulse import PulseTimings from process.models.superconductors import ( SuperconductorMaterial, SuperconductorModel, @@ -162,27 +163,6 @@ def pfcoil(self): * self.data.pf_coil.f_j_cs_start_pulse_end_flat_top ) - # Set up array of times - self.data.times.t_pulse_cumulative[0] = 0.0e0 - self.data.times.t_pulse_cumulative[1] = ( - self.data.times.t_plant_pulse_coil_precharge - ) - self.data.times.t_pulse_cumulative[2] = ( - self.data.times.t_pulse_cumulative[1] - + self.data.times.t_plant_pulse_plasma_current_ramp_up - ) - self.data.times.t_pulse_cumulative[3] = ( - self.data.times.t_pulse_cumulative[2] - + self.data.times.t_plant_pulse_fusion_ramp - ) - self.data.times.t_pulse_cumulative[4] = ( - self.data.times.t_pulse_cumulative[3] + self.data.times.t_plant_pulse_burn - ) - self.data.times.t_pulse_cumulative[5] = ( - self.data.times.t_pulse_cumulative[4] - + self.data.times.t_plant_pulse_plasma_current_ramp_down - ) - # Set up call to MHD scaling routine for coil currents. # First break up Central Solenoid solenoid into 'filaments' @@ -2740,12 +2720,20 @@ def outvolt(self): op.write(self.outfile, "\t" * 8 + "time (sec)") line = "\t\t" - for k in range(6): - line += f"\t\t{self.data.times.t_pulse_cumulative[k]:.2f}" + pulse_timings = PulseTimings( + t_plant_pulse_coil_precharge=self.data.times.t_plant_pulse_coil_precharge, + t_plant_pulse_plasma_current_ramp_up=self.data.times.t_plant_pulse_plasma_current_ramp_up, + t_plant_pulse_fusion_ramp=self.data.times.t_plant_pulse_fusion_ramp, + t_plant_pulse_burn=self.data.times.t_plant_pulse_burn, + t_plant_pulse_plasma_current_ramp_down=self.data.times.t_plant_pulse_plasma_current_ramp_down, + t_plant_pulse_dwell=self.data.times.t_plant_pulse_dwell, + ) + for k in range(pulse_timings.n_timing_intervals): + line += f"\t\t{pulse_timings.cumulative[k]:.2f}" op.write(self.outfile, line) line = "\t\t" - for k in range(6): + for k in range(pulse_timings.n_timing_intervals): label = self.data.times.timelabel[k] line += f"\t\t{label}" op.write(self.outfile, line) diff --git a/process/models/pulse.py b/process/models/pulse.py index c1d516dcb8..24af721ca8 100644 --- a/process/models/pulse.py +++ b/process/models/pulse.py @@ -65,6 +65,14 @@ def cumulative(self) -> tuple[float, float, float, float, float, float, float]: t6 = t5 + self.t_plant_pulse_dwell return (t0, t1, t2, t3, t4, t5, t6) + @property + def n_timing_points(self) -> int: + return len(self.cumulative) + + @property + def n_timing_intervals(self) -> int: + return int(self.n_timing_points - 1) + class Pulse(Model): """Class containing pulsed reactor calculations""" From 56966c9d45ae69ec2f3f3dbce9df94feec41f9f1 Mon Sep 17 00:00:00 2001 From: mn3981 Date: Mon, 3 Aug 2026 16:59:33 +0100 Subject: [PATCH 03/11] Refactor PulseTimings class to enhance timing calculations and add properties for PF coil active phases --- process/models/pulse.py | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/process/models/pulse.py b/process/models/pulse.py index 24af721ca8..4c2cf11355 100644 --- a/process/models/pulse.py +++ b/process/models/pulse.py @@ -55,7 +55,10 @@ def total(self) -> float: return self.no_burn + self.t_plant_pulse_burn @property - def cumulative(self) -> tuple[float, float, float, float, float, float, float]: + def total_pulse_cumulative( + self, + ) -> tuple[float, float, float, float, float, float, float]: + """Calculate the cumulative timing points for all pulse phases.""" t0 = 0.0 t1 = t0 + self.t_plant_pulse_coil_precharge t2 = t1 + self.t_plant_pulse_plasma_current_ramp_up @@ -66,12 +69,29 @@ def cumulative(self) -> tuple[float, float, float, float, float, float, float]: return (t0, t1, t2, t3, t4, t5, t6) @property - def n_timing_points(self) -> int: - return len(self.cumulative) + def n_pulse_points_total(self) -> int: + """Calculate the total number of timing points for all pulse phases.""" + return len(self.total_pulse_cumulative) @property - def n_timing_intervals(self) -> int: - return int(self.n_timing_points - 1) + def n_pulse_points_intervals_total(self) -> int: + """Calculate the total number of timing intervals for all pulse phases.""" + return int(self.n_pulse_points_total - 1) + + @property + def pf_active_cumulative(self) -> tuple[float, float, float, float, float, float]: + """Calculate the cumulative timing points for PF coil active phases.""" + return self.total_pulse_cumulative[:-1] # Exclude the last point (dwell) + + @property + def n_pf_active_points_total(self) -> int: + """Calculate the total number of timing points for PF coil active phases.""" + return len(self.pf_active_cumulative) + + @property + def n_pf_active_points_intervals(self) -> int: + """Calculate the total number of timing intervals for PF coil active phases.""" + return int(self.n_pf_active_points_total - 1) class Pulse(Model): From aaec5d89dfa34eba4c5dac9735917b3e946730f4 Mon Sep 17 00:00:00 2001 From: mn3981 Date: Mon, 3 Aug 2026 17:22:41 +0100 Subject: [PATCH 04/11] Refactor PFCoil power model to utilize PulseTimings dataclass for improved timing management --- process/models/pfcoil.py | 6 +-- process/models/power.py | 80 +++++++++++++++++++++++++--------------- 2 files changed, 54 insertions(+), 32 deletions(-) diff --git a/process/models/pfcoil.py b/process/models/pfcoil.py index dda47ce6f0..86afcee972 100644 --- a/process/models/pfcoil.py +++ b/process/models/pfcoil.py @@ -2728,12 +2728,12 @@ def outvolt(self): t_plant_pulse_plasma_current_ramp_down=self.data.times.t_plant_pulse_plasma_current_ramp_down, t_plant_pulse_dwell=self.data.times.t_plant_pulse_dwell, ) - for k in range(pulse_timings.n_timing_intervals): - line += f"\t\t{pulse_timings.cumulative[k]:.2f}" + for k in range(pulse_timings.n_pf_active_points_total): + line += f"\t\t{pulse_timings.pf_active_cumulative[k]:.2f}" op.write(self.outfile, line) line = "\t\t" - for k in range(pulse_timings.n_timing_intervals): + for k in range(pulse_timings.n_pf_active_points_total): label = self.data.times.timelabel[k] line += f"\t\t{label}" op.write(self.outfile, line) diff --git a/process/models/power.py b/process/models/power.py index f55bc0f784..6186ed8189 100644 --- a/process/models/power.py +++ b/process/models/power.py @@ -13,6 +13,7 @@ from process.core.model import Model from process.data_structure.blanket_variables import BlktModelTypes from process.data_structure.pfcoil_variables import NGC2, PFConductorModel +from process.models.pulse import PulseTimings class PumpingPowerModelTypes(IntEnum): @@ -50,7 +51,17 @@ def output(self): self.tfpwr(output=True) # Poloidal field coil power model ! - self.pfpwr(output=True) + self.pfpwr( + output=True, + PulseTimings=PulseTimings( + t_plant_pulse_coil_precharge=self.data.times.t_plant_pulse_coil_precharge, + t_plant_pulse_plasma_current_ramp_up=self.data.times.t_plant_pulse_plasma_current_ramp_up, + t_plant_pulse_fusion_ramp=self.data.times.t_plant_pulse_fusion_ramp, + t_plant_pulse_burn=self.data.times.t_plant_pulse_burn, + t_plant_pulse_plasma_current_ramp_down=self.data.times.t_plant_pulse_plasma_current_ramp_down, + t_plant_pulse_dwell=self.data.times.t_plant_pulse_dwell, + ), + ) # Plant AC power requirements self.acpow(output=True) @@ -67,7 +78,17 @@ def run(self): self.tfpwr(output=False) # Poloidal field coil power model - self.pfpwr(output=False) + self.pfpwr( + output=False, + PulseTimings=PulseTimings( + t_plant_pulse_coil_precharge=self.data.times.t_plant_pulse_coil_precharge, + t_plant_pulse_plasma_current_ramp_up=self.data.times.t_plant_pulse_plasma_current_ramp_up, + t_plant_pulse_fusion_ramp=self.data.times.t_plant_pulse_fusion_ramp, + t_plant_pulse_burn=self.data.times.t_plant_pulse_burn, + t_plant_pulse_plasma_current_ramp_down=self.data.times.t_plant_pulse_plasma_current_ramp_down, + t_plant_pulse_dwell=self.data.times.t_plant_pulse_dwell, + ), + ) # Plant heat transport part 1 self.component_thermal_powers() @@ -277,7 +298,7 @@ def _pf_loss_interval_total_j( return e_loss_pf_store_j + e_loss_pf_psu_j + e_loss_pf_bus_j - def pfpwr(self, output: bool): + def pfpwr(self, output: bool, PulseTimings: PulseTimings = PulseTimings): """PF coil power supply requirements This routine calculates the MVA, power and energy requirements @@ -292,8 +313,8 @@ def pfpwr(self, output: bool): output: """ + pulse_timings = PulseTimings # Local aliases for readability (no functional change) - t_pulse_cumulative = self.data.times.t_pulse_cumulative # [s] c_pf_coil_turn = self.data.pf_coil.c_pf_coil_turn # [A] ind_pf_cs_plasma_mutual = self.data.pf_coil.ind_pf_cs_plasma_mutual # [H] f_p_pf_energy_store_loss = ( @@ -310,16 +331,13 @@ def pfpwr(self, output: bool): p_pf_circuit_resistive_peak = np.zeros((NGC2,)) vpfi = np.zeros((NGC2,)) psmva = np.zeros((NGC2,)) - poloidalenergy = np.zeros((6,)) - inductxcurrent = np.zeros((6,)) - pfdissipation = np.zeros((5,)) + poloidalenergy = np.zeros((pulse_timings.n_pf_active_points_total,)) + inductxcurrent = np.zeros((pulse_timings.n_pf_active_points_total,)) + pfdissipation = np.zeros((pulse_timings.n_pf_active_points_intervals,)) # Bus length pfbusl = 8.0e0 * self.data.physics.rmajor + 140.0e0 - # Find power requirements for PF coils at - # self.data.times.t_pulse_cumulative(ktim) - # PF coil resistive power requirements # Bussing losses assume aluminium bussing with 100 A/cm**2 ic = -1 @@ -433,26 +451,27 @@ def pfpwr(self, output: bool): ) # Voltage in circuit idx_pf_coil at time, - # self.data.times.t_pulse_cumulative(3), + # pulse_timings.pf_active_cumulative[3], # due to changes in coil currents vpfi[idx_pf_coil] += vpfij # MVA in circuit idx_pf_coil at time, - # self.data.times.t_pulse_cumulative(3) due to changes in current + # pulse_timings.pf_active_cumulative[3] due to changes in current powpfii[idx_pf_coil] += ( vpfij * c_pf_coil_turn[idx_pf_coil, 2] / 1.0e6 ) # Term used for calculating stored energy at each time - for idx_time in range(6): + for idx_time in range(pulse_timings.n_pf_active_points_total): inductxcurrent[idx_time] += ( ind_pf_cs_plasma_mutual[idx_pf_coil, idx_circuit] * c_pf_coil_turn[idx_circuit, idx_time] ) # Stored magnetic energy of the poloidal field at each time - # idx_time is the time INDEX. 't_pulse_cumulative' is the time. - for idx_time in range(6): + # idx_time is the time INDEX. 'pulse_timings.pf_active_cumulative' is + # the time. + for idx_time in range(pulse_timings.n_pf_active_points_total): poloidalenergy[idx_time] += ( 0.5e0 * inductxcurrent[idx_time] @@ -460,8 +479,8 @@ def pfpwr(self, output: bool): ) # Resistive power in circuits at times - # self.data.times.t_pulse_cumulative(3) and - # self.data.times.t_pulse_cumulative(5) respectively (MW) + # pulse_timings.pf_active_cumulative[3] and + # pulse_timings.pf_active_cumulative[5] respectively (MW) powpfr += ( self.data.pf_coil.n_pf_coil_turns[idx_pf_coil] * c_pf_coil_turn[idx_pf_coil, 2] @@ -476,14 +495,15 @@ def pfpwr(self, output: bool): ) powpfi += powpfii[idx_pf_coil] - for idx_time_interval in range(5): + for idx_time_interval in range(pulse_timings.n_pf_active_points_intervals): # Stored magnetic energy of the poloidal field at each time - # idx_time_interval is the time index. 't_pulse_cumulative' is the time. + # idx_time_interval is the time index. 'pulse_timings.pf_active_cumulative' + # is the time. # Mean rate of change of stored energy between time and time+1 if ( abs( - t_pulse_cumulative[idx_time_interval + 1] - - t_pulse_cumulative[idx_time_interval] + pulse_timings.pf_active_cumulative[idx_time_interval + 1] + - pulse_timings.pf_active_cumulative[idx_time_interval] ) > 1.0e0 ): @@ -491,16 +511,16 @@ def pfpwr(self, output: bool): poloidalenergy[idx_time_interval + 1] - poloidalenergy[idx_time_interval] ) / ( - t_pulse_cumulative[idx_time_interval + 1] - - t_pulse_cumulative[idx_time_interval] + pulse_timings.pf_active_cumulative[idx_time_interval + 1] + - pulse_timings.pf_active_cumulative[idx_time_interval] ) else: # Flag when an interval is small or zero MDK 30/11/16 self.data.pf_power.poloidalpower[idx_time_interval] = 9.9e9 dt_pulse_phase_s = ( - t_pulse_cumulative[idx_time_interval + 1] - - t_pulse_cumulative[idx_time_interval] + pulse_timings.pf_active_cumulative[idx_time_interval + 1] + - pulse_timings.pf_active_cumulative[idx_time_interval] ) # Electrical energy dissipated in PFC power supplies as they increase or @@ -520,9 +540,13 @@ def pfpwr(self, output: bool): # Mean power dissipated # The flat top duration (time 4 to 5) is the denominator, as this is the time # when electricity is generated. - if t_pulse_cumulative[4] - t_pulse_cumulative[3] > 1.0e0: + if ( + pulse_timings.pf_active_cumulative[4] - pulse_timings.pf_active_cumulative[3] + > 1.0e0 + ): pfpower = sum(pfdissipation[:]) / ( - t_pulse_cumulative[4] - t_pulse_cumulative[3] + pulse_timings.pf_active_cumulative[4] + - pulse_timings.pf_active_cumulative[3] ) else: # Give up when an interval is small or zero. @@ -667,8 +691,6 @@ def pfpwr(self, output: bool): po.ocmmnt(self.outfile, "Energy stored in poloidal magnetic field :") po.oblnkl(self.outfile) - # write(self.outfile,50)(self.data.times.t_pulse_cumulative(time),time=1,6) - def acpow(self, output: bool): """AC power requirements From 1bbe31fc4359f5d147ec0346be41cda071fa0c51 Mon Sep 17 00:00:00 2001 From: mn3981 Date: Tue, 4 Aug 2026 10:52:56 +0100 Subject: [PATCH 05/11] Refactor plot functions to utilize PulseTimings dataclass for improved timing management --- process/core/io/plot/summary.py | 155 ++++++++++++++++---------------- 1 file changed, 75 insertions(+), 80 deletions(-) diff --git a/process/core/io/plot/summary.py b/process/core/io/plot/summary.py index e92d96ff09..f3d1080b57 100644 --- a/process/core/io/plot/summary.py +++ b/process/core/io/plot/summary.py @@ -79,6 +79,7 @@ PlasmaGeometryModelType, PlasmaShapeModelType, ) +from process.models.pulse import PulseTimings from process.models.superconductors import SuperconductorModel from process.models.tfcoil.base import ( TFCoilShapeModel, @@ -3200,26 +3201,21 @@ def plot_main_plasma_information( def plot_current_profiles_over_time(axis: plt.Axes, mfile: MFile, scan: int): """Plots the current profiles over time for PF circuits, CS coil, and plasma.""" - t_plant_pulse_coil_precharge = mfile.get("t_plant_pulse_coil_precharge", scan=scan) - t_plant_pulse_plasma_current_ramp_up = mfile.get( - "t_plant_pulse_plasma_current_ramp_up", scan=scan - ) - t_plant_pulse_fusion_ramp = mfile.get("t_plant_pulse_fusion_ramp", scan=scan) - t_plant_pulse_burn = mfile.get("t_plant_pulse_burn", scan=scan) - t_plant_pulse_plasma_current_ramp_down = mfile.get( - "t_plant_pulse_plasma_current_ramp_down", scan=scan + pulse_timings = PulseTimings( + t_plant_pulse_coil_precharge=mfile.get( + "t_plant_pulse_coil_precharge", scan=scan + ), + t_plant_pulse_plasma_current_ramp_up=mfile.get( + "t_plant_pulse_plasma_current_ramp_up", scan=scan + ), + t_plant_pulse_fusion_ramp=mfile.get("t_plant_pulse_fusion_ramp", scan=scan), + t_plant_pulse_burn=mfile.get("t_plant_pulse_burn", scan=scan), + t_plant_pulse_plasma_current_ramp_down=mfile.get( + "t_plant_pulse_plasma_current_ramp_down", scan=scan + ), + t_plant_pulse_dwell=mfile.get("t_plant_pulse_dwell", scan=scan), ) - # Define a cumulative sum list for each point in the pulse - t_steps = np.cumsum([ - 0, - t_plant_pulse_coil_precharge, - t_plant_pulse_plasma_current_ramp_up, - t_plant_pulse_fusion_ramp, - t_plant_pulse_burn, - t_plant_pulse_plasma_current_ramp_down, - ]) - # Find the number of PF circuits, n_pf_cs_plasma_circuits includes the CS and plasma circuits n_pf_cs_plasma_circuits = mfile.get("n_pf_cs_plasma_circuits", scan=scan) @@ -3228,11 +3224,12 @@ def plot_current_profiles_over_time(axis: plt.Axes, mfile: MFile, scan: int): pf_circuits = {} for i in range(int(n_pf_cs_plasma_circuits - 2)): pf_circuits[f"PF Circuit {i}"] = [ - mfile.get(f"pfc{i}t{j}", scan=scan) for j in range(6) + mfile.get(f"pfc{i}t{j}", scan=scan) + for j in range(pulse_timings.n_pf_active_points_total) ] # Change from 0 to 1 index to align with poloidal cross-section plot numbering axis.plot( - t_steps, + pulse_timings.pf_active_cumulative, pf_circuits[f"PF Circuit {i}"], label=f"PF Coil {i + 1}", linestyle="--", @@ -3240,8 +3237,16 @@ def plot_current_profiles_over_time(axis: plt.Axes, mfile: MFile, scan: int): # Since CS may not always be present try to retireve values try: - cs_circuit = [mfile.get(f"cs_t{i}", scan=scan) for i in range(6)] - axis.plot(t_steps, cs_circuit, label="CS Coil", linestyle="--") + cs_circuit = [ + mfile.get(f"cs_t{i}", scan=scan) + for i in range(pulse_timings.n_pf_active_points_total) + ] + axis.plot( + pulse_timings.pf_active_cumulative, + cs_circuit, + label="CS Coil", + linestyle="--", + ) except KeyError: pass @@ -3253,7 +3258,7 @@ def plot_current_profiles_over_time(axis: plt.Axes, mfile: MFile, scan: int): plasmat5 = mfile.get("plasmat5", scan=scan) # x-coirdinates for the plasma current - x_plasma = t_steps[1:] + x_plasma = pulse_timings.pf_active_cumulative[1:] # x-coirdinates for the plasma current y_plasma = [plasmat1, plasmat2, plasmat3, plasmat4, plasmat5] @@ -3266,7 +3271,7 @@ def plot_current_profiles_over_time(axis: plt.Axes, mfile: MFile, scan: int): # Annotate key points # Create a secondary x-axis for annotations secax = axis.secondary_xaxis("bottom") - secax.set_xticks(t_steps) + secax.set_xticks(pulse_timings.pf_active_cumulative) secax.set_xticklabels( [ "Precharge", @@ -3299,37 +3304,34 @@ def plot_current_profiles_over_time(axis: plt.Axes, mfile: MFile, scan: int): def plot_system_power_profiles_over_time(axis: plt.Axes, mfile: MFile, scan: int, fig): """Plots the power profiles over time for various systems.""" - t_precharge = mfile.get("t_plant_pulse_coil_precharge", scan=scan) - t_current_ramp_up = mfile.get("t_plant_pulse_plasma_current_ramp_up", scan=scan) - t_fusion_ramp = mfile.get("t_plant_pulse_fusion_ramp", scan=scan) - t_burn = mfile.get("t_plant_pulse_burn", scan=scan) - t_ramp_down = mfile.get("t_plant_pulse_plasma_current_ramp_down", scan=scan) - t_between_pulse = mfile.get("t_plant_pulse_dwell", scan=scan) - - # Define a cumulative sum list for each point in the pulse - t_steps = np.cumsum([ - 0, - t_precharge, - t_current_ramp_up, - t_fusion_ramp, - t_burn, - t_ramp_down, - t_between_pulse, - ]) + pulse_timings = PulseTimings( + t_plant_pulse_coil_precharge=mfile.get( + "t_plant_pulse_coil_precharge", scan=scan + ), + t_plant_pulse_plasma_current_ramp_up=mfile.get( + "t_plant_pulse_plasma_current_ramp_up", scan=scan + ), + t_plant_pulse_fusion_ramp=mfile.get("t_plant_pulse_fusion_ramp", scan=scan), + t_plant_pulse_burn=mfile.get("t_plant_pulse_burn", scan=scan), + t_plant_pulse_plasma_current_ramp_down=mfile.get( + "t_plant_pulse_plasma_current_ramp_down", scan=scan + ), + t_plant_pulse_dwell=mfile.get("t_plant_pulse_dwell", scan=scan), + ) # Create empty arrays for the power at each time step for each system power_profiles = { - "Fusion Power": np.zeros(len(t_steps)), - "Plant Base Load": np.zeros(len(t_steps)), - "Cryo Plant": np.zeros(len(t_steps)), - "Tritium Plant": np.zeros(len(t_steps)), - "Vacuum Pumps": np.zeros(len(t_steps)), - "TF Coil Supplies": np.zeros(len(t_steps)), - "PF Coil Supplies": np.zeros(len(t_steps)), - "Coolant Pump Elec Total": np.zeros(len(t_steps)), - "HCD Electric Total": np.zeros(len(t_steps)), - "Gross Electric Power": np.zeros(len(t_steps)), - "Net Electric Power": np.zeros(len(t_steps)), + "Fusion Power": np.zeros(pulse_timings.n_pulse_points_total), + "Plant Base Load": np.zeros(pulse_timings.n_pulse_points_total), + "Cryo Plant": np.zeros(pulse_timings.n_pulse_points_total), + "Tritium Plant": np.zeros(pulse_timings.n_pulse_points_total), + "Vacuum Pumps": np.zeros(pulse_timings.n_pulse_points_total), + "TF Coil Supplies": np.zeros(pulse_timings.n_pulse_points_total), + "PF Coil Supplies": np.zeros(pulse_timings.n_pulse_points_total), + "Coolant Pump Elec Total": np.zeros(pulse_timings.n_pulse_points_total), + "HCD Electric Total": np.zeros(pulse_timings.n_pulse_points_total), + "Gross Electric Power": np.zeros(pulse_timings.n_pulse_points_total), + "Net Electric Power": np.zeros(pulse_timings.n_pulse_points_total), } # Fill power_profiles arrays using vectorized assignment @@ -3346,7 +3348,7 @@ def plot_system_power_profiles_over_time(axis: plt.Axes, mfile: MFile, scan: int ("Coolant Pump Elec Total", "p_coolant_pump_elec_total_profile_mw"), ("HCD Electric Total", "p_hcd_electric_total_profile_mw"), ]: - for time in range(len(t_steps)): + for time in range(pulse_timings.n_pulse_points_total): power_profiles[label][time] = mfile.get(f"{key}{time}", scan=scan) # Define line styles for each system @@ -3368,7 +3370,9 @@ def plot_system_power_profiles_over_time(axis: plt.Axes, mfile: MFile, scan: int # Plot each system's power profile over time with different line styles for label, powers in power_profiles.items(): style = line_styles.get(label, "-") - axis.plot(t_steps, powers, label=label, linestyle=style) + axis.plot( + pulse_timings.total_pulse_cumulative, powers, label=label, linestyle=style + ) # Move the x-axis to 0 on the y-axis axis.spines["bottom"].set_position("zero") @@ -3376,7 +3380,7 @@ def plot_system_power_profiles_over_time(axis: plt.Axes, mfile: MFile, scan: int # Annotate key points # Create a secondary x-axis for annotations secax = axis.secondary_xaxis("bottom") - secax.set_xticks(t_steps) + secax.set_xticks(pulse_timings.total_pulse_cumulative) secax.set_xticklabels( [ "Precharge", @@ -10137,39 +10141,30 @@ def plot_cs_coil_structure( def plot_cs_stress_time_profile(axis: plt.Axes, mfile: MFile, scan: int) -> None: """Function to plot the time profile of the CS stress during the pulse.""" - t_plant_pulse_coil_precharge = mfile.get("t_plant_pulse_coil_precharge", scan=scan) - t_plant_pulse_plasma_current_ramp_up = mfile.get( - "t_plant_pulse_plasma_current_ramp_up", scan=scan - ) - t_plant_pulse_fusion_ramp = mfile.get("t_plant_pulse_fusion_ramp", scan=scan) - t_plant_pulse_burn = mfile.get("t_plant_pulse_burn", scan=scan) - t_plant_pulse_plasma_current_ramp_down = mfile.get( - "t_plant_pulse_plasma_current_ramp_down", scan=scan + pulse_timings = PulseTimings( + t_plant_pulse_coil_precharge=mfile.get( + "t_plant_pulse_coil_precharge", scan=scan + ), + t_plant_pulse_plasma_current_ramp_up=mfile.get( + "t_plant_pulse_plasma_current_ramp_up", scan=scan + ), + t_plant_pulse_fusion_ramp=mfile.get("t_plant_pulse_fusion_ramp", scan=scan), + t_plant_pulse_burn=mfile.get("t_plant_pulse_burn", scan=scan), + t_plant_pulse_plasma_current_ramp_down=mfile.get( + "t_plant_pulse_plasma_current_ramp_down", scan=scan + ), + t_plant_pulse_dwell=mfile.get("t_plant_pulse_dwell", scan=scan), ) - # Define a cumulative sum list for each point in the pulse - t_steps = np.cumsum([ - 0, - t_plant_pulse_coil_precharge, - t_plant_pulse_plasma_current_ramp_up, - t_plant_pulse_fusion_ramp, - t_plant_pulse_burn, - t_plant_pulse_plasma_current_ramp_down, - ]) - - stress_times = t_steps[ - :6 - ] # Get the first 6 time points corresponding to the stress profile - - stress_z_cs_self_midplane_profile = np.zeros(6) - for i in range(6): + stress_z_cs_self_midplane_profile = np.zeros(pulse_timings.n_pf_active_points_total) + for i in range(pulse_timings.n_pf_active_points_total): stress_z_cs_self_midplane_profile[i] = mfile.get( f"stress_z_cs_self_midplane_profile[{i}]", scan=scan ) # Plot stress vs time axis.plot( - stress_times, + pulse_timings.pf_active_cumulative, stress_z_cs_self_midplane_profile / 1e6, "o-", linewidth=2, From 2b286cc74e49ed4edae3e9f8f267f197e840aed7 Mon Sep 17 00:00:00 2001 From: mn3981 Date: Tue, 4 Aug 2026 11:32:51 +0100 Subject: [PATCH 06/11] Refactor Power model and test to utilize PulseTimings dataclass for improved pulse timing management --- process/models/power.py | 6 ++-- tests/unit/models/test_power.py | 54 ++++++++++++--------------------- 2 files changed, 23 insertions(+), 37 deletions(-) diff --git a/process/models/power.py b/process/models/power.py index 6186ed8189..e1602423b0 100644 --- a/process/models/power.py +++ b/process/models/power.py @@ -331,9 +331,9 @@ def pfpwr(self, output: bool, PulseTimings: PulseTimings = PulseTimings): p_pf_circuit_resistive_peak = np.zeros((NGC2,)) vpfi = np.zeros((NGC2,)) psmva = np.zeros((NGC2,)) - poloidalenergy = np.zeros((pulse_timings.n_pf_active_points_total,)) - inductxcurrent = np.zeros((pulse_timings.n_pf_active_points_total,)) - pfdissipation = np.zeros((pulse_timings.n_pf_active_points_intervals,)) + poloidalenergy = np.zeros(pulse_timings.n_pf_active_points_total) + inductxcurrent = np.zeros(pulse_timings.n_pf_active_points_total) + pfdissipation = np.zeros(pulse_timings.n_pf_active_points_intervals) # Bus length pfbusl = 8.0e0 * self.data.physics.rmajor + 140.0e0 diff --git a/tests/unit/models/test_power.py b/tests/unit/models/test_power.py index 41c5385a25..470fd284ff 100644 --- a/tests/unit/models/test_power.py +++ b/tests/unit/models/test_power.py @@ -3,6 +3,8 @@ import numpy as np import pytest +from process.models.pulse import PulseTimings + @pytest.fixture def power(process_models): @@ -166,7 +168,7 @@ class PfpwrParam(NamedTuple): ioptimz: Any = None - t_pulse_cumulative: Any = None + pulse_timings: PulseTimings = None intervallabel: Any = None @@ -787,20 +789,14 @@ class PfpwrParam(NamedTuple): False, ), ioptimz=1, - t_pulse_cumulative=np.array( - np.array( - ( - 0, - 500, - 677.21306969367811, - 687.21306969367811, - 10687.213069693678, - 10864.426139387357, - ), - order="F", - ), - order="F", - ).transpose(), + pulse_timings=PulseTimings( + t_plant_pulse_coil_precharge=500.0, + t_plant_pulse_plasma_current_ramp_up=177.21306969367816, + t_plant_pulse_fusion_ramp=10.0, + t_plant_pulse_burn=10000.0, + t_plant_pulse_plasma_current_ramp_down=177.21306969367816, + t_plant_pulse_dwell=500.0, + ), intervallabel=( "t_plant_pulse_coil_precharge ", "t_plant_pulse_plasma_current_ramp_up ", @@ -1429,20 +1425,14 @@ class PfpwrParam(NamedTuple): False, ), ioptimz=1, - t_pulse_cumulative=np.array( - np.array( - ( - 0, - 500, - 677.21306969367811, - 687.21306969367811, - 687.21306969367811, - 864.42613938735622, - ), - order="F", - ), - order="F", - ).transpose(), + pulse_timings=PulseTimings( + t_plant_pulse_coil_precharge=500.0, + t_plant_pulse_plasma_current_ramp_up=177.21306969367816, + t_plant_pulse_fusion_ramp=10.0, + t_plant_pulse_burn=0.0, + t_plant_pulse_plasma_current_ramp_down=177.21306969367816, + t_plant_pulse_dwell=500.0, + ), intervallabel=( "t_plant_pulse_coil_precharge ", "t_plant_pulse_plasma_current_ramp_up ", @@ -1532,17 +1522,13 @@ def test_pfpwr(pfpwrparam, monkeypatch, power): monkeypatch.setattr(power.data.numerics, "ioptimz", pfpwrparam.ioptimz) - monkeypatch.setattr( - power.data.times, "t_pulse_cumulative", pfpwrparam.t_pulse_cumulative - ) - monkeypatch.setattr( power.data.times, "t_plant_pulse_plasma_current_ramp_up", pfpwrparam.t_plant_pulse_plasma_current_ramp_up, ) - power.pfpwr(output=False) + power.pfpwr(output=False, PulseTimings=pfpwrparam.pulse_timings) assert power.data.heat_transport.peakmva == pytest.approx( pfpwrparam.expected_peakmva From b767622a76f78c0dc8a7aa0633c4c31e05aaaa67 Mon Sep 17 00:00:00 2001 From: mn3981 Date: Tue, 4 Aug 2026 11:53:17 +0100 Subject: [PATCH 07/11] Refactor TimesData and PulseTimings classes to remove unused labels and integrate point abbreviations for improved clarity in timing management --- process/data_structure/times_variables.py | 17 ----- process/models/pfcoil.py | 2 +- process/models/pulse.py | 11 +++ tests/unit/models/test_power.py | 88 +++++++++-------------- 4 files changed, 46 insertions(+), 72 deletions(-) diff --git a/process/data_structure/times_variables.py b/process/data_structure/times_variables.py index 7cd54a1067..348f12396b 100644 --- a/process/data_structure/times_variables.py +++ b/process/data_structure/times_variables.py @@ -38,23 +38,6 @@ class TimesData: ) """array of time points during plasma pulse (s)""" - timelabel: list[str] = field( - default_factory=lambda: ["Start", "BOP ", "EOR ", "BOF ", "EOF ", "EOP "] - ) - """array of time labels during plasma pulse (s)""" - - intervallabel: list[str] = field( - default_factory=lambda: [ - "t_plant_pulse_coil_precharge ", - "t_plant_pulse_plasma_current_ramp_up ", - "t_plant_pulse_fusion_ramp ", - "t_plant_pulse_burn ", - "t_plant_pulse_plasma_current_ramp_down ", - ] - ) - - """time intervals - as strings (s)""" - t_plant_pulse_plasma_current_ramp_up: float = 30.0 """Plant pulse time for plasma current to ramp up to approx. full value (s) (calculated if `i_pulsed_plant=0`) (`iteration variable 65`)""" diff --git a/process/models/pfcoil.py b/process/models/pfcoil.py index 86afcee972..a7efcfc58d 100644 --- a/process/models/pfcoil.py +++ b/process/models/pfcoil.py @@ -2734,7 +2734,7 @@ def outvolt(self): line = "\t\t" for k in range(pulse_timings.n_pf_active_points_total): - label = self.data.times.timelabel[k] + label = pulse_timings.point_abbreviations[k] line += f"\t\t{label}" op.write(self.outfile, line) diff --git a/process/models/pulse.py b/process/models/pulse.py index 4c2cf11355..ca286a3f43 100644 --- a/process/models/pulse.py +++ b/process/models/pulse.py @@ -2,6 +2,7 @@ import logging from dataclasses import dataclass +from typing import ClassVar from process.core import constants from process.core import process_output as po @@ -28,6 +29,16 @@ class PulseTimings: t_plant_pulse_dwell: float """Time for dwell (s)""" + point_abbreviations: ClassVar[tuple[str, ...]] = ( + "Start", + "BOP", + "EOR", + "BOF", + "EOF", + "EOP", + "Dwell", + ) + @property def plasma_present(self) -> float: """Calculate the total time during which plasma is present in the reactor.""" diff --git a/tests/unit/models/test_power.py b/tests/unit/models/test_power.py index 470fd284ff..2a861676fd 100644 --- a/tests/unit/models/test_power.py +++ b/tests/unit/models/test_power.py @@ -3,8 +3,6 @@ import numpy as np import pytest -from process.models.pulse import PulseTimings - @pytest.fixture def power(process_models): @@ -168,11 +166,7 @@ class PfpwrParam(NamedTuple): ioptimz: Any = None - pulse_timings: PulseTimings = None - - intervallabel: Any = None - - timelabel: Any = None + t_pulse_cumulative: Any = None t_plant_pulse_plasma_current_ramp_up: Any = None @@ -789,29 +783,20 @@ class PfpwrParam(NamedTuple): False, ), ioptimz=1, - pulse_timings=PulseTimings( - t_plant_pulse_coil_precharge=500.0, - t_plant_pulse_plasma_current_ramp_up=177.21306969367816, - t_plant_pulse_fusion_ramp=10.0, - t_plant_pulse_burn=10000.0, - t_plant_pulse_plasma_current_ramp_down=177.21306969367816, - t_plant_pulse_dwell=500.0, - ), - intervallabel=( - "t_plant_pulse_coil_precharge ", - "t_plant_pulse_plasma_current_ramp_up ", - "t_plant_pulse_fusion_ramp ", - "t_plant_pulse_burn ", - "t_plant_pulse_plasma_current_ramp_down ", - ), - timelabel=( - "Start ", - "BOP ", - "EOR ", - "BOF ", - "EOF ", - "EOP ", - ), + t_pulse_cumulative=np.array( + np.array( + ( + 0, + 500, + 677.21306969367811, + 687.21306969367811, + 10687.213069693678, + 10864.426139387357, + ), + order="F", + ), + order="F", + ).transpose(), t_plant_pulse_plasma_current_ramp_up=177.21306969367816, expected_peakmva=736.39062584245937, expected_pfckts=12, @@ -1425,29 +1410,20 @@ class PfpwrParam(NamedTuple): False, ), ioptimz=1, - pulse_timings=PulseTimings( - t_plant_pulse_coil_precharge=500.0, - t_plant_pulse_plasma_current_ramp_up=177.21306969367816, - t_plant_pulse_fusion_ramp=10.0, - t_plant_pulse_burn=0.0, - t_plant_pulse_plasma_current_ramp_down=177.21306969367816, - t_plant_pulse_dwell=500.0, - ), - intervallabel=( - "t_plant_pulse_coil_precharge ", - "t_plant_pulse_plasma_current_ramp_up ", - "t_plant_pulse_fusion_ramp ", - "t_plant_pulse_burn ", - "t_plant_pulse_plasma_current_ramp_down ", - ), - timelabel=( - "Start ", - "BOP ", - "EOR ", - "BOF ", - "EOF ", - "EOP ", - ), + t_pulse_cumulative=np.array( + np.array( + ( + 0, + 500, + 677.21306969367811, + 687.21306969367811, + 687.21306969367811, + 864.42613938735622, + ), + order="F", + ), + order="F", + ).transpose(), t_plant_pulse_plasma_current_ramp_up=177.21306969367816, expected_peakmva=90.673341440806112, expected_pfckts=12, @@ -1522,13 +1498,17 @@ def test_pfpwr(pfpwrparam, monkeypatch, power): monkeypatch.setattr(power.data.numerics, "ioptimz", pfpwrparam.ioptimz) + monkeypatch.setattr( + power.data.times, "t_pulse_cumulative", pfpwrparam.t_pulse_cumulative + ) + monkeypatch.setattr( power.data.times, "t_plant_pulse_plasma_current_ramp_up", pfpwrparam.t_plant_pulse_plasma_current_ramp_up, ) - power.pfpwr(output=False, PulseTimings=pfpwrparam.pulse_timings) + power.pfpwr(output=False) assert power.data.heat_transport.peakmva == pytest.approx( pfpwrparam.expected_peakmva From c3125cd64b5513ab0d46ad52c01648e2f20c35e1 Mon Sep 17 00:00:00 2001 From: mn3981 Date: Tue, 4 Aug 2026 13:16:44 +0100 Subject: [PATCH 08/11] Refactor Power model and test to utilize PulseTimings dataclass for timing management --- process/models/power.py | 3 +- tests/unit/models/test_power.py | 55 ++++++++++++--------------------- 2 files changed, 22 insertions(+), 36 deletions(-) diff --git a/process/models/power.py b/process/models/power.py index e1602423b0..edffde1732 100644 --- a/process/models/power.py +++ b/process/models/power.py @@ -412,7 +412,8 @@ def pfpwr(self, output: bool, PulseTimings: PulseTimings = PulseTimings): ) # Inductive MVA requirements, and stored energy - delktim = self.data.times.t_plant_pulse_plasma_current_ramp_up + # Use the timing object passed into pfpwr as the source of truth. + delktim = pulse_timings.t_plant_pulse_plasma_current_ramp_up # PF system (including Central Solenoid solenoid) inductive MVA requirements # self.data.pf_coil.c_pf_coil_turn(i,j) : current per turn of coil i at (end) diff --git a/tests/unit/models/test_power.py b/tests/unit/models/test_power.py index 2a861676fd..70a2a00837 100644 --- a/tests/unit/models/test_power.py +++ b/tests/unit/models/test_power.py @@ -3,6 +3,8 @@ import numpy as np import pytest +from process.models.pulse import PulseTimings + @pytest.fixture def power(process_models): @@ -166,7 +168,7 @@ class PfpwrParam(NamedTuple): ioptimz: Any = None - t_pulse_cumulative: Any = None + pulse_timings: PulseTimings = None t_plant_pulse_plasma_current_ramp_up: Any = None @@ -783,20 +785,14 @@ class PfpwrParam(NamedTuple): False, ), ioptimz=1, - t_pulse_cumulative=np.array( - np.array( - ( - 0, - 500, - 677.21306969367811, - 687.21306969367811, - 10687.213069693678, - 10864.426139387357, - ), - order="F", - ), - order="F", - ).transpose(), + pulse_timings=PulseTimings( + t_plant_pulse_coil_precharge=500, + t_plant_pulse_plasma_current_ramp_up=177.21306969367816, + t_plant_pulse_fusion_ramp=10, + t_plant_pulse_burn=10000, + t_plant_pulse_plasma_current_ramp_down=177.21306969367816, + t_plant_pulse_dwell=500, + ), t_plant_pulse_plasma_current_ramp_up=177.21306969367816, expected_peakmva=736.39062584245937, expected_pfckts=12, @@ -1410,21 +1406,14 @@ class PfpwrParam(NamedTuple): False, ), ioptimz=1, - t_pulse_cumulative=np.array( - np.array( - ( - 0, - 500, - 677.21306969367811, - 687.21306969367811, - 687.21306969367811, - 864.42613938735622, - ), - order="F", - ), - order="F", - ).transpose(), - t_plant_pulse_plasma_current_ramp_up=177.21306969367816, + pulse_timings=PulseTimings( + t_plant_pulse_coil_precharge=500, + t_plant_pulse_plasma_current_ramp_up=177.21306969367816, + t_plant_pulse_fusion_ramp=10, + t_plant_pulse_burn=0.0, + t_plant_pulse_plasma_current_ramp_down=177.21306969367816, + t_plant_pulse_dwell=500, + ), expected_peakmva=90.673341440806112, expected_pfckts=12, expected_peakpoloidalpower=9900, @@ -1498,17 +1487,13 @@ def test_pfpwr(pfpwrparam, monkeypatch, power): monkeypatch.setattr(power.data.numerics, "ioptimz", pfpwrparam.ioptimz) - monkeypatch.setattr( - power.data.times, "t_pulse_cumulative", pfpwrparam.t_pulse_cumulative - ) - monkeypatch.setattr( power.data.times, "t_plant_pulse_plasma_current_ramp_up", pfpwrparam.t_plant_pulse_plasma_current_ramp_up, ) - power.pfpwr(output=False) + power.pfpwr(output=False, PulseTimings=pfpwrparam.pulse_timings) assert power.data.heat_transport.peakmva == pytest.approx( pfpwrparam.expected_peakmva From 72047b0c3fd1c4c4dbef1b6a082e91a1d0eebdf6 Mon Sep 17 00:00:00 2001 From: mn3981 Date: Tue, 4 Aug 2026 13:30:56 +0100 Subject: [PATCH 09/11] Refactor plot functions to utilize updated point labels from PulseTimings dataclass for improved clarity in timing representation --- process/core/io/plot/summary.py | 21 ++++----------------- process/models/pulse.py | 11 ++++++++++- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/process/core/io/plot/summary.py b/process/core/io/plot/summary.py index f3d1080b57..c30e5ff3fe 100644 --- a/process/core/io/plot/summary.py +++ b/process/core/io/plot/summary.py @@ -3273,14 +3273,9 @@ def plot_current_profiles_over_time(axis: plt.Axes, mfile: MFile, scan: int): secax = axis.secondary_xaxis("bottom") secax.set_xticks(pulse_timings.pf_active_cumulative) secax.set_xticklabels( - [ - "Precharge", - r"$I_{\text{P}}$ Ramp-Up", - "Fusion Ramp", - "Burn", - "Ramp Down", - "Between Pulse", - ], + pulse_timings.point_labels[ + :-1 + ], # Exclude the last label as it corresponds to the dwell period rotation=60, ) secax.tick_params(axis="x", which="major") @@ -3382,15 +3377,7 @@ def plot_system_power_profiles_over_time(axis: plt.Axes, mfile: MFile, scan: int secax = axis.secondary_xaxis("bottom") secax.set_xticks(pulse_timings.total_pulse_cumulative) secax.set_xticklabels( - [ - "Precharge", - r"$I_{\text{P}}$ Ramp-Up", - "Fusion Ramp", - "Burn", - "Ramp Down", - "Between Pulse", - "Restart Pulse", - ], + pulse_timings.point_labels, rotation=60, ) secax.tick_params(axis="x", which="major") diff --git a/process/models/pulse.py b/process/models/pulse.py index ca286a3f43..0793226a39 100644 --- a/process/models/pulse.py +++ b/process/models/pulse.py @@ -30,7 +30,6 @@ class PulseTimings: """Time for dwell (s)""" point_abbreviations: ClassVar[tuple[str, ...]] = ( - "Start", "BOP", "EOR", "BOF", @@ -39,6 +38,16 @@ class PulseTimings: "Dwell", ) + point_labels: ClassVar[tuple[str, ...]] = ( + "Coil precharge", + "$I_{\\text{p}}$ Ramp-Up", + "Fusion ramp", + "Burn", + "$I_{\\text{p}}$ ramp-down", + "Dwell", + "Restart pulse", + ) + @property def plasma_present(self) -> float: """Calculate the total time during which plasma is present in the reactor.""" From 02e6759014841306d90f44dcff3e1c15203a89dc Mon Sep 17 00:00:00 2001 From: mn3981 Date: Tue, 4 Aug 2026 13:32:46 +0100 Subject: [PATCH 10/11] Remove unused t_pulse_cumulative field from TimesData dataclass for cleaner code --- process/data_structure/times_variables.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/process/data_structure/times_variables.py b/process/data_structure/times_variables.py index 348f12396b..c55acd6c29 100644 --- a/process/data_structure/times_variables.py +++ b/process/data_structure/times_variables.py @@ -33,11 +33,6 @@ class TimesData: t_plant_pulse_fusion_ramp: float = 10.0 """time for plasma temperature and density rise to full values (s)""" - t_pulse_cumulative: list[float] = field( - default_factory=lambda: np.zeros(6, dtype=np.float64) - ) - """array of time points during plasma pulse (s)""" - t_plant_pulse_plasma_current_ramp_up: float = 30.0 """Plant pulse time for plasma current to ramp up to approx. full value (s) (calculated if `i_pulsed_plant=0`) (`iteration variable 65`)""" From 438f7ee7ce8f5585be51128cb2fbd44b8e4602a2 Mon Sep 17 00:00:00 2001 From: mn3981 Date: Fri, 7 Aug 2026 10:39:04 +0100 Subject: [PATCH 11/11] Refactor Power model and tests to standardize PulseTimings parameter naming and enhance validation in PulseTimings dataclass --- process/core/io/plot/summary.py | 6 +- process/models/power.py | 133 +++++++++++++++++++------------- process/models/pulse.py | 19 ++++- tests/unit/models/test_power.py | 2 +- 4 files changed, 100 insertions(+), 60 deletions(-) diff --git a/process/core/io/plot/summary.py b/process/core/io/plot/summary.py index c30e5ff3fe..1b3f821e72 100644 --- a/process/core/io/plot/summary.py +++ b/process/core/io/plot/summary.py @@ -3271,7 +3271,8 @@ def plot_current_profiles_over_time(axis: plt.Axes, mfile: MFile, scan: int): # Annotate key points # Create a secondary x-axis for annotations secax = axis.secondary_xaxis("bottom") - secax.set_xticks(pulse_timings.pf_active_cumulative) + # Exclude the dwell point so tick positions and labels remain aligned. + secax.set_xticks(pulse_timings.pf_active_cumulative[:-1]) secax.set_xticklabels( pulse_timings.point_labels[ :-1 @@ -3375,7 +3376,8 @@ def plot_system_power_profiles_over_time(axis: plt.Axes, mfile: MFile, scan: int # Annotate key points # Create a secondary x-axis for annotations secax = axis.secondary_xaxis("bottom") - secax.set_xticks(pulse_timings.total_pulse_cumulative) + # Label phase starts only (exclude final end-of-dwell point). + secax.set_xticks(pulse_timings.total_pulse_cumulative[:-1]) secax.set_xticklabels( pulse_timings.point_labels, rotation=60, diff --git a/process/models/power.py b/process/models/power.py index edffde1732..1065b03060 100644 --- a/process/models/power.py +++ b/process/models/power.py @@ -53,7 +53,7 @@ def output(self): # Poloidal field coil power model ! self.pfpwr( output=True, - PulseTimings=PulseTimings( + pulse_timings=PulseTimings( t_plant_pulse_coil_precharge=self.data.times.t_plant_pulse_coil_precharge, t_plant_pulse_plasma_current_ramp_up=self.data.times.t_plant_pulse_plasma_current_ramp_up, t_plant_pulse_fusion_ramp=self.data.times.t_plant_pulse_fusion_ramp, @@ -80,7 +80,7 @@ def run(self): # Poloidal field coil power model self.pfpwr( output=False, - PulseTimings=PulseTimings( + pulse_timings=PulseTimings( t_plant_pulse_coil_precharge=self.data.times.t_plant_pulse_coil_precharge, t_plant_pulse_plasma_current_ramp_up=self.data.times.t_plant_pulse_plasma_current_ramp_up, t_plant_pulse_fusion_ramp=self.data.times.t_plant_pulse_fusion_ramp, @@ -298,7 +298,7 @@ def _pf_loss_interval_total_j( return e_loss_pf_store_j + e_loss_pf_psu_j + e_loss_pf_bus_j - def pfpwr(self, output: bool, PulseTimings: PulseTimings = PulseTimings): + def pfpwr(self, output: bool, pulse_timings: PulseTimings): """PF coil power supply requirements This routine calculates the MVA, power and energy requirements @@ -306,14 +306,22 @@ def pfpwr(self, output: bool, PulseTimings: PulseTimings = PulseTimings): The routine checks at the beginning of the flattop for the peak MVA, and at the end of flattop for the peak stored energy. The reactive (inductive) components use waves to calculate the - dI/dt at the time periods. + dI/dt at the time periods. + + Parameters + ---------- + output : bool + If True, write results to output files. + pulse_timings : PulseTimings + Pulse timing dataclass + + Parameters ---------- output: """ - pulse_timings = PulseTimings # Local aliases for readability (no functional change) c_pf_coil_turn = self.data.pf_coil.c_pf_coil_turn # [A] ind_pf_cs_plasma_mutual = self.data.pf_coil.ind_pf_cs_plasma_mutual # [H] @@ -1748,12 +1756,6 @@ def plant_electric_production(self): self.data.power.p_cryo_plant_electric_profile_mw, self.data.power.p_fusion_total_profile_mw, ) = self.power_profiles_over_time( - t_precharge=self.data.times.t_plant_pulse_coil_precharge, - t_current_ramp_up=self.data.times.t_plant_pulse_plasma_current_ramp_up, - t_fusion_ramp=self.data.times.t_plant_pulse_fusion_ramp, - t_burn=self.data.times.t_plant_pulse_burn, - t_ramp_down=self.data.times.t_plant_pulse_plasma_current_ramp_down, - t_between_pulse=self.data.times.t_plant_pulse_dwell, p_plant_electric_base_total_mw=self.data.heat_transport.p_plant_electric_base_total_mw, p_cryo_plant_electric_mw=self.data.heat_transport.p_cryo_plant_electric_mw, p_tritium_plant_electric_mw=self.data.heat_transport.p_tritium_plant_electric_mw, @@ -1765,6 +1767,14 @@ def plant_electric_production(self): p_fusion_total_mw=self.data.physics.p_fusion_total_mw, p_plant_electric_gross_mw=self.data.heat_transport.p_plant_electric_gross_mw, p_plant_electric_net_mw=self.data.heat_transport.p_plant_electric_net_mw, + pulse_timings=PulseTimings( + t_plant_pulse_coil_precharge=self.data.times.t_plant_pulse_coil_precharge, + t_plant_pulse_plasma_current_ramp_up=self.data.times.t_plant_pulse_plasma_current_ramp_up, + t_plant_pulse_fusion_ramp=self.data.times.t_plant_pulse_fusion_ramp, + t_plant_pulse_burn=self.data.times.t_plant_pulse_burn, + t_plant_pulse_plasma_current_ramp_down=self.data.times.t_plant_pulse_plasma_current_ramp_down, + t_plant_pulse_dwell=self.data.times.t_plant_pulse_dwell, + ), ) def cryo( @@ -2627,12 +2637,6 @@ def tfcpwr( @staticmethod def power_profiles_over_time( - t_precharge: float, - t_current_ramp_up: float, - t_fusion_ramp: float, - t_burn: float, - t_ramp_down: float, - t_between_pulse: float, p_plant_electric_base_total_mw: float, p_cryo_plant_electric_mw: float, p_tritium_plant_electric_mw: float, @@ -2644,68 +2648,87 @@ def power_profiles_over_time( p_fusion_total_mw: float, p_plant_electric_gross_mw: float, p_plant_electric_net_mw: float, - ) -> float: + pulse_timings: PulseTimings, + ) -> tuple[ + float, + float, + np.ndarray, + np.ndarray, + np.ndarray, + np.ndarray, + np.ndarray, + np.ndarray, + np.ndarray, + np.ndarray, + np.ndarray, + np.ndarray, + np.ndarray, + ]: """Calculate time-dependent power profiles for different electric systems Parameters ---------- - t_precharge : float - Precharge time (s). - t_current_ramp_up : float - Current ramp-up time (s). - t_fusion_ramp : float - Fusion ramp time (s). - t_burn : float - Burn time (s). - t_ramp_down : float - Ramp-down time (s). - t_between_pulse : float - Time between pulses (s). p_plant_electric_base_total_mw : float - Plant base electric load (MW). + Plant base electric load [MW]. p_cryo_plant_electric_mw : float - Cryogenic plant electric load (MW). + Cryogenic plant electric load [MW]. p_tritium_plant_electric_mw : float - Tritium plant electric load (MW). + Tritium plant electric load [MW]. vachtmw : float - Vacuum pumps electric load (MW). + Vacuum pumps electric load [MW]. p_tf_electric_supplies_mw : float - TF coil electric supplies (MW). + TF coil electric supplies [MW]. p_pf_electric_supplies_mw : float - PF coil electric supplies (MW). + PF coil electric supplies [MW]. p_coolant_pump_elec_total_mw : float - Total coolant pump electric load (MW). + Total coolant pump electric load [MW]. p_hcd_electric_total_mw : float - HCD electric total (MW). + HCD electric total [MW]. p_fusion_total_mw : float - Fusion power (MW). + Fusion power [MW]. p_plant_electric_gross_mw : float - Gross electric power produced (MW). + Gross electric power produced [MW]. p_plant_electric_net_mw : float - Net electric power produced (MW). + Net electric power produced [MW]. + pulse_timings : PulseTimings + Object containing pulse timing information. Returns ------- float - Total net electric energy produced over the pulse (MJ). + Total net electric energy produced over the pulse [MJ]. + float + Total net electric energy produced over the pulse [kWh]. + np.ndarray + Plant base electric load profile [MW]. + np.ndarray + Plant gross electric power profile [MW]. + np.ndarray + Plant net electric power profile [MW]. + np.ndarray + HCD electric total profile [MW]. + np.ndarray + Total coolant pump electric load profile [MW]. + np.ndarray + TF coil electric supplies profile [MW]. + np.ndarray + PF coil electric supplies profile [MW]. + np.ndarray + Vacuum pumps electric load profile [MW]. + np.ndarray + Tritium plant electric load profile [MW]. + np.ndarray + Cryogenic plant electric load profile [MW]. + np.ndarray + Fusion power profile [MW]. Notes ----- - Assumes step-function changes in power at each phase transition. - Negative values indicate power consumption (loads). """ - t_steps = np.cumsum([ - 0, - t_precharge, - t_current_ramp_up, - t_fusion_ramp, - t_burn, - t_ramp_down, - t_between_pulse, - ]) - # Number of time steps - n_steps = len(t_steps) + n_steps = pulse_timings.n_pulse_points_total # Initialize arrays for each power profile p_fusion_total_profile_mw = np.zeros(n_steps) @@ -2787,7 +2810,9 @@ def power_profiles_over_time( # Integrate net electric power over the pulse to get total energy produced (MJ) # Assume t_steps in seconds, power in MW, so energy in MJ - energy_made_mj = sp.integrate.trapezoid(p_plant_electric_net_profile_mw, t_steps) + energy_made_mj = sp.integrate.trapezoid( + p_plant_electric_net_profile_mw, pulse_timings.total_pulse_cumulative + ) energy_made_kwh = energy_made_mj / 3.6 return ( diff --git a/process/models/pulse.py b/process/models/pulse.py index 0793226a39..472ef59419 100644 --- a/process/models/pulse.py +++ b/process/models/pulse.py @@ -1,7 +1,7 @@ """Module containing the Pulse class for pulsed reactor calculations.""" import logging -from dataclasses import dataclass +from dataclasses import dataclass, fields from typing import ClassVar from process.core import constants @@ -14,7 +14,7 @@ @dataclass(frozen=True, slots=True) class PulseTimings: - """Class to hold the timing parameters for a pulsed reactor.""" + """Dataclass to hold the timing parameters for a pulsed reactor.""" t_plant_pulse_coil_precharge: float """Time for coil precharge (s)""" @@ -45,9 +45,22 @@ class PulseTimings: "Burn", "$I_{\\text{p}}$ ramp-down", "Dwell", - "Restart pulse", ) + def __post_init__(self) -> None: + """Validate class metadata against the timing fields.""" + n_timing_fields = len(fields(self)) + if len(self.point_labels) != n_timing_fields: + raise ValueError( + "PulseTimings.point_labels must contain exactly " + f"{n_timing_fields} entries; got {len(self.point_labels)}." + ) + if len(self.point_abbreviations) != n_timing_fields: + raise ValueError( + "PulseTimings.point_abbreviations must contain exactly " + f"{n_timing_fields} entries; got {len(self.point_abbreviations)}." + ) + @property def plasma_present(self) -> float: """Calculate the total time during which plasma is present in the reactor.""" diff --git a/tests/unit/models/test_power.py b/tests/unit/models/test_power.py index 70a2a00837..b52f9a1b69 100644 --- a/tests/unit/models/test_power.py +++ b/tests/unit/models/test_power.py @@ -1493,7 +1493,7 @@ def test_pfpwr(pfpwrparam, monkeypatch, power): pfpwrparam.t_plant_pulse_plasma_current_ramp_up, ) - power.pfpwr(output=False, PulseTimings=pfpwrparam.pulse_timings) + power.pfpwr(output=False, pulse_timings=pfpwrparam.pulse_timings) assert power.data.heat_transport.peakmva == pytest.approx( pfpwrparam.expected_peakmva