from collections import OrderedDict
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Tuple
import numpy as np
import numpy.typing as npt
import pandas as pd
from roxieapi.commons.roxie_constants import PlotLabels
[docs]
@dataclass
class BrickData:
"""Roxie Brick Data input"""
current: float
n1: int
n2: int
ncut: int
nodes: np.ndarray
[docs]
def to_table(self) -> str:
"""Return a string representation of the BrickData block .
Returns:
str: Brick data as string
"""
assert len(self.nodes) % 4 == 0, "Node length must be multiple of 4"
nodelen = len(self.nodes) // 4
output = (
f"{self.current:12g} {self.n1} {self.n2} {nodelen} {self.ncut}"
)
nodestr = "\n".join(
[f" {n[0]:13g} {n[1]:13g} {n[2]:13g}" for n in self.nodes]
)
return output + "\n" + nodestr
[docs]
@dataclass
class IronYokeOptions:
mesh_scale: int = 0
mir_inner_radius: float = 0
mir_rel_perm: float = 0
sym_yz: int = 0
sym_zx: int = 0
sym_xy: int = 0
rot_mode: int = 0
rot_div: int = 0
rot_sym: int = 0
[docs]
@dataclass
class Geometry:
"""Geometry Information for 2D/3D geometries.
For 2D geometries, each point is a [x,y] coordinate.
For 3D geometries, each point is a [x,y,z] coordinate.
nodes - List of points ([x,y,z]) making up the Geometry
elements - List of connected points as faces or cells in the form of [<p1>..<pn>]
boundaries - Dict of boundaries for a Geometry, in the form of {id: [[x,y,z]...[x,y,z]]}
"""
nodes: npt.NDArray[np.float64]
elements: Optional[List[List[int]]]
boundaries: Optional[Dict[int, npt.NDArray[np.float64]]]
[docs]
def generate_elements_for_coil_nodes(self) -> None:
"""Generate cells from points.
points are ordered in z direction, 4 points define one face.
Once cell is between two sets of 4 points
7+----+6
/| /|
3+----+2|
|4+--|-+5
|/ |/
0+----+1
"""
self.elements = [
list(range(i - 4, i + 4)) for i in range(4, len(self.nodes), 4)
]
[docs]
@dataclass
class CoilGeometry:
"""Geometry information for 2D coils"""
nr: int
block_id: int
layer_id: int
geometry: npt.NDArray[np.float64]
strands: Dict[int, npt.NDArray[np.float64]]
[docs]
@dataclass
class Base3DGeometry:
"""Base geometry for 3D objects"""
nr: int
geometry: Geometry
[docs]
@dataclass
class Brick3DGeometry(Base3DGeometry):
"""Geometry information for 3D bricks"""
[docs]
@dataclass
class Coil3DGeometry(Base3DGeometry):
"""Geometry information for 3D coils"""
block_id: int
layer_id: int
[docs]
@dataclass
class WedgeSurface:
"""Surface of a wedge"""
lower_edge: npt.NDArray[np.float64]
upper_edge: npt.NDArray[np.float64]
[docs]
@dataclass
class WedgeGeometry:
"""Geometry to store wedge information"""
layer: int
nr: int
inner_surface: Optional[WedgeSurface]
outer_surface: Optional[WedgeSurface]
block_inner: int
block_outer: int
[docs]
@dataclass
class BlockGeometry:
nr: int
inner_surface: WedgeSurface
outer_surface: WedgeSurface
[docs]
@dataclass
class BlockTopology:
"""Topology of blocks, conductors and strands"""
block_nr: int
block_orig: int
layer_nr: int
first_conductor: int
last_conductor: int
n_radial: int
n_azimuthal: int
first_strand: int
last_strand: int
ins_radial: float
ins_azimuthal: float
[docs]
@dataclass
class PlotAxis:
"""Plot Axis information (for roxie plots)"""
label: str
bounds: Optional[Tuple[float, float]]
log: bool
[docs]
@dataclass
class PlotLegend:
"""Plot Legend information (for roxie plots)"""
pos: Optional[str]
greyScale: Optional[bool]
min_val: Optional[float]
max_val: Optional[float]
[docs]
@dataclass
class PlotInfo:
"""Plot info object, containing all information to create a crossection or 3D plot"""
id: str
type: str
dataType: str
label: str
plotLegend: Optional[PlotLegend]
harmCoil: Optional[int]
vector_mappings: Optional[Dict[str, int]]
[docs]
@dataclass
class GraphInfo:
"""GraphInfo object, containing all information for a xy graph"""
id: int
graph_type: int
xval: str
yval: str
logx: bool
logy: bool
weight: float
label: Optional[str]
[docs]
@dataclass
class Plot:
"""Base Plot object, containing all information to create a crossection or 3D plot"""
title: str
id: int
axes: Dict[str, PlotAxis]
_plotInfos: List[PlotInfo]
active: Optional[PlotInfo] = field(init=False, default=None)
[docs]
@dataclass
class GraphPlot:
"""Graph plot information"""
title: str
id: int
axes: Dict[str, PlotAxis]
graphs: List[GraphInfo]
[docs]
@dataclass
class Plot2D(Plot):
"""Plot2D object, for crossection plots"""
[docs]
@staticmethod
def create(
title="New Plot2D",
xlim: Optional[Tuple[float, float]] = None,
ylim: Optional[Tuple[float, float]] = None,
) -> "Plot2D":
"""
Creates a new Plot2D object.
Parameters:
title (str): The title of the plot. Default is "New Plot2D".
xlim (Optional[Tuple[float, float]]): The limits for the x-axis. Default is None.
ylim (Optional[Tuple[float, float]]): The limits for the y-axis. Default is None.
Returns:
Plot2D: A new Plot2D object.
"""
return Plot2D(
title,
-1,
{
"X": PlotAxis("x in mm", bounds=xlim, log=False),
"Y": PlotAxis("y in mm", bounds=ylim, log=False),
},
[],
)
[docs]
def add_coilPlot(
self,
id: str,
label: str = "",
legend: Optional[PlotLegend] = None,
harm_coil: Optional[int] = None,
) -> "Plot2D":
"""
Add a coil plot component to the plot.
parameters:
id (str): The data id to be used.
label (str): Label of the coil plot.
legend (Optional[PlotLegend]): The legend of the component. Default is None.
harm_coil (Optional[int]): The harmonic coil number if the plot depends on a harmonic coil. Default is None.
"""
if not label:
label = PlotLabels.plot2D_desc.get(id, "")
self._plotInfos.append(
PlotInfo(id, "coilPlot", "scalar", label, legend, harm_coil, None)
)
return self
[docs]
def add_meshPlot(
self, id: str, label: str = "", legend: Optional[PlotLegend] = None
) -> "Plot2D":
"""Add a mesh plot to the plot object
:param id: The data id to be used.
:type id: str
:param label: Label of the mesh plot, defaults to ""
:type label: str, optional
:param legend: The legend of the component. Default is None.
:type legend: Optional[PlotLegend], optional
:return: _description_
:rtype: Plot2D
"""
if not label:
label = PlotLabels.plotMesh2D_desc.get(id, "")
self._plotInfos.append(
PlotInfo(id, "meshPlot", "scalar", label, legend, None, None)
)
return self
@property
def pointPlots(self) -> List[PlotInfo]:
"""Return all pointPlots defined in the Plot"""
return list(filter(lambda x: x.type == "pointPlot", self._plotInfos))
@property
def coilPlots(self) -> List[PlotInfo]:
"""Return all coilPlots defined in the Plot"""
return list(filter(lambda x: x.type == "coilPlot", self._plotInfos))
@property
def meshPlots(self) -> List[PlotInfo]:
"""Return all meshPlots defined in the Plot"""
return list(filter(lambda x: x.type == "meshPlot", self._plotInfos))
@property
def matrixPlots(self) -> List[PlotInfo]:
"""Return all matrixPlots defined in the Plot"""
return list(filter(lambda x: x.type == "matrixPlot", self._plotInfos))
@property
def irisPlots(self) -> List[PlotInfo]:
"""Return all irisPlots defined in the Plot"""
return list(filter(lambda x: x.type == "irisPlot", self._plotInfos))
[docs]
@dataclass
class Plot3D(Plot):
[docs]
@staticmethod
def create(
title="New Plot3D",
xlim: Optional[Tuple[float, float]] = None,
ylim: Optional[Tuple[float, float]] = None,
zlim: Optional[Tuple[float, float]] = None,
) -> "Plot3D":
"""
Creates a new Plot3D object
:param title: The plot title, defaults to "New Plot3D"
:type title: str, optional
:param xlim: Limits of x axis, defaults to None
:type xlim: Optional[Tuple[float, float]], optional
:param ylim: Limits of y axis, defaults to None
:type ylim: Optional[Tuple[float, float]], optional
:param zlim: Limits of z axis, defaults to None
:type zlim: Optional[Tuple[float, float]], optional
:return: The Plot3D description
:rtype: Plot3D
"""
return Plot3D(
title,
-1,
{
"X": PlotAxis("x in mm", bounds=xlim, log=False),
"Y": PlotAxis("y in mm", bounds=ylim, log=False),
"Z": PlotAxis("z in mm", bounds=zlim, log=False),
},
[],
)
[docs]
def add_coilPlot(
self,
id: str,
label: str = "",
legend: Optional[PlotLegend] = None,
) -> "Plot3D":
"""
Add a coil plot component to the plot.
parameters:
id (str): The data id to be used.
label (str): Label of the coil plot.
legend (Optional[PlotLegend]): The legend of the component. Default is None.
harm_coil (Optional[int]): The harmonic coil number if the plot depends on a harmonic coil. Default is None.
"""
if not label:
label = PlotLabels.plot3D_desc.get(id, "")
self._plotInfos.append(
PlotInfo(id, "coilPlot3D", "scalar", label, legend, None, None)
)
return self
[docs]
def add_meshPlot(
self, id: str, label: str = "", legend: Optional[PlotLegend] = None
) -> "Plot3D":
"""Add a mesh plot to the plot object
:param id: The data id to be used.
:type id: str
:param label: Label of the mesh plot, defaults to ""
:type label: str, optional
:param legend: The legend of the component. Default is None.
:type legend: Optional[PlotLegend], optional
:return: _description_
:rtype: Plot2D
"""
if not label:
label = PlotLabels.plotMesh3D_desc.get(id, "")
self._plotInfos.append(
PlotInfo(id, "meshPlot3D", "scalar", label, legend, None, None)
)
return self
@property
def coilPlots(self) -> List[PlotInfo]:
"""Return all coil plots defined"""
return list(filter(lambda x: x.type == "coilPlot3D", self._plotInfos))
@property
def meshPlots(self) -> List[PlotInfo]:
"""Return all mesh plots defined"""
return list(filter(lambda x: x.type == "meshPlot3D", self._plotInfos))
@property
def showSpacers(self) -> bool:
"""Flag if spacers are shown in the plot"""
return any((x.type == "spacerPlot3D" for x in self._plotInfos))
[docs]
@dataclass
class HarmonicCoil:
id: int
_coil_type: int
_measurement_type: int
_main_harmonic: int
params: Dict[str, float] = field(default_factory=dict)
bn: Dict[int, float] = field(default_factory=dict)
an: Dict[int, float] = field(default_factory=dict)
strandData: pd.DataFrame = field(default_factory=pd.DataFrame)
iris: pd.DataFrame = field(default_factory=pd.DataFrame)
@property
def main_harmonic(self) -> str:
if self._main_harmonic == 0:
return "None"
else:
ab = "A" if self.skew else "B"
return f"{ab}{self.order}"
@property
def order(self) -> int:
return self._main_harmonic // 2
@property
def skew(self) -> bool:
return self._main_harmonic % 2 == 1
@property
def absolute(self) -> bool:
return self._main_harmonic == 0
@property
def coil_type(self) -> str:
if self._coil_type == 1:
return "Cylindrical"
if self._coil_type == 2:
return "Wiggler"
if self._coil_type == 3:
return "Zonal"
if self._coil_type == 4:
return "Elliptical"
if self._coil_type == 5:
return "Cartx"
if self._coil_type == 6:
return "Carty"
if self._coil_type == 7:
return "Torus"
if self._coil_type == 8:
return "Fourier curve"
return f"unknown coil type {self._coil_type}"
@property
def bref(self) -> str:
nr = self._measurement_type
if nr == 0:
return "all"
if nr == 2:
return "Bcoil"
if nr == 1:
return "Biron"
if nr == 4:
return "Bmagn"
if nr == 3:
return "Biscc"
if nr == 5:
return "{Bcoil+Biron}"
if nr == 6:
return "{Bcoil+Biron+Bmagn}"
if nr == 7:
return "{Bcoil+Biron+Biscc}"
return "Unknown Meas type: {0}".format(nr)
[docs]
def get_coil_info(self) -> OrderedDict[str, str]:
d = OrderedDict()
d["type"] = self.coil_type
if self._coil_type == 1:
d["reference radius (mm)"] = str(self.params["rref"])
d["x position (mm)"] = str(self.params["xpos"])
d["y position (mm)"] = str(self.params["ypos"])
if self._coil_type == 2:
d["period length (mm)"] = str(self.params["period_length"])
d["x0 (mm)"] = str(self.params["x0"])
d["y0 (mm)"] = str(self.params["y0"])
if self._coil_type == 4:
d["semi minor axis (mm)"] = str(self.params["semi_minor_axis"])
d["semi major axis (mm)"] = str(self.params["semi_major_axis"])
if "nr_z" in self.params:
d["number of coils in Z direction"] = str(self.params["nr_z"])
d["coil length (mm)"] = str(self.params["coil_length"])
d["reference position"] = str(self.params["ref_pos"])
return d
[docs]
def get_field_info(self) -> OrderedDict[str, str]:
d = OrderedDict()
if not self.absolute:
d["main field (T)"] = str(self.params["main_field"])
mh = self._main_harmonic // 2 - 1
if mh == 0:
mhs = "T"
elif mh == 1:
mhs = "$\\frac{T}{m}$"
else:
mhs = f"$\\frac{{T}}{{m^{mh}}}$"
d[f"reference magnet strength ({mhs})"] = str(self.params["mag_strength"])
if "nr_z" in self.params:
d["MAGNETIC LENGTH (mm)"] = str(self.params["mag_length"])
d["error of harmonic analysis of br"] = str(self.params["error_br"])
return d
[docs]
def get_table(self) -> pd.DataFrame:
vals = [(i, self.bn[i], self.an[i]) for i in self.bn]
if self.absolute:
cols = ["Order", "Bn", "An"]
else:
cols = ["Order", "bn", "an"]
return pd.DataFrame(vals, columns=cols)
[docs]
@dataclass
class ObjectiveResult:
nr: int
value: float
raw_value: float
obj_name: str
obj_p1: int
obj_p2: int
[docs]
@dataclass
class DesignVariableResult:
nr: int
value: float
name: str
act_on: int
blocks: list[int]