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

1330 statements  

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

1import os 

2import re 

3 

4import gmsh 

5import copy 

6import numpy as np 

7import json 

8import logging 

9 

10from fiqus.utils.Utils import GmshUtils 

11from fiqus.utils.Utils import FilesAndFolders as Util 

12from fiqus.utils.Utils import GeometricFunctions as Func 

13from fiqus.data import DataFiQuS as dF 

14from fiqus.data import DataMultipole as dM 

15from fiqus.data import RegionsModelFiQuS as rM 

16 

17logger = logging.getLogger('FiQuS') 

18 

19class Mesh: 

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

21 verbose: bool = False): 

22 """ 

23 Class to generate mesh 

24 :param data: FiQuS data model 

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

26 """ 

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

28 self.mesh_folder = mesh_folder 

29 self.verbose: bool = verbose 

30 

31 self.md = dM.MultipoleData() 

32 self.rc = dM.MultipoleRegionCoordinate() 

33 self.rm = rM.RegionsModel() 

34 self.strands = None 

35 

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

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

38 self.occ = gmsh.model.occ 

39 self.mesh = gmsh.model.mesh 

40 

41 self.brep_curves = {} 

42 for name in list( 

43 set(self.data.magnet.geometry.electromagnetics.areas + self.data.magnet.geometry.thermal.areas)): 

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

45 

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

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

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

49 

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

51 self.ins_type_cond = {} 

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

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

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

55 'external': copy.deepcopy(qh_keys)} 

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

57 self.ins_type = {'mid_pole': {}, 'mid_winding': {}, 'mid_layer': {}, 'aux': {}, 'collar': {}, 'poles': {}} 

58 

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

60 

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

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

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

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

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

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

67 # yoke 

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

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

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

71 # key 

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

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

74 'BH_air': [255, 128, 0], # also orange 

75 'Air': [255, 128, 0], # also orange 

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

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

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

79 

80 def clear(self): 

81 self.md = dM.MultipoleData() 

82 self.rc = dM.MultipoleRegionCoordinate() 

83 self.rm = rM.RegionsModel() 

84 for name in self.brep_curves.keys(): 

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

86 gmsh.clear() 

87 

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

89 if gui: 

90 self.gu.launch_interactive_GUI() 

91 else: 

92 if gmsh.isInitialized(): 

93 gmsh.clear() 

94 gmsh.finalize() 

95 

96 def loadStrandPositions(self, run_type): 

97 with open(f"{self.geom_files}_{run_type}.strs", 'r') as f: 

98 self.strands = json.load(f) 

99 

100 def loadAuxiliaryFile(self, run_type): 

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

102 

103 def updateAuxiliaryFile(self, run_type): 

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

105 # md2 = Util.read_data_from_yaml(f"{self.geom_files}.aux", dM.MultipoleData) 

106 # md2.domains.physical_groups = self.md.domains.physical_groups 

107 # Util.write_data_to_yaml(f"{self.geom_files}.aux", md2.dict()) 

108 

109 def saveMeshFile(self, run_type): 

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

111 

112 def saveRegionFile(self, run_type): 

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

114 

115 def saveRegionCoordinateFile(self, run_type): 

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

117 

118 def getIronCurvesTags(self, physics_solved): 

119 for t in self.data.magnet.geometry.electromagnetics.areas if physics_solved == 'EM' else self.data.magnet.geometry.thermal.areas: 

120 for quadrant, qq in getattr(self.md.geometries, t).quadrants.items(): 

121 for ar_name, area in qq.areas.items(): 

122 if area.surface and not re.match(r"^ar.h", ar_name): 

123 self.brep_curves[t][quadrant] |= set(gmsh.model.getAdjacencies(2, area.surface)[1]) 

124 

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

126 thresholds = [] 

127 self.occ.synchronize() 

128 

129 if mesh.conductors.field.enabled: 

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

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

132 [line for coil_nr, coil in 

133 self.md.geometries.coil.anticlockwise_order.coils.items() for layer_nr, layer in 

134 coil.layers.items() 

135 for _, block_order in enumerate(layer) for _, line in 

136 self.md.geometries.coil.coils[coil_nr].poles[block_order.pole].layers[ 

137 layer_nr].windings[block_order.winding].blocks[ 

138 block_order.block].half_turns.lines.items()] 

139 ) 

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

141 

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

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

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

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

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

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

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

149 thresholds.append(threshold_conductors) 

150 

151 if mesh.wedges.field.enabled: 

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

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

154 [line for _, coil in self.md.geometries.wedges.coils.items() for _, layer in 

155 coil.layers.items() for _, wdg in layer.wedges.items() for _, line in 

156 wdg.lines.items()] 

157 ) 

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

159 

160 # 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()]}") 

161 

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

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

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

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

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

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

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

169 thresholds.append(threshold_wedges) 

170 

171 for area in geometry.areas: 

172 distance = self.mesh.field.add("Distance") 

173 if not (area == 'collar' and self.data.magnet.mesh.thermal.collar.Enforce_TSA_mapping and run_type == 'TH'): 

174 self.mesh.field.setNumbers(distance, "CurvesList", 

175 [line for _, qq in self.brep_curves[area].items() for line in qq]) 

176 else: 

177 # make sure this does not interfere with the enforced TSA mapping. Apply threshold to the inner collar points and the cooling holes 

178 vals = [x for sublist in self.md.geometries.collar.inner_boundary_tags.values() for x in 

179 sublist] ## flatten 

180 self.mesh.field.setNumbers(distance, "PointsList", list(set([point[1] for point in 

181 gmsh.model.getBoundary( 

182 [(1, line) for line in vals], 

183 combined=False, oriented=False)]))) 

184 

185 self.mesh.field.setNumber(distance, "Sampling", 100) 

186 

187 k = area if area != 'iron_yoke' else 'iron_field' 

188 if getattr(mesh, k).enabled: 

189 threshold = self.mesh.field.add("Threshold") 

190 self.mesh.field.setNumber(threshold, "InField", distance) 

191 self.mesh.field.setNumber(threshold, "SizeMin", getattr(mesh, k).SizeMin) 

192 self.mesh.field.setNumber(threshold, "SizeMax", getattr(mesh, k).SizeMax) 

193 self.mesh.field.setNumber(threshold, "DistMin", getattr(mesh, k).DistMin) 

194 self.mesh.field.setNumber(threshold, "DistMax", getattr(mesh, k).DistMax) 

195 thresholds.append(threshold) 

196 

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

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

199 self.mesh.field.setNumbers(distance_bore, "PointsList", 

200 [pnt for pnt_name, pnt in self.md.geometries.air.points.items() if 

201 'bore' in pnt_name]) 

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

203 

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

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

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

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

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

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

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

211 thresholds.append(threshold_bore) 

212 

213 if run_type == 'TH' and mesh.reference.enabled: # add reference meshing between collar and insulation 

214 """ 

215 (hardcoded) REFERENCE MESH for the FalconD-type magnet 

216 """ 

217 if not 'collar' in geometry.areas: 

218 raise Exception("Adding the reference segment without collar is not intended.") 

219 distance_ref = self.mesh.field.add("Distance") 

220 if 'FALCOND_C' in self.data.general.magnet_name.upper() and 'POLE' in self.data.general.magnet_name.upper(): 

221 lines = list(range(754, 855)) + list(range(910, 1011)) 

222 self.mesh.field.setNumbers(distance_ref, "CurvesList", lines) 

223 else: 

224 raise Exception("Reference meshing is not implemented for this magnet.") 

225 

226 self.mesh.field.setNumber(distance_ref, "Sampling", 100) 

227 

228 threshold_ref = self.mesh.field.add("Threshold") 

229 self.mesh.field.setNumber(threshold_ref, "InField", distance_ref) 

230 self.mesh.field.setNumber(threshold_ref, "SizeMin", mesh.reference.SizeMin) 

231 self.mesh.field.setNumber(threshold_ref, "SizeMax", mesh.reference.SizeMax) 

232 self.mesh.field.setNumber(threshold_ref, "DistMin", mesh.reference.DistMin) 

233 self.mesh.field.setNumber(threshold_ref, "DistMax", mesh.reference.DistMax) 

234 self.mesh.field.setNumber(threshold_ref, "StopAtDistMax", 1) 

235 thresholds.append(threshold_ref) 

236 

237 insulation_mesh_fields = [] 

238 if run_type == 'TH': 

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

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

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

242 if area_name.isdigit(): 

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

244 insulation_mesh_field = insulation_mesh_fields[-1] 

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

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

247 

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

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

250 self.mesh.field.setAsBackgroundMesh(background) 

251 

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

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

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

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

256 for _, block_order in enumerate(layer): 

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

258 layer_nr].windings[block_order.winding] 

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

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

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

262 elements = 1 

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

264 elements = max(1, round( 

265 cable.bare_cable_height_mean / mesh.conductors.transfinite.curve_target_size_height)) 

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

267 elements = max(1, round( 

268 cable.bare_cable_width / mesh.conductors.transfinite.curve_target_size_width)) 

269 

270 self.mesh.setTransfiniteCurve(line, elements) 

271 if mesh.conductors.transfinite.enabled_for == 'curves_and_surfaces' or mesh.model_dump().get( 

272 'isothermal_conductors', False): 

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

274 self.mesh.setTransfiniteSurface(area.surface) 

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

276 

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

278 # Apply transfinite curves to thin shell lines 

279 if geometry.use_TSA: 

280 gts = self.md.geometries.thin_shells 

281 conductor_target_sizes = {'width': mesh.conductors.transfinite.curve_target_size_width, 

282 'height': mesh.conductors.transfinite.curve_target_size_height} 

283 wedge_target_sizes = {'width': mesh.wedges.transfinite.curve_target_size_width, 

284 'height': mesh.wedges.transfinite.curve_target_size_height} 

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

286 gts.mid_layers_wdg_to_wdg, 

287 gts.mid_poles, gts.mid_windings, gts.mid_turn_blocks, gts.mid_wedge_turn, 

288 gts.mid_layers_aux], 

289 ['height', 'height', 'height', 'height', 'width', 'width', 'width', 'width', 

290 'height']): 

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

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

293 if mesh.isothermal_conductors or mesh.isothermal_wedges: 

294 elements = 1 

295 else: 

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

297 gmsh.model.getParametrizationBounds(1, line)]) 

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

299 conductor_target_sizes[side] 

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

301 # it's a wedge 

302 if ts_name.count('w') == 2 and mesh.wedges.transfinite.enabled_for in ['curves', 

303 'curves_and_surfaces']: 

304 self.mesh.setTransfiniteCurve(line, elements) 

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

306 'curves_and_surfaces']: 

307 self.mesh.setTransfiniteCurve(line, elements) 

308 

309 if geometry.use_TSA_new: 

310 if not self.data.magnet.mesh.thermal.collar.Enforce_TSA_mapping: 

311 r_tmp = np.abs(Func.points_distance(coords[:2], [0, 0])) 

312 gts = self.md.geometries.thin_shells.collar_layers 

313 # conductor and wedge target sizes are defined above 

314 collar_size = None 

315 # accounts for the distance between the collar and the TSA line: should be col2 = col1*r2/r1. r_tmp is approx. distance outer ht to centre 

316 for ts_name, ts in gts.items(): 

317 for name, line in ts.lines.items(): 

318 coords = gmsh.model.getValue(1, line, 

319 [i[0] for i in gmsh.model.getParametrizationBounds(1, line)]) 

320 if collar_size is None: 

321 collar_size = r_tmp / Func.points_distance(coords[:2], [0, 

322 0]) * self.data.magnet.mesh.thermal.collar.SizeMin 

323 target_size = min( 

324 wedge_target_sizes['width'] if ts_name.startswith('w') else conductor_target_sizes[ 

325 'width'], collar_size) 

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

327 

328 self.mesh.setTransfiniteCurve(line, elements) 

329 else: # since we force the mesh on the collar side anyway 

330 gts = self.md.geometries.thin_shells.collar_layers 

331 # conductor and wedge target sizes are defined above 

332 collar_size = self.data.magnet.mesh.thermal.collar.SizeMin 

333 for ts_name, ts in gts.items(): 

334 for name, line in ts.lines.items(): 

335 coords = gmsh.model.getValue(1, line, 

336 [i[0] for i in gmsh.model.getParametrizationBounds(1, line)]) 

337 target_size = collar_size 

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

339 if elements % 2 == 1: elements += 1 

340 self.mesh.setTransfiniteCurve(line, elements) 

341 

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

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

344 # Apply transfinite curves to insulation boundaries 

345 # else: 

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

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

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

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

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

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

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

353 # # 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()] + 

354 # # [(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], 

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

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

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

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

359 

360 # Apply transfinite curves to wedges 

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

362 if geometry.with_wedges: 

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

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

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

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

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

368 gmsh.model.getValue(0, pnts[1], [])) 

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

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

371 gmsh.model.getValue(0, pnts[1], [])) 

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

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

374 gmsh.model.getValue(0, pnts[1], [])) 

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

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

377 elements = 1 

378 elif 'i' in line_key: 

379 elements = max(1, round( 

380 inner_height / mesh.wedges.transfinite.curve_target_size_height)) 

381 elif 'o' in line_key: 

382 elements = max(1, round((inner_height if mesh.wedges.transfinite.enabled_for in [ 

383 'curves', 'curves_and_surfaces'] 

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

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

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

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

388 self.mesh.setTransfiniteCurve(line, elements) 

389 if mesh.wedges.transfinite.enabled_for == 'curves_and_surfaces' or mesh.model_dump().get( 

390 'isothermal_wedges', False): 

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

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

393 

394 def createPhysicalGroups(self, geometry): 

395 """ 

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

397 """ 

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

399 pg_tag = offset 

400 point_offset = 4000000 

401 point_tag = offset 

402 

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

404 pg = self.md.domains.physical_groups 

405 ge = self.md.domains.groups_entities 

406 

407 # Create the physical groups of iron + set color 

408 group_keys = geometry.areas + ['ref_mesh'] 

409 for key in group_keys: 

410 group_surfaces = getattr(ge, key) 

411 pg_surfaces = getattr(pg, key).surfaces 

412 for group_name, surfaces in group_surfaces.items(): 

413 pg_surfaces[group_name] = gmsh.model.addPhysicalGroup(2, surfaces, pg_tag) 

414 gmsh.model.setPhysicalName(2, pg_surfaces[group_name], group_name) 

415 if group_name not in self.colors: 

416 logger.warning(f"Color for group '{group_name}' not defined, using default color [0, 0, 0].") 

417 color = self.colors.get(group_name, [0, 0, 0]) 

418 gmsh.model.setColor([(2, i) for i in surfaces], *color) 

419 pg_tag += 1 

420 

421 # Create the physical group of air infinite + set color of entities.air 

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

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

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

425 pg_tag), 'bore_centers') 

426 pg_tag += 1 

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

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

429 pg_tag += 1 

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

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

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

433 self.colors['air_inf'][1], self.colors['air_inf'][2]) 

434 pg_tag += 1 

435 pg.air = gmsh.model.addPhysicalGroup(2, ge.air, pg_tag) 

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

437 gmsh.model.setColor([(2, i) for i in ge.air], self.colors['air'][0], self.colors['air'][1], 

438 self.colors['air'][2]) 

439 pg_tag += 1 

440 

441 # Create physical groups of half turns 

442 lyr_list_group = [] 

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

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

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

446 ht_list_group = [] 

447 ht_nodes_group = [] 

448 for nr, block_order in enumerate(layer): 

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

450 layer_nr].windings[block_order.winding] 

451 block = wnd.blocks[block_order.block] 

452 block_nr = block_order.block 

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

454 blk_pg = pg.blocks[block_nr] 

455 blk_pg.current_sign = block.current_sign 

456 blk_pg.conductor = wnd.conductor_name 

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

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

459 ht_list_group.extend(ht_list) 

460 if nr + 1 < len(layer): 

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

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

463 # Create 2D physical groups of half turns 

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

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

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

467 # Create physical group and assign name and color 

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

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

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

471 pg_tag += 1 

472 

473 # Assign thin-shell group 

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

475 if geometry.model_dump().get('correct_block_coil_tsa_checkered_scheme', False) and \ 

476 self.md.geometries.coil.coils[coil_nr].type == 'reversed-block-coil': 

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

478 else: 

479 azimuthal = 'a1' if lyr_list_group.index( 

480 'cl' + str(coil_nr) + 'ly' + str(layer_nr)) % 2 == 0 else 'a2' 

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

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

483 

484 # Create 1D physical groups of thin shells 

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

486 ht_nr = ht_line_key[:-1] 

487 # Create half turn line groups 

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

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

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

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

492 pg_tag += 1 

493 # Store thin shell tags 

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

495 

496 # Add single_node per half turn for EM mesh 

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

498 inner_line = \ 

499 gmsh.model.getEntitiesForPhysicalGroup(1, blk_pg.half_turns[int(ht_key)].lines['i'])[0] 

500 inner_nodes = gmsh.model.getBoundary([(1, inner_line)]) 

501 

502 higher_line = \ 

503 gmsh.model.getEntitiesForPhysicalGroup(1, blk_pg.half_turns[int(ht_key)].lines['h'])[0] 

504 higher_nodes = gmsh.model.getBoundary([(1, higher_line)]) 

505 

506 single_node = list(set(inner_nodes) & set(higher_nodes))[0][1] 

507 blk_pg.half_turns[int(ht_key)].single_node = gmsh.model.addPhysicalGroup(0, [single_node], 

508 point_offset + point_tag, 

509 f"{ht_key}_single_node") 

510 

511 point_tag += 1 

512 

513 # Create points region for projection 

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

515 point_list = [gmsh.model.getAdjacencies(1, gmsh.model.getAdjacencies(2, ht.surface)[1][0])[1][0] 

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

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

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

519 layer_nr].windings[block_order.winding].blocks[ 

520 block_order.block].half_turns.areas.items()] 

521 ### add one point per wedge 

522 wdg_points = [] 

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

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

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

526 wdg_points.append(list(gmsh.model.getAdjacencies(2, wedge.areas[str(wedge_nr)].surface)[1])[0]) 

527 

528 self.md.domains.physical_groups.half_turns_points = gmsh.model.addPhysicalGroup(0, wdg_points + point_list, 

529 int(4e6)) 

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

531 

532 # Create physical groups of insulations 

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

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

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

536 # Areas 

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

538 if area_name.isdigit(): 

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

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

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

542 self.colors['insul'][2]) 

543 pg_tag += 1 

544 

545 # Boundaries 

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

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

548 1, [bnd[1] for bnd in 

549 gmsh.model.getBoundary([(2, list(group.ins.areas.values())[0].surface)], combined=False, 

550 oriented=False)[:len(group.ins.lines)]], pg_tag) 

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

552 pg_tag += 1 

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

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

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

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

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

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

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

560 # pg_tag += 1 

561 

562 # Create physical groups of wedges 

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

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

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

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

567 wedge_pg = pg.wedges[wedge_nr] 

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

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

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

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

572 pg_tag += 1 

573 # Assign thin-shell group 

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

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

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

577 else: 

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

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

580 

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

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

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

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

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

586 pg_tag += 1 

587 

588 # Create physical groups of thin shells 

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

590 gts = self.md.geometries.thin_shells 

591 # Create physical groups of block mid-layer lines 

592 block_coil_flag = False 

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

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

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

596 if (any(int(blk_i) == blk_order.block for blk_order in 

597 self.md.geometries.coil.anticlockwise_order.coils[coil_nr].layers[1]) and 

598 any(int(blk_o) == blk_order.block for blk_order in 

599 self.md.geometries.coil.anticlockwise_order.coils[coil_nr].layers[ 

600 1])): # block-coil mid-pole case 

601 block_coil_flag = True 

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

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

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

605 pg_tag += 1 

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

607 if ht1 in ts.half_turn_lists[blk_i]: 

608 if block_coil_flag: 

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

610 else: 

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

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

613 else: 

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

615 if block_coil_flag: 

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

617 else: 

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

619 

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

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

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

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

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

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

626 pg_tag += 1 

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

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

629 line_name.index('_') + 1:] 

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

631 

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

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

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

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

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

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

638 pg_tag += 1 

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

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

641 line_name.index('_') + 1:] 

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

643 

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

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

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

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

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

649 pg_tag += 1 

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

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

652 

653 # Create non-mid-layer thin shells 

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

655 [gts.mid_poles, gts.mid_windings, gts.mid_turn_blocks, 

656 gts.mid_wedge_turn, gts.mid_layers_aux]): 

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

658 if '_' in ts_name: 

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

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

661 else: # mid_turn_blocks 

662 el_name1, el_name2 = ts_name, ts_name 

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

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

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

666 pg_tag += 1 

667 if ts_group_name == 'aux': 

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

669 else: 

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

671 pg.blocks[int(el_name2)].half_turns[int(ht2)].__dict__[ts_group_name + '_lines'][ 

672 line_name] = line_pg 

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

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

675 else: 

676 pg.blocks[int(el_name1)].half_turns[int(ht1)].__dict__[ts_group_name + '_lines'][ 

677 line_name] = line_pg 

678 

679 # Create physical groups for TSL 

680 # add the physical group inner collar lines, only nonempty for the thermal solution with collar 

681 lines = [] 

682 for quad, tags in self.md.geometries.collar.outer_boundary_tags.items(): 

683 if 'poles' in self.data.magnet.geometry.thermal.areas: 

684 lines.extend(tags[:-1]) # skip the last line, this is the line between the pole and collar 

685 else: 

686 lines.extend(tags) 

687 gmsh.model.occ.synchronize() 

688 tag = gmsh.model.addPhysicalGroup(1, lines) 

689 gmsh.model.setPhysicalName(1, tag, "outer_col") 

690 pg.outer_col = tag 

691 gmsh.model.occ.synchronize() 

692 

693 if geometry.model_dump().get('use_TSA_new', False): 

694 # line names are defined earlier when creating the geometry (so wedge and coil names are already separated) 

695 # f'{ht_nr}_x' for ht to mid and f'w{wedge_idx}_x' for wdg to mid 

696 # poles look like f'p{pole_idx}_x' 

697 layer = self.md.geometries.thin_shells.collar_layers 

698 for name, l in layer.items(): 

699 line = l.lines['1'] # default line name, contains only one line 

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

701 gmsh.model.setPhysicalName(1, line_pg, name) 

702 color = [0, 0, 0] 

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

704 # self.ins_type_col['mid_lines'][name] = line_pg 

705 self.ins_type['collar'][name] = line_pg 

706 

707 # only apply to TSA 

708 # add the physical group inner collar lines 

709 lines = [] 

710 for quad, tags in self.md.geometries.collar.inner_boundary_tags.items(): 

711 lines.extend(tags) 

712 gmsh.model.occ.synchronize() 

713 tag = gmsh.model.addPhysicalGroup(1, lines) 

714 gmsh.model.setPhysicalName(1, tag, "inner_col") 

715 pg.inner_col = tag 

716 gmsh.model.occ.synchronize() 

717 

718 # do the same for the poles, if they are defined 

719 if 'poles' in self.data.magnet.geometry.thermal.areas: 

720 layer = self.md.geometries.thin_shells.pole_layers 

721 for name, l in layer.items(): 

722 line = l.lines['1'] # default line name, contains only one line 

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

724 gmsh.model.setPhysicalName(1, line_pg, name) 

725 color = [0, 0, 0] 

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

727 self.ins_type['poles'][name] = line_pg 

728 

729 lines = [] 

730 for quad, tags in self.brep_curves['poles'].items(): 

731 lines.extend(tags) 

732 # maybe only select the lines relevant for TSA? 

733 gmsh.model.occ.synchronize() 

734 tag = gmsh.model.addPhysicalGroup(1, lines) 

735 gmsh.model.setPhysicalName(1, tag, "pole_boundary") 

736 pg.poles.curves["bdry"] = tag 

737 gmsh.model.occ.synchronize() 

738 

739 # Create physical group of collar cooling boudnary 

740 if self.data.magnet.solve.thermal.collar_cooling.enabled and 'use_TSA' in geometry.model_dump(): 

741 ## make physical group for collar cooling 

742 tag = gmsh.model.addPhysicalGroup(1, self.md.geometries.collar.cooling_tags) 

743 gmsh.model.setPhysicalName(1, tag, "collar cooling") 

744 pg.collar_cooling = tag 

745 gmsh.model.occ.synchronize() 

746 

747 # Create physical groups of symmetric boundaries 

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

749 line_tags_normal_free, line_tags_tangent_free = [], [] 

750 if geometry.symmetry == 'xy': 

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

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

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

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

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

756 self.md.domains.groups_entities.symmetric_boundaries.y 

757 elif geometry.symmetry == 'x': 

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

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

760 else: 

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

762 elif geometry.symmetry == 'y': 

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

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

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

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

767 if line_tags_normal_free: 

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

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

770 pg_tag += 1 

771 if line_tags_tangent_free: 

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

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

774 pg_tag += 1 

775 

776 def rearrangeThinShellsData(self): 

777 pg = self.md.domains.physical_groups 

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

779 qh = self.data.quench_protection.quench_heaters 

780 

781 def _store_qh_data(position, thin_shell, ts_tag): 

782 qh_ins = self.ins_type_qh[position][qh.iQH_toHalfTurn_From[ht_index]] 

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

784 qh_ins[thin_shell].append(ts_tag) 

785 if thin_shell not in self.qh_data[qh.iQH_toHalfTurn_From[ht_index]]: 

786 self.qh_data[qh.iQH_toHalfTurn_From[ht_index]][thin_shell] = {'conductor': blk_pg.conductor, 

787 'ht_side': qh_side} 

788 

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

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

791 for ln_name, ln_tag in ( 

792 pg_el.model_dump()[lines][lines_side] if lines_side else pg_el.model_dump()[lines]).items(): 

793 for ts_name in ts_groups[ts_grp]: 

794 if ts_name in geom_ts: 

795 if ln_name in ( 

796 geom_ts[ts_name][geom_ts_name2]['lines'] if geom_ts_name2 else geom_ts[ts_name]['lines']): 

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

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

799 break 

800 

801 # Collect thin shell tags 

802 # Half turns 

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

804 ts_groups = {'mid_wedge_turn': [ts_name for ts_name in self.md.geometries.thin_shells.mid_wedge_turn if 

805 blk_pg_nr == int(ts_name.split('_')[1])], 

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

807 'mid_winding': [ts_name for ts_name in ins_th.mid_winding if 

808 blk_pg_nr == int(ts_name.split('_')[0])], 

809 'mid_pole': [ts_name for ts_name in ins_th.mid_pole if 

810 blk_pg_nr == int(ts_name.split('_')[0])], 

811 'mid_layer': [ts_name for ts_name in ins_th.mid_layer if 

812 ts_name[0] != 'w' and blk_pg_nr == int(ts_name.split('_')[0]) 

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

814 ts_name.split('_')[1])], 

815 'mid_layer_qh_i': [ts_name for ts_name in ins_th.mid_layer if 

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

817 ts_name.split('_')[1])]} 

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

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

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

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

822 ht_index = qh.iQH_toHalfTurn_To.index(ht_nr) 

823 qh_side = qh.turns_sides[ht_index] 

824 if qh.iQH_toHalfTurn_From[ht_index] not in self.qh_data: self.qh_data[ 

825 qh.iQH_toHalfTurn_From[ht_index]] = {} 

826 else: 

827 qh_side = '' 

828 

829 # find conductor type of ht adjacent to wedge 

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

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

832 for ts_name in ts_groups['mid_wedge_turn']: 

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

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

835 break 

836 

837 self.ins_type_cond[str(blk_pg_nr)]['mid_turn'].extend( 

838 list(ht.mid_turn_lines.values())) # mid-turn insulation 

839 

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

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

842 

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

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

845 for ts_name in ts_groups['mid_layer_qh_i']: 

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

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

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

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

850 else: 

851 ts_lines = [] 

852 if line_name in ts_lines: 

853 _store_qh_data('internal', ts_name, line_tag) 

854 break 

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

856 _store_ts_tags(ht, geom_ts_name='mid_layers_ht_to_ht', geom_ts_name2='mid_layers', 

857 ts_grp='mid_layer', lines='mid_layer_lines', lines_side='inner') 

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

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

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

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

862 

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

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

865 for ts_name in ts_groups['mid_layer']: 

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

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

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

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

870 else: 

871 ts_lines = [] 

872 if line_name in ts_lines: 

873 _store_qh_data('internal', ts_name, line_tag) 

874 break 

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

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

877 for ts_name in ts_groups['mid_layer']: 

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

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

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

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

882 else: 

883 ts_lines = [] 

884 if line_name in ts_lines: 

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

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

887 break 

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

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

890 else: # outer insulation 

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

892 

893 # mid-pole insulation 

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

895 

896 # mid-winding insulation 

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

898 

899 if ht_list.index(ht_nr) == 0 and len(ht.mid_turn_lines) + len(ht.mid_winding_lines) + len( 

900 ht.mid_pole_lines) == 1: # lower angle external side insulation 

901 if qh_side == 'l': 

902 _store_qh_data('external', blk_pg_nr, ht.lines['l']) 

903 else: 

904 self.ins_type_cond[str(blk_pg_nr)]['lower'].append(ht.lines['l']) 

905 if ht_list.index(ht_nr) == len(ht_list) - 1 and len(ht.mid_turn_lines) + len( 

906 ht.mid_winding_lines) + len(ht.mid_pole_lines) == 1: # higher angle external side insulation 

907 if qh_side == 'h': 

908 _store_qh_data('external', blk_pg_nr, ht.lines['h']) 

909 else: 

910 self.ins_type_cond[str(blk_pg_nr)]['higher'].append(ht.lines['h']) 

911 

912 # Wedges 

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

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

915 'mid_layer': [ts_name for ts_name in ins_th.mid_layer if 

916 'w' + str(wdg_pg_nr) == ts_name.split('_')[0]]} 

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

918 'mid_turn': []} 

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

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

921 wdg_pg.lines['i']) 

922 if wdg_pg.mid_layer_lines.outer: 

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

924 for ts_name in ts_groups['mid_layer']: 

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

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

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

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

929 else: 

930 ts_lines = [] 

931 if line_name in ts_lines: 

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

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

934 break 

935 else: 

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

937 

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

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

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

941 if qh_nr != qh_nr2: 

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

943 for ts in common_ts_groups: 

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

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

946 for tag in common_tags: 

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

948 if self.qh_data[qh_nr2][ts]['ht_side'] == 'i': 

949 qh_name = str(qh_nr) + '_' + str(qh_nr2) 

950 else: 

951 qh_name = str(qh_nr2) + '_' + str(qh_nr) 

952 if qh_name not in self.ins_type_qh['internal_double']: self.ins_type_qh['internal_double'][ 

953 qh_name] = {} 

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

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

956 qh_ins_id[ts].append(tag) 

957 

958 def assignRegionsTags(self, geometry, mesh): 

959 def _get_input_insulation_data(i_name, i_type=None): 

960 ow_idx = next((index for index, couple in enumerate( 

961 self.data.magnet.solve.thermal.insulation_TSA.block_to_block.blocks_connection_overwrite) 

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

963 if i_type == 'mid_winding': 

964 mid_mat, mid_th = [self.data.magnet.solve.wedges.material], [] 

965 elif ow_idx is not None: 

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

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

968 else: 

969 mid_mat, mid_th = [self.data.magnet.solve.thermal.insulation_TSA.block_to_block.material], [] 

970 return mid_mat, mid_th 

971 

972 def _compute_insulation_thicknesses(tot_th, known_ins_th): 

973 if not mid_thicknesses: 

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

975 elif None in mid_thicknesses: 

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

977 mid_lyrs = [ 

978 th if th is not None else Func.sig_dig(tot_th - known_ins_th - input_ths) / mid_thicknesses.count( 

979 None) for th in mid_thicknesses] 

980 else: 

981 mid_lyrs = mid_thicknesses 

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

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

984 raise ValueError( 

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

986 else: 

987 return mid_lyrs, zeros 

988 

989 pg = self.md.domains.physical_groups 

990 qh = self.data.quench_protection.quench_heaters 

991 

992 # Air and air far field 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1009 

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

1011 self.rm.projection_points.name = 'projection_points' 

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

1013 

1014 # Initialize lists 

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

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

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

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

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

1020 if geometry.with_wedges: 

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

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

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

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

1025 initial_current = self.data.power_supply.I_initial 

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

1027 self.rm.powered[group].curve.names = [] 

1028 self.rm.powered[group].curve.numbers = [] 

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

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

1031 if geometry.with_wedges: 

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

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

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

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

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

1037 if geometry.with_wedges: 

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

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

1040 

1041 for attr in geometry.areas: 

1042 h = getattr(self.rm, attr).vol 

1043 h.names = [] 

1044 h.numbers = [] 

1045 

1046 # initialise reference 

1047 if self.data.magnet.mesh.thermal.reference.enabled: 

1048 self.rm.ref_mesh.vol.names = [] 

1049 self.rm.ref_mesh.vol.numbers = [] 

1050 

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

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

1053 

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

1055 unique_thin_shells = [] 

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

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

1058 else: 

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

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

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

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

1063 

1064 # always 

1065 if 'collar' in geometry.areas: 

1066 self.rm.thin_shells.bdry_curves['outer_collar'] = [pg.outer_col] 

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

1068 # Categorize insulation types 

1069 min_h = mesh.insulation.global_size 

1070 # min_h = 1 

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

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

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

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

1075 min_h_COL = mesh.insulation.TSA.global_size_COL if mesh.insulation.TSA.global_size_QH else min_h 

1076 

1077 # Conductor insulation layers 

1078 max_layer = len( 

1079 [k for k in self.md.geometries.coil.coils[1].poles[1].layers.keys()]) # todo: more elegant way ? 

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

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

1082 pg.blocks[int(el)].conductor].cable 

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

1084 if tags: 

1085 side_ins_type = [cond.material_insulation] 

1086 if ins_side in ['inner', 'outer']: 

1087 side_ins = [cond.th_insulation_along_width] 

1088 elif ins_side in ['higher', 'lower']: 

1089 side_ins = [cond.th_insulation_along_height] 

1090 else: # mid_turn 

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

1092 side_ins_type.append(cond.material_insulation) 

1093 if ins_side[0] in 'iohl' and el + ins_side[ 

1094 0] in self.data.magnet.solve.thermal.insulation_TSA.exterior.blocks: 

1095 add_mat_idx = self.data.magnet.solve.thermal.insulation_TSA.exterior.blocks.index( 

1096 el + ins_side[0]) 

1097 side_ins.extend( 

1098 self.data.magnet.solve.thermal.insulation_TSA.exterior.thicknesses_append[add_mat_idx]) 

1099 side_ins_type.extend( 

1100 self.data.magnet.solve.thermal.insulation_TSA.exterior.materials_append[add_mat_idx]) 

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

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

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

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

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

1106 if ins_side == 'outer': # scale thin shell lines linked to the collar 

1107 if el[0] == 'w': # no wedge correction 

1108 DUMMY = 1.0 

1109 self.rm.thin_shells.insulation_types.correction_factors.append(DUMMY) 

1110 else: 

1111 bare_to_ins = (self.data.conductors[ 

1112 pg.blocks[int(el)].conductor].cable.bare_cable_height_high + 2 * 

1113 self.data.conductors[ 

1114 pg.blocks[int(el)].conductor].cable.th_insulation_along_height) / \ 

1115 self.data.conductors[ 

1116 pg.blocks[int(el)].conductor].cable.bare_cable_height_high 

1117 for key, ht in pg.blocks[int(el)].half_turns.items(): # try only first key 

1118 if str(ht.group[-1]) == str(max_layer): # ensure outer layer ! 

1119 DUMMY = float(self.data.magnet.mesh.thermal.insulation.TSA.scale_factor_radial) 

1120 if DUMMY < 0.0: # default value 

1121 DUMMY = bare_to_ins 

1122 else: # inner layer 

1123 # print(ht.lines['o']) #-> without the break this prints the linetags of the desired lines 

1124 # we are within the ins_side = "outer" loop so 

1125 DUMMY = bare_to_ins 

1126 self.rm.thin_shells.insulation_types.correction_factors.append( 

1127 DUMMY) # default value if not outer layer 

1128 break # break after first key 

1129 elif ins_side[0] in 'hl': # hl, long ht side adjacent to the poles 

1130 DUMMY = float(self.data.magnet.mesh.thermal.insulation.TSA.scale_factor_azimuthal) 

1131 if DUMMY < 0.0: # default value 

1132 DUMMY = 1.0 

1133 self.rm.thin_shells.insulation_types.correction_factors.append(DUMMY) 

1134 else: 

1135 self.rm.thin_shells.insulation_types.correction_factors.append(1.0) # default value 

1136 for nr, ins_lyr in enumerate(side_ins): 

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

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

1139 self.rm.thin_shells.insulation_types.thicknesses[-1].extend( 

1140 [ins_lyr / tsa_layers] * tsa_layers) 

1141 self.rm.thin_shells.insulation_types.layers_material[-1].extend( 

1142 [side_ins_type[nr]] * tsa_layers) 

1143 

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

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

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

1147 if ins_type in ['collar', 'poles']: 

1148 continue # these are added later 

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

1150 # Get conductors insulation 

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

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

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

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

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

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

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

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

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

1160 else: 

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

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

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

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

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

1166 else: 

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

1168 # Get insulation layer thickness 

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

1170 # for aux: 1/2 due to triangular insulation shape, 1/2 because the triangle height is half the radial distance between the points of the ht insulation 

1171 ins_thickness = 1 / 2 * ins_th_dict['mid_layer'][ins_name] / 2 if ins_type == 'aux' else \ 

1172 ins_th_dict[ins_type][ins_name] 

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

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

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

1176 # Get insulation materials 

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

1178 # Initialize sublists 

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

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

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

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

1183 self.rm.thin_shells.insulation_types.correction_factors.append(1.0) # default value 

1184 

1185 for nr, ins_lyr in enumerate(side_ins): 

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

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

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

1189 self.rm.thin_shells.insulation_types.layers_material[-1].extend( 

1190 [side_ins_type[nr]] * tsa_layers) 

1191 

1192 # Quench heater insulation layers 

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

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

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

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

1197 if tags: 

1198 if ins_type == 'external': 

1199 data = self.qh_data[qh_nr] 

1200 if str(ts_name) + data[ts_name][ 

1201 'ht_side'] in self.data.magnet.solve.thermal.insulation_TSA.exterior.blocks: 

1202 add_mat_idx = self.data.magnet.solve.thermal.insulation_TSA.exterior.blocks.index( 

1203 str(ts_name) + data[ts_name]['ht_side']) 

1204 additional_ths = \ 

1205 self.data.magnet.solve.thermal.insulation_TSA.exterior.thicknesses_append[ 

1206 add_mat_idx] 

1207 additional_mats = \ 

1208 self.data.magnet.solve.thermal.insulation_TSA.exterior.materials_append[add_mat_idx] 

1209 else: 

1210 additional_ths, additional_mats = [], [] 

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

1212 ht_ins_th = cond.th_insulation_along_width if data[ts_name][ 

1213 'ht_side'] in 'io' else cond.th_insulation_along_height 

1214 side_ins = [ht_ins_th] + [s_ins for s_ins in qh.s_ins[qh_nr - 1]] + [ 

1215 qh.h[qh_nr - 1]] + [s_ins_He for s_ins_He in 

1216 qh.s_ins_He[qh_nr - 1]] + additional_ths 

1217 side_ins_type = [cond.material_insulation] + [type_ins for type_ins in 

1218 qh.type_ins[qh_nr - 1]] + ['SS'] + \ 

1219 [type_ins_He for type_ins_He in 

1220 qh.type_ins_He[qh_nr - 1]] + additional_mats 

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

1222 qh_list = [qh_nr] 

1223 elif ins_type == 'internal': 

1224 data = self.qh_data[qh_nr] 

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

1226 cond2 = self.data.conductors[ 

1227 self.wedge_cond[int(ts_name.split('_')[0][1:])] if 'w' in ts_name 

1228 else pg.blocks[ 

1229 int(ts_name.split('_')[1]) if data[ts_name]['ht_side'] == 'o' else int( 

1230 ts_name.split('_')[0])].conductor].cable 

1231 side_ins_qh = [s_ins for s_ins in qh.s_ins[qh_nr - 1]] + [qh.h[qh_nr - 1]] + [s_ins_He 

1232 for 

1233 s_ins_He 

1234 in 

1235 qh.s_ins_He[ 

1236 qh_nr - 1]] 

1237 mid_lyr_th, null_idx = _compute_insulation_thicknesses( 

1238 ins_th_dict['mid_layer'][ts_name], sum([cond.th_insulation_along_width, 

1239 cond2.th_insulation_along_width] + side_ins_qh)) 

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

1241 side_ins = [cond.th_insulation_along_width] + side_ins_qh + mid_lyr_th + [ 

1242 cond2.th_insulation_along_width] 

1243 side_ins_type = [cond.material_insulation] + [type_ins for type_ins in 

1244 qh.type_ins[qh_nr - 1]] + ['SS'] + \ 

1245 [type_ins_He for type_ins_He in 

1246 qh.type_ins_He[qh_nr - 1]] + mid_materials + [ 

1247 cond2.material_insulation] 

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

1249 qh_list = [qh_nr] 

1250 elif ins_type == 'internal_double': 

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

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

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

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

1255 side_ins_qh = [s_ins for s_ins in qh.s_ins[qh_nr1 - 1]] + [qh.h[qh_nr1 - 1]] + [s_ins_He 

1256 for 

1257 s_ins_He 

1258 in 

1259 qh.s_ins_He[ 

1260 qh_nr1 - 1]] 

1261 side_ins_qh2 = [s_ins_He for s_ins_He in qh.s_ins_He[qh_nr2 - 1][::-1]] + [ 

1262 qh.h[qh_nr2 - 1]] + [s_ins for s_ins in qh.s_ins[qh_nr2 - 1][::-1]] 

1263 mid_lyr_th, null_idx = _compute_insulation_thicknesses( 

1264 ins_th_dict['mid_layer'][ts_name], sum([cond.th_insulation_along_width, 

1265 cond2.th_insulation_along_width] + side_ins_qh + side_ins_qh2)) 

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

1267 side_ins = [cond.th_insulation_along_width] + side_ins_qh + mid_lyr_th + side_ins_qh2 + [ 

1268 cond2.th_insulation_along_width] 

1269 side_ins_type = [cond.material_insulation] + [type_ins for type_ins in 

1270 qh.type_ins[qh_nr1 - 1]] + ['SS'] + \ 

1271 [type_ins_He for type_ins_He in 

1272 qh.type_ins_He[qh_nr1 - 1]] + mid_materials + \ 

1273 [type_ins_He for type_ins_He in qh.type_ins_He[qh_nr2 - 1][::-1]] + [ 

1274 'SS'] + \ 

1275 [type_ins for type_ins in qh.type_ins[qh_nr2 - 1][::-1]] + [ 

1276 cond2.material_insulation] 

1277 qh_list = [qh_nr1, qh_nr2] 

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

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

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

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

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

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

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

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

1286 self.rm.thin_shells.quench_heaters.correction_factors.append(1.0) # default value 

1287 for nr, ins_lyr in enumerate(side_ins): 

1288 tsa_layers = max(mesh.insulation.TSA.minimum_discretizations_QH, 

1289 round(ins_lyr / min_h_QH)) 

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

1291 self.rm.thin_shells.quench_heaters.thicknesses[-1].extend( 

1292 [ins_lyr / tsa_layers] * tsa_layers) 

1293 self.rm.thin_shells.quench_heaters.layers_material[-1].extend( 

1294 [side_ins_type[nr]] * tsa_layers) 

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

1296 

1297 if geometry.model_dump().get('use_TSA_new', False): # collar 

1298 total_ins_th_dict = self.md.geometries.thin_shells.ins_thickness.collar 

1299 for ins_name, tags in self.ins_type['collar'].items(): # self.ins_type_col['mid_lines'] 

1300 qh_th = 0.0 # todo, correct for thickness, total ins th already takes into account the insulation layer but not qh 

1301 residual_th = total_ins_th_dict[ins_name] - qh_th 

1302 side_ins = [residual_th] # only one layer of insulation, as the other layer is captured in QH 

1303 # Get insulation materials 

1304 side_ins_type = [self.data.magnet.solve.thermal.insulation_TSA.between_collar.material] 

1305 

1306 self.rm.thin_shells.collar.layers_number.append(0) 

1307 self.rm.thin_shells.collar.thin_shells.append([tags]) 

1308 self.rm.thin_shells.collar.thicknesses.append([]) 

1309 self.rm.thin_shells.collar.layers_material.append([]) 

1310 

1311 cond_name = next(iter(self.data.conductors.keys())) 

1312 ins_to_bare_ratio = (self.data.conductors[cond_name].cable.bare_cable_height_high + 2 * 

1313 self.data.conductors[cond_name].cable.th_insulation_along_height) / \ 

1314 self.data.conductors[cond_name].cable.bare_cable_height_high 

1315 DUMMY = float(self.data.magnet.mesh.thermal.insulation.TSA.scale_factor_radial) 

1316 if DUMMY < 0.0: # default value 

1317 DUMMY = ins_to_bare_ratio # ins to bare scaling 

1318 self.rm.thin_shells.insulation_types.correction_factors.append(DUMMY) 

1319 

1320 for nr, ins_lyr in enumerate(side_ins): 

1321 tsa_layers = max(mesh.insulation.TSA.minimum_discretizations_COL, round(ins_lyr / min_h_COL)) 

1322 self.rm.thin_shells.collar.layers_number[-1] += tsa_layers 

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

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

1325 

1326 # mid collar groups 

1327 

1328 self.rm.thin_shells.ts_collar_groups['1_1'] = [] 

1329 self.rm.thin_shells.ts_collar_groups['2_1'] = [] 

1330 self.rm.thin_shells.ts_collar_groups['1_2'] = [] 

1331 self.rm.thin_shells.ts_collar_groups['2_2'] = [] 

1332 

1333 max_layer = len([k for k in self.md.geometries.coil.coils[1].poles[1].layers.keys()]) 

1334 for blk_nr, blk in self.md.domains.physical_groups.blocks.items(): # only need this for the outer layer :) 

1335 for ht_nr, el in blk.half_turns.items(): 

1336 if str(el.group[-1]) == str( 

1337 max_layer): #### 2nd index is swapped due to the checkboard pattern -> otherwise we have to swap it in the .pro file 

1338 self.rm.thin_shells.ts_collar_groups[ 

1339 el.group[1] + '_' + str(1 + int(el.group[-1]) % 2)].append( 

1340 self.ins_type['collar'][f'{ht_nr}_x']) # self.ins_type_col['mid_lines'] 

1341 

1342 # wedges 

1343 for wdg_nr, el in self.md.domains.physical_groups.wedges.items(): 

1344 if str(el.group[-1]) == str(max_layer): 

1345 self.rm.thin_shells.ts_collar_groups[el.group[1] + '_' + str(1 + int(el.group[-1]) % 2)].append( 

1346 self.ins_type['collar'][f'w{wdg_nr}_x']) # self.ins_type_col['mid_lines'] 

1347 

1348 # collar 

1349 self.rm.thin_shells.bdry_curves['collar'] = [pg.inner_col] 

1350 

1351 if geometry.model_dump().get('use_TSA', False): # poles 

1352 total_ins_th_dict = self.md.geometries.thin_shells.ins_thickness.poles 

1353 for ins_name, tags in self.ins_type['poles'].items(): 

1354 other_corrections = 0.0 # assuming the other corrections are zero #debug 

1355 residual_th = total_ins_th_dict[ins_name] - other_corrections 

1356 side_ins = [residual_th] 

1357 side_ins_type = [self.data.magnet.solve.thermal.insulation_TSA.between_collar.material] 

1358 # assuming the insulation between the pole and ht are the same as for the collar 

1359 

1360 self.rm.thin_shells.poles.layers_number.append(0) 

1361 self.rm.thin_shells.poles.thin_shells.append([tags]) 

1362 self.rm.thin_shells.poles.thicknesses.append([]) 

1363 self.rm.thin_shells.poles.layers_material.append([]) 

1364 

1365 # Scaling to the pole lines (2nd insulation layer) 

1366 if ins_name.startswith('pw'): # wedge pole line 

1367 DUMMY = 1.0 # default 

1368 elif ins_name.endswith('_r'): # radial line -> halfturn 

1369 DUMMY = float(self.data.magnet.mesh.thermal.insulation.TSA.scale_factor_radial) 

1370 if DUMMY < 0.0: DUMMY = ins_to_bare_ratio # default value 

1371 else: # pole lines 

1372 DUMMY = float(self.data.magnet.mesh.thermal.insulation.TSA.scale_factor_azimuthal) 

1373 if DUMMY < 0.0: DUMMY = 1.0 # default value 

1374 self.rm.thin_shells.poles.correction_factors.append(DUMMY) 

1375 

1376 for nr, ins_lyr in enumerate(side_ins): 

1377 tsa_layers = max(mesh.insulation.TSA.minimum_discretizations, 

1378 round(ins_lyr / min_h)) # use default TSA 

1379 self.rm.thin_shells.poles.layers_number[-1] += tsa_layers 

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

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

1382 

1383 # checkboard pattern to link with the hts 

1384 self.rm.thin_shells.ts_pole_groups['a_1_1'] = [] 

1385 self.rm.thin_shells.ts_pole_groups['a_2_1'] = [] 

1386 self.rm.thin_shells.ts_pole_groups['a_1_2'] = [] 

1387 self.rm.thin_shells.ts_pole_groups['a_2_2'] = [] 

1388 self.rm.thin_shells.ts_pole_groups['r_1_2'] = [] 

1389 self.rm.thin_shells.ts_pole_groups['r_2_2'] = [] 

1390 self.rm.thin_shells.ts_pole_groups['r_1_1'] = [] # empty 

1391 self.rm.thin_shells.ts_pole_groups['r_2_1'] = [] # empty 

1392 

1393 for key, tag in self.ins_type['poles'].items(): 

1394 alignment = key[-1] # either r or a aligned lines 

1395 # find the corresponding half turn 

1396 nr = key[1:key.index('_')] 

1397 if nr.startswith('w'): 

1398 # wedge to pole line 

1399 for _, gr in self.md.domains.physical_groups.wedges.items(): 

1400 # for name, tag in gr.aux_lines.items(): 

1401 tag = gr.aux_lines.get(nr) 

1402 if tag is not None: 

1403 group = gr.group 

1404 group = group[1] + '_' + str(1 + int(group[-1]) % 2) 

1405 line_tag = self.ins_type['poles'][f'p{nr}_r'] # need to get the correct tag 

1406 self.rm.thin_shells.ts_pole_groups['a_' + group].append(line_tag) 

1407 

1408 else: 

1409 ht_nr = int(nr) 

1410 for blk_nr, blk in self.md.domains.physical_groups.blocks.items(): # only need this for the outer layer :) 

1411 el = blk.half_turns.get(ht_nr, None) 

1412 if el is not None: 

1413 break 

1414 # alignment : direction of the normal vecetor 

1415 if alignment == 'r': #### 2nd index is swapped -> radial difference (inner to outer) 

1416 group = el.group[1] + '_' + str( 

1417 1 + int(el.group[-1]) % 2) # group = el.group[1] + '_' + el.group[-1] # 

1418 self.rm.thin_shells.ts_pole_groups['a_' + group].append(tag) 

1419 elif alignment == 'a': #### 1st index is swapped -> azimuthal difference (left to right) 

1420 group = str(1 + int(el.group[1]) % 2) + '_' + el.group[ 

1421 -1] # el.group[1] + '_' + el.group[-1] # 

1422 self.rm.thin_shells.ts_pole_groups['r_' + group].append(tag) 

1423 # save boundary line for the TSA 

1424 tag = pg.poles.curves.get("bdry", None) 

1425 if tag is not None: 

1426 self.rm.thin_shells.bdry_curves['poles'] = [tag] 

1427 

1428 # Powered 

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

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

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

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

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

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

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

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

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

1438 self.rm.powered[ht.group].curve.names.append(ht_name) 

1439 self.rm.powered[ht.group].curve.numbers.append(ht.single_node) 

1440 

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

1442 # Bare edges 

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

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

1445 if line in 'io': 

1446 self.rm.thin_shells.normals_directed['radially'].append(ht.lines[line]) 

1447 else: 

1448 self.rm.thin_shells.normals_directed['azimuthally'].append(ht.lines[line]) 

1449 

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

1451 # Auxiliary thin shells 

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

1453 # update this must be _out instead of in to be not assigned to bare_layers_{i}_{j} in the .pro 

1454 # (not used in my example, since ht.aux_lines is empty) 

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

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

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

1458 # Thin shells 

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

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

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

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

1463 for line_name, line_tag in dict(ht.mid_pole_lines, **ht.mid_winding_lines, 

1464 **ht.mid_turn_lines).items(): 

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

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

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

1468 

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

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

1471 # mid-turn thin shells precede r2 

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

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

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

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

1476 # mid-pole thin shells precede r2 

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

1478 self.rm.thin_shells.second_group_is_next['azimuthally'].append( 

1479 list(ht.mid_pole_lines.values())[0]) 

1480 # conductor edges precede r2 

1481 elif ht_list.index(ht_nr) == 0 and len(ht.mid_turn_lines) + len(ht.mid_winding_lines) + len( 

1482 ht.mid_pole_lines) == 1: 

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

1484 # mid-winding thin shells precede r2 

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

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

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

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

1489 # mid-turn thin shells precede r2 

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

1491 if 'w' in line_name: 

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

1493 # conductor edges precede r2 

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

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

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

1497 # mid-layer thin shells precede a2 

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

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

1500 elif not ht.mid_layer_lines.outer: 

1501 # conductor edges precede a2 

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

1503 

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

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

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

1507 

1508 # Wedges 

1509 if geometry.with_wedges: 

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

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

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

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

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

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

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

1517 

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

1519 # Bare edges 

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

1521 if line in 'io': 

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

1523 else: 

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

1525 # Auxiliary thin shells 

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

1527 # update this must be _out instead of in to be not assigned to bare_layers_{i}_{j} in the .pro 

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

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

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

1531 # Thin shells 

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

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

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

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

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

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

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

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

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

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

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

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

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

1545 elif not wdg.mid_layer_lines.outer: 

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

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

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

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

1550 

1551 # Unique mid layers 

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

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

1554 

1555 # Insulation 

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

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

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

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

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

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

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

1563 

1564 for attr in geometry.areas: 

1565 for group_name, surface in getattr(pg, attr).surfaces.items(): 

1566 h = getattr(self.rm, attr).vol 

1567 h.names.append(group_name) 

1568 h.numbers.append(surface) 

1569 

1570 if self.data.magnet.mesh.thermal.reference.enabled: 

1571 if not pg.ref_mesh.surfaces == {}: 

1572 for name, num in pg.ref_mesh.surfaces.items(): 

1573 self.rm.ref_mesh.vol.names.append(name) 

1574 self.rm.ref_mesh.vol.numbers.append(num) 

1575 

1576 # Boundary conditions 

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

1578 # Initialize lists 

1579 for bc_data, bc_rm in zip(self.data.magnet.solve.thermal.overwrite_boundary_conditions, 

1580 self.rm.boundaries.thermal): # b.c. type 

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

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

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

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

1585 for group in ['1_r1_a1', '2_r1_a1', '1_r2_a1', '2_r2_a1', '1_r1_a2', '2_r1_a2', '1_r2_a2', 

1586 '2_r2_a2']: 

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

1588 else: 

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

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

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

1592 

1593 # Apply general cooling and adiabatic 

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

1595 cooling_side = {'i': any(coil_side in self.data.magnet.solve.thermal.He_cooling.sides for coil_side in 

1596 ['inner', 'external']), 

1597 'o': any(coil_side in self.data.magnet.solve.thermal.He_cooling.sides for coil_side in 

1598 ['outer', 'external']), 

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

1600 else: 

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

1602 

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

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

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

1606 bnd_list_numbers[bc_type].append(line_tag) 

1607 if side in 'io': 

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

1609 else: 

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

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

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

1613 if line_tag in group: 

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

1615 break 

1616 

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

1618 'collar': self.rm.boundaries.thermal.collar} 

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

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

1621 DISABLE_BNDRY_COND = False # debug: this should always be false 

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

1623 # Half turn boundaries 

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

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

1626 for order in orders: 

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

1628 for ht_nr, ht in pg.blocks[ 

1629 order.block].half_turns.items(): # apply bdry condition if no TSL 

1630 if not ht.mid_layer_lines.inner: 

1631 __assign_bnd_tag(ht, 'ht' + str(ht_nr), 'i', 

1632 'Robin' if cooling_side['i'] else 'Neumann') 

1633 if not ht.mid_layer_lines.outer: 

1634 if DISABLE_BNDRY_COND and self.data.magnet.geometry.thermal.use_TSA_new and \ 

1635 ht.group[4] == max_layer: # do not add new boundary condition: 

1636 logger.warning("\033[93m DISABLING NEUMANN CONDITION") 

1637 else: 

1638 __assign_bnd_tag(ht, 'ht' + str(ht_nr), 'o', 

1639 'Robin' if cooling_side['o'] else 'Neumann') 

1640 if ht_list.index(ht_nr) == 0 and len(ht.mid_turn_lines) + len( 

1641 ht.mid_winding_lines) + len(ht.mid_pole_lines) == 1: 

1642 __assign_bnd_tag(ht, 'ht' + str(ht_nr), 'l', 

1643 'Robin' if cooling_side['hl'] else 'Neumann') 

1644 if ht_list.index(ht_nr) == len(ht_list) - 1 and len(ht.mid_turn_lines) + len( 

1645 ht.mid_winding_lines) + len(ht.mid_pole_lines) == 1: 

1646 __assign_bnd_tag(ht, 'ht' + str(ht_nr), 'h', 

1647 'Robin' if cooling_side['hl'] else 'Neumann') 

1648 if ht.aux_lines: 

1649 __assign_bnd_tag(ht, 'ht' + str(ht_nr), 'o', 

1650 'Robin' if cooling_side['hl'] else 'Neumann', 

1651 list(ht.aux_lines.values())[0]) 

1652 

1653 # Wedge boundaries 

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

1655 if not wdg.mid_layer_lines.inner: 

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

1657 if not wdg.mid_layer_lines.outer: 

1658 if DISABLE_BNDRY_COND and self.data.magnet.geometry.thermal.use_TSA_new and wdg.group[ 

1659 4] == max_layer: 

1660 logger.warning("\033[93m DISABLING NEUMANN CONDITION") 

1661 else: 

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

1663 if wdg.aux_lines: 

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

1665 list(wdg.aux_lines.values())[0]) 

1666 

1667 else: # insulation case, no TSA 

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

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

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

1671 bc_type = 'Robin' 

1672 else: 

1673 raise ValueError( 

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

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

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

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

1678 else: 

1679 bc_type = 'Neumann' 

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

1681 bnd_list_numbers[bc_type].append(tag) 

1682 

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

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

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

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

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

1688 

1689 if self.data.magnet.solve.thermal.collar_cooling.enabled: 

1690 cool = self.data.magnet.solve.thermal.collar_cooling 

1691 bc_rm['collar'].bc.numbers = [pg.collar_cooling] # only one physical group for the collar cooling 

1692 bc_rm['collar'].bc.values = [self.data.magnet.solve.thermal.collar_cooling.heat_transfer_coefficient, 

1693 cool.ref_temperature if cool.ref_temperature is not None else self.data.magnet.solve.thermal.init_temperature] # [coef or functionname, inittemp] 

1694 

1695 # save the boundary names and numbers 

1696 if bnd_list_names['Neumann']: 

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

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

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

1700 

1701 # Apply specific boundary conditions 

1702 for bc_data, bc_rm in zip(self.data.magnet.solve.thermal.overwrite_boundary_conditions, 

1703 self.rm.boundaries.thermal): # b.c. type 

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

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

1706 

1707 for _, bc in bc_data[ 

1708 1].items(): # all boundary conditions of one b.c. type (e.g., Dirichlet with different temperatures) 

1709 bnd_list_names = [] 

1710 bnd_list_numbers = [] 

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

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

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

1714 if not geometry.with_wedges: 

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

1716 # Fetch the physical group of the wedge 

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

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

1719 else: 

1720 # Fetch the physical group of the half turn 

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

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

1723 name = 'ht' + bnd 

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

1725 bnd_list_names.append(name) 

1726 bnd_list_numbers.append(line_pg_tag) 

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

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

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

1730 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 

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

1732 # Overwrite general cooling and adiabatic condition 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1747 # Assign the tag 

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

1749 # Extra grouping for Robin virtual shells 

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

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

1752 if line_pg_tag in group: 

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

1754 break 

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

1756 pass # todo: not supported yet 

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

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

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

1760 bc_rm[1].bc.values.append( 

1761 [bc.heat_transfer_coefficient, self.data.magnet.solve.thermal.init_temperature]) 

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

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

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

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

1766 

1767 def setMeshOptions(self): 

1768 """ 

1769 Meshes the generated domain 

1770 """ 

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

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

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

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

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

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

1777 

1778 def generateMesh(self): 

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

1780 self.mesh.generate(2) 

1781 # self.mesh.removeDuplicateNodes() 

1782 # self.occ.synchronize() 

1783 # self.gu.launch_interactive_GUI() 

1784 

1785 def checkMeshQuality(self): 

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

1787 

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

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

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

1791 self.mesh_parameters['Gamma'] = min(self.mesh.getElementQualities(elementTags=tags, qualityName='gamma')) 

1792 self.mesh_parameters['nodes'] = len(self.mesh.getNodes()[0]) 

1793 

1794 def saveClosestNeighboursList(self): 

1795 

1796 def _closest_node_on_reference(origin_points, reference_points): 

1797 """ 

1798 Compute list of lists with each list representing one original node (3 entries for 3 coordinates) and the closest 

1799 reference mesh node (3 entries for 3 coordinates). 

1800 :param origin_points: list of origin point as list of 3 floats per origin node 

1801 :param reference_points: list of coordinates of reference mesh points as list of 3 floats per reference mesh node 

1802 :return: returns list of lists with the closest reference mesh node per origin node 

1803 """ 

1804 closest_node = [] 

1805 for x in range(0, len(origin_points), 3): 

1806 origin_point = origin_points[x:x + 3] 

1807 # Compute distance list between origin point and reference point list 

1808 dist_lst = [Func.points_distance(origin_point, reference_points[y:y + 3]) for y in 

1809 range(0, len(reference_points), 3)] 

1810 min_idx = 3 * np.argmin(dist_lst) 

1811 closest_node.append(origin_point + reference_points[min_idx:min_idx + 3]) 

1812 return closest_node 

1813 

1814 def _get_closest_nodes(side): 

1815 origin_list = self.mesh.getNodesForPhysicalGroup(1, mid_layer)[1].tolist() 

1816 reference_list = self.mesh.getNodesForPhysicalGroup(1, el.lines[side])[1].tolist() 

1817 self.rc.neighbouring_nodes.groups[group].extend( 

1818 [node for node_list in _closest_node_on_reference(origin_list, reference_list) for node in node_list]) 

1819 

1820 logger.info( 

1821 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 . . .") 

1822 logger.info(f"Info : Finding closest reference nodes ...") 

1823 

1824 self.rc.neighbouring_nodes.groups['1_1'] = [] 

1825 self.rc.neighbouring_nodes.groups['2_1'] = [] 

1826 self.rc.neighbouring_nodes.groups['1_2'] = [] 

1827 self.rc.neighbouring_nodes.groups['2_2'] = [] 

1828 

1829 for blk_nr, blk in self.md.domains.physical_groups.blocks.items(): 

1830 for ht_nr, el in blk.half_turns.items(): 

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

1832 group = el.group[1] + '_' + el.group[-1] 

1833 for line_name, mid_layer in el.mid_layer_lines.inner.items(): _get_closest_nodes('i') 

1834 for line_name, mid_layer in el.mid_layer_lines.outer.items(): _get_closest_nodes('o') 

1835 for line_name, mid_layer in el.aux_lines.items(): _get_closest_nodes('o') 

1836 for line_name, mid_layer in el.mid_pole_lines.items(): _get_closest_nodes( 

1837 'l' if ht_list.index(ht_nr) == 0 else 'h') 

1838 for line_name, mid_layer in el.mid_winding_lines.items(): _get_closest_nodes( 

1839 'l' if ht_list.index(ht_nr) == 0 else 'h') 

1840 for line_name, mid_layer in el.mid_turn_lines.items(): 

1841 high = ht_list.index(ht_nr) == len(ht_list) - 1 if 'w' in line_name \ 

1842 else int(line_name[:line_name.index('_')]) == ht_nr 

1843 _get_closest_nodes('h' if high else 'l') 

1844 for wdg_nr, el in self.md.domains.physical_groups.wedges.items(): 

1845 group = el.group[1] + '_' + el.group[-1] 

1846 for line_name, mid_layer in el.mid_layer_lines.inner.items(): _get_closest_nodes('i') 

1847 for line_name, mid_layer in el.mid_layer_lines.outer.items(): _get_closest_nodes('o') 

1848 for line_name, mid_layer in el.aux_lines.items(): _get_closest_nodes('o') 

1849 for line_name, mid_layer in el.mid_turn_lines.items(): 

1850 _get_closest_nodes('l' if line_name == list(el.mid_turn_lines.keys())[0] else 'h') 

1851 

1852 logger.info( 

1853 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") 

1854 

1855 def saveClosestNeighboursList_new_TSA(self): 

1856 def _closest_node_on_reference(origin_points, reference_points): 

1857 closest_node = [] 

1858 for x in range(0, len(origin_points), 3): 

1859 origin_point = origin_points[x:x + 3] 

1860 # Compute distance list between origin point and reference point list 

1861 dist_lst = [Func.points_distance(origin_point, reference_points[y:y + 3]) for y in 

1862 range(0, len(reference_points), 3)] 

1863 min_idx = 3 * np.argmin(dist_lst) 

1864 closest_node.append(origin_point + reference_points[min_idx:min_idx + 3]) 

1865 if not closest_node: 

1866 raise ValueError("No closest nodes found - check mesh and physical groups!") 

1867 return closest_node 

1868 

1869 def _get_closest_nodes(name, reference_list, origin_list): 

1870 self.rc.neighbouring_nodes.groups[name].extend( 

1871 [node for node_list in _closest_node_on_reference(origin_list, reference_list) for node in node_list]) 

1872 

1873 logger.info( 

1874 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 (new TSA) . . .") 

1875 logger.info(f"Info : Finding closest reference nodes ...") 

1876 

1877 # two of these will always be empty, but depending on the layers it will either be a1 or a2 

1878 self.rc.neighbouring_nodes.groups['mid2ht_1_1'] = [] 

1879 self.rc.neighbouring_nodes.groups['mid2ht_2_1'] = [] 

1880 self.rc.neighbouring_nodes.groups['mid2ht_1_2'] = [] 

1881 self.rc.neighbouring_nodes.groups['mid2ht_2_2'] = [] 

1882 # map coils to origin_list: start with outer layer of HT 

1883 max_layer = len([k for k in self.md.geometries.coil.coils[1].poles[1].layers.keys()]) 

1884 

1885 origin_all = [self.mesh.getNodesForPhysicalGroup(1, line)[1].tolist() for _, line in 

1886 self.ins_type['collar'].items()] 

1887 origin_all = [node for sublist in origin_all for node in sublist] # flatten the list 

1888 

1889 for blk_nr, blk in self.md.domains.physical_groups.blocks.items(): 

1890 for ht_nr, el in blk.half_turns.items(): 

1891 if str(el.group[-1]) == str(max_layer): 

1892 group = el.group[1] + '_' + str((1 + int(el.group[-1])) % 2) # e.g. 1_2 

1893 origin = self.mesh.getNodesForPhysicalGroup(1, self.ins_type['collar'][str(ht_nr) + '_x'])[ 

1894 1].tolist() 

1895 ref_list = np.array(self.mesh.getNodesForPhysicalGroup(1, el.lines['o'])[1]).tolist() 

1896 _get_closest_nodes(reference_list=ref_list, name=f'mid2ht_{group}', origin_list=origin) 

1897 # collar mid 

1898 self.rc.neighbouring_nodes.groups['mid2col'] = [] 

1899 ref = np.array(self.mesh.getNodesForPhysicalGroup(1, self.md.domains.physical_groups.inner_col)[1]).tolist() 

1900 _get_closest_nodes(reference_list=ref, origin_list=origin_all, name='mid2col') 

1901 # collar wedges 

1902 for wdg_nr, el in self.md.domains.physical_groups.wedges.items(): ## one can conveniently add the wedges to the mid2ht groups 

1903 if str(el.group[-1]) == str(max_layer): 

1904 group = el.group[1] + '_' + str((1 + int(el.group[-1])) % 2) 

1905 origin = self.mesh.getNodesForPhysicalGroup(1, self.ins_type['collar']['w' + str(wdg_nr) + '_x'])[ 

1906 1].tolist() 

1907 ref_list = np.array(self.mesh.getNodesForPhysicalGroup(1, el.lines['o'])[1]).tolist() 

1908 _get_closest_nodes(reference_list=ref_list, name=f'mid2ht_{group}', origin_list=origin) 

1909 

1910 # POLES 

1911 self.rc.neighbouring_nodes.groups['pole_mid2ht_1_2'] = [] 

1912 self.rc.neighbouring_nodes.groups['pole_mid2ht_1_1'] = [] 

1913 self.rc.neighbouring_nodes.groups['pole_mid2ht_2_1'] = [] 

1914 self.rc.neighbouring_nodes.groups['pole_mid2ht_2_2'] = [] 

1915 self.rc.neighbouring_nodes.groups['mid2pol'] = [] 

1916 # TSA lines to poles 

1917 for tsl_name in self.ins_type['poles'].keys(): 

1918 # first half: mid2ht (also mid2wedge) 

1919 nr = tsl_name[1:tsl_name.index('_')] 

1920 origin = self.mesh.getNodesForPhysicalGroup(1, self.ins_type['poles'][tsl_name])[1].tolist() 

1921 if nr.startswith('w'): # this is a mid2wedge 

1922 ref_list = None 

1923 for i, gr in self.md.domains.physical_groups.wedges.items(): 

1924 # for name, tag in gr.aux_lines.items(): 

1925 tag = gr.aux_lines.get(nr) 

1926 if tag is not None: # should only be one 

1927 ref_list = self.mesh.getNodesForPhysicalGroup(1, tag)[1] 

1928 ref_list = [float(x) for x in ref_list] 

1929 break 

1930 # alignment is always the same here, for naming later 

1931 group = gr.group 

1932 group = group[1] + '_' + str(1 + int(group[-1]) % 2) 

1933 else: 

1934 ht_nr = int(nr) # e.g. p1_a and p12_a -> 1 and 12 

1935 alignment = tsl_name[-1] 

1936 # reference can be the nodes from the half turn, we don't have to specify the line 

1937 for blk in self.md.domains.physical_groups.blocks.values(): 

1938 el = blk.half_turns.get(ht_nr, None) 

1939 if el is not None: 

1940 ref_list = [] 

1941 [ref_list.extend(self.mesh.getNodesForPhysicalGroup(1, line)[1]) for line in el.lines.values()] 

1942 ref_list = [float(x) for x in ref_list] 

1943 break 

1944 # alignment : direction of the normal vector, for naming 

1945 if alignment == 'r': 

1946 # group = el.group[1] + '_' + el.group[-1] 

1947 group = el.group[1] + '_' + str(1 + int(el.group[-1]) % 2) 

1948 elif alignment == 'a': 

1949 # group = el.group[1] + '_' + el.group[-1] 

1950 group = str(1 + int(el.group[1]) % 2) + '_' + el.group[-1] 

1951 _get_closest_nodes(reference_list=ref_list, name=f'pole_mid2ht_{group}', origin_list=origin) 

1952 

1953 # second half: mid2pole 

1954 origin = [float(x) for x in 

1955 self.mesh.getNodesForPhysicalGroup(1, self.ins_type['poles'][tsl_name])[1].tolist()] 

1956 pole_bdry = self.md.domains.physical_groups.poles.curves.get("bdry", None) 

1957 ###ref_list = [float(x) for x in self.mesh.getNodesForPhysicalGroup(2, self.md.domains.physical_groups.poles.surfaces['SS'])[1].tolist()] 

1958 ref_list = [float(x) for x in self.mesh.getNodesForPhysicalGroup(1, pole_bdry)[1].tolist()] 

1959 # just use all the nodes 

1960 # todo: this can be optimized by selecting only the boundary of the pole 

1961 _get_closest_nodes(reference_list=ref_list, name=f'mid2pol', origin_list=origin) 

1962 

1963 logger.info( 

1964 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 (new TSA)") 

1965 

1966 def saveHalfTurnCornerPositions(self): 

1967 with open(f"{self.geom_files}.crns", 'r') as f: self.rc.coordinates_per_half_turn = json.load(f) 

1968 

1969 def selectMeshNodes(self, elements: str): 

1970 

1971 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 . . .") 

1972 logger.info(f"Info : Selecting a mesh node per isothermal {elements[:-1]} ...") 

1973 

1974 if elements == 'conductors': 

1975 bare_mesh = {'1_1': self.rm.powered['r1_a1'].vol.numbers, '2_1': self.rm.powered['r2_a1'].vol.numbers, 

1976 '1_2': self.rm.powered['r1_a2'].vol.numbers, '2_2': self.rm.powered['r2_a2'].vol.numbers} 

1977 groups = self.rc.isothermal_nodes.conductors 

1978 

1979 # dir + robin + mid_layers 

1980 # potentially all thin shell lines if easier 

1981 line_tags = self.rm.boundaries.thermal.temperature.groups['r1_a1'] + \ 

1982 self.rm.boundaries.thermal.temperature.groups['r1_a2'] + \ 

1983 self.rm.boundaries.thermal.temperature.groups['r2_a1'] + \ 

1984 self.rm.boundaries.thermal.temperature.groups['r2_a2'] + \ 

1985 self.rm.boundaries.thermal.cooling.groups['r1_a1'] + \ 

1986 self.rm.boundaries.thermal.cooling.groups['r1_a2'] + \ 

1987 self.rm.boundaries.thermal.cooling.groups['r2_a1'] + \ 

1988 self.rm.boundaries.thermal.cooling.groups['r2_a2'] + \ 

1989 self.rm.thin_shells.mid_turns_layers_poles 

1990 for tag in line_tags: 

1991 coords = list(self.mesh.getNodesForPhysicalGroup(dim=1, tag=tag)[1])[:3] 

1992 self.rc.isothermal_nodes.thin_shells[tag] = [float(coords[0]), float(coords[1]), float(coords[2])] 

1993 

1994 elif elements == 'wedges': 

1995 bare_mesh = {'1_1': self.rm.induced['r1_a1'].vol.numbers, '2_1': self.rm.induced['r2_a1'].vol.numbers, 

1996 '1_2': self.rm.induced['r1_a2'].vol.numbers, '2_2': self.rm.induced['r2_a2'].vol.numbers} 

1997 groups = self.rc.isothermal_nodes.wedges 

1998 else: 

1999 bare_mesh = {} 

2000 groups = {} 

2001 

2002 for group, tags_list in bare_mesh.items(): 

2003 groups[group] = {} 

2004 for tag in tags_list: 

2005 coords = list(self.mesh.getNodesForPhysicalGroup(dim=2, tag=tag)[1])[:3] 

2006 groups[group][tag] = [float(coords[0]), float(coords[1]), float(coords[2])] 

2007 

2008 for tag in self.rm.boundaries.thermal.cooling.groups['r' + group[0] + '_' + 'a' + group[-1]]: 

2009 coords = list(self.mesh.getNodesForPhysicalGroup(dim=1, tag=tag)[1])[:3] 

2010 groups[group][tag] = [float(coords[0]), float(coords[1]), float(coords[2])] 

2011 

2012 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") 

2013 # import time 

2014 # time.sleep(100) 

2015 # TODO: add to pro file automatically 

2016 

2017 # self.occ.synchronize() 

2018 # self.gu.launch_interactive_GUI() 

2019