From 6bc98ccf37aa1ea55357c96445fd6ba6c6dc19e7 Mon Sep 17 00:00:00 2001 From: Jonathan Matthews Date: Thu, 21 Aug 2025 09:55:49 +0100 Subject: [PATCH 01/61] addition of ParaterFrame dataclasses for classes in case_TF.py --- bluemira/magnets/case_tf.py | 399 ++++++++++-------------------------- 1 file changed, 105 insertions(+), 294 deletions(-) diff --git a/bluemira/magnets/case_tf.py b/bluemira/magnets/case_tf.py index 7031801e8c..d22db0812c 100644 --- a/bluemira/magnets/case_tf.py +++ b/bluemira/magnets/case_tf.py @@ -18,6 +18,7 @@ import math from abc import ABC, abstractmethod +from dataclasses import dataclass import matplotlib.pyplot as plt import numpy as np @@ -31,6 +32,8 @@ bluemira_print, bluemira_warn, ) +from bluemira.base.parameter_frame import Parameter, ParameterFrame +from bluemira.base.parameter_frame.typed import ParameterFrameLike from bluemira.magnets.registry import RegistrableMeta from bluemira.magnets.utils import parall_k, serie_k from bluemira.magnets.winding_pack import WindingPack, create_wp_from_dict @@ -44,6 +47,20 @@ # ------------------------------------------------------------------------------ # TFcoil cross section Geometry Base and Implementations # ------------------------------------------------------------------------------ +@dataclass +class TFCaseGeometryParams(ParameterFrame): + """ + Parameters needed for the TF casing geometry + """ + + Ri: Parameter[float] + """External radius of the TF coil case [m].""" + Rk: Parameter[float] + """Internal radius of the TF coil case [m].""" + theta_TF: Parameter[float] + """Toroidal angular span of the TF coil [degrees].""" + + class CaseGeometry(ABC): """ Abstract base class for TF case geometry profiles. @@ -52,132 +69,11 @@ class CaseGeometry(ABC): as well as geometric plotting and area calculation interfaces. """ - def __init__(self, Ri: float, Rk: float, theta_TF: float): # noqa: N803 - """ - Initialize the geometry base. + param_cls: type[TFCaseGeometryParams] = TFCaseGeometryParams - Parameters - ---------- - Ri : float - External radius of the TF coil case [m]. - Rk : float - Internal radius of the TF coil case [m]. - theta_TF : float - Toroidal angular span of the TF coil [degrees]. - """ - self._Ri = None - self.Ri = Ri - - self._Rk = None - self.Rk = Rk - - self.theta_TF = theta_TF - - @property - def Ri(self) -> float: # noqa: N802 - """ - External (outermost) radius of the TF case at the top [m]. - - Returns - ------- - float - Outer radius measured from the machine center to the case outer wall [m]. - """ - return self._Ri - - @Ri.setter - def Ri(self, value: float): # noqa: N802 - """ - Set the external (outermost) radius of the TF case. - - Parameters - ---------- - value : float - Outer radius [m]. Must be a strictly positive number. - - Raises - ------ - ValueError - If the provided radius is not positive. - """ - if value <= 0: - raise ValueError("Ri must be positive.") - self._Ri = value - - @property - def Rk(self) -> float: # noqa: N802 - """ - Internal (innermost) radius of the TF case at the top [m]. - - Returns - ------- - float - Inner radius measured from the machine center to the case outer wall [m]. - """ - return self._Rk - - @Rk.setter - def Rk(self, value: float): # noqa: N802 - """ - Set the internal (innermost) radius of the TF case. - - Parameters - ---------- - value : float - Outer radius [m]. Must be a strictly positive number. - - Raises - ------ - ValueError - If the provided radius is not positive. - """ - if value < 0: - raise ValueError("Rk must be positive.") - self._Rk = value - - @property - def theta_TF(self) -> float: - """ - Toroidal angular span of the TF coil [degrees]. - - Returns - ------- - float - Toroidal angular span [°]. - """ - return self._theta_TF - - @theta_TF.setter - def theta_TF(self, value: float): - """ - Set the toroidal angular span and update the internal radian representation. - - Parameters - ---------- - value : float - New toroidal angular span [degrees]. - - Raises - ------ - ValueError - If the provided value is not within (0, 360] degrees. - """ - if not (0.0 < value <= 360.0): # noqa: PLR2004 - raise ValueError("theta_TF must be in the range (0, 360] degrees.") - self._theta_TF = value - self._rad_theta_TF = np.radians(value) - - @property - def rad_theta_TF(self): - """ - Toroidal angular span of the TF coil [radians]. - - Returns - ------- - float - Toroidal aperture converted to radians. - """ - return self._rad_theta_TF + def __init__(self, params: ParameterFrameLike): + super().__init__(params) # fix when split into builders and designers + self.rad_theta_TF = np.radians(self.params.theta_TF.value) def dx_at_radius(self, radius: float) -> float: """ @@ -260,8 +156,11 @@ def area(self) -> float: """ return ( 0.5 - * (self.dx_at_radius(self.Ri) + self.dx_at_radius(self.Rk)) - * (self.Ri - self.Rk) + * ( + self.dx_at_radius(self.params.Ri.value) + + self.dx_at_radius(self.params.Rk.value) + ) + * (self.params.Ri.value - self.params.Rk.value) ) def build_polygon(self) -> np.ndarray: @@ -275,14 +174,14 @@ def build_polygon(self) -> np.ndarray: Coordinates are ordered counterclockwise starting from the top-left corner: [(-dx_outer/2, Ri), (dx_outer/2, Ri), (dx_inner/2, Rk), (-dx_inner/2, Rk)]. """ - dx_outer = self.dx_at_radius(self.Ri) - dx_inner = self.dx_at_radius(self.Rk) + dx_outer = self.dx_at_radius(self.params.Ri.value) + dx_inner = self.dx_at_radius(self.params.Rk.value) return np.array([ - [-dx_outer / 2, self.Ri], - [dx_outer / 2, self.Ri], - [dx_inner / 2, self.Rk], - [-dx_inner / 2, self.Rk], + [-dx_outer / 2, self.params.Ri.value], + [dx_outer / 2, self.params.Ri.value], + [dx_inner / 2, self.params.Rk.value], + [-dx_inner / 2, self.params.Rk.value], ]) def plot(self, ax=None, *, show=False) -> plt.Axes: @@ -332,7 +231,9 @@ def area(self) -> float: Cross-sectional area [m²] defined by the wedge between outer radius Ri and inner radius Rk over the toroidal angle theta_TF. """ - return 0.5 * self.rad_theta_TF * (self.Ri**2 - self.Rk**2) + return ( + 0.5 * self.rad_theta_TF * (self.params.Ri.value**2 - self.params.Rk.value**2) + ) def build_polygon(self, n_points: int = 50) -> np.ndarray: """ @@ -358,12 +259,12 @@ def build_polygon(self, n_points: int = 50) -> np.ndarray: angles_inner = np.linspace(theta2, theta1, n_points) arc_outer = np.column_stack(( - self.Ri * np.sin(angles_outer), - self.Ri * np.cos(angles_outer), + self.params.Ri.value * np.sin(angles_outer), + self.params.Ri.value * np.cos(angles_outer), )) arc_inner = np.column_stack(( - self.Rk * np.sin(angles_inner), - self.Rk * np.cos(angles_inner), + self.params.Rk.value * np.sin(angles_inner), + self.params.Rk.value * np.cos(angles_inner), )) return np.vstack((arc_outer, arc_inner)) @@ -399,6 +300,22 @@ def plot(self, ax=None, *, show=False): # ------------------------------------------------------------------------------ # CaseTF Class # ------------------------------------------------------------------------------ +@dataclass +class TFCaseParams(ParameterFrame): + """ + Parameters needed for the TF casing + """ + + Ri: Parameter[float] + """External radius at the top of the TF coil case [m].""" + theta_TF: Parameter[float] + """Toroidal angular aperture of the coil [degrees].""" + dy_ps: Parameter[float] + """Radial thickness of the poloidal support region [m].""" + dy_vault: Parameter[float] + """Radial thickness of the vault support region [m].""" + + class BaseCaseTF(CaseGeometry, ABC, metaclass=RegistrableMeta): """ Abstract Base Class for Toroidal Field Coil Case configurations. @@ -409,12 +326,11 @@ class BaseCaseTF(CaseGeometry, ABC, metaclass=RegistrableMeta): _registry_ = CASETF_REGISTRY _name_in_registry_ = None + param_cls: type[TFCaseParams] = TFCaseParams + def __init__( self, - Ri: float, # noqa: N803 - dy_ps: float, - dy_vault: float, - theta_TF: float, + params: ParameterFrameLike, mat_case: Material, WPs: list[WindingPack], # noqa: N803 name: str = "BaseCaseTF", @@ -439,28 +355,17 @@ def __init__( name : str, optional String identifier for the TF coil case instance (default is "BaseCaseTF"). """ - self._name = None - self.name = name - - self._dy_ps = None - self.dy_ps = dy_ps - - self._WPs = None - self.WPs = WPs - - self._mat_case = None - self.mat_case = mat_case - - self._Ri = None - self.Ri = Ri - - self._theta_TF = None - self.theta_TF = theta_TF - - # super().__init__(Ri=Ri, Rk=0, theta_TF=theta_TF) - - self._dy_vault = None - self.dy_vault = dy_vault + super().__init__( + params, + mat_case=mat_case, + WPs=WPs, + name=name, + ) + self.dx_i = 2 * self.params.Ri.value * np.tan(self.rad_theta_TF / 2) + self.dx_ps = ( + self.params.Ri.value + (self.params.Ri.value - self.params.dy_ps.value) + ) * np.tan(self.rad_theta_TF / 2) + self.update_dy_vault(self.params.dy_vault.value) @property def name(self) -> str: @@ -493,70 +398,24 @@ def name(self, value: str): raise TypeError("name must be a string.") self._name = value - @property - def dy_ps(self) -> float: - """ - Radial thickness of the poloidal support (PS) region [m]. - - Returns - ------- - float - Thickness of the upper structural cap between the TF case wall and the - first winding pack [m]. - """ - return self._dy_ps - - @dy_ps.setter - def dy_ps(self, value: float): + def update_dy_vault(self, value: float): """ - Set the thickness of the poloidal support region. + Update the value of the vault support region thickness Parameters ---------- value : float - Poloidal support thickness [m]. - - Raises - ------ - ValueError - If value is not positive. - """ - if value <= 0: - raise ValueError("dy_ps must be positive.") - self._dy_ps = value - - @property - def dy_vault(self) -> float: - """ - Radial thickness of the vault support region [m]. - - Returns - ------- - float - Thickness of the lower structural region supporting the winding packs [m]. + Vault thickness [m]. """ - return self._dy_vault + self.params.dy_vault.value = value + self.Rk = self.R_wp_k[-1] - self.params.dy_vault.value - @dy_vault.setter - def dy_vault(self, value: float): + def update_Rk(self, value: float): # noqa: N802 """ - Set the thickness of the vault support region. - - Parameters - ---------- - value : float - Vault thickness [m]. - - Raises - ------ - ValueError - If value is not positive. + Set the internal (innermost) radius of the TF case. """ - if value <= 0: - raise ValueError("dy_vault must be positive.") - self._dy_vault = value - - self.Rk = self.R_wp_k[-1] - self._dy_vault + self.Rk = value + self.params.dy_vault.value = self.R_wp_k[-1] - self._Rk @property @abstractmethod @@ -637,16 +496,6 @@ def WPs(self, value: list[WindingPack]): # noqa: N802 if hasattr(self, "dy_vault"): self.dy_vault = self.dy_vault - @property - def dx_i(self): - """Toroidal length of the coil case at its maximum radial position [m]""" - return 2 * self.Ri * np.tan(self._rad_theta_TF / 2) - - @property - def dx_ps(self): - """Average toroidal length of the ps plate [m]""" - return (self.Ri + (self.Ri - self.dy_ps)) * np.tan(self._rad_theta_TF / 2) - @property def n_conductors(self): """Total number of conductors in the winding pack.""" @@ -712,39 +561,6 @@ def R_wp_k(self): # noqa: N802 """ return self.R_wp_i - self.dy_wp_i - @property - def Rk(self) -> float: # noqa: N802 - """ - Internal (innermost) radius of the TF case at the top [m]. - - Returns - ------- - float - Inner radius measured from the machine center to the case outer wall [m]. - """ - return self._Rk - - @Rk.setter - def Rk(self, value: float): # noqa: N802 - """ - Set the internal (innermost) radius of the TF case. - - Parameters - ---------- - value : float - Outer radius [m]. Must be a strictly positive number. - - Raises - ------ - ValueError - If the provided radius is not positive. - """ - if value < 0: - raise ValueError("Rk must be positive.") - self._Rk = value - - self._dy_vault = self.R_wp_k[-1] - self._Rk - def plot(self, ax=None, *, show: bool = False, homogenized: bool = False): """ Schematic plot of the TF case cross-section including winding packs. @@ -991,10 +807,10 @@ def to_dict(self) -> dict: self, "_name_in_registry_", self.__class__.__name__ ), "name": self.name, - "Ri": self.Ri, - "dy_ps": self.dy_ps, - "dy_vault": self.dy_vault, - "theta_TF": self.theta_TF, + "Ri": self.params.Ri.value, + "dy_ps": self.params.dy_ps.value, + "dy_vault": self.params.dy_vault.value, + "theta_TF": self.params.theta_TF.value, "mat_case": self.mat_case.name, # Assume Material has 'name' attribute "WPs": [wp.to_dict() for wp in self.WPs], # Assume each WindingPack implements to_dict() @@ -1054,11 +870,11 @@ def __str__(self) -> str: """ return ( f"CaseTF '{self.name}'\n" - f" - Ri: {self.Ri:.3f} m\n" + f" - Ri: {self.params.Ri.value:.3f} m\n" f" - Rk: {self.Rk:.3f} m\n" - f" - dy_ps: {self.dy_ps:.3f} m\n" - f" - dy_vault: {self.dy_vault:.3f} m\n" - f" - theta_TF: {self.theta_TF:.2f}°\n" + f" - dy_ps: {self.params.dy_ps.value:.3f} m\n" + f" - dy_vault: {self.params.dy_vault.value:.3f} m\n" + f" - theta_TF: {self.params.theta_TF.value:.2f}°\n" f" - Material: {self.mat_case.name}\n" f" - Winding Packs: {len(self.WPs)} packs\n" ) @@ -1072,13 +888,11 @@ class TrapezoidalCaseTF(BaseCaseTF, TrapezoidalGeometry): _registry_ = CASETF_REGISTRY _name_in_registry_ = "TrapezoidalCaseTF" + param_cls: type[TFCaseParams] = TFCaseParams def __init__( self, - Ri: float, # noqa: N803 - dy_ps: float, - dy_vault: float, - theta_TF: float, + params: ParameterFrameLike, mat_case: Material, WPs: list[WindingPack], # noqa: N803 name: str = "TrapezoidalCaseTF", @@ -1086,10 +900,7 @@ def __init__( self._check_WPs(WPs) super().__init__( - Ri=Ri, - dy_ps=dy_ps, - dy_vault=dy_vault, - theta_TF=theta_TF, + params, mat_case=mat_case, WPs=WPs, name=name, @@ -1155,7 +966,7 @@ def Kx_ps(self, op_cond: OperationalConditions): # noqa: N802 float Equivalent radial stiffness of the poloidal support [Pa]. """ - return self.mat_case.youngs_modulus(op_cond) * self.dy_ps / self.dx_ps + return self.mat_case.youngs_modulus(op_cond) * * self.params.dy_ps.value / self.dx_ps def Kx_lat(self, op_cond: OperationalConditions): # noqa: N802 """ @@ -1198,7 +1009,7 @@ def Kx_vault(self, op_cond: OperationalConditions): # noqa: N802 float Equivalent radial stiffness of the vault [Pa]. """ - return self.mat_case.youngs_modulus(op_cond) * self.dy_vault / self.dx_vault + return self.mat_case.youngs_modulus(op_cond) * self.params.dy_vault.value / self.dx_vault def Kx(self, op_cond: OperationalConditions): # noqa: N802 """ @@ -1245,7 +1056,7 @@ def Ky_ps(self, op_cond: OperationalConditions): # noqa: N802 float Equivalent toroidal stiffness of the PS region [Pa]. """ - return self.mat_case.youngs_modulus(op_cond) * self.dx_ps / self.dy_ps + return self.mat_case.youngs_modulus(op_cond) * self.dx_ps / self.params.dy_ps.value def Ky_lat(self, op_cond: OperationalConditions): # noqa: N802 """ @@ -1266,7 +1077,7 @@ def Ky_lat(self, op_cond: OperationalConditions): # noqa: N802 Array of toroidal stiffness values for each lateral segment [Pa]. """ dx_lat = np.array([ - (self.R_wp_i[i] + self.R_wp_k[i]) / 2 * np.tan(self._rad_theta_TF / 2) + (self.R_wp_i[i] + self.R_wp_k[i]) / 2 * np.tan(self.rad_theta_TF / 2) - w.dx / 2 for i, w in enumerate(self.WPs) ]) @@ -1288,7 +1099,7 @@ def Ky_vault(self, op_cond: OperationalConditions): # noqa: N802 float Equivalent toroidal stiffness of the vault [Pa]. """ - return self.mat_case.youngs_modulus(op_cond) * self.dx_vault / self.dy_vault + return self.mat_case.youngs_modulus(op_cond) * self.dx_vault / self.params.dy_vault.value def Ky(self, op_cond: OperationalConditions): # noqa: N802 """ @@ -1492,7 +1303,7 @@ def _tresca_stress(self, pm: float, fz: float, op_cond: OperationalConditions): # The maximum principal stress acting on the case nose is the compressive # hoop stress generated in the equivalent shell from the magnetic pressure. From # the Shell theory, for an isotropic continuous shell with a thickness ratio: - beta = self.Rk / (self.Rk + self.dy_vault) + beta = self.Rk / (self.Rk + self.params.dy_vault.value) # the maximum hoop stress, corrected to account for the presence of the WP, is # placed at the innermost radius of the case as: sigma_theta = ( @@ -1558,7 +1369,7 @@ def optimize_vault_radial_thickness( if not result.success: raise ValueError("dy_vault optimization did not converge.") - self.dy_vault = result.x + self.params.dy_vault.value = result.x # print(f"Optimal dy_vault: {self.dy_vault}") # print(f"Tresca sigma: {self._tresca_stress(pm, fz, T=T, B=B) / 1e6} MPa") @@ -1602,7 +1413,7 @@ def _sigma_difference( This function modifies the case's vault thickness using the value provided in jacket_thickness. """ - self.dy_vault = dy_vault + self.params.dy_vault.value = dy_vault sigma = self._tresca_stress(pm, fz, op_cond) # bluemira_print(f"sigma: {sigma}, allowable_sigma: {allowable_sigma}, # diff: {sigma - allowable_sigma}") @@ -1691,11 +1502,11 @@ def optimize_jacket_and_vault( self._convergence_array.append([ i, conductor.dy_jacket, - self.dy_vault, + self.params.dy_vault.value, err_conductor_area_jacket, err_dy_vault, self.dy_wp_tot, - self.Ri - self.Rk, + self.params.Ri.value - self.Rk, ]) damping_factor = 0.3 @@ -1751,16 +1562,16 @@ def optimize_jacket_and_vault( bounds=bounds_dy_vault, ) - self.dy_vault = ( + self.params.dy_vault.value = ( 1 - damping_factor - ) * case_dy_vault0 + damping_factor * self.dy_vault + ) * case_dy_vault0 + damping_factor * self.params.dy_vault.value delta_case_dy_vault = abs(self.dy_vault - case_dy_vault0) - err_dy_vault = delta_case_dy_vault / self.dy_vault + err_dy_vault = delta_case_dy_vault / self.params.dy_vault.value tot_err = err_dy_vault + err_conductor_area_jacket debug_msg.append( - f"after optimization: case dy_vault = {self.dy_vault}\n" + f"after optimization: case dy_vault = {self.params.dy_vault.value}\n" f"err_dy_jacket = {err_conductor_area_jacket}\n " f"err_dy_vault = {err_dy_vault}\n " f"tot_err = {tot_err}" @@ -1770,11 +1581,11 @@ def optimize_jacket_and_vault( self._convergence_array.append([ i, conductor.dy_jacket, - self.dy_vault, + self.params.dy_vault.value, err_conductor_area_jacket, err_dy_vault, self.dy_wp_tot, - self.Ri - self.Rk, + self.params.Ri.value - self.Rk, ]) # final check From 43d562dfc7c291770d6e1ae646c5025cdc63b796 Mon Sep 17 00:00:00 2001 From: Jonathan Matthews Date: Thu, 21 Aug 2025 10:43:59 +0100 Subject: [PATCH 02/61] addition of ParaterFrame dataclasses for classes in cable.py --- bluemira/magnets/cable.py | 279 +++++++++++------------------------- bluemira/magnets/case_tf.py | 2 +- 2 files changed, 88 insertions(+), 193 deletions(-) diff --git a/bluemira/magnets/cable.py b/bluemira/magnets/cable.py index 98df8d8113..0c1f6d87db 100644 --- a/bluemira/magnets/cable.py +++ b/bluemira/magnets/cable.py @@ -8,6 +8,7 @@ from abc import ABC, abstractmethod from collections.abc import Callable +from dataclasses import dataclass from typing import Any import matplotlib.pyplot as plt @@ -17,6 +18,8 @@ from scipy.optimize import minimize_scalar from bluemira.base.look_and_feel import bluemira_error, bluemira_print, bluemira_warn +from bluemira.base.parameter_frame import Parameter, ParameterFrame +from bluemira.base.parameter_frame.typed import ParameterFrameLike from bluemira.magnets.registry import RegistrableMeta from bluemira.magnets.strand import ( Strand, @@ -34,6 +37,22 @@ # ------------------------------------------------------------------------------ # Cable Class # ------------------------------------------------------------------------------ +@dataclass +class CableParams(ParameterFrame): + """ + Parameters needed for the TF cable + """ + + n_sc_strand: Parameter[int] + """Number of superconducting strands.""" + n_stab_strand: Parameter[int] + """Number of stabilizing strands.""" + d_cooling_channel: Parameter[float] + """Diameter of the cooling channel [m].""" + void_fraction: Parameter[float] = 0.725 + """Ratio of material volume to total volume [unitless].""" + cos_theta: Parameter[float] = 0.97 + """Correction factor for twist in the cable layout.""" class ABCCable(ABC, metaclass=RegistrableMeta): @@ -51,16 +70,13 @@ class ABCCable(ABC, metaclass=RegistrableMeta): _registry_ = CABLE_REGISTRY _name_in_registry_: str | None = None # Abstract base classes should NOT register + param_cls: type[CableParams] = CableParams def __init__( self, sc_strand: SuperconductingStrand, stab_strand: Strand, - n_sc_strand: int, - n_stab_strand: int, - d_cooling_channel: float, - void_fraction: float = 0.725, - cos_theta: float = 0.97, + params: ParameterFrameLike, name: str = "Cable", ): """ @@ -90,24 +106,15 @@ def __init__( name : str Identifier for the cable instance. """ + super().__init__(params) # fix when split into builders and designers # initialize private variables - self._d_cooling_channel = None - self._void_fraction = None - self._n_sc_strand = None - self._n_stab_strand = None - self._cos_theta = None - self._shape = None + self._shape = None # remove? # assign # Setting self.name triggers automatic instance registration self.name = name self.sc_strand = sc_strand self.stab_strand = stab_strand - self.void_fraction = void_fraction - self.d_cooling_channel = d_cooling_channel - self.n_sc_strand = n_sc_strand - self.n_stab_strand = n_stab_strand - self.cos_theta = cos_theta @property @abstractmethod @@ -126,98 +133,6 @@ def aspect_ratio(self): """ return self.dx / self.dy - @property - def n_sc_strand(self): - """Number of superconducting strands""" - return self._n_sc_strand - - @n_sc_strand.setter - def n_sc_strand(self, value: int): - """ - Set the number of superconducting strands. - - Raises - ------ - ValueError - If the value is not positive. - """ - if value <= 0: - msg = f"The number of superconducting strands must be positive, got {value}" - bluemira_error(msg) - raise ValueError(msg) - self._n_sc_strand = int(np.ceil(value)) - - @property - def n_stab_strand(self): - """Number of stabilizing strands""" - return self._n_stab_strand - - @n_stab_strand.setter - def n_stab_strand(self, value: int): - """ - Set the number of stabilizer strands. - - Raises - ------ - ValueError - If the value is negative. - """ - if value < 0: - msg = f"The number of stabilizing strands must be positive, got {value}" - bluemira_error(msg) - raise ValueError(msg) - self._n_stab_strand = int(np.ceil(value)) - - @property - def d_cooling_channel(self): - """Diameter of the cooling channel [m].""" - return self._d_cooling_channel - - @d_cooling_channel.setter - def d_cooling_channel(self, value: float): - """ - Set the cooling channel diameter. - - Raises - ------ - ValueError - If the value is negative. - """ - if value < 0: - msg = f"diameter of the cooling channel must be positive, got {value}" - bluemira_error(msg) - raise ValueError(msg) - - self._d_cooling_channel = value - - @property - def void_fraction(self): - """Void fraction of the cable.""" - return self._void_fraction - - @void_fraction.setter - def void_fraction(self, value: float): - if value < 0 or value > 1: - msg = f"void_fraction must be between 0 and 1, got {value}" - bluemira_error(msg) - raise ValueError(msg) - - self._void_fraction = value - - @property - def cos_theta(self): - """Correction factor for strand orientation (twist).""" - return self._cos_theta - - @cos_theta.setter - def cos_theta(self, value: float): - if value <= 0 or value > 1: - msg = f"cos theta must be in the interval ]0, 1], got {value}" - bluemira_error(msg) - raise ValueError(msg) - - self._cos_theta = value - def rho(self, op_cond: OperationalConditions): """ Compute the average mass density of the cable [kg/m³]. @@ -289,24 +204,24 @@ def Cp(self, op_cond: OperationalConditions): # noqa: N802 @property def area_stab(self): """Area of the stabilizer region""" - return self.stab_strand.area * self.n_stab_strand + return self.stab_strand.area * self.params.n_stab_strand.value @property def area_sc(self): """Area of the superconductor region""" - return self.sc_strand.area * self.n_sc_strand + return self.sc_strand.area * self.params.n_sc_strand.value @property def area_cc(self): """Area of the cooling channel""" - return self.d_cooling_channel**2 / 4 * np.pi + return self.params.d_cooling_channel.value**2 / 4 * np.pi @property def area(self): """Area of the cable considering the void fraction""" return ( self.area_sc + self.area_stab - ) / self.void_fraction / self.cos_theta + self.area_cc + ) / self.params.void_fraction.value / self.params.cos_theta.value + self.area_cc def E(self, op_cond: OperationalConditions): # noqa: N802 """ @@ -490,7 +405,7 @@ def final_temperature_difference( - It modifies the internal state `self._n_stab_strand`, which may affect subsequent evaluations unless restored. """ - self._n_stab_strand = n_stab + self.params.n_stab_strand.value = n_stab solution = self._temperature_evolution( t0=t0, @@ -521,7 +436,7 @@ def final_temperature_difference( ) # Here we re-ensure the n_stab_strand to be an integer - self.n_stab_strand = self._n_stab_strand + self.params.n_stab_strand.value = int(np.ceil(self.params.n_stab_strand.value)) solution = self._temperature_evolution(t0, tf, initial_temperature, B_fun, I_fun) final_temperature = solution.y[0][-1] @@ -537,7 +452,7 @@ def final_temperature_difference( "Optimization failed to keep final temperature ≤ target. " "Try increasing the upper bound of n_stab or adjusting cable parameters." ) - bluemira_print(f"Optimal n_stab: {self.n_stab_strand}") + bluemira_print(f"Optimal n_stab: {self.params.n_stab_strand.value}") bluemira_print( f"Final temperature with optimal n_stab: {final_temperature:.2f} Kelvin" ) @@ -563,9 +478,9 @@ def final_temperature_difference( f"Target T: {target_temperature:.2f} K\n" f"Initial T: {initial_temperature:.2f} K\n" f"SC Strand: {self.sc_strand.name}\n" - f"n. sc. strand = {self.n_sc_strand}\n" + f"n. sc. strand = {self.params.n_sc_strand.value}\n" f"Stab. strand = {self.stab_strand.name}\n" - f"n. stab. strand = {self.n_stab_strand}\n" + f"n. stab. strand = {self.params.n_stab_strand.value}\n" ) props = {"boxstyle": "round", "facecolor": "white", "alpha": 0.8} ax_temp.text( @@ -669,7 +584,9 @@ def plot(self, xc: float = 0, yc: float = 0, *, show: bool = False, ax=None): points_ext = np.vstack((p0, p1, p2, p3, p0)) + pc points_cc = ( np.array([ - np.array([np.cos(theta), np.sin(theta)]) * self.d_cooling_channel / 2 + np.array([np.cos(theta), np.sin(theta)]) + * self.params.d_cooling_channel.value + / 2 for theta in np.linspace(0, np.radians(360), 19) ]) + pc @@ -700,16 +617,16 @@ def __str__(self): f"dx: {self.dx}\n" f"dy: {self.dy}\n" f"aspect ratio: {self.aspect_ratio}\n" - f"d cooling channel: {self.d_cooling_channel}\n" - f"void fraction: {self.void_fraction}\n" - f"cos(theta): {self.cos_theta}\n" + f"d cooling channel: {self.params.d_cooling_channel.value}\n" + f"void fraction: {self.params.void_fraction.value}\n" + f"cos(theta): {self.params.cos_theta.value}\n" f"----- sc strand -------\n" f"sc strand: {self.sc_strand!s}\n" f"----- stab strand -------\n" f"stab strand: {self.stab_strand!s}\n" f"-----------------------\n" - f"n sc strand: {self.n_sc_strand}\n" - f"n stab strand: {self.n_stab_strand}" + f"n sc strand: {self.params.n_sc_strand.value}\n" + f"n stab strand: {self.params.n_stab_strand.value}" ) def to_dict(self) -> dict: @@ -726,11 +643,11 @@ def to_dict(self) -> dict: self, "_name_in_registry_", self.__class__.__name__ ), "name": self.name, - "n_sc_strand": self.n_sc_strand, - "n_stab_strand": self.n_stab_strand, - "d_cooling_channel": self.d_cooling_channel, - "void_fraction": self.void_fraction, - "cos_theta": self.cos_theta, + "n_sc_strand": self.params.n_sc_strand.value, + "n_stab_strand": self.params.n_stab_strand.value, + "d_cooling_channel": self.params.d_cooling_channel.value, + "void_fraction": self.params.void_fraction.value, + "cos_theta": self.params.cos_theta.value, "sc_strand": self.sc_strand.to_dict(), "stab_strand": self.stab_strand.to_dict(), } @@ -786,6 +703,7 @@ def from_dict( else: stab_strand = create_strand_from_dict(strand_dict=stab_strand_data) + # how to resolve this with ParameterFrame? return cls( sc_strand=sc_strand, stab_strand=stab_strand, @@ -798,6 +716,26 @@ def from_dict( ) +@dataclass +class RectangularCableParams(ParameterFrame): + """ + Parameters needed for the TF cable + """ + + dx: Parameter[float] + """Cable width in the x-direction [m].""" + n_sc_strand: Parameter[int] + """Number of superconducting strands.""" + n_stab_strand: Parameter[int] + """Number of stabilizing strands.""" + d_cooling_channel: Parameter[float] + """Diameter of the cooling channel [m].""" + void_fraction: Parameter[float] = 0.725 + """Ratio of material volume to total volume [unitless].""" + cos_theta: Parameter[float] = 0.97 + """Correction factor for twist in the cable layout.""" + + class RectangularCable(ABCCable): """ Cable with a rectangular cross-section. @@ -810,14 +748,9 @@ class RectangularCable(ABCCable): def __init__( self, - dx: float, sc_strand: SuperconductingStrand, stab_strand: Strand, - n_sc_strand: int, - n_stab_strand: int, - d_cooling_channel: float, - void_fraction: float = 0.725, - cos_theta: float = 0.97, + params: ParameterFrameLike, name: str = "RectangularCable", ): """ @@ -852,51 +785,20 @@ def __init__( super().__init__( sc_strand=sc_strand, stab_strand=stab_strand, - n_sc_strand=n_sc_strand, - n_stab_strand=n_stab_strand, - d_cooling_channel=d_cooling_channel, - void_fraction=void_fraction, - cos_theta=cos_theta, + params=params, name=name, ) - # initialize private variables - self._dx = None - - # assign - self.dx = dx - - @property - def dx(self): - """Cable dimension in the x direction [m]""" - return self._dx - - @dx.setter - def dx(self, value: float): - """ - Set cable width in x-direction. - - Raises - ------ - ValueError - If value is not positive. - """ - if value <= 0: - msg = "dx must be positive" - bluemira_error(msg) - raise ValueError(msg) - self._dx = value - @property def dy(self): """Cable dimension in the y direction [m]""" - return self.area / self.dx + return self.area / self.params.dx.value # Decide if this function shall be a setter. # Defined as "normal" function to underline that it modifies dx. def set_aspect_ratio(self, value: float) -> None: """Modify dx in order to get the given aspect ratio""" - self.dx = np.sqrt(value * self.area) + self.params.dx.value = np.sqrt(value * self.area) # OD homogenized structural properties def Kx(self, op_cond: OperationalConditions): # noqa: N802 @@ -914,7 +816,7 @@ def Kx(self, op_cond: OperationalConditions): # noqa: N802 float Homogenized stiffness in the x-direction [Pa]. """ - return self.E(op_cond) * self.dy / self.dx + return self.E(op_cond) * self.dy / self.params.dx.value def Ky(self, op_cond: OperationalConditions): # noqa: N802 """ @@ -931,7 +833,7 @@ def Ky(self, op_cond: OperationalConditions): # noqa: N802 float Homogenized stiffness in the y-direction [Pa]. """ - return self.E(op_cond) * self.dx / self.dy + return self.E(op_cond) * self.params.dx.value / self.dy def to_dict(self) -> dict: """ @@ -944,7 +846,7 @@ def to_dict(self) -> dict: """ data = super().to_dict() data.update({ - "dx": self.dx, + "dx": self.params.dx.value, "aspect_ratio": self.aspect_ratio, }) return data @@ -1035,6 +937,7 @@ def from_dict( void_fraction = cable_dict.get("void_fraction", 0.725) cos_theta = cable_dict.get("cos_theta", 0.97) + # how to handle with parameterframe? # Create cable cable = cls( dx=dx, @@ -1129,16 +1032,13 @@ class SquareCable(ABCCable): """ _name_in_registry_ = "SquareCable" + param_cls: type[CableParams] = CableParams def __init__( self, sc_strand: SuperconductingStrand, stab_strand: Strand, - n_sc_strand: int, - n_stab_strand: int, - d_cooling_channel: float, - void_fraction: float = 0.725, - cos_theta: float = 0.97, + params: ParameterFrameLike, name: str = "SquareCable", ): """ @@ -1175,14 +1075,11 @@ def __init__( super().__init__( sc_strand=sc_strand, stab_strand=stab_strand, - n_sc_strand=n_sc_strand, - n_stab_strand=n_stab_strand, - d_cooling_channel=d_cooling_channel, - void_fraction=void_fraction, - cos_theta=cos_theta, + params=params, name=name, ) + # replace dx and dy with dl? @property def dx(self): """Cable dimension in the x direction [m]""" @@ -1281,6 +1178,7 @@ def from_dict( sc_strand = create_strand_from_dict(strand_dict=cable_dict["sc_strand"]) stab_strand = create_strand_from_dict(strand_dict=cable_dict["stab_strand"]) + # how to handle this? return cls( sc_strand=sc_strand, stab_strand=stab_strand, @@ -1360,16 +1258,13 @@ class RoundCable(ABCCable): """ _name_in_registry_ = "RoundCable" + param_cls: type[CableParams] = CableParams def __init__( self, sc_strand: SuperconductingStrand, stab_strand: Strand, - n_sc_strand: int, - n_stab_strand: int, - d_cooling_channel: float, - void_fraction: float = 0.725, - cos_theta: float = 0.97, + params: ParameterFrameLike, name: str = "RoundCable", ): """ @@ -1397,14 +1292,11 @@ def __init__( super().__init__( sc_strand=sc_strand, stab_strand=stab_strand, - n_sc_strand=n_sc_strand, - n_stab_strand=n_stab_strand, - d_cooling_channel=d_cooling_channel, - void_fraction=void_fraction, - cos_theta=cos_theta, + params=params, name=name, ) + # replace dx and dy with dr? @property def dx(self): """Cable dimension in the x direction [m] (i.e. cable's diameter)""" @@ -1497,7 +1389,9 @@ def plot(self, xc: float = 0, yc: float = 0, *, show: bool = False, ax=None): points_cc = ( np.array([ - np.array([np.cos(theta), np.sin(theta)]) * self.d_cooling_channel / 2 + np.array([np.cos(theta), np.sin(theta)]) + * self.params.d_cooling_channel.value + / 2 for theta in np.linspace(0, np.radians(360), 19) ]) + pc @@ -1563,6 +1457,7 @@ def from_dict( sc_strand = create_strand_from_dict(strand_dict=cable_dict["sc_strand"]) stab_strand = create_strand_from_dict(strand_dict=cable_dict["stab_strand"]) + # how to handle? return cls( sc_strand=sc_strand, stab_strand=stab_strand, diff --git a/bluemira/magnets/case_tf.py b/bluemira/magnets/case_tf.py index d22db0812c..99ddd64395 100644 --- a/bluemira/magnets/case_tf.py +++ b/bluemira/magnets/case_tf.py @@ -356,7 +356,7 @@ def __init__( String identifier for the TF coil case instance (default is "BaseCaseTF"). """ super().__init__( - params, + params=params, mat_case=mat_case, WPs=WPs, name=name, From 67631c7dd2378ed7a14e7b94303022330c9af9b3 Mon Sep 17 00:00:00 2001 From: Jonathan Matthews Date: Thu, 21 Aug 2025 11:12:36 +0100 Subject: [PATCH 03/61] addition of ParaterFrame dataclasses for classes in conductor.py --- bluemira/magnets/cable.py | 2 +- bluemira/magnets/conductor.py | 191 +++++++++++++++++----------------- 2 files changed, 96 insertions(+), 97 deletions(-) diff --git a/bluemira/magnets/cable.py b/bluemira/magnets/cable.py index 0c1f6d87db..9d9f86ac0c 100644 --- a/bluemira/magnets/cable.py +++ b/bluemira/magnets/cable.py @@ -446,7 +446,7 @@ def final_temperature_difference( f"Final temperature ({final_temperature:.2f} K) exceeds target " f"temperature " f"({target_temperature} K) even with maximum n_stab = " - f"{self.n_stab_strand}." + f"{self.params.n_stab_strand.value}." ) raise ValueError( "Optimization failed to keep final temperature ≤ target. " diff --git a/bluemira/magnets/conductor.py b/bluemira/magnets/conductor.py index 6a7cfa5f70..ff26328c8f 100644 --- a/bluemira/magnets/conductor.py +++ b/bluemira/magnets/conductor.py @@ -6,6 +6,7 @@ """Conductor class""" +from dataclasses import dataclass from typing import Any import matplotlib.pyplot as plt @@ -15,6 +16,8 @@ from scipy.optimize import minimize_scalar from bluemira.base.look_and_feel import bluemira_debug +from bluemira.base.parameter_frame import Parameter, ParameterFrame +from bluemira.base.parameter_frame.typed import ParameterFrameLike from bluemira.magnets.cable import ABCCable, create_cable_from_dict from bluemira.magnets.registry import RegistrableMeta from bluemira.magnets.utils import ( @@ -34,6 +37,20 @@ # ------------------------------------------------------------------------------ # Strand Class # ------------------------------------------------------------------------------ +@dataclass +class ConductorParams(ParameterFrame): + """ + Parameters needed for the conductor + """ + + dx_jacket: Parameter[float] + """x-thickness of the jacket [m].""" + dy_jacket: Parameter[float] + """y-tickness of the jacket [m].""" + dx_ins: Parameter[float] + """x-thickness of the insulator [m].""" + dy_ins: Parameter[float] + """y-thickness of the insulator [m].""" class Conductor(metaclass=RegistrableMeta): @@ -50,10 +67,7 @@ def __init__( cable: ABCCable, mat_jacket: Material, mat_ins: Material, - dx_jacket: float, - dy_jacket: float, - dx_ins: float, - dy_ins: float, + params: ParameterFrameLike, name: str = "Conductor", ): """ @@ -80,10 +94,7 @@ def __init__( string identifier """ self.name = name - self._dx_jacket = dx_jacket - self._dy_jacket = dy_jacket - self._dy_ins = dy_ins - self._dx_ins = dx_ins + self.params = params self.mat_ins = mat_ins self.mat_jacket = mat_jacket self.cable = cable @@ -91,59 +102,33 @@ def __init__( @property def dx(self): """x-dimension of the conductor [m]""" - return self.dx_ins * 2 + self.dx_jacket * 2 + self.cable.dx + return ( + self.params.dx_ins.value * 2 + + self.params.dx_jacket.value * 2 + + self.cable.dx + ) @property def dy(self): """y-dimension of the conductor [m]""" - return self.dy_ins * 2 + self.dy_jacket * 2 + self.cable.dy - - @property - def dx_jacket(self): - """Thickness in the x-direction of the jacket [m]""" - return self._dx_jacket - - @dx_jacket.setter - def dx_jacket(self, value): - self._dx_jacket = value - - @property - def dy_jacket(self): - """Thickness in the y-direction of the jacket [m]""" - return self._dy_jacket - - @dy_jacket.setter - def dy_jacket(self, value): - self._dy_jacket = value - - @property - def dx_ins(self): - """Thickness in the x-direction of the insulator [m]""" - return self._dx_ins - - @dx_ins.setter - def dx_ins(self, value): - self._dx_ins = value - - @property - def dy_ins(self): - """Thickness in the y-direction of the jacket [m]""" - return self._dy_ins - - @dy_ins.setter - def dy_ins(self, value): - self._dy_ins = value + return ( + self.params.dy_ins.value * 2 + + self.params.dy_jacket.value * 2 + + self.cable.dy + ) @property def area(self): """Area of the conductor [m^2]""" return self.dx * self.dy + # surely this should be done from inside out ie cable dx + jacket dx + # rather than out in and depend on more variables? @property def area_jacket(self): """Area of the jacket [m^2]""" - return (self.dx - 2 * self.dx_ins) * ( - self.dy - 2 * self.dy_ins + return (self.dx - 2 * self.params.dx_ins.value) * ( + self.dy - 2 * self.params.dy_ins.value ) - self.cable.area @property @@ -172,12 +157,12 @@ def to_dict(self) -> dict: ), "name": self.name, "cable": self.cable.to_dict(), - "mat_jacket": self.mat_jacket, - "mat_ins": self.mat_ins, - "dx_jacket": self.dx_jacket, - "dy_jacket": self.dy_jacket, - "dx_ins": self.dx_ins, - "dy_ins": self.dy_ins, + "mat_jacket": self.mat_jacket.name, + "mat_ins": self.mat_ins.name, + "dx_jacket": self.params.dx_jacket.value, + "dy_jacket": self.params.dy_jacket.value, + "dx_ins": self.params.dx_ins.value, + "dy_ins": self.params.dy_ins.value, } @classmethod @@ -310,7 +295,7 @@ def _Kx_topbot_ins(self, op_cond: OperationalConditions): # noqa: N802 float Axial stiffness [N/m] """ - return self._mat_ins_y_modulus(op_cond) * self.cable.dy / self.dx_ins + return self._mat_ins_y_modulus(op_cond) * self.cable.dy / self.params.dx_ins.value def _Kx_lat_ins(self, op_cond: OperationalConditions): # noqa: N802 """ @@ -321,7 +306,7 @@ def _Kx_lat_ins(self, op_cond: OperationalConditions): # noqa: N802 float Axial stiffness [N/m] """ - return self._mat_ins_y_modulus(op_cond) * self.dy_ins / self.dx + return self._mat_ins_y_modulus(op_cond) * self.params.dy_ins.value / self.dx def _Kx_lat_jacket(self, op_cond: OperationalConditions): # noqa: N802 """ @@ -334,8 +319,8 @@ def _Kx_lat_jacket(self, op_cond: OperationalConditions): # noqa: N802 """ return ( self._mat_jacket_y_modulus(op_cond) - * self.dy_jacket - / (self.dx - 2 * self.dx_ins) + * self.params.dy_jacket.value + / (self.dx - 2 * self.params.dx_ins.value) ) def _Kx_topbot_jacket(self, op_cond: OperationalConditions): # noqa: N802 @@ -347,7 +332,7 @@ def _Kx_topbot_jacket(self, op_cond: OperationalConditions): # noqa: N802 float Axial stiffness [N/m] """ - return self._mat_jacket_y_modulus(op_cond) * self.cable.dy / self.dx_jacket + return self._mat_jacket_y_modulus(op_cond) * self.cable.dy / self.params.dx_jacket.value def _Kx_cable(self, op_cond: OperationalConditions): # noqa: N802 """ @@ -392,7 +377,7 @@ def _Ky_topbot_ins(self, op_cond: OperationalConditions): # noqa: N802 float Axial stiffness [N/m] """ - return self._mat_ins_y_modulus(op_cond) * self.cable.dx / self.dy_ins + return self._mat_ins_y_modulus(op_cond) * self.cable.dx / self.params.dy_ins.value def _Ky_lat_ins(self, op_cond: OperationalConditions): # noqa: N802 """ @@ -403,7 +388,7 @@ def _Ky_lat_ins(self, op_cond: OperationalConditions): # noqa: N802 float Axial stiffness [N/m] """ - return self._mat_ins_y_modulus(op_cond) * self.dx_ins / self.dy + return self._mat_ins_y_modulus(op_cond) * self.params.dx_ins.value / self.dy def _Ky_lat_jacket(self, op_cond: OperationalConditions): # noqa: N802 """ @@ -416,8 +401,8 @@ def _Ky_lat_jacket(self, op_cond: OperationalConditions): # noqa: N802 """ return ( self._mat_jacket_y_modulus(op_cond) - * self.dx_jacket - / (self.dy - 2 * self.dy_ins) + * self.params.dx_jacket.value + / (self.dy - 2 * self.params.dy_ins.value) ) def _Ky_topbot_jacket(self, op_cond: OperationalConditions): # noqa: N802 @@ -429,7 +414,7 @@ def _Ky_topbot_jacket(self, op_cond: OperationalConditions): # noqa: N802 float Axial stiffness [N/m] """ - return self._mat_jacket_y_modulus(op_cond) * self.cable.dx / self.dy_jacket + return self._mat_jacket_y_modulus(op_cond) * self.cable.dx / self.params.dy_jacket.value def _Ky_cable(self, op_cond: OperationalConditions): # noqa: N802 """ @@ -506,7 +491,9 @@ def _tresca_sigma_jacket( raise ValueError("Invalid direction: choose either 'x' or 'y'.") if direction == "x": - saf_jacket = (self.cable.dx + 2 * self.dx_jacket) / (2 * self.dx_jacket) + saf_jacket = (self.cable.dx + 2 * self.params.dx_jacket.value) / ( + 2 * self.params.dx_jacket.value + ) K = parall_k([ # noqa: N806 2 * self._Ky_lat_ins(op_cond), @@ -520,7 +507,9 @@ def _tresca_sigma_jacket( X_jacket = 2 * self._Ky_lat_jacket(op_cond) / K # noqa: N806 else: - saf_jacket = (self.cable.dy + 2 * self.dy_jacket) / (2 * self.dy_jacket) + saf_jacket = (self.cable.dy + 2 * self.params.dy_jacket.value) / ( + 2 * self.params.dy_jacket.value + ) K = parall_k([ # noqa: N806 2 * self._Kx_lat_ins(op_cond), @@ -644,9 +633,9 @@ def sigma_difference( raise ValueError("Invalid direction: choose either 'x' or 'y'.") if direction == "x": - self.dx_jacket = jacket_thickness + self.params.dx_jacket.value = jacket_thickness else: - self.dy_jacket = jacket_thickness + self.params.dy_jacket.value = jacket_thickness sigma_r = self._tresca_sigma_jacket(pressure, fz, op_cond, direction) @@ -663,9 +652,9 @@ def sigma_difference( debug_msg = ["Method optimize_jacket_conductor:"] if direction == "x": - debug_msg.append(f"Previous dx_jacket: {self.dx_jacket}") + debug_msg.append(f"Previous dx_jacket: {self.params.dx_jacket.value}") else: - debug_msg.append(f"Previous dy_jacket: {self.dy_jacket}") + debug_msg.append(f"Previous dy_jacket: {self.params.dy_jacket.value}") method = "bounded" if bounds is not None else None @@ -683,11 +672,11 @@ def sigma_difference( if not result.success: raise ValueError("Optimization of the jacket conductor did not converge.") if direction == "x": - self.dx_jacket = result.x - debug_msg.append(f"Optimal dx_jacket: {self.dx_jacket}") + self.params.dx_jacket.value = result.x + debug_msg.append(f"Optimal dx_jacket: {self.params.dx_jacket.value}") else: - self.dy_jacket = result.x - debug_msg.append(f"Optimal dy_jacket: {self.dy_jacket}") + self.params.dy_jacket.value = result.x + debug_msg.append(f"Optimal dy_jacket: {self.params.dy_jacket.value}") debug_msg.append( f"Averaged sigma in the {direction}-direction: " f"{self._tresca_sigma_jacket(pressure, f_z, op_cond) / 1e6} MPa\n" @@ -741,8 +730,8 @@ def plot(self, xc: float = 0, yc: float = 0, *, show: bool = False, ax=None): _, ax = plt.subplots() pc = np.array([xc, yc]) - a = self.cable.dx / 2 + self.dx_jacket - b = self.cable.dy / 2 + self.dy_jacket + a = self.cable.dx / 2 + self.params.dx_jacket.value + b = self.cable.dy / 2 + self.params.dy_jacket.value p0 = np.array([-a, -b]) p1 = np.array([a, -b]) @@ -750,8 +739,8 @@ def plot(self, xc: float = 0, yc: float = 0, *, show: bool = False, ax=None): p3 = np.array([-a, b]) points_ext_jacket = np.vstack((p0, p1, p2, p3, p0)) + pc - c = a + self.dx_ins - d = b + self.dy_ins + c = a + self.params.dx_ins.value + d = b + self.params.dy_ins.value p0 = np.array([-c, -d]) p1 = np.array([c, -d]) @@ -790,13 +779,27 @@ def __str__(self): f"------- cable -------\n" f"cable: {self.cable!s}\n" f"---------------------\n" - f"dx_jacket: {self.dx_jacket}\n" - f"dy_jacket: {self.dy_jacket}\n" - f"dx_ins: {self.dx_ins}\n" - f"dy_ins: {self.dy_ins}" + f"dx_jacket: {self.params.dx_jacket.value}\n" + f"dy_jacket: {self.params.dy_jacket.value}\n" + f"dx_ins: {self.params.dx_ins.value}\n" + f"dy_ins: {self.params.dy_ins.value}" ) +@dataclass +class SymmetricConductorParams(ParameterFrame): + """ + Parameters needed for the symmetric conductor + + Just use dl? instead of a different dx and dy that are equal? + """ + + dx_jacket: Parameter[float] + """x-thickness of the jacket [m].""" + dx_ins: Parameter[float] + """x-thickness of the insulator [m].""" + + class SymmetricConductor(Conductor): """ Representation of a symmetric conductor in which both jacket and insulator @@ -810,8 +813,7 @@ def __init__( cable: ABCCable, mat_jacket: Material, mat_ins: Material, - dx_jacket: float, - dx_ins: float, + params: ParameterFrameLike, name: str = "SymmetricConductor", ): """ @@ -834,18 +836,15 @@ def __init__( string identifier """ - dy_jacket = dx_jacket - dy_ins = dx_ins super().__init__( cable=cable, mat_jacket=mat_jacket, mat_ins=mat_ins, - dx_jacket=dx_jacket, - dy_jacket=dy_jacket, - dx_ins=dx_ins, - dy_ins=dy_ins, + params=params, name=name, ) + self.dy_jacket = self.params.dx_jacket.value # needed or just property? + self.dy_ins = self.params.dx_ins.value # needed or just property? @property def dy_jacket(self): @@ -860,7 +859,7 @@ def dy_jacket(self): ----- Assumes the same value as `dx_jacket`, ensuring symmetry in both directions. """ - return self.dx_jacket + return self.params.dx_jacket.value @property def dy_ins(self): @@ -875,7 +874,7 @@ def dy_ins(self): ----- Assumes the same value as `dx_ins`, ensuring symmetry in both directions. """ - return self.dx_ins + return self.params.dx_ins.value def to_dict(self) -> dict: """ @@ -892,10 +891,10 @@ def to_dict(self) -> dict: ), "name": self.name, "cable": self.cable.to_dict(), - "mat_jacket": self.mat_jacket, - "mat_ins": self.mat_ins, - "dx_jacket": self.dx_jacket, - "dx_ins": self.dx_ins, + "mat_jacket": self.mat_jacket.name, + "mat_ins": self.mat_ins.name, + "dx_jacket": self.params.dx_jacket.value, + "dx_ins": self.params.dx_ins.value, } @classmethod From 92f5e8fb01558933d31feae493913741fb1ce789 Mon Sep 17 00:00:00 2001 From: Jonathan Matthews Date: Thu, 21 Aug 2025 11:28:29 +0100 Subject: [PATCH 04/61] addition of ParaterFrame dataclasses for classes in fatigue.py --- bluemira/magnets/conductor.py | 2 ++ bluemira/magnets/fatigue.py | 63 ++++++++++++++++++++++------------- 2 files changed, 41 insertions(+), 24 deletions(-) diff --git a/bluemira/magnets/conductor.py b/bluemira/magnets/conductor.py index ff26328c8f..859ab647e7 100644 --- a/bluemira/magnets/conductor.py +++ b/bluemira/magnets/conductor.py @@ -61,6 +61,7 @@ class Conductor(metaclass=RegistrableMeta): _registry_ = CONDUCTOR_REGISTRY _name_in_registry_ = "Conductor" + param_cls: type[ConductorParams] = ConductorParams def __init__( self, @@ -807,6 +808,7 @@ class SymmetricConductor(Conductor): """ _name_in_registry_ = "SymmetricConductor" + param_cls: type[SymmetricConductorParams] = SymmetricConductorParams def __init__( self, diff --git a/bluemira/magnets/fatigue.py b/bluemira/magnets/fatigue.py index 3962f31031..eed1761d40 100644 --- a/bluemira/magnets/fatigue.py +++ b/bluemira/magnets/fatigue.py @@ -13,6 +13,9 @@ import numpy as np +from bluemira.base.parameter_frame import Parameter, ParameterFrame +from bluemira.base.parameter_frame.typed import ParameterFrameLike + __all__ = [ "ConductorInfo", "EllipticalEmbeddedCrack", @@ -25,39 +28,51 @@ @dataclass -class ConductorInfo: +class ConductorInfo(ParameterFrame): """ Cable in conduit conductor information for Paris fatigue model """ - tk_radial: float # [m] in the loaded direction - width: float # [m] in the loaded direction - max_hoop_stress: float # [Pa] - residual_stress: float # [Pa] - walker_coeff: float + tk_radial: Parameter[float] # [m] in the loaded direction + width: Parameter[float] # [m] in the loaded direction + max_hoop_stress: Parameter[float] # [Pa] + residual_stress: Parameter[float] # [Pa] + walker_coeff: Parameter[float] @dataclass -class ParisFatigueMaterial: +class ParisFatigueMaterial(ParameterFrame): """ Material properties for the Paris fatigue model """ - C: float # Paris law material constant - m: float # Paris law material exponent - K_ic: float # Fracture toughness [Pa/m^(1/2)] + C: Parameter[float] # Paris law material constant + m: Parameter[float] # Paris law material exponent + K_ic: Parameter[float] # Fracture toughness [Pa/m^(1/2)] @dataclass -class ParisFatigueSafetyFactors: +class ParisFatigueSafetyFactors(ParameterFrame): """ Safety factors for the Paris fatigue model """ - sf_n_cycle: float - sf_depth_crack: float - sf_width_crack: float - sf_fracture: float + sf_n_cycle: Parameter[float] + sf_depth_crack: Parameter[float] + sf_width_crack: Parameter[float] + sf_fracture: Parameter[float] + + +@dataclass +class CrackParams(ParameterFrame): + """ + Parameters for the crack class + """ + + width: Parameter[float] + """Crack width along the plate length direction""" + depth: Parameter[float] + """Crack depth in the plate thickness direction""" def _stress_intensity_factor( @@ -156,10 +171,10 @@ class Crack(abc.ABC): """ alpha = None + param_cls: type[CrackParams] = CrackParams - def __init__(self, depth: float, width: float): - self.depth = depth # a - self.width = width # c + def __init__(self, params: ParameterFrameLike): + self.params = params @classmethod def from_area(cls, area: float, aspect_ratio: float): @@ -171,9 +186,9 @@ def from_area(cls, area: float, aspect_ratio: float): Crack New instance of the crack geometry. """ - depth = np.sqrt(area / (cls.alpha * np.pi * aspect_ratio)) - width = aspect_ratio * depth - return cls(depth, width) + cls.params.depth.value = np.sqrt(area / (cls.alpha * np.pi * aspect_ratio)) + cls.params.width.value = aspect_ratio * cls.params.depth.value + return cls(cls.params.depth.value, cls.params.width.value) @property def area(self) -> float: @@ -185,7 +200,7 @@ def area(self) -> float: float Area [m²]. """ - return self.alpha * np.pi * self.depth * self.width + return self.alpha * np.pi * self.params.depth.value * self.params.width.value @abc.abstractmethod def stress_intensity_factor( @@ -514,8 +529,8 @@ def calculate_n_pulses( max_crack_width = conductor.width / safety.sf_width_crack max_stress_intensity = material.K_ic / safety.sf_fracture - a = crack.depth - c = crack.width + a = crack.params.depth.value + c = crack.params.width.value K_max = 0.0 # noqa: N806 n_cycles = 0 From 77d0ab82899975f1d2dc6700b7cbeafc577a5b58 Mon Sep 17 00:00:00 2001 From: Jonathan Matthews Date: Thu, 21 Aug 2025 13:18:50 +0100 Subject: [PATCH 05/61] addition of ParaterFrame dataclasses for classes in strand.py --- bluemira/magnets/strand.py | 120 ++++++++----------------------------- 1 file changed, 26 insertions(+), 94 deletions(-) diff --git a/bluemira/magnets/strand.py b/bluemira/magnets/strand.py index 8086f665aa..eb7da8378d 100644 --- a/bluemira/magnets/strand.py +++ b/bluemira/magnets/strand.py @@ -12,6 +12,7 @@ - Automatic class and instance registration mechanisms """ +from dataclasses import dataclass from typing import Any import matplotlib.pyplot as plt @@ -21,6 +22,8 @@ from bluemira import display from bluemira.base.look_and_feel import bluemira_error +from bluemira.base.parameter_frame import Parameter, ParameterFrame +from bluemira.base.parameter_frame.typed import ParameterFrameLike from bluemira.display.plotter import PlotOptions from bluemira.geometry.face import BluemiraFace from bluemira.geometry.tools import make_circle @@ -32,9 +35,20 @@ STRAND_REGISTRY = {} + # ------------------------------------------------------------------------------ # Strand Class # ------------------------------------------------------------------------------ +@dataclass +class StrandParams(ParameterFrame): + """ + Parameters needed for the strand + """ + + d_strand: Parameter[float] = 0.82e-3 + """Strand diameter in meters (default 0.82e-3).""" + temperature: Parameter[float] | None = None + """Operating temperature [K].""" class Strand(metaclass=RegistrableMeta): @@ -47,12 +61,12 @@ class Strand(metaclass=RegistrableMeta): _registry_ = STRAND_REGISTRY _name_in_registry_ = "Strand" + param_cls: type[StrandParams] = StrandParams def __init__( self, materials: list[MaterialFraction], - d_strand: float = 0.82e-3, - temperature: float | None = None, + params: ParameterFrameLike, name: str | None = "Strand", ): """ @@ -69,15 +83,12 @@ def __init__( name : str or None, optional Name of the strand. Defaults to "Strand". """ - self._d_strand = None + self.params = params self._shape = None self._materials = None - self._temperature = None - self.d_strand = d_strand self.materials = materials self.name = name - self.temperature = temperature # Create homogenised material self._homogenised_material = mixture( @@ -127,84 +138,6 @@ def materials(self, new_materials: list): self._materials = new_materials - @property - def temperature(self) -> float | None: - """ - Operating temperature of the strand. - - Returns - ------- - float or None - Temperature in Kelvin. - """ - return self._temperature - - @temperature.setter - def temperature(self, value: float | None): - """ - Set a new operating temperature for the strand. - - Parameters - ---------- - value : float or None - New operating temperature in Kelvin. - - Raises - ------ - ValueError - If temperature is negative. - TypeError - If temperature is not a float or None. - """ - if value is not None: - if not isinstance(value, (float, int)): - raise TypeError( - f"temperature must be a float or int, got {type(value).__name__}." - ) - - if value < 0: - raise ValueError("Temperature cannot be negative.") - - self._temperature = float(value) if value is not None else None - - @property - def d_strand(self) -> float: - """ - Diameter of the strand. - - Returns - ------- - Parameter - Diameter [m]. - """ - return self._d_strand - - @d_strand.setter - def d_strand(self, d: float): - """ - Set the strand diameter and reset shape if changed. - - Parameters - ---------- - d : float or Parameter - New strand diameter. - - Raises - ------ - ValueError - If diameter is non-positive. - TypeError - If diameter is not a float number. - """ - if not isinstance(d, (float, int)): - raise TypeError(f"d_strand must be a float, got {type(d).__name__}") - if d <= 0: - raise ValueError("d_strand must be positive.") - - if self.d_strand is None or d != self.d_strand: - self._d_strand = float(d) - self._shape = None - @property def area(self) -> float: """ @@ -215,7 +148,7 @@ def area(self) -> float: float Area [m²]. """ - return np.pi * (self.d_strand**2) / 4 + return np.pi * (self.params.d_strand.value**2) / 4 @property def shape(self) -> BluemiraFace: @@ -228,7 +161,7 @@ def shape(self) -> BluemiraFace: Circular face of the strand. """ if self._shape is None: - self._shape = BluemiraFace([make_circle(self.d_strand)]) + self._shape = BluemiraFace([make_circle(self.params.d_strand.value)]) return self._shape def E(self, op_cond: OperationalConditions) -> float: # noqa: N802 @@ -355,7 +288,7 @@ def __str__(self) -> str: """ return ( f"name = {self.name}\n" - f"d_strand = {self.d_strand}\n" + f"d_strand = {self.params.d_strand.value}\n" f"materials = {self.materials}\n" f"shape = {self.shape}\n" ) @@ -374,8 +307,8 @@ def to_dict(self) -> dict: self, "_name_in_registry_", self.__class__.__name__ ), "name": self.name, - "d_strand": self.d_strand, - "temperature": self.temperature, + "d_strand": self.params.d_strand.value, + "temperature": self.params.temperature.value, "materials": [ { "material": m.material, @@ -442,7 +375,7 @@ class registration name. material_mix.append( MaterialFraction(material=material_obj, fraction=m["fraction"]) ) - + # resolve return cls( materials=material_mix, temperature=strand_dict.get("temperature"), @@ -467,12 +400,12 @@ class SuperconductingStrand(Strand): """ _name_in_registry_ = "SuperconductingStrand" + param_cls: type[StrandParams] = StrandParams def __init__( self, materials: list[MaterialFraction], - d_strand: float = 0.82e-3, - temperature: float | None = None, + params: ParameterFrameLike, name: str | None = "SuperconductingStrand", ): """ @@ -492,8 +425,7 @@ def __init__( """ super().__init__( materials=materials, - d_strand=d_strand, - temperature=temperature, + params=params, name=name, ) self._sc = self._check_materials() From 7210fbd4e4142b2788ff663dd202905cbd17144c Mon Sep 17 00:00:00 2001 From: Jonathan Matthews Date: Thu, 21 Aug 2025 13:37:46 +0100 Subject: [PATCH 06/61] reduced duplication in utils.py by replacing specific serie_r, serie_k, parallel_r, parallel_k with general summation and reciprocal_summation functions --- bluemira/magnets/utils.py | 56 +++++++++------------------------------ 1 file changed, 12 insertions(+), 44 deletions(-) diff --git a/bluemira/magnets/utils.py b/bluemira/magnets/utils.py index 597dd5cb3c..8eec3983a3 100644 --- a/bluemira/magnets/utils.py +++ b/bluemira/magnets/utils.py @@ -9,9 +9,9 @@ import numpy as np -def serie_r(arr: list | np.ndarray): +def summation(arr: list | np.ndarray): """ - Compute the serie (as for resistance) + Compute the simple summation of the series Parameters ---------- @@ -22,65 +22,33 @@ def serie_r(arr: list | np.ndarray): Returns ------- Result: float - """ - return np.sum(arr) + i.e. -def parall_r(arr: list | np.ndarray): + Y = sum(x1 + x2 + x3 ...) """ - Compute the parallel (as for resistance) - - Parameters - ---------- - arr: - list or numpy array containing the elements on which the parallel - shall be calculated - - Returns - ------- - Result: float - """ - out = 0 - for i in range(len(arr)): - out += 1 / arr[i] - return out**-1 + return np.sum(arr) -def serie_k(arr: list | np.ndarray): +def reciprocal_summation(arr: list | np.ndarray): """ - Compute the serie (as for spring) + Compute the inverse of the summation of a reciprocal series Parameters ---------- arr: - list or numpy array containing the elements on which the serie - shall be calculated + list or numpy array containing the elements on which the serie shall + be calculated Returns ------- Result: float - """ - out = 0 - for i in range(len(arr)): - out += 1 / arr[i] - return out**-1 - -def parall_k(arr: list | np.ndarray): - """ - Compute the parallel (as for spring) - - Parameters - ---------- - arr: - list or numpy array containing the elements on which the parallel - shall be calculated + i.e. - Returns - ------- - Result: float + Y = [sum(1/x1 + 1/x2 + 1/x3 ...)]^-1 """ - return np.sum(arr) + return (np.sum((1 / element) for element in arr)) ** -1 def delayed_exp_func(x0: float, tau: float, t_delay: float = 0): From 92e54375f2a99c9e529fb8f6de73f8a02a830362 Mon Sep 17 00:00:00 2001 From: Jonathan Matthews Date: Thu, 21 Aug 2025 13:39:13 +0100 Subject: [PATCH 07/61] replaced old summation functions with new general summations in cable case_tf and conductor files --- bluemira/magnets/cable.py | 6 +++--- bluemira/magnets/case_tf.py | 10 +++++----- bluemira/magnets/conductor.py | 27 +++++++++++---------------- 3 files changed, 19 insertions(+), 24 deletions(-) diff --git a/bluemira/magnets/cable.py b/bluemira/magnets/cable.py index 9d9f86ac0c..44d5b44ee6 100644 --- a/bluemira/magnets/cable.py +++ b/bluemira/magnets/cable.py @@ -26,7 +26,7 @@ SuperconductingStrand, create_strand_from_dict, ) -from bluemira.magnets.utils import parall_r, serie_r +from bluemira.magnets.utils import reciprocal_summation, summation # ------------------------------------------------------------------------------ # Global Registries @@ -172,7 +172,7 @@ def erho(self, op_cond: OperationalConditions): self.sc_strand.erho(op_cond) / self.area_sc, self.stab_strand.erho(op_cond) / self.area_stab, ]) - res_tot = parall_r(resistances) + res_tot = reciprocal_summation(resistances) return res_tot * self.area def Cp(self, op_cond: OperationalConditions): # noqa: N802 @@ -196,7 +196,7 @@ def Cp(self, op_cond: OperationalConditions): # noqa: N802 * self.area_stab * self.stab_strand.rho(op_cond), ]) - return serie_r(weighted_specific_heat) / ( + return summation(weighted_specific_heat) / ( self.area_sc * self.sc_strand.rho(op_cond) + self.area_stab * self.stab_strand.rho(op_cond) ) diff --git a/bluemira/magnets/case_tf.py b/bluemira/magnets/case_tf.py index 99ddd64395..41a2b203da 100644 --- a/bluemira/magnets/case_tf.py +++ b/bluemira/magnets/case_tf.py @@ -35,7 +35,7 @@ from bluemira.base.parameter_frame import Parameter, ParameterFrame from bluemira.base.parameter_frame.typed import ParameterFrameLike from bluemira.magnets.registry import RegistrableMeta -from bluemira.magnets.utils import parall_k, serie_k +from bluemira.magnets.utils import reciprocal_summation, summation from bluemira.magnets.winding_pack import WindingPack, create_wp_from_dict # ------------------------------------------------------------------------------ @@ -1032,14 +1032,14 @@ def Kx(self, op_cond: OperationalConditions): # noqa: N802 Total equivalent radial stiffness of the TF case [Pa]. """ temp = [ - serie_k([ + reciprocal_summation([ self.Kx_lat(op_cond)[i], w.Kx(op_cond), self.Kx_lat(op_cond)[i], ]) for i, w in enumerate(self.WPs) ] - return parall_k([self.Kx_ps(op_cond), self.Kx_vault(op_cond), *temp]) + return summation([self.Kx_ps(op_cond), self.Kx_vault(op_cond), *temp]) def Ky_ps(self, op_cond: OperationalConditions): # noqa: N802 """ @@ -1122,14 +1122,14 @@ def Ky(self, op_cond: OperationalConditions): # noqa: N802 Total equivalent toroidal stiffness of the TF case [Pa]. """ temp = [ - parall_k([ + summation([ self.Ky_lat(op_cond)[i], w.Ky(op_cond), self.Ky_lat(op_cond)[i], ]) for i, w in enumerate(self.WPs) ] - return serie_k([self.Ky_ps(op_cond), self.Ky_vault(op_cond), *temp]) + return reciprocal_summation([self.Ky_ps(op_cond), self.Ky_vault(op_cond), *temp]) def rearrange_conductors_in_wp( self, diff --git a/bluemira/magnets/conductor.py b/bluemira/magnets/conductor.py index 859ab647e7..1bcef76b54 100644 --- a/bluemira/magnets/conductor.py +++ b/bluemira/magnets/conductor.py @@ -20,12 +20,7 @@ from bluemira.base.parameter_frame.typed import ParameterFrameLike from bluemira.magnets.cable import ABCCable, create_cable_from_dict from bluemira.magnets.registry import RegistrableMeta -from bluemira.magnets.utils import ( - parall_k, - parall_r, - serie_k, - serie_r, -) +from bluemira.magnets.utils import reciprocal_summation, summation # ------------------------------------------------------------------------------ # Global Registries @@ -253,7 +248,7 @@ def erho(self, op_cond: OperationalConditions): self.cable.erho(op_cond) / self.cable.area, self.mat_jacket.electrical_resistivity(op_cond) / self.area_jacket, ]) - res_tot = parall_r(resistances) + res_tot = reciprocal_summation(resistances) return res_tot * self.area def Cp(self, op_cond: OperationalConditions): # noqa: N802 @@ -279,7 +274,7 @@ def Cp(self, op_cond: OperationalConditions): # noqa: N802 self.cable.Cp(op_cond) * self.cable.area, self.mat_jacket.specific_heat_capacity(op_cond) * self.area_jacket, ]) - return serie_r(weighted_specific_heat) / self.area + return summation(weighted_specific_heat) / self.area def _mat_ins_y_modulus(self, op_cond: OperationalConditions): return self.mat_ins.youngs_modulus(op_cond) @@ -355,10 +350,10 @@ def Kx(self, op_cond: OperationalConditions): # noqa: N802 float Axial stiffness [N/m] """ - return parall_k([ + return summation([ self._Kx_lat_ins(op_cond), self._Kx_lat_jacket(op_cond), - serie_k([ + reciprocal_summation([ self._Kx_topbot_ins(op_cond), self._Kx_topbot_jacket(op_cond), self._Kx_cable(op_cond), @@ -437,10 +432,10 @@ def Ky(self, op_cond: OperationalConditions): # noqa: N802 float Axial stiffness [N/m] """ - return parall_k([ + return summation([ self._Ky_lat_ins(op_cond), self._Ky_lat_jacket(op_cond), - serie_k([ + reciprocal_summation([ self._Ky_topbot_ins(op_cond), self._Ky_topbot_jacket(op_cond), self._Ky_cable(op_cond), @@ -496,10 +491,10 @@ def _tresca_sigma_jacket( 2 * self.params.dx_jacket.value ) - K = parall_k([ # noqa: N806 + K = summation([ # noqa: N806 2 * self._Ky_lat_ins(op_cond), 2 * self._Ky_lat_jacket(op_cond), - serie_k([ + reciprocal_summation([ self._Ky_cable(op_cond), self._Ky_topbot_jacket(op_cond) / 2, ]), @@ -512,10 +507,10 @@ def _tresca_sigma_jacket( 2 * self.params.dy_jacket.value ) - K = parall_k([ # noqa: N806 + K = summation([ # noqa: N806 2 * self._Kx_lat_ins(op_cond), 2 * self._Kx_lat_jacket(op_cond), - serie_k([ + reciprocal_summation([ self._Kx_cable(op_cond), self._Kx_topbot_jacket(op_cond) / 2, ]), From 003e780ecfdb87f285893ffd56993ff6a24c0464 Mon Sep 17 00:00:00 2001 From: Jonathan Matthews Date: Thu, 21 Aug 2025 13:43:48 +0100 Subject: [PATCH 08/61] addition of ParaterFrame dataclasses for classes in winding_pack.py --- bluemira/magnets/winding_pack.py | 39 ++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/bluemira/magnets/winding_pack.py b/bluemira/magnets/winding_pack.py index 441075787e..1a534fd3fb 100644 --- a/bluemira/magnets/winding_pack.py +++ b/bluemira/magnets/winding_pack.py @@ -6,12 +6,15 @@ """Winding pack module""" +from dataclasses import dataclass from typing import Any, ClassVar import matplotlib.pyplot as plt import numpy as np from matproplib import OperationalConditions +from bluemira.base.parameter_frame import Parameter, ParameterFrame +from bluemira.base.parameter_frame.typed import ParameterFrameLike from bluemira.magnets.conductor import Conductor, create_conductor_from_dict from bluemira.magnets.registry import RegistrableMeta @@ -19,6 +22,18 @@ WINDINGPACK_REGISTRY = {} +@dataclass +class WindingPackParams(ParameterFrame): + """ + Parameters needed for the Winding Pack + """ + + nx: Parameter[int] + """Number of conductors along the x-axis.""" + ny: Parameter[int] + """Number of conductors along the y-axis.""" + + class WindingPack(metaclass=RegistrableMeta): """ Represents a winding pack composed of a grid of conductors. @@ -35,9 +50,10 @@ class WindingPack(metaclass=RegistrableMeta): _registry_: ClassVar[dict] = WINDINGPACK_REGISTRY _name_in_registry_: ClassVar[str] = "WindingPack" + param_cls: type[WindingPackParams] = WindingPackParams def __init__( - self, conductor: Conductor, nx: int, ny: int, name: str = "WindingPack" + self, conductor: Conductor, params: ParameterFrameLike, name: str = "WindingPack" ): """ Initialize a WindingPack instance. @@ -54,19 +70,18 @@ def __init__( Name of the winding pack instance. """ self.conductor = conductor - self.nx = int(nx) - self.ny = int(ny) + self.params = params self.name = name @property def dx(self) -> float: """Return the total width of the winding pack [m].""" - return self.conductor.dx * self.nx + return self.conductor.dx * self.params.nx.value @property def dy(self) -> float: """Return the total height of the winding pack [m].""" - return self.conductor.dy * self.ny + return self.conductor.dy * self.params.ny.value @property def area(self) -> float: @@ -76,7 +91,7 @@ def area(self) -> float: @property def n_conductors(self) -> int: """Return the total number of conductors.""" - return self.nx * self.ny + return self.params.nx.value * self.params.ny.value @property def jacket_area(self) -> float: @@ -98,7 +113,7 @@ def Kx(self, op_cond: OperationalConditions) -> float: # noqa: N802 float Stiffness along the x-axis [N/m]. """ - return self.conductor.Kx(op_cond) * self.ny / self.nx + return self.conductor.Kx(op_cond) * self.params.ny.value / self.params.nx.value def Ky(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ @@ -115,7 +130,7 @@ def Ky(self, op_cond: OperationalConditions) -> float: # noqa: N802 float Stiffness along the y-axis [N/m]. """ - return self.conductor.Ky(op_cond) * self.nx / self.ny + return self.conductor.Ky(op_cond) * self.params.nx.value / self.params.ny.value def plot( self, @@ -165,8 +180,8 @@ def plot( ax.plot(points_ext[:, 0], points_ext[:, 1], "k") if not homogenized: - for i in range(self.nx): - for j in range(self.ny): + for i in range(self.params.nx.value): + for j in range(self.params.ny.value): xc_c = xc - self.dx / 2 + (i + 0.5) * self.conductor.dx yc_c = yc - self.dy / 2 + (j + 0.5) * self.conductor.dy self.conductor.plot(xc=xc_c, yc=yc_c, ax=ax) @@ -190,8 +205,8 @@ def to_dict(self) -> dict: ), "name": self.name, "conductor": self.conductor.to_dict(), - "nx": self.nx, - "ny": self.ny, + "nx": self.params.nx.value, + "ny": self.params.ny.value, } @classmethod From 433e4b49c45bd333cd33ebd28aa7403a6e9e7890 Mon Sep 17 00:00:00 2001 From: Jonathan Matthews Date: Thu, 21 Aug 2025 14:14:21 +0100 Subject: [PATCH 09/61] add param details to docstring of class they are used in --- bluemira/magnets/cable.py | 84 ++++++++++++++++---------------- bluemira/magnets/case_tf.py | 19 ++++---- bluemira/magnets/conductor.py | 28 ++++++----- bluemira/magnets/strand.py | 22 ++++++--- bluemira/magnets/winding_pack.py | 11 +++-- 5 files changed, 89 insertions(+), 75 deletions(-) diff --git a/bluemira/magnets/cable.py b/bluemira/magnets/cable.py index 44d5b44ee6..a2604c594d 100644 --- a/bluemira/magnets/cable.py +++ b/bluemira/magnets/cable.py @@ -93,16 +93,16 @@ def __init__( The superconducting strand. stab_strand : Strand The stabilizer strand. - n_sc_strand : int - Number of superconducting strands. - n_stab_strand : int - Number of stabilizing strands. - d_cooling_channel : float - Diameter of the cooling channel [m]. - void_fraction : float - Ratio of material volume to total volume [unitless]. - cos_theta : float - Correction factor for twist in the cable layout. + params: + Structure containing the input parameters. Keys are: + - n_sc_strand: int + - n_stab_strand: int + - d_cooling_channel: float + - void_fraction: float = 0.725 + - cos_theta: float = 0.97 + + See :class:`~bluemira.magnets.cable.CableParams` + for parameter details. name : str Identifier for the cable instance. """ @@ -745,6 +745,7 @@ class RectangularCable(ABCCable): """ _name_in_registry_ = "RectangularCable" + param_cls: type[RectangularCableParams] = RectangularCableParams def __init__( self, @@ -763,22 +764,21 @@ def __init__( Parameters ---------- - dx : float - Cable width in the x-direction [m]. sc_strand : SuperconductingStrand Superconducting strand. stab_strand : Strand Stabilizer strand. - n_sc_strand : int - Number of superconducting strands. - n_stab_strand : int - Number of stabilizer strands. - d_cooling_channel : float - Cooling channel diameter [m]. - void_fraction : float, optional - Void fraction (material_volume / total_volume). - cos_theta : float, optional - Correction factor for strand twist. + params: + Structure containing the input parameters. Keys are: + - dx: float + - n_sc_strand: int + - n_stab_strand: int + - d_cooling_channel: float + - void_fraction: float = 0.725 + - cos_theta: float = 0.97 + + See :class:`~bluemira.magnets.cable.RectangularCableParams` + for parameter details. name : str, optional Name of the cable. """ @@ -1055,16 +1055,16 @@ def __init__( strand of the superconductor stab_strand: strand of the stabilizer - d_cooling_channel: - diameter of the cooling channel - n_sc_strand: - number of superconducting strands - n_stab_strand: - number of stabilizer strands - void_fraction: - void fraction defined as material_volume/total_volume - cos_theta: - corrective factor that consider the twist of the cable + params: + Structure containing the input parameters. Keys are: + - n_sc_strand: int + - n_stab_strand: int + - d_cooling_channel: float + - void_fraction: float = 0.725 + - cos_theta: float = 0.97 + + See :class:`~bluemira.magnets.cable.CableParams` + for parameter details. name: cable string identifier @@ -1276,16 +1276,16 @@ def __init__( strand of the superconductor stab_strand: strand of the stabilizer - d_cooling_channel: - diameter of the cooling channel - n_sc_strand: - number of superconducting strands - n_stab_strand: - number of stabilizer strands - void_fraction: - void fraction defined as material_volume/total_volume - cos_theta: - corrective factor that consider the twist of the cable + params: + Structure containing the input parameters. Keys are: + - n_sc_strand: int + - n_stab_strand: int + - d_cooling_channel: float + - void_fraction: float = 0.725 + - cos_theta: float = 0.97 + + See :class:`~bluemira.magnets.cable.CableParams` + for parameter details. name: cable string identifier """ diff --git a/bluemira/magnets/case_tf.py b/bluemira/magnets/case_tf.py index 41a2b203da..7da8a8e41d 100644 --- a/bluemira/magnets/case_tf.py +++ b/bluemira/magnets/case_tf.py @@ -340,14 +340,15 @@ def __init__( Parameters ---------- - Ri : float - External radius at the top of the TF coil case [m]. - dy_ps : float - Radial thickness of the poloidal support region [m]. - dy_vault : float - Radial thickness of the vault support region [m]. - theta_TF : float - Toroidal angular aperture of the coil [degrees]. + params: + Structure containing the input parameters. Keys are: + - Ri: float + - theta_TF: float + - dy_ps: float + - dy_vault: float + + See :class:`~bluemira.magnets.case_tf.TFCaseParams` + for parameter details. mat_case : Material Structural material assigned to the TF coil case. WPs : list[WindingPack] @@ -412,7 +413,7 @@ def update_dy_vault(self, value: float): def update_Rk(self, value: float): # noqa: N802 """ - Set the internal (innermost) radius of the TF case. + Set or update the internal (innermost) radius of the TF case. """ self.Rk = value self.params.dy_vault.value = self.R_wp_k[-1] - self._Rk diff --git a/bluemira/magnets/conductor.py b/bluemira/magnets/conductor.py index 1bcef76b54..f02260ed75 100644 --- a/bluemira/magnets/conductor.py +++ b/bluemira/magnets/conductor.py @@ -78,14 +78,15 @@ def __init__( jacket's material mat_ins: insulator's material - dx_jacket: - x-thickness of the jacket - dy_jacket: - y-tickness of the jacket - dx_ins: - x-thickness of the insulator - dy_ins: - y-tickness of the insulator + params: + Structure containing the input parameters. Keys are: + - dx_jacket: float + - dy_jacket: float + - dx_ins: float + - dy_ins: float + + See :class:`~bluemira.magnets.conductor.ConductorParams` + for parameter details. name: string identifier """ @@ -825,10 +826,13 @@ def __init__( jacket's material mat_ins: insulator's material - dx_jacket: - x(y)-thickness of the jacket - dx_ins: - x(y)-thickness of the insulator + params: + Structure containing the input parameters. Keys are: + - dx_jacket: float + - dx_ins: float + + See :class:`~bluemira.magnets.conductor.SymmetricConductorParams` + for parameter details. name: string identifier diff --git a/bluemira/magnets/strand.py b/bluemira/magnets/strand.py index eb7da8378d..4325811aab 100644 --- a/bluemira/magnets/strand.py +++ b/bluemira/magnets/strand.py @@ -76,10 +76,13 @@ def __init__( ---------- materials : list of MaterialFraction Materials composing the strand with their fractions. - d_strand : float, optional - Strand diameter in meters (default 0.82e-3). - temperature : float, optional - Operating temperature [K]. + params: + Structure containing the input parameters. Keys are: + - d_strand: float + - temperature: float + + See :class:`~bluemira.magnets.strand.StrandParams` + for parameter details. name : str or None, optional Name of the strand. Defaults to "Strand". """ @@ -416,10 +419,13 @@ def __init__( materials : list of MaterialFraction Materials composing the strand with their fractions. One material must be a supercoductor. - d_strand : float, optional - Strand diameter in meters (default 0.82e-3). - temperature : float, optional - Operating temperature [K]. + params: + Structure containing the input parameters. Keys are: + - d_strand: float + - temperature: float + + See :class:`~bluemira.magnets.strand.StrandParams` + for parameter details. name : str or None, optional Name of the strand. Defaults to "Strand". """ diff --git a/bluemira/magnets/winding_pack.py b/bluemira/magnets/winding_pack.py index 1a534fd3fb..9a54855b46 100644 --- a/bluemira/magnets/winding_pack.py +++ b/bluemira/magnets/winding_pack.py @@ -62,10 +62,13 @@ def __init__( ---------- conductor : Conductor The conductor instance. - nx : int - Number of conductors along the x-direction. - ny : int - Number of conductors along the y-direction. + params: + Structure containing the input parameters. Keys are: + - nx : int + - ny : int + + See :class:`~bluemira.magnets.winding_pack.WindingPackParams` + for parameter details. name : str, optional Name of the winding pack instance. """ From 30ab237fe0c80079025af8f1d0de66a8d14e8b87 Mon Sep 17 00:00:00 2001 From: Jonathan Matthews Date: Thu, 21 Aug 2025 14:22:09 +0100 Subject: [PATCH 10/61] removal of default values for certain parameters and added dataclass inheritance for cables --- bluemira/magnets/cable.py | 16 +++------------- bluemira/magnets/strand.py | 6 +++--- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/bluemira/magnets/cable.py b/bluemira/magnets/cable.py index a2604c594d..01992254b1 100644 --- a/bluemira/magnets/cable.py +++ b/bluemira/magnets/cable.py @@ -49,9 +49,9 @@ class CableParams(ParameterFrame): """Number of stabilizing strands.""" d_cooling_channel: Parameter[float] """Diameter of the cooling channel [m].""" - void_fraction: Parameter[float] = 0.725 + void_fraction: Parameter[float] """Ratio of material volume to total volume [unitless].""" - cos_theta: Parameter[float] = 0.97 + cos_theta: Parameter[float] """Correction factor for twist in the cable layout.""" @@ -717,23 +717,13 @@ def from_dict( @dataclass -class RectangularCableParams(ParameterFrame): +class RectangularCableParams(CableParams): """ Parameters needed for the TF cable """ dx: Parameter[float] """Cable width in the x-direction [m].""" - n_sc_strand: Parameter[int] - """Number of superconducting strands.""" - n_stab_strand: Parameter[int] - """Number of stabilizing strands.""" - d_cooling_channel: Parameter[float] - """Diameter of the cooling channel [m].""" - void_fraction: Parameter[float] = 0.725 - """Ratio of material volume to total volume [unitless].""" - cos_theta: Parameter[float] = 0.97 - """Correction factor for twist in the cable layout.""" class RectangularCable(ABCCable): diff --git a/bluemira/magnets/strand.py b/bluemira/magnets/strand.py index 4325811aab..b774468cc5 100644 --- a/bluemira/magnets/strand.py +++ b/bluemira/magnets/strand.py @@ -45,9 +45,9 @@ class StrandParams(ParameterFrame): Parameters needed for the strand """ - d_strand: Parameter[float] = 0.82e-3 - """Strand diameter in meters (default 0.82e-3).""" - temperature: Parameter[float] | None = None + d_strand: Parameter[float] + """Strand diameter in meters.""" + temperature: Parameter[float] """Operating temperature [K].""" From 3aa987c12a071addc79d31d884ef25de25cfcfb5 Mon Sep 17 00:00:00 2001 From: Jonathan Matthews Date: Thu, 21 Aug 2025 14:25:47 +0100 Subject: [PATCH 11/61] remove unused _shape in cable --- bluemira/magnets/cable.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/bluemira/magnets/cable.py b/bluemira/magnets/cable.py index 01992254b1..de04fb6476 100644 --- a/bluemira/magnets/cable.py +++ b/bluemira/magnets/cable.py @@ -107,8 +107,6 @@ def __init__( Identifier for the cable instance. """ super().__init__(params) # fix when split into builders and designers - # initialize private variables - self._shape = None # remove? # assign # Setting self.name triggers automatic instance registration From 409483fbd50360657b72e0a1a8f05a4f489d8304 Mon Sep 17 00:00:00 2001 From: Jonathan Matthews Date: Thu, 21 Aug 2025 14:32:47 +0100 Subject: [PATCH 12/61] remove superfluous calcs that = 1 such as dx / dy when dx = dy --- bluemira/magnets/cable.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/bluemira/magnets/cable.py b/bluemira/magnets/cable.py index de04fb6476..20efe38561 100644 --- a/bluemira/magnets/cable.py +++ b/bluemira/magnets/cable.py @@ -708,8 +708,8 @@ def from_dict( n_sc_strand=cable_dict["n_sc_strand"], n_stab_strand=cable_dict["n_stab_strand"], d_cooling_channel=cable_dict["d_cooling_channel"], - void_fraction=cable_dict.get("void_fraction", 0.725), - cos_theta=cable_dict.get("cos_theta", 0.97), + void_fraction=cable_dict["void_fraction"], + cos_theta=cable_dict["cos_theta"], name=name or cable_dict.get("name"), ) @@ -922,8 +922,8 @@ def from_dict( n_sc_strand = cable_dict["n_sc_strand"] n_stab_strand = cable_dict["n_stab_strand"] d_cooling_channel = cable_dict["d_cooling_channel"] - void_fraction = cable_dict.get("void_fraction", 0.725) - cos_theta = cable_dict.get("cos_theta", 0.97) + void_fraction = cable_dict["void_fraction"] + cos_theta = cable_dict["cos_theta"] # how to handle with parameterframe? # Create cable @@ -1094,7 +1094,7 @@ def Kx(self, op_cond: OperationalConditions): # noqa: N802 float Homogenized stiffness in the x-direction [Pa]. """ - return self.E(op_cond) * self.dy / self.dx + return self.E(op_cond) def Ky(self, op_cond: OperationalConditions): # noqa: N802 """ @@ -1111,7 +1111,7 @@ def Ky(self, op_cond: OperationalConditions): # noqa: N802 float Homogenized stiffness in the y-direction [Pa]. """ - return self.E(op_cond) * self.dx / self.dy + return self.E(op_cond) def to_dict(self) -> dict: """ @@ -1173,8 +1173,8 @@ def from_dict( n_sc_strand=cable_dict["n_sc_strand"], n_stab_strand=cable_dict["n_stab_strand"], d_cooling_channel=cable_dict["d_cooling_channel"], - void_fraction=cable_dict.get("void_fraction", 0.725), - cos_theta=cable_dict.get("cos_theta", 0.97), + void_fraction=cable_dict["void_fraction"], + cos_theta=cable_dict["cos_theta"], name=name or cable_dict.get("name"), ) @@ -1317,7 +1317,7 @@ def Kx(self, op_cond: OperationalConditions): # noqa: N802 float Equivalent stiffness in the x-direction [Pa]. """ - return self.E(op_cond) * self.dy / self.dx + return self.E(op_cond) def Ky(self, op_cond: OperationalConditions): # noqa: N802 """ @@ -1338,7 +1338,7 @@ def Ky(self, op_cond: OperationalConditions): # noqa: N802 float Equivalent stiffness in the y-direction [Pa]. """ - return self.E(op_cond) * self.dx / self.dy + return self.E(op_cond) def plot(self, xc: float = 0, yc: float = 0, *, show: bool = False, ax=None): """ @@ -1452,8 +1452,8 @@ def from_dict( n_sc_strand=cable_dict["n_sc_strand"], n_stab_strand=cable_dict["n_stab_strand"], d_cooling_channel=cable_dict["d_cooling_channel"], - void_fraction=cable_dict.get("void_fraction", 0.725), - cos_theta=cable_dict.get("cos_theta", 0.97), + void_fraction=cable_dict["void_fraction"], + cos_theta=cable_dict["cos_theta"], name=name or cable_dict.get("name"), ) From 65f725daeda4ca4a4837be9cf477454d7775ac8c Mon Sep 17 00:00:00 2001 From: Jonathan Matthews Date: Thu, 21 Aug 2025 14:54:59 +0100 Subject: [PATCH 13/61] small changes to some functions and left comments against some parts --- bluemira/magnets/case_tf.py | 5 +++-- bluemira/magnets/conductor.py | 11 ++++++----- bluemira/magnets/strand.py | 4 ++-- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/bluemira/magnets/case_tf.py b/bluemira/magnets/case_tf.py index 7da8a8e41d..8b644496f6 100644 --- a/bluemira/magnets/case_tf.py +++ b/bluemira/magnets/case_tf.py @@ -411,6 +411,8 @@ def update_dy_vault(self, value: float): self.params.dy_vault.value = value self.Rk = self.R_wp_k[-1] - self.params.dy_vault.value + # jm - not used but replaces functionality of original Rk setter + # can't find when (if) it was used originally def update_Rk(self, value: float): # noqa: N802 """ Set or update the internal (innermost) radius of the TF case. @@ -494,8 +496,7 @@ def WPs(self, value: list[WindingPack]): # noqa: N802 self._WPs = value # fix dy_vault (this will recalculate Rk) - if hasattr(self, "dy_vault"): - self.dy_vault = self.dy_vault + self.update_dy_vault(self.params.dy_vault.value) @property def n_conductors(self): diff --git a/bluemira/magnets/conductor.py b/bluemira/magnets/conductor.py index f02260ed75..7a8bab4f50 100644 --- a/bluemira/magnets/conductor.py +++ b/bluemira/magnets/conductor.py @@ -119,8 +119,8 @@ def area(self): """Area of the conductor [m^2]""" return self.dx * self.dy - # surely this should be done from inside out ie cable dx + jacket dx - # rather than out in and depend on more variables? + # jm - surely this should be done from inside out ie cable dx + jacket dx + # rather than out in and depend on more variables? @property def area_jacket(self): """Area of the jacket [m^2]""" @@ -797,7 +797,8 @@ class SymmetricConductorParams(ParameterFrame): """x-thickness of the insulator [m].""" -class SymmetricConductor(Conductor): +class SymmetricConductor(Conductor): # jm - actually worthwhile or just set up + # conductor with dx = dy and don't duplicate? """ Representation of a symmetric conductor in which both jacket and insulator mantain a constant thickness (i.e. dy_jacket = dx_jacket and dy_ins = dx_ins). @@ -844,8 +845,8 @@ def __init__( params=params, name=name, ) - self.dy_jacket = self.params.dx_jacket.value # needed or just property? - self.dy_ins = self.params.dx_ins.value # needed or just property? + self.dy_jacket = self.params.dx_jacket.value # jm - needed or just property? + self.dy_ins = self.params.dx_ins.value # jm - needed or just property? @property def dy_jacket(self): diff --git a/bluemira/magnets/strand.py b/bluemira/magnets/strand.py index b774468cc5..9cd3788385 100644 --- a/bluemira/magnets/strand.py +++ b/bluemira/magnets/strand.py @@ -87,10 +87,10 @@ def __init__( Name of the strand. Defaults to "Strand". """ self.params = params - self._shape = None - self._materials = None + self._materials = None # jm - remove self.materials = materials + self.name = name # Create homogenised material From 97c26a4756dbc79a981b9928c6ac5a93558ffee2 Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Fri, 22 Aug 2025 14:04:55 +0100 Subject: [PATCH 14/61] =?UTF-8?q?=F0=9F=94=A5=20Remove=20dummy=20classes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bluemira/magnets/cable.py | 282 ++++++++------------------------------ 1 file changed, 56 insertions(+), 226 deletions(-) diff --git a/bluemira/magnets/cable.py b/bluemira/magnets/cable.py index 20efe38561..20a96e0cdd 100644 --- a/bluemira/magnets/cable.py +++ b/bluemira/magnets/cable.py @@ -78,6 +78,7 @@ def __init__( stab_strand: Strand, params: ParameterFrameLike, name: str = "Cable", + **props, ): """ Representation of a cable. Only the x-dimension of the cable is given as @@ -114,6 +115,10 @@ def __init__( self.sc_strand = sc_strand self.stab_strand = stab_strand + for k, v in props.items(): + setattr(self, k, v if callable(v) else lambda *arg, v=v, **kwargs: v) # noqa: ARG005 + self._props = list(props.keys()) + @property @abstractmethod def dx(self): @@ -648,6 +653,7 @@ def to_dict(self) -> dict: "cos_theta": self.params.cos_theta.value, "sc_strand": self.sc_strand.to_dict(), "stab_strand": self.stab_strand.to_dict(), + **{k: getattr(k)() for k in self._props}, } @classmethod @@ -679,7 +685,7 @@ def from_dict( ValueError If name_in_registry mismatch or duplicate instance name. """ - name_in_registry = cable_dict.get("name_in_registry") + name_in_registry = cable_dict.pop("name_in_registry", None) expected_name_in_registry = getattr(cls, "_name_in_registry_", cls.__name__) if name_in_registry != expected_name_in_registry: @@ -689,13 +695,13 @@ def from_dict( ) # Deserialize strands - sc_strand_data = cable_dict["sc_strand"] + sc_strand_data = cable_dict.pop("sc_strand") if isinstance(sc_strand_data, Strand): sc_strand = sc_strand_data else: sc_strand = create_strand_from_dict(strand_dict=sc_strand_data) - stab_strand_data = cable_dict["stab_strand"] + stab_strand_data = cable_dict.pop("stab_strand") if isinstance(stab_strand_data, Strand): stab_strand = stab_strand_data else: @@ -705,12 +711,13 @@ def from_dict( return cls( sc_strand=sc_strand, stab_strand=stab_strand, - n_sc_strand=cable_dict["n_sc_strand"], - n_stab_strand=cable_dict["n_stab_strand"], - d_cooling_channel=cable_dict["d_cooling_channel"], - void_fraction=cable_dict["void_fraction"], - cos_theta=cable_dict["cos_theta"], - name=name or cable_dict.get("name"), + n_sc_strand=cable_dict.pop("n_sc_strand"), + n_stab_strand=cable_dict.pop("n_stab_strand"), + d_cooling_channel=cable_dict.pop("d_cooling_channel"), + void_fraction=cable_dict.pop("void_fraction"), + cos_theta=cable_dict.pop("cos_theta"), + name=name or cable_dict.pop("name", None), + **cable_dict, ) @@ -741,6 +748,7 @@ def __init__( stab_strand: Strand, params: ParameterFrameLike, name: str = "RectangularCable", + **props, ): """ Representation of a cable. Only the x-dimension of the cable is given as @@ -768,15 +776,23 @@ def __init__( See :class:`~bluemira.magnets.cable.RectangularCableParams` for parameter details. name : str, optional - Name of the cable. + Name of the cable + props: + extra properties """ super().__init__( sc_strand=sc_strand, stab_strand=stab_strand, params=params, name=name, + **props, ) + @property + def dx(self): + """Cable dimension in the x direction [m]""" + return self.params.dx.value + @property def dy(self): """Cable dimension in the y direction [m]""" @@ -885,21 +901,21 @@ def from_dict( ) # Deserialize strands - sc_strand_data = cable_dict["sc_strand"] + sc_strand_data = cable_dict.pop("sc_strand") if isinstance(sc_strand_data, Strand): sc_strand = sc_strand_data else: sc_strand = create_strand_from_dict(strand_dict=sc_strand_data) - stab_strand_data = cable_dict["stab_strand"] + stab_strand_data = cable_dict.pop("stab_strand") if isinstance(stab_strand_data, Strand): stab_strand = stab_strand_data else: stab_strand = create_strand_from_dict(strand_dict=stab_strand_data) # Geometry parameters - dx = cable_dict.get("dx") - aspect_ratio = cable_dict.get("aspect_ratio") + dx = cable_dict.pop("dx", None) + aspect_ratio = cable_dict.pop("aspect_ratio") if dx is not None and aspect_ratio is not None: bluemira_warn( @@ -919,11 +935,11 @@ def from_dict( ) # Base cable parameters - n_sc_strand = cable_dict["n_sc_strand"] - n_stab_strand = cable_dict["n_stab_strand"] - d_cooling_channel = cable_dict["d_cooling_channel"] - void_fraction = cable_dict["void_fraction"] - cos_theta = cable_dict["cos_theta"] + n_sc_strand = cable_dict.pop("n_sc_strand") + n_stab_strand = cable_dict.pop("n_stab_strand") + d_cooling_channel = cable_dict.pop("d_cooling_channel") + void_fraction = cable_dict.pop("void_fraction") + cos_theta = cable_dict.pop("cos_theta") # how to handle with parameterframe? # Create cable @@ -936,7 +952,8 @@ def from_dict( d_cooling_channel=d_cooling_channel, void_fraction=void_fraction, cos_theta=cos_theta, - name=name or cable_dict.get("name"), + name=name or cable_dict.pop("name", None), + **cable_dict, ) # Adjust aspect ratio if needed @@ -946,72 +963,6 @@ def from_dict( return cable -class DummyRectangularCableHTS(RectangularCable): - """ - Dummy rectangular cable with young's moduli set to 120 GPa. - """ - - _name_in_registry_ = "DummyRectangularCableHTS" - - def __init__(self, *args, **kwargs): - kwargs.setdefault("name", "DummyRectangularCableHTS") - super().__init__(*args, **kwargs) - - def E(self, op_cond: OperationalConditions): # noqa: N802, PLR6301, ARG002 - """ - Return the Young's modulus of the cable material. - - This is a constant value specific to the implementation. Subclasses may override - this method to provide a temperature- or field-dependent modulus. The `kwargs` - parameter is unused here but retained for interface consistency. - - Parameters - ---------- - op_cond: OperationalConditions - Operational conditions including temperature, magnetic field, and strain - at which to calculate the material property. - - Returns - ------- - float - Young's modulus in Pascals [Pa]. - """ - return 120e9 - - -class DummyRectangularCableLTS(RectangularCable): - """ - Dummy square cable with young's moduli set to 0.1 GPa - """ - - _name_in_registry_ = "DummyRectangularCableLTS" - - def __init__(self, *args, **kwargs): - kwargs.setdefault("name", "DummyRectangularCableLTS") - super().__init__(*args, **kwargs) - - def E(self, op_cond): # noqa: N802, PLR6301, ARG002 - """ - Return the Young's modulus of the cable material. - - This implementation returns a fixed value (0.1 GPa). Subclasses may override - this method with more sophisticated behavior. `kwargs` are included for - compatibility but not used in this implementation. - - Parameters - ---------- - op_cond: OperationalConditions - Operational conditions including temperature, magnetic field, and strain - at which to calculate the material property. - - Returns - ------- - float - Young's modulus in Pascals [Pa]. - """ - return 0.1e9 - - class SquareCable(ABCCable): """ Cable with a square cross-section. @@ -1154,7 +1105,7 @@ def from_dict( If unique_name is False and a duplicate name is detected in the instance cache. """ - name_in_registry = cable_dict.get("name_in_registry") + name_in_registry = cable_dict.pop("name_in_registry", None) expected_name_in_registry = getattr(cls, "_name_in_registry_", cls.__name__) if name_in_registry != expected_name_in_registry: @@ -1163,80 +1114,22 @@ def from_dict( f"'{name_in_registry}'. Expected '{expected_name_in_registry}'." ) - sc_strand = create_strand_from_dict(strand_dict=cable_dict["sc_strand"]) - stab_strand = create_strand_from_dict(strand_dict=cable_dict["stab_strand"]) + sc_strand = create_strand_from_dict(strand_dict=cable_dict.pop("sc_strand")) + stab_strand = create_strand_from_dict(strand_dict=cable_dict.pop("stab_strand")) # how to handle this? return cls( sc_strand=sc_strand, stab_strand=stab_strand, - n_sc_strand=cable_dict["n_sc_strand"], - n_stab_strand=cable_dict["n_stab_strand"], - d_cooling_channel=cable_dict["d_cooling_channel"], - void_fraction=cable_dict["void_fraction"], - cos_theta=cable_dict["cos_theta"], - name=name or cable_dict.get("name"), + n_sc_strand=cable_dict.pop("n_sc_strand"), + n_stab_strand=cable_dict.pop("n_stab_strand"), + d_cooling_channel=cable_dict.pop("d_cooling_channel"), + void_fraction=cable_dict.pop("void_fraction"), + cos_theta=cable_dict.pop("cos_theta"), + name=name or cable_dict.pop("name", None), ) -class DummySquareCableHTS(SquareCable): - """ - Dummy square cable with Young's modulus set to 120 GPa. - """ - - _name_in_registry_ = "DummySquareCableHTS" - - def __init__(self, *args, **kwargs): - kwargs.setdefault("name", "DummySquareCableHTS") - super().__init__(*args, **kwargs) - - def E(self, op_cond: OperationalConditions): # noqa: N802, PLR6301, ARG002 - """ - Return the Young's modulus for the HTS dummy cable. - - Parameters - ---------- - op_cond: OperationalConditions - Operational conditions including temperature, magnetic field, and strain - at which to calculate the material property. - - Returns - ------- - float - Young's modulus in Pascals [Pa]. - """ - return 120e9 - - -class DummySquareCableLTS(SquareCable): - """ - Dummy square cable with Young's modulus set to 0.1 GPa. - """ - - _name_in_registry_ = "DummySquareCableLTS" - - def __init__(self, *args, **kwargs): - kwargs.setdefault("name", "DummySquareCableLTS") - super().__init__(*args, **kwargs) - - def E(self, op_cond: OperationalConditions): # noqa: N802, PLR6301, ARG002 - """ - Return the Young's modulus for the LTS dummy cable. - - Parameters - ---------- - op_cond: OperationalConditions - Operational conditions including temperature, magnetic field, and strain - at which to calculate the material property. - - Returns - ------- - float - Young's modulus in Pascals [Pa]. - """ - return 0.1e9 - - class RoundCable(ABCCable): """ A cable with round cross-section for superconducting applications. @@ -1433,7 +1326,7 @@ def from_dict( If unique_name is False and a duplicate name is detected in the instance cache. """ - name_in_registry = cable_dict.get("name_in_registry") + name_in_registry = cable_dict.pop("name_in_registry", None) expected_name_in_registry = getattr(cls, "_name_in_registry_", cls.__name__) if name_in_registry != expected_name_in_registry: @@ -1442,86 +1335,23 @@ def from_dict( f"'{name_in_registry}'. Expected '{expected_name_in_registry}'." ) - sc_strand = create_strand_from_dict(strand_dict=cable_dict["sc_strand"]) - stab_strand = create_strand_from_dict(strand_dict=cable_dict["stab_strand"]) + sc_strand = create_strand_from_dict(strand_dict=cable_dict.pop("sc_strand")) + stab_strand = create_strand_from_dict(strand_dict=cable_dict.pop("stab_strand")) # how to handle? return cls( sc_strand=sc_strand, stab_strand=stab_strand, - n_sc_strand=cable_dict["n_sc_strand"], - n_stab_strand=cable_dict["n_stab_strand"], - d_cooling_channel=cable_dict["d_cooling_channel"], - void_fraction=cable_dict["void_fraction"], - cos_theta=cable_dict["cos_theta"], - name=name or cable_dict.get("name"), + n_sc_strand=cable_dict.pop("n_sc_strand"), + n_stab_strand=cable_dict.pop("n_stab_strand"), + d_cooling_channel=cable_dict.pop("d_cooling_channel"), + void_fraction=cable_dict.pop("void_fraction"), + cos_theta=cable_dict.pop("cos_theta"), + name=name or cable_dict.pop("name", None), + **cable_dict, ) -class DummyRoundCableHTS(RoundCable): - """ - Dummy round cable with Young's modulus set to 120 GPa. - - This class provides a simplified round cable configuration for high-temperature - superconducting (HTS) analysis with a fixed stiffness value. - """ - - _name_in_registry_ = "DummyRoundCableHTS" - - def __init__(self, *args, **kwargs): - kwargs.setdefault("name", "DummyRoundCableHTS") - super().__init__(*args, **kwargs) - - def E(self, op_cond: OperationalConditions): # noqa: N802, PLR6301, ARG002 - """ - Return the Young's modulus for the HTS dummy round cable. - - Parameters - ---------- - op_cond: OperationalConditions - Operational conditions including temperature, magnetic field, and strain - at which to calculate the material property. - - Returns - ------- - float - Young's modulus in Pascals [Pa]. - """ - return 120e9 - - -class DummyRoundCableLTS(RoundCable): - """ - Dummy round cable with Young's modulus set to 0.1 GPa. - - This class provides a simplified round cable configuration for low-temperature - superconducting (LTS) analysis with a fixed, softer stiffness value. - """ - - _name_in_registry_ = "DummyRoundCableLTS" - - def __init__(self, *args, **kwargs): - kwargs.setdefault("name", "DummyRoundCableLTS") - super().__init__(*args, **kwargs) - - def E(self, op_cond: OperationalConditions): # noqa: N802, PLR6301, ARG002 - """ - Return the Young's modulus for the LTS dummy round cable. - - Parameters - ---------- - op_cond: OperationalConditions - Operational conditions including temperature, magnetic field, and strain - at which to calculate the material property. - - Returns - ------- - float - Young's modulus in Pascals [Pa]. - """ - return 0.1e9 - - def create_cable_from_dict( cable_dict: dict, name: str | None = None, From b0439ac00b9f9d30958762d020665b62d17aa43b Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Sat, 23 Aug 2025 07:29:46 +0100 Subject: [PATCH 15/61] =?UTF-8?q?=F0=9F=8E=A8=20More?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bluemira/magnets/cable.py | 90 +--- bluemira/magnets/case_tf.py | 76 +-- bluemira/magnets/conductor.py | 13 +- bluemira/magnets/fatigue.py | 12 +- bluemira/magnets/init_magnets_registry.py | 68 --- bluemira/magnets/registry.py | 445 ------------------ bluemira/magnets/strand.py | 13 +- bluemira/magnets/winding_pack.py | 7 +- bluemira/magnets/winding_pack_.py | 266 +++++++++++ examples/magnets/example_tf_wp_from_dict.py | 10 +- .../magnets/example_tf_wp_optimization.py | 13 +- 11 files changed, 310 insertions(+), 703 deletions(-) delete mode 100644 bluemira/magnets/init_magnets_registry.py delete mode 100644 bluemira/magnets/registry.py create mode 100644 bluemira/magnets/winding_pack_.py diff --git a/bluemira/magnets/cable.py b/bluemira/magnets/cable.py index 20a96e0cdd..9b85ce8e4a 100644 --- a/bluemira/magnets/cable.py +++ b/bluemira/magnets/cable.py @@ -20,7 +20,6 @@ from bluemira.base.look_and_feel import bluemira_error, bluemira_print, bluemira_warn from bluemira.base.parameter_frame import Parameter, ParameterFrame from bluemira.base.parameter_frame.typed import ParameterFrameLike -from bluemira.magnets.registry import RegistrableMeta from bluemira.magnets.strand import ( Strand, SuperconductingStrand, @@ -28,15 +27,7 @@ ) from bluemira.magnets.utils import reciprocal_summation, summation -# ------------------------------------------------------------------------------ -# Global Registries -# ------------------------------------------------------------------------------ -CABLE_REGISTRY = {} - -# ------------------------------------------------------------------------------ -# Cable Class -# ------------------------------------------------------------------------------ @dataclass class CableParams(ParameterFrame): """ @@ -55,7 +46,7 @@ class CableParams(ParameterFrame): """Correction factor for twist in the cable layout.""" -class ABCCable(ABC, metaclass=RegistrableMeta): +class ABCCable(ABC): """ Abstract base class for superconducting cables. @@ -68,7 +59,6 @@ class ABCCable(ABC, metaclass=RegistrableMeta): - Subclasses must define `dx`, `dy`, `Kx`, `Ky`, and `from_dict`. """ - _registry_ = CABLE_REGISTRY _name_in_registry_: str | None = None # Abstract base classes should NOT register param_cls: type[CableParams] = CableParams @@ -274,7 +264,7 @@ def _heat_balance_model_cable( """ # Calculate the rate of heat generation (Joule dissipation) if isinstance(temperature, np.ndarray): - temperature = temperature[0] + temperature = temperature.item() op_cond = OperationalConditions(temperature=temperature, magnetic_field=B_fun(t)) @@ -309,7 +299,7 @@ def _temperature_evolution( return solution - def optimize_n_stab_ths( + def optimise_n_stab_ths( self, t0: float, tf: float, @@ -421,15 +411,11 @@ def final_temperature_difference( # diff = abs(final_temperature - target_temperature) return abs(final_temperature - target_temperature) - method = None - if bounds is not None: - method = "bounded" - result = minimize_scalar( fun=final_temperature_difference, args=(t0, tf, initial_temperature, target_temperature, B_fun, I_fun), bounds=bounds, - method=method, + method=None if bounds is None else "bounded", ) if not result.success: @@ -460,72 +446,22 @@ def final_temperature_difference( f"Final temperature with optimal n_stab: {final_temperature:.2f} Kelvin" ) - if show: - _, (ax_temp, ax_ib) = plt.subplots(2, 1, figsize=(8, 8), sharex=True) - - # --- Plot Temperature Evolution --- - ax_temp.plot(solution.t, solution.y[0], "r*", label="Simulation points") - time_steps = np.linspace(t0, tf, 100) - ax_temp.plot( - time_steps, solution.sol(time_steps)[0], "b", label="Interpolated curve" - ) - ax_temp.grid(visible=True) - ax_temp.set_ylabel("Temperature [K]", fontsize=10) - ax_temp.set_title("Quench temperature evolution", fontsize=11) - ax_temp.legend(fontsize=9) - - ax_temp.tick_params(axis="y", labelcolor="k", labelsize=9) + @dataclass + class StabilisingStrandRes: + solution: Any + info_text: str - # Insert text box with additional info - info_text = ( + return StabilisingStrandRes( + solution, + ( f"Target T: {target_temperature:.2f} K\n" f"Initial T: {initial_temperature:.2f} K\n" f"SC Strand: {self.sc_strand.name}\n" f"n. sc. strand = {self.params.n_sc_strand.value}\n" f"Stab. strand = {self.stab_strand.name}\n" f"n. stab. strand = {self.params.n_stab_strand.value}\n" - ) - props = {"boxstyle": "round", "facecolor": "white", "alpha": 0.8} - ax_temp.text( - 0.65, - 0.5, - info_text, - transform=ax_temp.transAxes, - fontsize=9, - verticalalignment="top", - bbox=props, - ) - - # --- Plot I_fun(t) and B_fun(t) --- - time_steps_fine = np.linspace(t0, tf, 300) - I_values = [I_fun(t) for t in time_steps_fine] # noqa: N806 - B_values = [B_fun(t) for t in time_steps_fine] - - ax_ib.plot(time_steps_fine, I_values, "g", label="Current [A]") - ax_ib.set_ylabel("Current [A]", color="g", fontsize=10) - ax_ib.tick_params(axis="y", labelcolor="g", labelsize=9) - ax_ib.grid(visible=True) - - ax_ib_right = ax_ib.twinx() - ax_ib_right.plot( - time_steps_fine, B_values, "m--", label="Magnetic field [T]" - ) - ax_ib_right.set_ylabel("Magnetic field [T]", color="m", fontsize=10) - ax_ib_right.tick_params(axis="y", labelcolor="m", labelsize=9) - - # Labels - ax_ib.set_xlabel("Time [s]", fontsize=10) - ax_ib.tick_params(axis="x", labelsize=9) - - # Combined legend for both sides - lines, labels = ax_ib.get_legend_handles_labels() - lines2, labels2 = ax_ib_right.get_legend_handles_labels() - ax_ib.legend(lines + lines2, labels + labels2, loc="best", fontsize=9) - - plt.tight_layout() - plt.show() - - return result + ), + ) # OD homogenized structural properties @abstractmethod diff --git a/bluemira/magnets/case_tf.py b/bluemira/magnets/case_tf.py index 8b644496f6..33905a3947 100644 --- a/bluemira/magnets/case_tf.py +++ b/bluemira/magnets/case_tf.py @@ -34,19 +34,12 @@ ) from bluemira.base.parameter_frame import Parameter, ParameterFrame from bluemira.base.parameter_frame.typed import ParameterFrameLike -from bluemira.magnets.registry import RegistrableMeta +from bluemira.geometry.parameterisations import GeometryParameterisation +from bluemira.geometry.wire import BluemiraWire from bluemira.magnets.utils import reciprocal_summation, summation from bluemira.magnets.winding_pack import WindingPack, create_wp_from_dict -# ------------------------------------------------------------------------------ -# Global Registries -# ------------------------------------------------------------------------------ -CASETF_REGISTRY = {} - -# ------------------------------------------------------------------------------ -# TFcoil cross section Geometry Base and Implementations -# ------------------------------------------------------------------------------ @dataclass class TFCaseGeometryParams(ParameterFrame): """ @@ -132,7 +125,7 @@ def plot(self, ax=None, *, show: bool = False) -> plt.Axes: """ -class TrapezoidalGeometry(CaseGeometry): +class TrapezoidalGeometry(GeometryParameterisation): # TODO Opvariablesframe """ Geometry of a Toroidal Field (TF) coil case with trapezoidal cross-section. @@ -163,7 +156,7 @@ def area(self) -> float: * (self.params.Ri.value - self.params.Rk.value) ) - def build_polygon(self) -> np.ndarray: + def create_shape(self, label: str = "") -> BluemiraWire: """ Construct the (x, r) coordinates of the trapezoidal cross-section polygon. @@ -184,36 +177,8 @@ def build_polygon(self) -> np.ndarray: [-dx_inner / 2, self.params.Rk.value], ]) - def plot(self, ax=None, *, show=False) -> plt.Axes: - """ - Plot the trapezoidal cross-sectional shape of the TF case. - - Parameters - ---------- - ax : matplotlib.axes.Axes, optional - Axis object on which to draw the geometry. If None, a new figure and axis - are created. - show : bool, optional - If True, the plot is immediately displayed using plt.show(). Default is - False. - - Returns - ------- - matplotlib.axes.Axes - Axis object containing the plotted geometry. - """ - if ax is None: - _, ax = plt.subplots() - poly = self.build_polygon() - poly = np.vstack([poly, poly[0]]) # Close the polygon - ax.plot(poly[:, 0], poly[:, 1], "k-", linewidth=2) - ax.set_aspect("equal") - if show: - plt.show() - return ax - -class WedgedGeometry(CaseGeometry): +class WedgedGeometry(GeometryParameterisation): """ TF coil case shaped as a sector of an annulus (wedge with arcs). @@ -235,7 +200,7 @@ def area(self) -> float: 0.5 * self.rad_theta_TF * (self.params.Ri.value**2 - self.params.Rk.value**2) ) - def build_polygon(self, n_points: int = 50) -> np.ndarray: + def create_shape(self, label: str = "", n_points: int = 50) -> BluemiraWire: """ Build the polygon representing the wedge shape. @@ -269,33 +234,6 @@ def build_polygon(self, n_points: int = 50) -> np.ndarray: return np.vstack((arc_outer, arc_inner)) - def plot(self, ax=None, *, show=False): - """ - Plot the wedge-shaped TF coil case cross-section. - - Parameters - ---------- - ax : matplotlib.axes.Axes, optional - Axis on which to draw the geometry. If None, a new figure and axis are - created. - show : bool, optional - If True, immediately display the plot with plt.show(). Default is False. - - Returns - ------- - matplotlib.axes.Axes - The axis object containing the plot. - """ - if ax is None: - _, ax = plt.subplots() - poly = self.build_polygon() - poly = np.vstack([poly, poly[0]]) # Close the polygon - ax.plot(poly[:, 0], poly[:, 1], "k-", linewidth=2) - ax.set_aspect("equal") - if show: - plt.show() - return ax - # ------------------------------------------------------------------------------ # CaseTF Class @@ -316,7 +254,7 @@ class TFCaseParams(ParameterFrame): """Radial thickness of the vault support region [m].""" -class BaseCaseTF(CaseGeometry, ABC, metaclass=RegistrableMeta): +class BaseCaseTF(CaseGeometry, ABC): """ Abstract Base Class for Toroidal Field Coil Case configurations. diff --git a/bluemira/magnets/conductor.py b/bluemira/magnets/conductor.py index 7a8bab4f50..e0c66b2ed9 100644 --- a/bluemira/magnets/conductor.py +++ b/bluemira/magnets/conductor.py @@ -19,19 +19,9 @@ from bluemira.base.parameter_frame import Parameter, ParameterFrame from bluemira.base.parameter_frame.typed import ParameterFrameLike from bluemira.magnets.cable import ABCCable, create_cable_from_dict -from bluemira.magnets.registry import RegistrableMeta from bluemira.magnets.utils import reciprocal_summation, summation -# ------------------------------------------------------------------------------ -# Global Registries -# ------------------------------------------------------------------------------ -CONDUCTOR_REGISTRY = {} - - -# ------------------------------------------------------------------------------ -# Strand Class -# ------------------------------------------------------------------------------ @dataclass class ConductorParams(ParameterFrame): """ @@ -48,13 +38,12 @@ class ConductorParams(ParameterFrame): """y-thickness of the insulator [m].""" -class Conductor(metaclass=RegistrableMeta): +class Conductor: """ A generic conductor consisting of a cable surrounded by a jacket and an insulator. """ - _registry_ = CONDUCTOR_REGISTRY _name_in_registry_ = "Conductor" param_cls: type[ConductorParams] = ConductorParams diff --git a/bluemira/magnets/fatigue.py b/bluemira/magnets/fatigue.py index eed1761d40..01256e7348 100644 --- a/bluemira/magnets/fatigue.py +++ b/bluemira/magnets/fatigue.py @@ -10,6 +10,7 @@ import abc from dataclasses import dataclass +from typing import Final import numpy as np @@ -170,7 +171,6 @@ class Crack(abc.ABC): Crack width along the plate length direction """ - alpha = None param_cls: type[CrackParams] = CrackParams def __init__(self, params: ParameterFrameLike): @@ -202,6 +202,10 @@ def area(self) -> float: """ return self.alpha * np.pi * self.params.depth.value * self.params.width.value + @property + @abc.abstractmethod + def alpha(self) -> float: ... + @abc.abstractmethod def stress_intensity_factor( self, @@ -230,7 +234,7 @@ class QuarterEllipticalCornerCrack(Crack): Crack width along the plate length direction """ - alpha = 0.25 + alpha: Final[float] = 0.25 def stress_intensity_factor( # noqa: PLR6301 self, @@ -330,7 +334,7 @@ class SemiEllipticalSurfaceCrack(Crack): Crack width along the plate length direction """ - alpha = 0.5 + alpha: Final[float] = 0.5 def stress_intensity_factor( # noqa: PLR6301 self, @@ -421,7 +425,7 @@ class EllipticalEmbeddedCrack(Crack): Crack width along the plate length direction """ - alpha = 1.0 + alpha: Final[float] = 1.0 def stress_intensity_factor( # noqa: PLR6301 self, diff --git a/bluemira/magnets/init_magnets_registry.py b/bluemira/magnets/init_magnets_registry.py deleted file mode 100644 index 5f47977230..0000000000 --- a/bluemira/magnets/init_magnets_registry.py +++ /dev/null @@ -1,68 +0,0 @@ -# SPDX-FileCopyrightText: 2021-present M. Coleman, J. Cook, F. Franza -# SPDX-FileCopyrightText: 2021-present I.A. Maione, S. McIntosh -# SPDX-FileCopyrightText: 2021-present J. Morris, D. Short -# -# SPDX-License-Identifier: LGPL-2.1-or-later - -""" -Initialization functions to register magnet classes. - -These functions import necessary modules to trigger class registration -(via metaclasses) without polluting the importing namespace. -""" - - -def register_strands(): - """ - Import and register all known Strand classes. - - This triggers their metaclass registration into the STRAND_REGISTRY. - Importing here avoids polluting the top-level namespace. - - Classes registered - ------------------- - - Strand - - SuperconductingStrand - """ - - -def register_cables(): - """ - Import and register all known Cable classes. - - This triggers their metaclass registration into the CABLE_REGISTRY. - Importing here avoids polluting the top-level namespace. - - Classes registered - ------------------- - - Cable - - Specialized cable types (e.g., TwistedCables) - """ - - -def register_conductors(): - """ - Import and register all known Conductor classes. - - This triggers their metaclass registration into the CONDUCTOR_REGISTRY. - Importing here avoids polluting the top-level namespace. - - Classes registered - ------------------- - - Conductor - - Specialized conductors - """ - - -def register_all_magnets(): - """ - Import and register all known magnet-related classes. - - Calls `register_strands()`, `register_cables()`, and `register_conductors()` - to fully populate all internal registries. - - Use this function at initialization if you want all classes to be available. - """ - register_strands() - register_cables() - register_conductors() diff --git a/bluemira/magnets/registry.py b/bluemira/magnets/registry.py deleted file mode 100644 index 8159ab3acd..0000000000 --- a/bluemira/magnets/registry.py +++ /dev/null @@ -1,445 +0,0 @@ -# SPDX-FileCopyrightText: 2021-present M. Coleman, J. Cook, F. Franza -# SPDX-FileCopyrightText: 2021-present I.A. Maione, S. McIntosh -# SPDX-FileCopyrightText: 2021-present J. Morris, D. Short -# -# SPDX-License-Identifier: LGPL-2.1-or-later -""" -Generic class and instance registration utilities. - -This module provides: -- RegistrableMeta: A metaclass that automatically registers classes into a specified -registry. -- InstanceRegistrable: A mixin that automatically registers instances into a specified -global cache. - -Intended for use in frameworks where automatic discovery of classes -and instances is required, such as for strands, cables, conductors, or other physical -models. - -Usage ------ -Classes intended to be registered must: -- Define a class-level `_registry_` dictionary (for class registration). -- Optionally set a `_name_in_registry_` string (custom name for registration). - -Instances intended to be globally tracked must: -- Inherit from InstanceRegistrable. -- Provide a unique `name` attribute at creation. -""" - -from abc import ABCMeta -from typing import ClassVar - -from bluemira.base.look_and_feel import bluemira_debug - - -# ------------------------------------------------------------------------------ -# RegistrableMeta -# ------------------------------------------------------------------------------ -class RegistrableMeta(ABCMeta): - """ - Metaclass for automatic class registration into a registry. - - Enforces that: - - '_name_in_registry_' must be explicitly defined in every class body (no - inheritance allowed). - - '_registry_' can be inherited if not redefined. - """ - - def __new__(mcs, name, bases, namespace): - """ - Create and register a new class instance using the RegistrableMeta metaclass. - - This method: - - Automatically registers concrete (non-abstract) classes into a specified - registry. - - Enforces that concrete classes explicitly declare a '_name_in_registry_'. - - Allows '_registry_' to be inherited from base classes if not redefined. - - Parameters - ---------- - mcs : type - The metaclass (usually RegistrableMeta itself). - name : str - The name of the class being created. - bases : tuple of type - The base classes of the class being created. - namespace : dict - The attribute dictionary of the class. - - Returns - ------- - type - The newly created class. - - Raises - ------ - TypeError - If a concrete class does not define a '_name_in_registry_'. - If no '_registry_' can be found (either defined or inherited). - ValueError - If a duplicate '_name_in_registry_' is detected within the registry. - - Notes - ----- - Abstract base classes (ABCs) are exempted from registration requirements. - - Registration process: - - If the class is abstract, skip registration. - - Otherwise: - - Check for existence of '_registry_' (allow inheritance). - - Require explicit '_name_in_registry_' (must be defined in the class body). - - Insert the class into the registry under the specified name. - """ - bluemira_debug(f"Registering {name}...") # Debug print - - cls = super().__new__(mcs, name, bases, namespace) - - is_abstract = bool(getattr(cls, "__abstractmethods__", False)) - - if not is_abstract: - # Only enforce _name_in_registry_ and _registry_ for concrete classes - # _registry_ can be inherited - registry = getattr(cls, "_registry_", None) - - # _name_in_registry_ must be explicit in the class body - register_name = namespace.get("_name_in_registry_", None) - - # Checks - if registry is None: - raise TypeError( - f"Class {name} must define or inherit a '_registry_' for " - f"registration." - ) - - if register_name is None: - raise TypeError( - f"Class {name} must explicitly define a '_name_in_registry_' for " - f"registration." - ) - - # Registration - if register_name: - if register_name in registry: - raise ValueError( - f"Duplicate registration for class '{register_name}'." - ) - registry[register_name] = cls - cls._name_in_registry_ = register_name # Optional: for introspection - - return cls - - @classmethod - def unregister(cls): - """ - Unregister the class from its associated registry. - - This method removes the class from the `_registry_` dictionary under its - '_name_in_registry_' key. It is safe to call at runtime to dynamically - de-register classes, such as when reloading modules or cleaning up. - - Raises - ------ - AttributeError - If the class does not have a '_registry_' or '_name_in_registry_' attribute. - KeyError - If the class is not found in the registry (already unregistered or never - registered). - - Notes - ----- - - Only registered (non-abstract) classes have the 'unregister' method. - - Abstract base classes (ABCs) are skipped and do not perform registration. - """ - registry = getattr(cls, "_registry_", None) - name_in_registry = getattr(cls, "_name_in_registry_", None) - - if registry is None or name_in_registry is None: - raise AttributeError( - "Cannot unregister: missing '_registry_' or '_name_in_registry_'." - ) - - try: - del registry[name_in_registry] - except KeyError: - raise KeyError( - f"Class '{name_in_registry}' not found in the registry; it may have " - f"already been unregistered." - ) from None - - @classmethod - def get_registered_class(cls, name: str): - """ - Retrieve a registered class by name. - - Parameters - ---------- - name : str - Name of the registered class. - - Returns - ------- - type or None - The registered class, or None if not found. - - Raises - ------ - AttributeError - If the class does not define or inherit a '_registry_' attribute. - """ - registry = getattr(cls, "_registry_", None) - if registry is None: - raise AttributeError( - f"Class {cls.__name__} must define or inherit a '_registry_' attribute." - ) - return registry.get(name) - - @classmethod - def list_registered_classes(cls) -> list[str]: - """ - List names of all registered classes. - - Returns - ------- - list of str - List of names of registered classes. - - Raises - ------ - AttributeError - If the class does not define or inherit a '_registry_' attribute. - """ - registry = getattr(cls, "_registry_", None) - if registry is None: - raise AttributeError( - f"Class {cls.__name__} must define or inherit a '_registry_' attribute." - ) - return list(registry.keys()) - - @classmethod - def clear_registered_classes(cls): - """ - Clear all registered classes from the registry. - - This method removes all entries from the `_registry_` dictionary, - effectively unregistering all previously registered classes. - - Raises - ------ - AttributeError - If the class does not define or inherit a '_registry_' attribute. - """ - registry = getattr(cls, "_registry_", None) - if registry is None: - raise AttributeError( - f"Class {cls.__name__} must define or inherit a '_registry_' attribute." - ) - registry.clear() - - -# ------------------------------------------------------------------------------ -# InstanceRegistrable -# ------------------------------------------------------------------------------ - - -class InstanceRegistrable: - """ - Mixin class to automatically register instances into a global instance cache. - - This class provides: - - Automatic instance registration into a global cache. - - Optional control over registration (register or not). - - Optional automatic generation of unique names to avoid conflicts. - - Attributes - ---------- - _global_instance_cache_ : dict - Class-level cache shared among all instances for lookup by name. - _do_not_register : bool - If True, the instance will not be registered. - _unique : bool - If True, automatically generate a unique name if the desired name already exists. - """ - - _global_instance_cache_: ClassVar[dict] = {} - - def __init__(self, name: str, *, unique_name: bool = False): - """ - Initialize an instance and optionally register it. - - Parameters - ---------- - name : str - Desired name of the instance. - unique_name : bool, optional - If True, generate a unique name if the given name already exists. - If False (strict mode), raise a ValueError on duplicate names. - """ - self._unique_name = None - self.unique_name = unique_name - - # Setting the name will trigger registration (unless do_registration is False) - self._name = None - self.name = name - - @property - def unique_name(self) -> bool: - """ - Flag indicating whether to automatically generate a unique name on conflict. - - Returns - ------- - bool - True if automatic unique name generation is enabled (the passed name is - neglected) - False if strict name checking is enforced. - """ - return self._unique_name - - @unique_name.setter - def unique_name(self, value: bool): - """ - Set whether automatic unique name generation should be enabled. - - Parameters - ---------- - value : bool - If True, automatically generate a unique name if the desired name - is already registered. - If False, raise a ValueError if the name already exists. - """ - self._unique_name = value - - @property - def name(self) -> str: - """Return the instance name.""" - return self._name - - @name.setter - def name(self, value: str): - """ - Set the instance name and (re)register it according to registration rules. - - Behavior - -------- - - If `_do_not_register` is True, just assign the name without caching. - - If `unique_name` is True and the name already exists, automatically generate - a unique name. - - If `unique_name` is False and the name already exists, raise ValueError. - - Parameters - ---------- - value : str - Desired instance name. - - Raises - ------ - ValueError - If `unique_name` is False and the name is already registered. - """ - if hasattr(self, "_name") and self._name is not None: - self._unregister_self() - - if value is None: - self._name = None - return - - if value in self._global_instance_cache_: - if self.unique_name: - value = self.generate_unique_name(value) - else: - raise ValueError(f"Instance with name '{value}' already registered.") - - self._name = value - self._register_self() - - def _register_self(self): - """ - Register this instance into the global instance cache. - - Raises - ------ - AttributeError - If the instance does not have a 'name' attribute. - ValueError - If an instance with the same name already exists and unique is False. - """ - if getattr(self, "_do_not_register", False): - return # Skip registration if explicitly disabled - - if not hasattr(self, "name") or self.name is None: - raise AttributeError("Instance must have a 'name' attribute to register.") - - if self.name in self._global_instance_cache_: - if self.unique_name: - self.name = self.generate_unique_name(self.name) - else: - raise ValueError(f"Instance with name '{self.name}' already registered.") - - self._global_instance_cache_[self.name] = self - - def _unregister_self(self): - """ - Unregister this instance from the global instance cache. - """ - if hasattr(self, "name") and self.name in self._global_instance_cache_: - del self._global_instance_cache_[self.name] - - @classmethod - def get_registered_instance(cls, name: str): - """ - Retrieve a registered instance by name. - - Parameters - ---------- - name : str - Name of the registered instance. - - Returns - ------- - InstanceRegistrable or None - The registered instance, or None if not found. - """ - return cls._global_instance_cache_.get(name) - - @classmethod - def list_registered_instances(cls) -> list[str]: - """ - List names of all registered instances. - - Returns - ------- - list of str - List of names of registered instances. - """ - return list(cls._global_instance_cache_.keys()) - - @classmethod - def clear_registered_instances(cls): - """ - Clear all registered instances from the global cache. - """ - cls._global_instance_cache_.clear() - - @classmethod - def generate_unique_name(cls, base_name: str) -> str: - """ - Generate a unique name by appending a numeric suffix if necessary. - - Parameters - ---------- - base_name : str - Desired base name. - - Returns - ------- - str - Unique name guaranteed not to conflict with existing instances. - """ - if base_name not in cls._global_instance_cache_: - return base_name - - i = 1 - while f"{base_name}_{i}" in cls._global_instance_cache_: - i += 1 - return f"{base_name}_{i}" diff --git a/bluemira/magnets/strand.py b/bluemira/magnets/strand.py index 9cd3788385..b6346b9af1 100644 --- a/bluemira/magnets/strand.py +++ b/bluemira/magnets/strand.py @@ -27,18 +27,8 @@ from bluemira.display.plotter import PlotOptions from bluemira.geometry.face import BluemiraFace from bluemira.geometry.tools import make_circle -from bluemira.magnets.registry import RegistrableMeta -# ------------------------------------------------------------------------------ -# Global Registries -# ------------------------------------------------------------------------------ - -STRAND_REGISTRY = {} - -# ------------------------------------------------------------------------------ -# Strand Class -# ------------------------------------------------------------------------------ @dataclass class StrandParams(ParameterFrame): """ @@ -51,7 +41,7 @@ class StrandParams(ParameterFrame): """Operating temperature [K].""" -class Strand(metaclass=RegistrableMeta): +class Strand: """ Represents a strand with a circular cross-section, composed of a homogenized mixture of materials. @@ -59,7 +49,6 @@ class Strand(metaclass=RegistrableMeta): This class automatically registers itself and its instances. """ - _registry_ = STRAND_REGISTRY _name_in_registry_ = "Strand" param_cls: type[StrandParams] = StrandParams diff --git a/bluemira/magnets/winding_pack.py b/bluemira/magnets/winding_pack.py index 9a54855b46..9665232768 100644 --- a/bluemira/magnets/winding_pack.py +++ b/bluemira/magnets/winding_pack.py @@ -16,10 +16,6 @@ from bluemira.base.parameter_frame import Parameter, ParameterFrame from bluemira.base.parameter_frame.typed import ParameterFrameLike from bluemira.magnets.conductor import Conductor, create_conductor_from_dict -from bluemira.magnets.registry import RegistrableMeta - -# Global registries -WINDINGPACK_REGISTRY = {} @dataclass @@ -34,7 +30,7 @@ class WindingPackParams(ParameterFrame): """Number of conductors along the y-axis.""" -class WindingPack(metaclass=RegistrableMeta): +class WindingPack: """ Represents a winding pack composed of a grid of conductors. @@ -48,7 +44,6 @@ class WindingPack(metaclass=RegistrableMeta): Number of conductors along the y-axis. """ - _registry_: ClassVar[dict] = WINDINGPACK_REGISTRY _name_in_registry_: ClassVar[str] = "WindingPack" param_cls: type[WindingPackParams] = WindingPackParams diff --git a/bluemira/magnets/winding_pack_.py b/bluemira/magnets/winding_pack_.py new file mode 100644 index 0000000000..b5e1a6eb28 --- /dev/null +++ b/bluemira/magnets/winding_pack_.py @@ -0,0 +1,266 @@ +from bluemira.base.constants import MU_0_2PI +from bluemira.base.designer import Designer +from bluemira.base.look_and_feel import bluemira_print +from bluemira.base.parameter_frame import ParameterFrame +from bluemira.geometry.wire import BluemiraWire +from bluemira.magnets.cable import RectangularCable +from bluemira.magnets.case_tf import TrapezoidalCaseTF +from bluemira.magnets.conductor import SymmetricConductor +from bluemira.magnets.utils import delayed_exp_func +from bluemira.magnets.winding_pack import WindingPack +from bluemira.utilities.tools import get_class_from_module + + +class WindingPackDesignerParams(ParameterFrame): + R0 = 8.6 # [m] major machine radius + B0 = 4.39 # [T] magnetic field @R0 + A = 2.8 # machine aspect ratio + n_TF = 16 # number of TF coils + ripple = 6e-3 # requirement on the maximum plasma ripple + a = R0 / A # minor radius + d = 1.82 # additional distance to calculate the max external radius of the inner TF leg + Iop = 70.0e3 # operational current in each conductor + T_sc = 4.2 # operational temperature of superconducting cable + T_margin = 1.5 # temperature margin + t_delay = 3 # [s] + t0 = 0 # [s] + hotspot_target_temperature = 250.0 # [K] + R_VV = Ri * 1.05 # Vacuum vessel radius + S_VV = 100e6 # Vacuum vessel steel limit + d_strand_sc = 1.0e-3 + d_strand_stab = 1.0e-3 + + # allowable stress values + safety_factor = 1.5 * 1.3 + + dx = 0.05 # cable length... just a dummy value + B_ref = 15 # [T] Reference B field value (limit for LTS) + + +def B_TF_r(I_TF, n_TF, r): + """ + Compute the magnetic field generated by the TF coils, including ripple correction. + + Parameters + ---------- + I_TF : float + Toroidal field coil current [A]. + n_TF : int + Number of toroidal field coils. + r : float + Radial position from the tokamak center [m]. + + Returns + ------- + float + Magnetic field intensity [T]. + """ + return 1.08 * (MU_0_2PI * n_TF * I_TF / r) + + +class WindingPackDesigner(Designer[BluemiraWire]): + def run(self) -> BluemiraWire: + dr_plasma_side = R0 * 2 / 3 * 1e-2 # thickness of the plate before the WP + Ri = R0 - a - d # [m] max external radius of the internal TF leg + Re = (R0 + a) * (1 / ripple) ** ( + 1 / n_TF + ) # [m] max internal radius of the external TF leg + I_TF = B0 * R0 / MU_0_2PI / n_TF # total current in each TF coil + + # max magnetic field on the inner TF leg + + B_TF_i = B_TF_r(I_TF, n_TF, Ri) + + # magnetic pressure on the inner TF leg + pm = B_TF_i**2 / (2 * MU_0) + + # vertical tension acting on the equatorial section of inner TF leg + # i.e. half of the whole F_Z + t_z = 0.5 * np.log(Re / Ri) * MU_0_4PI * n_TF * I_TF**2 + + n_cond = np.floor(I_TF / Iop) # minimum number of conductors + bluemira_print(f"Total number of conductor: {n_cond}") + S_Y = 1e9 / safety_factor # [Pa] steel allowable limit + + # inductance (here approximated... better estimation in bluemira) + L = ( + MU_0 + * R0 + * (n_TF * n_cond) ** 2 + * (1 - np.sqrt(1 - (R0 - Ri) / R0)) + / n_TF + * 1.1 + ) + # Magnetic energy + Wm = 1 / 2 * L * n_TF * Iop**2 * 1e-9 + # Maximum tension... (empirical formula from Lorenzo... find a generic equation) + V_MAX = (7 * R0 - 3) / 6 * 1.1e3 + # Discharge characteristic times + Tau_discharge1 = L * Iop / V_MAX + Tau_discharge2 = B0 * I_TF * n_TF * (R0 / A) ** 2 / (R_VV * S_VV) + # Discharge characteristic time to be considered in the following + Tau_discharge = max([Tau_discharge1, Tau_discharge2]) + tf = Tau_discharge + bluemira_print(f"Maximum TF discharge time: {tf}") + + I_fun = delayed_exp_func(Iop, Tau_discharge, t_delay) + B_fun = delayed_exp_func(B_TF_i, Tau_discharge, t_delay) + + # Create a time array from 0 to 3*Tau_discharge + t = np.linspace(0, 3 * Tau_discharge, 500) + I_data = np.array([I_fun(t_i) for t_i in t]) + B_data = np.array([B_fun(t_i) for t_i in t]) + T_op = T_sc + T_margin # temperature considered for the superconducting cable + + stab_strand_config = self.build_config.get("stabilising_strand") + stab_strand_cls = get_class_from_module(stab_strand_config["class"]) + stab_strand = stab_strand_cls( + name=stab_strand_config.get("name"), + d_strand=self.params.d_strand_stab, + temperature=T_op, + material=stab_strand_config.get("material"), + ) + + sc_strand_config = self.build_config.get("superconducting_strand") + sc_strand_cls = get_class_from_module(sc_strand_config["class"]) + sc_strand = sc_strand_cls( + name=sc_strand_config.get("name"), + d_strand=self.params.d_strand_sc, + temperature=T_op, + material=sc_strand_config.get("material"), + ) + + Ic_sc = sc_strand.Ic(B=B_TF_i, temperature=(T_op)) + n_sc_strand = int(np.ceil(Iop / Ic_sc)) + + if B_TF_i < B_ref: + name = cable_name + "LTS" + E = 0.1e9 + else: + name = cable_name + "HTS" + E = 120e9 + + cable = RectangularCable( + name, + dx, + sc_strand=sc_strand, + stab_strand=stab_strand, + n_sc_strand=n_sc_strand, + n_stab_strand=500, + d_cooling_channel=1e-2, + void_fraction=0.7, + cos_theta=0.97, + E=E, + ) + + T_for_hts = T_op + cable_out = cable.optimise_n_stab_ths( + t0, + tf, + T_for_hts, + hotspot_target_temperature, + B_fun, + I_fun, + bounds=[1, 10000], + ) + conductor = SymmetricConductor( + cable=cable_out, + mat_jacket=ss316, + mat_ins=dummy_insulator, + dx_jacket=0.01, + dx_ins=1e-3, + ) + winding_pack = WindingPack( + conductor, 1, 1, name=None + ) # just a dummy WP to create the case + case = TrapezoidalCaseTF( + Ri=Ri, + dy_ps=dr_plasma_side, + dy_vault=0.7, + theta_TF=360 / n_TF, + mat_case=ss316, + WPs=[winding_pack], + ) + conductor_arrangement = case.rearrange_conductors_in_wp( + n_conductors=n_cond, + wp_reduction_factor=wp_reduction_factor, + min_gap_x=min_gap_x, + n_layers_reduction=n_layers_reduction, + layout=layout, + ) + case_out = case.optimize_jacket_and_vault( + pm=pm, + fz=t_z, + temperature=T_op, + B=B_TF_i, + allowable_sigma=S_Y, + bounds_cond_jacket=bounds_cond_jacket, + bounds_dy_vault=bounds_dy_vault, + layout=layout, + wp_reduction_factor=wp_reduction_factor, + min_gap_x=min_gap_x, + n_layers_reduction=n_layers_reduction, + max_niter=max_niter, + eps=err, + n_conds=n_cond, + ) + + +def plot_cable_temperature_evolution(result, t0, tf, ax, n_steps=100): + solution = result.solution + + ax.plot(solution.t, solution.y[0], "r*", label="Simulation points") + time_steps = np.linspace(t0, tf, n_steps) + ax.plot(time_steps, solution.sol(time_steps)[0], "b", label="Interpolated curve") + ax.grid(visible=True) + ax.set_ylabel("Temperature [K]", fontsize=10) + ax.set_title("Quench temperature evolution", fontsize=11) + ax.legend(fontsize=9) + + ax.tick_params(axis="y", labelcolor="k", labelsize=9) + + props = {"boxstyle": "round", "facecolor": "white", "alpha": 0.8} + ax.text( + 0.65, + 0.5, + result.info_text, + transform=ax.transAxes, + fontsize=9, + verticalalignment="top", + bbox=props, + ) + ax.figure.tight_layout() + + +def plot_I_B(I_fun, B_fun, t0, tf, ax, n_steps=300): + time_steps = np.linspace(t0, tf, n_steps) + I_values = [I_fun(t) for t in time_steps] # noqa: N806 + B_values = [B_fun(t) for t in time_steps] + + ax.plot(time_steps, I_values, "g", label="Current [A]") + ax.set_ylabel("Current [A]", color="g", fontsize=10) + ax.tick_params(axis="y", labelcolor="g", labelsize=9) + ax.grid(visible=True) + + ax_right = ax.twinx() + ax_right.plot(time_steps, B_values, "m--", label="Magnetic field [T]") + ax_right.set_ylabel("Magnetic field [T]", color="m", fontsize=10) + ax_right.tick_params(axis="y", labelcolor="m", labelsize=9) + + # Labels + ax.set_xlabel("Time [s]", fontsize=10) + ax.tick_params(axis="x", labelsize=9) + + # Combined legend for both sides + lines, labels = ax.get_legend_handles_labels() + lines2, labels2 = ax_right.get_legend_handles_labels() + ax.legend(lines + lines2, labels + labels2, loc="best", fontsize=9) + + ax.figure.tight_layout() + + +def plot_summary(result, t0, tf, I_fun, B_fun, n_steps, show=False): + f, (ax_temp, ax_ib) = plt.subplots(2, 1, figsize=(8, 8), sharex=True) + plot_cable_temperature_evolution(result, t0, tf, ax_temp, n_steps) + plot_I_B(I_fun, B_fun, t0, tf, ax_ib, n_steps * 3) + return f diff --git a/examples/magnets/example_tf_wp_from_dict.py b/examples/magnets/example_tf_wp_from_dict.py index 2ad3ae77e6..985a03968f 100644 --- a/examples/magnets/example_tf_wp_from_dict.py +++ b/examples/magnets/example_tf_wp_from_dict.py @@ -31,13 +31,14 @@ "name_in_registry": "SymmetricConductor", "name": "SymmetricConductor", "cable": { - "name_in_registry": "DummyRectangularCableLTS", - "name": "DummyRectangularCableLTS", + "name_in_registry": "RectangularCable", + "name": "RectangularCableLTS", "n_sc_strand": 321, "n_stab_strand": 476, "d_cooling_channel": 0.01, "void_fraction": 0.7, "cos_theta": 0.97, + "E": 0.1e9, "sc_strand": { "name_in_registry": "SuperconductingStrand", "name": "Nb3Sn_strand", @@ -73,13 +74,14 @@ "name_in_registry": "SymmetricConductor", "name": "SymmetricConductor", "cable": { - "name_in_registry": "DummyRectangularCableLTS", - "name": "DummyRectangularCableLTS", + "name_in_registry": "RectangularCable", + "name": "RectangularCableLTS", "n_sc_strand": 321, "n_stab_strand": 476, "d_cooling_channel": 0.01, "void_fraction": 0.7, "cos_theta": 0.97, + "E": 0.1e9, "sc_strand": { "name_in_registry": "SuperconductingStrand", "name": "Nb3Sn_strand", diff --git a/examples/magnets/example_tf_wp_optimization.py b/examples/magnets/example_tf_wp_optimization.py index cdbc0c7bf6..7fe2b0496b 100644 --- a/examples/magnets/example_tf_wp_optimization.py +++ b/examples/magnets/example_tf_wp_optimization.py @@ -31,10 +31,7 @@ from bluemira.base.constants import MU_0, MU_0_2PI, MU_0_4PI from bluemira.base.look_and_feel import bluemira_print -from bluemira.magnets.cable import ( - DummyRectangularCableHTS, - DummyRectangularCableLTS, -) +from bluemira.magnets.cable import RectangularCable from bluemira.magnets.case_tf import TrapezoidalCaseTF from bluemira.magnets.conductor import SymmetricConductor from bluemira.magnets.init_magnets_registry import register_all_magnets @@ -273,7 +270,8 @@ def B_TF_r(I_TF, n_TF, r): B_ref = 15 # [T] Reference B field value (limit for LTS) if B_TF_i < B_ref: - cable = DummyRectangularCableLTS( + cable = RectangularCable( + name="RectangularCableLTS", dx=dx, sc_strand=sc_strand, stab_strand=stab_strand, @@ -282,9 +280,11 @@ def B_TF_r(I_TF, n_TF, r): d_cooling_channel=1e-2, void_fraction=0.7, cos_theta=0.97, + E=0.1e9, ) else: - cable = DummyRectangularCableHTS( + cable = RectangularCable( + name="RectangularCableHTS", dx=dx, sc_strand=sc_strand, stab_strand=stab_strand, @@ -293,6 +293,7 @@ def B_TF_r(I_TF, n_TF, r): d_cooling_channel=1e-2, void_fraction=0.7, cos_theta=0.97, + E=120e9, ) cable.plot(0, 0, show=True) bluemira_print(f"cable area: {cable.area}") From b1c8444bea13273321c0dd2a92f0094301edc3f4 Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Wed, 27 Aug 2025 08:32:36 +0100 Subject: [PATCH 16/61] =?UTF-8?q?=F0=9F=8E=A8=20Docstrings=20etc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bluemira/magnets/cable.py | 306 ++++++++++++++------------- bluemira/magnets/case_tf.py | 335 +++++++++++++++--------------- bluemira/magnets/conductor.py | 215 +++++++++---------- bluemira/magnets/fatigue.py | 36 ++-- bluemira/magnets/strand.py | 107 +++++----- bluemira/magnets/utils.py | 35 ++-- bluemira/magnets/winding_pack.py | 57 +++-- bluemira/magnets/winding_pack_.py | 55 ++--- 8 files changed, 580 insertions(+), 566 deletions(-) diff --git a/bluemira/magnets/cable.py b/bluemira/magnets/cable.py index 9b85ce8e4a..940e641e72 100644 --- a/bluemira/magnets/cable.py +++ b/bluemira/magnets/cable.py @@ -6,10 +6,11 @@ """Cable class""" +from __future__ import annotations + from abc import ABC, abstractmethod -from collections.abc import Callable from dataclasses import dataclass -from typing import Any +from typing import TYPE_CHECKING, Any import matplotlib.pyplot as plt import numpy as np @@ -17,9 +18,13 @@ from scipy.integrate import solve_ivp from scipy.optimize import minimize_scalar -from bluemira.base.look_and_feel import bluemira_error, bluemira_print, bluemira_warn +from bluemira.base.look_and_feel import ( + bluemira_debug, + bluemira_error, + bluemira_print, + bluemira_warn, +) from bluemira.base.parameter_frame import Parameter, ParameterFrame -from bluemira.base.parameter_frame.typed import ParameterFrameLike from bluemira.magnets.strand import ( Strand, SuperconductingStrand, @@ -27,6 +32,11 @@ ) from bluemira.magnets.utils import reciprocal_summation, summation +if TYPE_CHECKING: + from collections.abc import Callable + + from bluemira.base.parameter_frame.typed import ParameterFrameLike + @dataclass class CableParams(ParameterFrame): @@ -80,9 +90,9 @@ def __init__( Parameters ---------- - sc_strand : SuperconductingStrand + sc_strand: The superconducting strand. - stab_strand : Strand + stab_strand: The stabilizer strand. params: Structure containing the input parameters. Keys are: @@ -94,7 +104,7 @@ def __init__( See :class:`~bluemira.magnets.cable.CableParams` for parameter details. - name : str + name: Identifier for the cable instance. """ super().__init__(params) # fix when split into builders and designers @@ -105,9 +115,24 @@ def __init__( self.sc_strand = sc_strand self.stab_strand = stab_strand + youngs_modulus: Callable[[Any, OperationalConditions], float] | float | None = ( + props.pop("E", None) + ) + if youngs_modulus is not None: + if "E" in vars(type(self)): + bluemira_debug("E already defined in class, ignoring") + else: + self.E = ( + youngs_modulus + if callable(youngs_modulus) + else lambda self, op_cond, v=youngs_modulus: youngs_modulus + ) + for k, v in props.items(): setattr(self, k, v if callable(v) else lambda *arg, v=v, **kwargs: v) # noqa: ARG005 - self._props = list(props.keys()) + self._props = list(props.keys()) + ( + [] if "E" in vars(type(self)) or youngs_modulus is None else ["E"] + ) @property @abstractmethod @@ -132,7 +157,7 @@ def rho(self, op_cond: OperationalConditions): Parameters ---------- - op_cond: OperationalConditions + op_cond: Operational conditions including temperature, magnetic field, and strain at which to calculate the material property. @@ -146,20 +171,21 @@ def rho(self, op_cond: OperationalConditions): + self.stab_strand.rho(op_cond) * self.area_stab ) / (self.area_sc + self.area_stab) - def erho(self, op_cond: OperationalConditions): + def erho(self, op_cond: OperationalConditions) -> float: """ Computes the cable's equivalent resistivity considering the resistance of its strands in parallel. Parameters ---------- - op_cond: OperationalConditions + op_cond: Operational conditions including temperature, magnetic field, and strain at which to calculate the material property. Returns ------- - float [Ohm m] + : + resistivity [Ohm m] """ resistances = np.array([ self.sc_strand.erho(op_cond) / self.area_sc, @@ -175,13 +201,14 @@ def Cp(self, op_cond: OperationalConditions): # noqa: N802 Parameters ---------- - op_cond: OperationalConditions + op_cond: Operational conditions including temperature, magnetic field, and strain at which to calculate the material property. Returns ------- - float [J/K/m] + : + Specific heat capacity [J/K/m] """ weighted_specific_heat = np.array([ self.sc_strand.Cp(op_cond) * self.area_sc * self.sc_strand.rho(op_cond), @@ -195,28 +222,28 @@ def Cp(self, op_cond: OperationalConditions): # noqa: N802 ) @property - def area_stab(self): + def area_stab(self) -> float: """Area of the stabilizer region""" return self.stab_strand.area * self.params.n_stab_strand.value @property - def area_sc(self): + def area_sc(self) -> float: """Area of the superconductor region""" return self.sc_strand.area * self.params.n_sc_strand.value @property - def area_cc(self): + def area_cc(self) -> float: """Area of the cooling channel""" return self.params.d_cooling_channel.value**2 / 4 * np.pi @property - def area(self): + def area(self) -> float: """Area of the cable considering the void fraction""" return ( self.area_sc + self.area_stab ) / self.params.void_fraction.value / self.params.cos_theta.value + self.area_cc - def E(self, op_cond: OperationalConditions): # noqa: N802 + def E(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ Return the effective Young's modulus of the cable [Pa]. @@ -225,13 +252,13 @@ def E(self, op_cond: OperationalConditions): # noqa: N802 Parameters ---------- - op_cond: OperationalConditions + op_cond: Operational conditions including temperature, magnetic field, and strain at which to calculate the material property. Returns ------- - float + : Default Young's modulus (0). """ raise NotImplementedError("E for Cable is not implemented.") @@ -242,25 +269,25 @@ def _heat_balance_model_cable( temperature: float, B_fun: Callable, I_fun: Callable, # noqa: N803 - ): + ) -> float: """ Calculate the derivative of temperature (dT/dt) for a 0D heat balance problem. Parameters ---------- - t : float - The current time in seconds. - temperature : float - The current temperature in Celsius. - B_fun : Callable - The magnetic field [T] as time function - I_fun : Callable - The current [A] flowing through the conductor as time function + t: + The current time in seconds. + temperature: + The current temperature in Celsius. + B_fun: + The magnetic field [T] as time function + I_fun: + The current [A] flowing through the conductor as time function Returns ------- - dTdt : float - The derivative of temperature with respect to time (dT/dt). + : + The derivative of temperature with respect to time (dT/dt). """ # Calculate the rate of heat generation (Joule dissipation) if isinstance(temperature, np.ndarray): @@ -305,9 +332,9 @@ def optimise_n_stab_ths( tf: float, initial_temperature: float, target_temperature: float, - B_fun: Callable, - I_fun: Callable, # noqa: N803 - bounds: np.ndarray = None, + B_fun: Callable[[float], float], + I_fun: Callable[[float], float], # noqa: N803 + bounds: np.ndarray | None = None, *, show: bool = False, ): @@ -336,7 +363,7 @@ def optimise_n_stab_ths( Returns ------- - result : scipy.optimize.OptimizeResult + : The result of the optimization process. Raises @@ -356,9 +383,9 @@ def final_temperature_difference( tf: float, initial_temperature: float, target_temperature: float, - B_fun: Callable, - I_fun: Callable, # noqa: N803 - ): + B_fun: Callable[[float], float], + I_fun: Callable[[float], float], # noqa: N803 + ) -> float: """ Compute the absolute temperature difference at final time between the simulated and target temperatures. @@ -370,24 +397,24 @@ def final_temperature_difference( Parameters ---------- - n_stab : int + n_stab: Number of stabilizer strands to set temporarily for this simulation. - t0 : float + t0: Initial time of the simulation [s]. - tf : float + tf: Final time of the simulation [s]. - initial_temperature : float + initial_temperature: Temperature at the start of the simulation [K]. - target_temperature : float + target_temperature: Desired temperature at the end of the simulation [K]. - B_fun : Callable + B_fun: Magnetic field as a time-dependent function [T]. - I_fun : Callable + I_fun: Current as a time-dependent function [A]. Returns ------- - float + : Absolute difference between the simulated final temperature and the target temperature [K]. @@ -472,7 +499,9 @@ def Kx(self, op_cond: OperationalConditions): # noqa: N802 def Ky(self, op_cond: OperationalConditions): # noqa: N802 """Total equivalent stiffness along y-axis""" - def plot(self, xc: float = 0, yc: float = 0, *, show: bool = False, ax=None): + def plot( + self, xc: float = 0, yc: float = 0, *, show: bool = False, ax=plt.Axes | None + ): """ Plot a schematic view of the cable cross-section. @@ -483,20 +512,20 @@ def plot(self, xc: float = 0, yc: float = 0, *, show: bool = False, ax=None): Parameters ---------- - xc : float, optional + xc: x-coordinate of the cable center in the plot [m]. Default is 0. - yc : float, optional + yc: y-coordinate of the cable center in the plot [m]. Default is 0. - show : bool, optional + show: If True, the plot is rendered immediately with `plt.show()`. Default is False. - ax : matplotlib.axes.Axes or None, optional + ax: The matplotlib Axes object to draw on. If None, a new figure and Axes are created internally. Returns ------- - matplotlib.axes.Axes + : The Axes object with the cable plot, which can be further customized or saved. @@ -539,7 +568,7 @@ def plot(self, xc: float = 0, yc: float = 0, *, show: bool = False, ax=None): plt.show() return ax - def __str__(self): + def __str__(self) -> str: """ Return a human-readable summary of the cable configuration. @@ -548,7 +577,7 @@ def __str__(self): Returns ------- - str + : A formatted multiline string describing the cable. """ return ( @@ -568,7 +597,7 @@ def __str__(self): f"n stab strand: {self.params.n_stab_strand.value}" ) - def to_dict(self) -> dict: + def to_dict(self) -> dict[str, str | float | int | dict[str, Any]]: """ Serialize the cable instance to a dictionary. @@ -578,9 +607,6 @@ def to_dict(self) -> dict: Dictionary containing cable and strand configuration. """ return { - "name_in_registry": getattr( - self, "_name_in_registry_", self.__class__.__name__ - ), "name": self.name, "n_sc_strand": self.params.n_sc_strand.value, "n_stab_strand": self.params.n_stab_strand.value, @@ -597,23 +623,21 @@ def from_dict( cls, cable_dict: dict[str, Any], name: str | None = None, - ) -> "ABCCable": + ) -> ABCCable: """ Deserialize a cable instance from a dictionary. Parameters ---------- - cls : type - Class to instantiate (Cable or subclass). - cable_dict : dict + cable_dict: Dictionary containing serialized cable data. - name : str + name: Name for the new instance. If None, attempts to use the 'name' field from the dictionary. Returns ------- - ABCCable + : Instantiated cable object. Raises @@ -696,9 +720,9 @@ def __init__( Parameters ---------- - sc_strand : SuperconductingStrand + sc_strand: Superconducting strand. - stab_strand : Strand + stab_strand: Stabilizer strand. params: Structure containing the input parameters. Keys are: @@ -711,7 +735,7 @@ def __init__( See :class:`~bluemira.magnets.cable.RectangularCableParams` for parameter details. - name : str, optional + name: Name of the cable props: extra properties @@ -725,63 +749,63 @@ def __init__( ) @property - def dx(self): + def dx(self) -> float: """Cable dimension in the x direction [m]""" return self.params.dx.value @property - def dy(self): + def dy(self) -> float: """Cable dimension in the y direction [m]""" return self.area / self.params.dx.value # Decide if this function shall be a setter. # Defined as "normal" function to underline that it modifies dx. - def set_aspect_ratio(self, value: float) -> None: + def set_aspect_ratio(self, value: float): """Modify dx in order to get the given aspect ratio""" self.params.dx.value = np.sqrt(value * self.area) # OD homogenized structural properties - def Kx(self, op_cond: OperationalConditions): # noqa: N802 + def Kx(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ Compute the total equivalent stiffness along the x-axis. Parameters ---------- - op_cond: OperationalConditions + op_cond: Operational conditions including temperature, magnetic field, and strain at which to calculate the material property. Returns ------- - float + : Homogenized stiffness in the x-direction [Pa]. """ - return self.E(op_cond) * self.dy / self.params.dx.value + return self.E(op_cond) * self.dy / self.params.dx.value - def Ky(self, op_cond: OperationalConditions): # noqa: N802 + def Ky(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ Compute the total equivalent stiffness along the y-axis. Parameters ---------- - op_cond: OperationalConditions + op_cond: Operational conditions including temperature, magnetic field, and strain at which to calculate the material property. Returns ------- - float + : Homogenized stiffness in the y-direction [Pa]. """ return self.E(op_cond) * self.params.dx.value / self.dy - def to_dict(self) -> dict: + def to_dict(self) -> dict[str, Any]: """ Serialize the rectangular cable into a dictionary. Returns ------- - dict + : Dictionary including rectangular cable parameters. """ data = super().to_dict() @@ -796,7 +820,7 @@ def from_dict( cls, cable_dict: dict[str, Any], name: str | None = None, - ) -> "RectangularCable": + ) -> RectangularCable: """ Deserialize a RectangularCable from a dictionary. @@ -809,17 +833,15 @@ def from_dict( Parameters ---------- - cls : type - Class to instantiate (Cable or subclass). - cable_dict : dict + cable_dict: Dictionary containing serialized cable data. - name : str + name: Name for the new instance. If None, attempts to use the 'name' field from the dictionary. Returns ------- - RectangularCable + : Instantiated rectangular cable object. Raises @@ -915,6 +937,7 @@ def __init__( stab_strand: Strand, params: ParameterFrameLike, name: str = "SquareCable", + **props, ): """ Representation of a square cable. @@ -952,61 +975,61 @@ def __init__( stab_strand=stab_strand, params=params, name=name, + **props, ) - # replace dx and dy with dl? @property - def dx(self): + def dx(self) -> float: """Cable dimension in the x direction [m]""" return np.sqrt(self.area) @property - def dy(self): + def dy(self) -> float: """Cable dimension in the y direction [m]""" return self.dx # OD homogenized structural properties - def Kx(self, op_cond: OperationalConditions): # noqa: N802 + def Kx(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ Compute the total equivalent stiffness along the x-axis. Parameters ---------- - op_cond: OperationalConditions + op_cond: Operational conditions including temperature, magnetic field, and strain at which to calculate the material property. Returns ------- - float + : Homogenized stiffness in the x-direction [Pa]. """ return self.E(op_cond) - def Ky(self, op_cond: OperationalConditions): # noqa: N802 + def Ky(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ Compute the total equivalent stiffness along the y-axis. Parameters ---------- - op_cond: OperationalConditions + op_cond: Operational conditions including temperature, magnetic field, and strain at which to calculate the material property. Returns ------- - float + : Homogenized stiffness in the y-direction [Pa]. """ return self.E(op_cond) - def to_dict(self) -> dict: + def to_dict(self) -> dict[str, Any]: """ Serialize the SquareCable. Returns ------- - dict + : Serialized dictionary. """ return super().to_dict() @@ -1016,23 +1039,21 @@ def from_dict( cls, cable_dict: dict[str, Any], name: str | None = None, - ) -> "SquareCable": + ) -> SquareCable: """ Deserialize a SquareCable from a dictionary. Parameters ---------- - cls : type - Class to instantiate (Cable or subclass). - cable_dict : dict + cable_dict: Dictionary containing serialized cable data. - name : str + name: Name for the new instance. If None, attempts to use the 'name' field from the dictionary. Returns ------- - SquareCable + : Instantiated square cable. Raises @@ -1041,15 +1062,6 @@ def from_dict( If unique_name is False and a duplicate name is detected in the instance cache. """ - name_in_registry = cable_dict.pop("name_in_registry", None) - expected_name_in_registry = getattr(cls, "_name_in_registry_", cls.__name__) - - if name_in_registry != expected_name_in_registry: - raise ValueError( - f"Cannot create {cls.__name__} from dictionary with name_in_registry " - f"'{name_in_registry}'. Expected '{expected_name_in_registry}'." - ) - sc_strand = create_strand_from_dict(strand_dict=cable_dict.pop("sc_strand")) stab_strand = create_strand_from_dict(strand_dict=cable_dict.pop("stab_strand")) @@ -1074,7 +1086,6 @@ class RoundCable(ABCCable): around a central cooling channel. """ - _name_in_registry_ = "RoundCable" param_cls: type[CableParams] = CableParams def __init__( @@ -1083,6 +1094,7 @@ def __init__( stab_strand: Strand, params: ParameterFrameLike, name: str = "RoundCable", + **props, ): """ Representation of a round cable @@ -1111,23 +1123,23 @@ def __init__( stab_strand=stab_strand, params=params, name=name, + **props, ) - # replace dx and dy with dr? @property - def dx(self): + def dx(self) -> float: """Cable dimension in the x direction [m] (i.e. cable's diameter)""" return np.sqrt(self.area * 4 / np.pi) @property - def dy(self): + def dy(self) -> float: """Cable dimension in the y direction [m] (i.e. cable's diameter)""" return self.dx # OD homogenized structural properties # A structural analysis should be performed to check how much the rectangular # approximation is fine also for the round cable. - def Kx(self, op_cond: OperationalConditions): # noqa: N802 + def Kx(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ Compute the equivalent stiffness of the cable along the x-axis. @@ -1137,18 +1149,18 @@ def Kx(self, op_cond: OperationalConditions): # noqa: N802 Parameters ---------- - op_cond: OperationalConditions + op_cond: Operational conditions including temperature, magnetic field, and strain at which to calculate the material property. Returns ------- - float + : Equivalent stiffness in the x-direction [Pa]. """ return self.E(op_cond) - def Ky(self, op_cond: OperationalConditions): # noqa: N802 + def Ky(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ Compute the equivalent stiffness of the cable along the y-axis. @@ -1158,36 +1170,43 @@ def Ky(self, op_cond: OperationalConditions): # noqa: N802 Parameters ---------- - op_cond: OperationalConditions + op_cond: Operational conditions including temperature, magnetic field, and strain at which to calculate the material property. Returns ------- - float + : Equivalent stiffness in the y-direction [Pa]. """ return self.E(op_cond) - def plot(self, xc: float = 0, yc: float = 0, *, show: bool = False, ax=None): + def plot( + self, + xc: float = 0, + yc: float = 0, + *, + show: bool = False, + ax: plt.Axes | None = None, + ) -> plt.Axes: """ Schematic plot of the cable cross-section. Parameters ---------- - xc : float, optional + xc: x-coordinate of the cable center [m]. Default is 0. - yc : float, optional + yc: y-coordinate of the cable center [m]. Default is 0. - show : bool, optional + show: If True, the plot is displayed immediately using `plt.show()`. Default is False. - ax : matplotlib.axes.Axes or None, optional + ax: Axis to plot on. If None, a new figure and axis are created. Returns ------- - matplotlib.axes.Axes + : The axis object containing the cable plot, useful for further customization or saving. """ @@ -1221,13 +1240,13 @@ def plot(self, xc: float = 0, yc: float = 0, *, show: bool = False, ax=None): plt.show() return ax - def to_dict(self) -> dict: + def to_dict(self) -> dict[str, Any]: """ Serialize the RoundCable. Returns ------- - dict + : Serialized dictionary. """ return super().to_dict() @@ -1237,23 +1256,21 @@ def from_dict( cls, cable_dict: dict[str, Any], name: str | None = None, - ) -> "RoundCable": + ) -> RoundCable: """ Deserialize a RoundCable from a dictionary. Parameters ---------- - cls : type - Class to instantiate (Cable or subclass). - cable_dict : dict + cable_dict: Dictionary containing serialized cable data. - name : str + name: Name for the new instance. If None, attempts to use the 'name' field from the dictionary. Returns ------- - RoundCable + : Instantiated square cable. Raises @@ -1262,15 +1279,6 @@ def from_dict( If unique_name is False and a duplicate name is detected in the instance cache. """ - name_in_registry = cable_dict.pop("name_in_registry", None) - expected_name_in_registry = getattr(cls, "_name_in_registry_", cls.__name__) - - if name_in_registry != expected_name_in_registry: - raise ValueError( - f"Cannot create {cls.__name__} from dictionary with name_in_registry " - f"'{name_in_registry}'. Expected '{expected_name_in_registry}'." - ) - sc_strand = create_strand_from_dict(strand_dict=cable_dict.pop("sc_strand")) stab_strand = create_strand_from_dict(strand_dict=cable_dict.pop("stab_strand")) @@ -1291,20 +1299,20 @@ def from_dict( def create_cable_from_dict( cable_dict: dict, name: str | None = None, -): +) -> ABCCable: """ Factory function to create a Cable or its subclass from a serialized dictionary. Parameters ---------- - cable_dict : dict + cable_dict: Dictionary with serialized cable data. Must include a 'name_in_registry' field. - name : str, optional + name: If given, overrides the name from the dictionary. Returns ------- - ABCCable + : Instantiated cable object. Raises diff --git a/bluemira/magnets/case_tf.py b/bluemira/magnets/case_tf.py index 33905a3947..d26fd80030 100644 --- a/bluemira/magnets/case_tf.py +++ b/bluemira/magnets/case_tf.py @@ -16,14 +16,15 @@ - Focused on the two-dimensional analysis of the inboard leg. """ +from __future__ import annotations + import math from abc import ABC, abstractmethod from dataclasses import dataclass +from typing import TYPE_CHECKING, Any import matplotlib.pyplot as plt import numpy as np -from matproplib import OperationalConditions -from matproplib.material import Material from scipy.optimize import minimize_scalar from bluemira.base.look_and_feel import ( @@ -33,12 +34,17 @@ bluemira_warn, ) from bluemira.base.parameter_frame import Parameter, ParameterFrame -from bluemira.base.parameter_frame.typed import ParameterFrameLike from bluemira.geometry.parameterisations import GeometryParameterisation from bluemira.geometry.wire import BluemiraWire from bluemira.magnets.utils import reciprocal_summation, summation from bluemira.magnets.winding_pack import WindingPack, create_wp_from_dict +if TYPE_CHECKING: + from matproplib import OperationalConditions + from matproplib.material import Material + + from bluemira.base.parameter_frame.typed import ParameterFrameLike + @dataclass class TFCaseGeometryParams(ParameterFrame): @@ -74,12 +80,12 @@ def dx_at_radius(self, radius: float) -> float: Parameters ---------- - radius : float + radius: Radial position at which to compute the toroidal width [m]. Returns ------- - float + : Toroidal width [m] at the given radius. """ return 2 * radius * np.tan(self.rad_theta_TF / 2) @@ -92,7 +98,7 @@ def area(self) -> float: Returns ------- - float + : Cross-sectional area [m²] enclosed by the case geometry. Notes @@ -101,22 +107,22 @@ def area(self) -> float: """ @abstractmethod - def plot(self, ax=None, *, show: bool = False) -> plt.Axes: + def plot(self, ax: plt.Axes = None, *, show: bool = False) -> plt.Axes: """ Plot the cross-sectional geometry of the TF case. Parameters ---------- - ax : matplotlib.axes.Axes, optional + ax: Axis on which to draw the geometry. If None, a new figure and axis are created. - show : bool, optional + show: If True, the plot is displayed immediately using plt.show(). Default is False. Returns ------- - matplotlib.axes.Axes + : The axis object containing the plot. Notes @@ -144,7 +150,7 @@ def area(self) -> float: Returns ------- - float + : Cross-sectional area [m²]. """ return ( @@ -162,7 +168,7 @@ def create_shape(self, label: str = "") -> BluemiraWire: Returns ------- - np.ndarray + : Array of shape (4, 2) representing the corners of the trapezoid. Coordinates are ordered counterclockwise starting from the top-left corner: [(-dx_outer/2, Ri), (dx_outer/2, Ri), (dx_inner/2, Rk), (-dx_inner/2, Rk)]. @@ -192,7 +198,7 @@ def area(self) -> float: Returns ------- - float + : Cross-sectional area [m²] defined by the wedge between outer radius Ri and inner radius Rk over the toroidal angle theta_TF. """ @@ -209,12 +215,12 @@ def create_shape(self, label: str = "", n_points: int = 50) -> BluemiraWire: Parameters ---------- - n_points : int, optional + n_points: Number of points to discretize each arc. Default is 50. Returns ------- - np.ndarray + : Array of (x, y) coordinates [m] describing the wedge polygon. """ theta1 = -self.rad_theta_TF / 2 @@ -235,9 +241,6 @@ def create_shape(self, label: str = "", n_points: int = 50) -> BluemiraWire: return np.vstack((arc_outer, arc_inner)) -# ------------------------------------------------------------------------------ -# CaseTF Class -# ------------------------------------------------------------------------------ @dataclass class TFCaseParams(ParameterFrame): """ @@ -261,9 +264,6 @@ class BaseCaseTF(CaseGeometry, ABC): Defines the universal properties common to all TF case geometries. """ - _registry_ = CASETF_REGISTRY - _name_in_registry_ = None - param_cls: type[TFCaseParams] = TFCaseParams def __init__( @@ -287,11 +287,11 @@ def __init__( See :class:`~bluemira.magnets.case_tf.TFCaseParams` for parameter details. - mat_case : Material + mat_case: Structural material assigned to the TF coil case. - WPs : list[WindingPack] + WPs: List of winding pack objects embedded inside the TF case. - name : str, optional + name: String identifier for the TF coil case instance (default is "BaseCaseTF"). """ super().__init__( @@ -313,7 +313,7 @@ def name(self) -> str: Returns ------- - str + : Human-readable label for the coil case instance. """ return self._name @@ -325,7 +325,7 @@ def name(self, value: str): Parameters ---------- - value : str + value: Case name. Raises @@ -343,7 +343,7 @@ def update_dy_vault(self, value: float): Parameters ---------- - value : float + value: Vault thickness [m]. """ self.params.dy_vault.value = value @@ -366,7 +366,7 @@ def dx_vault(self): Returns ------- - float + : Average length of the vault in the toroidal direction [m]. """ @@ -377,7 +377,7 @@ def mat_case(self) -> Material: Returns ------- - Material + : Material object providing mechanical and thermal properties. """ return self._mat_case @@ -389,7 +389,7 @@ def mat_case(self, value: Material): Parameters ---------- - value : Material + value: Material object. Raises @@ -407,7 +407,7 @@ def WPs(self) -> list[WindingPack]: # noqa: N802 Returns ------- - list of WindingPack + : Winding pack instances composing the internal coil layout. """ return self._WPs @@ -419,7 +419,7 @@ def WPs(self, value: list[WindingPack]): # noqa: N802 Parameters ---------- - value : list[WindingPack] + value: List containing only WindingPack objects. Raises @@ -437,7 +437,7 @@ def WPs(self, value: list[WindingPack]): # noqa: N802 self.update_dy_vault(self.params.dy_vault.value) @property - def n_conductors(self): + def n_conductors(self) -> int: """Total number of conductors in the winding pack.""" return sum(w.n_conductors for w in self.WPs) @@ -448,7 +448,7 @@ def dy_wp_i(self) -> np.ndarray: Returns ------- - np.ndarray + : Array containing the radial thickness [m] of each Winding Pack. Each element corresponds to one WP in the self.WPs list. """ @@ -461,7 +461,7 @@ def dy_wp_tot(self) -> float: Returns ------- - float + : Total radial thickness [m] summed over all winding packs. """ return sum(self.dy_wp_i) @@ -473,7 +473,7 @@ def R_wp_i(self) -> np.ndarray: # noqa: N802 Returns ------- - np.ndarray + : Array of radial positions [m] corresponding to the outer edge of each WP. """ dy_wp_cumsum = np.cumsum(np.concatenate(([0.0], self.dy_wp_i))) @@ -495,54 +495,54 @@ def R_wp_k(self): # noqa: N802 Returns ------- - np.ndarray + : Array of radial positions [m] corresponding to the outer edge of each winding pack. """ return self.R_wp_i - self.dy_wp_i - def plot(self, ax=None, *, show: bool = False, homogenized: bool = False): + def plot( + self, + ax: plt.Axes | None = None, + *, + show: bool = False, + homogenized: bool = False, + ) -> plt.Axes: """ Schematic plot of the TF case cross-section including winding packs. Parameters ---------- - ax : matplotlib.axes.Axes, optional + ax: Axis on which to draw the figure. If `None`, a new figure and axis will be created. - show : bool, optional + show: If `True`, displays the plot immediately using `plt.show()`. Default is `False`. - homogenized : bool, optional + homogenized: If `True`, plots winding packs as homogenized blocks. If `False`, plots individual conductors inside WPs. Default is `False`. Returns ------- - matplotlib.axes.Axes + : The axis object containing the rendered plot. """ if ax is None: _, ax = plt.subplots() ax.set_aspect("equal", adjustable="box") - # -------------------------------------- # Plot external case boundary (delegate) - # -------------------------------------- super().plot(ax=ax, show=False) - # -------------------------------------- # Plot winding packs - # -------------------------------------- for i, wp in enumerate(self.WPs): xc_wp = 0.0 yc_wp = self.R_wp_i[i] - wp.dy / 2 ax = wp.plot(xc=xc_wp, yc=yc_wp, ax=ax, homogenized=homogenized) - # -------------------------------------- # Finalize plot - # -------------------------------------- ax.set_xlabel("Toroidal direction [m]") ax.set_ylabel("Radial direction [m]") ax.set_title(f"TF Case Cross Section: {self.name}") @@ -553,37 +553,36 @@ def plot(self, ax=None, *, show: bool = False, homogenized: bool = False): return ax @property - def area_case_jacket(self): + def area_case_jacket(self) -> float: """ Area of the case jacket (excluding winding pack regions). Returns ------- - float Case jacket area [m²], computed as total area minus total WP area. """ return self.area - self.area_wps @property - def area_wps(self): + def area_wps(self) -> float: """ Total area occupied by all winding packs. Returns ------- - float + : Combined area of the winding packs [m²]. """ return np.sum([w.area for w in self.WPs]) @property - def area_wps_jacket(self): + def area_wps_jacket(self) -> float: """ Total jacket area of all winding packs. Returns ------- - float + : Combined area of conductor jackets in all WPs [m²]. """ return np.sum([w.jacket_area for w in self.WPs]) @@ -599,7 +598,7 @@ def area_jacket_total(self) -> float: Returns ------- - float + : Combined area of the case structure and the conductor jackets [m²]. Notes @@ -623,15 +622,15 @@ def rearrange_conductors_in_wp( Parameters ---------- - n_conductors : int + n_conductors: Total number of conductors to distribute. - wp_reduction_factor : float + wp_reduction_factor: Fractional reduction of available toroidal space for WPs. - min_gap_x : float + min_gap_x: Minimum gap between the WP and the case boundary in toroidal direction [m]. - n_layers_reduction : int + n_layers_reduction: Number of layers to remove after each WP. - layout : str, optional + layout: Layout strategy ("auto", "layer", "pancake"). """ @@ -651,15 +650,15 @@ def enforce_wp_layout_rules( Parameters ---------- - n_conductors : int + n_conductors: Number of conductors to allocate. - dx_WP : float + dx_WP: Available toroidal width for the winding pack [m]. - dx_cond : float + dx_cond: Toroidal width of a single conductor [m]. - dy_cond : float + dy_cond: Radial height of a single conductor [m]. - layout : str + layout: Layout type: - "auto" : no constraints - "layer" : enforce even number of turns (ny % 2 == 0) @@ -667,8 +666,10 @@ def enforce_wp_layout_rules( Returns ------- - tuple[int, int] - n_layers_max (nx), n_turns_max (ny) + : + n_layers_max (nx) + : + n_turns_max (ny) Raises ------ @@ -718,21 +719,21 @@ def optimize_vault_radial_thickness( Parameters ---------- - pm : float + pm: Radial magnetic pressure [Pa]. - fz : float + fz: Axial electromagnetic force [N]. - T : float + T: Operating temperature [K]. - B : float + B: Magnetic field strength [T]. - allowable_sigma : float + allowable_sigma: Allowable maximum stress [Pa]. - bounds : np.ndarray, optional + bounds: Optimization bounds for vault thickness [m]. """ - def to_dict(self) -> dict: + def to_dict(self) -> dict[str, float | str | list[dict[str, float | str | Any]]]: """ Serialize the BaseCaseTF instance into a dictionary. @@ -743,9 +744,6 @@ def to_dict(self) -> dict: information. """ return { - "name_in_registry": getattr( - self, "_name_in_registry_", self.__class__.__name__ - ), "name": self.name, "Ri": self.params.Ri.value, "dy_ps": self.params.dy_ps.value, @@ -757,20 +755,20 @@ def to_dict(self) -> dict: } @classmethod - def from_dict(cls, case_dict: dict, name: str | None = None) -> "BaseCaseTF": + def from_dict(cls, case_dict: dict, name: str | None = None) -> BaseCaseTF: """ Deserialize a BaseCaseTF instance from a dictionary. Parameters ---------- - case_dict : dict + case_dict: Dictionary containing serialized TF case data. - name : str, optional + name: Optional name override for the new instance. Returns ------- - BaseCaseTF + : Reconstructed TF case instance. Raises @@ -778,15 +776,6 @@ def from_dict(cls, case_dict: dict, name: str | None = None) -> "BaseCaseTF": ValueError If the 'name_in_registry' field does not match this class. """ - name_in_registry = case_dict.get("name_in_registry") - expected_name_in_registry = getattr(cls, "_name_in_registry_", cls.__name__) - - if name_in_registry != expected_name_in_registry: - raise ValueError( - f"Cannot create {cls.__name__} from dictionary with name_in_registry " - f"'{name_in_registry}'. Expected '{expected_name_in_registry}'." - ) - WPs = [create_wp_from_dict(wp_dict) for wp_dict in case_dict["WPs"]] # noqa:N806 return cls( @@ -805,7 +794,7 @@ def __str__(self) -> str: Returns ------- - str + : Multiline string summarizing key properties of the TF case. """ return ( @@ -826,8 +815,6 @@ class TrapezoidalCaseTF(BaseCaseTF, TrapezoidalGeometry): Note: this class considers a set of Winding Pack with the same conductor (instance). """ - _registry_ = CASETF_REGISTRY - _name_in_registry_ = "TrapezoidalCaseTF" param_cls: type[TFCaseParams] = TFCaseParams def __init__( @@ -856,7 +843,7 @@ def _check_WPs( # noqa: PLR6301, N802 Parameters ---------- - WPs : list of WindingPack + WPs: List of winding pack objects to validate. Raises @@ -886,29 +873,31 @@ def dx_vault(self): Returns ------- - float + : Average length of the vault in the toroidal direction [m]. """ return (self.R_wp_k[-1] + self.Rk) * np.tan(self.rad_theta_TF / 2) - def Kx_ps(self, op_cond: OperationalConditions): # noqa: N802 + def Kx_ps(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ Compute the equivalent radial stiffness of the poloidal support (PS) region. Parameters ---------- - op_cond: OperationalConditions + op_cond: Operational conditions including temperature, magnetic field, and strain at which to calculate the material property. Returns ------- - float + : Equivalent radial stiffness of the poloidal support [Pa]. """ - return self.mat_case.youngs_modulus(op_cond) * * self.params.dy_ps.value / self.dx_ps + return ( + self.mat_case.youngs_modulus(op_cond) * self.params.dy_ps.value / self.dx_ps + ) - def Kx_lat(self, op_cond: OperationalConditions): # noqa: N802 + def Kx_lat(self, op_cond: OperationalConditions) -> np.ndarray: # noqa: N802 """ Compute the equivalent radial stiffness of the lateral case sections. @@ -917,13 +906,13 @@ def Kx_lat(self, op_cond: OperationalConditions): # noqa: N802 Parameters ---------- - op_cond: OperationalConditions + op_cond: Operational conditions including temperature, magnetic field, and strain at which to calculate the material property. Returns ------- - np.ndarray + : Array of radial stiffness values for each lateral segment [Pa]. """ dx_lat = np.array([ @@ -934,24 +923,28 @@ def Kx_lat(self, op_cond: OperationalConditions): # noqa: N802 dy_lat = np.array([w.dy for w in self.WPs]) return self.mat_case.youngs_modulus(op_cond) * dy_lat / dx_lat - def Kx_vault(self, op_cond: OperationalConditions): # noqa: N802 + def Kx_vault(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ Compute the equivalent radial stiffness of the vault region. Parameters ---------- - op_cond: OperationalConditions + op_cond: Operational conditions including temperature, magnetic field, and strain at which to calculate the material property. Returns ------- - float + : Equivalent radial stiffness of the vault [Pa]. """ - return self.mat_case.youngs_modulus(op_cond) * self.params.dy_vault.value / self.dx_vault + return ( + self.mat_case.youngs_modulus(op_cond) + * self.params.dy_vault.value + / self.dx_vault + ) - def Kx(self, op_cond: OperationalConditions): # noqa: N802 + def Kx(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ Compute the total equivalent radial stiffness of the entire case structure. @@ -962,13 +955,13 @@ def Kx(self, op_cond: OperationalConditions): # noqa: N802 Parameters ---------- - op_cond: OperationalConditions + op_cond: Operational conditions including temperature, magnetic field, and strain at which to calculate the material property. Returns ------- - float + : Total equivalent radial stiffness of the TF case [Pa]. """ temp = [ @@ -981,24 +974,26 @@ def Kx(self, op_cond: OperationalConditions): # noqa: N802 ] return summation([self.Kx_ps(op_cond), self.Kx_vault(op_cond), *temp]) - def Ky_ps(self, op_cond: OperationalConditions): # noqa: N802 + def Ky_ps(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ Compute the equivalent toroidal stiffness of the poloidal support (PS) region. Parameters ---------- - op_cond: OperationalConditions + op_cond: Operational conditions including temperature, magnetic field, and strain at which to calculate the material property. Returns ------- - float + : Equivalent toroidal stiffness of the PS region [Pa]. """ - return self.mat_case.youngs_modulus(op_cond) * self.dx_ps / self.params.dy_ps.value + return ( + self.mat_case.youngs_modulus(op_cond) * self.dx_ps / self.params.dy_ps.value + ) - def Ky_lat(self, op_cond: OperationalConditions): # noqa: N802 + def Ky_lat(self, op_cond: OperationalConditions) -> np.ndarray: # noqa: N802 """ Compute the equivalent toroidal stiffness of lateral case sections per winding pack. @@ -1007,13 +1002,13 @@ def Ky_lat(self, op_cond: OperationalConditions): # noqa: N802 Parameters ---------- - op_cond: OperationalConditions + op_cond: Operational conditions including temperature, magnetic field, and strain at which to calculate the material property. Returns ------- - np.ndarray + : Array of toroidal stiffness values for each lateral segment [Pa]. """ dx_lat = np.array([ @@ -1024,24 +1019,28 @@ def Ky_lat(self, op_cond: OperationalConditions): # noqa: N802 dy_lat = np.array([w.dy for w in self.WPs]) return self.mat_case.youngs_modulus(op_cond) * dx_lat / dy_lat - def Ky_vault(self, op_cond: OperationalConditions): # noqa: N802 + def Ky_vault(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ Compute the equivalent toroidal stiffness of the vault region. Parameters ---------- - op_cond: OperationalConditions + op_cond: Operational conditions including temperature, magnetic field, and strain at which to calculate the material property. Returns ------- - float + : Equivalent toroidal stiffness of the vault [Pa]. """ - return self.mat_case.youngs_modulus(op_cond) * self.dx_vault / self.params.dy_vault.value + return ( + self.mat_case.youngs_modulus(op_cond) + * self.dx_vault + / self.params.dy_vault.value + ) - def Ky(self, op_cond: OperationalConditions): # noqa: N802 + def Ky(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ Compute the total equivalent toroidal stiffness of the entire case structure. @@ -1052,13 +1051,13 @@ def Ky(self, op_cond: OperationalConditions): # noqa: N802 Parameters ---------- - op_cond: OperationalConditions + op_cond: Operational conditions including temperature, magnetic field, and strain at which to calculate the material property. Returns ------- - float + : Total equivalent toroidal stiffness of the TF case [Pa]. """ temp = [ @@ -1085,15 +1084,15 @@ def rearrange_conductors_in_wp( Parameters ---------- - n_conductors : int + n_conductors: Total number of conductors to be allocated. - wp_reduction_factor : float + wp_reduction_factor: Fractional reduction of the total available toroidal space for WPs. - min_gap_x : float + min_gap_x: Minimum allowable toroidal gap between WP and boundary [m]. - n_layers_reduction : int + n_layers_reduction: Number of horizontal layers to reduce after each WP. - layout : str, optional + layout: Layout type ("auto", "layer", "pancake"). Raises @@ -1212,7 +1211,9 @@ def rearrange_conductors_in_wp( bluemira_error(msg) raise ValueError(msg) - def _tresca_stress(self, pm: float, fz: float, op_cond: OperationalConditions): + def _tresca_stress( + self, pm: float, fz: float, op_cond: OperationalConditions + ) -> float: """ Estimate the maximum principal (Tresca) stress on the inner case of the TF coil. @@ -1226,17 +1227,17 @@ def _tresca_stress(self, pm: float, fz: float, op_cond: OperationalConditions): Parameters ---------- - pm : float + pm: Radial magnetic pressure acting on the case [Pa]. - fz : float + fz: Vertical force acting on the inner leg of the case [N]. - op_cond: OperationalConditions + op_cond: Operational conditions including temperature, magnetic field, and strain at which to calculate the material property. Returns ------- - float + : Estimated maximum stress [Pa] acting on the case nose (hoop + vertical contribution). """ @@ -1263,7 +1264,7 @@ def optimize_vault_radial_thickness( self, pm: float, fz: float, - op_cond, + op_cond: OperationalConditions, allowable_sigma: float, bounds: np.array = None, ): @@ -1272,21 +1273,22 @@ def optimize_vault_radial_thickness( Parameters ---------- - pm : + pm: The magnetic pressure applied along the radial direction (Pa). - f_z : + f_z: The force applied in the z direction, perpendicular to the case cross-section (N). - op_cond: OperationalConditions + op_cond: Operational conditions including temperature, magnetic field, and strain at which to calculate the material properties. - allowable_sigma : + allowable_sigma: The allowable stress (Pa) for the jacket material. - bounds : + bounds: Optional bounds for the jacket thickness optimization (default is None). Returns ------- + : The result of the optimization process containing information about the optimal vault thickness. @@ -1322,36 +1324,37 @@ def _sigma_difference( fz: float, op_cond: OperationalConditions, allowable_sigma: float, - ): + ) -> float: """ Fitness function for the optimization problem. It calculates the absolute difference between the Tresca stress and the allowable stress. Parameters ---------- - dy_vault : + dy_vault: The thickness of the vault in the direction perpendicular to the applied pressure(m). - pm : + pm: The magnetic pressure applied along the radial direction (Pa). - fz : + fz: The force applied in the z direction, perpendicular to the case cross-section (N). - op_cond: OperationalConditions + op_cond: Operational conditions including temperature, magnetic field, and strain at which to calculate the material properties. - allowable_sigma : + allowable_sigma: The allowable stress (Pa) for the vault material. Returns ------- + : The absolute difference between the calculated Tresca stress and the allowable stress (Pa). Notes ----- - This function modifies the case's vault thickness - using the value provided in jacket_thickness. + This function modifies the case's vault thickness + using the value provided in jacket_thickness. """ self.params.dy_vault.value = dy_vault sigma = self._tresca_stress(pm, fz, op_cond) @@ -1365,8 +1368,8 @@ def optimize_jacket_and_vault( fz: float, op_cond: OperationalConditions, allowable_sigma: float, - bounds_cond_jacket: np.array = None, - bounds_dy_vault: np.array = None, + bounds_cond_jacket: np.ndarray | None = None, + bounds_dy_vault: np.ndarray | None = None, layout: str = "auto", wp_reduction_factor: float = 0.8, min_gap_x: float = 0.05, @@ -1389,38 +1392,38 @@ def optimize_jacket_and_vault( Parameters ---------- - pm : float + pm: Radial magnetic pressure on the conductor [Pa]. - fz : float + fz: Axial electromagnetic force on the winding pack [N]. - op_cond: OperationalConditions + op_cond: Operational conditions including temperature, magnetic field, and strain at which to calculate the material properties. - allowable_sigma : float + allowable_sigma: Maximum allowable stress for structural material [Pa]. - bounds_cond_jacket : np.ndarray, optional + bounds_cond_jacket: Min/max bounds for conductor jacket area optimization [m²]. - bounds_dy_vault : np.ndarray, optional + bounds_dy_vault: Min/max bounds for the case vault thickness optimization [m]. - layout : str, optional + layout: Cable layout strategy; "auto" or predefined layout name. - wp_reduction_factor : float, optional + wp_reduction_factor: Reduction factor applied to WP footprint during conductor rearrangement. - min_gap_x : float, optional + min_gap_x: Minimum spacing between adjacent conductors [m]. - n_layers_reduction : int, optional + n_layers_reduction: Number of conductor layers to remove when reducing WP height. - max_niter : int, optional + max_niter: Maximum number of optimization iterations. - eps : float, optional + eps: Convergence threshold for the combined optimization loop. - n_conds : int, optional + n_conds: Target total number of conductors in the winding pack. If None, the self number of conductors is used. Notes ----- - The function modifies the internal state of `conductor` and `self.dy_vault`. + The function modifies the internal state of `conductor` and `self.dy_vault`. """ debug_msg = ["Method optimize_jacket_and_vault"] @@ -1602,14 +1605,14 @@ def create_case_tf_from_dict( Parameters ---------- - case_dict : dict + case_dict: Serialized case dictionary, must include 'name_in_registry' field. - name : str, optional + name: Name to assign to the created case. If None, uses the name in the dictionary. Returns ------- - BaseCaseTF + : A fully instantiated CaseTF (or subclass) object. Raises diff --git a/bluemira/magnets/conductor.py b/bluemira/magnets/conductor.py index e0c66b2ed9..8518b2e6a7 100644 --- a/bluemira/magnets/conductor.py +++ b/bluemira/magnets/conductor.py @@ -6,21 +6,26 @@ """Conductor class""" +from __future__ import annotations + from dataclasses import dataclass -from typing import Any +from typing import TYPE_CHECKING, Any import matplotlib.pyplot as plt import numpy as np -from matproplib import OperationalConditions -from matproplib.material import Material from scipy.optimize import minimize_scalar from bluemira.base.look_and_feel import bluemira_debug from bluemira.base.parameter_frame import Parameter, ParameterFrame -from bluemira.base.parameter_frame.typed import ParameterFrameLike from bluemira.magnets.cable import ABCCable, create_cable_from_dict from bluemira.magnets.utils import reciprocal_summation, summation +if TYPE_CHECKING: + from matproplib import OperationalConditions + from matproplib.material import Material + + from bluemira.base.parameter_frame.typed import ParameterFrameLike + @dataclass class ConductorParams(ParameterFrame): @@ -44,7 +49,6 @@ class Conductor: insulator. """ - _name_in_registry_ = "Conductor" param_cls: type[ConductorParams] = ConductorParams def __init__( @@ -124,23 +128,21 @@ def area_ins(self): Returns ------- - float [m²] + : + area [m²] """ return self.area - self.area_jacket - self.cable.area - def to_dict(self) -> dict: + def to_dict(self) -> dict[str, Any]: """ Serialize the conductor instance to a dictionary. Returns ------- - dict + : Dictionary with serialized conductor data. """ return { - "name_in_registry": getattr( - self, "_name_in_registry_", self.__class__.__name__ - ), "name": self.name, "cable": self.cable.to_dict(), "mat_jacket": self.mat_jacket.name, @@ -156,23 +158,21 @@ def from_dict( cls, conductor_dict: dict[str, Any], name: str | None = None, - ) -> "Conductor": + ) -> Conductor: """ Deserialize a Conductor instance from a dictionary. Parameters ---------- - cls : type - Class to instantiate (Conductor or subclass). - conductor_dict : dict + conductor_dict: Dictionary containing serialized conductor data. - name : str + name: Name for the new instance. If None, attempts to use the 'name' field from the dictionary. Returns ------- - Conductor + : A fully reconstructed Conductor instance. Raises @@ -182,16 +182,6 @@ def from_dict( registration name, or if the name already exists and unique_name is False. """ - # Validate registration name - name_in_registry = conductor_dict.get("name_in_registry") - expected_name_in_registry = getattr(cls, "_name_in_registry_", cls.__name__) - - if name_in_registry != expected_name_in_registry: - raise ValueError( - f"Cannot create {cls.__name__} from dictionary with name_in_registry " - f"'{name_in_registry}'. Expected '{expected_name_in_registry}'." - ) - # Deserialize cable cable = create_cable_from_dict( cable_dict=conductor_dict["cable"], @@ -215,20 +205,21 @@ def from_dict( name=name or conductor_dict.get("name"), ) - def erho(self, op_cond: OperationalConditions): + def erho(self, op_cond: OperationalConditions) -> float: """ Computes the conductor's equivalent resistivity considering the resistance of its strands in parallel. Parameters ---------- - op_cond: OperationalConditions + op_cond: Operational conditions including temperature, magnetic field, and strain at which to calculate the material property. Returns ------- - float [Ohm m] + : + resistivity [Ohm m] Notes ----- @@ -241,20 +232,21 @@ def erho(self, op_cond: OperationalConditions): res_tot = reciprocal_summation(resistances) return res_tot * self.area - def Cp(self, op_cond: OperationalConditions): # noqa: N802 + def Cp(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ Computes the conductor's equivalent specific heat considering the specific heats of its components in series. Parameters ---------- - op_cond: OperationalConditions + op_cond: Operational conditions including temperature, magnetic field, and strain at which to calculate the material property. Returns ------- - float [J/K/m] + : + Specific heat capacity [J/K/m] Notes ----- @@ -272,35 +264,37 @@ def _mat_ins_y_modulus(self, op_cond: OperationalConditions): def _mat_jacket_y_modulus(self, op_cond: OperationalConditions): return self.mat_jacket.youngs_modulus(op_cond) - def _Kx_topbot_ins(self, op_cond: OperationalConditions): # noqa: N802 + def _Kx_topbot_ins(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ Equivalent stiffness of the top/bottom insulator in the x-direction. Returns ------- - float + : Axial stiffness [N/m] """ - return self._mat_ins_y_modulus(op_cond) * self.cable.dy / self.params.dx_ins.value + return ( + self._mat_ins_y_modulus(op_cond) * self.cable.dy / self.params.dx_ins.value + ) - def _Kx_lat_ins(self, op_cond: OperationalConditions): # noqa: N802 + def _Kx_lat_ins(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ Equivalent stiffness of the lateral insulator in the x-direction. Returns ------- - float + : Axial stiffness [N/m] """ - return self._mat_ins_y_modulus(op_cond) * self.params.dy_ins.value / self.dx + return self._mat_ins_y_modulus(op_cond) * self.params.dy_ins.value / self.dx - def _Kx_lat_jacket(self, op_cond: OperationalConditions): # noqa: N802 + def _Kx_lat_jacket(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ Equivalent stiffness of the lateral jacket in the x-direction. Returns ------- - float + : Axial stiffness [N/m] """ return ( @@ -309,35 +303,39 @@ def _Kx_lat_jacket(self, op_cond: OperationalConditions): # noqa: N802 / (self.dx - 2 * self.params.dx_ins.value) ) - def _Kx_topbot_jacket(self, op_cond: OperationalConditions): # noqa: N802 + def _Kx_topbot_jacket(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ Equivalent stiffness of the top/bottom jacket in the x-direction. Returns ------- - float + : Axial stiffness [N/m] """ - return self._mat_jacket_y_modulus(op_cond) * self.cable.dy / self.params.dx_jacket.value + return ( + self._mat_jacket_y_modulus(op_cond) + * self.cable.dy + / self.params.dx_jacket.value + ) - def _Kx_cable(self, op_cond: OperationalConditions): # noqa: N802 + def _Kx_cable(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ Equivalent stiffness of the cable in the x-direction. Returns ------- - float + : Axial stiffness [N/m] """ return self.cable.Kx(op_cond) - def Kx(self, op_cond: OperationalConditions): # noqa: N802 + def Kx(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ Equivalent stiffness of the conductor in the x-direction. Returns ------- - float + : Axial stiffness [N/m] """ return summation([ @@ -354,35 +352,37 @@ def Kx(self, op_cond: OperationalConditions): # noqa: N802 self._Kx_lat_ins(op_cond), ]) - def _Ky_topbot_ins(self, op_cond: OperationalConditions): # noqa: N802 + def _Ky_topbot_ins(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ Equivalent stiffness of the top/bottom insulator in the y-direction. Returns ------- - float + : Axial stiffness [N/m] """ - return self._mat_ins_y_modulus(op_cond) * self.cable.dx / self.params.dy_ins.value + return ( + self._mat_ins_y_modulus(op_cond) * self.cable.dx / self.params.dy_ins.value + ) - def _Ky_lat_ins(self, op_cond: OperationalConditions): # noqa: N802 + def _Ky_lat_ins(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ Equivalent stiffness of the lateral insulator in the y-direction. Returns ------- - float + : Axial stiffness [N/m] """ - return self._mat_ins_y_modulus(op_cond) * self.params.dx_ins.value / self.dy + return self._mat_ins_y_modulus(op_cond) * self.params.dx_ins.value / self.dy - def _Ky_lat_jacket(self, op_cond: OperationalConditions): # noqa: N802 + def _Ky_lat_jacket(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ Equivalent stiffness of the lateral jacket in the y-direction. Returns ------- - float + : Axial stiffness [N/m] """ return ( @@ -391,35 +391,39 @@ def _Ky_lat_jacket(self, op_cond: OperationalConditions): # noqa: N802 / (self.dy - 2 * self.params.dy_ins.value) ) - def _Ky_topbot_jacket(self, op_cond: OperationalConditions): # noqa: N802 + def _Ky_topbot_jacket(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ Equivalent stiffness of the top/bottom jacket in the y-direction. Returns ------- - float + : Axial stiffness [N/m] """ - return self._mat_jacket_y_modulus(op_cond) * self.cable.dx / self.params.dy_jacket.value + return ( + self._mat_jacket_y_modulus(op_cond) + * self.cable.dx + / self.params.dy_jacket.value + ) - def _Ky_cable(self, op_cond: OperationalConditions): # noqa: N802 + def _Ky_cable(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ Equivalent stiffness of the cable in the y-direction. Returns ------- - float + : Axial stiffness [N/m] """ return self.cable.Ky(op_cond) - def Ky(self, op_cond: OperationalConditions): # noqa: N802 + def Ky(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ Equivalent stiffness of the conductor in the y-direction. Returns ------- - float + : Axial stiffness [N/m] """ return summation([ @@ -452,20 +456,21 @@ def _tresca_sigma_jacket( Parameters ---------- - pressure : + pressure: The pressure applied along the specified direction (Pa). - f_z : + f_z: The force applied in the z direction, perpendicular to the conductor cross-section (N). op_cond: OperationalConditions Operational conditions including temperature, magnetic field, and strain at which to calculate the material property. - direction : + direction: The direction along which the pressure is applied ('x' or 'y'). Default is 'x'. Returns ------- + : The calculated Tresca stress in the jacket (Pa). Raises @@ -508,8 +513,7 @@ def _tresca_sigma_jacket( X_jacket = 2 * self._Kx_lat_jacket(op_cond) / K # noqa: N806 - # tresca_stress = pressure * X_jacket * saf_jacket + f_z / self.area_jacket - + # tresca_stress return pressure * X_jacket * saf_jacket + f_z / self.area_jacket def optimize_jacket_conductor( @@ -527,24 +531,25 @@ def optimize_jacket_conductor( Parameters ---------- - pressure : + pressure: The pressure applied along the specified direction (Pa). - f_z : + f_z: The force applied in the z direction, perpendicular to the conductor cross-section (N). - op_cond: OperationalConditions + op_cond: Operational conditions including temperature, magnetic field, and strain at which to calculate the material properties. - allowable_sigma : + allowable_sigma: The allowable stress (Pa) for the jacket material. - bounds : + bounds: Optional bounds for the jacket thickness optimization (default is None). - direction : + direction: The direction along which the pressure is applied ('x' or 'y'). Default is 'x'. Returns ------- + : The result of the optimization process containing information about the optimal jacket thickness. @@ -568,7 +573,7 @@ def sigma_difference( op_cond: OperationalConditions, allowable_sigma: float, direction: str = "x", - ): + ) -> float: """ Objective function for optimizing conductor jacket thickness based on the Tresca yield criterion. @@ -580,26 +585,26 @@ def sigma_difference( Parameters ---------- - jacket_thickness : float + jacket_thickness: Proposed thickness of the conductor jacket [m] in the direction perpendicular to the applied pressure. - pressure : float + pressure: Magnetic or mechanical pressure applied along the specified direction [Pa]. - fz : float + fz: Axial or vertical force applied perpendicular to the cross-section [N]. - op_cond: OperationalConditions + op_cond: Operational conditions including temperature, magnetic field, and strain at which to calculate the material property. - allowable_sigma : float + allowable_sigma: Maximum allowed stress for the jacket material [Pa]. - direction : str, optional + direction: Direction of the applied pressure. Can be either 'x' (horizontal) or 'y' (vertical). Default is 'x'. Returns ------- - float + : Absolute difference between the calculated Tresca stress and the allowable stress [Pa]. @@ -685,22 +690,22 @@ def plot(self, xc: float = 0, yc: float = 0, *, show: bool = False, ax=None): Parameters ---------- - xc : float, optional + xc: X-coordinate of the conductor center in the reference coordinate system. Default is 0. - yc : float, optional + yc: Y-coordinate of the conductor center in the reference coordinate system. Default is 0. - show : bool, optional + show: If True, the figure is rendered immediately using `plt.show()`. Default is False. - ax : matplotlib.axes.Axes or None, optional + ax: Axis on which to render the plot. If None, a new figure and axis will be created internally. Returns ------- - ax : matplotlib.axes.Axes + ax: The axis containing the rendered plot. Notes @@ -745,13 +750,13 @@ def plot(self, xc: float = 0, yc: float = 0, *, show: bool = False, ax=None): return ax - def __str__(self): + def __str__(self) -> str: """ Generate a human-readable string representation of the conductor. Returns ------- - str + : A multi-line summary of the conductor's key dimensions and its nested cable description. This includes: - Total x and y dimensions, @@ -793,7 +798,6 @@ class SymmetricConductor(Conductor): # jm - actually worthwhile or just set mantain a constant thickness (i.e. dy_jacket = dx_jacket and dy_ins = dx_ins). """ - _name_in_registry_ = "SymmetricConductor" param_cls: type[SymmetricConductorParams] = SymmetricConductorParams def __init__( @@ -877,9 +881,6 @@ def to_dict(self) -> dict: Dictionary with serialized symmetric conductor data. """ return { - "name_in_registry": getattr( - self, "_name_in_registry_", self.__class__.__name__ - ), "name": self.name, "cable": self.cable.to_dict(), "mat_jacket": self.mat_jacket.name, @@ -893,22 +894,20 @@ def from_dict( cls, conductor_dict: dict[str, Any], name: str | None = None, - ) -> "SymmetricConductor": + ) -> SymmetricConductor: """ Deserialize a SymmetricConductor instance from a dictionary. Parameters ---------- - cls : type - Class to instantiate (SymmetricConductor). - conductor_dict : dict + conductor_dict: Dictionary containing serialized conductor data. - name : str, optional + name: Name for the new instance. Returns ------- - SymmetricConductor + : A fully reconstructed SymmetricConductor instance. Raises @@ -916,16 +915,6 @@ def from_dict( ValueError If the 'name_in_registry' does not match the expected registration name. """ - # Validate registration name - name_in_registry = conductor_dict.get("name_in_registry") - expected_name_in_registry = getattr(cls, "_name_in_registry_", cls.__name__) - - if name_in_registry != expected_name_in_registry: - raise ValueError( - f"Cannot create {cls.__name__} from dictionary with name_in_registry " - f"'{name_in_registry}'. Expected '{expected_name_in_registry}'." - ) - # Deserialize cable cable = create_cable_from_dict( cable_dict=conductor_dict["cable"], @@ -951,21 +940,21 @@ def from_dict( def create_conductor_from_dict( conductor_dict: dict, name: str | None = None, -) -> "Conductor": +) -> Conductor: """ Factory function to create a Conductor (or subclass) from a serialized dictionary. Parameters ---------- - conductor_dict : dict + conductor_dict: Serialized conductor dictionary, must include 'name_in_registry' field. - name : str, optional + name: Name to assign to the created conductor. If None, uses the name in the dictionary. Returns ------- - Conductor + : A fully instantiated Conductor (or subclass) object. Raises diff --git a/bluemira/magnets/fatigue.py b/bluemira/magnets/fatigue.py index 01256e7348..d83241a710 100644 --- a/bluemira/magnets/fatigue.py +++ b/bluemira/magnets/fatigue.py @@ -8,14 +8,18 @@ Paris Law fatigue model with FE-inspired analytical crack propagation """ +from __future__ import annotations + import abc from dataclasses import dataclass -from typing import Final +from typing import TYPE_CHECKING, Final import numpy as np from bluemira.base.parameter_frame import Parameter, ParameterFrame -from bluemira.base.parameter_frame.typed import ParameterFrameLike + +if TYPE_CHECKING: + from bluemira.base.parameter_frame.typed import ParameterFrameLike __all__ = [ "ConductorInfo", @@ -103,7 +107,7 @@ def _boundary_correction_factor( Returns ------- - float + : Boundary correction factor F. """ return (m1 + m2 * a_d_t**2 + m3 * a_d_t**4) * g * f_phi * f_w @@ -115,7 +119,7 @@ def _bending_correction_factor(h1: float, h2: float, p: float, phi: float) -> fl Returns ------- - float + : Bending correction factor. """ return h1 + (h2 - h1) * np.sin(phi) ** p @@ -127,7 +131,7 @@ def _ellipse_shape_factor(ratio: float) -> float: Returns ------- - float + : Shape factor Q. """ return 1.0 + 1.464 * ratio**1.65 @@ -139,7 +143,7 @@ def _angular_location_correction(a: float, c: float, phi: float) -> float: Returns ------- - float + : Angular correction factor f_phi. """ if a <= c: @@ -153,7 +157,7 @@ def _finite_width_correction(a_d_t: float, c: float, w: float) -> float: Returns ------- - float + : Finite width correction factor. """ return 1.0 / np.sqrt(np.cos(np.sqrt(a_d_t) * np.pi * c / (2 * w))) # (11) @@ -177,13 +181,13 @@ def __init__(self, params: ParameterFrameLike): self.params = params @classmethod - def from_area(cls, area: float, aspect_ratio: float): + def from_area(cls, area: float, aspect_ratio: float) -> Crack: """ Instatiate a crack from an area and aspect ratio Returns ------- - Crack + : New instance of the crack geometry. """ cls.params.depth.value = np.sqrt(area / (cls.alpha * np.pi * aspect_ratio)) @@ -197,7 +201,7 @@ def area(self) -> float: Returns ------- - float + : Area [m²]. """ return self.alpha * np.pi * self.params.depth.value * self.params.width.value @@ -268,7 +272,8 @@ def stress_intensity_factor( # noqa: PLR6301 Returns ------- - Stress intensity factor + : + Stress intensity factor Notes ----- @@ -368,7 +373,8 @@ def stress_intensity_factor( # noqa: PLR6301 Returns ------- - Stress intensity factor + : + Stress intensity factor Notes ----- @@ -459,7 +465,8 @@ def stress_intensity_factor( # noqa: PLR6301 Returns ------- - Stress intensity factor + : + Stress intensity factor Notes ----- @@ -514,7 +521,8 @@ def calculate_n_pulses( Returns ------- - Number of plasma pulses + : + Number of plasma pulses Notes ----- diff --git a/bluemira/magnets/strand.py b/bluemira/magnets/strand.py index b6346b9af1..55cf7e7689 100644 --- a/bluemira/magnets/strand.py +++ b/bluemira/magnets/strand.py @@ -12,8 +12,10 @@ - Automatic class and instance registration mechanisms """ +from __future__ import annotations + from dataclasses import dataclass -from typing import Any +from typing import TYPE_CHECKING, Any import matplotlib.pyplot as plt import numpy as np @@ -28,6 +30,9 @@ from bluemira.geometry.face import BluemiraFace from bluemira.geometry.tools import make_circle +if TYPE_CHECKING: + from bluemira.base.parameter_frame.typed import ParameterFrameLike + @dataclass class StrandParams(ParameterFrame): @@ -49,7 +54,6 @@ class Strand: This class automatically registers itself and its instances. """ - _name_in_registry_ = "Strand" param_cls: type[StrandParams] = StrandParams def __init__( @@ -63,7 +67,7 @@ def __init__( Parameters ---------- - materials : list of MaterialFraction + materials: Materials composing the strand with their fractions. params: Structure containing the input parameters. Keys are: @@ -72,12 +76,11 @@ def __init__( See :class:`~bluemira.magnets.strand.StrandParams` for parameter details. - name : str or None, optional + name: Name of the strand. Defaults to "Strand". """ self.params = params - self._materials = None # jm - remove self.materials = materials self.name = name @@ -90,25 +93,25 @@ def __init__( ) @property - def materials(self) -> list: + def materials(self) -> list[MaterialFraction]: """ List of MaterialFraction materials composing the strand. Returns ------- - list of MaterialFraction + : Materials and their fractions. """ return self._materials @materials.setter - def materials(self, new_materials: list): + def materials(self, new_materials: list[MaterialFraction]): """ Set a new list of materials for the strand. Parameters ---------- - new_materials : list of MaterialFraction + new_materials: New materials to set. Raises @@ -137,7 +140,7 @@ def area(self) -> float: Returns ------- - float + : Area [m²]. """ return np.pi * (self.params.d_strand.value**2) / 4 @@ -149,7 +152,7 @@ def shape(self) -> BluemiraFace: Returns ------- - BluemiraFace + : Circular face of the strand. """ if self._shape is None: @@ -162,13 +165,13 @@ def E(self, op_cond: OperationalConditions) -> float: # noqa: N802 Parameters ---------- - op_cond: OperationalConditions + op_cond: Operational conditions including temperature, magnetic field, and strain at which to calculate the material property. Returns ------- - float + : Young's modulus [Pa]. """ return self._homogenised_material.youngs_modulus(op_cond) @@ -179,13 +182,13 @@ def rho(self, op_cond: OperationalConditions) -> float: Parameters ---------- - op_cond: OperationalConditions + op_cond: Operational conditions including temperature, magnetic field, and strain at which to calculate the material property. Returns ------- - float + : Density [kg/m³]. """ return self._homogenised_material.density(op_cond) @@ -196,13 +199,13 @@ def erho(self, op_cond: OperationalConditions) -> float: Parameters ---------- - op_cond: OperationalConditions + op_cond: Operational conditions including temperature, magnetic field, and strain at which to calculate the material property. Returns ------- - float + : Electrical resistivity [Ohm·m]. """ # Treat parallel calculation for resistivity @@ -220,13 +223,13 @@ def Cp(self, op_cond: OperationalConditions) -> float: # noqa: N802 Parameters ---------- - op_cond: OperationalConditions + op_cond: Operational conditions including temperature, magnetic field, and strain at which to calculate the material property. Returns ------- - float + : Specific heat [J/kg/K]. """ # Treat volume/specific heat capacity calculation @@ -245,17 +248,19 @@ def Cp(self, op_cond: OperationalConditions) -> float: # noqa: N802 ) return self._homogenised_material.specific_heat_capacity(op_cond) - def plot(self, ax=None, *, show: bool = True, **kwargs): + def plot( + self, ax: plt.Axes | None = None, *, show: bool = True, **kwargs + ) -> plt.Axes: """ Plot a 2D cross-section of the strand. Parameters ---------- - ax : matplotlib.axes.Axes, optional + ax: Axis to plot on. - show : bool, optional + show: Whether to show the plot immediately. - kwargs : dict + kwargs: Additional arguments passed to the plot function. Returns @@ -275,7 +280,7 @@ def __str__(self) -> str: Returns ------- - str + : Description of the strand. """ return ( @@ -285,19 +290,16 @@ def __str__(self) -> str: f"shape = {self.shape}\n" ) - def to_dict(self) -> dict: + def to_dict(self) -> dict[str, Any]: """ Serialize the strand instance to a dictionary. Returns ------- - dict + : Dictionary with serialized strand data. """ return { - "name_in_registry": getattr( - self, "_name_in_registry_", self.__class__.__name__ - ), "name": self.name, "d_strand": self.params.d_strand.value, "temperature": self.params.temperature.value, @@ -315,17 +317,15 @@ def from_dict( cls, strand_dict: dict[str, Any], name: str | None = None, - ) -> "Strand": + ) -> Strand: """ Deserialize a Strand instance from a dictionary. Parameters ---------- - cls : type - Class to instantiate (Strand or subclass). - strand_dict : dict + strand_dict: Dictionary containing serialized strand data. - name : str + name: Name for the new instance. If None, attempts to use the 'name' field from the dictionary. @@ -405,7 +405,7 @@ def __init__( Parameters ---------- - materials : list of MaterialFraction + materials: Materials composing the strand with their fractions. One material must be a supercoductor. params: @@ -415,7 +415,7 @@ def __init__( See :class:`~bluemira.magnets.strand.StrandParams` for parameter details. - name : str or None, optional + name: Name of the strand. Defaults to "Strand". """ super().__init__( @@ -431,7 +431,7 @@ def _check_materials(self) -> MaterialFraction: Returns ------- - MaterialFraction + : The identified superconducting material. Raises @@ -466,7 +466,7 @@ def sc_area(self) -> float: Returns ------- - float + : Superconducting area [m²]. """ return self.area * self._sc.fraction @@ -477,13 +477,13 @@ def Jc(self, op_cond: OperationalConditions) -> float: # noqa:N802 Parameters ---------- - op_cond: OperationalConditions + op_cond: Operational conditions including temperature, magnetic field, and strain at which to calculate the material property. Returns ------- - float + : Critical current density [A/m²]. """ if op_cond.strain is None: @@ -496,13 +496,13 @@ def Ic(self, op_cond: OperationalConditions) -> float: # noqa:N802 Parameters ---------- - op_cond: OperationalConditions + op_cond: Operational conditions including temperature, magnetic field, and strain at which to calculate the material property. Returns ------- - float + : Critical current [A]. """ return self.Jc(op_cond) * self.sc_area @@ -511,30 +511,30 @@ def plot_Ic_B( # noqa:N802 self, B: np.ndarray, temperature: float, - ax=None, + ax: plt.Axes | None = None, *, show: bool = True, **kwargs, - ): + ) -> plt.Axes: """ Plot critical current Ic as a function of magnetic field B. Parameters ---------- - B : np.ndarray + B: Array of magnetic field values [T]. - temperature : float + temperature: Operating temperature [K]. - ax : matplotlib.axes.Axes, optional + ax: Axis to plot on. If None, a new figure is created. - show : bool, optional + show: Whether to immediately show the plot. - kwargs : dict + kwargs: Additional arguments passed to Ic calculation. Returns ------- - matplotlib.axes.Axes + : Axis with the plotted Ic vs B curve. """ if ax is None: @@ -564,9 +564,6 @@ def plot_Ic_B( # noqa:N802 return ax -# ------------------------------------------------------------------------------ -# Supporting functions -# ------------------------------------------------------------------------------ def create_strand_from_dict( strand_dict: dict[str, Any], name: str | None = None, @@ -576,10 +573,10 @@ def create_strand_from_dict( Parameters ---------- - strand_dict : dict + strand_dict: Dictionary with serialized strand data. Must include a 'name_in_registry' field corresponding to a registered class. - name : str, optional + name: If given, overrides the name from the dictionary. Returns diff --git a/bluemira/magnets/utils.py b/bluemira/magnets/utils.py index 8eec3983a3..99c444b80f 100644 --- a/bluemira/magnets/utils.py +++ b/bluemira/magnets/utils.py @@ -6,10 +6,12 @@ """Utils for magnets""" +from collections.abc import Callable, Sequence + import numpy as np -def summation(arr: list | np.ndarray): +def summation(arr: Sequence[float]) -> float: """ Compute the simple summation of the series @@ -21,16 +23,17 @@ def summation(arr: list | np.ndarray): Returns ------- - Result: float - - i.e. + : + the resulting summation - Y = sum(x1 + x2 + x3 ...) + Notes + ----- + Y = sum(x1...xn) """ return np.sum(arr) -def reciprocal_summation(arr: list | np.ndarray): +def reciprocal_summation(arr: Sequence[float]) -> float: """ Compute the inverse of the summation of a reciprocal series @@ -42,16 +45,19 @@ def reciprocal_summation(arr: list | np.ndarray): Returns ------- - Result: float - - i.e. + : + resulting summation + Notes + ----- Y = [sum(1/x1 + 1/x2 + 1/x3 ...)]^-1 """ return (np.sum((1 / element) for element in arr)) ** -1 -def delayed_exp_func(x0: float, tau: float, t_delay: float = 0): +def delayed_exp_func( + x0: float, tau: float, t_delay: float = 0 +) -> Callable[[float], float]: """ Delayed Exponential function @@ -59,16 +65,17 @@ def delayed_exp_func(x0: float, tau: float, t_delay: float = 0): Parameters ---------- - x0: float + x0: initial value - tau: float + tau: characteristic time constant - t_delay: float + t_delay: delay time Returns ------- - A Callable - exponential function + : + An exponential function """ diff --git a/bluemira/magnets/winding_pack.py b/bluemira/magnets/winding_pack.py index 9665232768..bad3c9dcc6 100644 --- a/bluemira/magnets/winding_pack.py +++ b/bluemira/magnets/winding_pack.py @@ -36,11 +36,11 @@ class WindingPack: Attributes ---------- - conductor : Conductor + conductor: The base conductor type used in the winding pack. - nx : int + nx: Number of conductors along the x-axis. - ny : int + ny: Number of conductors along the y-axis. """ @@ -55,16 +55,16 @@ def __init__( Parameters ---------- - conductor : Conductor + conductor: The conductor instance. params: Structure containing the input parameters. Keys are: - - nx : int - - ny : int + - nx: int + - ny: int See :class:`~bluemira.magnets.winding_pack.WindingPackParams` for parameter details. - name : str, optional + name: Name of the winding pack instance. """ self.conductor = conductor @@ -102,13 +102,13 @@ def Kx(self, op_cond: OperationalConditions) -> float: # noqa: N802 Parameters ---------- - op_cond: OperationalConditions + op_cond: Operational conditions including temperature, magnetic field, and strain at which to calculate the material property. Returns ------- - float + : Stiffness along the x-axis [N/m]. """ return self.conductor.Kx(op_cond) * self.params.ny.value / self.params.nx.value @@ -119,13 +119,13 @@ def Ky(self, op_cond: OperationalConditions) -> float: # noqa: N802 Parameters ---------- - op_cond: OperationalConditions + op_cond: Operational conditions including temperature, magnetic field, and strain at which to calculate the material property. Returns ------- - float + : Stiffness along the y-axis [N/m]. """ return self.conductor.Ky(op_cond) * self.params.nx.value / self.params.ny.value @@ -136,28 +136,28 @@ def plot( yc: float = 0, *, show: bool = False, - ax=None, + ax: plt.Axes | None = None, homogenized: bool = True, - ): + ) -> plt.Axes: """ Plot the winding pack geometry. Parameters ---------- - xc : float + xc: Center x-coordinate [m]. - yc : float + yc: Center y-coordinate [m]. - show : bool, optional + show: If True, immediately show the plot. - ax : matplotlib.axes.Axes, optional + ax: Axes object to draw on. - homogenized : bool, optional + homogenized: If True, plot as a single block. Otherwise, plot individual conductors. Returns ------- - matplotlib.axes.Axes + : Axes object containing the plot. """ if ax is None: @@ -188,19 +188,16 @@ def plot( plt.show() return ax - def to_dict(self) -> dict: + def to_dict(self) -> dict[str, Any]: """ Serialize the WindingPack to a dictionary. Returns ------- - dict + : Serialized dictionary of winding pack attributes. """ return { - "name_in_registry": getattr( - self, "_name_in_registry_", self.__class__.__name__ - ), "name": self.name, "conductor": self.conductor.to_dict(), "nx": self.params.nx.value, @@ -218,15 +215,15 @@ def from_dict( Parameters ---------- - windingpack_dict : dict + windingpack_dict: Serialized winding pack dictionary. - name : str + name: Name for the new instance. If None, attempts to use the 'name' field from the dictionary. Returns ------- - WindingPack + : Reconstructed WindingPack instance. Raises @@ -268,15 +265,15 @@ def create_wp_from_dict( Parameters ---------- - windingpack_dict : dict + windingpack_dict: Dictionary containing serialized winding pack data. Must include a 'name_in_registry' field matching a registered class. - name : str, optional + name: Optional name override for the reconstructed WindingPack. Returns ------- - WindingPack + : An instance of the appropriate WindingPack subclass. Raises diff --git a/bluemira/magnets/winding_pack_.py b/bluemira/magnets/winding_pack_.py index b5e1a6eb28..e6d1d6f2ee 100644 --- a/bluemira/magnets/winding_pack_.py +++ b/bluemira/magnets/winding_pack_.py @@ -1,3 +1,6 @@ +import matplotlib.pyplot as plt +import numpy as np + from bluemira.base.constants import MU_0_2PI from bluemira.base.designer import Designer from bluemira.base.look_and_feel import bluemira_print @@ -19,7 +22,7 @@ class WindingPackDesignerParams(ParameterFrame): ripple = 6e-3 # requirement on the maximum plasma ripple a = R0 / A # minor radius d = 1.82 # additional distance to calculate the max external radius of the inner TF leg - Iop = 70.0e3 # operational current in each conductor + operational_current = 70.0e3 # operational current in each conductor T_sc = 4.2 # operational temperature of superconducting cable T_margin = 1.5 # temperature margin t_delay = 3 # [s] @@ -37,13 +40,13 @@ class WindingPackDesignerParams(ParameterFrame): B_ref = 15 # [T] Reference B field value (limit for LTS) -def B_TF_r(I_TF, n_TF, r): +def B_TF_r(tf_current, n_TF, r): """ Compute the magnetic field generated by the TF coils, including ripple correction. Parameters ---------- - I_TF : float + tf_current : float Toroidal field coil current [A]. n_TF : int Number of toroidal field coils. @@ -55,30 +58,32 @@ def B_TF_r(I_TF, n_TF, r): float Magnetic field intensity [T]. """ - return 1.08 * (MU_0_2PI * n_TF * I_TF / r) + return 1.08 * (MU_0_2PI * n_TF * tf_current / r) class WindingPackDesigner(Designer[BluemiraWire]): def run(self) -> BluemiraWire: dr_plasma_side = R0 * 2 / 3 * 1e-2 # thickness of the plate before the WP Ri = R0 - a - d # [m] max external radius of the internal TF leg - Re = (R0 + a) * (1 / ripple) ** ( - 1 / n_TF - ) # [m] max internal radius of the external TF leg - I_TF = B0 * R0 / MU_0_2PI / n_TF # total current in each TF coil + + # [m] max internal radius of the external TF leg + Re = (R0 + a) * (1 / ripple) ** (1 / n_TF) + total_tf_current = B0 * R0 / MU_0_2PI / n_TF # total current in each TF coil # max magnetic field on the inner TF leg - B_TF_i = B_TF_r(I_TF, n_TF, Ri) + B_TF_i = B_TF_r(total_tf_current, n_TF, Ri) # magnetic pressure on the inner TF leg pm = B_TF_i**2 / (2 * MU_0) # vertical tension acting on the equatorial section of inner TF leg # i.e. half of the whole F_Z - t_z = 0.5 * np.log(Re / Ri) * MU_0_4PI * n_TF * I_TF**2 + t_z = 0.5 * np.log(Re / Ri) * MU_0_4PI * n_TF * total_tf_current**2 - n_cond = np.floor(I_TF / Iop) # minimum number of conductors + n_cond = np.floor( + total_tf_current / operational_current + ) # minimum number of conductors bluemira_print(f"Total number of conductor: {n_cond}") S_Y = 1e9 / safety_factor # [Pa] steel allowable limit @@ -92,22 +97,22 @@ def run(self) -> BluemiraWire: * 1.1 ) # Magnetic energy - Wm = 1 / 2 * L * n_TF * Iop**2 * 1e-9 + Wm = 1 / 2 * L * n_TF * operational_current**2 * 1e-9 # Maximum tension... (empirical formula from Lorenzo... find a generic equation) V_MAX = (7 * R0 - 3) / 6 * 1.1e3 - # Discharge characteristic times - Tau_discharge1 = L * Iop / V_MAX - Tau_discharge2 = B0 * I_TF * n_TF * (R0 / A) ** 2 / (R_VV * S_VV) # Discharge characteristic time to be considered in the following - Tau_discharge = max([Tau_discharge1, Tau_discharge2]) - tf = Tau_discharge - bluemira_print(f"Maximum TF discharge time: {tf}") - - I_fun = delayed_exp_func(Iop, Tau_discharge, t_delay) - B_fun = delayed_exp_func(B_TF_i, Tau_discharge, t_delay) - - # Create a time array from 0 to 3*Tau_discharge - t = np.linspace(0, 3 * Tau_discharge, 500) + tau_discharge = max([ + L * operational_current / V_MAX, + B0 * total_tf_current * n_TF * (R0 / A) ** 2 / (R_VV * S_VV), + ]) + tf = tau_discharge + bluemira_print(f"Maximum TF discharge time: {tau_discharge}") + + I_fun = delayed_exp_func(operational_current, tau_discharge, t_delay) + B_fun = delayed_exp_func(B_TF_i, tau_discharge, t_delay) + + # Create a time array from 0 to 3*tau_discharge + t = np.linspace(0, 3 * tau_discharge, 500) I_data = np.array([I_fun(t_i) for t_i in t]) B_data = np.array([B_fun(t_i) for t_i in t]) T_op = T_sc + T_margin # temperature considered for the superconducting cable @@ -131,7 +136,7 @@ def run(self) -> BluemiraWire: ) Ic_sc = sc_strand.Ic(B=B_TF_i, temperature=(T_op)) - n_sc_strand = int(np.ceil(Iop / Ic_sc)) + n_sc_strand = int(np.ceil(operational_current / Ic_sc)) if B_TF_i < B_ref: name = cable_name + "LTS" From f31dd32d0e10a50e37ca70a1c7fd85372a33f950 Mon Sep 17 00:00:00 2001 From: Jonathan Matthews Date: Wed, 27 Aug 2025 09:49:46 +0100 Subject: [PATCH 17/61] add OptVariablesFrame for geometry parameterisations for Trapezoidal and Wedged cases --- bluemira/magnets/case_tf.py | 162 ++++++++++++++++++++++++++++-------- 1 file changed, 125 insertions(+), 37 deletions(-) diff --git a/bluemira/magnets/case_tf.py b/bluemira/magnets/case_tf.py index d26fd80030..e46df23556 100644 --- a/bluemira/magnets/case_tf.py +++ b/bluemira/magnets/case_tf.py @@ -35,15 +35,36 @@ ) from bluemira.base.parameter_frame import Parameter, ParameterFrame from bluemira.geometry.parameterisations import GeometryParameterisation -from bluemira.geometry.wire import BluemiraWire +from bluemira.geometry.tools import make_polygon from bluemira.magnets.utils import reciprocal_summation, summation from bluemira.magnets.winding_pack import WindingPack, create_wp_from_dict +from bluemira.utilities.opt_variables import OptVariable, OptVariablesFrame, VarDictT, ov if TYPE_CHECKING: from matproplib import OperationalConditions from matproplib.material import Material from bluemira.base.parameter_frame.typed import ParameterFrameLike + from bluemira.geometry.wire import BluemiraWire + + +def _dx_at_radius(radius: float, rad_theta: float) -> float: + """ + Compute the toroidal width at a given radial position. + + Parameters + ---------- + radius: + Radial position at which to compute the toroidal width [m]. + rad_theta: + Toroidal angular span of the TF coil [radians]. + + Returns + ------- + : + Toroidal width [m] at the given radius. + """ + return 2 * radius * np.tan(rad_theta / 2) @dataclass @@ -74,22 +95,6 @@ def __init__(self, params: ParameterFrameLike): super().__init__(params) # fix when split into builders and designers self.rad_theta_TF = np.radians(self.params.theta_TF.value) - def dx_at_radius(self, radius: float) -> float: - """ - Compute the toroidal width at a given radial position. - - Parameters - ---------- - radius: - Radial position at which to compute the toroidal width [m]. - - Returns - ------- - : - Toroidal width [m] at the given radius. - """ - return 2 * radius * np.tan(self.rad_theta_TF / 2) - @property @abstractmethod def area(self) -> float: @@ -131,7 +136,34 @@ def plot(self, ax: plt.Axes = None, *, show: bool = False) -> plt.Axes: """ -class TrapezoidalGeometry(GeometryParameterisation): # TODO Opvariablesframe +@dataclass +class TrapezoidalGeometryOptVariables(OptVariablesFrame): + """Optimisiation variables for Trapezoidal Geometry.""" + + Ri: OptVariable = ov( + "Ri", + 3, # value? + lower_bound=0, + upper_bound=np.inf, + description="External radius of the TF coil case [m].", + ) + Rk: OptVariable = ov( + "Rk", + 5, # value? + lower_bound=0, + upper_bound=np.inf, + description="Internal radius of the TF coil case [m].", + ) + theta_TF: OptVariable = ov( + "theta_TF", + 15, # value? + lower_bound=0, + upper_bound=360, + description="Toroidal angular span of the TF coil [degrees].", + ) + + +class TrapezoidalGeometry(GeometryParameterisation[TrapezoidalGeometryOptVariables]): """ Geometry of a Toroidal Field (TF) coil case with trapezoidal cross-section. @@ -140,6 +172,18 @@ class TrapezoidalGeometry(GeometryParameterisation): # TODO Opvariablesframe for magnetic and mechanical optimization. """ + def __init__(self, var_dict: VarDictT | None = None): + variables = TrapezoidalGeometryOptVariables() + variables.adjust_variables(var_dict, strict_bounds=False) + super().__init__(variables) + + @property + def rad_theta(self) -> float: + """ + Compute the Toroidal angular span of the TF coil in radians + """ + return np.radians(self.variables.theta_TF.value) + @property def area(self) -> float: """ @@ -156,10 +200,10 @@ def area(self) -> float: return ( 0.5 * ( - self.dx_at_radius(self.params.Ri.value) - + self.dx_at_radius(self.params.Rk.value) + _dx_at_radius(self.variables.Ri.value, self.rad_theta) + + _dx_at_radius(self.variables.Rk.value, self.rad_theta) ) - * (self.params.Ri.value - self.params.Rk.value) + * (self.variables.Ri.value - self.variables.Rk.value) ) def create_shape(self, label: str = "") -> BluemiraWire: @@ -173,18 +217,48 @@ def create_shape(self, label: str = "") -> BluemiraWire: Coordinates are ordered counterclockwise starting from the top-left corner: [(-dx_outer/2, Ri), (dx_outer/2, Ri), (dx_inner/2, Rk), (-dx_inner/2, Rk)]. """ - dx_outer = self.dx_at_radius(self.params.Ri.value) - dx_inner = self.dx_at_radius(self.params.Rk.value) + dx_outer = _dx_at_radius(self.variables.Ri.value, self.rad_theta) + dx_inner = _dx_at_radius(self.variables.Rk.value, self.rad_theta) + + return make_polygon( + [ + [-dx_outer / 2, self.variables.Ri.value], + [dx_outer / 2, self.variables.Ri.value], + [dx_inner / 2, self.variables.Rk.value], + [-dx_inner / 2, self.variables.Rk.value], + ], + label=label, + ) - return np.array([ - [-dx_outer / 2, self.params.Ri.value], - [dx_outer / 2, self.params.Ri.value], - [dx_inner / 2, self.params.Rk.value], - [-dx_inner / 2, self.params.Rk.value], - ]) + +@dataclass +class WedgedGeometryOptVariables(OptVariablesFrame): + """Optimisiation variables for Wedged Geometry.""" + + Ri: OptVariable = ov( + "Ri", + 3, # value? + lower_bound=0, + upper_bound=np.inf, + description="External radius of the TF coil case [m].", + ) + Rk: OptVariable = ov( + "Rk", + 5, # value? + lower_bound=0, + upper_bound=np.inf, + description="Internal radius of the TF coil case [m].", + ) + theta_TF: OptVariable = ov( + "theta_TF", + 15, # value? + lower_bound=0, + upper_bound=360, + description="Toroidal angular span of the TF coil [degrees].", + ) -class WedgedGeometry(GeometryParameterisation): +class WedgedGeometry(GeometryParameterisation[WedgedGeometryOptVariables]): """ TF coil case shaped as a sector of an annulus (wedge with arcs). @@ -192,6 +266,18 @@ class WedgedGeometry(GeometryParameterisation): connected by radial lines, forming a wedge-like shape. """ + def __init__(self, var_dict: VarDictT | None = None): + variables = WedgedGeometryOptVariables() + variables.adjust_variables(var_dict, strict_bounds=False) + super().__init__(variables) + + @property + def rad_theta(self) -> float: + """ + Compute the Toroidal angular span of the TF coil in radians + """ + return np.radians(self.variables.theta_TF.value) + def area(self) -> float: """ Compute the cross-sectional area of the wedge geometry. @@ -203,7 +289,9 @@ def area(self) -> float: and inner radius Rk over the toroidal angle theta_TF. """ return ( - 0.5 * self.rad_theta_TF * (self.params.Ri.value**2 - self.params.Rk.value**2) + 0.5 + * self.rad_theta + * (self.variables.Ri.value**2 - self.variables.Rk.value**2) ) def create_shape(self, label: str = "", n_points: int = 50) -> BluemiraWire: @@ -223,22 +311,22 @@ def create_shape(self, label: str = "", n_points: int = 50) -> BluemiraWire: : Array of (x, y) coordinates [m] describing the wedge polygon. """ - theta1 = -self.rad_theta_TF / 2 + theta1 = -self.rad_theta / 2 theta2 = -theta1 angles_outer = np.linspace(theta1, theta2, n_points) angles_inner = np.linspace(theta2, theta1, n_points) arc_outer = np.column_stack(( - self.params.Ri.value * np.sin(angles_outer), - self.params.Ri.value * np.cos(angles_outer), + self.variables.Ri.value * np.sin(angles_outer), + self.variables.Ri.value * np.cos(angles_outer), )) arc_inner = np.column_stack(( - self.params.Rk.value * np.sin(angles_inner), - self.params.Rk.value * np.cos(angles_inner), + self.variables.Rk.value * np.sin(angles_inner), + self.variables.Rk.value * np.cos(angles_inner), )) - return np.vstack((arc_outer, arc_inner)) + return make_polygon(np.vstack((arc_outer, arc_inner)), label=label) @dataclass From c1ebf2ada231cbc7528489e715a20229f6f0b2c6 Mon Sep 17 00:00:00 2001 From: Jonathan Matthews Date: Wed, 27 Aug 2025 10:34:14 +0100 Subject: [PATCH 18/61] consolidate some of the stiffness calculations in case_tf --- bluemira/magnets/case_tf.py | 145 ++++++++---------------------------- 1 file changed, 32 insertions(+), 113 deletions(-) diff --git a/bluemira/magnets/case_tf.py b/bluemira/magnets/case_tf.py index e46df23556..505442f453 100644 --- a/bluemira/magnets/case_tf.py +++ b/bluemira/magnets/case_tf.py @@ -966,51 +966,6 @@ def dx_vault(self): """ return (self.R_wp_k[-1] + self.Rk) * np.tan(self.rad_theta_TF / 2) - def Kx_ps(self, op_cond: OperationalConditions) -> float: # noqa: N802 - """ - Compute the equivalent radial stiffness of the poloidal support (PS) region. - - Parameters - ---------- - op_cond: - Operational conditions including temperature, magnetic field, and strain - at which to calculate the material property. - - Returns - ------- - : - Equivalent radial stiffness of the poloidal support [Pa]. - """ - return ( - self.mat_case.youngs_modulus(op_cond) * self.params.dy_ps.value / self.dx_ps - ) - - def Kx_lat(self, op_cond: OperationalConditions) -> np.ndarray: # noqa: N802 - """ - Compute the equivalent radial stiffness of the lateral case sections. - - These are the mechanical links between each winding pack and the outer case. - Each lateral segment is approximated as a rectangular element. - - Parameters - ---------- - op_cond: - Operational conditions including temperature, magnetic field, and strain - at which to calculate the material property. - - Returns - ------- - : - Array of radial stiffness values for each lateral segment [Pa]. - """ - dx_lat = np.array([ - (self.R_wp_i[i] + self.R_wp_k[i]) / 2 * np.tan(self.rad_theta_TF / 2) - - w.dx / 2 - for i, w in enumerate(self.WPs) - ]) - dy_lat = np.array([w.dy for w in self.WPs]) - return self.mat_case.youngs_modulus(op_cond) * dy_lat / dx_lat - def Kx_vault(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ Compute the equivalent radial stiffness of the vault region. @@ -1052,19 +1007,36 @@ def Kx(self, op_cond: OperationalConditions) -> float: # noqa: N802 : Total equivalent radial stiffness of the TF case [Pa]. """ + # toroidal stiffness of the poloidal support region + kx_ps = ( + self.mat_case.youngs_modulus(op_cond) / self.dx_ps * self.params.dy_ps.value + ) + dx_lat = np.array([ + (self.R_wp_i[i] + self.R_wp_k[i]) / 2 * np.tan(self.rad_theta_TF / 2) + - w.dx / 2 + for i, w in enumerate(self.WPs) + ]) + dy_lat = np.array([w.dy for w in self.WPs]) + # toroidal stiffness of lateral case sections per winding pack + kx_lat = self.mat_case.youngs_modulus(op_cond) / dx_lat * dy_lat temp = [ reciprocal_summation([ - self.Kx_lat(op_cond)[i], + kx_lat[i], w.Kx(op_cond), - self.Kx_lat(op_cond)[i], + kx_lat[i], ]) for i, w in enumerate(self.WPs) ] - return summation([self.Kx_ps(op_cond), self.Kx_vault(op_cond), *temp]) + return summation([kx_ps, self.Kx_vault(op_cond), *temp]) - def Ky_ps(self, op_cond: OperationalConditions) -> float: # noqa: N802 + def Ky(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ - Compute the equivalent toroidal stiffness of the poloidal support (PS) region. + Compute the total equivalent toroidal stiffness of the entire case structure. + + Combines: + - Each winding pack and its adjacent lateral case sections in parallel + - These parallel combinations are arranged in series with the PS and + vault regions Parameters ---------- @@ -1075,88 +1047,35 @@ def Ky_ps(self, op_cond: OperationalConditions) -> float: # noqa: N802 Returns ------- : - Equivalent toroidal stiffness of the PS region [Pa]. + Total equivalent toroidal stiffness of the TF case [Pa]. """ - return ( + # toroidal stiffness of the poloidal support region + ky_ps = ( self.mat_case.youngs_modulus(op_cond) * self.dx_ps / self.params.dy_ps.value ) - - def Ky_lat(self, op_cond: OperationalConditions) -> np.ndarray: # noqa: N802 - """ - Compute the equivalent toroidal stiffness of lateral case sections - per winding pack. - - Each lateral piece is treated as a rectangular beam in the toroidal direction. - - Parameters - ---------- - op_cond: - Operational conditions including temperature, magnetic field, and strain - at which to calculate the material property. - - Returns - ------- - : - Array of toroidal stiffness values for each lateral segment [Pa]. - """ dx_lat = np.array([ (self.R_wp_i[i] + self.R_wp_k[i]) / 2 * np.tan(self.rad_theta_TF / 2) - w.dx / 2 for i, w in enumerate(self.WPs) ]) dy_lat = np.array([w.dy for w in self.WPs]) - return self.mat_case.youngs_modulus(op_cond) * dx_lat / dy_lat - - def Ky_vault(self, op_cond: OperationalConditions) -> float: # noqa: N802 - """ - Compute the equivalent toroidal stiffness of the vault region. - - Parameters - ---------- - op_cond: - Operational conditions including temperature, magnetic field, and strain - at which to calculate the material property. - - Returns - ------- - : - Equivalent toroidal stiffness of the vault [Pa]. - """ - return ( + # toroidal stiffness of lateral case sections per winding pack + ky_lat = self.mat_case.youngs_modulus(op_cond) * dx_lat / dy_lat + # toroidal stiffness of the vault region + ky_vault = ( self.mat_case.youngs_modulus(op_cond) * self.dx_vault / self.params.dy_vault.value ) - - def Ky(self, op_cond: OperationalConditions) -> float: # noqa: N802 - """ - Compute the total equivalent toroidal stiffness of the entire case structure. - - Combines: - - Each winding pack and its adjacent lateral case sections in parallel - - These parallel combinations are arranged in series with the PS and - vault regions - - Parameters - ---------- - op_cond: - Operational conditions including temperature, magnetic field, and strain - at which to calculate the material property. - - Returns - ------- - : - Total equivalent toroidal stiffness of the TF case [Pa]. - """ temp = [ summation([ - self.Ky_lat(op_cond)[i], + ky_lat[i], w.Ky(op_cond), - self.Ky_lat(op_cond)[i], + ky_lat[i], ]) for i, w in enumerate(self.WPs) ] - return reciprocal_summation([self.Ky_ps(op_cond), self.Ky_vault(op_cond), *temp]) + return reciprocal_summation([ky_ps, ky_vault, *temp]) def rearrange_conductors_in_wp( self, From 54614c9cb244e45773c06fd66a0560fd65c29e57 Mon Sep 17 00:00:00 2001 From: Jonathan Matthews Date: Wed, 27 Aug 2025 11:16:23 +0100 Subject: [PATCH 19/61] remove unnecessary function for cable K when can just call it directly --- bluemira/magnets/conductor.py | 34 ++++++---------------------------- 1 file changed, 6 insertions(+), 28 deletions(-) diff --git a/bluemira/magnets/conductor.py b/bluemira/magnets/conductor.py index 8518b2e6a7..2aaebc36fb 100644 --- a/bluemira/magnets/conductor.py +++ b/bluemira/magnets/conductor.py @@ -258,10 +258,10 @@ def Cp(self, op_cond: OperationalConditions) -> float: # noqa: N802 ]) return summation(weighted_specific_heat) / self.area - def _mat_ins_y_modulus(self, op_cond: OperationalConditions): + def _mat_ins_y_modulus(self, op_cond: OperationalConditions): # why? return self.mat_ins.youngs_modulus(op_cond) - def _mat_jacket_y_modulus(self, op_cond: OperationalConditions): + def _mat_jacket_y_modulus(self, op_cond: OperationalConditions): # why? return self.mat_jacket.youngs_modulus(op_cond) def _Kx_topbot_ins(self, op_cond: OperationalConditions) -> float: # noqa: N802 @@ -318,17 +318,6 @@ def _Kx_topbot_jacket(self, op_cond: OperationalConditions) -> float: # noqa: N / self.params.dx_jacket.value ) - def _Kx_cable(self, op_cond: OperationalConditions) -> float: # noqa: N802 - """ - Equivalent stiffness of the cable in the x-direction. - - Returns - ------- - : - Axial stiffness [N/m] - """ - return self.cable.Kx(op_cond) - def Kx(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ Equivalent stiffness of the conductor in the x-direction. @@ -344,7 +333,7 @@ def Kx(self, op_cond: OperationalConditions) -> float: # noqa: N802 reciprocal_summation([ self._Kx_topbot_ins(op_cond), self._Kx_topbot_jacket(op_cond), - self._Kx_cable(op_cond), + self.cable.Kx(op_cond), self._Kx_topbot_jacket(op_cond), self._Kx_topbot_ins(op_cond), ]), @@ -406,17 +395,6 @@ def _Ky_topbot_jacket(self, op_cond: OperationalConditions) -> float: # noqa: N / self.params.dy_jacket.value ) - def _Ky_cable(self, op_cond: OperationalConditions) -> float: # noqa: N802 - """ - Equivalent stiffness of the cable in the y-direction. - - Returns - ------- - : - Axial stiffness [N/m] - """ - return self.cable.Ky(op_cond) - def Ky(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ Equivalent stiffness of the conductor in the y-direction. @@ -432,7 +410,7 @@ def Ky(self, op_cond: OperationalConditions) -> float: # noqa: N802 reciprocal_summation([ self._Ky_topbot_ins(op_cond), self._Ky_topbot_jacket(op_cond), - self._Ky_cable(op_cond), + self.cable.Ky(op_cond), self._Ky_topbot_jacket(op_cond), self._Ky_topbot_ins(op_cond), ]), @@ -490,7 +468,7 @@ def _tresca_sigma_jacket( 2 * self._Ky_lat_ins(op_cond), 2 * self._Ky_lat_jacket(op_cond), reciprocal_summation([ - self._Ky_cable(op_cond), + self.cable.Ky(op_cond), self._Ky_topbot_jacket(op_cond) / 2, ]), ]) @@ -506,7 +484,7 @@ def _tresca_sigma_jacket( 2 * self._Kx_lat_ins(op_cond), 2 * self._Kx_lat_jacket(op_cond), reciprocal_summation([ - self._Kx_cable(op_cond), + self.cable.Kx(op_cond), self._Kx_topbot_jacket(op_cond) / 2, ]), ]) From 4dd6c7a4250856330688a161b028bbc8add9f7f8 Mon Sep 17 00:00:00 2001 From: Jonathan Matthews Date: Wed, 27 Aug 2025 12:57:00 +0100 Subject: [PATCH 20/61] make dx and dy's consistent with rest of bluemira to mean half-width rather than full width --- bluemira/magnets/cable.py | 38 +++++++++---------- bluemira/magnets/case_tf.py | 51 ++++++++++++------------- bluemira/magnets/conductor.py | 64 +++++++++++++++++--------------- bluemira/magnets/winding_pack.py | 20 +++++----- 4 files changed, 85 insertions(+), 88 deletions(-) diff --git a/bluemira/magnets/cable.py b/bluemira/magnets/cable.py index 940e641e72..9910fa0f9d 100644 --- a/bluemira/magnets/cable.py +++ b/bluemira/magnets/cable.py @@ -137,12 +137,12 @@ def __init__( @property @abstractmethod def dx(self): - """Cable dimension in the x-direction [m].""" + """Half Cable dimension in the x-direction [m].""" @property @abstractmethod def dy(self): - """Cable dimension in the y-direction [m].""" + """Half Cable dimension in the y-direction [m].""" @property def aspect_ratio(self): @@ -541,13 +541,11 @@ def plot( _, ax = plt.subplots() pc = np.array([xc, yc]) - a = self.dx / 2 - b = self.dy / 2 - p0 = np.array([-a, -b]) - p1 = np.array([a, -b]) - p2 = np.array([[a, b]]) - p3 = np.array([-a, b]) + p0 = np.array([-self.dx, -self.dy]) + p1 = np.array([self.dx, -self.dy]) + p2 = np.array([[self.dx, self.dy]]) + p3 = np.array([-self.dx, self.dy]) points_ext = np.vstack((p0, p1, p2, p3, p0)) + pc points_cc = ( @@ -688,7 +686,7 @@ class RectangularCableParams(CableParams): """ dx: Parameter[float] - """Cable width in the x-direction [m].""" + """Cable half-width in the x-direction [m].""" class RectangularCable(ABCCable): @@ -750,19 +748,19 @@ def __init__( @property def dx(self) -> float: - """Cable dimension in the x direction [m]""" + """Half Cable dimension in the x direction [m]""" return self.params.dx.value @property def dy(self) -> float: - """Cable dimension in the y direction [m]""" - return self.area / self.params.dx.value + """Half Cable dimension in the y direction [m]""" + return self.area / self.params.dx.value / 4 # Decide if this function shall be a setter. # Defined as "normal" function to underline that it modifies dx. def set_aspect_ratio(self, value: float): """Modify dx in order to get the given aspect ratio""" - self.params.dx.value = np.sqrt(value * self.area) + self.params.dx.value = np.sqrt(value * self.area) / 2 # OD homogenized structural properties def Kx(self, op_cond: OperationalConditions) -> float: # noqa: N802 @@ -980,12 +978,12 @@ def __init__( @property def dx(self) -> float: - """Cable dimension in the x direction [m]""" - return np.sqrt(self.area) + """Half Cable dimension in the x direction [m]""" + return np.sqrt(self.area / 4) @property def dy(self) -> float: - """Cable dimension in the y direction [m]""" + """Half Cable dimension in the y direction [m]""" return self.dx # OD homogenized structural properties @@ -1128,12 +1126,12 @@ def __init__( @property def dx(self) -> float: - """Cable dimension in the x direction [m] (i.e. cable's diameter)""" - return np.sqrt(self.area * 4 / np.pi) + """Half Cable dimension in the x direction [m] (i.e. cable's radius)""" + return np.sqrt(self.area / np.pi) @property def dy(self) -> float: - """Cable dimension in the y direction [m] (i.e. cable's diameter)""" + """Half Cable dimension in the y direction [m] (i.e. cable's radius)""" return self.dx # OD homogenized structural properties @@ -1217,7 +1215,7 @@ def plot( points_ext = ( np.array([ - np.array([np.cos(theta), np.sin(theta)]) * self.dx / 2 + np.array([np.cos(theta), np.sin(theta)]) * self.dx for theta in np.linspace(0, np.radians(360), 19) ]) + pc diff --git a/bluemira/magnets/case_tf.py b/bluemira/magnets/case_tf.py index 505442f453..c79d2762a8 100644 --- a/bluemira/magnets/case_tf.py +++ b/bluemira/magnets/case_tf.py @@ -50,7 +50,7 @@ def _dx_at_radius(radius: float, rad_theta: float) -> float: """ - Compute the toroidal width at a given radial position. + Compute the toroidal half-width at a given radial position. Parameters ---------- @@ -64,7 +64,7 @@ def _dx_at_radius(radius: float, rad_theta: float) -> float: : Toroidal width [m] at the given radius. """ - return 2 * radius * np.tan(rad_theta / 2) + return radius * np.tan(rad_theta / 2) @dataclass @@ -198,13 +198,9 @@ def area(self) -> float: Cross-sectional area [m²]. """ return ( - 0.5 - * ( - _dx_at_radius(self.variables.Ri.value, self.rad_theta) - + _dx_at_radius(self.variables.Rk.value, self.rad_theta) - ) - * (self.variables.Ri.value - self.variables.Rk.value) - ) + 2 * _dx_at_radius(self.variables.Ri.value, self.rad_theta) + + 2 * _dx_at_radius(self.variables.Rk.value, self.rad_theta) + ) * (self.variables.Ri.value - self.variables.Rk.value) def create_shape(self, label: str = "") -> BluemiraWire: """ @@ -215,17 +211,17 @@ def create_shape(self, label: str = "") -> BluemiraWire: : Array of shape (4, 2) representing the corners of the trapezoid. Coordinates are ordered counterclockwise starting from the top-left corner: - [(-dx_outer/2, Ri), (dx_outer/2, Ri), (dx_inner/2, Rk), (-dx_inner/2, Rk)]. + [(-dx_outer, Ri), (dx_outer, Ri), (dx_inner, Rk), (-dx_inner, Rk)]. """ - dx_outer = _dx_at_radius(self.variables.Ri.value, self.rad_theta) - dx_inner = _dx_at_radius(self.variables.Rk.value, self.rad_theta) + dx_outer = 2 * _dx_at_radius(self.variables.Ri.value, self.rad_theta) + dx_inner = 2 * _dx_at_radius(self.variables.Rk.value, self.rad_theta) return make_polygon( [ - [-dx_outer / 2, self.variables.Ri.value], - [dx_outer / 2, self.variables.Ri.value], - [dx_inner / 2, self.variables.Rk.value], - [-dx_inner / 2, self.variables.Rk.value], + [-dx_outer, self.variables.Ri.value], + [dx_outer, self.variables.Ri.value], + [dx_inner, self.variables.Rk.value], + [-dx_inner, self.variables.Rk.value], ], label=label, ) @@ -388,10 +384,13 @@ def __init__( WPs=WPs, name=name, ) - self.dx_i = 2 * self.params.Ri.value * np.tan(self.rad_theta_TF / 2) + # Toroidal half-length of the coil case at its maximum radial position [m] + self.dx_i = _dx_at_radius(self.params.Ri.value, self.rad_theta_TF) + # Average toroidal length of the ps plate self.dx_ps = ( self.params.Ri.value + (self.params.Ri.value - self.params.dy_ps.value) ) * np.tan(self.rad_theta_TF / 2) + # sets Rk self.update_dy_vault(self.params.dy_vault.value) @property @@ -540,7 +539,7 @@ def dy_wp_i(self) -> np.ndarray: Array containing the radial thickness [m] of each Winding Pack. Each element corresponds to one WP in the self.WPs list. """ - return np.array([wp.dy for wp in self.WPs]) + return np.array([2 * wp.dy for wp in self.WPs]) @property def dy_wp_tot(self) -> float: @@ -741,11 +740,11 @@ def enforce_wp_layout_rules( n_conductors: Number of conductors to allocate. dx_WP: - Available toroidal width for the winding pack [m]. + Available toroidal half-width for the winding pack [m]. dx_cond: - Toroidal width of a single conductor [m]. + Toroidal half-width of a single conductor [m]. dy_cond: - Radial height of a single conductor [m]. + Radial half-height of a single conductor [m]. layout: Layout type: - "auto" : no constraints @@ -1012,11 +1011,10 @@ def Kx(self, op_cond: OperationalConditions) -> float: # noqa: N802 self.mat_case.youngs_modulus(op_cond) / self.dx_ps * self.params.dy_ps.value ) dx_lat = np.array([ - (self.R_wp_i[i] + self.R_wp_k[i]) / 2 * np.tan(self.rad_theta_TF / 2) - - w.dx / 2 + (self.R_wp_i[i] + self.R_wp_k[i]) / 2 * np.tan(self.rad_theta_TF / 2) - w.dx for i, w in enumerate(self.WPs) ]) - dy_lat = np.array([w.dy for w in self.WPs]) + dy_lat = np.array([2 * w.dy for w in self.WPs]) # toroidal stiffness of lateral case sections per winding pack kx_lat = self.mat_case.youngs_modulus(op_cond) / dx_lat * dy_lat temp = [ @@ -1054,11 +1052,10 @@ def Ky(self, op_cond: OperationalConditions) -> float: # noqa: N802 self.mat_case.youngs_modulus(op_cond) * self.dx_ps / self.params.dy_ps.value ) dx_lat = np.array([ - (self.R_wp_i[i] + self.R_wp_k[i]) / 2 * np.tan(self.rad_theta_TF / 2) - - w.dx / 2 + (self.R_wp_i[i] + self.R_wp_k[i]) / 2 * np.tan(self.rad_theta_TF / 2) - w.dx for i, w in enumerate(self.WPs) ]) - dy_lat = np.array([w.dy for w in self.WPs]) + dy_lat = np.array([2 * w.dy for w in self.WPs]) # toroidal stiffness of lateral case sections per winding pack ky_lat = self.mat_case.youngs_modulus(op_cond) * dx_lat / dy_lat # toroidal stiffness of the vault region diff --git a/bluemira/magnets/conductor.py b/bluemira/magnets/conductor.py index 2aaebc36fb..6f5210c71d 100644 --- a/bluemira/magnets/conductor.py +++ b/bluemira/magnets/conductor.py @@ -91,35 +91,27 @@ def __init__( @property def dx(self): - """x-dimension of the conductor [m]""" - return ( - self.params.dx_ins.value * 2 - + self.params.dx_jacket.value * 2 - + self.cable.dx - ) + """Half x-dimension of the conductor [m]""" + return self.params.dx_ins.value + self.params.dx_jacket.value + self.cable.dx @property def dy(self): - """y-dimension of the conductor [m]""" - return ( - self.params.dy_ins.value * 2 - + self.params.dy_jacket.value * 2 - + self.cable.dy - ) + """Half y-dimension of the conductor [m]""" + return self.params.dy_ins.value + self.params.dy_jacket.value + self.cable.dy @property def area(self): """Area of the conductor [m^2]""" - return self.dx * self.dy + return self.dx * self.dy * 4 - # jm - surely this should be done from inside out ie cable dx + jacket dx - # rather than out in and depend on more variables? @property def area_jacket(self): """Area of the jacket [m^2]""" - return (self.dx - 2 * self.params.dx_ins.value) * ( - self.dy - 2 * self.params.dy_ins.value - ) - self.cable.area + return ( + 4 + * (self.cable.dx + self.params.dx_jacket.value) + * (self.cable.dy + self.params.dy_jacket.value) + ) @property def area_ins(self): @@ -274,7 +266,10 @@ def _Kx_topbot_ins(self, op_cond: OperationalConditions) -> float: # noqa: N802 Axial stiffness [N/m] """ return ( - self._mat_ins_y_modulus(op_cond) * self.cable.dy / self.params.dx_ins.value + self._mat_ins_y_modulus(op_cond) + * 2 + * self.cable.dy + / self.params.dx_ins.value ) def _Kx_lat_ins(self, op_cond: OperationalConditions) -> float: # noqa: N802 @@ -286,7 +281,9 @@ def _Kx_lat_ins(self, op_cond: OperationalConditions) -> float: # noqa: N802 : Axial stiffness [N/m] """ - return self._mat_ins_y_modulus(op_cond) * self.params.dy_ins.value / self.dx + return ( + self._mat_ins_y_modulus(op_cond) * self.params.dy_ins.value / (2 * self.dx) + ) def _Kx_lat_jacket(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ @@ -300,7 +297,7 @@ def _Kx_lat_jacket(self, op_cond: OperationalConditions) -> float: # noqa: N802 return ( self._mat_jacket_y_modulus(op_cond) * self.params.dy_jacket.value - / (self.dx - 2 * self.params.dx_ins.value) + / (2 * self.dx - 2 * self.params.dx_ins.value) ) def _Kx_topbot_jacket(self, op_cond: OperationalConditions) -> float: # noqa: N802 @@ -314,6 +311,7 @@ def _Kx_topbot_jacket(self, op_cond: OperationalConditions) -> float: # noqa: N """ return ( self._mat_jacket_y_modulus(op_cond) + * 2 * self.cable.dy / self.params.dx_jacket.value ) @@ -351,7 +349,10 @@ def _Ky_topbot_ins(self, op_cond: OperationalConditions) -> float: # noqa: N802 Axial stiffness [N/m] """ return ( - self._mat_ins_y_modulus(op_cond) * self.cable.dx / self.params.dy_ins.value + self._mat_ins_y_modulus(op_cond) + * 2 + * self.cable.dx + / self.params.dy_ins.value ) def _Ky_lat_ins(self, op_cond: OperationalConditions) -> float: # noqa: N802 @@ -363,7 +364,9 @@ def _Ky_lat_ins(self, op_cond: OperationalConditions) -> float: # noqa: N802 : Axial stiffness [N/m] """ - return self._mat_ins_y_modulus(op_cond) * self.params.dx_ins.value / self.dy + return ( + self._mat_ins_y_modulus(op_cond) * self.params.dx_ins.value / (2 * self.dy) + ) def _Ky_lat_jacket(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ @@ -377,7 +380,7 @@ def _Ky_lat_jacket(self, op_cond: OperationalConditions) -> float: # noqa: N802 return ( self._mat_jacket_y_modulus(op_cond) * self.params.dx_jacket.value - / (self.dy - 2 * self.params.dy_ins.value) + / (2 * self.dy - 2 * self.params.dy_ins.value) ) def _Ky_topbot_jacket(self, op_cond: OperationalConditions) -> float: # noqa: N802 @@ -391,6 +394,7 @@ def _Ky_topbot_jacket(self, op_cond: OperationalConditions) -> float: # noqa: N """ return ( self._mat_jacket_y_modulus(op_cond) + * 2 * self.cable.dx / self.params.dy_jacket.value ) @@ -460,8 +464,8 @@ def _tresca_sigma_jacket( raise ValueError("Invalid direction: choose either 'x' or 'y'.") if direction == "x": - saf_jacket = (self.cable.dx + 2 * self.params.dx_jacket.value) / ( - 2 * self.params.dx_jacket.value + saf_jacket = (self.cable.dx + self.params.dx_jacket.value) / ( + self.params.dx_jacket.value ) K = summation([ # noqa: N806 @@ -476,8 +480,8 @@ def _tresca_sigma_jacket( X_jacket = 2 * self._Ky_lat_jacket(op_cond) / K # noqa: N806 else: - saf_jacket = (self.cable.dy + 2 * self.params.dy_jacket.value) / ( - 2 * self.params.dy_jacket.value + saf_jacket = (self.cable.dy + self.params.dy_jacket.value) / ( + self.params.dy_jacket.value ) K = summation([ # noqa: N806 @@ -699,8 +703,8 @@ def plot(self, xc: float = 0, yc: float = 0, *, show: bool = False, ax=None): _, ax = plt.subplots() pc = np.array([xc, yc]) - a = self.cable.dx / 2 + self.params.dx_jacket.value - b = self.cable.dy / 2 + self.params.dy_jacket.value + a = self.cable.dx + self.params.dx_jacket.value + b = self.cable.dy + self.params.dy_jacket.value p0 = np.array([-a, -b]) p1 = np.array([a, -b]) diff --git a/bluemira/magnets/winding_pack.py b/bluemira/magnets/winding_pack.py index bad3c9dcc6..26fb294224 100644 --- a/bluemira/magnets/winding_pack.py +++ b/bluemira/magnets/winding_pack.py @@ -73,18 +73,18 @@ def __init__( @property def dx(self) -> float: - """Return the total width of the winding pack [m].""" + """Return the half width of the winding pack [m].""" return self.conductor.dx * self.params.nx.value @property def dy(self) -> float: - """Return the total height of the winding pack [m].""" + """Return the half height of the winding pack [m].""" return self.conductor.dy * self.params.ny.value @property def area(self) -> float: """Return the total cross-sectional area [m²].""" - return self.dx * self.dy + return 4 * self.dx * self.dy @property def n_conductors(self) -> int: @@ -164,13 +164,11 @@ def plot( _, ax = plt.subplots() pc = np.array([xc, yc]) - a = self.dx / 2 - b = self.dy / 2 - p0 = np.array([-a, -b]) - p1 = np.array([a, -b]) - p2 = np.array([a, b]) - p3 = np.array([-a, b]) + p0 = np.array([-self.dx, -self.dy]) + p1 = np.array([self.dx, -self.dy]) + p2 = np.array([self.dx, self.dy]) + p3 = np.array([-self.dx, self.dy]) points_ext = np.vstack((p0, p1, p2, p3, p0)) + pc @@ -180,8 +178,8 @@ def plot( if not homogenized: for i in range(self.params.nx.value): for j in range(self.params.ny.value): - xc_c = xc - self.dx / 2 + (i + 0.5) * self.conductor.dx - yc_c = yc - self.dy / 2 + (j + 0.5) * self.conductor.dy + xc_c = xc - self.dx + (2 * i + 1) * self.conductor.dx + yc_c = yc - self.dy + (2 * j + 1) * self.conductor.dy self.conductor.plot(xc=xc_c, yc=yc_c, ax=ax) if show: From 3da7a07d0d6c98c6267255863165bdba72dd1e4f Mon Sep 17 00:00:00 2001 From: Jonathan Matthews Date: Wed, 27 Aug 2025 16:13:48 +0100 Subject: [PATCH 21/61] remove _mat_ins_y_modulus and _mat_jacket_y_modulus and just called the material directly --- bluemira/magnets/conductor.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/bluemira/magnets/conductor.py b/bluemira/magnets/conductor.py index 6f5210c71d..374b9c78b3 100644 --- a/bluemira/magnets/conductor.py +++ b/bluemira/magnets/conductor.py @@ -250,12 +250,6 @@ def Cp(self, op_cond: OperationalConditions) -> float: # noqa: N802 ]) return summation(weighted_specific_heat) / self.area - def _mat_ins_y_modulus(self, op_cond: OperationalConditions): # why? - return self.mat_ins.youngs_modulus(op_cond) - - def _mat_jacket_y_modulus(self, op_cond: OperationalConditions): # why? - return self.mat_jacket.youngs_modulus(op_cond) - def _Kx_topbot_ins(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ Equivalent stiffness of the top/bottom insulator in the x-direction. @@ -266,7 +260,7 @@ def _Kx_topbot_ins(self, op_cond: OperationalConditions) -> float: # noqa: N802 Axial stiffness [N/m] """ return ( - self._mat_ins_y_modulus(op_cond) + self.mat_ins.youngs_modulus(op_cond) * 2 * self.cable.dy / self.params.dx_ins.value @@ -282,7 +276,9 @@ def _Kx_lat_ins(self, op_cond: OperationalConditions) -> float: # noqa: N802 Axial stiffness [N/m] """ return ( - self._mat_ins_y_modulus(op_cond) * self.params.dy_ins.value / (2 * self.dx) + self.mat_ins.youngs_modulus(op_cond) + * self.params.dy_ins.value + / (2 * self.dx) ) def _Kx_lat_jacket(self, op_cond: OperationalConditions) -> float: # noqa: N802 @@ -295,7 +291,7 @@ def _Kx_lat_jacket(self, op_cond: OperationalConditions) -> float: # noqa: N802 Axial stiffness [N/m] """ return ( - self._mat_jacket_y_modulus(op_cond) + self.mat_jacket.youngs_modulus(op_cond) * self.params.dy_jacket.value / (2 * self.dx - 2 * self.params.dx_ins.value) ) @@ -310,7 +306,7 @@ def _Kx_topbot_jacket(self, op_cond: OperationalConditions) -> float: # noqa: N Axial stiffness [N/m] """ return ( - self._mat_jacket_y_modulus(op_cond) + self.mat_jacket.youngs_modulus(op_cond) * 2 * self.cable.dy / self.params.dx_jacket.value @@ -349,7 +345,7 @@ def _Ky_topbot_ins(self, op_cond: OperationalConditions) -> float: # noqa: N802 Axial stiffness [N/m] """ return ( - self._mat_ins_y_modulus(op_cond) + self.mat_ins.youngs_modulus(op_cond) * 2 * self.cable.dx / self.params.dy_ins.value @@ -365,7 +361,9 @@ def _Ky_lat_ins(self, op_cond: OperationalConditions) -> float: # noqa: N802 Axial stiffness [N/m] """ return ( - self._mat_ins_y_modulus(op_cond) * self.params.dx_ins.value / (2 * self.dy) + self.mat_ins.youngs_modulus(op_cond) + * self.params.dx_ins.value + / (2 * self.dy) ) def _Ky_lat_jacket(self, op_cond: OperationalConditions) -> float: # noqa: N802 @@ -378,7 +376,7 @@ def _Ky_lat_jacket(self, op_cond: OperationalConditions) -> float: # noqa: N802 Axial stiffness [N/m] """ return ( - self._mat_jacket_y_modulus(op_cond) + self.mat_jacket.youngs_modulus(op_cond) * self.params.dx_jacket.value / (2 * self.dy - 2 * self.params.dy_ins.value) ) @@ -393,7 +391,7 @@ def _Ky_topbot_jacket(self, op_cond: OperationalConditions) -> float: # noqa: N Axial stiffness [N/m] """ return ( - self._mat_jacket_y_modulus(op_cond) + self.mat_jacket.youngs_modulus(op_cond) * 2 * self.cable.dx / self.params.dy_jacket.value From fe7ece4b84d97c871f94cc208d1c511b56a5bdc0 Mon Sep 17 00:00:00 2001 From: Jonathan Matthews Date: Thu, 28 Aug 2025 15:08:08 +0100 Subject: [PATCH 22/61] First pass at a TFCoilXYDesigner class that breaks up creation of strand, cable, conductor, winding pack and casing --- bluemira/magnets/tfcoil_designer.py | 366 ++++++++++++++++++++++++++++ 1 file changed, 366 insertions(+) create mode 100644 bluemira/magnets/tfcoil_designer.py diff --git a/bluemira/magnets/tfcoil_designer.py b/bluemira/magnets/tfcoil_designer.py new file mode 100644 index 0000000000..3adee5e151 --- /dev/null +++ b/bluemira/magnets/tfcoil_designer.py @@ -0,0 +1,366 @@ +# SPDX-FileCopyrightText: 2021-present M. Coleman, J. Cook, F. Franza +# SPDX-FileCopyrightText: 2021-present I.A. Maione, S. McIntosh +# SPDX-FileCopyrightText: 2021-present J. Morris, D. Short +# +# SPDX-License-Identifier: LGPL-2.1-or-later +"""Designer for TF Coil XY cross section.""" + +from dataclasses import dataclass + +import numpy as np + +from bluemira.base.constants import MU_0, MU_0_2PI, MU_0_4PI +from bluemira.base.designer import Designer +from bluemira.base.parameter_frame import Parameter, ParameterFrame +from bluemira.magnets.cable import RectangularCable, RoundCable, SquareCable +from bluemira.magnets.case_tf import TrapezoidalCaseTF +from bluemira.magnets.conductor import Conductor, SymmetricConductor +from bluemira.magnets.strand import Strand, SuperconductingStrand +from bluemira.magnets.utils import delayed_exp_func +from bluemira.magnets.winding_pack import WindingPack + + +@dataclass +class TFCoilXYDesignerParams(ParameterFrame): + """ + Parameters needed for all aspects of the TF coil design + """ + + R0: Parameter[float] + """Major radius [m]""" + B0: Parameter[float] + """Magnetic field at R0 [T]""" + A: Parameter[float] + """Aspect ratio""" + n_TF: Parameter[float] + """Number of TF coils""" + ripple: Parameter[float] + """Maximum plasma ripple""" + d: Parameter[float] + """Additional distance to calculate max external radius of inner TF leg""" + Iop: Parameter[float] + """Operational current in conductor""" + T_sc: Parameter[float] + """Operational temperature of superconducting cable""" + T_margin: Parameter[float] + """Temperature margin""" + t_delay: Parameter[float] + """Time delay for exponential functions""" + t0: Parameter[float] + """Initial time""" + hotspot_target_temperature: Parameter[float] + """Target temperature for hotspot for cable optimisiation""" + S_VV: Parameter[float] + """Vacuum vessel steel limit""" + d_strand_sc: Parameter[float] + """Diameter of superconducting strand""" + d_strand_stab: Parameter[float] + """Diameter of stabilising strand""" + safety_factor: Parameter[float] + """Allowable stress values""" + dx: Parameter[float] + """Cable length""" + B_ref: Parameter[float] + """Reference value for B field (LTS limit) [T]""" + layout: Parameter[str] + """Cable layout strategy""" + wp_reduction_factor: Parameter[float] + """Fractional reduction of available toroidal space for WPs""" + n_layers_reduction: Parameter[int] + """Number of layers to remove after each WP""" + bounds_cond_jacket: Parameter[np.ndarray] + """Min/max bounds for conductor jacket area optimization [m²]""" + bounds_dy_vault: Parameter[np.ndarray] + """Min/max bounds for the case vault thickness optimization [m]""" + max_niter: Parameter[int] + """Maximum number of optimization iterations""" + eps: Parameter[float] + """Convergence threshold for the combined optimization loop.""" + Tau_discharge: Parameter[float] + """Characteristic time constant""" + + +class TFCoilXYDesigner(Designer): + """ + Handles initialisation of TF Coil XY cross section from the individual parts: + - Strands + - Cable + - Conductor + - Winding Pack + - Casing + + Will output a CaseTF object that allows for the access of all constituent parts + and their properties. + """ + + def __init__( + self, + params: dict | ParameterFrame, + build_config: dict, + ): + super().__init__(params, build_config) + + def _derived_values(self): + a = self.params.R0.value / self.params.A.value + Ri = self.params.R0.value - a - self.params.d.value # noqa: N806 + Re = (self.params.R0.value + a) * (1 / self.params.ripple.value) ** ( # noqa: N806 + 1 / self.params.n_TF.value + ) + B_TF_i = 1.08 * ( + MU_0_2PI + * self.params.n_TF.value + * ( + self.params.B0.value + * self.params.R0.value + / MU_0_2PI + / self.params.n_TF.value + ) + / Ri + ) + pm = B_TF_i**2 / (2 * MU_0) + t_z = ( + 0.5 + * np.log(Re / Ri) + * MU_0_4PI + * self.params.n_TF.value + * ( + self.params.B0.value + * self.params.R0.value + / MU_0_2PI + / self.params.n_TF.value + ) + ** 2 + ) + T_op = self.params.T_sc.value + self.params.T_margin.value # noqa: N806 + s_y = 1e9 / self.params.safety_factor.value + n_cond = int( + np.floor( + ( + self.params.B0.value + * self.params.R0.value + / MU_0_2PI + / self.params.n_TF.value + ) + / self.params.Iop.value + ) + ) + min_gap_x = int( + np.floor( + ( + self.params.B0.value + * self.params.R0.value + / MU_0_2PI + / self.params.n_TF.value + ) + / self.params.Iop.value + ) + ) + I_fun = delayed_exp_func( # noqa: N806 + self.params.Iop.value, + self.params.Tau_discharge.value, + self.params.t_delay.value, + ) + B_fun = delayed_exp_func( + B_TF_i, self.params.Tau_discharge.value, self.params.t_delay.value + ) + return { + "a": a, + "Ri": Ri, + "Re": Re, + "B_TF_I": B_TF_i, + "pm": pm, + "t_z": t_z, + "T_op": T_op, + "s_y": s_y, + "n_cond": n_cond, + "min_gap_x": min_gap_x, + "I_fun": I_fun, + "B_fun": B_fun, + } + + def run(self): + """ + Run the TF coil XY design problem. + + Returns + ------- + case: + TF case object all parts that make it up. + """ + # sort configs + stab_strand_config = self.build_config.get("stabilising_strand") + sc_strand_config = self.build_config.get("superconducting_strand") + conductor_config = self.build_config.get("conductor") + case_config = self.build_config.get("case") + # sort params (break down into smaller parts rather than pass in all params?) + stab_strand_params = self.params.get("stab_strand") + sc_strand_params = self.params.get("sc_strand") + cable_params = self.params.get("cable") + conductor_params = self.params.get("conductor") + winding_pack_params = self.params.get("winding_pack") + case_params = self.params.get("case") + optimisation_params = self.params.get("optimisation") + derived_params = self._derived_values() + + stab_strand = self._make_stab_strand(stab_strand_config, stab_strand_params) + sc_strand = self._make_sc_strand(sc_strand_config, sc_strand_params) + initial_cable = self._make_cable(cable_params, stab_strand, sc_strand) + # param frame optimisation stuff? + optimised_cable = initial_cable.optimise_n_stab_ths( + t0=optimisation_params.t0.value, + tf=optimisation_params.Tau_discharge.value, + T_for_hts=derived_params["T_op"], + hotspot_target_temperature=optimisation_params.hotspot_target_temperature.value, + B_fun=derived_params["B_fun"], + I_fun=derived_params["I_fun"], + bounds=[1, 10000], + ) + conductor = self._make_conductor( + conductor_config, conductor_params, optimised_cable + ) + winding_pack = self._make_winding_pack(winding_pack_params, conductor) + case = self._make_case(case_config, case_params, [winding_pack]) + # param frame optimisation stuff? + case.rearrange_conductors_in_wp( + n_conductors=derived_params["n_cond"], + wp_reduction_factor=optimisation_params.wp_reduction_factor.value, + min_gap_x=derived_params["min_gap_x"], + n_layers_reduction=optimisation_params.n_layers_reduction.value, + layout=optimisation_params.layout.value, + ) + # param frame optimisation stuff? + case.optimize_jacket_and_vault( + pm=derived_params["pm"], + fz=derived_params["t_z"], + temperature=derived_params["T_op"], + B=derived_params["B_TF_i"], + allowable_sigma=derived_params["s_y"], + bounds_cond_jacket=optimisation_params.bounds_cond_jacket.value, + bounds_dy_vault=optimisation_params.bounds_dy_vault.value, + layout=optimisation_params.layout.value, + wp_reduction_factor=optimisation_params.wp_reduction_factor.value, + min_gap_x=derived_params["min_gap_x"], + n_layers_reduction=optimisation_params.n_layers_reduction.value, + max_niter=optimisation_params.max_niter.value, + eps=optimisation_params.eps.value, + n_conds=derived_params["n_cond"], + ) + return case + + def B_TF_r(self, tf_current, r): + """ + Compute the magnetic field generated by the TF coils, + including ripple correction. + + Parameters + ---------- + tf_current : float + Toroidal field coil current [A]. + n_TF : int + Number of toroidal field coils. + r : float + Radial position from the tokamak center [m]. + + Returns + ------- + float + Magnetic field intensity [T]. + """ + return 1.08 * (MU_0_2PI * self.params.n_TF.value * tf_current / r) + + def _make_stab_strand(self): + stab_strand_config = self.build_config.get("stabilising_strand") + stab_strand_params = self.params.get("stab_strand") + return Strand( + materials=stab_strand_config.get("material"), + params=stab_strand_params, + name="stab_strand", + ) + + def _make_sc_strand(self): + sc_strand_config = self.build_config.get("superconducting_strand") + sc_strand_params = self.params.get("sc_strand") + return SuperconductingStrand( + materials=sc_strand_config.get("material"), + params=sc_strand_params, + name="sc_strand", + ) + + def _make_cable(self, stab_strand, sc_strand): + cable_params = self.params.get("cable") + if cable_params.cable_type == "Rectangular": + cable = RectangularCable( + sc_strand=sc_strand, + stab_strand=stab_strand, + params=cable_params, + name="RectangularCable", + ) + elif cable_params.cable_type == "Square": + cable = SquareCable( + sc_strand=sc_strand, + stab_strand=stab_strand, + params=cable_params, + name="SquareCable", + ) + elif cable_params.cable_type == "Round": + cable = RoundCable( + sc_strand=sc_strand, + stab_strand=stab_strand, + params=cable_params, + name="RoundCable", + ) + else: + raise ValueError( + f"Cable type {cable_params.cable_type} is not known." + "Available options are 'Rectangular', 'Square' and 'Round'." + ) + return cable + + def _make_conductor(self, cable): + conductor_config = self.build_config.get("conductor") + conductor_params = self.params.get("conductor") + if conductor_params.conductor_type == "Conductor": + conductor = Conductor( + cable=cable, + mat_jacket=conductor_config.get("mat_jacket"), + mat_ins=conductor_config.get("mat_ins"), + params=conductor_params, + name="Conductor", + ) + elif conductor_params.conductor_type == "SymmetricConductor": + conductor = SymmetricConductor( + cable=cable, + mat_jacket=conductor_config.get("mat_jacket"), + mat_ins=conductor_config.get("mat_ins"), + params=conductor_params, + name="SymmetricConductor", + ) + else: + raise ValueError( + f"Conductor type {conductor_params.conductor_type} is not known." + "Available options are 'Conductor' and 'SymmetricConductor'." + ) + return conductor + + def _make_winding_pack(self, conductor): + winding_pack_params = self.params.get("winding_pack") + return WindingPack( + conductor=conductor, params=winding_pack_params, name="winding_pack" + ) + + def _make_case(self, WPs): # noqa: N803 + case_config = self.build_config.get("case") + case_params = self.params.get("case") + if case_params.case_type == "Trapezoidal": + case = TrapezoidalCaseTF( + params=case_params, + mat_case=case_config.get("material"), + WPs=WPs, + name="TrapezoidalCase", + ) + else: + raise ValueError( + f"Case type {case_params.case_type} is not known." + "Available options are 'Trapezoidal'." + ) + return case From 3ef1a646745465d45fcf66846b20d24d29fab11e Mon Sep 17 00:00:00 2001 From: Jonathan Matthews Date: Thu, 28 Aug 2025 16:26:54 +0100 Subject: [PATCH 23/61] rename and reshuffling of some params --- bluemira/magnets/strand.py | 20 ++- bluemira/magnets/tfcoil_designer.py | 242 +++++++++++++++++++--------- 2 files changed, 183 insertions(+), 79 deletions(-) diff --git a/bluemira/magnets/strand.py b/bluemira/magnets/strand.py index 55cf7e7689..455c9d6f74 100644 --- a/bluemira/magnets/strand.py +++ b/bluemira/magnets/strand.py @@ -42,7 +42,7 @@ class StrandParams(ParameterFrame): d_strand: Parameter[float] """Strand diameter in meters.""" - temperature: Parameter[float] + operating_temperature: Parameter[float] """Operating temperature [K].""" @@ -72,7 +72,7 @@ def __init__( params: Structure containing the input parameters. Keys are: - d_strand: float - - temperature: float + - operating_temperature: float See :class:`~bluemira.magnets.strand.StrandParams` for parameter details. @@ -302,7 +302,7 @@ def to_dict(self) -> dict[str, Any]: return { "name": self.name, "d_strand": self.params.d_strand.value, - "temperature": self.params.temperature.value, + "temperature": self.params.operating_temperature.value, "materials": [ { "material": m.material, @@ -370,7 +370,7 @@ class registration name. # resolve return cls( materials=material_mix, - temperature=strand_dict.get("temperature"), + operating_temperature=strand_dict.get("operating_temperature"), d_strand=strand_dict.get("d_strand"), name=name or strand_dict.get("name"), ) @@ -379,6 +379,14 @@ class registration name. # ------------------------------------------------------------------------------ # SuperconductingStrand Class # ------------------------------------------------------------------------------ +@dataclass +class SuperconductingStrandParams(StrandParams): + """ + Parameters needed for the strand + """ + + d_strand_sc: Parameter[float] # not sure this will work? + """Superconducting Strand diameter in meters.""" class SuperconductingStrand(Strand): @@ -410,8 +418,7 @@ def __init__( a supercoductor. params: Structure containing the input parameters. Keys are: - - d_strand: float - - temperature: float + - d_strand_sc: float See :class:`~bluemira.magnets.strand.StrandParams` for parameter details. @@ -424,6 +431,7 @@ def __init__( name=name, ) self._sc = self._check_materials() + self.params.d_strand.value = self.params.d_strand_sc.value def _check_materials(self) -> MaterialFraction: """ diff --git a/bluemira/magnets/tfcoil_designer.py b/bluemira/magnets/tfcoil_designer.py index 3adee5e151..546b07af6c 100644 --- a/bluemira/magnets/tfcoil_designer.py +++ b/bluemira/magnets/tfcoil_designer.py @@ -7,6 +7,7 @@ from dataclasses import dataclass +import matplotlib.pyplot as plt import numpy as np from bluemira.base.constants import MU_0, MU_0_2PI, MU_0_4PI @@ -26,6 +27,7 @@ class TFCoilXYDesignerParams(ParameterFrame): Parameters needed for all aspects of the TF coil design """ + # base params R0: Parameter[float] """Major radius [m]""" B0: Parameter[float] @@ -38,6 +40,63 @@ class TFCoilXYDesignerParams(ParameterFrame): """Maximum plasma ripple""" d: Parameter[float] """Additional distance to calculate max external radius of inner TF leg""" + S_VV: Parameter[float] + """Vacuum vessel steel limit""" + safety_factor: Parameter[float] + """Allowable stress values""" + B_ref: Parameter[float] + """Reference value for B field (LTS limit) [T]""" + + # strand params + d_strand_sc: Parameter[float] + """Diameter of superconducting strand""" + d_strand: Parameter[float] + """Diameter of stabilising strand""" + operating_temperature: Parameter[float] + """Operating temperature for the strands [K]""" + + # cable params + n_sc_strand: Parameter[int] + """Number of superconducting strands.""" + n_stab_strand: Parameter[int] + """Number of stabilizing strands.""" + d_cooling_channel: Parameter[float] + """Diameter of the cooling channel [m].""" + void_fraction: Parameter[float] + """Ratio of material volume to total volume [unitless].""" + cos_theta: Parameter[float] + """Correction factor for twist in the cable layout.""" + dx: Parameter[float] + """Cable half-width in the x-direction [m].""" + + # conductor params + dx_jacket: Parameter[float] + """x-thickness of the jacket [m].""" + dy_jacket: Parameter[float] + """y-tickness of the jacket [m].""" + dx_ins: Parameter[float] + """x-thickness of the insulator [m].""" + dy_ins: Parameter[float] + """y-thickness of the insulator [m].""" + + # winding pack params + nx: Parameter[int] + """Number of conductors along the x-axis.""" + ny: Parameter[int] + """Number of conductors along the y-axis.""" + + # case params + Ri: Parameter[float] + """External radius of the TF coil case [m].""" + Rk: Parameter[float] + """Internal radius of the TF coil case [m].""" + theta_TF: Parameter[float] + """Toroidal angular span of the TF coil [degrees].""" + dy_ps: Parameter[float] + """Radial thickness of the poloidal support region [m].""" + dy_vault: Parameter[float] + """Radial thickness of the vault support region [m].""" + Iop: Parameter[float] """Operational current in conductor""" T_sc: Parameter[float] @@ -46,22 +105,14 @@ class TFCoilXYDesignerParams(ParameterFrame): """Temperature margin""" t_delay: Parameter[float] """Time delay for exponential functions""" + + # optimisation params t0: Parameter[float] """Initial time""" + Tau_discharge: Parameter[float] + """Characteristic time constant""" hotspot_target_temperature: Parameter[float] """Target temperature for hotspot for cable optimisiation""" - S_VV: Parameter[float] - """Vacuum vessel steel limit""" - d_strand_sc: Parameter[float] - """Diameter of superconducting strand""" - d_strand_stab: Parameter[float] - """Diameter of stabilising strand""" - safety_factor: Parameter[float] - """Allowable stress values""" - dx: Parameter[float] - """Cable length""" - B_ref: Parameter[float] - """Reference value for B field (LTS limit) [T]""" layout: Parameter[str] """Cable layout strategy""" wp_reduction_factor: Parameter[float] @@ -76,8 +127,6 @@ class TFCoilXYDesignerParams(ParameterFrame): """Maximum number of optimization iterations""" eps: Parameter[float] """Convergence threshold for the combined optimization loop.""" - Tau_discharge: Parameter[float] - """Characteristic time constant""" class TFCoilXYDesigner(Designer): @@ -101,6 +150,7 @@ def __init__( super().__init__(params, build_config) def _derived_values(self): + # Needed params that are calculated using the base params a = self.params.R0.value / self.params.A.value Ri = self.params.R0.value - a - self.params.d.value # noqa: N806 Re = (self.params.R0.value + a) * (1 / self.params.ripple.value) ** ( # noqa: N806 @@ -132,6 +182,9 @@ def _derived_values(self): ** 2 ) T_op = self.params.T_sc.value + self.params.T_margin.value # noqa: N806 + self.params.operating_temperature.value = ( + T_op # this necessary? Or just remove T_sc and T_margin + ) s_y = 1e9 / self.params.safety_factor.value n_cond = int( np.floor( @@ -192,41 +245,32 @@ def run(self): sc_strand_config = self.build_config.get("superconducting_strand") conductor_config = self.build_config.get("conductor") case_config = self.build_config.get("case") - # sort params (break down into smaller parts rather than pass in all params?) - stab_strand_params = self.params.get("stab_strand") - sc_strand_params = self.params.get("sc_strand") - cable_params = self.params.get("cable") - conductor_params = self.params.get("conductor") - winding_pack_params = self.params.get("winding_pack") - case_params = self.params.get("case") - optimisation_params = self.params.get("optimisation") + # params that are function of another param derived_params = self._derived_values() - stab_strand = self._make_stab_strand(stab_strand_config, stab_strand_params) - sc_strand = self._make_sc_strand(sc_strand_config, sc_strand_params) - initial_cable = self._make_cable(cable_params, stab_strand, sc_strand) + stab_strand = self._make_stab_strand(stab_strand_config) + sc_strand = self._make_sc_strand(sc_strand_config) + initial_cable = self._make_cable(stab_strand, sc_strand) # param frame optimisation stuff? optimised_cable = initial_cable.optimise_n_stab_ths( - t0=optimisation_params.t0.value, - tf=optimisation_params.Tau_discharge.value, + t0=self.params.t0.value, + tf=self.params.Tau_discharge.value, T_for_hts=derived_params["T_op"], - hotspot_target_temperature=optimisation_params.hotspot_target_temperature.value, + hotspot_target_temperature=self.params.hotspot_target_temperature.value, B_fun=derived_params["B_fun"], I_fun=derived_params["I_fun"], bounds=[1, 10000], ) - conductor = self._make_conductor( - conductor_config, conductor_params, optimised_cable - ) - winding_pack = self._make_winding_pack(winding_pack_params, conductor) - case = self._make_case(case_config, case_params, [winding_pack]) + conductor = self._make_conductor(conductor_config, optimised_cable) + winding_pack = self._make_winding_pack(conductor) + case = self._make_case(case_config, [winding_pack]) # param frame optimisation stuff? case.rearrange_conductors_in_wp( n_conductors=derived_params["n_cond"], - wp_reduction_factor=optimisation_params.wp_reduction_factor.value, + wp_reduction_factor=self.params.wp_reduction_factor.value, min_gap_x=derived_params["min_gap_x"], - n_layers_reduction=optimisation_params.n_layers_reduction.value, - layout=optimisation_params.layout.value, + n_layers_reduction=self.params.n_layers_reduction.value, + layout=self.params.layout.value, ) # param frame optimisation stuff? case.optimize_jacket_and_vault( @@ -235,14 +279,14 @@ def run(self): temperature=derived_params["T_op"], B=derived_params["B_TF_i"], allowable_sigma=derived_params["s_y"], - bounds_cond_jacket=optimisation_params.bounds_cond_jacket.value, - bounds_dy_vault=optimisation_params.bounds_dy_vault.value, - layout=optimisation_params.layout.value, - wp_reduction_factor=optimisation_params.wp_reduction_factor.value, + bounds_cond_jacket=self.params.bounds_cond_jacket.value, + bounds_dy_vault=self.params.bounds_dy_vault.value, + layout=self.params.layout.value, + wp_reduction_factor=self.params.wp_reduction_factor.value, min_gap_x=derived_params["min_gap_x"], - n_layers_reduction=optimisation_params.n_layers_reduction.value, - max_niter=optimisation_params.max_niter.value, - eps=optimisation_params.eps.value, + n_layers_reduction=self.params.n_layers_reduction.value, + max_niter=self.params.max_niter.value, + eps=self.params.eps.value, n_conds=derived_params["n_cond"], ) return case @@ -270,97 +314,149 @@ def B_TF_r(self, tf_current, r): def _make_stab_strand(self): stab_strand_config = self.build_config.get("stabilising_strand") - stab_strand_params = self.params.get("stab_strand") return Strand( materials=stab_strand_config.get("material"), - params=stab_strand_params, + params=self.params, name="stab_strand", ) def _make_sc_strand(self): sc_strand_config = self.build_config.get("superconducting_strand") - sc_strand_params = self.params.get("sc_strand") return SuperconductingStrand( materials=sc_strand_config.get("material"), - params=sc_strand_params, + params=self.params, name="sc_strand", ) def _make_cable(self, stab_strand, sc_strand): - cable_params = self.params.get("cable") - if cable_params.cable_type == "Rectangular": + if self.params.cable_type == "Rectangular": cable = RectangularCable( sc_strand=sc_strand, stab_strand=stab_strand, - params=cable_params, + params=self.params, name="RectangularCable", ) - elif cable_params.cable_type == "Square": + elif self.params.cable_type == "Square": cable = SquareCable( sc_strand=sc_strand, stab_strand=stab_strand, - params=cable_params, + params=self.params, name="SquareCable", ) - elif cable_params.cable_type == "Round": + elif self.params.cable_type == "Round": cable = RoundCable( sc_strand=sc_strand, stab_strand=stab_strand, - params=cable_params, + params=self.params, name="RoundCable", ) else: raise ValueError( - f"Cable type {cable_params.cable_type} is not known." + f"Cable type {self.params.cable_type} is not known." "Available options are 'Rectangular', 'Square' and 'Round'." ) return cable def _make_conductor(self, cable): conductor_config = self.build_config.get("conductor") - conductor_params = self.params.get("conductor") - if conductor_params.conductor_type == "Conductor": + if self.params.conductor_type == "Conductor": conductor = Conductor( cable=cable, - mat_jacket=conductor_config.get("mat_jacket"), - mat_ins=conductor_config.get("mat_ins"), - params=conductor_params, + mat_jacket=conductor_config.get("jacket", "material"), + mat_ins=conductor_config.get("ins", "material"), + params=self.params, name="Conductor", ) - elif conductor_params.conductor_type == "SymmetricConductor": + elif self.params.conductor_type == "SymmetricConductor": conductor = SymmetricConductor( cable=cable, - mat_jacket=conductor_config.get("mat_jacket"), - mat_ins=conductor_config.get("mat_ins"), - params=conductor_params, + mat_jacket=conductor_config.get("jacket", "material"), + mat_ins=conductor_config.get("ins", "material"), + params=self.params, name="SymmetricConductor", ) else: raise ValueError( - f"Conductor type {conductor_params.conductor_type} is not known." + f"Conductor type {self.params.conductor_type} is not known." "Available options are 'Conductor' and 'SymmetricConductor'." ) return conductor def _make_winding_pack(self, conductor): - winding_pack_params = self.params.get("winding_pack") - return WindingPack( - conductor=conductor, params=winding_pack_params, name="winding_pack" - ) + return WindingPack(conductor=conductor, params=self.params, name="winding_pack") def _make_case(self, WPs): # noqa: N803 case_config = self.build_config.get("case") - case_params = self.params.get("case") - if case_params.case_type == "Trapezoidal": + if self.params.case_type == "Trapezoidal": case = TrapezoidalCaseTF( - params=case_params, + params=self.params, mat_case=case_config.get("material"), WPs=WPs, name="TrapezoidalCase", ) else: raise ValueError( - f"Case type {case_params.case_type} is not known." + f"Case type {self.params.case_type} is not known." "Available options are 'Trapezoidal'." ) return case + + +def plot_cable_temperature_evolution(result, t0, tf, ax, n_steps=100): + solution = result.solution + + ax.plot(solution.t, solution.y[0], "r*", label="Simulation points") + time_steps = np.linspace(t0, tf, n_steps) + ax.plot(time_steps, solution.sol(time_steps)[0], "b", label="Interpolated curve") + ax.grid(visible=True) + ax.set_ylabel("Temperature [K]", fontsize=10) + ax.set_title("Quench temperature evolution", fontsize=11) + ax.legend(fontsize=9) + + ax.tick_params(axis="y", labelcolor="k", labelsize=9) + + props = {"boxstyle": "round", "facecolor": "white", "alpha": 0.8} + ax.text( + 0.65, + 0.5, + result.info_text, + transform=ax.transAxes, + fontsize=9, + verticalalignment="top", + bbox=props, + ) + ax.figure.tight_layout() + + +def plot_I_B(I_fun, B_fun, t0, tf, ax, n_steps=300): + time_steps = np.linspace(t0, tf, n_steps) + I_values = [I_fun(t) for t in time_steps] # noqa: N806 + B_values = [B_fun(t) for t in time_steps] + + ax.plot(time_steps, I_values, "g", label="Current [A]") + ax.set_ylabel("Current [A]", color="g", fontsize=10) + ax.tick_params(axis="y", labelcolor="g", labelsize=9) + ax.grid(visible=True) + + ax_right = ax.twinx() + ax_right.plot(time_steps, B_values, "m--", label="Magnetic field [T]") + ax_right.set_ylabel("Magnetic field [T]", color="m", fontsize=10) + ax_right.tick_params(axis="y", labelcolor="m", labelsize=9) + + # Labels + ax.set_xlabel("Time [s]", fontsize=10) + ax.tick_params(axis="x", labelsize=9) + + # Combined legend for both sides + lines, labels = ax.get_legend_handles_labels() + lines2, labels2 = ax_right.get_legend_handles_labels() + ax.legend(lines + lines2, labels + labels2, loc="best", fontsize=9) + + ax.figure.tight_layout() + + +def plot_summary(result, t0, tf, I_fun, B_fun, n_steps, show=False): + f, (ax_temp, ax_ib) = plt.subplots(2, 1, figsize=(8, 8), sharex=True) + plot_cable_temperature_evolution(result, t0, tf, ax_temp, n_steps) + plot_I_B(I_fun, B_fun, t0, tf, ax_ib, n_steps * 3) + return f From da0b5e4c7e7a2d9e7114f5f3a30cfeda198b72f6 Mon Sep 17 00:00:00 2001 From: Jonathan Matthews Date: Tue, 2 Sep 2025 10:44:28 +0100 Subject: [PATCH 24/61] remove parameter frames from module classes ie strand, cable etc and only using paramater frame for designer class --- bluemira/magnets/cable.py | 233 +++++++++++++++---------------- bluemira/magnets/case_tf.py | 166 +++++++++------------- bluemira/magnets/conductor.py | 194 +++++++++---------------- bluemira/magnets/strand.py | 77 +++------- bluemira/magnets/winding_pack.py | 50 +++---- 5 files changed, 280 insertions(+), 440 deletions(-) diff --git a/bluemira/magnets/cable.py b/bluemira/magnets/cable.py index 9910fa0f9d..a9101e327d 100644 --- a/bluemira/magnets/cable.py +++ b/bluemira/magnets/cable.py @@ -24,7 +24,6 @@ bluemira_print, bluemira_warn, ) -from bluemira.base.parameter_frame import Parameter, ParameterFrame from bluemira.magnets.strand import ( Strand, SuperconductingStrand, @@ -35,26 +34,6 @@ if TYPE_CHECKING: from collections.abc import Callable - from bluemira.base.parameter_frame.typed import ParameterFrameLike - - -@dataclass -class CableParams(ParameterFrame): - """ - Parameters needed for the TF cable - """ - - n_sc_strand: Parameter[int] - """Number of superconducting strands.""" - n_stab_strand: Parameter[int] - """Number of stabilizing strands.""" - d_cooling_channel: Parameter[float] - """Diameter of the cooling channel [m].""" - void_fraction: Parameter[float] - """Ratio of material volume to total volume [unitless].""" - cos_theta: Parameter[float] - """Correction factor for twist in the cable layout.""" - class ABCCable(ABC): """ @@ -70,13 +49,16 @@ class ABCCable(ABC): """ _name_in_registry_: str | None = None # Abstract base classes should NOT register - param_cls: type[CableParams] = CableParams def __init__( self, sc_strand: SuperconductingStrand, stab_strand: Strand, - params: ParameterFrameLike, + n_sc_strand: int, + n_stab_strand: int, + d_cooling_channel: float, + void_fraction: float, + cos_theta: float, name: str = "Cable", **props, ): @@ -94,26 +76,29 @@ def __init__( The superconducting strand. stab_strand: The stabilizer strand. - params: - Structure containing the input parameters. Keys are: - - n_sc_strand: int - - n_stab_strand: int - - d_cooling_channel: float - - void_fraction: float = 0.725 - - cos_theta: float = 0.97 - - See :class:`~bluemira.magnets.cable.CableParams` - for parameter details. + n_sc_strand: + Number of superconducting strands. + n_stab_strand: + Number of stabilizing strands. + d_cooling_channel: + Diameter of the cooling channel [m]. + void_fraction: + Ratio of material volume to total volume [unitless]. + cos_theta: + Correction factor for twist in the cable layout. name: Identifier for the cable instance. """ - super().__init__(params) # fix when split into builders and designers - # assign # Setting self.name triggers automatic instance registration self.name = name self.sc_strand = sc_strand self.stab_strand = stab_strand + self.n_sc_strand = n_sc_strand + self.n_stab_strand = n_stab_strand + self.d_cooling_channel = d_cooling_channel + self.void_fraction = void_fraction + self.cos_theta = cos_theta youngs_modulus: Callable[[Any, OperationalConditions], float] | float | None = ( props.pop("E", None) @@ -224,24 +209,24 @@ def Cp(self, op_cond: OperationalConditions): # noqa: N802 @property def area_stab(self) -> float: """Area of the stabilizer region""" - return self.stab_strand.area * self.params.n_stab_strand.value + return self.stab_strand.area * self.n_stab_strand @property def area_sc(self) -> float: """Area of the superconductor region""" - return self.sc_strand.area * self.params.n_sc_strand.value + return self.sc_strand.area * self.n_sc_strand @property def area_cc(self) -> float: """Area of the cooling channel""" - return self.params.d_cooling_channel.value**2 / 4 * np.pi + return self.d_cooling_channel**2 / 4 * np.pi @property def area(self) -> float: """Area of the cable considering the void fraction""" return ( self.area_sc + self.area_stab - ) / self.params.void_fraction.value / self.params.cos_theta.value + self.area_cc + ) / self.void_fraction / self.cos_theta + self.area_cc def E(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ @@ -425,7 +410,7 @@ def final_temperature_difference( - It modifies the internal state `self._n_stab_strand`, which may affect subsequent evaluations unless restored. """ - self.params.n_stab_strand.value = n_stab + self.n_stab_strand = n_stab solution = self._temperature_evolution( t0=t0, @@ -452,7 +437,7 @@ def final_temperature_difference( ) # Here we re-ensure the n_stab_strand to be an integer - self.params.n_stab_strand.value = int(np.ceil(self.params.n_stab_strand.value)) + self.n_stab_strand = int(np.ceil(self.n_stab_strand)) solution = self._temperature_evolution(t0, tf, initial_temperature, B_fun, I_fun) final_temperature = solution.y[0][-1] @@ -462,13 +447,13 @@ def final_temperature_difference( f"Final temperature ({final_temperature:.2f} K) exceeds target " f"temperature " f"({target_temperature} K) even with maximum n_stab = " - f"{self.params.n_stab_strand.value}." + f"{self.n_stab_strand}." ) raise ValueError( "Optimization failed to keep final temperature ≤ target. " "Try increasing the upper bound of n_stab or adjusting cable parameters." ) - bluemira_print(f"Optimal n_stab: {self.params.n_stab_strand.value}") + bluemira_print(f"Optimal n_stab: {self.n_stab_strand}") bluemira_print( f"Final temperature with optimal n_stab: {final_temperature:.2f} Kelvin" ) @@ -484,9 +469,9 @@ class StabilisingStrandRes: f"Target T: {target_temperature:.2f} K\n" f"Initial T: {initial_temperature:.2f} K\n" f"SC Strand: {self.sc_strand.name}\n" - f"n. sc. strand = {self.params.n_sc_strand.value}\n" + f"n. sc. strand = {self.n_sc_strand}\n" f"Stab. strand = {self.stab_strand.name}\n" - f"n. stab. strand = {self.params.n_stab_strand.value}\n" + f"n. stab. strand = {self.n_stab_strand}\n" ), ) @@ -550,9 +535,7 @@ def plot( points_ext = np.vstack((p0, p1, p2, p3, p0)) + pc points_cc = ( np.array([ - np.array([np.cos(theta), np.sin(theta)]) - * self.params.d_cooling_channel.value - / 2 + np.array([np.cos(theta), np.sin(theta)]) * self.d_cooling_channel / 2 for theta in np.linspace(0, np.radians(360), 19) ]) + pc @@ -583,16 +566,16 @@ def __str__(self) -> str: f"dx: {self.dx}\n" f"dy: {self.dy}\n" f"aspect ratio: {self.aspect_ratio}\n" - f"d cooling channel: {self.params.d_cooling_channel.value}\n" - f"void fraction: {self.params.void_fraction.value}\n" - f"cos(theta): {self.params.cos_theta.value}\n" + f"d cooling channel: {self.d_cooling_channel}\n" + f"void fraction: {self.void_fraction}\n" + f"cos(theta): {self.cos_theta}\n" f"----- sc strand -------\n" f"sc strand: {self.sc_strand!s}\n" f"----- stab strand -------\n" f"stab strand: {self.stab_strand!s}\n" f"-----------------------\n" - f"n sc strand: {self.params.n_sc_strand.value}\n" - f"n stab strand: {self.params.n_stab_strand.value}" + f"n sc strand: {self.n_sc_strand}\n" + f"n stab strand: {self.n_stab_strand}" ) def to_dict(self) -> dict[str, str | float | int | dict[str, Any]]: @@ -606,11 +589,11 @@ def to_dict(self) -> dict[str, str | float | int | dict[str, Any]]: """ return { "name": self.name, - "n_sc_strand": self.params.n_sc_strand.value, - "n_stab_strand": self.params.n_stab_strand.value, - "d_cooling_channel": self.params.d_cooling_channel.value, - "void_fraction": self.params.void_fraction.value, - "cos_theta": self.params.cos_theta.value, + "n_sc_strand": self.n_sc_strand, + "n_stab_strand": self.n_stab_strand, + "d_cooling_channel": self.d_cooling_channel, + "void_fraction": self.void_fraction, + "cos_theta": self.cos_theta, "sc_strand": self.sc_strand.to_dict(), "stab_strand": self.stab_strand.to_dict(), **{k: getattr(k)() for k in self._props}, @@ -679,16 +662,6 @@ def from_dict( ) -@dataclass -class RectangularCableParams(CableParams): - """ - Parameters needed for the TF cable - """ - - dx: Parameter[float] - """Cable half-width in the x-direction [m].""" - - class RectangularCable(ABCCable): """ Cable with a rectangular cross-section. @@ -698,13 +671,17 @@ class RectangularCable(ABCCable): """ _name_in_registry_ = "RectangularCable" - param_cls: type[RectangularCableParams] = RectangularCableParams def __init__( self, sc_strand: SuperconductingStrand, stab_strand: Strand, - params: ParameterFrameLike, + n_sc_strand: int, + n_stab_strand: int, + d_cooling_channel: float, + void_fraction: float, + cos_theta: float, + dx: float, name: str = "RectangularCable", **props, ): @@ -722,17 +699,18 @@ def __init__( Superconducting strand. stab_strand: Stabilizer strand. - params: - Structure containing the input parameters. Keys are: - - dx: float - - n_sc_strand: int - - n_stab_strand: int - - d_cooling_channel: float - - void_fraction: float = 0.725 - - cos_theta: float = 0.97 - - See :class:`~bluemira.magnets.cable.RectangularCableParams` - for parameter details. + n_sc_strand: + Number of superconducting strands. + n_stab_strand: + Number of stabilizing strands. + d_cooling_channel: + Diameter of the cooling channel [m]. + void_fraction: + Ratio of material volume to total volume [unitless]. + cos_theta: + Correction factor for twist in the cable layout. + dx: + Cable half-width in the x-direction [m]. name: Name of the cable props: @@ -741,26 +719,26 @@ def __init__( super().__init__( sc_strand=sc_strand, stab_strand=stab_strand, - params=params, + n_sc_strand=n_sc_strand, + n_stab_strand=n_stab_strand, + d_cooling_channel=d_cooling_channel, + void_fraction=void_fraction, + cos_theta=cos_theta, name=name, **props, ) - - @property - def dx(self) -> float: - """Half Cable dimension in the x direction [m]""" - return self.params.dx.value + self.dx = dx @property def dy(self) -> float: """Half Cable dimension in the y direction [m]""" - return self.area / self.params.dx.value / 4 + return self.area / self.dx / 4 # Decide if this function shall be a setter. # Defined as "normal" function to underline that it modifies dx. def set_aspect_ratio(self, value: float): """Modify dx in order to get the given aspect ratio""" - self.params.dx.value = np.sqrt(value * self.area) / 2 + self.dx = np.sqrt(value * self.area) / 2 # OD homogenized structural properties def Kx(self, op_cond: OperationalConditions) -> float: # noqa: N802 @@ -778,7 +756,7 @@ def Kx(self, op_cond: OperationalConditions) -> float: # noqa: N802 : Homogenized stiffness in the x-direction [Pa]. """ - return self.E(op_cond) * self.dy / self.params.dx.value + return self.E(op_cond) * self.dy / self.dx def Ky(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ @@ -795,7 +773,7 @@ def Ky(self, op_cond: OperationalConditions) -> float: # noqa: N802 : Homogenized stiffness in the y-direction [Pa]. """ - return self.E(op_cond) * self.params.dx.value / self.dy + return self.E(op_cond) * self.dx / self.dy def to_dict(self) -> dict[str, Any]: """ @@ -808,7 +786,7 @@ def to_dict(self) -> dict[str, Any]: """ data = super().to_dict() data.update({ - "dx": self.params.dx.value, + "dx": self.dx, "aspect_ratio": self.aspect_ratio, }) return data @@ -927,13 +905,16 @@ class SquareCable(ABCCable): """ _name_in_registry_ = "SquareCable" - param_cls: type[CableParams] = CableParams def __init__( self, sc_strand: SuperconductingStrand, stab_strand: Strand, - params: ParameterFrameLike, + n_sc_strand: int, + n_stab_strand: int, + d_cooling_channel: float, + void_fraction: float, + cos_theta: float, name: str = "SquareCable", **props, ): @@ -951,16 +932,16 @@ def __init__( strand of the superconductor stab_strand: strand of the stabilizer - params: - Structure containing the input parameters. Keys are: - - n_sc_strand: int - - n_stab_strand: int - - d_cooling_channel: float - - void_fraction: float = 0.725 - - cos_theta: float = 0.97 - - See :class:`~bluemira.magnets.cable.CableParams` - for parameter details. + n_sc_strand: + Number of superconducting strands. + n_stab_strand: + Number of stabilizing strands. + d_cooling_channel: + Diameter of the cooling channel [m]. + void_fraction: + Ratio of material volume to total volume [unitless]. + cos_theta: + Correction factor for twist in the cable layout. name: cable string identifier @@ -971,7 +952,11 @@ def __init__( super().__init__( sc_strand=sc_strand, stab_strand=stab_strand, - params=params, + n_sc_strand=n_sc_strand, + n_stab_strand=n_stab_strand, + d_cooling_channel=d_cooling_channel, + void_fraction=void_fraction, + cos_theta=cos_theta, name=name, **props, ) @@ -1084,13 +1069,15 @@ class RoundCable(ABCCable): around a central cooling channel. """ - param_cls: type[CableParams] = CableParams - def __init__( self, sc_strand: SuperconductingStrand, stab_strand: Strand, - params: ParameterFrameLike, + n_sc_strand: int, + n_stab_strand: int, + d_cooling_channel: float, + void_fraction: float, + cos_theta: float, name: str = "RoundCable", **props, ): @@ -1103,23 +1090,27 @@ def __init__( strand of the superconductor stab_strand: strand of the stabilizer - params: - Structure containing the input parameters. Keys are: - - n_sc_strand: int - - n_stab_strand: int - - d_cooling_channel: float - - void_fraction: float = 0.725 - - cos_theta: float = 0.97 - - See :class:`~bluemira.magnets.cable.CableParams` - for parameter details. + n_sc_strand: + Number of superconducting strands. + n_stab_strand: + Number of stabilizing strands. + d_cooling_channel: + Diameter of the cooling channel [m]. + void_fraction: + Ratio of material volume to total volume [unitless]. + cos_theta: + Correction factor for twist in the cable layout. name: cable string identifier """ super().__init__( sc_strand=sc_strand, stab_strand=stab_strand, - params=params, + n_sc_strand=n_sc_strand, + n_stab_strand=n_stab_strand, + d_cooling_channel=d_cooling_channel, + void_fraction=void_fraction, + cos_theta=cos_theta, name=name, **props, ) @@ -1223,9 +1214,7 @@ def plot( points_cc = ( np.array([ - np.array([np.cos(theta), np.sin(theta)]) - * self.params.d_cooling_channel.value - / 2 + np.array([np.cos(theta), np.sin(theta)]) * self.d_cooling_channel / 2 for theta in np.linspace(0, np.radians(360), 19) ]) + pc diff --git a/bluemira/magnets/case_tf.py b/bluemira/magnets/case_tf.py index c79d2762a8..17c3a9f594 100644 --- a/bluemira/magnets/case_tf.py +++ b/bluemira/magnets/case_tf.py @@ -33,7 +33,6 @@ bluemira_print, bluemira_warn, ) -from bluemira.base.parameter_frame import Parameter, ParameterFrame from bluemira.geometry.parameterisations import GeometryParameterisation from bluemira.geometry.tools import make_polygon from bluemira.magnets.utils import reciprocal_summation, summation @@ -44,7 +43,6 @@ from matproplib import OperationalConditions from matproplib.material import Material - from bluemira.base.parameter_frame.typed import ParameterFrameLike from bluemira.geometry.wire import BluemiraWire @@ -67,33 +65,28 @@ def _dx_at_radius(radius: float, rad_theta: float) -> float: return radius * np.tan(rad_theta / 2) -@dataclass -class TFCaseGeometryParams(ParameterFrame): - """ - Parameters needed for the TF casing geometry - """ - - Ri: Parameter[float] - """External radius of the TF coil case [m].""" - Rk: Parameter[float] - """Internal radius of the TF coil case [m].""" - theta_TF: Parameter[float] - """Toroidal angular span of the TF coil [degrees].""" - - class CaseGeometry(ABC): """ Abstract base class for TF case geometry profiles. Provides access to radial dimensions and toroidal width calculations as well as geometric plotting and area calculation interfaces. - """ - param_cls: type[TFCaseGeometryParams] = TFCaseGeometryParams + Parameters + ---------- + Ri: + External radius of the TF coil case [m]. + Rk: + Internal radius of the TF coil case [m]. + theta_TF: + Toroidal angular span of the TF coil [degrees]. + """ - def __init__(self, params: ParameterFrameLike): - super().__init__(params) # fix when split into builders and designers - self.rad_theta_TF = np.radians(self.params.theta_TF.value) + def __init__(self, Ri: float, Rk: float, theta_TF: float): + self.Ri = Ri + self.Rk = Rk + self.theta_TF = theta_TF + self.rad_theta_TF = np.radians(self.theta_TF) # property if theta_TF gets reset? @property @abstractmethod @@ -325,22 +318,6 @@ def create_shape(self, label: str = "", n_points: int = 50) -> BluemiraWire: return make_polygon(np.vstack((arc_outer, arc_inner)), label=label) -@dataclass -class TFCaseParams(ParameterFrame): - """ - Parameters needed for the TF casing - """ - - Ri: Parameter[float] - """External radius at the top of the TF coil case [m].""" - theta_TF: Parameter[float] - """Toroidal angular aperture of the coil [degrees].""" - dy_ps: Parameter[float] - """Radial thickness of the poloidal support region [m].""" - dy_vault: Parameter[float] - """Radial thickness of the vault support region [m].""" - - class BaseCaseTF(CaseGeometry, ABC): """ Abstract Base Class for Toroidal Field Coil Case configurations. @@ -348,11 +325,12 @@ class BaseCaseTF(CaseGeometry, ABC): Defines the universal properties common to all TF case geometries. """ - param_cls: type[TFCaseParams] = TFCaseParams - def __init__( self, - params: ParameterFrameLike, + Ri: float, + theta_TF: float, + dy_ps: float, + dy_vault: float, mat_case: Material, WPs: list[WindingPack], # noqa: N803 name: str = "BaseCaseTF", @@ -362,15 +340,14 @@ def __init__( Parameters ---------- - params: - Structure containing the input parameters. Keys are: - - Ri: float - - theta_TF: float - - dy_ps: float - - dy_vault: float - - See :class:`~bluemira.magnets.case_tf.TFCaseParams` - for parameter details. + Ri: + External radius of the TF coil case [m]. + theta_TF: + Toroidal angular span of the TF coil [degrees]. + dy_ps: + Radial thickness of the poloidal support region [m]. + dy_vault: + Radial thickness of the vault support region [m]. mat_case: Structural material assigned to the TF coil case. WPs: @@ -379,19 +356,20 @@ def __init__( String identifier for the TF coil case instance (default is "BaseCaseTF"). """ super().__init__( - params=params, + Ri=Ri, + theta_TF=theta_TF, mat_case=mat_case, WPs=WPs, name=name, ) + self.dy_ps = dy_ps + self.dy_vault = dy_vault # Toroidal half-length of the coil case at its maximum radial position [m] - self.dx_i = _dx_at_radius(self.params.Ri.value, self.rad_theta_TF) + self.dx_i = _dx_at_radius(self.Ri, self.rad_theta_TF) # Average toroidal length of the ps plate - self.dx_ps = ( - self.params.Ri.value + (self.params.Ri.value - self.params.dy_ps.value) - ) * np.tan(self.rad_theta_TF / 2) + self.dx_ps = (self.Ri + (self.Ri - self.dy_ps)) * np.tan(self.rad_theta_TF / 2) # sets Rk - self.update_dy_vault(self.params.dy_vault.value) + self.update_dy_vault(self.dy_vault) @property def name(self) -> str: @@ -433,8 +411,8 @@ def update_dy_vault(self, value: float): value: Vault thickness [m]. """ - self.params.dy_vault.value = value - self.Rk = self.R_wp_k[-1] - self.params.dy_vault.value + self.dy_vault = value + self.Rk = self.R_wp_k[-1] - self.dy_vault # jm - not used but replaces functionality of original Rk setter # can't find when (if) it was used originally @@ -443,7 +421,7 @@ def update_Rk(self, value: float): # noqa: N802 Set or update the internal (innermost) radius of the TF case. """ self.Rk = value - self.params.dy_vault.value = self.R_wp_k[-1] - self._Rk + self.dy_vault = self.R_wp_k[-1] - self.Rk @property @abstractmethod @@ -521,7 +499,7 @@ def WPs(self, value: list[WindingPack]): # noqa: N802 self._WPs = value # fix dy_vault (this will recalculate Rk) - self.update_dy_vault(self.params.dy_vault.value) + self.update_dy_vault(self.dy_vault) @property def n_conductors(self) -> int: @@ -832,10 +810,10 @@ def to_dict(self) -> dict[str, float | str | list[dict[str, float | str | Any]]] """ return { "name": self.name, - "Ri": self.params.Ri.value, - "dy_ps": self.params.dy_ps.value, - "dy_vault": self.params.dy_vault.value, - "theta_TF": self.params.theta_TF.value, + "Ri": self.Ri, + "dy_ps": self.dy_ps, + "dy_vault": self.dy_vault, + "theta_TF": self.theta_TF, "mat_case": self.mat_case.name, # Assume Material has 'name' attribute "WPs": [wp.to_dict() for wp in self.WPs], # Assume each WindingPack implements to_dict() @@ -886,11 +864,11 @@ def __str__(self) -> str: """ return ( f"CaseTF '{self.name}'\n" - f" - Ri: {self.params.Ri.value:.3f} m\n" + f" - Ri: {self.Ri:.3f} m\n" f" - Rk: {self.Rk:.3f} m\n" - f" - dy_ps: {self.params.dy_ps.value:.3f} m\n" - f" - dy_vault: {self.params.dy_vault.value:.3f} m\n" - f" - theta_TF: {self.params.theta_TF.value:.2f}°\n" + f" - dy_ps: {self.dy_ps:.3f} m\n" + f" - dy_vault: {self.dy_vault:.3f} m\n" + f" - theta_TF: {self.theta_TF:.2f}°\n" f" - Material: {self.mat_case.name}\n" f" - Winding Packs: {len(self.WPs)} packs\n" ) @@ -902,11 +880,12 @@ class TrapezoidalCaseTF(BaseCaseTF, TrapezoidalGeometry): Note: this class considers a set of Winding Pack with the same conductor (instance). """ - param_cls: type[TFCaseParams] = TFCaseParams - def __init__( self, - params: ParameterFrameLike, + Ri: float, + theta_TF: float, + dy_ps: float, + dy_vault: float, mat_case: Material, WPs: list[WindingPack], # noqa: N803 name: str = "TrapezoidalCaseTF", @@ -914,7 +893,10 @@ def __init__( self._check_WPs(WPs) super().__init__( - params, + Ri=Ri, + theta_TF=theta_TF, + dy_ps=dy_ps, + dy_vault=dy_vault, mat_case=mat_case, WPs=WPs, name=name, @@ -980,11 +962,7 @@ def Kx_vault(self, op_cond: OperationalConditions) -> float: # noqa: N802 : Equivalent radial stiffness of the vault [Pa]. """ - return ( - self.mat_case.youngs_modulus(op_cond) - * self.params.dy_vault.value - / self.dx_vault - ) + return self.mat_case.youngs_modulus(op_cond) * self.dy_vault / self.dx_vault def Kx(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ @@ -1007,9 +985,7 @@ def Kx(self, op_cond: OperationalConditions) -> float: # noqa: N802 Total equivalent radial stiffness of the TF case [Pa]. """ # toroidal stiffness of the poloidal support region - kx_ps = ( - self.mat_case.youngs_modulus(op_cond) / self.dx_ps * self.params.dy_ps.value - ) + kx_ps = self.mat_case.youngs_modulus(op_cond) / self.dx_ps * self.dy_ps dx_lat = np.array([ (self.R_wp_i[i] + self.R_wp_k[i]) / 2 * np.tan(self.rad_theta_TF / 2) - w.dx for i, w in enumerate(self.WPs) @@ -1048,9 +1024,7 @@ def Ky(self, op_cond: OperationalConditions) -> float: # noqa: N802 Total equivalent toroidal stiffness of the TF case [Pa]. """ # toroidal stiffness of the poloidal support region - ky_ps = ( - self.mat_case.youngs_modulus(op_cond) * self.dx_ps / self.params.dy_ps.value - ) + ky_ps = self.mat_case.youngs_modulus(op_cond) * self.dx_ps / self.dy_ps dx_lat = np.array([ (self.R_wp_i[i] + self.R_wp_k[i]) / 2 * np.tan(self.rad_theta_TF / 2) - w.dx for i, w in enumerate(self.WPs) @@ -1059,11 +1033,7 @@ def Ky(self, op_cond: OperationalConditions) -> float: # noqa: N802 # toroidal stiffness of lateral case sections per winding pack ky_lat = self.mat_case.youngs_modulus(op_cond) * dx_lat / dy_lat # toroidal stiffness of the vault region - ky_vault = ( - self.mat_case.youngs_modulus(op_cond) - * self.dx_vault - / self.params.dy_vault.value - ) + ky_vault = self.mat_case.youngs_modulus(op_cond) * self.dx_vault / self.dy_vault temp = [ summation([ ky_lat[i], @@ -1248,7 +1218,7 @@ def _tresca_stress( # The maximum principal stress acting on the case nose is the compressive # hoop stress generated in the equivalent shell from the magnetic pressure. From # the Shell theory, for an isotropic continuous shell with a thickness ratio: - beta = self.Rk / (self.Rk + self.params.dy_vault.value) + beta = self.Rk / (self.Rk + self.dy_vault) # the maximum hoop stress, corrected to account for the presence of the WP, is # placed at the innermost radius of the case as: sigma_theta = ( @@ -1315,7 +1285,7 @@ def optimize_vault_radial_thickness( if not result.success: raise ValueError("dy_vault optimization did not converge.") - self.params.dy_vault.value = result.x + self.dy_vault = result.x # print(f"Optimal dy_vault: {self.dy_vault}") # print(f"Tresca sigma: {self._tresca_stress(pm, fz, T=T, B=B) / 1e6} MPa") @@ -1360,7 +1330,7 @@ def _sigma_difference( This function modifies the case's vault thickness using the value provided in jacket_thickness. """ - self.params.dy_vault.value = dy_vault + self.dy_vault = dy_vault sigma = self._tresca_stress(pm, fz, op_cond) # bluemira_print(f"sigma: {sigma}, allowable_sigma: {allowable_sigma}, # diff: {sigma - allowable_sigma}") @@ -1449,11 +1419,11 @@ def optimize_jacket_and_vault( self._convergence_array.append([ i, conductor.dy_jacket, - self.params.dy_vault.value, + self.dy_vault, err_conductor_area_jacket, err_dy_vault, self.dy_wp_tot, - self.params.Ri.value - self.Rk, + self.Ri - self.Rk, ]) damping_factor = 0.3 @@ -1509,16 +1479,16 @@ def optimize_jacket_and_vault( bounds=bounds_dy_vault, ) - self.params.dy_vault.value = ( + self.dy_vault = ( 1 - damping_factor - ) * case_dy_vault0 + damping_factor * self.params.dy_vault.value + ) * case_dy_vault0 + damping_factor * self.dy_vault delta_case_dy_vault = abs(self.dy_vault - case_dy_vault0) - err_dy_vault = delta_case_dy_vault / self.params.dy_vault.value + err_dy_vault = delta_case_dy_vault / self.dy_vault tot_err = err_dy_vault + err_conductor_area_jacket debug_msg.append( - f"after optimization: case dy_vault = {self.params.dy_vault.value}\n" + f"after optimization: case dy_vault = {self.dy_vault}\n" f"err_dy_jacket = {err_conductor_area_jacket}\n " f"err_dy_vault = {err_dy_vault}\n " f"tot_err = {tot_err}" @@ -1528,11 +1498,11 @@ def optimize_jacket_and_vault( self._convergence_array.append([ i, conductor.dy_jacket, - self.params.dy_vault.value, + self.dy_vault, err_conductor_area_jacket, err_dy_vault, self.dy_wp_tot, - self.params.Ri.value - self.Rk, + self.Ri - self.Rk, ]) # final check diff --git a/bluemira/magnets/conductor.py b/bluemira/magnets/conductor.py index 374b9c78b3..c5ac633890 100644 --- a/bluemira/magnets/conductor.py +++ b/bluemira/magnets/conductor.py @@ -8,7 +8,6 @@ from __future__ import annotations -from dataclasses import dataclass from typing import TYPE_CHECKING, Any import matplotlib.pyplot as plt @@ -16,7 +15,6 @@ from scipy.optimize import minimize_scalar from bluemira.base.look_and_feel import bluemira_debug -from bluemira.base.parameter_frame import Parameter, ParameterFrame from bluemira.magnets.cable import ABCCable, create_cable_from_dict from bluemira.magnets.utils import reciprocal_summation, summation @@ -24,24 +22,6 @@ from matproplib import OperationalConditions from matproplib.material import Material - from bluemira.base.parameter_frame.typed import ParameterFrameLike - - -@dataclass -class ConductorParams(ParameterFrame): - """ - Parameters needed for the conductor - """ - - dx_jacket: Parameter[float] - """x-thickness of the jacket [m].""" - dy_jacket: Parameter[float] - """y-tickness of the jacket [m].""" - dx_ins: Parameter[float] - """x-thickness of the insulator [m].""" - dy_ins: Parameter[float] - """y-thickness of the insulator [m].""" - class Conductor: """ @@ -49,14 +29,15 @@ class Conductor: insulator. """ - param_cls: type[ConductorParams] = ConductorParams - def __init__( self, cable: ABCCable, mat_jacket: Material, mat_ins: Material, - params: ParameterFrameLike, + dx_jacket: float, + dy_jacket: float, + dx_ins: float, + dy_ins: float, name: str = "Conductor", ): """ @@ -71,20 +52,22 @@ def __init__( jacket's material mat_ins: insulator's material - params: - Structure containing the input parameters. Keys are: - - dx_jacket: float - - dy_jacket: float - - dx_ins: float - - dy_ins: float - - See :class:`~bluemira.magnets.conductor.ConductorParams` - for parameter details. + dx_jacket: + x-thickness of the jacket [m]. + dy_jacket: + y-thickness of the jacket [m]. + dx_ins: + x-thickness of the insulator [m]. + dy_ins: + y-thickness of the insulator [m]. name: string identifier """ self.name = name - self.params = params + self.dx_jacket = dx_jacket + self.dy_jacket = dy_jacket + self.dx_ins = dx_ins + self.dy_ins = dy_ins self.mat_ins = mat_ins self.mat_jacket = mat_jacket self.cable = cable @@ -92,12 +75,12 @@ def __init__( @property def dx(self): """Half x-dimension of the conductor [m]""" - return self.params.dx_ins.value + self.params.dx_jacket.value + self.cable.dx + return self.dx_ins + self.dx_jacket + self.cable.dx @property def dy(self): """Half y-dimension of the conductor [m]""" - return self.params.dy_ins.value + self.params.dy_jacket.value + self.cable.dy + return self.dy_ins + self.dy_jacket + self.cable.dy @property def area(self): @@ -107,11 +90,7 @@ def area(self): @property def area_jacket(self): """Area of the jacket [m^2]""" - return ( - 4 - * (self.cable.dx + self.params.dx_jacket.value) - * (self.cable.dy + self.params.dy_jacket.value) - ) + return 4 * (self.cable.dx + self.dx_jacket) * (self.cable.dy + self.dy_jacket) @property def area_ins(self): @@ -139,10 +118,10 @@ def to_dict(self) -> dict[str, Any]: "cable": self.cable.to_dict(), "mat_jacket": self.mat_jacket.name, "mat_ins": self.mat_ins.name, - "dx_jacket": self.params.dx_jacket.value, - "dy_jacket": self.params.dy_jacket.value, - "dx_ins": self.params.dx_ins.value, - "dy_ins": self.params.dy_ins.value, + "dx_jacket": self.dx_jacket, + "dy_jacket": self.dy_jacket, + "dx_ins": self.dx_ins, + "dy_ins": self.dy_ins, } @classmethod @@ -259,12 +238,7 @@ def _Kx_topbot_ins(self, op_cond: OperationalConditions) -> float: # noqa: N802 : Axial stiffness [N/m] """ - return ( - self.mat_ins.youngs_modulus(op_cond) - * 2 - * self.cable.dy - / self.params.dx_ins.value - ) + return self.mat_ins.youngs_modulus(op_cond) * 2 * self.cable.dy / self.dx_ins def _Kx_lat_ins(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ @@ -275,11 +249,7 @@ def _Kx_lat_ins(self, op_cond: OperationalConditions) -> float: # noqa: N802 : Axial stiffness [N/m] """ - return ( - self.mat_ins.youngs_modulus(op_cond) - * self.params.dy_ins.value - / (2 * self.dx) - ) + return self.mat_ins.youngs_modulus(op_cond) * self.dy_ins / (2 * self.dx) def _Kx_lat_jacket(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ @@ -292,8 +262,8 @@ def _Kx_lat_jacket(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ return ( self.mat_jacket.youngs_modulus(op_cond) - * self.params.dy_jacket.value - / (2 * self.dx - 2 * self.params.dx_ins.value) + * self.dy_jacket + / (2 * self.dx - 2 * self.dx_ins) ) def _Kx_topbot_jacket(self, op_cond: OperationalConditions) -> float: # noqa: N802 @@ -306,10 +276,7 @@ def _Kx_topbot_jacket(self, op_cond: OperationalConditions) -> float: # noqa: N Axial stiffness [N/m] """ return ( - self.mat_jacket.youngs_modulus(op_cond) - * 2 - * self.cable.dy - / self.params.dx_jacket.value + self.mat_jacket.youngs_modulus(op_cond) * 2 * self.cable.dy / self.dx_jacket ) def Kx(self, op_cond: OperationalConditions) -> float: # noqa: N802 @@ -344,12 +311,7 @@ def _Ky_topbot_ins(self, op_cond: OperationalConditions) -> float: # noqa: N802 : Axial stiffness [N/m] """ - return ( - self.mat_ins.youngs_modulus(op_cond) - * 2 - * self.cable.dx - / self.params.dy_ins.value - ) + return self.mat_ins.youngs_modulus(op_cond) * 2 * self.cable.dx / self.dy_ins def _Ky_lat_ins(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ @@ -360,11 +322,7 @@ def _Ky_lat_ins(self, op_cond: OperationalConditions) -> float: # noqa: N802 : Axial stiffness [N/m] """ - return ( - self.mat_ins.youngs_modulus(op_cond) - * self.params.dx_ins.value - / (2 * self.dy) - ) + return self.mat_ins.youngs_modulus(op_cond) * self.dx_ins / (2 * self.dy) def _Ky_lat_jacket(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ @@ -377,8 +335,8 @@ def _Ky_lat_jacket(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ return ( self.mat_jacket.youngs_modulus(op_cond) - * self.params.dx_jacket.value - / (2 * self.dy - 2 * self.params.dy_ins.value) + * self.dx_jacket + / (2 * self.dy - 2 * self.dy_ins) ) def _Ky_topbot_jacket(self, op_cond: OperationalConditions) -> float: # noqa: N802 @@ -391,10 +349,7 @@ def _Ky_topbot_jacket(self, op_cond: OperationalConditions) -> float: # noqa: N Axial stiffness [N/m] """ return ( - self.mat_jacket.youngs_modulus(op_cond) - * 2 - * self.cable.dx - / self.params.dy_jacket.value + self.mat_jacket.youngs_modulus(op_cond) * 2 * self.cable.dx / self.dy_jacket ) def Ky(self, op_cond: OperationalConditions) -> float: # noqa: N802 @@ -462,9 +417,7 @@ def _tresca_sigma_jacket( raise ValueError("Invalid direction: choose either 'x' or 'y'.") if direction == "x": - saf_jacket = (self.cable.dx + self.params.dx_jacket.value) / ( - self.params.dx_jacket.value - ) + saf_jacket = (self.cable.dx + self.dx_jacket) / (self.dx_jacket) K = summation([ # noqa: N806 2 * self._Ky_lat_ins(op_cond), @@ -478,9 +431,7 @@ def _tresca_sigma_jacket( X_jacket = 2 * self._Ky_lat_jacket(op_cond) / K # noqa: N806 else: - saf_jacket = (self.cable.dy + self.params.dy_jacket.value) / ( - self.params.dy_jacket.value - ) + saf_jacket = (self.cable.dy + self.dy_jacket) / (self.dy_jacket) K = summation([ # noqa: N806 2 * self._Kx_lat_ins(op_cond), @@ -604,9 +555,9 @@ def sigma_difference( raise ValueError("Invalid direction: choose either 'x' or 'y'.") if direction == "x": - self.params.dx_jacket.value = jacket_thickness + self.dx_jacket = jacket_thickness else: - self.params.dy_jacket.value = jacket_thickness + self.dy_jacket = jacket_thickness sigma_r = self._tresca_sigma_jacket(pressure, fz, op_cond, direction) @@ -623,9 +574,9 @@ def sigma_difference( debug_msg = ["Method optimize_jacket_conductor:"] if direction == "x": - debug_msg.append(f"Previous dx_jacket: {self.params.dx_jacket.value}") + debug_msg.append(f"Previous dx_jacket: {self.dx_jacket}") else: - debug_msg.append(f"Previous dy_jacket: {self.params.dy_jacket.value}") + debug_msg.append(f"Previous dy_jacket: {self.dy_jacket}") method = "bounded" if bounds is not None else None @@ -643,11 +594,11 @@ def sigma_difference( if not result.success: raise ValueError("Optimization of the jacket conductor did not converge.") if direction == "x": - self.params.dx_jacket.value = result.x - debug_msg.append(f"Optimal dx_jacket: {self.params.dx_jacket.value}") + self.dx_jacket = result.x + debug_msg.append(f"Optimal dx_jacket: {self.dx_jacket}") else: - self.params.dy_jacket.value = result.x - debug_msg.append(f"Optimal dy_jacket: {self.params.dy_jacket.value}") + self.dy_jacket = result.x + debug_msg.append(f"Optimal dy_jacket: {self.dy_jacket}") debug_msg.append( f"Averaged sigma in the {direction}-direction: " f"{self._tresca_sigma_jacket(pressure, f_z, op_cond) / 1e6} MPa\n" @@ -701,8 +652,8 @@ def plot(self, xc: float = 0, yc: float = 0, *, show: bool = False, ax=None): _, ax = plt.subplots() pc = np.array([xc, yc]) - a = self.cable.dx + self.params.dx_jacket.value - b = self.cable.dy + self.params.dy_jacket.value + a = self.cable.dx + self.dx_jacket + b = self.cable.dy + self.dy_jacket p0 = np.array([-a, -b]) p1 = np.array([a, -b]) @@ -710,8 +661,8 @@ def plot(self, xc: float = 0, yc: float = 0, *, show: bool = False, ax=None): p3 = np.array([-a, b]) points_ext_jacket = np.vstack((p0, p1, p2, p3, p0)) + pc - c = a + self.params.dx_ins.value - d = b + self.params.dy_ins.value + c = a + self.dx_ins + d = b + self.dy_ins p0 = np.array([-c, -d]) p1 = np.array([c, -d]) @@ -750,27 +701,13 @@ def __str__(self) -> str: f"------- cable -------\n" f"cable: {self.cable!s}\n" f"---------------------\n" - f"dx_jacket: {self.params.dx_jacket.value}\n" - f"dy_jacket: {self.params.dy_jacket.value}\n" - f"dx_ins: {self.params.dx_ins.value}\n" - f"dy_ins: {self.params.dy_ins.value}" + f"dx_jacket: {self.dx_jacket}\n" + f"dy_jacket: {self.dy_jacket}\n" + f"dx_ins: {self.dx_ins}\n" + f"dy_ins: {self.dy_ins}" ) -@dataclass -class SymmetricConductorParams(ParameterFrame): - """ - Parameters needed for the symmetric conductor - - Just use dl? instead of a different dx and dy that are equal? - """ - - dx_jacket: Parameter[float] - """x-thickness of the jacket [m].""" - dx_ins: Parameter[float] - """x-thickness of the insulator [m].""" - - class SymmetricConductor(Conductor): # jm - actually worthwhile or just set up # conductor with dx = dy and don't duplicate? """ @@ -778,14 +715,13 @@ class SymmetricConductor(Conductor): # jm - actually worthwhile or just set mantain a constant thickness (i.e. dy_jacket = dx_jacket and dy_ins = dx_ins). """ - param_cls: type[SymmetricConductorParams] = SymmetricConductorParams - def __init__( self, cable: ABCCable, mat_jacket: Material, mat_ins: Material, - params: ParameterFrameLike, + dx_jacket: float, + dx_ins: float, name: str = "SymmetricConductor", ): """ @@ -800,13 +736,10 @@ def __init__( jacket's material mat_ins: insulator's material - params: - Structure containing the input parameters. Keys are: - - dx_jacket: float - - dx_ins: float - - See :class:`~bluemira.magnets.conductor.SymmetricConductorParams` - for parameter details. + dx_jacket: + x-thickness of the jacket [m]. + dx_ins: + x-thickness of the insulator [m]. name: string identifier @@ -815,11 +748,10 @@ def __init__( cable=cable, mat_jacket=mat_jacket, mat_ins=mat_ins, - params=params, + dx_jacket=dx_jacket, + dx_ins=dx_ins, name=name, ) - self.dy_jacket = self.params.dx_jacket.value # jm - needed or just property? - self.dy_ins = self.params.dx_ins.value # jm - needed or just property? @property def dy_jacket(self): @@ -834,7 +766,7 @@ def dy_jacket(self): ----- Assumes the same value as `dx_jacket`, ensuring symmetry in both directions. """ - return self.params.dx_jacket.value + return self.dx_jacket @property def dy_ins(self): @@ -849,7 +781,7 @@ def dy_ins(self): ----- Assumes the same value as `dx_ins`, ensuring symmetry in both directions. """ - return self.params.dx_ins.value + return self.dx_ins def to_dict(self) -> dict: """ @@ -865,8 +797,8 @@ def to_dict(self) -> dict: "cable": self.cable.to_dict(), "mat_jacket": self.mat_jacket.name, "mat_ins": self.mat_ins.name, - "dx_jacket": self.params.dx_jacket.value, - "dx_ins": self.params.dx_ins.value, + "dx_jacket": self.dx_jacket, + "dx_ins": self.dx_ins, } @classmethod diff --git a/bluemira/magnets/strand.py b/bluemira/magnets/strand.py index 455c9d6f74..6318db0f46 100644 --- a/bluemira/magnets/strand.py +++ b/bluemira/magnets/strand.py @@ -14,8 +14,7 @@ from __future__ import annotations -from dataclasses import dataclass -from typing import TYPE_CHECKING, Any +from typing import Any import matplotlib.pyplot as plt import numpy as np @@ -24,27 +23,10 @@ from bluemira import display from bluemira.base.look_and_feel import bluemira_error -from bluemira.base.parameter_frame import Parameter, ParameterFrame -from bluemira.base.parameter_frame.typed import ParameterFrameLike from bluemira.display.plotter import PlotOptions from bluemira.geometry.face import BluemiraFace from bluemira.geometry.tools import make_circle -if TYPE_CHECKING: - from bluemira.base.parameter_frame.typed import ParameterFrameLike - - -@dataclass -class StrandParams(ParameterFrame): - """ - Parameters needed for the strand - """ - - d_strand: Parameter[float] - """Strand diameter in meters.""" - operating_temperature: Parameter[float] - """Operating temperature [K].""" - class Strand: """ @@ -54,12 +36,11 @@ class Strand: This class automatically registers itself and its instances. """ - param_cls: type[StrandParams] = StrandParams - def __init__( self, materials: list[MaterialFraction], - params: ParameterFrameLike, + d_strand: float, + operating_temperature: float, name: str | None = "Strand", ): """ @@ -69,18 +50,16 @@ def __init__( ---------- materials: Materials composing the strand with their fractions. - params: - Structure containing the input parameters. Keys are: - - d_strand: float - - operating_temperature: float + d_strand: + Strand diameter [m]. + operating_temperature: float + Operating temperature [K]. - See :class:`~bluemira.magnets.strand.StrandParams` - for parameter details. name: Name of the strand. Defaults to "Strand". """ - self.params = params - + self.d_strand = d_strand + self.operating_temperature = operating_temperature self.materials = materials self.name = name @@ -143,7 +122,7 @@ def area(self) -> float: : Area [m²]. """ - return np.pi * (self.params.d_strand.value**2) / 4 + return np.pi * (self.d_strand**2) / 4 @property def shape(self) -> BluemiraFace: @@ -156,7 +135,7 @@ def shape(self) -> BluemiraFace: Circular face of the strand. """ if self._shape is None: - self._shape = BluemiraFace([make_circle(self.params.d_strand.value)]) + self._shape = BluemiraFace([make_circle(self.d_strand)]) return self._shape def E(self, op_cond: OperationalConditions) -> float: # noqa: N802 @@ -285,7 +264,7 @@ def __str__(self) -> str: """ return ( f"name = {self.name}\n" - f"d_strand = {self.params.d_strand.value}\n" + f"d_strand = {self.d_strand}\n" f"materials = {self.materials}\n" f"shape = {self.shape}\n" ) @@ -301,8 +280,8 @@ def to_dict(self) -> dict[str, Any]: """ return { "name": self.name, - "d_strand": self.params.d_strand.value, - "temperature": self.params.operating_temperature.value, + "d_strand": self.d_strand, + "temperature": self.operating_temperature, "materials": [ { "material": m.material, @@ -379,16 +358,6 @@ class registration name. # ------------------------------------------------------------------------------ # SuperconductingStrand Class # ------------------------------------------------------------------------------ -@dataclass -class SuperconductingStrandParams(StrandParams): - """ - Parameters needed for the strand - """ - - d_strand_sc: Parameter[float] # not sure this will work? - """Superconducting Strand diameter in meters.""" - - class SuperconductingStrand(Strand): """ Represents a superconducting strand with a circular cross-section. @@ -400,12 +369,12 @@ class SuperconductingStrand(Strand): """ _name_in_registry_ = "SuperconductingStrand" - param_cls: type[StrandParams] = StrandParams def __init__( self, materials: list[MaterialFraction], - params: ParameterFrameLike, + d_strand: float, + operating_temperature: float, name: str | None = "SuperconductingStrand", ): """ @@ -416,22 +385,20 @@ def __init__( materials: Materials composing the strand with their fractions. One material must be a supercoductor. - params: - Structure containing the input parameters. Keys are: - - d_strand_sc: float - - See :class:`~bluemira.magnets.strand.StrandParams` - for parameter details. + d_strand: + Strand diameter [m]. + operating_temperature: float + Operating temperature [K]. name: Name of the strand. Defaults to "Strand". """ super().__init__( materials=materials, - params=params, + d_strand=d_strand, + operating_temperature=operating_temperature, name=name, ) self._sc = self._check_materials() - self.params.d_strand.value = self.params.d_strand_sc.value def _check_materials(self) -> MaterialFraction: """ diff --git a/bluemira/magnets/winding_pack.py b/bluemira/magnets/winding_pack.py index 26fb294224..91acc02fac 100644 --- a/bluemira/magnets/winding_pack.py +++ b/bluemira/magnets/winding_pack.py @@ -6,30 +6,15 @@ """Winding pack module""" -from dataclasses import dataclass from typing import Any, ClassVar import matplotlib.pyplot as plt import numpy as np from matproplib import OperationalConditions -from bluemira.base.parameter_frame import Parameter, ParameterFrame -from bluemira.base.parameter_frame.typed import ParameterFrameLike from bluemira.magnets.conductor import Conductor, create_conductor_from_dict -@dataclass -class WindingPackParams(ParameterFrame): - """ - Parameters needed for the Winding Pack - """ - - nx: Parameter[int] - """Number of conductors along the x-axis.""" - ny: Parameter[int] - """Number of conductors along the y-axis.""" - - class WindingPack: """ Represents a winding pack composed of a grid of conductors. @@ -45,10 +30,9 @@ class WindingPack: """ _name_in_registry_: ClassVar[str] = "WindingPack" - param_cls: type[WindingPackParams] = WindingPackParams def __init__( - self, conductor: Conductor, params: ParameterFrameLike, name: str = "WindingPack" + self, conductor: Conductor, nx: int, ny: int, name: str = "WindingPack" ): """ Initialize a WindingPack instance. @@ -57,29 +41,27 @@ def __init__( ---------- conductor: The conductor instance. - params: - Structure containing the input parameters. Keys are: - - nx: int - - ny: int - - See :class:`~bluemira.magnets.winding_pack.WindingPackParams` - for parameter details. + nx: + Number of conductors along the x-axis. + ny: + Number of conductors along the y-axis. name: Name of the winding pack instance. """ self.conductor = conductor - self.params = params + self.nx = nx + self.ny = ny self.name = name @property def dx(self) -> float: """Return the half width of the winding pack [m].""" - return self.conductor.dx * self.params.nx.value + return self.conductor.dx * self.nx @property def dy(self) -> float: """Return the half height of the winding pack [m].""" - return self.conductor.dy * self.params.ny.value + return self.conductor.dy * self.ny @property def area(self) -> float: @@ -89,7 +71,7 @@ def area(self) -> float: @property def n_conductors(self) -> int: """Return the total number of conductors.""" - return self.params.nx.value * self.params.ny.value + return self.nx * self.ny @property def jacket_area(self) -> float: @@ -111,7 +93,7 @@ def Kx(self, op_cond: OperationalConditions) -> float: # noqa: N802 : Stiffness along the x-axis [N/m]. """ - return self.conductor.Kx(op_cond) * self.params.ny.value / self.params.nx.value + return self.conductor.Kx(op_cond) * self.ny / self.nx def Ky(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ @@ -128,7 +110,7 @@ def Ky(self, op_cond: OperationalConditions) -> float: # noqa: N802 : Stiffness along the y-axis [N/m]. """ - return self.conductor.Ky(op_cond) * self.params.nx.value / self.params.ny.value + return self.conductor.Ky(op_cond) * self.nx / self.ny def plot( self, @@ -176,8 +158,8 @@ def plot( ax.plot(points_ext[:, 0], points_ext[:, 1], "k") if not homogenized: - for i in range(self.params.nx.value): - for j in range(self.params.ny.value): + for i in range(self.nx): + for j in range(self.ny): xc_c = xc - self.dx + (2 * i + 1) * self.conductor.dx yc_c = yc - self.dy + (2 * j + 1) * self.conductor.dy self.conductor.plot(xc=xc_c, yc=yc_c, ax=ax) @@ -198,8 +180,8 @@ def to_dict(self) -> dict[str, Any]: return { "name": self.name, "conductor": self.conductor.to_dict(), - "nx": self.params.nx.value, - "ny": self.params.ny.value, + "nx": self.nx, + "ny": self.ny, } @classmethod From 789f7aca435477f2ea468b22b6552391ce97b2c8 Mon Sep 17 00:00:00 2001 From: Jonathan Matthews Date: Tue, 2 Sep 2025 11:17:33 +0100 Subject: [PATCH 25/61] ammended the designer to work better with multiple input WPs --- bluemira/magnets/tfcoil_designer.py | 121 ++++++++++++++++++++-------- 1 file changed, 86 insertions(+), 35 deletions(-) diff --git a/bluemira/magnets/tfcoil_designer.py b/bluemira/magnets/tfcoil_designer.py index 546b07af6c..6b3dbaacd8 100644 --- a/bluemira/magnets/tfcoil_designer.py +++ b/bluemira/magnets/tfcoil_designer.py @@ -240,30 +240,30 @@ def run(self): case: TF case object all parts that make it up. """ - # sort configs - stab_strand_config = self.build_config.get("stabilising_strand") - sc_strand_config = self.build_config.get("superconducting_strand") - conductor_config = self.build_config.get("conductor") - case_config = self.build_config.get("case") # params that are function of another param derived_params = self._derived_values() - stab_strand = self._make_stab_strand(stab_strand_config) - sc_strand = self._make_sc_strand(sc_strand_config) - initial_cable = self._make_cable(stab_strand, sc_strand) - # param frame optimisation stuff? - optimised_cable = initial_cable.optimise_n_stab_ths( - t0=self.params.t0.value, - tf=self.params.Tau_discharge.value, - T_for_hts=derived_params["T_op"], - hotspot_target_temperature=self.params.hotspot_target_temperature.value, - B_fun=derived_params["B_fun"], - I_fun=derived_params["I_fun"], - bounds=[1, 10000], - ) - conductor = self._make_conductor(conductor_config, optimised_cable) - winding_pack = self._make_winding_pack(conductor) - case = self._make_case(case_config, [winding_pack]) + n_WPs = len(self.params.nx.value) + if n_WPs > 1: + self._check_arrays_match() + winding_pack = [] + for i_WP in n_WPs: + stab_strand = self._make_stab_strand(i_WP) + sc_strand = self._make_sc_strand(i_WP) + initial_cable = self._make_cable(stab_strand, sc_strand, i_WP) + # param frame optimisation stuff? + optimised_cable = initial_cable.optimise_n_stab_ths( + t0=self.params.t0.value, + tf=self.params.Tau_discharge.value, + T_for_hts=derived_params["T_op"], + hotspot_target_temperature=self.params.hotspot_target_temperature.value, + B_fun=derived_params["B_fun"], + I_fun=derived_params["I_fun"], + bounds=[1, 10000], + ) + conductor = self._make_conductor(optimised_cable, i_WP) + winding_pack += [self._make_winding_pack(conductor, i_WP)] + case = self._make_case(winding_pack) # param frame optimisation stuff? case.rearrange_conductors_in_wp( n_conductors=derived_params["n_cond"], @@ -291,6 +291,30 @@ def run(self): ) return case + def _check_arrays_match(self): + n = len(self.params.nx.value) + param_list = [ + "d_strand_sc", + "d_strand", + "operating_temperature", + "n_sc_strand", + "n_stab_strand", + "d_cooling_channel", + "void_fraction", + "cos_theta", + "dx", + "dx_jacket", + "dy_jacket", + "dx_ins", + "dy_ins", + "ny", + ] + for param in param_list: + if len(self.params.get(param).value) != n: + self.params.get(param).value = [ + self.params.get(param).value for _ in range(n) + ] + def B_TF_r(self, tf_current, r): """ Compute the magnetic field generated by the TF coils, @@ -312,42 +336,57 @@ def B_TF_r(self, tf_current, r): """ return 1.08 * (MU_0_2PI * self.params.n_TF.value * tf_current / r) - def _make_stab_strand(self): + def _make_stab_strand(self, i_WP): stab_strand_config = self.build_config.get("stabilising_strand") return Strand( materials=stab_strand_config.get("material"), - params=self.params, + d_strand=self.params.d_strand.value[i_WP], + operating_temperature=self.params.operating_temperature.value[i_WP], name="stab_strand", ) - def _make_sc_strand(self): + def _make_sc_strand(self, i_WP): sc_strand_config = self.build_config.get("superconducting_strand") return SuperconductingStrand( materials=sc_strand_config.get("material"), - params=self.params, + d_strand=self.params.d_strand_sc.value[i_WP], + operating_temperature=self.params.operating_temperature.value[i_WP], name="sc_strand", ) - def _make_cable(self, stab_strand, sc_strand): + def _make_cable(self, stab_strand, sc_strand, i_WP): if self.params.cable_type == "Rectangular": cable = RectangularCable( sc_strand=sc_strand, stab_strand=stab_strand, - params=self.params, + n_sc_strand=self.params.n_sc_strand.value[i_WP], + n_stab_strand=self.params.n_stab_strand.value[i_WP], + d_cooling_channel=self.params.d_cooling_channel.value[i_WP], + void_fraction=self.params.void_fraction.value[i_WP], + cos_theta=self.params.cos_theta.value[i_WP], + dx=self.params.dx.value[i_WP], name="RectangularCable", ) elif self.params.cable_type == "Square": cable = SquareCable( sc_strand=sc_strand, stab_strand=stab_strand, - params=self.params, + n_sc_strand=self.params.n_sc_strand.value[i_WP], + n_stab_strand=self.params.n_stab_strand.value[i_WP], + d_cooling_channel=self.params.d_cooling_channel.value[i_WP], + void_fraction=self.params.void_fraction.value[i_WP], + cos_theta=self.params.cos_theta.value[i_WP], name="SquareCable", ) elif self.params.cable_type == "Round": cable = RoundCable( sc_strand=sc_strand, stab_strand=stab_strand, - params=self.params, + n_sc_strand=self.params.n_sc_strand.value[i_WP], + n_stab_strand=self.params.n_stab_strand.value[i_WP], + d_cooling_channel=self.params.d_cooling_channel.value[i_WP], + void_fraction=self.params.void_fraction.value[i_WP], + cos_theta=self.params.cos_theta.value[i_WP], name="RoundCable", ) else: @@ -357,14 +396,17 @@ def _make_cable(self, stab_strand, sc_strand): ) return cable - def _make_conductor(self, cable): + def _make_conductor(self, cable, i_WP): conductor_config = self.build_config.get("conductor") if self.params.conductor_type == "Conductor": conductor = Conductor( cable=cable, mat_jacket=conductor_config.get("jacket", "material"), mat_ins=conductor_config.get("ins", "material"), - params=self.params, + dx_jacket=self.params.dx_jacket.value[i_WP], + dy_jacket=self.params.dy_jacket.value[i_WP], + dx_ins=self.params.dx_ins.value[i_WP], + dy_ins=self.params.dy_ins.value[i_WP], name="Conductor", ) elif self.params.conductor_type == "SymmetricConductor": @@ -372,7 +414,8 @@ def _make_conductor(self, cable): cable=cable, mat_jacket=conductor_config.get("jacket", "material"), mat_ins=conductor_config.get("ins", "material"), - params=self.params, + dx_jacket=self.params.dx_jacket.value[i_WP], + dx_ins=self.params.dx_ins.value[i_WP], name="SymmetricConductor", ) else: @@ -382,14 +425,22 @@ def _make_conductor(self, cable): ) return conductor - def _make_winding_pack(self, conductor): - return WindingPack(conductor=conductor, params=self.params, name="winding_pack") + def _make_winding_pack(self, conductor, i_WP): + return WindingPack( + conductor=conductor, + nx=self.params.nx.value[i_WP], + ny=self.params.ny.value[i_WP], + name="winding_pack", + ) def _make_case(self, WPs): # noqa: N803 case_config = self.build_config.get("case") if self.params.case_type == "Trapezoidal": case = TrapezoidalCaseTF( - params=self.params, + Ri=self.params.Ri.value, + theta_TF=self.params.theta_TF.value, + dy_ps=self.params.dy_ps.value, + dy_vault=self.params.dy_vault.value, mat_case=case_config.get("material"), WPs=WPs, name="TrapezoidalCase", From 0ad7abec2226bf2e13faec471d41bd70d7c2709f Mon Sep 17 00:00:00 2001 From: Jonathan Matthews Date: Tue, 2 Sep 2025 11:51:44 +0100 Subject: [PATCH 26/61] fix to handling of materials in designer --- bluemira/magnets/tfcoil_designer.py | 38 ++++++++++++++++------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/bluemira/magnets/tfcoil_designer.py b/bluemira/magnets/tfcoil_designer.py index 6b3dbaacd8..4d90fd2052 100644 --- a/bluemira/magnets/tfcoil_designer.py +++ b/bluemira/magnets/tfcoil_designer.py @@ -9,6 +9,7 @@ import matplotlib.pyplot as plt import numpy as np +from matproplib import OperationalConditions from bluemira.base.constants import MU_0, MU_0_2PI, MU_0_4PI from bluemira.base.designer import Designer @@ -276,8 +277,10 @@ def run(self): case.optimize_jacket_and_vault( pm=derived_params["pm"], fz=derived_params["t_z"], - temperature=derived_params["T_op"], - B=derived_params["B_TF_i"], + op_cond=OperationalConditions( + temperature=derived_params["T_op"], + magnetic_field=derived_params["B_TF_i"], + ), allowable_sigma=derived_params["s_y"], bounds_cond_jacket=self.params.bounds_cond_jacket.value, bounds_dy_vault=self.params.bounds_dy_vault.value, @@ -339,7 +342,7 @@ def B_TF_r(self, tf_current, r): def _make_stab_strand(self, i_WP): stab_strand_config = self.build_config.get("stabilising_strand") return Strand( - materials=stab_strand_config.get("material"), + materials=stab_strand_config.get("materials"), d_strand=self.params.d_strand.value[i_WP], operating_temperature=self.params.operating_temperature.value[i_WP], name="stab_strand", @@ -348,14 +351,15 @@ def _make_stab_strand(self, i_WP): def _make_sc_strand(self, i_WP): sc_strand_config = self.build_config.get("superconducting_strand") return SuperconductingStrand( - materials=sc_strand_config.get("material"), + materials=sc_strand_config.get("materials"), d_strand=self.params.d_strand_sc.value[i_WP], operating_temperature=self.params.operating_temperature.value[i_WP], name="sc_strand", ) def _make_cable(self, stab_strand, sc_strand, i_WP): - if self.params.cable_type == "Rectangular": + cable_config = self.build_config.get("cable") + if cable_config.get("type") == "Rectangular": cable = RectangularCable( sc_strand=sc_strand, stab_strand=stab_strand, @@ -367,7 +371,7 @@ def _make_cable(self, stab_strand, sc_strand, i_WP): dx=self.params.dx.value[i_WP], name="RectangularCable", ) - elif self.params.cable_type == "Square": + elif cable_config.get("type") == "Square": cable = SquareCable( sc_strand=sc_strand, stab_strand=stab_strand, @@ -378,7 +382,7 @@ def _make_cable(self, stab_strand, sc_strand, i_WP): cos_theta=self.params.cos_theta.value[i_WP], name="SquareCable", ) - elif self.params.cable_type == "Round": + elif cable_config.get("type") == "Round": cable = RoundCable( sc_strand=sc_strand, stab_strand=stab_strand, @@ -391,36 +395,36 @@ def _make_cable(self, stab_strand, sc_strand, i_WP): ) else: raise ValueError( - f"Cable type {self.params.cable_type} is not known." + f"Cable type {cable_config.get('type')} is not known." "Available options are 'Rectangular', 'Square' and 'Round'." ) return cable def _make_conductor(self, cable, i_WP): conductor_config = self.build_config.get("conductor") - if self.params.conductor_type == "Conductor": + if conductor_config.get("type") == "Conductor": conductor = Conductor( cable=cable, - mat_jacket=conductor_config.get("jacket", "material"), - mat_ins=conductor_config.get("ins", "material"), + mat_jacket=conductor_config.get("jacket_material"), + mat_ins=conductor_config.get("ins_material"), dx_jacket=self.params.dx_jacket.value[i_WP], dy_jacket=self.params.dy_jacket.value[i_WP], dx_ins=self.params.dx_ins.value[i_WP], dy_ins=self.params.dy_ins.value[i_WP], name="Conductor", ) - elif self.params.conductor_type == "SymmetricConductor": + elif conductor_config.get("type") == "SymmetricConductor": conductor = SymmetricConductor( cable=cable, - mat_jacket=conductor_config.get("jacket", "material"), - mat_ins=conductor_config.get("ins", "material"), + mat_jacket=conductor_config.get("jacket_material"), + mat_ins=conductor_config.get("ins_material"), dx_jacket=self.params.dx_jacket.value[i_WP], dx_ins=self.params.dx_ins.value[i_WP], name="SymmetricConductor", ) else: raise ValueError( - f"Conductor type {self.params.conductor_type} is not known." + f"Conductor type {conductor_config.get('type')} is not known." "Available options are 'Conductor' and 'SymmetricConductor'." ) return conductor @@ -435,7 +439,7 @@ def _make_winding_pack(self, conductor, i_WP): def _make_case(self, WPs): # noqa: N803 case_config = self.build_config.get("case") - if self.params.case_type == "Trapezoidal": + if case_config.get("type") == "Trapezoidal": case = TrapezoidalCaseTF( Ri=self.params.Ri.value, theta_TF=self.params.theta_TF.value, @@ -447,7 +451,7 @@ def _make_case(self, WPs): # noqa: N803 ) else: raise ValueError( - f"Case type {self.params.case_type} is not known." + f"Case type {case_config.get('type')} is not known." "Available options are 'Trapezoidal'." ) return case From b45e2490380216e0620f6078900e11c313aeda82 Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Tue, 2 Sep 2025 14:47:12 +0100 Subject: [PATCH 27/61] =?UTF-8?q?=E2=9C=A8=20Get=20class?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bluemira/magnets/tfcoil_designer.py | 160 +++++++++++----------------- 1 file changed, 65 insertions(+), 95 deletions(-) diff --git a/bluemira/magnets/tfcoil_designer.py b/bluemira/magnets/tfcoil_designer.py index 4d90fd2052..bd64576c6a 100644 --- a/bluemira/magnets/tfcoil_designer.py +++ b/bluemira/magnets/tfcoil_designer.py @@ -14,12 +14,10 @@ from bluemira.base.constants import MU_0, MU_0_2PI, MU_0_4PI from bluemira.base.designer import Designer from bluemira.base.parameter_frame import Parameter, ParameterFrame -from bluemira.magnets.cable import RectangularCable, RoundCable, SquareCable -from bluemira.magnets.case_tf import TrapezoidalCaseTF -from bluemira.magnets.conductor import Conductor, SymmetricConductor -from bluemira.magnets.strand import Strand, SuperconductingStrand +from bluemira.magnets.cable import RectangularCable +from bluemira.magnets.conductor import SymmetricConductor from bluemira.magnets.utils import delayed_exp_func -from bluemira.magnets.winding_pack import WindingPack +from bluemira.utilities.tools import get_class_from_module @dataclass @@ -341,120 +339,92 @@ def B_TF_r(self, tf_current, r): def _make_stab_strand(self, i_WP): stab_strand_config = self.build_config.get("stabilising_strand") - return Strand( - materials=stab_strand_config.get("materials"), + cls_name = stab_strand_config["class"] + stab_strand_cls = get_class_from_module(cls_name) + return stab_strand_cls( + materials=stab_strand_config["materials"], d_strand=self.params.d_strand.value[i_WP], operating_temperature=self.params.operating_temperature.value[i_WP], - name="stab_strand", + name=stab_strand_config.get("name", cls_name.rsplit("::", 1)[-1]), ) def _make_sc_strand(self, i_WP): sc_strand_config = self.build_config.get("superconducting_strand") - return SuperconductingStrand( - materials=sc_strand_config.get("materials"), + cls_name = sc_strand_config["class"] + sc_strand_cls = get_class_from_module(cls_name) + return sc_strand_cls( + materials=sc_strand_config["materials"], d_strand=self.params.d_strand_sc.value[i_WP], operating_temperature=self.params.operating_temperature.value[i_WP], - name="sc_strand", + name=sc_strand_config.get("name", cls_name.rsplit("::", 1)[-1]), ) def _make_cable(self, stab_strand, sc_strand, i_WP): cable_config = self.build_config.get("cable") - if cable_config.get("type") == "Rectangular": - cable = RectangularCable( - sc_strand=sc_strand, - stab_strand=stab_strand, - n_sc_strand=self.params.n_sc_strand.value[i_WP], - n_stab_strand=self.params.n_stab_strand.value[i_WP], - d_cooling_channel=self.params.d_cooling_channel.value[i_WP], - void_fraction=self.params.void_fraction.value[i_WP], - cos_theta=self.params.cos_theta.value[i_WP], - dx=self.params.dx.value[i_WP], - name="RectangularCable", - ) - elif cable_config.get("type") == "Square": - cable = SquareCable( - sc_strand=sc_strand, - stab_strand=stab_strand, - n_sc_strand=self.params.n_sc_strand.value[i_WP], - n_stab_strand=self.params.n_stab_strand.value[i_WP], - d_cooling_channel=self.params.d_cooling_channel.value[i_WP], - void_fraction=self.params.void_fraction.value[i_WP], - cos_theta=self.params.cos_theta.value[i_WP], - name="SquareCable", - ) - elif cable_config.get("type") == "Round": - cable = RoundCable( - sc_strand=sc_strand, - stab_strand=stab_strand, - n_sc_strand=self.params.n_sc_strand.value[i_WP], - n_stab_strand=self.params.n_stab_strand.value[i_WP], - d_cooling_channel=self.params.d_cooling_channel.value[i_WP], - void_fraction=self.params.void_fraction.value[i_WP], - cos_theta=self.params.cos_theta.value[i_WP], - name="RoundCable", - ) - else: - raise ValueError( - f"Cable type {cable_config.get('type')} is not known." - "Available options are 'Rectangular', 'Square' and 'Round'." - ) - return cable + cls_name = stab_strand_config["class"] + cable_cls = get_class_from_module(cls_name) + return cable_cls( + sc_strand=sc_strand, + stab_strand=stab_strand, + n_sc_strand=self.params.n_sc_strand.value[i_WP], + n_stab_strand=self.params.n_stab_strand.value[i_WP], + d_cooling_channel=self.params.d_cooling_channel.value[i_WP], + void_fraction=self.params.void_fraction.value[i_WP], + cos_theta=self.params.cos_theta.value[i_WP], + name=cable_config.get("name", cls_name.rsplit("::", 1)[-1]), + **( + {"dx": self.params.dx.value[i_WP]} + if issubclass(cable_cls, RectangularCable) + else {} + ), + ) def _make_conductor(self, cable, i_WP): conductor_config = self.build_config.get("conductor") - if conductor_config.get("type") == "Conductor": - conductor = Conductor( - cable=cable, - mat_jacket=conductor_config.get("jacket_material"), - mat_ins=conductor_config.get("ins_material"), - dx_jacket=self.params.dx_jacket.value[i_WP], - dy_jacket=self.params.dy_jacket.value[i_WP], - dx_ins=self.params.dx_ins.value[i_WP], - dy_ins=self.params.dy_ins.value[i_WP], - name="Conductor", - ) - elif conductor_config.get("type") == "SymmetricConductor": - conductor = SymmetricConductor( - cable=cable, - mat_jacket=conductor_config.get("jacket_material"), - mat_ins=conductor_config.get("ins_material"), - dx_jacket=self.params.dx_jacket.value[i_WP], - dx_ins=self.params.dx_ins.value[i_WP], - name="SymmetricConductor", - ) - else: - raise ValueError( - f"Conductor type {conductor_config.get('type')} is not known." - "Available options are 'Conductor' and 'SymmetricConductor'." - ) - return conductor + cls_name = conductor_config["class"] + conductor_cls = get_class_from_module(cls_name) + return conductor_cls( + cable=cable, + mat_jacket=conductor_config["jacket_material"], + mat_ins=conductor_config["ins_material"], + dx_jacket=self.params.dx_jacket.value[i_WP], + dx_ins=self.params.dx_ins.value[i_WP], + name=conductor_config.get("name", cls_name.rsplit("::", 1)[-1]), + **( + {} + if issubclass(conductor_cls, SymmetricConductor) + else { + "dy_jacket": self.params.dy_jacket.value[i_WP], + "dy_ins": self.params.dy_ins.value[i_WP], + } + ), + ) def _make_winding_pack(self, conductor, i_WP): - return WindingPack( + winding_pack_config = self.build_config.get("winding_pack") + cls_name = winding_pack_config["class"] + winding_pack_cls = get_class_from_module(cls_name) + return winding_pack_cls( conductor=conductor, nx=self.params.nx.value[i_WP], ny=self.params.ny.value[i_WP], - name="winding_pack", + name=winding_pack_config.get("name", cls_name.rsplit("::", 1)[-1]), ) def _make_case(self, WPs): # noqa: N803 case_config = self.build_config.get("case") - if case_config.get("type") == "Trapezoidal": - case = TrapezoidalCaseTF( - Ri=self.params.Ri.value, - theta_TF=self.params.theta_TF.value, - dy_ps=self.params.dy_ps.value, - dy_vault=self.params.dy_vault.value, - mat_case=case_config.get("material"), - WPs=WPs, - name="TrapezoidalCase", - ) - else: - raise ValueError( - f"Case type {case_config.get('type')} is not known." - "Available options are 'Trapezoidal'." - ) - return case + cls_name = case_config["class"] + case_cls = get_class_from_module(cls_name) + + return case_cls( + Ri=self.params.Ri.value, + theta_TF=self.params.theta_TF.value, + dy_ps=self.params.dy_ps.value, + dy_vault=self.params.dy_vault.value, + mat_case=case_config["material"], + WPs=WPs, + name=case_config.get("name", cls_name.rsplit("::", 1)[-1]), + ) def plot_cable_temperature_evolution(result, t0, tf, ax, n_steps=100): From bd710a2177aec51cc29aa64fe4e2d548c98f20fe Mon Sep 17 00:00:00 2001 From: Jonathan Matthews Date: Tue, 2 Sep 2025 16:31:41 +0100 Subject: [PATCH 28/61] creation of new example to go with designer and fixes to classes to do with inheritance --- bluemira/magnets/cable.py | 7 +- bluemira/magnets/case_tf.py | 78 +---- bluemira/magnets/conductor.py | 19 +- bluemira/magnets/tfcoil_designer.py | 377 ++++++++++++------------ examples/magnets/example_tf_creation.py | 159 ++++++++++ 5 files changed, 380 insertions(+), 260 deletions(-) create mode 100644 examples/magnets/example_tf_creation.py diff --git a/bluemira/magnets/cable.py b/bluemira/magnets/cable.py index a9101e327d..60748aa41c 100644 --- a/bluemira/magnets/cable.py +++ b/bluemira/magnets/cable.py @@ -727,7 +727,12 @@ def __init__( name=name, **props, ) - self.dx = dx + self._dx = dx + + @property + def dx(self) -> float: + """Half Cable dimension in the x direction [m]""" + return self._dx @property def dy(self) -> float: diff --git a/bluemira/magnets/case_tf.py b/bluemira/magnets/case_tf.py index 17c3a9f594..c5e5a05b72 100644 --- a/bluemira/magnets/case_tf.py +++ b/bluemira/magnets/case_tf.py @@ -65,70 +65,6 @@ def _dx_at_radius(radius: float, rad_theta: float) -> float: return radius * np.tan(rad_theta / 2) -class CaseGeometry(ABC): - """ - Abstract base class for TF case geometry profiles. - - Provides access to radial dimensions and toroidal width calculations - as well as geometric plotting and area calculation interfaces. - - Parameters - ---------- - Ri: - External radius of the TF coil case [m]. - Rk: - Internal radius of the TF coil case [m]. - theta_TF: - Toroidal angular span of the TF coil [degrees]. - """ - - def __init__(self, Ri: float, Rk: float, theta_TF: float): - self.Ri = Ri - self.Rk = Rk - self.theta_TF = theta_TF - self.rad_theta_TF = np.radians(self.theta_TF) # property if theta_TF gets reset? - - @property - @abstractmethod - def area(self) -> float: - """ - Compute the cross-sectional area of the TF case. - - Returns - ------- - : - Cross-sectional area [m²] enclosed by the case geometry. - - Notes - ----- - Must be implemented by each specific geometry class. - """ - - @abstractmethod - def plot(self, ax: plt.Axes = None, *, show: bool = False) -> plt.Axes: - """ - Plot the cross-sectional geometry of the TF case. - - Parameters - ---------- - ax: - Axis on which to draw the geometry. If None, a new figure and axis are - created. - show: - If True, the plot is displayed immediately using plt.show(). - Default is False. - - Returns - ------- - : - The axis object containing the plot. - - Notes - ----- - Must be implemented by each specific geometry class. - """ - - @dataclass class TrapezoidalGeometryOptVariables(OptVariablesFrame): """Optimisiation variables for Trapezoidal Geometry.""" @@ -318,7 +254,7 @@ def create_shape(self, label: str = "", n_points: int = 50) -> BluemiraWire: return make_polygon(np.vstack((arc_outer, arc_inner)), label=label) -class BaseCaseTF(CaseGeometry, ABC): +class BaseCaseTF(ABC): """ Abstract Base Class for Toroidal Field Coil Case configurations. @@ -355,13 +291,11 @@ def __init__( name: String identifier for the TF coil case instance (default is "BaseCaseTF"). """ - super().__init__( - Ri=Ri, - theta_TF=theta_TF, - mat_case=mat_case, - WPs=WPs, - name=name, - ) + self.Ri=Ri, + self.theta_TF=theta_TF, + self.mat_case=mat_case, + self.WPs=WPs, + self.name=name, self.dy_ps = dy_ps self.dy_vault = dy_vault # Toroidal half-length of the coil case at its maximum radial position [m] diff --git a/bluemira/magnets/conductor.py b/bluemira/magnets/conductor.py index c5ac633890..9d94cc34d6 100644 --- a/bluemira/magnets/conductor.py +++ b/bluemira/magnets/conductor.py @@ -65,13 +65,24 @@ def __init__( """ self.name = name self.dx_jacket = dx_jacket - self.dy_jacket = dy_jacket + self._dy_jacket = dy_jacket self.dx_ins = dx_ins - self.dy_ins = dy_ins + self._dy_ins = dy_ins self.mat_ins = mat_ins self.mat_jacket = mat_jacket self.cable = cable + + @property + def dy_jacket(self): + """y-thickness of the jacket [m]""" + return self._dy_jacket + + @property + def dy_ins(self): + """y-thickness of the ins [m]""" + return self._dy_ins + @property def dx(self): """Half x-dimension of the conductor [m]""" @@ -744,12 +755,16 @@ def __init__( string identifier """ + dy_jacket=dx_jacket + dy_ins=dx_ins super().__init__( cable=cable, mat_jacket=mat_jacket, mat_ins=mat_ins, dx_jacket=dx_jacket, + dy_jacket=dy_jacket, dx_ins=dx_ins, + dy_ins=dy_ins, name=name, ) diff --git a/bluemira/magnets/tfcoil_designer.py b/bluemira/magnets/tfcoil_designer.py index bd64576c6a..fdc3308755 100644 --- a/bluemira/magnets/tfcoil_designer.py +++ b/bluemira/magnets/tfcoil_designer.py @@ -6,10 +6,12 @@ """Designer for TF Coil XY cross section.""" from dataclasses import dataclass +from typing import TYPE_CHECKING import matplotlib.pyplot as plt import numpy as np from matproplib import OperationalConditions +from matproplib.material import MaterialFraction from bluemira.base.constants import MU_0, MU_0_2PI, MU_0_4PI from bluemira.base.designer import Designer @@ -19,7 +21,7 @@ from bluemira.magnets.utils import delayed_exp_func from bluemira.utilities.tools import get_class_from_module - +from bluemira.base.parameter_frame.typed import ParameterFrameLike @dataclass class TFCoilXYDesignerParams(ParameterFrame): """ @@ -46,55 +48,55 @@ class TFCoilXYDesignerParams(ParameterFrame): B_ref: Parameter[float] """Reference value for B field (LTS limit) [T]""" - # strand params - d_strand_sc: Parameter[float] - """Diameter of superconducting strand""" - d_strand: Parameter[float] - """Diameter of stabilising strand""" - operating_temperature: Parameter[float] - """Operating temperature for the strands [K]""" - - # cable params - n_sc_strand: Parameter[int] - """Number of superconducting strands.""" - n_stab_strand: Parameter[int] - """Number of stabilizing strands.""" - d_cooling_channel: Parameter[float] - """Diameter of the cooling channel [m].""" - void_fraction: Parameter[float] - """Ratio of material volume to total volume [unitless].""" - cos_theta: Parameter[float] - """Correction factor for twist in the cable layout.""" - dx: Parameter[float] - """Cable half-width in the x-direction [m].""" - - # conductor params - dx_jacket: Parameter[float] - """x-thickness of the jacket [m].""" - dy_jacket: Parameter[float] - """y-tickness of the jacket [m].""" - dx_ins: Parameter[float] - """x-thickness of the insulator [m].""" - dy_ins: Parameter[float] - """y-thickness of the insulator [m].""" - - # winding pack params - nx: Parameter[int] - """Number of conductors along the x-axis.""" - ny: Parameter[int] - """Number of conductors along the y-axis.""" - - # case params - Ri: Parameter[float] - """External radius of the TF coil case [m].""" - Rk: Parameter[float] - """Internal radius of the TF coil case [m].""" - theta_TF: Parameter[float] - """Toroidal angular span of the TF coil [degrees].""" - dy_ps: Parameter[float] - """Radial thickness of the poloidal support region [m].""" - dy_vault: Parameter[float] - """Radial thickness of the vault support region [m].""" + # # strand params + # d_strand_sc: Parameter[float] + # """Diameter of superconducting strand""" + # d_strand: Parameter[float] + # """Diameter of stabilising strand""" + # operating_temperature: Parameter[float] + # """Operating temperature for the strands [K]""" + + # # cable params + # n_sc_strand: Parameter[int] + # """Number of superconducting strands.""" + # n_stab_strand: Parameter[int] + # """Number of stabilizing strands.""" + # d_cooling_channel: Parameter[float] + # """Diameter of the cooling channel [m].""" + # void_fraction: Parameter[float] + # """Ratio of material volume to total volume [unitless].""" + # cos_theta: Parameter[float] + # """Correction factor for twist in the cable layout.""" + # dx: Parameter[float] + # """Cable half-width in the x-direction [m].""" + + # # conductor params + # dx_jacket: Parameter[float] + # """x-thickness of the jacket [m].""" + # dy_jacket: Parameter[float] + # """y-tickness of the jacket [m].""" + # dx_ins: Parameter[float] + # """x-thickness of the insulator [m].""" + # dy_ins: Parameter[float] + # """y-thickness of the insulator [m].""" + + # # winding pack params + # nx: Parameter[int] + # """Number of conductors along the x-axis.""" + # ny: Parameter[int] + # """Number of conductors along the y-axis.""" + + # # case params + # Ri: Parameter[float] + # """External radius of the TF coil case [m].""" + # Rk: Parameter[float] + # """Internal radius of the TF coil case [m].""" + # theta_TF: Parameter[float] + # """Toroidal angular span of the TF coil [degrees].""" + # dy_ps: Parameter[float] + # """Radial thickness of the poloidal support region [m].""" + # dy_vault: Parameter[float] + # """Radial thickness of the vault support region [m].""" Iop: Parameter[float] """Operational current in conductor""" @@ -105,27 +107,27 @@ class TFCoilXYDesignerParams(ParameterFrame): t_delay: Parameter[float] """Time delay for exponential functions""" - # optimisation params - t0: Parameter[float] - """Initial time""" - Tau_discharge: Parameter[float] - """Characteristic time constant""" - hotspot_target_temperature: Parameter[float] - """Target temperature for hotspot for cable optimisiation""" - layout: Parameter[str] - """Cable layout strategy""" - wp_reduction_factor: Parameter[float] - """Fractional reduction of available toroidal space for WPs""" - n_layers_reduction: Parameter[int] - """Number of layers to remove after each WP""" - bounds_cond_jacket: Parameter[np.ndarray] - """Min/max bounds for conductor jacket area optimization [m²]""" - bounds_dy_vault: Parameter[np.ndarray] - """Min/max bounds for the case vault thickness optimization [m]""" - max_niter: Parameter[int] - """Maximum number of optimization iterations""" - eps: Parameter[float] - """Convergence threshold for the combined optimization loop.""" + # # optimisation params + # t0: Parameter[float] + # """Initial time""" + # Tau_discharge: Parameter[float] + # """Characteristic time constant""" + # hotspot_target_temperature: Parameter[float] + # """Target temperature for hotspot for cable optimisiation""" + # layout: Parameter[str] + # """Cable layout strategy""" + # wp_reduction_factor: Parameter[float] + # """Fractional reduction of available toroidal space for WPs""" + # n_layers_reduction: Parameter[int] + # """Number of layers to remove after each WP""" + # bounds_cond_jacket: Parameter[np.ndarray] + # """Min/max bounds for conductor jacket area optimization [m²]""" + # bounds_dy_vault: Parameter[np.ndarray] + # """Min/max bounds for the case vault thickness optimization [m]""" + # max_niter: Parameter[int] + # """Maximum number of optimization iterations""" + # eps: Parameter[float] + # """Convergence threshold for the combined optimization loop.""" class TFCoilXYDesigner(Designer): @@ -141,14 +143,16 @@ class TFCoilXYDesigner(Designer): and their properties. """ + param_cls: type[TFCoilXYDesignerParams] = TFCoilXYDesignerParams + def __init__( self, - params: dict | ParameterFrame, + params: dict | ParameterFrameLike, build_config: dict, ): - super().__init__(params, build_config) + super().__init__(params=params, build_config=build_config) - def _derived_values(self): + def _derived_values(self, optimsiation_params): # Needed params that are calculated using the base params a = self.params.R0.value / self.params.A.value Ri = self.params.R0.value - a - self.params.d.value # noqa: N806 @@ -181,9 +185,6 @@ def _derived_values(self): ** 2 ) T_op = self.params.T_sc.value + self.params.T_margin.value # noqa: N806 - self.params.operating_temperature.value = ( - T_op # this necessary? Or just remove T_sc and T_margin - ) s_y = 1e9 / self.params.safety_factor.value n_cond = int( np.floor( @@ -209,11 +210,11 @@ def _derived_values(self): ) I_fun = delayed_exp_func( # noqa: N806 self.params.Iop.value, - self.params.Tau_discharge.value, + optimsiation_params["Tau_discharge"], self.params.t_delay.value, ) B_fun = delayed_exp_func( - B_TF_i, self.params.Tau_discharge.value, self.params.t_delay.value + B_TF_i, optimsiation_params["Tau_discharge"], self.params.t_delay.value ) return { "a": a, @@ -239,37 +240,53 @@ def run(self): case: TF case object all parts that make it up. """ - # params that are function of another param - derived_params = self._derived_values() + # configs + stab_strand_config = self.build_config.get("stabilising_strand") + sc_strand_config = self.build_config.get("superconducting_strand") + cable_config = self.build_config.get("cable") + conductor_config = self.build_config.get("conductor") + winding_pack_config = self.build_config.get("winding_pack") + case_config = self.build_config.get("case") + # winding pack sets + n_WPs = self.build_config.get("winding_pack").get("sets") + # params + stab_strand_params = self._check_arrays_match(n_WPs, stab_strand_config.get("params")) + sc_strand_params = self._check_arrays_match(n_WPs, sc_strand_config.get("params")) + cable_params = self._check_arrays_match(n_WPs, cable_config.get("params")) + conductor_params = self._check_arrays_match(n_WPs, conductor_config.get("params")) + winding_pack_params = self._check_arrays_match(n_WPs, winding_pack_config.get("params")) + case_params = case_config.get("params") + optimisation_params = self.build_config.get("optimisation_params") + derived_params = self._derived_values(optimisation_params) - n_WPs = len(self.params.nx.value) - if n_WPs > 1: - self._check_arrays_match() winding_pack = [] - for i_WP in n_WPs: - stab_strand = self._make_stab_strand(i_WP) - sc_strand = self._make_sc_strand(i_WP) - initial_cable = self._make_cable(stab_strand, sc_strand, i_WP) + for i_WP in range(n_WPs): + print(i_WP) + stab_strand = self._make_strand(i_WP, stab_strand_config, stab_strand_params) + sc_strand = self._make_strand(i_WP, sc_strand_config, sc_strand_params) + initial_cable = self._make_cable(stab_strand, sc_strand, i_WP, cable_config, cable_params) # param frame optimisation stuff? optimised_cable = initial_cable.optimise_n_stab_ths( - t0=self.params.t0.value, - tf=self.params.Tau_discharge.value, - T_for_hts=derived_params["T_op"], - hotspot_target_temperature=self.params.hotspot_target_temperature.value, + t0=optimisation_params["t0"], + tf=optimisation_params["Tau_discharge"], + initial_temperature=derived_params["T_op"], + target_temperature=optimisation_params["hotspot_target_temperature"], B_fun=derived_params["B_fun"], I_fun=derived_params["I_fun"], bounds=[1, 10000], ) - conductor = self._make_conductor(optimised_cable, i_WP) - winding_pack += [self._make_winding_pack(conductor, i_WP)] - case = self._make_case(winding_pack) + conductor = self._make_conductor(optimised_cable, i_WP, conductor_config, conductor_params) + print(conductor) + winding_pack += [self._make_winding_pack(conductor, i_WP, winding_pack_config, winding_pack_params)] + print(winding_pack) + case = self._make_case(winding_pack, case_config, case_params) # param frame optimisation stuff? case.rearrange_conductors_in_wp( n_conductors=derived_params["n_cond"], - wp_reduction_factor=self.params.wp_reduction_factor.value, + wp_reduction_factor=optimisation_params["wp_reduction_factor"], min_gap_x=derived_params["min_gap_x"], - n_layers_reduction=self.params.n_layers_reduction.value, - layout=self.params.layout.value, + n_layers_reduction=optimisation_params["n_layers_reduction"], + layout=optimisation_params["layout"], ) # param frame optimisation stuff? case.optimize_jacket_and_vault( @@ -280,41 +297,33 @@ def run(self): magnetic_field=derived_params["B_TF_i"], ), allowable_sigma=derived_params["s_y"], - bounds_cond_jacket=self.params.bounds_cond_jacket.value, - bounds_dy_vault=self.params.bounds_dy_vault.value, - layout=self.params.layout.value, - wp_reduction_factor=self.params.wp_reduction_factor.value, + bounds_cond_jacket=optimisation_params["bounds_cond_jacket"], + bounds_dy_vault=optimisation_params["bounds_dy_vault"], + layout=optimisation_params["layout"], + wp_reduction_factor=optimisation_params["wp_reduction_factor"], min_gap_x=derived_params["min_gap_x"], - n_layers_reduction=self.params.n_layers_reduction.value, - max_niter=self.params.max_niter.value, - eps=self.params.eps.value, + n_layers_reduction=optimisation_params["n_layers_reduction"], + max_niter=optimisation_params["max_niter"], + eps=optimisation_params["eps"], n_conds=derived_params["n_cond"], ) return case - def _check_arrays_match(self): - n = len(self.params.nx.value) - param_list = [ - "d_strand_sc", - "d_strand", - "operating_temperature", - "n_sc_strand", - "n_stab_strand", - "d_cooling_channel", - "void_fraction", - "cos_theta", - "dx", - "dx_jacket", - "dy_jacket", - "dx_ins", - "dy_ins", - "ny", - ] - for param in param_list: - if len(self.params.get(param).value) != n: - self.params.get(param).value = [ - self.params.get(param).value for _ in range(n) - ] + def _check_arrays_match(self, n_WPs, param_list): + if n_WPs > 1: + for param in param_list: + if np.size(param_list[param]) != n_WPs: + param_list[param] = [ + param_list[param] for _ in range(n_WPs) + ] + return param_list + elif n_WPs == 1: + return param_list + else: + raise ValueError( + f"Invalid value {n_WPs} for winding pack 'sets' in config." + "Value should be an integer >= 1." + ) def B_TF_r(self, tf_current, r): """ @@ -337,93 +346,91 @@ def B_TF_r(self, tf_current, r): """ return 1.08 * (MU_0_2PI * self.params.n_TF.value * tf_current / r) - def _make_stab_strand(self, i_WP): - stab_strand_config = self.build_config.get("stabilising_strand") - cls_name = stab_strand_config["class"] - stab_strand_cls = get_class_from_module(cls_name) + def _make_strand(self, i_WP, config, params): + cls_name = config["class"] + stab_strand_cls = get_class_from_module(cls_name, default_module="bluemira.magnets.strand") + material_mix = [] + for m in config.get("materials"): + material_data = m["material"] + if isinstance(material_data, str): + raise TypeError( + "Material data must be a Material instance, not a string - " + "TEMPORARY." + ) + material_obj = material_data + + material_mix.append( + MaterialFraction(material=material_obj, fraction=m["fraction"]) + ) return stab_strand_cls( - materials=stab_strand_config["materials"], - d_strand=self.params.d_strand.value[i_WP], - operating_temperature=self.params.operating_temperature.value[i_WP], - name=stab_strand_config.get("name", cls_name.rsplit("::", 1)[-1]), + materials=material_mix, + d_strand=params["d_strand"][i_WP], + operating_temperature=params["operating_temperature"][i_WP], + name="stab_strand", ) - def _make_sc_strand(self, i_WP): - sc_strand_config = self.build_config.get("superconducting_strand") - cls_name = sc_strand_config["class"] - sc_strand_cls = get_class_from_module(cls_name) - return sc_strand_cls( - materials=sc_strand_config["materials"], - d_strand=self.params.d_strand_sc.value[i_WP], - operating_temperature=self.params.operating_temperature.value[i_WP], - name=sc_strand_config.get("name", cls_name.rsplit("::", 1)[-1]), - ) - def _make_cable(self, stab_strand, sc_strand, i_WP): - cable_config = self.build_config.get("cable") - cls_name = stab_strand_config["class"] - cable_cls = get_class_from_module(cls_name) + def _make_cable(self, stab_strand, sc_strand, i_WP, config, params): + cls_name = config["class"] + cable_cls = get_class_from_module(cls_name, default_module="bluemira.magnets.cable") return cable_cls( sc_strand=sc_strand, stab_strand=stab_strand, - n_sc_strand=self.params.n_sc_strand.value[i_WP], - n_stab_strand=self.params.n_stab_strand.value[i_WP], - d_cooling_channel=self.params.d_cooling_channel.value[i_WP], - void_fraction=self.params.void_fraction.value[i_WP], - cos_theta=self.params.cos_theta.value[i_WP], - name=cable_config.get("name", cls_name.rsplit("::", 1)[-1]), + n_sc_strand=params["n_sc_strand"][i_WP], + n_stab_strand=params["n_stab_strand"][i_WP], + d_cooling_channel=params["d_cooling_channel"][i_WP], + void_fraction=params["void_fraction"][i_WP], + cos_theta=params["cos_theta"][i_WP], + name=config.get("name", cls_name.rsplit("::", 1)[-1]), **( - {"dx": self.params.dx.value[i_WP]} + {"dx": params["dx"][i_WP]} if issubclass(cable_cls, RectangularCable) else {} ), ) - def _make_conductor(self, cable, i_WP): - conductor_config = self.build_config.get("conductor") - cls_name = conductor_config["class"] - conductor_cls = get_class_from_module(cls_name) + def _make_conductor(self, cable, i_WP, config, params): + cls_name = config["class"] + conductor_cls = get_class_from_module(cls_name, default_module="bluemira.magnets.conductor") return conductor_cls( cable=cable, - mat_jacket=conductor_config["jacket_material"], - mat_ins=conductor_config["ins_material"], - dx_jacket=self.params.dx_jacket.value[i_WP], - dx_ins=self.params.dx_ins.value[i_WP], - name=conductor_config.get("name", cls_name.rsplit("::", 1)[-1]), + mat_jacket=config["jacket_material"], + mat_ins=config["ins_material"], + dx_jacket=params["dx_jacket"][i_WP], + dx_ins=params["dx_ins"][i_WP], + name=config.get("name", cls_name.rsplit("::", 1)[-1]), **( {} if issubclass(conductor_cls, SymmetricConductor) else { - "dy_jacket": self.params.dy_jacket.value[i_WP], - "dy_ins": self.params.dy_ins.value[i_WP], + "dy_jacket": params["dy_jacket"][i_WP], + "dy_ins": params["dy_ins"][i_WP], } ), ) - def _make_winding_pack(self, conductor, i_WP): - winding_pack_config = self.build_config.get("winding_pack") - cls_name = winding_pack_config["class"] - winding_pack_cls = get_class_from_module(cls_name) + def _make_winding_pack(self, conductor, i_WP, config, params): + cls_name = config["class"] + winding_pack_cls = get_class_from_module(cls_name, default_module="bluemira.magnets.winding_pack") return winding_pack_cls( conductor=conductor, - nx=self.params.nx.value[i_WP], - ny=self.params.ny.value[i_WP], - name=winding_pack_config.get("name", cls_name.rsplit("::", 1)[-1]), + nx=params["nx"][i_WP], + ny=params["ny"][i_WP], + name="winding_pack", ) - def _make_case(self, WPs): # noqa: N803 - case_config = self.build_config.get("case") - cls_name = case_config["class"] - case_cls = get_class_from_module(cls_name) + def _make_case(self, WPs, config, params): # noqa: N803 + cls_name = config["class"] + case_cls = get_class_from_module(cls_name, default_module="bluemira.magnets.case_tf") return case_cls( - Ri=self.params.Ri.value, - theta_TF=self.params.theta_TF.value, - dy_ps=self.params.dy_ps.value, - dy_vault=self.params.dy_vault.value, - mat_case=case_config["material"], + Ri=params["Ri"], + theta_TF=params["theta_TF"], + dy_ps=params["dy_ps"], + dy_vault=params["dy_vault"], + mat_case=config["material"], WPs=WPs, - name=case_config.get("name", cls_name.rsplit("::", 1)[-1]), + name=config.get("name", cls_name.rsplit("::", 1)[-1]), ) diff --git a/examples/magnets/example_tf_creation.py b/examples/magnets/example_tf_creation.py new file mode 100644 index 0000000000..a390f3b058 --- /dev/null +++ b/examples/magnets/example_tf_creation.py @@ -0,0 +1,159 @@ +# SPDX-FileCopyrightText: 2021-present M. Coleman, J. Cook, F. Franza +# SPDX-FileCopyrightText: 2021-present I.A. Maione, S. McIntosh +# SPDX-FileCopyrightText: 2021-present J. Morris, D. Short +# +# SPDX-License-Identifier: LGPL-2.1-or-later + +""" +Example script demonstrating the design of the TF coil xy cross section. +This involves the design and optimisation of each module: strand, cable, +conductor, winding pack and casing. +""" + +import matplotlib.pyplot as plt +import numpy as np + +from eurofusion_materials.library.magnet_branch_mats import ( + COPPER_100, + COPPER_300, + DUMMY_INSULATOR_MAG, + NB3SN_MAG, + SS316_LN_MAG, +) + +from bluemira.magnets.tfcoil_designer import TFCoilXYDesigner + +config = { + "stabilising_strand": { + "class": "Strand", + "materials": [{"material": COPPER_300, "fraction": 1.0}], + "params": { + "d_strand": 1.0e-3, + "operating_temperature": 5.7, + } + }, + "superconducting_strand": { + "class": "SuperconductingStrand", + "materials": [ + {"material": NB3SN_MAG, "fraction": 0.5}, + {"material": COPPER_100, "fraction": 0.5}, + ], + "params": { + "d_strand": 1.0e-3, + "operating_temperature": 5.7, + } + }, + "cable":{ + "class": "RectangularCable", + "params": { + "n_sc_strand": 321, + "n_stab_strand": 476, + "d_cooling_channel": 0.01, + "void_fraction": 0.7, + "cos_theta": 0.97, + "dx": 0.034648435154495685, + } + }, + "conductor": { + "class": "SymmetricConductor", + "jacket_material": SS316_LN_MAG, + "ins_material": DUMMY_INSULATOR_MAG, + "params": { + "dx_jacket": 0.0030808556812487366, + "dy_jacket": 0.0, + "dx_ins": 0.0030808556812487366, + "dy_ins": 0.0, + } + }, + "winding_pack": { + "class": "WindingPack", + "sets": 2, + "params": { + "nx": [25, 18], + "ny": [6, 1], + } + }, + "case": { + "class": "TrapezoidalCaseTF", + "material": SS316_LN_MAG, + "params": { + "Ri": 3.708571428571428, + "Rk": 0.0, + "theta_TF": 22.5, + "dy_ps": 0.05733333333333333, + "dy_vault": 0.4529579163961617, + } + }, + "optimisation_params": { + "t0": 0, + "Tau_discharge": 20, + "hotspot_target_temperature": 250, + "layout": "auto", + "wp_reduction_factor": 0.75, + "n_layers_reduction": 4, + "bounds_cond_jacket": np.array([1e-5, 0.2]), + "bounds_dy_vault": np.array([0.1, 2]), + "max_niter": 100, + "eps": 1e-6 + } +} + +params = { + # base + "R0": { + "value": 8.6, + "unit": "m" + }, + "B0": { + "value": 4.39, + "unit": "T" + }, + "A": { + "value": 2.8, + "unit": "dimensionless" + }, + "n_TF": { + "value": 16, + "unit": "dimensionless" + }, + "ripple": { + "value": 6e-3, + "unit": "dimensionless" + }, + "d": { + "value": 1.82, + "unit": "m" + }, + "S_VV": { + "value": 100e6, + "unit": "dimensionless" + }, + "safety_factor": { + "value": 1.5*1.3, + "unit": "dimensionless" + }, + "B_ref": { + "value": 15, + "unit": "T" + }, + # misc params + "Iop": { + "value": 70.0e3, + "unit": "A" + }, + "T_sc": { + "value": 4.2, + "unit": "K" + }, + "T_margin": { + "value": 1.5, + "unit": "K" + }, + "t_delay": { + "value": 3, + "unit": "s" + }, +} +tf_coil_xy = TFCoilXYDesigner(params=params, build_config=config).execute() +tf_coil_xy.plot(show=True, homogenized=False) +# tf_coil_xy.plot_convergence() From 606ac5e73e79be53bd13e6cd059db257e78a88f2 Mon Sep 17 00:00:00 2001 From: Jonathan Matthews Date: Wed, 3 Sep 2025 11:14:04 +0100 Subject: [PATCH 29/61] small fixes to resolve errors when running example --- bluemira/magnets/case_tf.py | 31 ++++--- bluemira/magnets/strand.py | 2 +- bluemira/magnets/tfcoil_designer.py | 125 ++++++++++++++++------------ 3 files changed, 91 insertions(+), 67 deletions(-) diff --git a/bluemira/magnets/case_tf.py b/bluemira/magnets/case_tf.py index c5e5a05b72..91f6580335 100644 --- a/bluemira/magnets/case_tf.py +++ b/bluemira/magnets/case_tf.py @@ -291,17 +291,17 @@ def __init__( name: String identifier for the TF coil case instance (default is "BaseCaseTF"). """ - self.Ri=Ri, - self.theta_TF=theta_TF, - self.mat_case=mat_case, - self.WPs=WPs, - self.name=name, + self.Ri = Ri + self.theta_TF = theta_TF self.dy_ps = dy_ps self.dy_vault = dy_vault + self.mat_case = mat_case + self.WPs = WPs + self.name = name # Toroidal half-length of the coil case at its maximum radial position [m] - self.dx_i = _dx_at_radius(self.Ri, self.rad_theta_TF) + self.dx_i = _dx_at_radius(self.Ri, self.rad_theta) # Average toroidal length of the ps plate - self.dx_ps = (self.Ri + (self.Ri - self.dy_ps)) * np.tan(self.rad_theta_TF / 2) + self.dx_ps = (self.Ri + (self.Ri - self.dy_ps)) * np.tan(self.rad_theta / 2) # sets Rk self.update_dy_vault(self.dy_vault) @@ -336,6 +336,13 @@ def name(self, value: str): raise TypeError("name must be a string.") self._name = value + @property + def rad_theta(self) -> float: + """ + Compute the Toroidal angular span of the TF coil in radians + """ + return np.radians(self.theta_TF) + def update_dy_vault(self, value: float): """ Update the value of the vault support region thickness @@ -879,7 +886,7 @@ def dx_vault(self): : Average length of the vault in the toroidal direction [m]. """ - return (self.R_wp_k[-1] + self.Rk) * np.tan(self.rad_theta_TF / 2) + return (self.R_wp_k[-1] + self.Rk) * np.tan(self.rad_theta / 2) def Kx_vault(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ @@ -921,7 +928,7 @@ def Kx(self, op_cond: OperationalConditions) -> float: # noqa: N802 # toroidal stiffness of the poloidal support region kx_ps = self.mat_case.youngs_modulus(op_cond) / self.dx_ps * self.dy_ps dx_lat = np.array([ - (self.R_wp_i[i] + self.R_wp_k[i]) / 2 * np.tan(self.rad_theta_TF / 2) - w.dx + (self.R_wp_i[i] + self.R_wp_k[i]) / 2 * np.tan(self.rad_theta / 2) - w.dx for i, w in enumerate(self.WPs) ]) dy_lat = np.array([2 * w.dy for w in self.WPs]) @@ -960,7 +967,7 @@ def Ky(self, op_cond: OperationalConditions) -> float: # noqa: N802 # toroidal stiffness of the poloidal support region ky_ps = self.mat_case.youngs_modulus(op_cond) * self.dx_ps / self.dy_ps dx_lat = np.array([ - (self.R_wp_i[i] + self.R_wp_k[i]) / 2 * np.tan(self.rad_theta_TF / 2) - w.dx + (self.R_wp_i[i] + self.R_wp_k[i]) / 2 * np.tan(self.rad_theta / 2) - w.dx for i, w in enumerate(self.WPs) ]) dy_lat = np.array([2 * w.dy for w in self.WPs]) @@ -1056,10 +1063,10 @@ def rearrange_conductors_in_wp( else: dx_WP = n_layers_max * conductor.dx # noqa: N806 - gap_0 = R_wp_i * np.tan(self.rad_theta_TF / 2) - dx_WP / 2 + gap_0 = R_wp_i * np.tan(self.rad_theta / 2) - dx_WP gap_1 = min_gap_x - max_dy = (gap_0 - gap_1) / np.tan(self.rad_theta_TF / 2) + max_dy = (gap_0 - gap_1) / np.tan(self.rad_theta / 2) n_turns_max = min( int(np.floor(max_dy / conductor.dy)), int(np.ceil(remaining_conductors / n_layers_max)), diff --git a/bluemira/magnets/strand.py b/bluemira/magnets/strand.py index 6318db0f46..8a73b10725 100644 --- a/bluemira/magnets/strand.py +++ b/bluemira/magnets/strand.py @@ -63,7 +63,7 @@ def __init__( self.materials = materials self.name = name - + self._shape = None # Create homogenised material self._homogenised_material = mixture( name=name, diff --git a/bluemira/magnets/tfcoil_designer.py b/bluemira/magnets/tfcoil_designer.py index fdc3308755..48c3caab46 100644 --- a/bluemira/magnets/tfcoil_designer.py +++ b/bluemira/magnets/tfcoil_designer.py @@ -6,7 +6,6 @@ """Designer for TF Coil XY cross section.""" from dataclasses import dataclass -from typing import TYPE_CHECKING import matplotlib.pyplot as plt import numpy as np @@ -16,12 +15,13 @@ from bluemira.base.constants import MU_0, MU_0_2PI, MU_0_4PI from bluemira.base.designer import Designer from bluemira.base.parameter_frame import Parameter, ParameterFrame +from bluemira.base.parameter_frame.typed import ParameterFrameLike from bluemira.magnets.cable import RectangularCable from bluemira.magnets.conductor import SymmetricConductor from bluemira.magnets.utils import delayed_exp_func from bluemira.utilities.tools import get_class_from_module -from bluemira.base.parameter_frame.typed import ParameterFrameLike + @dataclass class TFCoilXYDesignerParams(ParameterFrame): """ @@ -152,7 +152,7 @@ def __init__( ): super().__init__(params=params, build_config=build_config) - def _derived_values(self, optimsiation_params): + def _derived_values(self, optimsiation_params, case_params): # Needed params that are calculated using the base params a = self.params.R0.value / self.params.A.value Ri = self.params.R0.value - a - self.params.d.value # noqa: N806 @@ -197,17 +197,8 @@ def _derived_values(self, optimsiation_params): / self.params.Iop.value ) ) - min_gap_x = int( - np.floor( - ( - self.params.B0.value - * self.params.R0.value - / MU_0_2PI - / self.params.n_TF.value - ) - / self.params.Iop.value - ) - ) + min_gap_x = 2 * case_params["dy_ps"] # 2 * thickness of the plate before the WP + I_fun = delayed_exp_func( # noqa: N806 self.params.Iop.value, optimsiation_params["Tau_discharge"], @@ -220,7 +211,7 @@ def _derived_values(self, optimsiation_params): "a": a, "Ri": Ri, "Re": Re, - "B_TF_I": B_TF_i, + "B_TF_i": B_TF_i, "pm": pm, "t_z": t_z, "T_op": T_op, @@ -250,35 +241,55 @@ def run(self): # winding pack sets n_WPs = self.build_config.get("winding_pack").get("sets") # params - stab_strand_params = self._check_arrays_match(n_WPs, stab_strand_config.get("params")) - sc_strand_params = self._check_arrays_match(n_WPs, sc_strand_config.get("params")) + stab_strand_params = self._check_arrays_match( + n_WPs, stab_strand_config.get("params") + ) + sc_strand_params = self._check_arrays_match( + n_WPs, sc_strand_config.get("params") + ) cable_params = self._check_arrays_match(n_WPs, cable_config.get("params")) - conductor_params = self._check_arrays_match(n_WPs, conductor_config.get("params")) - winding_pack_params = self._check_arrays_match(n_WPs, winding_pack_config.get("params")) + conductor_params = self._check_arrays_match( + n_WPs, conductor_config.get("params") + ) + winding_pack_params = self._check_arrays_match( + n_WPs, winding_pack_config.get("params") + ) case_params = case_config.get("params") optimisation_params = self.build_config.get("optimisation_params") - derived_params = self._derived_values(optimisation_params) + derived_params = self._derived_values(optimisation_params, case_params) winding_pack = [] for i_WP in range(n_WPs): - print(i_WP) - stab_strand = self._make_strand(i_WP, stab_strand_config, stab_strand_params) - sc_strand = self._make_strand(i_WP, sc_strand_config, sc_strand_params) - initial_cable = self._make_cable(stab_strand, sc_strand, i_WP, cable_config, cable_params) - # param frame optimisation stuff? - optimised_cable = initial_cable.optimise_n_stab_ths( - t0=optimisation_params["t0"], - tf=optimisation_params["Tau_discharge"], - initial_temperature=derived_params["T_op"], - target_temperature=optimisation_params["hotspot_target_temperature"], - B_fun=derived_params["B_fun"], - I_fun=derived_params["I_fun"], - bounds=[1, 10000], - ) - conductor = self._make_conductor(optimised_cable, i_WP, conductor_config, conductor_params) - print(conductor) - winding_pack += [self._make_winding_pack(conductor, i_WP, winding_pack_config, winding_pack_params)] - print(winding_pack) + if i_WP == 0: + # current functionality requires conductors are the same for both WPs + # in future allow for different conductor objects so can vary cable and strands + # between the sets of the winding pack? + stab_strand = self._make_strand( + i_WP, stab_strand_config, stab_strand_params + ) + sc_strand = self._make_strand(i_WP, sc_strand_config, sc_strand_params) + cable = self._make_cable( + stab_strand, sc_strand, i_WP, cable_config, cable_params + ) + # param frame optimisation stuff? + result = cable.optimise_n_stab_ths( + t0=optimisation_params["t0"], + tf=optimisation_params["Tau_discharge"], + initial_temperature=derived_params["T_op"], + target_temperature=optimisation_params["hotspot_target_temperature"], + B_fun=derived_params["B_fun"], + I_fun=derived_params["I_fun"], + bounds=[1, 10000], + ) + conductor = self._make_conductor( + cable, i_WP, conductor_config, conductor_params + ) + winding_pack += [ + self._make_winding_pack( + conductor, i_WP, winding_pack_config, winding_pack_params + ) + ] + case = self._make_case(winding_pack, case_config, case_params) # param frame optimisation stuff? case.rearrange_conductors_in_wp( @@ -311,19 +322,16 @@ def run(self): def _check_arrays_match(self, n_WPs, param_list): if n_WPs > 1: - for param in param_list: + for param in param_list: if np.size(param_list[param]) != n_WPs: - param_list[param] = [ - param_list[param] for _ in range(n_WPs) - ] + param_list[param] = [param_list[param] for _ in range(n_WPs)] return param_list - elif n_WPs == 1: + if n_WPs == 1: return param_list - else: - raise ValueError( - f"Invalid value {n_WPs} for winding pack 'sets' in config." - "Value should be an integer >= 1." - ) + raise ValueError( + f"Invalid value {n_WPs} for winding pack 'sets' in config." + "Value should be an integer >= 1." + ) def B_TF_r(self, tf_current, r): """ @@ -348,7 +356,9 @@ def B_TF_r(self, tf_current, r): def _make_strand(self, i_WP, config, params): cls_name = config["class"] - stab_strand_cls = get_class_from_module(cls_name, default_module="bluemira.magnets.strand") + stab_strand_cls = get_class_from_module( + cls_name, default_module="bluemira.magnets.strand" + ) material_mix = [] for m in config.get("materials"): material_data = m["material"] @@ -369,10 +379,11 @@ def _make_strand(self, i_WP, config, params): name="stab_strand", ) - def _make_cable(self, stab_strand, sc_strand, i_WP, config, params): cls_name = config["class"] - cable_cls = get_class_from_module(cls_name, default_module="bluemira.magnets.cable") + cable_cls = get_class_from_module( + cls_name, default_module="bluemira.magnets.cable" + ) return cable_cls( sc_strand=sc_strand, stab_strand=stab_strand, @@ -391,7 +402,9 @@ def _make_cable(self, stab_strand, sc_strand, i_WP, config, params): def _make_conductor(self, cable, i_WP, config, params): cls_name = config["class"] - conductor_cls = get_class_from_module(cls_name, default_module="bluemira.magnets.conductor") + conductor_cls = get_class_from_module( + cls_name, default_module="bluemira.magnets.conductor" + ) return conductor_cls( cable=cable, mat_jacket=config["jacket_material"], @@ -411,7 +424,9 @@ def _make_conductor(self, cable, i_WP, config, params): def _make_winding_pack(self, conductor, i_WP, config, params): cls_name = config["class"] - winding_pack_cls = get_class_from_module(cls_name, default_module="bluemira.magnets.winding_pack") + winding_pack_cls = get_class_from_module( + cls_name, default_module="bluemira.magnets.winding_pack" + ) return winding_pack_cls( conductor=conductor, nx=params["nx"][i_WP], @@ -421,7 +436,9 @@ def _make_winding_pack(self, conductor, i_WP, config, params): def _make_case(self, WPs, config, params): # noqa: N803 cls_name = config["class"] - case_cls = get_class_from_module(cls_name, default_module="bluemira.magnets.case_tf") + case_cls = get_class_from_module( + cls_name, default_module="bluemira.magnets.case_tf" + ) return case_cls( Ri=params["Ri"], From abdc3d205b48b218d51ecb8b324baf45c63ad5a6 Mon Sep 17 00:00:00 2001 From: Jonathan Matthews Date: Wed, 3 Sep 2025 15:50:43 +0100 Subject: [PATCH 30/61] final fixes for simple example to work --- bluemira/magnets/cable.py | 9 ++- bluemira/magnets/case_tf.py | 94 +++++++++++++------------ bluemira/magnets/tfcoil_designer.py | 4 +- examples/magnets/example_tf_creation.py | 90 +++++++---------------- 4 files changed, 83 insertions(+), 114 deletions(-) diff --git a/bluemira/magnets/cable.py b/bluemira/magnets/cable.py index 60748aa41c..4bc8f66b79 100644 --- a/bluemira/magnets/cable.py +++ b/bluemira/magnets/cable.py @@ -110,7 +110,7 @@ def __init__( self.E = ( youngs_modulus if callable(youngs_modulus) - else lambda self, op_cond, v=youngs_modulus: youngs_modulus + else lambda op_cond, v=youngs_modulus: youngs_modulus ) for k, v in props.items(): @@ -246,7 +246,12 @@ def E(self, op_cond: OperationalConditions) -> float: # noqa: N802 : Default Young's modulus (0). """ - raise NotImplementedError("E for Cable is not implemented.") + try: + # if fixed E value was not input + return self._E(op_cond) + except TypeError: + # if fixed E value was input + return self._E def _heat_balance_model_cable( self, diff --git a/bluemira/magnets/case_tf.py b/bluemira/magnets/case_tf.py index 91f6580335..46534f826b 100644 --- a/bluemira/magnets/case_tf.py +++ b/bluemira/magnets/case_tf.py @@ -111,7 +111,7 @@ def rad_theta(self) -> float: """ Compute the Toroidal angular span of the TF coil in radians """ - return np.radians(self.variables.theta_TF.value) + return np.radians(self.variables.theta_TF) @property def area(self) -> float: @@ -127,9 +127,9 @@ def area(self) -> float: Cross-sectional area [m²]. """ return ( - 2 * _dx_at_radius(self.variables.Ri.value, self.rad_theta) - + 2 * _dx_at_radius(self.variables.Rk.value, self.rad_theta) - ) * (self.variables.Ri.value - self.variables.Rk.value) + 2 * _dx_at_radius(self.variables.Ri, self.rad_theta) + + 2 * _dx_at_radius(self.variables.Rk, self.rad_theta) + ) * (self.variables.Ri - self.variables.Rk) def create_shape(self, label: str = "") -> BluemiraWire: """ @@ -142,16 +142,17 @@ def create_shape(self, label: str = "") -> BluemiraWire: Coordinates are ordered counterclockwise starting from the top-left corner: [(-dx_outer, Ri), (dx_outer, Ri), (dx_inner, Rk), (-dx_inner, Rk)]. """ - dx_outer = 2 * _dx_at_radius(self.variables.Ri.value, self.rad_theta) - dx_inner = 2 * _dx_at_radius(self.variables.Rk.value, self.rad_theta) + dx_outer = 2 * _dx_at_radius(self.variables.Ri, self.rad_theta) + dx_inner = 2 * _dx_at_radius(self.variables.Rk, self.rad_theta) return make_polygon( [ - [-dx_outer, self.variables.Ri.value], - [dx_outer, self.variables.Ri.value], - [dx_inner, self.variables.Rk.value], - [-dx_inner, self.variables.Rk.value], + [-dx_outer, 0.0, self.variables.Ri], + [dx_outer, 0.0, self.variables.Ri], + [dx_inner, 0.0, self.variables.Rk], + [-dx_inner, 0.0, self.variables.Rk], ], + closed=True, label=label, ) @@ -201,7 +202,7 @@ def rad_theta(self) -> float: """ Compute the Toroidal angular span of the TF coil in radians """ - return np.radians(self.variables.theta_TF.value) + return np.radians(self.variables.theta_TF) def area(self) -> float: """ @@ -213,11 +214,7 @@ def area(self) -> float: Cross-sectional area [m²] defined by the wedge between outer radius Ri and inner radius Rk over the toroidal angle theta_TF. """ - return ( - 0.5 - * self.rad_theta - * (self.variables.Ri.value**2 - self.variables.Rk.value**2) - ) + return 0.5 * self.rad_theta * (self.variables.Ri**2 - self.variables.Rk**2) def create_shape(self, label: str = "", n_points: int = 50) -> BluemiraWire: """ @@ -243,18 +240,18 @@ def create_shape(self, label: str = "", n_points: int = 50) -> BluemiraWire: angles_inner = np.linspace(theta2, theta1, n_points) arc_outer = np.column_stack(( - self.variables.Ri.value * np.sin(angles_outer), - self.variables.Ri.value * np.cos(angles_outer), + self.variables.Ri * np.sin(angles_outer), + self.variables.Ri * np.cos(angles_outer), )) arc_inner = np.column_stack(( - self.variables.Rk.value * np.sin(angles_inner), - self.variables.Rk.value * np.cos(angles_inner), + self.variables.Rk * np.sin(angles_inner), + self.variables.Rk * np.cos(angles_inner), )) return make_polygon(np.vstack((arc_outer, arc_inner)), label=label) -class BaseCaseTF(ABC): +class CaseTF(ABC): """ Abstract Base Class for Toroidal Field Coil Case configurations. @@ -269,6 +266,7 @@ def __init__( dy_vault: float, mat_case: Material, WPs: list[WindingPack], # noqa: N803 + geometry: GeometryParameterisation, name: str = "BaseCaseTF", ): """ @@ -291,17 +289,20 @@ def __init__( name: String identifier for the TF coil case instance (default is "BaseCaseTF"). """ - self.Ri = Ri - self.theta_TF = theta_TF + self.geometry = geometry + self.geometry.variables.Ri = Ri + self.geometry.variables.theta_TF = theta_TF self.dy_ps = dy_ps self.dy_vault = dy_vault self.mat_case = mat_case self.WPs = WPs self.name = name # Toroidal half-length of the coil case at its maximum radial position [m] - self.dx_i = _dx_at_radius(self.Ri, self.rad_theta) + self.dx_i = _dx_at_radius(self.geometry.variables.Ri, self.rad_theta) # Average toroidal length of the ps plate - self.dx_ps = (self.Ri + (self.Ri - self.dy_ps)) * np.tan(self.rad_theta / 2) + self.dx_ps = ( + self.geometry.variables.Ri + (self.geometry.variables.Ri - self.dy_ps) + ) * np.tan(self.rad_theta / 2) # sets Rk self.update_dy_vault(self.dy_vault) @@ -341,7 +342,7 @@ def rad_theta(self) -> float: """ Compute the Toroidal angular span of the TF coil in radians """ - return np.radians(self.theta_TF) + return np.radians(self.geometry.variables.theta_TF) def update_dy_vault(self, value: float): """ @@ -353,7 +354,7 @@ def update_dy_vault(self, value: float): Vault thickness [m]. """ self.dy_vault = value - self.Rk = self.R_wp_k[-1] - self.dy_vault + self.geometry.variables.Rk = self.R_wp_k[-1] - self.dy_vault # jm - not used but replaces functionality of original Rk setter # can't find when (if) it was used originally @@ -361,8 +362,8 @@ def update_Rk(self, value: float): # noqa: N802 """ Set or update the internal (innermost) radius of the TF case. """ - self.Rk = value - self.dy_vault = self.R_wp_k[-1] - self.Rk + self.geometry.variables.Rk = value + self.dy_vault = self.R_wp_k[-1] - self.geometry.variables.Rk @property @abstractmethod @@ -483,7 +484,7 @@ def R_wp_i(self) -> np.ndarray: # noqa: N802 Array of radial positions [m] corresponding to the outer edge of each WP. """ dy_wp_cumsum = np.cumsum(np.concatenate(([0.0], self.dy_wp_i))) - result_initial = self.Ri - self.dy_ps + result_initial = self.geometry.variables.Ri - self.dy_ps if len(dy_wp_cumsum) == 1: result = np.array([result_initial]) else: @@ -539,8 +540,7 @@ def plot( _, ax = plt.subplots() ax.set_aspect("equal", adjustable="box") - # Plot external case boundary (delegate) - super().plot(ax=ax, show=False) + self.geometry.plot(ax=ax) # Plot winding packs for i, wp in enumerate(self.WPs): @@ -567,7 +567,7 @@ def area_case_jacket(self) -> float: ------- Case jacket area [m²], computed as total area minus total WP area. """ - return self.area - self.area_wps + return self.geometry.area - self.area_wps @property def area_wps(self) -> float: @@ -751,17 +751,17 @@ def to_dict(self) -> dict[str, float | str | list[dict[str, float | str | Any]]] """ return { "name": self.name, - "Ri": self.Ri, + "Ri": self.geometry.variables.Ri, "dy_ps": self.dy_ps, "dy_vault": self.dy_vault, - "theta_TF": self.theta_TF, + "theta_TF": self.geometry.variables.theta_TF, "mat_case": self.mat_case.name, # Assume Material has 'name' attribute "WPs": [wp.to_dict() for wp in self.WPs], # Assume each WindingPack implements to_dict() } @classmethod - def from_dict(cls, case_dict: dict, name: str | None = None) -> BaseCaseTF: + def from_dict(cls, case_dict: dict, name: str | None = None) -> CaseTF: """ Deserialize a BaseCaseTF instance from a dictionary. @@ -805,17 +805,17 @@ def __str__(self) -> str: """ return ( f"CaseTF '{self.name}'\n" - f" - Ri: {self.Ri:.3f} m\n" - f" - Rk: {self.Rk:.3f} m\n" + f" - Ri: {self.geometry.variables.Ri:.3f} m\n" + f" - Rk: {self.geometry.variables.Rk:.3f} m\n" f" - dy_ps: {self.dy_ps:.3f} m\n" f" - dy_vault: {self.dy_vault:.3f} m\n" - f" - theta_TF: {self.theta_TF:.2f}°\n" + f" - theta_TF: {self.geometry.variables.theta_TF:.2f}°\n" f" - Material: {self.mat_case.name}\n" f" - Winding Packs: {len(self.WPs)} packs\n" ) -class TrapezoidalCaseTF(BaseCaseTF, TrapezoidalGeometry): +class TrapezoidalCaseTF(CaseTF): """ Toroidal Field Coil Case with Trapezoidal Geometry. Note: this class considers a set of Winding Pack with the same conductor (instance). @@ -833,6 +833,7 @@ def __init__( ): self._check_WPs(WPs) + geom = TrapezoidalGeometry() super().__init__( Ri=Ri, theta_TF=theta_TF, @@ -841,6 +842,7 @@ def __init__( mat_case=mat_case, WPs=WPs, name=name, + geometry=geom, ) def _check_WPs( # noqa: PLR6301, N802 @@ -886,7 +888,9 @@ def dx_vault(self): : Average length of the vault in the toroidal direction [m]. """ - return (self.R_wp_k[-1] + self.Rk) * np.tan(self.rad_theta / 2) + return (self.R_wp_k[-1] + self.geometry.variables.Rk) * np.tan( + self.rad_theta / 2 + ) def Kx_vault(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ @@ -1159,7 +1163,7 @@ def _tresca_stress( # The maximum principal stress acting on the case nose is the compressive # hoop stress generated in the equivalent shell from the magnetic pressure. From # the Shell theory, for an isotropic continuous shell with a thickness ratio: - beta = self.Rk / (self.Rk + self.dy_vault) + beta = self.geometry.variables.Rk / (self.geometry.variables.Rk + self.dy_vault) # the maximum hoop stress, corrected to account for the presence of the WP, is # placed at the innermost radius of the case as: sigma_theta = ( @@ -1364,7 +1368,7 @@ def optimize_jacket_and_vault( err_conductor_area_jacket, err_dy_vault, self.dy_wp_tot, - self.Ri - self.Rk, + self.geometry.variables.Ri - self.geometry.variables.Rk, ]) damping_factor = 0.3 @@ -1443,7 +1447,7 @@ def optimize_jacket_and_vault( err_conductor_area_jacket, err_dy_vault, self.dy_wp_tot, - self.Ri - self.Rk, + self.geometry.variables.Ri - self.geometry.variables.Rk, ]) # final check @@ -1514,7 +1518,7 @@ def plot_convergence(self): def create_case_tf_from_dict( case_dict: dict, name: str | None = None, -) -> BaseCaseTF: +) -> CaseTF: """ Factory function to create a CaseTF (or subclass) from a serialized dictionary. diff --git a/bluemira/magnets/tfcoil_designer.py b/bluemira/magnets/tfcoil_designer.py index 48c3caab46..a1dfc876fa 100644 --- a/bluemira/magnets/tfcoil_designer.py +++ b/bluemira/magnets/tfcoil_designer.py @@ -394,9 +394,9 @@ def _make_cable(self, stab_strand, sc_strand, i_WP, config, params): cos_theta=params["cos_theta"][i_WP], name=config.get("name", cls_name.rsplit("::", 1)[-1]), **( - {"dx": params["dx"][i_WP]} + {"dx": params["dx"][i_WP], "E": params["E"][i_WP]} if issubclass(cable_cls, RectangularCable) - else {} + else {"E": params["E"][i_WP]} ), ) diff --git a/examples/magnets/example_tf_creation.py b/examples/magnets/example_tf_creation.py index a390f3b058..b12f59af6a 100644 --- a/examples/magnets/example_tf_creation.py +++ b/examples/magnets/example_tf_creation.py @@ -10,9 +10,7 @@ conductor, winding pack and casing. """ -import matplotlib.pyplot as plt import numpy as np - from eurofusion_materials.library.magnet_branch_mats import ( COPPER_100, COPPER_300, @@ -30,20 +28,20 @@ "params": { "d_strand": 1.0e-3, "operating_temperature": 5.7, - } + }, }, "superconducting_strand": { "class": "SuperconductingStrand", "materials": [ - {"material": NB3SN_MAG, "fraction": 0.5}, - {"material": COPPER_100, "fraction": 0.5}, + {"material": NB3SN_MAG, "fraction": 0.5}, + {"material": COPPER_100, "fraction": 0.5}, ], "params": { "d_strand": 1.0e-3, "operating_temperature": 5.7, - } + }, }, - "cable":{ + "cable": { "class": "RectangularCable", "params": { "n_sc_strand": 321, @@ -52,7 +50,8 @@ "void_fraction": 0.7, "cos_theta": 0.97, "dx": 0.034648435154495685, - } + "E": 0.1e9, + }, }, "conductor": { "class": "SymmetricConductor", @@ -63,7 +62,7 @@ "dy_jacket": 0.0, "dx_ins": 0.0030808556812487366, "dy_ins": 0.0, - } + }, }, "winding_pack": { "class": "WindingPack", @@ -71,7 +70,7 @@ "params": { "nx": [25, 18], "ny": [6, 1], - } + }, }, "case": { "class": "TrapezoidalCaseTF", @@ -82,7 +81,7 @@ "theta_TF": 22.5, "dy_ps": 0.05733333333333333, "dy_vault": 0.4529579163961617, - } + }, }, "optimisation_params": { "t0": 0, @@ -94,65 +93,26 @@ "bounds_cond_jacket": np.array([1e-5, 0.2]), "bounds_dy_vault": np.array([0.1, 2]), "max_niter": 100, - "eps": 1e-6 - } + "eps": 1e-6, + }, } params = { # base - "R0": { - "value": 8.6, - "unit": "m" - }, - "B0": { - "value": 4.39, - "unit": "T" - }, - "A": { - "value": 2.8, - "unit": "dimensionless" - }, - "n_TF": { - "value": 16, - "unit": "dimensionless" - }, - "ripple": { - "value": 6e-3, - "unit": "dimensionless" - }, - "d": { - "value": 1.82, - "unit": "m" - }, - "S_VV": { - "value": 100e6, - "unit": "dimensionless" - }, - "safety_factor": { - "value": 1.5*1.3, - "unit": "dimensionless" - }, - "B_ref": { - "value": 15, - "unit": "T" - }, + "R0": {"value": 8.6, "unit": "m"}, + "B0": {"value": 4.39, "unit": "T"}, + "A": {"value": 2.8, "unit": "dimensionless"}, + "n_TF": {"value": 16, "unit": "dimensionless"}, + "ripple": {"value": 6e-3, "unit": "dimensionless"}, + "d": {"value": 1.82, "unit": "m"}, + "S_VV": {"value": 100e6, "unit": "dimensionless"}, + "safety_factor": {"value": 1.5 * 1.3, "unit": "dimensionless"}, + "B_ref": {"value": 15, "unit": "T"}, # misc params - "Iop": { - "value": 70.0e3, - "unit": "A" - }, - "T_sc": { - "value": 4.2, - "unit": "K" - }, - "T_margin": { - "value": 1.5, - "unit": "K" - }, - "t_delay": { - "value": 3, - "unit": "s" - }, + "Iop": {"value": 70.0e3, "unit": "A"}, + "T_sc": {"value": 4.2, "unit": "K"}, + "T_margin": {"value": 1.5, "unit": "K"}, + "t_delay": {"value": 3, "unit": "s"}, } tf_coil_xy = TFCoilXYDesigner(params=params, build_config=config).execute() tf_coil_xy.plot(show=True, homogenized=False) From e9849f9c0741eb5302a5c5dc33d4001ddb3c0515 Mon Sep 17 00:00:00 2001 From: Jonathan Matthews Date: Thu, 4 Sep 2025 15:54:24 +0100 Subject: [PATCH 31/61] fix to mistake in how winding packs are handled within case geometry --- bluemira/magnets/case_tf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bluemira/magnets/case_tf.py b/bluemira/magnets/case_tf.py index 46534f826b..3de5139d07 100644 --- a/bluemira/magnets/case_tf.py +++ b/bluemira/magnets/case_tf.py @@ -545,7 +545,7 @@ def plot( # Plot winding packs for i, wp in enumerate(self.WPs): xc_wp = 0.0 - yc_wp = self.R_wp_i[i] - wp.dy / 2 + yc_wp = self.R_wp_i[i] - wp.dy ax = wp.plot(xc=xc_wp, yc=yc_wp, ax=ax, homogenized=homogenized) # Finalize plot From 7aae41caf61f43216a8b2f4a5aaa4ae52e8d8120 Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Wed, 10 Sep 2025 10:00:51 +0100 Subject: [PATCH 32/61] =?UTF-8?q?=F0=9F=94=A5=20Remove=20youngs=20modulus?= =?UTF-8?q?=20extra=20func?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bluemira/magnets/cable.py | 93 +++++++-------------------------------- 1 file changed, 15 insertions(+), 78 deletions(-) diff --git a/bluemira/magnets/cable.py b/bluemira/magnets/cable.py index 4bc8f66b79..118ad68a1c 100644 --- a/bluemira/magnets/cable.py +++ b/bluemira/magnets/cable.py @@ -88,6 +88,11 @@ def __init__( Correction factor for twist in the cable layout. name: Identifier for the cable instance. + + Raises + ------ + ValueError + If E not defined on the class and not passed in as a kwarg """ # assign # Setting self.name triggers automatic instance registration @@ -103,15 +108,16 @@ def __init__( youngs_modulus: Callable[[Any, OperationalConditions], float] | float | None = ( props.pop("E", None) ) - if youngs_modulus is not None: - if "E" in vars(type(self)): - bluemira_debug("E already defined in class, ignoring") - else: - self.E = ( - youngs_modulus - if callable(youngs_modulus) - else lambda op_cond, v=youngs_modulus: youngs_modulus - ) + if "E" not in vars(type(self)): + if youngs_modulus is None: + raise ValueError("E undefined on the class and not passed into the init") + self.E = ( + youngs_modulus + if callable(youngs_modulus) + else lambda op_cond, v=youngs_modulus: youngs_modulus # noqa: ARG005 + ) + elif youngs_modulus is not None: + bluemira_debug("E already defined in class, ignoring") for k, v in props.items(): setattr(self, k, v if callable(v) else lambda *arg, v=v, **kwargs: v) # noqa: ARG005 @@ -228,31 +234,6 @@ def area(self) -> float: self.area_sc + self.area_stab ) / self.void_fraction / self.cos_theta + self.area_cc - def E(self, op_cond: OperationalConditions) -> float: # noqa: N802 - """ - Return the effective Young's modulus of the cable [Pa]. - - This is a default placeholder implementation in the base class. - Subclasses may use `kwargs` to modify behavior. - - Parameters - ---------- - op_cond: - Operational conditions including temperature, magnetic field, and strain - at which to calculate the material property. - - Returns - ------- - : - Default Young's modulus (0). - """ - try: - # if fixed E value was not input - return self._E(op_cond) - except TypeError: - # if fixed E value was input - return self._E - def _heat_balance_model_cable( self, t: float, @@ -325,8 +306,6 @@ def optimise_n_stab_ths( B_fun: Callable[[float], float], I_fun: Callable[[float], float], # noqa: N803 bounds: np.ndarray | None = None, - *, - show: bool = False, ): """ Optimize the number of stabilizer strand in the superconducting cable using a @@ -348,8 +327,6 @@ def optimise_n_stab_ths( Current [A] as a time-dependent function. bounds: Lower and upper limits for the number of stabilizer strands. - show: - If True, the behavior of temperature over time is plotted. Returns ------- @@ -1291,43 +1268,3 @@ def from_dict( name=name or cable_dict.pop("name", None), **cable_dict, ) - - -def create_cable_from_dict( - cable_dict: dict, - name: str | None = None, -) -> ABCCable: - """ - Factory function to create a Cable or its subclass from a serialized dictionary. - - Parameters - ---------- - cable_dict: - Dictionary with serialized cable data. Must include a 'name_in_registry' field. - name: - If given, overrides the name from the dictionary. - - Returns - ------- - : - Instantiated cable object. - - Raises - ------ - ValueError - If 'name_in_registry' is missing or no matching class is found. - """ - name_in_registry = cable_dict.get("name_in_registry") - if name_in_registry is None: - raise ValueError( - "Serialized cable dictionary must contain a 'name_in_registry' field." - ) - - cls = CABLE_REGISTRY.get(name_in_registry) - if cls is None: - raise ValueError( - f"No registered cable class with registration name '{name_in_registry}'. " - "Available classes are: " + ", ".join(CABLE_REGISTRY.keys()) - ) - - return cls.from_dict(name=name, cable_dict=cable_dict) From f66857446a485f200ec85c51bd335f223ccdd93a Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Wed, 10 Sep 2025 14:06:58 +0100 Subject: [PATCH 33/61] =?UTF-8?q?=F0=9F=8E=A8=20More=20half=20width=20chan?= =?UTF-8?q?ges=20and=20ov=20useage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bluemira/magnets/cable.py | 2 +- bluemira/magnets/case_tf.py | 79 +++++++++++---------- bluemira/magnets/conductor.py | 9 ++- bluemira/magnets/tfcoil_designer.py | 4 ++ examples/magnets/example_tf_creation.py | 15 ++-- examples/magnets/example_tf_wp_from_dict.py | 3 +- 6 files changed, 62 insertions(+), 50 deletions(-) diff --git a/bluemira/magnets/cable.py b/bluemira/magnets/cable.py index 118ad68a1c..3fab048b35 100644 --- a/bluemira/magnets/cable.py +++ b/bluemira/magnets/cable.py @@ -951,7 +951,7 @@ def __init__( @property def dx(self) -> float: """Half Cable dimension in the x direction [m]""" - return np.sqrt(self.area / 4) + return np.sqrt(self.area) / 2 @property def dy(self) -> float: diff --git a/bluemira/magnets/case_tf.py b/bluemira/magnets/case_tf.py index 3de5139d07..7d3edc98ff 100644 --- a/bluemira/magnets/case_tf.py +++ b/bluemira/magnets/case_tf.py @@ -60,7 +60,7 @@ def _dx_at_radius(radius: float, rad_theta: float) -> float: Returns ------- : - Toroidal width [m] at the given radius. + Half toroidal width [m] at the given radius. """ return radius * np.tan(rad_theta / 2) @@ -111,7 +111,7 @@ def rad_theta(self) -> float: """ Compute the Toroidal angular span of the TF coil in radians """ - return np.radians(self.variables.theta_TF) + return np.radians(self.variables.theta_TF.value) @property def area(self) -> float: @@ -126,10 +126,10 @@ def area(self) -> float: : Cross-sectional area [m²]. """ - return ( - 2 * _dx_at_radius(self.variables.Ri, self.rad_theta) - + 2 * _dx_at_radius(self.variables.Rk, self.rad_theta) - ) * (self.variables.Ri - self.variables.Rk) + return (self.variables.Ri.value - self.variables.Rk.value) * ( + _dx_at_radius(self.variables.Ri.value, self.rad_theta) + + _dx_at_radius(self.variables.Rk.value, self.rad_theta) + ) def create_shape(self, label: str = "") -> BluemiraWire: """ @@ -142,15 +142,15 @@ def create_shape(self, label: str = "") -> BluemiraWire: Coordinates are ordered counterclockwise starting from the top-left corner: [(-dx_outer, Ri), (dx_outer, Ri), (dx_inner, Rk), (-dx_inner, Rk)]. """ - dx_outer = 2 * _dx_at_radius(self.variables.Ri, self.rad_theta) - dx_inner = 2 * _dx_at_radius(self.variables.Rk, self.rad_theta) + dx_outer = _dx_at_radius(self.variables.Ri.value, self.rad_theta) + dx_inner = _dx_at_radius(self.variables.Rk.value, self.rad_theta) return make_polygon( [ - [-dx_outer, 0.0, self.variables.Ri], - [dx_outer, 0.0, self.variables.Ri], - [dx_inner, 0.0, self.variables.Rk], - [-dx_inner, 0.0, self.variables.Rk], + [-dx_outer, 0.0, self.variables.Ri.value], + [dx_outer, 0.0, self.variables.Ri.value], + [dx_inner, 0.0, self.variables.Rk.value], + [-dx_inner, 0.0, self.variables.Rk.value], ], closed=True, label=label, @@ -202,7 +202,7 @@ def rad_theta(self) -> float: """ Compute the Toroidal angular span of the TF coil in radians """ - return np.radians(self.variables.theta_TF) + return np.radians(self.variables.theta_TF.value) def area(self) -> float: """ @@ -214,7 +214,11 @@ def area(self) -> float: Cross-sectional area [m²] defined by the wedge between outer radius Ri and inner radius Rk over the toroidal angle theta_TF. """ - return 0.5 * self.rad_theta * (self.variables.Ri**2 - self.variables.Rk**2) + return ( + 0.5 + * self.rad_theta + * (self.variables.Ri.value**2 - self.variables.Rk.value**2) + ) def create_shape(self, label: str = "", n_points: int = 50) -> BluemiraWire: """ @@ -240,12 +244,12 @@ def create_shape(self, label: str = "", n_points: int = 50) -> BluemiraWire: angles_inner = np.linspace(theta2, theta1, n_points) arc_outer = np.column_stack(( - self.variables.Ri * np.sin(angles_outer), - self.variables.Ri * np.cos(angles_outer), + self.variables.Ri.value * np.sin(angles_outer), + self.variables.Ri.value * np.cos(angles_outer), )) arc_inner = np.column_stack(( - self.variables.Rk * np.sin(angles_inner), - self.variables.Rk * np.cos(angles_inner), + self.variables.Rk.value * np.sin(angles_inner), + self.variables.Rk.value * np.cos(angles_inner), )) return make_polygon(np.vstack((arc_outer, arc_inner)), label=label) @@ -290,18 +294,19 @@ def __init__( String identifier for the TF coil case instance (default is "BaseCaseTF"). """ self.geometry = geometry - self.geometry.variables.Ri = Ri - self.geometry.variables.theta_TF = theta_TF + self.geometry.variables.Ri.value = Ri + self.geometry.variables.theta_TF.value = theta_TF self.dy_ps = dy_ps self.dy_vault = dy_vault self.mat_case = mat_case self.WPs = WPs self.name = name # Toroidal half-length of the coil case at its maximum radial position [m] - self.dx_i = _dx_at_radius(self.geometry.variables.Ri, self.rad_theta) + self.dx_i = _dx_at_radius(self.geometry.variables.Ri.value, self.rad_theta) # Average toroidal length of the ps plate self.dx_ps = ( - self.geometry.variables.Ri + (self.geometry.variables.Ri - self.dy_ps) + self.geometry.variables.Ri.value + + (self.geometry.variables.Ri.value - self.dy_ps) ) * np.tan(self.rad_theta / 2) # sets Rk self.update_dy_vault(self.dy_vault) @@ -342,7 +347,7 @@ def rad_theta(self) -> float: """ Compute the Toroidal angular span of the TF coil in radians """ - return np.radians(self.geometry.variables.theta_TF) + return np.radians(self.geometry.variables.theta_TF.value) def update_dy_vault(self, value: float): """ @@ -354,7 +359,7 @@ def update_dy_vault(self, value: float): Vault thickness [m]. """ self.dy_vault = value - self.geometry.variables.Rk = self.R_wp_k[-1] - self.dy_vault + self.geometry.variables.Rk.value = self.R_wp_k[-1] - self.dy_vault # jm - not used but replaces functionality of original Rk setter # can't find when (if) it was used originally @@ -362,8 +367,8 @@ def update_Rk(self, value: float): # noqa: N802 """ Set or update the internal (innermost) radius of the TF case. """ - self.geometry.variables.Rk = value - self.dy_vault = self.R_wp_k[-1] - self.geometry.variables.Rk + self.geometry.variables.Rk.value = value + self.dy_vault = self.R_wp_k[-1] - self.geometry.variables.Rk.value @property @abstractmethod @@ -484,7 +489,7 @@ def R_wp_i(self) -> np.ndarray: # noqa: N802 Array of radial positions [m] corresponding to the outer edge of each WP. """ dy_wp_cumsum = np.cumsum(np.concatenate(([0.0], self.dy_wp_i))) - result_initial = self.geometry.variables.Ri - self.dy_ps + result_initial = self.geometry.variables.Ri.value - self.dy_ps if len(dy_wp_cumsum) == 1: result = np.array([result_initial]) else: @@ -751,10 +756,10 @@ def to_dict(self) -> dict[str, float | str | list[dict[str, float | str | Any]]] """ return { "name": self.name, - "Ri": self.geometry.variables.Ri, + "Ri": self.geometry.variables.Ri.value, "dy_ps": self.dy_ps, "dy_vault": self.dy_vault, - "theta_TF": self.geometry.variables.theta_TF, + "theta_TF": self.geometry.variables.theta_TF.value, "mat_case": self.mat_case.name, # Assume Material has 'name' attribute "WPs": [wp.to_dict() for wp in self.WPs], # Assume each WindingPack implements to_dict() @@ -805,11 +810,11 @@ def __str__(self) -> str: """ return ( f"CaseTF '{self.name}'\n" - f" - Ri: {self.geometry.variables.Ri:.3f} m\n" - f" - Rk: {self.geometry.variables.Rk:.3f} m\n" + f" - Ri: {self.geometry.variables.Ri.value:.3f} m\n" + f" - Rk: {self.geometry.variables.Rk.value:.3f} m\n" f" - dy_ps: {self.dy_ps:.3f} m\n" f" - dy_vault: {self.dy_vault:.3f} m\n" - f" - theta_TF: {self.geometry.variables.theta_TF:.2f}°\n" + f" - theta_TF: {self.geometry.variables.theta_TF.value:.2f}°\n" f" - Material: {self.mat_case.name}\n" f" - Winding Packs: {len(self.WPs)} packs\n" ) @@ -888,7 +893,7 @@ def dx_vault(self): : Average length of the vault in the toroidal direction [m]. """ - return (self.R_wp_k[-1] + self.geometry.variables.Rk) * np.tan( + return (self.R_wp_k[-1] + self.geometry.variables.Rk.value) * np.tan( self.rad_theta / 2 ) @@ -1163,7 +1168,9 @@ def _tresca_stress( # The maximum principal stress acting on the case nose is the compressive # hoop stress generated in the equivalent shell from the magnetic pressure. From # the Shell theory, for an isotropic continuous shell with a thickness ratio: - beta = self.geometry.variables.Rk / (self.geometry.variables.Rk + self.dy_vault) + beta = self.geometry.variables.Rk.value / ( + self.geometry.variables.Rk.value + self.dy_vault + ) # the maximum hoop stress, corrected to account for the presence of the WP, is # placed at the innermost radius of the case as: sigma_theta = ( @@ -1368,7 +1375,7 @@ def optimize_jacket_and_vault( err_conductor_area_jacket, err_dy_vault, self.dy_wp_tot, - self.geometry.variables.Ri - self.geometry.variables.Rk, + self.geometry.variables.Ri.value - self.geometry.variables.Rk.value, ]) damping_factor = 0.3 @@ -1447,7 +1454,7 @@ def optimize_jacket_and_vault( err_conductor_area_jacket, err_dy_vault, self.dy_wp_tot, - self.geometry.variables.Ri - self.geometry.variables.Rk, + self.geometry.variables.Ri.value - self.geometry.variables.Rk.value, ]) # final check diff --git a/bluemira/magnets/conductor.py b/bluemira/magnets/conductor.py index 9d94cc34d6..3b700d6762 100644 --- a/bluemira/magnets/conductor.py +++ b/bluemira/magnets/conductor.py @@ -15,7 +15,7 @@ from scipy.optimize import minimize_scalar from bluemira.base.look_and_feel import bluemira_debug -from bluemira.magnets.cable import ABCCable, create_cable_from_dict +from bluemira.magnets.cable import ABCCable from bluemira.magnets.utils import reciprocal_summation, summation if TYPE_CHECKING: @@ -72,12 +72,11 @@ def __init__( self.mat_jacket = mat_jacket self.cable = cable - @property def dy_jacket(self): """y-thickness of the jacket [m]""" return self._dy_jacket - + @property def dy_ins(self): """y-thickness of the ins [m]""" @@ -755,8 +754,8 @@ def __init__( string identifier """ - dy_jacket=dx_jacket - dy_ins=dx_ins + dy_jacket = dx_jacket + dy_ins = dx_ins super().__init__( cable=cable, mat_jacket=mat_jacket, diff --git a/bluemira/magnets/tfcoil_designer.py b/bluemira/magnets/tfcoil_designer.py index a1dfc876fa..343a691df6 100644 --- a/bluemira/magnets/tfcoil_designer.py +++ b/bluemira/magnets/tfcoil_designer.py @@ -106,6 +106,8 @@ class TFCoilXYDesignerParams(ParameterFrame): """Temperature margin""" t_delay: Parameter[float] """Time delay for exponential functions""" + strain: Parameter[float] + """Strain on system""" # # optimisation params # t0: Parameter[float] @@ -220,6 +222,7 @@ def _derived_values(self, optimsiation_params, case_params): "min_gap_x": min_gap_x, "I_fun": I_fun, "B_fun": B_fun, + "strain": self.params.strain.value, } def run(self): @@ -306,6 +309,7 @@ def run(self): op_cond=OperationalConditions( temperature=derived_params["T_op"], magnetic_field=derived_params["B_TF_i"], + strain=derived_params["strain"], ), allowable_sigma=derived_params["s_y"], bounds_cond_jacket=optimisation_params["bounds_cond_jacket"], diff --git a/examples/magnets/example_tf_creation.py b/examples/magnets/example_tf_creation.py index b12f59af6a..d8a94136f6 100644 --- a/examples/magnets/example_tf_creation.py +++ b/examples/magnets/example_tf_creation.py @@ -49,7 +49,7 @@ "d_cooling_channel": 0.01, "void_fraction": 0.7, "cos_theta": 0.97, - "dx": 0.034648435154495685, + "dx": 0.017324217577247843, "E": 0.1e9, }, }, @@ -58,10 +58,10 @@ "jacket_material": SS316_LN_MAG, "ins_material": DUMMY_INSULATOR_MAG, "params": { - "dx_jacket": 0.0030808556812487366, - "dy_jacket": 0.0, - "dx_ins": 0.0030808556812487366, - "dy_ins": 0.0, + "dx_jacket": 0.0015404278406243683, + # "dy_jacket": 0.0, + "dx_ins": 0.0005, + # "dy_ins": 0.0, }, }, "winding_pack": { @@ -79,8 +79,8 @@ "Ri": 3.708571428571428, "Rk": 0.0, "theta_TF": 22.5, - "dy_ps": 0.05733333333333333, - "dy_vault": 0.4529579163961617, + "dy_ps": 0.028666666666666667, + "dy_vault": 0.22647895819808084, }, }, "optimisation_params": { @@ -113,6 +113,7 @@ "T_sc": {"value": 4.2, "unit": "K"}, "T_margin": {"value": 1.5, "unit": "K"}, "t_delay": {"value": 3, "unit": "s"}, + "strain": {"value": 0.0055, "unit": ""}, } tf_coil_xy = TFCoilXYDesigner(params=params, build_config=config).execute() tf_coil_xy.plot(show=True, homogenized=False) diff --git a/examples/magnets/example_tf_wp_from_dict.py b/examples/magnets/example_tf_wp_from_dict.py index 985a03968f..283323f93e 100644 --- a/examples/magnets/example_tf_wp_from_dict.py +++ b/examples/magnets/example_tf_wp_from_dict.py @@ -1,5 +1,4 @@ import matplotlib.pyplot as plt -from bluemira.base.look_and_feel import bluemira_print import numpy as np from eurofusion_materials.library.magnet_branch_mats import ( COPPER_100, @@ -10,6 +9,8 @@ ) from matproplib import OperationalConditions +from bluemira.base.look_and_feel import bluemira_print + op_cond = OperationalConditions(temperature=5.7, magnetic_field=10.0, strain=0.0055) from bluemira.base.constants import MU_0, MU_0_2PI, MU_0_4PI From a233a74192ff677bbddf0b555fdbaf0fab88bbe7 Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Wed, 10 Sep 2025 14:18:14 +0100 Subject: [PATCH 34/61] =?UTF-8?q?=F0=9F=90=9B=20Deal=20with=20strand=20ini?= =?UTF-8?q?t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bluemira/magnets/cable.py | 1 - bluemira/magnets/strand.py | 23 ++++------------------- 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/bluemira/magnets/cable.py b/bluemira/magnets/cable.py index 3fab048b35..7aa6abee32 100644 --- a/bluemira/magnets/cable.py +++ b/bluemira/magnets/cable.py @@ -1256,7 +1256,6 @@ def from_dict( sc_strand = create_strand_from_dict(strand_dict=cable_dict.pop("sc_strand")) stab_strand = create_strand_from_dict(strand_dict=cable_dict.pop("stab_strand")) - # how to handle? return cls( sc_strand=sc_strand, stab_strand=stab_strand, diff --git a/bluemira/magnets/strand.py b/bluemira/magnets/strand.py index 8a73b10725..664007aed4 100644 --- a/bluemira/magnets/strand.py +++ b/bluemira/magnets/strand.py @@ -26,6 +26,7 @@ from bluemira.display.plotter import PlotOptions from bluemira.geometry.face import BluemiraFace from bluemira.geometry.tools import make_circle +from bluemira.utilities.tools import get_class_from_module class Strand: @@ -559,23 +560,7 @@ def create_strand_from_dict( Strand An instance of the appropriate Strand subclass. - Raises - ------ - ValueError - If 'name_in_registry' is missing from the dictionary. - If no matching registered class is found. """ - name_in_registry = strand_dict.get("name_in_registry") - if name_in_registry is None: - raise ValueError( - "Serialized strand dictionary must contain a 'name_in_registry' field." - ) - - cls = STRAND_REGISTRY.get(name_in_registry) - if cls is None: - raise ValueError( - f"No registered strand class with registration name '{name_in_registry}'. " - "Available classes are: " + ", ".join(STRAND_REGISTRY.keys()) - ) - - return cls.from_dict(name=name, strand_dict=strand_dict) + return get_class_from_module( + strand_dict.pop("class"), default_module="bluemira.magnets.strand" + ).from_dict(name=name, strand_dict=strand_dict) From 2352ada8563f55d8ef50c2d49896996babe2b532 Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Wed, 10 Sep 2025 15:16:45 +0100 Subject: [PATCH 35/61] =?UTF-8?q?=F0=9F=9A=A7=20Undo=20proper=20dx=20dy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bluemira/magnets/cable.py | 33 +++++++++++---------- bluemira/magnets/case_tf.py | 30 ++++++++++--------- bluemira/magnets/conductor.py | 38 ++++++++++++------------- bluemira/magnets/winding_pack.py | 20 +++++++------ examples/magnets/example_tf_creation.py | 10 +++---- 5 files changed, 69 insertions(+), 62 deletions(-) diff --git a/bluemira/magnets/cable.py b/bluemira/magnets/cable.py index 7aa6abee32..3caa9493af 100644 --- a/bluemira/magnets/cable.py +++ b/bluemira/magnets/cable.py @@ -128,12 +128,12 @@ def __init__( @property @abstractmethod def dx(self): - """Half Cable dimension in the x-direction [m].""" + """Cable dimension in the x-direction [m].""" @property @abstractmethod def dy(self): - """Half Cable dimension in the y-direction [m].""" + """Cable dimension in the y-direction [m].""" @property def aspect_ratio(self): @@ -509,10 +509,13 @@ def plot( pc = np.array([xc, yc]) - p0 = np.array([-self.dx, -self.dy]) - p1 = np.array([self.dx, -self.dy]) - p2 = np.array([[self.dx, self.dy]]) - p3 = np.array([-self.dx, self.dy]) + a = self.dx / 2 + b = self.dy / 2 + + p0 = np.array([-a, -b]) + p1 = np.array([a, -b]) + p2 = np.array([[a, b]]) + p3 = np.array([-a, b]) points_ext = np.vstack((p0, p1, p2, p3, p0)) + pc points_cc = ( @@ -719,13 +722,13 @@ def dx(self) -> float: @property def dy(self) -> float: """Half Cable dimension in the y direction [m]""" - return self.area / self.dx / 4 + return self.area / self.dx # Decide if this function shall be a setter. # Defined as "normal" function to underline that it modifies dx. def set_aspect_ratio(self, value: float): """Modify dx in order to get the given aspect ratio""" - self.dx = np.sqrt(value * self.area) / 2 + self.dx = np.sqrt(value * self.area) # OD homogenized structural properties def Kx(self, op_cond: OperationalConditions) -> float: # noqa: N802 @@ -950,12 +953,12 @@ def __init__( @property def dx(self) -> float: - """Half Cable dimension in the x direction [m]""" - return np.sqrt(self.area) / 2 + """Cable dimension in the x direction [m]""" + return np.sqrt(self.area) @property def dy(self) -> float: - """Half Cable dimension in the y direction [m]""" + """Cable dimension in the y direction [m]""" return self.dx # OD homogenized structural properties @@ -1104,12 +1107,12 @@ def __init__( @property def dx(self) -> float: - """Half Cable dimension in the x direction [m] (i.e. cable's radius)""" - return np.sqrt(self.area / np.pi) + """Cable dimension in the x direction [m] (i.e. cable's radius)""" + return np.sqrt(self.area * 4 / np.pi) @property def dy(self) -> float: - """Half Cable dimension in the y direction [m] (i.e. cable's radius)""" + """Cable dimension in the y direction [m] (i.e. cable's radius)""" return self.dx # OD homogenized structural properties @@ -1193,7 +1196,7 @@ def plot( points_ext = ( np.array([ - np.array([np.cos(theta), np.sin(theta)]) * self.dx + np.array([np.cos(theta), np.sin(theta)]) * self.dx / 2 for theta in np.linspace(0, np.radians(360), 19) ]) + pc diff --git a/bluemira/magnets/case_tf.py b/bluemira/magnets/case_tf.py index 7d3edc98ff..f4724c7fea 100644 --- a/bluemira/magnets/case_tf.py +++ b/bluemira/magnets/case_tf.py @@ -62,7 +62,7 @@ def _dx_at_radius(radius: float, rad_theta: float) -> float: : Half toroidal width [m] at the given radius. """ - return radius * np.tan(rad_theta / 2) + return 2 * radius * np.tan(rad_theta / 2) @dataclass @@ -126,9 +126,13 @@ def area(self) -> float: : Cross-sectional area [m²]. """ - return (self.variables.Ri.value - self.variables.Rk.value) * ( - _dx_at_radius(self.variables.Ri.value, self.rad_theta) - + _dx_at_radius(self.variables.Rk.value, self.rad_theta) + return ( + 0.5 + * (self.variables.Ri.value - self.variables.Rk.value) + * ( + _dx_at_radius(self.variables.Ri.value, self.rad_theta) + + _dx_at_radius(self.variables.Rk.value, self.rad_theta) + ) ) def create_shape(self, label: str = "") -> BluemiraWire: @@ -142,8 +146,8 @@ def create_shape(self, label: str = "") -> BluemiraWire: Coordinates are ordered counterclockwise starting from the top-left corner: [(-dx_outer, Ri), (dx_outer, Ri), (dx_inner, Rk), (-dx_inner, Rk)]. """ - dx_outer = _dx_at_radius(self.variables.Ri.value, self.rad_theta) - dx_inner = _dx_at_radius(self.variables.Rk.value, self.rad_theta) + dx_outer = _dx_at_radius(self.variables.Ri.value, self.rad_theta) / 2 + dx_inner = _dx_at_radius(self.variables.Rk.value, self.rad_theta) / 2 return make_polygon( [ @@ -464,7 +468,7 @@ def dy_wp_i(self) -> np.ndarray: Array containing the radial thickness [m] of each Winding Pack. Each element corresponds to one WP in the self.WPs list. """ - return np.array([2 * wp.dy for wp in self.WPs]) + return np.array([wp.dy for wp in self.WPs]) @property def dy_wp_tot(self) -> float: @@ -550,7 +554,7 @@ def plot( # Plot winding packs for i, wp in enumerate(self.WPs): xc_wp = 0.0 - yc_wp = self.R_wp_i[i] - wp.dy + yc_wp = self.R_wp_i[i] - wp.dy / 2 ax = wp.plot(xc=xc_wp, yc=yc_wp, ax=ax, homogenized=homogenized) # Finalize plot @@ -937,10 +941,10 @@ def Kx(self, op_cond: OperationalConditions) -> float: # noqa: N802 # toroidal stiffness of the poloidal support region kx_ps = self.mat_case.youngs_modulus(op_cond) / self.dx_ps * self.dy_ps dx_lat = np.array([ - (self.R_wp_i[i] + self.R_wp_k[i]) / 2 * np.tan(self.rad_theta / 2) - w.dx + (self.R_wp_i[i] + self.R_wp_k[i]) / 2 * np.tan(self.rad_theta / 2) - w.dx / 2 for i, w in enumerate(self.WPs) ]) - dy_lat = np.array([2 * w.dy for w in self.WPs]) + dy_lat = np.array([w.dy for w in self.WPs]) # toroidal stiffness of lateral case sections per winding pack kx_lat = self.mat_case.youngs_modulus(op_cond) / dx_lat * dy_lat temp = [ @@ -976,7 +980,7 @@ def Ky(self, op_cond: OperationalConditions) -> float: # noqa: N802 # toroidal stiffness of the poloidal support region ky_ps = self.mat_case.youngs_modulus(op_cond) * self.dx_ps / self.dy_ps dx_lat = np.array([ - (self.R_wp_i[i] + self.R_wp_k[i]) / 2 * np.tan(self.rad_theta / 2) - w.dx + (self.R_wp_i[i] + self.R_wp_k[i]) / 2 * np.tan(self.rad_theta / 2) - w.dx / 2 for i, w in enumerate(self.WPs) ]) dy_lat = np.array([2 * w.dy for w in self.WPs]) @@ -1052,7 +1056,7 @@ def rearrange_conductors_in_wp( if i == 1: n_layers_max = math.floor(dx_WP / conductor.dx) if layout == "pancake": - n_layers_max = math.floor(dx_WP / conductor.dx / 2.0) * 2 + n_layers_max = math.floor(dx_WP / conductor.dx / 2) * 2 if n_layers_max == 0: n_layers_max = 2 else: @@ -1072,7 +1076,7 @@ def rearrange_conductors_in_wp( else: dx_WP = n_layers_max * conductor.dx # noqa: N806 - gap_0 = R_wp_i * np.tan(self.rad_theta / 2) - dx_WP + gap_0 = R_wp_i * np.tan(self.rad_theta / 2) - dx_WP / 2 gap_1 = min_gap_x max_dy = (gap_0 - gap_1) / np.tan(self.rad_theta / 2) diff --git a/bluemira/magnets/conductor.py b/bluemira/magnets/conductor.py index 3b700d6762..2ba3a234ad 100644 --- a/bluemira/magnets/conductor.py +++ b/bluemira/magnets/conductor.py @@ -84,23 +84,25 @@ def dy_ins(self): @property def dx(self): - """Half x-dimension of the conductor [m]""" - return self.dx_ins + self.dx_jacket + self.cable.dx + """x-dimension of the conductor [m]""" + return self.dx_ins * 2 + self.dx_jacket * 2 + self.cable.dx @property def dy(self): - """Half y-dimension of the conductor [m]""" - return self.dy_ins + self.dy_jacket + self.cable.dy + """y-dimension of the conductor [m]""" + return self.dy_ins * 2 + self.dy_jacket * 2 + self.cable.dy @property def area(self): """Area of the conductor [m^2]""" - return self.dx * self.dy * 4 + return self.dx * self.dy @property def area_jacket(self): """Area of the jacket [m^2]""" - return 4 * (self.cable.dx + self.dx_jacket) * (self.cable.dy + self.dy_jacket) + return (self.dx - 2 * self.dx_ins) * ( + self.dy - 2 * self.dy_ins + ) - self.cable.area @property def area_ins(self): @@ -248,7 +250,7 @@ def _Kx_topbot_ins(self, op_cond: OperationalConditions) -> float: # noqa: N802 : Axial stiffness [N/m] """ - return self.mat_ins.youngs_modulus(op_cond) * 2 * self.cable.dy / self.dx_ins + return self.mat_ins.youngs_modulus(op_cond) * self.cable.dy / self.dx_ins def _Kx_lat_ins(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ @@ -259,7 +261,7 @@ def _Kx_lat_ins(self, op_cond: OperationalConditions) -> float: # noqa: N802 : Axial stiffness [N/m] """ - return self.mat_ins.youngs_modulus(op_cond) * self.dy_ins / (2 * self.dx) + return self.mat_ins.youngs_modulus(op_cond) * self.dy_ins / self.dx def _Kx_lat_jacket(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ @@ -273,7 +275,7 @@ def _Kx_lat_jacket(self, op_cond: OperationalConditions) -> float: # noqa: N802 return ( self.mat_jacket.youngs_modulus(op_cond) * self.dy_jacket - / (2 * self.dx - 2 * self.dx_ins) + / (self.dx - 2 * self.dx_ins) ) def _Kx_topbot_jacket(self, op_cond: OperationalConditions) -> float: # noqa: N802 @@ -285,9 +287,7 @@ def _Kx_topbot_jacket(self, op_cond: OperationalConditions) -> float: # noqa: N : Axial stiffness [N/m] """ - return ( - self.mat_jacket.youngs_modulus(op_cond) * 2 * self.cable.dy / self.dx_jacket - ) + return self.mat_jacket.youngs_modulus(op_cond) * self.cable.dy / self.dx_jacket def Kx(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ @@ -321,7 +321,7 @@ def _Ky_topbot_ins(self, op_cond: OperationalConditions) -> float: # noqa: N802 : Axial stiffness [N/m] """ - return self.mat_ins.youngs_modulus(op_cond) * 2 * self.cable.dx / self.dy_ins + return self.mat_ins.youngs_modulus(op_cond) * self.cable.dx / self.dy_ins def _Ky_lat_ins(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ @@ -332,7 +332,7 @@ def _Ky_lat_ins(self, op_cond: OperationalConditions) -> float: # noqa: N802 : Axial stiffness [N/m] """ - return self.mat_ins.youngs_modulus(op_cond) * self.dx_ins / (2 * self.dy) + return self.mat_ins.youngs_modulus(op_cond) * self.dx_ins / self.dy def _Ky_lat_jacket(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ @@ -346,7 +346,7 @@ def _Ky_lat_jacket(self, op_cond: OperationalConditions) -> float: # noqa: N802 return ( self.mat_jacket.youngs_modulus(op_cond) * self.dx_jacket - / (2 * self.dy - 2 * self.dy_ins) + / (self.dy - 2 * self.dy_ins) ) def _Ky_topbot_jacket(self, op_cond: OperationalConditions) -> float: # noqa: N802 @@ -358,9 +358,7 @@ def _Ky_topbot_jacket(self, op_cond: OperationalConditions) -> float: # noqa: N : Axial stiffness [N/m] """ - return ( - self.mat_jacket.youngs_modulus(op_cond) * 2 * self.cable.dx / self.dy_jacket - ) + return self.mat_jacket.youngs_modulus(op_cond) * self.cable.dx / self.dy_jacket def Ky(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ @@ -662,8 +660,8 @@ def plot(self, xc: float = 0, yc: float = 0, *, show: bool = False, ax=None): _, ax = plt.subplots() pc = np.array([xc, yc]) - a = self.cable.dx + self.dx_jacket - b = self.cable.dy + self.dy_jacket + a = self.cable.dx / 2 + self.dx_jacket + b = self.cable.dy / 2 + self.dy_jacket p0 = np.array([-a, -b]) p1 = np.array([a, -b]) diff --git a/bluemira/magnets/winding_pack.py b/bluemira/magnets/winding_pack.py index 91acc02fac..1fcaa6f8e6 100644 --- a/bluemira/magnets/winding_pack.py +++ b/bluemira/magnets/winding_pack.py @@ -55,18 +55,18 @@ def __init__( @property def dx(self) -> float: - """Return the half width of the winding pack [m].""" + """Return the width of the winding pack [m].""" return self.conductor.dx * self.nx @property def dy(self) -> float: - """Return the half height of the winding pack [m].""" + """Return the height of the winding pack [m].""" return self.conductor.dy * self.ny @property def area(self) -> float: """Return the total cross-sectional area [m²].""" - return 4 * self.dx * self.dy + return self.dx * self.dy @property def n_conductors(self) -> int: @@ -146,11 +146,13 @@ def plot( _, ax = plt.subplots() pc = np.array([xc, yc]) + a = self.dx / 2 + b = self.dy / 2 - p0 = np.array([-self.dx, -self.dy]) - p1 = np.array([self.dx, -self.dy]) - p2 = np.array([self.dx, self.dy]) - p3 = np.array([-self.dx, self.dy]) + p0 = np.array([-a, -b]) + p1 = np.array([a, -b]) + p2 = np.array([a, b]) + p3 = np.array([-a, b]) points_ext = np.vstack((p0, p1, p2, p3, p0)) + pc @@ -160,8 +162,8 @@ def plot( if not homogenized: for i in range(self.nx): for j in range(self.ny): - xc_c = xc - self.dx + (2 * i + 1) * self.conductor.dx - yc_c = yc - self.dy + (2 * j + 1) * self.conductor.dy + xc_c = xc - self.dx / 2 + (i + 0.5) * self.conductor.dx + yc_c = yc - self.dy / 2 + (j + 0.5) * self.conductor.dy self.conductor.plot(xc=xc_c, yc=yc_c, ax=ax) if show: diff --git a/examples/magnets/example_tf_creation.py b/examples/magnets/example_tf_creation.py index d8a94136f6..46388d935c 100644 --- a/examples/magnets/example_tf_creation.py +++ b/examples/magnets/example_tf_creation.py @@ -49,7 +49,7 @@ "d_cooling_channel": 0.01, "void_fraction": 0.7, "cos_theta": 0.97, - "dx": 0.017324217577247843, + "dx": 0.017324217577247843 * 2, "E": 0.1e9, }, }, @@ -58,9 +58,9 @@ "jacket_material": SS316_LN_MAG, "ins_material": DUMMY_INSULATOR_MAG, "params": { - "dx_jacket": 0.0015404278406243683, + "dx_jacket": 0.0015404278406243683 * 2, # "dy_jacket": 0.0, - "dx_ins": 0.0005, + "dx_ins": 0.0005 * 2, # "dy_ins": 0.0, }, }, @@ -79,8 +79,8 @@ "Ri": 3.708571428571428, "Rk": 0.0, "theta_TF": 22.5, - "dy_ps": 0.028666666666666667, - "dy_vault": 0.22647895819808084, + "dy_ps": 0.028666666666666667 * 2, + "dy_vault": 0.22647895819808084 * 2, }, }, "optimisation_params": { From 0c20ceed9e6b2b4c35a2eda69c659f6465bcca5e Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Wed, 10 Sep 2025 15:23:18 +0100 Subject: [PATCH 36/61] =?UTF-8?q?=F0=9F=92=AC=20Spelling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bluemira/magnets/cable.py | 32 +++++++------- bluemira/magnets/case_tf.py | 66 ++++++++++++++--------------- bluemira/magnets/conductor.py | 20 ++++----- bluemira/magnets/tfcoil_designer.py | 10 ++--- bluemira/magnets/winding_pack_.py | 2 +- 5 files changed, 65 insertions(+), 65 deletions(-) diff --git a/bluemira/magnets/cable.py b/bluemira/magnets/cable.py index 3caa9493af..08ff989390 100644 --- a/bluemira/magnets/cable.py +++ b/bluemira/magnets/cable.py @@ -40,7 +40,7 @@ class ABCCable(ABC): Abstract base class for superconducting cables. Defines the general structure and common methods for cables - composed of superconducting and stabilizer strands. + composed of superconducting and stabiliser strands. Notes ----- @@ -75,7 +75,7 @@ def __init__( sc_strand: The superconducting strand. stab_strand: - The stabilizer strand. + The stabiliser strand. n_sc_strand: Number of superconducting strands. n_stab_strand: @@ -214,7 +214,7 @@ def Cp(self, op_cond: OperationalConditions): # noqa: N802 @property def area_stab(self) -> float: - """Area of the stabilizer region""" + """Area of the stabiliser region""" return self.stab_strand.area * self.n_stab_strand @property @@ -308,7 +308,7 @@ def optimise_n_stab_ths( bounds: np.ndarray | None = None, ): """ - Optimize the number of stabilizer strand in the superconducting cable using a + Optimise the number of stabiliser strand in the superconducting cable using a 0-D hot spot criteria. Parameters @@ -326,21 +326,21 @@ def optimise_n_stab_ths( I_fun : Current [A] as a time-dependent function. bounds: - Lower and upper limits for the number of stabilizer strands. + Lower and upper limits for the number of stabiliser strands. Returns ------- : - The result of the optimization process. + The result of the optimisation process. Raises ------ ValueError - If the optimization process does not converge. + If the optimisiation process does not converge. Notes ----- - - The number of stabilizer strands in the cable is modified directly. + - The number of stabiliser strands in the cable is modified directly. - Cooling material contribution is neglected when applying the hot spot criteria. """ @@ -365,7 +365,7 @@ def final_temperature_difference( Parameters ---------- n_stab: - Number of stabilizer strands to set temporarily for this simulation. + Number of stabiliser strands to set temporarily for this simulation. t0: Initial time of the simulation [s]. tf: @@ -387,7 +387,7 @@ def final_temperature_difference( Notes ----- - - This method is typically used as a cost function for optimization routines + - This method is typically used as a cost function for optimisation routines (e.g., minimizing the temperature error by tuning `n_stab`). - It modifies the internal state `self._n_stab_strand`, which may affect subsequent evaluations unless restored. @@ -414,7 +414,7 @@ def final_temperature_difference( if not result.success: raise ValueError( - "n_stab optimization did not converge. Check your input parameters " + "n_stab optimisation did not converge. Check your input parameters " "or initial bracket." ) @@ -432,7 +432,7 @@ def final_temperature_difference( f"{self.n_stab_strand}." ) raise ValueError( - "Optimization failed to keep final temperature ≤ target. " + "Optimisation failed to keep final temperature ≤ target. " "Try increasing the upper bound of n_stab or adjusting cable parameters." ) bluemira_print(f"Optimal n_stab: {self.n_stab_strand}") @@ -539,7 +539,7 @@ def __str__(self) -> str: Return a human-readable summary of the cable configuration. Includes geometric properties, void and twist factors, and a string - representation of both the superconducting and stabilizer strands. + representation of both the superconducting and stabiliser strands. Returns ------- @@ -921,7 +921,7 @@ def __init__( sc_strand: strand of the superconductor stab_strand: - strand of the stabilizer + strand of the stabiliser n_sc_strand: Number of superconducting strands. n_stab_strand: @@ -1055,7 +1055,7 @@ class RoundCable(ABCCable): """ A cable with round cross-section for superconducting applications. - This cable type includes superconducting and stabilizer strands arranged + This cable type includes superconducting and stabiliser strands arranged around a central cooling channel. """ @@ -1079,7 +1079,7 @@ def __init__( sc_strand: strand of the superconductor stab_strand: - strand of the stabilizer + strand of the stabiliser n_sc_strand: Number of superconducting strands. n_stab_strand: diff --git a/bluemira/magnets/case_tf.py b/bluemira/magnets/case_tf.py index f4724c7fea..39cb520cc4 100644 --- a/bluemira/magnets/case_tf.py +++ b/bluemira/magnets/case_tf.py @@ -7,7 +7,7 @@ """ Toroidal Field (TF) Coil 2D Case Class. -This class models and optimizes the cross-sectional layout of the inboard leg of a TF +This class models and optimises the cross-sectional layout of the inboard leg of a TF coil. It is designed to define and adjust the distribution of structural materials and winding pack arrangement to achieve optimal performance and mechanical robustness. @@ -98,7 +98,7 @@ class TrapezoidalGeometry(GeometryParameterisation[TrapezoidalGeometryOptVariabl The coil cross-section has a trapezoidal shape: wider at the outer radius (Ri) and narrower at the inner radius (Rk), reflecting typical TF coil designs - for magnetic and mechanical optimization. + for magnetic and mechanical optimisation. """ def __init__(self, var_dict: VarDictT | None = None): @@ -720,7 +720,7 @@ def enforce_wp_layout_rules( return n_layers_max, n_turns_max @abstractmethod - def optimize_vault_radial_thickness( + def optimise_vault_radial_thickness( self, pm: float, fz: float, @@ -730,7 +730,7 @@ def optimize_vault_radial_thickness( bounds: np.ndarray = None, ): """ - Abstract method to optimize the radial thickness of the vault support region. + Abstract method to optimise the radial thickness of the vault support region. Parameters ---------- @@ -745,7 +745,7 @@ def optimize_vault_radial_thickness( allowable_sigma: Allowable maximum stress [Pa]. bounds: - Optimization bounds for vault thickness [m]. + Optimisation bounds for vault thickness [m]. """ def to_dict(self) -> dict[str, float | str | list[dict[str, float | str | Any]]]: @@ -1190,7 +1190,7 @@ def _tresca_stress( sigma_z = fz / (self.area_case_jacket + self.area_wps_jacket) return sigma_theta + sigma_z - def optimize_vault_radial_thickness( + def optimise_vault_radial_thickness( self, pm: float, fz: float, @@ -1199,7 +1199,7 @@ def optimize_vault_radial_thickness( bounds: np.array = None, ): """ - Optimize the vault radial thickness of the case + Optimise the vault radial thickness of the case Parameters ---------- @@ -1214,18 +1214,18 @@ def optimize_vault_radial_thickness( allowable_sigma: The allowable stress (Pa) for the jacket material. bounds: - Optional bounds for the jacket thickness optimization (default is None). + Optional bounds for the jacket thickness optimisation (default is None). Returns ------- : - The result of the optimization process containing information about the + The result of the optimisation process containing information about the optimal vault thickness. Raises ------ ValueError - If the optimization process did not converge. + If the optimisation process did not converge. """ method = None if bounds is not None: @@ -1240,7 +1240,7 @@ def optimize_vault_radial_thickness( ) if not result.success: - raise ValueError("dy_vault optimization did not converge.") + raise ValueError("dy_vault optimisation did not converge.") self.dy_vault = result.x # print(f"Optimal dy_vault: {self.dy_vault}") # print(f"Tresca sigma: {self._tresca_stress(pm, fz, T=T, B=B) / 1e6} MPa") @@ -1256,7 +1256,7 @@ def _sigma_difference( allowable_sigma: float, ) -> float: """ - Fitness function for the optimization problem. It calculates the absolute + Fitness function for the optimisation problem. It calculates the absolute difference between the Tresca stress and the allowable stress. Parameters @@ -1292,7 +1292,7 @@ def _sigma_difference( # diff: {sigma - allowable_sigma}") return abs(sigma - allowable_sigma) - def optimize_jacket_and_vault( + def optimise_jacket_and_vault( self, pm: float, fz: float, @@ -1309,14 +1309,14 @@ def optimize_jacket_and_vault( n_conds: int | None = None, ): """ - Jointly optimize the conductor jacket and case vault thickness + Jointly optimise the conductor jacket and case vault thickness under electromagnetic loading constraints. - This method performs an iterative optimization of: + This method performs an iterative optimisation of: - The cross-sectional area of the conductor jacket. - The vault radial thickness of the TF coil casing. - The optimization loop continues until the relative change in + The optimisation loop continues until the relative change in jacket area and vault thickness drops below the specified convergence threshold `eps`, or `max_niter` is reached. @@ -1332,9 +1332,9 @@ def optimize_jacket_and_vault( allowable_sigma: Maximum allowable stress for structural material [Pa]. bounds_cond_jacket: - Min/max bounds for conductor jacket area optimization [m²]. + Min/max bounds for conductor jacket area optimisation [m²]. bounds_dy_vault: - Min/max bounds for the case vault thickness optimization [m]. + Min/max bounds for the case vault thickness optimisation [m]. layout: Cable layout strategy; "auto" or predefined layout name. wp_reduction_factor: @@ -1344,9 +1344,9 @@ def optimize_jacket_and_vault( n_layers_reduction: Number of conductor layers to remove when reducing WP height. max_niter: - Maximum number of optimization iterations. + Maximum number of optimisation iterations. eps: - Convergence threshold for the combined optimization loop. + Convergence threshold for the combined optimisation loop. n_conds: Target total number of conductors in the winding pack. If None, the self number of conductors is used. @@ -1355,7 +1355,7 @@ def optimize_jacket_and_vault( ----- The function modifies the internal state of `conductor` and `self.dy_vault`. """ - debug_msg = ["Method optimize_jacket_and_vault"] + debug_msg = ["Method optimise_jacket_and_vault"] # Initialize convergence array self._convergence_array = [] @@ -1393,7 +1393,7 @@ def optimize_jacket_and_vault( case_dy_vault0 = self.dy_vault debug_msg.append( - f"before optimization: conductor jacket area = {conductor.area_jacket}" + f"before optimisation: conductor jacket area = {conductor.area_jacket}" ) cond_area_jacket0 = conductor.area_jacket t_z_cable_jacket = ( @@ -1402,12 +1402,12 @@ def optimize_jacket_and_vault( / (self.area_case_jacket + self.area_wps_jacket) / self.n_conductors ) - conductor.optimize_jacket_conductor( + conductor.optimise_jacket_conductor( pm, t_z_cable_jacket, op_cond, allowable_sigma, bounds_cond_jacket ) debug_msg.extend([ f"t_z_cable_jacket: {t_z_cable_jacket}", - f"after optimization: conductor jacket area = {conductor.area_jacket}", + f"after optimisation: conductor jacket area = {conductor.area_jacket}", ]) conductor.dx_jacket = ( @@ -1426,8 +1426,8 @@ def optimize_jacket_and_vault( layout=layout, ) - debug_msg.append(f"before optimization: case dy_vault = {self.dy_vault}") - self.optimize_vault_radial_thickness( + debug_msg.append(f"before optimisation: case dy_vault = {self.dy_vault}") + self.optimise_vault_radial_thickness( pm=pm, fz=fz, op_cond=op_cond, @@ -1444,7 +1444,7 @@ def optimize_jacket_and_vault( tot_err = err_dy_vault + err_conductor_area_jacket debug_msg.append( - f"after optimization: case dy_vault = {self.dy_vault}\n" + f"after optimisation: case dy_vault = {self.dy_vault}\n" f"err_dy_jacket = {err_conductor_area_jacket}\n " f"err_dy_vault = {err_dy_vault}\n " f"tot_err = {tot_err}" @@ -1464,23 +1464,23 @@ def optimize_jacket_and_vault( # final check if i < max_niter: bluemira_print( - f"Optimization of jacket and vault reached after " + f"Optimisation of jacket and vault reached after " f"{i} iterations. Total error: {tot_err} < {eps}." ) ax = self.plot(show=False, homogenized=False) - ax.set_title("Case design after optimization") + ax.set_title("Case design after optimisation") plt.show() else: bluemira_warn( - f"Maximum number of optimization iterations {max_niter} " + f"Maximum number of optimisation iterations {max_niter} " f"reached. A total of {tot_err} > {eps} has been obtained." ) def plot_convergence(self): """ - Plot the evolution of thicknesses and error values over optimization iterations. + Plot the evolution of thicknesses and error values over optimisation iterations. Raises ------ @@ -1488,7 +1488,7 @@ def plot_convergence(self): If no convergence data available """ if not hasattr(self, "_convergence_array") or not self._convergence_array: - raise RuntimeError("No convergence data available. Run optimization first.") + raise RuntimeError("No convergence data available. Run optimisation first.") convergence_data = np.array(self._convergence_array) @@ -1517,7 +1517,7 @@ def plot_convergence(self): axs[1].plot(iterations, err_dy_vault, marker="s", label="err_dy_vault") axs[1].set_ylabel("Relative Error") axs[1].set_xlabel("Iteration") - axs[1].set_title("Evolution of Errors during Optimization") + axs[1].set_title("Evolution of Errors during Optimisation") axs[1].set_yscale("log") # Log scale for better visibility if needed axs[1].legend() axs[1].grid(visible=True) diff --git a/bluemira/magnets/conductor.py b/bluemira/magnets/conductor.py index 2ba3a234ad..8dd26c3a2f 100644 --- a/bluemira/magnets/conductor.py +++ b/bluemira/magnets/conductor.py @@ -455,7 +455,7 @@ def _tresca_sigma_jacket( # tresca_stress return pressure * X_jacket * saf_jacket + f_z / self.area_jacket - def optimize_jacket_conductor( + def optimise_jacket_conductor( self, pressure: float, f_z: float, @@ -465,7 +465,7 @@ def optimize_jacket_conductor( direction: str = "x", ): """ - Optimize the jacket dimension of a conductor based on allowable stress using + Optimise the jacket dimension of a conductor based on allowable stress using the Tresca criterion. Parameters @@ -481,7 +481,7 @@ def optimize_jacket_conductor( allowable_sigma: The allowable stress (Pa) for the jacket material. bounds: - Optional bounds for the jacket thickness optimization (default is None). + Optional bounds for the jacket thickness optimisation (default is None). direction: The direction along which the pressure is applied ('x' or 'y'). Default is 'x'. @@ -489,17 +489,17 @@ def optimize_jacket_conductor( Returns ------- : - The result of the optimization process containing information about the + The result of the optimisation process containing information about the optimal jacket thickness. Raises ------ ValueError - If the optimization process did not converge. + If the optimisation process did not converge. Notes ----- - This function uses the Tresca yield criterion to optimize the thickness of the + This function uses the Tresca yield criterion to optimise the thickness of the jacket surrounding the conductor. This function directly update the conductor's jacket thickness along the x direction to the optimal value. @@ -514,7 +514,7 @@ def sigma_difference( direction: str = "x", ) -> float: """ - Objective function for optimizing conductor jacket thickness based on the + Objective function for optimising conductor jacket thickness based on the Tresca yield criterion. This function computes the absolute difference between the calculated Tresca @@ -556,7 +556,7 @@ def sigma_difference( ----- - This function updates the conductor's internal jacket dimension ( `dx_jacket` or `dy_jacket`) with the trial value `jacket_thickness`. - - It is intended for use with scalar optimization algorithms such as + - It is intended for use with scalar optimisation algorithms such as `scipy.optimize.minimize_scalar`. """ if direction not in {"x", "y"}: @@ -579,7 +579,7 @@ def sigma_difference( return diff - debug_msg = ["Method optimize_jacket_conductor:"] + debug_msg = ["Method optimise_jacket_conductor:"] if direction == "x": debug_msg.append(f"Previous dx_jacket: {self.dx_jacket}") @@ -600,7 +600,7 @@ def sigma_difference( ) if not result.success: - raise ValueError("Optimization of the jacket conductor did not converge.") + raise ValueError("Optimisation of the jacket conductor did not converge.") if direction == "x": self.dx_jacket = result.x debug_msg.append(f"Optimal dx_jacket: {self.dx_jacket}") diff --git a/bluemira/magnets/tfcoil_designer.py b/bluemira/magnets/tfcoil_designer.py index 343a691df6..db0b12e4ae 100644 --- a/bluemira/magnets/tfcoil_designer.py +++ b/bluemira/magnets/tfcoil_designer.py @@ -123,13 +123,13 @@ class TFCoilXYDesignerParams(ParameterFrame): # n_layers_reduction: Parameter[int] # """Number of layers to remove after each WP""" # bounds_cond_jacket: Parameter[np.ndarray] - # """Min/max bounds for conductor jacket area optimization [m²]""" + # """Min/max bounds for conductor jacket area optimisation [m²]""" # bounds_dy_vault: Parameter[np.ndarray] - # """Min/max bounds for the case vault thickness optimization [m]""" + # """Min/max bounds for the case vault thickness optimisation [m]""" # max_niter: Parameter[int] - # """Maximum number of optimization iterations""" + # """Maximum number of optimisation iterations""" # eps: Parameter[float] - # """Convergence threshold for the combined optimization loop.""" + # """Convergence threshold for the combined optimisation loop.""" class TFCoilXYDesigner(Designer): @@ -303,7 +303,7 @@ def run(self): layout=optimisation_params["layout"], ) # param frame optimisation stuff? - case.optimize_jacket_and_vault( + case.optimise_jacket_and_vault( pm=derived_params["pm"], fz=derived_params["t_z"], op_cond=OperationalConditions( diff --git a/bluemira/magnets/winding_pack_.py b/bluemira/magnets/winding_pack_.py index e6d1d6f2ee..d9f979fa82 100644 --- a/bluemira/magnets/winding_pack_.py +++ b/bluemira/magnets/winding_pack_.py @@ -193,7 +193,7 @@ def run(self) -> BluemiraWire: n_layers_reduction=n_layers_reduction, layout=layout, ) - case_out = case.optimize_jacket_and_vault( + case_out = case.optimise_jacket_and_vault( pm=pm, fz=t_z, temperature=T_op, From b33f667b704b897cc31cc3e13d82b14ba44f8fc9 Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Wed, 10 Sep 2025 15:59:50 +0100 Subject: [PATCH 37/61] =?UTF-8?q?=F0=9F=8E=A8=20Put=20back=20functions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bluemira/magnets/cable.py | 7 +- bluemira/magnets/case_tf.py | 160 ++++++++++++++++++++++++++++-------- 2 files changed, 130 insertions(+), 37 deletions(-) diff --git a/bluemira/magnets/cable.py b/bluemira/magnets/cable.py index 08ff989390..0947730f2f 100644 --- a/bluemira/magnets/cable.py +++ b/bluemira/magnets/cable.py @@ -110,7 +110,10 @@ def __init__( ) if "E" not in vars(type(self)): if youngs_modulus is None: - raise ValueError("E undefined on the class and not passed into the init") + + def youngs_modulus(op_cond): + raise NotImplementedError("E for Cable is not implemented.") + self.E = ( youngs_modulus if callable(youngs_modulus) @@ -716,7 +719,7 @@ def __init__( @property def dx(self) -> float: - """Half Cable dimension in the x direction [m]""" + """Cable dimension in the x direction [m]""" return self._dx @property diff --git a/bluemira/magnets/case_tf.py b/bluemira/magnets/case_tf.py index 39cb520cc4..c8fd085c77 100644 --- a/bluemira/magnets/case_tf.py +++ b/bluemira/magnets/case_tf.py @@ -71,21 +71,21 @@ class TrapezoidalGeometryOptVariables(OptVariablesFrame): Ri: OptVariable = ov( "Ri", - 3, # value? + 0, lower_bound=0, upper_bound=np.inf, description="External radius of the TF coil case [m].", ) Rk: OptVariable = ov( "Rk", - 5, # value? + 0, lower_bound=0, upper_bound=np.inf, description="Internal radius of the TF coil case [m].", ) theta_TF: OptVariable = ov( "theta_TF", - 15, # value? + 0, lower_bound=0, upper_bound=360, description="Toroidal angular span of the TF coil [degrees].", @@ -167,21 +167,21 @@ class WedgedGeometryOptVariables(OptVariablesFrame): Ri: OptVariable = ov( "Ri", - 3, # value? + 0, lower_bound=0, upper_bound=np.inf, description="External radius of the TF coil case [m].", ) Rk: OptVariable = ov( "Rk", - 5, # value? + 0, lower_bound=0, upper_bound=np.inf, description="Internal radius of the TF coil case [m].", ) theta_TF: OptVariable = ov( "theta_TF", - 15, # value? + 0, lower_bound=0, upper_bound=360, description="Toroidal angular span of the TF coil [degrees].", @@ -305,13 +305,6 @@ def __init__( self.mat_case = mat_case self.WPs = WPs self.name = name - # Toroidal half-length of the coil case at its maximum radial position [m] - self.dx_i = _dx_at_radius(self.geometry.variables.Ri.value, self.rad_theta) - # Average toroidal length of the ps plate - self.dx_ps = ( - self.geometry.variables.Ri.value - + (self.geometry.variables.Ri.value - self.dy_ps) - ) * np.tan(self.rad_theta / 2) # sets Rk self.update_dy_vault(self.dy_vault) @@ -452,6 +445,19 @@ def WPs(self, value: list[WindingPack]): # noqa: N802 # fix dy_vault (this will recalculate Rk) self.update_dy_vault(self.dy_vault) + @property + def dx_i(self): + """Toroidal length of the coil case at its maximum radial position [m]""" + return _dx_at_radius(self.geometry.variables.Ri.value, self.rad_theta) + + @property + def dx_ps(self): + """Average toroidal length of the ps plate [m]""" + return ( + self.geometry.variables.Ri.value + + (self.geometry.variables.Ri.value - self.dy_ps) + ) * np.tan(self.rad_theta / 2) + @property def n_conductors(self) -> int: """Total number of conductors in the winding pack.""" @@ -918,6 +924,48 @@ def Kx_vault(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ return self.mat_case.youngs_modulus(op_cond) * self.dy_vault / self.dx_vault + def Kx_ps(self, op_cond: OperationalConditions): # noqa: N802 + """ + Compute the equivalent radial stiffness of the poloidal support (PS) region. + + Parameters + ---------- + op_cond: OperationalConditions + Operational conditions including temperature, magnetic field, and strain + at which to calculate the material property. + + Returns + ------- + float + Equivalent radial stiffness of the poloidal support [Pa]. + """ + return self.mat_case.youngs_modulus(op_cond) * self.dy_ps / self.dx_ps + + def Kx_lat(self, op_cond: OperationalConditions): # noqa: N802 + """ + Compute the equivalent radial stiffness of the lateral case sections. + + These are the mechanical links between each winding pack and the outer case. + Each lateral segment is approximated as a rectangular element. + + Parameters + ---------- + op_cond: OperationalConditions + Operational conditions including temperature, magnetic field, and strain + at which to calculate the material property. + + Returns + ------- + np.ndarray + Array of radial stiffness values for each lateral segment [Pa]. + """ + dx_lat = np.array([ + (self.R_wp_i[i] + self.R_wp_k[i]) / 2 * np.tan(self.rad_theta / 2) - w.dx / 2 + for i, w in enumerate(self.WPs) + ]) + dy_lat = np.array([w.dy for w in self.WPs]) + return self.mat_case.youngs_modulus(op_cond) * dy_lat / dx_lat + def Kx(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ Compute the total equivalent radial stiffness of the entire case structure. @@ -938,15 +986,7 @@ def Kx(self, op_cond: OperationalConditions) -> float: # noqa: N802 : Total equivalent radial stiffness of the TF case [Pa]. """ - # toroidal stiffness of the poloidal support region - kx_ps = self.mat_case.youngs_modulus(op_cond) / self.dx_ps * self.dy_ps - dx_lat = np.array([ - (self.R_wp_i[i] + self.R_wp_k[i]) / 2 * np.tan(self.rad_theta / 2) - w.dx / 2 - for i, w in enumerate(self.WPs) - ]) - dy_lat = np.array([w.dy for w in self.WPs]) - # toroidal stiffness of lateral case sections per winding pack - kx_lat = self.mat_case.youngs_modulus(op_cond) / dx_lat * dy_lat + kx_lat = self.Kx_lat(op_cond) temp = [ reciprocal_summation([ kx_lat[i], @@ -955,7 +995,7 @@ def Kx(self, op_cond: OperationalConditions) -> float: # noqa: N802 ]) for i, w in enumerate(self.WPs) ] - return summation([kx_ps, self.Kx_vault(op_cond), *temp]) + return summation([self.Kx_ps(op_cond), self.Kx_vault(op_cond), *temp]) def Ky(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ @@ -977,17 +1017,7 @@ def Ky(self, op_cond: OperationalConditions) -> float: # noqa: N802 : Total equivalent toroidal stiffness of the TF case [Pa]. """ - # toroidal stiffness of the poloidal support region - ky_ps = self.mat_case.youngs_modulus(op_cond) * self.dx_ps / self.dy_ps - dx_lat = np.array([ - (self.R_wp_i[i] + self.R_wp_k[i]) / 2 * np.tan(self.rad_theta / 2) - w.dx / 2 - for i, w in enumerate(self.WPs) - ]) - dy_lat = np.array([2 * w.dy for w in self.WPs]) - # toroidal stiffness of lateral case sections per winding pack - ky_lat = self.mat_case.youngs_modulus(op_cond) * dx_lat / dy_lat - # toroidal stiffness of the vault region - ky_vault = self.mat_case.youngs_modulus(op_cond) * self.dx_vault / self.dy_vault + ky_lat = self.Ky_lat(op_cond) temp = [ summation([ ky_lat[i], @@ -996,7 +1026,67 @@ def Ky(self, op_cond: OperationalConditions) -> float: # noqa: N802 ]) for i, w in enumerate(self.WPs) ] - return reciprocal_summation([ky_ps, ky_vault, *temp]) + return reciprocal_summation([self.Ky_ps(op_cond), self.Ky_vault(op_cond), *temp]) + + def Ky_vault(self, op_cond: OperationalConditions): # noqa: N802 + """ + Compute the equivalent toroidal stiffness of the vault region. + + Parameters + ---------- + op_cond: OperationalConditions + Operational conditions including temperature, magnetic field, and strain + at which to calculate the material property. + + Returns + ------- + float + Equivalent toroidal stiffness of the vault [Pa]. + """ + return self.mat_case.youngs_modulus(op_cond) * self.dx_vault / self.dy_vault + + def Ky_ps(self, op_cond: OperationalConditions): # noqa: N802 + """ + Compute the equivalent toroidal stiffness of the poloidal support (PS) region. + + Parameters + ---------- + op_cond: OperationalConditions + Operational conditions including temperature, magnetic field, and strain + at which to calculate the material property. + + Returns + ------- + : + Equivalent toroidal stiffness of the PS region [Pa]. + """ + return self.mat_case.youngs_modulus(op_cond) * self.dx_ps / self.dy_ps + + def Ky_lat(self, op_cond: OperationalConditions): # noqa: N802 + """ + Compute the equivalent toroidal stiffness of lateral case sections + per winding pack. + + Each lateral piece is treated as a rectangular beam in the toroidal direction. + + Parameters + ---------- + op_cond: OperationalConditions + Operational conditions including temperature, magnetic field, and strain + at which to calculate the material property. + + Returns + ------- + np.ndarray + Array of toroidal stiffness values for each lateral segment [Pa]. + """ + dx_lat = np.array([ + (self.R_wp_i[i] + self.R_wp_k[i]) / 2 * np.tan(self._rad_theta / 2) + - w.dx / 2 + for i, w in enumerate(self.WPs) + ]) + dy_lat = np.array([w.dy for w in self.WPs]) + return self.mat_case.youngs_modulus(op_cond) * dx_lat / dy_lat def rearrange_conductors_in_wp( self, From 4ac0d7b34aa4f25f4969f1ecab5077b29156ea2c Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Wed, 10 Sep 2025 16:25:07 +0100 Subject: [PATCH 38/61] =?UTF-8?q?=F0=9F=8E=A8=20More=20undo=20dx?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bluemira/magnets/cable.py | 6 +++--- bluemira/magnets/case_tf.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bluemira/magnets/cable.py b/bluemira/magnets/cable.py index 0947730f2f..398fb40b38 100644 --- a/bluemira/magnets/cable.py +++ b/bluemira/magnets/cable.py @@ -724,7 +724,7 @@ def dx(self) -> float: @property def dy(self) -> float: - """Half Cable dimension in the y direction [m]""" + """Cable dimension in the y direction [m]""" return self.area / self.dx # Decide if this function shall be a setter. @@ -1110,12 +1110,12 @@ def __init__( @property def dx(self) -> float: - """Cable dimension in the x direction [m] (i.e. cable's radius)""" + """Cable dimension in the x direction [m] (i.e. cable's diameter)""" return np.sqrt(self.area * 4 / np.pi) @property def dy(self) -> float: - """Cable dimension in the y direction [m] (i.e. cable's radius)""" + """Cable dimension in the y direction [m] (i.e. cable's diameter)""" return self.dx # OD homogenized structural properties diff --git a/bluemira/magnets/case_tf.py b/bluemira/magnets/case_tf.py index c8fd085c77..19f226bdec 100644 --- a/bluemira/magnets/case_tf.py +++ b/bluemira/magnets/case_tf.py @@ -60,7 +60,7 @@ def _dx_at_radius(radius: float, rad_theta: float) -> float: Returns ------- : - Half toroidal width [m] at the given radius. + Toroidal width [m] at the given radius. """ return 2 * radius * np.tan(rad_theta / 2) From fa41552ddd43b953fd997ead3ad88ac357ed2189 Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Wed, 10 Sep 2025 16:33:01 +0100 Subject: [PATCH 39/61] =?UTF-8?q?=F0=9F=8E=A8=20Move=204=20diff?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bluemira/magnets/case_tf.py | 121 +++++++++++++++++----------------- bluemira/magnets/conductor.py | 16 ++--- 2 files changed, 69 insertions(+), 68 deletions(-) diff --git a/bluemira/magnets/case_tf.py b/bluemira/magnets/case_tf.py index 19f226bdec..d12fc1fdd4 100644 --- a/bluemira/magnets/case_tf.py +++ b/bluemira/magnets/case_tf.py @@ -907,22 +907,6 @@ def dx_vault(self): self.rad_theta / 2 ) - def Kx_vault(self, op_cond: OperationalConditions) -> float: # noqa: N802 - """ - Compute the equivalent radial stiffness of the vault region. - - Parameters - ---------- - op_cond: - Operational conditions including temperature, magnetic field, and strain - at which to calculate the material property. - - Returns - ------- - : - Equivalent radial stiffness of the vault [Pa]. - """ - return self.mat_case.youngs_modulus(op_cond) * self.dy_vault / self.dx_vault def Kx_ps(self, op_cond: OperationalConditions): # noqa: N802 """ @@ -966,6 +950,23 @@ def Kx_lat(self, op_cond: OperationalConditions): # noqa: N802 dy_lat = np.array([w.dy for w in self.WPs]) return self.mat_case.youngs_modulus(op_cond) * dy_lat / dx_lat + def Kx_vault(self, op_cond: OperationalConditions) -> float: # noqa: N802 + """ + Compute the equivalent radial stiffness of the vault region. + + Parameters + ---------- + op_cond: + Operational conditions including temperature, magnetic field, and strain + at which to calculate the material property. + + Returns + ------- + : + Equivalent radial stiffness of the vault [Pa]. + """ + return self.mat_case.youngs_modulus(op_cond) * self.dy_vault / self.dx_vault + def Kx(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ Compute the total equivalent radial stiffness of the entire case structure. @@ -988,49 +989,38 @@ def Kx(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ kx_lat = self.Kx_lat(op_cond) temp = [ - reciprocal_summation([ + summation([ kx_lat[i], w.Kx(op_cond), kx_lat[i], ]) for i, w in enumerate(self.WPs) ] - return summation([self.Kx_ps(op_cond), self.Kx_vault(op_cond), *temp]) + return reciprocal_summation([self.Kx_ps(op_cond), self.Kx_vault(op_cond), *temp]) - def Ky(self, op_cond: OperationalConditions) -> float: # noqa: N802 + def Ky_ps(self, op_cond: OperationalConditions): # noqa: N802 """ - Compute the total equivalent toroidal stiffness of the entire case structure. - - Combines: - - Each winding pack and its adjacent lateral case sections in parallel - - These parallel combinations are arranged in series with the PS and - vault regions + Compute the equivalent toroidal stiffness of the poloidal support (PS) region. Parameters ---------- - op_cond: + op_cond: OperationalConditions Operational conditions including temperature, magnetic field, and strain at which to calculate the material property. Returns ------- : - Total equivalent toroidal stiffness of the TF case [Pa]. + Equivalent toroidal stiffness of the PS region [Pa]. """ - ky_lat = self.Ky_lat(op_cond) - temp = [ - summation([ - ky_lat[i], - w.Ky(op_cond), - ky_lat[i], - ]) - for i, w in enumerate(self.WPs) - ] - return reciprocal_summation([self.Ky_ps(op_cond), self.Ky_vault(op_cond), *temp]) + return self.mat_case.youngs_modulus(op_cond) * self.dx_ps / self.dy_ps - def Ky_vault(self, op_cond: OperationalConditions): # noqa: N802 + def Ky_lat(self, op_cond: OperationalConditions): # noqa: N802 """ - Compute the equivalent toroidal stiffness of the vault region. + Compute the equivalent toroidal stiffness of lateral case sections + per winding pack. + + Each lateral piece is treated as a rectangular beam in the toroidal direction. Parameters ---------- @@ -1040,14 +1030,20 @@ def Ky_vault(self, op_cond: OperationalConditions): # noqa: N802 Returns ------- - float - Equivalent toroidal stiffness of the vault [Pa]. + np.ndarray + Array of toroidal stiffness values for each lateral segment [Pa]. """ - return self.mat_case.youngs_modulus(op_cond) * self.dx_vault / self.dy_vault + dx_lat = np.array([ + (self.R_wp_i[i] + self.R_wp_k[i]) / 2 * np.tan(self._rad_theta / 2) + - w.dx / 2 + for i, w in enumerate(self.WPs) + ]) + dy_lat = np.array([w.dy for w in self.WPs]) + return self.mat_case.youngs_modulus(op_cond) * dx_lat / dy_lat - def Ky_ps(self, op_cond: OperationalConditions): # noqa: N802 + def Ky_vault(self, op_cond: OperationalConditions): # noqa: N802 """ - Compute the equivalent toroidal stiffness of the poloidal support (PS) region. + Compute the equivalent toroidal stiffness of the vault region. Parameters ---------- @@ -1057,36 +1053,41 @@ def Ky_ps(self, op_cond: OperationalConditions): # noqa: N802 Returns ------- - : - Equivalent toroidal stiffness of the PS region [Pa]. + float + Equivalent toroidal stiffness of the vault [Pa]. """ - return self.mat_case.youngs_modulus(op_cond) * self.dx_ps / self.dy_ps + return self.mat_case.youngs_modulus(op_cond) * self.dx_vault / self.dy_vault - def Ky_lat(self, op_cond: OperationalConditions): # noqa: N802 + def Ky(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ - Compute the equivalent toroidal stiffness of lateral case sections - per winding pack. + Compute the total equivalent toroidal stiffness of the entire case structure. - Each lateral piece is treated as a rectangular beam in the toroidal direction. + Combines: + - Each winding pack and its adjacent lateral case sections in parallel + - These parallel combinations are arranged in series with the PS and + vault regions Parameters ---------- - op_cond: OperationalConditions + op_cond: Operational conditions including temperature, magnetic field, and strain at which to calculate the material property. Returns ------- - np.ndarray - Array of toroidal stiffness values for each lateral segment [Pa]. + : + Total equivalent toroidal stiffness of the TF case [Pa]. """ - dx_lat = np.array([ - (self.R_wp_i[i] + self.R_wp_k[i]) / 2 * np.tan(self._rad_theta / 2) - - w.dx / 2 + ky_lat = self.Ky_lat(op_cond) + temp = [ + reciprocal_summation([ + ky_lat[i], + w.Ky(op_cond), + ky_lat[i], + ]) for i, w in enumerate(self.WPs) - ]) - dy_lat = np.array([w.dy for w in self.WPs]) - return self.mat_case.youngs_modulus(op_cond) * dx_lat / dy_lat + ] + return summation([self.Ky_ps(op_cond), self.Ky_vault(op_cond), *temp]) def rearrange_conductors_in_wp( self, diff --git a/bluemira/magnets/conductor.py b/bluemira/magnets/conductor.py index 8dd26c3a2f..c577f5f1c1 100644 --- a/bluemira/magnets/conductor.py +++ b/bluemira/magnets/conductor.py @@ -298,10 +298,10 @@ def Kx(self, op_cond: OperationalConditions) -> float: # noqa: N802 : Axial stiffness [N/m] """ - return summation([ + return reciprocal_summation([ self._Kx_lat_ins(op_cond), self._Kx_lat_jacket(op_cond), - reciprocal_summation([ + summation([ self._Kx_topbot_ins(op_cond), self._Kx_topbot_jacket(op_cond), self.cable.Kx(op_cond), @@ -369,10 +369,10 @@ def Ky(self, op_cond: OperationalConditions) -> float: # noqa: N802 : Axial stiffness [N/m] """ - return summation([ + return reciprocal_summation([ self._Ky_lat_ins(op_cond), self._Ky_lat_jacket(op_cond), - reciprocal_summation([ + summation([ self._Ky_topbot_ins(op_cond), self._Ky_topbot_jacket(op_cond), self.cable.Ky(op_cond), @@ -427,10 +427,10 @@ def _tresca_sigma_jacket( if direction == "x": saf_jacket = (self.cable.dx + self.dx_jacket) / (self.dx_jacket) - K = summation([ # noqa: N806 + K = reciprocal_summation([ # noqa: N806 2 * self._Ky_lat_ins(op_cond), 2 * self._Ky_lat_jacket(op_cond), - reciprocal_summation([ + summation([ self.cable.Ky(op_cond), self._Ky_topbot_jacket(op_cond) / 2, ]), @@ -441,10 +441,10 @@ def _tresca_sigma_jacket( else: saf_jacket = (self.cable.dy + self.dy_jacket) / (self.dy_jacket) - K = summation([ # noqa: N806 + K = reciprocal_summation([ # noqa: N806 2 * self._Kx_lat_ins(op_cond), 2 * self._Kx_lat_jacket(op_cond), - reciprocal_summation([ + summation([ self.cable.Kx(op_cond), self._Kx_topbot_jacket(op_cond) / 2, ]), From 0c3f66d21dbf7f96fb67974865d10bb04d225c79 Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Wed, 10 Sep 2025 16:52:02 +0100 Subject: [PATCH 40/61] =?UTF-8?q?=F0=9F=8E=A8=20Remove=20extra=20opt=20var?= =?UTF-8?q?iables?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bluemira/magnets/case_tf.py | 37 +++++-------------------------------- 1 file changed, 5 insertions(+), 32 deletions(-) diff --git a/bluemira/magnets/case_tf.py b/bluemira/magnets/case_tf.py index d12fc1fdd4..5f54c39415 100644 --- a/bluemira/magnets/case_tf.py +++ b/bluemira/magnets/case_tf.py @@ -66,7 +66,7 @@ def _dx_at_radius(radius: float, rad_theta: float) -> float: @dataclass -class TrapezoidalGeometryOptVariables(OptVariablesFrame): +class CaseGeometryOptVariables(OptVariablesFrame): """Optimisiation variables for Trapezoidal Geometry.""" Ri: OptVariable = ov( @@ -92,7 +92,7 @@ class TrapezoidalGeometryOptVariables(OptVariablesFrame): ) -class TrapezoidalGeometry(GeometryParameterisation[TrapezoidalGeometryOptVariables]): +class TrapezoidalGeometry(GeometryParameterisation[CaseGeometryOptVariables]): """ Geometry of a Toroidal Field (TF) coil case with trapezoidal cross-section. @@ -102,7 +102,7 @@ class TrapezoidalGeometry(GeometryParameterisation[TrapezoidalGeometryOptVariabl """ def __init__(self, var_dict: VarDictT | None = None): - variables = TrapezoidalGeometryOptVariables() + variables = CaseGeometryOptVariables() variables.adjust_variables(var_dict, strict_bounds=False) super().__init__(variables) @@ -161,34 +161,7 @@ def create_shape(self, label: str = "") -> BluemiraWire: ) -@dataclass -class WedgedGeometryOptVariables(OptVariablesFrame): - """Optimisiation variables for Wedged Geometry.""" - - Ri: OptVariable = ov( - "Ri", - 0, - lower_bound=0, - upper_bound=np.inf, - description="External radius of the TF coil case [m].", - ) - Rk: OptVariable = ov( - "Rk", - 0, - lower_bound=0, - upper_bound=np.inf, - description="Internal radius of the TF coil case [m].", - ) - theta_TF: OptVariable = ov( - "theta_TF", - 0, - lower_bound=0, - upper_bound=360, - description="Toroidal angular span of the TF coil [degrees].", - ) - - -class WedgedGeometry(GeometryParameterisation[WedgedGeometryOptVariables]): +class WedgedGeometry(GeometryParameterisation[CaseGeometryOptVariables]): """ TF coil case shaped as a sector of an annulus (wedge with arcs). @@ -197,7 +170,7 @@ class WedgedGeometry(GeometryParameterisation[WedgedGeometryOptVariables]): """ def __init__(self, var_dict: VarDictT | None = None): - variables = WedgedGeometryOptVariables() + variables = CaseGeometryOptVariables() variables.adjust_variables(var_dict, strict_bounds=False) super().__init__(variables) From e7a127467979dd5c188fba4a8a40d6fad1c31cd9 Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Wed, 10 Sep 2025 17:00:57 +0100 Subject: [PATCH 41/61] =?UTF-8?q?=F0=9F=8E=A8=20Move=204=20diff?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bluemira/magnets/conductor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bluemira/magnets/conductor.py b/bluemira/magnets/conductor.py index c577f5f1c1..ecff40b38f 100644 --- a/bluemira/magnets/conductor.py +++ b/bluemira/magnets/conductor.py @@ -425,7 +425,7 @@ def _tresca_sigma_jacket( raise ValueError("Invalid direction: choose either 'x' or 'y'.") if direction == "x": - saf_jacket = (self.cable.dx + self.dx_jacket) / (self.dx_jacket) + saf_jacket = (self.cable.dx + 2 * self.dx_jacket) / (2 * self.dx_jacket) K = reciprocal_summation([ # noqa: N806 2 * self._Ky_lat_ins(op_cond), @@ -439,7 +439,7 @@ def _tresca_sigma_jacket( X_jacket = 2 * self._Ky_lat_jacket(op_cond) / K # noqa: N806 else: - saf_jacket = (self.cable.dy + self.dy_jacket) / (self.dy_jacket) + saf_jacket = (self.cable.dy + 2 * self.dy_jacket) / (2 * self.dy_jacket) K = reciprocal_summation([ # noqa: N806 2 * self._Kx_lat_ins(op_cond), From a40be26e1eb5110fb14e90ebd4d69c086d39207f Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Wed, 10 Sep 2025 17:05:49 +0100 Subject: [PATCH 42/61] =?UTF-8?q?=F0=9F=94=A5=20Remove=20pf?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bluemira/magnets/fatigue.py | 68 ++++++++++++++----------------------- 1 file changed, 25 insertions(+), 43 deletions(-) diff --git a/bluemira/magnets/fatigue.py b/bluemira/magnets/fatigue.py index d83241a710..c4bbd4b528 100644 --- a/bluemira/magnets/fatigue.py +++ b/bluemira/magnets/fatigue.py @@ -12,15 +12,10 @@ import abc from dataclasses import dataclass -from typing import TYPE_CHECKING, Final +from typing import Final import numpy as np -from bluemira.base.parameter_frame import Parameter, ParameterFrame - -if TYPE_CHECKING: - from bluemira.base.parameter_frame.typed import ParameterFrameLike - __all__ = [ "ConductorInfo", "EllipticalEmbeddedCrack", @@ -33,51 +28,39 @@ @dataclass -class ConductorInfo(ParameterFrame): +class ConductorInfo: """ Cable in conduit conductor information for Paris fatigue model """ - tk_radial: Parameter[float] # [m] in the loaded direction - width: Parameter[float] # [m] in the loaded direction - max_hoop_stress: Parameter[float] # [Pa] - residual_stress: Parameter[float] # [Pa] - walker_coeff: Parameter[float] + tk_radial: float # [m] in the loaded direction + width: float # [m] in the loaded direction + max_hoop_stress: float # [Pa] + residual_stress: float # [Pa] + walker_coeff: float @dataclass -class ParisFatigueMaterial(ParameterFrame): +class ParisFatigueMaterial: """ Material properties for the Paris fatigue model """ - C: Parameter[float] # Paris law material constant - m: Parameter[float] # Paris law material exponent - K_ic: Parameter[float] # Fracture toughness [Pa/m^(1/2)] + C: float # Paris law material constant + m: float # Paris law material exponent + K_ic: float # Fracture toughness [Pa/m^(1/2)] @dataclass -class ParisFatigueSafetyFactors(ParameterFrame): +class ParisFatigueSafetyFactors: """ Safety factors for the Paris fatigue model """ - sf_n_cycle: Parameter[float] - sf_depth_crack: Parameter[float] - sf_width_crack: Parameter[float] - sf_fracture: Parameter[float] - - -@dataclass -class CrackParams(ParameterFrame): - """ - Parameters for the crack class - """ - - width: Parameter[float] - """Crack width along the plate length direction""" - depth: Parameter[float] - """Crack depth in the plate thickness direction""" + sf_n_cycle: float + sf_depth_crack: float + sf_width_crack: float + sf_fracture: float def _stress_intensity_factor( @@ -175,10 +158,9 @@ class Crack(abc.ABC): Crack width along the plate length direction """ - param_cls: type[CrackParams] = CrackParams - - def __init__(self, params: ParameterFrameLike): - self.params = params + def __init__(self, depth, width): + self.depth = depth + self.width = width @classmethod def from_area(cls, area: float, aspect_ratio: float) -> Crack: @@ -190,9 +172,9 @@ def from_area(cls, area: float, aspect_ratio: float) -> Crack: : New instance of the crack geometry. """ - cls.params.depth.value = np.sqrt(area / (cls.alpha * np.pi * aspect_ratio)) - cls.params.width.value = aspect_ratio * cls.params.depth.value - return cls(cls.params.depth.value, cls.params.width.value) + depth = np.sqrt(area / (cls.alpha * np.pi * aspect_ratio)) + width = aspect_ratio * depth + return cls(depth, width) @property def area(self) -> float: @@ -204,7 +186,7 @@ def area(self) -> float: : Area [m²]. """ - return self.alpha * np.pi * self.params.depth.value * self.params.width.value + return self.alpha * np.pi * self.depth * self.width @property @abc.abstractmethod @@ -541,8 +523,8 @@ def calculate_n_pulses( max_crack_width = conductor.width / safety.sf_width_crack max_stress_intensity = material.K_ic / safety.sf_fracture - a = crack.params.depth.value - c = crack.params.width.value + a = crack.depth + c = crack.width K_max = 0.0 # noqa: N806 n_cycles = 0 From b523b0eede9d074dc961a3987ac60b9e51200d93 Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Wed, 10 Sep 2025 17:34:57 +0100 Subject: [PATCH 43/61] =?UTF-8?q?=F0=9F=8E=A8=20Cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bluemira/magnets/case_tf.py | 1 - bluemira/magnets/tfcoil_designer.py | 54 +++++++---------------------- 2 files changed, 12 insertions(+), 43 deletions(-) diff --git a/bluemira/magnets/case_tf.py b/bluemira/magnets/case_tf.py index 5f54c39415..b1778ed08c 100644 --- a/bluemira/magnets/case_tf.py +++ b/bluemira/magnets/case_tf.py @@ -880,7 +880,6 @@ def dx_vault(self): self.rad_theta / 2 ) - def Kx_ps(self, op_cond: OperationalConditions): # noqa: N802 """ Compute the equivalent radial stiffness of the poloidal support (PS) region. diff --git a/bluemira/magnets/tfcoil_designer.py b/bluemira/magnets/tfcoil_designer.py index db0b12e4ae..cb94e95f11 100644 --- a/bluemira/magnets/tfcoil_designer.py +++ b/bluemira/magnets/tfcoil_designer.py @@ -156,50 +156,20 @@ def __init__( def _derived_values(self, optimsiation_params, case_params): # Needed params that are calculated using the base params - a = self.params.R0.value / self.params.A.value - Ri = self.params.R0.value - a - self.params.d.value # noqa: N806 - Re = (self.params.R0.value + a) * (1 / self.params.ripple.value) ** ( # noqa: N806 - 1 / self.params.n_TF.value - ) - B_TF_i = 1.08 * ( - MU_0_2PI - * self.params.n_TF.value - * ( - self.params.B0.value - * self.params.R0.value - / MU_0_2PI - / self.params.n_TF.value - ) - / Ri - ) + R0 = self.params.R0.value + n_TF = self.params.n_TF.value + B0 = self.params.B0.value + + a = R0 / self.params.A.value + Ri = R0 - a - self.params.d.value # noqa: N806 + Re = (R0 + a) * (1 / self.params.ripple.value) ** (1 / n_TF) # noqa: N806 + B_TF_i = 1.08 * (MU_0_2PI * n_TF * (B0 * R0 / MU_0_2PI / n_TF) / Ri) pm = B_TF_i**2 / (2 * MU_0) - t_z = ( - 0.5 - * np.log(Re / Ri) - * MU_0_4PI - * self.params.n_TF.value - * ( - self.params.B0.value - * self.params.R0.value - / MU_0_2PI - / self.params.n_TF.value - ) - ** 2 - ) + t_z = 0.5 * np.log(Re / Ri) * MU_0_4PI * n_TF * (B0 * R0 / MU_0_2PI / n_TF) ** 2 T_op = self.params.T_sc.value + self.params.T_margin.value # noqa: N806 s_y = 1e9 / self.params.safety_factor.value - n_cond = int( - np.floor( - ( - self.params.B0.value - * self.params.R0.value - / MU_0_2PI - / self.params.n_TF.value - ) - / self.params.Iop.value - ) - ) - min_gap_x = 2 * case_params["dy_ps"] # 2 * thickness of the plate before the WP + n_cond = (self.params.B0.value * R0 / MU_0_2PI / n_TF) // self.params.Iop.value + min_gap_x = 2 * (R0 * 2 / 3 * 1e-2) # 2 * thickness of the plate before the WP I_fun = delayed_exp_func( # noqa: N806 self.params.Iop.value, @@ -218,7 +188,7 @@ def _derived_values(self, optimsiation_params, case_params): "t_z": t_z, "T_op": T_op, "s_y": s_y, - "n_cond": n_cond, + "n_cond": int(n_cond), "min_gap_x": min_gap_x, "I_fun": I_fun, "B_fun": B_fun, From 4c4da737e8f1b02b31d376922d86fa061994b211 Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Thu, 11 Sep 2025 09:33:45 +0100 Subject: [PATCH 44/61] =?UTF-8?q?=F0=9F=8E=A8=20Reorganise?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bluemira/magnets/cable.py | 10 +- bluemira/magnets/case_tf.py | 18 +- bluemira/magnets/conductor.py | 16 +- bluemira/magnets/strand.py | 2 +- bluemira/magnets/tfcoil_designer.py | 361 +++++++++++++++------------- bluemira/magnets/winding_pack.py | 6 +- 6 files changed, 223 insertions(+), 190 deletions(-) diff --git a/bluemira/magnets/cable.py b/bluemira/magnets/cable.py index 398fb40b38..5c0e240e2b 100644 --- a/bluemira/magnets/cable.py +++ b/bluemira/magnets/cable.py @@ -460,7 +460,7 @@ class StabilisingStrandRes: ), ) - # OD homogenized structural properties + # OD homogenised structural properties @abstractmethod def Kx(self, op_cond: OperationalConditions): # noqa: N802 """Total equivalent stiffness along x-axis""" @@ -733,7 +733,7 @@ def set_aspect_ratio(self, value: float): """Modify dx in order to get the given aspect ratio""" self.dx = np.sqrt(value * self.area) - # OD homogenized structural properties + # OD homogenised structural properties def Kx(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ Compute the total equivalent stiffness along the x-axis. @@ -964,7 +964,7 @@ def dy(self) -> float: """Cable dimension in the y direction [m]""" return self.dx - # OD homogenized structural properties + # OD homogenised structural properties def Kx(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ Compute the total equivalent stiffness along the x-axis. @@ -1118,14 +1118,14 @@ def dy(self) -> float: """Cable dimension in the y direction [m] (i.e. cable's diameter)""" return self.dx - # OD homogenized structural properties + # OD homogenised structural properties # A structural analysis should be performed to check how much the rectangular # approximation is fine also for the round cable. def Kx(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ Compute the equivalent stiffness of the cable along the x-axis. - This is a homogenized 1D structural property derived from the Young's modulus + This is a homogenised 1D structural property derived from the Young's modulus and the cable's geometry. The stiffness reflects the effective resistance to deformation in the x-direction. diff --git a/bluemira/magnets/case_tf.py b/bluemira/magnets/case_tf.py index b1778ed08c..583c4849f6 100644 --- a/bluemira/magnets/case_tf.py +++ b/bluemira/magnets/case_tf.py @@ -501,7 +501,7 @@ def plot( ax: plt.Axes | None = None, *, show: bool = False, - homogenized: bool = False, + homogenised: bool = False, ) -> plt.Axes: """ Schematic plot of the TF case cross-section including winding packs. @@ -514,8 +514,8 @@ def plot( show: If `True`, displays the plot immediately using `plt.show()`. Default is `False`. - homogenized: - If `True`, plots winding packs as homogenized blocks. + homogenised: + If `True`, plots winding packs as homogenised blocks. If `False`, plots individual conductors inside WPs. Default is `False`. @@ -534,7 +534,7 @@ def plot( for i, wp in enumerate(self.WPs): xc_wp = 0.0 yc_wp = self.R_wp_i[i] - wp.dy / 2 - ax = wp.plot(xc=xc_wp, yc=yc_wp, ax=ax, homogenized=homogenized) + ax = wp.plot(xc=xc_wp, yc=yc_wp, ax=ax, homogenised=homogenised) # Finalize plot ax.set_xlabel("Toroidal direction [m]") @@ -961,14 +961,14 @@ def Kx(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ kx_lat = self.Kx_lat(op_cond) temp = [ - summation([ + reciprocal_summation([ kx_lat[i], w.Kx(op_cond), kx_lat[i], ]) for i, w in enumerate(self.WPs) ] - return reciprocal_summation([self.Kx_ps(op_cond), self.Kx_vault(op_cond), *temp]) + return summation([self.Kx_ps(op_cond), self.Kx_vault(op_cond), *temp]) def Ky_ps(self, op_cond: OperationalConditions): # noqa: N802 """ @@ -1052,14 +1052,14 @@ def Ky(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ ky_lat = self.Ky_lat(op_cond) temp = [ - reciprocal_summation([ + summation([ ky_lat[i], w.Ky(op_cond), ky_lat[i], ]) for i, w in enumerate(self.WPs) ] - return summation([self.Ky_ps(op_cond), self.Ky_vault(op_cond), *temp]) + return reciprocal_summation([self.Ky_ps(op_cond), self.Ky_vault(op_cond), *temp]) def rearrange_conductors_in_wp( self, @@ -1531,7 +1531,7 @@ def optimise_jacket_and_vault( f"{i} iterations. Total error: {tot_err} < {eps}." ) - ax = self.plot(show=False, homogenized=False) + ax = self.plot(show=False, homogenised=False) ax.set_title("Case design after optimisation") plt.show() diff --git a/bluemira/magnets/conductor.py b/bluemira/magnets/conductor.py index ecff40b38f..05df77db4d 100644 --- a/bluemira/magnets/conductor.py +++ b/bluemira/magnets/conductor.py @@ -298,10 +298,10 @@ def Kx(self, op_cond: OperationalConditions) -> float: # noqa: N802 : Axial stiffness [N/m] """ - return reciprocal_summation([ + return summation([ self._Kx_lat_ins(op_cond), self._Kx_lat_jacket(op_cond), - summation([ + reciprocal_summation([ self._Kx_topbot_ins(op_cond), self._Kx_topbot_jacket(op_cond), self.cable.Kx(op_cond), @@ -369,10 +369,10 @@ def Ky(self, op_cond: OperationalConditions) -> float: # noqa: N802 : Axial stiffness [N/m] """ - return reciprocal_summation([ + return summation([ self._Ky_lat_ins(op_cond), self._Ky_lat_jacket(op_cond), - summation([ + reciprocal_summation([ self._Ky_topbot_ins(op_cond), self._Ky_topbot_jacket(op_cond), self.cable.Ky(op_cond), @@ -427,10 +427,10 @@ def _tresca_sigma_jacket( if direction == "x": saf_jacket = (self.cable.dx + 2 * self.dx_jacket) / (2 * self.dx_jacket) - K = reciprocal_summation([ # noqa: N806 + K = summation([ # noqa: N806 2 * self._Ky_lat_ins(op_cond), 2 * self._Ky_lat_jacket(op_cond), - summation([ + reciprocal_summation([ self.cable.Ky(op_cond), self._Ky_topbot_jacket(op_cond) / 2, ]), @@ -441,10 +441,10 @@ def _tresca_sigma_jacket( else: saf_jacket = (self.cable.dy + 2 * self.dy_jacket) / (2 * self.dy_jacket) - K = reciprocal_summation([ # noqa: N806 + K = summation([ # noqa: N806 2 * self._Kx_lat_ins(op_cond), 2 * self._Kx_lat_jacket(op_cond), - summation([ + reciprocal_summation([ self.cable.Kx(op_cond), self._Kx_topbot_jacket(op_cond) / 2, ]), diff --git a/bluemira/magnets/strand.py b/bluemira/magnets/strand.py index 664007aed4..bba7c38f85 100644 --- a/bluemira/magnets/strand.py +++ b/bluemira/magnets/strand.py @@ -31,7 +31,7 @@ class Strand: """ - Represents a strand with a circular cross-section, composed of a homogenized + Represents a strand with a circular cross-section, composed of a homogenised mixture of materials. This class automatically registers itself and its instances. diff --git a/bluemira/magnets/tfcoil_designer.py b/bluemira/magnets/tfcoil_designer.py index cb94e95f11..9488f4de88 100644 --- a/bluemira/magnets/tfcoil_designer.py +++ b/bluemira/magnets/tfcoil_designer.py @@ -5,6 +5,7 @@ # SPDX-License-Identifier: LGPL-2.1-or-later """Designer for TF Coil XY cross section.""" +from collections.abc import Callable from dataclasses import dataclass import matplotlib.pyplot as plt @@ -17,6 +18,7 @@ from bluemira.base.parameter_frame import Parameter, ParameterFrame from bluemira.base.parameter_frame.typed import ParameterFrameLike from bluemira.magnets.cable import RectangularCable +from bluemira.magnets.case_tf import CaseTF from bluemira.magnets.conductor import SymmetricConductor from bluemira.magnets.utils import delayed_exp_func from bluemira.utilities.tools import get_class_from_module @@ -132,7 +134,104 @@ class TFCoilXYDesignerParams(ParameterFrame): # """Convergence threshold for the combined optimisation loop.""" -class TFCoilXYDesigner(Designer): +@dataclass +class DerivedTFCoilXYDesignerParams: + a: float + Ri: float + Re: float + B_TF_i: float + pm: float + t_z: float + T_op: float + s_y: float + n_cond: float + min_gap_x: float + I_fun: Callable[[float], float] + B_fun: Callable[[float], float] + strain: float + + +@dataclass +class TFCoilXY: + case: CaseTF + derived_params: DerivedTFCoilXYDesignerParams + op_config: dict[str, float] + + def plot_I_B(self, ax, n_steps=300): + time_steps = np.linspace( + self.op_config["t0"], self.op_config["Tau_discharge"], n_steps + ) + I_values = [self.derived_params.I_fun(t) for t in time_steps] # noqa: N806 + B_values = [self.derived_params.B_fun(t) for t in time_steps] + + ax.plot(time_steps, I_values, "g", label="Current [A]") + ax.set_ylabel("Current [A]", color="g", fontsize=10) + ax.tick_params(axis="y", labelcolor="g", labelsize=9) + ax.grid(visible=True) + + ax_right = ax.twinx() + ax_right.plot(time_steps, B_values, "m--", label="Magnetic field [T]") + ax_right.set_ylabel("Magnetic field [T]", color="m", fontsize=10) + ax_right.tick_params(axis="y", labelcolor="m", labelsize=9) + + # Labels + ax.set_xlabel("Time [s]", fontsize=10) + ax.tick_params(axis="x", labelsize=9) + + # Combined legend for both sides + lines, labels = ax.get_legend_handles_labels() + lines2, labels2 = ax_right.get_legend_handles_labels() + ax.legend(lines + lines2, labels + labels2, loc="best", fontsize=9) + + ax.figure.tight_layout() + + def plot_cable_temperature_evolution(self, ax, n_steps=100): + solution = self.case.solution + + ax.plot(solution.t, solution.y[0], "r*", label="Simulation points") + time_steps = np.linspace( + self.op_config["t0"], self.op_config["Tau_discharge"], n_steps + ) + ax.plot(time_steps, solution.sol(time_steps)[0], "b", label="Interpolated curve") + ax.grid(visible=True) + ax.set_ylabel("Temperature [K]", fontsize=10) + ax.set_title("Quench temperature evolution", fontsize=11) + ax.legend(fontsize=9) + + ax.tick_params(axis="y", labelcolor="k", labelsize=9) + + props = {"boxstyle": "round", "facecolor": "white", "alpha": 0.8} + ax.text( + 0.65, + 0.5, + self.case.info_text, + transform=ax.transAxes, + fontsize=9, + verticalalignment="top", + bbox=props, + ) + ax.figure.tight_layout() + + def plot_summary(self, n_steps, show=False): + f, (ax_temp, ax_ib) = plt.subplots(2, 1, figsize=(8, 8), sharex=True) + self.plot_cable_temperature_evolution(ax_temp, n_steps) + self.plot_I_B(ax_ib, n_steps * 3) + return f + + def plot( + self, + ax: plt.Axes | None = None, + *, + show: bool = False, + homogenised: bool = False, + ) -> plt.Axes: + return self.case.plot(ax=ax, show=show, homogenised=homogenised) + + def plot_convergence(self): + return self.case.plot_convergence() + + +class TFCoilXYDesigner(Designer[TFCoilXY]): """ Handles initialisation of TF Coil XY cross section from the individual parts: - Strands @@ -154,145 +253,126 @@ def __init__( ): super().__init__(params=params, build_config=build_config) - def _derived_values(self, optimsiation_params, case_params): + def _derived_values(self, op_config): # Needed params that are calculated using the base params R0 = self.params.R0.value n_TF = self.params.n_TF.value B0 = self.params.B0.value - a = R0 / self.params.A.value - Ri = R0 - a - self.params.d.value # noqa: N806 - Re = (R0 + a) * (1 / self.params.ripple.value) ** (1 / n_TF) # noqa: N806 + Ri = R0 - a - self.params.d.value + Re = (R0 + a) * (1 / self.params.ripple.value) ** (1 / n_TF) B_TF_i = 1.08 * (MU_0_2PI * n_TF * (B0 * R0 / MU_0_2PI / n_TF) / Ri) - pm = B_TF_i**2 / (2 * MU_0) t_z = 0.5 * np.log(Re / Ri) * MU_0_4PI * n_TF * (B0 * R0 / MU_0_2PI / n_TF) ** 2 - T_op = self.params.T_sc.value + self.params.T_margin.value # noqa: N806 - s_y = 1e9 / self.params.safety_factor.value - n_cond = (self.params.B0.value * R0 / MU_0_2PI / n_TF) // self.params.Iop.value - min_gap_x = 2 * (R0 * 2 / 3 * 1e-2) # 2 * thickness of the plate before the WP - - I_fun = delayed_exp_func( # noqa: N806 - self.params.Iop.value, - optimsiation_params["Tau_discharge"], - self.params.t_delay.value, - ) - B_fun = delayed_exp_func( - B_TF_i, optimsiation_params["Tau_discharge"], self.params.t_delay.value + return DerivedTFCoilXYDesignerParams( + a=a, + Ri=Ri, + Re=Re, + B_TF_i=B_TF_i, + pm=B_TF_i**2 / (2 * MU_0), + t_z=t_z, + T_op=self.params.T_sc.value + self.params.T_margin.value, + s_y=1e9 / self.params.safety_factor.value, + n_cond=(self.params.B0.value * R0 / MU_0_2PI / n_TF) + // self.params.Iop.value, + # 2 * thickness of the plate before the WP + min_gap_x=2 * (R0 * 2 / 3 * 1e-2), + I_fun=delayed_exp_func( + self.params.Iop.value, + op_config["Tau_discharge"], + self.params.t_delay.value, + ), + B_fun=delayed_exp_func( + B_TF_i, op_config["Tau_discharge"], self.params.t_delay.value + ), + strain=self.params.strain.value, ) - return { - "a": a, - "Ri": Ri, - "Re": Re, - "B_TF_i": B_TF_i, - "pm": pm, - "t_z": t_z, - "T_op": T_op, - "s_y": s_y, - "n_cond": int(n_cond), - "min_gap_x": min_gap_x, - "I_fun": I_fun, - "B_fun": B_fun, - "strain": self.params.strain.value, - } - - def run(self): - """ - Run the TF coil XY design problem. - Returns - ------- - case: - TF case object all parts that make it up. - """ - # configs + def _make_conductor(self, optimisation_params, derived_params, n_WPs, WP_i=0): + # current functionality requires conductors are the same for both WPs + # in future allow for different conductor objects so can vary cable and strands + # between the sets of the winding pack? stab_strand_config = self.build_config.get("stabilising_strand") sc_strand_config = self.build_config.get("superconducting_strand") cable_config = self.build_config.get("cable") conductor_config = self.build_config.get("conductor") - winding_pack_config = self.build_config.get("winding_pack") - case_config = self.build_config.get("case") - # winding pack sets - n_WPs = self.build_config.get("winding_pack").get("sets") - # params + stab_strand_params = self._check_arrays_match( n_WPs, stab_strand_config.get("params") ) sc_strand_params = self._check_arrays_match( n_WPs, sc_strand_config.get("params") ) - cable_params = self._check_arrays_match(n_WPs, cable_config.get("params")) conductor_params = self._check_arrays_match( n_WPs, conductor_config.get("params") ) - winding_pack_params = self._check_arrays_match( - n_WPs, winding_pack_config.get("params") + + cable_params = self._check_arrays_match(n_WPs, cable_config.get("params")) + + stab_strand = self._make_strand(WP_i, stab_strand_config, stab_strand_params) + sc_strand = self._make_strand(WP_i, sc_strand_config, sc_strand_params) + cable = self._make_cable( + stab_strand, sc_strand, WP_i, cable_config, cable_params + ) + # param frame optimisation stuff? + result = cable.optimise_n_stab_ths( + t0=optimisation_params["t0"], + tf=optimisation_params["Tau_discharge"], + initial_temperature=derived_params.T_op, + target_temperature=optimisation_params["hotspot_target_temperature"], + B_fun=derived_params.B_fun, + I_fun=derived_params.I_fun, + bounds=[1, 10000], ) - case_params = case_config.get("params") + + return self._make_conductor_cls(cable, WP_i, conductor_config, conductor_params) + + def run(self): + """ + Run the TF coil XY design problem. + + Returns + ------- + case: + TF case object all parts that make it up. + """ + wp_config = self.build_config.get("winding_pack") + n_WPs = int(wp_config.get("sets")) + optimisation_params = self.build_config.get("optimisation_params") - derived_params = self._derived_values(optimisation_params, case_params) - - winding_pack = [] - for i_WP in range(n_WPs): - if i_WP == 0: - # current functionality requires conductors are the same for both WPs - # in future allow for different conductor objects so can vary cable and strands - # between the sets of the winding pack? - stab_strand = self._make_strand( - i_WP, stab_strand_config, stab_strand_params - ) - sc_strand = self._make_strand(i_WP, sc_strand_config, sc_strand_params) - cable = self._make_cable( - stab_strand, sc_strand, i_WP, cable_config, cable_params - ) - # param frame optimisation stuff? - result = cable.optimise_n_stab_ths( - t0=optimisation_params["t0"], - tf=optimisation_params["Tau_discharge"], - initial_temperature=derived_params["T_op"], - target_temperature=optimisation_params["hotspot_target_temperature"], - B_fun=derived_params["B_fun"], - I_fun=derived_params["I_fun"], - bounds=[1, 10000], - ) - conductor = self._make_conductor( - cable, i_WP, conductor_config, conductor_params - ) - winding_pack += [ - self._make_winding_pack( - conductor, i_WP, winding_pack_config, winding_pack_params - ) - ] + derived_params = self._derived_values(optimisation_params) - case = self._make_case(winding_pack, case_config, case_params) - # param frame optimisation stuff? - case.rearrange_conductors_in_wp( - n_conductors=derived_params["n_cond"], - wp_reduction_factor=optimisation_params["wp_reduction_factor"], - min_gap_x=derived_params["min_gap_x"], - n_layers_reduction=optimisation_params["n_layers_reduction"], - layout=optimisation_params["layout"], + conductor, conductor_result = self._make_conductor( + optimisation_params, derived_params, n_WPs, WP_i=0 ) + wp_params = self._check_arrays_match(n_WPs, wp_config.pop("params")) + winding_pack = [ + self._make_winding_pack(conductor, i_WP, wp_config, wp_params) + for i_WP in range(n_WPs) + ] + + case = self._make_case(winding_pack, derived_params, optimisation_params) + # param frame optimisation stuff? case.optimise_jacket_and_vault( - pm=derived_params["pm"], - fz=derived_params["t_z"], + pm=derived_params.pm, + fz=derived_params.t_z, op_cond=OperationalConditions( - temperature=derived_params["T_op"], - magnetic_field=derived_params["B_TF_i"], - strain=derived_params["strain"], + temperature=derived_params.T_op, + magnetic_field=derived_params.B_TF_i, + strain=derived_params.strain, ), - allowable_sigma=derived_params["s_y"], + allowable_sigma=derived_params.s_y, bounds_cond_jacket=optimisation_params["bounds_cond_jacket"], bounds_dy_vault=optimisation_params["bounds_dy_vault"], layout=optimisation_params["layout"], wp_reduction_factor=optimisation_params["wp_reduction_factor"], - min_gap_x=derived_params["min_gap_x"], + min_gap_x=derived_params.min_gap_x, n_layers_reduction=optimisation_params["n_layers_reduction"], max_niter=optimisation_params["max_niter"], eps=optimisation_params["eps"], - n_conds=derived_params["n_cond"], + n_conds=derived_params.n_cond, ) - return case + return TFCoilXY(case, derived_params, optimisation_params) def _check_arrays_match(self, n_WPs, param_list): if n_WPs > 1: @@ -374,7 +454,7 @@ def _make_cable(self, stab_strand, sc_strand, i_WP, config, params): ), ) - def _make_conductor(self, cable, i_WP, config, params): + def _make_conductor_cls(self, cable, i_WP, config, params): cls_name = config["class"] conductor_cls = get_class_from_module( cls_name, default_module="bluemira.magnets.conductor" @@ -408,13 +488,16 @@ def _make_winding_pack(self, conductor, i_WP, config, params): name="winding_pack", ) - def _make_case(self, WPs, config, params): # noqa: N803 + def _make_case(self, WPs, derived_params, optimisation_params): # noqa: N803 + config = self.build_config.get("case") + params = config.get("params") + cls_name = config["class"] case_cls = get_class_from_module( cls_name, default_module="bluemira.magnets.case_tf" ) - return case_cls( + case = case_cls( Ri=params["Ri"], theta_TF=params["theta_TF"], dy_ps=params["dy_ps"], @@ -424,62 +507,12 @@ def _make_case(self, WPs, config, params): # noqa: N803 name=config.get("name", cls_name.rsplit("::", 1)[-1]), ) - -def plot_cable_temperature_evolution(result, t0, tf, ax, n_steps=100): - solution = result.solution - - ax.plot(solution.t, solution.y[0], "r*", label="Simulation points") - time_steps = np.linspace(t0, tf, n_steps) - ax.plot(time_steps, solution.sol(time_steps)[0], "b", label="Interpolated curve") - ax.grid(visible=True) - ax.set_ylabel("Temperature [K]", fontsize=10) - ax.set_title("Quench temperature evolution", fontsize=11) - ax.legend(fontsize=9) - - ax.tick_params(axis="y", labelcolor="k", labelsize=9) - - props = {"boxstyle": "round", "facecolor": "white", "alpha": 0.8} - ax.text( - 0.65, - 0.5, - result.info_text, - transform=ax.transAxes, - fontsize=9, - verticalalignment="top", - bbox=props, - ) - ax.figure.tight_layout() - - -def plot_I_B(I_fun, B_fun, t0, tf, ax, n_steps=300): - time_steps = np.linspace(t0, tf, n_steps) - I_values = [I_fun(t) for t in time_steps] # noqa: N806 - B_values = [B_fun(t) for t in time_steps] - - ax.plot(time_steps, I_values, "g", label="Current [A]") - ax.set_ylabel("Current [A]", color="g", fontsize=10) - ax.tick_params(axis="y", labelcolor="g", labelsize=9) - ax.grid(visible=True) - - ax_right = ax.twinx() - ax_right.plot(time_steps, B_values, "m--", label="Magnetic field [T]") - ax_right.set_ylabel("Magnetic field [T]", color="m", fontsize=10) - ax_right.tick_params(axis="y", labelcolor="m", labelsize=9) - - # Labels - ax.set_xlabel("Time [s]", fontsize=10) - ax.tick_params(axis="x", labelsize=9) - - # Combined legend for both sides - lines, labels = ax.get_legend_handles_labels() - lines2, labels2 = ax_right.get_legend_handles_labels() - ax.legend(lines + lines2, labels + labels2, loc="best", fontsize=9) - - ax.figure.tight_layout() - - -def plot_summary(result, t0, tf, I_fun, B_fun, n_steps, show=False): - f, (ax_temp, ax_ib) = plt.subplots(2, 1, figsize=(8, 8), sharex=True) - plot_cable_temperature_evolution(result, t0, tf, ax_temp, n_steps) - plot_I_B(I_fun, B_fun, t0, tf, ax_ib, n_steps * 3) - return f + # param frame optimisation stuff? + case.rearrange_conductors_in_wp( + n_conductors=derived_params.n_cond, + wp_reduction_factor=optimisation_params["wp_reduction_factor"], + min_gap_x=derived_params.min_gap_x, + n_layers_reduction=optimisation_params["n_layers_reduction"], + layout=optimisation_params["layout"], + ) + return case diff --git a/bluemira/magnets/winding_pack.py b/bluemira/magnets/winding_pack.py index 1fcaa6f8e6..e02589a31a 100644 --- a/bluemira/magnets/winding_pack.py +++ b/bluemira/magnets/winding_pack.py @@ -119,7 +119,7 @@ def plot( *, show: bool = False, ax: plt.Axes | None = None, - homogenized: bool = True, + homogenised: bool = True, ) -> plt.Axes: """ Plot the winding pack geometry. @@ -134,7 +134,7 @@ def plot( If True, immediately show the plot. ax: Axes object to draw on. - homogenized: + homogenised: If True, plot as a single block. Otherwise, plot individual conductors. Returns @@ -159,7 +159,7 @@ def plot( ax.fill(points_ext[:, 0], points_ext[:, 1], "gold", snap=False) ax.plot(points_ext[:, 0], points_ext[:, 1], "k") - if not homogenized: + if not homogenised: for i in range(self.nx): for j in range(self.ny): xc_c = xc - self.dx / 2 + (i + 0.5) * self.conductor.dx From 686cfe7c789ad27bbad4b5931442a2a7660b124f Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Thu, 11 Sep 2025 09:52:31 +0100 Subject: [PATCH 45/61] =?UTF-8?q?=F0=9F=94=A5=20Remove=20while=20loop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bluemira/magnets/case_tf.py | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/bluemira/magnets/case_tf.py b/bluemira/magnets/case_tf.py index 583c4849f6..86cd433a33 100644 --- a/bluemira/magnets/case_tf.py +++ b/bluemira/magnets/case_tf.py @@ -1111,17 +1111,13 @@ def rearrange_conductors_in_wp( remaining_conductors = n_conductors # maximum number of winding packs in WPs i_max = 50 - i = 0 - while i < i_max and remaining_conductors > 0: - i += 1 - + n_layers_max = 0 + for i in range(i_max): # maximum number of turns on the considered WP - if i == 1: + if i == 0: n_layers_max = math.floor(dx_WP / conductor.dx) if layout == "pancake": - n_layers_max = math.floor(dx_WP / conductor.dx / 2) * 2 - if n_layers_max == 0: - n_layers_max = 2 + n_layers_max = (math.floor(dx_WP / conductor.dx / 2) * 2) or 2 else: n_layers_max -= n_layers_reduction @@ -1177,17 +1173,18 @@ def rearrange_conductors_in_wp( ) remaining_conductors -= n_layers_max * n_turns_max - if remaining_conductors < 0: - bluemira_warn( - f"{abs(remaining_conductors)}/{n_layers_max * n_turns_max}" - f"have been added to complete the last winding pack (nx" - f"={n_layers_max}, ny={n_turns_max})." - ) - R_wp_i -= n_turns_max * conductor.dy # noqa: N806 debug_msg.append( f"n_layers_max: {n_layers_max}, n_turns_max: {n_turns_max}" ) + if remaining_conductors <= 0: + if remaining_conductors < 0: + bluemira_warn( + f"{abs(remaining_conductors)}/{n_layers_max * n_turns_max}" + f"have been added to complete the last winding pack (nx" + f"={n_layers_max}, ny={n_turns_max})." + ) + break bluemira_debug("\n".join(debug_msg)) self.WPs = WPs From d4f22a6fd7bedc966073e6acc89cf89c5db3aa85 Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Thu, 11 Sep 2025 13:48:14 +0100 Subject: [PATCH 46/61] =?UTF-8?q?=F0=9F=8E=A8=20Small=20changes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bluemira/magnets/tfcoil_designer.py | 90 +++++++++++++++++++++++-- examples/magnets/example_tf_creation.py | 2 +- 2 files changed, 84 insertions(+), 8 deletions(-) diff --git a/bluemira/magnets/tfcoil_designer.py b/bluemira/magnets/tfcoil_designer.py index 9488f4de88..0761f10eaf 100644 --- a/bluemira/magnets/tfcoil_designer.py +++ b/bluemira/magnets/tfcoil_designer.py @@ -7,14 +7,22 @@ from collections.abc import Callable from dataclasses import dataclass +from typing import Any import matplotlib.pyplot as plt import numpy as np +import numpy.typing as npt from matproplib import OperationalConditions from matproplib.material import MaterialFraction +from scipy.optimize import minimize_scalar from bluemira.base.constants import MU_0, MU_0_2PI, MU_0_4PI from bluemira.base.designer import Designer +from bluemira.base.look_and_feel import ( + bluemira_print, + bluemira_warn, + bluemira_debug, +) from bluemira.base.parameter_frame import Parameter, ParameterFrame from bluemira.base.parameter_frame.typed import ParameterFrameLike from bluemira.magnets.cable import RectangularCable @@ -154,6 +162,7 @@ class DerivedTFCoilXYDesignerParams: @dataclass class TFCoilXY: case: CaseTF + convergence: npt.NDArray derived_params: DerivedTFCoilXYDesignerParams op_config: dict[str, float] @@ -228,7 +237,46 @@ def plot( return self.case.plot(ax=ax, show=show, homogenised=homogenised) def plot_convergence(self): - return self.case.plot_convergence() + """ + Plot the evolution of thicknesses and error values over optimisation iterations. + + Raises + ------ + RuntimeError + If no convergence data available + """ + iterations = self.convergence[:, 0] + dy_jacket = self.convergence[:, 1] + dy_vault = self.convergence[:, 2] + err_dy_jacket = self.convergence[:, 3] + err_dy_vault = self.convergence[:, 4] + dy_wp_tot = self.convergence[:, 5] + Ri_minus_Rk = self.convergence[:, 6] # noqa: N806 + + _, axs = plt.subplots(2, 1, figsize=(10, 10), sharex=True) + + # Top subplot: Thicknesses + axs[0].plot(iterations, dy_jacket, marker="o", label="dy_jacket [m]") + axs[0].plot(iterations, dy_vault, marker="s", label="dy_vault [m]") + axs[0].plot(iterations, dy_wp_tot, marker="^", label="dy_wp_tot [m]") + axs[0].plot(iterations, Ri_minus_Rk, marker="v", label="Ri - Rk [m]") + axs[0].set_ylabel("Thickness [m]") + axs[0].set_title("Evolution of Jacket, Vault, and WP Thicknesses") + axs[0].legend() + axs[0].grid(visible=True) + + # Bottom subplot: Errors + axs[1].plot(iterations, err_dy_jacket, marker="o", label="err_dy_jacket") + axs[1].plot(iterations, err_dy_vault, marker="s", label="err_dy_vault") + axs[1].set_ylabel("Relative Error") + axs[1].set_xlabel("Iteration") + axs[1].set_title("Evolution of Errors during Optimisation") + axs[1].set_yscale("log") # Log scale for better visibility if needed + axs[1].legend() + axs[1].grid(visible=True) + + plt.tight_layout() + plt.show() class TFCoilXYDesigner(Designer[TFCoilXY]): @@ -248,7 +296,7 @@ class TFCoilXYDesigner(Designer[TFCoilXY]): def __init__( self, - params: dict | ParameterFrameLike, + params: ParameterFrameLike, build_config: dict, ): super().__init__(params=params, build_config=build_config) @@ -272,8 +320,9 @@ def _derived_values(self, op_config): t_z=t_z, T_op=self.params.T_sc.value + self.params.T_margin.value, s_y=1e9 / self.params.safety_factor.value, - n_cond=(self.params.B0.value * R0 / MU_0_2PI / n_TF) - // self.params.Iop.value, + n_cond=int( + self.params.B0.value * R0 / MU_0_2PI / n_TF // self.params.Iop.value + ), # 2 * thickness of the plate before the WP min_gap_x=2 * (R0 * 2 / 3 * 1e-2), I_fun=delayed_exp_func( @@ -433,7 +482,7 @@ def _make_strand(self, i_WP, config, params): name="stab_strand", ) - def _make_cable(self, stab_strand, sc_strand, i_WP, config, params): + def _make_cable_cls(self, stab_strand, sc_strand, i_WP, config, params): cls_name = config["class"] cable_cls = get_class_from_module( cls_name, default_module="bluemira.magnets.cable" @@ -454,6 +503,26 @@ def _make_cable(self, stab_strand, sc_strand, i_WP, config, params): ), ) + def _make_cable(self, WP_i, n_WPs): + stab_strand_config = self.build_config.get("stabilising_strand") + sc_strand_config = self.build_config.get("superconducting_strand") + cable_config = self.build_config.get("cable") + + stab_strand_params = self._check_arrays_match( + n_WPs, stab_strand_config.get("params") + ) + sc_strand_params = self._check_arrays_match( + n_WPs, sc_strand_config.get("params") + ) + + cable_params = self._check_arrays_match(n_WPs, cable_config.get("params")) + + stab_strand = self._make_strand(WP_i, stab_strand_config, stab_strand_params) + sc_strand = self._make_strand(WP_i, sc_strand_config, sc_strand_params) + return self._make_cable_cls( + stab_strand, sc_strand, WP_i, cable_config, cable_params + ) + def _make_conductor_cls(self, cable, i_WP, config, params): cls_name = config["class"] conductor_cls = get_class_from_module( @@ -476,6 +545,13 @@ def _make_conductor_cls(self, cable, i_WP, config, params): ), ) + def _make_conductor(self, cable, n_WPs, WP_i=0): + conductor_config = self.build_config.get("conductor") + conductor_params = self._check_arrays_match( + n_WPs, conductor_config.get("params") + ) + return self._make_conductor_cls(cable, WP_i, conductor_config, conductor_params) + def _make_winding_pack(self, conductor, i_WP, config, params): cls_name = config["class"] winding_pack_cls = get_class_from_module( @@ -483,8 +559,8 @@ def _make_winding_pack(self, conductor, i_WP, config, params): ) return winding_pack_cls( conductor=conductor, - nx=params["nx"][i_WP], - ny=params["ny"][i_WP], + nx=int(params["nx"][i_WP]), + ny=int(params["ny"][i_WP]), name="winding_pack", ) diff --git a/examples/magnets/example_tf_creation.py b/examples/magnets/example_tf_creation.py index 46388d935c..f61ed132fc 100644 --- a/examples/magnets/example_tf_creation.py +++ b/examples/magnets/example_tf_creation.py @@ -116,5 +116,5 @@ "strain": {"value": 0.0055, "unit": ""}, } tf_coil_xy = TFCoilXYDesigner(params=params, build_config=config).execute() -tf_coil_xy.plot(show=True, homogenized=False) +tf_coil_xy.plot(show=True, homogenised=False) # tf_coil_xy.plot_convergence() From 877620097a2906d5d3a1fed8a89914b6f69d1bfa Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Thu, 11 Sep 2025 13:53:26 +0100 Subject: [PATCH 47/61] =?UTF-8?q?=F0=9F=9A=9A=20Rearrange2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bluemira/magnets/tfcoil_designer.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/bluemira/magnets/tfcoil_designer.py b/bluemira/magnets/tfcoil_designer.py index 0761f10eaf..8cba31dc98 100644 --- a/bluemira/magnets/tfcoil_designer.py +++ b/bluemira/magnets/tfcoil_designer.py @@ -359,9 +359,7 @@ def _make_conductor(self, optimisation_params, derived_params, n_WPs, WP_i=0): stab_strand = self._make_strand(WP_i, stab_strand_config, stab_strand_params) sc_strand = self._make_strand(WP_i, sc_strand_config, sc_strand_params) - cable = self._make_cable( - stab_strand, sc_strand, WP_i, cable_config, cable_params - ) + cable = self._make_cable(WP_i, n_WPs) # param frame optimisation stuff? result = cable.optimise_n_stab_ths( t0=optimisation_params["t0"], @@ -390,7 +388,7 @@ def run(self): optimisation_params = self.build_config.get("optimisation_params") derived_params = self._derived_values(optimisation_params) - conductor, conductor_result = self._make_conductor( + conductor = self._make_conductor( optimisation_params, derived_params, n_WPs, WP_i=0 ) wp_params = self._check_arrays_match(n_WPs, wp_config.pop("params")) @@ -421,7 +419,9 @@ def run(self): eps=optimisation_params["eps"], n_conds=derived_params.n_cond, ) - return TFCoilXY(case, derived_params, optimisation_params) + return TFCoilXY( + case, case._convergence_array, derived_params, optimisation_params + ) def _check_arrays_match(self, n_WPs, param_list): if n_WPs > 1: @@ -545,13 +545,6 @@ def _make_conductor_cls(self, cable, i_WP, config, params): ), ) - def _make_conductor(self, cable, n_WPs, WP_i=0): - conductor_config = self.build_config.get("conductor") - conductor_params = self._check_arrays_match( - n_WPs, conductor_config.get("params") - ) - return self._make_conductor_cls(cable, WP_i, conductor_config, conductor_params) - def _make_winding_pack(self, conductor, i_WP, config, params): cls_name = config["class"] winding_pack_cls = get_class_from_module( From 4bd70132afa47340afdebdc6c733e0840387efe9 Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Thu, 11 Sep 2025 14:04:34 +0100 Subject: [PATCH 48/61] =?UTF-8?q?=F0=9F=9A=9A=20Rearrange3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bluemira/magnets/tfcoil_designer.py | 110 ++++++++++++++-------------- 1 file changed, 55 insertions(+), 55 deletions(-) diff --git a/bluemira/magnets/tfcoil_designer.py b/bluemira/magnets/tfcoil_designer.py index 8cba31dc98..b3911c2d80 100644 --- a/bluemira/magnets/tfcoil_designer.py +++ b/bluemira/magnets/tfcoil_designer.py @@ -336,42 +336,26 @@ def _derived_values(self, op_config): strain=self.params.strain.value, ) - def _make_conductor(self, optimisation_params, derived_params, n_WPs, WP_i=0): - # current functionality requires conductors are the same for both WPs - # in future allow for different conductor objects so can vary cable and strands - # between the sets of the winding pack? - stab_strand_config = self.build_config.get("stabilising_strand") - sc_strand_config = self.build_config.get("superconducting_strand") - cable_config = self.build_config.get("cable") - conductor_config = self.build_config.get("conductor") - - stab_strand_params = self._check_arrays_match( - n_WPs, stab_strand_config.get("params") - ) - sc_strand_params = self._check_arrays_match( - n_WPs, sc_strand_config.get("params") - ) - conductor_params = self._check_arrays_match( - n_WPs, conductor_config.get("params") - ) - - cable_params = self._check_arrays_match(n_WPs, cable_config.get("params")) + def B_TF_r(self, tf_current, r): + """ + Compute the magnetic field generated by the TF coils, + including ripple correction. - stab_strand = self._make_strand(WP_i, stab_strand_config, stab_strand_params) - sc_strand = self._make_strand(WP_i, sc_strand_config, sc_strand_params) - cable = self._make_cable(WP_i, n_WPs) - # param frame optimisation stuff? - result = cable.optimise_n_stab_ths( - t0=optimisation_params["t0"], - tf=optimisation_params["Tau_discharge"], - initial_temperature=derived_params.T_op, - target_temperature=optimisation_params["hotspot_target_temperature"], - B_fun=derived_params.B_fun, - I_fun=derived_params.I_fun, - bounds=[1, 10000], - ) + Parameters + ---------- + tf_current : float + Toroidal field coil current [A]. + n_TF : int + Number of toroidal field coils. + r : float + Radial position from the tokamak center [m]. - return self._make_conductor_cls(cable, WP_i, conductor_config, conductor_params) + Returns + ------- + float + Magnetic field intensity [T]. + """ + return 1.08 * (MU_0_2PI * self.params.n_TF.value * tf_current / r) def run(self): """ @@ -436,27 +420,6 @@ def _check_arrays_match(self, n_WPs, param_list): "Value should be an integer >= 1." ) - def B_TF_r(self, tf_current, r): - """ - Compute the magnetic field generated by the TF coils, - including ripple correction. - - Parameters - ---------- - tf_current : float - Toroidal field coil current [A]. - n_TF : int - Number of toroidal field coils. - r : float - Radial position from the tokamak center [m]. - - Returns - ------- - float - Magnetic field intensity [T]. - """ - return 1.08 * (MU_0_2PI * self.params.n_TF.value * tf_current / r) - def _make_strand(self, i_WP, config, params): cls_name = config["class"] stab_strand_cls = get_class_from_module( @@ -545,6 +508,43 @@ def _make_conductor_cls(self, cable, i_WP, config, params): ), ) + def _make_conductor(self, optimisation_params, derived_params, n_WPs, WP_i=0): + # current functionality requires conductors are the same for both WPs + # in future allow for different conductor objects so can vary cable and strands + # between the sets of the winding pack? + stab_strand_config = self.build_config.get("stabilising_strand") + sc_strand_config = self.build_config.get("superconducting_strand") + cable_config = self.build_config.get("cable") + conductor_config = self.build_config.get("conductor") + + stab_strand_params = self._check_arrays_match( + n_WPs, stab_strand_config.get("params") + ) + sc_strand_params = self._check_arrays_match( + n_WPs, sc_strand_config.get("params") + ) + conductor_params = self._check_arrays_match( + n_WPs, conductor_config.get("params") + ) + + cable_params = self._check_arrays_match(n_WPs, cable_config.get("params")) + + stab_strand = self._make_strand(WP_i, stab_strand_config, stab_strand_params) + sc_strand = self._make_strand(WP_i, sc_strand_config, sc_strand_params) + cable = self._make_cable(WP_i, n_WPs) + # param frame optimisation stuff? + result = cable.optimise_n_stab_ths( + t0=optimisation_params["t0"], + tf=optimisation_params["Tau_discharge"], + initial_temperature=derived_params.T_op, + target_temperature=optimisation_params["hotspot_target_temperature"], + B_fun=derived_params.B_fun, + I_fun=derived_params.I_fun, + bounds=[1, 10000], + ) + + return self._make_conductor_cls(cable, WP_i, conductor_config, conductor_params) + def _make_winding_pack(self, conductor, i_WP, config, params): cls_name = config["class"] winding_pack_cls = get_class_from_module( From 0e81ebc113bcfe9b5827ed7d883665913d64f1f9 Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Thu, 11 Sep 2025 15:51:55 +0100 Subject: [PATCH 49/61] =?UTF-8?q?=F0=9F=9A=9A=20Rearrange4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bluemira/magnets/cable.py | 176 ++++++---------------------- bluemira/magnets/tfcoil_designer.py | 149 ++++++++++++++++++----- 2 files changed, 154 insertions(+), 171 deletions(-) diff --git a/bluemira/magnets/cable.py b/bluemira/magnets/cable.py index 5c0e240e2b..9a4f58c3dd 100644 --- a/bluemira/magnets/cable.py +++ b/bluemira/magnets/cable.py @@ -9,19 +9,15 @@ from __future__ import annotations from abc import ABC, abstractmethod -from dataclasses import dataclass from typing import TYPE_CHECKING, Any import matplotlib.pyplot as plt import numpy as np from matproplib import OperationalConditions from scipy.integrate import solve_ivp -from scipy.optimize import minimize_scalar from bluemira.base.look_and_feel import ( bluemira_debug, - bluemira_error, - bluemira_print, bluemira_warn, ) from bluemira.magnets.strand import ( @@ -300,165 +296,67 @@ def _temperature_evolution( return solution - def optimise_n_stab_ths( + def final_temperature_difference( self, + n_stab: int, t0: float, tf: float, initial_temperature: float, target_temperature: float, B_fun: Callable[[float], float], I_fun: Callable[[float], float], # noqa: N803 - bounds: np.ndarray | None = None, - ): + ) -> float: """ - Optimise the number of stabiliser strand in the superconducting cable using a - 0-D hot spot criteria. + Compute the absolute temperature difference at final time between the + simulated and target temperatures. + + This method modifies the private attribute `_n_stab_strand` to update the + cable configuration, simulates the temperature evolution over time, and + returns the absolute difference between the final temperature and the + specified target. Parameters ---------- + n_stab: + Number of stabiliser strands to set temporarily for this simulation. t0: - Initial time [s]. + Initial time of the simulation [s]. tf: - Final time [s]. + Final time of the simulation [s]. initial_temperature: - Temperature [K] at initial time. + Temperature at the start of the simulation [K]. target_temperature: - Target temperature [K] at final time. - B_fun : - Magnetic field [T] as a time-dependent function. - I_fun : - Current [A] as a time-dependent function. - bounds: - Lower and upper limits for the number of stabiliser strands. + Desired temperature at the end of the simulation [K]. + B_fun: + Magnetic field as a time-dependent function [T]. + I_fun: + Current as a time-dependent function [A]. Returns ------- : - The result of the optimisation process. - - Raises - ------ - ValueError - If the optimisiation process does not converge. + Absolute difference between the simulated final temperature and the + target temperature [K]. Notes ----- - - The number of stabiliser strands in the cable is modified directly. - - Cooling material contribution is neglected when applying the hot spot criteria. + - This method is typically used as a cost function for optimisation routines + (e.g., minimizing the temperature error by tuning `n_stab`). + - It modifies the internal state `self._n_stab_strand`, which may affect + subsequent evaluations unless restored. """ - - def final_temperature_difference( - n_stab: int, - t0: float, - tf: float, - initial_temperature: float, - target_temperature: float, - B_fun: Callable[[float], float], - I_fun: Callable[[float], float], # noqa: N803 - ) -> float: - """ - Compute the absolute temperature difference at final time between the - simulated and target temperatures. - - This method modifies the private attribute `_n_stab_strand` to update the - cable configuration, simulates the temperature evolution over time, and - returns the absolute difference between the final temperature and the - specified target. - - Parameters - ---------- - n_stab: - Number of stabiliser strands to set temporarily for this simulation. - t0: - Initial time of the simulation [s]. - tf: - Final time of the simulation [s]. - initial_temperature: - Temperature at the start of the simulation [K]. - target_temperature: - Desired temperature at the end of the simulation [K]. - B_fun: - Magnetic field as a time-dependent function [T]. - I_fun: - Current as a time-dependent function [A]. - - Returns - ------- - : - Absolute difference between the simulated final temperature and the - target temperature [K]. - - Notes - ----- - - This method is typically used as a cost function for optimisation routines - (e.g., minimizing the temperature error by tuning `n_stab`). - - It modifies the internal state `self._n_stab_strand`, which may affect - subsequent evaluations unless restored. - """ - self.n_stab_strand = n_stab - - solution = self._temperature_evolution( - t0=t0, - tf=tf, - initial_temperature=initial_temperature, - B_fun=B_fun, - I_fun=I_fun, - ) - final_temperature = float(solution.y[0][-1]) - # diff = abs(final_temperature - target_temperature) - return abs(final_temperature - target_temperature) - - result = minimize_scalar( - fun=final_temperature_difference, - args=(t0, tf, initial_temperature, target_temperature, B_fun, I_fun), - bounds=bounds, - method=None if bounds is None else "bounded", - ) - - if not result.success: - raise ValueError( - "n_stab optimisation did not converge. Check your input parameters " - "or initial bracket." - ) - - # Here we re-ensure the n_stab_strand to be an integer - self.n_stab_strand = int(np.ceil(self.n_stab_strand)) - - solution = self._temperature_evolution(t0, tf, initial_temperature, B_fun, I_fun) - final_temperature = solution.y[0][-1] - - if final_temperature > target_temperature: - bluemira_error( - f"Final temperature ({final_temperature:.2f} K) exceeds target " - f"temperature " - f"({target_temperature} K) even with maximum n_stab = " - f"{self.n_stab_strand}." - ) - raise ValueError( - "Optimisation failed to keep final temperature ≤ target. " - "Try increasing the upper bound of n_stab or adjusting cable parameters." - ) - bluemira_print(f"Optimal n_stab: {self.n_stab_strand}") - bluemira_print( - f"Final temperature with optimal n_stab: {final_temperature:.2f} Kelvin" - ) - - @dataclass - class StabilisingStrandRes: - solution: Any - info_text: str - - return StabilisingStrandRes( - solution, - ( - f"Target T: {target_temperature:.2f} K\n" - f"Initial T: {initial_temperature:.2f} K\n" - f"SC Strand: {self.sc_strand.name}\n" - f"n. sc. strand = {self.n_sc_strand}\n" - f"Stab. strand = {self.stab_strand.name}\n" - f"n. stab. strand = {self.n_stab_strand}\n" - ), + self.n_stab_strand = n_stab + + solution = self._temperature_evolution( + t0=t0, + tf=tf, + initial_temperature=initial_temperature, + B_fun=B_fun, + I_fun=I_fun, ) + final_temperature = float(solution.y[0][-1]) + # diff = abs(final_temperature - target_temperature) + return abs(final_temperature - target_temperature) # OD homogenised structural properties @abstractmethod diff --git a/bluemira/magnets/tfcoil_designer.py b/bluemira/magnets/tfcoil_designer.py index b3911c2d80..3e41924f04 100644 --- a/bluemira/magnets/tfcoil_designer.py +++ b/bluemira/magnets/tfcoil_designer.py @@ -19,9 +19,8 @@ from bluemira.base.constants import MU_0, MU_0_2PI, MU_0_4PI from bluemira.base.designer import Designer from bluemira.base.look_and_feel import ( + bluemira_error, bluemira_print, - bluemira_warn, - bluemira_debug, ) from bluemira.base.parameter_frame import Parameter, ParameterFrame from bluemira.base.parameter_frame.typed import ParameterFrameLike @@ -372,9 +371,18 @@ def run(self): optimisation_params = self.build_config.get("optimisation_params") derived_params = self._derived_values(optimisation_params) - conductor = self._make_conductor( - optimisation_params, derived_params, n_WPs, WP_i=0 + # param frame optimisation stuff? + cable = self.optimise_cable_n_stab_ths( + self._make_cable(n_WPs, WP_i=0), + t0=optimisation_params["t0"], + tf=optimisation_params["Tau_discharge"], + initial_temperature=derived_params.T_op, + target_temperature=optimisation_params["hotspot_target_temperature"], + B_fun=derived_params.B_fun, + I_fun=derived_params.I_fun, + bounds=[1, 10000], ) + conductor = self._make_conductor(cable.cable, n_WPs, WP_i=0) wp_params = self._check_arrays_match(n_WPs, wp_config.pop("params")) winding_pack = [ self._make_winding_pack(conductor, i_WP, wp_config, wp_params) @@ -407,6 +415,109 @@ def run(self): case, case._convergence_array, derived_params, optimisation_params ) + def optimise_cable_n_stab_ths( + self, + cable, + t0: float, + tf: float, + initial_temperature: float, + target_temperature: float, + B_fun: Callable[[float], float], + I_fun: Callable[[float], float], # noqa: N803 + bounds: np.ndarray | None = None, + ): + """ + Optimise the number of stabiliser strand in the superconducting cable using a + 0-D hot spot criteria. + + Parameters + ---------- + t0: + Initial time [s]. + tf: + Final time [s]. + initial_temperature: + Temperature [K] at initial time. + target_temperature: + Target temperature [K] at final time. + B_fun : + Magnetic field [T] as a time-dependent function. + I_fun : + Current [A] as a time-dependent function. + bounds: + Lower and upper limits for the number of stabiliser strands. + + Returns + ------- + : + The result of the optimisation process. + + Raises + ------ + ValueError + If the optimisiation process does not converge. + + Notes + ----- + - The number of stabiliser strands in the cable is modified directly. + - Cooling material contribution is neglected when applying the hot spot criteria. + """ + result = minimize_scalar( + fun=cable.final_temperature_difference, + args=(t0, tf, initial_temperature, target_temperature, B_fun, I_fun), + bounds=bounds, + method=None if bounds is None else "bounded", + ) + + if not result.success: + raise ValueError( + "n_stab optimisation did not converge. Check your input parameters " + "or initial bracket." + ) + + # Here we re-ensure the n_stab_strand to be an integer + cable.n_stab_strand = int(np.ceil(cable.n_stab_strand)) + + solution = cable._temperature_evolution( + t0, tf, initial_temperature, B_fun, I_fun + ) + final_temperature = solution.y[0][-1] + + if final_temperature > target_temperature: + bluemira_error( + f"Final temperature ({final_temperature:.2f} K) exceeds target " + f"temperature " + f"({target_temperature} K) even with maximum n_stab = " + f"{cable.n_stab_strand}." + ) + raise ValueError( + "Optimisation failed to keep final temperature ≤ target. " + "Try increasing the upper bound of n_stab or adjusting cable parameters." + ) + bluemira_print(f"Optimal n_stab: {cable.n_stab_strand}") + bluemira_print( + f"Final temperature with optimal n_stab: {final_temperature:.2f} Kelvin" + ) + + @dataclass + class StabilisingStrandRes: + cable: Any + solution: Any + info_text: str + + return StabilisingStrandRes( + cable, + solution, + ( + f"Target T: {target_temperature:.2f} K\n" + f"Initial T: {initial_temperature:.2f} K\n" + f"SC Strand: {cable.sc_strand.name}\n" + f"n. sc. strand = {cable.n_sc_strand}\n" + f"Stab. strand = {cable.stab_strand.name}\n" + f"n. stab. strand = {cable.n_stab_strand}\n" + ), + ) + def _check_arrays_match(self, n_WPs, param_list): if n_WPs > 1: for param in param_list: @@ -466,7 +577,7 @@ def _make_cable_cls(self, stab_strand, sc_strand, i_WP, config, params): ), ) - def _make_cable(self, WP_i, n_WPs): + def _make_cable(self, n_WPs, WP_i): stab_strand_config = self.build_config.get("stabilising_strand") sc_strand_config = self.build_config.get("superconducting_strand") cable_config = self.build_config.get("cable") @@ -508,41 +619,15 @@ def _make_conductor_cls(self, cable, i_WP, config, params): ), ) - def _make_conductor(self, optimisation_params, derived_params, n_WPs, WP_i=0): + def _make_conductor(self, cable, n_WPs, WP_i=0): # current functionality requires conductors are the same for both WPs # in future allow for different conductor objects so can vary cable and strands # between the sets of the winding pack? - stab_strand_config = self.build_config.get("stabilising_strand") - sc_strand_config = self.build_config.get("superconducting_strand") - cable_config = self.build_config.get("cable") conductor_config = self.build_config.get("conductor") - - stab_strand_params = self._check_arrays_match( - n_WPs, stab_strand_config.get("params") - ) - sc_strand_params = self._check_arrays_match( - n_WPs, sc_strand_config.get("params") - ) conductor_params = self._check_arrays_match( n_WPs, conductor_config.get("params") ) - cable_params = self._check_arrays_match(n_WPs, cable_config.get("params")) - - stab_strand = self._make_strand(WP_i, stab_strand_config, stab_strand_params) - sc_strand = self._make_strand(WP_i, sc_strand_config, sc_strand_params) - cable = self._make_cable(WP_i, n_WPs) - # param frame optimisation stuff? - result = cable.optimise_n_stab_ths( - t0=optimisation_params["t0"], - tf=optimisation_params["Tau_discharge"], - initial_temperature=derived_params.T_op, - target_temperature=optimisation_params["hotspot_target_temperature"], - B_fun=derived_params.B_fun, - I_fun=derived_params.I_fun, - bounds=[1, 10000], - ) - return self._make_conductor_cls(cable, WP_i, conductor_config, conductor_params) def _make_winding_pack(self, conductor, i_WP, config, params): From 9bfb0f38eaa01e876a7da8d2ee275bdb8fa72e00 Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Thu, 11 Sep 2025 16:03:35 +0100 Subject: [PATCH 50/61] rearrange5 --- bluemira/magnets/case_tf.py | 321 ---------------------------- bluemira/magnets/tfcoil_designer.py | 256 +++++++++++++++++++++- 2 files changed, 250 insertions(+), 327 deletions(-) diff --git a/bluemira/magnets/case_tf.py b/bluemira/magnets/case_tf.py index 86cd433a33..5863fce042 100644 --- a/bluemira/magnets/case_tf.py +++ b/bluemira/magnets/case_tf.py @@ -25,12 +25,10 @@ import matplotlib.pyplot as plt import numpy as np -from scipy.optimize import minimize_scalar from bluemira.base.look_and_feel import ( bluemira_debug, bluemira_error, - bluemira_print, bluemira_warn, ) from bluemira.geometry.parameterisations import GeometryParameterisation @@ -698,35 +696,6 @@ def enforce_wp_layout_rules( return n_layers_max, n_turns_max - @abstractmethod - def optimise_vault_radial_thickness( - self, - pm: float, - fz: float, - T: float, # noqa: N803 - B: float, - allowable_sigma: float, - bounds: np.ndarray = None, - ): - """ - Abstract method to optimise the radial thickness of the vault support region. - - Parameters - ---------- - pm: - Radial magnetic pressure [Pa]. - fz: - Axial electromagnetic force [N]. - T: - Operating temperature [K]. - B: - Magnetic field strength [T]. - allowable_sigma: - Allowable maximum stress [Pa]. - bounds: - Optimisation bounds for vault thickness [m]. - """ - def to_dict(self) -> dict[str, float | str | list[dict[str, float | str | Any]]]: """ Serialize the BaseCaseTF instance into a dictionary. @@ -1250,63 +1219,6 @@ def _tresca_stress( sigma_z = fz / (self.area_case_jacket + self.area_wps_jacket) return sigma_theta + sigma_z - def optimise_vault_radial_thickness( - self, - pm: float, - fz: float, - op_cond: OperationalConditions, - allowable_sigma: float, - bounds: np.array = None, - ): - """ - Optimise the vault radial thickness of the case - - Parameters - ---------- - pm: - The magnetic pressure applied along the radial direction (Pa). - f_z: - The force applied in the z direction, perpendicular to the case - cross-section (N). - op_cond: - Operational conditions including temperature, magnetic field, and strain - at which to calculate the material properties. - allowable_sigma: - The allowable stress (Pa) for the jacket material. - bounds: - Optional bounds for the jacket thickness optimisation (default is None). - - Returns - ------- - : - The result of the optimisation process containing information about the - optimal vault thickness. - - Raises - ------ - ValueError - If the optimisation process did not converge. - """ - method = None - if bounds is not None: - method = "bounded" - - result = minimize_scalar( - fun=self._sigma_difference, - args=(pm, fz, op_cond, allowable_sigma), - bounds=bounds, - method=method, - options={"xatol": 1e-4}, - ) - - if not result.success: - raise ValueError("dy_vault optimisation did not converge.") - self.dy_vault = result.x - # print(f"Optimal dy_vault: {self.dy_vault}") - # print(f"Tresca sigma: {self._tresca_stress(pm, fz, T=T, B=B) / 1e6} MPa") - - return result - def _sigma_difference( self, dy_vault: float, @@ -1352,239 +1264,6 @@ def _sigma_difference( # diff: {sigma - allowable_sigma}") return abs(sigma - allowable_sigma) - def optimise_jacket_and_vault( - self, - pm: float, - fz: float, - op_cond: OperationalConditions, - allowable_sigma: float, - bounds_cond_jacket: np.ndarray | None = None, - bounds_dy_vault: np.ndarray | None = None, - layout: str = "auto", - wp_reduction_factor: float = 0.8, - min_gap_x: float = 0.05, - n_layers_reduction: int = 4, - max_niter: int = 10, - eps: float = 1e-8, - n_conds: int | None = None, - ): - """ - Jointly optimise the conductor jacket and case vault thickness - under electromagnetic loading constraints. - - This method performs an iterative optimisation of: - - The cross-sectional area of the conductor jacket. - - The vault radial thickness of the TF coil casing. - - The optimisation loop continues until the relative change in - jacket area and vault thickness drops below the specified - convergence threshold `eps`, or `max_niter` is reached. - - Parameters - ---------- - pm: - Radial magnetic pressure on the conductor [Pa]. - fz: - Axial electromagnetic force on the winding pack [N]. - op_cond: - Operational conditions including temperature, magnetic field, and strain - at which to calculate the material properties. - allowable_sigma: - Maximum allowable stress for structural material [Pa]. - bounds_cond_jacket: - Min/max bounds for conductor jacket area optimisation [m²]. - bounds_dy_vault: - Min/max bounds for the case vault thickness optimisation [m]. - layout: - Cable layout strategy; "auto" or predefined layout name. - wp_reduction_factor: - Reduction factor applied to WP footprint during conductor rearrangement. - min_gap_x: - Minimum spacing between adjacent conductors [m]. - n_layers_reduction: - Number of conductor layers to remove when reducing WP height. - max_niter: - Maximum number of optimisation iterations. - eps: - Convergence threshold for the combined optimisation loop. - n_conds: - Target total number of conductors in the winding pack. If None, the self - number of conductors is used. - - Notes - ----- - The function modifies the internal state of `conductor` and `self.dy_vault`. - """ - debug_msg = ["Method optimise_jacket_and_vault"] - - # Initialize convergence array - self._convergence_array = [] - - if n_conds is None: - n_conds = self.n_conductors - - conductor = self.WPs[0].conductor - - self._check_WPs(self.WPs) - - i = 0 - err_conductor_area_jacket = 10000 * eps - err_dy_vault = 10000 * eps - tot_err = err_dy_vault + err_conductor_area_jacket - - self._convergence_array.append([ - i, - conductor.dy_jacket, - self.dy_vault, - err_conductor_area_jacket, - err_dy_vault, - self.dy_wp_tot, - self.geometry.variables.Ri.value - self.geometry.variables.Rk.value, - ]) - - damping_factor = 0.3 - - while i < max_niter and tot_err > eps: - i += 1 - debug_msg.append(f"Internal optimazion - iteration {i}") - - # Store current values - cond_dx_jacket0 = conductor.dx_jacket - case_dy_vault0 = self.dy_vault - - debug_msg.append( - f"before optimisation: conductor jacket area = {conductor.area_jacket}" - ) - cond_area_jacket0 = conductor.area_jacket - t_z_cable_jacket = ( - fz - * self.area_wps_jacket - / (self.area_case_jacket + self.area_wps_jacket) - / self.n_conductors - ) - conductor.optimise_jacket_conductor( - pm, t_z_cable_jacket, op_cond, allowable_sigma, bounds_cond_jacket - ) - debug_msg.extend([ - f"t_z_cable_jacket: {t_z_cable_jacket}", - f"after optimisation: conductor jacket area = {conductor.area_jacket}", - ]) - - conductor.dx_jacket = ( - 1 - damping_factor - ) * cond_dx_jacket0 + damping_factor * conductor.dx_jacket - - err_conductor_area_jacket = ( - abs(conductor.area_jacket - cond_area_jacket0) / cond_area_jacket0 - ) - - self.rearrange_conductors_in_wp( - n_conds, - wp_reduction_factor, - min_gap_x, - n_layers_reduction, - layout=layout, - ) - - debug_msg.append(f"before optimisation: case dy_vault = {self.dy_vault}") - self.optimise_vault_radial_thickness( - pm=pm, - fz=fz, - op_cond=op_cond, - allowable_sigma=allowable_sigma, - bounds=bounds_dy_vault, - ) - - self.dy_vault = ( - 1 - damping_factor - ) * case_dy_vault0 + damping_factor * self.dy_vault - - delta_case_dy_vault = abs(self.dy_vault - case_dy_vault0) - err_dy_vault = delta_case_dy_vault / self.dy_vault - tot_err = err_dy_vault + err_conductor_area_jacket - - debug_msg.append( - f"after optimisation: case dy_vault = {self.dy_vault}\n" - f"err_dy_jacket = {err_conductor_area_jacket}\n " - f"err_dy_vault = {err_dy_vault}\n " - f"tot_err = {tot_err}" - ) - - # Store iteration results in convergence array - self._convergence_array.append([ - i, - conductor.dy_jacket, - self.dy_vault, - err_conductor_area_jacket, - err_dy_vault, - self.dy_wp_tot, - self.geometry.variables.Ri.value - self.geometry.variables.Rk.value, - ]) - - # final check - if i < max_niter: - bluemira_print( - f"Optimisation of jacket and vault reached after " - f"{i} iterations. Total error: {tot_err} < {eps}." - ) - - ax = self.plot(show=False, homogenised=False) - ax.set_title("Case design after optimisation") - plt.show() - - else: - bluemira_warn( - f"Maximum number of optimisation iterations {max_niter} " - f"reached. A total of {tot_err} > {eps} has been obtained." - ) - - def plot_convergence(self): - """ - Plot the evolution of thicknesses and error values over optimisation iterations. - - Raises - ------ - RuntimeError - If no convergence data available - """ - if not hasattr(self, "_convergence_array") or not self._convergence_array: - raise RuntimeError("No convergence data available. Run optimisation first.") - - convergence_data = np.array(self._convergence_array) - - iterations = convergence_data[:, 0] - dy_jacket = convergence_data[:, 1] - dy_vault = convergence_data[:, 2] - err_dy_jacket = convergence_data[:, 3] - err_dy_vault = convergence_data[:, 4] - dy_wp_tot = convergence_data[:, 5] - Ri_minus_Rk = convergence_data[:, 6] # noqa: N806 - - _, axs = plt.subplots(2, 1, figsize=(10, 10), sharex=True) - - # Top subplot: Thicknesses - axs[0].plot(iterations, dy_jacket, marker="o", label="dy_jacket [m]") - axs[0].plot(iterations, dy_vault, marker="s", label="dy_vault [m]") - axs[0].plot(iterations, dy_wp_tot, marker="^", label="dy_wp_tot [m]") - axs[0].plot(iterations, Ri_minus_Rk, marker="v", label="Ri - Rk [m]") - axs[0].set_ylabel("Thickness [m]") - axs[0].set_title("Evolution of Jacket, Vault, and WP Thicknesses") - axs[0].legend() - axs[0].grid(visible=True) - - # Bottom subplot: Errors - axs[1].plot(iterations, err_dy_jacket, marker="o", label="err_dy_jacket") - axs[1].plot(iterations, err_dy_vault, marker="s", label="err_dy_vault") - axs[1].set_ylabel("Relative Error") - axs[1].set_xlabel("Iteration") - axs[1].set_title("Evolution of Errors during Optimisation") - axs[1].set_yscale("log") # Log scale for better visibility if needed - axs[1].legend() - axs[1].grid(visible=True) - - plt.tight_layout() - plt.show() - def create_case_tf_from_dict( case_dict: dict, diff --git a/bluemira/magnets/tfcoil_designer.py b/bluemira/magnets/tfcoil_designer.py index 3e41924f04..708499cfb9 100644 --- a/bluemira/magnets/tfcoil_designer.py +++ b/bluemira/magnets/tfcoil_designer.py @@ -21,6 +21,7 @@ from bluemira.base.look_and_feel import ( bluemira_error, bluemira_print, + bluemira_warn, ) from bluemira.base.parameter_frame import Parameter, ParameterFrame from bluemira.base.parameter_frame.typed import ParameterFrameLike @@ -389,10 +390,9 @@ def run(self): for i_WP in range(n_WPs) ] - case = self._make_case(winding_pack, derived_params, optimisation_params) - # param frame optimisation stuff? - case.optimise_jacket_and_vault( + case, convergence_array = self.optimise_jacket_and_vault( + self._make_case(winding_pack, derived_params, optimisation_params), pm=derived_params.pm, fz=derived_params.t_z, op_cond=OperationalConditions( @@ -411,9 +411,7 @@ def run(self): eps=optimisation_params["eps"], n_conds=derived_params.n_cond, ) - return TFCoilXY( - case, case._convergence_array, derived_params, optimisation_params - ) + return TFCoilXY(case, convergence_array, derived_params, optimisation_params) def optimise_cable_n_stab_ths( self, @@ -518,6 +516,252 @@ class StabilisingStrandRes: ), ) + def optimise_jacket_and_vault( + self, + case: CaseTF, + pm: float, + fz: float, + op_cond: OperationalConditions, + allowable_sigma: float, + bounds_cond_jacket: np.ndarray | None = None, + bounds_dy_vault: np.ndarray | None = None, + layout: str = "auto", + wp_reduction_factor: float = 0.8, + min_gap_x: float = 0.05, + n_layers_reduction: int = 4, + max_niter: int = 10, + eps: float = 1e-8, + n_conds: int | None = None, + ): + """ + Jointly optimise the conductor jacket and case vault thickness + under electromagnetic loading constraints. + + This method performs an iterative optimisation of: + - The cross-sectional area of the conductor jacket. + - The vault radial thickness of the TF coil casing. + + The optimisation loop continues until the relative change in + jacket area and vault thickness drops below the specified + convergence threshold `eps`, or `max_niter` is reached. + + Parameters + ---------- + pm: + Radial magnetic pressure on the conductor [Pa]. + fz: + Axial electromagnetic force on the winding pack [N]. + op_cond: + Operational conditions including temperature, magnetic field, and strain + at which to calculate the material properties. + allowable_sigma: + Maximum allowable stress for structural material [Pa]. + bounds_cond_jacket: + Min/max bounds for conductor jacket area optimisation [m²]. + bounds_dy_vault: + Min/max bounds for the case vault thickness optimisation [m]. + layout: + Cable layout strategy; "auto" or predefined layout name. + wp_reduction_factor: + Reduction factor applied to WP footprint during conductor rearrangement. + min_gap_x: + Minimum spacing between adjacent conductors [m]. + n_layers_reduction: + Number of conductor layers to remove when reducing WP height. + max_niter: + Maximum number of optimisation iterations. + eps: + Convergence threshold for the combined optimisation loop. + n_conds: + Target total number of conductors in the winding pack. If None, the self + number of conductors is used. + + Notes + ----- + The function modifies the internal state of `conductor` and `self.dy_vault`. + """ + debug_msg = ["Method optimise_jacket_and_vault"] + + # Initialize convergence array + convergence_array = [] + + if n_conds is None: + n_conds = case.n_conductors + + conductor = case.WPs[0].conductor + + case._check_WPs(case.WPs) + + err_conductor_area_jacket = 10000 * eps + err_dy_vault = 10000 * eps + tot_err = err_dy_vault + err_conductor_area_jacket + + convergence_array.append([ + 0, + conductor.dy_jacket, + case.dy_vault, + err_conductor_area_jacket, + err_dy_vault, + case.dy_wp_tot, + case.geometry.variables.Ri.value - case.geometry.variables.Rk.value, + ]) + + damping_factor = 0.3 + + for i in range(1, max_niter): + if tot_err <= eps: + bluemira_print( + f"Optimisation of jacket and vault reached after " + f"{i - 1} iterations. Total error: {tot_err} < {eps}." + ) + + ax = case.plot(show=False, homogenised=False) + ax.set_title("Case design after optimisation") + plt.show() + break + debug_msg.append(f"Internal optimazion - iteration {i}") + + # Store current values + cond_dx_jacket0 = conductor.dx_jacket + case_dy_vault0 = case.dy_vault + + debug_msg.append( + f"before optimisation: conductor jacket area = {conductor.area_jacket}" + ) + cond_area_jacket0 = conductor.area_jacket + t_z_cable_jacket = ( + fz + * case.area_wps_jacket + / (case.area_case_jacket + case.area_wps_jacket) + / case.n_conductors + ) + conductor.optimise_jacket_conductor( + pm, t_z_cable_jacket, op_cond, allowable_sigma, bounds_cond_jacket + ) + debug_msg.extend([ + f"t_z_cable_jacket: {t_z_cable_jacket}", + f"after optimisation: conductor jacket area = {conductor.area_jacket}", + ]) + + conductor.dx_jacket = ( + 1 - damping_factor + ) * cond_dx_jacket0 + damping_factor * conductor.dx_jacket + + err_conductor_area_jacket = ( + abs(conductor.area_jacket - cond_area_jacket0) / cond_area_jacket0 + ) + + case.rearrange_conductors_in_wp( + n_conds, + wp_reduction_factor, + min_gap_x, + n_layers_reduction, + layout=layout, + ) + + debug_msg.append(f"before optimisation: case dy_vault = {case.dy_vault}") + result = self.optimise_vault_radial_thickness( + case, + pm=pm, + fz=fz, + op_cond=op_cond, + allowable_sigma=allowable_sigma, + bounds=bounds_dy_vault, + ) + + case.dy_vault = result.x + # print(f"Optimal dy_vault: {case.dy_vault}") + # print(f"Tresca sigma: {case._tresca_stress(pm, fz, T=T, B=B) / 1e6} MPa") + + case.dy_vault = ( + 1 - damping_factor + ) * case_dy_vault0 + damping_factor * case.dy_vault + + delta_case_dy_vault = abs(case.dy_vault - case_dy_vault0) + err_dy_vault = delta_case_dy_vault / case.dy_vault + tot_err = err_dy_vault + err_conductor_area_jacket + + debug_msg.append( + f"after optimisation: case dy_vault = {case.dy_vault}\n" + f"err_dy_jacket = {err_conductor_area_jacket}\n " + f"err_dy_vault = {err_dy_vault}\n " + f"tot_err = {tot_err}" + ) + + # Store iteration results in convergence array + convergence_array.append([ + i, + conductor.dy_jacket, + case.dy_vault, + err_conductor_area_jacket, + err_dy_vault, + case.dy_wp_tot, + case.geometry.variables.Ri.value - case.geometry.variables.Rk.value, + ]) + + else: + bluemira_warn( + f"Maximum number of optimisation iterations {max_niter} " + f"reached. A total of {tot_err} > {eps} has been obtained." + ) + + return case, np.array(convergence_array) + + def optimise_vault_radial_thickness( + self, + case, + pm: float, + fz: float, + op_cond: OperationalConditions, + allowable_sigma: float, + bounds: np.array = None, + ): + """ + Optimise the vault radial thickness of the case + + Parameters + ---------- + pm: + The magnetic pressure applied along the radial direction (Pa). + f_z: + The force applied in the z direction, perpendicular to the case + cross-section (N). + op_cond: + Operational conditions including temperature, magnetic field, and strain + at which to calculate the material properties. + allowable_sigma: + The allowable stress (Pa) for the jacket material. + bounds: + Optional bounds for the jacket thickness optimisation (default is None). + + Returns + ------- + : + The result of the optimisation process containing information about the + optimal vault thickness. + + Raises + ------ + ValueError + If the optimisation process did not converge. + """ + method = None + if bounds is not None: + method = "bounded" + + result = minimize_scalar( + fun=case._sigma_difference, + args=(pm, fz, op_cond, allowable_sigma), + bounds=bounds, + method=method, + options={"xatol": 1e-4}, + ) + + if not result.success: + raise ValueError("dy_vault optimisation did not converge.") + + return result + def _check_arrays_match(self, n_WPs, param_list): if n_WPs > 1: for param in param_list: From 68f52782cadd54df335a69add720f162ee6c3683 Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Thu, 11 Sep 2025 16:09:33 +0100 Subject: [PATCH 51/61] =?UTF-8?q?=F0=9F=9A=9A=20Rearrange6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bluemira/magnets/conductor.py | 174 +++++++--------------------- bluemira/magnets/tfcoil_designer.py | 102 +++++++++++++++- 2 files changed, 140 insertions(+), 136 deletions(-) diff --git a/bluemira/magnets/conductor.py b/bluemira/magnets/conductor.py index 05df77db4d..14c63cdde9 100644 --- a/bluemira/magnets/conductor.py +++ b/bluemira/magnets/conductor.py @@ -12,9 +12,7 @@ import matplotlib.pyplot as plt import numpy as np -from scipy.optimize import minimize_scalar -from bluemira.base.look_and_feel import bluemira_debug from bluemira.magnets.cable import ABCCable from bluemira.magnets.utils import reciprocal_summation, summation @@ -455,168 +453,80 @@ def _tresca_sigma_jacket( # tresca_stress return pressure * X_jacket * saf_jacket + f_z / self.area_jacket - def optimise_jacket_conductor( + def sigma_difference( self, + jacket_thickness: float, pressure: float, - f_z: float, + fz: float, op_cond: OperationalConditions, allowable_sigma: float, - bounds: np.ndarray | None = None, direction: str = "x", - ): + ) -> float: """ - Optimise the jacket dimension of a conductor based on allowable stress using - the Tresca criterion. + Objective function for optimising conductor jacket thickness based on the + Tresca yield criterion. + + This function computes the absolute difference between the calculated Tresca + stress in the jacket and the allowable stress. It is used as a fitness + function during scalar minimization to determine the optimal jacket + thickness. Parameters ---------- + jacket_thickness: + Proposed thickness of the conductor jacket [m] in the direction + perpendicular to the applied pressure. pressure: - The pressure applied along the specified direction (Pa). - f_z: - The force applied in the z direction, perpendicular to the conductor - cross-section (N). + Magnetic or mechanical pressure applied along the specified direction + [Pa]. + fz: + Axial or vertical force applied perpendicular to the cross-section [N]. op_cond: Operational conditions including temperature, magnetic field, and strain - at which to calculate the material properties. + at which to calculate the material property. allowable_sigma: - The allowable stress (Pa) for the jacket material. - bounds: - Optional bounds for the jacket thickness optimisation (default is None). + Maximum allowed stress for the jacket material [Pa]. direction: - The direction along which the pressure is applied ('x' or 'y'). Default is - 'x'. + Direction of the applied pressure. Can be either 'x' (horizontal) or + 'y' (vertical). Default is 'x'. Returns ------- : - The result of the optimisation process containing information about the - optimal jacket thickness. + Absolute difference between the calculated Tresca stress and the + allowable stress [Pa]. Raises ------ ValueError - If the optimisation process did not converge. + If the `direction` is not 'x' or 'y'. Notes ----- - This function uses the Tresca yield criterion to optimise the thickness of the - jacket surrounding the conductor. - This function directly update the conductor's jacket thickness along the x - direction to the optimal value. - """ - - def sigma_difference( - jacket_thickness: float, - pressure: float, - fz: float, - op_cond: OperationalConditions, - allowable_sigma: float, - direction: str = "x", - ) -> float: - """ - Objective function for optimising conductor jacket thickness based on the - Tresca yield criterion. - - This function computes the absolute difference between the calculated Tresca - stress in the jacket and the allowable stress. It is used as a fitness - function during scalar minimization to determine the optimal jacket - thickness. - - Parameters - ---------- - jacket_thickness: - Proposed thickness of the conductor jacket [m] in the direction - perpendicular to the applied pressure. - pressure: - Magnetic or mechanical pressure applied along the specified direction - [Pa]. - fz: - Axial or vertical force applied perpendicular to the cross-section [N]. - op_cond: - Operational conditions including temperature, magnetic field, and strain - at which to calculate the material property. - allowable_sigma: - Maximum allowed stress for the jacket material [Pa]. - direction: - Direction of the applied pressure. Can be either 'x' (horizontal) or - 'y' (vertical). Default is 'x'. - - Returns - ------- - : - Absolute difference between the calculated Tresca stress and the - allowable stress [Pa]. - - Raises - ------ - ValueError - If the `direction` is not 'x' or 'y'. - - Notes - ----- - - This function updates the conductor's internal jacket dimension ( - `dx_jacket` or `dy_jacket`) with the trial value `jacket_thickness`. - - It is intended for use with scalar optimisation algorithms such as - `scipy.optimize.minimize_scalar`. - """ - if direction not in {"x", "y"}: - raise ValueError("Invalid direction: choose either 'x' or 'y'.") - - if direction == "x": - self.dx_jacket = jacket_thickness - else: - self.dy_jacket = jacket_thickness - - sigma_r = self._tresca_sigma_jacket(pressure, fz, op_cond, direction) - - # Normal difference - diff = abs(sigma_r - allowable_sigma) - - # Penalty if stress exceeds allowable - if sigma_r > allowable_sigma: - penalty = 1e6 + (sigma_r - allowable_sigma) * 1e6 - return diff + penalty - - return diff - - debug_msg = ["Method optimise_jacket_conductor:"] + - This function updates the conductor's internal jacket dimension ( + `dx_jacket` or `dy_jacket`) with the trial value `jacket_thickness`. + - It is intended for use with scalar optimisation algorithms such as + `scipy.optimize.minimize_scalar`. + """ + if direction not in {"x", "y"}: + raise ValueError("Invalid direction: choose either 'x' or 'y'.") if direction == "x": - debug_msg.append(f"Previous dx_jacket: {self.dx_jacket}") + self.dx_jacket = jacket_thickness else: - debug_msg.append(f"Previous dy_jacket: {self.dy_jacket}") + self.dy_jacket = jacket_thickness - method = "bounded" if bounds is not None else None + sigma_r = self._tresca_sigma_jacket(pressure, fz, op_cond, direction) - if method == "bounded": - debug_msg.append(f"bounds: {bounds}") + # Normal difference + diff = abs(sigma_r - allowable_sigma) - result = minimize_scalar( - fun=sigma_difference, - args=(pressure, f_z, op_cond, allowable_sigma), - bounds=bounds, - method=method, - options={"xatol": 1e-4}, - ) - - if not result.success: - raise ValueError("Optimisation of the jacket conductor did not converge.") - if direction == "x": - self.dx_jacket = result.x - debug_msg.append(f"Optimal dx_jacket: {self.dx_jacket}") - else: - self.dy_jacket = result.x - debug_msg.append(f"Optimal dy_jacket: {self.dy_jacket}") - debug_msg.append( - f"Averaged sigma in the {direction}-direction: " - f"{self._tresca_sigma_jacket(pressure, f_z, op_cond) / 1e6} MPa\n" - f"Allowable stress in the {direction}-direction: {allowable_sigma / 1e6} " - f"MPa." - ) - debug_msg = "\n".join(debug_msg) - bluemira_debug(debug_msg) + # Penalty if stress exceeds allowable + if sigma_r > allowable_sigma: + penalty = 1e6 + (sigma_r - allowable_sigma) * 1e6 + return diff + penalty - return result + return diff def plot(self, xc: float = 0, yc: float = 0, *, show: bool = False, ax=None): """ diff --git a/bluemira/magnets/tfcoil_designer.py b/bluemira/magnets/tfcoil_designer.py index 708499cfb9..b52574c5fe 100644 --- a/bluemira/magnets/tfcoil_designer.py +++ b/bluemira/magnets/tfcoil_designer.py @@ -19,6 +19,7 @@ from bluemira.base.constants import MU_0, MU_0_2PI, MU_0_4PI from bluemira.base.designer import Designer from bluemira.base.look_and_feel import ( + bluemira_debug, bluemira_error, bluemira_print, bluemira_warn, @@ -635,8 +636,13 @@ def optimise_jacket_and_vault( / (case.area_case_jacket + case.area_wps_jacket) / case.n_conductors ) - conductor.optimise_jacket_conductor( - pm, t_z_cable_jacket, op_cond, allowable_sigma, bounds_cond_jacket + self.optimise_jacket_conductor( + conductor, + pm, + t_z_cable_jacket, + op_cond, + allowable_sigma, + bounds_cond_jacket, ) debug_msg.extend([ f"t_z_cable_jacket: {t_z_cable_jacket}", @@ -669,13 +675,13 @@ def optimise_jacket_and_vault( bounds=bounds_dy_vault, ) - case.dy_vault = result.x + # case.dy_vault = result.x # print(f"Optimal dy_vault: {case.dy_vault}") # print(f"Tresca sigma: {case._tresca_stress(pm, fz, T=T, B=B) / 1e6} MPa") case.dy_vault = ( 1 - damping_factor - ) * case_dy_vault0 + damping_factor * case.dy_vault + ) * case_dy_vault0 + damping_factor * result.x delta_case_dy_vault = abs(case.dy_vault - case_dy_vault0) err_dy_vault = delta_case_dy_vault / case.dy_vault @@ -707,6 +713,94 @@ def optimise_jacket_and_vault( return case, np.array(convergence_array) + def optimise_jacket_conductor( + self, + conductor, + pressure: float, + f_z: float, + op_cond: OperationalConditions, + allowable_sigma: float, + bounds: np.ndarray | None = None, + direction: str = "x", + ): + """ + Optimise the jacket dimension of a conductor based on allowable stress using + the Tresca criterion. + + Parameters + ---------- + pressure: + The pressure applied along the specified direction (Pa). + f_z: + The force applied in the z direction, perpendicular to the conductor + cross-section (N). + op_cond: + Operational conditions including temperature, magnetic field, and strain + at which to calculate the material properties. + allowable_sigma: + The allowable stress (Pa) for the jacket material. + bounds: + Optional bounds for the jacket thickness optimisation (default is None). + direction: + The direction along which the pressure is applied ('x' or 'y'). Default is + 'x'. + + Returns + ------- + : + The result of the optimisation process containing information about the + optimal jacket thickness. + + Raises + ------ + ValueError + If the optimisation process did not converge. + + Notes + ----- + This function uses the Tresca yield criterion to optimise the thickness of the + jacket surrounding the conductor. + This function directly update the conductor's jacket thickness along the x + direction to the optimal value. + """ + debug_msg = ["Method optimise_jacket_conductor:"] + + if direction == "x": + debug_msg.append(f"Previous dx_jacket: {conductor.dx_jacket}") + else: + debug_msg.append(f"Previous dy_jacket: {conductor.dy_jacket}") + + method = "bounded" if bounds is not None else None + + if method == "bounded": + debug_msg.append(f"bounds: {bounds}") + + result = minimize_scalar( + fun=conductor.sigma_difference, + args=(pressure, f_z, op_cond, allowable_sigma), + bounds=bounds, + method=method, + options={"xatol": 1e-4}, + ) + + if not result.success: + raise ValueError("Optimisation of the jacket conductor did not converge.") + if direction == "x": + conductor.dx_jacket = result.x + debug_msg.append(f"Optimal dx_jacket: {conductor.dx_jacket}") + else: + conductor.dy_jacket = result.x + debug_msg.append(f"Optimal dy_jacket: {conductor.dy_jacket}") + debug_msg.append( + f"Averaged sigma in the {direction}-direction: " + f"{conductor._tresca_sigma_jacket(pressure, f_z, op_cond) / 1e6} MPa\n" + f"Allowable stress in the {direction}-direction: {allowable_sigma / 1e6} " + f"MPa." + ) + bluemira_debug("\n".join(debug_msg)) + + return result + def optimise_vault_radial_thickness( self, case, From 643493a66e5e5777d524d5b21cb0d80725d18176 Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Thu, 11 Sep 2025 16:23:55 +0100 Subject: [PATCH 52/61] =?UTF-8?q?=F0=9F=94=A5=20Remove=20test=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bluemira/magnets/winding_pack_.py | 271 ------------------------------ 1 file changed, 271 deletions(-) delete mode 100644 bluemira/magnets/winding_pack_.py diff --git a/bluemira/magnets/winding_pack_.py b/bluemira/magnets/winding_pack_.py deleted file mode 100644 index d9f979fa82..0000000000 --- a/bluemira/magnets/winding_pack_.py +++ /dev/null @@ -1,271 +0,0 @@ -import matplotlib.pyplot as plt -import numpy as np - -from bluemira.base.constants import MU_0_2PI -from bluemira.base.designer import Designer -from bluemira.base.look_and_feel import bluemira_print -from bluemira.base.parameter_frame import ParameterFrame -from bluemira.geometry.wire import BluemiraWire -from bluemira.magnets.cable import RectangularCable -from bluemira.magnets.case_tf import TrapezoidalCaseTF -from bluemira.magnets.conductor import SymmetricConductor -from bluemira.magnets.utils import delayed_exp_func -from bluemira.magnets.winding_pack import WindingPack -from bluemira.utilities.tools import get_class_from_module - - -class WindingPackDesignerParams(ParameterFrame): - R0 = 8.6 # [m] major machine radius - B0 = 4.39 # [T] magnetic field @R0 - A = 2.8 # machine aspect ratio - n_TF = 16 # number of TF coils - ripple = 6e-3 # requirement on the maximum plasma ripple - a = R0 / A # minor radius - d = 1.82 # additional distance to calculate the max external radius of the inner TF leg - operational_current = 70.0e3 # operational current in each conductor - T_sc = 4.2 # operational temperature of superconducting cable - T_margin = 1.5 # temperature margin - t_delay = 3 # [s] - t0 = 0 # [s] - hotspot_target_temperature = 250.0 # [K] - R_VV = Ri * 1.05 # Vacuum vessel radius - S_VV = 100e6 # Vacuum vessel steel limit - d_strand_sc = 1.0e-3 - d_strand_stab = 1.0e-3 - - # allowable stress values - safety_factor = 1.5 * 1.3 - - dx = 0.05 # cable length... just a dummy value - B_ref = 15 # [T] Reference B field value (limit for LTS) - - -def B_TF_r(tf_current, n_TF, r): - """ - Compute the magnetic field generated by the TF coils, including ripple correction. - - Parameters - ---------- - tf_current : float - Toroidal field coil current [A]. - n_TF : int - Number of toroidal field coils. - r : float - Radial position from the tokamak center [m]. - - Returns - ------- - float - Magnetic field intensity [T]. - """ - return 1.08 * (MU_0_2PI * n_TF * tf_current / r) - - -class WindingPackDesigner(Designer[BluemiraWire]): - def run(self) -> BluemiraWire: - dr_plasma_side = R0 * 2 / 3 * 1e-2 # thickness of the plate before the WP - Ri = R0 - a - d # [m] max external radius of the internal TF leg - - # [m] max internal radius of the external TF leg - Re = (R0 + a) * (1 / ripple) ** (1 / n_TF) - total_tf_current = B0 * R0 / MU_0_2PI / n_TF # total current in each TF coil - - # max magnetic field on the inner TF leg - - B_TF_i = B_TF_r(total_tf_current, n_TF, Ri) - - # magnetic pressure on the inner TF leg - pm = B_TF_i**2 / (2 * MU_0) - - # vertical tension acting on the equatorial section of inner TF leg - # i.e. half of the whole F_Z - t_z = 0.5 * np.log(Re / Ri) * MU_0_4PI * n_TF * total_tf_current**2 - - n_cond = np.floor( - total_tf_current / operational_current - ) # minimum number of conductors - bluemira_print(f"Total number of conductor: {n_cond}") - S_Y = 1e9 / safety_factor # [Pa] steel allowable limit - - # inductance (here approximated... better estimation in bluemira) - L = ( - MU_0 - * R0 - * (n_TF * n_cond) ** 2 - * (1 - np.sqrt(1 - (R0 - Ri) / R0)) - / n_TF - * 1.1 - ) - # Magnetic energy - Wm = 1 / 2 * L * n_TF * operational_current**2 * 1e-9 - # Maximum tension... (empirical formula from Lorenzo... find a generic equation) - V_MAX = (7 * R0 - 3) / 6 * 1.1e3 - # Discharge characteristic time to be considered in the following - tau_discharge = max([ - L * operational_current / V_MAX, - B0 * total_tf_current * n_TF * (R0 / A) ** 2 / (R_VV * S_VV), - ]) - tf = tau_discharge - bluemira_print(f"Maximum TF discharge time: {tau_discharge}") - - I_fun = delayed_exp_func(operational_current, tau_discharge, t_delay) - B_fun = delayed_exp_func(B_TF_i, tau_discharge, t_delay) - - # Create a time array from 0 to 3*tau_discharge - t = np.linspace(0, 3 * tau_discharge, 500) - I_data = np.array([I_fun(t_i) for t_i in t]) - B_data = np.array([B_fun(t_i) for t_i in t]) - T_op = T_sc + T_margin # temperature considered for the superconducting cable - - stab_strand_config = self.build_config.get("stabilising_strand") - stab_strand_cls = get_class_from_module(stab_strand_config["class"]) - stab_strand = stab_strand_cls( - name=stab_strand_config.get("name"), - d_strand=self.params.d_strand_stab, - temperature=T_op, - material=stab_strand_config.get("material"), - ) - - sc_strand_config = self.build_config.get("superconducting_strand") - sc_strand_cls = get_class_from_module(sc_strand_config["class"]) - sc_strand = sc_strand_cls( - name=sc_strand_config.get("name"), - d_strand=self.params.d_strand_sc, - temperature=T_op, - material=sc_strand_config.get("material"), - ) - - Ic_sc = sc_strand.Ic(B=B_TF_i, temperature=(T_op)) - n_sc_strand = int(np.ceil(operational_current / Ic_sc)) - - if B_TF_i < B_ref: - name = cable_name + "LTS" - E = 0.1e9 - else: - name = cable_name + "HTS" - E = 120e9 - - cable = RectangularCable( - name, - dx, - sc_strand=sc_strand, - stab_strand=stab_strand, - n_sc_strand=n_sc_strand, - n_stab_strand=500, - d_cooling_channel=1e-2, - void_fraction=0.7, - cos_theta=0.97, - E=E, - ) - - T_for_hts = T_op - cable_out = cable.optimise_n_stab_ths( - t0, - tf, - T_for_hts, - hotspot_target_temperature, - B_fun, - I_fun, - bounds=[1, 10000], - ) - conductor = SymmetricConductor( - cable=cable_out, - mat_jacket=ss316, - mat_ins=dummy_insulator, - dx_jacket=0.01, - dx_ins=1e-3, - ) - winding_pack = WindingPack( - conductor, 1, 1, name=None - ) # just a dummy WP to create the case - case = TrapezoidalCaseTF( - Ri=Ri, - dy_ps=dr_plasma_side, - dy_vault=0.7, - theta_TF=360 / n_TF, - mat_case=ss316, - WPs=[winding_pack], - ) - conductor_arrangement = case.rearrange_conductors_in_wp( - n_conductors=n_cond, - wp_reduction_factor=wp_reduction_factor, - min_gap_x=min_gap_x, - n_layers_reduction=n_layers_reduction, - layout=layout, - ) - case_out = case.optimise_jacket_and_vault( - pm=pm, - fz=t_z, - temperature=T_op, - B=B_TF_i, - allowable_sigma=S_Y, - bounds_cond_jacket=bounds_cond_jacket, - bounds_dy_vault=bounds_dy_vault, - layout=layout, - wp_reduction_factor=wp_reduction_factor, - min_gap_x=min_gap_x, - n_layers_reduction=n_layers_reduction, - max_niter=max_niter, - eps=err, - n_conds=n_cond, - ) - - -def plot_cable_temperature_evolution(result, t0, tf, ax, n_steps=100): - solution = result.solution - - ax.plot(solution.t, solution.y[0], "r*", label="Simulation points") - time_steps = np.linspace(t0, tf, n_steps) - ax.plot(time_steps, solution.sol(time_steps)[0], "b", label="Interpolated curve") - ax.grid(visible=True) - ax.set_ylabel("Temperature [K]", fontsize=10) - ax.set_title("Quench temperature evolution", fontsize=11) - ax.legend(fontsize=9) - - ax.tick_params(axis="y", labelcolor="k", labelsize=9) - - props = {"boxstyle": "round", "facecolor": "white", "alpha": 0.8} - ax.text( - 0.65, - 0.5, - result.info_text, - transform=ax.transAxes, - fontsize=9, - verticalalignment="top", - bbox=props, - ) - ax.figure.tight_layout() - - -def plot_I_B(I_fun, B_fun, t0, tf, ax, n_steps=300): - time_steps = np.linspace(t0, tf, n_steps) - I_values = [I_fun(t) for t in time_steps] # noqa: N806 - B_values = [B_fun(t) for t in time_steps] - - ax.plot(time_steps, I_values, "g", label="Current [A]") - ax.set_ylabel("Current [A]", color="g", fontsize=10) - ax.tick_params(axis="y", labelcolor="g", labelsize=9) - ax.grid(visible=True) - - ax_right = ax.twinx() - ax_right.plot(time_steps, B_values, "m--", label="Magnetic field [T]") - ax_right.set_ylabel("Magnetic field [T]", color="m", fontsize=10) - ax_right.tick_params(axis="y", labelcolor="m", labelsize=9) - - # Labels - ax.set_xlabel("Time [s]", fontsize=10) - ax.tick_params(axis="x", labelsize=9) - - # Combined legend for both sides - lines, labels = ax.get_legend_handles_labels() - lines2, labels2 = ax_right.get_legend_handles_labels() - ax.legend(lines + lines2, labels + labels2, loc="best", fontsize=9) - - ax.figure.tight_layout() - - -def plot_summary(result, t0, tf, I_fun, B_fun, n_steps, show=False): - f, (ax_temp, ax_ib) = plt.subplots(2, 1, figsize=(8, 8), sharex=True) - plot_cable_temperature_evolution(result, t0, tf, ax_temp, n_steps) - plot_I_B(I_fun, B_fun, t0, tf, ax_ib, n_steps * 3) - return f From fa8866ba7a796d42e2abfac250b2edac2a89c8e2 Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Thu, 11 Sep 2025 16:28:04 +0100 Subject: [PATCH 53/61] =?UTF-8?q?=F0=9F=92=AC=20Spelling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bluemira/magnets/cable.py | 60 ++++++++++++++--------------- bluemira/magnets/case_tf.py | 20 +++++----- bluemira/magnets/conductor.py | 28 +++++++------- bluemira/magnets/strand.py | 18 ++++----- bluemira/magnets/tfcoil_designer.py | 4 +- bluemira/magnets/winding_pack.py | 18 ++++----- 6 files changed, 74 insertions(+), 74 deletions(-) diff --git a/bluemira/magnets/cable.py b/bluemira/magnets/cable.py index 9a4f58c3dd..1e9a7528c9 100644 --- a/bluemira/magnets/cable.py +++ b/bluemira/magnets/cable.py @@ -75,7 +75,7 @@ def __init__( n_sc_strand: Number of superconducting strands. n_stab_strand: - Number of stabilizing strands. + Number of stabilising strands. d_cooling_channel: Diameter of the cooling channel [m]. void_fraction: @@ -341,7 +341,7 @@ def final_temperature_difference( Notes ----- - This method is typically used as a cost function for optimisation routines - (e.g., minimizing the temperature error by tuning `n_stab`). + (e.g., minimising the temperature error by tuning `n_stab`). - It modifies the internal state `self._n_stab_strand`, which may affect subsequent evaluations unless restored. """ @@ -373,7 +373,7 @@ def plot( """ Plot a schematic view of the cable cross-section. - This method visualizes the outer shape of the cable and the cooling channel, + This method visualises the outer shape of the cable and the cooling channel, assuming a rectangular or elliptical layout based on `dx`, `dy`, and `d_cooling_channel`. It draws the cable centered at (xc, yc) within the current coordinate system. @@ -394,7 +394,7 @@ def plot( Returns ------- : - The Axes object with the cable plot, which can be further customized + The Axes object with the cable plot, which can be further customised or saved. Notes @@ -466,7 +466,7 @@ def __str__(self) -> str: def to_dict(self) -> dict[str, str | float | int | dict[str, Any]]: """ - Serialize the cable instance to a dictionary. + Serialise the cable instance to a dictionary. Returns ------- @@ -492,12 +492,12 @@ def from_dict( name: str | None = None, ) -> ABCCable: """ - Deserialize a cable instance from a dictionary. + Deserialise a cable instance from a dictionary. Parameters ---------- cable_dict: - Dictionary containing serialized cable data. + Dictionary containing serialised cable data. name: Name for the new instance. If None, attempts to use the 'name' field from the dictionary. @@ -521,7 +521,7 @@ def from_dict( f"'{name_in_registry}'. Expected '{expected_name_in_registry}'." ) - # Deserialize strands + # Deserialise strands sc_strand_data = cable_dict.pop("sc_strand") if isinstance(sc_strand_data, Strand): sc_strand = sc_strand_data @@ -584,11 +584,11 @@ def __init__( sc_strand: Superconducting strand. stab_strand: - Stabilizer strand. + Stabiliser strand. n_sc_strand: Number of superconducting strands. n_stab_strand: - Number of stabilizing strands. + Number of stabilising strands. d_cooling_channel: Diameter of the cooling channel [m]. void_fraction: @@ -645,7 +645,7 @@ def Kx(self, op_cond: OperationalConditions) -> float: # noqa: N802 Returns ------- : - Homogenized stiffness in the x-direction [Pa]. + Homogenised stiffness in the x-direction [Pa]. """ return self.E(op_cond) * self.dy / self.dx @@ -662,13 +662,13 @@ def Ky(self, op_cond: OperationalConditions) -> float: # noqa: N802 Returns ------- : - Homogenized stiffness in the y-direction [Pa]. + Homogenised stiffness in the y-direction [Pa]. """ return self.E(op_cond) * self.dx / self.dy def to_dict(self) -> dict[str, Any]: """ - Serialize the rectangular cable into a dictionary. + Serialise the rectangular cable into a dictionary. Returns ------- @@ -689,7 +689,7 @@ def from_dict( name: str | None = None, ) -> RectangularCable: """ - Deserialize a RectangularCable from a dictionary. + Deserialise a RectangularCable from a dictionary. Behavior: - If both 'dx' and 'aspect_ratio' are provided, a warning is issued and @@ -701,7 +701,7 @@ def from_dict( Parameters ---------- cable_dict: - Dictionary containing serialized cable data. + Dictionary containing serialised cable data. name: Name for the new instance. If None, attempts to use the 'name' field from the dictionary. @@ -725,7 +725,7 @@ def from_dict( f"'{name_in_registry}'. Expected '{expected_name_in_registry}'." ) - # Deserialize strands + # Deserialise strands sc_strand_data = cable_dict.pop("sc_strand") if isinstance(sc_strand_data, Strand): sc_strand = sc_strand_data @@ -755,7 +755,7 @@ def from_dict( if dx is None: raise ValueError( - "Serialized RectangularCable must include at least 'dx' or " + "Serialised RectangularCable must include at least 'dx' or " "'aspect_ratio'." ) @@ -826,7 +826,7 @@ def __init__( n_sc_strand: Number of superconducting strands. n_stab_strand: - Number of stabilizing strands. + Number of stabilising strands. d_cooling_channel: Diameter of the cooling channel [m]. void_fraction: @@ -876,7 +876,7 @@ def Kx(self, op_cond: OperationalConditions) -> float: # noqa: N802 Returns ------- : - Homogenized stiffness in the x-direction [Pa]. + Homogenised stiffness in the x-direction [Pa]. """ return self.E(op_cond) @@ -893,18 +893,18 @@ def Ky(self, op_cond: OperationalConditions) -> float: # noqa: N802 Returns ------- : - Homogenized stiffness in the y-direction [Pa]. + Homogenised stiffness in the y-direction [Pa]. """ return self.E(op_cond) def to_dict(self) -> dict[str, Any]: """ - Serialize the SquareCable. + Serialise the SquareCable. Returns ------- : - Serialized dictionary. + Serialised dictionary. """ return super().to_dict() @@ -915,12 +915,12 @@ def from_dict( name: str | None = None, ) -> SquareCable: """ - Deserialize a SquareCable from a dictionary. + Deserialise a SquareCable from a dictionary. Parameters ---------- cable_dict: - Dictionary containing serialized cable data. + Dictionary containing serialised cable data. name: Name for the new instance. If None, attempts to use the 'name' field from the dictionary. @@ -984,7 +984,7 @@ def __init__( n_sc_strand: Number of superconducting strands. n_stab_strand: - Number of stabilizing strands. + Number of stabilising strands. d_cooling_channel: Diameter of the cooling channel [m]. void_fraction: @@ -1087,7 +1087,7 @@ def plot( Returns ------- : - The axis object containing the cable plot, useful for further customization + The axis object containing the cable plot, useful for further customisation or saving. """ if ax is None: @@ -1120,12 +1120,12 @@ def plot( def to_dict(self) -> dict[str, Any]: """ - Serialize the RoundCable. + Serialise the RoundCable. Returns ------- : - Serialized dictionary. + Serialised dictionary. """ return super().to_dict() @@ -1136,12 +1136,12 @@ def from_dict( name: str | None = None, ) -> RoundCable: """ - Deserialize a RoundCable from a dictionary. + Deserialise a RoundCable from a dictionary. Parameters ---------- cable_dict: - Dictionary containing serialized cable data. + Dictionary containing serialised cable data. name: Name for the new instance. If None, attempts to use the 'name' field from the dictionary. diff --git a/bluemira/magnets/case_tf.py b/bluemira/magnets/case_tf.py index 5863fce042..f25ff7314d 100644 --- a/bluemira/magnets/case_tf.py +++ b/bluemira/magnets/case_tf.py @@ -199,7 +199,7 @@ def create_shape(self, label: str = "", n_points: int = 50) -> BluemiraWire: """ Build the polygon representing the wedge shape. - The polygon is created by discretizing the outer and inner arcs + The polygon is created by discretising the outer and inner arcs into a series of points connected sequentially. Parameters @@ -249,7 +249,7 @@ def __init__( name: str = "BaseCaseTF", ): """ - Initialize a BaseCaseTF instance. + Initialise a BaseCaseTF instance. Parameters ---------- @@ -534,7 +534,7 @@ def plot( yc_wp = self.R_wp_i[i] - wp.dy / 2 ax = wp.plot(xc=xc_wp, yc=yc_wp, ax=ax, homogenised=homogenised) - # Finalize plot + # Finalise plot ax.set_xlabel("Toroidal direction [m]") ax.set_ylabel("Radial direction [m]") ax.set_title(f"TF Case Cross Section: {self.name}") @@ -698,12 +698,12 @@ def enforce_wp_layout_rules( def to_dict(self) -> dict[str, float | str | list[dict[str, float | str | Any]]]: """ - Serialize the BaseCaseTF instance into a dictionary. + Serialise the BaseCaseTF instance into a dictionary. Returns ------- dict - Serialized data representing the TF case, including geometry and material + Serialised data representing the TF case, including geometry and material information. """ return { @@ -720,12 +720,12 @@ def to_dict(self) -> dict[str, float | str | list[dict[str, float | str | Any]]] @classmethod def from_dict(cls, case_dict: dict, name: str | None = None) -> CaseTF: """ - Deserialize a BaseCaseTF instance from a dictionary. + Deserialise a BaseCaseTF instance from a dictionary. Parameters ---------- case_dict: - Dictionary containing serialized TF case data. + Dictionary containing serialised TF case data. name: Optional name override for the new instance. @@ -758,7 +758,7 @@ def __str__(self) -> str: Returns ------- : - Multiline string summarizing key properties of the TF case. + Multiline string summarising key properties of the TF case. """ return ( f"CaseTF '{self.name}'\n" @@ -1270,12 +1270,12 @@ def create_case_tf_from_dict( name: str | None = None, ) -> CaseTF: """ - Factory function to create a CaseTF (or subclass) from a serialized dictionary. + Factory function to create a CaseTF (or subclass) from a serialised dictionary. Parameters ---------- case_dict: - Serialized case dictionary, must include 'name_in_registry' field. + Serialised case dictionary, must include 'name_in_registry' field. name: Name to assign to the created case. If None, uses the name in the dictionary. diff --git a/bluemira/magnets/conductor.py b/bluemira/magnets/conductor.py index 14c63cdde9..58ba9e5592 100644 --- a/bluemira/magnets/conductor.py +++ b/bluemira/magnets/conductor.py @@ -116,12 +116,12 @@ def area_ins(self): def to_dict(self) -> dict[str, Any]: """ - Serialize the conductor instance to a dictionary. + Serialise the conductor instance to a dictionary. Returns ------- : - Dictionary with serialized conductor data. + Dictionary with serialised conductor data. """ return { "name": self.name, @@ -141,12 +141,12 @@ def from_dict( name: str | None = None, ) -> Conductor: """ - Deserialize a Conductor instance from a dictionary. + Deserialise a Conductor instance from a dictionary. Parameters ---------- conductor_dict: - Dictionary containing serialized conductor data. + Dictionary containing serialised conductor data. name: Name for the new instance. If None, attempts to use the 'name' field from the dictionary. @@ -163,7 +163,7 @@ def from_dict( registration name, or if the name already exists and unique_name is False. """ - # Deserialize cable + # Deserialise cable cable = create_cable_from_dict( cable_dict=conductor_dict["cable"], ) @@ -468,7 +468,7 @@ def sigma_difference( This function computes the absolute difference between the calculated Tresca stress in the jacket and the allowable stress. It is used as a fitness - function during scalar minimization to determine the optimal jacket + function during scalar minimisation to determine the optimal jacket thickness. Parameters @@ -533,7 +533,7 @@ def plot(self, xc: float = 0, yc: float = 0, *, show: bool = False, ax=None): Plot a schematic cross-section of the conductor, including cable, jacket, and insulator layers. - This method visualizes the hierarchical geometry of the conductor centered + This method visualises the hierarchical geometry of the conductor centered at a given position. The jacket and insulator are drawn as rectangles, while the internal cable uses its own plotting method. @@ -707,12 +707,12 @@ def dy_ins(self): def to_dict(self) -> dict: """ - Serialize the symmetric conductor instance to a dictionary. + Serialise the symmetric conductor instance to a dictionary. Returns ------- dict - Dictionary with serialized symmetric conductor data. + Dictionary with serialised symmetric conductor data. """ return { "name": self.name, @@ -730,12 +730,12 @@ def from_dict( name: str | None = None, ) -> SymmetricConductor: """ - Deserialize a SymmetricConductor instance from a dictionary. + Deserialise a SymmetricConductor instance from a dictionary. Parameters ---------- conductor_dict: - Dictionary containing serialized conductor data. + Dictionary containing serialised conductor data. name: Name for the new instance. @@ -749,7 +749,7 @@ def from_dict( ValueError If the 'name_in_registry' does not match the expected registration name. """ - # Deserialize cable + # Deserialise cable cable = create_cable_from_dict( cable_dict=conductor_dict["cable"], ) @@ -776,12 +776,12 @@ def create_conductor_from_dict( name: str | None = None, ) -> Conductor: """ - Factory function to create a Conductor (or subclass) from a serialized dictionary. + Factory function to create a Conductor (or subclass) from a serialised dictionary. Parameters ---------- conductor_dict: - Serialized conductor dictionary, must include 'name_in_registry' field. + Serialised conductor dictionary, must include 'name_in_registry' field. name: Name to assign to the created conductor. If None, uses the name in the dictionary. diff --git a/bluemira/magnets/strand.py b/bluemira/magnets/strand.py index bba7c38f85..6816cc9bbb 100644 --- a/bluemira/magnets/strand.py +++ b/bluemira/magnets/strand.py @@ -45,7 +45,7 @@ def __init__( name: str | None = "Strand", ): """ - Initialize a Strand instance. + Initialise a Strand instance. Parameters ---------- @@ -272,12 +272,12 @@ def __str__(self) -> str: def to_dict(self) -> dict[str, Any]: """ - Serialize the strand instance to a dictionary. + Serialise the strand instance to a dictionary. Returns ------- : - Dictionary with serialized strand data. + Dictionary with serialised strand data. """ return { "name": self.name, @@ -299,12 +299,12 @@ def from_dict( name: str | None = None, ) -> Strand: """ - Deserialize a Strand instance from a dictionary. + Deserialise a Strand instance from a dictionary. Parameters ---------- strand_dict: - Dictionary containing serialized strand data. + Dictionary containing serialised strand data. name: Name for the new instance. If None, attempts to use the 'name' field from the dictionary. @@ -333,7 +333,7 @@ class registration name. f"Expected '{expected_name_in_registry}'." ) - # Deserialize materials + # Deserialise materials material_mix = [] for m in strand_dict["materials"]: material_data = m["material"] @@ -379,7 +379,7 @@ def __init__( name: str | None = "SuperconductingStrand", ): """ - Initialize a superconducting strand. + Initialise a superconducting strand. Parameters ---------- @@ -545,12 +545,12 @@ def create_strand_from_dict( name: str | None = None, ): """ - Factory function to create a Strand or its subclass from a serialized dictionary. + Factory function to create a Strand or its subclass from a serialised dictionary. Parameters ---------- strand_dict: - Dictionary with serialized strand data. Must include a 'name_in_registry' field + Dictionary with serialised strand data. Must include a 'name_in_registry' field corresponding to a registered class. name: If given, overrides the name from the dictionary. diff --git a/bluemira/magnets/tfcoil_designer.py b/bluemira/magnets/tfcoil_designer.py index b52574c5fe..d4b65f627c 100644 --- a/bluemira/magnets/tfcoil_designer.py +++ b/bluemira/magnets/tfcoil_designer.py @@ -71,7 +71,7 @@ class TFCoilXYDesignerParams(ParameterFrame): # n_sc_strand: Parameter[int] # """Number of superconducting strands.""" # n_stab_strand: Parameter[int] - # """Number of stabilizing strands.""" + # """Number of stabilising strands.""" # d_cooling_channel: Parameter[float] # """Diameter of the cooling channel [m].""" # void_fraction: Parameter[float] @@ -583,7 +583,7 @@ def optimise_jacket_and_vault( """ debug_msg = ["Method optimise_jacket_and_vault"] - # Initialize convergence array + # Initialise convergence array convergence_array = [] if n_conds is None: diff --git a/bluemira/magnets/winding_pack.py b/bluemira/magnets/winding_pack.py index e02589a31a..86b8b9270f 100644 --- a/bluemira/magnets/winding_pack.py +++ b/bluemira/magnets/winding_pack.py @@ -35,7 +35,7 @@ def __init__( self, conductor: Conductor, nx: int, ny: int, name: str = "WindingPack" ): """ - Initialize a WindingPack instance. + Initialise a WindingPack instance. Parameters ---------- @@ -172,12 +172,12 @@ def plot( def to_dict(self) -> dict[str, Any]: """ - Serialize the WindingPack to a dictionary. + Serialise the WindingPack to a dictionary. Returns ------- : - Serialized dictionary of winding pack attributes. + Serialised dictionary of winding pack attributes. """ return { "name": self.name, @@ -193,12 +193,12 @@ def from_dict( name: str | None = None, ) -> "WindingPack": """ - Deserialize a WindingPack from a dictionary. + Deserialise a WindingPack from a dictionary. Parameters ---------- windingpack_dict: - Serialized winding pack dictionary. + Serialised winding pack dictionary. name: Name for the new instance. If None, attempts to use the 'name' field from the dictionary. @@ -223,7 +223,7 @@ def from_dict( f"'{name_in_registry}'. Expected '{expected_name_in_registry}'." ) - # Deserialize conductor + # Deserialise conductor conductor = create_conductor_from_dict( conductor_dict=windingpack_dict["conductor"], name=None, @@ -242,13 +242,13 @@ def create_wp_from_dict( name: str | None = None, ) -> WindingPack: """ - Factory function to create a WindingPack or its subclass from a serialized + Factory function to create a WindingPack or its subclass from a serialised dictionary. Parameters ---------- windingpack_dict: - Dictionary containing serialized winding pack data. + Dictionary containing serialised winding pack data. Must include a 'name_in_registry' field matching a registered class. name: Optional name override for the reconstructed WindingPack. @@ -267,7 +267,7 @@ def create_wp_from_dict( name_in_registry = windingpack_dict.get("name_in_registry") if name_in_registry is None: raise ValueError( - "Serialized winding pack dictionary must contain a 'name_in_registry' field." + "Serialised winding pack dictionary must contain a 'name_in_registry' field." ) cls = WINDINGPACK_REGISTRY.get(name_in_registry) From 3612121cecf68faba0c343f6a5d4b326fc4bdca4 Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Thu, 11 Sep 2025 16:55:51 +0100 Subject: [PATCH 54/61] =?UTF-8?q?=F0=9F=8E=A8=20Optimisation=20config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bluemira/magnets/tfcoil_designer.py | 98 +++++++++++++++-------------- 1 file changed, 52 insertions(+), 46 deletions(-) diff --git a/bluemira/magnets/tfcoil_designer.py b/bluemira/magnets/tfcoil_designer.py index d4b65f627c..7a9e6015da 100644 --- a/bluemira/magnets/tfcoil_designer.py +++ b/bluemira/magnets/tfcoil_designer.py @@ -98,8 +98,6 @@ class TFCoilXYDesignerParams(ParameterFrame): # """Number of conductors along the y-axis.""" # # case params - # Ri: Parameter[float] - # """External radius of the TF coil case [m].""" # Rk: Parameter[float] # """Internal radius of the TF coil case [m].""" # theta_TF: Parameter[float] @@ -120,33 +118,12 @@ class TFCoilXYDesignerParams(ParameterFrame): strain: Parameter[float] """Strain on system""" - # # optimisation params - # t0: Parameter[float] - # """Initial time""" - # Tau_discharge: Parameter[float] - # """Characteristic time constant""" - # hotspot_target_temperature: Parameter[float] - # """Target temperature for hotspot for cable optimisiation""" - # layout: Parameter[str] - # """Cable layout strategy""" - # wp_reduction_factor: Parameter[float] - # """Fractional reduction of available toroidal space for WPs""" - # n_layers_reduction: Parameter[int] - # """Number of layers to remove after each WP""" - # bounds_cond_jacket: Parameter[np.ndarray] - # """Min/max bounds for conductor jacket area optimisation [m²]""" - # bounds_dy_vault: Parameter[np.ndarray] - # """Min/max bounds for the case vault thickness optimisation [m]""" - # max_niter: Parameter[int] - # """Maximum number of optimisation iterations""" - # eps: Parameter[float] - # """Convergence threshold for the combined optimisation loop.""" - @dataclass class DerivedTFCoilXYDesignerParams: a: float Ri: float + """External radius of the TF coil case [m].""" Re: float B_TF_i: float pm: float @@ -160,16 +137,41 @@ class DerivedTFCoilXYDesignerParams: strain: float +@dataclass +class OptimisationConfig: + t0: float + """Initial time""" + Tau_discharge: float + """Characteristic time constant""" + hotspot_target_temperature: float + """Target temperature for hotspot for cable optimisiation""" + layout: str + """Cable layout strategy""" + wp_reduction_factor: float + """Fractional reduction of available toroidal space for WPs""" + n_layers_reduction: int + """Number of layers to remove after each WP""" + bounds_cond_jacket: npt.NDArray + """Min/max bounds for conductor jacket area optimisation [m²]""" + bounds_dy_vault: npt.NDArray + """Min/max bounds for the case vault thickness optimisation [m]""" + max_niter: int + """Maximum number of optimisation iterations""" + eps: float + """Convergence threshold for the combined optimisation loop.""" + + @dataclass class TFCoilXY: case: CaseTF + cable_soln: Any convergence: npt.NDArray derived_params: DerivedTFCoilXYDesignerParams - op_config: dict[str, float] + op_config: OptimisationConfig def plot_I_B(self, ax, n_steps=300): time_steps = np.linspace( - self.op_config["t0"], self.op_config["Tau_discharge"], n_steps + self.op_config.t0, self.op_config.Tau_discharge, n_steps ) I_values = [self.derived_params.I_fun(t) for t in time_steps] # noqa: N806 B_values = [self.derived_params.B_fun(t) for t in time_steps] @@ -196,11 +198,11 @@ def plot_I_B(self, ax, n_steps=300): ax.figure.tight_layout() def plot_cable_temperature_evolution(self, ax, n_steps=100): - solution = self.case.solution + solution = self.cable_soln.solution ax.plot(solution.t, solution.y[0], "r*", label="Simulation points") time_steps = np.linspace( - self.op_config["t0"], self.op_config["Tau_discharge"], n_steps + self.op_config.t0, self.op_config.Tau_discharge, n_steps ) ax.plot(time_steps, solution.sol(time_steps)[0], "b", label="Interpolated curve") ax.grid(visible=True) @@ -214,7 +216,7 @@ def plot_cable_temperature_evolution(self, ax, n_steps=100): ax.text( 0.65, 0.5, - self.case.info_text, + self.cable_soln.info_text, transform=ax.transAxes, fontsize=9, verticalalignment="top", @@ -328,11 +330,11 @@ def _derived_values(self, op_config): min_gap_x=2 * (R0 * 2 / 3 * 1e-2), I_fun=delayed_exp_func( self.params.Iop.value, - op_config["Tau_discharge"], + op_config.Tau_discharge, self.params.t_delay.value, ), B_fun=delayed_exp_func( - B_TF_i, op_config["Tau_discharge"], self.params.t_delay.value + B_TF_i, op_config.Tau_discharge, self.params.t_delay.value ), strain=self.params.strain.value, ) @@ -370,16 +372,18 @@ def run(self): wp_config = self.build_config.get("winding_pack") n_WPs = int(wp_config.get("sets")) - optimisation_params = self.build_config.get("optimisation_params") + optimisation_params = OptimisationConfig( + **self.build_config.get("optimisation_params") + ) derived_params = self._derived_values(optimisation_params) # param frame optimisation stuff? cable = self.optimise_cable_n_stab_ths( self._make_cable(n_WPs, WP_i=0), - t0=optimisation_params["t0"], - tf=optimisation_params["Tau_discharge"], + t0=optimisation_params.t0, + tf=optimisation_params.Tau_discharge, initial_temperature=derived_params.T_op, - target_temperature=optimisation_params["hotspot_target_temperature"], + target_temperature=optimisation_params.hotspot_target_temperature, B_fun=derived_params.B_fun, I_fun=derived_params.I_fun, bounds=[1, 10000], @@ -402,17 +406,19 @@ def run(self): strain=derived_params.strain, ), allowable_sigma=derived_params.s_y, - bounds_cond_jacket=optimisation_params["bounds_cond_jacket"], - bounds_dy_vault=optimisation_params["bounds_dy_vault"], - layout=optimisation_params["layout"], - wp_reduction_factor=optimisation_params["wp_reduction_factor"], + bounds_cond_jacket=optimisation_params.bounds_cond_jacket, + bounds_dy_vault=optimisation_params.bounds_dy_vault, + layout=optimisation_params.layout, + wp_reduction_factor=optimisation_params.wp_reduction_factor, min_gap_x=derived_params.min_gap_x, - n_layers_reduction=optimisation_params["n_layers_reduction"], - max_niter=optimisation_params["max_niter"], - eps=optimisation_params["eps"], + n_layers_reduction=optimisation_params.n_layers_reduction, + max_niter=optimisation_params.max_niter, + eps=optimisation_params.eps, n_conds=derived_params.n_cond, ) - return TFCoilXY(case, convergence_array, derived_params, optimisation_params) + return TFCoilXY( + case, cable, convergence_array, derived_params, optimisation_params + ) def optimise_cable_n_stab_ths( self, @@ -1002,9 +1008,9 @@ def _make_case(self, WPs, derived_params, optimisation_params): # noqa: N803 # param frame optimisation stuff? case.rearrange_conductors_in_wp( n_conductors=derived_params.n_cond, - wp_reduction_factor=optimisation_params["wp_reduction_factor"], + wp_reduction_factor=optimisation_params.wp_reduction_factor, min_gap_x=derived_params.min_gap_x, - n_layers_reduction=optimisation_params["n_layers_reduction"], - layout=optimisation_params["layout"], + n_layers_reduction=optimisation_params.n_layers_reduction, + layout=optimisation_params.layout, ) return case From 2a2e940cc16cbc77697a61dc7a52f7002cebcb33 Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Fri, 12 Sep 2025 08:41:01 +0100 Subject: [PATCH 55/61] =?UTF-8?q?=F0=9F=94=A5=20Remove=20some=20unneeded?= =?UTF-8?q?=20dictionary=20stuff?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bluemira/magnets/cable.py | 296 +--------------------------- bluemira/magnets/case_tf.py | 81 +------- bluemira/magnets/conductor.py | 146 +------------- bluemira/magnets/strand.py | 95 --------- bluemira/magnets/tfcoil_designer.py | 20 +- bluemira/magnets/winding_pack.py | 101 +--------- 6 files changed, 16 insertions(+), 723 deletions(-) diff --git a/bluemira/magnets/cable.py b/bluemira/magnets/cable.py index 1e9a7528c9..61b742ec17 100644 --- a/bluemira/magnets/cable.py +++ b/bluemira/magnets/cable.py @@ -16,15 +16,8 @@ from matproplib import OperationalConditions from scipy.integrate import solve_ivp -from bluemira.base.look_and_feel import ( - bluemira_debug, - bluemira_warn, -) -from bluemira.magnets.strand import ( - Strand, - SuperconductingStrand, - create_strand_from_dict, -) +from bluemira.base.look_and_feel import bluemira_debug +from bluemira.magnets.strand import Strand, SuperconductingStrand from bluemira.magnets.utils import reciprocal_summation, summation if TYPE_CHECKING: @@ -38,14 +31,8 @@ class ABCCable(ABC): Defines the general structure and common methods for cables composed of superconducting and stabiliser strands. - Notes - ----- - - This class is abstract and cannot be instantiated directly. - - Subclasses must define `dx`, `dy`, `Kx`, `Ky`, and `from_dict`. """ - _name_in_registry_: str | None = None # Abstract base classes should NOT register - def __init__( self, sc_strand: SuperconductingStrand, @@ -485,68 +472,6 @@ def to_dict(self) -> dict[str, str | float | int | dict[str, Any]]: **{k: getattr(k)() for k in self._props}, } - @classmethod - def from_dict( - cls, - cable_dict: dict[str, Any], - name: str | None = None, - ) -> ABCCable: - """ - Deserialise a cable instance from a dictionary. - - Parameters - ---------- - cable_dict: - Dictionary containing serialised cable data. - name: - Name for the new instance. If None, attempts to use the 'name' field from - the dictionary. - - Returns - ------- - : - Instantiated cable object. - - Raises - ------ - ValueError - If name_in_registry mismatch or duplicate instance name. - """ - name_in_registry = cable_dict.pop("name_in_registry", None) - expected_name_in_registry = getattr(cls, "_name_in_registry_", cls.__name__) - - if name_in_registry != expected_name_in_registry: - raise ValueError( - f"Cannot create {cls.__name__} from dictionary with name_in_registry " - f"'{name_in_registry}'. Expected '{expected_name_in_registry}'." - ) - - # Deserialise strands - sc_strand_data = cable_dict.pop("sc_strand") - if isinstance(sc_strand_data, Strand): - sc_strand = sc_strand_data - else: - sc_strand = create_strand_from_dict(strand_dict=sc_strand_data) - - stab_strand_data = cable_dict.pop("stab_strand") - if isinstance(stab_strand_data, Strand): - stab_strand = stab_strand_data - else: - stab_strand = create_strand_from_dict(strand_dict=stab_strand_data) - - # how to resolve this with ParameterFrame? - return cls( - sc_strand=sc_strand, - stab_strand=stab_strand, - n_sc_strand=cable_dict.pop("n_sc_strand"), - n_stab_strand=cable_dict.pop("n_stab_strand"), - d_cooling_channel=cable_dict.pop("d_cooling_channel"), - void_fraction=cable_dict.pop("void_fraction"), - cos_theta=cable_dict.pop("cos_theta"), - name=name or cable_dict.pop("name", None), - **cable_dict, - ) - class RectangularCable(ABCCable): """ @@ -556,8 +481,6 @@ class RectangularCable(ABCCable): the total area and x-dimension. """ - _name_in_registry_ = "RectangularCable" - def __init__( self, sc_strand: SuperconductingStrand, @@ -682,111 +605,6 @@ def to_dict(self) -> dict[str, Any]: }) return data - @classmethod - def from_dict( - cls, - cable_dict: dict[str, Any], - name: str | None = None, - ) -> RectangularCable: - """ - Deserialise a RectangularCable from a dictionary. - - Behavior: - - If both 'dx' and 'aspect_ratio' are provided, a warning is issued and - aspect_ratio is applied. - - If only 'aspect_ratio' is provided, dx and dy are calculated accordingly. - - If only 'dx' is provided, it is used as-is. - - If neither is provided, raises a ValueError. - - Parameters - ---------- - cable_dict: - Dictionary containing serialised cable data. - name: - Name for the new instance. If None, attempts to use the 'name' field from - the dictionary. - - Returns - ------- - : - Instantiated rectangular cable object. - - Raises - ------ - ValueError - If neither 'dx' nor 'aspect_ratio' is provided. - """ - name_in_registry = cable_dict.get("name_in_registry") - expected_name_in_registry = getattr(cls, "_name_in_registry_", cls.__name__) - - if name_in_registry != expected_name_in_registry: - raise ValueError( - f"Cannot create {cls.__name__} from dictionary with name_in_registry " - f"'{name_in_registry}'. Expected '{expected_name_in_registry}'." - ) - - # Deserialise strands - sc_strand_data = cable_dict.pop("sc_strand") - if isinstance(sc_strand_data, Strand): - sc_strand = sc_strand_data - else: - sc_strand = create_strand_from_dict(strand_dict=sc_strand_data) - - stab_strand_data = cable_dict.pop("stab_strand") - if isinstance(stab_strand_data, Strand): - stab_strand = stab_strand_data - else: - stab_strand = create_strand_from_dict(strand_dict=stab_strand_data) - - # Geometry parameters - dx = cable_dict.pop("dx", None) - aspect_ratio = cable_dict.pop("aspect_ratio") - - if dx is not None and aspect_ratio is not None: - bluemira_warn( - "Both 'dx' and 'aspect_ratio' specified. Aspect ratio will override dx " - "after creation." - ) - - if aspect_ratio is not None and dx is None: - # Default dx if only aspect ratio is provided. It will be recalculated at - # the end when set_aspect_ratio is called - dx = 0.01 - - if dx is None: - raise ValueError( - "Serialised RectangularCable must include at least 'dx' or " - "'aspect_ratio'." - ) - - # Base cable parameters - n_sc_strand = cable_dict.pop("n_sc_strand") - n_stab_strand = cable_dict.pop("n_stab_strand") - d_cooling_channel = cable_dict.pop("d_cooling_channel") - void_fraction = cable_dict.pop("void_fraction") - cos_theta = cable_dict.pop("cos_theta") - - # how to handle with parameterframe? - # Create cable - cable = cls( - dx=dx, - sc_strand=sc_strand, - stab_strand=stab_strand, - n_sc_strand=n_sc_strand, - n_stab_strand=n_stab_strand, - d_cooling_channel=d_cooling_channel, - void_fraction=void_fraction, - cos_theta=cos_theta, - name=name or cable_dict.pop("name", None), - **cable_dict, - ) - - # Adjust aspect ratio if needed - if aspect_ratio is not None: - cable.set_aspect_ratio(aspect_ratio) - - return cable - class SquareCable(ABCCable): """ @@ -795,8 +613,6 @@ class SquareCable(ABCCable): Both dx and dy are derived from the total cross-sectional area. """ - _name_in_registry_ = "SquareCable" - def __init__( self, sc_strand: SuperconductingStrand, @@ -897,60 +713,6 @@ def Ky(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ return self.E(op_cond) - def to_dict(self) -> dict[str, Any]: - """ - Serialise the SquareCable. - - Returns - ------- - : - Serialised dictionary. - """ - return super().to_dict() - - @classmethod - def from_dict( - cls, - cable_dict: dict[str, Any], - name: str | None = None, - ) -> SquareCable: - """ - Deserialise a SquareCable from a dictionary. - - Parameters - ---------- - cable_dict: - Dictionary containing serialised cable data. - name: - Name for the new instance. If None, attempts to use the 'name' field from - the dictionary. - - Returns - ------- - : - Instantiated square cable. - - Raises - ------ - ValueError - If unique_name is False and a duplicate name is detected in the instance - cache. - """ - sc_strand = create_strand_from_dict(strand_dict=cable_dict.pop("sc_strand")) - stab_strand = create_strand_from_dict(strand_dict=cable_dict.pop("stab_strand")) - - # how to handle this? - return cls( - sc_strand=sc_strand, - stab_strand=stab_strand, - n_sc_strand=cable_dict.pop("n_sc_strand"), - n_stab_strand=cable_dict.pop("n_stab_strand"), - d_cooling_channel=cable_dict.pop("d_cooling_channel"), - void_fraction=cable_dict.pop("void_fraction"), - cos_theta=cable_dict.pop("cos_theta"), - name=name or cable_dict.pop("name", None), - ) - class RoundCable(ABCCable): """ @@ -1117,57 +879,3 @@ def plot( if show: plt.show() return ax - - def to_dict(self) -> dict[str, Any]: - """ - Serialise the RoundCable. - - Returns - ------- - : - Serialised dictionary. - """ - return super().to_dict() - - @classmethod - def from_dict( - cls, - cable_dict: dict[str, Any], - name: str | None = None, - ) -> RoundCable: - """ - Deserialise a RoundCable from a dictionary. - - Parameters - ---------- - cable_dict: - Dictionary containing serialised cable data. - name: - Name for the new instance. If None, attempts to use the 'name' field from - the dictionary. - - Returns - ------- - : - Instantiated square cable. - - Raises - ------ - ValueError - If unique_name is False and a duplicate name is detected in the instance - cache. - """ - sc_strand = create_strand_from_dict(strand_dict=cable_dict.pop("sc_strand")) - stab_strand = create_strand_from_dict(strand_dict=cable_dict.pop("stab_strand")) - - return cls( - sc_strand=sc_strand, - stab_strand=stab_strand, - n_sc_strand=cable_dict.pop("n_sc_strand"), - n_stab_strand=cable_dict.pop("n_stab_strand"), - d_cooling_channel=cable_dict.pop("d_cooling_channel"), - void_fraction=cable_dict.pop("void_fraction"), - cos_theta=cable_dict.pop("cos_theta"), - name=name or cable_dict.pop("name", None), - **cable_dict, - ) diff --git a/bluemira/magnets/case_tf.py b/bluemira/magnets/case_tf.py index f25ff7314d..294dc77390 100644 --- a/bluemira/magnets/case_tf.py +++ b/bluemira/magnets/case_tf.py @@ -34,7 +34,7 @@ from bluemira.geometry.parameterisations import GeometryParameterisation from bluemira.geometry.tools import make_polygon from bluemira.magnets.utils import reciprocal_summation, summation -from bluemira.magnets.winding_pack import WindingPack, create_wp_from_dict +from bluemira.magnets.winding_pack import WindingPack from bluemira.utilities.opt_variables import OptVariable, OptVariablesFrame, VarDictT, ov if TYPE_CHECKING: @@ -712,45 +712,10 @@ def to_dict(self) -> dict[str, float | str | list[dict[str, float | str | Any]]] "dy_ps": self.dy_ps, "dy_vault": self.dy_vault, "theta_TF": self.geometry.variables.theta_TF.value, - "mat_case": self.mat_case.name, # Assume Material has 'name' attribute + "mat_case": self.mat_case, "WPs": [wp.to_dict() for wp in self.WPs], - # Assume each WindingPack implements to_dict() } - @classmethod - def from_dict(cls, case_dict: dict, name: str | None = None) -> CaseTF: - """ - Deserialise a BaseCaseTF instance from a dictionary. - - Parameters - ---------- - case_dict: - Dictionary containing serialised TF case data. - name: - Optional name override for the new instance. - - Returns - ------- - : - Reconstructed TF case instance. - - Raises - ------ - ValueError - If the 'name_in_registry' field does not match this class. - """ - WPs = [create_wp_from_dict(wp_dict) for wp_dict in case_dict["WPs"]] # noqa:N806 - - return cls( - Ri=case_dict["Ri"], - dy_ps=case_dict["dy_ps"], - dy_vault=case_dict["dy_vault"], - theta_TF=case_dict["theta_TF"], - mat_case=case_dict["mat_case"], - WPs=WPs, - name=name or case_dict.get("name"), - ) - def __str__(self) -> str: """ Generate a human-readable summary of the TF case. @@ -1263,45 +1228,3 @@ def _sigma_difference( # bluemira_print(f"sigma: {sigma}, allowable_sigma: {allowable_sigma}, # diff: {sigma - allowable_sigma}") return abs(sigma - allowable_sigma) - - -def create_case_tf_from_dict( - case_dict: dict, - name: str | None = None, -) -> CaseTF: - """ - Factory function to create a CaseTF (or subclass) from a serialised dictionary. - - Parameters - ---------- - case_dict: - Serialised case dictionary, must include 'name_in_registry' field. - name: - Name to assign to the created case. If None, uses the name in the dictionary. - - Returns - ------- - : - A fully instantiated CaseTF (or subclass) object. - - Raises - ------ - ValueError - If no class is registered with the given name_in_registry. - """ - name_in_registry = case_dict.get("name_in_registry") - if name_in_registry is None: - raise ValueError("CaseTF dictionary must include 'name_in_registry' field.") - - case_cls = CASETF_REGISTRY.get(name_in_registry) - if case_cls is None: - available = list(CASETF_REGISTRY.keys()) - raise ValueError( - f"No registered CaseTF class with name_in_registry '{name_in_registry}'. " - f"Available: {available}" - ) - - return case_cls.from_dict( - name=name, - case_dict=case_dict, - ) diff --git a/bluemira/magnets/conductor.py b/bluemira/magnets/conductor.py index 58ba9e5592..31b40d01cc 100644 --- a/bluemira/magnets/conductor.py +++ b/bluemira/magnets/conductor.py @@ -126,66 +126,14 @@ def to_dict(self) -> dict[str, Any]: return { "name": self.name, "cable": self.cable.to_dict(), - "mat_jacket": self.mat_jacket.name, - "mat_ins": self.mat_ins.name, + "mat_jacket": self.mat_jacket, + "mat_ins": self.mat_ins, "dx_jacket": self.dx_jacket, "dy_jacket": self.dy_jacket, "dx_ins": self.dx_ins, "dy_ins": self.dy_ins, } - @classmethod - def from_dict( - cls, - conductor_dict: dict[str, Any], - name: str | None = None, - ) -> Conductor: - """ - Deserialise a Conductor instance from a dictionary. - - Parameters - ---------- - conductor_dict: - Dictionary containing serialised conductor data. - name: - Name for the new instance. If None, attempts to use the 'name' field from - the dictionary. - - Returns - ------- - : - A fully reconstructed Conductor instance. - - Raises - ------ - ValueError - If the 'name_in_registry' field does not match the expected class - registration name, - or if the name already exists and unique_name is False. - """ - # Deserialise cable - cable = create_cable_from_dict( - cable_dict=conductor_dict["cable"], - ) - - # Resolve jacket material - mat_jacket = conductor_dict["mat_jacket"] - - # Resolve insulation material - mat_ins = conductor_dict["mat_ins"] - - # Instantiate - return cls( - cable=cable, - mat_jacket=mat_jacket, - mat_ins=mat_ins, - dx_jacket=conductor_dict["dx_jacket"], - dy_jacket=conductor_dict["dy_jacket"], - dx_ins=conductor_dict["dx_ins"], - dy_ins=conductor_dict["dy_ins"], - name=name or conductor_dict.get("name"), - ) - def erho(self, op_cond: OperationalConditions) -> float: """ Computes the conductor's equivalent resistivity considering the resistance @@ -722,93 +670,3 @@ def to_dict(self) -> dict: "dx_jacket": self.dx_jacket, "dx_ins": self.dx_ins, } - - @classmethod - def from_dict( - cls, - conductor_dict: dict[str, Any], - name: str | None = None, - ) -> SymmetricConductor: - """ - Deserialise a SymmetricConductor instance from a dictionary. - - Parameters - ---------- - conductor_dict: - Dictionary containing serialised conductor data. - name: - Name for the new instance. - - Returns - ------- - : - A fully reconstructed SymmetricConductor instance. - - Raises - ------ - ValueError - If the 'name_in_registry' does not match the expected registration name. - """ - # Deserialise cable - cable = create_cable_from_dict( - cable_dict=conductor_dict["cable"], - ) - - # Resolve jacket material - mat_jacket = conductor_dict["mat_jacket"] - - # Resolve insulation material - mat_ins = conductor_dict["mat_ins"] - - # Instantiate - return cls( - cable=cable, - mat_jacket=mat_jacket, - mat_ins=mat_ins, - dx_jacket=conductor_dict["dx_jacket"], - dx_ins=conductor_dict["dx_ins"], - name=name or conductor_dict.get("name"), - ) - - -def create_conductor_from_dict( - conductor_dict: dict, - name: str | None = None, -) -> Conductor: - """ - Factory function to create a Conductor (or subclass) from a serialised dictionary. - - Parameters - ---------- - conductor_dict: - Serialised conductor dictionary, must include 'name_in_registry' field. - name: - Name to assign to the created conductor. If None, uses the name in the - dictionary. - - Returns - ------- - : - A fully instantiated Conductor (or subclass) object. - - Raises - ------ - ValueError - If no class is registered with the given name_in_registry. - """ - name_in_registry = conductor_dict.get("name_in_registry") - if name_in_registry is None: - raise ValueError("Conductor dictionary must include 'name_in_registry' field.") - - conductor_cls = CONDUCTOR_REGISTRY.get(name_in_registry) - if conductor_cls is None: - available = list(CONDUCTOR_REGISTRY.keys()) - raise ValueError( - f"No registered conductor class with name_in_registry '{name_in_registry}'. " - f"Available: {available}" - ) - - return conductor_cls.from_dict( - name=name, - conductor_dict=conductor_dict, - ) diff --git a/bluemira/magnets/strand.py b/bluemira/magnets/strand.py index 6816cc9bbb..4b99bc3773 100644 --- a/bluemira/magnets/strand.py +++ b/bluemira/magnets/strand.py @@ -26,7 +26,6 @@ from bluemira.display.plotter import PlotOptions from bluemira.geometry.face import BluemiraFace from bluemira.geometry.tools import make_circle -from bluemira.utilities.tools import get_class_from_module class Strand: @@ -292,73 +291,7 @@ def to_dict(self) -> dict[str, Any]: ], } - @classmethod - def from_dict( - cls, - strand_dict: dict[str, Any], - name: str | None = None, - ) -> Strand: - """ - Deserialise a Strand instance from a dictionary. - - Parameters - ---------- - strand_dict: - Dictionary containing serialised strand data. - name: - Name for the new instance. If None, attempts to use the 'name' field from - the dictionary. - - Returns - ------- - Strand - A new instantiated Strand object. - Raises - ------ - TypeError - If the materials in the dictionary are not valid MaterialFraction instances. - ValueError - If the name_in_registry in the dictionary does not match the expected - class registration name. - """ - # Validate registration name - name_in_registry = strand_dict.get("name_in_registry") - expected_name_in_registry = getattr(cls, "_name_in_registry_", cls.__name__) - - if name_in_registry != expected_name_in_registry: - raise ValueError( - f"Cannot create {cls.__name__} from dictionary with name_in_registry " - f"'{name_in_registry}'. " - f"Expected '{expected_name_in_registry}'." - ) - - # Deserialise materials - material_mix = [] - for m in strand_dict["materials"]: - material_data = m["material"] - if isinstance(material_data, str): - raise TypeError( - "Material data must be a Material instance, not a string - " - "TEMPORARY." - ) - material_obj = material_data - - material_mix.append( - MaterialFraction(material=material_obj, fraction=m["fraction"]) - ) - # resolve - return cls( - materials=material_mix, - operating_temperature=strand_dict.get("operating_temperature"), - d_strand=strand_dict.get("d_strand"), - name=name or strand_dict.get("name"), - ) - - -# ------------------------------------------------------------------------------ -# SuperconductingStrand Class -# ------------------------------------------------------------------------------ class SuperconductingStrand(Strand): """ Represents a superconducting strand with a circular cross-section. @@ -369,8 +302,6 @@ class SuperconductingStrand(Strand): Automatically registered using the RegistrableMeta metaclass. """ - _name_in_registry_ = "SuperconductingStrand" - def __init__( self, materials: list[MaterialFraction], @@ -538,29 +469,3 @@ def plot_Ic_B( # noqa:N802 plt.show() return ax - - -def create_strand_from_dict( - strand_dict: dict[str, Any], - name: str | None = None, -): - """ - Factory function to create a Strand or its subclass from a serialised dictionary. - - Parameters - ---------- - strand_dict: - Dictionary with serialised strand data. Must include a 'name_in_registry' field - corresponding to a registered class. - name: - If given, overrides the name from the dictionary. - - Returns - ------- - Strand - An instance of the appropriate Strand subclass. - - """ - return get_class_from_module( - strand_dict.pop("class"), default_module="bluemira.magnets.strand" - ).from_dict(name=name, strand_dict=strand_dict) diff --git a/bluemira/magnets/tfcoil_designer.py b/bluemira/magnets/tfcoil_designer.py index 7a9e6015da..577f573aee 100644 --- a/bluemira/magnets/tfcoil_designer.py +++ b/bluemira/magnets/tfcoil_designer.py @@ -130,7 +130,7 @@ class DerivedTFCoilXYDesignerParams: t_z: float T_op: float s_y: float - n_cond: float + n_cond: int min_gap_x: float I_fun: Callable[[float], float] B_fun: Callable[[float], float] @@ -304,7 +304,7 @@ def __init__( ): super().__init__(params=params, build_config=build_config) - def _derived_values(self, op_config): + def _derived_values(self, op_config: OptimisationConfig): # Needed params that are calculated using the base params R0 = self.params.R0.value n_TF = self.params.n_TF.value @@ -379,7 +379,7 @@ def run(self): # param frame optimisation stuff? cable = self.optimise_cable_n_stab_ths( - self._make_cable(n_WPs, WP_i=0), + self._make_cable(n_wp, WP_i=0), t0=optimisation_params.t0, tf=optimisation_params.Tau_discharge, initial_temperature=derived_params.T_op, @@ -672,7 +672,7 @@ def optimise_jacket_and_vault( ) debug_msg.append(f"before optimisation: case dy_vault = {case.dy_vault}") - result = self.optimise_vault_radial_thickness( + case_dy_vault_result = self.optimise_vault_radial_thickness( case, pm=pm, fz=fz, @@ -687,7 +687,7 @@ def optimise_jacket_and_vault( case.dy_vault = ( 1 - damping_factor - ) * case_dy_vault0 + damping_factor * result.x + ) * case_dy_vault0 + damping_factor * case_dy_vault_result.x delta_case_dy_vault = abs(case.dy_vault - case_dy_vault0) err_dy_vault = delta_case_dy_vault / case.dy_vault @@ -888,10 +888,8 @@ def _make_strand(self, i_WP, config, params): "Material data must be a Material instance, not a string - " "TEMPORARY." ) - material_obj = material_data - material_mix.append( - MaterialFraction(material=material_obj, fraction=m["fraction"]) + MaterialFraction(material=material_data, fraction=m["fraction"]) ) return stab_strand_cls( materials=material_mix, @@ -963,14 +961,12 @@ def _make_conductor_cls(self, cable, i_WP, config, params): ), ) - def _make_conductor(self, cable, n_WPs, WP_i=0): + def _make_conductor(self, cable, n_wp, WP_i=0): # current functionality requires conductors are the same for both WPs # in future allow for different conductor objects so can vary cable and strands # between the sets of the winding pack? conductor_config = self.build_config.get("conductor") - conductor_params = self._check_arrays_match( - n_WPs, conductor_config.get("params") - ) + conductor_params = self._check_arrays_match(n_wp, conductor_config.get("params")) return self._make_conductor_cls(cable, WP_i, conductor_config, conductor_params) diff --git a/bluemira/magnets/winding_pack.py b/bluemira/magnets/winding_pack.py index 86b8b9270f..abe76704b8 100644 --- a/bluemira/magnets/winding_pack.py +++ b/bluemira/magnets/winding_pack.py @@ -6,13 +6,13 @@ """Winding pack module""" -from typing import Any, ClassVar +from typing import Any import matplotlib.pyplot as plt import numpy as np from matproplib import OperationalConditions -from bluemira.magnets.conductor import Conductor, create_conductor_from_dict +from bluemira.magnets.conductor import Conductor class WindingPack: @@ -29,8 +29,6 @@ class WindingPack: Number of conductors along the y-axis. """ - _name_in_registry_: ClassVar[str] = "WindingPack" - def __init__( self, conductor: Conductor, nx: int, ny: int, name: str = "WindingPack" ): @@ -185,98 +183,3 @@ def to_dict(self) -> dict[str, Any]: "nx": self.nx, "ny": self.ny, } - - @classmethod - def from_dict( - cls, - windingpack_dict: dict[str, Any], - name: str | None = None, - ) -> "WindingPack": - """ - Deserialise a WindingPack from a dictionary. - - Parameters - ---------- - windingpack_dict: - Serialised winding pack dictionary. - name: - Name for the new instance. If None, attempts to use the 'name' field from - the dictionary. - - Returns - ------- - : - Reconstructed WindingPack instance. - - Raises - ------ - ValueError - If 'name_in_registry' does not match the expected class. - """ - # Validate name_in_registry - name_in_registry = windingpack_dict.get("name_in_registry") - expected_name_in_registry = getattr(cls, "_name_in_registry_", cls.__name__) - - if name_in_registry != expected_name_in_registry: - raise ValueError( - f"Cannot create {cls.__name__} from dictionary with name_in_registry " - f"'{name_in_registry}'. Expected '{expected_name_in_registry}'." - ) - - # Deserialise conductor - conductor = create_conductor_from_dict( - conductor_dict=windingpack_dict["conductor"], - name=None, - ) - - return cls( - conductor=conductor, - nx=windingpack_dict["nx"], - ny=windingpack_dict["ny"], - name=name or windingpack_dict.get("name"), - ) - - -def create_wp_from_dict( - windingpack_dict: dict[str, Any], - name: str | None = None, -) -> WindingPack: - """ - Factory function to create a WindingPack or its subclass from a serialised - dictionary. - - Parameters - ---------- - windingpack_dict: - Dictionary containing serialised winding pack data. - Must include a 'name_in_registry' field matching a registered class. - name: - Optional name override for the reconstructed WindingPack. - - Returns - ------- - : - An instance of the appropriate WindingPack subclass. - - Raises - ------ - ValueError - If 'name_in_registry' is missing from the dictionary. - If no matching registered class is found. - """ - name_in_registry = windingpack_dict.get("name_in_registry") - if name_in_registry is None: - raise ValueError( - "Serialised winding pack dictionary must contain a 'name_in_registry' field." - ) - - cls = WINDINGPACK_REGISTRY.get(name_in_registry) - if cls is None: - available = ", ".join(WINDINGPACK_REGISTRY.keys()) - raise ValueError( - f"No registered winding pack class with registration name '" - f"{name_in_registry}'. " - f"Available: {available}." - ) - - return cls.from_dict(windingpack_dict=windingpack_dict, name=name) From 1690510be4b85b5fa2d27e2d4f225e472957513e Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Fri, 12 Sep 2025 08:58:19 +0100 Subject: [PATCH 56/61] =?UTF-8?q?=F0=9F=9A=A7=20Possible=20differences?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bluemira/magnets/cable.py | 4 ++++ bluemira/magnets/case_tf.py | 10 ++++++---- bluemira/magnets/conductor.py | 13 ++++++------- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/bluemira/magnets/cable.py b/bluemira/magnets/cable.py index 61b742ec17..1be0524136 100644 --- a/bluemira/magnets/cable.py +++ b/bluemira/magnets/cable.py @@ -694,6 +694,7 @@ def Kx(self, op_cond: OperationalConditions) -> float: # noqa: N802 : Homogenised stiffness in the x-direction [Pa]. """ + # TODO possible reason for floating point difference return self.E(op_cond) def Ky(self, op_cond: OperationalConditions) -> float: # noqa: N802 @@ -711,6 +712,7 @@ def Ky(self, op_cond: OperationalConditions) -> float: # noqa: N802 : Homogenised stiffness in the y-direction [Pa]. """ + # TODO possible reason for floating point difference return self.E(op_cond) @@ -800,6 +802,7 @@ def Kx(self, op_cond: OperationalConditions) -> float: # noqa: N802 : Equivalent stiffness in the x-direction [Pa]. """ + # TODO possible reason for floating point difference return self.E(op_cond) def Ky(self, op_cond: OperationalConditions) -> float: # noqa: N802 @@ -821,6 +824,7 @@ def Ky(self, op_cond: OperationalConditions) -> float: # noqa: N802 : Equivalent stiffness in the y-direction [Pa]. """ + # TODO possible reason for floating point difference return self.E(op_cond) def plot( diff --git a/bluemira/magnets/case_tf.py b/bluemira/magnets/case_tf.py index 294dc77390..b9d797a911 100644 --- a/bluemira/magnets/case_tf.py +++ b/bluemira/magnets/case_tf.py @@ -46,7 +46,7 @@ def _dx_at_radius(radius: float, rad_theta: float) -> float: """ - Compute the toroidal half-width at a given radial position. + Compute the toroidal width at a given radial position. Parameters ---------- @@ -645,11 +645,11 @@ def enforce_wp_layout_rules( n_conductors: Number of conductors to allocate. dx_WP: - Available toroidal half-width for the winding pack [m]. + Available toroidal width for the winding pack [m]. dx_cond: - Toroidal half-width of a single conductor [m]. + Toroidal width of a single conductor [m]. dy_cond: - Radial half-height of a single conductor [m]. + Radial height of a single conductor [m]. layout: Layout type: - "auto" : no constraints @@ -893,6 +893,7 @@ def Kx(self, op_cond: OperationalConditions) -> float: # noqa: N802 : Total equivalent radial stiffness of the TF case [Pa]. """ + # TODO possible reason for floating point difference kx_lat = self.Kx_lat(op_cond) temp = [ reciprocal_summation([ @@ -984,6 +985,7 @@ def Ky(self, op_cond: OperationalConditions) -> float: # noqa: N802 : Total equivalent toroidal stiffness of the TF case [Pa]. """ + # TODO possible reason for floating point difference ky_lat = self.Ky_lat(op_cond) temp = [ summation([ diff --git a/bluemira/magnets/conductor.py b/bluemira/magnets/conductor.py index 31b40d01cc..02277f93af 100644 --- a/bluemira/magnets/conductor.py +++ b/bluemira/magnets/conductor.py @@ -574,8 +574,7 @@ def __str__(self) -> str: ) -class SymmetricConductor(Conductor): # jm - actually worthwhile or just set up - # conductor with dx = dy and don't duplicate? +class SymmetricConductor(Conductor): """ Representation of a symmetric conductor in which both jacket and insulator mantain a constant thickness (i.e. dy_jacket = dx_jacket and dy_ins = dx_ins). @@ -624,13 +623,13 @@ def __init__( ) @property - def dy_jacket(self): + def dy_jacket(self) -> float: """ y-thickness of the jacket [m]. Returns ------- - float + : Notes ----- @@ -639,13 +638,13 @@ def dy_jacket(self): return self.dx_jacket @property - def dy_ins(self): + def dy_ins(self) -> float: """ y-thickness of the insulator [m]. Returns ------- - float + : Notes ----- @@ -659,7 +658,7 @@ def to_dict(self) -> dict: Returns ------- - dict + : Dictionary with serialised symmetric conductor data. """ return { From 173b40f8af16e671d8c7458680b949e23e493ff9 Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Fri, 12 Sep 2025 10:49:55 +0100 Subject: [PATCH 57/61] =?UTF-8?q?=F0=9F=8E=A8=20Cleanup=20with=20units?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bluemira/magnets/tfcoil_designer.py | 301 +++++++++++++----------- examples/magnets/example_tf_creation.py | 46 ++-- 2 files changed, 184 insertions(+), 163 deletions(-) diff --git a/bluemira/magnets/tfcoil_designer.py b/bluemira/magnets/tfcoil_designer.py index 577f573aee..f796d11fca 100644 --- a/bluemira/magnets/tfcoil_designer.py +++ b/bluemira/magnets/tfcoil_designer.py @@ -5,7 +5,7 @@ # SPDX-License-Identifier: LGPL-2.1-or-later """Designer for TF Coil XY cross section.""" -from collections.abc import Callable +from collections.abc import Callable, Iterable from dataclasses import dataclass from typing import Any @@ -59,54 +59,6 @@ class TFCoilXYDesignerParams(ParameterFrame): B_ref: Parameter[float] """Reference value for B field (LTS limit) [T]""" - # # strand params - # d_strand_sc: Parameter[float] - # """Diameter of superconducting strand""" - # d_strand: Parameter[float] - # """Diameter of stabilising strand""" - # operating_temperature: Parameter[float] - # """Operating temperature for the strands [K]""" - - # # cable params - # n_sc_strand: Parameter[int] - # """Number of superconducting strands.""" - # n_stab_strand: Parameter[int] - # """Number of stabilising strands.""" - # d_cooling_channel: Parameter[float] - # """Diameter of the cooling channel [m].""" - # void_fraction: Parameter[float] - # """Ratio of material volume to total volume [unitless].""" - # cos_theta: Parameter[float] - # """Correction factor for twist in the cable layout.""" - # dx: Parameter[float] - # """Cable half-width in the x-direction [m].""" - - # # conductor params - # dx_jacket: Parameter[float] - # """x-thickness of the jacket [m].""" - # dy_jacket: Parameter[float] - # """y-tickness of the jacket [m].""" - # dx_ins: Parameter[float] - # """x-thickness of the insulator [m].""" - # dy_ins: Parameter[float] - # """y-thickness of the insulator [m].""" - - # # winding pack params - # nx: Parameter[int] - # """Number of conductors along the x-axis.""" - # ny: Parameter[int] - # """Number of conductors along the y-axis.""" - - # # case params - # Rk: Parameter[float] - # """Internal radius of the TF coil case [m].""" - # theta_TF: Parameter[float] - # """Toroidal angular span of the TF coil [degrees].""" - # dy_ps: Parameter[float] - # """Radial thickness of the poloidal support region [m].""" - # dy_vault: Parameter[float] - # """Radial thickness of the vault support region [m].""" - Iop: Parameter[float] """Operational current in conductor""" T_sc: Parameter[float] @@ -119,9 +71,64 @@ class TFCoilXYDesignerParams(ParameterFrame): """Strain on system""" +@dataclass +class StrandParams(ParameterFrame): + """Stand parameters""" + + d_strand: Parameter[float] + operating_temperature: Parameter[float] + + +@dataclass +class CableParams(ParameterFrame): + """Cable parameters""" + + d_cooling_channel: Parameter[float] + void_fraction: Parameter[float] + cos_theta: Parameter[float] + dx: Parameter[float] + + +@dataclass +class CableParamsWithE(CableParams): + """Cable parameters with custom youngs modulus""" + + E: Parameter[float] + + +@dataclass +class SymConductorParams(ParameterFrame): + """Symmetric conductor parameters""" + + dx_ins: Parameter[float] + dx_jacket: Parameter[float] + + +@dataclass +class ConductorParams(SymConductorParams): + """Conductor parameters""" + + dy_ins: Parameter[float] + dy_jacket: Parameter[float] + + +@dataclass +class CaseParams(ParameterFrame): + """Case parameters""" + + # Ri: Parameter[float] + # Rk: Parameter[float] + theta_TF: Parameter[float] + dy_ps: Parameter[float] + dy_vault: Parameter[float] + + @dataclass class DerivedTFCoilXYDesignerParams: + """Derived parameters for TF coils cross section design""" + a: float + """Aspect ratio""" Ri: float """External radius of the TF coil case [m].""" Re: float @@ -134,11 +141,12 @@ class DerivedTFCoilXYDesignerParams: min_gap_x: float I_fun: Callable[[float], float] B_fun: Callable[[float], float] - strain: float @dataclass class OptimisationConfig: + """Optimisation configuration""" + t0: float """Initial time""" Tau_discharge: float @@ -161,15 +169,27 @@ class OptimisationConfig: """Convergence threshold for the combined optimisation loop.""" +@dataclass +class StabilisingStrandRes: + """Cable opt results""" + + cable: Any + solution: Any + info_text: str + + @dataclass class TFCoilXY: + """TFCoil cross section solution""" + case: CaseTF - cable_soln: Any + cable_soln: StabilisingStrandRes convergence: npt.NDArray derived_params: DerivedTFCoilXYDesignerParams op_config: OptimisationConfig - def plot_I_B(self, ax, n_steps=300): + def plot_I_B(self, ax, n_steps=300): # noqa: N802 + """Plot current and magnetic field evolution in optimisation""" time_steps = np.linspace( self.op_config.t0, self.op_config.Tau_discharge, n_steps ) @@ -198,6 +218,7 @@ def plot_I_B(self, ax, n_steps=300): ax.figure.tight_layout() def plot_cable_temperature_evolution(self, ax, n_steps=100): + """Plot temperature evolution in optimisation""" solution = self.cable_soln.solution ax.plot(solution.t, solution.y[0], "r*", label="Simulation points") @@ -224,10 +245,13 @@ def plot_cable_temperature_evolution(self, ax, n_steps=100): ) ax.figure.tight_layout() - def plot_summary(self, n_steps, show=False): + def plot_summary(self, n_steps, *, show=False): + """Plot summary of optimisation""" # noqa: DOC201 f, (ax_temp, ax_ib) = plt.subplots(2, 1, figsize=(8, 8), sharex=True) self.plot_cable_temperature_evolution(ax_temp, n_steps) self.plot_I_B(ax_ib, n_steps * 3) + if show: + plt.show() return f def plot( @@ -237,9 +261,10 @@ def plot( show: bool = False, homogenised: bool = False, ) -> plt.Axes: + """Plot the full cross section""" # noqa: DOC201 return self.case.plot(ax=ax, show=show, homogenised=homogenised) - def plot_convergence(self): + def plot_convergence(self, *, show: bool = False): """ Plot the evolution of thicknesses and error values over optimisation iterations. @@ -279,7 +304,8 @@ def plot_convergence(self): axs[1].grid(visible=True) plt.tight_layout() - plt.show() + if show: + plt.show() class TFCoilXYDesigner(Designer[TFCoilXY]): @@ -306,12 +332,12 @@ def __init__( def _derived_values(self, op_config: OptimisationConfig): # Needed params that are calculated using the base params - R0 = self.params.R0.value + R0 = self.params.R0.value # noqa: N806 n_TF = self.params.n_TF.value B0 = self.params.B0.value a = R0 / self.params.A.value - Ri = R0 - a - self.params.d.value - Re = (R0 + a) * (1 / self.params.ripple.value) ** (1 / n_TF) + Ri = R0 - a - self.params.d.value # noqa: N806 + Re = (R0 + a) * (1 / self.params.ripple.value) ** (1 / n_TF) # noqa: N806 B_TF_i = 1.08 * (MU_0_2PI * n_TF * (B0 * R0 / MU_0_2PI / n_TF) / Ri) t_z = 0.5 * np.log(Re / Ri) * MU_0_4PI * n_TF * (B0 * R0 / MU_0_2PI / n_TF) ** 2 return DerivedTFCoilXYDesignerParams( @@ -336,7 +362,6 @@ def _derived_values(self, op_config: OptimisationConfig): B_fun=delayed_exp_func( B_TF_i, op_config.Tau_discharge, self.params.t_delay.value ), - strain=self.params.strain.value, ) def B_TF_r(self, tf_current, r): @@ -370,16 +395,16 @@ def run(self): TF case object all parts that make it up. """ wp_config = self.build_config.get("winding_pack") - n_WPs = int(wp_config.get("sets")) + n_wp = int(wp_config.get("sets", 1)) optimisation_params = OptimisationConfig( **self.build_config.get("optimisation_params") ) derived_params = self._derived_values(optimisation_params) - # param frame optimisation stuff? + # Only a singular type of cable and conductor currently possible cable = self.optimise_cable_n_stab_ths( - self._make_cable(n_wp, WP_i=0), + self._make_cable(wp_i=0), t0=optimisation_params.t0, tf=optimisation_params.Tau_discharge, initial_temperature=derived_params.T_op, @@ -388,11 +413,9 @@ def run(self): I_fun=derived_params.I_fun, bounds=[1, 10000], ) - conductor = self._make_conductor(cable.cable, n_WPs, WP_i=0) - wp_params = self._check_arrays_match(n_WPs, wp_config.pop("params")) + conductor = self._make_conductor(cable.cable, wp_i=0) winding_pack = [ - self._make_winding_pack(conductor, i_WP, wp_config, wp_params) - for i_WP in range(n_WPs) + self._make_winding_pack(conductor, wp_i, wp_config) for wp_i in range(n_wp) ] # param frame optimisation stuff? @@ -403,7 +426,7 @@ def run(self): op_cond=OperationalConditions( temperature=derived_params.T_op, magnetic_field=derived_params.B_TF_i, - strain=derived_params.strain, + strain=self.params.strain.value, ), allowable_sigma=derived_params.s_y, bounds_cond_jacket=optimisation_params.bounds_cond_jacket, @@ -420,8 +443,8 @@ def run(self): case, cable, convergence_array, derived_params, optimisation_params ) + @staticmethod def optimise_cable_n_stab_ths( - self, cable, t0: float, tf: float, @@ -504,12 +527,6 @@ def optimise_cable_n_stab_ths( f"Final temperature with optimal n_stab: {final_temperature:.2f} Kelvin" ) - @dataclass - class StabilisingStrandRes: - cable: Any - solution: Any - info_text: str - return StabilisingStrandRes( cable, solution, @@ -539,7 +556,7 @@ def optimise_jacket_and_vault( max_niter: int = 10, eps: float = 1e-8, n_conds: int | None = None, - ): + ) -> tuple[CaseTF, npt.NDArray]: """ Jointly optimise the conductor jacket and case vault thickness under electromagnetic loading constraints. @@ -583,6 +600,13 @@ def optimise_jacket_and_vault( Target total number of conductors in the winding pack. If None, the self number of conductors is used. + Returns + ------- + : + Case object + : + convergence array + Notes ----- The function modifies the internal state of `conductor` and `self.dy_vault`. @@ -719,8 +743,8 @@ def optimise_jacket_and_vault( return case, np.array(convergence_array) + @staticmethod def optimise_jacket_conductor( - self, conductor, pressure: float, f_z: float, @@ -807,8 +831,8 @@ def optimise_jacket_conductor( return result + @staticmethod def optimise_vault_radial_thickness( - self, case, pm: float, fz: float, @@ -862,22 +886,9 @@ def optimise_vault_radial_thickness( return result - def _check_arrays_match(self, n_WPs, param_list): - if n_WPs > 1: - for param in param_list: - if np.size(param_list[param]) != n_WPs: - param_list[param] = [param_list[param] for _ in range(n_WPs)] - return param_list - if n_WPs == 1: - return param_list - raise ValueError( - f"Invalid value {n_WPs} for winding pack 'sets' in config." - "Value should be an integer >= 1." - ) - - def _make_strand(self, i_WP, config, params): + def _make_strand(self, wp_i, config, params): cls_name = config["class"] - stab_strand_cls = get_class_from_module( + strand_cls = get_class_from_module( cls_name, default_module="bluemira.magnets.strand" ) material_mix = [] @@ -891,100 +902,110 @@ def _make_strand(self, i_WP, config, params): material_mix.append( MaterialFraction(material=material_data, fraction=m["fraction"]) ) - return stab_strand_cls( + + return strand_cls( materials=material_mix, - d_strand=params["d_strand"][i_WP], - operating_temperature=params["operating_temperature"][i_WP], - name="stab_strand", + d_strand=self._check_iterable(wp_i, params.d_strand.value), + operating_temperature=self._check_iterable( + wp_i, params.operating_temperature.value + ), + name=config.get("name", cls_name.rsplit("::", 1)[-1]), ) - def _make_cable_cls(self, stab_strand, sc_strand, i_WP, config, params): + @staticmethod + def _check_iterable(wp_i, param_val): + return param_val[wp_i] if isinstance(param_val, Iterable) else param_val + + def _make_cable_cls(self, stab_strand, sc_strand, wp_i, config, params): cls_name = config["class"] cable_cls = get_class_from_module( cls_name, default_module="bluemira.magnets.cable" ) + extras = {} + if issubclass(cable_cls, RectangularCable): + extras["dx"] = self._check_iterable(wp_i, params.dx.value) + if hasattr(params, "E"): + extras["E"] = self._check_iterable(wp_i, params.E.value) return cable_cls( sc_strand=sc_strand, stab_strand=stab_strand, - n_sc_strand=params["n_sc_strand"][i_WP], - n_stab_strand=params["n_stab_strand"][i_WP], - d_cooling_channel=params["d_cooling_channel"][i_WP], - void_fraction=params["void_fraction"][i_WP], - cos_theta=params["cos_theta"][i_WP], + n_sc_strand=self._check_iterable(wp_i, config["n_sc_strand"]), + n_stab_strand=self._check_iterable(wp_i, config["n_stab_strand"]), + d_cooling_channel=self._check_iterable(wp_i, params.d_cooling_channel.value), + void_fraction=self._check_iterable(wp_i, params.void_fraction.value), + cos_theta=self._check_iterable(wp_i, params.cos_theta.value), name=config.get("name", cls_name.rsplit("::", 1)[-1]), - **( - {"dx": params["dx"][i_WP], "E": params["E"][i_WP]} - if issubclass(cable_cls, RectangularCable) - else {"E": params["E"][i_WP]} - ), + **extras, ) - def _make_cable(self, n_WPs, WP_i): + def _make_cable(self, wp_i): stab_strand_config = self.build_config.get("stabilising_strand") sc_strand_config = self.build_config.get("superconducting_strand") cable_config = self.build_config.get("cable") - stab_strand_params = self._check_arrays_match( - n_WPs, stab_strand_config.get("params") - ) - sc_strand_params = self._check_arrays_match( - n_WPs, sc_strand_config.get("params") - ) + stab_strand_params = StrandParams.from_dict(stab_strand_config.pop("params")) + sc_strand_params = StrandParams.from_dict(sc_strand_config.pop("params")) + cable_params = cable_config.pop("params") - cable_params = self._check_arrays_match(n_WPs, cable_config.get("params")) + cable_params = ( + CableParamsWithE.from_dict(cable_params) + if "E" in cable_params + else CableParams.from_dict(cable_params) + ) - stab_strand = self._make_strand(WP_i, stab_strand_config, stab_strand_params) - sc_strand = self._make_strand(WP_i, sc_strand_config, sc_strand_params) + stab_strand = self._make_strand(wp_i, stab_strand_config, stab_strand_params) + sc_strand = self._make_strand(wp_i, sc_strand_config, sc_strand_params) return self._make_cable_cls( - stab_strand, sc_strand, WP_i, cable_config, cable_params + stab_strand, sc_strand, wp_i, cable_config, cable_params ) - def _make_conductor_cls(self, cable, i_WP, config, params): + def _make_conductor(self, cable, wp_i=0): + # current functionality requires conductors are the same for both WPs + # in future allow for different conductor objects so can vary cable and strands + # between the sets of the winding pack? + config = self.build_config.get("conductor") + cls_name = config["class"] conductor_cls = get_class_from_module( cls_name, default_module="bluemira.magnets.conductor" ) + if issubclass(conductor_cls, SymmetricConductor): + params = SymConductorParams.from_dict(config.pop("params")) + else: + params = ConductorParams.from_dict(config.pop("params")) + return conductor_cls( cable=cable, mat_jacket=config["jacket_material"], mat_ins=config["ins_material"], - dx_jacket=params["dx_jacket"][i_WP], - dx_ins=params["dx_ins"][i_WP], + dx_jacket=self._check_iterable(wp_i, params.dx_jacket.value), + dx_ins=self._check_iterable(wp_i, params.dx_ins.value), name=config.get("name", cls_name.rsplit("::", 1)[-1]), **( {} if issubclass(conductor_cls, SymmetricConductor) else { - "dy_jacket": params["dy_jacket"][i_WP], - "dy_ins": params["dy_ins"][i_WP], + "dy_jacket": self._check_iterable(wp_i, params.dy_jacket.value), + "dy_ins": self._check_iterable(wp_i, params.dy_ins.value), } ), ) - def _make_conductor(self, cable, n_wp, WP_i=0): - # current functionality requires conductors are the same for both WPs - # in future allow for different conductor objects so can vary cable and strands - # between the sets of the winding pack? - conductor_config = self.build_config.get("conductor") - conductor_params = self._check_arrays_match(n_wp, conductor_config.get("params")) - - return self._make_conductor_cls(cable, WP_i, conductor_config, conductor_params) - - def _make_winding_pack(self, conductor, i_WP, config, params): + def _make_winding_pack(self, conductor, wp_i, config): cls_name = config["class"] winding_pack_cls = get_class_from_module( cls_name, default_module="bluemira.magnets.winding_pack" ) return winding_pack_cls( conductor=conductor, - nx=int(params["nx"][i_WP]), - ny=int(params["ny"][i_WP]), + nx=int(self._check_iterable(wp_i, config["nx"])), + ny=int(self._check_iterable(wp_i, config["ny"])), name="winding_pack", ) def _make_case(self, WPs, derived_params, optimisation_params): # noqa: N803 config = self.build_config.get("case") - params = config.get("params") + params = CaseParams.from_dict(config.get("params")) cls_name = config["class"] case_cls = get_class_from_module( @@ -992,10 +1013,10 @@ def _make_case(self, WPs, derived_params, optimisation_params): # noqa: N803 ) case = case_cls( - Ri=params["Ri"], - theta_TF=params["theta_TF"], - dy_ps=params["dy_ps"], - dy_vault=params["dy_vault"], + Ri=derived_params.Ri, + theta_TF=params.theta_TF.value, + dy_ps=params.dy_ps.value, + dy_vault=params.dy_vault.value, mat_case=config["material"], WPs=WPs, name=config.get("name", cls_name.rsplit("::", 1)[-1]), diff --git a/examples/magnets/example_tf_creation.py b/examples/magnets/example_tf_creation.py index f61ed132fc..cd6de9cb57 100644 --- a/examples/magnets/example_tf_creation.py +++ b/examples/magnets/example_tf_creation.py @@ -26,8 +26,8 @@ "class": "Strand", "materials": [{"material": COPPER_300, "fraction": 1.0}], "params": { - "d_strand": 1.0e-3, - "operating_temperature": 5.7, + "d_strand": {"value": 1.0e-3, "unit": "m"}, + "operating_temperature": {"value": 5.7, "unit": "K"}, }, }, "superconducting_strand": { @@ -37,20 +37,20 @@ {"material": COPPER_100, "fraction": 0.5}, ], "params": { - "d_strand": 1.0e-3, - "operating_temperature": 5.7, + "d_strand": {"value": 1.0e-3, "unit": "m"}, + "operating_temperature": {"value": 5.7, "unit": "K"}, }, }, "cable": { "class": "RectangularCable", + "n_sc_strand": 321, + "n_stab_strand": 476, "params": { - "n_sc_strand": 321, - "n_stab_strand": 476, - "d_cooling_channel": 0.01, - "void_fraction": 0.7, - "cos_theta": 0.97, - "dx": 0.017324217577247843 * 2, - "E": 0.1e9, + "d_cooling_channel": {"value": 0.01, "unit": "m"}, + "void_fraction": {"value": 0.7, "unit": ""}, + "cos_theta": {"value": 0.97, "unit": ""}, + "dx": {"value": 0.017324217577247843 * 2, "unit": "m"}, + "E": {"value": 0.1e9, "unit": ""}, }, }, "conductor": { @@ -58,29 +58,27 @@ "jacket_material": SS316_LN_MAG, "ins_material": DUMMY_INSULATOR_MAG, "params": { - "dx_jacket": 0.0015404278406243683 * 2, + "dx_jacket": {"value": 0.0015404278406243683 * 2, "unit": "m"}, # "dy_jacket": 0.0, - "dx_ins": 0.0005 * 2, + "dx_ins": {"value": 0.0005 * 2, "unit": "m"}, # "dy_ins": 0.0, }, }, "winding_pack": { "class": "WindingPack", "sets": 2, - "params": { - "nx": [25, 18], - "ny": [6, 1], - }, + "nx": [25, 18], + "ny": [6, 1], }, "case": { "class": "TrapezoidalCaseTF", "material": SS316_LN_MAG, "params": { - "Ri": 3.708571428571428, - "Rk": 0.0, - "theta_TF": 22.5, - "dy_ps": 0.028666666666666667 * 2, - "dy_vault": 0.22647895819808084 * 2, + # "Ri": {"value": 3.708571428571428, "unit": "m"}, + # "Rk": {"value": 0, "unit": "m"}, + "theta_TF": {"value": 22.5, "unit": "deg"}, + "dy_ps": {"value": 0.028666666666666667 * 2, "unit": "m"}, + "dy_vault": {"value": 0.22647895819808084 * 2, "unit": "m"}, }, }, "optimisation_params": { @@ -115,6 +113,8 @@ "t_delay": {"value": 3, "unit": "s"}, "strain": {"value": 0.0055, "unit": ""}, } + tf_coil_xy = TFCoilXYDesigner(params=params, build_config=config).execute() tf_coil_xy.plot(show=True, homogenised=False) -# tf_coil_xy.plot_convergence() +tf_coil_xy.plot_convergence(show=True) +tf_coil_xy.plot_summary(100, show=True) From bd4f43ea0989bec428cf547c1d029a72692f7ead Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Fri, 12 Sep 2025 11:16:11 +0100 Subject: [PATCH 58/61] =?UTF-8?q?=F0=9F=8E=A8=20Make=20tests=20pass?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bluemira/magnets/cable.py | 28 +++++++---- tests/magnets/test_cable.py | 89 ++++++++++----------------------- tests/magnets/test_conductor.py | 28 ++--------- tests/magnets/test_strand.py | 22 ++++---- 4 files changed, 60 insertions(+), 107 deletions(-) diff --git a/bluemira/magnets/cable.py b/bluemira/magnets/cable.py index 1be0524136..278806087e 100644 --- a/bluemira/magnets/cable.py +++ b/bluemira/magnets/cable.py @@ -17,12 +17,13 @@ from scipy.integrate import solve_ivp from bluemira.base.look_and_feel import bluemira_debug -from bluemira.magnets.strand import Strand, SuperconductingStrand from bluemira.magnets.utils import reciprocal_summation, summation if TYPE_CHECKING: from collections.abc import Callable + from bluemira.magnets.strand import Strand, SuperconductingStrand + class ABCCable(ABC): """ @@ -91,11 +92,13 @@ def __init__( youngs_modulus: Callable[[Any, OperationalConditions], float] | float | None = ( props.pop("E", None) ) + + def ym(op_cond): + raise NotImplementedError("E for Cable is not implemented.") + if "E" not in vars(type(self)): if youngs_modulus is None: - - def youngs_modulus(op_cond): - raise NotImplementedError("E for Cable is not implemented.") + youngs_modulus = ym self.E = ( youngs_modulus @@ -108,7 +111,7 @@ def youngs_modulus(op_cond): for k, v in props.items(): setattr(self, k, v if callable(v) else lambda *arg, v=v, **kwargs: v) # noqa: ARG005 self._props = list(props.keys()) + ( - [] if "E" in vars(type(self)) or youngs_modulus is None else ["E"] + [] if "E" in vars(type(self)) or youngs_modulus == ym else ["E"] ) @property @@ -355,7 +358,12 @@ def Ky(self, op_cond: OperationalConditions): # noqa: N802 """Total equivalent stiffness along y-axis""" def plot( - self, xc: float = 0, yc: float = 0, *, show: bool = False, ax=plt.Axes | None + self, + xc: float = 0, + yc: float = 0, + *, + show: bool = False, + ax: plt.Axes | None = None, ): """ Plot a schematic view of the cable cross-section. @@ -451,7 +459,7 @@ def __str__(self) -> str: f"n stab strand: {self.n_stab_strand}" ) - def to_dict(self) -> dict[str, str | float | int | dict[str, Any]]: + def to_dict(self, op_cond) -> dict[str, str | float | int | dict[str, Any]]: """ Serialise the cable instance to a dictionary. @@ -469,7 +477,7 @@ def to_dict(self) -> dict[str, str | float | int | dict[str, Any]]: "cos_theta": self.cos_theta, "sc_strand": self.sc_strand.to_dict(), "stab_strand": self.stab_strand.to_dict(), - **{k: getattr(k)() for k in self._props}, + **{k: getattr(self, k)(op_cond) for k in self._props}, } @@ -589,7 +597,7 @@ def Ky(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ return self.E(op_cond) * self.dx / self.dy - def to_dict(self) -> dict[str, Any]: + def to_dict(self, op_cond) -> dict[str, Any]: """ Serialise the rectangular cable into a dictionary. @@ -598,7 +606,7 @@ def to_dict(self) -> dict[str, Any]: : Dictionary including rectangular cable parameters. """ - data = super().to_dict() + data = super().to_dict(op_cond) data.update({ "dx": self.dx, "aspect_ratio": self.aspect_ratio, diff --git a/tests/magnets/test_cable.py b/tests/magnets/test_cable.py index 0dd05dada7..471bc758fe 100644 --- a/tests/magnets/test_cable.py +++ b/tests/magnets/test_cable.py @@ -5,7 +5,6 @@ # SPDX-License-Identifier: LGPL-2.1-or-later -import matplotlib.pyplot as plt import numpy as np import pytest from eurofusion_materials.library.magnet_branch_mats import ( @@ -15,18 +14,13 @@ from matproplib import OperationalConditions from matproplib.material import MaterialFraction -from bluemira.magnets.cable import ( - DummyRoundCableLTS, - DummySquareCableLTS, - RectangularCable, -) +from bluemira.magnets.cable import RectangularCable, RoundCable, SquareCable from bluemira.magnets.strand import Strand, SuperconductingStrand +from bluemira.magnets.tfcoil_designer import TFCoilXYDesigner DummySteel = SS316_LN_MAG DummySuperconductor = NB3SN_MAG -# -- Pytest Fixtures ---------------------------------------------------------- - @pytest.fixture def sc_strand(): @@ -34,6 +28,7 @@ def sc_strand(): name="SC", materials=[MaterialFraction(material=DummySuperconductor, fraction=1.0)], d_strand=0.001, + operating_temperature=5.7, ) @@ -43,6 +38,7 @@ def stab_strand(): name="Stab", materials=[MaterialFraction(material=DummySteel, fraction=1.0)], d_strand=0.001, + operating_temperature=5.7, ) @@ -55,12 +51,11 @@ def cable(sc_strand, stab_strand): n_sc_strand=10, n_stab_strand=20, d_cooling_channel=0.001, + void_fraction=0.725, + cos_theta=0.97, ) -# -- Core Cable Tests --------------------------------------------------------- - - def test_geometry_and_area(cable): assert cable.dx > 0 assert cable.dy > 0 @@ -86,8 +81,7 @@ def test_str_output(cable): assert "stab strand" in summary -def test_plot(monkeypatch, cable): - monkeypatch.setattr(plt, "show", lambda: None) +def test_plot(cable): ax = cable.plot(show=True) assert hasattr(ax, "fill") @@ -103,7 +97,7 @@ def I_fun(t): # noqa: ARG001 assert result.success -def test_optimize_n_stab_ths(monkeypatch, sc_strand, stab_strand): +def test_optimise_n_stab_ths(sc_strand, stab_strand): cable = RectangularCable( dx=0.01, sc_strand=sc_strand, @@ -111,8 +105,9 @@ def test_optimize_n_stab_ths(monkeypatch, sc_strand, stab_strand): n_sc_strand=10, n_stab_strand=5, d_cooling_channel=0.001, + void_fraction=0.725, + cos_theta=0.97, ) - monkeypatch.setattr(plt, "show", lambda: None) def B_fun(t): # noqa: ARG001 return 5 @@ -120,7 +115,8 @@ def B_fun(t): # noqa: ARG001 def I_fun(t): # noqa: ARG001 return 1000 - result = cable.optimize_n_stab_ths( + result = TFCoilXYDesigner.optimise_cable_n_stab_ths( + cable, t0=0, tf=0.1, initial_temperature=20, @@ -129,49 +125,29 @@ def I_fun(t): # noqa: ARG001 I_fun=I_fun, bounds=(1, 100), ) - assert result.success - - -def test_invalid_parameters(sc_strand, stab_strand): - with pytest.raises(ValueError, match="dx must be positive"): - RectangularCable( - dx=-0.01, - sc_strand=sc_strand, - stab_strand=stab_strand, - n_sc_strand=10, - n_stab_strand=5, - d_cooling_channel=0.001, - ) - - with pytest.raises(ValueError, match="void_fraction must be between 0 and 1"): - RectangularCable( - dx=0.01, - sc_strand=sc_strand, - stab_strand=stab_strand, - n_sc_strand=10, - n_stab_strand=5, - d_cooling_channel=0.001, - void_fraction=1.5, - ) - - -# -- Square & Round Cable Types ----------------------------------------------- + assert result.solution.success def test_square_and_round_cables(sc_strand, stab_strand): - square = DummySquareCableLTS( + square = SquareCable( sc_strand=sc_strand, stab_strand=stab_strand, n_sc_strand=5, n_stab_strand=5, d_cooling_channel=0.001, + void_fraction=0.725, + cos_theta=0.97, + E=0.1e9, ) - round_ = DummyRoundCableLTS( + round_ = RoundCable( sc_strand=sc_strand, stab_strand=stab_strand, n_sc_strand=5, n_stab_strand=5, d_cooling_channel=0.001, + void_fraction=0.725, + cos_theta=0.97, + E=0.1e9, ) dummy_op_cond = OperationalConditions(temperature=4.0) assert square.dx > 0 @@ -191,7 +167,7 @@ def test_square_and_round_cables(sc_strand, stab_strand): assert np.isclose(round_.Ky(dummy_op_cond), round_.E(dummy_op_cond), rtol=1e-8) -def test_cable_to_from_dict(sc_strand, stab_strand): +def test_cable_to_dict(sc_strand, stab_strand): # Create a RectangularCable for testing cable_original = RectangularCable( dx=0.01, @@ -200,24 +176,11 @@ def test_cable_to_from_dict(sc_strand, stab_strand): n_sc_strand=10, n_stab_strand=5, d_cooling_channel=0.001, + void_fraction=0.725, + cos_theta=0.97, ) # Convert to dictionary - cable_dict = cable_original.to_dict() + cable_dict = cable_original.to_dict(OperationalConditions(temperature=5)) - # Reconstruct from dictionary - cable_reconstructed = RectangularCable.from_dict(cable_dict) - - # Verify key attributes match - assert cable_original.n_sc_strand == cable_reconstructed.n_sc_strand - assert cable_original.n_stab_strand == cable_reconstructed.n_stab_strand - assert cable_original.dx == pytest.approx(cable_reconstructed.dx) - assert cable_original.d_cooling_channel == pytest.approx( - cable_reconstructed.d_cooling_channel - ) - assert cable_original.void_fraction == pytest.approx( - cable_reconstructed.void_fraction - ) - assert cable_original.cos_theta == pytest.approx(cable_reconstructed.cos_theta) - assert cable_original.sc_strand.name == cable_reconstructed.sc_strand.name - assert cable_original.stab_strand.name == cable_reconstructed.stab_strand.name + assert cable_dict["n_sc_strand"] == 10 diff --git a/tests/magnets/test_conductor.py b/tests/magnets/test_conductor.py index c3f1d2c3a9..ccaae0d6fa 100644 --- a/tests/magnets/test_conductor.py +++ b/tests/magnets/test_conductor.py @@ -19,10 +19,6 @@ from bluemira.magnets.conductor import Conductor from bluemira.magnets.strand import Strand, SuperconductingStrand -# --------------------------------------------------------------------------- -# Fixtures -# --------------------------------------------------------------------------- - @pytest.fixture def mat_jacket(): @@ -42,6 +38,7 @@ def sc_strand(): name="SC", materials=[MaterialFraction(material=sc, fraction=1.0)], d_strand=0.001, + operating_temperature=5.7, ) @@ -53,6 +50,7 @@ def stab_strand(): name="Stab", materials=[MaterialFraction(material=stab, fraction=1.0)], d_strand=0.001, + operating_temperature=5.7, ) @@ -65,6 +63,8 @@ def rectangular_cable(sc_strand, stab_strand): n_sc_strand=10, n_stab_strand=10, d_cooling_channel=0.001, + void_fraction=0.725, + cos_theta=0.97, ) @@ -82,11 +82,6 @@ def conductor(rectangular_cable, mat_jacket, mat_ins): ) -# --------------------------------------------------------------------------- -# Tests -# --------------------------------------------------------------------------- - - def test_geometry_and_area(conductor): assert conductor.dx > 0 assert conductor.dy > 0 @@ -105,18 +100,3 @@ def test_plot(monkeypatch, conductor): monkeypatch.setattr(plt, "show", lambda: None) ax = conductor.plot(show=True) assert hasattr(ax, "fill") - - -def test_to_from_dict(conductor): - config = conductor.to_dict() - restored = Conductor.from_dict(config) - - assert restored.name == conductor.name - assert restored.dx_jacket == pytest.approx(conductor.dx_jacket) - assert restored.dy_jacket == pytest.approx(conductor.dy_jacket) - assert restored.dx_ins == pytest.approx(conductor.dx_ins) - assert restored.dy_ins == pytest.approx(conductor.dy_ins) - assert restored.mat_jacket.name == conductor.mat_jacket.name - assert restored.mat_ins.name == conductor.mat_ins.name - assert restored.cable.n_sc_strand == conductor.cable.n_sc_strand - assert restored.cable.sc_strand.name == conductor.cable.sc_strand.name diff --git a/tests/magnets/test_strand.py b/tests/magnets/test_strand.py index af5190fb34..864444e81e 100644 --- a/tests/magnets/test_strand.py +++ b/tests/magnets/test_strand.py @@ -26,34 +26,36 @@ def test_strand_area(): mat = MaterialFraction(material=DummySuperconductor1, fraction=1.0) - strand = Strand(name="test_strand", materials=[mat], d_strand=0.001) + strand = Strand( + name="test_strand", materials=[mat], d_strand=0.001, operating_temperature=5.7 + ) expected_area = np.pi * (0.001**2) / 4 assert np.isclose(strand.area, expected_area) -def test_strand_invalid_diameter(): - mat = MaterialFraction(material=DummySuperconductor1, fraction=1.0) - with pytest.raises(ValueError, match="positive"): - Strand(name="invalid_strand", materials=[mat], d_strand=-0.001) - - def test_superconducting_strand_invalid_materials(): # Two superconductors — should raise ValueError mat1 = MaterialFraction(material=DummySuperconductor1, fraction=0.5) mat2 = MaterialFraction(material=DummySuperconductor2, fraction=0.5) with pytest.raises(ValueError, match="Only one superconductor material"): - SuperconductingStrand(name="invalid", materials=[mat1, mat2]) + SuperconductingStrand( + name="invalid", materials=[mat1, mat2], d_strand=0, operating_temperature=0 + ) # No superconductors — should raise ValueError mat3 = MaterialFraction(material=DummySteel, fraction=1.0) with pytest.raises(ValueError, match="No superconducting material"): - SuperconductingStrand(name="invalid", materials=[mat3]) + SuperconductingStrand( + name="invalid", materials=[mat3], d_strand=0, operating_temperature=0 + ) def test_strand_material_properties(): sc = DummySuperconductor1 mat = MaterialFraction(material=sc, fraction=1.0) - strand = Strand(name="mat_test", materials=[mat], d_strand=0.001) + strand = Strand( + name="mat_test", materials=[mat], d_strand=0.001, operating_temperature=5.7 + ) temperature = 20 op_cond = OperationalConditions(temperature=20) From 85ac2b9b1a9f00733d5debc84140b900d9da61cc Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Mon, 15 Sep 2025 08:26:05 +0100 Subject: [PATCH 59/61] =?UTF-8?q?=F0=9F=8E=A8=20Simplifications=20and=20ty?= =?UTF-8?q?ping?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bluemira/magnets/cable.py | 92 ++++-------- bluemira/magnets/case_tf.py | 137 +++++++----------- bluemira/magnets/tfcoil_designer.py | 103 ++++++++----- bluemira/magnets/utils.py | 2 +- examples/magnets/example_tf_creation.py | 5 +- examples/magnets/example_tf_wp_from_dict.py | 14 +- .../magnets/example_tf_wp_optimization.py | 9 +- tests/magnets/test_cable.py | 6 +- 8 files changed, 167 insertions(+), 201 deletions(-) diff --git a/bluemira/magnets/cable.py b/bluemira/magnets/cable.py index 278806087e..72236c23cb 100644 --- a/bluemira/magnets/cable.py +++ b/bluemira/magnets/cable.py @@ -147,9 +147,9 @@ def rho(self, op_cond: OperationalConditions): Averaged mass density in kg/m³. """ return ( - self.sc_strand.rho(op_cond) * self.area_sc - + self.stab_strand.rho(op_cond) * self.area_stab - ) / (self.area_sc + self.area_stab) + self.sc_strand.rho(op_cond) * self.area_sc_region + + self.stab_strand.rho(op_cond) * self.area_stab_region + ) / (self.area_sc_region + self.area_stab_region) def erho(self, op_cond: OperationalConditions) -> float: """ @@ -168,8 +168,8 @@ def erho(self, op_cond: OperationalConditions) -> float: resistivity [Ohm m] """ resistances = np.array([ - self.sc_strand.erho(op_cond) / self.area_sc, - self.stab_strand.erho(op_cond) / self.area_stab, + self.sc_strand.erho(op_cond) / self.area_sc_region, + self.stab_strand.erho(op_cond) / self.area_stab_region, ]) res_tot = reciprocal_summation(resistances) return res_tot * self.area @@ -191,28 +191,30 @@ def Cp(self, op_cond: OperationalConditions): # noqa: N802 Specific heat capacity [J/K/m] """ weighted_specific_heat = np.array([ - self.sc_strand.Cp(op_cond) * self.area_sc * self.sc_strand.rho(op_cond), + self.sc_strand.Cp(op_cond) + * self.area_sc_region + * self.sc_strand.rho(op_cond), self.stab_strand.Cp(op_cond) - * self.area_stab + * self.area_stab_region * self.stab_strand.rho(op_cond), ]) return summation(weighted_specific_heat) / ( - self.area_sc * self.sc_strand.rho(op_cond) - + self.area_stab * self.stab_strand.rho(op_cond) + self.area_sc_region * self.sc_strand.rho(op_cond) + + self.area_stab_region * self.stab_strand.rho(op_cond) ) @property - def area_stab(self) -> float: + def area_stab_region(self) -> float: """Area of the stabiliser region""" return self.stab_strand.area * self.n_stab_strand @property - def area_sc(self) -> float: + def area_sc_region(self) -> float: """Area of the superconductor region""" return self.sc_strand.area * self.n_sc_strand @property - def area_cc(self) -> float: + def area_cooling_channel(self) -> float: """Area of the cooling channel""" return self.d_cooling_channel**2 / 4 * np.pi @@ -220,8 +222,8 @@ def area_cc(self) -> float: def area(self) -> float: """Area of the cable considering the void fraction""" return ( - self.area_sc + self.area_stab - ) / self.void_fraction / self.cos_theta + self.area_cc + self.area_sc_region + self.area_stab_region + ) / self.void_fraction / self.cos_theta + self.area_cooling_channel def _heat_balance_model_cable( self, @@ -270,8 +272,8 @@ def _temperature_evolution( t0: float, tf: float, initial_temperature: float, - B_fun: Callable, - I_fun: Callable, # noqa: N803 + B_fun: Callable[[float], float], + I_fun: Callable[[float], float], # noqa: N803 ): solution = solve_ivp( self._heat_balance_model_cable, @@ -348,7 +350,6 @@ def final_temperature_difference( # diff = abs(final_temperature - target_temperature) return abs(final_temperature - target_temperature) - # OD homogenised structural properties @abstractmethod def Kx(self, op_cond: OperationalConditions): # noqa: N802 """Total equivalent stiffness along x-axis""" @@ -556,13 +557,11 @@ def dy(self) -> float: """Cable dimension in the y direction [m]""" return self.area / self.dx - # Decide if this function shall be a setter. - # Defined as "normal" function to underline that it modifies dx. - def set_aspect_ratio(self, value: float): + @ABCCable.aspect_ratio.setter + def aspect_ratio(self, value: float): """Modify dx in order to get the given aspect ratio""" self.dx = np.sqrt(value * self.area) - # OD homogenised structural properties def Kx(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ Compute the total equivalent stiffness along the x-axis. @@ -578,7 +577,7 @@ def Kx(self, op_cond: OperationalConditions) -> float: # noqa: N802 : Homogenised stiffness in the x-direction [Pa]. """ - return self.E(op_cond) * self.dy / self.dx + return self.E(op_cond) / self.aspect_ratio def Ky(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ @@ -595,7 +594,7 @@ def Ky(self, op_cond: OperationalConditions) -> float: # noqa: N802 : Homogenised stiffness in the y-direction [Pa]. """ - return self.E(op_cond) * self.dx / self.dy + return self.E(op_cond) * self.aspect_ratio def to_dict(self, op_cond) -> dict[str, Any]: """ @@ -614,7 +613,7 @@ def to_dict(self, op_cond) -> dict[str, Any]: return data -class SquareCable(ABCCable): +class SquareCable(RectangularCable): """ Cable with a square cross-section. @@ -686,42 +685,16 @@ def dy(self) -> float: """Cable dimension in the y direction [m]""" return self.dx - # OD homogenised structural properties - def Kx(self, op_cond: OperationalConditions) -> float: # noqa: N802 - """ - Compute the total equivalent stiffness along the x-axis. - - Parameters - ---------- - op_cond: - Operational conditions including temperature, magnetic field, and strain - at which to calculate the material property. - - Returns - ------- - : - Homogenised stiffness in the x-direction [Pa]. + @property + def aspect_ratio(self): """ - # TODO possible reason for floating point difference - return self.E(op_cond) - - def Ky(self, op_cond: OperationalConditions) -> float: # noqa: N802 + Compute the aspect ratio of the cable cross-section. """ - Compute the total equivalent stiffness along the y-axis. + return 1 - Parameters - ---------- - op_cond: - Operational conditions including temperature, magnetic field, and strain - at which to calculate the material property. - - Returns - ------- - : - Homogenised stiffness in the y-direction [Pa]. - """ - # TODO possible reason for floating point difference - return self.E(op_cond) + @aspect_ratio.setter + def aspect_ratio(self, _: Any): + raise AttributeError(f"Aspect Ratio cannot be set on {type(self)}") class RoundCable(ABCCable): @@ -788,9 +761,8 @@ def dy(self) -> float: """Cable dimension in the y direction [m] (i.e. cable's diameter)""" return self.dx - # OD homogenised structural properties - # A structural analysis should be performed to check how much the rectangular - # approximation is fine also for the round cable. + # TODO: A structural analysis should be performed to check how much the rectangular + # approximation is fine also for the round cable. def Kx(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ Compute the equivalent stiffness of the cable along the x-axis. diff --git a/bluemira/magnets/case_tf.py b/bluemira/magnets/case_tf.py index b9d797a911..1f0610206d 100644 --- a/bluemira/magnets/case_tf.py +++ b/bluemira/magnets/case_tf.py @@ -239,12 +239,12 @@ class CaseTF(ABC): def __init__( self, - Ri: float, + Ri: float, # noqa: N803 theta_TF: float, dy_ps: float, dy_vault: float, mat_case: Material, - WPs: list[WindingPack], # noqa: N803 + wps: list[WindingPack], geometry: GeometryParameterisation, name: str = "BaseCaseTF", ): @@ -263,7 +263,7 @@ def __init__( Radial thickness of the vault support region [m]. mat_case: Structural material assigned to the TF coil case. - WPs: + wps: List of winding pack objects embedded inside the TF case. name: String identifier for the TF coil case instance (default is "BaseCaseTF"). @@ -274,42 +274,11 @@ def __init__( self.dy_ps = dy_ps self.dy_vault = dy_vault self.mat_case = mat_case - self.WPs = WPs + self.wps = wps self.name = name # sets Rk self.update_dy_vault(self.dy_vault) - @property - def name(self) -> str: - """ - Name identifier of the TF case. - - Returns - ------- - : - Human-readable label for the coil case instance. - """ - return self._name - - @name.setter - def name(self, value: str): - """ - Set the name of the TF case. - - Parameters - ---------- - value: - Case name. - - Raises - ------ - TypeError - If value is not a string. - """ - if not isinstance(value, str): - raise TypeError("name must be a string.") - self._name = value - @property def rad_theta(self) -> float: """ @@ -381,7 +350,7 @@ def mat_case(self, value: Material): self._mat_case = value @property - def WPs(self) -> list[WindingPack]: # noqa: N802 + def wps(self) -> list[WindingPack]: """ List of winding pack (WP) objects embedded inside the TF case. @@ -390,10 +359,10 @@ def WPs(self) -> list[WindingPack]: # noqa: N802 : Winding pack instances composing the internal coil layout. """ - return self._WPs + return self._wps - @WPs.setter - def WPs(self, value: list[WindingPack]): # noqa: N802 + @wps.setter + def wps(self, value: list[WindingPack]): """ Set the winding pack objects list. @@ -408,10 +377,10 @@ def WPs(self, value: list[WindingPack]): # noqa: N802 If value is not a list of WindingPack instances. """ if not isinstance(value, list): - raise TypeError("WPs must be a list of WindingPack objects.") + raise TypeError("wps must be a list of WindingPack objects.") if not all(isinstance(wp, WindingPack) for wp in value): - raise TypeError("All elements of WPs must be WindingPack instances.") - self._WPs = value + raise TypeError("All elements of wps must be WindingPack instances.") + self._wps = value # fix dy_vault (this will recalculate Rk) self.update_dy_vault(self.dy_vault) @@ -432,7 +401,7 @@ def dx_ps(self): @property def n_conductors(self) -> int: """Total number of conductors in the winding pack.""" - return sum(w.n_conductors for w in self.WPs) + return sum(w.n_conductors for w in self.wps) @property def dy_wp_i(self) -> np.ndarray: @@ -443,9 +412,9 @@ def dy_wp_i(self) -> np.ndarray: ------- : Array containing the radial thickness [m] of each Winding Pack. - Each element corresponds to one WP in the self.WPs list. + Each element corresponds to one WP in the self.wps list. """ - return np.array([wp.dy for wp in self.WPs]) + return np.array([wp.dy for wp in self.wps]) @property def dy_wp_tot(self) -> float: @@ -476,8 +445,8 @@ def R_wp_i(self) -> np.ndarray: # noqa: N802 else: result = result_initial - dy_wp_cumsum[:-1] - if len(result) != len(self.WPs): - bluemira_error(f"Mismatch: {len(result)} R_wp_i vs {len(self.WPs)} WPs!") + if len(result) != len(self.wps): + bluemira_error(f"Mismatch: {len(result)} R_wp_i vs {len(self.wps)} wps!") return result @@ -514,7 +483,7 @@ def plot( Default is `False`. homogenised: If `True`, plots winding packs as homogenised blocks. - If `False`, plots individual conductors inside WPs. + If `False`, plots individual conductors inside wps. Default is `False`. Returns @@ -529,7 +498,7 @@ def plot( self.geometry.plot(ax=ax) # Plot winding packs - for i, wp in enumerate(self.WPs): + for i, wp in enumerate(self.wps): xc_wp = 0.0 yc_wp = self.R_wp_i[i] - wp.dy / 2 ax = wp.plot(xc=xc_wp, yc=yc_wp, ax=ax, homogenised=homogenised) @@ -565,7 +534,7 @@ def area_wps(self) -> float: : Combined area of the winding packs [m²]. """ - return np.sum([w.area for w in self.WPs]) + return np.sum([w.area for w in self.wps]) @property def area_wps_jacket(self) -> float: @@ -575,9 +544,9 @@ def area_wps_jacket(self) -> float: Returns ------- : - Combined area of conductor jackets in all WPs [m²]. + Combined area of conductor jackets in all wps [m²]. """ - return np.sum([w.jacket_area for w in self.WPs]) + return np.sum([w.jacket_area for w in self.wps]) @property def area_jacket_total(self) -> float: @@ -586,7 +555,7 @@ def area_jacket_total(self) -> float: - The case jacket area (structural material surrounding the winding packs). - The conductor jackets area (jackets enclosing the individual conductors - inside the WPs). + inside the wps). Returns ------- @@ -617,7 +586,7 @@ def rearrange_conductors_in_wp( n_conductors: Total number of conductors to distribute. wp_reduction_factor: - Fractional reduction of available toroidal space for WPs. + Fractional reduction of available toroidal space for wps. min_gap_x: Minimum gap between the WP and the case boundary in toroidal direction [m]. n_layers_reduction: @@ -713,7 +682,7 @@ def to_dict(self) -> dict[str, float | str | list[dict[str, float | str | Any]]] "dy_vault": self.dy_vault, "theta_TF": self.geometry.variables.theta_TF.value, "mat_case": self.mat_case, - "WPs": [wp.to_dict() for wp in self.WPs], + "wps": [wp.to_dict() for wp in self.wps], } def __str__(self) -> str: @@ -733,7 +702,7 @@ def __str__(self) -> str: f" - dy_vault: {self.dy_vault:.3f} m\n" f" - theta_TF: {self.geometry.variables.theta_TF.value:.2f}°\n" f" - Material: {self.mat_case.name}\n" - f" - Winding Packs: {len(self.WPs)} packs\n" + f" - Winding Packs: {len(self.wps)} packs\n" ) @@ -745,15 +714,15 @@ class TrapezoidalCaseTF(CaseTF): def __init__( self, - Ri: float, + Ri: float, # noqa: N803 theta_TF: float, dy_ps: float, dy_vault: float, mat_case: Material, - WPs: list[WindingPack], # noqa: N803 + wps: list[WindingPack], name: str = "TrapezoidalCaseTF", ): - self._check_WPs(WPs) + self._check_wps(wps) geom = TrapezoidalGeometry() super().__init__( @@ -762,22 +731,22 @@ def __init__( dy_ps=dy_ps, dy_vault=dy_vault, mat_case=mat_case, - WPs=WPs, + wps=wps, name=name, geometry=geom, ) - def _check_WPs( # noqa: PLR6301, N802 + def _check_wps( # noqa: PLR6301 self, - WPs: list[WindingPack], # noqa:N803 + wps: list[WindingPack], ): """ - Validate that the provided winding packs (WPs) are non-empty and share the + Validate that the provided winding packs (wps) are non-empty and share the same conductor. Parameters ---------- - WPs: + wps: List of winding pack objects to validate. Raises @@ -787,15 +756,15 @@ def _check_WPs( # noqa: PLR6301, N802 ValueError If winding packs have different conductor instances. """ - if not WPs: + if not wps: raise ValueError("At least one non-empty winding pack must be provided.") - first_conductor = WPs[0].conductor - for i, wp in enumerate(WPs[1:], start=1): + first_conductor = wps[0].conductor + for i, wp in enumerate(wps[1:], start=1): if wp.conductor is not first_conductor: bluemira_warn( f"[Winding pack at index {i} uses a different conductor object " - f"than the first one. This module requires all WPs to " + f"than the first one. This module requires all wps to " f"share the same conductor instance." f"Please verify the inputs or unify the conductor assignment." ) @@ -851,9 +820,9 @@ def Kx_lat(self, op_cond: OperationalConditions): # noqa: N802 """ dx_lat = np.array([ (self.R_wp_i[i] + self.R_wp_k[i]) / 2 * np.tan(self.rad_theta / 2) - w.dx / 2 - for i, w in enumerate(self.WPs) + for i, w in enumerate(self.wps) ]) - dy_lat = np.array([w.dy for w in self.WPs]) + dy_lat = np.array([w.dy for w in self.wps]) return self.mat_case.youngs_modulus(op_cond) * dy_lat / dx_lat def Kx_vault(self, op_cond: OperationalConditions) -> float: # noqa: N802 @@ -901,7 +870,7 @@ def Kx(self, op_cond: OperationalConditions) -> float: # noqa: N802 w.Kx(op_cond), kx_lat[i], ]) - for i, w in enumerate(self.WPs) + for i, w in enumerate(self.wps) ] return summation([self.Kx_ps(op_cond), self.Kx_vault(op_cond), *temp]) @@ -943,9 +912,9 @@ def Ky_lat(self, op_cond: OperationalConditions): # noqa: N802 dx_lat = np.array([ (self.R_wp_i[i] + self.R_wp_k[i]) / 2 * np.tan(self._rad_theta / 2) - w.dx / 2 - for i, w in enumerate(self.WPs) + for i, w in enumerate(self.wps) ]) - dy_lat = np.array([w.dy for w in self.WPs]) + dy_lat = np.array([w.dy for w in self.wps]) return self.mat_case.youngs_modulus(op_cond) * dx_lat / dy_lat def Ky_vault(self, op_cond: OperationalConditions): # noqa: N802 @@ -993,7 +962,7 @@ def Ky(self, op_cond: OperationalConditions) -> float: # noqa: N802 w.Ky(op_cond), ky_lat[i], ]) - for i, w in enumerate(self.WPs) + for i, w in enumerate(self.wps) ] return reciprocal_summation([self.Ky_ps(op_cond), self.Ky_vault(op_cond), *temp]) @@ -1006,7 +975,7 @@ def rearrange_conductors_in_wp( layout: str = "auto", ): """ - Rearrange the total number of conductors into winding packs (WPs) + Rearrange the total number of conductors into winding packs within the TF coil case geometry using enforce_wp_layout_rules. Parameters @@ -1014,7 +983,7 @@ def rearrange_conductors_in_wp( n_conductors: Total number of conductors to be allocated. wp_reduction_factor: - Fractional reduction of the total available toroidal space for WPs. + Fractional reduction of the total available toroidal space for winding packs. min_gap_x: Minimum allowable toroidal gap between WP and boundary [m]. n_layers_reduction: @@ -1028,7 +997,7 @@ def rearrange_conductors_in_wp( If there is not enough space to allocate all the conductors. """ debug_msg = ["Method rearrange_conductors_in_wp"] - conductor = self.WPs[0].conductor + conductor = self.wps[0].conductor R_wp_i = self.R_wp_i[0] # noqa: N806 dx_WP = self.dx_i * wp_reduction_factor # noqa: N806 @@ -1042,10 +1011,10 @@ def rearrange_conductors_in_wp( f"n_conductors = {n_conductors}", ]) - WPs = [] # noqa: N806 + wps = [] # number of conductors to be allocated remaining_conductors = n_conductors - # maximum number of winding packs in WPs + # maximum number of winding packs in wps i_max = 50 n_layers_max = 0 for i in range(i_max): @@ -1064,7 +1033,7 @@ def rearrange_conductors_in_wp( ) if n_layers_max >= remaining_conductors: - WPs.append( + wps.append( WindingPack(conductor=conductor, nx=remaining_conductors, ny=1) ) remaining_conductors = 0 @@ -1095,16 +1064,16 @@ def rearrange_conductors_in_wp( if n_layers_max * n_turns_max > remaining_conductors: n_turns_max -= 1 - WPs.append( + wps.append( WindingPack(conductor=conductor, nx=n_layers_max, ny=n_turns_max) ) remaining_conductors -= n_layers_max * n_turns_max - WPs.append( + wps.append( WindingPack(conductor=conductor, nx=remaining_conductors, ny=1) ) remaining_conductors = 0 else: - WPs.append( + wps.append( WindingPack(conductor=conductor, nx=n_layers_max, ny=n_turns_max) ) remaining_conductors -= n_layers_max * n_turns_max @@ -1123,7 +1092,7 @@ def rearrange_conductors_in_wp( break bluemira_debug("\n".join(debug_msg)) - self.WPs = WPs + self.wps = wps # just a final check if self.n_conductors != n_conductors: diff --git a/bluemira/magnets/tfcoil_designer.py b/bluemira/magnets/tfcoil_designer.py index f796d11fca..79a358f2d5 100644 --- a/bluemira/magnets/tfcoil_designer.py +++ b/bluemira/magnets/tfcoil_designer.py @@ -5,9 +5,11 @@ # SPDX-License-Identifier: LGPL-2.1-or-later """Designer for TF Coil XY cross section.""" -from collections.abc import Callable, Iterable +from __future__ import annotations + +from collections.abc import Callable, Sequence from dataclasses import dataclass -from typing import Any +from typing import TYPE_CHECKING, Any import matplotlib.pyplot as plt import numpy as np @@ -25,13 +27,17 @@ bluemira_warn, ) from bluemira.base.parameter_frame import Parameter, ParameterFrame -from bluemira.base.parameter_frame.typed import ParameterFrameLike -from bluemira.magnets.cable import RectangularCable -from bluemira.magnets.case_tf import CaseTF -from bluemira.magnets.conductor import SymmetricConductor +from bluemira.magnets.cable import ABCCable, RectangularCable +from bluemira.magnets.conductor import Conductor, SymmetricConductor from bluemira.magnets.utils import delayed_exp_func from bluemira.utilities.tools import get_class_from_module +if TYPE_CHECKING: + from bluemira.base.parameter_frame.typed import ParameterFrameLike + from bluemira.magnets.case_tf import CaseTF + from bluemira.magnets.strand import Strand, SuperconductingStrand + from bluemira.magnets.winding_pack import WindingPack + @dataclass class TFCoilXYDesignerParams(ParameterFrame): @@ -156,12 +162,12 @@ class OptimisationConfig: layout: str """Cable layout strategy""" wp_reduction_factor: float - """Fractional reduction of available toroidal space for WPs""" + """Fractional reduction of available toroidal space for Winding Packs""" n_layers_reduction: int """Number of layers to remove after each WP""" - bounds_cond_jacket: npt.NDArray + bounds_cond_jacket: tuple[float, float] """Min/max bounds for conductor jacket area optimisation [m²]""" - bounds_dy_vault: npt.NDArray + bounds_dy_vault: tuple[float, float] """Min/max bounds for the case vault thickness optimisation [m]""" max_niter: int """Maximum number of optimisation iterations""" @@ -173,7 +179,7 @@ class OptimisationConfig: class StabilisingStrandRes: """Cable opt results""" - cable: Any + cable: ABCCable solution: Any info_text: str @@ -330,7 +336,9 @@ def __init__( ): super().__init__(params=params, build_config=build_config) - def _derived_values(self, op_config: OptimisationConfig): + def _derived_values( + self, op_config: OptimisationConfig + ) -> DerivedTFCoilXYDesignerParams: # Needed params that are calculated using the base params R0 = self.params.R0.value # noqa: N806 n_TF = self.params.n_TF.value @@ -364,17 +372,15 @@ def _derived_values(self, op_config: OptimisationConfig): ), ) - def B_TF_r(self, tf_current, r): + def B_TF_r(self, tf_current: float, r: float): """ Compute the magnetic field generated by the TF coils, including ripple correction. Parameters ---------- - tf_current : float + tf_current: Toroidal field coil current [A]. - n_TF : int - Number of toroidal field coils. r : float Radial position from the tokamak center [m]. @@ -385,7 +391,7 @@ def B_TF_r(self, tf_current, r): """ return 1.08 * (MU_0_2PI * self.params.n_TF.value * tf_current / r) - def run(self): + def run(self) -> TFCoilXY: """ Run the TF coil XY design problem. @@ -445,7 +451,7 @@ def run(self): @staticmethod def optimise_cable_n_stab_ths( - cable, + cable: ABCCable, t0: float, tf: float, initial_temperature: float, @@ -453,7 +459,7 @@ def optimise_cable_n_stab_ths( B_fun: Callable[[float], float], I_fun: Callable[[float], float], # noqa: N803 bounds: np.ndarray | None = None, - ): + ) -> StabilisingStrandRes: """ Optimise the number of stabiliser strand in the superconducting cable using a 0-D hot spot criteria. @@ -490,6 +496,8 @@ def optimise_cable_n_stab_ths( - The number of stabiliser strands in the cable is modified directly. - Cooling material contribution is neglected when applying the hot spot criteria. """ + # final_temperature_difference modifies n_stab_strand + # which is used in later calculations of area_stab_region result = minimize_scalar( fun=cable.final_temperature_difference, args=(t0, tf, initial_temperature, target_temperature, B_fun, I_fun), @@ -547,8 +555,8 @@ def optimise_jacket_and_vault( fz: float, op_cond: OperationalConditions, allowable_sigma: float, - bounds_cond_jacket: np.ndarray | None = None, - bounds_dy_vault: np.ndarray | None = None, + bounds_cond_jacket: tuple[float, float] | None = None, + bounds_dy_vault: tuple[float, float] | None = None, layout: str = "auto", wp_reduction_factor: float = 0.8, min_gap_x: float = 0.05, @@ -619,9 +627,9 @@ def optimise_jacket_and_vault( if n_conds is None: n_conds = case.n_conductors - conductor = case.WPs[0].conductor + conductor = case.wps[0].conductor - case._check_WPs(case.WPs) + case._check_wps(case.wps) err_conductor_area_jacket = 10000 * eps err_dy_vault = 10000 * eps @@ -745,12 +753,12 @@ def optimise_jacket_and_vault( @staticmethod def optimise_jacket_conductor( - conductor, + conductor: Conductor, pressure: float, f_z: float, op_cond: OperationalConditions, allowable_sigma: float, - bounds: np.ndarray | None = None, + bounds: tuple[float, float] | None = None, direction: str = "x", ): """ @@ -805,6 +813,8 @@ def optimise_jacket_conductor( if method == "bounded": debug_msg.append(f"bounds: {bounds}") + # sigma difference modifies dx_jacket or dy_jacket which intern is used in + # tresca_sigma_jacket calculation result = minimize_scalar( fun=conductor.sigma_difference, args=(pressure, f_z, op_cond, allowable_sigma), @@ -833,12 +843,12 @@ def optimise_jacket_conductor( @staticmethod def optimise_vault_radial_thickness( - case, + case: CaseTF, pm: float, fz: float, op_cond: OperationalConditions, allowable_sigma: float, - bounds: np.array = None, + bounds: tuple[float, float] | None = None, ): """ Optimise the vault radial thickness of the case @@ -873,6 +883,7 @@ def optimise_vault_radial_thickness( if bounds is not None: method = "bounded" + # sigma difference modifies dy_vault which is then used in tresca_stress result = minimize_scalar( fun=case._sigma_difference, args=(pm, fz, op_cond, allowable_sigma), @@ -886,7 +897,9 @@ def optimise_vault_radial_thickness( return result - def _make_strand(self, wp_i, config, params): + def _make_strand( + self, wp_i: int, config: dict[str, str | float], params: StrandParams + ) -> Strand: cls_name = config["class"] strand_cls = get_class_from_module( cls_name, default_module="bluemira.magnets.strand" @@ -913,10 +926,17 @@ def _make_strand(self, wp_i, config, params): ) @staticmethod - def _check_iterable(wp_i, param_val): - return param_val[wp_i] if isinstance(param_val, Iterable) else param_val + def _check_iterable(wp_i: int, param_val: float | Sequence[float]) -> float: + return param_val[wp_i] if isinstance(param_val, Sequence) else param_val - def _make_cable_cls(self, stab_strand, sc_strand, wp_i, config, params): + def _make_cable_cls( + self, + stab_strand: Strand, + sc_strand: SuperconductingStrand, + wp_i: int, + config: dict[str, float | str], + params: CableParams, + ) -> ABCCable: cls_name = config["class"] cable_cls = get_class_from_module( cls_name, default_module="bluemira.magnets.cable" @@ -938,7 +958,7 @@ def _make_cable_cls(self, stab_strand, sc_strand, wp_i, config, params): **extras, ) - def _make_cable(self, wp_i): + def _make_cable(self, wp_i: int) -> ABCCable: stab_strand_config = self.build_config.get("stabilising_strand") sc_strand_config = self.build_config.get("superconducting_strand") cable_config = self.build_config.get("cable") @@ -959,10 +979,10 @@ def _make_cable(self, wp_i): stab_strand, sc_strand, wp_i, cable_config, cable_params ) - def _make_conductor(self, cable, wp_i=0): - # current functionality requires conductors are the same for both WPs - # in future allow for different conductor objects so can vary cable and strands - # between the sets of the winding pack? + def _make_conductor(self, cable: ABCCable, wp_i: int = 0) -> Conductor: + # TODO: current functionality requires conductors are the same for both wps + # in future allow for different conductor objects so can vary cable and + # strands between the sets of the winding pack? config = self.build_config.get("conductor") cls_name = config["class"] @@ -991,7 +1011,9 @@ def _make_conductor(self, cable, wp_i=0): ), ) - def _make_winding_pack(self, conductor, wp_i, config): + def _make_winding_pack( + self, conductor: Conductor, wp_i: int, config: dict[str, float | str] + ) -> WindingPack: cls_name = config["class"] winding_pack_cls = get_class_from_module( cls_name, default_module="bluemira.magnets.winding_pack" @@ -1003,7 +1025,12 @@ def _make_winding_pack(self, conductor, wp_i, config): name="winding_pack", ) - def _make_case(self, WPs, derived_params, optimisation_params): # noqa: N803 + def _make_case( + self, + wps: list[WindingPack], + derived_params: DerivedTFCoilXYDesignerParams, + optimisation_params: OptimisationConfig, + ) -> CaseTF: config = self.build_config.get("case") params = CaseParams.from_dict(config.get("params")) @@ -1018,7 +1045,7 @@ def _make_case(self, WPs, derived_params, optimisation_params): # noqa: N803 dy_ps=params.dy_ps.value, dy_vault=params.dy_vault.value, mat_case=config["material"], - WPs=WPs, + wps=wps, name=config.get("name", cls_name.rsplit("::", 1)[-1]), ) diff --git a/bluemira/magnets/utils.py b/bluemira/magnets/utils.py index 99c444b80f..b382d4c48b 100644 --- a/bluemira/magnets/utils.py +++ b/bluemira/magnets/utils.py @@ -52,7 +52,7 @@ def reciprocal_summation(arr: Sequence[float]) -> float: ----- Y = [sum(1/x1 + 1/x2 + 1/x3 ...)]^-1 """ - return (np.sum((1 / element) for element in arr)) ** -1 + return (np.sum(1 / np.array(arr))) ** -1 def delayed_exp_func( diff --git a/examples/magnets/example_tf_creation.py b/examples/magnets/example_tf_creation.py index cd6de9cb57..f50b1fc5b9 100644 --- a/examples/magnets/example_tf_creation.py +++ b/examples/magnets/example_tf_creation.py @@ -10,7 +10,6 @@ conductor, winding pack and casing. """ -import numpy as np from eurofusion_materials.library.magnet_branch_mats import ( COPPER_100, COPPER_300, @@ -88,8 +87,8 @@ "layout": "auto", "wp_reduction_factor": 0.75, "n_layers_reduction": 4, - "bounds_cond_jacket": np.array([1e-5, 0.2]), - "bounds_dy_vault": np.array([0.1, 2]), + "bounds_cond_jacket": (1e-5, 0.2), + "bounds_dy_vault": (0.1, 2), "max_niter": 100, "eps": 1e-6, }, diff --git a/examples/magnets/example_tf_wp_from_dict.py b/examples/magnets/example_tf_wp_from_dict.py index 283323f93e..c973d2df58 100644 --- a/examples/magnets/example_tf_wp_from_dict.py +++ b/examples/magnets/example_tf_wp_from_dict.py @@ -24,7 +24,7 @@ "dy_vault": 0.4529579163961617, "theta_TF": 22.5, "mat_case": SS316_LN_MAG, - "WPs": [ + "wps": [ { "name_in_registry": "WindingPack", "name": "WindingPack", @@ -156,10 +156,10 @@ err = 1e-6 # optimize number of stabilizer strands -sc_strand = case_tf.WPs[0].conductor.cable.sc_strand +sc_strand = case_tf.wps[0].conductor.cable.sc_strand op_cond = OperationalConditions(temperature=T_op, magnetic_field=B_TF_i, strain=0.0055) Ic_sc = sc_strand.Ic(op_cond) -case_tf.WPs[0].conductor.cable.n_sc_strand = int(np.ceil(Iop / Ic_sc)) +case_tf.wps[0].conductor.cable.n_sc_strand = int(np.ceil(Iop / Ic_sc)) from bluemira.magnets.utils import delayed_exp_func @@ -177,7 +177,7 @@ import time t = time.time() -case_tf.WPs[0].conductor.cable.optimize_n_stab_ths( +case_tf.wps[0].conductor.cable.optimize_n_stab_ths( t0, tf, T_for_hts, @@ -230,7 +230,7 @@ [-scalex[0] * case_tf.dx_i, -case_tf.dx_i / 2], [case_tf.Ri, case_tf.Ri], "k:" ) - for i in range(len(case_tf.WPs)): + for i in range(len(case_tf.wps)): ax.plot( [-scalex[0] * case_tf.dx_i, -case_tf.dx_i / 2], [case_tf.R_wp_i[i], case_tf.R_wp_i[i]], @@ -255,8 +255,8 @@ bluemira_print("Convergence should be: 9.020308301268381e-07 after 11 iterations") op_cond = OperationalConditions(temperature=T_op, magnetic_field=B_TF_i, strain=0.0055) -I_sc = case_tf.WPs[0].conductor.cable.sc_strand.Ic(op_cond) -I_max = I_sc * case_tf.WPs[0].conductor.cable.n_sc_strand +I_sc = case_tf.wps[0].conductor.cable.sc_strand.Ic(op_cond) +I_max = I_sc * case_tf.wps[0].conductor.cable.n_sc_strand I_TF_max = I_max * case_tf.n_conductors print(I_max) print(I_TF_max) diff --git a/examples/magnets/example_tf_wp_optimization.py b/examples/magnets/example_tf_wp_optimization.py index 7fe2b0496b..831e8ddd80 100644 --- a/examples/magnets/example_tf_wp_optimization.py +++ b/examples/magnets/example_tf_wp_optimization.py @@ -303,9 +303,8 @@ def B_TF_r(I_TF, n_TF, r): # ***Change cable aspect ratio*** # %% aspect_ratio = 1.2 -cable.set_aspect_ratio( - aspect_ratio -) # This adjusts the cable dimensions while maintaining the total cross-sectional area. +# This adjusts the cable dimensions while maintaining the total cross-sectional area. +cable.aspect_ratio = aspect_ratio cable.plot(0, 0, show=True) bluemira_print(f"cable area: {cable.area}") @@ -435,7 +434,7 @@ def B_TF_r(I_TF, n_TF, r): dy_vault=0.7, theta_TF=360 / n_TF, mat_case=SS316_LN_MAG, - WPs=[wp1], + wps=[wp1], ) print(f"pre-wp reduction factor: {wp_reduction_factor}") @@ -497,7 +496,7 @@ def B_TF_r(I_TF, n_TF, r): ax.plot([-scalex[0] * case.dx_i, -case.dx_i / 2], [case.Ri, case.Ri], "k:") - for i in range(len(case.WPs)): + for i in range(len(case.wps)): ax.plot( [-scalex[0] * case.dx_i, -case.dx_i / 2], [case.R_wp_i[i], case.R_wp_i[i]], diff --git a/tests/magnets/test_cable.py b/tests/magnets/test_cable.py index 471bc758fe..fae6344923 100644 --- a/tests/magnets/test_cable.py +++ b/tests/magnets/test_cable.py @@ -61,9 +61,9 @@ def test_geometry_and_area(cable): assert cable.dy > 0 assert cable.area > 0 assert cable.aspect_ratio > 0 - assert cable.area_cc > 0 - assert cable.area_stab > 0 - assert cable.area_sc > 0 + assert cable.area_cooling_channel > 0 + assert cable.area_stab_region > 0 + assert cable.area_sc_region > 0 def test_material_properties(cable): From 8c8e13175be9c40a36fc392c034b50b86296bc4f Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Tue, 16 Sep 2025 10:15:12 +0100 Subject: [PATCH 60/61] =?UTF-8?q?=F0=9F=90=9B=20Missing=20underscore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bluemira/magnets/cable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bluemira/magnets/cable.py b/bluemira/magnets/cable.py index 72236c23cb..78e2d5c1cd 100644 --- a/bluemira/magnets/cable.py +++ b/bluemira/magnets/cable.py @@ -560,7 +560,7 @@ def dy(self) -> float: @ABCCable.aspect_ratio.setter def aspect_ratio(self, value: float): """Modify dx in order to get the given aspect ratio""" - self.dx = np.sqrt(value * self.area) + self._dx = np.sqrt(value * self.area) def Kx(self, op_cond: OperationalConditions) -> float: # noqa: N802 """ From f8dd07806080168f57ae458df3b4fe7005980434 Mon Sep 17 00:00:00 2001 From: james <81617086+je-cook@users.noreply.github.com> Date: Wed, 17 Sep 2025 09:23:47 +0100 Subject: [PATCH 61/61] =?UTF-8?q?=F0=9F=94=A5=20Remove=20old=20examples?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/magnets/example_tf_wp_from_dict.py | 264 --------- .../magnets/example_tf_wp_optimization.py | 525 ------------------ 2 files changed, 789 deletions(-) delete mode 100644 examples/magnets/example_tf_wp_from_dict.py delete mode 100644 examples/magnets/example_tf_wp_optimization.py diff --git a/examples/magnets/example_tf_wp_from_dict.py b/examples/magnets/example_tf_wp_from_dict.py deleted file mode 100644 index c973d2df58..0000000000 --- a/examples/magnets/example_tf_wp_from_dict.py +++ /dev/null @@ -1,264 +0,0 @@ -import matplotlib.pyplot as plt -import numpy as np -from eurofusion_materials.library.magnet_branch_mats import ( - COPPER_100, - COPPER_300, - DUMMY_INSULATOR_MAG, - NB3SN_MAG, - SS316_LN_MAG, -) -from matproplib import OperationalConditions - -from bluemira.base.look_and_feel import bluemira_print - -op_cond = OperationalConditions(temperature=5.7, magnetic_field=10.0, strain=0.0055) - -from bluemira.base.constants import MU_0, MU_0_2PI, MU_0_4PI -from bluemira.magnets.case_tf import create_case_tf_from_dict - -case_tf_dict = { - "name_in_registry": "TrapezoidalCaseTF", - "name": "TrapezoidalCaseTF", - "Ri": 3.708571428571428, - "dy_ps": 0.05733333333333333, - "dy_vault": 0.4529579163961617, - "theta_TF": 22.5, - "mat_case": SS316_LN_MAG, - "wps": [ - { - "name_in_registry": "WindingPack", - "name": "WindingPack", - "conductor": { - "name_in_registry": "SymmetricConductor", - "name": "SymmetricConductor", - "cable": { - "name_in_registry": "RectangularCable", - "name": "RectangularCableLTS", - "n_sc_strand": 321, - "n_stab_strand": 476, - "d_cooling_channel": 0.01, - "void_fraction": 0.7, - "cos_theta": 0.97, - "E": 0.1e9, - "sc_strand": { - "name_in_registry": "SuperconductingStrand", - "name": "Nb3Sn_strand", - "d_strand": 0.001, - "temperature": 5.7, - "materials": [ - {"material": NB3SN_MAG, "fraction": 0.5}, - {"material": COPPER_100, "fraction": 0.5}, - ], - }, - "stab_strand": { - "name_in_registry": "Strand", - "name": "Stabilizer", - "d_strand": 0.001, - "temperature": 5.7, - "materials": [{"material": COPPER_300, "fraction": 1.0}], - }, - "dx": 0.034648435154495685, - "aspect_ratio": 1.2, - }, - "mat_jacket": SS316_LN_MAG, - "mat_ins": DUMMY_INSULATOR_MAG, - "dx_jacket": 0.0030808556812487366, - "dx_ins": 0.001, - }, - "nx": 25, - "ny": 6, - }, - { - "name_in_registry": "WindingPack", - "name": "WindingPack", - "conductor": { - "name_in_registry": "SymmetricConductor", - "name": "SymmetricConductor", - "cable": { - "name_in_registry": "RectangularCable", - "name": "RectangularCableLTS", - "n_sc_strand": 321, - "n_stab_strand": 476, - "d_cooling_channel": 0.01, - "void_fraction": 0.7, - "cos_theta": 0.97, - "E": 0.1e9, - "sc_strand": { - "name_in_registry": "SuperconductingStrand", - "name": "Nb3Sn_strand", - "d_strand": 0.001, - "temperature": 5.7, - "materials": [ - {"material": NB3SN_MAG, "fraction": 0.5}, - {"material": COPPER_100, "fraction": 0.5}, - ], - }, - "stab_strand": { - "name_in_registry": "Strand", - "name": "Stabilizer", - "d_strand": 0.001, - "temperature": 5.7, - "materials": [{"material": COPPER_300, "fraction": 1.0}], - }, - "dx": 0.034648435154495685, - "aspect_ratio": 1.2, - }, - "mat_jacket": SS316_LN_MAG, - "mat_ins": DUMMY_INSULATOR_MAG, - "dx_jacket": 0.0030808556812487366, - "dx_ins": 0.001, - }, - "nx": 18, - "ny": 1, - }, - ], -} - -case_tf = create_case_tf_from_dict(case_tf_dict) - -case_tf.plot(show=True, homogenized=False) - -# Machine parameters (should match the original setup) -R0 = 8.6 -B0 = 4.39 -A = 2.8 -n_TF = 16 -ripple = 6e-3 -# operational current per conductor -Iop = 70.0e3 -# Safety factor to be considered on the allowable stress -safety_factor = 1.5 * 1.3 - -# Derived values -a = R0 / A -d = 1.82 -Ri = R0 - a - d -Re = (R0 + a) * (1 / ripple) ** (1 / n_TF) -B_TF_i = 1.08 * (MU_0_2PI * n_TF * (B0 * R0 / MU_0_2PI / n_TF) / Ri) -pm = B_TF_i**2 / (2 * MU_0) -t_z = 0.5 * np.log(Re / Ri) * MU_0_4PI * n_TF * (B0 * R0 / MU_0_2PI / n_TF) ** 2 -T_sc = 4.2 -T_margin = 1.5 -T_op = T_sc + T_margin -S_Y = 1e9 / safety_factor -n_cond = int(np.floor((B0 * R0 / MU_0_2PI / n_TF) / Iop)) - -# Layout and WP parameters -layout = "auto" -wp_reduction_factor = 0.75 -min_gap_x = 2 * (R0 * 2 / 3 * 1e-2) # 2 * dr_plasma_side -n_layers_reduction = 4 - -# Optimization parameters already defined earlier -bounds_cond_jacket = np.array([1e-5, 0.2]) -bounds_dy_vault = np.array([0.1, 2]) -max_niter = 100 -err = 1e-6 - -# optimize number of stabilizer strands -sc_strand = case_tf.wps[0].conductor.cable.sc_strand -op_cond = OperationalConditions(temperature=T_op, magnetic_field=B_TF_i, strain=0.0055) -Ic_sc = sc_strand.Ic(op_cond) -case_tf.wps[0].conductor.cable.n_sc_strand = int(np.ceil(Iop / Ic_sc)) - -from bluemira.magnets.utils import delayed_exp_func - -Tau_discharge = 20 # [s] -t_delay = 3 # [s] -t0 = 0 # [s] -hotspot_target_temperature = 250.0 # [K] - -tf = Tau_discharge -T_for_hts = T_op -I_fun = delayed_exp_func(Iop, Tau_discharge, t_delay) -B_fun = delayed_exp_func(B_TF_i, Tau_discharge, t_delay) - - -import time - -t = time.time() -case_tf.wps[0].conductor.cable.optimize_n_stab_ths( - t0, - tf, - T_for_hts, - hotspot_target_temperature, - B_fun, - I_fun, - bounds=[1, 10000], - show=False, -) - -print("Time taken for optimization:", time.time() - t) - -# Optimize case with structural constraints -case_tf.optimize_jacket_and_vault( - pm=pm, - fz=t_z, - op_cond=OperationalConditions( - temperature=T_op, magnetic_field=B_TF_i, strain=0.0055 - ), - allowable_sigma=S_Y, - bounds_cond_jacket=bounds_cond_jacket, - bounds_dy_vault=bounds_dy_vault, - layout=layout, - wp_reduction_factor=wp_reduction_factor, - min_gap_x=min_gap_x, - n_layers_reduction=n_layers_reduction, - max_niter=max_niter, - eps=err, - n_conds=n_cond, -) - -case_tf.plot_convergence() - -show = True -homogenized = True -if show: - scalex = np.array([2, 1]) - scaley = np.array([1, 1.2]) - - ax = case_tf.plot(homogenized=homogenized) - ax.set_aspect("equal") - - # Fix the x and y limits - ax.set_xlim(-scalex[0] * case_tf.dx_i, scalex[1] * case_tf.dx_i) - ax.set_ylim(scaley[0] * 0, scaley[1] * case_tf.Ri) - - deltax = [-case_tf.dx_i / 2, case_tf.dx_i / 2] - - ax.plot( - [-scalex[0] * case_tf.dx_i, -case_tf.dx_i / 2], [case_tf.Ri, case_tf.Ri], "k:" - ) - - for i in range(len(case_tf.wps)): - ax.plot( - [-scalex[0] * case_tf.dx_i, -case_tf.dx_i / 2], - [case_tf.R_wp_i[i], case_tf.R_wp_i[i]], - "k:", - ) - - ax.plot( - [-scalex[0] * case_tf.dx_i, -case_tf.dx_i / 2], - [case_tf.R_wp_k[-1], case_tf.R_wp_k[-1]], - "k:", - ) - ax.plot( - [-scalex[0] * case_tf.dx_i, -case_tf.dx_i / 2], [case_tf.Rk, case_tf.Rk], "k:" - ) - - ax.set_title("Equatorial cross section of the TF WP") - ax.set_xlabel("Toroidal direction [m]") - ax.set_ylabel("Radial direction [m]") - - plt.show() - -bluemira_print("Convergence should be: 9.020308301268381e-07 after 11 iterations") - -op_cond = OperationalConditions(temperature=T_op, magnetic_field=B_TF_i, strain=0.0055) -I_sc = case_tf.wps[0].conductor.cable.sc_strand.Ic(op_cond) -I_max = I_sc * case_tf.wps[0].conductor.cable.n_sc_strand -I_TF_max = I_max * case_tf.n_conductors -print(I_max) -print(I_TF_max) -I_TF = R0 * B0 / (MU_0_2PI * n_TF) -print(I_TF) diff --git a/examples/magnets/example_tf_wp_optimization.py b/examples/magnets/example_tf_wp_optimization.py deleted file mode 100644 index 831e8ddd80..0000000000 --- a/examples/magnets/example_tf_wp_optimization.py +++ /dev/null @@ -1,525 +0,0 @@ -# SPDX-FileCopyrightText: 2021-present M. Coleman, J. Cook, F. Franza -# SPDX-FileCopyrightText: 2021-present I.A. Maione, S. McIntosh -# SPDX-FileCopyrightText: 2021-present J. Morris, D. Short -# -# SPDX-License-Identifier: LGPL-2.1-or-later - -""" -Example script demonstrating the use of the Bluemira WP and TF coil modules -for conductor design, thermal and structural optimization, and case layout visualization. -""" - -# %% md -# # This is an example that shows the application of the WP module for the TF coils -# %% md -# ## Some import -# %% - -import matplotlib.pyplot as plt -import numpy as np -from eurofusion_materials.library.magnet_branch_mats import ( - COPPER_100, - COPPER_300, - DUMMY_INSULATOR_MAG, - NB3SN_MAG, - SS316_LN_MAG, -) -from matplotlib import cm - -# get some materials from EUROfusion materials library -from matproplib import OperationalConditions - -from bluemira.base.constants import MU_0, MU_0_2PI, MU_0_4PI -from bluemira.base.look_and_feel import bluemira_print -from bluemira.magnets.cable import RectangularCable -from bluemira.magnets.case_tf import TrapezoidalCaseTF -from bluemira.magnets.conductor import SymmetricConductor -from bluemira.magnets.init_magnets_registry import register_all_magnets -from bluemira.magnets.strand import create_strand_from_dict -from bluemira.magnets.utils import ( - delayed_exp_func, -) -from bluemira.magnets.winding_pack import WindingPack - -# %% -# cache all the magnets classes for future use -register_all_magnets() - - -# %% md -# ## Plot options -# %% -# Enable interactive mode -# %matplotlib notebook - -show = True -homogenized = False -# %% md -# -# ## Input (and derived) values -# *Note: these values shall be provided internally by bluemira code (as reactor -# settings or as information coming from the TF coils builder)

-# -# **Machine (generic)** -# %% -R0 = 8.6 # [m] major machine radius -B0 = 4.39 # [T] magnetic field @R0 -A = 2.8 # machine aspect ratio -n_TF = 16 # number of TF coils -ripple = 6e-3 # requirement on the maximum plasma ripple - -a = R0 / A # minor radius -# %% md -# **Inputs for the TF coils** -# %% -d = 1.82 # additional distance to calculate the max external radius of the inner TF leg -Iop = 70.0e3 # operational current in each conductor -dr_plasma_side = R0 * 2 / 3 * 1e-2 # thickness of the plate before the WP -T_sc = 4.2 # operational temperature of superconducting cable -T_margin = 1.5 # temperature margin -T_op = T_sc + T_margin # temperature considered for the superconducting cable -t_delay = 3 # [s] -t0 = 0 # [s] -hotspot_target_temperature = 250.0 # [K] - -Ri = R0 - a - d # [m] max external radius of the internal TF leg -Re = (R0 + a) * (1 / ripple) ** ( - 1 / n_TF -) # [m] max internal radius of the external TF leg -I_TF = B0 * R0 / MU_0_2PI / n_TF # total current in each TF coil - - -# magnetic field generated by the TF coils with a correction factor that takes into -# account the ripple -def B_TF_r(I_TF, n_TF, r): - """ - Compute the magnetic field generated by the TF coils, including ripple correction. - - Parameters - ---------- - I_TF : float - Toroidal field coil current [A]. - n_TF : int - Number of toroidal field coils. - r : float - Radial position from the tokamak center [m]. - - Returns - ------- - float - Magnetic field intensity [T]. - """ - return 1.08 * (MU_0_2PI * n_TF * I_TF / r) - - -# max magnetic field on the inner TF leg -B_TF_i = B_TF_r(I_TF, n_TF, Ri) -# magnetic pressure on the inner TF leg -pm = B_TF_i**2 / (2 * MU_0) - -# vertical tension acting on the equatorial section of inner TF leg -# i.e. half of the whole F_Z -t_z = 0.5 * np.log(Re / Ri) * MU_0_4PI * n_TF * I_TF**2 - -n_cond = np.floor(I_TF / Iop) # minimum number of conductors -bluemira_print(f"Total number of conductor: {n_cond}") -# %% md -# ***Additional data*** -# %% -R_VV = Ri * 1.05 # Vacuum vessel radius -S_VV = 100e6 # Vacuum vessel steel limit - -# allowable stress values -safety_factor = 1.5 * 1.3 -S_Y = 1e9 / safety_factor # [Pa] steel allowable limit - -# %% md -# ## Calculation of the maximum discharge time for the TF coils -# %% -# inductance (here approximated... better estimation in bluemira) -L = MU_0 * R0 * (n_TF * n_cond) ** 2 * (1 - np.sqrt(1 - (R0 - Ri) / R0)) / n_TF * 1.1 -# Magnetic energy -Wm = 1 / 2 * L * n_TF * Iop**2 * 1e-9 -# Maximum tension... (empirical formula from Lorenzo... find a generic equation) -V_MAX = (7 * R0 - 3) / 6 * 1.1e3 -# Discharge characteristic times -Tau_discharge1 = L * Iop / V_MAX -Tau_discharge2 = B0 * I_TF * n_TF * (R0 / A) ** 2 / (R_VV * S_VV) -# Discharge characteristic time to be considered in the following -Tau_discharge = max([Tau_discharge1, Tau_discharge2]) -tf = Tau_discharge -bluemira_print(f"Maximum TF discharge time: {tf}") -# %% md -# ## Current and magnetic field behaviour during discharge -# -# %% -I_fun = delayed_exp_func(Iop, Tau_discharge, t_delay) -B_fun = delayed_exp_func(B_TF_i, Tau_discharge, t_delay) - -# Create a time array from 0 to 3*Tau_discharge -t = np.linspace(0, 3 * Tau_discharge, 500) -I_data = np.array([I_fun(t_i) for t_i in t]) -B_data = np.array([B_fun(t_i) for t_i in t]) - -# Create a figure and axis -fig, ax1 = plt.subplots() - -# Plot I_fun -ax1.plot(t, I_data, "b-o", label="Current (I)", markevery=30) -ax1.set_xlabel("Time [s]") -ax1.set_ylabel("Current (I) [A]", color="b") -ax1.tick_params("y", colors="b") - -# Create a twin y-axis for the magnetic field B -ax2 = ax1.twinx() -ax2.plot(t, B_data, "r:s", label="Magnetic Field (B)", markevery=30) -ax2.set_ylabel("Magnetic Field (B) [T]", color="r") -ax2.tick_params("y", colors="r") - -# Add grid and title -ax1.grid(visible=True) -plt.title("Current (I) and Magnetic Field (B) vs Time") - -# Show the plot -plt.show() -# %% md -# ### Define materials (at the beginning the conductor is defined with a dummy number -# of stabilizer strands) -# %% -# create the strand -# Define strand configurations as dictionaries -sc_strand_dict = { - "name_in_registry": "SuperconductingStrand", - "name": "Nb3Sn_strand", - "d_strand": 1.0e-3, - "temperature": T_op, - "materials": [ - {"material": NB3SN_MAG, "fraction": 0.5}, - {"material": COPPER_100, "fraction": 0.5}, - ], -} - -stab_strand_dict = { - "name_in_registry": "Strand", - "name": "Stabilizer", - "d_strand": 1.0e-3, - "temperature": T_op, - "materials": [{"material": COPPER_300, "fraction": 1.0}], -} - -# Create strand objects using class-based factory methods -sc_strand = create_strand_from_dict(name="Nb3Sn_strand", strand_dict=sc_strand_dict) -stab_strand = create_strand_from_dict(name="Stabilizer", strand_dict=stab_strand_dict) - -# plot the critical current in a range of B between [10,16] -Bt_arr = np.linspace(10, 16, 100) -sc_strand.plot_Ic_B(Bt_arr, temperature=(T_sc + T_margin)) - -# %% md -# #### Plot number of conductor vs Iop -# %% -# Define the range of operating current (Iop) values -Iop_range = ( - np.linspace(30, 100, 100) * 1e3 -) # 5 equally spaced values between 30 and 100 A -op_conds = [ - OperationalConditions(temperature=T_sc + T_margin, magnetic_field=Bti) - for Bti in Bt_arr -] -Ic_sc_arr = np.array([sc_strand.Ic(op) for op in op_conds]) - -# Create a colormap to assign colors to different Iop values -colors = cm.viridis(np.linspace(0, 1, len(Iop_range))) # Use the 'viridis' colormap - -# Create a figure and axis -fig, ax = plt.subplots() - -# Plot the number of superconducting strands as a function of B for different Iop values -for Iop_ref, color in zip(Iop_range, colors, strict=False): - n_sc_strand = Iop_ref / Ic_sc_arr # Calculate number of strands - ax.plot(Bt_arr, n_sc_strand, color=color, label=f"Iop = {Iop} A") - # Add plot title, axis labels, and grid -ax.set_title("Number of strands as function of B and Iop") # Title -ax.set_xlabel("B [T]") # X-axis label -ax.set_ylabel("Nc strands") # Y-axis label -ax.grid(visible=True) - -# Create a ScalarMappable to map colors to the colorbar -sm = plt.cm.ScalarMappable( - cmap="viridis", norm=plt.Normalize(vmin=Iop_range.min(), vmax=Iop_range.max()) -) -sm.set_array([]) # Dummy array for the ScalarMappable - -# Add the colorbar to the figure -cbar = fig.colorbar(sm, ax=ax) -cbar.set_label("Iop [A]") # Label the colorbar - -# Show the plot -plt.show() - -# %% md -# **Calculate number of superconducting strands considering the strand critical -# current at B_TF_i and T_sc + T_margin** -# %% -op_cond = OperationalConditions(temperature=T_op, magnetic_field=B_TF_i) -Ic_sc = sc_strand.Ic(op_cond) -n_sc_strand = int(np.ceil(Iop / Ic_sc)) - -########################################################### -dx = 0.05 # cable length... just a dummy value -B_ref = 15 # [T] Reference B field value (limit for LTS) - -if B_TF_i < B_ref: - cable = RectangularCable( - name="RectangularCableLTS", - dx=dx, - sc_strand=sc_strand, - stab_strand=stab_strand, - n_sc_strand=n_sc_strand, - n_stab_strand=500, - d_cooling_channel=1e-2, - void_fraction=0.7, - cos_theta=0.97, - E=0.1e9, - ) -else: - cable = RectangularCable( - name="RectangularCableHTS", - dx=dx, - sc_strand=sc_strand, - stab_strand=stab_strand, - n_sc_strand=n_sc_strand, - n_stab_strand=500, - d_cooling_channel=1e-2, - void_fraction=0.7, - cos_theta=0.97, - E=120e9, - ) -cable.plot(0, 0, show=True) -bluemira_print(f"cable area: {cable.area}") -# %% md -# -# %% md -# ***Change cable aspect ratio*** -# %% -aspect_ratio = 1.2 -# This adjusts the cable dimensions while maintaining the total cross-sectional area. -cable.aspect_ratio = aspect_ratio -cable.plot(0, 0, show=True) -bluemira_print(f"cable area: {cable.area}") - -# operational_point = {"temperature": 5.7, "B": B(0)} - -mats = [NB3SN_MAG, COPPER_100, COPPER_300, SS316_LN_MAG] -mat_names = ["nb3sn", "copper100", "copper300", "ss316"] -temperatures = np.linspace(5, 250, 500) -magnetic_field = B_fun(0) - -# Prepare plots -# Adjusted for 5 plots (3x2 grid) -fig, axes = plt.subplots(3, 2, figsize=(12, 15)) -fig.suptitle("Material Properties vs Temperature", fontsize=16) - -ax1, ax2, ax3, ax4, ax5 = axes.flatten()[:5] # Extracting the first five axes - -for mat, name in zip(mats, mat_names, strict=False): - # Calculate properties over the temperature range - op_conds = [ - OperationalConditions(temperature=T, magnetic_field=magnetic_field) - for T in temperatures - ] - density = np.array([mat.density(op) for op in op_conds]) - cp_per_mass = np.array([mat.specific_heat_capacity(op) for op in op_conds]) - cp_per_volume = cp_per_mass * density - erho = np.array([mat.electrical_resistivity(op) for op in op_conds]) - E = np.array([mat.youngs_modulus(op) for op in op_conds]) - - # Plot density - ax1.plot(temperatures, density, label=name) - # Plot Cp [J/Kg/K] - ax2.plot(temperatures, cp_per_mass, label=name) - # Plot Cp [J/K/m³] - ax3.plot(temperatures, cp_per_volume, label=name) - # Plot erho [Ohm m] - ax4.plot(temperatures, erho, label=name) - # Plot E (assuming Young's modulus) - ax5.plot(temperatures, E, label=name) - -# Configure plots -ax1.set_title("Density [kg/m³] vs Temperature") -ax1.set_xlabel("Temperature [K]") -ax1.set_ylabel("Density [kg/m³]") -ax1.legend() -ax1.grid(visible=True) - -ax2.set_title("Heat Capacity per Mass [J/Kg/K] vs Temperature") -ax2.set_xlabel("Temperature [K]") -ax2.set_ylabel("Cp [J/Kg/K]") -ax2.legend() -ax2.grid(visible=True) - -ax3.set_title("Heat Capacity per Volume [J/K/m³] vs Temperature") -ax3.set_xlabel("Temperature [K]") -ax3.set_ylabel("Cp [J/K/m³]") -ax3.legend() -ax3.grid(visible=True) - -ax4.set_title("Electrical Resistivity [Ohm·m] vs Temperature") -ax4.set_xlabel("Temperature [K]") -ax4.set_ylabel("Resistivity [Ohm·m]") -ax4.legend() -ax4.grid(visible=True) - -ax5.set_title("E vs Temperature") # Modify title according to the meaning of E -ax5.set_xlabel("Temperature [K]") -ax5.set_ylabel("E [Unit]") # Change to correct unit -ax5.legend() -ax5.grid(visible=True) - -plt.tight_layout(rect=[0, 0, 1, 0.96]) -plt.show() - -# %% -# optimize the number of stabilizer strands using the hot spot criteria. -# Note: optimize_n_stab_ths changes adjust cable.dy while maintaining cable.dx. It -# could be possible to add a parameter maintain constant the aspect ratio if -# necessary. -bluemira_print( - f"before optimization: dx_cable = {cable.dx}, aspect ratio = {cable.aspect_ratio}" -) -T_for_hts = T_op -result = cable.optimize_n_stab_ths( - t0, - tf, - T_for_hts, - hotspot_target_temperature, - B_fun, - I_fun, - bounds=[1, 10000], - show=show, -) -bluemira_print( - f"after optimization: dx_cable = {cable.dx}, aspect ratio = {cable.aspect_ratio}" -) - -cable.set_aspect_ratio(aspect_ratio) -bluemira_print( - f"Adjust aspect ratio: dx_cable = {cable.dx}, aspect ratio = {cable.aspect_ratio}" -) - -# %% -########################################################### -# Create a conductor with the specified cable -conductor = SymmetricConductor( - cable=cable, - mat_jacket=SS316_LN_MAG, - mat_ins=DUMMY_INSULATOR_MAG, - dx_jacket=0.01, - dx_ins=1e-3, -) - -# %% -# case parameters -layout = "auto" # "layer" or "pancake" -wp_reduction_factor = 0.75 -min_gap_x = 2 * dr_plasma_side -n_layers_reduction = 4 - -# creation of the case -wp1 = WindingPack(conductor, 1, 1, name=None) # just a dummy WP to create the case - -case = TrapezoidalCaseTF( - Ri=Ri, - dy_ps=dr_plasma_side, - dy_vault=0.7, - theta_TF=360 / n_TF, - mat_case=SS316_LN_MAG, - wps=[wp1], -) - -print(f"pre-wp reduction factor: {wp_reduction_factor}") - -# arrangement of conductors into the winding pack and case -case.rearrange_conductors_in_wp( - n_conductors=n_cond, - wp_reduction_factor=wp_reduction_factor, - min_gap_x=min_gap_x, - n_layers_reduction=n_layers_reduction, - layout=layout, -) - -ax = case.plot(show=False, homogenized=False) -ax.set_title("Case design before optimization") -plt.show() - -bluemira_print(f"Previous number of conductors: {n_cond}") -bluemira_print(f"New number of conductors: {case.n_conductors}") - -# %% md -# ## Optimize cable jacket and case vault thickness -# %% -# Optimization parameters -bounds_cond_jacket = np.array([1e-5, 0.2]) -bounds_dy_vault = np.array([0.1, 2]) -max_niter = 100 -err = 1e-6 - -# case.optimize_jacket_and_vault( -case.optimize_jacket_and_vault( - pm=pm, - fz=t_z, - op_cond=OperationalConditions(temperature=T_op, magnetic_field=B_TF_i), - allowable_sigma=S_Y, - bounds_cond_jacket=bounds_cond_jacket, - bounds_dy_vault=bounds_dy_vault, - layout=layout, - wp_reduction_factor=wp_reduction_factor, - min_gap_x=min_gap_x, - n_layers_reduction=n_layers_reduction, - max_niter=max_niter, - eps=err, - n_conds=n_cond, -) - -if show: - scalex = np.array([2, 1]) - scaley = np.array([1, 1.2]) - - ax = case.plot(homogenized=homogenized) - ax.set_aspect("equal") - - # Fix the x and y limits - ax.set_xlim(-scalex[0] * case.dx_i, scalex[1] * case.dx_i) - ax.set_ylim(scaley[0] * 0, scaley[1] * case.Ri) - - deltax = [-case.dx_i / 2, case.dx_i / 2] - - ax.plot([-scalex[0] * case.dx_i, -case.dx_i / 2], [case.Ri, case.Ri], "k:") - - for i in range(len(case.wps)): - ax.plot( - [-scalex[0] * case.dx_i, -case.dx_i / 2], - [case.R_wp_i[i], case.R_wp_i[i]], - "k:", - ) - - ax.plot( - [-scalex[0] * case.dx_i, -case.dx_i / 2], - [case.R_wp_k[-1], case.R_wp_k[-1]], - "k:", - ) - ax.plot([-scalex[0] * case.dx_i, -case.dx_i / 2], [case.Rk, case.Rk], "k:") - - ax.set_title("Equatorial cross section of the TF WP") - ax.set_xlabel("Toroidal direction [m]") - ax.set_ylabel("Radial direction [m]") - - plt.show() - - -bluemira_print("Convergence should be: 9.066682976310327e-07 after 68 iterations") -# %% -# new operational current -bluemira_print(f"Operational current after optimization: {I_TF / case.n_conductors}") - -case.plot_convergence()