Coverage for fiqus/post_processors/PostProcessMultipole.py: 11%

275 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2026-01-24 01:38 +0000

1import os 

2from pathlib import Path 

3import gmsh 

4import json 

5import numpy as np 

6import pandas as pd 

7import matplotlib.pyplot as plt 

8import matplotlib.lines as lines 

9import matplotlib.patches as patches 

10from matplotlib.collections import PatchCollection 

11 

12from fiqus.utils.Utils import GmshUtils 

13from fiqus.utils.Utils import GeometricFunctions as Func 

14from fiqus.utils.Utils import RoxieParsers 

15from fiqus.utils.Utils import FilesAndFolders as Util 

16from fiqus.data import DataFiQuS as dF 

17from fiqus.data import DataMultipole as dM 

18from fiqus.data import RegionsModelFiQuS as rM 

19 

20 

21class PostProcess: 

22 def __init__(self, data: dF.FDM() = None, solution_folder: str = None, verbose: bool = False): 

23 """ 

24 Class to post process results 

25 :param data: FiQuS data model 

26 :param verbose: If True more information is printed in python console. 

27 """ 

28 self.data: dF.FDM() = data 

29 self.solution_folder = solution_folder 

30 self.verbose: bool = verbose 

31 self.md = dM.MultipoleData() 

32 self.rm = rM.RegionsModel() 

33 

34 self.gu = GmshUtils(self.solution_folder, self.verbose) 

35 self.gu.initialize(verbosity_Gmsh=self.data.run.verbosity_Gmsh) 

36 

37 self.brep_curves = {} 

38 for name in self.data.magnet.geometry.electromagnetics.areas: 

39 self.brep_curves[name] = {1: set(), 2: set(), 3: set(), 4: set()} 

40 self.strands = None 

41 self.crns = None 

42 self.avg_temperatures = pd.DataFrame() 

43 self.postprocess_parameters = dict.fromkeys(['overall_error', 'minimum_diff', 'maximum_diff']) 

44 self.mesh_folder = os.path.dirname(self.solution_folder) 

45 self.geom_files = os.path.join(os.path.dirname(self.mesh_folder), self.data.general.magnet_name) 

46 self.mesh_files = os.path.join(self.mesh_folder, self.data.general.magnet_name) 

47 self.model_file = os.path.join(self.solution_folder, self.data.general.magnet_name) 

48 self.postproc_settings = pd.DataFrame() 

49 

50 self.supported_variables = {'magnetic_flux_density': 'b', 

51 'temperature': 'T'} 

52 if any([var not in self.supported_variables.values() 

53 for var in self.data.magnet.postproc.electromagnetics.variables + self.data.magnet.postproc.thermal.variables]): 

54 pass 

55 # raise Exception(f"The interpolation of the field at the strands locations can not be executed: " 

56 # f"a variable listed in 'post_processors' -> 'variables' is not supported. " 

57 # f"Supported variables are: {self.supported_variables.values()}") 

58 self.physical_quantities_abbreviations = \ 

59 {'magnetic_flux_density': ('BX/T', 'BY/T'), 

60 'temperature': ('T/K', '-')} 

61 self.physical_quantity = None 

62 self.formatted_headline = "{0:>5}{1:>8}{2:>7}{3:>12}{4:>13}{5:>8}{6:>11}{7:>16}{8:>8}{9:>10}\n\n" 

63 self.formatted_content = "{0:>6}{1:>6}{2:>7}{3:>13}{4:>13}{5:>11}{6:>11}{7:>11}{8:>9}{9:>8}\n" 

64 self.map2d_headline_names = [] 

65 

66 def prepare_settings(self, settings): 

67 self.postproc_settings = pd.DataFrame({ 

68 'variables': settings.variables, 

69 'volumes': settings.volumes}) 

70 if 'compare_to_ROXIE' in settings.model_dump(): 

71 self.physical_quantity = 'magnetic_flux_density' 

72 else: 

73 self.physical_quantity = 'temperature' 

74 self.map2d_headline_names = ['BL.', 'COND.', 'NO.', 'X-POS/MM', 'Y-POS/MM'] + \ 

75 [abbr for abbr in self.physical_quantities_abbreviations[self.physical_quantity]] + \ 

76 ['AREA/MM**2', 'CURRENT', 'FILL FAC.'] 

77 

78 if settings.plot_all != 'false': 

79 self.fiqus = None 

80 self.roxie = None 

81 fig1 = plt.figure(1) 

82 self.ax = fig1.add_subplot() 

83 self.ax.set_xlabel('x [cm]') # adjust other plots to cm 

84 self.ax.set_ylabel('y [cm]') 

85 # self.ax.set_xlim(0, 0.09) 

86 # self.ax.set_ylim(0, 0.09) 

87 

88 if not settings.model_dump().get('take_average_conductor_temperature', False): 

89 if settings.model_dump().get('compare_to_ROXIE', False): 

90 fig2 = plt.figure(2) 

91 self.ax2 = fig2.add_subplot(projection='3d') 

92 self.ax2.set_xlabel('x [m]') 

93 self.ax2.set_ylabel('y [m]') 

94 self.ax2.set_zlabel('Absolute Error [T]') 

95 self.fig4 = plt.figure(4) 

96 self.ax4 = plt.axes() 

97 self.ax4.set_xlabel('x [cm]') 

98 self.ax4.set_ylabel('y [cm]') 

99 self.ax4.set_aspect('equal', 'box') 

100 fig3 = plt.figure(3) 

101 self.ax3 = fig3.add_subplot(projection='3d') 

102 self.ax3.set_xlabel('x [m]') 

103 self.ax3.set_ylabel('y [m]') 

104 self.ax3.set_zlabel('norm(B) [T]' if 'compare_to_ROXIE' in settings.model_dump() else '') 

105 

106 if 'compare_to_ROXIE' in settings.model_dump() and 'b' in settings.variables: 

107 b_index = settings.variables.index('b') 

108 file_to_open = os.path.join(self.solution_folder, f"b_{settings.volumes[b_index]}.pos") 

109 gmsh.open(file_to_open) 

110 elif 'T' in settings.variables: 

111 T_index = settings.variables.index('T') 

112 file_to_open = os.path.join(self.solution_folder, f"T_{settings.volumes[T_index]}.pos") 

113 gmsh.open(file_to_open) 

114 

115 

116 

117 

118 def clear(self): 

119 self.md = dM.MultipoleData() 

120 self.rm = rM.RegionsModel() 

121 plt.close('all') 

122 gmsh.clear() 

123 

124 def ending_step(self, gui: bool = False): 

125 if gui: 

126 self.gu.launch_interactive_GUI() 

127 else: 

128 if gmsh.isInitialized(): 

129 gmsh.clear() 

130 gmsh.finalize() 

131 

132 def loadAuxiliaryFile(self, run_type): 

133 self.md = Util.read_data_from_yaml(f"{self.mesh_files}_{run_type}.aux", dM.MultipoleData) 

134 

135 def loadRegionFile(self): 

136 self.rm = Util.read_data_from_yaml(f"{self.mesh_files}_TH.reg", rM.RegionsModel) 

137 

138 def loadStrandPositions(self, run_type): 

139 self.strands = json.load(open(f"{self.geom_files}_{run_type}.strs")) 

140 

141 def loadHalfTurnCornerPositions(self): 

142 self.crns = json.load(open(f"{self.geom_files}.crns")) 

143 

144 def plotHalfTurnGeometry(self, compare_to_ROXIE): 

145 for i in range(len(self.crns['iH'])): 

146 self.ax.add_line(lines.Line2D([self.crns['iH'][i][0], self.crns['iL'][i][0]], 

147 [self.crns['iH'][i][1], self.crns['iL'][i][1]], color='green')) 

148 self.ax.add_line(lines.Line2D([self.crns['oH'][i][0], self.crns['oL'][i][0]], 

149 [self.crns['oH'][i][1], self.crns['oL'][i][1]], color='green')) 

150 self.ax.add_line(lines.Line2D([self.crns['oL'][i][0], self.crns['iL'][i][0]], 

151 [self.crns['oL'][i][1], self.crns['iL'][i][1]], color='green')) 

152 self.ax.add_line(lines.Line2D([self.crns['iH'][i][0], self.crns['oH'][i][0]], 

153 [self.crns['iH'][i][1], self.crns['oH'][i][1]], color='green')) 

154 cc_fiqus = Func.centroid([self.crns['iH'][i][0], self.crns['iL'][i][0], 

155 self.crns['oL'][i][0], self.crns['oH'][i][0]], 

156 [self.crns['iH'][i][1], self.crns['iL'][i][1], 

157 self.crns['oL'][i][1], self.crns['oH'][i][1]]) 

158 

159 if compare_to_ROXIE: 

160 self.ax.add_line(lines.Line2D([self.crns['iHr'][i][0], self.crns['iLr'][i][0]], 

161 [self.crns['iHr'][i][1], self.crns['iLr'][i][1]], 

162 color='red', linestyle='dashed')) 

163 self.ax.add_line(lines.Line2D([self.crns['oHr'][i][0], self.crns['oLr'][i][0]], 

164 [self.crns['oHr'][i][1], self.crns['oLr'][i][1]], 

165 color='red', linestyle='dashed')) 

166 self.ax.add_line(lines.Line2D([self.crns['oLr'][i][0], self.crns['iLr'][i][0]], 

167 [self.crns['oLr'][i][1], self.crns['iLr'][i][1]], 

168 color='red', linestyle='dashed')) 

169 self.ax.add_line(lines.Line2D([self.crns['iHr'][i][0], self.crns['oHr'][i][0]], 

170 [self.crns['iHr'][i][1], self.crns['oHr'][i][1]], 

171 color='red', linestyle='dashed')) 

172 self.ax.text((self.crns['oLr'][i][0] + self.crns['iLr'][i][0]) / 2, 

173 (self.crns['oLr'][i][1] + self.crns['iLr'][i][1]) / 2, 

174 'R' + str(i + 1), style='italic', bbox={'facecolor': 'red', 'pad': 2}) 

175 self.ax.text((self.crns['iHr'][i][0] + self.crns['oHr'][i][0]) / 2, 

176 (self.crns['iHr'][i][1] + self.crns['oHr'][i][1]) / 2, 

177 'L' + str(i + 1), style='italic', bbox={'facecolor': 'red', 'pad': 2}) 

178 cc_roxie = Func.centroid( 

179 [self.crns['iHr'][i][0], self.crns['iLr'][i][0], self.crns['oLr'][i][0], self.crns['oHr'][i][0]], 

180 [self.crns['iHr'][i][1], self.crns['iLr'][i][1], self.crns['oLr'][i][1], self.crns['oHr'][i][1]]) 

181 self.roxie = self.ax.scatter(cc_roxie[0], cc_roxie[1], edgecolor='r', facecolor='none') 

182 

183 self.fiqus = self.ax.scatter(cc_fiqus[0], cc_fiqus[1], c="green") 

184 

185 def postProcess(self, postproc): 

186 df_ref = pd.DataFrame() 

187 model_file_extension = 'EM' if 'compare_to_ROXIE' in postproc.model_dump() else 'TH' 

188 

189 if postproc.model_dump().get('compare_to_ROXIE', False): 

190 # flag_self_field = False 

191 # path_map2d = Path(postproc.compare_to_ROXIE, "MQXA_All_" + 

192 # f"{'WithIron_' if self.data.magnet.geometry.with_iron_yoke else 'NoIron_'}" + 

193 # f"{'WithSelfField' if flag_self_field else 'NoSelfField'}" + 

194 # f"{'' if flag_contraction else '_no_contraction'}" + ".map2d") 

195 df_ref = RoxieParsers.parseMap2d(map2dFile=Path(postproc.compare_to_ROXIE), physical_quantity='magnetic_flux_density') 

196 BB_roxie = np.linalg.norm(df_ref[['BX/T', 'BY/T']].values, axis=1) 

197 if postproc.plot_all != 'false': 

198 path_cond2d = Path(os.path.join(os.path.dirname(postproc.compare_to_ROXIE), self.data.general.magnet_name + ".cond2d")) 

199 # path_cond2d = Path(os.path.dirname(postproc.compare_to_ROXIE), "MQXA_All_NoIron_NoSelfField" + 

200 # f"{'' if flag_contraction else '_no_contraction'}" + ".cond2d") 

201 if os.path.isfile(path_cond2d): 

202 conductorPositionsList = RoxieParsers.parseCond2d(path_cond2d) 

203 

204 # Collect strands coordinates 

205 strands_x = df_ref['X-POS/MM'] / 1e3 if postproc.model_dump().get('compare_to_ROXIE', False) else self.strands['x'] 

206 strands_y = df_ref['Y-POS/MM'] / 1e3 if postproc.model_dump().get('compare_to_ROXIE', False) else self.strands['y'] 

207 

208 # Probe physical quantity values from view and region areas 

209 physical_quantity_values = {'x': [], 'y': []} 

210 cond_areas, current_signs = [], [] 

211 if postproc.model_dump().get('take_average_conductor_temperature', False): 

212 half_turns = {name[:-3]: str(values.vol.numbers[i]) 

213 for group, values in self.rm.powered.items() for i, name in enumerate(values.vol.names)} 

214 self.avg_temperatures = pd.concat([pd.read_csv(os.path.join(self.solution_folder, 'T_avg', 'T_avg_0.txt'), 

215 delimiter=r'\s+', header=None, usecols=[0], names=['Time'])] + 

216 [pd.read_csv(os.path.join(self.solution_folder, 'T_avg', f'T_avg_{i}.txt'), 

217 delimiter=r'\s+', header=None, usecols=[1], names=[ht.upper()]) for i, ht in enumerate(half_turns)], axis=1) 

218 self.avg_temperatures['Time'] = pd.read_csv(os.path.join(self.solution_folder, 'T_avg', 'T_avg_0.txt'), 

219 delimiter=r'\s+', header=None, usecols=[0], names=['Time'])['Time'] 

220 self.avg_temperatures = self.avg_temperatures[['Time'] + ['HT' + str(i) for i in range(1, self.strands['ht'][-1] + 1)]] 

221 columns_to_format = self.avg_temperatures.columns[1:] 

222 self.avg_temperatures[columns_to_format] = self.avg_temperatures[columns_to_format].round(4) 

223 self.avg_temperatures.to_csv(os.path.join(self.solution_folder, 'half_turn_temperatures_over_time.csv'), index=False) 

224 else: 

225 print(f"Info : {self.data.general.magnet_name} - I n t e r p o l a t i n g . . .") 

226 print(f"Info : Interpolating {'magnetic flux density' if 'compare_to_ROXIE' in postproc.model_dump() else 'temperature'} ...") 

227 

228 # view = gmsh.view.getTags()[0] if len(postproc.variables) == 1 else self.postproc_settings[ 

229 # (self.postproc_settings['variables'] == self.supported_variables[self.physical_quantity]) & 

230 # (self.postproc_settings['volumes'] == 'Omega_p')].index[0] 

231 

232 try: view = gmsh.view.getTags()[0] 

233 except IndexError: 

234 print("Error with post processing") 

235 return 

236 

237 for i in range(len(strands_x)): 

238 is_new_conductor = i == 0 or self.strands['ht'][i] != self.strands['ht'][i - 1] 

239 is_new_block = i == 0 or self.strands['block'][i] != self.strands['block'][i - 1] 

240 

241 # Print update 

242 if is_new_block: 

243 perc = round(self.strands['block'][i] / self.strands['block'][-1] * 100) 

244 print(f"Info : [{' ' if perc < 10 else ''}{' ' if perc < 100 else ''}{perc}%] Interpolating within block {self.strands['block'][i]}") 

245 

246 # Probe 

247 probe_data = gmsh.view.probe(view, strands_x[i], strands_y[i], 0)[0] 

248 if 'compare_to_ROXIE' in postproc.model_dump(): 

249 physical_quantity_values['x'].append(probe_data[0]) 

250 physical_quantity_values['y'].append(probe_data[1]) 

251 else: 

252 physical_quantity_values['x'].append(probe_data[0]) 

253 physical_quantity_values['y'].append(0) 

254 

255 # Plot conductor and block identifiers 

256 if postproc.model_dump().get('compare_to_ROXIE', False) and postproc.plot_all != 'false': 

257 if is_new_conductor: 

258 self.ax.text(df_ref['X-POS/MM'][i] / 1e3, df_ref['Y-POS/MM'][i] / 1e3, str(self.strands['ht'][i]), 

259 style='italic', bbox={'facecolor': 'blue', 'pad': 3}) 

260 if is_new_block: 

261 mid_strand_index = round(self.strands['ht'].count(self.strands['ht'][i]) / 2) 

262 self.ax.text(df_ref['X-POS/MM'][i + mid_strand_index] / 1e3, df_ref['Y-POS/MM'][i + mid_strand_index] / 1e3, 

263 str(self.strands['block'][i]), style='italic', bbox={'facecolor': 'green', 'pad': 3}) 

264 

265 # Get current sign 

266 current_signs.append(self.md.domains.physical_groups.blocks[self.strands['block'][i]].current_sign) 

267 

268 # Get region area 

269 if is_new_conductor: 

270 gmsh.plugin.setNumber("MeshVolume", "Dimension", 2) 

271 gmsh.plugin.setNumber("MeshVolume", "PhysicalGroup", 

272 self.md.domains.physical_groups.blocks[self.strands['block'][i]].half_turns[self.strands['ht'][i]].tag) 

273 gmsh.plugin.run("MeshVolume") 

274 cond_areas.append(gmsh.view.getListData(gmsh.view.getTags()[-1])[2][-1][-1]) 

275 else: 

276 cond_areas.append(cond_areas[-1]) 

277 

278 print(f"Info : {self.data.general.magnet_name} - E n d I n t e r p o l a t i n g") 

279 

280 # Assemble map2d content 

281 strands_nr = 0 

282 content = [] 

283 for i, ht in enumerate(self.strands['ht']): 

284 if i == 0 or ht != self.strands['ht'][i - 1]: 

285 strands_nr = self.strands['ht'].count(ht) 

286 content.append(self.formatted_content.format( 

287 int(self.strands['block'][i]), # bl 

288 int(ht), # cond 

289 int(i + 1), # no 

290 f"{strands_x[i] * 1e3:.4f}", # x 

291 f"{strands_y[i] * 1e3:.4f}", # y 

292 f"{physical_quantity_values['x'][i]:.4f}", # pq_x 

293 f"{physical_quantity_values['y'][i]:.4f}", # pq_y 

294 f"{cond_areas[i] / strands_nr * 1e6:.4f}", # area 

295 f"{current_signs[i] * self.data.power_supply.I_initial / strands_nr:.2f}", # curr 

296 f"{df_ref['FILL FAC.'][i] if postproc.model_dump().get('compare_to_ROXIE', False) else 0:.4f}")) # fill_fac 

297 

298 # Save map2d file 

299 with open(f"{self.model_file}_{model_file_extension}.map2d", 'w') as file: 

300 file.write(self.formatted_headline.format(*self.map2d_headline_names)) 

301 file.writelines(content) 

302 print(f"Info : Map2D file saved.") 

303 print(f"WARNING : [Map2D] All strand surface areas are equal within a conductor. Refer to the ROXIE map2d file for actual values") 

304 if not postproc.model_dump().get('compare_to_ROXIE', True): 

305 print(f"WARNING : [Map2D] No data is available for Filling Factor. Refer to the ROXIE map2d file for correct values") 

306 

307 # Compute errors 

308 pq = np.linalg.norm(np.column_stack((np.array(physical_quantity_values['x']), np.array(physical_quantity_values['y']))), axis=1) 

309 if postproc.model_dump().get('compare_to_ROXIE', False): 

310 BB_err = pq - BB_roxie 

311 self.postprocess_parameters['overall_error'] = np.mean(abs(BB_err)) 

312 self.postprocess_parameters['minimum_diff'] = np.min(BB_err) 

313 self.postprocess_parameters['maximum_diff'] = np.max(BB_err) 

314 

315 if postproc.plot_all != 'false': 

316 if postproc.model_dump().get('take_average_conductor_temperature', False): 

317 min_value = self.avg_temperatures.iloc[:, 1:].min().min() 

318 max_value = self.avg_temperatures.iloc[:, 1:].max().max() 

319 ht_polygons = [patches.Polygon(np.array([(self.crns['iHr'][i][0], self.crns['iHr'][i][1]), 

320 (self.crns['iLr'][i][0], self.crns['iLr'][i][1]), 

321 (self.crns['oLr'][i][0], self.crns['oLr'][i][1]), 

322 (self.crns['oHr'][i][0], self.crns['oHr'][i][1])]) * 1e2, 

323 closed=True) for i in range(len(self.crns['iHr']))] 

324 collection = PatchCollection(ht_polygons) 

325 self.ax.add_collection(collection) 

326 cmap = plt.get_cmap('plasma') 

327 norm = plt.Normalize(vmin=min_value, vmax=max_value) 

328 cbar = plt.colorbar(plt.cm.ScalarMappable(cmap=cmap, norm=norm), ax=self.ax) 

329 cbar.set_label('Temperature [K]') 

330 self.ax.autoscale_view() 

331 for i in range(self.avg_temperatures['Time'].size): 

332 collection.set_facecolor(cmap(norm(self.avg_temperatures.iloc[i, 1:]))) 

333 if postproc.plot_all == 'true': 

334 plt.pause(0.05) 

335 

336 else: 

337 self.plotHalfTurnGeometry(postproc.model_dump().get('compare_to_ROXIE', False)) 

338 map2d_strands = self.ax.scatter(strands_x, strands_y, edgecolor='black', facecolor='black', s=10) 

339 

340 scatter3D_pos = self.ax3.scatter3D(strands_x, strands_y, pq, c=pq, cmap='Greens', vmin=0, vmax=10) 

341 

342 if postproc.model_dump().get('compare_to_ROXIE', False): 

343 if os.path.isfile(path_cond2d): 

344 conductors_corners = [condPos.xyCorner for condPos in conductorPositionsList] 

345 for corners in conductors_corners: 

346 for corner in range(len(corners)): 

347 self.ax.scatter(corners[corner][0] / 1e3, corners[corner][1] / 1e3, edgecolor='black', facecolor='black', s=10) 

348 

349 self.ax2.scatter3D(strands_x, strands_y, BB_err, c=BB_err, cmap='viridis') # , vmin=-0.2, vmax=0.2) 

350 scatter4 = self.ax4.scatter(np.array(strands_x) * 1e2, np.array(strands_y) * 1e2, s=1, c=np.array(BB_err) * 1e3, cmap='viridis') 

351 scatter3D_pos_roxie = self.ax3.scatter3D(strands_x, strands_y, BB_roxie, c=BB_roxie, cmap='Reds', vmin=0, vmax=10) 

352 

353 cax4 = self.fig4.add_axes((self.ax4.get_position().x1 + 0.02, self.ax4.get_position().y0, 

354 0.02, self.ax4.get_position().height)) 

355 cbar = plt.colorbar(scatter4, cax=cax4) 

356 cbar.ax.set_ylabel('Absolute error [mT]', rotation=270) 

357 self.ax3.legend([scatter3D_pos, scatter3D_pos_roxie], ['FiQuS', 'ROXIE'], numpoints=1) 

358 self.ax.legend([self.fiqus, self.roxie, map2d_strands], ['FiQuS', 'ROXIE'], numpoints=1) 

359 self.fig4.savefig(f"{os.path.join(self.solution_folder, self.data.general.magnet_name)}.svg", bbox_inches='tight') 

360 

361 if postproc.plot_all == 'true': 

362 plt.show() 

363 

364 # os.remove(os.path.join(self.solution_folder, 'b_Omega_p.pos')) 

365 # os.remove(f"{os.path.join(self.solution_folder, self.data.general.magnet_name)}.pre") 

366 # os.remove(f"{os.path.join(os.path.dirname(self.solution_folder), self.data.general.magnet_name)}.msh") 

367 

368 def completeMap2d(self): 

369 def _quadrant(x, y): 

370 if x < 0 and y < 0: return 3 

371 elif x < 0: return 2 

372 elif y < 0: return 4 

373 else: return 1 

374 

375 if self.data.magnet.geometry.electromagnetics.symmetry == 'xy': 

376 if self.strands['poles'] == 2: 

377 mirror_components = {2: [-1, 1], 3: [1, 1], 4: [-1, 1]} 

378 elif self.strands['poles'] == 4: 

379 mirror_components = {2: [1, -1], 3: [-1, -1], 4: [-1, 1]} 

380 elif self.data.magnet.geometry.electromagnetics.symmetry == 'x': 

381 if self.strands['poles'] == 2: 

382 mirror_components = {3: [-1, 1], 4: [-1, 1]} 

383 elif self.strands['poles'] == 4: 

384 mirror_components = {3: [-1, 1], 4: [-1, 1]} 

385 elif self.data.magnet.geometry.electromagnetics.symmetry == 'y': 

386 if self.strands['poles'] == 2: 

387 mirror_components = {2: [-1, 1], 3: [-1, 1]} 

388 elif self.strands['poles'] == 4: 

389 mirror_components = {2: [1, -1], 3: [1, -1]} 

390 else: 

391 mirror_components = {} 

392 

393 print(f"Info : {self.data.general.magnet_name} - M i r r o r i n g . . .") 

394 print(f"Info : Mirroring by symmetry ...") 

395 blocks_nr = self.strands['block'][-1] 

396 

397 with open(f"{self.model_file}_EM.map2d", 'r') as file: 

398 file_content = file.read() 

399 content_by_row = file_content.split('\n') 

400 new_content = [content_by_row[0] + '\n' + content_by_row[1] + '\n'] 

401 prev_block: int = 0 

402 for row in content_by_row[2:-1]: 

403 entries = row.split() 

404 str_nr, x_coord, y_coord = entries[2], float(entries[3]), float(entries[4]) 

405 qdr = _quadrant(x_coord, y_coord) 

406 if qdr in mirror_components: 

407 #found = re.search(f" {str(abs(x_coord))} +{str(abs(y_coord))}", file_content) 

408 #BB = [row_ref for row_ref in content_by_row if f" {abs(x_coord):.4f} {abs(y_coord):.4f}" in row_ref][0].split()[5:7] 

409 BB = content_by_row[self.strands['mirrored'][str_nr] + 1].split()[5:7] 

410 row = row.replace(entries[5], f'{mirror_components[qdr][0] * float(BB[0]):.4f}') 

411 row = row.replace(entries[6], f'{mirror_components[qdr][1] * float(BB[1]):.4f}') 

412 if int(entries[0]) > prev_block: 

413 perc = round(int(entries[0]) / blocks_nr * 100) 

414 print("Info : [" + f"{' ' if perc < 10 else ''}" + f"{' ' if perc < 100 else ''}" + f"{perc}" + 

415 "%] Mirroring within block" + f"{entries[0]}") 

416 prev_block = int(entries[0]) 

417 new_content.append(row + '\n') 

418 with open(f"{self.model_file}_EM.map2d", 'w') as file: 

419 file.writelines(new_content) 

420 print(f"Info : {self.data.general.magnet_name} - E n d M i r r o r i n g")