Source code for scpn_fusion.engineering.balance_of_plant

# SPDX-License-Identifier: AGPL-3.0-or-later
# Commercial license available
# © Concepts 1996–2026 Miroslav Šotek. All rights reserved.
# © Code 2020–2026 Miroslav Šotek. All rights reserved.
# ORCID: 0009-0009-3560-0851
# Contact: www.anulum.li | protoscience@anulum.li
# SCPN Fusion Core — Balance Of Plant
"""Balance-of-plant performance model for conversion efficiency and parasitic loads."""

from __future__ import annotations

import logging
import math
from typing import Any, TypedDict

import matplotlib.pyplot as plt

from scpn_fusion.engineering.thermal_hydraulics import CoolantLoop, PumpingPowerResult

logger = logging.getLogger(__name__)


[docs] class PlantPowerBreakdown(TypedDict): """Parasitic-load breakdown for balance-of-plant accounting.""" Cryo: float Pumps: float Heating_Plug: float Misc: float
[docs] class PlantPerformance(TypedDict): """Balance-of-plant scalar performance metrics.""" P_fusion: float P_thermal: float P_gross: float P_recirc: float P_net: float Q_plasma: float Q_eng: float breakdown: PlantPowerBreakdown hydraulics: PumpingPowerResult
[docs] class PowerPlantModel: """SCPN Balance of Plant (BOP) Simulator. Calculates the conversion of Fusion Energy to Grid Electricity. Includes parasitic loads (Magnets, Heating, Pumping). """ def __init__(self, coolant_type: str = "water") -> None: # Efficiency Parameters self.eta_thermal = 0.35 # Rankine Cycle efficiency (Steam turbines) self.eta_direct = 0.60 # Direct Conversion (if advanced fuels) - unlikely for DT self.eta_heating = 0.40 # Wall-plug efficiency of NBI/ECRH systems # Parasitic Loads (Base load in MW) self.P_cryo = 30.0 # Cryogenics for Superconductors self.P_bop_misc = 15.0 # Control rooms, lights, HVAC # Thermal Hydraulics self.coolant = CoolantLoop(coolant_type)
[docs] def calculate_plant_performance( self, P_fusion_MW: float, P_aux_absorbed_MW: float, *, coolant_parallel_channels: int = 1, coolant_length_m: float = 100.0, coolant_diameter_m: float = 0.05, coolant_temperature_rise_k: float = 50.0, ) -> PlantPerformance: """Calculate instantaneous plant power with explicitly configured cooling. Parameters ---------- P_fusion_MW : float Nonnegative finite total alpha plus neutron fusion power in MW. P_aux_absorbed_MW : float Nonnegative finite heating power absorbed by the plasma in MW. coolant_parallel_channels : int Number of identical equal-flow cooling paths, default one. coolant_length_m : float Length of each cooling path in metres. coolant_diameter_m : float Internal diameter of each cooling path in metres. coolant_temperature_rise_k : float Coolant temperature rise per path in kelvin. Returns ------- PlantPerformance Thermal, gross, recirculating and net powers in MW, gain ratios, component electrical loads and explicit hydraulic diagnostics. Negative net power is preserved. Historical zero-denominator gain convention returns zero; it does not establish a finite physical gain. Raises ------ ValueError For invalid input powers or invalid cooling configuration. Notes ----- Default cooling is one 5 cm pipe, not a plant-scale layout. Supply actual geometry and channel count; do not calibrate them solely to target net electricity. CoolantLoop's constant-property limitations apply. """ for name, value in (("P_fusion_MW", P_fusion_MW), ("P_aux_absorbed_MW", P_aux_absorbed_MW)): if not math.isfinite(value) or value < 0: raise ValueError(f"{name} must be finite and non-negative.") # 1. Thermal Power Generation P_neutron = 0.8 * P_fusion_MW P_alpha = 0.2 * P_fusion_MW P_aux_thermal = P_aux_absorbed_MW # Exothermic Lithium Reactions in Blanket (Energy Multiplication) M_blanket = 1.15 P_thermal_blanket = P_neutron * M_blanket P_thermal_total = P_thermal_blanket + P_alpha + P_aux_thermal # 2. Gross Electrical Generation P_gross_electric = P_thermal_total * self.eta_thermal # 3. Parasitic Consumption (Recirculating Power) # Physics-based pumping power pump_res = self.coolant.calculate_pumping_power( P_thermal_total, delta_T=coolant_temperature_rise_k, L=coolant_length_m, D=coolant_diameter_m, parallel_channels=coolant_parallel_channels, ) P_pump = float(pump_res["P_pump_MW"]) # Power needed to drive the Aux Heating systems (Efficiency loss) P_aux_wall_plug = P_aux_absorbed_MW / self.eta_heating # Total House Load P_recirculating = self.P_cryo + P_pump + self.P_bop_misc + P_aux_wall_plug # 4. Net Electricity P_net_electric = P_gross_electric - P_recirculating # 5. Metrics Q_plasma = P_fusion_MW / P_aux_absorbed_MW if P_aux_absorbed_MW > 0 else 0 Q_engineering = P_gross_electric / P_recirculating if P_recirculating > 0 else 0 return { "hydraulics": pump_res, "P_fusion": P_fusion_MW, "P_thermal": P_thermal_total, "P_gross": P_gross_electric, "P_recirc": P_recirculating, "P_net": P_net_electric, "Q_plasma": Q_plasma, "Q_eng": Q_engineering, "breakdown": { "Cryo": self.P_cryo, "Pumps": P_pump, "Heating_Plug": P_aux_wall_plug, "Misc": self.P_bop_misc, }, }
[docs] def plot_sankey_diagram(self, metrics: PlantPerformance) -> Any: """Visualise power flow as a text-based or simple bar summary.""" labels = ["Fusion Power", "Thermal Total", "Gross Elec", "Recirculating", "NET TO GRID"] values = [ metrics["P_fusion"], metrics["P_thermal"], metrics["P_gross"], metrics["P_recirc"], metrics["P_net"], ] colors = ["red", "orange", "blue", "gray", "green"] fig, ax = plt.subplots(figsize=(10, 6)) ax.bar(labels, values, color=colors) ax.set_ylabel("Power (MW)") ax.set_title(f"Plant Power Balance (Net: {metrics['P_net']:.1f} MWe)") # Threshold line ax.axhline(0, color="black", linewidth=1) return fig
if __name__ == "__main__": logging.basicConfig(level=logging.INFO) # Test Run plant = PowerPlantModel() # Scenario: 500MW Fusion, 50MW Heating res = plant.calculate_plant_performance(500.0, 50.0) logger.info("--- FUSION PLANT STATUS ---") logger.info("Fusion Power: %.1f MW", res["P_fusion"]) logger.info("Gross Electric: %.1f MW", res["P_gross"]) logger.info("House Load: %.1f MW", res["P_recirc"]) logger.info("NET TO GRID: %.1f MW", res["P_net"]) logger.info("Q_eng: %.2f", res["Q_eng"]) logger.info("Screening calculation only; net power does not establish commercial viability.")