Coverage for fiqus/mesh_generators/MeshMultipole.py: 86%

978 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2025-11-03 01:32 +0000

1import os 

2import gmsh 

3import copy 

4import numpy as np 

5import json 

6import logging 

7 

8from fiqus.utils.Utils import GmshUtils 

9from fiqus.utils.Utils import FilesAndFolders as Util 

10from fiqus.utils.Utils import GeometricFunctions as Func 

11from fiqus.data import DataFiQuS as dF 

12from fiqus.data import DataMultipole as dM 

13from fiqus.data import RegionsModelFiQuS as rM 

14 

15logger = logging.getLogger(__name__) 

16 

17class Mesh: 

18 def __init__(self, data: dF.FDM() = None, mesh_folder: str = None, 

19 verbose: bool = False): 

20 """ 

21 Class to generate mesh 

22 :param data: FiQuS data model 

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

24 """ 

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

26 self.mesh_folder = mesh_folder 

27 self.verbose: bool = verbose 

28 

29 self.md = dM.MultipoleData() 

30 self.rc = dM.MultipoleRegionCoordinate() 

31 self.rm = rM.RegionsModel() 

32 self.strands = None 

33 

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

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

36 self.occ = gmsh.model.occ 

37 self.mesh = gmsh.model.mesh 

38 

39 self.brep_iron_curves = {1: set(), 2: set(), 3: set(), 4: set()} 

40 self.mesh_parameters = dict.fromkeys(['SJ', 'SICN', 'SIGE', 'Gamma', 'nodes']) 

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

42 self.model_file = os.path.join(self.mesh_folder, self.data.general.magnet_name) 

43 

44 # Insulation sequence involving cable insulation only (turn-to-turn, outlying conductor edge) 

45 self.ins_type_cond = {} 

46 # Insulation sequence involving quench heaters (outlying or mid-layer/pole) 

47 qh_keys = {key: {} for key in range(1, self.data.quench_protection.quench_heaters.N_strips + 1)} 

48 self.ins_type_qh = {'internal_double': {}, 'internal': copy.deepcopy(qh_keys), 'external': copy.deepcopy(qh_keys)} 

49 # Insulation sequence between blocks (layer-to-layer, pole-to-pole) 

50 self.ins_type = {'mid_pole': {}, 'mid_winding': {}, 'mid_layer': {}, 'aux': {}} 

51 self.qh_data, self.wedge_cond = {}, {} 

52 

53 self.colors = {'wedges': [86, 180, 233], # sky blue 

54 'insul': [119, 136, 153], # light slate grey 

55 'half_turns_pos': [213, 94, 0], # vermilion 

56 'half_turns_neg': [255, 136, 42], # light vermilion 

57 'air': [240, 228, 66], # yellow 

58 'air_inf': [220, 208, 46], # dark yellow 

59 # yoke 

60 'BHiron1': [0, 114, 178], # blue 

61 'BHiron2': [0, 158, 115], # bluish green 

62 'BHiron4': [86, 180, 233], # sky blue 

63 # key 

64 'BHiron3': [220, 208, 46], # dark yellow 

65 # [230, 159, 0], # orange 

66 # collar 

67 'BHiron5': [204, 121, 167], # hopbush 

68 'BHiron6': [0, 114, 178], # blue 

69 'BHiron7': [204, 121, 167]} # reddish purple 

70 

71 def clear(self): 

72 self.md = dM.MultipoleData() 

73 self.rc = dM.MultipoleRegionCoordinate() 

74 self.rm = rM.RegionsModel() 

75 gmsh.clear() 

76 

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

78 if gui: 

79 self.gu.launch_interactive_GUI() 

80 else: 

81 gmsh.clear() 

82 gmsh.finalize() 

83 

84 def loadStrandPositions(self, run_type): 

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

86 

87 def loadAuxiliaryFile(self, run_type): 

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

89 

90 def updateAuxiliaryFile(self, run_type): 

91 Util.write_data_to_yaml(f'{self.model_file}_{run_type}.aux', self.md.model_dump()) 

92 

93 def saveMeshFile(self, run_type): 

94 gmsh.write(f'{self.model_file}_{run_type}.msh') 

95 

96 def saveRegionFile(self, run_type): 

97 Util.write_data_to_yaml(f'{self.model_file}_{run_type}.reg', self.rm.model_dump()) 

98 

99 def saveRegionCoordinateFile(self, run_type): 

100 Util.write_data_to_yaml(f'{self.model_file}_{run_type}.reco', self.rc.model_dump()) 

101 

102 def getIronCurvesTags(self): 

103 for quadrant, qq in self.md.geometries.iron.quadrants.items(): 

104 for _, area in qq.areas.items(): 

105 if area.surface: 

106 self.brep_iron_curves[quadrant] |= set(gmsh.model.getAdjacencies(2, area.surface)[1]) 

107 

108 def defineMesh(self, geometry, mesh, run_type): 

109 thresholds = [] 

110 self.occ.synchronize() 

111 

112 if mesh.conductors.field.enabled: 

113 distance_conductors = self.mesh.field.add("Distance") 

114 self.mesh.field.setNumbers(distance_conductors, "CurvesList", 

115 [line for coil_nr, coil in self.md.geometries.coil.anticlockwise_order.coils.items() for layer_nr, layer in coil.layers.items() 

116 for _, block_order in enumerate(layer) for _, line in self.md.geometries.coil.coils[coil_nr].poles[block_order.pole].layers[ 

117 layer_nr].windings[block_order.winding].blocks[block_order.block].half_turns.lines.items()] 

118 ) 

119 self.mesh.field.setNumber(distance_conductors, "Sampling", 100) 

120 

121 threshold_conductors = self.mesh.field.add("Threshold") 

122 self.mesh.field.setNumber(threshold_conductors, "InField", distance_conductors) 

123 self.mesh.field.setNumber(threshold_conductors, "SizeMin", mesh.conductors.field.SizeMin) 

124 self.mesh.field.setNumber(threshold_conductors, "SizeMax", mesh.conductors.field.SizeMax) 

125 self.mesh.field.setNumber(threshold_conductors, "DistMin", mesh.conductors.field.DistMin) 

126 self.mesh.field.setNumber(threshold_conductors, "DistMax", mesh.conductors.field.DistMax) 

127 self.mesh.field.setNumber(threshold_conductors, "StopAtDistMax", 1) 

128 thresholds.append(threshold_conductors) 

129 

130 if mesh.wedges.field.enabled: 

131 distance_wedges = self.mesh.field.add("Distance") 

132 self.mesh.field.setNumbers(distance_wedges, "CurvesList", 

133 [line for _, coil in self.md.geometries.wedges.coils.items() for _, layer in coil.layers.items() for _, wdg in layer.wedges.items() for _, line in wdg.lines.items()] 

134 ) 

135 self.mesh.field.setNumber(distance_wedges, "Sampling", 100) 

136 

137 # raise Exception(f"cannot set threshold for wedges field: {[line for _, coil in self.md.geometries.wedges.coils.items() for _, layer in coil.layers.items() for _, wdg in layer.wedges.items() for _, line in wdg.lines.items()]}") 

138 

139 threshold_wedges = self.mesh.field.add("Threshold") 

140 self.mesh.field.setNumber(threshold_wedges, "InField", distance_wedges) 

141 self.mesh.field.setNumber(threshold_wedges, "SizeMin", mesh.wedges.field.SizeMin) 

142 self.mesh.field.setNumber(threshold_wedges, "SizeMax", mesh.wedges.field.SizeMax) 

143 self.mesh.field.setNumber(threshold_wedges, "DistMin", mesh.wedges.field.DistMin) 

144 self.mesh.field.setNumber(threshold_wedges, "DistMax", mesh.wedges.field.DistMax) 

145 self.mesh.field.setNumber(threshold_wedges, "StopAtDistMax", 1) 

146 thresholds.append(threshold_wedges) 

147 

148 if geometry.with_iron_yoke: 

149 distance_iron = self.mesh.field.add("Distance") 

150 self.mesh.field.setNumbers(distance_iron, "CurvesList", [line for _, qq in self.brep_iron_curves.items() for line in qq]) 

151 self.mesh.field.setNumber(distance_iron, "Sampling", 100) 

152 

153 if mesh.iron_field.enabled: 

154 

155 threshold_iron = self.mesh.field.add("Threshold") 

156 self.mesh.field.setNumber(threshold_iron, "InField", distance_iron) 

157 self.mesh.field.setNumber(threshold_iron, "SizeMin", mesh.iron_field.SizeMin) 

158 self.mesh.field.setNumber(threshold_iron, "SizeMax", mesh.iron_field.SizeMax) 

159 self.mesh.field.setNumber(threshold_iron, "DistMin", mesh.iron_field.DistMin) 

160 self.mesh.field.setNumber(threshold_iron, "DistMax", mesh.iron_field.DistMax) 

161 # the iron field is typically rather coarse so that 'overwriting' other fields is not a problem 

162 # its distMax however would need to be large for StopAtDistMax to be effective 

163 #self.mesh.field.setNumber(threshold_iron, "StopAtDistMax", 1) 

164 thresholds.append(threshold_iron) 

165 

166 if run_type == 'EM' and mesh.bore_field.enabled: 

167 

168 distance_bore = self.mesh.field.add("Distance") 

169 self.mesh.field.setNumbers(distance_bore, "PointsList", [pnt for pnt_name, pnt in self.md.geometries.air.points.items() if 'bore' in pnt_name]) 

170 self.mesh.field.setNumber(distance_bore, "Sampling", 100) 

171 

172 threshold_bore = self.mesh.field.add("Threshold") 

173 self.mesh.field.setNumber(threshold_bore, "InField", distance_bore) 

174 self.mesh.field.setNumber(threshold_bore, "SizeMin", mesh.bore_field.SizeMin) 

175 self.mesh.field.setNumber(threshold_bore, "SizeMax", mesh.bore_field.SizeMax) 

176 self.mesh.field.setNumber(threshold_bore, "DistMin", mesh.bore_field.DistMin) 

177 self.mesh.field.setNumber(threshold_bore, "DistMax", mesh.bore_field.DistMax) 

178 self.mesh.field.setNumber(threshold_bore, "StopAtDistMax", 1) 

179 thresholds.append(threshold_bore) 

180 

181 insulation_mesh_fields = [] 

182 if run_type == 'TH': 

183 for coil_nr, coil in self.md.geometries.insulation.coils.items(): 

184 for _, group in coil.group.items(): 

185 # Areas 

186 for area_name, area in group.ins.areas.items(): 

187 if area_name.isdigit(): 

188 insulation_mesh_fields.append(self.mesh.field.add("Constant")) 

189 insulation_mesh_field = insulation_mesh_fields[-1] 

190 self.mesh.field.setNumbers(insulation_mesh_field, "SurfacesList", [area.surface]) 

191 self.mesh.field.setNumber(insulation_mesh_field, "VIn", mesh.insulation.global_size) 

192 

193 background = self.mesh.field.add("Min") 

194 self.mesh.field.setNumbers(background, "FieldsList", thresholds + insulation_mesh_fields) 

195 self.mesh.field.setAsBackgroundMesh(background) 

196 

197 # Apply transfinite curves and potentially surfaces to conductors and wedges 

198 if mesh.conductors.transfinite.enabled_for in ['curves', 'curves_and_surfaces']: 

199 for coil_nr, coil in self.md.geometries.coil.anticlockwise_order.coils.items(): 

200 for layer_nr, layer in coil.layers.items(): 

201 for _, block_order in enumerate(layer): 

202 winding = self.md.geometries.coil.coils[coil_nr].poles[block_order.pole].layers[ 

203 layer_nr].windings[block_order.winding] 

204 cable = self.data.conductors[winding.conductor_name].cable 

205 for line_key, line in winding.blocks[block_order.block].half_turns.lines.items(): 

206 if mesh.model_dump().get('isothermal_conductors', False): 

207 elements = 1 

208 elif any([i in line_key for i in ['i', 'o']]): 

209 elements = max(1, round(cable.bare_cable_height_mean / mesh.conductors.transfinite.curve_target_size_height)) 

210 elif any([i in line_key for i in ['l', 'h']]): 

211 elements = max(1, round(cable.bare_cable_width / mesh.conductors.transfinite.curve_target_size_width)) 

212 

213 self.mesh.setTransfiniteCurve(line, elements) 

214 if mesh.conductors.transfinite.enabled_for=='curves_and_surfaces' or mesh.model_dump().get('isothermal_conductors', False): 

215 for _, area in winding.blocks[block_order.block].half_turns.areas.items(): 

216 self.mesh.setTransfiniteSurface(area.surface) 

217 self.mesh.setRecombine(2, area.surface) 

218 

219 if 'insulation' in mesh.model_dump() and 'TSA' in mesh.model_dump()["insulation"]: 

220 # Apply transfinite curves to thin shell lines 

221 if geometry.use_TSA: 

222 gts = self.md.geometries.thin_shells 

223 conductor_target_sizes = {'width': mesh.conductors.transfinite.curve_target_size_width, 'height': mesh.conductors.transfinite.curve_target_size_height} 

224 wedge_target_sizes = {'width': mesh.wedges.transfinite.curve_target_size_width, 'height': mesh.wedges.transfinite.curve_target_size_height} 

225 for ts_group, side in zip([gts.mid_layers_ht_to_ht, gts.mid_layers_ht_to_wdg, gts.mid_layers_wdg_to_ht, gts.mid_layers_wdg_to_wdg, 

226 gts.mid_poles, gts.mid_windings, gts.mid_turn_blocks, gts.mid_wedge_turn, gts.mid_layers_aux], 

227 ['height', 'height', 'height', 'height', 'width', 'width', 'width', 'width', 'height']): 

228 for ts_name, ts in ts_group.items(): 

229 for _, line in ts.lines.items() if isinstance(ts, dM.Region) else ts.mid_layers.lines.items(): 

230 if mesh.isothermal_conductors or mesh.isothermal_wedges: 

231 elements = 1 

232 else: 

233 coords = gmsh.model.getValue(1, line, [i[0] for i in gmsh.model.getParametrizationBounds(1, line)]) 

234 target_size = wedge_target_sizes[side] if ts_name.count('w') == 2 else conductor_target_sizes[side] 

235 elements = max(1, round(Func.points_distance(coords[:2], coords[3:-1]) / target_size)) 

236 

237 # it's a wedge 

238 if ts_name.count('w') == 2 and mesh.wedges.transfinite.enabled_for in ['curves', 'curves_and_surfaces']: 

239 self.mesh.setTransfiniteCurve(line, elements) 

240 elif ts_name.count('w') != 2 and mesh.conductors.transfinite.enabled_for in ['curves', 'curves_and_surfaces']: 

241 self.mesh.setTransfiniteCurve(line, elements) 

242 # COMMENTED since this overwrites also the cable transfinite meshes and in general,  

243 # restricting the insulation boundaries to be transfinite seems very restrictive due to their complex geometry 

244 # Apply transfinite curves to insulation boundaries 

245 # else: 

246 # for coil_nr, coil in self.md.geometries.insulation.coils.items(): 

247 # for group_nr, group in coil.group.items(): 

248 # cable_height = self.data.conductors[self.md.geometries.coil.coils[coil_nr].poles[ 

249 # group.blocks[0][0]].layers[group.blocks[0][1]].windings[group.blocks[0][2]].conductor_name].cable.bare_cable_height_mean 

250 # for line in [bnd[1] for bnd in gmsh.model.getBoundary( 

251 # [(2, list(group.ins.areas.values())[0].surface)], # + 

252 # # [(2, ht.surface) for blk_order in group.blocks for ht in 

253 # # self.md.geometries.coil.coils[coil_nr].poles[blk_order[0]].layers[blk_order[1]].windings[blk_order[2]].blocks[blk_order[3]].half_turns.areas.values()] + 

254 # # [(2, self.md.geometries.wedges.coils[coil_nr].layers[wdg_order[0]].wedges[wdg_order[1]].areas[str(wdg_order[1])].surface) for wdg_order in group.wedges], 

255 # combined=True, oriented=False)]: 

256 # pnts = gmsh.model.getAdjacencies(1, line)[1] 

257 # length = Func.points_distance(gmsh.model.getValue(0, pnts[0], []), gmsh.model.getValue(0, pnts[1], [])) 

258 # self.mesh.setTransfiniteCurve(line,max(1, round(length / (mesh.conductor_target_sizes.width if length > 2 * cable_height else mesh.conductor_target_sizes.height)))) 

259 

260 # Apply transfinite curves to wedges 

261 if mesh.wedges.transfinite.enabled_for in ['curves', 'curves_and_surfaces']: 

262 if geometry.with_wedges: 

263 for coil_nr, coil in self.md.geometries.wedges.coils.items(): 

264 for layer_nr, layer in coil.layers.items(): 

265 for _, wedge in layer.wedges.items(): 

266 pnts = gmsh.model.getAdjacencies(1, wedge.lines['i'])[1] 

267 inner_height = Func.points_distance(gmsh.model.getValue(0, pnts[0], []), gmsh.model.getValue(0, pnts[1], [])) 

268 pnts = gmsh.model.getAdjacencies(1, wedge.lines['l'])[1] 

269 width = Func.points_distance(gmsh.model.getValue(0, pnts[0], []), gmsh.model.getValue(0, pnts[1], [])) 

270 pnts = gmsh.model.getAdjacencies(1, wedge.lines['o'])[1] 

271 outer_height = Func.points_distance(gmsh.model.getValue(0, pnts[0], []), gmsh.model.getValue(0, pnts[1], [])) 

272 for line_key, line in wedge.lines.items(): 

273 if mesh.model_dump().get('isothermal_wedges', False): 

274 elements = 1 

275 elif 'i' in line_key: 

276 elements = max(1, round(inner_height / mesh.wedges.transfinite.curve_target_size_height)) 

277 elif 'o' in line_key: 

278 elements = max(1, round((inner_height if mesh.wedges.transfinite.enabled_for in ['curves', 'curves_and_surfaces'] 

279 else outer_height) / mesh.wedges.transfinite.curve_target_size_height)) 

280 elif any([i in line_key for i in ['l', 'h']]): 

281 elements = max(1, round(width / mesh.wedges.transfinite.curve_target_size_width)) 

282 if mesh.wedges.transfinite.enabled_for in ['curves', 'curves_and_surfaces']: 

283 self.mesh.setTransfiniteCurve(line, elements) 

284 if mesh.wedges.transfinite.enabled_for=='curves_and_surfaces' or mesh.model_dump().get('isothermal_wedges', False): 

285 self.mesh.setTransfiniteSurface(list(wedge.areas.values())[0].surface) 

286 self.mesh.setRecombine(2, list(wedge.areas.values())[0].surface) 

287 

288 def createPhysicalGroups(self, geometry): 

289 """ 

290 Creates physical groups by grouping the mirrored entities according to the Roxie domains 

291 """ 

292 offset: int = 1 if 'symmetry' in geometry.model_dump() else int(1e6) 

293 pg_tag = offset 

294 

295 # Create physical groups of iron yoke regions and block insulation 

296 pg = self.md.domains.physical_groups 

297 for group_name, surfaces in self.md.domains.groups_entities.iron.items(): 

298 pg.iron.surfaces[group_name] = gmsh.model.addPhysicalGroup(2, surfaces, pg_tag) 

299 gmsh.model.setPhysicalName(2, pg.iron.surfaces[group_name], group_name) 

300 color = self.colors[group_name] 

301 gmsh.model.setColor([(2, i) for i in surfaces], color[0], color[1], color[2]) 

302 pg_tag += 1 

303 

304 # Create the physical group of air infinite 

305 if 'symmetry' in geometry.model_dump(): 

306 gmsh.model.setPhysicalName(0, gmsh.model.addPhysicalGroup( 

307 0, [pnt for pnt_name, pnt in self.md.geometries.air.points.items() if 'bore_field' in pnt_name], pg_tag), 'bore_centers') 

308 pg_tag += 1 

309 pg.air_inf_bnd = gmsh.model.addPhysicalGroup(1, [self.md.geometries.air_inf.lines['outer']], pg_tag) 

310 gmsh.model.setPhysicalName(1, pg.air_inf_bnd, 'air_inf_bnd') 

311 pg_tag += 1 

312 pg.air_inf = gmsh.model.addPhysicalGroup(2, [self.md.geometries.air_inf.areas['outer'].surface], pg_tag) 

313 gmsh.model.setPhysicalName(2, pg.air_inf, 'air_inf') 

314 gmsh.model.setColor([(2, self.md.geometries.air_inf.areas['outer'].surface)], self.colors['air_inf'][0], self.colors['air_inf'][1], self.colors['air_inf'][2]) 

315 pg_tag += 1 

316 pg.air = gmsh.model.addPhysicalGroup(2, self.md.domains.groups_entities.air, pg_tag) 

317 gmsh.model.setPhysicalName(2, pg.air, 'air') 

318 gmsh.model.setColor([(2, i) for i in self.md.domains.groups_entities.air], self.colors['air'][0], self.colors['air'][1], self.colors['air'][2]) 

319 pg_tag += 1 

320 

321 # Create physical groups of half turns 

322 lyr_list_group = [] 

323 for coil_nr, coil in self.md.geometries.coil.anticlockwise_order.coils.items(): 

324 lyr_list_group.extend(['cl' + str(coil_nr) + 'ly' + str(lyr) for lyr in list(coil.layers.keys())]) 

325 for layer_nr, layer in coil.layers.items(): 

326 ht_list_group = [] 

327 for nr, block_order in enumerate(layer): 

328 wnd = self.md.geometries.coil.coils[coil_nr].poles[block_order.pole].layers[ 

329 layer_nr].windings[block_order.winding] 

330 block = wnd.blocks[block_order.block] 

331 block_nr = block_order.block 

332 pg.blocks[block_nr] = dM.PoweredBlock() 

333 blk_pg = pg.blocks[block_nr] 

334 blk_pg.current_sign = block.current_sign 

335 blk_pg.conductor = wnd.conductor_name 

336 color = self.colors['half_turns_pos'] if block.current_sign > 0 else self.colors['half_turns_neg'] 

337 ht_list = list(block.half_turns.areas.keys()) 

338 ht_list_group.extend(ht_list) 

339 if nr + 1 < len(layer): 

340 if layer[nr + 1].pole == block_order.pole and layer[nr + 1].winding != block_order.winding: 

341 ht_list_group.append('w' + str(nr)) 

342 # Create 2D physical groups of half turns 

343 for ht_key, ht in block.half_turns.areas.items(): 

344 blk_pg.half_turns[int(ht_key)] = dM.PoweredGroup() 

345 ht_pg = blk_pg.half_turns[int(ht_key)] 

346 # Create physical group and assign name and color 

347 ht_pg.tag = gmsh.model.addPhysicalGroup(2, [ht.surface], pg_tag) 

348 gmsh.model.setPhysicalName(2, ht_pg.tag, ht_key) 

349 gmsh.model.setColor([(2, ht.surface)], color[0], color[1], color[2]) 

350 pg_tag += 1 

351 # Assign thin-shell group 

352 # the check for reversed block coil is not tested well 

353 if geometry.model_dump().get('correct_block_coil_tsa_checkered_scheme', False) and self.md.geometries.coil.coils[coil_nr].type == 'reversed-block-coil': 

354 azimuthal = 'a1' if list(wnd.blocks.keys()).index(block_nr) % 2 == 0 else 'a2' 

355 else: 

356 azimuthal = 'a1' if lyr_list_group.index('cl' + str(coil_nr) + 'ly' + str(layer_nr)) % 2 == 0 else 'a2' 

357 radial = 'r1' if ht_list_group.index(ht_key) % 2 == 0 else 'r2' 

358 ht_pg.group = radial + '_' + azimuthal 

359 if geometry.model_dump().get('use_TSA', False): 

360 # Create 1D physical groups of thin shells 

361 for ht_line_key, ht_line in block.half_turns.lines.items(): 

362 ht_nr = ht_line_key[:-1] 

363 # Create half turn line groups 

364 line_pg = gmsh.model.addPhysicalGroup(1, [ht_line], pg_tag) 

365 gmsh.model.setPhysicalName(1, line_pg, ht_line_key) 

366 color = [0, 0, 0] if blk_pg.half_turns[int(ht_nr)].group[:2] == 'r1' else [150, 150, 150] 

367 gmsh.model.setColor([(1, ht_line)], color[0], color[1], color[2]) 

368 pg_tag += 1 

369 # Store thin shell tags 

370 blk_pg.half_turns[int(ht_nr)].lines[ht_line_key[-1]] = line_pg 

371 

372 # Create points region for projection 

373 if 'use_TSA' in geometry.model_dump(): 

374 self.md.domains.physical_groups.half_turns_points = gmsh.model.addPhysicalGroup( 

375 0, [gmsh.model.getAdjacencies(1, gmsh.model.getAdjacencies(2, ht.surface)[1][0])[1][0] 

376 for coil_nr, coil in self.md.geometries.coil.anticlockwise_order.coils.items() 

377 for layer_nr, layer in coil.layers.items() for block_order in layer 

378 for ht_key, ht in self.md.geometries.coil.coils[coil_nr].poles[block_order.pole].layers[ 

379 layer_nr].windings[block_order.winding].blocks[block_order.block].half_turns.areas.items()], int(4e6)) 

380 gmsh.model.setPhysicalName(0, self.md.domains.physical_groups.half_turns_points, 'points') 

381 

382 # Create physical groups of insulations 

383 if not geometry.model_dump().get('use_TSA', True): 

384 for coil_nr, coil in self.md.geometries.insulation.coils.items(): 

385 for group_nr, group in coil.group.items(): 

386 # Areas 

387 for area_name, area in group.ins.areas.items(): 

388 if area_name.isdigit(): 

389 pg.insulations.surfaces[area_name] = gmsh.model.addPhysicalGroup(2, [area.surface], pg_tag) 

390 gmsh.model.setPhysicalName(2, pg.insulations.surfaces[area_name], 'insul_' + area_name) 

391 gmsh.model.setColor([(2, area.surface)], self.colors['insul'][0], self.colors['insul'][1], self.colors['insul'][2]) 

392 pg_tag += 1 

393 

394 # Boundaries 

395 area_name = list(group.ins.areas.keys())[0] # todo: test for Mono 

396 pg.insulations.curves['ext' + area_name] = gmsh.model.addPhysicalGroup( 

397 1, [bnd[1] for bnd in gmsh.model.getBoundary([(2, list(group.ins.areas.values())[0].surface)], combined=False, oriented=False)[:len(group.ins.lines)]], pg_tag) 

398 gmsh.model.setPhysicalName(1, pg.insulations.curves['ext' + area_name], 'insul_ext' + area_name) 

399 pg_tag += 1 

400 # todo: NOT COMPLETED: would work if the tags were updated in the Geometry script after saving and loading brep 

401 # side_lines = {'i': [], 'o': [], 'l': [], 'h': []} 

402 # for line_name, line in group.ins.lines.items(): 

403 # side_lines[line_name[-1] if line_name[-1].isalpha() else sorted(line_name)[-1]].append(line) 

404 # for side, side_line in side_lines.items(): 

405 # pg.insulations.curves[str(group_nr) + side] = gmsh.model.addPhysicalGroup(1, side_line, pg_tag) 

406 # gmsh.model.setPhysicalName(1, pg.insulations.curves[str(group_nr) + side], str(group_nr) + side) 

407 # pg_tag += 1 

408 

409 # Create physical groups of wedges 

410 for coil_nr, coil in self.md.geometries.wedges.coils.items(): 

411 for layer_nr, layer in coil.layers.items(): 

412 for wedge_nr, wedge in layer.wedges.items(): 

413 pg.wedges[wedge_nr] = dM.WedgeGroup() 

414 wedge_pg = pg.wedges[wedge_nr] 

415 wedge_pg.tag = gmsh.model.addPhysicalGroup(2, [wedge.areas[str(wedge_nr)].surface], pg_tag) 

416 gmsh.model.setPhysicalName(2, wedge_pg.tag, 'w' + str(wedge_nr)) 

417 gmsh.model.setColor([(2, wedge.areas[str(wedge_nr)].surface)], 

418 self.colors['wedges'][0], self.colors['wedges'][1], self.colors['wedges'][2]) 

419 pg_tag += 1 

420 # Assign thin-shell group 

421 prev_block_hts = pg.blocks[layer.block_prev[wedge_nr]].half_turns 

422 if len(list(prev_block_hts.keys())) > 1: 

423 wedge_pg.group = prev_block_hts[list(prev_block_hts.keys())[-2]].group 

424 else: 

425 prev_group = prev_block_hts[list(prev_block_hts.keys())[0]].group 

426 wedge_pg.group = ('r1' if prev_group[1] == '2' else 'r2') + prev_group[prev_group.index('_'):] 

427 if geometry.model_dump().get('use_TSA', False): 

428 # Create 1D physical groups of thin shells 

429 for line_key, line in wedge.lines.items(): 

430 wedge_pg.lines[line_key] = gmsh.model.addPhysicalGroup(1, [line], pg_tag) 

431 gmsh.model.setPhysicalName(1, wedge_pg.lines[line_key], 'w' + str(wedge_nr) + line_key) 

432 color = [0, 0, 0] if wedge_pg.group[:2] == 'r1' else [150, 150, 150] 

433 gmsh.model.setColor([(1, line)], color[0], color[1], color[2]) 

434 pg_tag += 1 

435 

436 # Create physical groups of thin shells 

437 if geometry.model_dump().get('use_TSA', False): 

438 gts = self.md.geometries.thin_shells 

439 # Create physical groups of block mid-layer lines 

440 block_coil_flag = False 

441 for ts_name, ts in gts.mid_layers_ht_to_ht.items(): 

442 blk_i, blk_o = ts_name[:ts_name.index('_')], ts_name[ts_name.index('_') + 1:] 

443 for coil_nr, coil in self.md.geometries.coil.anticlockwise_order.coils.items(): 

444 if (any(int(blk_i) == blk_order.block for blk_order in self.md.geometries.coil.anticlockwise_order.coils[coil_nr].layers[1]) and 

445 any(int(blk_o) == blk_order.block for blk_order in self.md.geometries.coil.anticlockwise_order.coils[coil_nr].layers[1])): # block-coil mid-pole case 

446 block_coil_flag = True 

447 for line_name, line in ts.mid_layers.lines.items(): 

448 line_pg = gmsh.model.addPhysicalGroup(1, [line], pg_tag) 

449 gmsh.model.setPhysicalName(1, line_pg, line_name) 

450 pg_tag += 1 

451 ht1, ht2 = int(line_name[:line_name.index('_')]), int(line_name[line_name.index('_') + 1:]) 

452 if ht1 in ts.half_turn_lists[blk_i]: 

453 if block_coil_flag: 

454 pg.blocks[int(blk_i)].half_turns[ht1].mid_layer_lines.inner[line_name] = line_pg 

455 else: 

456 pg.blocks[int(blk_i)].half_turns[ht1].mid_layer_lines.outer[line_name] = line_pg 

457 pg.blocks[int(blk_o)].half_turns[ht2].mid_layer_lines.inner[line_name] = line_pg 

458 else: 

459 pg.blocks[int(blk_o)].half_turns[ht1].mid_layer_lines.inner[line_name] = line_pg 

460 if block_coil_flag: 

461 pg.blocks[int(blk_i)].half_turns[ht2].mid_layer_lines.inner[line_name] = line_pg 

462 else: 

463 pg.blocks[int(blk_i)].half_turns[ht2].mid_layer_lines.outer[line_name] = line_pg 

464 

465 # Create physical groups of wedge-to-block mid-layer lines 

466 for ts_name, ts in gts.mid_layers_wdg_to_ht.items(): 

467 wdg, blk = ts_name.split('_') 

468 for line_name, line in ts.lines.items(): 

469 line_pg = gmsh.model.addPhysicalGroup(1, [line], pg_tag) 

470 gmsh.model.setPhysicalName(1, line_pg, line_name) 

471 pg_tag += 1 

472 pg.wedges[int(wdg[1:])].mid_layer_lines.outer[line_name] = line_pg 

473 ht = line_name[:line_name.index('_')] if line_name[line_name.index('_') + 1:] == wdg else line_name[line_name.index('_') + 1:] 

474 pg.blocks[int(blk)].half_turns[int(ht)].mid_layer_lines.inner[line_name] = line_pg 

475 

476 # Create physical groups of block-to-wedge mid-layer lines 

477 for ts_name, ts in gts.mid_layers_ht_to_wdg.items(): 

478 wdg, blk = ts_name.split('_') 

479 for line_name, line in ts.lines.items(): 

480 line_pg = gmsh.model.addPhysicalGroup(1, [line], pg_tag) 

481 gmsh.model.setPhysicalName(1, line_pg, line_name) 

482 pg_tag += 1 

483 pg.wedges[int(wdg[1:])].mid_layer_lines.inner[line_name] = line_pg 

484 ht = line_name[:line_name.index('_')] if line_name[line_name.index('_') + 1:] == wdg else line_name[line_name.index('_') + 1:] 

485 pg.blocks[int(blk)].half_turns[int(ht)].mid_layer_lines.outer[line_name] = line_pg 

486 

487 # Create physical groups of wedge-to-wedge mid-layer lines 

488 for ts_name, ts in gts.mid_layers_wdg_to_wdg.items(): 

489 wdg_i, wdg_o = ts_name[1:ts_name.index('_')], ts_name[ts_name.index('_') + 2:] 

490 line_pg = gmsh.model.addPhysicalGroup(1, [ts.lines[list(ts.lines.keys())[0]]], pg_tag) 

491 gmsh.model.setPhysicalName(1, line_pg, ts_name) 

492 pg_tag += 1 

493 pg.wedges[int(wdg_i)].mid_layer_lines.outer[ts_name] = line_pg 

494 pg.wedges[int(wdg_o)].mid_layer_lines.inner[ts_name] = line_pg 

495 

496 # Create non-mid-layer thin shells 

497 for ts_group_name, ts_group in zip(['mid_pole', 'mid_winding', 'mid_turn', 'mid_turn', 'aux'], 

498 [gts.mid_poles, gts.mid_windings, gts.mid_turn_blocks, gts.mid_wedge_turn, gts.mid_layers_aux]): 

499 for ts_name, ts in ts_group.items(): 

500 if '_' in ts_name: 

501 el_name1, el_name2 = ts_name.split('_') 

502 el_name1, el_name2 = el_name1.strip('w'), el_name2.strip('w') 

503 else: # mid_turn_blocks 

504 el_name1, el_name2 = ts_name, ts_name 

505 for line_name, line in ts.lines.items(): 

506 line_pg = gmsh.model.addPhysicalGroup(1, [line], pg_tag) 

507 gmsh.model.setPhysicalName(1, line_pg, line_name) 

508 pg_tag += 1 

509 if ts_group_name == 'aux': 

510 ht1 = int(list(ts.points.keys())[0][:-1]) 

511 else: 

512 ht1, ht2 = line_name.split('_') 

513 pg.blocks[int(el_name2)].half_turns[int(ht2)].__dict__[ts_group_name + '_lines'][line_name] = line_pg 

514 if ts_name[0] == 'w': 

515 pg.wedges[int(el_name1)].__dict__[ts_group_name + '_lines'][line_name] = line_pg 

516 else: 

517 pg.blocks[int(el_name1)].half_turns[int(ht1)].__dict__[ts_group_name + '_lines'][line_name] = line_pg 

518 

519 # Create physical groups of symmetric boundaries 

520 if geometry.model_dump().get('symmetry', 'none') != 'none': 

521 line_tags_normal_free, line_tags_tangent_free = [], [] 

522 if geometry.symmetry == 'xy': 

523 if len(self.md.geometries.coil.coils[1].poles) == 2: 

524 line_tags_normal_free = self.md.domains.groups_entities.symmetric_boundaries.y 

525 line_tags_tangent_free = self.md.domains.groups_entities.symmetric_boundaries.x 

526 elif len(self.md.geometries.coil.coils[1].poles) == 4: # todo: think about other multi-pole types 

527 line_tags_tangent_free = self.md.domains.groups_entities.symmetric_boundaries.x +\ 

528 self.md.domains.groups_entities.symmetric_boundaries.y 

529 elif geometry.symmetry == 'x': 

530 if 'solenoid' in self.md.geometries.coil.coils[1].type: 

531 line_tags_normal_free = self.md.domains.groups_entities.symmetric_boundaries.x 

532 else: 

533 line_tags_tangent_free = self.md.domains.groups_entities.symmetric_boundaries.x 

534 elif geometry.symmetry == 'y': 

535 if len(self.md.geometries.coil.coils[1].poles) == 2: 

536 line_tags_normal_free = self.md.domains.groups_entities.symmetric_boundaries.y 

537 elif len(self.md.geometries.coil.coils[1].poles) == 4: 

538 line_tags_tangent_free = self.md.domains.groups_entities.symmetric_boundaries.y 

539 if line_tags_normal_free: 

540 pg.symmetric_boundaries.normal_free = gmsh.model.addPhysicalGroup(1, line_tags_normal_free, pg_tag) 

541 gmsh.model.setPhysicalName(1, pg.symmetric_boundaries.normal_free, 'normal_free_bnd') 

542 pg_tag += 1 

543 if line_tags_tangent_free: 

544 pg.symmetric_boundaries.tangential_free = gmsh.model.addPhysicalGroup(1, line_tags_tangent_free, pg_tag) 

545 gmsh.model.setPhysicalName(1, pg.symmetric_boundaries.tangential_free, 'tangent_free_bnd') 

546 pg_tag += 1 

547 

548 def rearrangeThinShellsData(self): 

549 pg = self.md.domains.physical_groups 

550 ins_th = self.md.geometries.thin_shells.ins_thickness 

551 qh = self.data.quench_protection.quench_heaters 

552 

553 def _store_qh_data(position, thin_shell, ts_tag): 

554 qh_ins = self.ins_type_qh[position][qh.ids[ht_index]] 

555 if thin_shell not in qh_ins: qh_ins[thin_shell] = [] 

556 qh_ins[thin_shell].append(ts_tag) 

557 if thin_shell not in self.qh_data[qh.ids[ht_index]]: 

558 self.qh_data[qh.ids[ht_index]][thin_shell] = {'conductor': blk_pg.conductor, 'ht_side': qh_side} 

559 

560 def _store_ts_tags(pg_el, geom_ts_name='', geom_ts_name2=None, ts_grp='', lines='', lines_side=None): 

561 geom_ts = self.md.geometries.thin_shells.model_dump()[geom_ts_name] 

562 for ln_name, ln_tag in (pg_el.model_dump()[lines][lines_side] if lines_side else pg_el.model_dump()[lines]).items(): 

563 for ts_name in ts_groups[ts_grp]: 

564 if ts_name in geom_ts: 

565 if ln_name in (geom_ts[ts_name][geom_ts_name2]['lines'] if geom_ts_name2 else geom_ts[ts_name]['lines']): 

566 if ts_name not in self.ins_type[ts_grp]: self.ins_type[ts_grp][ts_name] = [] 

567 self.ins_type[ts_grp][ts_name].append(ln_tag) 

568 break 

569 

570 # Collect thin shell tags 

571 # Half turns 

572 for blk_pg_nr, blk_pg in pg.blocks.items(): 

573 ts_groups = {'mid_wedge_turn': [ts_name for ts_name in self.md.geometries.thin_shells.mid_wedge_turn if blk_pg_nr == int(ts_name.split('_')[1])], 

574 'aux': [ts_name for ts_name in ins_th.mid_layer if str(blk_pg_nr) in ts_name.split('_')], 

575 'mid_winding': [ts_name for ts_name in ins_th.mid_winding if blk_pg_nr == int(ts_name.split('_')[0])], 

576 'mid_pole': [ts_name for ts_name in ins_th.mid_pole if blk_pg_nr == int(ts_name.split('_')[0])], 

577 'mid_layer': [ts_name for ts_name in ins_th.mid_layer if ts_name[0] != 'w' and blk_pg_nr == int(ts_name.split('_')[0]) 

578 or ts_name[0] == 'w' and ts_name.split('_')[1][0] != 'w' and blk_pg_nr == int(ts_name.split('_')[1])], 

579 'mid_layer_qh_i': [ts_name for ts_name in ins_th.mid_layer if ts_name.split('_')[1][0] != 'w' and blk_pg_nr == int(ts_name.split('_')[1])]} 

580 ht_list = list(blk_pg.half_turns.keys()) 

581 self.ins_type_cond[str(blk_pg_nr)] = {'inner': [], 'outer': [], 'higher': [], 'lower': [], 'mid_turn': []} 

582 for ht_nr, ht in blk_pg.half_turns.items(): 

583 if ht_nr in qh.turns and qh.N_strips > 0: # check if a quench heater strip touches the current half-turn 

584 ht_index = qh.turns.index(ht_nr) 

585 qh_side = qh.turns_sides[ht_index] 

586 if qh.ids[ht_index] not in self.qh_data: self.qh_data[qh.ids[ht_index]] = {} 

587 else: qh_side = '' 

588 

589 # find conductor type of ht adjacent to wedge 

590 if ht_list.index(ht_nr) == len(ht_list) - 1 and ht.mid_turn_lines: 

591 for line_name, line_tag in ht.mid_turn_lines.items(): 

592 for ts_name in ts_groups['mid_wedge_turn']: 

593 if line_name in self.md.geometries.thin_shells.mid_wedge_turn[ts_name].lines: 

594 self.wedge_cond[int(ts_name[1:ts_name.index('_')])] = blk_pg.conductor 

595 break 

596 

597 self.ins_type_cond[str(blk_pg_nr)]['mid_turn'].extend(list(ht.mid_turn_lines.values())) # mid-turn insulation 

598 

599 if ht.aux_lines: # outer mid-layer insulation 

600 _store_ts_tags(ht, geom_ts_name='mid_layers_aux', ts_grp='aux', lines='aux_lines') 

601 

602 if ht.mid_layer_lines.inner and qh_side == 'i': 

603 for line_name, line_tag in ht.mid_layer_lines.inner.items(): 

604 for ts_name in ts_groups['mid_layer_qh_i']: 

605 if ts_name in self.md.geometries.thin_shells.mid_layers_ht_to_ht: 

606 ts_lines = self.md.geometries.thin_shells.mid_layers_ht_to_ht[ts_name].mid_layers.lines 

607 elif ts_name in self.md.geometries.thin_shells.mid_layers_wdg_to_ht: 

608 ts_lines = self.md.geometries.thin_shells.mid_layers_wdg_to_ht[ts_name].lines 

609 else: ts_lines = [] 

610 if line_name in ts_lines: 

611 _store_qh_data('internal', ts_name, line_tag) 

612 break 

613 elif ht.mid_layer_lines.inner: # block-coil (!) inner mid-layer insulation 

614 _store_ts_tags(ht, geom_ts_name='mid_layers_ht_to_ht', geom_ts_name2='mid_layers', ts_grp='mid_layer', lines='mid_layer_lines', lines_side='inner') 

615 elif not ht.mid_layer_lines.inner and qh_side == 'i': # quench heater inner insulation 

616 _store_qh_data('external', blk_pg_nr, ht.lines['i']) 

617 elif not ht.mid_layer_lines.inner: # inner insulation 

618 self.ins_type_cond[str(blk_pg_nr)]['inner'].append(ht.lines['i']) 

619 

620 if ht.mid_layer_lines.outer and qh_side == 'o': # mid-layer quench heater insulation 

621 for line_name, line_tag in ht.mid_layer_lines.outer.items(): 

622 for ts_name in ts_groups['mid_layer']: 

623 if ts_name in self.md.geometries.thin_shells.mid_layers_ht_to_ht: 

624 ts_lines = self.md.geometries.thin_shells.mid_layers_ht_to_ht[ts_name].mid_layers.lines 

625 elif ts_name in self.md.geometries.thin_shells.mid_layers_ht_to_wdg: 

626 ts_lines = self.md.geometries.thin_shells.mid_layers_ht_to_wdg[ts_name].lines 

627 else: ts_lines = [] 

628 if line_name in ts_lines: 

629 _store_qh_data('internal', ts_name, line_tag) 

630 break 

631 elif ht.mid_layer_lines.outer: # mid-layer insulation 

632 for line_name, line_tag in ht.mid_layer_lines.outer.items(): 

633 for ts_name in ts_groups['mid_layer']: 

634 if ts_name in self.md.geometries.thin_shells.mid_layers_ht_to_ht: 

635 ts_lines = self.md.geometries.thin_shells.mid_layers_ht_to_ht[ts_name].mid_layers.lines 

636 elif ts_name in self.md.geometries.thin_shells.mid_layers_ht_to_wdg: 

637 ts_lines = self.md.geometries.thin_shells.mid_layers_ht_to_wdg[ts_name].lines 

638 else: ts_lines = [] 

639 if line_name in ts_lines: 

640 if ts_name not in self.ins_type['mid_layer']: self.ins_type['mid_layer'][ts_name] = [] 

641 self.ins_type['mid_layer'][ts_name].append(line_tag) 

642 break 

643 elif not ht.mid_layer_lines.outer and qh_side == 'o': # quench heater outer insulation 

644 _store_qh_data('external', blk_pg_nr, ht.lines['o']) 

645 else: # outer insulation 

646 self.ins_type_cond[str(blk_pg_nr)]['outer'].append(ht.lines['o']) 

647 

648 # mid-pole insulation 

649 _store_ts_tags(ht, geom_ts_name='mid_poles', ts_grp='mid_pole', lines='mid_pole_lines') 

650 

651 # mid-winding insulation 

652 _store_ts_tags(ht, geom_ts_name='mid_windings', ts_grp='mid_winding', lines='mid_winding_lines') 

653 

654 if ht_list.index(ht_nr) == 0 and len(ht.mid_turn_lines) + len(ht.mid_winding_lines) + len(ht.mid_pole_lines) == 1: # lower angle external side insulation 

655 if qh_side == 'l': _store_qh_data('external', blk_pg_nr, ht.lines['l']) 

656 else: self.ins_type_cond[str(blk_pg_nr)]['lower'].append(ht.lines['l']) 

657 if ht_list.index(ht_nr) == len(ht_list) - 1 and len(ht.mid_turn_lines) + len(ht.mid_winding_lines) + len(ht.mid_pole_lines) == 1: # higher angle external side insulation 

658 if qh_side == 'h': _store_qh_data('external', blk_pg_nr, ht.lines['h']) 

659 else: self.ins_type_cond[str(blk_pg_nr)]['higher'].append(ht.lines['h']) 

660 

661 # Wedges 

662 for wdg_pg_nr, wdg_pg in pg.wedges.items(): 

663 ts_groups = {'aux': [ts_name for ts_name in ins_th.mid_layer if 'w' + str(wdg_pg_nr) in ts_name.split('_')], 

664 'mid_layer': [ts_name for ts_name in ins_th.mid_layer if 'w' + str(wdg_pg_nr) == ts_name.split('_')[0]]} 

665 self.ins_type_cond['w' + str(wdg_pg_nr)] = {'inner': [], 'outer': [], 'higher': [], 'lower': [], 'mid_turn': []} 

666 if wdg_pg.aux_lines: _store_ts_tags(wdg_pg, geom_ts_name='mid_layers_aux', ts_grp='aux', lines='aux_lines') 

667 if not wdg_pg.mid_layer_lines.inner: self.ins_type_cond['w' + str(wdg_pg_nr)]['inner'].append(wdg_pg.lines['i']) 

668 if wdg_pg.mid_layer_lines.outer: 

669 for line_name, line_tag in wdg_pg.mid_layer_lines.outer.items(): 

670 for ts_name in ts_groups['mid_layer']: 

671 if ts_name in self.md.geometries.thin_shells.mid_layers_wdg_to_ht: 

672 ts_lines = self.md.geometries.thin_shells.mid_layers_wdg_to_ht[ts_name].lines 

673 elif ts_name in self.md.geometries.thin_shells.mid_layers_wdg_to_wdg: 

674 ts_lines = self.md.geometries.thin_shells.mid_layers_wdg_to_wdg[ts_name].lines 

675 else: ts_lines = [] 

676 if line_name in ts_lines: 

677 if ts_name not in self.ins_type['mid_layer']: self.ins_type['mid_layer'][ts_name] = [] 

678 self.ins_type['mid_layer'][ts_name].append(line_tag) 

679 break 

680 else: self.ins_type_cond['w' + str(wdg_pg_nr)]['outer'].append(wdg_pg.lines['o']) 

681 

682 # Collect common thin shells for double qh mid-layers 

683 for qh_nr, ts_groups in self.ins_type_qh['internal'].items(): 

684 for qh_nr2, ts_groups2 in self.ins_type_qh['internal'].items(): 

685 if qh_nr != qh_nr2: 

686 common_ts_groups = list(set(ts_groups) & set(ts_groups2)) 

687 for ts in common_ts_groups: 

688 tags, tags2 = ts_groups[ts], ts_groups2[ts] 

689 common_tags = list(set(tags) & set(tags2)) 

690 for tag in common_tags: 

691 tags.remove(tag), tags2.remove(tag) 

692 if self.qh_data[qh_nr2][ts]['ht_side'] == 'i': qh_name = str(qh_nr) + '_' + str(qh_nr2) 

693 else: qh_name = str(qh_nr2) + '_' + str(qh_nr) 

694 if qh_name not in self.ins_type_qh['internal_double']: self.ins_type_qh['internal_double'][qh_name] = {} 

695 qh_ins_id = self.ins_type_qh['internal_double'][qh_name] 

696 if ts not in qh_ins_id: qh_ins_id[ts] = [] 

697 qh_ins_id[ts].append(tag) 

698 

699 def assignRegionsTags(self, geometry, mesh): 

700 def _get_input_insulation_data(i_name, i_type=None): 

701 ow_idx = next((index for index, couple in enumerate(self.data.magnet.solve.thermal.insulation_TSA.block_to_block.blocks_connection_overwrite) 

702 if all(element in couple for element in i_name.split('_'))), None) 

703 if i_type == 'mid_winding': mid_mat, mid_th = [self.data.magnet.solve.wedges.material], [] 

704 elif ow_idx is not None: 

705 mid_mat = self.data.magnet.solve.thermal.insulation_TSA.block_to_block.materials_overwrite[ow_idx] 

706 mid_th = self.data.magnet.solve.thermal.insulation_TSA.block_to_block.thicknesses_overwrite[ow_idx] 

707 else: mid_mat, mid_th = [self.data.magnet.solve.thermal.insulation_TSA.block_to_block.material], [] 

708 return mid_mat, mid_th 

709 

710 def _compute_insulation_thicknesses(tot_th, known_ins_th): 

711 if not mid_thicknesses: 

712 mid_lyrs = [Func.sig_dig(tot_th - known_ins_th) / len(mid_materials)] * len(mid_materials) 

713 elif None in mid_thicknesses: 

714 input_ths = sum([th for th in mid_thicknesses if th is not None]) 

715 mid_lyrs = [th if th is not None else Func.sig_dig(tot_th - known_ins_th - input_ths) / mid_thicknesses.count(None) for th in mid_thicknesses] 

716 else: mid_lyrs = mid_thicknesses 

717 zeros = [nbr for nbr, th in enumerate(mid_lyrs) if th < 1e-8] 

718 if tot_th - known_ins_th - sum(mid_lyrs) < -1e-8: 

719 raise ValueError("Layer-to-layer insulation exceeds the space between blocks: check 'solve'->'insulation_TSA'->'block_to_block'->'thicknesses_overwrite'") 

720 else: return mid_lyrs, zeros 

721 

722 pg = self.md.domains.physical_groups 

723 qh = self.data.quench_protection.quench_heaters 

724 

725 # Air and air far field 

726 if 'bore_field' in mesh.model_dump(): 

727 self.rm.air_far_field.vol.radius_out = float(abs(max(gmsh.model.getValue(0, gmsh.model.getAdjacencies( 

728 1, self.md.geometries.air_inf.lines['outer'])[1][0], []), key=abs))) 

729 self.rm.air_far_field.vol.radius_in = float(abs(max(gmsh.model.getValue(0, gmsh.model.getAdjacencies( 

730 1, self.md.geometries.air_inf.lines['inner'])[1][0], []), key=abs))) 

731 self.rm.air.vol.name = "Air" 

732 self.rm.air.vol.number = pg.air 

733 self.rm.air_far_field.vol.names = ["AirInf"] 

734 self.rm.air_far_field.vol.numbers = [pg.air_inf] 

735 self.rm.air_far_field.surf.name = "Surface_Inf" 

736 self.rm.air_far_field.surf.number = pg.air_inf_bnd 

737 if geometry.model_dump().get('symmetry', 'none') != 'none': 

738 self.rm.boundaries.symmetry.normal_free.name = 'normal_free_bnd' 

739 self.rm.boundaries.symmetry.normal_free.number = pg.symmetric_boundaries.normal_free 

740 self.rm.boundaries.symmetry.tangential_free.name = 'tangent_free_bnd' 

741 self.rm.boundaries.symmetry.tangential_free.number = pg.symmetric_boundaries.tangential_free 

742 

743 if 'use_TSA' in geometry.model_dump(): 

744 self.rm.projection_points.name = 'projection_points' 

745 self.rm.projection_points.number = self.md.domains.physical_groups.half_turns_points 

746 

747 # Initialize lists 

748 for group in ['r1_a1', 'r2_a1', 'r1_a2', 'r2_a2']: 

749 self.rm.powered[group] = rM.Powered() 

750 self.rm.powered[group].vol.names = [] 

751 self.rm.powered[group].vol.numbers = [] 

752 for cond_name in self.data.conductors.keys(): self.rm.powered[group].conductors[cond_name] = [] 

753 if geometry.with_wedges: 

754 self.rm.induced[group] = rM.Induced() 

755 self.rm.induced[group].vol.names = [] 

756 self.rm.induced[group].vol.numbers = [] 

757 if 'bore_field' in mesh.model_dump(): 

758 initial_current = self.data.power_supply.I_initial 

759 self.rm.powered[group].vol.currents = [] 

760 if geometry.model_dump().get('use_TSA', False): 

761 self.rm.powered[group].surf_in.names = [] 

762 self.rm.powered[group].surf_in.numbers = [] 

763 self.rm.powered[group].surf_out.names = [] 

764 self.rm.powered[group].surf_out.numbers = [] 

765 if geometry.with_wedges: 

766 self.rm.induced[group].surf_in.names = [] 

767 self.rm.induced[group].surf_in.numbers = [] 

768 self.rm.induced[group].surf_out.names = [] 

769 self.rm.induced[group].surf_out.numbers = [] 

770 if geometry.with_iron_yoke: 

771 self.rm.iron_yoke.vol.names = [] 

772 self.rm.iron_yoke.vol.numbers = [] 

773 if geometry.model_dump().get('use_TSA', False): 

774 unique_thin_shells = [] 

775 self.rm.thin_shells.second_group_is_next['azimuthally'] = [] 

776 self.rm.thin_shells.second_group_is_next['radially'] = [] 

777 self.rm.thin_shells.normals_directed['azimuthally'] = [] 

778 self.rm.thin_shells.normals_directed['radially'] = [] 

779 else: 

780 self.rm.insulator.vol.names = [] 

781 self.rm.insulator.vol.numbers = [] 

782 self.rm.insulator.surf.names = [] 

783 self.rm.insulator.surf.numbers = [] 

784 

785 if geometry.model_dump().get('use_TSA', False): 

786 # Categorize insulation types 

787 min_h = mesh.insulation.global_size 

788 # min_h = 1 

789 # for conductor in self.data.conductors.keys(): 

790 # min_h = min([self.data.conductors[conductor].cable.th_insulation_along_height, 

791 # self.data.conductors[conductor].cable.th_insulation_along_width, min_h]) 

792 min_h_QH = mesh.insulation.TSA.global_size_QH if mesh.insulation.TSA.global_size_QH else min_h 

793 

794 # Conductor insulation layers 

795 for el, ins in self.ins_type_cond.items(): 

796 cond = self.data.conductors[self.wedge_cond[int(el[1:])]].cable if 'w' in el else self.data.conductors[pg.blocks[int(el)].conductor].cable 

797 for ins_side, tags in ins.items(): 

798 if tags: 

799 side_ins_type = [cond.material_insulation] 

800 if ins_side in ['inner', 'outer']: side_ins = [cond.th_insulation_along_width] 

801 elif ins_side in ['higher', 'lower']: side_ins = [cond.th_insulation_along_height] 

802 else: # mid_turn 

803 side_ins = [cond.th_insulation_along_height, cond.th_insulation_along_height] 

804 side_ins_type.append(cond.material_insulation) 

805 if ins_side[0] in 'iohl' and el + ins_side[0] in self.data.magnet.solve.thermal.insulation_TSA.exterior.blocks: 

806 add_mat_idx = self.data.magnet.solve.thermal.insulation_TSA.exterior.blocks.index(el + ins_side[0]) 

807 side_ins.extend(self.data.magnet.solve.thermal.insulation_TSA.exterior.thicknesses_append[add_mat_idx]) 

808 side_ins_type.extend(self.data.magnet.solve.thermal.insulation_TSA.exterior.materials_append[add_mat_idx]) 

809 if ins_side[0] in 'il': side_ins.reverse(), side_ins_type.reverse() 

810 self.rm.thin_shells.insulation_types.layers_number.append(0) 

811 self.rm.thin_shells.insulation_types.thin_shells.append(list(set(tags))) 

812 self.rm.thin_shells.insulation_types.thicknesses.append([]) 

813 self.rm.thin_shells.insulation_types.layers_material.append([]) 

814 for nr, ins_lyr in enumerate(side_ins): 

815 tsa_layers = max(mesh.insulation.TSA.minimum_discretizations, round(ins_lyr / min_h)) 

816 self.rm.thin_shells.insulation_types.layers_number[-1] += tsa_layers 

817 self.rm.thin_shells.insulation_types.thicknesses[-1].extend([ins_lyr / tsa_layers] * tsa_layers) 

818 self.rm.thin_shells.insulation_types.layers_material[-1].extend([side_ins_type[nr]] * tsa_layers) 

819 

820 # Mid-pole, mid-winding, and mid-layer insulation layers 

821 ins_th_dict = self.md.geometries.thin_shells.ins_thickness.model_dump() 

822 for ins_type, ins in self.ins_type.items(): 

823 for ins_name, tags in ins.items(): 

824 # Get conductors insulation 

825 if ins_name.count('w') == 2: 

826 el1, el2 = int(ins_name[1:ins_name.index('_')]), int(ins_name[ins_name.index('_') + 2:]) 

827 cond1 = self.data.conductors[self.wedge_cond[el1]].cable 

828 cond2 = self.data.conductors[self.wedge_cond[el2]].cable 

829 elif ins_name.count('w') == 1: 

830 el1, el2 = int(ins_name[1:ins_name.index('_')]), int(ins_name[ins_name.index('_') + 1:]) 

831 cond1 = self.data.conductors[self.wedge_cond[el1]].cable 

832 cond2 = self.data.conductors[pg.blocks[el2].conductor].cable 

833 if ins_name in self.md.geometries.thin_shells.mid_layers_ht_to_wdg: cond1, cond2 = cond2, cond1 

834 else: 

835 el1, el2 = int(ins_name[:ins_name.index('_')]), int(ins_name[ins_name.index('_') + 1:]) 

836 cond1 = self.data.conductors[pg.blocks[el1].conductor].cable 

837 cond2 = self.data.conductors[pg.blocks[el2].conductor].cable 

838 if ins_type in ['mid_layer', 'aux']: 

839 cond_ins1, cond_ins2 = cond1.th_insulation_along_width, cond2.th_insulation_along_width 

840 else: 

841 cond_ins1, cond_ins2 = cond1.th_insulation_along_height, cond2.th_insulation_along_height 

842 # Get insulation layer thickness 

843 mid_materials, mid_thicknesses = _get_input_insulation_data(ins_name, i_type=ins_type) 

844 ins_thickness = ins_th_dict['mid_layer'][ins_name] / 2 if ins_type == 'aux' else ins_th_dict[ins_type][ins_name] 

845 mid_lyr_th, null_idx = _compute_insulation_thicknesses(ins_thickness, cond_ins1 + cond_ins2) 

846 for idx in null_idx: mid_lyr_th.pop(idx), mid_materials.pop(idx) 

847 side_ins = [cond_ins1] + mid_lyr_th + [cond_ins2] 

848 # Get insulation materials 

849 side_ins_type = [cond1.material_insulation] + mid_materials + [cond2.material_insulation] 

850 # Initialize sublists 

851 self.rm.thin_shells.insulation_types.layers_number.append(0) 

852 self.rm.thin_shells.insulation_types.thin_shells.append(list(set(tags))) 

853 self.rm.thin_shells.insulation_types.thicknesses.append([]) 

854 self.rm.thin_shells.insulation_types.layers_material.append([]) 

855 for nr, ins_lyr in enumerate(side_ins): 

856 tsa_layers = max(mesh.insulation.TSA.minimum_discretizations, round(ins_lyr / min_h)) 

857 self.rm.thin_shells.insulation_types.layers_number[-1] += tsa_layers 

858 self.rm.thin_shells.insulation_types.thicknesses[-1].extend([ins_lyr / tsa_layers] * tsa_layers) 

859 self.rm.thin_shells.insulation_types.layers_material[-1].extend([side_ins_type[nr]] * tsa_layers) 

860 

861 # Quench heater insulation layers 

862 for ins_type, ins in self.ins_type_qh.items(): 

863 for qh_nr, ts_groups in ins.items(): 

864 for ts_name, tags in ts_groups.items(): 

865 if ins_type != 'external': mid_materials, mid_thicknesses = _get_input_insulation_data(ts_name) 

866 if tags: 

867 if ins_type == 'external': 

868 data = self.qh_data[qh_nr] 

869 if str(ts_name) + data[ts_name]['ht_side'] in self.data.magnet.solve.thermal.insulation_TSA.exterior.blocks: 

870 add_mat_idx = self.data.magnet.solve.thermal.insulation_TSA.exterior.blocks.index(str(ts_name) + data[ts_name]['ht_side']) 

871 additional_ths = self.data.magnet.solve.thermal.insulation_TSA.exterior.thicknesses_append[add_mat_idx] 

872 additional_mats = self.data.magnet.solve.thermal.insulation_TSA.exterior.materials_append[add_mat_idx] 

873 else: additional_ths, additional_mats = [], [] 

874 cond = self.data.conductors[data[ts_name]['conductor']].cable 

875 ht_ins_th = cond.th_insulation_along_width if data[ts_name]['ht_side'] in 'io' else cond.th_insulation_along_height 

876 side_ins = [ht_ins_th] + [h_ins for h_ins in qh.h_ins[qh_nr - 1]] + [qh.h[qh_nr - 1]] + [h_ground_ins for h_ground_ins in qh.h_ground_ins[qh_nr - 1]] + additional_ths 

877 side_ins_type = [cond.material_insulation] + [type_ins for type_ins in qh.type_ins[qh_nr - 1]] + ['SS'] +\ 

878 [type_ground_ins for type_ground_ins in qh.type_ground_ins[qh_nr - 1]] + additional_mats 

879 if data[ts_name]['ht_side'] in 'il': side_ins.reverse(), side_ins_type.reverse() 

880 qh_list = [qh_nr] 

881 elif ins_type == 'internal': 

882 data = self.qh_data[qh_nr] 

883 cond = self.data.conductors[data[ts_name]['conductor']].cable 

884 cond2 = self.data.conductors[self.wedge_cond[int(ts_name.split('_')[0][1:])] if 'w' in ts_name 

885 else pg.blocks[int(ts_name.split('_')[1]) if data[ts_name]['ht_side'] == 'o' else int(ts_name.split('_')[0])].conductor].cable 

886 side_ins_qh = [h_ins for h_ins in qh.h_ins[qh_nr - 1]] + [qh.h[qh_nr - 1]] + [h_ground_ins for h_ground_ins in qh.h_ground_ins[qh_nr - 1]] 

887 mid_lyr_th, null_idx = _compute_insulation_thicknesses( 

888 ins_th_dict['mid_layer'][ts_name], sum([cond.th_insulation_along_width, cond2.th_insulation_along_width] + side_ins_qh)) 

889 for idx in null_idx: mid_lyr_th.pop(idx), mid_materials.pop(idx) 

890 side_ins = [cond.th_insulation_along_width] + side_ins_qh + mid_lyr_th + [cond2.th_insulation_along_width] 

891 side_ins_type = [cond.material_insulation] + [type_ins for type_ins in qh.type_ins[qh_nr - 1]] + ['SS'] +\ 

892 [type_ground_ins for type_ground_ins in qh.type_ground_ins[qh_nr - 1]] + mid_materials + [cond2.material_insulation] 

893 if data[ts_name]['ht_side'] == 'i': side_ins.reverse(), side_ins_type.reverse() 

894 qh_list = [qh_nr] 

895 elif ins_type == 'internal_double': 

896 qh_nr1, qh_nr2 = int(qh_nr.split('_')[0]), int(qh_nr.split('_')[1]) 

897 data, data2 = self.qh_data[qh_nr1], self.qh_data[qh_nr2] 

898 cond = self.data.conductors[data[ts_name]['conductor']].cable 

899 cond2 = self.data.conductors[data2[ts_name]['conductor']].cable 

900 side_ins_qh = [h_ins for h_ins in qh.h_ins[qh_nr1 - 1]] + [qh.h[qh_nr1 - 1]] + [h_ground_ins for h_ground_ins in qh.h_ground_ins[qh_nr1 - 1]] 

901 side_ins_qh2 = [h_ground_ins for h_ground_ins in qh.h_ground_ins[qh_nr2 - 1][::-1]] + [qh.h[qh_nr2 - 1]] + [h_ins for h_ins in qh.h_ins[qh_nr2 - 1][::-1]] 

902 mid_lyr_th, null_idx = _compute_insulation_thicknesses( 

903 ins_th_dict['mid_layer'][ts_name], sum([cond.th_insulation_along_width, cond2.th_insulation_along_width] + side_ins_qh + side_ins_qh2)) 

904 for idx in null_idx: mid_lyr_th.pop(idx), mid_materials.pop(idx) 

905 side_ins = [cond.th_insulation_along_width] + side_ins_qh + mid_lyr_th + side_ins_qh2 + [cond2.th_insulation_along_width] 

906 side_ins_type = [cond.material_insulation] + [type_ins for type_ins in qh.type_ins[qh_nr1 - 1]] + ['SS'] +\ 

907 [type_ground_ins for type_ground_ins in qh.type_ground_ins[qh_nr1 - 1]] + mid_materials +\ 

908 [type_ground_ins for type_ground_ins in qh.type_ground_ins[qh_nr2 - 1][::-1]] + ['SS'] +\ 

909 [type_ins for type_ins in qh.type_ins[qh_nr2 - 1][::-1]] + [cond2.material_insulation] 

910 qh_list = [qh_nr1, qh_nr2] 

911 qh_labels = [1 if m == 'SS' else None for m in side_ins_type] 

912 ss_indexes = [index for index, value in enumerate(qh_labels) if value == 1] 

913 for nr, idx in enumerate(ss_indexes): qh_labels[idx] = qh_list[nr] 

914 self.rm.thin_shells.quench_heaters.layers_number.append(0) 

915 self.rm.thin_shells.quench_heaters.thin_shells.append(list(set(tags))) 

916 self.rm.thin_shells.quench_heaters.thicknesses.append([]) 

917 self.rm.thin_shells.quench_heaters.layers_material.append([]) 

918 self.rm.thin_shells.quench_heaters.label.append([]) 

919 for nr, ins_lyr in enumerate(side_ins): 

920 tsa_layers = max(mesh.insulation.TSA.minimum_discretizations_QH, round(ins_lyr / min_h_QH)) 

921 self.rm.thin_shells.quench_heaters.layers_number[-1] += tsa_layers 

922 self.rm.thin_shells.quench_heaters.thicknesses[-1].extend([ins_lyr / tsa_layers] * tsa_layers) 

923 self.rm.thin_shells.quench_heaters.layers_material[-1].extend([side_ins_type[nr]] * tsa_layers) 

924 self.rm.thin_shells.quench_heaters.label[-1].extend([qh_labels[nr]] * tsa_layers) 

925 

926 # Powered 

927 for blk_nr, blk in pg.blocks.items(): 

928 ht_list = list(blk.half_turns.keys()) 

929 for ht_nr, ht in blk.half_turns.items(): 

930 ht_name = f"ht{ht_nr}_{'EM' if 'bore_field' in mesh.model_dump() else 'TH'}" 

931 self.rm.powered[ht.group].conductors[blk.conductor].append(ht_name) 

932 self.rm.powered[ht.group].vol.names.append(ht_name) 

933 self.rm.powered[ht.group].vol.numbers.append(ht.tag) 

934 if 'bore_field' in mesh.model_dump(): 

935 self.rm.powered[ht.group].vol.currents.append(initial_current * (1 if blk.current_sign > 0 else -1)) 

936 if geometry.model_dump().get('use_TSA', False): 

937 for line in ['l', 'i', 'o', 'h']: 

938 # Bare edges 

939 self.rm.powered[ht.group].surf_in.names.append(ht_name + line) 

940 self.rm.powered[ht.group].surf_in.numbers.append(ht.lines[line]) 

941 if line in 'io': self.rm.thin_shells.normals_directed['radially'].append(ht.lines[line]) 

942 else: self.rm.thin_shells.normals_directed['azimuthally'].append(ht.lines[line]) 

943 # Auxiliary thin shells 

944 for line_name, line_tag in ht.aux_lines.items(): 

945 self.rm.powered[ht.group].surf_in.names.append(line_name) 

946 self.rm.powered[ht.group].surf_in.numbers.append(line_tag) 

947 self.rm.thin_shells.normals_directed['radially'].append(line_tag) 

948 # Thin shells 

949 for line_name, line_tag in dict(ht.mid_layer_lines.inner, **ht.mid_layer_lines.outer).items(): 

950 self.rm.powered[ht.group].surf_out.names.append(line_name) 

951 self.rm.powered[ht.group].surf_out.numbers.append(line_tag) 

952 self.rm.thin_shells.normals_directed['radially'].append(line_tag) 

953 for line_name, line_tag in dict(ht.mid_pole_lines, **ht.mid_winding_lines, **ht.mid_turn_lines).items(): 

954 self.rm.powered[ht.group].surf_out.names.append(line_name) 

955 self.rm.powered[ht.group].surf_out.numbers.append(line_tag) 

956 self.rm.thin_shells.normals_directed['azimuthally'].append(line_tag) 

957 

958 # Which thin shells or exterior conductor edges precede a second group (r2 or a2) 

959 if ht.group[1] == '2': 

960 # mid-turn thin shells precede r2 

961 for line_name, line_tag in ht.mid_turn_lines.items(): 

962 if (ht_list.index(ht_nr) != 0 and int(line_name[line_name.index('_') + 1:]) == ht_nr) or\ 

963 (ht_list.index(ht_nr) == 0 and 'w' in line_name): 

964 self.rm.thin_shells.second_group_is_next['azimuthally'].append(line_tag) 

965 # mid-pole thin shells precede r2 

966 if ht_list.index(ht_nr) == 0 and ht.mid_pole_lines: 

967 self.rm.thin_shells.second_group_is_next['azimuthally'].append(list(ht.mid_pole_lines.values())[0]) 

968 # conductor edges precede r2 

969 elif ht_list.index(ht_nr) == 0 and len(ht.mid_turn_lines) + len(ht.mid_winding_lines) + len(ht.mid_pole_lines) == 1: 

970 self.rm.thin_shells.second_group_is_next['azimuthally'].append(ht.lines['l']) 

971 # mid-winding thin shells precede r2 

972 for line_name, line_tag in ht.mid_winding_lines.items(): 

973 if int(line_name[line_name.index('_') + 1:]) == ht_nr: 

974 self.rm.thin_shells.second_group_is_next['azimuthally'].append(line_tag) 

975 elif ht_list.index(ht_nr) == len(ht_list) - 1: 

976 # mid-turn thin shells precede r2 

977 for line_name, line_tag in ht.mid_turn_lines.items(): 

978 if 'w' in line_name: 

979 self.rm.thin_shells.second_group_is_next['azimuthally'].append(line_tag) 

980 # conductor edges precede r2 

981 if len(ht.mid_turn_lines) + len(ht.mid_winding_lines) + len(ht.mid_pole_lines) == 1: 

982 self.rm.thin_shells.second_group_is_next['azimuthally'].append(ht.lines['h']) 

983 if ht.group[4] == '2': 

984 # mid-layer thin shells precede a2 

985 for line_name, line_tag in ht.mid_layer_lines.inner.items(): 

986 self.rm.thin_shells.second_group_is_next['radially'].append(line_tag) 

987 elif not ht.mid_layer_lines.outer: 

988 # conductor edges precede a2 

989 self.rm.thin_shells.second_group_is_next['radially'].append(ht.lines['o']) 

990 

991 if geometry.model_dump().get('use_TSA', False): 

992 for group in ['r1_a1', 'r2_a1', 'r1_a2', 'r2_a2']: 

993 unique_thin_shells.extend(self.rm.powered[group].surf_out.numbers) 

994 

995 # Wedges 

996 if geometry.with_wedges: 

997 for wdg_nr, wdg in pg.wedges.items(): 

998 wdg_name = f"w{wdg_nr}_{'EM' if 'bore_field' in mesh.model_dump() else 'TH'}" 

999 self.rm.induced[wdg.group].vol.names.append(wdg_name) 

1000 self.rm.induced[wdg.group].vol.numbers.append(wdg.tag) 

1001 if geometry.model_dump().get('use_TSA', False): 

1002 # Bare edges 

1003 for line in ['l', 'i', 'o', 'h']: 

1004 self.rm.induced[wdg.group].surf_in.names.append(wdg_name + line) 

1005 self.rm.induced[wdg.group].surf_in.numbers.append(wdg.lines[line]) 

1006 if line in 'io': 

1007 self.rm.thin_shells.normals_directed['radially'].append(wdg.lines[line]) 

1008 else: 

1009 self.rm.thin_shells.normals_directed['azimuthally'].append(wdg.lines[line]) 

1010 # Auxiliary thin shells 

1011 for line_name, line_tag in wdg.aux_lines.items(): 

1012 self.rm.induced[wdg.group].surf_in.names.append(line_name) 

1013 self.rm.induced[wdg.group].surf_in.numbers.append(line_tag) 

1014 self.rm.thin_shells.normals_directed['radially'].append(line_tag) 

1015 # Thin shells 

1016 for line_name, line_tag in dict(wdg.mid_layer_lines.inner, **wdg.mid_layer_lines.outer).items(): 

1017 self.rm.induced[wdg.group].surf_out.names.append(line_name) 

1018 self.rm.induced[wdg.group].surf_out.numbers.append(line_tag) 

1019 self.rm.thin_shells.normals_directed['radially'].append(line_tag) 

1020 for line_name, line_tag in wdg.mid_turn_lines.items(): 

1021 self.rm.induced[wdg.group].surf_out.names.append(line_name) 

1022 self.rm.induced[wdg.group].surf_out.numbers.append(line_tag) 

1023 self.rm.thin_shells.normals_directed['azimuthally'].append(line_tag) 

1024 # Which thin shells or exterior conductor edges precede a second group (r2 or a2) 

1025 if wdg.group[4] == '2': 

1026 for line_name, line_tag in wdg.mid_layer_lines.inner.items(): 

1027 if line_name.count('w') == 2: 

1028 self.rm.thin_shells.second_group_is_next['radially'].append(line_tag) 

1029 elif not wdg.mid_layer_lines.outer: 

1030 self.rm.thin_shells.second_group_is_next['radially'].append(wdg.lines['o']) 

1031 if geometry.model_dump().get('use_TSA', False): 

1032 for group in ['r1_a1', 'r2_a1', 'r1_a2', 'r2_a2']: 

1033 unique_thin_shells.extend(self.rm.induced[group].surf_out.numbers) 

1034 

1035 # Unique mid layers 

1036 if geometry.model_dump().get('use_TSA', False): 

1037 self.rm.thin_shells.mid_turns_layers_poles = list(set(unique_thin_shells)) 

1038 

1039 # Insulation 

1040 for group_name, surface in pg.insulations.surfaces.items(): 

1041 self.rm.insulator.vol.names.append('ins' + group_name) 

1042 self.rm.insulator.vol.numbers.append(surface) 

1043 if 'insulation' in mesh.model_dump() and 'TSA' in mesh.model_dump()["insulation"]: 

1044 for group_name, curve in pg.insulations.curves.items(): 

1045 self.rm.insulator.surf.names.append('ins' + group_name) 

1046 self.rm.insulator.surf.numbers.append(curve) 

1047 

1048 # Iron 

1049 for group_name, surface in pg.iron.surfaces.items(): 

1050 self.rm.iron_yoke.vol.names.append(group_name) 

1051 self.rm.iron_yoke.vol.numbers.append(surface) 

1052 

1053 # Boundary conditions 

1054 if 'insulation' in mesh.model_dump() and 'TSA' in mesh.model_dump()["insulation"]: 

1055 # Initialize lists 

1056 for bc_data, bc_rm in zip(self.data.magnet.solve.thermal.overwrite_boundary_conditions, self.rm.boundaries.thermal): # b.c. type 

1057 bc_rm[1].bc.names = [] 

1058 bc_rm[1].bc.numbers = [] 

1059 if bc_data[0] == 'cooling': 

1060 bc_rm[1].bc.values = [] 

1061 for group in ['1_r1_a1', '2_r1_a1', '1_r2_a1', '2_r2_a1', '1_r1_a2', '2_r1_a2', '1_r2_a2', '2_r2_a2']: 

1062 bc_rm[1].groups[group] = [] 

1063 else: 

1064 bc_rm[1].bc.value = [] 

1065 for group in ['r1_a1', 'r2_a1', 'r1_a2', 'r2_a2']: 

1066 bc_rm[1].groups[group] = [] 

1067 

1068 # Apply general cooling and adiabatic 

1069 if self.data.magnet.solve.thermal.He_cooling.enabled: 

1070 cooling_side = {'i': any(coil_side in self.data.magnet.solve.thermal.He_cooling.sides for coil_side in ['inner', 'external']), 

1071 'o': any(coil_side in self.data.magnet.solve.thermal.He_cooling.sides for coil_side in ['outer', 'external']), 

1072 'hl': self.data.magnet.solve.thermal.He_cooling.sides == 'external'} 

1073 else: 

1074 cooling_side = {'i': False, 'o': False, 'hl': False} 

1075 

1076 def __assign_bnd_tag(el, name, side, bc_type, tag=None): 

1077 line_tag = tag if tag else el.lines[side] 

1078 bnd_list_names[bc_type].append(name + side) 

1079 bnd_list_numbers[bc_type].append(line_tag) 

1080 if side in 'io': new_group = el.group[:3] + 'a1' if el.group[4] == '2' else el.group[:3] + 'a2' 

1081 else: new_group = 'r1' + el.group[2:] if el.group[1] == '2' else 'r2' + el.group[2:] 

1082 bc_rm[bc_type].groups[new_group].append(line_tag) 

1083 for group_name, group in self.rm.thin_shells.groups.items(): 

1084 if line_tag in group: 

1085 bc_rm[bc_type].groups[el.group[0] + '_' + new_group].append(line_tag) 

1086 break 

1087 

1088 bc_rm = {'Robin': self.rm.boundaries.thermal.cooling, 'Neumann': self.rm.boundaries.thermal.heat_flux} 

1089 bnd_list_names = {'Robin': [], 'Neumann': []} 

1090 bnd_list_numbers = {'Robin': [], 'Neumann': []} 

1091 if geometry.model_dump().get('use_TSA', False): 

1092 # Half turn boundaries 

1093 for coil_nr, coil in self.md.geometries.coil.anticlockwise_order.coils.items(): 

1094 for lyr_nr, orders in coil.layers.items(): 

1095 for order in orders: 

1096 ht_list = list(pg.blocks[order.block].half_turns.keys()) 

1097 for ht_nr, ht in pg.blocks[order.block].half_turns.items(): 

1098 if not ht.mid_layer_lines.inner: 

1099 __assign_bnd_tag(ht, 'ht' + str(ht_nr), 'i', 'Robin' if cooling_side['i'] else 'Neumann') 

1100 if not ht.mid_layer_lines.outer: 

1101 __assign_bnd_tag(ht, 'ht' + str(ht_nr), 'o', 'Robin' if cooling_side['o'] else 'Neumann') 

1102 if ht_list.index(ht_nr) == 0 and len(ht.mid_turn_lines) + len(ht.mid_winding_lines) + len(ht.mid_pole_lines) == 1: 

1103 __assign_bnd_tag(ht, 'ht' + str(ht_nr), 'l', 'Robin' if cooling_side['hl'] else 'Neumann') 

1104 if ht_list.index(ht_nr) == len(ht_list) - 1 and len(ht.mid_turn_lines) + len(ht.mid_winding_lines) + len(ht.mid_pole_lines) == 1: 

1105 __assign_bnd_tag(ht, 'ht' + str(ht_nr), 'h', 'Robin' if cooling_side['hl'] else 'Neumann') 

1106 if ht.aux_lines: 

1107 __assign_bnd_tag(ht, 'ht' + str(ht_nr), 'o', 'Robin' if cooling_side['hl'] else 'Neumann', list(ht.aux_lines.values())[0]) 

1108 # Wedge boundaries 

1109 for wdg_nr, wdg in pg.wedges.items(): 

1110 if not wdg.mid_layer_lines.inner: 

1111 __assign_bnd_tag(wdg, 'wd' + str(wdg_nr), 'i', 'Robin' if cooling_side['i'] else 'Neumann') 

1112 if not wdg.mid_layer_lines.outer: 

1113 __assign_bnd_tag(wdg, 'wd' + str(wdg_nr), 'o', 'Robin' if cooling_side['o'] else 'Neumann') 

1114 if wdg.aux_lines: 

1115 __assign_bnd_tag(wdg, 'wd' + str(wdg_nr), 'o', 'Robin' if cooling_side['hl'] else 'Neumann', list(wdg.aux_lines.values())[0]) 

1116 else: # insulation case 

1117 for curves_group, tag in pg.insulations.curves.items(): 

1118 if self.data.magnet.solve.thermal.He_cooling.enabled: 

1119 if self.data.magnet.solve.thermal.He_cooling.sides == 'external': bc_type = 'Robin' 

1120 else: 

1121 raise ValueError(f"Cooling side '{self.data.magnet.solve.thermal.He_cooling.sides}' is not supported for meshed insulation models.") 

1122 # bc_type = 'Robin' if (self.data.magnet.solve.thermal.He_cooling.sides == 'external' or 

1123 # ('inner' in self.data.magnet.solve.thermal.He_cooling.sides and curves_group[-1] == 'i') or 

1124 # ('outer' in self.data.magnet.solve.thermal.He_cooling.sides and curves_group[-1] == 'o')) else 'Neumann' 

1125 else: 

1126 bc_type = 'Neumann' 

1127 bnd_list_names[bc_type].append('ins' + curves_group) 

1128 bnd_list_numbers[bc_type].append(tag) 

1129 if self.data.magnet.solve.thermal.He_cooling.enabled: 

1130 bc_rm['Robin'].bc.names.append(bnd_list_names['Robin']) 

1131 bc_rm['Robin'].bc.numbers.append(bnd_list_numbers['Robin']) 

1132 bc_rm['Robin'].bc.values.append([self.data.magnet.solve.thermal.He_cooling.heat_transfer_coefficient, 

1133 self.data.magnet.solve.thermal.init_temperature]) 

1134 if bnd_list_names['Neumann']: 

1135 bc_rm['Neumann'].bc.names.append(bnd_list_names['Neumann']) 

1136 bc_rm['Neumann'].bc.numbers.append(bnd_list_numbers['Neumann']) 

1137 bc_rm['Neumann'].bc.value.append(0.) 

1138 

1139 # Apply specific boundary conditions 

1140 for bc_data, bc_rm in zip(self.data.magnet.solve.thermal.overwrite_boundary_conditions, self.rm.boundaries.thermal): # b.c. type 

1141 # bc_data is a tuple like: ('temperature', {'const_T1': boundaries, value)}) 

1142 # bc_rm is a tuple like: ('temperature', DirichletCondition(names, numbers, value)) 

1143 

1144 for _, bc in bc_data[1].items(): # all boundary conditions of one b.c. type (e.g., Dirichlet with different temperatures) 

1145 bnd_list_names = [] 

1146 bnd_list_numbers = [] 

1147 if geometry.model_dump().get('use_TSA', False): 

1148 for bnd in bc.boundaries: # all boundaries of one boundary condition 

1149 if bnd[0] == 'w': 

1150 if not geometry.with_wedges: 

1151 raise Exception('Wedge regions are disabled.') 

1152 # Fetch the physical group of the wedge 

1153 pg_el = pg.wedges[int(bnd[1:-1])] 

1154 name = 'wd' + bnd[1:] 

1155 else: 

1156 # Fetch the physical group of the half turn 

1157 ht_index = self.strands['ht'].index(int(bnd[:-1])) 

1158 pg_el = pg.blocks[self.strands['block'][ht_index]].half_turns[int(bnd[:-1])] 

1159 name = 'ht' + bnd 

1160 line_pg_tag = pg_el.lines[bnd[-1]] 

1161 bnd_list_names.append(name) 

1162 bnd_list_numbers.append(line_pg_tag) 

1163 # Find the half turn group this boundary is assigned to and take the complementary 

1164 if bnd[-1] in 'io': 

1165 new_group = pg_el.group[:3] + 'a1' if pg_el.group[4] == '2' else pg_el.group[:3] + 'a2' 

1166 else: # ['l', 'h'] todo: if applied to an inner line (i.e., not domain boundaries), extra code needed because the line would belong to two groups 

1167 new_group = 'r1' + pg_el.group[2:] if pg_el.group[1] == '2' else 'r2' + pg_el.group[2:] 

1168 # Overwrite general cooling and adiabatic condition 

1169 if self.data.magnet.solve.thermal.He_cooling.enabled: 

1170 if name in self.rm.boundaries.thermal.cooling.bc.names[0]: 

1171 bnd_idx = self.rm.boundaries.thermal.cooling.bc.names[0].index(name) 

1172 self.rm.boundaries.thermal.cooling.bc.names[0].pop(bnd_idx) 

1173 bnd_idx = self.rm.boundaries.thermal.cooling.bc.numbers[0].pop(bnd_idx) 

1174 self.rm.boundaries.thermal.cooling.groups[new_group].pop( 

1175 self.rm.boundaries.thermal.cooling.groups[new_group].index(bnd_idx)) 

1176 if self.data.magnet.solve.thermal.He_cooling.sides != 'external': 

1177 if name in self.rm.boundaries.thermal.heat_flux.bc.names[0]: 

1178 bnd_idx = self.rm.boundaries.thermal.heat_flux.bc.names[0].index(name) 

1179 self.rm.boundaries.thermal.heat_flux.bc.names[0].pop(bnd_idx) 

1180 bnd_idx = self.rm.boundaries.thermal.heat_flux.bc.numbers[0].pop(bnd_idx) 

1181 self.rm.boundaries.thermal.heat_flux.groups[new_group].pop( 

1182 self.rm.boundaries.thermal.heat_flux.groups[new_group].index(bnd_idx)) 

1183 # Assign the tag 

1184 bc_rm[1].groups[new_group].append(line_pg_tag) 

1185 # Extra grouping for Robin virtual shells 

1186 if bc_data[0] == 'cooling': 

1187 for group_name, group in self.rm.thin_shells.groups.items(): 

1188 if line_pg_tag in group: 

1189 bc_rm[1].groups[group_name[0] + '_' + new_group].append(line_pg_tag) 

1190 break 

1191 else: # the b.c. are assigned to insulation boundaries instead 

1192 pass # todo: not supported yet 

1193 bc_rm[1].bc.names.append(bnd_list_names) 

1194 bc_rm[1].bc.numbers.append(bnd_list_numbers) 

1195 if bc_data[0] == 'cooling': 

1196 bc_rm[1].bc.values.append([bc.heat_transfer_coefficient, self.data.magnet.solve.thermal.init_temperature]) 

1197 elif bc_data[0] == 'temperature': 

1198 bc_rm[1].bc.value.append(bc.const_temperature) 

1199 elif bc_data[0] == 'heat_flux': 

1200 bc_rm[1].bc.value.append(bc.const_heat_flux) 

1201 

1202 def setMeshOptions(self, run_type): 

1203 """ 

1204 Meshes the generated domain 

1205 """ 

1206 mesh = self.data.magnet.mesh 

1207 gmsh.option.setNumber("Mesh.MeshSizeExtendFromBoundary", 0) 

1208 gmsh.option.setNumber("Mesh.MeshSizeFromPoints", 0) 

1209 gmsh.option.setNumber("Mesh.MeshSizeFromCurvature", 0) 

1210 gmsh.option.setNumber("Mesh.Algorithm", 6) 

1211 gmsh.option.setNumber("Mesh.Optimize", 1) 

1212 gmsh.option.setNumber("Mesh.ElementOrder", 1) 

1213 

1214 def generateMesh(self): 

1215 gmsh.option.setNumber("General.Terminal", self.verbose) 

1216 self.mesh.generate(2) 

1217 # self.mesh.removeDuplicateNodes() 

1218 # self.occ.synchronize() 

1219 # self.gu.launch_interactive_GUI() 

1220 

1221 def checkMeshQuality(self): 

1222 tags = self.mesh.getElements(2)[1][0] 

1223 

1224 self.mesh_parameters['SJ'] = min(self.mesh.getElementQualities(elementTags=tags, qualityName='minSJ')) 

1225 self.mesh_parameters['SICN'] = min(self.mesh.getElementQualities(elementTags=tags, qualityName='minSICN')) 

1226 self.mesh_parameters['SIGE'] = min(self.mesh.getElementQualities(elementTags=tags, qualityName='minSIGE')) 

1227 self.mesh_parameters['Gamma'] = min(self.mesh.getElementQualities(elementTags=tags, qualityName='gamma')) 

1228 self.mesh_parameters['nodes'] = len(self.mesh.getNodes()[0]) 

1229 

1230 # gmsh.plugin.setNumber("AnalyseMeshQuality", "JacobianDeterminant", 1) 

1231 # gmsh.plugin.setNumber("AnalyseMeshQuality", "CreateView", 100) 

1232 # test = gmsh.plugin.run("AnalyseMeshQuality") 

1233 # test2 = gmsh.view.getModelData(test, test) 

1234 

1235 # gmsh.logger.getLastError() 

1236 # gmsh.logger.get() 

1237 

1238 def saveClosestNeighboursList(self): 

1239 

1240 def _closest_node_on_reference(origin_points, reference_points): 

1241 """ 

1242 Compute list of lists with each list representing one original node (3 entries for 3 coordinates) and the closest 

1243 reference mesh node (3 entries for 3 coordinates). 

1244 :param origin_points: list of origin point as list of 3 floats per origin node 

1245 :param reference_points: list of coordinates of reference mesh points as list of 3 floats per reference mesh node 

1246 :return: returns list of lists with the closest reference mesh node per origin node 

1247 """ 

1248 closest_node = [] 

1249 for x in range(0, len(origin_points), 3): 

1250 origin_point = origin_points[x:x + 3] 

1251 # Compute distance list between origin point and reference point list 

1252 dist_lst = [Func.points_distance(origin_point, reference_points[y:y + 3]) for y in range(0, len(reference_points), 3)] 

1253 min_idx = 3 * np.argmin(dist_lst) 

1254 closest_node.append(origin_point + reference_points[min_idx:min_idx + 3]) 

1255 return closest_node 

1256 

1257 def _get_closest_nodes(side): 

1258 origin_list = self.mesh.getNodesForPhysicalGroup(1, mid_layer)[1].tolist() 

1259 reference_list = self.mesh.getNodesForPhysicalGroup(1, el.lines[side])[1].tolist() 

1260 self.rc.neighbouring_nodes.groups[group].extend( 

1261 [node for node_list in _closest_node_on_reference(origin_list, reference_list) for node in node_list]) 

1262 

1263 logger.info(f"Info : {self.data.general.magnet_name} - F i n d i n g C l o s e s t N e i g h b o u r s . . .") 

1264 logger.info(f"Info : Finding closest reference nodes ...") 

1265 

1266 self.rc.neighbouring_nodes.groups['1_1'] = [] 

1267 self.rc.neighbouring_nodes.groups['2_1'] = [] 

1268 self.rc.neighbouring_nodes.groups['1_2'] = [] 

1269 self.rc.neighbouring_nodes.groups['2_2'] = [] 

1270 

1271 for blk_nr, blk in self.md.domains.physical_groups.blocks.items(): 

1272 for ht_nr, el in blk.half_turns.items(): 

1273 ht_list = list(blk.half_turns.keys()) 

1274 group = el.group[1] + '_' + el.group[-1] 

1275 for line_name, mid_layer in el.mid_layer_lines.inner.items(): _get_closest_nodes('i') 

1276 for line_name, mid_layer in el.mid_layer_lines.outer.items(): _get_closest_nodes('o') 

1277 for line_name, mid_layer in el.aux_lines.items(): _get_closest_nodes('o') 

1278 for line_name, mid_layer in el.mid_pole_lines.items(): _get_closest_nodes('l' if ht_list.index(ht_nr) == 0 else 'h') 

1279 for line_name, mid_layer in el.mid_winding_lines.items(): _get_closest_nodes('l' if ht_list.index(ht_nr) == 0 else 'h') 

1280 for line_name, mid_layer in el.mid_turn_lines.items(): 

1281 high = ht_list.index(ht_nr) == len(ht_list) - 1 if 'w' in line_name\ 

1282 else int(line_name[:line_name.index('_')]) == ht_nr 

1283 _get_closest_nodes('h' if high else 'l') 

1284 for wdg_nr, el in self.md.domains.physical_groups.wedges.items(): 

1285 group = el.group[1] + '_' + el.group[-1] 

1286 for line_name, mid_layer in el.mid_layer_lines.inner.items(): _get_closest_nodes('i') 

1287 for line_name, mid_layer in el.mid_layer_lines.outer.items(): _get_closest_nodes('o') 

1288 for line_name, mid_layer in el.aux_lines.items(): _get_closest_nodes('o') 

1289 for line_name, mid_layer in el.mid_turn_lines.items(): 

1290 _get_closest_nodes('l' if line_name == list(el.mid_turn_lines.keys())[0] else 'h') 

1291 

1292 logger.info(f"Info : {self.data.general.magnet_name} - E n d F i n d i n g C l o s e s t N e i g h b o u r s") 

1293 

1294 def selectMeshNodes(self, elements: str): 

1295 

1296 logger.info(f"Info : {self.data.general.magnet_name} - S e l e c t i n g M e s h N o d e s . . .") 

1297 logger.info(f"Info : Selecting a mesh node per isothermal {elements[:-1]} ...") 

1298 

1299 if elements == 'conductors': 

1300 bare_mesh = {'1_1': self.rm.powered['r1_a1'].vol.numbers, '2_1': self.rm.powered['r2_a1'].vol.numbers, 

1301 '1_2': self.rm.powered['r1_a2'].vol.numbers, '2_2': self.rm.powered['r2_a2'].vol.numbers} 

1302 groups = self.rc.isothermal_nodes.conductors 

1303 

1304 # dir + robin + mid_layers 

1305 # potentially all thin shell lines if easier 

1306 line_tags = self.rm.boundaries.thermal.temperature.groups['r1_a1'] + \ 

1307 self.rm.boundaries.thermal.temperature.groups['r1_a2'] + \ 

1308 self.rm.boundaries.thermal.temperature.groups['r2_a1'] + \ 

1309 self.rm.boundaries.thermal.temperature.groups['r2_a2'] + \ 

1310 self.rm.boundaries.thermal.cooling.groups['r1_a1'] + \ 

1311 self.rm.boundaries.thermal.cooling.groups['r1_a2'] + \ 

1312 self.rm.boundaries.thermal.cooling.groups['r2_a1'] + \ 

1313 self.rm.boundaries.thermal.cooling.groups['r2_a2'] + \ 

1314 self.rm.thin_shells.mid_turns_layers_poles 

1315 for tag in line_tags: 

1316 coords = list(self.mesh.getNodesForPhysicalGroup(dim=1, tag=tag)[1])[:3] 

1317 self.rc.isothermal_nodes.thin_shells[tag] = [float(coords[0]), float(coords[1]), float(coords[2])] 

1318 

1319 elif elements == 'wedges': 

1320 bare_mesh = {'1_1': self.rm.induced['r1_a1'].vol.numbers, '2_1': self.rm.induced['r2_a1'].vol.numbers, 

1321 '1_2': self.rm.induced['r1_a2'].vol.numbers, '2_2': self.rm.induced['r2_a2'].vol.numbers} 

1322 groups = self.rc.isothermal_nodes.wedges 

1323 else: 

1324 bare_mesh = {} 

1325 groups = {} 

1326 

1327 for group, tags_list in bare_mesh.items(): 

1328 groups[group] = {} 

1329 for tag in tags_list: 

1330 coords = list(self.mesh.getNodesForPhysicalGroup(dim=2, tag=tag)[1])[:3] 

1331 groups[group][tag] = [float(coords[0]), float(coords[1]), float(coords[2])] 

1332 

1333 for tag in self.rm.boundaries.thermal.cooling.groups['r' + group[0] + '_' + 'a' + group[-1]]: 

1334 coords = list(self.mesh.getNodesForPhysicalGroup(dim=1, tag=tag)[1])[:3] 

1335 groups[group][tag] = [float(coords[0]), float(coords[1]), float(coords[2])] 

1336 

1337 logger.info(f"Info : {self.data.general.magnet_name} - E n d S e l e c t i n g M e s h N o d e s") 

1338 # import time 

1339 # time.sleep(100) 

1340 # TODO: add to pro file automatically 

1341 

1342 # self.occ.synchronize() 

1343 # self.gu.launch_interactive_GUI() 

1344