Coverage for fiqus/plotters/PlotPythonConductorAC.py: 8%
491 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-12-25 02:54 +0100
« prev ^ index » next coverage.py v7.4.4, created at 2024-12-25 02:54 +0100
1import os
2import numpy as np
3import re
4import matplotlib.pyplot as plt
5from matplotlib.ticker import FuncFormatter
6from matplotlib.animation import FuncAnimation
7import pandas as pd
8from scipy import integrate, interpolate
9from ruamel.yaml import YAML
10import subprocess
12# from fiqus.utils.Utils import FilesAndFolders as Util
13# from fiqus.data.DataFiQuSConductorAC_Strand import CACStrandSolve, CACStrandPostproc, CACStrandMesh, CACStrandGeometry
15def create_non_overwriting_filepath(folder_path, base_name, extension, overwrite):
16 """
17 Creates a filepath that does not overwrite any existing files.
19 This function checks if a file already exists at the specified filepath. If the file exists and `overwrite` is False,
20 it modifies the filepath to create a new file instead of overwriting the existing one.
21 If `overwrite` is True or the file does not exist, it returns the filepath as it is.
23 Parameters
24 ----------
25 folder_path : str
26 The path to the folder where the file will be created.
27 base_name : str
28 The base name of the file.
29 extension : str
30 The extension of the file.
31 overwrite : bool, optional
32 If True, the function will overwrite an existing file. If False, the function will modify the filepath to avoid overwriting. Defaults to False.
34 Returns
35 -------
36 str
37 The final filepath. If `overwrite` is False and a file already exists at the original filepath, this will be a new filepath that does not overwrite any existing files.
38 """
39 if os.path.exists(os.path.join(folder_path, base_name+extension)) and not overwrite:
40 counter = 1
41 new_name = base_name + f"_{counter}" + extension
42 while os.path.exists(os.path.join(folder_path, new_name)):
43 new_name = base_name + f"_{counter}" + extension
44 counter += 1
45 return os.path.join(folder_path, new_name)
47 return os.path.join(folder_path, base_name+extension)
49class YamlWrapper:
50 """
51 A wrapper class for YAML data that allows accessing dictionary data using dot notation.
52 """
53 def __init__(self, data):
54 for key, value in data.items():
55 if isinstance(value, dict):
56 value = YamlWrapper(value)
57 self.__dict__[key] = value
59 def __getattr__(self, item):
60 return self.__dict__.get(item)
62 def __setattr__(self, key, value):
63 self.__dict__[key] = value
65def load_yaml(file_path):
66 """
67 Load a YAML file and return the data as a YamlWrapper object. This enables accessing the data using dot notation (e.g., data.key.subkey), without needing a predefined pydantic model, allowing for better backwards compatibility.
68 """
69 yaml = YAML()
70 with open(file_path, 'r') as file:
71 data = yaml.load(file)
72 return YamlWrapper(data)
74def is_latex_installed():
75 """
76 Check if LaTeX is installed on the system.
77 """
78 try:
79 subprocess.run(['pdflatex', '--version'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
80 return True
81 except subprocess.CalledProcessError:
82 return False
83 except FileNotFoundError:
84 return False
86class SimulationData:
87 """
88 Class used to store and manage data from a single simulation.
90 This class is responsible for loading and organizing the data from a single simulation.
91 It stores the data in various attributes and provides methods for retrieving and processing the data.
93 """
94 def __init__(self, model_data_output_path, geometry_name, mesh_name, solution_name) -> None:
95 self.model_data_output_path = model_data_output_path # This is the path to the folder where the model output data is stored (e.g. geometries)
96 self.geometry_name = geometry_name # Name of the geometry folder
97 self.mesh_name = mesh_name # Name of the mesh folder
98 self.solution_name = solution_name # Name of the solution folder
100 # Organize the folders:
101 self.geometry_folder = os.path.join(self.model_data_output_path, geometry_name) # Path to the geometry folder
102 self.mesh_folder = os.path.join(self.geometry_folder, mesh_name) # Path to the mesh folder
103 self.solution_folder = os.path.join(self.mesh_folder, solution_name) # Path to the solution folder
105 # Store the YAML input-files in a data model, fdm:
106 self.geometry, self.mesh, self.solve = self.retrieve_fiqusDataModel()
108 # Store losses, simulation time and check if the simulation crashed:
109 temp_file_path = os.path.join(self.solution_folder, 'test_temporary')
110 loss_file = [f for f in os.listdir(temp_file_path) if f.startswith('power') and f.endswith('.txt')][0]
111 self.power_columns = ['Time', 'FilamentLoss', 'CouplingLoss', 'EddyLoss', 'TotalLoss', 'CouplingLoss_dyn', 'TotalLoss_dyn'] # Only in the case dynamic correction is used, must be changed later
112 self.power = pd.read_csv(os.path.join(self.solution_folder, 'test_temporary', loss_file), sep = ' ', names=self.power_columns) # Store instantaneous losses as pandas dataframe
113 self.crash = True if 'crash_report.txt' in os.listdir(temp_file_path) else False
115 # Add a row of zeros at the beginning of the dataframe to account for the initial condition:
116 self.power = pd.concat([pd.DataFrame({col: 0 for col in self.power_columns}, index=[0]), self.power]).reset_index(drop=True)
117 # Integrate the losses to obtain the cumulative power and the total power per cycle:
118 self.cumulative_power, self.total_power_per_cycle = self.integrate_power() # Store cumulative power and total cumulative power per cycle
119 # Store simulation time:
120 try:
121 with open(os.path.join(self.solution_folder, 'test_temporary', 'simulation_time.txt'), 'r') as f:
122 self.simulation_time = float(f.readline().strip())
123 except:
124 self.simulation_time = None # If the simulation time file does not exist, the simulation has not finished running.
126 # Store the rest of the post-processing data:
127 self.time = self.power['Time']
128 self.instantaneous_temperature = self.load_standard_data(os.path.join(temp_file_path, 'temperature.txt'), 1, add_initial_zero=True)
129 self.temperature = self.load_standard_data(os.path.join(temp_file_path, 'temperature.txt'), 2, add_initial_zero=True)
130 self.I_transport = self.load_standard_data(os.path.join(temp_file_path, 'I_transport.txt'), 1)
131 self.V_transport = self.load_standard_data(os.path.join(temp_file_path, 'V_transport.txt'), 1)
132 self.hs_val = self.load_standard_data(os.path.join(temp_file_path, 'hs_val.txt'), 1)
133 self.magn_fil = self.load_standard_data(os.path.join(temp_file_path, 'magn_fil.txt'), [1, 2, 3])
134 self.magn_matrix = self.load_standard_data(os.path.join(temp_file_path, 'magn_matrix.txt'), [1, 2, 3])
135 self.I = self.load_standard_data(os.path.join(temp_file_path, 'I.txt'), 1, len(self.time))
136 self.V = self.load_standard_data(os.path.join(temp_file_path, 'V.txt'), 1, len(self.time))
137 self.I_integral = self.load_standard_data(os.path.join(temp_file_path, 'I_integral.txt'), 1, len(self.time))
138 self.I_abs_integral = self.load_standard_data(os.path.join(temp_file_path, 'I_abs_integral.txt'), 1, len(self.time))
139 self.magnetic_energy_internal = self.load_standard_data(os.path.join(temp_file_path, 'magnetic_energy_internal.txt'), 1)
140 self.Ip = self.load_standard_data(os.path.join(temp_file_path, 'Ip.txt'), 1, len(self.time))
141 self.Vp = self.load_standard_data(os.path.join(temp_file_path, 'Vp.txt'), 1, len(self.time))
144 def load_standard_data(self, file_path, columns, reshape = None, add_initial_zero = False):
145 """
146 There are many output .txt-files with similar format. This function loads the data from one of these files and returns it as a numpy array.
147 If the file does not exist, None is returned without raising an error.
148 """
149 try:
150 data = np.loadtxt(file_path, comments='#', usecols=columns)
151 if reshape:
152 data = data.reshape(-1, reshape).T
153 except IOError:
154 return None
156 if add_initial_zero:
157 if len(data.shape) == 1:
158 data = np.insert(data, 0, 0)
159 else:
160 zeros = np.zeros((1, data.shape[1]))
161 data = np.vstack((zeros, data))
162 return data
164 def retrieve_fiqusDataModel(self):
165 """
166 This function reads the YAML input-files for geometry, mesh and solve and stores them in three dictionaries which are returned.
167 This function is to be called only once, when the object is created.
168 """
169 geometry_dataModel = load_yaml(os.path.join(self.geometry_folder, 'geometry.yaml'))
170 mesh_dataModel = load_yaml(os.path.join(self.mesh_folder, 'mesh.yaml'))
171 solution_dataModel = load_yaml(os.path.join(self.solution_folder, 'solve.yaml'))
173 return geometry_dataModel, mesh_dataModel, solution_dataModel
175 def integrate_power(self):
176 """
177 This function integrates the instantaneous power over time to obtain the cumulative power.
178 It also calculates the total cumulative power per cycle.
179 The cumulative power is returned as a pandas dataframe and the total cumulative power per cycle is returned as a dictionary.
180 """
181 find_closest_idx = lambda arr, val: np.abs(arr - val).argmin()
183 t = np.array(self.power['Time'])
184 t_final = t[-1]
185 t_init = find_closest_idx(t, t_final/2)
187 cumulative_power = pd.DataFrame(columns= self.power_columns)
188 total_power_per_cycle = {}
190 cumulative_power['Time'] = self.power["Time"]
191 for column in self.power_columns[1:]:
192 cumulative_power[column] = np.insert(integrate.cumulative_trapezoid(self.power[column], t), 0, 0)
193 total_power_per_cycle[column] = 2 * (cumulative_power[column].iloc[-1]-cumulative_power[column].iloc[t_init]) # / (np.pi*matrix_radius**2 * loss_factor) # Why do we divide by pi*matrix_radius**2*loss_factor?
195 return cumulative_power, total_power_per_cycle
197 def plot_instantaneous_power(self, show:bool = True, title:str = "Power", save_plot:bool = False, save_folder_path:str = None, save_file_name:str = None, overwrite:bool = False):
198 plt.figure()
199 plt.plot(self.power['Time'], self.power[self.power_columns[1:]] , label = self.power_columns[1:])
200 plt.xlabel('Time [s]')
201 plt.ylabel('Power [W/m]')
202 plt.legend()
204 # Configure title:
205 # Class attributes can be accessed by using '<< ... >>' in the title string.
206 commands = re.findall('<<(.*?)>>', title)
207 for c in commands:
208 title = title.replace(f"<<{c}>>", str(eval('self.'+c)))
209 plt.title(title)
212 if save_plot: # Save the plot
213 filePath = create_non_overwriting_filepath(save_folder_path, save_file_name, '.png', overwrite)
214 plt.savefig(filePath)
216 if show:
217 plt.show()
218 else:
219 plt.close()
221class PlotPython:
222 """
223 This class loads and stores the data from the simulations specified in a csv file and can apply various postprocessing operations on the data.
224 The data from each simulation is saved as a SimulationData object which is subsequently stored in a list in this class.
225 """
226 def __init__(self, fdm, csv_filename = None, lossMap_gridData_folder = None, inputs_folder_path='', outputs_folder_path='') -> None:
227 self.fdm = fdm
228 self.inputs_folder_path = inputs_folder_path # This is the path to the folder where the input data is stored
229 self.model_data_output_path = outputs_folder_path # This is the path to the folder where the model output data is stored (e.g. geometries)
230 self.outputs_folder_path = os.path.join(outputs_folder_path, fdm.magnet.postproc.batch_postproc.output_folder) # This is the path to the folder where the postprocessed data is written
233 if not os.path.exists(self.outputs_folder_path):
234 os.makedirs(self.outputs_folder_path)
236 if csv_filename is not None:
237 try:
238 self.input_csv = pd.read_csv(os.path.join(self.inputs_folder_path, f'{csv_filename}.csv')) # Read the csv file with the input data
239 except:
240 raise FileNotFoundError(f'No csv file with the name {fdm.magnet.postproc.batch_postproc.postProc_csv}.csv was found in the inputs folder.')
242 self.simulation_collection = self.retrieve_simulation_data()
244 self.avg_simulation_time = np.mean([sd.simulation_time for sd in self.simulation_collection])
245 self.total_simulation_time = np.sum([sd.simulation_time for sd in self.simulation_collection])
247 print('Number of simulations considered: ', len(self.simulation_collection) )
248 print('Average simulation time: ', self.avg_simulation_time, 's')
249 print('Total simulation time: ', self.total_simulation_time, 's')
253 elif lossMap_gridData_folder is not None:
254 self.input_csv = None
255 self.simulation_collection = None
257 self.totalLoss_gridData = self.load_lossMap_gridData('TotalLoss', lossMap_gridData_folder)
258 self.filamentLoss_gridData = self.load_lossMap_gridData('FilamentLoss', lossMap_gridData_folder)
259 self.eddyLoss_gridData = self.load_lossMap_gridData('EddyLoss', lossMap_gridData_folder)
260 self.couplingLoss_gridData = self.load_lossMap_gridData('CouplingLoss', lossMap_gridData_folder)
261 else:
262 raise ValueError('No input data specified. Either a csv file or a folder with loss map grid data must be provided.')
267 def retrieve_simulation_data(self):
268 """
269 This function iterates over the input CSV-file (specifying which simulations to postprocess) and returns a list of SimulationData objects
270 containing all the simulation data. If no CSV-file is specified, the data from the single simulation specified in the input YAML-file is returned.
271 """
272 if self.input_csv is not None:
273 simulationCollection = []
274 for index, row in self.input_csv.iterrows():
275 if pd.isna(row['input.run.geometry']) and pd.isna(row['input.run.mesh']) and pd.isna(row['input.run.solution']):
276 continue
277 geometry_name = 'Geometry_'+str(row['input.run.geometry'])
278 mesh_name = 'Mesh_'+str(row['input.run.mesh'])
280 if isinstance(row['input.run.solution'], float) and row['input.run.solution'].is_integer():
281 solution_name = 'Solution_'+str(int(row['input.run.solution']))
282 else:
283 solution_name = 'Solution_'+str(row['input.run.solution'])
285 # Check if the row refers to a valid simulation by checking if the solution folder exists:
286 # solution_folder = os.path.join(os.getcwd(), 'tests', '_outputs', self.fdm.general.magnet_name, geometry_name, mesh_name, solution_name)
287 solution_folder = os.path.join(self.model_data_output_path, geometry_name, mesh_name, solution_name)
288 if os.path.exists(solution_folder): # If the solution folder exists, add the simulation to the simulationCollection
289 sd = SimulationData(self.model_data_output_path, geometry_name, mesh_name, solution_name)
290 if sd.simulation_time is not None: # Only add the simulation if it has finished running (and therefore has written the simulation time to a file)
291 simulationCollection.append(sd)
292 else:
293 simulationCollection = [SimulationData(self.model_data_output_path, 'Geometry_'+self.fdm.run.geometry, 'Mesh_'+self.fdm.run.mesh, 'Solution_'+self.fdm.run.solution)]
295 return self.sort_simulationCollection(self.filter_simulationCollection(simulationCollection))
297 def filter_simulationCollection(self, simulationCollection):
298 """
299 This function is used to filter the simulationCollection based on the filter criterion specified in the yaml input file.
300 An example of a filter criterion is '<<solve.source_parameters.sine.frequency>> == 18', which will disregard all simulations with frequency != 18Hz.
301 """
302 if self.fdm.magnet.postproc.batch_postproc.filter.apply_filter:
303 filter_criterion = self.fdm.magnet.postproc.batch_postproc.filter.filter_criterion
304 class_params = re.findall('<<(.*?)>>', filter_criterion)
305 for cp in class_params:
306 filter_criterion = filter_criterion.replace(f"<<{cp}>>", 'sd.'+cp)
307 filtering_function = eval(f'lambda sd: {filter_criterion}')
308 return list(filter(filtering_function, simulationCollection))
309 else:
310 return simulationCollection
312 def sort_simulationCollection(self, simulationCollection):
313 """
314 This function is used to sort the simulationCollection based on the sort key specified in the yaml input file.
315 An example of a sort key is 'solve.source_parameters.sine.frequency', which will sort the simulations based on frequency.
316 """
317 if self.fdm.magnet.postproc.batch_postproc.sort.apply_sort:
318 sorting_function = eval(f'lambda sd: sd.{self.fdm.magnet.postproc.batch_postproc.sort.sort_key}')
319 return sorted(simulationCollection, key=sorting_function)
320 else:
321 return simulationCollection
323 def lossMap_createGridData(self, lossType = 'TotalLoss', x_val_to_include = None, y_val_to_include = None):
324 """
325 This function creates the grid data needed for the loss map, based on the yaml input file.
326 Given a collection of simulations it interpolates the loss data between the datapoints to a grid and returns the grid data.
327 """
328 lm = self.fdm.magnet.postproc.batch_postproc.loss_map
330 # Extract data from simulation collection and normalize
331 x_arr = np.array([eval('sd.'+lm.x_val)/lm.x_norm for sd in self.simulation_collection])
332 y_arr = np.array([eval('sd.'+lm.y_val)/lm.y_norm for sd in self.simulation_collection])
333 loss = np.array([sd.total_power_per_cycle[lossType]/lm.loss_norm for sd in self.simulation_collection])
335 # Logarithmic scaling
336 if lm.x_log: x_arr = np.log10(x_arr)
337 if lm.y_log: y_arr = np.log10(y_arr)
338 if lm.loss_log: loss = np.log10(loss)
340 x_arr_interpolated = np.linspace(min(x_arr), max(x_arr), lm.x_steps)
341 y_arr_interpolated = np.linspace(min(y_arr), max(y_arr), lm.y_steps)
342 # Insert specific values to the grid if they are not already included (useful for cross sections)
343 if x_val_to_include is not None and x_val_to_include not in x_arr_interpolated:
344 x_arr_interpolated = np.insert(x_arr_interpolated, np.where(x_arr_interpolated > x_val_to_include)[0][0], x_val_to_include)
345 if y_val_to_include is not None and y_val_to_include not in y_arr_interpolated:
346 y_arr_interpolated = np.insert(y_arr_interpolated, np.where(y_arr_interpolated > y_val_to_include)[0][0], y_val_to_include)
348 # Create grid
349 X, Y = np.meshgrid(x_arr_interpolated, y_arr_interpolated, indexing='ij')
350 gridPoints = np.c_[X.ravel(), Y.ravel()]
351 dataPoints = np.c_[x_arr, y_arr]
353 # Interpolate the simulation data onto the grid
354 V = interpolate.griddata(
355 dataPoints,
356 loss,
357 gridPoints,
358 method='linear' # Cubic produces cleaner plots. Any incentive to go back to linear?
359 ).reshape(X.shape)
361 return X, Y, V, dataPoints
363 def save_lossMap_gridData(self, save_folder_name = 'lossMap_gridData'):
364 """
365 This function calls the lossMap_createGridData function and saves the grid data.
366 """
367 lm = self.fdm.magnet.postproc.batch_postproc.loss_map
369 lossTypes = ['TotalLoss', 'FilamentLoss', 'EddyLoss', 'CouplingLoss', 'CouplingLoss_dyn', 'TotalLoss_dyn'] # Only in the case dynamic correction is used, must be changed later
370 # 1) Create a folder to store the output files
371 gridData_folder_path = create_non_overwriting_filepath(self.outputs_folder_path, save_folder_name, '', self.fdm.run.overwrite)
372 if not os.path.exists(gridData_folder_path): os.makedirs(gridData_folder_path)
373 # 2) Create the grid data for each loss type and save it
374 for lossType in lossTypes:
375 X, Y, V, _ = self.lossMap_createGridData(lossType)
376 if lm.x_log: X = np.power(10, X)
377 if lm.y_log: Y = np.power(10, Y)
378 if lm.loss_log: V = np.power(10, V)
379 np.savetxt(os.path.join(gridData_folder_path, f'{lossType}.txt'), np.column_stack((X.ravel(), Y.ravel(), V.ravel())), delimiter=' ', header=f'{lm.x_val} {lm.y_val} {lossType}', comments='')
381 def load_lossMap_gridData(self, lossType = 'TotalLoss', save_folder_name = 'lossMap_gridData'):
382 """
383 This function loads the grid data for a given loss type.
384 """
385 lm = self.fdm.magnet.postproc.batch_postproc.loss_map
386 gridData_folder_path = os.path.join(self.inputs_folder_path, save_folder_name)
388 if not os.path.exists(gridData_folder_path):
389 raise FileNotFoundError(f'The folder {gridData_folder_path} does not exist.')
391 X, Y, V = np.loadtxt(os.path.join(gridData_folder_path, f'{lossType}.txt'), unpack=True, skiprows=1)
393 if lm.x_log: X = np.log10(X)
394 if lm.y_log: Y = np.log10(Y)
395 if lm.loss_log: V = np.log10(V)
397 # Get the unique counts of X and Y
398 unique_X = np.unique(X)
399 unique_Y = np.unique(Y)
401 # Reshape the data
402 X = X.reshape((len(unique_X), len(unique_Y)))
403 Y = Y.reshape((len(unique_X), len(unique_Y)))
404 V = V.reshape((len(unique_X), len(unique_Y)))
406 return X, Y, V
408 # def add_value_to_gridData(self, X, Y, V, x_val = None, y_val = None):
409 # """
410 # This function adds a value to the grid data.
411 # Steps:
412 # 1) Revert the grid data to 1D arrays
413 # 2) Add x or y value or both to the arrays
414 # 3) Reshape the arrays back to grid data, interpolating the loss to the new grid points
415 # """
416 # gridPoints
418 def save_magnetization(self):
419 """
420 This function saves the magnetization data for all simulations in the simulation collection.
421 """
422 magnetization_folder_path = create_non_overwriting_filepath(self.outputs_folder_path, 'magnetization', '', self.fdm.run.overwrite)
423 if not os.path.exists(magnetization_folder_path): os.makedirs(magnetization_folder_path)
424 for sd in self.simulation_collection:
425 magnetization = sd.magn_fil + sd.magn_matrix
426 magnetization = np.c_[sd.time, magnetization]
427 np.savetxt(os.path.join(magnetization_folder_path, f'magn_f{sd.solve.source_parameters.sine.frequency}_b{sd.solve.source_parameters.sine.field_amplitude}_I{sd.solve.source_parameters.sine.current_amplitude}.txt'), magnetization, delimiter=' ', header='t x y z', comments='')
434 def lossMap_crossSection(self, slice_value, axis_to_cut = 'x'):
435 """
436 This function returns the data corresponding to a cross section of the loss map, for all loss types.
437 Given an axis and a value, it sweeps the other axis for the closest value and returns the data.
438 Example: Given slice value 0 and axis x, it returns the data for the cross section at x = 0.
439 """
441 lm = self.fdm.magnet.postproc.batch_postproc.loss_map
442 if axis_to_cut == 'x':
443 x_val_to_include = slice_value
444 y_val_to_include = None
445 elif axis_to_cut == 'y':
446 x_val_to_include = None
447 y_val_to_include = slice_value
448 X, Y, V, dataPoints = self.lossMap_createGridData('TotalLoss', x_val_to_include, y_val_to_include)
449 _,_,FilamentLoss, _ = self.lossMap_createGridData('FilamentLoss', x_val_to_include, y_val_to_include)
450 _,_,EddyLoss, _ = self.lossMap_createGridData('EddyLoss', x_val_to_include, y_val_to_include)
451 _,_,CouplingLoss, _ = self.lossMap_createGridData('CouplingLoss', x_val_to_include, y_val_to_include)
454 if axis_to_cut == 'x':
455 index = np.abs(X[:, 0] - slice_value).argmin()
456 slice_vals = Y[index, :]
458 elif axis_to_cut == 'y':
459 index = np.abs(Y[0, :] - slice_value).argmin()
460 slice_vals = X[:, index]
462 # Extract the loss values for the constant frequency across all applied fields
463 totalLoss = V[index, :] if axis_to_cut == 'x' else V[:, index]
464 filamentLoss = FilamentLoss[index, :] if axis_to_cut == 'x' else FilamentLoss[:, index]
465 eddyLoss = EddyLoss[index, :] if axis_to_cut == 'x' else EddyLoss[:, index]
466 couplingLoss = CouplingLoss[index, :] if axis_to_cut == 'x' else CouplingLoss[:, index]
468 return slice_vals, totalLoss, filamentLoss, eddyLoss, couplingLoss
470 def plot_lossMap_crossSection(self):
471 """
472 This function calls the lossMap_crossSection function and plots the data it returns, which is the loss for all values of one axis, given a constant value of the other axis.
473 """
475 if is_latex_installed():
476 plt.rcParams['text.usetex'] = True
477 plt.rcParams['font.family'] = 'times'
478 plt.rcParams['font.size'] = 20
480 lm = self.fdm.magnet.postproc.batch_postproc.loss_map
481 slice_value = lm.cross_section.cut_value
482 axis_to_cut = lm.cross_section.axis_to_cut
484 if (lm.x_log and axis_to_cut == 'x') or (lm.y_log and axis_to_cut == 'y'):
485 slice_value = np.log10(slice_value)
487 slice_vals, totalLoss, filamentLoss, eddyLoss, couplingLoss = self.lossMap_crossSection(slice_value, axis_to_cut = axis_to_cut)
489 def log_formatter(x, pos):
490 """
491 Format the tick labels on the plot.
492 """
493 return f"$10^{{{int(x)}}}$"
495 # Plot the loss with respect to applied field for the constant frequency
496 fig, ax = plt.subplots(figsize=(8, 6))
497 ax.plot(slice_vals, totalLoss, label=f'Total Loss')
498 ax.plot(slice_vals, filamentLoss, label=f'Filament Loss')
499 ax.plot(slice_vals, eddyLoss, label=f'Eddy Loss')
500 ax.plot(slice_vals, couplingLoss, label=f'Coupling Loss')
502 tick_formatter = FuncFormatter(log_formatter)
503 if lm.x_log and axis_to_cut == 'y' or lm.y_log and axis_to_cut == 'x':
504 ax.xaxis.set_major_formatter(tick_formatter)
505 if lm.loss_log:
506 ax.yaxis.set_major_formatter(tick_formatter)
509 title = lm.cross_section.title.replace('<<cut_value>>', str(round(10**slice_value, 3)))
510 ax.set_title(title)
511 ax.set_xlabel(lm.ylabel if axis_to_cut == 'x' else lm.xlabel)
512 ax.set_ylabel(lm.cross_section.ylabel)
513 ax.legend()
515 # np.savetxt(os.path.join(self.outputs_folder_path, 'lossMaps_cut_0p2T_0A.txt'), np.column_stack((10**slice_vals, 10**totalLoss, 10**eddyLoss, 10**couplingLoss, 10**filamentLoss)), delimiter=' ', header='f total eddy coupling filament', comments='')
517 if lm.cross_section.save_plot:
518 filePath = create_non_overwriting_filepath(self.outputs_folder_path, lm.cross_section.filename, '.png', self.fdm.run.overwrite)
519 plt.savefig(filePath)
521 if self.fdm.run.launch_gui: plt.show()
523 def animate_lossMap_crossSection(self):
524 """
525 This function is similar to the plot_lossMap_crossSection function, but instead of plotting the loss for at a constant crossection,
526 it sweeps the crossection over a chosen axis and plots the loss for each crossection as an animation.
527 """
528 lm = self.fdm.magnet.postproc.batch_postproc.loss_map
529 axis = lm.cross_section_sweep.axis_to_sweep
531 X, Y, V, dataPoints = self.lossMap_createGridData('TotalLoss')
532 x_vals = X[:, 0] # x-values from the loss map
533 y_vals = Y[0, :] # y-values from the loss map
535 if axis == 'x':
536 A = np.zeros((lm.y_steps, 4, lm.x_steps))
537 axis_to_sweep = x_vals
538 constant_axis = y_vals
539 elif axis == 'y':
540 A = np.zeros((lm.x_steps, 4, lm.y_steps))
541 axis_to_sweep = y_vals
542 constant_axis = x_vals
545 for i, val in enumerate(axis_to_sweep):
546 _, totalLoss, filamentLoss, eddyLoss, couplingLoss = self.lossMap_crossSection(val, axis_to_cut = axis)
547 A[:, 0, i] = totalLoss
548 A[:, 1, i] = filamentLoss
549 A[:, 2, i] = eddyLoss
550 A[:, 3, i] = couplingLoss
552 # Initialize the plot
553 fig, ax = plt.subplots()
554 lines = ax.plot(constant_axis, A[:, :, 0], lw=2, label = ['total Loss', 'filament Loss', 'eddy Loss', 'coupling Loss'])
556 # Set plot limits and labels
557 ax.set_xlim(constant_axis[0], constant_axis[-1])
558 ax.set_ylim(np.min(A), np.max(A))
559 ax.set_xlabel(lm.ylabel if axis == 'x' else lm.xlabel)
560 ax.set_ylabel(lm.cross_section_sweep.ylabel)
563 # Define the animation update function
564 def update(frame):
565 for i, line in enumerate(lines):
566 line.set_ydata(A[:, i, frame])
568 if axis == 'x':
569 if lm.x_log:
570 sweep_value = 10**x_vals[frame]
571 else:
572 sweep_value = x_vals[frame]
573 elif axis == 'y':
574 if lm.y_log:
575 sweep_value = 10**y_vals[frame]
576 else:
577 sweep_value = y_vals[frame]
579 title = lm.cross_section_sweep.title.replace('<<sweep_value>>', str(round(sweep_value, 3)))
580 ax.set_title(title)
581 return lines,# line1, line2, line3, line4
583 # Create the animation
584 dt = 0.1
585 ani = FuncAnimation(fig, update, frames=lm.x_steps if axis == 'x' else lm.y_steps, interval=dt*1000, blit=False)
587 # Show the animation
588 plt.legend()
589 plt.grid()
591 if lm.cross_section_sweep.save_plot:
592 filepath = create_non_overwriting_filepath(folder_path=self.outputs_folder_path, base_name=lm.cross_section_sweep.filename, extension='.gif', overwrite=self.fdm.run.overwrite)
593 ani.save(filepath, writer='imagemagick', fps=1/dt)
595 if self.fdm.run.launch_gui: plt.show()
597 def create_lossMap(self):
598 """
599 This function creates a loss map based on the inputs given in the loss_map section of the input file.
600 The loss-map can be plotted and saved as a .png file.
601 """
602 lm = self.fdm.magnet.postproc.batch_postproc.loss_map
604 if self.simulation_collection:
605 X, Y, V, dataPoints = self.lossMap_createGridData(lm.loss_type)
606 else:
607 X, Y, V = self.totalLoss_gridData
609 if is_latex_installed():
610 plt.rcParams['text.usetex'] = True
611 plt.rcParams['font.family'] = 'times'
612 plt.rcParams['font.size'] = 20
614 fig, ax = plt.subplots(figsize=(10,8))
616 c = plt.pcolormesh(X, Y, V, shading='gouraud', cmap='plasma_r')
617 c_min = min([np.ceil(np.min(V)) for V in [V]])
618 c_max = max([np.floor(np.max(V)) for V in [V]])
619 c_ticks = [int(val) for val in np.arange(c_min, c_max+1)]
620 cont = plt.contour(X,Y,V, c_ticks, colors='k', linestyles='dashed')
622 if lm.show_datapoints:
623 plt.scatter(dataPoints[:, 0], dataPoints[:, 1], s=50, edgecolors='k')
626 if lm.show_loss_type_dominance_contour:
627 sigmoid = lambda x: 1/(1+np.exp(-x))
628 if self.simulation_collection:
629 _, _, FilamentLoss, _ = self.lossMap_createGridData(lossType='FilamentLoss')
630 _, _, CouplingLoss, _ = self.lossMap_createGridData(lossType='CouplingLoss')
631 _, _, EddyLoss, _ = self.lossMap_createGridData(lossType='EddyLoss')
632 # else:
633 # _, _, FilamentLoss = self.filamentLoss_gridData
634 # _, _, CouplingLoss = self.couplingLoss_gridData
635 # _, _, EddyLoss = self.eddyLoss_gridData
636 fil_vs_coupling_loss = np.maximum(FilamentLoss, EddyLoss) - CouplingLoss
637 fil_vs_eddy_loss = EddyLoss - np.maximum(FilamentLoss, CouplingLoss)
638 plt.contour(X,Y,sigmoid(fil_vs_coupling_loss),[0.5], colors='k')
639 plt.contour(X,Y,sigmoid(fil_vs_eddy_loss),[0.5], colors='k')
641 cbar = fig.colorbar(c, ticks=c_ticks)#, labels=c_labels)
642 # cbar.ax.set_xticks([-7, -6, -5, -4, -3, -2, -1, 0, 1])
643 # cbar.ax.set_yticklabels([r'$10^{-7}$', r'$10^{-6}$', r'$10^{-5}$', r'$10^{-4}$', r'$10^{-3}$', r'$10^{-2}$', r'$10^{-1}$', r'$10^0$', r'$10^1$'])
644 cbar.ax.set_yticklabels([f"$10^{{{val}}}$" for val in c_ticks])
645 # plt.grid(alpha=0.5)
646 # plt.title(lm.title)
647 # plt.xlabel(lm.xlabel)
648 # plt.ylabel(lm.ylabel)
649 plt.title(r'Loss per cycle (J/m)')
650 plt.xlabel(r'Frequency $f$ (Hz)')
651 plt.ylabel(r'Field amplitude $b$ (T)')
654 # plt.annotate(r'Coupling', (np.log10(1.0), np.log10(0.007)), color='white')
655 # plt.annotate(r'Filament', (np.log10(0.012), np.log10(0.74)), color='white')
656 # plt.annotate(r'(uncoupled)', (np.log10(0.012), np.log10(0.55)), color='white')
657 # plt.annotate(r'Filament', (np.log10(45), np.log10(0.38)), color='white')
658 # plt.annotate(r'(coupled)', (np.log10(45), np.log10(0.28)), color='white')
659 # plt.annotate(r'Eddy', (np.log10(2000), np.log10(0.03)), color='white')
661 # ax.plot(np.log10(0.03), np.log10(0.2), 'o', color='white')#, xytext=(np.log10(0.03), np.log10(0.12)), arrowprops=dict(facecolor='black', shrink=0.02))
662 # ax.plot(np.log10(30), np.log10(1), 'o', color='white')#, xytext=(np.log10(40), np.log10(0.8)), arrowprops=dict(facecolor='black', shrink=0.02))
663 # ax.plot(np.log10(3), np.log10(0.2), 'o', color='white')#, xytext=(np.log10(2), np.log10(0.2)), arrowprops=dict(facecolor='black', shrink=0.02))
664 # ax.plot(np.log10(5000), np.log10(0.2), 'o', color='white')#, xytext=(np.log10(5000), np.log10(0.1)), arrowprops=dict(facecolor='black', shrink=0.02))
666 # ax.annotate('(a)', xy=(np.log10(0.03), np.log10(0.2)), xycoords='data', ha='right', va='bottom', fontsize=20, color='white')
667 # ax.annotate('(b)', xy=(np.log10(3), np.log10(0.2)), xycoords='data', ha='right', va='bottom', fontsize=20, color='white')
668 # ax.annotate('(c)', xy=(np.log10(30), np.log10(1)), xycoords='data', ha='right', va='bottom', fontsize=20, color='white')
669 # ax.annotate('(d)', xy=(np.log10(5000), np.log10(0.2)), xycoords='data', ha='right', va='bottom', fontsize=20, color='white')
671 # Define custom tick labels for x-axis
672 x_min_log = int(np.log10(min([eval('sd.'+lm.x_val) for sd in self.simulation_collection])))
673 x_max_log = int(np.log10(max([eval('sd.'+lm.x_val) for sd in self.simulation_collection])))
674 x = np.arange(x_min_log, x_max_log+1)
675 # Create a list of minor ticks
676 minor_x_labels = []
677 # 1) Add the ticks from x_min_log to ceil(x_min_log) to the minor_x_test list
678 new_ticks = np.linspace(10.0**np.floor(x_min_log), 10.0**np.ceil(x_min_log), 10)[:-1]
679 new_ticks = np.unique(new_ticks[new_ticks >= 10.0**x_min_log])
680 minor_x_labels.extend(new_ticks)
681 # 2) Add the ticks from ceil(x_min_log) to floor(x_max_log) to the minor_x_test list
682 for x_val in x:
683 new_ticks = np.linspace(10.0**x_val, 10.0**(x_val+1), 10)[1:-1]
684 if x_val == x[-1]:
685 new_ticks = new_ticks[new_ticks <= 10.0**x_max_log]
686 minor_x_labels.extend(new_ticks)
687 minor_x = [np.log10(val) for val in minor_x_labels]
689 new_x_labels = [f"$10^{{{val}}}$" for val in x]
690 plt.xticks(x, new_x_labels)
691 plt.xticks(minor_x, minor=True)
693 # Define custom tick labels for y-axis
694 y_min_log = np.log10(min([eval('sd.'+lm.y_val) for sd in self.simulation_collection]))
695 y_max_log = np.log10(max([eval('sd.'+lm.y_val) for sd in self.simulation_collection]))
696 y = np.arange(np.ceil(y_min_log), np.floor(y_max_log)+1)
697 # Create a list of minor ticks
698 minor_y_labels = []
699 # 1) Add the ticks from y_min_log to ceil(y_min_log) to the minor_y_test list
700 new_ticks = np.linspace(10.0**np.floor(y_min_log), 10.0**np.ceil(y_min_log), 10)[:-1]
701 new_ticks = np.unique(new_ticks[new_ticks >= 10.0**y_min_log])
702 minor_y_labels.extend(new_ticks)
703 # 2) Add the ticks from ceil(y_min_log) to floor(y_max_log) to the minor_y_test list
704 for y_val in y:
705 new_ticks = np.linspace(10.0**y_val, 10.0**(y_val+1), 10)[1:-1]
706 if y_val == y[-1]:
707 new_ticks = new_ticks[new_ticks <= 10.0**y_max_log]
708 minor_y_labels.extend(new_ticks)
710 new_y_labels = [f"$10^{{{int(val)}}}$" for val in y]
711 minor_y = [np.log10(val) for val in minor_y_labels]
712 plt.yticks(y, new_y_labels)
713 plt.yticks(minor_y, minor=True)
715 # plt.savefig('C:/Users/jdular/cernbox/Documents/Reports/CERN_Reports/linkedFluxPaper/fig/loss_map_54fil_noI.pdf', bbox_inches='tight')
718 if lm.save_plot:
719 filePath = create_non_overwriting_filepath(self.outputs_folder_path, lm.filename, '.pdf', self.fdm.run.overwrite)
720 plt.savefig(filePath, bbox_inches='tight')
722 if self.fdm.run.launch_gui: plt.show()
725 def plot2d(self):
726 """
727 This function is used to create a 2d plot. It is supposed to be flexible and work for various kinds of plots one may want to create.
728 """
729 if is_latex_installed():
730 plt.rcParams['text.usetex'] = True
731 plt.rcParams['font.family'] = 'times'
732 # plt.rcParams['font.size'] = 20
734 # Create the title (or titles if combined_plot is False)
735 title = self.fdm.magnet.postproc.batch_postproc.plot2d.title
736 if self.fdm.magnet.postproc.batch_postproc.plot2d.combined_plot:
737 sd = self.simulation_collection[0]
738 commands = re.findall('<<(.*?)>>', title)
739 for c in commands:
740 title = title.replace(f"<<{c}>>", str(eval('sd.'+c)))
741 else:
742 titles = []
743 for sd in self.simulation_collection:
744 commands = re.findall('<<(.*?)>>', title)
745 title_i = title
746 for c in commands:
747 title_i = title_i.replace(f"<<{c}>>", str(eval('sd.'+c)))
748 titles.append(title_i)
750 # Create the labels
751 label_list = self.fdm.magnet.postproc.batch_postproc.plot2d.labels
752 labels = np.zeros((len(self.simulation_collection), len(label_list)), dtype=object)
753 for i, sd in enumerate(self.simulation_collection):
754 simulation_labels = []
755 for l in label_list:
756 commands = re.findall('<<(.*?)>>', l)
757 for c in commands:
758 l = l.replace(f"<<{c}>>", str(eval('sd.'+c)))
760 simulation_labels.append(l)
761 labels[i, :] = simulation_labels
763 # colors = plt.cm.get_cmap('magma').resampled(len(self.simulation_collection)).colors
764 colors = plt.cm.get_cmap('viridis').resampled(len(self.simulation_collection)).colors
766 # Load the x-values:
767 x_val = self.fdm.magnet.postproc.batch_postproc.plot2d.x_val
768 commands = re.findall('<<(.*?)>>', x_val)
769 for c in commands:
770 x_val = x_val.replace(f"<<{c}>>", str('sd.'+c))
771 x_arr = np.array([eval(x_val) for sd in self.simulation_collection], dtype=object)
773 # Load the y-values:
774 y_vals = self.fdm.magnet.postproc.batch_postproc.plot2d.y_vals
775 y_arr = np.zeros((len(self.simulation_collection), len(y_vals)), dtype=object)
776 for i, sd in enumerate(self.simulation_collection):
777 for j, y_val in enumerate(y_vals):
778 commands = re.findall('<<(.*?)>>', y_val)
779 for c in commands:
780 y_val = y_val.replace(f"<<{c}>>", str('sd.'+c))
781 y_arr[i, j] = eval(y_val)
783 # data = np.column_stack((x_arr, y_arr))
784 # np.savetxt(os.path.join(self.outputs_folder_path, self.fdm.magnet.postproc.batch_postproc.plot2d.filename+'.txt'), data, delimiter=' ', header='f total eddy coupling filament', comments='')
789 # Plot and save the data:
790 if not self.fdm.magnet.postproc.batch_postproc.plot2d.combined_plot and self.fdm.magnet.postproc.batch_postproc.plot2d.save_plot:
791 # Create a folder to save the plots if combined_plot is False and save_plot is True:
792 filename = self.fdm.magnet.postproc.batch_postproc.plot2d.filename
793 folder_path = create_non_overwriting_filepath(self.outputs_folder_path, filename, '', self.fdm.run.overwrite)
794 if not os.path.exists(folder_path): os.makedirs(folder_path)
796 # Check if the y-values are all floats:
797 y_is_float = np.all(np.apply_along_axis(lambda arr: np.all(np.vectorize(isinstance)(arr, float)), axis=1, arr=y_arr))
798 # If they are all floats (not arrays), we can make a single plot, spanning all simulations, instead of one plot per simulation.
799 if y_is_float:
800 for column in range(y_arr.shape[1]):
801 plt.plot(x_arr, y_arr[:, column], label=label_list[column])
802 if self.fdm.magnet.postproc.batch_postproc.plot2d.legend:
803 plt.legend()
804 plt.grid()
805 plt.xlabel(self.fdm.magnet.postproc.batch_postproc.plot2d.xlabel)
806 plt.ylabel(self.fdm.magnet.postproc.batch_postproc.plot2d.ylabel)
807 plt.title(title)
808 if self.fdm.magnet.postproc.batch_postproc.plot2d.x_log:
809 plt.xscale('log')
810 if self.fdm.magnet.postproc.batch_postproc.plot2d.y_log:
811 plt.yscale('log')
813 if self.fdm.magnet.postproc.batch_postproc.plot2d.save_plot:
814 filename = self.fdm.magnet.postproc.batch_postproc.plot2d.filename
815 filePath = create_non_overwriting_filepath(self.outputs_folder_path, filename, '.png', self.fdm.run.overwrite)
816 plt.savefig(filePath)
819 else:
820 for i, (x, y_vals_per_sim, labels_per_sim, color) in enumerate(zip(x_arr, y_arr, labels, colors)):
821 if self.fdm.magnet.postproc.batch_postproc.plot2d.combined_plot:
822 # If combined_plot is true, plot all the data in the same figure:
823 # x_sin = 2*np.sin(2*np.pi*x/x.iloc[-1]) #temporary for missing hs_val
824 for y, label in zip(y_vals_per_sim, labels_per_sim):
825 # plt.plot(x_sin, x_sin+1.6*y, self.fdm.magnet.postproc.batch_postproc.plot2d.linestyle, label=label, color = color) #temporary for missing hs_val
826 plt.plot(x, y, self.fdm.magnet.postproc.batch_postproc.plot2d.linestyle, label=label, color = color)
827 else:
828 # If combined_plot is false, plot data from each simulation in a separate figure:
829 plt.figure()
830 for y, label in zip(y_vals_per_sim, labels_per_sim):
831 plt.plot(x, y, self.fdm.magnet.postproc.batch_postproc.plot2d.linestyle, label=label)
833 # Set the plot options
834 if self.fdm.magnet.postproc.batch_postproc.plot2d.legend:
835 plt.legend()
836 plt.grid()
837 plt.xlabel(self.fdm.magnet.postproc.batch_postproc.plot2d.xlabel)
838 plt.ylabel(self.fdm.magnet.postproc.batch_postproc.plot2d.ylabel)
839 plt.title(title if self.fdm.magnet.postproc.batch_postproc.plot2d.combined_plot else titles[i])
842 if not self.fdm.magnet.postproc.batch_postproc.plot2d.combined_plot and self.fdm.magnet.postproc.batch_postproc.plot2d.save_plot:
843 # If combined_plot is false we expect a lot of plots and save them all into a folder:
844 filename = self.fdm.magnet.postproc.batch_postproc.plot2d.filename
845 plt.savefig(os.path.join(folder_path, filename+f"_{i}.png"))
847 if self.fdm.magnet.postproc.batch_postproc.plot2d.combined_plot and self.fdm.magnet.postproc.batch_postproc.plot2d.save_plot:
848 # If combined_plot is true we expect only one plot and save it in the main folder:
849 filename = self.fdm.magnet.postproc.batch_postproc.plot2d.filename
850 filePath = create_non_overwriting_filepath(self.outputs_folder_path, filename, '.png', self.fdm.run.overwrite)
851 plt.savefig(filePath, dpi=300)
853 if self.fdm.run.launch_gui: plt.show()