diff --git a/.gitignore b/.gitignore
index 86dd30aa24..51e6de487d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -63,3 +63,5 @@ quench_data.DAT
env-fork
*SIG_TF.json
!tests/integration/data/large_tokamak_SIG_TF.json
+*.html
+!documentation/**/*.html
diff --git a/documentation/images/plotly_sankey.png b/documentation/images/plotly_sankey.png
new file mode 100644
index 0000000000..98daf56ac7
Binary files /dev/null and b/documentation/images/plotly_sankey.png differ
diff --git a/documentation/usage/plotting.md b/documentation/usage/plotting.md
index a5f4cd60d7..3df621bd62 100644
--- a/documentation/usage/plotting.md
+++ b/documentation/usage/plotting.md
@@ -136,3 +136,21 @@ python process/io/plot_radial_build.py -f path/to/MFILE.DAT
Figure 12: Simple radial build plot
+---------------
+
+### Interactive Sankey diagram
+
+`plot_plotly_sankey` is to plot an interactive `.html` Sankey diagram for looking at the plants power balance. It can be run as follows:
+
+```bash
+python process/io/plot_plotly_sankey.py -m path/to/MFILE.DAT
+```
+
+
+{ width="100%"}
+Figure 13: Interactive HTML Sankey diagram
+
+
+
+
+
diff --git a/process/io/plot_plotly_sankey.py b/process/io/plot_plotly_sankey.py
new file mode 100644
index 0000000000..f960d03e89
--- /dev/null
+++ b/process/io/plot_plotly_sankey.py
@@ -0,0 +1,410 @@
+import argparse
+import pathlib
+import re
+
+import matplotlib as mpl
+
+try:
+ import plotly.graph_objects as go
+
+ PLOT_SANKEY = True
+except ImportError:
+ PLOT_SANKEY = False
+
+from process.io.mfile import MFile
+
+mpl.use("Agg")
+
+
+def main(args=None):
+ ###########################################################
+ # Usage
+
+ if not PLOT_SANKEY:
+ print(
+ "\nPlotly is not installed, unable to create sankey diagram!\n"
+ "Install plotly by installing the optional 'plotly' dependency "
+ "e.g. \"pip install -e '.[plotly]'\""
+ )
+ return
+
+ parser = argparse.ArgumentParser(
+ description="Program to plot\
+ the power flow in PROCESS using a Sankey diagram."
+ )
+
+ parser.add_argument("-e", "--end", default="pdf", help="file format, default = pdf")
+
+ parser.add_argument(
+ "-m", "--mfile", default="MFILE.DAT", help="mfile name, default = MFILE.DAT"
+ )
+
+ args = parser.parse_args(args)
+
+ #########################################################
+ # main program
+
+ plot_power_balance_sankey(args.mfile)
+
+
+def plot_power_balance_sankey(m_file):
+ m_file = MFile(m_file)
+ p_hcd_injected_total_mw = m_file.data["p_hcd_injected_total_mw"].get_scan(-1)
+ p_plasma_ohmic_mw = m_file.data["p_plasma_ohmic_mw"].get_scan(-1)
+ p_alpha_total_mw = m_file.data["p_alpha_total_mw"].get_scan(-1)
+ p_neutron_total_mw = m_file.data["p_neutron_total_mw"].get_scan(-1)
+ p_plasma_rad_mw = m_file.data["p_plasma_rad_mw"].get_scan(-1)
+ p_fw_rad_total_mw = m_file.data["p_fw_rad_total_mw"].get_scan(-1)
+ p_fw_alpha_mw = p_alpha_total_mw * (
+ 1 - m_file.data["f_p_alpha_plasma_deposited"].get_scan(-1)
+ )
+ p_blkt_nuclear_heat_total_mw = m_file.data["p_blkt_nuclear_heat_total_mw"].get_scan(
+ -1
+ )
+
+ # Define node labels (linearized flow)
+ labels = [
+ "H&CD injector", # 0
+ "Ohmic", # 1
+ "Plasma Fusion Power", # 2
+ "Alpha particles", # 3
+ "Neutrons", # 4
+ "Radiation", # 5
+ "First Wall", # 6
+ "Blanket", # 7
+ "Divertor", # 8
+ "FW+Blkt", # 9
+ "Primary Thermal", # 10
+ "Turbine", # 11
+ "Gross Electric", # 12
+ "Net Electric", # 13
+ "HCD Electric Power", # 14
+ "HCD electric losses", # 15
+ "Core systems", # 16
+ "Cryo plant", # 17
+ "Base plant load", # 18
+ "TF power supplies", # 19
+ "PF power supplies", # 20
+ "Vacuum pumps", # 21
+ "Tritium plant", # 22
+ "Coolant pumps electric", # 23
+ "Coolant pump electric losses", # 24
+ "Divertor pump", # 25
+ "FW+Blkt pumps", # 26
+ "Shield pump", # 27
+ "Shield", # 28
+ "Secondary heat", # 29
+ "TF nuclear heat", # 30
+ "H&CD & Diagnostics", # 31
+ "Total Secondary Heat", # 32
+ "Turbine Loss", # 33
+ "Blanket neutron multiplication", # 34
+ ]
+
+ # Define links (source, target, value) for a more linear flow
+ sources = [
+ 0, # 0: H&CD to Fusion
+ 1, # 1: Ohmic to Fusion
+ 2, # 2: Fusion to Alpha
+ 2, # 3: Fusion to Neutrons
+ 2, # 4: Fusion to Radiation
+ 3, # 5: Alpha to First Wall
+ 4, # 6: Neutrons to Blanket
+ 5, # 7: Radiation to First Wall
+ 4, # 8: Neutrons to Divertor
+ 5, # 9: Radiation to Divertor
+ 6, # 10: First Wall to FW+Blkt
+ 7, # 11: Blanket to FW+Blkt
+ 8, # 12: Divertor to FW+Blkt
+ 9, # 13: FW+Blkt to Primary Thermal
+ 10, # 14: Primary Thermal to Turbine
+ 11, # 15: Turbine to Gross Electric
+ 12, # 16: Gross Electric to Net Electric
+ 12, # 17: Gross Electric to HCD Electric Power
+ 14, # 18: HCD Electric Power to HCD electric losses
+ 14, # 19: HCD Electric Power to H&CD
+ 12, # 20: Gross Electric to Core systems
+ 16, # 21: Core systems to Cryo plant
+ 16, # 22: Core systems to Base plant load
+ 16, # 23: Core systems to TF coils
+ 16, # 24: Core systems to PF coils
+ 16, # 25: Core systems to Vacuum pumps
+ 16, # 26: Core systems to Tritium plant
+ 12, # 27: Gross Electric to Coolant pumps electric
+ 23, # 28: Coolant pumps electric to Coolant pump electric losses
+ 23, # 29: Coolant pumps electric to Divertor pump
+ 23, # 30: Coolant pumps electric to FW+Blkt pumps
+ 26, # 31: FW+Blkt pumps to FW+Blkt
+ 25, # 32: Divertor pump to Divertor
+ 23, # 33: Coolant pumps electric to Shield pump
+ 27, # 34: Shield pump to Shield
+ 28, # 35: Shield to primary thermal
+ 4, # 36: Neutrons to shield
+ 17, # 37: Cryo plant to secondary heat
+ 18, # 38: Base plant load to secondary heat
+ 19, # 39: TF coils to secondary heat
+ 20, # 40: PF coils to secondary heat
+ 21, # 41: Vacuum pumps to secondary heat
+ 22, # 42: Tritium plant to secondary heat
+ 4, # 43: Neutrons to tf
+ 30, # 44: TF nuclear heat to secondary heat
+ 15, # 45: HCD electric losses to secondary heat
+ 24, # 46: Coolant pumps electric to secondary heat
+ 6, # 47: FW pump to primary heat, Should only show if FW and Bkt pumps are separate
+ 7, # 48: Blkt pump to primary heat, Should only show if FW and Blkt pumps are separate
+ 2, # 49 Should show in beams are present
+ 2, # 50: Should show in beams are present
+ 4, # 51 Neutrons to CP shield, should only show if CP shield is present
+ 2, # 52 Plasma separatrix power to divertor
+ 8, # 53 Divertor secondary heat,
+ 28, # 54 Shield secondary heat
+ 4, # 55 Neutron power to H&CD & Diagnostics
+ 5, # 56: Radiation to H&CD & Diagnostics
+ 29, # 57: Total Secondary Heat
+ 31, # 58: H&CD & Diagnostics secondary heat
+ 11, # 59: Turbine Loss
+ 4, # 60: FW nuclear heat
+ 3, # 61: Alpha particles back to plasma
+ 34, # 62: Blanket neutron multiplication
+ ]
+ targets = [
+ 2, # 0: H&CD to Fusion
+ 2, # 1: Ohmic to Fusion
+ 3, # 2: Fusion to Alpha
+ 4, # 3: Fusion to Neutrons
+ 5, # 4: Fusion to Radiation
+ 6, # 5: Alpha to First Wall
+ 7, # 6: Neutrons to Blanket
+ 6, # 7: Radiation to First Wall
+ 8, # 8: Neutrons to Divertor
+ 8, # 9: Radiation to Divertor
+ 9, # 10: First Wall to FW+Blkt
+ 9, # 11: Blanket to FW+Blkt
+ 10, # 12: Divertor to FW+Blkt
+ 10, # 13: FW+Blkt to Primary Thermal
+ 11, # 14: Primary Thermal to Turbine
+ 12, # 15: Turbine to Gross Electric
+ 13, # 16: Gross Electric to Net Electric
+ 14, # 17: Gross Electric to HCD Electric Power
+ 15, # 18: HCD Electric Power to HCD electric losses
+ 0, # 19: HCD Electric Power to H&CD
+ 16, # 20: Gross Electric to Core systems
+ 17, # 21: Core systems to Cryo plant
+ 18, # 22: Core systems to Base plant load
+ 19, # 23: Core systems to TF coils
+ 20, # 24: Core systems to PF coils
+ 21, # 25: Core systems to Vacuum pumps
+ 22, # 26: Core systems to Tritium plant
+ 23, # 27: Gross Electric to Coolant pumps electric
+ 24, # 28: Coolant pumps electric to Coolant pump electric losses
+ 25, # 29: Coolant pumps electric to Divertor pump
+ 26, # 30: Coolant pumps electric to FW+Blkt pumps
+ 9, # 31: FW+Blkt pumps to FW+Blkt
+ 8, # 32: Divertor pump to Divertor
+ 27, # 33: Coolant pumps electric to Shield pump
+ 28, # 34: Shield pump to Shield
+ 10, # 35: Shield to primary thermal
+ 28, # 36: Neutrons to shield
+ 29, # 37: Cryo plant to secondary heat
+ 29, # 38: Base plant load to secondary heat
+ 29, # 39: TF coils to secondary heat
+ 29, # 40: PF coils to secondary heat
+ 29, # 41: Vacuum pumps to secondary heat
+ 29, # 42: Tritium plant to secondary heat
+ 30, # 43: Neutrons to tf
+ 29, # 44: TF nuclear heat to secondary heat
+ 29, # 45: HCD electric losses to secondary heat
+ 29, # 46: Coolant pumps electric to secondary heat
+ 9, # 47: FW pump to primary heat, Should only show if FW and Bkt pumps are separate
+ 9, # 48: Blkt pump to primary heat, Should only show if FW and Blkt pumps are separate
+ 6, # 49 Should show in beams are present
+ 6, # 50: Should show in beams are present
+ 28, # 51 Neutrons to CP shield, should only show if CP shield is present
+ 8, # 52 Plasma separatrix power to divertor
+ 29, # 53 Divertor secondary heat,
+ 29, # 54 Shield secondary heat
+ 31, # 55 Neutron power to H&CD & Diagnostics
+ 31, # 56: Radiation to H&CD & Diagnostics
+ 32, # 57: Total Secondary Heat
+ 32, # 58: H&CD & Diagnostics secondary heat
+ 33, # 59: Turbine Loss
+ 6, # 60: FW nuclear heat
+ 2, # 61: Alpha particles back to plasma
+ 7, # 62: Blanket neutron multiplication
+ ]
+ values = [
+ p_hcd_injected_total_mw, # 0
+ p_plasma_ohmic_mw, # 1
+ p_alpha_total_mw, # 2
+ p_neutron_total_mw, # 3
+ p_plasma_rad_mw, # 4
+ p_fw_alpha_mw, # 5
+ p_blkt_nuclear_heat_total_mw
+ - m_file.data["p_blkt_multiplication_mw"].get_scan(-1), # 6
+ p_fw_rad_total_mw, # 7
+ m_file.data["p_div_nuclear_heat_total_mw"].get_scan(-1), # 8
+ m_file.data["p_div_rad_total_mw"].get_scan(-1), # 9
+ m_file.data["p_fw_heat_deposited_mw"].get_scan(-1), # 10
+ m_file.data["p_blkt_heat_deposited_mw"].get_scan(-1), # 11
+ m_file.data["p_div_heat_deposited_mw"].get_scan(-1), # 12
+ m_file.data["p_fw_blkt_heat_deposited_mw"].get_scan(-1), # 13
+ m_file.data["p_plant_primary_heat_mw"].get_scan(-1), # 14
+ m_file.data["p_plant_electric_gross_mw"].get_scan(-1), # 15
+ m_file.data["p_plant_electric_net_mw"].get_scan(-1), # 16
+ m_file.data["p_hcd_electric_total_mw"].get_scan(-1), # 17
+ m_file.data["p_hcd_electric_loss_mw"].get_scan(-1), # 18
+ p_hcd_injected_total_mw, # 19
+ m_file.data["p_plant_core_systems_elec_mw"].get_scan(-1), # 20
+ m_file.data["p_cryo_plant_electric_mw"].get_scan(-1), # 21
+ m_file.data["p_plant_electric_base_total_mw"].get_scan(-1), # 22
+ m_file.data["p_tf_electric_supplies_mw"].get_scan(-1), # 23
+ m_file.data["p_pf_electric_supplies_mw"].get_scan(-1), # 24
+ m_file.data["vachtmw"].get_scan(-1), # 25
+ m_file.data["p_tritium_plant_electric_mw"].get_scan(-1), # 26
+ m_file.data["p_coolant_pump_elec_total_mw"].get_scan(-1), # 27
+ m_file.data["p_coolant_pump_loss_total_mw"].get_scan(-1), # 28
+ m_file.data["p_div_coolant_pump_mw"].get_scan(-1), # 29
+ m_file.data["p_fw_blkt_coolant_pump_mw"].get_scan(-1), # 30
+ m_file.data["p_fw_blkt_coolant_pump_mw"].get_scan(-1), # 31
+ m_file.data["p_div_coolant_pump_mw"].get_scan(-1), # 32
+ m_file.data["p_shld_coolant_pump_mw"].get_scan(-1), # 33
+ m_file.data["p_shld_coolant_pump_mw"].get_scan(-1), # 34
+ m_file.data["p_shld_heat_deposited_mw"].get_scan(-1), # 35
+ m_file.data["p_shld_nuclear_heat_mw"].get_scan(-1), # 36
+ m_file.data["p_cryo_plant_electric_mw"].get_scan(-1), # 37
+ m_file.data["p_plant_electric_base_total_mw"].get_scan(-1), # 38
+ m_file.data["p_tf_electric_supplies_mw"].get_scan(-1), # 39
+ m_file.data["p_pf_electric_supplies_mw"].get_scan(-1), # 40
+ m_file.data["vachtmw"].get_scan(-1), # 41
+ m_file.data["p_tritium_plant_electric_mw"].get_scan(-1), # 42
+ m_file.data["p_tf_nuclear_heat_mw"].get_scan(-1), # 43
+ m_file.data["p_tf_nuclear_heat_mw"].get_scan(-1), # 44
+ m_file.data["p_hcd_electric_loss_mw"].get_scan(-1), # 45
+ m_file.data["p_coolant_pump_loss_total_mw"].get_scan(-1), # 46
+ m_file.data["p_fw_coolant_pump_mw"].get_scan(
+ -1
+ ), # 47 Should only show if FW and Bkt pumps are seperate
+ m_file.data["p_blkt_coolant_pump_mw"].get_scan(
+ -1
+ ), # 48 Should only show if FW and Blkt pumps are seperate
+ m_file.data["p_beam_shine_through_mw"].get_scan(
+ -1
+ ), # 49 Should show in beams are present
+ m_file.data["p_beam_orbit_loss_mw"].get_scan(
+ -1
+ ), # 50 Should show in beams are present
+ m_file.data["p_cp_shield_nuclear_heat_mw"].get_scan(
+ -1
+ ), # 51 Neutrons to CP shield, should only show if CP shield is present
+ m_file.data["p_plasma_separatrix_mw"].get_scan(
+ -1
+ ), # 52 Plasma separatrix power to divertor
+ m_file.data["p_div_secondary_heat_mw"].get_scan(
+ -1
+ ), # 53 Divertor secondary heat,
+ m_file.data["p_shld_secondary_heat_mw"].get_scan(
+ -1
+ ), # 54 Shield secondary heat
+ m_file.data["p_fw_hcd_nuclear_heat_mw"].get_scan(
+ -1
+ ), # 55 Neutron power to H&CD & Diagnostics
+ m_file.data["p_fw_hcd_rad_total_mw"].get_scan(
+ -1
+ ), # 56: Radiation to H&CD & Diagnostics
+ m_file.data["p_plant_secondary_heat_mw"].get_scan(
+ -1
+ ), # 57: Total Secondary Heat
+ m_file.data["p_hcd_secondary_heat_mw"].get_scan(
+ -1
+ ), # 58: H&CD & Diagnostics secondary heat
+ m_file.data["p_turbine_loss_mw"].get_scan(-1), # 59: Turbine Loss
+ m_file.data["p_fw_nuclear_heat_total_mw"].get_scan(-1), # 60: FW nuclear heat
+ p_alpha_total_mw
+ * m_file.data["f_p_alpha_plasma_deposited"].get_scan(
+ -1
+ ), # 61: Alpha particles back to plasma
+ m_file.data["p_blkt_multiplication_mw"].get_scan(-1),
+ ]
+
+ # Define colors for each node (hex or rgba)
+ node_colors = [
+ "#1f77b4", # 0: H&CD injector
+ "#ff7f0e", # 1: Ohmic
+ "#2ca02c", # 2: Plasma Fusion Power
+ "#d62728", # 3: Alpha particles
+ "#9467bd", # 4: Neutrons
+ "#8c564b", # 5: Radiation
+ "#e377c2", # 6: First Wall
+ "#7f7f7f", # 7: Blanket
+ "#bcbd22", # 8: Divertor
+ "#17becf", # 9: FW+Blkt
+ "#aec7e8", # 10: Primary Thermal
+ "#ffbb78", # 11: Turbine
+ "#98df8a", # 12: Gross Electric
+ "#ff9896", # 13: Net Electric
+ "#c5b0d5", # 14: HCD Electric Power
+ "#c49c94", # 15: HCD electric losses
+ "#f7b6d2", # 16: Core systems
+ "#c7c7c7", # 17: Cryo plant
+ "#dbdb8d", # 18: Base plant load
+ "#9edae5", # 19: TF coils
+ "#393b79", # 20: PF coils
+ "#637939", # 21: Vacuum pumps
+ "#8c6d31", # 22: Tritium plant
+ "#843c39", # 23: Coolant pumps electric
+ "#7b4173", # 24: Coolant pump electric losses
+ "#5254a3", # 25: Divertor pump
+ "#6b6ecf", # 26: FW+Blkt pumps
+ "#b5cf6b", # 27: Shield pump
+ "#cedb9c", # 28: Shield
+ "#9c9ede", # 29: Secondary heat
+ "#e7ba52", # 30: TF nuclear heat
+ "#ad494a", # 31: H&CD & Diagnostics
+ "#a55194", # 32: Total Secondary Heat
+ "#393b79", # 33: Turbine Loss
+ "#637939", # 34: Blanket neutron multiplication
+ ]
+
+ # Assign link colors to match their source node
+ link_colors = [node_colors[src] for src in sources]
+
+ # Add value labels to the links
+ value_labels = [f"{v:.3f} MW" for v in values]
+
+ sankey_dict = {
+ "type": "sankey",
+ "node": {
+ "pad": 30,
+ "thickness": 20,
+ "line": {"color": "black", "width": 0.5},
+ "label": labels,
+ "color": node_colors,
+ },
+ "link": {
+ "source": sources,
+ "target": targets,
+ "value": values,
+ "label": value_labels,
+ "color": link_colors,
+ },
+ }
+ fig = go.Figure(data=[sankey_dict])
+
+ fig.update_layout({
+ "title_text": "Fusion Power Balance Sankey Diagram",
+ "font_size": 7,
+ "autosize": True,
+ "margin": {"l": 40, "r": 40, "t": 40, "b": 40},
+ })
+ # Strip 'MFILE' from the filename for the HTML output
+ # Remove the character before "MFILE" and "MFILE" itself from the filename
+ html_output_path = pathlib.Path(
+ re.sub(r"(.)?[ \.\_]?MFILE", r"\1_plotly_sankey", m_file.filename)
+ ).with_suffix(".html")
+ fig.write_html(str(html_output_path))
+ print(f"Interactive Sankey diagram saved to {html_output_path}")
+ return fig
+
+
+if __name__ == "__main__":
+ main()
diff --git a/process/main.py b/process/main.py
index f908542267..b21cbd1654 100644
--- a/process/main.py
+++ b/process/main.py
@@ -73,7 +73,12 @@
from process.hcpb import CCFE_HCPB
from process.ife import IFE
from process.impurity_radiation import initialise_imprad
-from process.io import mfile, plot_proc, plot_radial_build, plot_sankey
+from process.io import (
+ mfile,
+ plot_plotly_sankey,
+ plot_proc,
+ plot_radial_build,
+)
from process.io import obsolete_vars as ov
# For VaryRun
@@ -244,7 +249,9 @@ def post_process(self):
if mfile_path.exists():
plot_proc.main(args=["-f", mfile_str])
plot_radial_build.main(args=["-f", mfile_str, "-nm"])
- plot_sankey.main(args=["-m", mfile_str])
+
+ plot_plotly_sankey.main(args=["-m", mfile_str])
+
else:
logger.error("mfile to be used for plotting doesn't exist")
diff --git a/setup.py b/setup.py
index 9a16ff0312..166f216c18 100644
--- a/setup.py
+++ b/setup.py
@@ -42,6 +42,7 @@
"extras_require": {
"test": ["pytest>=5.4.1", "requests>=2.30", "testbook>=0.4"],
"examples": ["pillow>=5.1.0", "jupyter==1.0.0", "pdf2image==1.16.0"],
+ "plotly": ["plotly>=5.15.0,<6"],
},
"entry_points": {"console_scripts": ["process=process.main:main"]},
"extra_link_args": EXTRA_ARGS,