fiqus 2026.1.0__py3-none-any.whl → 2026.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. fiqus/MainFiQuS.py +1 -8
  2. fiqus/data/DataConductor.py +4 -8
  3. fiqus/data/DataFiQuSMultipole.py +358 -167
  4. fiqus/data/DataModelCommon.py +30 -15
  5. fiqus/data/DataMultipole.py +33 -10
  6. fiqus/data/DataWindingsCCT.py +37 -37
  7. fiqus/data/RegionsModelFiQuS.py +1 -1
  8. fiqus/geom_generators/GeometryMultipole.py +751 -54
  9. fiqus/getdp_runners/RunGetdpMultipole.py +181 -31
  10. fiqus/mains/MainMultipole.py +109 -17
  11. fiqus/mesh_generators/MeshCCT.py +209 -209
  12. fiqus/mesh_generators/MeshMultipole.py +938 -263
  13. fiqus/parsers/ParserCOND.py +2 -1
  14. fiqus/parsers/ParserDAT.py +16 -16
  15. fiqus/parsers/ParserGetDPOnSection.py +212 -212
  16. fiqus/parsers/ParserGetDPTimeTable.py +134 -134
  17. fiqus/parsers/ParserMSH.py +53 -53
  18. fiqus/parsers/ParserRES.py +142 -142
  19. fiqus/plotters/PlotPythonCCT.py +133 -133
  20. fiqus/plotters/PlotPythonMultipole.py +18 -18
  21. fiqus/post_processors/PostProcessMultipole.py +16 -6
  22. fiqus/pre_processors/PreProcessCCT.py +175 -175
  23. fiqus/pro_assemblers/ProAssembler.py +3 -3
  24. fiqus/pro_material_functions/ironBHcurves.pro +246 -246
  25. fiqus/pro_templates/combined/CC_Module.pro +1213 -0
  26. fiqus/pro_templates/combined/ConductorAC_template.pro +1025 -0
  27. fiqus/pro_templates/combined/Multipole_template.pro +2738 -1338
  28. fiqus/pro_templates/combined/TSA_materials.pro +102 -2
  29. fiqus/pro_templates/combined/materials.pro +54 -3
  30. fiqus/utils/Utils.py +18 -25
  31. fiqus/utils/update_data_settings.py +1 -1
  32. {fiqus-2026.1.0.dist-info → fiqus-2026.1.2.dist-info}/METADATA +64 -68
  33. {fiqus-2026.1.0.dist-info → fiqus-2026.1.2.dist-info}/RECORD +42 -40
  34. {fiqus-2026.1.0.dist-info → fiqus-2026.1.2.dist-info}/WHEEL +1 -1
  35. tests/test_geometry_generators.py +29 -32
  36. tests/test_mesh_generators.py +35 -34
  37. tests/test_solvers.py +32 -31
  38. tests/utils/fiqus_test_classes.py +396 -147
  39. tests/utils/generate_reference_files_ConductorAC.py +57 -57
  40. tests/utils/helpers.py +76 -1
  41. {fiqus-2026.1.0.dist-info → fiqus-2026.1.2.dist-info}/LICENSE.txt +0 -0
  42. {fiqus-2026.1.0.dist-info → fiqus-2026.1.2.dist-info}/top_level.txt +0 -0
@@ -4,6 +4,7 @@ import subprocess
4
4
  import logging
5
5
  import timeit
6
6
  import re
7
+ import pandas as pd
7
8
 
8
9
  import gmsh
9
10
  from fiqus.pro_assemblers.ProAssembler import ASS_PRO as aP
@@ -13,8 +14,9 @@ from fiqus.data import DataFiQuS as dF
13
14
  from fiqus.data import RegionsModelFiQuS as rM
14
15
  from fiqus.data import DataMultipole as dM
15
16
 
16
- logger = logging.getLogger(__name__)
17
+ logger = logging.getLogger('FiQuS')
17
18
 
19
+ trigger_time_deactivated = 99999
18
20
 
19
21
  class AssignNaming:
20
22
  def __init__(self, data: dF.FDM() = None):
@@ -24,16 +26,30 @@ class AssignNaming:
24
26
  """
25
27
  self.data: dF.FDM() = data
26
28
 
27
- self.naming_conv = {'omega': 'Omega', 'boundary': 'Bd_', 'powered': '_p', 'induced': '_i', 'air': '_a',
28
- 'air_far_field': '_aff', 'iron': '_bh', 'conducting': '_c', 'insulator': '_ins', 'terms': 'Terms'}
29
+ self.naming_conv = {'omega': 'Omega',
30
+ 'boundary': 'Bnd_',
31
+ 'powered': '_p', 'induced': '_i',
32
+ 'air': '_a',
33
+ 'air_far_field': '_aff',
34
+ 'conducting': '_c',
35
+ 'insulator': '_ins',
36
+ 'terms': 'Terms',
37
+ 'iron_yoke': '_bh', #todo: consider renaming this
38
+ 'collar': '_collar',
39
+ 'ref_mesh': '_refmesh',
40
+ 'poles': '_poles'}
41
+
29
42
  self.data.magnet.postproc.electromagnetics.volumes = \
30
- [self.naming_conv['omega'] + (self.naming_conv[var] if var != 'omega' else '') for var in self.data.magnet.postproc.electromagnetics.volumes]
43
+ [self.naming_conv['omega'] + (self.naming_conv[var] if var != 'omega' else '') for var in
44
+ self.data.magnet.postproc.electromagnetics.volumes]
31
45
  self.data.magnet.postproc.thermal.volumes = \
32
- [self.naming_conv['omega'] + (self.naming_conv[var] if var != 'omega' else '') for var in self.data.magnet.postproc.thermal.volumes]
46
+ [self.naming_conv['omega'] + (self.naming_conv[var] if var != 'omega' else '') for var in
47
+ self.data.magnet.postproc.thermal.volumes]
33
48
 
34
49
 
35
50
  class RunGetdpMultipole:
36
- def __init__(self, data: AssignNaming = None, solution_folder: str = None, GetDP_path: str = None, verbose: bool = False):
51
+ def __init__(self, data: AssignNaming = None, solution_folder: str = None, GetDP_path: str = None,
52
+ verbose: bool = False):
37
53
  """
38
54
  Class to solve pro file
39
55
  :param data: FiQuS data model
@@ -48,62 +64,116 @@ class RunGetdpMultipole:
48
64
  self.solution_folder = solution_folder
49
65
  self.GetDP_path = GetDP_path
50
66
  self.verbose: bool = verbose
51
- self.call_method = 'subprocess' # or onelab
67
+ self.call_method = 'subprocess' # or onelab or subprocess or alt_solver
52
68
 
53
69
  self.rm_EM = rM.RegionsModel()
54
70
  self.rm_TH = rM.RegionsModel()
55
- self.rc = dM.MultipoleRegionCoordinate()\
71
+ self.aux = {
72
+ "half_turns": {
73
+ "ht": {}, # Dictionary to store half-turn data
74
+ "max_reg": None, # Maximum region number to avoid overlaps of physical regions
75
+ "block": {} # Dictionary that stores the relation between magnet blocks(groups) and half-turn areas
76
+
77
+ },
78
+ "e_cliq": {}, # Dictionary that stores the E-CLIQ source current LUTs from a csv
79
+ "tsa_collar": {},
80
+ "sim_info": {
81
+ 'date': '',
82
+ 'author': ''
83
+ }
84
+ }
85
+ self.material_properties_model = None
86
+ self.rohm = {} # rohm parameter dictionary
87
+ self.rohf = {} # rohf parameter dictionary
88
+ self.rc = dM.MultipoleRegionCoordinate() \
56
89
  if self.data.magnet.mesh.thermal.isothermal_conductors and self.data.magnet.solve.thermal.solve_type else None
57
90
 
58
91
  self.gu = GmshUtils(self.solution_folder, self.verbose)
59
92
  self.gu.initialize(verbosity_Gmsh=self.data.run.verbosity_Gmsh)
60
93
  self.occ = gmsh.model.occ
61
94
  self.mesh = gmsh.model.mesh
62
-
63
- self.brep_iron_curves = {1: set(), 2: set(), 3: set(), 4: set()}
95
+ self.brep_curves = {}
96
+ for name in self.data.magnet.geometry.electromagnetics.areas:
97
+ self.brep_curves[name] = {1: set(), 2: set(), 3: set(), 4: set()}
64
98
  self.mesh_files = os.path.join(os.path.dirname(self.solution_folder), self.data.general.magnet_name)
65
99
  self.model_file = os.path.join(self.solution_folder, 'Center_line.csv')
66
100
 
67
101
  self.mf = {'EM': f"{self.mesh_files}_EM.msh", 'TH': f"{self.mesh_files}_TH.msh"}
68
102
 
69
103
  def loadRegionFiles(self):
104
+ """
105
+ Read the regions' files and store the information in a variable. used for extracting the maximum region number.
106
+ :param
107
+ :return: Data from the regions files
108
+ """
70
109
  if self.data.magnet.solve.electromagnetics.solve_type:
71
110
  self.rm_EM = Util.read_data_from_yaml(f"{self.mesh_files}_EM.reg", rM.RegionsModel)
72
111
  if self.data.magnet.solve.thermal.solve_type:
73
112
  self.rm_TH = Util.read_data_from_yaml(f"{self.mesh_files}_TH.reg", rM.RegionsModel)
74
113
 
75
114
  def loadRegionCoordinateFile(self):
115
+ """
116
+ Read the reco file and store the information in a variable.
117
+ :param
118
+ :return: Data from the reco file
119
+ """
76
120
  self.rc = Util.read_data_from_yaml(f"{self.mesh_files}_TH.reco", dM.MultipoleRegionCoordinate)
77
121
 
78
122
  def assemblePro(self):
123
+ """
124
+ Assemble the .pro file using the right template depending on the inputs from the yaml
125
+ :param
126
+ :return: Assembled .pro file
127
+ """
79
128
  logger.info(f"Assembling pro file...")
80
129
  start_time = timeit.default_timer()
81
- ap = aP(file_base_path=os.path.join(self.solution_folder, self.data.general.magnet_name), naming_conv=self.naming_conv)
82
- BH_curves_path = os.path.join(pathlib.Path(os.path.dirname(__file__)).parent, 'pro_material_functions', 'ironBHcurves.pro')
83
- ap.assemble_combined_pro(template='Multipole_template.pro', rm_EM=self.rm_EM, rm_TH=self.rm_TH, rc=self.rc, dm=self.data, mf=self.mf, BH_curves_path=BH_curves_path)
130
+ ap = aP(file_base_path=os.path.join(self.solution_folder, self.data.general.magnet_name),
131
+ naming_conv=self.naming_conv)
132
+ BH_curves_path = os.path.join(pathlib.Path(os.path.dirname(__file__)).parent, 'pro_material_functions',
133
+ 'ironBHcurves.pro')
134
+ template = 'Multipole_template.pro'
135
+ ap.assemble_combined_pro(template=template, rm_EM=self.rm_EM, rm_TH=self.rm_TH, rc=self.rc, dm=self.data,
136
+ mf=self.mf, BH_curves_path=BH_curves_path, aux=self.aux)
84
137
  logger.info(
85
138
  f"Assembling pro file took"
86
139
  f" {timeit.default_timer() - start_time:.2f} s."
87
140
  )
88
141
 
89
142
  def solve_and_postprocess(self):
143
+ """
144
+ Generates the necessary GetDP commands to solve and postprocess the model, and runs them.
145
+ :param
146
+ :return: None
147
+ """
90
148
  commands = None
91
149
  if self.call_method == 'onelab':
92
150
  commands = f"-solve -v2 -pos -verbose {self.data.run.verbosity_GetDP}"
93
151
  elif self.call_method == 'subprocess':
94
- commands = []
95
- commands.append(["-solve", 'resolution', "-v2", "-pos", "Dummy", "-verbose", str(self.data.run.verbosity_GetDP)])
96
-
152
+ commands = [["-solve", 'resolution', "-v2", "-pos", "Dummy", "-verbose", str(self.data.run.verbosity_GetDP),
153
+ "-mat_mumps_icntl_14", "250"]]
97
154
  self._run(commands=commands)
98
155
 
99
156
  def postprocess(self):
157
+ """
158
+ Generates the necessary GetDP commands to postprocess the model, and runs them.
159
+ :param
160
+ :return: None
161
+ """
100
162
  if self.call_method == 'onelab':
101
- command = "-v2 -pos -verbose {self.data.run.verbosity_GetDP}"
163
+ commands = f"-v2 -pos -verbose {self.data.run.verbosity_GetDP}"
102
164
  elif self.call_method == 'subprocess':
103
- command = [["-v2", "-pos", "-verbose", str(self.data.run.verbosity_GetDP)]]
104
- self._run(commands=command)
165
+ commands = [["-v2", "-pos", "Dummy", "-verbose", str(self.data.run.verbosity_GetDP)]]
166
+ elif self.call_method == 'alt_solver':
167
+ commands = [["-solve", 'resolution', "-v2", "-pos", "Dummy"]]
168
+
169
+ self._run(commands=commands)
105
170
 
106
171
  def _run(self, commands):
172
+ """
173
+ runs GetDP with the specified commands. Additionally it captures and logs the output.
174
+ :param commands: List of commands to run GetDP with
175
+ :return: None
176
+ """
107
177
  logger.info("Solving...")
108
178
  start_time = timeit.default_timer()
109
179
  if self.call_method == 'onelab':
@@ -111,7 +181,7 @@ class RunGetdpMultipole:
111
181
  gmsh.onelab.run(f"{self.data.general.magnet_name}",
112
182
  f"{self.GetDP_path} {os.path.join(self.solution_folder, self.data.general.magnet_name)}.pro {command}")
113
183
  gmsh.onelab.setChanged("GetDP", 0)
114
- elif self.call_method == 'subprocess':
184
+ elif self.call_method == 'subprocess' or self.call_method == 'alt_solver':
115
185
  # subprocess.call([f"{self.GetDP_path}", f"{os.path.join(self.solution_folder, self.data.general.magnet_name)}.pro"] + command + ["-msh", f"{self.mesh_files}.msh"])
116
186
 
117
187
  # view_tag = gmsh.view.getTags() # this should be b
@@ -122,14 +192,26 @@ class RunGetdpMultipole:
122
192
  mpi_prefix = ["mpiexec", "-np", str(self.data.magnet.solve.noOfMPITasks)]
123
193
  else:
124
194
  mpi_prefix = []
125
-
195
+
126
196
  for command in commands:
127
- getdpProcess = subprocess.Popen(
128
- mpi_prefix + [f"{self.GetDP_path}", f"{os.path.join(self.solution_folder, self.data.general.magnet_name)}.pro"] +
129
- command,
130
- stdout=subprocess.PIPE,
131
- stderr=subprocess.STDOUT,
132
- )
197
+ use_predefined_pro = False # If True, one can use a predefined .pro file
198
+ if use_predefined_pro:
199
+ logger.warning("Using predefined .pro file")
200
+ getdpProcess = subprocess.Popen(
201
+ mpi_prefix + [f"{self.GetDP_path}",
202
+ f"{os.path.join(os.path.abspath(os.path.join(self.solution_folder, '..', '..', '..')), '_out/TEST')}.pro"] +
203
+ command,
204
+ stdout=subprocess.PIPE,
205
+ stderr=subprocess.STDOUT,
206
+ )
207
+ else:
208
+ getdpProcess = subprocess.Popen(
209
+ mpi_prefix + [f"{self.GetDP_path}",
210
+ f"{os.path.join(self.solution_folder, self.data.general.magnet_name)}.pro"] +
211
+ command,
212
+ stdout=subprocess.PIPE,
213
+ stderr=subprocess.STDOUT,
214
+ )
133
215
  with getdpProcess.stdout:
134
216
  for line in iter(getdpProcess.stdout.readline, b""):
135
217
  line = line.decode("utf-8").rstrip()
@@ -139,8 +221,7 @@ class RunGetdpMultipole:
139
221
  logger.info(parsedLine)
140
222
  elif "Warning" in line:
141
223
  parsedLine = re.sub(r"Warning\s*:\s*", "", line)
142
- if "Unknown" not in parsedLine:
143
- logger.warning(parsedLine)
224
+ logger.warning(parsedLine)
144
225
  elif "Error" in line:
145
226
  parsedLine = re.sub(r"Error\s*:\s*", "", line)
146
227
  logger.error(parsedLine)
@@ -163,8 +244,77 @@ class RunGetdpMultipole:
163
244
  )
164
245
 
165
246
  def ending_step(self, gui: bool = False):
247
+ """
248
+ Finalize the GetDP runner, either by launching the GUI or finalizing gmsh.
249
+ :param gui: If True, launches the interactive GUI; otherwise finalizes gmsh.
250
+ :return: None
251
+ """
166
252
  if gui:
167
253
  self.gu.launch_interactive_GUI()
168
254
  else:
169
- gmsh.clear()
170
- gmsh.finalize()
255
+ if gmsh.isInitialized():
256
+ gmsh.clear()
257
+ gmsh.finalize()
258
+
259
+ def read_aux_file(self, aux_file_path):
260
+ """
261
+ Read the auxiliary file and store the information in a variable. Necessary to extract half-turn information from blocks.
262
+ :param aux_file_path: Path to the auxiliary file
263
+ :return: Data from the auxiliary file
264
+ """
265
+ try:
266
+ self.aux_data = Util.read_data_from_yaml(aux_file_path, dM.MultipoleData)
267
+ logger.info(f"Auxiliary data loaded from {aux_file_path}")
268
+ except FileNotFoundError:
269
+ logger.error(f"Auxiliary file not found: {aux_file_path}")
270
+ except Exception as e:
271
+ logger.error(f"Error reading auxiliary file: {e}")
272
+
273
+ def extract_half_turn_blocks(self):
274
+ """
275
+ Extract a dictionary from the .aux file where the keys are integers corresponding to the block number
276
+ and the dictionary entries for these keys are a list of all the half-turn areas included in that block.
277
+ :return: Dictionary with block numbers as keys and lists of half-turn areas as values
278
+ """
279
+ aux_data = self.aux_data
280
+
281
+ half_turn_areas = {}
282
+ info_block = {}
283
+ coils = aux_data.geometries.coil.coils
284
+ for coil_nr, coil_data in coils.items():
285
+ poles = coil_data.poles
286
+ for pole_nr, pole_data in poles.items():
287
+ layers = pole_data.layers
288
+ for layer_nr, layer_data in layers.items():
289
+ windings = layer_data.windings
290
+ for winding_nr, winding_data in windings.items():
291
+ blocks = winding_data.blocks
292
+ for block_nr, block_data in blocks.items():
293
+ half_turns = block_data.half_turns.areas
294
+ info_block[block_nr] = [int(area) for area in half_turns.keys()]
295
+ half_turn_areas[int(block_nr)] = [int(area) for area in half_turns.keys()]
296
+
297
+ self.aux["half_turns"]["ht"] = half_turn_areas
298
+ self.aux["half_turns"]["block"] = info_block
299
+ pass
300
+
301
+ def extract_specific_TSA_lines(self):
302
+ """
303
+ Extract a dictionary from the thermal .aux file, collects the outer lines of the half-turns and wedges used for the TSA collar
304
+ """
305
+ aux_data = self.aux_data
306
+ pg = aux_data.domains.physical_groups
307
+ max_layer = len([k for k in self.aux_data.geometries.coil.coils[1].poles[1].layers.keys()]) # todo : more elegant way ? @emma
308
+ tags = []
309
+ for block in pg.blocks.values():
310
+ for ht in block.half_turns.values():
311
+ if ht.group[-1] == str(max_layer): # r1_a2 or r2_a2
312
+ tags.append(ht.lines['o'])
313
+ self.aux["tsa_collar"]['ht_lines'] = tags
314
+
315
+ tags = []
316
+ for wedge in pg.wedges.values():
317
+ if wedge.group[-1] == str(max_layer): # r1_a2 or r2_a2
318
+ tags.append(wedge.lines['o'])
319
+ self.aux["tsa_collar"]['wedge_lines'] = tags
320
+ pass
@@ -1,7 +1,9 @@
1
+ import logging
1
2
  import os
2
3
  import gmsh
3
4
  import time
4
5
 
6
+ from fiqus.plotters.PlotPythonMultipole import PlotPythonMultipole
5
7
  from fiqus.utils.Utils import GmshUtils
6
8
  from fiqus.utils.Utils import FilesAndFolders as Util
7
9
  from fiqus.data import DataFiQuS as dF
@@ -11,11 +13,10 @@ from fiqus.mesh_generators.MeshMultipole import Mesh
11
13
  from fiqus.getdp_runners.RunGetdpMultipole import RunGetdpMultipole
12
14
  from fiqus.getdp_runners.RunGetdpMultipole import AssignNaming
13
15
  from fiqus.post_processors.PostProcessMultipole import PostProcess
14
- from fiqus.plotters.PlotPythonMultipole import PlotPythonMultipole
15
16
 
16
17
 
17
18
  class MainMultipole:
18
- def __init__(self, fdm: dF.FDM = None, rgd_path: str = None, verbose: bool = None):
19
+ def __init__(self, fdm: dF.FDM = None, rgd_path: str = None, verbose: bool = None, inputs_folder_path = None):
19
20
  """
20
21
  Main class for working with simulations for multipole type magnets
21
22
  :param fdm: FiQuS data model
@@ -30,11 +31,13 @@ class MainMultipole:
30
31
  self.geom_folder = None
31
32
  self.mesh_folder = None
32
33
  self.solution_folder = None
34
+ self.inputs_folder_path = inputs_folder_path
33
35
 
34
36
  def force_symmetry(self):
35
37
  fdm = self.fdm.__deepcopy__()
36
38
  fdm.magnet.geometry.electromagnetics.symmetry = 'x'
37
39
  return fdm
40
+
38
41
  def generate_geometry(self, gui: bool = False):
39
42
  geom = Util.read_data_from_yaml(self.rgd, FiQuSGeometry)
40
43
  fdm = self.force_symmetry() if 'solenoid' in geom.Roxie_Data.coil.coils[1].type else self.fdm # todo: this should be handled by pydantic
@@ -43,14 +46,16 @@ class MainMultipole:
43
46
  plotter.plot_coil_wedges()
44
47
  gg = Geometry(data=fdm, geom=geom, geom_folder=self.geom_folder, verbose=self.verbose)
45
48
  gg.saveHalfTurnCornerPositions()
49
+
46
50
  geometry_settings = {'EM': fdm.magnet.geometry.electromagnetics, 'TH': self.fdm.magnet.geometry.thermal}
47
51
  geometry_type_list = []
48
- if fdm.magnet.geometry.electromagnetics.create: geometry_type_list.append('EM')
49
- if fdm.magnet.geometry.thermal.create: geometry_type_list.append('TH')
52
+
53
+ if geometry_settings['EM'].create: geometry_type_list.append('EM')
54
+ if geometry_settings['TH'].create: geometry_type_list.append('TH')
50
55
  for geometry_type in geometry_type_list:
51
56
  gg.saveStrandPositions(geometry_type)
52
- if geometry_settings[geometry_type].with_iron_yoke:
53
- gg.constructIronGeometry(geometry_settings[geometry_type].symmetry if geometry_type == 'EM' else 'none')
57
+ if any(geometry_settings[geometry_type].areas):
58
+ gg.constructIronGeometry(geometry_settings[geometry_type].symmetry if geometry_type == 'EM' else 'none', geometry_settings[geometry_type], run_type = geometry_type)
54
59
  gg.constructCoilGeometry(geometry_type)
55
60
  if geometry_settings[geometry_type].with_wedges:
56
61
  gg.constructWedgeGeometry(geometry_settings[geometry_type].use_TSA if geometry_type == 'TH' else False)
@@ -60,9 +65,17 @@ class MainMultipole:
60
65
  gg.constructThinShells(geometry_settings[geometry_type].with_wedges)
61
66
  else:
62
67
  gg.constructInsulationGeometry()
68
+ if geometry_settings[geometry_type].use_TSA_new:
69
+ gg.constructAdditionalThinShells()
70
+
63
71
  gg.buildDomains(geometry_type, geometry_settings[geometry_type].symmetry if geometry_type == 'EM' else 'none')
64
72
  if geometry_type == 'EM':
65
73
  gg.fragment()
74
+ if geometry_type =='TH' and 'poles' in geometry_settings[geometry_type].areas:
75
+ # make sure geometry is connected
76
+ gmsh.model.occ.removeAllDuplicates()
77
+ gmsh.model.occ.synchronize()
78
+
66
79
  gg.saveBoundaryRepresentationFile(geometry_type)
67
80
  gg.loadBoundaryRepresentationFile(geometry_type)
68
81
  gg.updateTags(geometry_type, geometry_settings[geometry_type].symmetry if geometry_type == 'EM' else 'none')
@@ -91,29 +104,84 @@ class MainMultipole:
91
104
  gmsh.open(model_file + f'_{run_type}.brep')
92
105
 
93
106
  def mesh(self, gui: bool = False):
94
- mm = Mesh(data=self.fdm, mesh_folder=self.mesh_folder, verbose=self.verbose)
107
+ def _create_physical_group_for_reference(self):
108
+ """
109
+ This code generates the reference models for the FALCOND_C
110
+ """
111
+ ### hardcoded, we need only one reference
112
+ if self.fdm.general.magnet_name == 'TEST_MULTIPOLE_FALCOND_C_TSA_COLLAR_POLE':
113
+ CUT_REFERENCE = True # self specify loop and surf
114
+
115
+ L_col = [56, 57, 58, 35, 16, 49, 48, 47]
116
+ l1 = gmsh.model.occ.addLine(746,48)
117
+ l2 = gmsh.model.occ.addLine(41,691)
118
+ L_inner = list(range(854, 754-1, -1))
119
+ loop1 = gmsh.model.occ.addCurveLoop([l1] + L_col + [l2] + L_inner)
120
+
121
+ R_col = [52, 53, 54, 26, 6, 45, 44, 43]
122
+ r1 = gmsh.model.occ.addLine(38,902)
123
+ r2 = gmsh.model.occ.addLine(847,45)
124
+ R_inner = list(range(910, 1010+1))
125
+ loop2 = gmsh.model.occ.addCurveLoop([r2]+ R_col + [r1] + R_inner)
126
+
127
+ surf1 = gmsh.model.occ.addPlaneSurface([loop1])
128
+ surf2 = gmsh.model.occ.addPlaneSurface([loop2])
129
+ surf = [surf1, surf2] # tag
130
+
131
+ else: raise Exception("Reference meshing is not implemented for this magnet.")
132
+ gmsh.model.occ.synchronize()
133
+
134
+ #add to physical groups / domains -> write to aux file
135
+ file = os.path.join(self.geom_folder, self.fdm.general.magnet_name + '_TH.aux')
136
+ with open(file) as f:
137
+ lines = f.readlines()
138
+ updated_lines = []
139
+ flag = 0
140
+ for line in lines:
141
+ updated_lines.append(line)
142
+ if flag == 0 and 'groups_entities:' in line:
143
+ flag = 1
144
+ if flag==1 and ('ref_mesh: {}' in line):
145
+ if type(surf) == list:
146
+ updated_lines[-1] = f' ref_mesh:\n {self.fdm.magnet.solve.thermal.insulation_TSA.between_collar.material}: {surf}\n'
147
+ else:
148
+ updated_lines[-1] = f' ref_mesh:\n {self.fdm.magnet.solve.thermal.insulation_TSA.between_collar.material}: [{surf}]\n'
149
+ #if not CUT_REFERENCE else f' ref_mesh:\n {self.fdm.magnet.solve.thermal.insulation_TSA.between_collar.material}: '+ str(surf) +'\n'
150
+ flag = -1
151
+
152
+ with open(file, 'w') as f:
153
+ f.writelines(updated_lines)
154
+ logger = logging.getLogger('FiQuS')
155
+ logger.warning("Overwrite the .aux file to include the new domain")
156
+
157
+ mm = Mesh(data=self.fdm, mesh_folder=self.mesh_folder, verbose=self.verbose) ## same mesh object is used for both thermal and EM
95
158
  geom = Util.read_data_from_yaml(self.rgd, FiQuSGeometry)
96
159
  fdm = self.force_symmetry() if 'solenoid' in geom.Roxie_Data.coil.coils[1].type else self.fdm
97
160
  geometry_settings = {'EM': fdm.magnet.geometry.electromagnetics, 'TH': self.fdm.magnet.geometry.thermal}
98
161
  mesh_settings = {'EM': fdm.magnet.mesh.electromagnetics, 'TH': fdm.magnet.mesh.thermal}
162
+
99
163
  mesh_type_list = []
100
- if fdm.magnet.mesh.electromagnetics.create: mesh_type_list.append('EM')
101
- if fdm.magnet.mesh.thermal.create: mesh_type_list.append('TH')
164
+ if mesh_settings['EM'].create: mesh_type_list.append('EM')
165
+ if mesh_settings['TH'].create: mesh_type_list.append('TH')
102
166
  for physics_solved in mesh_type_list:
103
167
  self.load_geometry_for_mesh(physics_solved)
104
- if physics_solved == 'TH' and self.fdm.magnet.geometry.thermal.use_TSA:
105
- mm.loadStrandPositions(physics_solved)
168
+ if physics_solved == 'TH' and mesh_settings['TH'].reference.enabled and 'collar' in geometry_settings['TH'].areas:
169
+ if self.fdm.magnet.geometry.thermal.use_TSA_new or self.fdm.magnet.geometry.thermal.use_TSA:
170
+ raise Exception('Reference solution is not implemented for collar with TSA')
171
+ if 'iron_yoke' in geometry_settings['TH'].areas:
172
+ raise Exception('Reference solution is intended (read: hardcoded) without iron yoke')
173
+ _create_physical_group_for_reference(self)
106
174
  mm.loadAuxiliaryFile(physics_solved)
107
- if geometry_settings[physics_solved].with_iron_yoke:
108
- mm.getIronCurvesTags()
175
+ if any(geometry_settings[physics_solved].areas):
176
+ mm.getIronCurvesTags(physics_solved)
109
177
  mm.defineMesh(geometry_settings[physics_solved], mesh_settings[physics_solved], physics_solved)
110
178
  mm.createPhysicalGroups(geometry_settings[physics_solved])
111
179
  mm.updateAuxiliaryFile(physics_solved)
112
180
  if geometry_settings[physics_solved].model_dump().get('use_TSA', False):
113
- mm.rearrangeThinShellsData()
181
+ mm.rearrangeThinShellsData() # rearrange data for the pro file, technically optional
114
182
  mm.assignRegionsTags(geometry_settings[physics_solved], mesh_settings[physics_solved])
115
183
  mm.saveRegionFile(physics_solved)
116
- mm.setMeshOptions(physics_solved)
184
+ mm.setMeshOptions()
117
185
  mm.generateMesh()
118
186
  mm.checkMeshQuality()
119
187
  mm.saveMeshFile(physics_solved)
@@ -121,7 +189,10 @@ class MainMultipole:
121
189
  mm.saveClosestNeighboursList()
122
190
  if self.fdm.magnet.mesh.thermal.isothermal_conductors: mm.selectMeshNodes(elements='conductors')
123
191
  if self.fdm.magnet.geometry.thermal.with_wedges and self.fdm.magnet.mesh.thermal.isothermal_wedges: mm.selectMeshNodes(elements='wedges')
124
- mm.saveRegionCoordinateFile(physics_solved)
192
+ if geometry_settings[physics_solved].model_dump().get('use_TSA_new', False):
193
+ mm.saveClosestNeighboursList_new_TSA()
194
+ mm.saveHalfTurnCornerPositions()
195
+ mm.saveRegionCoordinateFile(physics_solved)
125
196
  mm.clear()
126
197
  mm.ending_step(gui)
127
198
  return mm.mesh_parameters
@@ -134,16 +205,37 @@ class MainMultipole:
134
205
 
135
206
  def solve_and_postprocess_getdp(self, gui: bool = False):
136
207
  an = AssignNaming(data=self.fdm)
137
- rg = RunGetdpMultipole(data=an, solution_folder=self.solution_folder, GetDP_path=self.GetDP_path, verbose=self.verbose)
208
+ rg = RunGetdpMultipole(data=an, solution_folder=self.solution_folder, GetDP_path=self.GetDP_path,
209
+ verbose=self.verbose)
210
+ rg.loadRegionFiles()
211
+ if self.fdm.magnet.solve.thermal.solve_type and self.fdm.magnet.geometry.thermal.use_TSA:
212
+ rg.loadRegionCoordinateFile()
213
+ rg.assemblePro()
214
+ start_time = time.time()
215
+ rg.solve_and_postprocess()
216
+ rg.ending_step(gui)
217
+ return time.time() - start_time
218
+
219
+ def solve_and_postprocess_getdp(self, gui: bool = False):
220
+ an = AssignNaming(data=self.fdm)
221
+ rg = RunGetdpMultipole(data=an, solution_folder=self.solution_folder, GetDP_path=self.GetDP_path,
222
+ verbose=self.verbose)
138
223
  rg.loadRegionFiles()
139
224
  if self.fdm.magnet.solve.thermal.solve_type and self.fdm.magnet.geometry.thermal.use_TSA:
140
225
  rg.loadRegionCoordinateFile()
226
+ rg.read_aux_file(os.path.join(self.mesh_folder, f"{self.fdm.general.magnet_name}_EM.aux"))
227
+ rg.extract_half_turn_blocks()
228
+ if self.fdm.magnet.geometry.thermal.use_TSA_new:
229
+ rg.read_aux_file(os.path.join(self.mesh_folder,
230
+ f"{self.fdm.general.magnet_name}_TH.aux")) # now load the thermal aux file
231
+ rg.extract_specific_TSA_lines()
141
232
  rg.assemblePro()
142
233
  start_time = time.time()
143
234
  rg.solve_and_postprocess()
144
235
  rg.ending_step(gui)
145
236
  return time.time() - start_time
146
237
 
238
+
147
239
  def post_process_getdp(self, gui: bool = False):
148
240
  an = AssignNaming(data=self.fdm)
149
241
  rg = RunGetdpMultipole(data=an, solution_folder=self.solution_folder, GetDP_path=self.GetDP_path, verbose=self.verbose)