fiqus 2025.12.0__py3-none-any.whl → 2026.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. fiqus/MainFiQuS.py +4 -8
  2. fiqus/data/DataConductor.py +108 -11
  3. fiqus/data/DataFiQuS.py +2 -1
  4. fiqus/data/DataFiQuSConductorAC_CC.py +345 -0
  5. fiqus/data/DataFiQuSConductorAC_Strand.py +3 -3
  6. fiqus/data/DataFiQuSMultipole.py +363 -165
  7. fiqus/data/DataModelCommon.py +30 -15
  8. fiqus/data/DataMultipole.py +33 -10
  9. fiqus/data/DataWindingsCCT.py +37 -37
  10. fiqus/data/RegionsModelFiQuS.py +1 -1
  11. fiqus/geom_generators/GeometryConductorAC_CC.py +1906 -0
  12. fiqus/geom_generators/GeometryMultipole.py +751 -54
  13. fiqus/getdp_runners/RunGetdpConductorAC_CC.py +123 -0
  14. fiqus/getdp_runners/RunGetdpMultipole.py +181 -31
  15. fiqus/mains/MainConductorAC_CC.py +148 -0
  16. fiqus/mains/MainMultipole.py +109 -17
  17. fiqus/mesh_generators/MeshCCT.py +209 -209
  18. fiqus/mesh_generators/MeshConductorAC_CC.py +1305 -0
  19. fiqus/mesh_generators/MeshMultipole.py +938 -263
  20. fiqus/parsers/ParserCOND.py +2 -1
  21. fiqus/parsers/ParserDAT.py +16 -16
  22. fiqus/parsers/ParserGetDPOnSection.py +212 -212
  23. fiqus/parsers/ParserGetDPTimeTable.py +134 -134
  24. fiqus/parsers/ParserMSH.py +53 -53
  25. fiqus/parsers/ParserRES.py +142 -142
  26. fiqus/plotters/PlotPythonCCT.py +133 -133
  27. fiqus/plotters/PlotPythonMultipole.py +18 -18
  28. fiqus/post_processors/PostProcessAC_CC.py +65 -0
  29. fiqus/post_processors/PostProcessMultipole.py +16 -6
  30. fiqus/pre_processors/PreProcessCCT.py +175 -175
  31. fiqus/pro_assemblers/ProAssembler.py +3 -3
  32. fiqus/pro_material_functions/ironBHcurves.pro +246 -246
  33. fiqus/pro_templates/combined/CAC_CC_template.pro +542 -0
  34. fiqus/pro_templates/combined/CC_Module.pro +1213 -0
  35. fiqus/pro_templates/combined/Multipole_template.pro +2738 -1338
  36. fiqus/pro_templates/combined/TSA_materials.pro +102 -2
  37. fiqus/pro_templates/combined/materials.pro +54 -3
  38. fiqus/utils/Utils.py +18 -25
  39. fiqus/utils/update_data_settings.py +1 -1
  40. {fiqus-2025.12.0.dist-info → fiqus-2026.1.1.dist-info}/METADATA +81 -77
  41. {fiqus-2025.12.0.dist-info → fiqus-2026.1.1.dist-info}/RECORD +52 -44
  42. {fiqus-2025.12.0.dist-info → fiqus-2026.1.1.dist-info}/WHEEL +1 -1
  43. tests/test_geometry_generators.py +47 -30
  44. tests/test_mesh_generators.py +69 -30
  45. tests/test_solvers.py +67 -29
  46. tests/utils/fiqus_test_classes.py +396 -147
  47. tests/utils/generate_reference_files_ConductorAC.py +57 -57
  48. tests/utils/helpers.py +76 -1
  49. /fiqus/pro_templates/combined/{ConductorACRutherford_template.pro → CAC_Rutherford_template.pro} +0 -0
  50. /fiqus/pro_templates/combined/{ConductorAC_template.pro → CAC_Strand_template.pro} +0 -0
  51. {fiqus-2025.12.0.dist-info → fiqus-2026.1.1.dist-info/licenses}/LICENSE.txt +0 -0
  52. {fiqus-2025.12.0.dist-info → fiqus-2026.1.1.dist-info}/top_level.txt +0 -0
@@ -1,3 +1,5 @@
1
+ import copy
2
+ import logging
1
3
  import os
2
4
  import gmsh
3
5
  import numpy as np
@@ -14,6 +16,7 @@ from fiqus.data.DataRoxieParser import Corner
14
16
  from fiqus.data.DataRoxieParser import Coord
15
17
  import re
16
18
 
19
+ logger = logging.getLogger('FiQuS')
17
20
  class Geometry:
18
21
  def __init__(self, data: dF.FDM() = None, geom: FiQuSGeometry() = None,
19
22
  geom_folder: str = None, verbose: bool = False):
@@ -25,6 +28,11 @@ class Geometry:
25
28
  """
26
29
  self.data: dF.FDM() = data
27
30
  self.geom: FiQuSGeometry() = geom.Roxie_Data
31
+
32
+ # move cooling holes to a desired position
33
+ if self.data.magnet.solve.thermal.collar_cooling.move_cooling_holes:
34
+ self.geom.iron.key_points = self.move_keypoints(self.geom.iron.key_points, self.data.magnet.solve.thermal.collar_cooling.move_cooling_holes)
35
+
28
36
  self.geom_folder = geom_folder
29
37
  self.verbose: bool = verbose
30
38
 
@@ -40,6 +48,9 @@ class Geometry:
40
48
  self.ins_wire_lines = {} # for meshed insulation
41
49
  self.block_coil_mid_pole_blks = {}
42
50
 
51
+ self.nc = {'collar': 'c', 'iron_yoke': 'i', 'poles': 'p'}
52
+ self.inv_nc = {v: k for k, v in self.nc.items()} #invert naming convention
53
+
43
54
  if self.data.magnet.geometry.electromagnetics.symmetry != 'none':
44
55
  self.symmetric_loop_lines = {'x': [], 'y': []}
45
56
  self.symmetric_bnds = {'x_p': {'pnts': [], 'line_pnts': []}, 'y_p': {'pnts': [], 'line_pnts': []},
@@ -54,8 +65,9 @@ class Geometry:
54
65
  if gui:
55
66
  self.gu.launch_interactive_GUI()
56
67
  else:
57
- gmsh.clear()
58
- gmsh.finalize()
68
+ if gmsh.isInitialized():
69
+ gmsh.clear()
70
+ gmsh.finalize()
59
71
 
60
72
  def saveHalfTurnCornerPositions(self):
61
73
  self.occ.synchronize()
@@ -74,8 +86,9 @@ class Geometry:
74
86
  iL.append([ht.iL.x, ht.iL.y])
75
87
  oH.append([ht.oH.x, ht.oH.y])
76
88
  oL.append([ht.oL.x, ht.oL.y])
77
- json.dump({'iH': iH, 'iL': iL, 'oH': oH, 'oL': oL,
78
- 'iHr': iHr, 'iLr': iLr, 'oHr': oHr, 'oLr': oLr}, open(f"{self.model_file}.crns", 'w'))
89
+ with open(f"{self.model_file}.crns", 'w') as f:
90
+ json.dump({'iH': iH, 'iL': iL, 'oH': oH, 'oL': oL,
91
+ 'iHr': iHr, 'iLr': iLr, 'oHr': oHr, 'oLr': oLr}, f)
79
92
 
80
93
  def saveStrandPositions(self, run_type):
81
94
  symmetry = self.data.magnet.geometry.electromagnetics.symmetry if run_type == 'EM' else 'none'
@@ -108,9 +121,10 @@ class Geometry:
108
121
  subdf = df[(condition[qdr][0] * df['parser_x'] < 0) & (condition[qdr][1] * df['parser_y'] < 0)]
109
122
  for strand, x, y in zip(subdf.index, subdf['parser_x'], subdf['parser_y']):
110
123
  mirrored[strand] = df[(df['parser_x'] == mrr[0] * x) & (df['parser_y'] == mrr[1] * y)].index.item()
111
- json.dump({'x': parser_x, 'y': parser_y, 'block': blocks, 'ht': ht, 'mirrored': mirrored,
112
- 'pole_1_blocks': pole_blocks, 'poles': len(self.geom.coil.coils[1].poles)},
113
- open(f"{self.model_file}_{run_type}.strs", 'w'))
124
+ with open(f"{self.model_file}_{run_type}.strs", 'w') as f:
125
+ json.dump({'x': parser_x, 'y': parser_y, 'block': blocks, 'ht': ht, 'mirrored': mirrored,
126
+ 'pole_1_blocks': pole_blocks, 'poles': len(self.geom.coil.coils[1].poles)},
127
+ f)
114
128
 
115
129
  def saveBoundaryRepresentationFile(self, run_type):
116
130
  self.occ.synchronize()
@@ -206,7 +220,7 @@ class Geometry:
206
220
  elif 'current' in angles_to_correct:
207
221
  if angle_next < np.pi / 2: angle_next += correction_angle
208
222
  elif angle_next > np.pi * 3 / 2: angle_next = angle_next + correction_angle - 2 * np.pi
209
- if abs(angle_curr - angle_next) < 1e-6: # todo: check if needed
223
+ if abs(angle_curr - angle_next) < 1e-6:
210
224
  thin_shell_endpoints[side], angles[side], which_block[side] = pnt_curr, angle_curr, 'current'
211
225
  elif angle_curr * (-1 if side == 'lower' else 1) < angle_next * (-1 if side == 'lower' else 1):
212
226
  thin_shell_endpoints[side], angles[side], which_block[side] = pnt_curr, angle_curr, 'current'
@@ -215,11 +229,15 @@ class Geometry:
215
229
  if angles['higher'] < angles['lower']: return None
216
230
  else: return thin_shell_endpoints, which_block
217
231
 
218
- def constructIronGeometry(self, symmetry):
232
+ def create_geom_dict(self, geometry_setting):
233
+ return {v: k in geometry_setting.areas for k, v in self.nc.items()}
234
+
235
+ def constructIronGeometry(self, symmetry, geometry_setting, run_type):
219
236
  """
220
237
  Generates points, hyper lines, and curve loops for the iron yoke
221
238
  """
222
- iron = self.geom.iron
239
+ iron = self.geom.iron #roxie
240
+
223
241
  if symmetry == 'xy':
224
242
  self.md.geometries.iron.quadrants = {1: dM.Region()}
225
243
  list_bnds = ['x_p', 'y_p']
@@ -230,12 +248,16 @@ class Geometry:
230
248
  self.md.geometries.iron.quadrants = {1: dM.Region(), 4: dM.Region()}
231
249
  list_bnds = ['y_p', 'y_n']
232
250
  else:
233
- self.md.geometries.iron.quadrants = {1: dM.Region(), 2: dM.Region(), 4: dM.Region(), 3: dM.Region()}
251
+ for k in self.nc.keys(): getattr(self.md.geometries, k).quadrants = {1: dM.Region(), 2: dM.Region(), 4: dM.Region(), 3: dM.Region()}
234
252
  list_bnds = []
235
- quadrants = self.md.geometries.iron.quadrants
236
253
 
237
254
  lc = 1e-2
255
+ geom_dict = self.create_geom_dict(geometry_setting)
256
+
238
257
  for point_name, point in iron.key_points.items():
258
+ identifier = next((k for k in self.inv_nc.keys() if re.match(f'^{k}', point_name[2:])), None)
259
+ if not geom_dict.get(identifier, False): continue
260
+ quadrants = getattr(self.md.geometries, self.inv_nc[identifier]).quadrants #re.sub(r'\d+', '', point_name[2:])
239
261
  if symmetry in ['x', 'xy']:
240
262
  if point.y == 0.:
241
263
  self.symmetric_bnds['x_p']['pnts'].append([point_name, point.x])
@@ -273,6 +295,9 @@ class Geometry:
273
295
  symmetric_bnds_order = {'x': [], 'y': []}
274
296
  sym_lines_tags = {'x_p': [], 'y_p': [], 'x_n': [], 'y_n': []}
275
297
  for line_name, line in iron.hyper_lines.items():
298
+ identifier = next((k for k in self.inv_nc.keys() if re.match(f'^{k}', line_name[2:])), None)
299
+ if not geom_dict.get(identifier, False): continue
300
+ quadrants = getattr(self.md.geometries, self.inv_nc[identifier]).quadrants #re.sub(r'\d+', '', line_name[2:])
276
301
  pt1 = iron.key_points[line.kp1]
277
302
  pt2 = iron.key_points[line.kp2]
278
303
  if line.type == 'line':
@@ -359,7 +384,7 @@ class Geometry:
359
384
 
360
385
  for quadrant, qq in quadrants.items():
361
386
  qq.lines[line_name] = self.occ.addCircleArc(
362
- qq.points[line.kp1], qq.points[new_point_name], qq.points[line.kp2])
387
+ qq.points[line.kp1], qq.points[line.kp3], qq.points[line.kp2], center=False)
363
388
 
364
389
  elif line.type == 'circle':
365
390
  center = [(pt1.x + pt2.x) / 2, (pt1.y + pt2.y) / 2]
@@ -445,6 +470,7 @@ class Geometry:
445
470
  raise ValueError('Hyper line {} not supported'.format(line.type))
446
471
 
447
472
  if symmetry != 'none':
473
+ quadrants = self.md.geometries.iron_yoke.quadrants
448
474
  indexes = {'x_p': 1, 'y_p': 1, 'x_n': 1, 'y_n': 1}
449
475
  self.md.geometries.air_inf.points['center'] = self.occ.addPoint(0, 0, 0)
450
476
  for sym in list_bnds:
@@ -479,12 +505,74 @@ class Geometry:
479
505
  sym_lines_tags['y_p'].reverse()
480
506
  self.symmetric_loop_lines['y'] = sym_lines_tags['y_p'] + sym_lines_tags['y_n']
481
507
 
482
- for quadrant, qq in quadrants.items():
483
- for area_name, area in iron.hyper_areas.items():
484
- qq.areas[area_name] = dM.Area(loop=self.occ.addCurveLoop([qq.lines[line] for line in area.lines]))
485
- if (iron.hyper_areas[area_name].material not in self.md.domains.groups_entities.iron and
486
- iron.hyper_areas[area_name].material != 'BH_air'):
487
- self.md.domains.groups_entities.iron[iron.hyper_areas[area_name].material] = []
508
+ # add all areas of each quadrant. Useful for brep curves and meshing
509
+ for key in geometry_setting.areas: # only consider areas that are implemented
510
+ quadrants = getattr(self.md.geometries, key).quadrants
511
+ for quadrant, qq in quadrants.items():
512
+ for area_name, area in iron.hyper_areas.items(): ## all areas
513
+ def _add_loop():
514
+ # prevent additional curveloop generation when Enforcing the TSA mapping on the collar
515
+ if (run_type == 'TH'
516
+ and self.data.magnet.mesh.thermal.collar.Enforce_TSA_mapping
517
+ and (area_name.startswith('arc') and not area_name.startswith('arch') or area_name.startswith('arp'))
518
+ ): # need to disable the pole area too as it is linked to the same curve
519
+ qq.areas[area_name] = dM.Area() ## initialise area without loop
520
+ else:
521
+ qq.areas[area_name] = dM.Area(
522
+ loop=self.occ.addCurveLoop([qq.lines[line] for line in area.lines]))
523
+
524
+ if iron.hyper_areas[area_name].material not in getattr(self.md.domains.groups_entities, key) and \
525
+ iron.hyper_areas[area_name].material != 'BH_air': ## add the material to the keys
526
+ # for the collar region, it is possible to overwrite the material -> intercept it here
527
+ if key == 'collar' and (self.data.magnet.solve.collar.material != iron.hyper_areas[area_name].material) and self.data.magnet.solve.collar.material is not None:
528
+ logger.warning("Overwriting the collar material for area {} to {} ".format(area_name, self.data.magnet.solve.collar.material))
529
+ iron.hyper_areas[area_name].material = self.data.magnet.solve.collar.material
530
+ getattr(self.md.domains.groups_entities, key)[iron.hyper_areas[area_name].material] = []
531
+
532
+ identifier = next((k for k in geom_dict.keys() if re.match(f'^{k}', area_name[2:])),
533
+ None) # match key from geom_dict to the area name (see naming convention)
534
+
535
+ if key == self.inv_nc.get(identifier, None): # re.sub(r'\d+', '', area_name[2:]),
536
+ _add_loop() # adds arch to collar, because c is in the naming convention of the collar
537
+ elif area_name.startswith('arh') and key == 'iron_yoke': # if not previous but it is a hole, assume iron
538
+ _add_loop()
539
+
540
+ # define inner collar lines
541
+ def define_inner_collar():
542
+ """
543
+ Defines the inner collar line used for the thermal TSA + for the A projection
544
+ """
545
+ self.occ.synchronize()
546
+ # only works if the inner collar line is an arc -> just disable 'arc' and calc for all lines
547
+ # alternative method. Find all "arc" lines and then select the closest to the center
548
+ for quad, object in self.md.geometries.collar.quadrants.items():
549
+ arc_line_tags = [object.lines[name] for name in object.lines.keys() if
550
+ self.geom.iron.hyper_lines[name].type == 'arc']
551
+ closest_dist = 1000.
552
+ for tag in arc_line_tags:
553
+ x, y, _ = gmsh.model.getValue(1, tag, [0.5]) # pick one point on the arc
554
+ dist = np.sqrt(x ** 2 + y ** 2)
555
+ if dist < closest_dist:
556
+ closest_dist = dist
557
+ closest_line = tag ## assumes it is only one line per quadrant
558
+ self.md.geometries.collar.inner_boundary_tags[quad] = [closest_line]
559
+ def define_collar_cooling():
560
+ """
561
+ Defines the cooling holes in the collar
562
+ """
563
+ self.occ.synchronize()
564
+ line_names = [item for key in self.geom.iron.hyper_areas.keys() if 'ch' in key for item in self.geom.iron.hyper_areas[key].lines]
565
+ # line names are the same in each quadrant. Tags are unique
566
+ for quad, qq in self.md.geometries.collar.quadrants.items():
567
+ self.md.geometries.collar.cooling_tags.extend([qq.lines[line] for line in line_names])
568
+ # these tags are only used to be skipped for enfrocing_TSA_mapping
569
+
570
+ # we need the inner collar lines if we want to do TSA, so no need to define it
571
+ if run_type == 'TH' and self.data.magnet.geometry.thermal.use_TSA_new: define_inner_collar()
572
+ # we only need to specify the air holes if we want cooling OR if we enforce TSA nodes on the collar
573
+ if run_type == 'TH' and (self.data.magnet.solve.thermal.collar_cooling.enabled or self.data.magnet.mesh.thermal.collar.Enforce_TSA_mapping): define_collar_cooling()
574
+
575
+
488
576
 
489
577
  def constructWedgeGeometry(self, use_TSA):
490
578
  """
@@ -689,6 +777,11 @@ class Geometry:
689
777
  wedge_reg.lines['l'] = self.occ.addLine(wedge_reg.points['il'], wedge_reg.points['ol'])
690
778
  wedge_reg.lines['i'] = self.occ.addCircleArc(wedge_reg.points['ih'], wedge_reg.points['inner_center'], wedge_reg.points['il'])
691
779
  wedge_reg.lines['o'] = self.occ.addCircleArc(wedge_reg.points['oh'], wedge_reg.points['outer_center'], wedge_reg.points['ol'])
780
+ """
781
+ logger.warning("Using straight wedge geometry") # required for the projection
782
+ wedge_reg.lines['i'] = self.occ.addLine(wedge_reg.points['ih'], wedge_reg.points['il'])
783
+ wedge_reg.lines['o'] = self.occ.addLine(wedge_reg.points['oh'], wedge_reg.points['ol'])
784
+ """
692
785
  wedge_reg.areas[str(wedge_nr)] = dM.Area(loop=self.occ.addCurveLoop(
693
786
  [wedge_reg.lines['i'], wedge_reg.lines['l'], wedge_reg.lines['o'], wedge_reg.lines['h']]))
694
787
 
@@ -1412,9 +1505,49 @@ class Geometry:
1412
1505
  count_rest = -abs(indexes[0] - indexes[1]) if (case == 'beg' and i == 1) or (case == 'end' and i == 0) else 1 + abs(indexes[0] - indexes[1])
1413
1506
  else:
1414
1507
  count_rest = -abs(indexes[0] - indexes[1]) if case == 'beg' else 1 + abs(indexes[0] - indexes[1])
1508
+
1509
+ line_name = 'mid_layer_' + mid_l_name + ('b' if i == 1 else '') + (
1510
+ '_l' if case == 'beg' else '_h')
1511
+
1512
+ gmsh.model.occ.synchronize()
1513
+ """
1514
+ The line that connects two layers may overlap with a new (pole) region. This can be avoided by
1515
+ modifying the line to be an L shape by adding an intermediate point.
1516
+ We add the point along the upper half-turn radial direction towards the center.
1517
+ """
1518
+ p = ins_pnt[pnt_tag_name] # point to be extended
1519
+ linetag = gmsh.model.getAdjacencies(0, ins_pnt[pnt_tag_name])[0][0] # line to be extended
1520
+ # find the distance to move the point towards the center
1521
+ coord1 = gmsh.model.getValue(0, p, [])
1522
+ coord2 = gmsh.model.getValue(0, ins_pnt_prev[pnt_tag_name_opposite], [])
1523
+ # this is the
1524
+ r1 = np.sqrt(coord1[0] ** 2 + coord1[1] ** 2)
1525
+ r2 = np.sqrt(coord2[0] ** 2 + coord2[1] ** 2)
1526
+ distance = (r1 - r2) / 2
1527
+
1528
+ p1, p2 = [b[1] for b in gmsh.model.getBoundary([(1, linetag)], oriented=True)]
1529
+ dir_vector = gmsh.model.getValue(0, p1, [])-gmsh.model.getValue(0, p2, [])
1530
+ unit_vector = dir_vector / np.linalg.norm(dir_vector)
1531
+ coord = gmsh.model.getValue(0, p, [])
1532
+ X = self.occ.addPoint(
1533
+ coord[0] + unit_vector[0] * distance,
1534
+ coord[1] + unit_vector[1] * distance,
1535
+ 0)
1536
+ gmsh.model.occ.synchronize()
1537
+
1538
+ ins_group.lines[line_name + "_A"] = self.occ.addLine(ins_pnt[pnt_tag_name], X)
1539
+ ins_group.lines[line_name + "_B"] = self.occ.addLine(X, ins_pnt_prev[pnt_tag_name_opposite])
1540
+
1541
+ ordered_lines[group_nr].append(
1542
+ [line_name+"_A", count + count_rest -1 if case == 'beg' else count + count_rest, ins_group.lines[line_name+"_A"]])
1543
+ ordered_lines[group_nr].append(
1544
+ [line_name+"_B", count + count_rest if case == 'beg' else count + count_rest -1, ins_group.lines[line_name+"_B"]])
1545
+
1546
+ """ # original code
1415
1547
  line_name = 'mid_layer_' + mid_l_name + ('b' if i == 1 else '') + ('_l' if case == 'beg' else '_h')
1416
1548
  ins_group.lines[line_name] = self.occ.addLine(ins_pnt[pnt_tag_name], ins_pnt_prev[pnt_tag_name_opposite])
1417
1549
  ordered_lines[group_nr].append([line_name, count + count_rest, ins_group.lines[line_name]])
1550
+ """
1418
1551
  break
1419
1552
  # Create all edges of the first block sticking out completely todo: might have to be extended to multiple blocks
1420
1553
  if current_blk != first_block and current_blk not in past_blocks:
@@ -1490,11 +1623,430 @@ class Geometry:
1490
1623
  else:
1491
1624
  ins_group.areas[area_name].loop = self.occ.addCurveLoop([ins_group.lines[line] for line in [x[0] for x in ordered_lines[group_nr]]])
1492
1625
 
1626
+ def constructThinShells_poles(self):
1627
+ ts_layer = self.md.geometries.thin_shells.pole_layers
1628
+ ts_av_ins_thick = self.md.geometries.thin_shells.ins_thickness.poles
1629
+
1630
+ def _construct_thin_shell_corners_to_line(pnt1, pnt2, pole_line, name):
1631
+ # use gmsh to calculate distance to a line
1632
+ coord_a = gmsh.model.getClosestPoint(1, pole_line, coord=(pnt1[0], pnt1[1], 0))[0]
1633
+ coord_b = gmsh.model.getClosestPoint(1, pole_line, coord=(pnt2[0], pnt2[1], 0))[0]
1634
+ # draw new point at half the distance between iH and coord_a
1635
+ new_i = self.occ.addPoint((pnt1[0] + coord_a[0]) / 2, (pnt1[1] + coord_a[1]) / 2, 0)
1636
+ new_o = self.occ.addPoint((pnt2[0] + coord_b[0]) / 2, (pnt2[1] + coord_b[1]) / 2, 0)
1637
+
1638
+ ts_layer[name] = dM.Region()
1639
+
1640
+ self.occ.synchronize()
1641
+ ts_layer[name].lines['1'] = self.occ.addLine(new_i, new_o)
1642
+ self.occ.synchronize()
1643
+
1644
+ cond_name = next(iter(self.data.conductors.keys()))
1645
+ other_material = 0.5*(self.data.conductors[cond_name].cable.th_insulation_along_height+self.data.conductors[cond_name].cable.th_insulation_along_width)#todo, better select which one
1646
+ # distance -> Average between coord_a and iH and coord_b and oH AND remove the G10 thickness
1647
+ ts_av_ins_thick[name] = float(0.5 * (np.sqrt((pnt1[0] - coord_a[0]) ** 2 + (pnt1[1] - coord_a[1]) ** 2) +
1648
+ np.sqrt((pnt2[0] - coord_b[0]) ** 2 + (pnt2[1] - coord_b[1]) ** 2))) - other_material
1649
+ return
1650
+
1651
+ def _find_line_closest_to_points(pnt1, pnt2, line_list_tags):
1652
+ """
1653
+ Should work for any (pole) geometry. Given the half turn corner points pnt1 = [x1, y1], pnt2 = [x2, y2],
1654
+ and a list of pole line tags, we need to select which one is on the opposite side to construct the thin shell line
1655
+ thus, we search the closest line(s) to each corner point and then take the intersection of those two sets
1656
+ """
1657
+ closest_lines = {0: [], 1: []}
1658
+ for i, pnt in enumerate([pnt1, pnt2]):
1659
+ min_dist = float('inf')
1660
+ for line_tag in line_list_tags:
1661
+ tag1, tag2 = gmsh.model.getAdjacencies(1, line_tag)[1]
1662
+ start_pnt = gmsh.model.getValue(0, tag1, [])
1663
+ end_pnt = gmsh.model.getValue(0, tag2, [])
1664
+ v = end_pnt - start_pnt
1665
+ w = pnt - start_pnt
1666
+ c1 = np.dot(w, v)
1667
+ c2 = np.dot(v, v)
1668
+ # avoid extending the line
1669
+ if c1 <= 0:
1670
+ dist = np.linalg.norm(pnt - start_pnt) # Closest to p1
1671
+ elif c2 <= c1:
1672
+ dist = np.linalg.norm(pnt - end_pnt) # Closest to p2
1673
+ else:
1674
+ b = c1 / c2
1675
+ Pb = start_pnt + b * v
1676
+ dist = np.linalg.norm(pnt - Pb)
1677
+
1678
+ if np.isclose(min_dist, dist):
1679
+ closest_lines[i].append(line_tag) # add to list
1680
+ elif dist < min_dist:
1681
+ min_dist = dist
1682
+ closest_lines[i] = [line_tag] # overwrite
1683
+
1684
+ # return the intersection of closest lines
1685
+ return list(set(closest_lines[0]).intersection(set(closest_lines[1])))[0] # should be only one line
1686
+
1687
+ def _split_lines_azimuthal_radial(line_tag_list):
1688
+ alines = []
1689
+ rlines = []
1690
+ for tag in line_tag_list:
1691
+ pointTags = gmsh.model.getAdjacencies(1, tag)[1] # get tag
1692
+ p1 = gmsh.model.getValue(0, pointTags[0], [])
1693
+ p2 = gmsh.model.getValue(0, pointTags[1], [])
1694
+
1695
+ dr = np.sqrt(p2[0] ** 2 + p2[1] ** 2) - np.sqrt(p1[0] ** 2 + p1[1] ** 2)
1696
+ dt = (np.arctan2(p2[1], p2[0]) - np.arctan2(p1[1], p1[0])) * np.sqrt(p2[0] ** 2 + p2[1] ** 2) ## convert to length
1697
+ if np.abs(dt)>np.abs(dr):
1698
+ alines.append(tag)
1699
+ else:
1700
+ rlines.append(tag)
1701
+ return alines, rlines
1702
+
1703
+ def _wedge_to_pole_lines():
1704
+ """
1705
+ These lines do not exist, those that already exist contain the G10 insulation so we need more.
1706
+ """
1707
+ for _, region in self.md.geometries.thin_shells.mid_layers_aux.items(): #those are the g10 layers, we need to duplicate this line to account for the kapton
1708
+ # obtain the position of the lines and draw more on top of it
1709
+ for name, line_tag in region.lines.items():
1710
+ name = f"p{name}_r" # all radial lines
1711
+ ts_layer[name] = dM.Region()
1712
+
1713
+ point_tag = gmsh.model.getBoundary([(1, line_tag)], oriented=False)
1714
+ p1, p2 = point_tag[0][1], point_tag[1][1]
1715
+ x1, y1, _ = gmsh.model.getValue(0, p1, [])
1716
+ x2, y2, _ = gmsh.model.getValue(0, p2, [])
1717
+ # create new line
1718
+ p1_new = gmsh.model.occ.addPoint(x1, y1, 0.0)
1719
+ p2_new = gmsh.model.occ.addPoint(x2, y2, 0.0)
1720
+ self.occ.synchronize()
1721
+ ts_layer[name].lines['1'] = self.occ.addLine(p1_new, p2_new)
1722
+ self.occ.synchronize()
1723
+
1724
+ # thickness = distance between the wedge and the pole - G10 thickness
1725
+ # how to approximate this thickness? -> use the same thickness as for the ht 108
1726
+ """
1727
+ cond_name = next(iter(self.data.conductors.keys()))
1728
+ other_material = 0.5 * (
1729
+ self.data.conductors[cond_name].cable.th_insulation_along_height + self.data.conductors[
1730
+ cond_name].cable.th_insulation_along_width)
1731
+ # distance -> Average between coord_a and iH and coord_b and oH AND remove the G10 thickness ?
1732
+ ts_av_ins_thick[name] = ???
1733
+ """
1734
+ ts_av_ins_thick[name] = ts_av_ins_thick['p108_a'] #@emma hardcoded thickness, use the same as for this ht
1735
+
1736
+ max_layer = max(self.geom.coil.coils[1].poles[1].layers.keys())
1737
+ # first, the lines connecting hts to the pole
1738
+ for coil_nr, coil in self.md.geometries.coil.anticlockwise_order.coils.items(): # coilnr is only 1 here
1739
+ for layer_nr, layer in coil.layers.items(): # we need to add radial TS lines for both layers
1740
+ for nr, blk_order in enumerate(layer):
1741
+ block = self.geom.coil.coils[coil_nr].poles[blk_order.pole].layers[
1742
+ layer_nr].windings[blk_order.winding].blocks[blk_order.block]
1743
+ ht_list = list(self.md.geometries.coil.coils[coil_nr].poles[blk_order.pole].layers[
1744
+ layer_nr].windings[blk_order.winding].blocks[
1745
+ blk_order.block].half_turns.areas.keys())
1746
+
1747
+ blk_index_next = nr + 1 if nr + 1 < len(layer) else 0
1748
+ block_order_next = layer[blk_index_next]
1749
+ block_next = self.geom.coil.coils[coil_nr].poles[block_order_next.pole].layers[
1750
+ layer_nr].windings[block_order_next.winding].blocks[block_order_next.block]
1751
+ ht_list_next = list(self.md.geometries.coil.coils[coil_nr].poles[block_order_next.pole].layers[
1752
+ layer_nr].windings[block_order_next.winding].blocks[
1753
+ block_order_next.block].half_turns.areas.keys())
1754
+ ht_last = int(ht_list[-1])
1755
+ ht_next_first = int(ht_list_next[0])
1756
+ # if winding nr is the same and pole is the same -> pole connection
1757
+
1758
+ if blk_order.pole == block_order_next.pole and blk_order.winding == block_order_next.winding:
1759
+ # Create radial lines for pole connection
1760
+ pole_lines_a, pole_lines_r = _split_lines_azimuthal_radial(
1761
+ [v for qq in self.md.geometries.poles.quadrants.keys() for v in
1762
+ self.md.geometries.poles.quadrants[qq].lines.values()])
1763
+ # todo maybe this can be speed up if we select the correct quadrant
1764
+ if layer_nr != max_layer:
1765
+ # add azimuthal thin shells at the ht side 'o' (normals radial)
1766
+ # Now we consider all the HT's from one block, this might not hold true in general
1767
+
1768
+ for ht in map(int, ht_list):
1769
+ oH = [block.half_turns[ht].corners.bare.oH.x,
1770
+ block.half_turns[ht].corners.bare.oH.y, 0.0]
1771
+ oL = [block.half_turns[ht].corners.bare.oL.x,
1772
+ block.half_turns[ht].corners.bare.oL.y, 0.0]
1773
+ _construct_thin_shell_corners_to_line(
1774
+ oH, oL,
1775
+ _find_line_closest_to_points(oH, oL, pole_lines_a),
1776
+ name=f"p{ht}_r")
1777
+
1778
+ for ht in map(int, ht_list_next):
1779
+ oH = [block_next.half_turns[ht].corners.bare.oH.x,
1780
+ block_next.half_turns[ht].corners.bare.oH.y, 0.0]
1781
+ oL = [block_next.half_turns[ht].corners.bare.oL.x,
1782
+ block_next.half_turns[ht].corners.bare.oL.y, 0.0]
1783
+ _construct_thin_shell_corners_to_line(
1784
+ oH, oL,
1785
+ _find_line_closest_to_points(oH, oL, pole_lines_a),
1786
+ name=f"p{ht}_r")
1787
+
1788
+ iH = [block.half_turns[ht_last].corners.bare.iH.x,
1789
+ block.half_turns[ht_last].corners.bare.iH.y, 0.0]
1790
+ oH = [block.half_turns[ht_last].corners.bare.oH.x,
1791
+ block.half_turns[ht_last].corners.bare.oH.y, 0.0]
1792
+ _construct_thin_shell_corners_to_line(
1793
+ iH, oH,
1794
+ _find_line_closest_to_points(iH, oH, pole_lines_r),
1795
+ name=f"p{ht_last}_a")
1796
+
1797
+ iL = [block_next.half_turns[ht_next_first].corners.bare.iL.x,
1798
+ block_next.half_turns[ht_next_first].corners.bare.iL.y, 0.0]
1799
+ oL = [block_next.half_turns[ht_next_first].corners.bare.oL.x,
1800
+ block_next.half_turns[ht_next_first].corners.bare.oL.y, 0.0]
1801
+ _construct_thin_shell_corners_to_line(
1802
+ iL, oL,
1803
+ _find_line_closest_to_points(iL, oL, pole_lines_r),
1804
+ name=f"p{ht_next_first}_a")
1805
+ # second, the lines connecting wedge to the pole
1806
+ _wedge_to_pole_lines()
1807
+ def constructAdditionalThinShells(self):
1808
+ def _create_lines_ht_alignment(ohx, ohy, olx, oly, ihx, ihy, ilx, ily):
1809
+ def __line_circle_intersection(p1, p2):
1810
+ x1, y1 = p1
1811
+ x2, y2 = p2
1812
+ dx = x2 - x1
1813
+ dy = y2 - y1
1814
+
1815
+ A = dx**2 + dy**2
1816
+ B = 2 * (dx*x1 + dy*y1)
1817
+ C = x1**2 + y1**2 - R**2 # R collar is known from outer scope
1818
+
1819
+ disc = B**2 - 4*A*C
1820
+ if disc < 0:
1821
+ logger.warning(" No intersection between line and circle.")
1822
+ return [] # no intersection
1823
+ else:
1824
+ sqrt_disc = np.sqrt(disc)
1825
+ t1 = (-B + sqrt_disc) / (2*A)
1826
+ t2 = (-B - sqrt_disc) / (2*A)
1827
+ return [(x1 + t1*dx, y1 + t1*dy),
1828
+ (x1 + t2*dx, y1 + t2*dy)]
1829
+
1830
+ mid_bare_o = np.mean([[ohx, ohy], [olx, oly]], axis=0)
1831
+ mid_bare_i = np.mean([[ihx, ihy], [ilx, ily]], axis=0)
1832
+ offsets = np.array([[ohx, ohy], [olx, oly]]) - mid_bare_o
1833
+ quad = 1 if mid_bare_o[0] >= 0 and mid_bare_o[1] >= 0 else (
1834
+ 2 if mid_bare_o[0] <= 0 <= mid_bare_o[1] else (3 if mid_bare_o[0] <= 0 and mid_bare_o[1] <= 0 else 4))
1835
+ inters = __line_circle_intersection(mid_bare_o, mid_bare_i)
1836
+ quadrants = {
1837
+ 1: lambda x, y: x >= 0 and y >= 0,
1838
+ 2: lambda x, y: x <= 0 <= y,
1839
+ 3: lambda x, y: x <= 0 and y <= 0,
1840
+ 4: lambda x, y: x >= 0 >= y,
1841
+ }
1842
+ check = quadrants[quad]
1843
+ for x, y in inters:
1844
+ if check(x, y): # select the correct intersection based on the quadrant
1845
+ xi, yi = x, y
1846
+ break
1847
+
1848
+ # add a point halfway between (x,y) and (xi, yi)
1849
+ dr = float(np.sqrt((xi - mid_bare_o[0]) ** 2 + (yi - mid_bare_o[1]) ** 2))
1850
+ new_point_coords = [mid_bare_o[0] + 0.5 * (xi - mid_bare_o[0]), mid_bare_o[1] + 0.5 * (yi - mid_bare_o[1])]
1851
+ for offset in offsets:
1852
+ point_tag_final = self.occ.addPoint(new_point_coords[0] + offset[0], new_point_coords[1] + offset[1], 0)
1853
+ self.occ.synchronize()
1854
+ return dr, point_tag_final
1855
+ def _embed_points_to_collar_curve():
1856
+ def __add_point_in_curve(points, curve_tag):
1857
+ return self.occ.fragment([(0, k) for k in points], [(1, curve_tag)], removeObject=True)[0]
1858
+
1859
+ ### First, cut the collar line
1860
+ self.occ.synchronize()
1861
+ new_tags = {1: [], 2: [], 3: [], 4: []} # tuples
1862
+ new_point_tags = {1: [], 2: [], 3: [], 4: []} # only tags
1863
+ collar_size = self.data.magnet.mesh.thermal.collar.SizeMin
1864
+ for ts_name, ts in self.md.geometries.thin_shells.collar_layers.items():
1865
+ for name, line in ts.lines.items():
1866
+ coords = gmsh.model.getValue(1, line, [i[0] for i in gmsh.model.getParametrizationBounds(1, line)])
1867
+ t1 = np.arctan2(coords[0], coords[1])
1868
+ t2 = np.arctan2(coords[3], coords[4])
1869
+ quad = t1 // (np.pi / 2) + 1 if t1 > 0 else 4 + t1 // (np.pi / 2) + 1
1870
+ curve_tag = self.md.geometries.collar.inner_boundary_tags[quad][0]
1871
+ start, end = min(t1 % (np.pi / 2), t2 % (np.pi / 2)), max(t1 % (np.pi / 2),
1872
+ t2 % (np.pi / 2)) # ensure start < end
1873
+ tmp_coords = gmsh.model.getValue(1, line, [i[0] for i in gmsh.model.getParametrizationBounds(1, line)])
1874
+ target_size = collar_size
1875
+ elements = max(1, round(Func.points_distance(tmp_coords[:2], tmp_coords[3:-1]) / target_size))+1
1876
+ if elements%2 == 1: elements += 1
1877
+ para_coords = np.linspace(start, end, elements, endpoint=True)
1878
+
1879
+ for u in para_coords:
1880
+ if quad == 1 or quad == 3:
1881
+ u = np.pi / 2 - u ## magic
1882
+ x, y, z = gmsh.model.getValue(1, curve_tag, [u]) # Evaluate point on curve
1883
+ new_point_tags[quad].append(self.occ.addPoint(x, y, z)) # Add point coordinates to the list
1884
+
1885
+ for q in new_point_tags.keys():
1886
+ new_tags[q].extend(__add_point_in_curve(new_point_tags[q], self.md.geometries.collar.inner_boundary_tags[q][0]))
1887
+ # so for occ the old curve no longer exists, but in the objects the tag is still there
1888
+
1889
+ REMOVED_TAGS = [self.md.geometries.collar.inner_boundary_tags[q][0] for q in new_tags.keys()]
1890
+
1891
+ # update boundary tags
1892
+ for quad, taglist in new_tags.items():
1893
+ curvelist = [tag[1] for tag in taglist if tag[0]==1 ] # select the curves only
1894
+ self.md.geometries.collar.inner_boundary_tags[quad] = curvelist # replace
1895
+ for k, new_tag in enumerate(self.md.geometries.collar.inner_boundary_tags[quad]):
1896
+ self.md.geometries.collar.quadrants[quad].lines[str(k)] = new_tag ## adding new lines
1897
+
1898
+ # REDEFINE THE CURVELOOP
1899
+ loop_list = []
1900
+ tmpdict = copy.deepcopy(self.md.geometries.collar.quadrants[quad].lines)
1901
+ for tag_name, tag in tmpdict.items():
1902
+ if tag in self.md.geometries.collar.cooling_tags: # skip holes
1903
+ continue
1904
+ if tag in REMOVED_TAGS:
1905
+ # remove from dictionary
1906
+ del self.md.geometries.collar.quadrants[quad].lines[tag_name]
1907
+ continue
1908
+ loop_list.append(tag)
1909
+ self.occ.synchronize()
1910
+
1911
+ for area in self.md.geometries.collar.quadrants[quad].areas:
1912
+ if area.startswith('arc') and not area.startswith('arch'): #collar region, not the holes
1913
+ self.md.geometries.collar.quadrants[quad].areas[area] = dM.Area(
1914
+ loop=self.occ.addCurveLoop(loop_list))
1915
+
1916
+ # Cut the pole lines
1917
+ """
1918
+ idea, loop over the thin shell lines, then draw line from the HT corner to the line ends, then intersect it with the pole curve
1919
+ just find intersection and select the shortest one ?
1920
+
1921
+ # not implemented, maybe not necessary
1922
+ """
1923
+ for quad in [1, 2, 3, 4]:
1924
+ # additionally add the pole area back
1925
+ for area in self.md.geometries.poles.quadrants[quad].areas:
1926
+ if area.startswith('arp'):
1927
+ self.md.geometries.poles.quadrants[quad].areas[area] = dM.Area(
1928
+ loop=self.occ.addCurveLoop(list(self.md.geometries.poles.quadrants[quad].lines.values())))
1929
+
1930
+ self.occ.synchronize()
1931
+
1932
+ # point still needs to be added
1933
+ ts_layer = self.md.geometries.thin_shells.collar_layers
1934
+ ts_av_ins_thick = self.md.geometries.thin_shells.ins_thickness.collar
1935
+ collar_tag_dict = self.md.geometries.collar.inner_boundary_tags
1936
+ enforce_TSA_mapping_collar = self.data.magnet.mesh.thermal.collar.Enforce_TSA_mapping # if True, cut the collar curve into segments, otherwise use the whole curve
1937
+
1938
+ center = self.occ.addPoint(0, 0, 0)
1939
+ collar_x, collar_y, _ = gmsh.model.getValue(1, collar_tag_dict[1][0], [0])
1940
+ R = np.sqrt(collar_x ** 2 + collar_y ** 2) # radius of collar curve
1941
+
1942
+ alignment = ['radial', 'ht'][1] # pick ht alignment
1943
+
1944
+ # COLLAR
1945
+ for pid, pole in self.geom.coil.coils[1].poles.items():
1946
+ layer_num = max(pole.layers.keys()) # outside layer
1947
+ for wid, winding in pole.layers[layer_num].windings.items():
1948
+ for block_idx in winding.blocks.keys():
1949
+ block = winding.blocks[block_idx]
1950
+ ht_nr_area = list(self.md.geometries.coil.coils[1].poles[pid].layers[layer_num].windings[wid].blocks[block_idx].half_turns.areas.keys())
1951
+ i = 0
1952
+ for ht_nr, ht in block.half_turns.items(): #ht_idx is not the same as number
1953
+ ht_old = ht_nr_area[i]
1954
+ i+=1
1955
+ dr = 0.
1956
+ if alignment == 'radial':
1957
+ for (x, y), (x1, y1) in zip([[ht.corners.bare.oH.x, ht.corners.bare.oH.y], [ht.corners.bare.oL.x, ht.corners.bare.oL.y]],
1958
+ [[ht.corners.bare.iH.x, ht.corners.bare.iH.y], [ht.corners.bare.iL.x, ht.corners.bare.iL.y]]): # uses the bare coordinates of the HT
1959
+ t = np.arctan2(y, x)
1960
+ quad = t // (np.pi / 2) + 1 if t > 0 else 4 + t // (np.pi / 2) + 1
1961
+ collar_idx = collar_tag_dict[quad][0] # get the (debug: ONE) collar curve tag for the quadrant
1962
+
1963
+ dr_prev = dr
1964
+
1965
+ dummy = self.occ.addPoint(x, y, 0)
1966
+ dr = self.occ.get_distance(0, dummy, 1, collar_idx)[0]
1967
+ self.occ.synchronize()
1968
+ self.occ.remove([(0, dummy)])
1969
+ point_tag_final = self.occ.addPoint(x + 0.5 * dr * np.cos(t), y + 0.5 * dr * np.sin(t), 0)
1970
+
1971
+ elif alignment == 'ht': # distance to the next half turn (more relevant for coil to coil distances)
1972
+ dr, point_tag_final = _create_lines_ht_alignment(ohx = ht.corners.bare.oH.x, ohy=ht.corners.bare.oH.y,
1973
+ olx = ht.corners.bare.oL.x, oly=ht.corners.bare.oL.y,
1974
+ ihx = ht.corners.bare.iH.x, ihy=ht.corners.bare.iH.y,
1975
+ ilx = ht.corners.bare.iL.x, ily=ht.corners.bare.iL.y)
1976
+ # save the line
1977
+ name = f'{ht_nr}_x'
1978
+ ts_layer[name] = dM.Region()
1979
+ self.occ.synchronize()
1980
+ ts_layer[name].lines['1'] = self.occ.addLine(point_tag_final-1, point_tag_final)
1981
+ cond_name = next(iter(self.data.conductors.keys()))
1982
+ if alignment == 'radial':
1983
+ ts_av_ins_thick[name] = 0.5*(dr+dr_prev) - self.data.conductors[cond_name].cable.th_insulation_along_width # approx thickness, average - insulation
1984
+ elif alignment == 'ht':
1985
+ ts_av_ins_thick[name] = dr - self.data.conductors[cond_name].cable.th_insulation_along_width
1986
+ self.occ.synchronize()
1987
+
1988
+ # WEDGES
1989
+ if self.data.magnet.geometry.thermal.with_wedges:
1990
+ for wedge_idx, wedge in self.md.geometries.wedges.coils[1].layers[max(self.md.geometries.wedges.coils[1].layers.keys())].wedges.items():
1991
+ dr=0.
1992
+ if alignment == 'radial':
1993
+ raise NotImplementedError("Wedge radial alignment not implemented")
1994
+
1995
+ elif alignment == 'ht': # distance to the next half turn (more relevant for coil to coil distances)
1996
+ ohx, ohy, _ = gmsh.model.getValue(0, wedge.points['oh'], [])
1997
+ olx, oly, _ = gmsh.model.getValue(0, wedge.points['ol'], [])
1998
+ ihx, ihy, _ = gmsh.model.getValue(0, wedge.points['ih'], [])
1999
+ ilx, ily, _ = gmsh.model.getValue(0, wedge.points['il'], [])
2000
+ dr, point_tag_final = _create_lines_ht_alignment(ohx=ohx, ohy=ohy, olx=olx, oly=oly,
2001
+ ihx=ihx, ihy=ihy, ilx=ilx, ily=ily)
2002
+
2003
+ # find the smallest thickness
2004
+ dr_b = 0.0
2005
+ for x, y in [[ohx, ohy],
2006
+ [olx, oly]]:
2007
+ dummy = self.occ.addPoint(x, y, 0)
2008
+ dr_a = dr_b
2009
+ t = np.arctan2(y, x)
2010
+ quad = t // (np.pi / 2) + 1 if t > 0 else 4 + t // (np.pi / 2) + 1
2011
+ collar_idx = collar_tag_dict[quad][0]
2012
+ dr_b = self.occ.get_distance(0, dummy, 1, collar_idx)[0]
2013
+ self.occ.remove([(0, dummy)])
2014
+
2015
+ # save line
2016
+ name = f'w{wedge_idx}_x'
2017
+ ts_layer[name] = dM.Region()
2018
+ ts_layer[name].lines['1'] = self.occ.addLine(point_tag_final - 1, point_tag_final) # make index line trivial (no distinction)
2019
+
2020
+ cond_name = next(iter(self.data.conductors.keys()))
2021
+ #### ts_av_ins_thick[name] = dr - self.data.conductors[cond_name].cable.th_insulation_along_width
2022
+ # This overshoots the thickness. The wedges are curved and the center between the corners (straight line) is further away from the collar than the curved wedge boundary.
2023
+ # Maybe one could use gmsh to obtain the distance between the outer curve and the collar, but this should be close to taking the minimum distance of the cornerpoints as both curves are
2024
+ # circle segments of (approximately) two concentric circles around the centre
2025
+
2026
+ logger = logging.getLogger('FiQuS')
2027
+ logger.warning("Using alternative wedge insulation thickness approximation ")
2028
+ ts_av_ins_thick[name] = min(dr_a,dr_b)- self.data.conductors[cond_name].cable.th_insulation_along_width # approx thickness, average - insulation
2029
+
2030
+ # POLES
2031
+ if 'poles' in self.data.magnet.geometry.thermal.areas:
2032
+ # generate additional thin shell lines
2033
+ self.constructThinShells_poles()
2034
+
2035
+ #gmsh.fltk.run() constructed correctly
2036
+
2037
+ if enforce_TSA_mapping_collar:
2038
+ self.occ.synchronize()
2039
+ _embed_points_to_collar_curve() ## both coils and wedges
2040
+
2041
+ self.occ.remove([(0, center)]) # remove center point
2042
+ self.occ.synchronize()
1493
2043
  def constructThinShells(self, with_wedges):
2044
+ # default
1494
2045
  ins_th = self.md.geometries.thin_shells.ins_thickness
1495
2046
  mid_pole_ts = self.md.geometries.thin_shells.mid_poles
1496
2047
  mid_winding_ts = self.md.geometries.thin_shells.mid_windings
1497
2048
  mid_turn_ts = self.md.geometries.thin_shells.mid_turn_blocks
2049
+ # not default
1498
2050
  mid_layer_ts = self.md.geometries.thin_shells.mid_layers_ht_to_ht
1499
2051
  mid_layer_ts_aux = self.md.geometries.thin_shells.mid_layers_aux
1500
2052
 
@@ -1520,6 +2072,7 @@ class Geometry:
1520
2072
  oH = [block.half_turns[ht_last].corners.bare.oH.x, block.half_turns[ht_last].corners.bare.oH.y]
1521
2073
  oL = [block_next.half_turns[ht_next_first].corners.bare.oL.x, block_next.half_turns[ht_next_first].corners.bare.oL.y]
1522
2074
  ts_name = str(blk_order.block) + '_' + str(block_order_next.block)
2075
+
1523
2076
  for ts, th, condition in zip([mid_pole_ts, mid_winding_ts], [ins_th.mid_pole, ins_th.mid_winding],
1524
2077
  # ['_ly' + str(layer_nr), '_wd' + str(blk_order.winding) + '_wd' + str(block_order_next.winding)],
1525
2078
  [self.geom.coil.coils[coil_nr].type == 'cos-theta' and block_order_next.pole != blk_order.pole,
@@ -1596,7 +2149,18 @@ class Geometry:
1596
2149
  line_name = pnt_current[:-1] + '_' + ordered_pnts[iter_nr][0][:-1]
1597
2150
  else:
1598
2151
  line_name = pnt_current[:-1] + '_' + pnt_next[:-1]
1599
- ts.mid_layers.lines[line_name] = self.occ.addLine(pnt[2], ordered_pnts[nr + 1][2])
2152
+
2153
+ # TODO look into why this exception handling is needed for SMC magnet with ESC coils - the following meshing stage does not work
2154
+ try:
2155
+ tag = self.occ.addLine(pnt[2], ordered_pnts[nr + 1][2])
2156
+ ts.mid_layers.lines[line_name] = tag
2157
+ except Exception as e:
2158
+ ts.mid_layers.lines[line_name] = tag # this will be the last tag, i.e. from previously created line
2159
+ x1, y1, z1 = gmsh.model.occ.getBoundingBox(1, pnt[2])[:3]
2160
+ x2, y2, z2 = gmsh.model.occ.getBoundingBox(1, ordered_pnts[nr + 1][2])[:3]
2161
+ distance = np.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2 + (z2 - z1) ** 2)
2162
+ logger.info(f"{e} {line_name} between point tags {pnt[2], ordered_pnts[nr + 1][2]} and distance between points {distance}")
2163
+
1600
2164
  if ts_name in mid_layer_ts_aux:
1601
2165
  aux_pnt = list(mid_layer_ts_aux[ts_name].points.keys())[0]
1602
2166
  other_pnt = ordered_pnts[0 if aux_pnt[-1] == 'l' else -1]
@@ -1736,26 +2300,28 @@ class Geometry:
1736
2300
  """
1737
2301
  iron = self.geom.iron
1738
2302
  gm = self.md.geometries
1739
- with_iron_yoke = self.data.magnet.geometry.electromagnetics.with_iron_yoke if run_type == 'EM'\
1740
- else self.data.magnet.geometry.thermal.with_iron_yoke
1741
- with_wedges = self.data.magnet.geometry.electromagnetics.with_wedges if run_type == 'EM' \
1742
- else self.data.magnet.geometry.thermal.with_wedges
1743
-
1744
- # Build iron yoke domains
1745
- if with_iron_yoke:
1746
- for quadrant, qq in gm.iron.quadrants.items():
2303
+ geometry_setting = self.data.magnet.geometry.electromagnetics if run_type == 'EM' \
2304
+ else self.data.magnet.geometry.thermal
2305
+
2306
+ with_wedges = geometry_setting.with_wedges
2307
+
2308
+ inv_nc = {v: k for k, v in self.nc.items()} #invert naming convention
2309
+ for a in geometry_setting.areas: # a in ['iron_yoke', 'collar', ...]:
2310
+ for quadrant, qq in getattr(gm, a).quadrants.items():
1747
2311
  for area_name, area in qq.areas.items():
1748
- build = True
1749
- loops = [area.loop]
1750
- for hole_key, hole in iron.hyper_holes.items():
1751
- if area_name == hole.areas[1]:
1752
- loops.append(qq.areas[hole.areas[0]].loop)
1753
- elif area_name == hole.areas[0]: # or iron.hyper_areas[area_name].material == 'BH_air':
1754
- build = False
1755
- if build:
1756
- area.surface = self.occ.addPlaneSurface(loops)
1757
- # Group areas per material type
1758
- self.md.domains.groups_entities.iron[iron.hyper_areas[area_name].material].append(area.surface)
2312
+ identifier = next((k for k in self.inv_nc.keys() if (k in area_name[2:])), None)#re.sub(r'\d+', '',area_name[2:])
2313
+ if a == inv_nc.get(identifier, None): # ensure it is part of the iron yoke or collar (iron, collar)
2314
+ build = True
2315
+ loops = [area.loop]
2316
+ for hole_key, hole in iron.hyper_holes.items():
2317
+ if area_name == hole.areas[1]:
2318
+ loops.append(qq.areas[hole.areas[0]].loop)
2319
+ elif area_name == hole.areas[0]: #skip holes
2320
+ area.surface = self.occ.addPlaneSurface(loops) # also build the holes. An existing curveloop without area is very annoying
2321
+ build = False
2322
+ if build:
2323
+ area.surface = self.occ.addPlaneSurface(loops)
2324
+ getattr(self.md.domains.groups_entities, a)[iron.hyper_areas[area_name].material].append(area.surface) ## save the material
1759
2325
 
1760
2326
  # Build coil domains
1761
2327
  for coil_nr, coil in gm.coil.coils.items():
@@ -1774,7 +2340,7 @@ class Geometry:
1774
2340
  wedge.areas[str(wedge_nr)].surface = self.occ.addPlaneSurface([wedge.areas[str(wedge_nr)].loop])
1775
2341
 
1776
2342
  # Build insulation domains
1777
- if run_type == 'TH' and not self.data.magnet.geometry.thermal.use_TSA:
2343
+ if run_type == 'TH' and not geometry_setting.use_TSA:
1778
2344
  for coil_nr, coil in gm.insulation.coils.items():
1779
2345
  for group_nr, group in coil.group.items():
1780
2346
  holes = []
@@ -1794,10 +2360,10 @@ class Geometry:
1794
2360
 
1795
2361
  # Create and build air far field
1796
2362
  if run_type == 'EM':
1797
- if self.data.magnet.geometry.electromagnetics.with_iron_yoke:
2363
+ if 'iron_yoke' in geometry_setting.areas:
1798
2364
  for i in iron.key_points:
1799
- gm.iron.max_radius = max(gm.iron.max_radius, max(iron.key_points[i].x, iron.key_points[i].y))
1800
- greatest_radius = gm.iron.max_radius
2365
+ gm.iron_yoke.max_radius = max(gm.iron_yoke.max_radius, max(iron.key_points[i].x, iron.key_points[i].y)) # this also contains other regions, e.g. collar but this has no effect
2366
+ greatest_radius = gm.iron_yoke.max_radius
1801
2367
  else: # no iron yoke data available
1802
2368
  for coil_nr, coil in self.geom.coil.coils.items():
1803
2369
  for pole_nr, pole in coil.poles.items():
@@ -1807,8 +2373,8 @@ class Geometry:
1807
2373
  abs(pole.layers[len(pole.layers)].windings[first_winding].blocks[first_block].block_corners.oL.y),
1808
2374
  gm.coil.max_radius)
1809
2375
  greatest_radius = gm.coil.max_radius
1810
- radius_in = greatest_radius * (2.5 if self.data.magnet.geometry.electromagnetics.with_iron_yoke else 6)
1811
- radius_out = greatest_radius * (3.2 if self.data.magnet.geometry.electromagnetics.with_iron_yoke else 8)
2376
+ radius_in = greatest_radius * (2.5 if 'iron_yoke' in geometry_setting.areas else 6)
2377
+ radius_out = greatest_radius * (3.2 if 'iron_yoke' in geometry_setting.areas else 8)
1812
2378
  air_inf_center_x, air_inf_center_y = 0, 0
1813
2379
  for coil_nr, coil in self.md.geometries.coil.coils.items():
1814
2380
  air_inf_center_x += coil.bore_center.x
@@ -1859,8 +2425,8 @@ class Geometry:
1859
2425
  # self.md.domains.groups_entities.air_inf = [gm.air_inf.areas['outer'].surface]
1860
2426
  gm.air_inf.areas['inner'].surface = self.occ.addPlaneSurface([gm.air_inf.areas['inner'].loop])
1861
2427
 
1862
- # self.occ.synchronize()
1863
- # self.gu.launch_interactive_GUI()
2428
+ self.occ.synchronize()
2429
+ #self.gu.launch_interactive_GUI()
1864
2430
 
1865
2431
  def fragment(self):
1866
2432
  """
@@ -1869,9 +2435,14 @@ class Geometry:
1869
2435
  # Collect surfaces to be subtracted by background air
1870
2436
  holes = []
1871
2437
 
1872
- # Iron
1873
- for group_name, surfaces in self.md.domains.groups_entities.iron.items():
1874
- holes.extend([(2, s) for s in surfaces])
2438
+ # iron yoke and collar
2439
+ group_keys = self.nc.keys()
2440
+
2441
+ for key in group_keys:
2442
+ group = getattr(self.md.domains.groups_entities, key)
2443
+ for _, surfaces in group.items():
2444
+ holes.extend([(2, s) for s in surfaces])
2445
+
1875
2446
  # Coils
1876
2447
  for coil_nr, coil in self.md.geometries.coil.coils.items():
1877
2448
  for pole_nr, pole in coil.poles.items():
@@ -1904,7 +2475,6 @@ class Geometry:
1904
2475
  self.md.domains.groups_entities.air.append(e[1])
1905
2476
 
1906
2477
  def updateTags(self, run_type, symmetry):
1907
-
1908
2478
  # Update half turn line tags
1909
2479
  for coil_nr, coil in self.md.geometries.coil.coils.items():
1910
2480
  for pole_nr, pole in coil.poles.items():
@@ -1921,7 +2491,108 @@ class Geometry:
1921
2491
  hts.lines[ht_nr + 'o'] = first_tag + 2
1922
2492
  hts.lines[ht_nr + 'h'] = first_tag + 3
1923
2493
 
1924
- # Update insulation line tags
2494
+ # Update collar tags
2495
+ if run_type == "TH" and 'collar' in self.data.magnet.geometry.thermal.areas:
2496
+ for quad, old_tags in self.md.geometries.collar.quadrants.items():
2497
+ self.md.geometries.collar.inner_boundary_tags[quad] = [] # reset the inner boundary tags
2498
+ new_tags = []
2499
+ for name, area in self.md.geometries.collar.quadrants[quad].areas.items(): # arcol contains the boundaries of the holes too
2500
+ if not re.match(r"^ar.h", name):
2501
+ # the issue is that you don't know which line is which, e.g. which is the inner collar line
2502
+ new_tags.extend([int(k) for k in gmsh.model.getAdjacencies(2, area.surface)[1]])
2503
+
2504
+ for k, name in enumerate(self.md.geometries.collar.quadrants[quad].lines.keys()):
2505
+ self.md.geometries.collar.quadrants[quad].lines[name] = new_tags[k]
2506
+
2507
+ # Update inner collar tags
2508
+ collar_lines = [self.md.geometries.collar.quadrants[quad].lines[name] for name in self.md.geometries.collar.quadrants[quad].lines.keys()]
2509
+ closest_dist = 1000.
2510
+ closest_lines = []
2511
+ max_dist = 0.0
2512
+ max_lines = []
2513
+ # We assume that the middle of the curve of the collar is the closest one to the centre (0,0)
2514
+ for tag in collar_lines:
2515
+ ##x, y, _ = gmsh.model.getValue(1, tag, [0.0]) # pick one point on the line
2516
+ curve_type = gmsh.model.getType(1, tag)
2517
+ if curve_type == 'Line': # find the middle of the line
2518
+ tag1, tag2 = gmsh.model.getAdjacencies(1, tag)[1]
2519
+ x1, y1, z1 = gmsh.model.getValue(0, tag1, [])
2520
+ x2, y2, z2 = gmsh.model.getValue(0, tag2, [])
2521
+ x = 0.5 * (x1 + x2)
2522
+ y = 0.5 * (y1 + y2)
2523
+ # take the average of the end points
2524
+ dist = np.sqrt(x ** 2 + y ** 2)
2525
+ elif curve_type == "Circle": # use any point on the circle (same distance because concentric with origin)
2526
+ x, y, z = gmsh.model.getValue(1, tag, [0.5])
2527
+ dist = np.sqrt(x ** 2 + y ** 2)
2528
+
2529
+ if dist < closest_dist+1e-10:
2530
+ if dist < closest_dist-1e-10: # clear if new min is found
2531
+ closest_dist = dist
2532
+ closest_lines = []
2533
+ closest_lines.append(tag)
2534
+ if dist > max_dist-1e-10:
2535
+ if dist > max_dist+1e-10: # clear if new max is found
2536
+ max_dist = dist
2537
+ max_lines = []
2538
+ max_lines.append(tag)
2539
+ self.md.geometries.collar.inner_boundary_tags[quad] = closest_lines
2540
+ self.md.geometries.collar.outer_boundary_tags[quad] = max_lines
2541
+
2542
+ # outer collar tags, does not work because geom.iron has the old tags and this is not accurate anymore
2543
+ """
2544
+ outer = [old_tags.lines[name] for name in old_tags.lines.keys() if
2545
+ self.geom.iron.hyper_lines[name].type == 'line']
2546
+ logger.info("outer tags", outer)
2547
+ self.md.geometries.collar.outer_boundary_tags[quad] = outer
2548
+ """
2549
+
2550
+ # concerning the TSL, it seems impossible to get the tags of the lines, as they are not adjacent to a surface nor (yet) grouped in a physical group
2551
+ # only way to ensure equal tags is by creating them in the same way as gmsh orders the lines. (e.g. all at the start or all at the end would work)
2552
+ # we cannot swap order... soo lets hope that just a shift in numbers is sufficient
2553
+ if run_type == 'TH' and self.data.magnet.geometry.thermal.use_TSA_new and self.data.magnet.mesh.thermal.collar.Enforce_TSA_mapping:
2554
+ shift = len(self.md.geometries.collar.inner_boundary_tags[1])*4 -4
2555
+
2556
+ if 'poles' in self.data.magnet.geometry.thermal.areas:
2557
+ shift -= (1*4) # we shifted too much
2558
+
2559
+ ## update TSA collar lines
2560
+ for _, ts in self.md.geometries.thin_shells.collar_layers.items():
2561
+ ts.lines['1'] += shift
2562
+ for _, ts in self.md.geometries.thin_shells.pole_layers.items():
2563
+ ts.lines['1'] += shift
2564
+
2565
+ atts = ['mid_layers_ht_to_ht', 'mid_layers_wdg_to_ht', 'mid_layers_ht_to_wdg', 'mid_layers_wdg_to_wdg', 'mid_poles', 'mid_windings', 'mid_turn_blocks' , 'mid_wedge_turn']
2566
+ for at in atts:
2567
+ for _, ts_region in getattr(self.md.geometries.thin_shells, at).items():
2568
+ try: ts_region = ts_region.mid_layers
2569
+ except AttributeError: pass
2570
+ for key in ts_region.lines.keys():
2571
+ ts_region.lines[key] += shift
2572
+
2573
+ # Update coil cooling tags
2574
+ ### no consistent way to get the tags of the cooling lines, so we assume that they are ordered in the same way as gmsh orders the lines
2575
+ if self.data.magnet.solve.thermal.collar_cooling.enabled:
2576
+ tags =[]
2577
+ ## if we want all cooling holes
2578
+ if self.data.magnet.solve.thermal.collar_cooling.which == 'all':
2579
+ for quad, region in self.md.geometries.collar.quadrants.items():
2580
+ for name, area in region.areas.items():
2581
+ if re.match(r"^ar.h", name):
2582
+ tags.extend([int(k) for k in gmsh.model.getAdjacencies(2, area.surface)[1]])
2583
+ else:
2584
+ nr_applied_cooling = self.data.magnet.solve.thermal.collar_cooling.which
2585
+ nr = 1
2586
+ for _, quad_data in self.md.geometries.collar.quadrants.items():
2587
+ for name, area in quad_data.areas.items():
2588
+ if re.match(r"^ar.h", name):
2589
+ if nr in nr_applied_cooling:
2590
+ tags.extend([int(k) for k in gmsh.model.getAdjacencies(2, area.surface)[1]])
2591
+ nr += 1
2592
+ self.md.geometries.collar.cooling_tags = tags
2593
+
2594
+
2595
+ # Update insulation line tags
1925
2596
  if run_type == 'TH' and not self.data.magnet.geometry.thermal.use_TSA:
1926
2597
  pass # todo
1927
2598
 
@@ -1984,5 +2655,31 @@ class Geometry:
1984
2655
  corner_max = [tol, y_coord + tol, tol]
1985
2656
  self.md.domains.groups_entities.symmetric_boundaries.y = _get_bnd_lines()
1986
2657
 
1987
- # self.occ.synchronize()
1988
- # self.gu.launch_interactive_GUI()
2658
+ def move_keypoints(self, keypoints, displacement, keypoint_names=None):
2659
+ if keypoint_names is None:
2660
+ keypoint_names = []
2661
+ for name, hole in self.geom.iron.hyper_areas.items():
2662
+ if not 'ch' in name: # ch -> collar hole
2663
+ continue
2664
+ line_names = hole.lines
2665
+ keypoint_names.append(list(set([getattr(self.geom.iron.hyper_lines[line], kp_name)
2666
+ for line in line_names for kp_name in ['kp1', 'kp2', 'kp3']])))
2667
+ if type(displacement) == list:
2668
+ list_displacement = displacement
2669
+ elif str(displacement) == "0":
2670
+ list_displacement = [[0.0, 0.0], [0.0, 0.0]]
2671
+ elif str(displacement) == "1":
2672
+ list_displacement = [[0.004, -0.015], [0.03, -0.025]]
2673
+ elif str(displacement) == "2":
2674
+ list_displacement = [[0.004, -0.015], [0.0, 0.0]]
2675
+ elif str(displacement) == "3":
2676
+ list_displacement = [[-0.035, 0.045], [-0.004, -0.0015]]
2677
+ else:
2678
+ raise ValueError("displacement_type not recognized")
2679
+ for i, hole in enumerate(keypoint_names):
2680
+ for name in hole:
2681
+ if name is None:
2682
+ continue
2683
+ keypoints[name].x += list_displacement[i][0]
2684
+ keypoints[name].y += list_displacement[i][1]
2685
+ return keypoints