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

268 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2025-01-14 02:37 +0100

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_iron_curves = {1: set(), 2: set(), 3: set(), 4: set()} 

38 self.strands = None 

39 self.crns = None 

40 self.avg_temperatures = pd.DataFrame() 

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

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

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

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

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

46 self.postproc_settings = pd.DataFrame() 

47 

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

49 'temperature': 'T'} 

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

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

52 pass 

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

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

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

56 self.physical_quantities_abbreviations = \ 

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

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

59 self.physical_quantity = None 

60 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" 

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

62 self.map2d_headline_names = [] 

63 

64 def prepare_settings(self, settings): 

65 self.postproc_settings = pd.DataFrame({ 

66 'variables': settings.variables, 

67 'volumes': settings.volumes}) 

68 if 'compare_to_ROXIE' in settings.dict(): 

69 self.physical_quantity = 'magnetic_flux_density' 

70 else: 

71 self.physical_quantity = 'temperature' 

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

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

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

75 

76 if settings.plot_all != 'false': 

77 self.fiqus = None 

78 self.roxie = None 

79 fig1 = plt.figure(1) 

80 self.ax = fig1.add_subplot() 

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

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

83 # self.ax.set_xlim(0, 0.09) # todo 

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

85 

86 if not settings.dict().get('take_average_conductor_temperature', False): 

87 if settings.dict().get('compare_to_ROXIE', False): 

88 fig2 = plt.figure(2) 

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

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

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

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

93 self.fig4 = plt.figure(4) 

94 self.ax4 = plt.axes() 

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

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

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

98 fig3 = plt.figure(3) 

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

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

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

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

103 

104 if 'compare_to_ROXIE' in settings.dict() and 'b' in settings.variables: 

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

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

107 elif 'T' in settings.variables: 

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

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

110 

111 gmsh.open(file_to_open) 

112 

113 def clear(self): 

114 self.md = dM.MultipoleData() 

115 self.rm = rM.RegionsModel() 

116 plt.close('all') 

117 gmsh.clear() 

118 

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

120 if gui: 

121 self.gu.launch_interactive_GUI() 

122 else: 

123 gmsh.clear() 

124 gmsh.finalize() 

125 

126 def loadAuxiliaryFile(self, run_type): 

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

128 

129 def loadRegionFile(self): 

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

131 

132 def loadStrandPositions(self, run_type): 

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

134 

135 def loadHalfTurnCornerPositions(self): 

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

137 

138 def plotHalfTurnGeometry(self, compare_to_ROXIE): 

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

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

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

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

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

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

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

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

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

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

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

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

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

152 

153 if compare_to_ROXIE: 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

172 cc_roxie = Func.centroid( 

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

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

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

176 

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

178 

179 def postProcess(self, postproc): 

180 df_ref = pd.DataFrame() 

181 model_file_extension = 'EM' if 'compare_to_ROXIE' in postproc.dict() else 'TH' 

182 

183 if postproc.dict().get('compare_to_ROXIE', False): 

184 # flag_self_field = False 

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

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

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

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

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

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

191 if postproc.plot_all != 'false': 

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

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

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

195 if os.path.isfile(path_cond2d): 

196 conductorPositionsList = RoxieParsers.parseCond2d(path_cond2d) 

197 

198 # Collect strands coordinates 

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

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

201 

202 # Probe physical quantity values from view and region areas 

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

204 cond_areas, current_signs = [], [] 

205 if postproc.dict().get('take_average_conductor_temperature', False): 

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

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

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

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

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

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

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

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

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

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

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

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

218 else: 

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

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

221 

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

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

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

225 

226 view = gmsh.view.getTags()[0] 

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

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

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

230 

231 # Print update 

232 if is_new_block: 

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

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

235 

236 # Probe 

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

238 if 'compare_to_ROXIE' in postproc.dict(): 

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

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

241 else: 

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

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

244 

245 # Plot conductor and block identifiers 

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

247 if is_new_conductor: 

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

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

250 if is_new_block: 

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

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

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

254 

255 # Get current sign 

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

257 

258 # Get region area 

259 if is_new_conductor: 

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

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

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

263 gmsh.plugin.run("MeshVolume") 

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

265 else: 

266 cond_areas.append(cond_areas[-1]) 

267 

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

269 

270 # Assemble map2d content 

271 strands_nr = 0 

272 content = [] 

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

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

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

276 content.append(self.formatted_content.format( 

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

278 int(ht), # cond 

279 int(i + 1), # no 

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

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

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

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

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

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

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

287 

288 # Save map2d file 

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

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

291 file.writelines(content) 

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

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

294 if not postproc.dict().get('compare_to_ROXIE', True): 

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

296 

297 # Compute errors 

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

299 if postproc.dict().get('compare_to_ROXIE', False): 

300 BB_err = pq - BB_roxie 

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

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

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

304 

305 if postproc.plot_all != 'false': 

306 if postproc.dict().get('take_average_conductor_temperature', False): 

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

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

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

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

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

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

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

314 collection = PatchCollection(ht_polygons) 

315 self.ax.add_collection(collection) 

316 cmap = plt.get_cmap('plasma') 

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

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

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

320 self.ax.autoscale_view() 

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

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

323 if postproc.plot_all == 'true': 

324 plt.pause(0.05) 

325 

326 else: 

327 self.plotHalfTurnGeometry(postproc.dict().get('compare_to_ROXIE', False)) 

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

329 

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

331 

332 if postproc.dict().get('compare_to_ROXIE', False): 

333 if os.path.isfile(path_cond2d): 

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

335 for corners in conductors_corners: 

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

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

338 

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

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

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

342 

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

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

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

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

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

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

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

350 

351 if postproc.plot_all == 'true': 

352 plt.show() 

353 

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

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

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

357 

358 def completeMap2d(self): 

359 def _quadrant(x, y): 

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

361 elif x < 0: return 2 

362 elif y < 0: return 4 

363 else: return 1 

364 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

380 else: 

381 mirror_components = {} 

382 

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

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

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

386 

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

388 file_content = file.read() 

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

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

391 prev_block: int = 0 

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

393 entries = row.split() 

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

395 qdr = _quadrant(x_coord, y_coord) 

396 if qdr in mirror_components: 

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

398 #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] 

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

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

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

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

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

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

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

406 prev_block = int(entries[0]) 

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

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

409 file.writelines(new_content) 

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