From 73ae95525d34bd419c086471960d4ce15173b74b Mon Sep 17 00:00:00 2001 From: mn3981 Date: Mon, 8 Jun 2026 11:23:45 +0100 Subject: [PATCH 01/25] Create initial poloidal power plotting function --- process/core/io/plot/summary.py | 300 ++++++++++++++++++++++++++++++++ 1 file changed, 300 insertions(+) diff --git a/process/core/io/plot/summary.py b/process/core/io/plot/summary.py index e92d96ff09..067cb2eb3c 100644 --- a/process/core/io/plot/summary.py +++ b/process/core/io/plot/summary.py @@ -14706,6 +14706,298 @@ def plot_blkt_structure( ) +def plot_poloidal_power_distribution( + ax: plt.Axes, + m_file: MFile, + scan: int, + radial_build: dict[str, float], + colour_scheme: Literal[1, 2], +): + """Plot the poloidal power distribution on the first wall and blanket and annotate relevant angles""" + # MFILE variables needed to plot the blkt structure and angles + rmajor = m_file.get("rmajor", scan=scan) + rminor = m_file.get("rminor", scan=scan) + dr_fw_plasma_gap_outboard = m_file.get("dr_fw_plasma_gap_outboard", scan=scan) + dr_fw_plasma_gap_inboard = m_file.get("dr_fw_plasma_gap_inboard", scan=scan) + dr_fw_inboard = m_file.get("dr_fw_inboard", scan=scan) + dr_fw_outboard = m_file.get("dr_fw_outboard", scan=scan) + dr_blkt_outboard = m_file.get("dr_blkt_outboard", scan=scan) + dr_blkt_inboard = m_file.get("dr_blkt_inboard", scan=scan) + dz_blkt_half = m_file.get("dz_blkt_half", scan=scan) + deg_blkt_outboard_poloidal_plasma = m_file.get( + "deg_blkt_outboard_poloidal_plasma", scan=scan + ) + deg_blkt_inboard_poloidal_plasma = m_file.get( + "deg_blkt_inboard_poloidal_plasma", scan=scan + ) + f_deg_blkt_outboard_poloidal_plasma = m_file.get( + "f_deg_blkt_outboard_poloidal_plasma", scan=scan + ) + f_deg_blkt_inboard_poloidal_plasma = m_file.get( + "f_deg_blkt_inboard_poloidal_plasma", scan=scan + ) + deg_div_poloidal_plasma = m_file.get("deg_div_poloidal_plasma", scan=scan) + f_ster_div_single = m_file.get("f_ster_div_single", scan=scan) + i_single_null = m_file.get("i_single_null", scan=scan) + + # ====================== + + plot_blanket(ax, m_file, scan, radial_build, colour_scheme) + plot_plasma(ax, m_file, scan, colour_scheme) + plot_firstwall(ax, m_file, scan, radial_build, colour_scheme) + + ax.set_xlabel("Radial position [m]") + ax.set_ylabel("Vertical position [m]") + ax.set_title("Blanket and First Wall Poloidal Cross-Section") + ax.minorticks_on() + ax.grid(which="minor", linestyle=":", linewidth=0.5, alpha=0.5) + + r_blkt_outboard_out = ( + rmajor + rminor + dr_fw_outboard + dr_fw_plasma_gap_outboard + dr_blkt_outboard + ) + r_blkt_inboard_in = ( + rmajor - rminor - dr_fw_plasma_gap_inboard - dr_fw_inboard - dr_blkt_inboard + ) + r_fw_outboard_in = r_blkt_outboard_out - dr_blkt_outboard - dr_fw_outboard + r_fw_inboard_out = r_blkt_inboard_in + dr_blkt_inboard + dr_fw_inboard + + # Plot a horizontal line at dz_blkt_half (blanket half height) + ax.axhline( + dz_blkt_half, + color="purple", + linestyle="--", + linewidth=1.5, + label="Blanket Half Height", + ) + ax.axhline( + -dz_blkt_half, + color="purple", + linestyle="--", + linewidth=1.5, + label="Blanket Half Height", + ) + + if i_single_null == 0: + # Plot arrows for the outboard blanket angles + ax.annotate( + "", + xy=(rmajor, 0), + xytext=(rmajor, dz_blkt_half), + arrowprops={"arrowstyle": "<-", "color": "purple"}, + zorder=5, + ) + # If single null then only plot the lower arrow for the outboard blanket angle + ax.annotate( + "", + xy=(rmajor, 0), + xytext=(rmajor, -dz_blkt_half), + arrowprops={"arrowstyle": "<-", "color": "purple"}, + zorder=5, + ) + + # Plot arc showing the angle between the two outboard blanket arrows + arc_radius = 1.0 + + # 3 to 6 o'clock position is -90 degrees, + angle_start = -90.0 + if i_single_null == 1: + angle_end = 90.0 + deg_div_poloidal_plasma + elif i_single_null == 0: + # 3 to 12 o'clock position is +90 degrees + angle_end = 90.0 + + theta = np.linspace(np.deg2rad(angle_start), np.deg2rad(angle_end), 50) + arc_x = rmajor + arc_radius * np.cos(theta) + arc_y = arc_radius * np.sin(theta) + + ax.plot(arc_x, arc_y, color="purple", linewidth=2) + + # Add angle label at the arc + mid_angle = np.deg2rad((angle_start + angle_end) / 2) + label_radius = arc_radius * 1.8 + label_x = rmajor + label_radius * np.cos(mid_angle) + label_y = label_radius * np.sin(mid_angle) + + # Plot the info box for the outboard blanket + ax.text( + label_x, + label_y, + f"{deg_blkt_outboard_poloidal_plasma:.1f}°\n({f_deg_blkt_outboard_poloidal_plasma * 100:.1f}%)", + fontsize=7, + color="purple", + ha="center", + va="center", + weight="bold", + bbox={ + "boxstyle": "round", + "facecolor": "white", + "alpha": 0.8, + "edgecolor": "purple", + "linewidth": 1.5, + }, + ) + + # Plot arrows for the inboard blanket angles + ax.annotate( + "", + xy=(rmajor, 0), + xytext=(r_fw_inboard_out, dz_blkt_half), + arrowprops={"arrowstyle": "<-", "color": "green"}, + zorder=5, + ) + + ax.annotate( + "", + xy=(rmajor, 0), + xytext=(r_fw_inboard_out, -dz_blkt_half), + arrowprops={"arrowstyle": "<-", "color": "green"}, + zorder=5, + ) + + # Plot arc showing the angle between the two inboard blanket arrows + arc_radius = 1.0 + angle_start = -deg_blkt_inboard_poloidal_plasma / 2 + angle_end = deg_blkt_inboard_poloidal_plasma / 2 + + theta = np.linspace(np.deg2rad(angle_start), np.deg2rad(angle_end), 50) + arc_x = rmajor - arc_radius * np.cos(theta) + arc_y = arc_radius * np.sin(theta) + + ax.plot(arc_x, arc_y, color="green", linewidth=2) + + # Add angle label at the arc + mid_angle = np.deg2rad((angle_start + angle_end) / 2) + label_radius = arc_radius * 1.8 + label_x = rmajor - label_radius * np.cos(mid_angle) + label_y = label_radius * np.sin(mid_angle) + + # Plot the info box for the inboard blanket + ax.text( + label_x, + label_y, + f"{deg_blkt_inboard_poloidal_plasma:.1f}°\n({f_deg_blkt_inboard_poloidal_plasma * 100:.1f}%)", + fontsize=7, + color="green", + ha="center", + va="center", + weight="bold", + bbox={ + "boxstyle": "round", + "facecolor": "white", + "alpha": 0.8, + "edgecolor": "green", + "linewidth": 1.5, + }, + zorder=5, + ) + + # Plot arrows for the divertor angles + # If double null then plot the upper also + if i_single_null == 0: + # Plot arc showing the angle between the two arrows (divertor angle) + arc_radius = 1.5 + # 3 to 12 o'clock position is +90 degrees, + angle_start = 90.0 + angle_end = 90.0 + deg_div_poloidal_plasma + + theta = np.linspace(np.deg2rad(angle_start), np.deg2rad(angle_end), 50) + arc_x = rmajor + arc_radius * np.cos(theta) + arc_y = arc_radius * np.sin(theta) + + ax.plot(arc_x, arc_y, color="black", linewidth=2) + + # Add angle label at the arc + mid_angle = np.deg2rad((angle_start + angle_end) / 2) + label_radius = arc_radius * 1.8 + label_x = rmajor + label_radius * np.cos(mid_angle) + label_y = label_radius * np.sin(mid_angle) + + ax.text( + label_x, + label_y, + f"{deg_div_poloidal_plasma:.1f}°\n({f_ster_div_single * 100:.1f}%)", + fontsize=7, + color="black", + ha="center", + va="center", + weight="bold", + bbox={ + "boxstyle": "round", + "facecolor": "white", + "alpha": 0.8, + "edgecolor": "black", + "linewidth": 1.5, + }, + zorder=5, + ) + + # Plot arc showing the angle between the two arrows for the lower divertor (divertor angle) + arc_radius = 1.5 + # 3 to 6 o'clock is -90 degrees + angle_start = -90.0 + angle_end = -90.0 - deg_div_poloidal_plasma + + theta = np.linspace(np.deg2rad(angle_start), np.deg2rad(angle_end), 50) + arc_x = rmajor + arc_radius * np.cos(theta) + arc_y = arc_radius * np.sin(theta) + + ax.plot(arc_x, arc_y, color="black", linewidth=2) + + # Add angle label at the arc + mid_angle = np.deg2rad((angle_start + angle_end) / 2) + label_radius = arc_radius * 1.8 + label_x = rmajor + label_radius * np.cos(mid_angle) + label_y = label_radius * np.sin(mid_angle) + + # Plot the info box for the lower divertor angle + ax.text( + label_x, + label_y, + f"{deg_div_poloidal_plasma:.1f}°\n({f_ster_div_single * 100:.1f}%)", + fontsize=7, + color="black", + ha="center", + va="center", + weight="bold", + bbox={ + "boxstyle": "round", + "facecolor": "white", + "alpha": 0.8, + "edgecolor": "black", + "linewidth": 1.5, + }, + zorder=5, + ) + + # Plot vertical lines at the inner and outer radial boundaries of the blanket + ax.axvline( + r_blkt_inboard_in, color="black", linestyle="--", linewidth=1.5, zorder=10 + ) + ax.axvline( + r_blkt_outboard_out, color="black", linestyle="--", linewidth=1.5, zorder=10 + ) + ax.axvline(r_fw_inboard_out, color="black", linestyle="--", linewidth=1.5, zorder=10) + + ax.axvline(r_fw_outboard_in, color="black", linestyle="--", linewidth=1.5, zorder=10) + + ax.axvline( + rmajor, + color="black", + linestyle="--", + linewidth=1.5, + label="Major Radius $R_0$", + ) + + # Plot midplane line (horizontal dashed line at Z=0) + ax.axhline( + 0.0, + color="black", + linestyle="--", + linewidth=1.5, + label="Midplane", + ) + + def plot_detailed_plasma_parameters(axis: plt.Axes, fig, mfile: MFile, scan: int): """Function to plot detailed plasma parameters from physics data. @@ -16464,6 +16756,14 @@ def _add_page(name: str | None = None): colour_scheme, ) + plot_poloidal_power_distribution( + ax=figs[35].add_subplot(111, aspect="equal"), + m_file=m_file, + scan=scan, + radial_build=radial_build, + colour_scheme=colour_scheme, + ) + plot_main_power_flow( _add_page("main_power_flow").add_subplot(111, aspect="equal"), m_file, From 59842200628539a9e4110520072325b6c6bf1842 Mon Sep 17 00:00:00 2001 From: mn3981 Date: Mon, 8 Jun 2026 11:49:24 +0100 Subject: [PATCH 02/25] Add radiation power variables for inboard and outboard first wall in FWBSData --- process/data_structure/fwbs_variables.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/process/data_structure/fwbs_variables.py b/process/data_structure/fwbs_variables.py index eed0c7be87..334d85ab89 100644 --- a/process/data_structure/fwbs_variables.py +++ b/process/data_structure/fwbs_variables.py @@ -379,6 +379,12 @@ class FWBSData: p_fw_rad_total_mw: float = 0.0 """Radiation power incident on the first wall (MW)""" + p_fw_inboard_rad_mw: float = 0.0 + """Radiation power incident on the inboard first wall [MW]""" + + p_fw_outboard_rad_mw: float = 0.0 + """Radiation power incident on the outboard first wall [MW]""" + p_fw_hcd_rad_total_mw: float = 0.0 """Radiation power incident on the heating and current drive systems on the first wall (MW)""" From e8b190f24a541fc3f8c902e24e8b145d0fe92a8e Mon Sep 17 00:00:00 2001 From: mn3981 Date: Mon, 8 Jun 2026 12:10:10 +0100 Subject: [PATCH 03/25] Refactor radiation power calculations for first wall in DCLL and CCFE_HCPB; add new calculations in FirstWall model --- process/models/blankets/dcll.py | 7 ------- process/models/blankets/hcpb.py | 7 ------- process/models/fw.py | 19 +++++++++++++++++++ tests/unit/models/blankets/test_ccfe_hcpb.py | 8 -------- tests/unit/models/test_dcll.py | 8 -------- 5 files changed, 19 insertions(+), 30 deletions(-) diff --git a/process/models/blankets/dcll.py b/process/models/blankets/dcll.py index 82616d4293..aced7d3116 100644 --- a/process/models/blankets/dcll.py +++ b/process/models/blankets/dcll.py @@ -228,13 +228,6 @@ def dcll_neutronics_and_power(self, output: bool): # FW - # Radiation power incident on first wall (MW) - self.data.fwbs.p_fw_rad_total_mw = ( - self.data.physics.p_plasma_rad_mw - - self.data.fwbs.p_div_rad_total_mw - - self.data.fwbs.p_fw_hcd_rad_total_mw - ) - # Surface heat flux on first wall (MW) # All of the fast particle losses go to the outer wall. self.data.fwbs.psurffwo = ( diff --git a/process/models/blankets/hcpb.py b/process/models/blankets/hcpb.py index bdb885a324..b3d28a01a7 100644 --- a/process/models/blankets/hcpb.py +++ b/process/models/blankets/hcpb.py @@ -781,13 +781,6 @@ def powerflow_calc(self, output: bool): self.data.physics.p_plasma_rad_mw * self.data.fwbs.f_a_fw_outboard_hcd ) - # Radiation power incident on first wall (MW) - self.data.fwbs.p_fw_rad_total_mw = ( - self.data.physics.p_plasma_rad_mw - - self.data.fwbs.p_div_rad_total_mw - - self.data.fwbs.p_fw_hcd_rad_total_mw - ) - # If we have chosen pressurised water as the blanket coolant, set the # coolant outlet temperature as 20 deg C below the boiling point if self.data.fwbs.i_blkt_coolant_type == CoolantType.WATER: diff --git a/process/models/fw.py b/process/models/fw.py index 733c75fb38..7a4ce1f9d0 100644 --- a/process/models/fw.py +++ b/process/models/fw.py @@ -128,6 +128,25 @@ def run(self): self.data.physics.p_neutron_total_mw / self.data.first_wall.a_fw_total ) + # Radiation power incident on first wall (MW) + # Set based on the total radiation power and the angular fractions taken up by + # the inboard and outboard first wall, which are calculated based on the + # geometry of the first wall and the plasma. + self.data.fwbs.p_fw_rad_total_mw = self.data.physics.p_plasma_rad_mw * ( + self.data.blanket.f_deg_blkt_outboard_poloidal_plasma + + self.data.blanket.f_deg_blkt_inboard_poloidal_plasma + ) + + self.data.fwbs.p_fw_inboard_rad_mw = ( + self.data.physics.p_plasma_rad_mw + * self.data.blanket.f_deg_blkt_inboard_poloidal_plasma + ) + + self.data.fwbs.p_fw_outboard_rad_mw = ( + self.data.physics.p_plasma_rad_mw + * self.data.blanket.f_deg_blkt_outboard_poloidal_plasma + ) + if self.data.physics.i_pflux_fw_neutron == 1: self.data.physics.pflux_fw_rad_mw = ( self.data.physics.ffwal diff --git a/tests/unit/models/blankets/test_ccfe_hcpb.py b/tests/unit/models/blankets/test_ccfe_hcpb.py index 40264bc429..8b63c0b701 100644 --- a/tests/unit/models/blankets/test_ccfe_hcpb.py +++ b/tests/unit/models/blankets/test_ccfe_hcpb.py @@ -620,8 +620,6 @@ class PowerflowCalcParam(NamedTuple): expected_p_div_rad_total_mw: Any = None - expected_p_fw_rad_total_mw: Any = None - expected_psurffwi: Any = None expected_psurffwo: Any = None @@ -673,7 +671,6 @@ class PowerflowCalcParam(NamedTuple): t_in_bb=573.13, t_out_bb=773.13, p_fw_blkt_coolant_pump_mw=0, - expected_p_fw_rad_total_mw=254.39207240222791, expected_psurffwi=97.271629070225231, expected_psurffwo=176.95628839065773, expected_p_shld_coolant_pump_mw=0.0068056297940224456, @@ -717,7 +714,6 @@ class PowerflowCalcParam(NamedTuple): t_in_bb=573.13, t_out_bb=773.13, p_fw_blkt_coolant_pump_mw=202.00455086503842, - expected_p_fw_rad_total_mw=254.39207240222791, expected_psurffwi=97.271629070225259, expected_psurffwo=176.95009681558912, expected_p_shld_coolant_pump_mw=0.007019085478296147, @@ -814,10 +810,6 @@ def test_powerflow_calc(powerflowcalcparam, monkeypatch, ccfe_hcpb): ccfe_hcpb.powerflow_calc(False) - assert ccfe_hcpb.data.fwbs.p_fw_rad_total_mw == pytest.approx( - powerflowcalcparam.expected_p_fw_rad_total_mw - ) - assert ccfe_hcpb.data.fwbs.psurffwi == pytest.approx( powerflowcalcparam.expected_psurffwi ) diff --git a/tests/unit/models/test_dcll.py b/tests/unit/models/test_dcll.py index 2aace346fe..24623d814d 100644 --- a/tests/unit/models/test_dcll.py +++ b/tests/unit/models/test_dcll.py @@ -40,8 +40,6 @@ class DcllNeutronicsAndPowerParam(NamedTuple): p_fw_alpha_mw: Any = None - expected_p_fw_rad_total_mw: Any = None - expected_p_fw_nuclear_heat_total_mw: Any = None expected_p_blkt_nuclear_heat_total_mw: Any = None @@ -65,7 +63,6 @@ class DcllNeutronicsAndPowerParam(NamedTuple): p_neutron_total_mw=1587.7386535917431, p_plasma_rad_mw=287.44866938104849, p_fw_alpha_mw=19.835845058655043, - expected_p_fw_rad_total_mw=254.39207240222791, expected_p_fw_nuclear_heat_total_mw=196.72081918001697, expected_p_blkt_nuclear_heat_total_mw=1533.4949914565693, expected_p_blkt_multiplication_mw=325.06710220789364, @@ -83,7 +80,6 @@ class DcllNeutronicsAndPowerParam(NamedTuple): p_neutron_total_mw=1587.2430556964196, p_plasma_rad_mw=287.44866938104849, p_fw_alpha_mw=19.829653483586444, - expected_p_fw_rad_total_mw=254.39207240222791, expected_p_fw_nuclear_heat_total_mw=196.65941460078642, expected_p_blkt_nuclear_heat_total_mw=1533.0163252173013, expected_p_blkt_multiplication_mw=324.96563552675644, @@ -148,10 +144,6 @@ def test_dcll_neutronics_and_power(dcllneutronicsandpowerparam, monkeypatch, dcl dcll.dcll_neutronics_and_power(False) - assert dcll.data.fwbs.p_fw_rad_total_mw == pytest.approx( - dcllneutronicsandpowerparam.expected_p_fw_rad_total_mw - ) - assert dcll.data.fwbs.p_fw_nuclear_heat_total_mw == pytest.approx( dcllneutronicsandpowerparam.expected_p_fw_nuclear_heat_total_mw ) From 003e6d2095362d69c1c736ed82c80f4996d2886c Mon Sep 17 00:00:00 2001 From: mn3981 Date: Mon, 8 Jun 2026 12:45:29 +0100 Subject: [PATCH 04/25] Rename 'psurffwi' to 'p_fw_inboard_surface_heat' for clarity and consistency across models and tests --- process/data_structure/fwbs_variables.py | 4 ++-- process/models/blankets/blanket_library.py | 15 +++++++++------ process/models/blankets/dcll.py | 7 ++++--- process/models/blankets/hcpb.py | 9 +++++---- process/models/engineering/ivc_functions.py | 6 +++--- process/models/stellarator/stellarator.py | 4 ++-- tests/unit/models/blankets/test_ccfe_hcpb.py | 8 ++++---- 7 files changed, 29 insertions(+), 24 deletions(-) diff --git a/process/data_structure/fwbs_variables.py b/process/data_structure/fwbs_variables.py index 334d85ab89..ee97e81baa 100644 --- a/process/data_structure/fwbs_variables.py +++ b/process/data_structure/fwbs_variables.py @@ -170,8 +170,8 @@ class FWBSData: f_a_fw_coolant_outboard: float = 0.0 """Outboard FW coolant cross-sectional area void fraction""" - psurffwi: float = 0.0 - """Surface heat flux on first wall [MW] (sum = p_fw_rad_total_mw)""" + p_fw_inboard_surface_heat_mw: float = 0.0 + """Surface heat flux on inboard first wall [MW] """ psurffwo: float = 0.0 """Surface heat flux on first wall [MW] (sum = p_fw_rad_total_mw)""" diff --git a/process/models/blankets/blanket_library.py b/process/models/blankets/blanket_library.py index b381985142..b4c646f27f 100644 --- a/process/models/blankets/blanket_library.py +++ b/process/models/blankets/blanket_library.py @@ -2238,10 +2238,10 @@ def thermo_hydraulic_model(self, output: bool): if self.data.fwbs.i_blkt_dual_coolant == 2: f_nuc_fwi = ( self.data.blanket.p_fw_inboard_nuclear_heat_mw - + self.data.fwbs.psurffwi + + self.data.fwbs.p_fw_inboard_surface_heat_mw ) / ( self.data.blanket.p_fw_inboard_nuclear_heat_mw - + self.data.fwbs.psurffwi + + self.data.fwbs.p_fw_inboard_surface_heat_mw + pnucblkti_struct ) f_nuc_fwo = ( @@ -2255,10 +2255,10 @@ def thermo_hydraulic_model(self, output: bool): else: f_nuc_fwi = ( self.data.blanket.p_fw_inboard_nuclear_heat_mw - + self.data.fwbs.psurffwi + + self.data.fwbs.p_fw_inboard_surface_heat_mw ) / ( self.data.blanket.p_fw_inboard_nuclear_heat_mw - + self.data.fwbs.psurffwi + + self.data.fwbs.p_fw_inboard_surface_heat_mw + self.data.blanket.p_blkt_nuclear_heat_inboard_mw ) f_nuc_fwo = ( @@ -2310,7 +2310,7 @@ def thermo_hydraulic_model(self, output: bool): self.data.fwbs.radius_fw_channel, self.data.build.dr_fw_inboard, self.data.first_wall.a_fw_inboard, - self.data.fwbs.psurffwi, + self.data.fwbs.p_fw_inboard_surface_heat_mw, self.data.blanket.p_fw_inboard_nuclear_heat_mw, "Inboard first wall", ) @@ -2338,7 +2338,10 @@ def thermo_hydraulic_model(self, output: bool): # Total mass flow rate to remove inboard FW power (kg/s) self.data.blanket.mflow_fw_inboard_coolant_total = ( 1.0e6 - * (self.data.blanket.p_fw_inboard_nuclear_heat_mw + self.data.fwbs.psurffwi) + * ( + self.data.blanket.p_fw_inboard_nuclear_heat_mw + + self.data.fwbs.p_fw_inboard_surface_heat_mw + ) / (self.data.fwbs.cp_fw * (fwoutleti - self.data.fwbs.temp_fw_coolant_in)) ) # Total mass flow rate to remove outboard FW power (kg/s) diff --git a/process/models/blankets/dcll.py b/process/models/blankets/dcll.py index aced7d3116..cc5e51535d 100644 --- a/process/models/blankets/dcll.py +++ b/process/models/blankets/dcll.py @@ -237,8 +237,9 @@ def dcll_neutronics_and_power(self, output: bool): + self.data.current_drive.p_beam_orbit_loss_mw + self.data.physics.p_fw_alpha_mw ) - self.data.fwbs.psurffwi = self.data.fwbs.p_fw_rad_total_mw * ( - 1 - self.data.first_wall.a_fw_outboard / self.data.first_wall.a_fw_total + self.data.fwbs.p_fw_inboard_surface_heat_mw = ( + self.data.fwbs.p_fw_rad_total_mw + * (1 - self.data.first_wall.a_fw_outboard / self.data.first_wall.a_fw_total) ) if output: @@ -352,7 +353,7 @@ def dcll_power_and_heating(self, output: bool): f_p_shld_coolant_pump_total_heat=self.data.heat_transport.f_p_shld_coolant_pump_total_heat, f_p_div_coolant_pump_total_heat=self.data.heat_transport.f_p_div_coolant_pump_total_heat, p_fw_nuclear_heat_total_mw=self.data.fwbs.p_fw_nuclear_heat_total_mw, - psurffwi=self.data.fwbs.psurffwi, + p_fw_inboard_surface_heat_mw=self.data.fwbs.p_fw_inboard_surface_heat_mw, psurffwo=self.data.fwbs.psurffwo, p_blkt_nuclear_heat_total_mw=self.data.fwbs.p_blkt_nuclear_heat_total_mw, p_shld_nuclear_heat_mw=self.data.fwbs.p_shld_nuclear_heat_mw, diff --git a/process/models/blankets/hcpb.py b/process/models/blankets/hcpb.py index b3d28a01a7..c25c503a32 100644 --- a/process/models/blankets/hcpb.py +++ b/process/models/blankets/hcpb.py @@ -802,8 +802,9 @@ def powerflow_calc(self, output: bool): + self.data.current_drive.p_beam_orbit_loss_mw + self.data.physics.p_fw_alpha_mw ) - self.data.fwbs.psurffwi = self.data.fwbs.p_fw_rad_total_mw * ( - 1 - self.data.first_wall.a_fw_outboard / self.data.first_wall.a_fw_total + self.data.fwbs.p_fw_inboard_surface_heat_mw = ( + self.data.fwbs.p_fw_rad_total_mw + * (1 - self.data.first_wall.a_fw_outboard / self.data.first_wall.a_fw_total) ) i_p_coolant_pumping = PumpingPowerModelTypes(self.data.fwbs.i_p_coolant_pumping) @@ -820,7 +821,7 @@ def powerflow_calc(self, output: bool): f_p_shld_coolant_pump_total_heat=self.data.heat_transport.f_p_shld_coolant_pump_total_heat, f_p_div_coolant_pump_total_heat=self.data.heat_transport.f_p_div_coolant_pump_total_heat, p_fw_nuclear_heat_total_mw=self.data.fwbs.p_fw_nuclear_heat_total_mw, - psurffwi=self.data.fwbs.psurffwi, + p_fw_inboard_surface_heat_mw=self.data.fwbs.p_fw_inboard_surface_heat_mw, psurffwo=self.data.fwbs.psurffwo, p_blkt_nuclear_heat_total_mw=self.data.fwbs.p_blkt_nuclear_heat_total_mw, p_shld_nuclear_heat_mw=self.data.heat_transport.p_shld_nuclear_heat_mw, @@ -881,7 +882,7 @@ def powerflow_calc(self, output: bool): fpump = t_in_compressor / (self.data.fwbs.etaiso * dt_he) * (pfactor - 1) p_plasma = ( self.data.fwbs.p_fw_nuclear_heat_total_mw - + self.data.fwbs.psurffwi + + self.data.fwbs.p_fw_inboard_surface_heat_mw + self.data.fwbs.psurffwo + self.data.fwbs.p_blkt_nuclear_heat_total_mw ) diff --git a/process/models/engineering/ivc_functions.py b/process/models/engineering/ivc_functions.py index 01aa62f09f..9bfa67fc9d 100644 --- a/process/models/engineering/ivc_functions.py +++ b/process/models/engineering/ivc_functions.py @@ -30,7 +30,7 @@ def pumping_powers_as_fractions( f_p_shld_coolant_pump_total_heat: float, f_p_div_coolant_pump_total_heat: float, p_fw_nuclear_heat_total_mw: float, - psurffwi: float, + p_fw_inboard_surface_heat_mw: float, psurffwo: float, p_blkt_nuclear_heat_total_mw: float, p_shld_nuclear_heat_mw: float, @@ -54,7 +54,7 @@ def pumping_powers_as_fractions( Fraction for divertor coolant pump. p_fw_nuclear_heat_total_mw : float Total FW nuclear heating (MW). - psurffwi : float + p_fw_inboard_surface_heat_mw : float Inboard FW surface heating (MW). psurffwo : float Outboard FW surface heating (MW). @@ -77,7 +77,7 @@ def pumping_powers_as_fractions( Tuple of pumping powers (MW) for FW, blanket, shield, and divertor. """ p_fw_coolant_pump_mw = f_p_fw_coolant_pump_total_heat * ( - p_fw_nuclear_heat_total_mw + psurffwi + psurffwo + p_fw_nuclear_heat_total_mw + p_fw_inboard_surface_heat_mw + psurffwo ) p_blkt_coolant_pump_mw = ( f_p_blkt_coolant_pump_total_heat * p_blkt_nuclear_heat_total_mw diff --git a/process/models/stellarator/stellarator.py b/process/models/stellarator/stellarator.py index e75fc106c8..892b639ab3 100644 --- a/process/models/stellarator/stellarator.py +++ b/process/models/stellarator/stellarator.py @@ -844,7 +844,7 @@ def st_fwbs(self, output: bool): # Surface heat flux on first wall (MW) # (sum = self.data.fwbs.p_fw_rad_total_mw) - psurffwi = ( + p_fw_inboard_surface_heat_mw = ( self.data.fwbs.p_fw_rad_total_mw * self.data.first_wall.a_fw_inboard / self.data.first_wall.a_fw_total @@ -910,7 +910,7 @@ def st_fwbs(self, output: bool): * ( p_fw_inboard_nuclear_heat_mw + p_fw_outboard_nuclear_heat_mw - + psurffwi + + p_fw_inboard_surface_heat_mw + psurffwo + self.data.current_drive.p_beam_orbit_loss_mw ) diff --git a/tests/unit/models/blankets/test_ccfe_hcpb.py b/tests/unit/models/blankets/test_ccfe_hcpb.py index 8b63c0b701..66f2458dcc 100644 --- a/tests/unit/models/blankets/test_ccfe_hcpb.py +++ b/tests/unit/models/blankets/test_ccfe_hcpb.py @@ -580,7 +580,7 @@ class PowerflowCalcParam(NamedTuple): p_cp_shield_nuclear_heat_mw: Any = None - psurffwi: Any = None + p_fw_inboard_surface_heat_mw: Any = None psurffwo: Any = None @@ -652,7 +652,7 @@ class PowerflowCalcParam(NamedTuple): p_shld_nuclear_heat_mw=1.3611259588044891, etaiso=0.90000000000000002, p_cp_shield_nuclear_heat_mw=0, - psurffwi=0, + p_fw_inboard_surface_heat_mw=0, psurffwo=0, p_fw_coolant_pump_mw=0, f_p_fw_coolant_pump_total_heat=0.0050000000000000001, @@ -695,7 +695,7 @@ class PowerflowCalcParam(NamedTuple): p_shld_nuclear_heat_mw=1.4038170956592293, etaiso=0.90000000000000002, p_cp_shield_nuclear_heat_mw=0, - psurffwi=97.271629070225231, + p_fw_inboard_surface_heat_mw=97.271629070225231, psurffwo=176.95628839065773, p_fw_coolant_pump_mw=0, f_p_fw_coolant_pump_total_heat=0.0050000000000000001, @@ -810,7 +810,7 @@ def test_powerflow_calc(powerflowcalcparam, monkeypatch, ccfe_hcpb): ccfe_hcpb.powerflow_calc(False) - assert ccfe_hcpb.data.fwbs.psurffwi == pytest.approx( + assert ccfe_hcpb.data.fwbs.p_fw_inboard_surface_heat_mw == pytest.approx( powerflowcalcparam.expected_psurffwi ) From f362643b20b7afea5428cded0223025e8e7d4f8b Mon Sep 17 00:00:00 2001 From: mn3981 Date: Mon, 8 Jun 2026 13:09:01 +0100 Subject: [PATCH 05/25] Rename 'psurffwo' to 'p_fw_outboard_surface_heat_mw' for clarity and consistency across models and tests --- process/data_structure/fwbs_variables.py | 4 ++-- process/models/blankets/blanket_library.py | 15 +++++++++------ process/models/blankets/dcll.py | 4 ++-- process/models/blankets/hcpb.py | 6 +++--- process/models/engineering/ivc_functions.py | 8 +++++--- process/models/stellarator/stellarator.py | 4 ++-- tests/unit/models/blankets/test_ccfe_hcpb.py | 8 ++++---- 7 files changed, 27 insertions(+), 22 deletions(-) diff --git a/process/data_structure/fwbs_variables.py b/process/data_structure/fwbs_variables.py index ee97e81baa..a50fc34fc9 100644 --- a/process/data_structure/fwbs_variables.py +++ b/process/data_structure/fwbs_variables.py @@ -173,8 +173,8 @@ class FWBSData: p_fw_inboard_surface_heat_mw: float = 0.0 """Surface heat flux on inboard first wall [MW] """ - psurffwo: float = 0.0 - """Surface heat flux on first wall [MW] (sum = p_fw_rad_total_mw)""" + p_fw_outboard_surface_heat_mw: float = 0.0 + """Surface heat flux on outboard first wall [MW]""" vol_fw_total: float = 0.0 """First wall volume [m3]""" diff --git a/process/models/blankets/blanket_library.py b/process/models/blankets/blanket_library.py index b4c646f27f..d1c463fe96 100644 --- a/process/models/blankets/blanket_library.py +++ b/process/models/blankets/blanket_library.py @@ -2246,10 +2246,10 @@ def thermo_hydraulic_model(self, output: bool): ) f_nuc_fwo = ( self.data.blanket.p_fw_outboard_nuclear_heat_mw - + self.data.fwbs.psurffwo + + self.data.fwbs.p_fw_outboard_surface_heat_mw ) / ( self.data.blanket.p_fw_outboard_nuclear_heat_mw - + self.data.fwbs.psurffwo + + self.data.fwbs.p_fw_outboard_surface_heat_mw + pnucblkto_struct ) else: @@ -2263,10 +2263,10 @@ def thermo_hydraulic_model(self, output: bool): ) f_nuc_fwo = ( self.data.blanket.p_fw_outboard_nuclear_heat_mw - + self.data.fwbs.psurffwo + + self.data.fwbs.p_fw_outboard_surface_heat_mw ) / ( self.data.blanket.p_fw_outboard_nuclear_heat_mw - + self.data.fwbs.psurffwo + + self.data.fwbs.p_fw_outboard_surface_heat_mw + self.data.blanket.p_blkt_nuclear_heat_outboard_mw ) @@ -2324,7 +2324,7 @@ def thermo_hydraulic_model(self, output: bool): self.data.fwbs.radius_fw_channel, self.data.build.dr_fw_outboard, self.data.first_wall.a_fw_outboard, - self.data.fwbs.psurffwo, + self.data.fwbs.p_fw_outboard_surface_heat_mw, self.data.blanket.p_fw_outboard_nuclear_heat_mw, "Outboard first wall", ) @@ -2347,7 +2347,10 @@ def thermo_hydraulic_model(self, output: bool): # Total mass flow rate to remove outboard FW power (kg/s) self.data.blanket.mflow_fw_outboard_coolant_total = ( 1.0e6 - * (self.data.blanket.p_fw_outboard_nuclear_heat_mw + self.data.fwbs.psurffwo) + * ( + self.data.blanket.p_fw_outboard_nuclear_heat_mw + + self.data.fwbs.p_fw_outboard_surface_heat_mw + ) / (self.data.fwbs.cp_fw * (fwoutleto - self.data.fwbs.temp_fw_coolant_in)) ) diff --git a/process/models/blankets/dcll.py b/process/models/blankets/dcll.py index cc5e51535d..a0bb4df43f 100644 --- a/process/models/blankets/dcll.py +++ b/process/models/blankets/dcll.py @@ -230,7 +230,7 @@ def dcll_neutronics_and_power(self, output: bool): # Surface heat flux on first wall (MW) # All of the fast particle losses go to the outer wall. - self.data.fwbs.psurffwo = ( + self.data.fwbs.p_fw_outboard_surface_heat_mw = ( self.data.fwbs.p_fw_rad_total_mw * self.data.first_wall.a_fw_outboard / self.data.first_wall.a_fw_total @@ -354,7 +354,7 @@ def dcll_power_and_heating(self, output: bool): f_p_div_coolant_pump_total_heat=self.data.heat_transport.f_p_div_coolant_pump_total_heat, p_fw_nuclear_heat_total_mw=self.data.fwbs.p_fw_nuclear_heat_total_mw, p_fw_inboard_surface_heat_mw=self.data.fwbs.p_fw_inboard_surface_heat_mw, - psurffwo=self.data.fwbs.psurffwo, + p_fw_outboard_surface_heat_mw=self.data.fwbs.p_fw_outboard_surface_heat_mw, p_blkt_nuclear_heat_total_mw=self.data.fwbs.p_blkt_nuclear_heat_total_mw, p_shld_nuclear_heat_mw=self.data.fwbs.p_shld_nuclear_heat_mw, p_cp_shield_nuclear_heat_mw=self.data.fwbs.p_cp_shield_nuclear_heat_mw, diff --git a/process/models/blankets/hcpb.py b/process/models/blankets/hcpb.py index c25c503a32..608602be47 100644 --- a/process/models/blankets/hcpb.py +++ b/process/models/blankets/hcpb.py @@ -795,7 +795,7 @@ def powerflow_calc(self, output: bool): # Surface heat flux on first wall (outboard and inboard) (MW) # All of the fast particle losses go to the outer wall. - self.data.fwbs.psurffwo = ( + self.data.fwbs.p_fw_outboard_surface_heat_mw = ( self.data.fwbs.p_fw_rad_total_mw * self.data.first_wall.a_fw_outboard / self.data.first_wall.a_fw_total @@ -822,7 +822,7 @@ def powerflow_calc(self, output: bool): f_p_div_coolant_pump_total_heat=self.data.heat_transport.f_p_div_coolant_pump_total_heat, p_fw_nuclear_heat_total_mw=self.data.fwbs.p_fw_nuclear_heat_total_mw, p_fw_inboard_surface_heat_mw=self.data.fwbs.p_fw_inboard_surface_heat_mw, - psurffwo=self.data.fwbs.psurffwo, + p_fw_outboard_surface_heat_mw=self.data.fwbs.p_fw_outboard_surface_heat_mw, p_blkt_nuclear_heat_total_mw=self.data.fwbs.p_blkt_nuclear_heat_total_mw, p_shld_nuclear_heat_mw=self.data.heat_transport.p_shld_nuclear_heat_mw, p_cp_shield_nuclear_heat_mw=self.data.fwbs.p_cp_shield_nuclear_heat_mw, @@ -883,7 +883,7 @@ def powerflow_calc(self, output: bool): p_plasma = ( self.data.fwbs.p_fw_nuclear_heat_total_mw + self.data.fwbs.p_fw_inboard_surface_heat_mw - + self.data.fwbs.psurffwo + + self.data.fwbs.p_fw_outboard_surface_heat_mw + self.data.fwbs.p_blkt_nuclear_heat_total_mw ) self.data.primary_pumping.p_fw_blkt_coolant_pump_mw = ( diff --git a/process/models/engineering/ivc_functions.py b/process/models/engineering/ivc_functions.py index 9bfa67fc9d..bb10e60765 100644 --- a/process/models/engineering/ivc_functions.py +++ b/process/models/engineering/ivc_functions.py @@ -31,7 +31,7 @@ def pumping_powers_as_fractions( f_p_div_coolant_pump_total_heat: float, p_fw_nuclear_heat_total_mw: float, p_fw_inboard_surface_heat_mw: float, - psurffwo: float, + p_fw_outboard_surface_heat_mw: float, p_blkt_nuclear_heat_total_mw: float, p_shld_nuclear_heat_mw: float, p_cp_shield_nuclear_heat_mw: float, @@ -56,7 +56,7 @@ def pumping_powers_as_fractions( Total FW nuclear heating (MW). p_fw_inboard_surface_heat_mw : float Inboard FW surface heating (MW). - psurffwo : float + p_fw_outboard_surface_heat_mw : float Outboard FW surface heating (MW). p_blkt_nuclear_heat_total_mw : float Total blanket nuclear heating (MW). @@ -77,7 +77,9 @@ def pumping_powers_as_fractions( Tuple of pumping powers (MW) for FW, blanket, shield, and divertor. """ p_fw_coolant_pump_mw = f_p_fw_coolant_pump_total_heat * ( - p_fw_nuclear_heat_total_mw + p_fw_inboard_surface_heat_mw + psurffwo + p_fw_nuclear_heat_total_mw + + p_fw_inboard_surface_heat_mw + + p_fw_outboard_surface_heat_mw ) p_blkt_coolant_pump_mw = ( f_p_blkt_coolant_pump_total_heat * p_blkt_nuclear_heat_total_mw diff --git a/process/models/stellarator/stellarator.py b/process/models/stellarator/stellarator.py index 892b639ab3..3b7aa9481b 100644 --- a/process/models/stellarator/stellarator.py +++ b/process/models/stellarator/stellarator.py @@ -849,7 +849,7 @@ def st_fwbs(self, output: bool): * self.data.first_wall.a_fw_inboard / self.data.first_wall.a_fw_total ) - psurffwo = ( + p_fw_outboard_surface_heat_mw = ( self.data.fwbs.p_fw_rad_total_mw * self.data.first_wall.a_fw_outboard / self.data.first_wall.a_fw_total @@ -911,7 +911,7 @@ def st_fwbs(self, output: bool): p_fw_inboard_nuclear_heat_mw + p_fw_outboard_nuclear_heat_mw + p_fw_inboard_surface_heat_mw - + psurffwo + + p_fw_outboard_surface_heat_mw + self.data.current_drive.p_beam_orbit_loss_mw ) ) diff --git a/tests/unit/models/blankets/test_ccfe_hcpb.py b/tests/unit/models/blankets/test_ccfe_hcpb.py index 66f2458dcc..2925f09a1d 100644 --- a/tests/unit/models/blankets/test_ccfe_hcpb.py +++ b/tests/unit/models/blankets/test_ccfe_hcpb.py @@ -582,7 +582,7 @@ class PowerflowCalcParam(NamedTuple): p_fw_inboard_surface_heat_mw: Any = None - psurffwo: Any = None + p_fw_outboard_surface_heat_mw: Any = None p_fw_coolant_pump_mw: Any = None @@ -653,7 +653,7 @@ class PowerflowCalcParam(NamedTuple): etaiso=0.90000000000000002, p_cp_shield_nuclear_heat_mw=0, p_fw_inboard_surface_heat_mw=0, - psurffwo=0, + p_fw_outboard_surface_heat_mw=0, p_fw_coolant_pump_mw=0, f_p_fw_coolant_pump_total_heat=0.0050000000000000001, p_blkt_coolant_pump_mw=0, @@ -696,7 +696,7 @@ class PowerflowCalcParam(NamedTuple): etaiso=0.90000000000000002, p_cp_shield_nuclear_heat_mw=0, p_fw_inboard_surface_heat_mw=97.271629070225231, - psurffwo=176.95628839065773, + p_fw_outboard_surface_heat_mw=176.95628839065773, p_fw_coolant_pump_mw=0, f_p_fw_coolant_pump_total_heat=0.0050000000000000001, p_blkt_coolant_pump_mw=0, @@ -814,7 +814,7 @@ def test_powerflow_calc(powerflowcalcparam, monkeypatch, ccfe_hcpb): powerflowcalcparam.expected_psurffwi ) - assert ccfe_hcpb.data.fwbs.psurffwo == pytest.approx( + assert ccfe_hcpb.data.fwbs.p_fw_outboard_surface_heat_mw == pytest.approx( powerflowcalcparam.expected_psurffwo ) From 66dfee9d9e771cce7c84fc0d03572e69fcfb8de4 Mon Sep 17 00:00:00 2001 From: mn3981 Date: Mon, 8 Jun 2026 13:16:54 +0100 Subject: [PATCH 06/25] Rename 'p_fw_alpha_mw' to 'p_fw_alpha_surface_total_mw' for clarity and consistency across models and tests --- documentation/source/eng-models/power-requirements.md | 2 +- process/core/io/plot/summary.py | 6 +++--- process/data_structure/physics_variables.py | 4 ++-- process/models/blankets/dcll.py | 2 +- process/models/blankets/hcpb.py | 2 +- process/models/fw.py | 9 +++++---- process/models/power.py | 10 +++++----- process/models/stellarator/stellarator.py | 5 +++-- tests/unit/models/blankets/test_ccfe_hcpb.py | 6 +++--- tests/unit/models/test_dcll.py | 6 +++--- 10 files changed, 27 insertions(+), 25 deletions(-) diff --git a/documentation/source/eng-models/power-requirements.md b/documentation/source/eng-models/power-requirements.md index ae4ff5b798..4eaa647538 100644 --- a/documentation/source/eng-models/power-requirements.md +++ b/documentation/source/eng-models/power-requirements.md @@ -214,7 +214,7 @@ $$ - $P_{\text{FW, nuclear}}$ & $P_{\text{Blkt, nuclear}}$ is the nuclear heating from neutron interaction (which includes the energy multiplication (`f_p_blkt_multiplication`) for the blanket.) - $P_{\text{FW,}\gamma}$ is the photon radiation incident on the FW (`p_fw_rad_total_mw`). -- $P_{\alpha,\text{loss}}$ is the plasma lost alpha power (`p_fw_alpha_mw`) +- $P_{\alpha,\text{loss}}$ is the plasma lost alpha power (`p_fw_alpha_surface_total_mw`) 8: The thermal power deposited in the shields is calculated: diff --git a/process/core/io/plot/summary.py b/process/core/io/plot/summary.py index 067cb2eb3c..9288a83037 100644 --- a/process/core/io/plot/summary.py +++ b/process/core/io/plot/summary.py @@ -527,7 +527,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur axis.text( 0.22, 0.81, - f"$P_{{\\alpha,{{loss}}}}$\n{mfile.get('p_fw_alpha_mw', scan=scan):,.2f} MW", + f"$P_{{\\alpha,{{loss}}}}$\n{mfile.get('p_fw_alpha_surface_total_mw', scan=scan):,.2f} MW", transform=fig.transFigure, horizontalalignment="left", verticalalignment="bottom", @@ -1371,7 +1371,7 @@ def plot_main_power_flow(axis: plt.Axes, mfile: MFile, scan: int, fig: plt.Figur axis.text( 0.46, 0.85, - f"$P_{{\\text{{FW, }}\\alpha}}$:\n{mfile.get('p_fw_alpha_mw', scan=scan):.2f} MW", + f"$P_{{\\text{{FW, }}\\alpha}}$:\n{mfile.get('p_fw_alpha_surface_total_mw', scan=scan):.2f} MW", fontsize=9, verticalalignment="bottom", horizontalalignment="left", @@ -2877,7 +2877,7 @@ def plot_main_plasma_information( ) textstr_alpha = ( - f"$P_{{\\alpha,\\text{{loss}}}}$ {mfile.get('p_fw_alpha_mw', scan=scan):.2f} MW \n" + f"$P_{{\\alpha,\\text{{loss}}}}$ {mfile.get('p_fw_alpha_surface_total_mw', scan=scan):.2f} MW \n" f"$f_{{\\alpha,\\text{{coupled}}}}$ {mfile.get('f_p_alpha_plasma_deposited', scan=scan):.2f}" ) diff --git a/process/data_structure/physics_variables.py b/process/data_structure/physics_variables.py index 6775f7ea6a..7b046ce9f8 100644 --- a/process/data_structure/physics_variables.py +++ b/process/data_structure/physics_variables.py @@ -1033,8 +1033,8 @@ class PhysicsData: f_pden_alpha_electron_mw: float = 0.0 """Alpha power per volume to electrons [MW/m3]""" - p_fw_alpha_mw: float = 0.0 - """alpha power escaping plasma and reaching first wall (MW)""" + p_fw_alpha_surface_total_mw: float = 0.0 + """Total alpha power escaping plasma and reaching first walls (MW)""" f_pden_alpha_ions_mw: float = 0.0 """alpha power per volume to ions (MW/m3)""" diff --git a/process/models/blankets/dcll.py b/process/models/blankets/dcll.py index a0bb4df43f..9975a9e5f9 100644 --- a/process/models/blankets/dcll.py +++ b/process/models/blankets/dcll.py @@ -235,7 +235,7 @@ def dcll_neutronics_and_power(self, output: bool): * self.data.first_wall.a_fw_outboard / self.data.first_wall.a_fw_total + self.data.current_drive.p_beam_orbit_loss_mw - + self.data.physics.p_fw_alpha_mw + + self.data.physics.p_fw_alpha_surface_total_mw ) self.data.fwbs.p_fw_inboard_surface_heat_mw = ( self.data.fwbs.p_fw_rad_total_mw diff --git a/process/models/blankets/hcpb.py b/process/models/blankets/hcpb.py index 608602be47..ffd6e9c927 100644 --- a/process/models/blankets/hcpb.py +++ b/process/models/blankets/hcpb.py @@ -800,7 +800,7 @@ def powerflow_calc(self, output: bool): * self.data.first_wall.a_fw_outboard / self.data.first_wall.a_fw_total + self.data.current_drive.p_beam_orbit_loss_mw - + self.data.physics.p_fw_alpha_mw + + self.data.physics.p_fw_alpha_surface_total_mw ) self.data.fwbs.p_fw_inboard_surface_heat_mw = ( self.data.fwbs.p_fw_rad_total_mw diff --git a/process/models/fw.py b/process/models/fw.py index 7a4ce1f9d0..937281b5a5 100644 --- a/process/models/fw.py +++ b/process/models/fw.py @@ -163,8 +163,9 @@ def run(self): ) # Power transported to the first wall by escaped alpha particles - self.data.physics.p_fw_alpha_mw = self.data.physics.p_alpha_total_mw * ( - 1.0e0 - self.data.physics.f_p_alpha_plasma_deposited + self.data.physics.p_fw_alpha_surface_total_mw = ( + self.data.physics.p_alpha_total_mw + * (1.0e0 - self.data.physics.f_p_alpha_plasma_deposited) ) @staticmethod @@ -872,8 +873,8 @@ def output_fw_surface_loads(self): po.ovarre( self.outfile, "Fast alpha particle power incident on the first-wall (MW)", - "(p_fw_alpha_mw)", - self.data.physics.p_fw_alpha_mw, + "(p_fw_alpha_surface_total_mw)", + self.data.physics.p_fw_alpha_surface_total_mw, "OP ", ) po.ovarre( diff --git a/process/models/power.py b/process/models/power.py index f55bc0f784..72430eaee4 100644 --- a/process/models/power.py +++ b/process/models/power.py @@ -880,7 +880,7 @@ def component_thermal_powers(self): + self.data.heat_transport.p_blkt_breeder_pump_mw + self.data.primary_pumping.p_fw_blkt_coolant_pump_mw + self.data.current_drive.p_beam_orbit_loss_mw - + self.data.physics.p_fw_alpha_mw + + self.data.physics.p_fw_alpha_surface_total_mw + self.data.current_drive.p_beam_shine_through_mw ) else: @@ -891,7 +891,7 @@ def component_thermal_powers(self): + self.data.fwbs.p_blkt_nuclear_heat_total_mw + self.data.primary_pumping.p_fw_blkt_coolant_pump_mw + self.data.current_drive.p_beam_orbit_loss_mw - + self.data.physics.p_fw_alpha_mw + + self.data.physics.p_fw_alpha_surface_total_mw + self.data.current_drive.p_beam_shine_through_mw ) @@ -901,7 +901,7 @@ def component_thermal_powers(self): + self.data.fwbs.p_fw_rad_total_mw + self.data.heat_transport.p_fw_coolant_pump_mw + self.data.current_drive.p_beam_orbit_loss_mw - + self.data.physics.p_fw_alpha_mw + + self.data.physics.p_fw_alpha_surface_total_mw + self.data.current_drive.p_beam_shine_through_mw ) @@ -1114,8 +1114,8 @@ def output_plant_thermal_powers(self): po.ovarre( self.outfile, "Lost alpha-particle heat deposited in FW [MW]", - "(p_fw_alpha_mw)", - self.data.physics.p_fw_alpha_mw, + "(p_fw_alpha_surface_total_mw)", + self.data.physics.p_fw_alpha_surface_total_mw, ) po.ovarre( self.outfile, diff --git a/process/models/stellarator/stellarator.py b/process/models/stellarator/stellarator.py index 3b7aa9481b..1128b4748d 100644 --- a/process/models/stellarator/stellarator.py +++ b/process/models/stellarator/stellarator.py @@ -2215,8 +2215,9 @@ def st_phys(self, output): # Power transported to the first wall by escaped alpha particles - self.data.physics.p_fw_alpha_mw = self.data.physics.p_alpha_total_mw * ( - 1.0e0 - self.data.physics.f_p_alpha_plasma_deposited + self.data.physics.p_fw_alpha_surface_total_mw = ( + self.data.physics.p_alpha_total_mw + * (1.0e0 - self.data.physics.f_p_alpha_plasma_deposited) ) # Nominal mean photon wall load diff --git a/tests/unit/models/blankets/test_ccfe_hcpb.py b/tests/unit/models/blankets/test_ccfe_hcpb.py index 2925f09a1d..0ff1561a81 100644 --- a/tests/unit/models/blankets/test_ccfe_hcpb.py +++ b/tests/unit/models/blankets/test_ccfe_hcpb.py @@ -602,7 +602,7 @@ class PowerflowCalcParam(NamedTuple): p_plasma_rad_mw: Any = None - p_fw_alpha_mw: Any = None + p_fw_alpha_surface_total_mw: Any = None p_plasma_separatrix_mw: Any = None @@ -663,7 +663,7 @@ class PowerflowCalcParam(NamedTuple): p_div_coolant_pump_mw=0, f_p_div_coolant_pump_total_heat=0.0050000000000000001, p_plasma_rad_mw=287.44866938104849, - p_fw_alpha_mw=19.835845058655043, + p_fw_alpha_surface_total_mw=19.835845058655043, p_plasma_separatrix_mw=143.6315222649435, p_he=8000000, dp_he=550000, @@ -706,7 +706,7 @@ class PowerflowCalcParam(NamedTuple): p_div_coolant_pump_mw=1.7970292653352464, f_p_div_coolant_pump_total_heat=0.0050000000000000001, p_plasma_rad_mw=287.44866938104849, - p_fw_alpha_mw=19.829653483586444, + p_fw_alpha_surface_total_mw=19.829653483586444, p_plasma_separatrix_mw=143.51338080047339, p_he=8000000, dp_he=550000, diff --git a/tests/unit/models/test_dcll.py b/tests/unit/models/test_dcll.py index 24623d814d..2210e07275 100644 --- a/tests/unit/models/test_dcll.py +++ b/tests/unit/models/test_dcll.py @@ -38,7 +38,7 @@ class DcllNeutronicsAndPowerParam(NamedTuple): p_plasma_rad_mw: Any = None - p_fw_alpha_mw: Any = None + p_fw_alpha_surface_total_mw: Any = None expected_p_fw_nuclear_heat_total_mw: Any = None @@ -62,7 +62,7 @@ class DcllNeutronicsAndPowerParam(NamedTuple): n_divertors=1, p_neutron_total_mw=1587.7386535917431, p_plasma_rad_mw=287.44866938104849, - p_fw_alpha_mw=19.835845058655043, + p_fw_alpha_surface_total_mw=19.835845058655043, expected_p_fw_nuclear_heat_total_mw=196.72081918001697, expected_p_blkt_nuclear_heat_total_mw=1533.4949914565693, expected_p_blkt_multiplication_mw=325.06710220789364, @@ -79,7 +79,7 @@ class DcllNeutronicsAndPowerParam(NamedTuple): n_divertors=1, p_neutron_total_mw=1587.2430556964196, p_plasma_rad_mw=287.44866938104849, - p_fw_alpha_mw=19.829653483586444, + p_fw_alpha_surface_total_mw=19.829653483586444, expected_p_fw_nuclear_heat_total_mw=196.65941460078642, expected_p_blkt_nuclear_heat_total_mw=1533.0163252173013, expected_p_blkt_multiplication_mw=324.96563552675644, From 9aa34bca569016542e8a9c912b4daaf4319c217a Mon Sep 17 00:00:00 2001 From: mn3981 Date: Mon, 8 Jun 2026 13:19:01 +0100 Subject: [PATCH 07/25] Add alpha particle heat flux variables for inboard and outboard first wall --- process/data_structure/fwbs_variables.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/process/data_structure/fwbs_variables.py b/process/data_structure/fwbs_variables.py index a50fc34fc9..3c9e0398dc 100644 --- a/process/data_structure/fwbs_variables.py +++ b/process/data_structure/fwbs_variables.py @@ -176,6 +176,12 @@ class FWBSData: p_fw_outboard_surface_heat_mw: float = 0.0 """Surface heat flux on outboard first wall [MW]""" + p_fw_inboard_alpha_surface_mw: float = 0.0 + """Alpha particle heat flux on inboard first wall [MW]""" + + p_fw_outboard_alpha_surface_mw: float = 0.0 + """Alpha particle heat flux on outboard first wall [MW]""" + vol_fw_total: float = 0.0 """First wall volume [m3]""" From 1c89fcf7a52fd23c3b7a3c0d2914945c0bcf2149 Mon Sep 17 00:00:00 2001 From: mn3981 Date: Mon, 8 Jun 2026 13:24:15 +0100 Subject: [PATCH 08/25] Add alpha power deposition calculations for first wall surfaces --- process/models/fw.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/process/models/fw.py b/process/models/fw.py index 937281b5a5..43bdac59e4 100644 --- a/process/models/fw.py +++ b/process/models/fw.py @@ -167,6 +167,11 @@ def run(self): self.data.physics.p_alpha_total_mw * (1.0e0 - self.data.physics.f_p_alpha_plasma_deposited) ) + + # Will assume that all alpha power reaching the first wall is deposited on the + # outboard side. + self.data.fwbs.p_fw_inboard_alpha_surface_mw = 0.0 + self.data.fwbs.p_fw_outboard_alpha_surface_mw = self.data.physics.p_fw_alpha_surface_total_mw @staticmethod def calculate_first_wall_half_height( From ba2355d11bb035e8d7bad0ecb2b2400f2664779b Mon Sep 17 00:00:00 2001 From: mn3981 Date: Mon, 8 Jun 2026 13:37:36 +0100 Subject: [PATCH 09/25] Refactor heat flux calculations by removing redundant code and updating test parameters for HCPB model --- process/models/blankets/dcll.py | 14 ----------- process/models/blankets/hcpb.py | 14 ----------- process/models/fw.py | 25 +++++++++++++++++--- tests/unit/models/blankets/test_ccfe_hcpb.py | 20 ++-------------- 4 files changed, 24 insertions(+), 49 deletions(-) diff --git a/process/models/blankets/dcll.py b/process/models/blankets/dcll.py index 9975a9e5f9..34c2103e73 100644 --- a/process/models/blankets/dcll.py +++ b/process/models/blankets/dcll.py @@ -228,20 +228,6 @@ def dcll_neutronics_and_power(self, output: bool): # FW - # Surface heat flux on first wall (MW) - # All of the fast particle losses go to the outer wall. - self.data.fwbs.p_fw_outboard_surface_heat_mw = ( - self.data.fwbs.p_fw_rad_total_mw - * self.data.first_wall.a_fw_outboard - / self.data.first_wall.a_fw_total - + self.data.current_drive.p_beam_orbit_loss_mw - + self.data.physics.p_fw_alpha_surface_total_mw - ) - self.data.fwbs.p_fw_inboard_surface_heat_mw = ( - self.data.fwbs.p_fw_rad_total_mw - * (1 - self.data.first_wall.a_fw_outboard / self.data.first_wall.a_fw_total) - ) - if output: po.osubhd( self.outfile, "DCLL model: Nuclear and Radiation Heating of Components" diff --git a/process/models/blankets/hcpb.py b/process/models/blankets/hcpb.py index ffd6e9c927..fe1b8a53ec 100644 --- a/process/models/blankets/hcpb.py +++ b/process/models/blankets/hcpb.py @@ -793,20 +793,6 @@ def powerflow_calc(self, output: bool): outlet_saturated_fluid_properties.temperature - 20.0 ) # in K - # Surface heat flux on first wall (outboard and inboard) (MW) - # All of the fast particle losses go to the outer wall. - self.data.fwbs.p_fw_outboard_surface_heat_mw = ( - self.data.fwbs.p_fw_rad_total_mw - * self.data.first_wall.a_fw_outboard - / self.data.first_wall.a_fw_total - + self.data.current_drive.p_beam_orbit_loss_mw - + self.data.physics.p_fw_alpha_surface_total_mw - ) - self.data.fwbs.p_fw_inboard_surface_heat_mw = ( - self.data.fwbs.p_fw_rad_total_mw - * (1 - self.data.first_wall.a_fw_outboard / self.data.first_wall.a_fw_total) - ) - i_p_coolant_pumping = PumpingPowerModelTypes(self.data.fwbs.i_p_coolant_pumping) if i_p_coolant_pumping == PumpingPowerModelTypes.FRACTION_OF_HEAT: # User sets mechanical pumping power directly diff --git a/process/models/fw.py b/process/models/fw.py index 43bdac59e4..21e901eac6 100644 --- a/process/models/fw.py +++ b/process/models/fw.py @@ -167,11 +167,30 @@ def run(self): self.data.physics.p_alpha_total_mw * (1.0e0 - self.data.physics.f_p_alpha_plasma_deposited) ) - - # Will assume that all alpha power reaching the first wall is deposited on the + + # Will assume that all alpha power reaching the first wall is deposited on the # outboard side. self.data.fwbs.p_fw_inboard_alpha_surface_mw = 0.0 - self.data.fwbs.p_fw_outboard_alpha_surface_mw = self.data.physics.p_fw_alpha_surface_total_mw + self.data.fwbs.p_fw_outboard_alpha_surface_mw = ( + self.data.physics.p_fw_alpha_surface_total_mw + ) + + # Surface heat flux on first wall (MW) + # All of the fast particle losses go to the outer wall, as do all beam losses + # and shine through. + # Some power is lost to HCD and ports on the outboard wall, so this is + # taken into account with a coverage factor. + self.data.fwbs.p_fw_outboard_surface_heat_mw = ( + self.data.fwbs.p_fw_outboard_rad_mw + + self.data.current_drive.p_beam_orbit_loss_mw + + self.data.fwbs.p_fw_outboard_alpha_surface_mw + + self.data.current_drive.p_beam_shine_through_mw + ) * (1.0 - self.data.fwbs.f_a_fw_outboard_hcd) + + self.data.fwbs.p_fw_inboard_surface_heat_mw = ( + self.data.fwbs.p_fw_inboard_rad_mw + + self.data.fwbs.p_fw_inboard_alpha_surface_mw + ) @staticmethod def calculate_first_wall_half_height( diff --git a/tests/unit/models/blankets/test_ccfe_hcpb.py b/tests/unit/models/blankets/test_ccfe_hcpb.py index 0ff1561a81..2795333215 100644 --- a/tests/unit/models/blankets/test_ccfe_hcpb.py +++ b/tests/unit/models/blankets/test_ccfe_hcpb.py @@ -620,10 +620,6 @@ class PowerflowCalcParam(NamedTuple): expected_p_div_rad_total_mw: Any = None - expected_psurffwi: Any = None - - expected_psurffwo: Any = None - expected_p_shld_coolant_pump_mw: Any = None expected_p_div_coolant_pump_mw: Any = None @@ -671,11 +667,9 @@ class PowerflowCalcParam(NamedTuple): t_in_bb=573.13, t_out_bb=773.13, p_fw_blkt_coolant_pump_mw=0, - expected_psurffwi=97.271629070225231, - expected_psurffwo=176.95628839065773, expected_p_shld_coolant_pump_mw=0.0068056297940224456, expected_p_div_coolant_pump_mw=1.7970292653352464, - expected_p_fw_blkt_coolant_pump_mw=202.00455086503842, + expected_p_fw_blkt_coolant_pump_mw=175.06074627472202, ), PowerflowCalcParam( a_fw_outboard=1168.1172772224481, @@ -714,11 +708,9 @@ class PowerflowCalcParam(NamedTuple): t_in_bb=573.13, t_out_bb=773.13, p_fw_blkt_coolant_pump_mw=202.00455086503842, - expected_psurffwi=97.271629070225259, - expected_psurffwo=176.95009681558912, expected_p_shld_coolant_pump_mw=0.007019085478296147, expected_p_div_coolant_pump_mw=1.7961533897828594, - expected_p_fw_blkt_coolant_pump_mw=201.94492795635171, + expected_p_fw_blkt_coolant_pump_mw=201.94553629918676, ), ], ) @@ -810,14 +802,6 @@ def test_powerflow_calc(powerflowcalcparam, monkeypatch, ccfe_hcpb): ccfe_hcpb.powerflow_calc(False) - assert ccfe_hcpb.data.fwbs.p_fw_inboard_surface_heat_mw == pytest.approx( - powerflowcalcparam.expected_psurffwi - ) - - assert ccfe_hcpb.data.fwbs.p_fw_outboard_surface_heat_mw == pytest.approx( - powerflowcalcparam.expected_psurffwo - ) - assert ccfe_hcpb.data.heat_transport.p_shld_coolant_pump_mw == pytest.approx( powerflowcalcparam.expected_p_shld_coolant_pump_mw ) From c0d8dcc47deb5643a86b89fe0a82939777ba40cc Mon Sep 17 00:00:00 2001 From: mn3981 Date: Mon, 8 Jun 2026 14:38:53 +0100 Subject: [PATCH 10/25] Update radiation and alpha particle heat flux descriptions for first wall surfaces --- process/core/io/plot/summary.py | 6 +++-- process/models/fw.py | 40 ++++++++++++++++++++++++++++----- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/process/core/io/plot/summary.py b/process/core/io/plot/summary.py index 9288a83037..ab3120cd9c 100644 --- a/process/core/io/plot/summary.py +++ b/process/core/io/plot/summary.py @@ -14822,7 +14822,8 @@ def plot_poloidal_power_distribution( ax.text( label_x, label_y, - f"{deg_blkt_outboard_poloidal_plasma:.1f}°\n({f_deg_blkt_outboard_poloidal_plasma * 100:.1f}%)", + f"$P_{{\\gamma}}$={m_file.get('p_fw_outboard_rad_mw', scan=scan):.1f}MW\n" + f"$P_{{\\alpha}}$={m_file.get('p_fw_outboard_alpha_surface_mw', scan=scan):.1f}MW", fontsize=7, color="purple", ha="center", @@ -14875,7 +14876,8 @@ def plot_poloidal_power_distribution( ax.text( label_x, label_y, - f"{deg_blkt_inboard_poloidal_plasma:.1f}°\n({f_deg_blkt_inboard_poloidal_plasma * 100:.1f}%)", + f"$P_{{\\gamma}}$={m_file.get('p_fw_inboard_rad_mw', scan=scan):.1f}MW\n" + f"$P_{{\\alpha}}$={m_file.get('p_fw_inboard_alpha_surface_mw', scan=scan):.1f}MW", fontsize=7, color="green", ha="center", diff --git a/process/models/fw.py b/process/models/fw.py index 21e901eac6..af8f10ecb2 100644 --- a/process/models/fw.py +++ b/process/models/fw.py @@ -868,7 +868,7 @@ def output_fw_surface_loads(self): po.ovarre( self.outfile, - "Nominal mean radiation load on vessel first-wall (MW/m^2)", + "Nominal mean radiation load on vessel first-wall [MW/m²]", "(pflux_fw_rad_mw)", self.data.physics.pflux_fw_rad_mw, "OP ", @@ -882,28 +882,58 @@ def output_fw_surface_loads(self): ) po.ovarre( self.outfile, - "Maximum permitted radiation first-wall load (MW/m^2)", + "Maximum permitted radiation first-wall load [MW/m²]", "(pflux_fw_rad_max)", self.data.constraints.pflux_fw_rad_max, "IP ", ) po.ovarre( self.outfile, - "Peak radiation wall load (MW/m^2)", + "Peak radiation wall load [MW/m²]", "(pflux_fw_rad_max_mw)", self.data.constraints.pflux_fw_rad_max_mw, "OP ", ) po.ovarre( self.outfile, - "Fast alpha particle power incident on the first-wall (MW)", + "Radiation heat flux on inboard first wall [MW]", + "(p_fw_inboard_rad_mw)", + self.data.fwbs.p_fw_inboard_rad_mw, + "OP ", + ) + po.ovarre( + self.outfile, + "Radiation heat flux on outboard first wall [MW]", + "(p_fw_outboard_rad_mw)", + self.data.fwbs.p_fw_outboard_rad_mw, + "OP ", + ) + po.oblnkl(self.outfile) + po.ovarre( + self.outfile, + "Alpha particle heat flux on inboard first wall [MW]", + "(p_fw_inboard_alpha_surface_mw)", + self.data.fwbs.p_fw_inboard_alpha_surface_mw, + "OP ", + ) + po.ovarre( + self.outfile, + "Alpha particle heat flux on outboard first wall [MW]", + "(p_fw_outboard_alpha_surface_mw)", + self.data.fwbs.p_fw_outboard_alpha_surface_mw, + "OP ", + ) + po.ovarre( + self.outfile, + "Fast alpha particle power incident on the first-wall [MW]", "(p_fw_alpha_surface_total_mw)", self.data.physics.p_fw_alpha_surface_total_mw, "OP ", ) + po.oblnkl(self.outfile) po.ovarre( self.outfile, - "Nominal mean neutron load on vessel first-wall (MW/m^2)", + "Nominal mean neutron load on vessel first-wall [MW/m²]", "(pflux_fw_neutron_mw)", self.data.physics.pflux_fw_neutron_mw, "OP ", From 8f675b7570cbc94c003ab8fb667d03ef2db669a8 Mon Sep 17 00:00:00 2001 From: mn3981 Date: Mon, 8 Jun 2026 15:04:16 +0100 Subject: [PATCH 11/25] Change FW load logic --- process/models/fw.py | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/process/models/fw.py b/process/models/fw.py index af8f10ecb2..81e6813b99 100644 --- a/process/models/fw.py +++ b/process/models/fw.py @@ -128,23 +128,25 @@ def run(self): self.data.physics.p_neutron_total_mw / self.data.first_wall.a_fw_total ) - # Radiation power incident on first wall (MW) - # Set based on the total radiation power and the angular fractions taken up by - # the inboard and outboard first wall, which are calculated based on the - # geometry of the first wall and the plasma. - self.data.fwbs.p_fw_rad_total_mw = self.data.physics.p_plasma_rad_mw * ( - self.data.blanket.f_deg_blkt_outboard_poloidal_plasma - + self.data.blanket.f_deg_blkt_inboard_poloidal_plasma - ) - self.data.fwbs.p_fw_inboard_rad_mw = ( self.data.physics.p_plasma_rad_mw * self.data.blanket.f_deg_blkt_inboard_poloidal_plasma ) + # Some power is lost to HCD and ports on the outboard wall, so this is taken + # into account with a coverage factor. self.data.fwbs.p_fw_outboard_rad_mw = ( self.data.physics.p_plasma_rad_mw * self.data.blanket.f_deg_blkt_outboard_poloidal_plasma + * (1.0 - self.data.fwbs.f_a_fw_outboard_hcd) + ) + + # Radiation power incident on first wall (MW) + # Set based on the total radiation power and the angular fractions taken up by + # the inboard and outboard first wall, which are calculated based on the + # geometry of the first wall and the plasma. + self.data.fwbs.p_fw_rad_total_mw = ( + self.data.fwbs.p_fw_inboard_rad_mw + self.data.fwbs.p_fw_outboard_rad_mw ) if self.data.physics.i_pflux_fw_neutron == 1: @@ -163,16 +165,21 @@ def run(self): ) # Power transported to the first wall by escaped alpha particles - self.data.physics.p_fw_alpha_surface_total_mw = ( + # Some is lost to HCD and ports on the outboard wall, so this is taken into + # account with a coverage factor. + + self.data.fwbs.p_fw_outboard_alpha_surface_mw = ( self.data.physics.p_alpha_total_mw * (1.0e0 - self.data.physics.f_p_alpha_plasma_deposited) + * (1.0 - self.data.fwbs.f_a_fw_outboard_hcd) ) - # Will assume that all alpha power reaching the first wall is deposited on the # outboard side. self.data.fwbs.p_fw_inboard_alpha_surface_mw = 0.0 - self.data.fwbs.p_fw_outboard_alpha_surface_mw = ( - self.data.physics.p_fw_alpha_surface_total_mw + + self.data.physics.p_fw_alpha_surface_total_mw = ( + self.data.fwbs.p_fw_outboard_alpha_surface_mw + + self.data.fwbs.p_fw_inboard_alpha_surface_mw ) # Surface heat flux on first wall (MW) @@ -185,7 +192,7 @@ def run(self): + self.data.current_drive.p_beam_orbit_loss_mw + self.data.fwbs.p_fw_outboard_alpha_surface_mw + self.data.current_drive.p_beam_shine_through_mw - ) * (1.0 - self.data.fwbs.f_a_fw_outboard_hcd) + ) self.data.fwbs.p_fw_inboard_surface_heat_mw = ( self.data.fwbs.p_fw_inboard_rad_mw From 1454028b2c42a69c3684445ace8928cb3fc9e8e0 Mon Sep 17 00:00:00 2001 From: mn3981 Date: Mon, 8 Jun 2026 15:30:50 +0100 Subject: [PATCH 12/25] Add new surface flux variables --- process/data_structure/fwbs_variables.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/process/data_structure/fwbs_variables.py b/process/data_structure/fwbs_variables.py index 3c9e0398dc..6e5420d68a 100644 --- a/process/data_structure/fwbs_variables.py +++ b/process/data_structure/fwbs_variables.py @@ -182,6 +182,18 @@ class FWBSData: p_fw_outboard_alpha_surface_mw: float = 0.0 """Alpha particle heat flux on outboard first wall [MW]""" + pflux_fw_inboard_neutron_surface_average_mw: float = 0.0 + """Average neutron flux on inboard first wall surface [MW/m²]""" + + pflux_fw_outboard_neutron_surface_average_mw: float = 0.0 + """Average neutron flux on outboard first wall surface [MW/m²]""" + + pflux_fw_inboard_rad_surface_average_mw: float = 0.0 + """Average radiation flux on inboard first wall surface [MW/m²]""" + + pflux_fw_outboard_rad_surface_average_mw: float = 0.0 + """Average radiation flux on outboard first wall surface [MW/m²]""" + vol_fw_total: float = 0.0 """First wall volume [m3]""" From 2e8457bb4e3733d3d62971945fb57c42dcc9913b Mon Sep 17 00:00:00 2001 From: mn3981 Date: Mon, 8 Jun 2026 16:45:56 +0100 Subject: [PATCH 13/25] Add radiation outboard surface values --- process/models/fw.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/process/models/fw.py b/process/models/fw.py index 81e6813b99..c093df324d 100644 --- a/process/models/fw.py +++ b/process/models/fw.py @@ -149,6 +149,21 @@ def run(self): self.data.fwbs.p_fw_inboard_rad_mw + self.data.fwbs.p_fw_outboard_rad_mw ) + # Radiation surface heat flux on first wall (MW/m²) + # The full area is used as the radiation is assumed to be uniformly distributed + # across the first wall, so the coverage factors are not applied here. + self.data.fwbs.pflux_fw_inboard_rad_surface_average_mw = ( + self.data.physics.p_plasma_rad_mw + * self.data.blanket.f_deg_blkt_inboard_poloidal_plasma + / self.data.first_wall.a_fw_inboard_full_coverage + ) + + self.data.fwbs.pflux_fw_outboard_rad_surface_average_mw = ( + self.data.physics.p_plasma_rad_mw + * self.data.blanket.f_deg_blkt_outboard_poloidal_plasma + / self.data.first_wall.a_fw_outboard_full_coverage + ) + if self.data.physics.i_pflux_fw_neutron == 1: self.data.physics.pflux_fw_rad_mw = ( self.data.physics.ffwal @@ -908,6 +923,13 @@ def output_fw_surface_loads(self): self.data.fwbs.p_fw_inboard_rad_mw, "OP ", ) + po.ovarre( + self.outfile, + "Radiation surface heat flux on inboard first wall [MW/m²]", + "(pflux_fw_inboard_rad_surface_average_mw)", + self.data.fwbs.pflux_fw_inboard_rad_surface_average_mw, + "OP ", + ) po.ovarre( self.outfile, "Radiation heat flux on outboard first wall [MW]", @@ -915,6 +937,13 @@ def output_fw_surface_loads(self): self.data.fwbs.p_fw_outboard_rad_mw, "OP ", ) + po.ovarre( + self.outfile, + "Radiation surface heat flux on outboard first wall [MW/m²]", + "(pflux_fw_outboard_rad_surface_average_mw)", + self.data.fwbs.pflux_fw_outboard_rad_surface_average_mw, + "OP ", + ) po.oblnkl(self.outfile) po.ovarre( self.outfile, From 88962e09882e5f29a6f0fca4d2272561a9078a5d Mon Sep 17 00:00:00 2001 From: mn3981 Date: Tue, 9 Jun 2026 11:26:27 +0100 Subject: [PATCH 14/25] Add neutron surface heat flux calculations for first wall components --- process/core/io/plot/summary.py | 76 ++++++++++++++++++++++++--------- process/models/fw.py | 30 ++++++++++++- 2 files changed, 85 insertions(+), 21 deletions(-) diff --git a/process/core/io/plot/summary.py b/process/core/io/plot/summary.py index ab3120cd9c..0b0e76b9e3 100644 --- a/process/core/io/plot/summary.py +++ b/process/core/io/plot/summary.py @@ -14812,18 +14812,13 @@ def plot_poloidal_power_distribution( ax.plot(arc_x, arc_y, color="purple", linewidth=2) - # Add angle label at the arc - mid_angle = np.deg2rad((angle_start + angle_end) / 2) - label_radius = arc_radius * 1.8 - label_x = rmajor + label_radius * np.cos(mid_angle) - label_y = label_radius * np.sin(mid_angle) - # Plot the info box for the outboard blanket ax.text( - label_x, - label_y, - f"$P_{{\\gamma}}$={m_file.get('p_fw_outboard_rad_mw', scan=scan):.1f}MW\n" - f"$P_{{\\alpha}}$={m_file.get('p_fw_outboard_alpha_surface_mw', scan=scan):.1f}MW", + rmajor * 1.75, + 0.0, + f"$P_{{\\gamma}}$={m_file.get('p_fw_outboard_rad_mw', scan=scan):.3f} MW\n" + f"$\\Gamma_{{\\gamma}}$={m_file.get('pflux_fw_outboard_rad_surface_average_mw', scan=scan):.3f} MW/m²\n\n" + f"$P_{{\\alpha}}$={m_file.get('p_fw_outboard_alpha_surface_mw', scan=scan):.3f} MW", fontsize=7, color="purple", ha="center", @@ -14832,12 +14827,21 @@ def plot_poloidal_power_distribution( bbox={ "boxstyle": "round", "facecolor": "white", - "alpha": 0.8, + "alpha": 1.0, "edgecolor": "purple", "linewidth": 1.5, }, ) + # Plot arrow for the outboard blanket + ax.annotate( + "", + xy=(rmajor, 0), + xytext=(rmajor + rminor, 0), + arrowprops={"arrowstyle": "<-", "color": "purple", "linewidth": 1.5}, + zorder=5, + ) + # Plot arrows for the inboard blanket angles ax.annotate( "", @@ -14847,6 +14851,28 @@ def plot_poloidal_power_distribution( zorder=5, ) + # Plot the total loads at the centre of the plasma + ax.text( + rmajor, + 0.0, + f"$P_{{\\gamma}}$={m_file.get('p_plasma_rad_mw', scan=scan):.1f}MW\n" + f"$P_{{\\alpha}}$={m_file.get('p_fw_outboard_alpha_surface_mw', scan=scan):.1f}MW\n" + f"$P_n$={m_file.get('p_neutron_total_mw', scan=scan):.1f}MW", + fontsize=7, + color="black", + ha="center", + va="center", + weight="bold", + bbox={ + "boxstyle": "round", + "facecolor": "wheat", + "alpha": 1.0, + "edgecolor": "black", + "linewidth": 1.5, + }, + zorder=20, + ) + ax.annotate( "", xy=(rmajor, 0), @@ -14872,12 +14898,13 @@ def plot_poloidal_power_distribution( label_x = rmajor - label_radius * np.cos(mid_angle) label_y = label_radius * np.sin(mid_angle) - # Plot the info box for the inboard blanket + # Plot the info box for the inboard blanket/FW ax.text( - label_x, - label_y, - f"$P_{{\\gamma}}$={m_file.get('p_fw_inboard_rad_mw', scan=scan):.1f}MW\n" - f"$P_{{\\alpha}}$={m_file.get('p_fw_inboard_alpha_surface_mw', scan=scan):.1f}MW", + rmajor / 3, + 0.0, + f"$P_{{\\gamma}}$={m_file.get('p_fw_inboard_rad_mw', scan=scan):.2f} MW\n" + f"$\\Gamma_{{\\gamma}}$={m_file.get('pflux_fw_inboard_rad_surface_average_mw', scan=scan):.3f} MW/m²\n\n" + f"$P_{{\\alpha}}$={m_file.get('p_fw_inboard_alpha_surface_mw', scan=scan):.2f}MW", fontsize=7, color="green", ha="center", @@ -14886,13 +14913,22 @@ def plot_poloidal_power_distribution( bbox={ "boxstyle": "round", "facecolor": "white", - "alpha": 0.8, + "alpha": 1.0, "edgecolor": "green", "linewidth": 1.5, }, zorder=5, ) + # Plot arrow for the inboard blanket + ax.annotate( + "", + xy=(rmajor, 0), + xytext=(rmajor - rminor, 0), + arrowprops={"arrowstyle": "<-", "color": "green", "linewidth": 1.5}, + zorder=5, + ) + # Plot arrows for the divertor angles # If double null then plot the upper also if i_single_null == 0: @@ -14926,7 +14962,7 @@ def plot_poloidal_power_distribution( bbox={ "boxstyle": "round", "facecolor": "white", - "alpha": 0.8, + "alpha": 1.0, "edgecolor": "black", "linewidth": 1.5, }, @@ -14964,7 +15000,7 @@ def plot_poloidal_power_distribution( bbox={ "boxstyle": "round", "facecolor": "white", - "alpha": 0.8, + "alpha": 1.0, "edgecolor": "black", "linewidth": 1.5, }, @@ -14999,6 +15035,8 @@ def plot_poloidal_power_distribution( label="Midplane", ) + ax.set_xlim(-rmajor / 4, 2 * rmajor) + def plot_detailed_plasma_parameters(axis: plt.Axes, fig, mfile: MFile, scan: int): """Function to plot detailed plasma parameters from physics data. diff --git a/process/models/fw.py b/process/models/fw.py index c093df324d..5c85ad902d 100644 --- a/process/models/fw.py +++ b/process/models/fw.py @@ -150,14 +150,14 @@ def run(self): ) # Radiation surface heat flux on first wall (MW/m²) - # The full area is used as the radiation is assumed to be uniformly distributed + # The full area is used as the radiation is assumed to be uniformly distributed # across the first wall, so the coverage factors are not applied here. self.data.fwbs.pflux_fw_inboard_rad_surface_average_mw = ( self.data.physics.p_plasma_rad_mw * self.data.blanket.f_deg_blkt_inboard_poloidal_plasma / self.data.first_wall.a_fw_inboard_full_coverage ) - + self.data.fwbs.pflux_fw_outboard_rad_surface_average_mw = ( self.data.physics.p_plasma_rad_mw * self.data.blanket.f_deg_blkt_outboard_poloidal_plasma @@ -214,6 +214,18 @@ def run(self): + self.data.fwbs.p_fw_inboard_alpha_surface_mw ) + self.data.fwbs.pflux_fw_outboard_neutron_surface_average_mw = ( + self.data.physics.p_neutron_total_mw + * self.data.blanket.f_deg_blkt_outboard_poloidal_plasma + / self.data.first_wall.a_fw_outboard_full_coverage + ) + + self.data.fwbs.pflux_fw_inboard_neutron_surface_average_mw = ( + self.data.physics.p_neutron_total_mw + * self.data.blanket.f_deg_blkt_inboard_poloidal_plasma + / self.data.first_wall.a_fw_inboard_full_coverage + ) + @staticmethod def calculate_first_wall_half_height( z_plasma_xpoint_lower: float, @@ -974,3 +986,17 @@ def output_fw_surface_loads(self): self.data.physics.pflux_fw_neutron_mw, "OP ", ) + po.ovarre( + self.outfile, + "Nominal mean neutron load on inboard first-wall [MW/m²]", + "(pflux_fw_inboard_neutron_mw)", + self.data.fwbs.pflux_fw_inboard_neutron_surface_average_mw, + "OP ", + ) + po.ovarre( + self.outfile, + "Nominal mean neutron load on outboard first-wall [MW/m²]", + "(pflux_fw_outboard_neutron_mw)", + self.data.fwbs.pflux_fw_outboard_neutron_surface_average_mw, + "OP ", + ) From ec5fbb454649d3f076a461a296ff36451c74001b Mon Sep 17 00:00:00 2001 From: mn3981 Date: Tue, 9 Jun 2026 13:47:28 +0100 Subject: [PATCH 15/25] Add neutron heat flux variables for inboard and outboard first wall components --- process/core/io/plot/summary.py | 19 +++++++++++----- process/data_structure/fwbs_variables.py | 6 +++++ process/models/fw.py | 28 ++++++++++++++++++++++-- 3 files changed, 45 insertions(+), 8 deletions(-) diff --git a/process/core/io/plot/summary.py b/process/core/io/plot/summary.py index 0b0e76b9e3..1cf5ac3e71 100644 --- a/process/core/io/plot/summary.py +++ b/process/core/io/plot/summary.py @@ -14717,6 +14717,8 @@ def plot_poloidal_power_distribution( # MFILE variables needed to plot the blkt structure and angles rmajor = m_file.get("rmajor", scan=scan) rminor = m_file.get("rminor", scan=scan) + triang = m_file.get("triang", scan=scan) + kappa = m_file.get("kappa", scan=scan) dr_fw_plasma_gap_outboard = m_file.get("dr_fw_plasma_gap_outboard", scan=scan) dr_fw_plasma_gap_inboard = m_file.get("dr_fw_plasma_gap_inboard", scan=scan) dr_fw_inboard = m_file.get("dr_fw_inboard", scan=scan) @@ -14818,7 +14820,9 @@ def plot_poloidal_power_distribution( 0.0, f"$P_{{\\gamma}}$={m_file.get('p_fw_outboard_rad_mw', scan=scan):.3f} MW\n" f"$\\Gamma_{{\\gamma}}$={m_file.get('pflux_fw_outboard_rad_surface_average_mw', scan=scan):.3f} MW/m²\n\n" - f"$P_{{\\alpha}}$={m_file.get('p_fw_outboard_alpha_surface_mw', scan=scan):.3f} MW", + f"$P_{{\\alpha}}$={m_file.get('p_fw_outboard_alpha_surface_mw', scan=scan):.3f} MW\n\n" + f"$P_n$={m_file.get('p_fw_outboard_neutron_incident_mw', scan=scan):.3f} MW\n" + f"$\\Gamma_{{n}}$={m_file.get('pflux_fw_outboard_neutron_surface_average_mw', scan=scan):.3f} MW/m²\n", fontsize=7, color="purple", ha="center", @@ -14902,9 +14906,11 @@ def plot_poloidal_power_distribution( ax.text( rmajor / 3, 0.0, - f"$P_{{\\gamma}}$={m_file.get('p_fw_inboard_rad_mw', scan=scan):.2f} MW\n" + f"$P_{{\\gamma}}$={m_file.get('p_fw_inboard_rad_mw', scan=scan):.3f} MW\n" f"$\\Gamma_{{\\gamma}}$={m_file.get('pflux_fw_inboard_rad_surface_average_mw', scan=scan):.3f} MW/m²\n\n" - f"$P_{{\\alpha}}$={m_file.get('p_fw_inboard_alpha_surface_mw', scan=scan):.2f}MW", + f"$P_{{\\alpha}}$={m_file.get('p_fw_inboard_alpha_surface_mw', scan=scan):.3f} MW\n\n" + f"$P_n$={m_file.get('p_fw_inboard_neutron_incident_mw', scan=scan):.3f} MW\n" + f"$\\Gamma_{{n}}$={m_file.get('pflux_fw_inboard_neutron_surface_average_mw', scan=scan):.3f} MW/m²\n\n", fontsize=7, color="green", ha="center", @@ -14989,9 +14995,10 @@ def plot_poloidal_power_distribution( # Plot the info box for the lower divertor angle ax.text( - label_x, - label_y, - f"{deg_div_poloidal_plasma:.1f}°\n({f_ster_div_single * 100:.1f}%)", + rmajor - (triang * rminor), + -(rminor * kappa), + f"$P_{{\\gamma}}$={m_file.get('p_div_rad_total_mw', scan=scan):.3f} MW\n\n" + f"$P_n$={m_file.get('p_div_nuclear_heat_total_mw', scan=scan):.3f} MW\n", fontsize=7, color="black", ha="center", diff --git a/process/data_structure/fwbs_variables.py b/process/data_structure/fwbs_variables.py index 6e5420d68a..69738449fd 100644 --- a/process/data_structure/fwbs_variables.py +++ b/process/data_structure/fwbs_variables.py @@ -182,6 +182,12 @@ class FWBSData: p_fw_outboard_alpha_surface_mw: float = 0.0 """Alpha particle heat flux on outboard first wall [MW]""" + p_fw_inboard_neutron_incident_mw: float = 0.0 + """Neutron heat flux incident on inboard first wall [MW]""" + + p_fw_outboard_neutron_incident_mw: float = 0.0 + """Neutron heat flux incident on outboard first wall [MW]""" + pflux_fw_inboard_neutron_surface_average_mw: float = 0.0 """Average neutron flux on inboard first wall surface [MW/m²]""" diff --git a/process/models/fw.py b/process/models/fw.py index 5c85ad902d..8cf3439750 100644 --- a/process/models/fw.py +++ b/process/models/fw.py @@ -214,6 +214,16 @@ def run(self): + self.data.fwbs.p_fw_inboard_alpha_surface_mw ) + self.data.fwbs.p_fw_inboard_neutron_incident_mw = ( + self.data.physics.p_neutron_total_mw + * self.data.blanket.f_deg_blkt_inboard_poloidal_plasma + ) + + self.data.fwbs.p_fw_outboard_neutron_incident_mw = ( + self.data.physics.p_neutron_total_mw + * self.data.blanket.f_deg_blkt_outboard_poloidal_plasma + ) + self.data.fwbs.pflux_fw_outboard_neutron_surface_average_mw = ( self.data.physics.p_neutron_total_mw * self.data.blanket.f_deg_blkt_outboard_poloidal_plasma @@ -986,17 +996,31 @@ def output_fw_surface_loads(self): self.data.physics.pflux_fw_neutron_mw, "OP ", ) + po.ovarre( + self.outfile, + "Neutron heat flux on inboard first wall [MW]", + "(p_fw_inboard_neutron_incident_mw)", + self.data.fwbs.p_fw_inboard_neutron_incident_mw, + "OP ", + ) po.ovarre( self.outfile, "Nominal mean neutron load on inboard first-wall [MW/m²]", - "(pflux_fw_inboard_neutron_mw)", + "(pflux_fw_inboard_neutron_surface_average_mw)", self.data.fwbs.pflux_fw_inboard_neutron_surface_average_mw, "OP ", ) + po.ovarre( + self.outfile, + "Neutron heat flux on outboard first wall [MW]", + "(p_fw_outboard_neutron_incident_mw)", + self.data.fwbs.p_fw_outboard_neutron_incident_mw, + "OP ", + ) po.ovarre( self.outfile, "Nominal mean neutron load on outboard first-wall [MW/m²]", - "(pflux_fw_outboard_neutron_mw)", + "(pflux_fw_outboard_neutron_surface_average_mw)", self.data.fwbs.pflux_fw_outboard_neutron_surface_average_mw, "OP ", ) From 49ec8adcb57ab144a14ca4e79d130b246c89ba5a Mon Sep 17 00:00:00 2001 From: mn3981 Date: Tue, 9 Jun 2026 14:24:48 +0100 Subject: [PATCH 16/25] Refactor first wall radiation calculations and add unit tests for surface load calculations --- process/models/fw.py | 139 +++++++++++++++++++++++------------ tests/unit/models/test_fw.py | 75 +++++++++++++++++++ 2 files changed, 168 insertions(+), 46 deletions(-) diff --git a/process/models/fw.py b/process/models/fw.py index 8cf3439750..db34a523fa 100644 --- a/process/models/fw.py +++ b/process/models/fw.py @@ -128,17 +128,29 @@ def run(self): self.data.physics.p_neutron_total_mw / self.data.first_wall.a_fw_total ) - self.data.fwbs.p_fw_inboard_rad_mw = ( - self.data.physics.p_plasma_rad_mw - * self.data.blanket.f_deg_blkt_inboard_poloidal_plasma + # Radiation surface heat flux on first wall (MW/m²) + # The full area is used as the radiation is assumed to be uniformly distributed + # across the first wall, so the coverage factors are not applied here. + ( + self.data.fwbs.p_fw_inboard_rad_mw, + self.data.fwbs.pflux_fw_inboard_rad_surface_average_mw, + ) = self.calculate_fw_surface_load( + p_plasma_source_mw=self.data.physics.p_plasma_rad_mw, + f_deg_blkt_poloidal_plasma=self.data.blanket.f_deg_blkt_inboard_poloidal_plasma, + a_fw_full_coverage=self.data.first_wall.a_fw_inboard_full_coverage, + f_a_fw_hcd_ports=0.0, # Inboard is toroidally continous with no ports or + # HCD, so no coverage factor applied ) - # Some power is lost to HCD and ports on the outboard wall, so this is taken - # into account with a coverage factor. - self.data.fwbs.p_fw_outboard_rad_mw = ( - self.data.physics.p_plasma_rad_mw - * self.data.blanket.f_deg_blkt_outboard_poloidal_plasma - * (1.0 - self.data.fwbs.f_a_fw_outboard_hcd) + ( + self.data.fwbs.p_fw_outboard_rad_mw, + self.data.fwbs.pflux_fw_outboard_rad_surface_average_mw, + ) = self.calculate_fw_surface_load( + p_plasma_source_mw=self.data.physics.p_plasma_rad_mw, + f_deg_blkt_poloidal_plasma=self.data.blanket.f_deg_blkt_outboard_poloidal_plasma, + a_fw_full_coverage=self.data.first_wall.a_fw_outboard_full_coverage, + f_a_fw_hcd_ports=self.data.fwbs.f_a_fw_outboard_hcd, # Coverage factor + # applied to outboard wall to account for HCD and ports ) # Radiation power incident on first wall (MW) @@ -149,21 +161,6 @@ def run(self): self.data.fwbs.p_fw_inboard_rad_mw + self.data.fwbs.p_fw_outboard_rad_mw ) - # Radiation surface heat flux on first wall (MW/m²) - # The full area is used as the radiation is assumed to be uniformly distributed - # across the first wall, so the coverage factors are not applied here. - self.data.fwbs.pflux_fw_inboard_rad_surface_average_mw = ( - self.data.physics.p_plasma_rad_mw - * self.data.blanket.f_deg_blkt_inboard_poloidal_plasma - / self.data.first_wall.a_fw_inboard_full_coverage - ) - - self.data.fwbs.pflux_fw_outboard_rad_surface_average_mw = ( - self.data.physics.p_plasma_rad_mw - * self.data.blanket.f_deg_blkt_outboard_poloidal_plasma - / self.data.first_wall.a_fw_outboard_full_coverage - ) - if self.data.physics.i_pflux_fw_neutron == 1: self.data.physics.pflux_fw_rad_mw = ( self.data.physics.ffwal @@ -183,11 +180,20 @@ def run(self): # Some is lost to HCD and ports on the outboard wall, so this is taken into # account with a coverage factor. - self.data.fwbs.p_fw_outboard_alpha_surface_mw = ( - self.data.physics.p_alpha_total_mw - * (1.0e0 - self.data.physics.f_p_alpha_plasma_deposited) - * (1.0 - self.data.fwbs.f_a_fw_outboard_hcd) + ( + self.data.fwbs.p_fw_outboard_alpha_surface_mw, + _, + ) = self.calculate_fw_surface_load( + p_plasma_source_mw=( + self.data.physics.p_alpha_total_mw + * (1.0e0 - self.data.physics.f_p_alpha_plasma_deposited) + ), + f_deg_blkt_poloidal_plasma=1.0, + a_fw_full_coverage=self.data.first_wall.a_fw_outboard_full_coverage, + f_a_fw_hcd_ports=self.data.fwbs.f_a_fw_outboard_hcd, # Coverage factor + # applied to outboard wall to account for HCD and ports ) + # Will assume that all alpha power reaching the first wall is deposited on the # outboard side. self.data.fwbs.p_fw_inboard_alpha_surface_mw = 0.0 @@ -214,26 +220,26 @@ def run(self): + self.data.fwbs.p_fw_inboard_alpha_surface_mw ) - self.data.fwbs.p_fw_inboard_neutron_incident_mw = ( - self.data.physics.p_neutron_total_mw - * self.data.blanket.f_deg_blkt_inboard_poloidal_plasma - ) - - self.data.fwbs.p_fw_outboard_neutron_incident_mw = ( - self.data.physics.p_neutron_total_mw - * self.data.blanket.f_deg_blkt_outboard_poloidal_plasma - ) - - self.data.fwbs.pflux_fw_outboard_neutron_surface_average_mw = ( - self.data.physics.p_neutron_total_mw - * self.data.blanket.f_deg_blkt_outboard_poloidal_plasma - / self.data.first_wall.a_fw_outboard_full_coverage + ( + self.data.fwbs.p_fw_inboard_neutron_incident_mw, + self.data.fwbs.pflux_fw_inboard_neutron_surface_average_mw, + ) = self.calculate_fw_surface_load( + p_plasma_source_mw=self.data.physics.p_neutron_total_mw, + f_deg_blkt_poloidal_plasma=self.data.blanket.f_deg_blkt_inboard_poloidal_plasma, + a_fw_full_coverage=self.data.first_wall.a_fw_inboard_full_coverage, + f_a_fw_hcd_ports=0.0, # Inboard is toroidally continous with no ports + # or HCD, so no coverage factor applied ) - self.data.fwbs.pflux_fw_inboard_neutron_surface_average_mw = ( - self.data.physics.p_neutron_total_mw - * self.data.blanket.f_deg_blkt_inboard_poloidal_plasma - / self.data.first_wall.a_fw_inboard_full_coverage + ( + self.data.fwbs.p_fw_outboard_neutron_incident_mw, + self.data.fwbs.pflux_fw_outboard_neutron_surface_average_mw, + ) = self.calculate_fw_surface_load( + p_plasma_source_mw=self.data.physics.p_neutron_total_mw, + f_deg_blkt_poloidal_plasma=self.data.blanket.f_deg_blkt_outboard_poloidal_plasma, + a_fw_full_coverage=self.data.first_wall.a_fw_outboard_full_coverage, + f_a_fw_hcd_ports=self.data.fwbs.f_a_fw_outboard_hcd, # Coverage factor + # applied to outboard wall to account for HCD and ports ) @staticmethod @@ -764,6 +770,47 @@ def fw_temp( mflow_fw_coolant, ) + @staticmethod + def calculate_fw_surface_load( + p_plasma_source_mw: float, + f_deg_blkt_poloidal_plasma: float, + a_fw_full_coverage: float, + f_a_fw_hcd_ports: float = 0.0, + ) -> tuple[float, float]: + """Calculate the surface load on the first wall due to some incident power + + Parameters + ---------- + p_plasma_source_mw: + Total plasma power source, can be neutrons or other forms of energy etc [MW] + f_deg_blkt_poloidal_plasma: + Fraction of the plasma poloidal circumference covered by the first wall. + a_fw_full_coverage: + Area of the first wall with full coverage [m²]. + f_a_fw_hcd_ports: + Fraction of the first wall area occupied by HCD and ports. + This is a loss term that reduces the effective area for power deposition on + the first wall. Default is 0.0 (no loss). + + Returns + ------- + tuple + Power incident on the first wall [MW] and the surface heat flux on the + first wall [MW/m²]. + + Notes + ----- + We use the full coverage area here as the surface is toroidally continous and + the radiation is assumed to be uniformly distributed across the first wall, + so the coverage factors are not applied here. + """ + p_fw_incident_mw = ( + p_plasma_source_mw * f_deg_blkt_poloidal_plasma * (1.0 - f_a_fw_hcd_ports) + ) + pflux_fw_inboard_surface_average_mw = p_fw_incident_mw / a_fw_full_coverage + + return p_fw_incident_mw, pflux_fw_inboard_surface_average_mw + @staticmethod def calculate_total_fw_channels( a_fw_inboard: float, diff --git a/tests/unit/models/test_fw.py b/tests/unit/models/test_fw.py index 2ad7c0e33c..4c792a2746 100644 --- a/tests/unit/models/test_fw.py +++ b/tests/unit/models/test_fw.py @@ -162,3 +162,78 @@ def test_fw_temp(fwtempparam, monkeypatch, fw): assert rhofmean == pytest.approx(fwtempparam.expected_rhofmean, rel=1e-4) assert massrate == pytest.approx(fwtempparam.expected_massrate, rel=1e-4) + + +class CalculateFwSurfaceLoadParam(NamedTuple): + p_plasma_source_mw: float + f_deg_blkt_poloidal_plasma: float + a_fw_full_coverage: float + f_a_fw_hcd_ports: float = 0.0 + expected_p_fw_incident_mw: float = None + expected_pflux_fw_surface_mw_m2: float = None + label: str = None + + +@pytest.mark.parametrize( + "calculatefwsurfaceloadparam", + [ + CalculateFwSurfaceLoadParam( + p_plasma_source_mw=1000.0, + f_deg_blkt_poloidal_plasma=0.8, + a_fw_full_coverage=100.0, + f_a_fw_hcd_ports=0.0, + expected_p_fw_incident_mw=800.0, + expected_pflux_fw_surface_mw_m2=8.0, + label="No HCD ports loss", + ), + CalculateFwSurfaceLoadParam( + p_plasma_source_mw=1000.0, + f_deg_blkt_poloidal_plasma=0.8, + a_fw_full_coverage=100.0, + f_a_fw_hcd_ports=0.1, + expected_p_fw_incident_mw=720.0, + expected_pflux_fw_surface_mw_m2=7.2, + label="With HCD ports loss", + ), + CalculateFwSurfaceLoadParam( + p_plasma_source_mw=500.0, + f_deg_blkt_poloidal_plasma=1.0, + a_fw_full_coverage=50.0, + f_a_fw_hcd_ports=0.2, + expected_p_fw_incident_mw=400.0, + expected_pflux_fw_surface_mw_m2=8.0, + label="Full poloidal coverage with ports loss", + ), + CalculateFwSurfaceLoadParam( + p_plasma_source_mw=2000.0, + f_deg_blkt_poloidal_plasma=0.5, + a_fw_full_coverage=200.0, + f_a_fw_hcd_ports=0.0, + expected_p_fw_incident_mw=1000.0, + expected_pflux_fw_surface_mw_m2=5.0, + label="Lower poloidal coverage", + ), + ], +) +def test_calculate_fw_surface_load(calculatefwsurfaceloadparam, fw): + """ + Unit test for calculate_fw_surface_load. + + :param calculatefwsurfaceloadparam: the data used to assert in this test. + :type calculatefwsurfaceloadparam: CalculateFwSurfaceLoadParam + """ + + p_fw_incident_mw, pflux_fw_surface_mw_m2 = fw.calculate_fw_surface_load( + p_plasma_source_mw=calculatefwsurfaceloadparam.p_plasma_source_mw, + f_deg_blkt_poloidal_plasma=calculatefwsurfaceloadparam.f_deg_blkt_poloidal_plasma, + a_fw_full_coverage=calculatefwsurfaceloadparam.a_fw_full_coverage, + f_a_fw_hcd_ports=calculatefwsurfaceloadparam.f_a_fw_hcd_ports, + ) + + assert p_fw_incident_mw == pytest.approx( + calculatefwsurfaceloadparam.expected_p_fw_incident_mw + ) + + assert pflux_fw_surface_mw_m2 == pytest.approx( + calculatefwsurfaceloadparam.expected_pflux_fw_surface_mw_m2 + ) From 5de6a187cccff360e87bdd77aa2e7d9f0d1c587b Mon Sep 17 00:00:00 2001 From: mn3981 Date: Wed, 10 Jun 2026 09:16:19 +0100 Subject: [PATCH 17/25] Add calculation method for outboard surface heat loads and corresponding unit tests --- process/models/fw.py | 48 +++++++++++++++++++++++--- tests/unit/models/test_fw.py | 66 ++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 5 deletions(-) diff --git a/process/models/fw.py b/process/models/fw.py index db34a523fa..fd5180da49 100644 --- a/process/models/fw.py +++ b/process/models/fw.py @@ -208,11 +208,11 @@ def run(self): # and shine through. # Some power is lost to HCD and ports on the outboard wall, so this is # taken into account with a coverage factor. - self.data.fwbs.p_fw_outboard_surface_heat_mw = ( - self.data.fwbs.p_fw_outboard_rad_mw - + self.data.current_drive.p_beam_orbit_loss_mw - + self.data.fwbs.p_fw_outboard_alpha_surface_mw - + self.data.current_drive.p_beam_shine_through_mw + self.data.fwbs.p_fw_outboard_surface_heat_mw = self.calculate_fw_outboard_surface_loads( + p_fw_outboard_rad_mw=self.data.fwbs.p_fw_outboard_rad_mw, + p_beam_orbit_loss_mw=self.data.current_drive.p_beam_orbit_loss_mw, + p_fw_outboard_alpha_surface_mw=self.data.fwbs.p_fw_outboard_alpha_surface_mw, + p_beam_shine_through_mw=self.data.current_drive.p_beam_shine_through_mw, ) self.data.fwbs.p_fw_inboard_surface_heat_mw = ( @@ -1071,3 +1071,41 @@ def output_fw_surface_loads(self): self.data.fwbs.pflux_fw_outboard_neutron_surface_average_mw, "OP ", ) + + @staticmethod + def calculate_fw_outboard_surface_loads( + p_fw_outboard_rad_mw: float, + p_beam_orbit_loss_mw: float, + p_fw_outboard_alpha_surface_mw: float, + p_beam_shine_through_mw: float, + ) -> float: + """Calculate the surface loads on the first wall. + + Parameters + ---------- + p_fw_outboard_rad_mw: + Radiation heat flux on outboard first wall [MW]. + p_beam_orbit_loss_mw: + Beam orbit loss power [MW]. + p_fw_outboard_alpha_surface_mw: + Alpha particle heat flux on outboard first wall [MW]. + p_beam_shine_through_mw: + Beam shine through power [MW]. + + Returns + ------- + p_fw_outboard_surface_heat_mw: + Total surface heat flux on the outboard first wall [MW]. + + """ + # Surface heat flux on first wall (MW) + # All of the fast particle losses go to the outer wall, as do all beam losses + # and shine through. + # Some power is lost to HCD and ports on the outboard wall, so this is + # taken into account with a coverage factor. + return ( + p_fw_outboard_rad_mw + + p_beam_orbit_loss_mw + + p_fw_outboard_alpha_surface_mw + + p_beam_shine_through_mw + ) diff --git a/tests/unit/models/test_fw.py b/tests/unit/models/test_fw.py index 4c792a2746..83420223e5 100644 --- a/tests/unit/models/test_fw.py +++ b/tests/unit/models/test_fw.py @@ -237,3 +237,69 @@ def test_calculate_fw_surface_load(calculatefwsurfaceloadparam, fw): assert pflux_fw_surface_mw_m2 == pytest.approx( calculatefwsurfaceloadparam.expected_pflux_fw_surface_mw_m2 ) + + +class CalculateFwOutboardSurfaceLoadsParam(NamedTuple): + p_fw_outboard_rad_mw: float + p_beam_orbit_loss_mw: float + p_fw_outboard_alpha_surface_mw: float + p_beam_shine_through_mw: float + expected_p_fw_outboard_surface_heat_mw: float + label: str = None + + +@pytest.mark.parametrize( + "calculatefwoutboardsurfaceloadsparam", + [ + CalculateFwOutboardSurfaceLoadsParam( + p_fw_outboard_rad_mw=100.0, + p_beam_orbit_loss_mw=50.0, + p_fw_outboard_alpha_surface_mw=75.0, + p_beam_shine_through_mw=25.0, + expected_p_fw_outboard_surface_heat_mw=250.0, + label="Equal power contributions", + ), + CalculateFwOutboardSurfaceLoadsParam( + p_fw_outboard_rad_mw=500.0, + p_beam_orbit_loss_mw=100.0, + p_fw_outboard_alpha_surface_mw=150.0, + p_beam_shine_through_mw=50.0, + expected_p_fw_outboard_surface_heat_mw=800.0, + label="Dominant radiation load", + ), + CalculateFwOutboardSurfaceLoadsParam( + p_fw_outboard_rad_mw=0.0, + p_beam_orbit_loss_mw=0.0, + p_fw_outboard_alpha_surface_mw=0.0, + p_beam_shine_through_mw=0.0, + expected_p_fw_outboard_surface_heat_mw=0.0, + label="Zero power contributions", + ), + CalculateFwOutboardSurfaceLoadsParam( + p_fw_outboard_rad_mw=200.0, + p_beam_orbit_loss_mw=75.0, + p_fw_outboard_alpha_surface_mw=100.0, + p_beam_shine_through_mw=25.0, + expected_p_fw_outboard_surface_heat_mw=400.0, + label="Mixed power contributions", + ), + ], +) +def test_calculate_fw_outboard_surface_loads(calculatefwoutboardsurfaceloadsparam, fw): + """ + Unit test for calculate_fw_outboard_surface_loads. + + :param calculatefwoutboardsurfaceloadsparam: the data used to assert in this test. + :type calculatefwoutboardsurfaceloadsparam: CalculateFwOutboardSurfaceLoadsParam + """ + + p_fw_outboard_surface_heat_mw = fw.calculate_fw_outboard_surface_loads( + p_fw_outboard_rad_mw=calculatefwoutboardsurfaceloadsparam.p_fw_outboard_rad_mw, + p_beam_orbit_loss_mw=calculatefwoutboardsurfaceloadsparam.p_beam_orbit_loss_mw, + p_fw_outboard_alpha_surface_mw=calculatefwoutboardsurfaceloadsparam.p_fw_outboard_alpha_surface_mw, + p_beam_shine_through_mw=calculatefwoutboardsurfaceloadsparam.p_beam_shine_through_mw, + ) + + assert p_fw_outboard_surface_heat_mw == pytest.approx( + calculatefwoutboardsurfaceloadsparam.expected_p_fw_outboard_surface_heat_mw + ) From 2113b5aa33a62e52746dadd61009103a49aca539 Mon Sep 17 00:00:00 2001 From: mn3981 Date: Tue, 16 Jun 2026 14:02:41 +0100 Subject: [PATCH 18/25] Add toroidal angle calculations for inboard first wall and update output methods --- process/data_structure/fwbs_variables.py | 9 ++++ process/models/fw.py | 67 ++++++++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/process/data_structure/fwbs_variables.py b/process/data_structure/fwbs_variables.py index 69738449fd..e8e3f3f4ce 100644 --- a/process/data_structure/fwbs_variables.py +++ b/process/data_structure/fwbs_variables.py @@ -750,5 +750,14 @@ class FWBSData: dz_fw_half: float = 0.0 """Half-height of first wall structure [m]""" + rad_fw_inboard_plasma_centre_toroidal: float = 0.0 + """Toroidal angle taken up by the inboard first wall as seen from the toroidal centre of the plasma [rad]""" + + deg_fw_inboard_plasma_centre_toroidal: float = 0.0 + """Toroidal angle taken up by the inboard first wall as seen from the toroidal centre of the plasma [deg]""" + + f_rad_fw_inboard_plasma_centre_toroidal: float = 0.0 + """Fraction of total toroidal angle taken up by the inboard first wall as seen from the toroidal centre of the plasma""" + CREATE_DICTS_FROM_DATACLASS = FWBSData diff --git a/process/models/fw.py b/process/models/fw.py index fd5180da49..e866b3aa86 100644 --- a/process/models/fw.py +++ b/process/models/fw.py @@ -128,6 +128,20 @@ def run(self): self.data.physics.p_neutron_total_mw / self.data.first_wall.a_fw_total ) + ( + self.data.fwbs.rad_fw_inboard_plasma_centre_toroidal, + self.data.fwbs.f_rad_fw_inboard_plasma_centre_toroidal, + ) = self.calculate_fw_inboard_load_toroidal_angle( + rmajor=self.data.physics.rmajor, + dr_fw_inboard_plasma=( + self.data.build.dr_fw_plasma_gap_inboard + self.data.physics.rminor + ), + ) + + self.data.fwbs.deg_fw_inboard_plasma_centre_toroidal = np.degrees( + self.data.fwbs.rad_fw_inboard_plasma_centre_toroidal + ) + # Radiation surface heat flux on first wall (MW/m²) # The full area is used as the radiation is assumed to be uniformly distributed # across the first wall, so the coverage factors are not applied here. @@ -841,6 +855,39 @@ def calculate_total_fw_channels( n_fw_outboard_channels = a_fw_outboard / (len_fw_channel * dx_fw_module) return int(n_fw_inboard_channels), int(n_fw_outboard_channels) + @staticmethod + def calculate_fw_inboard_load_toroidal_angle( + rmajor, dr_fw_inboard_plasma + ) -> tuple[float, float]: + """Calculate the toroidal angle subtended by the inboard first wall. + + Parameters + ---------- + rmajor : float + Plasma major radius of the tokamak (m). + dr_fw_inboard_plasma : float + Radial distance between centre of plasma and inboard first wall surface (m). + + + Returns + ------- + tuple + Toroidal angle subtended by the inboard first wall (radians) and the fraction of total toroidal angle. + + Notes + ----- + This formula is used assuming an isotropic ring source at `rmajor` and finds the + toroidal angle of particles that will hit the inboard first wall as if the + emission was from a flat sheet at the midplane + + + """ + rad_fw_inboard_toroidal = 2 * np.arcsin( + (rmajor - dr_fw_inboard_plasma) / (rmajor) + ) + f_rad_fw_inboard_toroidal = (rad_fw_inboard_toroidal) / (2 * np.pi) + return rad_fw_inboard_toroidal, f_rad_fw_inboard_toroidal + def output_fw_geometry(self): """Outputs the first wall geometry details to the output file.""" po.oheadr(self.outfile, "First wall build") @@ -913,6 +960,26 @@ def output_fw_geometry(self): self.data.blanket.n_fw_outboard_channels, "OP ", ) + po.oblnkl(self.outfile) + + po.ovarre( + self.outfile, + "Toroidal angle subtended by inboard first wall from centre of the plasma [radians]", + "(rad_fw_inboard_plasma_centre_toroidal)", + self.data.fwbs.rad_fw_inboard_plasma_centre_toroidal, + ) + po.ovarre( + self.outfile, + "Toroidal angle subtended by inboard first wall from centre of the plasma [degrees]", + "(deg_fw_inboard_plasma_centre_toroidal)", + self.data.fwbs.deg_fw_inboard_plasma_centre_toroidal, + ) + po.ovarre( + self.outfile, + "Fraction of total toroidal angle subtended by inboard first wall from centre of the plasma", + "(f_rad_fw_inboard_plasma_centre_toroidal)", + self.data.fwbs.f_rad_fw_inboard_plasma_centre_toroidal, + ) def output_fw_pumping(self): """Outputs the first wall pumping details to the output file.""" From 276ba6e303f6d2a5915accf81957ae8b91a46b82 Mon Sep 17 00:00:00 2001 From: mn3981 Date: Thu, 18 Jun 2026 10:36:18 +0100 Subject: [PATCH 19/25] Add function to plot inboard toroidal angle load and integrate into main plot --- process/core/io/plot/summary.py | 175 +++++++++++++++++++++++++++++++- 1 file changed, 174 insertions(+), 1 deletion(-) diff --git a/process/core/io/plot/summary.py b/process/core/io/plot/summary.py index 1cf5ac3e71..50c74e27fe 100644 --- a/process/core/io/plot/summary.py +++ b/process/core/io/plot/summary.py @@ -16212,6 +16212,168 @@ def plot_cs_von_mises_2d_contour( axis.set_title("CS Von Mises Stress Contour at BOP") +def plot_fw_inboard_toroidal_angle_load( + axis: plt.Axes, + mfile: MFile, + scan: int, + demo_ranges: bool, + colour_scheme: Literal[1, 2], +): + + rad_fw_inboard_plasma_centre_toroidal = mfile.get( + "rad_fw_inboard_plasma_centre_toroidal", scan=scan + ) + deg_fw_inboard_plasma_centre_toroidal = mfile.get( + "deg_fw_inboard_plasma_centre_toroidal", scan=scan + ) + f_rad_fw_inboard_plasma_centre_toroidal = mfile.get( + "f_rad_fw_inboard_plasma_centre_toroidal", scan=scan + ) + + # Get rmajor and first wall distance + rmajor = mfile.get("rmajor", scan=scan) + rminor = mfile.get("rminor", scan=scan) + dr_fw_plasma_gap_inboard = mfile.get("dr_fw_plasma_gap_inboard", scan=scan) + a = rminor + dr_fw_plasma_gap_inboard + + x_array = np.linspace(0, rmajor, 100) + # Calculate toroidal position: y = -(R-a)/sqrt(a*(2*R-a))*(R-x) + y = -(rmajor - a) / np.sqrt(a * (2 * rmajor - a)) * (rmajor - x_array) + axis.plot( + x_array, + y, + color="black", + linewidth=2, + linestyle="--", + label="First Wall Load Line", + ) + axis.plot( + x_array, + -y, + color="black", + linewidth=2, + linestyle="--", + label="First Wall Load Line", + ) + toroidal_cross_section(axis, mfile, scan, demo_ranges, colour_scheme) + + angle_start = -deg_fw_inboard_plasma_centre_toroidal / 2 + angle_end = deg_fw_inboard_plasma_centre_toroidal / 2 + + theta = np.linspace(np.deg2rad(angle_start), np.deg2rad(angle_end), 50) + arc_x = rmajor - rad_fw_inboard_plasma_centre_toroidal * np.cos(theta) + arc_y = rad_fw_inboard_plasma_centre_toroidal * np.sin(theta) + + axis.plot(arc_x, arc_y, color="red", linewidth=2) + + # Add angle label at the arc + mid_angle = np.deg2rad((angle_start + angle_end) / 2) + label_radius = 0.5 * rmajor + label_x = rmajor - label_radius * np.cos(mid_angle) + label_y = label_radius * np.sin(mid_angle) + + axis.text( + label_x, + label_y, + f"${deg_fw_inboard_plasma_centre_toroidal:.1f}°$\n({f_rad_fw_inboard_plasma_centre_toroidal * 100:.2f}%)", + fontsize=10, + color="red", + ha="center", + va="center", + weight="bold", + bbox={ + "boxstyle": "round,pad=0.3", + "facecolor": "white", + "edgecolor": "red", + "alpha": 1.0, + }, + zorder=100, + ) + + axis.set_ylim([-axis.get_ylim()[1], axis.get_ylim()[1]]) + + +def plot_fw_inboard_toroidal_angle_load( + axis: plt.Axes, + mfile: MFile, + scan: int, + demo_ranges: bool, + colour_scheme: Literal[1, 2], +): + + rad_fw_inboard_plasma_centre_toroidal = mfile.get( + "rad_fw_inboard_plasma_centre_toroidal", scan=scan + ) + deg_fw_inboard_plasma_centre_toroidal = mfile.get( + "deg_fw_inboard_plasma_centre_toroidal", scan=scan + ) + f_rad_fw_inboard_plasma_centre_toroidal = mfile.get( + "f_rad_fw_inboard_plasma_centre_toroidal", scan=scan + ) + + # Get rmajor and first wall distance + rmajor = mfile.get("rmajor", scan=scan) + rminor = mfile.get("rminor", scan=scan) + dr_fw_plasma_gap_inboard = mfile.get("dr_fw_plasma_gap_inboard", scan=scan) + a = rminor + dr_fw_plasma_gap_inboard + + x_array = np.linspace(0, rmajor, 100) + # Calculate toroidal position: y = -(R-a)/sqrt(a*(2*R-a))*(R-x) + y = -(rmajor - a) / np.sqrt(a * (2 * rmajor - a)) * (rmajor - x_array) + axis.plot( + x_array, + y, + color="black", + linewidth=2, + linestyle="--", + label="First Wall Load Line", + ) + axis.plot( + x_array, + -y, + color="black", + linewidth=2, + linestyle="--", + label="First Wall Load Line", + ) + toroidal_cross_section(axis, mfile, scan, demo_ranges, colour_scheme) + + angle_start = -deg_fw_inboard_plasma_centre_toroidal / 2 + angle_end = deg_fw_inboard_plasma_centre_toroidal / 2 + + theta = np.linspace(np.deg2rad(angle_start), np.deg2rad(angle_end), 50) + arc_x = rmajor - rad_fw_inboard_plasma_centre_toroidal * np.cos(theta) + arc_y = rad_fw_inboard_plasma_centre_toroidal * np.sin(theta) + + axis.plot(arc_x, arc_y, color="red", linewidth=2) + + # Add angle label at the arc + mid_angle = np.deg2rad((angle_start + angle_end) / 2) + label_radius = 0.5 * rmajor + label_x = rmajor - label_radius * np.cos(mid_angle) + label_y = label_radius * np.sin(mid_angle) + + axis.text( + label_x, + label_y, + f"${deg_fw_inboard_plasma_centre_toroidal:.1f}°$\n({f_rad_fw_inboard_plasma_centre_toroidal * 100:.2f}%)", + fontsize=10, + color="red", + ha="center", + va="center", + weight="bold", + bbox={ + "boxstyle": "round,pad=0.3", + "facecolor": "white", + "edgecolor": "red", + "alpha": 1.0, + }, + zorder=100, + ) + + axis.set_ylim([-axis.get_ylim()[1], axis.get_ylim()[1]]) + + def main_plot( m_file: MFile, scan: int, @@ -16803,8 +16965,19 @@ def _add_page(name: str | None = None): colour_scheme, ) + ax_blanket = figs[34].add_subplot(122, aspect="equal") + plot_blkt_structure(ax_blanket, figs[34], m_file, scan, radial_build, colour_scheme) + plot_fw_inboard_toroidal_angle_load( + axis=figs[34].add_subplot(121, aspect="equal"), + mfile=m_file, + scan=scan, + demo_ranges=demo_ranges, + colour_scheme=colour_scheme, + ) + + plot_blkt_pipe_bends(figs[35], m_file, scan) plot_poloidal_power_distribution( - ax=figs[35].add_subplot(111, aspect="equal"), + ax=figs[35].add_subplot(122, aspect="equal"), m_file=m_file, scan=scan, radial_build=radial_build, From b3695f39472464e4dcc2fc7823f721203edafb57 Mon Sep 17 00:00:00 2001 From: mn3981 Date: Thu, 18 Jun 2026 10:57:35 +0100 Subject: [PATCH 20/25] Add method to calculate solid angle components for first wall geometry --- process/models/fw.py | 67 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/process/models/fw.py b/process/models/fw.py index e866b3aa86..6b1d035aba 100644 --- a/process/models/fw.py +++ b/process/models/fw.py @@ -20,6 +20,7 @@ CoolantType, gnielinski_heat_transfer_coefficient, ) +from process.models.physics.physics import DivertorNumberModels logger = logging.getLogger(__name__) @@ -888,6 +889,72 @@ def calculate_fw_inboard_load_toroidal_angle( f_rad_fw_inboard_toroidal = (rad_fw_inboard_toroidal) / (2 * np.pi) return rad_fw_inboard_toroidal, f_rad_fw_inboard_toroidal + @staticmethod + def calculate_component_solid_angle_components( + deg_fw_inboard_plasma_centre_toroidal: float, + deg_fw_outboard_plasma_centre_toroidal: float, + deg_blkt_outboard_poloidal_plasma: float, + deg_blkt_inboard_poloidal_plasma: float, + deg_div_poloidal_plasma: float, + i_single_null: int, + ) -> tuple[float, float, float, float]: + """Calculate the solid angle subtended by the inboard and outboard first wall. + + + Parameters + ---------- + deg_fw_inboard_plasma_centre_toroidal : float + Toroidal angle subtended by the inboard first wall from the centre of the plasma [degrees]. + deg_fw_outboard_plasma_centre_toroidal : float + Toroidal angle subtended by the outboard first wall from the centre of the plasma [degrees]. + deg_blkt_outboard_poloidal_plasma : float + Poloidal angle subtended by the outboard first wall from the centre of the plasma [degrees]. + deg_blkt_inboard_poloidal_plasma : float + Poloidal angle subtended by the inboard first wall from the centre of the plasma [degrees]. + deg_div_poloidal_plasma : float + Poloidal angle subtended by the divertor from the centre of the plasma [degrees]. + i_single_null : int + Flag indicating whether the configuration is single null (1) or double null (0). + + Returns + ------- + tuple + Weighted solid angle contributions of the inboard first wall, outboard first wall, + lower divertor, and upper divertor (if applicable) as fractions of the total solid angle. + """ + weighted_inboard = (deg_fw_inboard_plasma_centre_toroidal / 360.0) * ( + deg_blkt_inboard_poloidal_plasma / 360.0 + ) + weighted_outboard = (deg_fw_outboard_plasma_centre_toroidal / 360.0) * ( + deg_blkt_outboard_poloidal_plasma / 360.0 + ) + weighted_div_lower = deg_div_poloidal_plasma / 360.0 + + if i_single_null == DivertorNumberModels.DOUBLE_NULL: + weighted_div_upper = deg_div_poloidal_plasma / 360.0 + total_weighting = np.sum([ + weighted_inboard, + weighted_outboard, + weighted_div_lower, + weighted_div_upper, + ]) + return ( + weighted_inboard / total_weighting, + weighted_outboard / total_weighting, + weighted_div_lower / total_weighting, + weighted_div_upper / total_weighting, + ) + total_weighting = np.sum([ + weighted_inboard, + weighted_outboard, + weighted_div_lower, + ]) + return ( + weighted_inboard / total_weighting, + weighted_outboard / total_weighting, + weighted_div_lower / total_weighting, + ) + def output_fw_geometry(self): """Outputs the first wall geometry details to the output file.""" po.oheadr(self.outfile, "First wall build") From f2e1c2b682253ea9c06225576537f32538a99de2 Mon Sep 17 00:00:00 2001 From: mn3981 Date: Thu, 18 Jun 2026 11:01:39 +0100 Subject: [PATCH 21/25] Add solid angle fraction attributes for lower and upper divertor ring sources --- process/data_structure/divertor_variables.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/process/data_structure/divertor_variables.py b/process/data_structure/divertor_variables.py index a986a67243..aeeafafb50 100644 --- a/process/data_structure/divertor_variables.py +++ b/process/data_structure/divertor_variables.py @@ -95,5 +95,11 @@ class DivertorData: deg_div_poloidal_plasma: float = 0.0 """Divertor poloidal angle subtended by plasma (degrees)""" + f_ster_div_lower_ring_source: float = 0.0 + """Solid angle fraction of lower divertor assuming a ring source""" + + f_ster_div_upper_ring_source: float = 0.0 + """Solid angle fraction of upper divertor assuming a ring source""" + CREATE_DICTS_FROM_DATACLASS = DivertorData From c151a612bd31aa6ca06d2046fba7b62c7a2119b1 Mon Sep 17 00:00:00 2001 From: mn3981 Date: Thu, 18 Jun 2026 14:44:43 +0100 Subject: [PATCH 22/25] Add InVesselSolidAngleFractions class and update solid angle calculation method --- process/models/fw.py | 51 +++++++++++++++++++++++++++++++++----------- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/process/models/fw.py b/process/models/fw.py index 6b1d035aba..163a962361 100644 --- a/process/models/fw.py +++ b/process/models/fw.py @@ -1,6 +1,7 @@ """Module containing first wall routines""" import logging +from dataclasses import dataclass import numpy as np @@ -25,6 +26,21 @@ logger = logging.getLogger(__name__) +@dataclass(slots=True) +class InVesselSolidAngleFractions: + f_ster_fw_inboard_ring_source: float = 0.0 + """Solid angle fraction of inboard FW assuming a ring source""" + + f_ster_fw_outboard_ring_source: float = 0.0 + """Solid angle fraction of outboard FW assuming a ring source""" + + f_ster_div_lower_ring_source: float = 0.0 + """Solid angle fraction of lower divertor assuming a ring source""" + + f_ster_div_upper_ring_source: float = 0.0 + """Solid angle fraction of upper divertor assuming a ring source""" + + class FirstWall(Model): """Calculate the first wall parameters""" @@ -143,6 +159,15 @@ def run(self): self.data.fwbs.rad_fw_inboard_plasma_centre_toroidal ) + self.calculate_component_solid_angle_components( + deg_fw_inboard_plasma_centre_toroidal=self.data.fwbs.deg_fw_inboard_plasma_centre_toroidal, + deg_fw_outboard_plasma_centre_toroidal=self.data.fwbs.deg_fw_outboard_plasma_centre_toroidal, + deg_blkt_outboard_poloidal_plasma=self.data.blanket.f_deg_blkt_outboard_poloidal_plasma, + deg_blkt_inboard_poloidal_plasma=self.data.blanket.f_deg_blkt_inboard_poloidal_plasma, + deg_div_poloidal_plasma=self.data.divertor.deg_div_poloidal_plasma, + i_single_null=self.data.divertor.i_single_null, + ) + # Radiation surface heat flux on first wall (MW/m²) # The full area is used as the radiation is assumed to be uniformly distributed # across the first wall, so the coverage factors are not applied here. @@ -897,7 +922,7 @@ def calculate_component_solid_angle_components( deg_blkt_inboard_poloidal_plasma: float, deg_div_poloidal_plasma: float, i_single_null: int, - ) -> tuple[float, float, float, float]: + ) -> InVesselSolidAngleFractions: """Calculate the solid angle subtended by the inboard and outboard first wall. @@ -918,9 +943,8 @@ def calculate_component_solid_angle_components( Returns ------- - tuple - Weighted solid angle contributions of the inboard first wall, outboard first wall, - lower divertor, and upper divertor (if applicable) as fractions of the total solid angle. + InVesselSolidAngleFractions + """ weighted_inboard = (deg_fw_inboard_plasma_centre_toroidal / 360.0) * ( deg_blkt_inboard_poloidal_plasma / 360.0 @@ -938,21 +962,22 @@ def calculate_component_solid_angle_components( weighted_div_lower, weighted_div_upper, ]) - return ( - weighted_inboard / total_weighting, - weighted_outboard / total_weighting, - weighted_div_lower / total_weighting, - weighted_div_upper / total_weighting, + return InVesselSolidAngleFractions( + f_ster_fw_inboard_ring_source=weighted_inboard / total_weighting, + f_ster_fw_outboard_ring_source=weighted_outboard / total_weighting, + f_ster_div_lower_ring_source=weighted_div_lower / total_weighting, + f_ster_div_upper_ring_source=weighted_div_upper / total_weighting, ) total_weighting = np.sum([ weighted_inboard, weighted_outboard, weighted_div_lower, ]) - return ( - weighted_inboard / total_weighting, - weighted_outboard / total_weighting, - weighted_div_lower / total_weighting, + return InVesselSolidAngleFractions( + f_ster_fw_inboard_ring_source=weighted_inboard / total_weighting, + f_ster_fw_outboard_ring_source=weighted_outboard / total_weighting, + f_ster_div_lower_ring_source=weighted_div_lower / total_weighting, + f_ster_div_upper_ring_source=0.0, ) def output_fw_geometry(self): From b80664cad7eebc2e1f25cf1d6703a136e6a09b96 Mon Sep 17 00:00:00 2001 From: mn3981 Date: Mon, 22 Jun 2026 15:20:21 +0100 Subject: [PATCH 23/25] Post rebase fixes --- process/models/fw.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/process/models/fw.py b/process/models/fw.py index 163a962361..9930799821 100644 --- a/process/models/fw.py +++ b/process/models/fw.py @@ -21,7 +21,7 @@ CoolantType, gnielinski_heat_transfer_coefficient, ) -from process.models.physics.physics import DivertorNumberModels +from process.data_structure.physics_variables import DivertorNumberModels logger = logging.getLogger(__name__) @@ -161,11 +161,11 @@ def run(self): self.calculate_component_solid_angle_components( deg_fw_inboard_plasma_centre_toroidal=self.data.fwbs.deg_fw_inboard_plasma_centre_toroidal, - deg_fw_outboard_plasma_centre_toroidal=self.data.fwbs.deg_fw_outboard_plasma_centre_toroidal, - deg_blkt_outboard_poloidal_plasma=self.data.blanket.f_deg_blkt_outboard_poloidal_plasma, - deg_blkt_inboard_poloidal_plasma=self.data.blanket.f_deg_blkt_inboard_poloidal_plasma, + deg_fw_outboard_plasma_centre_toroidal=1.0-self.data.fwbs.deg_fw_inboard_plasma_centre_toroidal, + deg_blkt_outboard_poloidal_plasma=self.data.blanket.deg_blkt_outboard_poloidal_plasma, + deg_blkt_inboard_poloidal_plasma=self.data.blanket.deg_blkt_inboard_poloidal_plasma, deg_div_poloidal_plasma=self.data.divertor.deg_div_poloidal_plasma, - i_single_null=self.data.divertor.i_single_null, + i_single_null=self.data.physics.i_single_null, ) # Radiation surface heat flux on first wall (MW/m²) From f7ace7596b8229be70b43b0535684554b2e08b68 Mon Sep 17 00:00:00 2001 From: mn3981 Date: Mon, 22 Jun 2026 15:44:05 +0100 Subject: [PATCH 24/25] Add solid angle fraction attributes for inboard and outboard first wall ring sources --- process/data_structure/fwbs_variables.py | 6 ++++ process/models/fw.py | 37 ++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/process/data_structure/fwbs_variables.py b/process/data_structure/fwbs_variables.py index e8e3f3f4ce..c60ae275e7 100644 --- a/process/data_structure/fwbs_variables.py +++ b/process/data_structure/fwbs_variables.py @@ -758,6 +758,12 @@ class FWBSData: f_rad_fw_inboard_plasma_centre_toroidal: float = 0.0 """Fraction of total toroidal angle taken up by the inboard first wall as seen from the toroidal centre of the plasma""" + + f_ster_fw_inboard_ring_source: float = 0.0 + """Solid angle fraction of inboard FW assuming a ring source""" + + f_ster_fw_outboard_ring_source: float = 0.0 + """Solid angle fraction of outboard FW assuming a ring source""" CREATE_DICTS_FROM_DATACLASS = FWBSData diff --git a/process/models/fw.py b/process/models/fw.py index 9930799821..84e9406d98 100644 --- a/process/models/fw.py +++ b/process/models/fw.py @@ -159,15 +159,21 @@ def run(self): self.data.fwbs.rad_fw_inboard_plasma_centre_toroidal ) - self.calculate_component_solid_angle_components( + in_vessel_solid_angle_fractions = self.calculate_component_solid_angle_components( deg_fw_inboard_plasma_centre_toroidal=self.data.fwbs.deg_fw_inboard_plasma_centre_toroidal, - deg_fw_outboard_plasma_centre_toroidal=1.0-self.data.fwbs.deg_fw_inboard_plasma_centre_toroidal, + deg_fw_outboard_plasma_centre_toroidal=360.0-self.data.fwbs.deg_fw_inboard_plasma_centre_toroidal, deg_blkt_outboard_poloidal_plasma=self.data.blanket.deg_blkt_outboard_poloidal_plasma, deg_blkt_inboard_poloidal_plasma=self.data.blanket.deg_blkt_inboard_poloidal_plasma, deg_div_poloidal_plasma=self.data.divertor.deg_div_poloidal_plasma, i_single_null=self.data.physics.i_single_null, ) + + self.data.fwbs.f_ster_fw_inboard_ring_source = in_vessel_solid_angle_fractions.f_ster_fw_inboard_ring_source + self.data.fwbs.f_ster_fw_outboard_ring_source = in_vessel_solid_angle_fractions.f_ster_fw_outboard_ring_source + self.data.divertor.f_ster_div_lower_ring_source = in_vessel_solid_angle_fractions.f_ster_div_lower_ring_source + self.data.divertor.f_ster_div_upper_ring_source = in_vessel_solid_angle_fractions.f_ster_div_upper_ring_source + # Radiation surface heat flux on first wall (MW/m²) # The full area is used as the radiation is assumed to be uniformly distributed # across the first wall, so the coverage factors are not applied here. @@ -946,6 +952,7 @@ def calculate_component_solid_angle_components( InVesselSolidAngleFractions """ + print(deg_fw_outboard_plasma_centre_toroidal) weighted_inboard = (deg_fw_inboard_plasma_centre_toroidal / 360.0) * ( deg_blkt_inboard_poloidal_plasma / 360.0 ) @@ -1072,6 +1079,32 @@ def output_fw_geometry(self): "(f_rad_fw_inboard_plasma_centre_toroidal)", self.data.fwbs.f_rad_fw_inboard_plasma_centre_toroidal, ) + po.oblnkl(self.outfile) + po.ovarre( + self.outfile, + "", + "(f_ster_fw_inboard_ring_source)", + self.data.fwbs.f_ster_fw_inboard_ring_source, + ) + po.ovarre( + self.outfile, + "", + "(f_ster_fw_outboard_ring_source)", + self.data.fwbs.f_ster_fw_outboard_ring_source, + ) + po.ovarre( + self.outfile, + "", + "(f_ster_div_lower_ring_source)", + self.data.divertor.f_ster_div_lower_ring_source, + ) + po.ovarre( + self.outfile, + "", + "(f_ster_div_upper_ring_source)", + self.data.divertor.f_ster_div_upper_ring_source, + ) + def output_fw_pumping(self): """Outputs the first wall pumping details to the output file.""" From 0469a0ab69458679b7006612ad6d7c0e033129d3 Mon Sep 17 00:00:00 2001 From: mn3981 Date: Thu, 9 Jul 2026 10:30:17 +0100 Subject: [PATCH 25/25] Post merge fixes --- process/core/io/plot/summary.py | 16 +++++++-------- process/data_structure/fwbs_variables.py | 4 ++-- process/models/fw.py | 25 +++++++++++++++--------- 3 files changed, 25 insertions(+), 20 deletions(-) diff --git a/process/core/io/plot/summary.py b/process/core/io/plot/summary.py index 50c74e27fe..67ad0afa05 100644 --- a/process/core/io/plot/summary.py +++ b/process/core/io/plot/summary.py @@ -16954,30 +16954,28 @@ def _add_page(name: str | None = None): ) plot_fw_90_deg_pipe_bend(pages["fw_td_cross_section"].add_subplot(337), m_file, scan) - plot_blkt_pipe_bends(_add_page("blkt_pipe_bends"), m_file, scan) - ax_blanket = pages["blkt_pipe_bends"].add_subplot(122, aspect="equal") + + ax_blanket = _add_page("vessel_cross_section").add_subplot(122, aspect="equal") plot_blkt_structure( ax_blanket, - pages["blkt_pipe_bends"], + pages["vessel_cross_section"], m_file, scan, radial_build, colour_scheme, ) - - ax_blanket = figs[34].add_subplot(122, aspect="equal") - plot_blkt_structure(ax_blanket, figs[34], m_file, scan, radial_build, colour_scheme) plot_fw_inboard_toroidal_angle_load( - axis=figs[34].add_subplot(121, aspect="equal"), + axis=pages["vessel_cross_section"].add_subplot(121, aspect="equal"), mfile=m_file, scan=scan, demo_ranges=demo_ranges, colour_scheme=colour_scheme, ) - plot_blkt_pipe_bends(figs[35], m_file, scan) + ax_blanket_bends = _add_page("vessel_power_distribution") + plot_blkt_pipe_bends(pages["vessel_power_distribution"], m_file, scan) plot_poloidal_power_distribution( - ax=figs[35].add_subplot(122, aspect="equal"), + ax=pages["vessel_power_distribution"].add_subplot(122, aspect="equal"), m_file=m_file, scan=scan, radial_build=radial_build, diff --git a/process/data_structure/fwbs_variables.py b/process/data_structure/fwbs_variables.py index c60ae275e7..e934cf1f5a 100644 --- a/process/data_structure/fwbs_variables.py +++ b/process/data_structure/fwbs_variables.py @@ -758,10 +758,10 @@ class FWBSData: f_rad_fw_inboard_plasma_centre_toroidal: float = 0.0 """Fraction of total toroidal angle taken up by the inboard first wall as seen from the toroidal centre of the plasma""" - + f_ster_fw_inboard_ring_source: float = 0.0 """Solid angle fraction of inboard FW assuming a ring source""" - + f_ster_fw_outboard_ring_source: float = 0.0 """Solid angle fraction of outboard FW assuming a ring source""" diff --git a/process/models/fw.py b/process/models/fw.py index 84e9406d98..909945c943 100644 --- a/process/models/fw.py +++ b/process/models/fw.py @@ -10,6 +10,7 @@ from process.core.coolprop_interface import FluidProperties from process.core.exceptions import ProcessValueError from process.core.model import Model +from process.data_structure.physics_variables import DivertorNumberModels from process.models.build import FwBlktVVShape from process.models.engineering.ivc_functions import ( calculate_pipe_bend_radius, @@ -21,7 +22,6 @@ CoolantType, gnielinski_heat_transfer_coefficient, ) -from process.data_structure.physics_variables import DivertorNumberModels logger = logging.getLogger(__name__) @@ -161,19 +161,27 @@ def run(self): in_vessel_solid_angle_fractions = self.calculate_component_solid_angle_components( deg_fw_inboard_plasma_centre_toroidal=self.data.fwbs.deg_fw_inboard_plasma_centre_toroidal, - deg_fw_outboard_plasma_centre_toroidal=360.0-self.data.fwbs.deg_fw_inboard_plasma_centre_toroidal, + deg_fw_outboard_plasma_centre_toroidal=360.0 + - self.data.fwbs.deg_fw_inboard_plasma_centre_toroidal, deg_blkt_outboard_poloidal_plasma=self.data.blanket.deg_blkt_outboard_poloidal_plasma, deg_blkt_inboard_poloidal_plasma=self.data.blanket.deg_blkt_inboard_poloidal_plasma, deg_div_poloidal_plasma=self.data.divertor.deg_div_poloidal_plasma, i_single_null=self.data.physics.i_single_null, ) - - self.data.fwbs.f_ster_fw_inboard_ring_source = in_vessel_solid_angle_fractions.f_ster_fw_inboard_ring_source - self.data.fwbs.f_ster_fw_outboard_ring_source = in_vessel_solid_angle_fractions.f_ster_fw_outboard_ring_source - self.data.divertor.f_ster_div_lower_ring_source = in_vessel_solid_angle_fractions.f_ster_div_lower_ring_source - self.data.divertor.f_ster_div_upper_ring_source = in_vessel_solid_angle_fractions.f_ster_div_upper_ring_source - + self.data.fwbs.f_ster_fw_inboard_ring_source = ( + in_vessel_solid_angle_fractions.f_ster_fw_inboard_ring_source + ) + self.data.fwbs.f_ster_fw_outboard_ring_source = ( + in_vessel_solid_angle_fractions.f_ster_fw_outboard_ring_source + ) + self.data.divertor.f_ster_div_lower_ring_source = ( + in_vessel_solid_angle_fractions.f_ster_div_lower_ring_source + ) + self.data.divertor.f_ster_div_upper_ring_source = ( + in_vessel_solid_angle_fractions.f_ster_div_upper_ring_source + ) + # Radiation surface heat flux on first wall (MW/m²) # The full area is used as the radiation is assumed to be uniformly distributed # across the first wall, so the coverage factors are not applied here. @@ -1104,7 +1112,6 @@ def output_fw_geometry(self): "(f_ster_div_upper_ring_source)", self.data.divertor.f_ster_div_upper_ring_source, ) - def output_fw_pumping(self): """Outputs the first wall pumping details to the output file."""