fiqus 2025.11.0__py3-none-any.whl → 2025.12.0__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 (27) hide show
  1. fiqus/MainFiQuS.py +6 -0
  2. fiqus/data/DataConductor.py +9 -1
  3. fiqus/data/DataFiQuS.py +3 -3
  4. fiqus/data/DataFiQuSConductorAC_Rutherford.py +569 -0
  5. fiqus/data/DataFiQuSHomogenizedConductor.py +478 -0
  6. fiqus/geom_generators/GeometryConductorAC_Rutherford.py +706 -0
  7. fiqus/geom_generators/GeometryConductorAC_Strand_RutherfordCopy.py +1848 -0
  8. fiqus/geom_generators/GeometryHomogenizedConductor.py +183 -0
  9. fiqus/getdp_runners/RunGetdpConductorAC_Rutherford.py +200 -0
  10. fiqus/getdp_runners/RunGetdpHomogenizedConductor.py +178 -0
  11. fiqus/mains/MainConductorAC_Rutherford.py +76 -0
  12. fiqus/mains/MainHomogenizedConductor.py +112 -0
  13. fiqus/mesh_generators/MeshConductorAC_Rutherford.py +235 -0
  14. fiqus/mesh_generators/MeshConductorAC_Strand_RutherfordCopy.py +718 -0
  15. fiqus/mesh_generators/MeshHomogenizedConductor.py +229 -0
  16. fiqus/post_processors/PostProcessAC_Rutherford.py +142 -0
  17. fiqus/post_processors/PostProcessHomogenizedConductor.py +114 -0
  18. fiqus/pro_templates/combined/ConductorACRutherford_template.pro +1742 -0
  19. fiqus/pro_templates/combined/HomogenizedConductor_template.pro +1663 -0
  20. {fiqus-2025.11.0.dist-info → fiqus-2025.12.0.dist-info}/METADATA +6 -5
  21. {fiqus-2025.11.0.dist-info → fiqus-2025.12.0.dist-info}/RECORD +27 -11
  22. tests/test_geometry_generators.py +20 -0
  23. tests/test_mesh_generators.py +38 -0
  24. tests/test_solvers.py +100 -0
  25. {fiqus-2025.11.0.dist-info → fiqus-2025.12.0.dist-info}/LICENSE.txt +0 -0
  26. {fiqus-2025.11.0.dist-info → fiqus-2025.12.0.dist-info}/WHEEL +0 -0
  27. {fiqus-2025.11.0.dist-info → fiqus-2025.12.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,183 @@
1
+ import os, json, gmsh, logging
2
+
3
+ from fiqus.utils.Utils import GmshUtils
4
+ from typing import Tuple
5
+
6
+ logger = logging.getLogger('FiQuS')
7
+
8
+ class Rectangle:
9
+ """ A class to represent a rectangular surface in gmsh. """
10
+ def __init__( self, x_center, y_center, z_center, width, height, name) -> None:
11
+ self.name = name
12
+
13
+ self.x_center = x_center
14
+ self.y_center = y_center
15
+ self.z_center = z_center
16
+ self.width = width
17
+ self.height = height
18
+
19
+ self.tag = gmsh.model.occ.addRectangle(x_center-width/2, y_center-height/2, z_center, width, height)
20
+ self.dimTag: Tuple = (2, self.tag)
21
+
22
+
23
+ class Circle:
24
+ """ A class to represent a circle surface in gmsh. """
25
+ def __init__(self, x_center, y_center, z_center, radius, name) -> None:
26
+ self.name = name
27
+
28
+ self.x_center = x_center
29
+ self.y_center = y_center
30
+ self.z_center = z_center
31
+ self.radius = radius
32
+
33
+ self.tag = gmsh.model.occ.addDisk(self.x_center, self.y_center, self.z_center, radius, radius)
34
+ self.dimTag: Tuple = (2, self.tag)
35
+
36
+
37
+ class HomogenizedConductor:
38
+ """
39
+ A class representing the geometry of a 2D cross section containing multiple cables and excitation coils in an isolating medium.
40
+
41
+ :ivar cables: list of surfaces representing the cable cross sections.
42
+ :vartype cables: list[list[Surface]]
43
+ :ivar excitation_coils: list of surfaces representing the excitation coils cross sections.
44
+ :vartype excitation_coils: list[list[Surface]]
45
+ :ivar Air: Surrounding surface representing the air region.
46
+ :vartype Air: Surface
47
+ """
48
+ def __init__(self, fdm) -> None:
49
+ """
50
+ Initializes the HomogenizedConductor object.
51
+ """
52
+ self.fdm = fdm
53
+
54
+ self.cables = self.generate_cables()
55
+ self.excitation_coils = self.generate_excitation_coils()
56
+ self.air = self.generate_air()
57
+
58
+ def generate_cables(self):
59
+ """
60
+ This function generates a list of rectangular surfaces according to the specifications of the cables within the yaml.
61
+
62
+ :return: List of rectangle surfaces
63
+ """
64
+ cables = []
65
+ for idx, cable in enumerate(self.fdm.magnet.geometry.cables_definition):
66
+ cables.append(Rectangle(cable.center_position[0],cable.center_position[1],cable.center_position[2],
67
+ cable.width, cable.height, name='Cable'+str(idx+1)))
68
+ return cables
69
+
70
+ def generate_excitation_coils(self):
71
+ """
72
+ This function generates a list of rectangular surfaces according to the specifications of the excitation coils within the yaml.
73
+
74
+ :return: List of rectangle surfaces
75
+ """
76
+ excitation_coils = []
77
+ for idx, excitation_coil in enumerate(self.fdm.magnet.geometry.excitation_coils):
78
+ excitation_coils.append(Rectangle(excitation_coil.center_position[0],excitation_coil.center_position[1],excitation_coil.center_position[2],
79
+ excitation_coil.width, excitation_coil.height, name='Coil'+str(idx+1)))
80
+ return excitation_coils
81
+
82
+ def generate_air(self):
83
+ """
84
+ This function generates the surrounding air surface by fragmenting a base surface with the previously defined cable surfaces.
85
+
86
+ :raises ValueError: 'circle' is the only defined air surface form
87
+ :return: fragmented air surface
88
+ """
89
+ if self.fdm.magnet.geometry.air_form == 'circle':
90
+ air_pos = self.fdm.magnet.geometry.air.center_position
91
+ air = Circle(air_pos[0],air_pos[1],air_pos[2],
92
+ self.fdm.magnet.geometry.air.radius, name='Air')
93
+ else:
94
+ raise ValueError('Undefined air_form.')
95
+ # fragment air with cables and excitation coils
96
+ gmsh.model.occ.synchronize()
97
+ outDimTags, outDimTagsMap = gmsh.model.occ.fragment([(2, air.tag)], [cable.dimTag for cable in self.cables] + [excitation_coil.dimTag for excitation_coil in self.excitation_coils])
98
+ return air
99
+
100
+ def get_vi_dictionary(self):
101
+ """
102
+ This function dumps the dimTags of all surfaces into one dictionary, which can be used to generate the volume information file.
103
+
104
+ :return: dictionary with volume information
105
+ """
106
+ vi_dictionary = {}
107
+ # add all cables
108
+ for cable in self.cables:
109
+ vi_dictionary.update({str(cable.name):list(cable.dimTag)})
110
+ # add all excitation coils
111
+ for excitation_coil in self.excitation_coils:
112
+ vi_dictionary.update({str(excitation_coil.name):list(excitation_coil.dimTag)})
113
+ # add air
114
+ vi_dictionary.update({str(self.air.name):list(self.air.dimTag)})
115
+ return vi_dictionary
116
+
117
+
118
+ class Geometry:
119
+ def __init__(self, fdm, inputs_folder_path, verbose=True) -> None:
120
+ """
121
+ Initializes the Geometry class.
122
+
123
+ :param fdm: The fiqus data model.
124
+ :type fdm: object
125
+ :param inputs_folder_path: The full path to the folder with input files, i.e., conductor and STEP files.
126
+ :type inputs_folder_path: str
127
+ :param verbose: If True, more information is printed in the Python console. Defaults to True.
128
+ :type verbose: bool, optional
129
+ """
130
+ self.fdm = fdm
131
+ self.inputs_folder = inputs_folder_path
132
+ self.geom_folder = os.path.join(os.getcwd())
133
+ self.geom_file = os.path.join(self.geom_folder, f'{fdm.general.magnet_name}.brep')
134
+ self.vi_file = os.path.join(self.geom_folder, f'{fdm.general.magnet_name}.vi')
135
+ self.verbose = verbose
136
+ # start GMSH
137
+ self.gu = GmshUtils(self.geom_folder, self.verbose)
138
+ self.gu.initialize(verbosity_Gmsh=fdm.run.verbosity_Gmsh)
139
+ # To see the surfaces in a better way in GUI:
140
+ gmsh.option.setNumber("Geometry.SurfaceType", 2)
141
+
142
+ def generate_geometry(self):
143
+ """
144
+ Generates the geometry of conductors within a surrounding air region as specified in the yaml.
145
+
146
+ :param gui: If True, launches an interactive GUI after generating the geometry. Default is False.
147
+ :type gui: bool, optional
148
+ :return: None
149
+ """
150
+ logger.info("Generating geometry")
151
+
152
+ # 1) Either load the geometry from a yaml file or create the model from scratch
153
+ self.geometry = HomogenizedConductor(fdm=self.fdm)
154
+
155
+ gmsh.model.occ.synchronize()
156
+ logger.info("Writing geometry")
157
+ gmsh.write(self.geom_file) # Write the geometry to a .brep file
158
+
159
+ def load_geometry(self):
160
+ """ Loads geometry from .brep file. """
161
+ logger.info("Loading geometry")
162
+
163
+ gmsh.clear()
164
+ gmsh.model.occ.importShapes(self.geom_file, format="brep")
165
+ gmsh.model.occ.synchronize()
166
+
167
+ def generate_vi_file(self, gui=False):
168
+ """
169
+ Generates volume information file. Volume information file stores dimTags of all
170
+ the stored volumes. Since this model is 2D, those volumes are equivalent to simple surfaces.
171
+ """
172
+
173
+ dimTagsDict = self.geometry.get_vi_dictionary()
174
+ with open(self.vi_file, 'w') as f:
175
+ json.dump(dimTagsDict, f)
176
+
177
+ if gui:
178
+ #self.generate_physical_groups()
179
+ self.gu.launch_interactive_GUI()
180
+ else:
181
+ if gmsh.isInitialized():
182
+ gmsh.clear()
183
+ gmsh.finalize()
@@ -0,0 +1,200 @@
1
+ import timeit
2
+ import json
3
+ import logging
4
+ import math
5
+ from enum import Enum
6
+ import operator
7
+ import itertools
8
+ import os
9
+ import pickle
10
+ import subprocess
11
+ import re
12
+ import pandas as pd
13
+
14
+ import gmsh
15
+ import numpy as np
16
+
17
+ from fiqus.data import RegionsModelFiQuS
18
+ from fiqus.utils.Utils import GmshUtils, FilesAndFolders
19
+ from fiqus.data.RegionsModelFiQuS import RegionsModel
20
+ # import fiqus.data.DataConductorACGeom as geom
21
+
22
+ from fiqus.pro_assemblers.ProAssembler import ASS_PRO
23
+
24
+ logger = logging.getLogger('FiQuS')
25
+
26
+ class Solve:
27
+ def __init__(self, fdm, GetDP_path, geometry_folder, mesh_folder, verbose=True):
28
+ self.fdm = fdm
29
+ self.cacdm = fdm.magnet
30
+ self.GetDP_path = GetDP_path
31
+ self.solution_folder = os.path.join(os.getcwd())
32
+ self.magnet_name = fdm.general.magnet_name
33
+ self.geometry_folder = geometry_folder
34
+ self.mesh_folder = mesh_folder
35
+ self.mesh_file = os.path.join(self.mesh_folder, f"{self.magnet_name}.msh")
36
+ self.pro_file = os.path.join(self.solution_folder, f"{self.magnet_name}.pro")
37
+ self.regions_file = os.path.join(mesh_folder, f"{self.magnet_name}.regions")
38
+
39
+ self.verbose = verbose
40
+ self.gu = GmshUtils(self.solution_folder, self.verbose)
41
+ self.gu.initialize(verbosity_Gmsh=fdm.run.verbosity_Gmsh)
42
+
43
+ self.ass_pro = ASS_PRO(os.path.join(self.solution_folder, self.magnet_name))
44
+ self.regions_model = FilesAndFolders.read_data_from_yaml(self.regions_file, RegionsModel)
45
+ self.material_properties_model = None
46
+
47
+ self.ed = {} # excitation dictionary
48
+
49
+ gmsh.option.setNumber("General.Terminal", verbose)
50
+
51
+ def read_excitation(self, inputs_folder_path):
52
+ """
53
+ Function for reading csv for the 'from_file' excitation case
54
+ :type inputs_folder_path: str
55
+ """
56
+ # if self.cacdm.solve.source_parameters.source_type == 'from_file':
57
+ # input_file = os.path.join(inputs_folder_path, self.cacdm.solve.source_parameters.source_csv_file)
58
+ # logger.info(f'Getting applied field and transport current from file: {input_file}')
59
+ # df = pd.read_csv(input_file, delimiter=',', engine='python')
60
+ # excitation_time = df['time'].to_numpy(dtype='float').tolist()
61
+ # self.ed['time'] = excitation_time
62
+ # excitation_b = df['b'].to_numpy(dtype='float').tolist()
63
+ # self.ed['b'] = excitation_b
64
+ # excitation_I = df['I'].to_numpy(dtype='float').tolist()
65
+ # self.ed['I'] = excitation_I
66
+
67
+ if self.cacdm.solve.source_parameters.excitation_coils.enable and self.cacdm.solve.source_parameters.excitation_coils.source_csv_file:
68
+ input_file = os.path.join(inputs_folder_path, self.cacdm.solve.source_parameters.excitation_coils.source_csv_file)
69
+ logger.info(f'Getting excitation coils currents from file: {input_file}')
70
+ df = pd.read_csv(input_file, delimiter=',', engine='python')
71
+
72
+ if( len(df.columns) != len(self.cacdm.geometry.excitation_coils.centers)+1):
73
+ logger.warning('Number of excitation coils in geometry ('+ str(len(self.cacdm.geometry.excitation_coils.centers))+') and input source file ('+str(len(df.columns))+') not compatible')
74
+
75
+ excitation_time = df['time'].to_numpy(dtype='float').tolist()
76
+ self.ed['time'] = excitation_time
77
+ for i in range(1, len(df.columns)):
78
+ Istr = 'I'+str(i)
79
+ excitation_value = df[Istr].to_numpy(dtype='float').tolist()
80
+ self.ed[Istr] = excitation_value
81
+
82
+ # def get_material_properties(self, inputs_folder_path):
83
+ # """
84
+ # Function for reading material properties from the geometry YAML file.
85
+ # This reads the 'solution' section of the YAML file and stores it in the solution folder.
86
+ # This could also be a place to change the material properties in the future.
87
+ # """
88
+ # if self.cacdm.geometry.io_settings.load.load_from_yaml:
89
+ # input_yaml_file = os.path.join(inputs_folder_path, self.cacdm.geometry.io_settings.load.filename)
90
+ # Conductor_dm = FilesAndFolders.read_data_from_yaml(input_yaml_file, geom.Conductor)
91
+ # solution_parameters = Conductor_dm.Solution
92
+ # FilesAndFolders.write_data_to_yaml(os.path.join(self.solution_folder, "MaterialProperties.yaml"), solution_parameters.dict())
93
+ # self.material_properties_model = solution_parameters
94
+
95
+
96
+
97
+
98
+ def assemble_pro(self):
99
+ logger.info("Assembling .pro file")
100
+ self.ass_pro.assemble_combined_pro(template = self.cacdm.solve.pro_template, rm = self.regions_model, dm = self.fdm, ed=self.ed, mp=self.material_properties_model)
101
+
102
+ def run_getdp(self, solve = True, postOperation = True, gui = False):
103
+
104
+ # f = self.cacdm.solve.source_parameters.frequency
105
+ # bmax = self.cacdm.solve.source_parameters.applied_field_amplitude
106
+ # Imax_ratio = self.cacdm.solve.source_parameters.ratio_of_max_imposed_current_amplitude
107
+
108
+ command = ["-v2", "-verbose", "3"]
109
+ if solve:
110
+ # command += ["-solve", "js_to_hs_2"] if not self.cacdm.solve.frequency_domain_solver.enable else ["-solve", "MagDyn_freq"] # for debugging purposes
111
+ command += ["-solve", "MagDyn"] if not self.cacdm.solve.frequency_domain_solver.enable else ["-solve", "MagDyn_freq"]
112
+ # Solve_only seems to always call postproc, here we only save .pos-files if specified in input file
113
+ # if (postOperation and not solve) or (solve and postOperation and self.cacdm.postproc.generate_pos_files):
114
+ # if self.cacdm.solve.formulation_parameters.dynamic_correction:
115
+ # command += " -pos MagDyn MagDyn_dynCorr"
116
+ # else:
117
+ command += ["-pos", "MagDyn"]
118
+
119
+ logger.info(f"Running GetDP with command: {command}")
120
+ startTime = timeit.default_timer()
121
+ # subprocess.run(f"{self.GetDP_path} {self.pro_file} {command} -msh {self.mesh_file}")
122
+
123
+ if self.cacdm.solve.general_parameters.noOfMPITasks:
124
+ mpi_prefix = ["mpiexec", "-np", str(self.cacdm.solve.general_parameters.noOfMPITasks)]
125
+ else:
126
+ mpi_prefix = []
127
+
128
+ getdpProcess = subprocess.Popen(mpi_prefix + [self.GetDP_path] + [self.pro_file] + command + ["-msh"] + [self.mesh_file], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
129
+
130
+ with getdpProcess.stdout:
131
+ for line in iter(getdpProcess.stdout.readline, b""):
132
+ line = line.decode("utf-8").rstrip()
133
+ line = line.split("\r")[-1]
134
+ if not "Test" in line:
135
+ if line.startswith("Info"):
136
+ parsedLine = re.sub(r"Info\s+:\s+", "", line)
137
+ logger.info(parsedLine)
138
+ elif line.startswith("Warning"):
139
+ parsedLine = re.sub(r"Warning\s+:\s+", "", line)
140
+ logger.warning(parsedLine)
141
+ elif line.startswith("Error"):
142
+ parsedLine = re.sub(r"Error\s+:\s+", "", line)
143
+ logger.error(parsedLine)
144
+ logger.error("Solving CAC failed.")
145
+ # raise Exception(parsedLine)
146
+ elif re.match("##", line):
147
+ logger.critical(line)
148
+ else:
149
+ logger.info(line)
150
+
151
+ simulation_time = timeit.default_timer()-startTime
152
+ # Save simulation time:
153
+ if solve:
154
+ logger.info(f"Solving Rutherford has finished in {round(simulation_time, 3)} seconds.")
155
+ with open(self.solution_folder+f'\\txt_files\\simulation_time.txt', 'w') as file:
156
+ file.write(str(simulation_time))
157
+
158
+
159
+ if gui and ((postOperation and not solve) or (solve and postOperation and self.cacdm.postproc.generate_pos_files)):
160
+ # gmsh.option.setNumber("Geometry.Volumes", 1)
161
+ # gmsh.option.setNumber("Geometry.Surfaces", 1)
162
+ # gmsh.option.setNumber("Geometry.Curves", 1)
163
+ # gmsh.option.setNumber("Geometry.Points", 0)
164
+ posFiles = [
165
+ fileName
166
+ for fileName in os.listdir(self.solution_folder)
167
+ if fileName.endswith(".pos")
168
+ ]
169
+ for posFile in posFiles:
170
+ gmsh.open(os.path.join(self.solution_folder, posFile))
171
+ self.gu.launch_interactive_GUI()
172
+ else:
173
+ if gmsh.isInitialized():
174
+ gmsh.clear()
175
+ gmsh.finalize()
176
+
177
+ def cleanup(self):
178
+ """
179
+ This funtion is used to remove .pre and .res files from the solution folder, as they may be large and not needed.
180
+ """
181
+ magnet_name = self.fdm.general.magnet_name
182
+ cleanup = self.cacdm.postproc.cleanup
183
+
184
+ if cleanup.remove_res_file:
185
+ res_file_path = os.path.join(self.solution_folder, f"{magnet_name}.res")
186
+ if os.path.exists(res_file_path):
187
+ os.remove(res_file_path)
188
+ logger.info(f"Removed {magnet_name}.res")
189
+
190
+ if cleanup.remove_pre_file:
191
+ pre_file_path = os.path.join(self.solution_folder, f"{magnet_name}.pre")
192
+ if os.path.exists(pre_file_path):
193
+ os.remove(pre_file_path)
194
+ logger.info(f"Removed {magnet_name}.pre")
195
+
196
+ if cleanup.remove_msh_file:
197
+ msh_file_path = os.path.join(self.mesh_folder, f"{magnet_name}.msh")
198
+ if os.path.exists(msh_file_path):
199
+ os.remove(msh_file_path)
200
+ logger.info(f"Removed {magnet_name}.msh")
@@ -0,0 +1,178 @@
1
+ import os, re, subprocess, logging, timeit
2
+ import pandas as pd
3
+ import gmsh
4
+
5
+ from fiqus.data.DataFiQuSConductor import Conductor, SolutionParameters
6
+ from fiqus.data.RegionsModelFiQuS import RegionsModel
7
+ from fiqus.utils.Utils import GmshUtils, FilesAndFolders
8
+
9
+ from fiqus.pro_assemblers.ProAssembler import ASS_PRO
10
+
11
+ logger = logging.getLogger('FiQuS')
12
+
13
+ class Solve:
14
+ def __init__(self, fdm, GetDP_path, geometry_folder, mesh_folder, verbose=True):
15
+ self.fdm = fdm
16
+ self.GetDP_path = GetDP_path
17
+
18
+ self.solution_folder = os.path.join(os.getcwd())
19
+ self.geometry_folder = geometry_folder
20
+ self.mesh_folder = mesh_folder
21
+
22
+ self.mesh_file = os.path.join(self.mesh_folder, f"{self.fdm.general.magnet_name}.msh")
23
+ self.pro_file = os.path.join(self.solution_folder, f"{self.fdm.general.magnet_name}.pro")
24
+ self.regions_file = os.path.join(mesh_folder, f"{self.fdm.general.magnet_name}.regions")
25
+
26
+ self.verbose = verbose
27
+ self.gu = GmshUtils(self.solution_folder, self.verbose)
28
+ self.gu.initialize(verbosity_Gmsh=fdm.run.verbosity_Gmsh)
29
+
30
+ self.ass_pro = ASS_PRO(os.path.join(self.solution_folder, self.fdm.general.magnet_name))
31
+ self.regions_model = FilesAndFolders.read_data_from_yaml(self.regions_file, RegionsModel)
32
+ self.material_properties_model = None
33
+
34
+ self.ed = {} # excitation dictionary
35
+ self.rohm = {} # rohm parameter dictionary
36
+ self.rohf = {} # rohf parameter dictionary
37
+
38
+ gmsh.option.setNumber("General.Terminal", verbose)
39
+
40
+ def read_ro_parameters(self, inputs_folder_path):
41
+ """
42
+ Function for reading the CSV files containing the reduced order parameters for ROHF and ROHM.
43
+ The function expects CSV files with an descriptive first header row (i.e. 'alphas,kappas,taus') and following rows with comma seperated parameter values for each cell starting from cell 1.
44
+
45
+ :param inputs_folder_path: The full path to the folder with input files
46
+ """
47
+ class reduced_order_params:
48
+ " This structure is used to access the parameter dictionaries inside the pro template with a convenient syntax 'mp.rohf' or 'mp.rohm'."
49
+ def __init__(self, rohf_dict=None, rohm_dict=None):
50
+ self.rohf = rohf_dict
51
+ self.rohm = rohm_dict
52
+
53
+ if self.fdm.magnet.solve.rohm.enable and self.fdm.magnet.solve.rohm.parameter_csv_file:
54
+ parameter_file = os.path.join(inputs_folder_path, self.fdm.magnet.solve.rohm.parameter_csv_file)
55
+ df = pd.read_csv(parameter_file, delimiter=',', engine='python')
56
+ df.index += 1
57
+ logger.info(f'Using ROHM parameters from file: {parameter_file}:')
58
+ logger.info(df)
59
+ self.rohm = df.to_dict(orient='dict')
60
+
61
+ if self.fdm.magnet.solve.rohf.enable and self.fdm.magnet.solve.rohf.parameter_csv_file:
62
+ parameter_file = os.path.join(inputs_folder_path, self.fdm.magnet.solve.rohf.parameter_csv_file)
63
+ df = pd.read_csv(parameter_file, delimiter=',', engine='python')
64
+ df.index += 1
65
+ logger.info(f'Using ROHF parameters from file: {parameter_file}:')
66
+ logger.info(df)
67
+ self.rohf = df.to_dict(orient='dict')
68
+
69
+ # For the moment the reduced order parameters are treated as material properties mp structure.
70
+ self.material_properties_model = reduced_order_params(rohf_dict=self.rohf, rohm_dict=self.rohm)
71
+
72
+ def read_excitation(self, inputs_folder_path):
73
+ """
74
+ Function for reading a CSV file for the 'from_file' excitation case.
75
+
76
+ :param inputs_folder_path: The full path to the folder with input files.
77
+ :type inputs_folder_path: str
78
+ """
79
+ if self.fdm.magnet.solve.source_parameters.source_type == 'piecewise' and self.fdm.magnet.solve.source_parameters.piecewise.source_csv_file:
80
+ input_file = os.path.join(inputs_folder_path, self.fdm.magnet.solve.source_parameters.piecewise.source_csv_file)
81
+ logger.info(f'Using excitation from file: {input_file}')
82
+ df = pd.read_csv(input_file, delimiter=',', engine='python')
83
+ excitation_time = df['time'].to_numpy(dtype='float').tolist()
84
+ self.ed['time'] = excitation_time
85
+ excitation_value = df['value'].to_numpy(dtype='float').tolist()
86
+ self.ed['value'] = excitation_value
87
+
88
+ def assemble_pro(self):
89
+ logger.info("Assembling .pro file")
90
+ self.ass_pro.assemble_combined_pro(template = self.fdm.magnet.solve.pro_template, rm = self.regions_model, dm = self.fdm, ed=self.ed, mp=self.material_properties_model)
91
+
92
+ def run_getdp(self, solve = True, postOperation = True, gui = False):
93
+
94
+ command = ["-v2", "-verbose", "3", "-mat_mumps_icntl_14","100"]
95
+ if solve:
96
+ command += ["-solve", "MagDyn"]
97
+ command += ["-pos", "MagDyn"]
98
+
99
+ if self.fdm.magnet.solve.general_parameters.noOfMPITasks:
100
+ mpi_prefix = ["mpiexec", "-np", str(self.fdm.magnet.solve.general_parameters.noOfMPITasks)]
101
+ else:
102
+ mpi_prefix = []
103
+
104
+ startTime = timeit.default_timer()
105
+
106
+ getdpProcess = subprocess.Popen(mpi_prefix + [self.GetDP_path, self.pro_file, "-msh", self.mesh_file] + command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
107
+
108
+ with getdpProcess.stdout:
109
+ for line in iter(getdpProcess.stdout.readline, b""):
110
+ line = line.decode("utf-8").rstrip()
111
+ line = line.split("\r")[-1]
112
+ if not "Test" in line:
113
+ if line.startswith("Info"):
114
+ parsedLine = re.sub(r"Info\s+:\s+", "", line)
115
+ logger.info(parsedLine)
116
+ elif line.startswith("Warning"):
117
+ parsedLine = re.sub(r"Warning\s+:\s+", "", line)
118
+ logger.warning(parsedLine)
119
+ elif line.startswith("Error"):
120
+ parsedLine = re.sub(r"Error\s+:\s+", "", line)
121
+ logger.error(parsedLine)
122
+ logger.error("Solving HomogenizedConductor failed.")
123
+ # raise Exception(parsedLine)
124
+ elif re.match("##", line):
125
+ logger.critical(line)
126
+ else:
127
+ logger.info(line)
128
+
129
+ simulation_time = timeit.default_timer()-startTime
130
+ # Save simulation time:
131
+ if solve:
132
+ logger.info(f"Solving HomogenizedConductor has finished in {round(simulation_time, 3)} seconds.")
133
+ with open(os.path.join(self.solution_folder, 'txt_files', 'simulation_time.txt'), 'w') as file:
134
+ file.write(str(simulation_time))
135
+
136
+
137
+ if gui and ((postOperation and not solve) or (solve and postOperation and self.fdm.magnet.postproc.generate_pos_files)):
138
+ # gmsh.option.setNumber("Geometry.Volumes", 1)
139
+ # gmsh.option.setNumber("Geometry.Surfaces", 1)
140
+ # gmsh.option.setNumber("Geometry.Curves", 1)
141
+ # gmsh.option.setNumber("Geometry.Points", 0)
142
+ posFiles = [
143
+ fileName
144
+ for fileName in os.listdir(self.solution_folder)
145
+ if fileName.endswith(".pos")
146
+ ]
147
+ for posFile in reversed(posFiles):
148
+ gmsh.open(os.path.join(self.solution_folder, posFile))
149
+ self.gu.launch_interactive_GUI()
150
+ else:
151
+ if gmsh.isInitialized():
152
+ gmsh.clear()
153
+ gmsh.finalize()
154
+
155
+ def cleanup(self):
156
+ """
157
+ This funtion is used to remove .msh, .pre and .res files from the solution folder, as they may be large and not needed.
158
+ """
159
+ magnet_name = self.fdm.general.magnet_name
160
+ cleanup = self.fdm.magnet.postproc.cleanup
161
+
162
+ if cleanup.remove_res_file:
163
+ res_file_path = os.path.join(self.solution_folder, f"{magnet_name}.res")
164
+ if os.path.exists(res_file_path):
165
+ os.remove(res_file_path)
166
+ logger.info(f"Removed {magnet_name}.res")
167
+
168
+ if cleanup.remove_pre_file:
169
+ pre_file_path = os.path.join(self.solution_folder, f"{magnet_name}.pre")
170
+ if os.path.exists(pre_file_path):
171
+ os.remove(pre_file_path)
172
+ logger.info(f"Removed {magnet_name}.pre")
173
+
174
+ if cleanup.remove_msh_file:
175
+ msh_file_path = os.path.join(self.mesh_folder, f"{magnet_name}.msh")
176
+ if os.path.exists(msh_file_path):
177
+ os.remove(msh_file_path)
178
+ logger.info(f"Removed {magnet_name}.msh")
@@ -0,0 +1,76 @@
1
+ import math
2
+ import os
3
+
4
+ from fiqus.geom_generators.GeometryConductorAC_Rutherford import Geometry
5
+ from fiqus.mesh_generators.MeshConductorAC_Rutherford import CableMesh
6
+ from fiqus.getdp_runners.RunGetdpConductorAC_Rutherford import Solve
7
+ from fiqus.post_processors.PostProcessAC_Rutherford import PostProcess
8
+ from fiqus.plotters.PlotPythonConductorAC import PlotPythonConductorAC
9
+
10
+ from fiqus.mains.MainConductorAC_Strand import MainConductorAC_Strand
11
+
12
+ class MainConductorAC_Rutherford(MainConductorAC_Strand):
13
+
14
+ def generate_geometry(self, gui=False):
15
+ """
16
+ Generates the cable geometry.
17
+ """
18
+ os.chdir(self.geom_folder)
19
+ g = Geometry(fdm=self.fdm, inputs_folder_path=self.inputs_folder_path, verbose=self.verbose)
20
+ g.generate_cable_geometry(gui)
21
+
22
+
23
+
24
+ def mesh(self, gui: bool = False):
25
+ """
26
+ Generates the mesh for the cable geometry.
27
+ """
28
+ os.chdir(self.mesh_folder)
29
+
30
+ m = CableMesh(fdm=self.fdm, verbose=self.verbose)
31
+ m.generate_mesh(self.geom_folder)
32
+ m.generate_cuts()
33
+ m.generate_regions_file()
34
+ m.save_mesh(gui)
35
+
36
+ return {"test": 0}
37
+
38
+ def solve_and_postprocess_getdp(self, gui: bool = False):
39
+ """
40
+ Assembles the .pro-file from the template, then runs the simulation and the post-processing steps using GetDP.
41
+ """
42
+ os.chdir(self.solution_folder)
43
+
44
+ # Checks if the strand type in the conductor data model is 'Homogenized', other types are not allowed (e.g., round) to avoid confusion as inputs in them won't be used
45
+ if self.fdm.conductors[self.fdm.magnet.solve.conductor_name].strand.type != 'Homogenized':
46
+ raise Exception(f"The strand type must be 'Homogenized' in the conductors section of the input .yaml file, any other type is not allowed.")
47
+
48
+ s = Solve(self.fdm, self.GetDP_path, self.geom_folder, self.mesh_folder, self.verbose)
49
+ s.read_excitation(inputs_folder_path=self.inputs_folder_path)
50
+ # s.get_solution_parameters_from_yaml(inputs_folder_path=self.inputs_folder_path)
51
+ s.assemble_pro()
52
+ s.run_getdp(solve = True, postOperation = True, gui = gui)
53
+ s.cleanup()
54
+
55
+ def post_process_getdp(self, gui: bool = False):
56
+ """
57
+ Runs the post-processing steps trough GetDP.
58
+ """
59
+ os.chdir(self.solution_folder)
60
+
61
+ s = Solve(self.fdm, self.GetDP_path, self.geom_folder, self.mesh_folder, self.verbose)
62
+ s.read_excitation(inputs_folder_path=self.inputs_folder_path)
63
+ # s.get_solution_parameters_from_yaml(inputs_folder_path=self.inputs_folder_path)
64
+ s.assemble_pro()
65
+ s.run_getdp(solve = False, postOperation = True, gui = gui)
66
+
67
+ def post_process_python(self, gui: bool = False):
68
+ """
69
+ Runs the post-processing steps in the python PostProccess class.
70
+ """
71
+ os.chdir(self.solution_folder)
72
+
73
+ p = PostProcess(self.solution_folder)
74
+ p.show()
75
+
76
+ return {'test': 0}