fiqus 2025.2.0__py3-none-any.whl → 2025.10.0__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 (54) hide show
  1. fiqus/MainFiQuS.py +4 -9
  2. fiqus/data/DataConductor.py +350 -301
  3. fiqus/data/DataFiQuS.py +42 -115
  4. fiqus/data/DataFiQuSCCT.py +150 -150
  5. fiqus/data/DataFiQuSConductor.py +97 -84
  6. fiqus/data/DataFiQuSConductorAC_Strand.py +701 -565
  7. fiqus/data/DataModelCommon.py +439 -0
  8. fiqus/data/DataMultipole.py +0 -13
  9. fiqus/data/DataRoxieParser.py +7 -0
  10. fiqus/data/DataWindingsCCT.py +37 -37
  11. fiqus/data/RegionsModelFiQuS.py +61 -104
  12. fiqus/geom_generators/GeometryCCT.py +904 -905
  13. fiqus/geom_generators/GeometryConductorAC_Strand.py +1863 -1391
  14. fiqus/geom_generators/GeometryMultipole.py +5 -4
  15. fiqus/geom_generators/GeometryPancake3D.py +1 -1
  16. fiqus/getdp_runners/RunGetdpCCT.py +13 -4
  17. fiqus/getdp_runners/RunGetdpConductorAC_Strand.py +341 -201
  18. fiqus/getdp_runners/RunGetdpPancake3D.py +2 -2
  19. fiqus/mains/MainConductorAC_Strand.py +141 -133
  20. fiqus/mains/MainMultipole.py +6 -5
  21. fiqus/mains/MainPancake3D.py +3 -4
  22. fiqus/mesh_generators/MeshCCT.py +209 -209
  23. fiqus/mesh_generators/MeshConductorAC_Strand.py +709 -656
  24. fiqus/mesh_generators/MeshMultipole.py +43 -46
  25. fiqus/parsers/ParserDAT.py +16 -16
  26. fiqus/parsers/ParserGetDPOnSection.py +212 -212
  27. fiqus/parsers/ParserGetDPTimeTable.py +134 -134
  28. fiqus/parsers/ParserMSH.py +53 -53
  29. fiqus/parsers/ParserPOS.py +214 -214
  30. fiqus/parsers/ParserRES.py +142 -142
  31. fiqus/plotters/PlotPythonCCT.py +133 -133
  32. fiqus/plotters/PlotPythonConductorAC.py +1079 -855
  33. fiqus/plotters/PlotPythonMultipole.py +18 -18
  34. fiqus/post_processors/PostProcessCCT.py +444 -440
  35. fiqus/post_processors/PostProcessConductorAC.py +997 -49
  36. fiqus/post_processors/PostProcessMultipole.py +19 -19
  37. fiqus/pre_processors/PreProcessCCT.py +175 -175
  38. fiqus/pro_material_functions/ironBHcurves.pro +246 -246
  39. fiqus/pro_templates/combined/CCT_template.pro +275 -274
  40. fiqus/pro_templates/combined/ConductorAC_template.pro +1474 -1025
  41. fiqus/pro_templates/combined/Multipole_template.pro +5 -5
  42. fiqus/utils/Utils.py +12 -7
  43. {fiqus-2025.2.0.dist-info → fiqus-2025.10.0.dist-info}/METADATA +65 -63
  44. fiqus-2025.10.0.dist-info/RECORD +86 -0
  45. {fiqus-2025.2.0.dist-info → fiqus-2025.10.0.dist-info}/WHEEL +1 -1
  46. tests/test_geometry_generators.py +4 -0
  47. tests/test_mesh_generators.py +5 -0
  48. tests/test_solvers.py +41 -4
  49. tests/utils/fiqus_test_classes.py +15 -6
  50. tests/utils/generate_reference_files_ConductorAC.py +57 -57
  51. tests/utils/helpers.py +97 -97
  52. fiqus-2025.2.0.dist-info/RECORD +0 -85
  53. {fiqus-2025.2.0.dist-info → fiqus-2025.10.0.dist-info}/LICENSE.txt +0 -0
  54. {fiqus-2025.2.0.dist-info → fiqus-2025.10.0.dist-info}/top_level.txt +0 -0
@@ -1,905 +1,904 @@
1
- import os
2
- import copy
3
- import math
4
- import collections
5
- import logging
6
-
7
- import json
8
- import timeit
9
- import numpy as np
10
- import gmsh
11
-
12
- from fiqus.data import RegionsModelFiQuS as Reg_Mod_FiQ
13
- from fiqus.data.DataWindingsCCT import WindingsInformation # for winding information
14
- from fiqus.utils.Utils import FilesAndFolders as uff
15
- from fiqus.utils.Utils import GmshUtils
16
-
17
- logger = logging.getLogger(__name__)
18
- class Hexahedrons:
19
-
20
- def __init__(self):
21
- """
22
- Points generator for CCT windings to give hexahedron volumes for hexahedron meshing in Gmsh.
23
- """
24
- self.vertices_to_surf = [[0, 4, 7, 3], [1, 5, 6, 2], [1, 5, 4, 0], [2, 6, 7, 3], [0, 1, 2, 3], [4, 5, 6, 7]]
25
- # Node ordering following Gmsh for hexahedron https://gmsh.info/doc/texinfo/gmsh.html#Legacy-formats --> node ordering
26
- # above _vertices_to_surf is only used for manipulation of the hexahedra points on the data model level. Gmsh uses points connections and line connections for creating surfaces
27
-
28
- self.hexes = {}
29
- self._corner_points = {}
30
- self._sur_names = ['x-', 'x+', 'y-', 'y+', 'z-', 'z+']
31
- self._op_sign = {'+': '-', '-': '+'}
32
- self._sign = {1: '+', -1: '-'}
33
-
34
- def _add_corner_points(self, corner_num, x, y, z, ct):
35
- self._corner_points[corner_num] = {'x': x, 'y': y, 'z': z, 'ct': ct} # ct is channel turn
36
-
37
- def _generate_hexes(self):
38
- for h in range(len(self._corner_points[0]['x'])):
39
- self.hexes[h] = {}
40
- for p_num, p_coor in self._corner_points.items():
41
- self.hexes[h][p_num] = {}
42
- for coor in ['x', 'y', 'z']:
43
- self.hexes[h][p_num][coor] = p_coor[coor][h]
44
- self.hexes[h]['ct'] = p_coor['ct'][h]
45
- return p_coor['ct'][h]
46
-
47
- def _add_elem_to_elem(self, start_elem, dir_str, dist):
48
- """
49
- :param start_elem: idx of start element
50
- :param dir_str: direction of adding element, options are: 'x-', 'x+', 'y-', 'y+', 'z-', 'z+'
51
- :param dist: length of new element in the direction specified above [m]
52
- :return: index of created element
53
- """
54
- op_dir = dir_str[0] + self._op_sign[dir_str[1]]
55
- new_elem = start_elem + math.copysign(1, start_elem)
56
- self.hexes[new_elem] = {}
57
- source_points = self.vertices_to_surf[self._sur_names.index(dir_str)]
58
- destination_points = self.vertices_to_surf[self._sur_names.index(op_dir)]
59
- for d_p, s_p in zip(destination_points + source_points, source_points + source_points):
60
- self.hexes[new_elem][d_p] = copy.deepcopy(self.hexes[start_elem][s_p])
61
- for s_p in source_points: # modif_points:
62
- self.hexes[new_elem][s_p][dir_str[0]] = self.hexes[new_elem][s_p][dir_str[0]] + dist
63
- self.hexes[new_elem]['ct'] = self.hexes[start_elem]['ct']
64
- return new_elem
65
-
66
- def _reorder_and_renumber_hexes(self):
67
- self.hexes = collections.OrderedDict(sorted(self.hexes.items()))
68
- first_hex_num = list(self.hexes.keys())[0]
69
- temp_dict = {}
70
- for h_num, h_dict in self.hexes.items():
71
- temp_dict[h_num - first_hex_num + 1] = h_dict
72
- self.hexes = temp_dict
73
- for h_num, h_dict in self.hexes.items():
74
- turn = h_dict.pop('ct', None)
75
- self.hexes[h_num] = collections.OrderedDict(sorted(h_dict.items()))
76
- self.hexes[h_num]['ct'] = turn
77
-
78
-
79
- class Winding(Hexahedrons):
80
-
81
- def __init__(self, cctdm, layer, post_proc=False):
82
-
83
- super(Winding, self).__init__()
84
- self.cctdm = cctdm
85
- self.layer = layer
86
- self.post_proc = post_proc
87
- self.z_corr = 0
88
- self._populate_corners()
89
- self.z_corr = self._center_z_and_offset()
90
- self._generate_hexes()
91
-
92
- def _populate_corners(self):
93
- r"""
94
- Generates hexahedron with 8 corners numbered 0-7. The size of hexahedron is taken from groove size of cct as specified in yaml. The length is from number of elements per turn.
95
- y (v)
96
- 3----------2
97
- |\ ^ |\
98
- | \ | | \
99
- | \ | | \
100
- | 7------+---6
101
- | | +-- |-- | -> x (u)
102
- 0---+---\--1 |
103
- \ | \ \ |
104
- \ | \ \ |
105
- \| z(w)\|
106
- 4----------5
107
-
108
- :return: none
109
- """
110
- self._sign_of_rot = math.copysign(1, self.cctdm.geometry.windings.alphas[self.layer])
111
- if self._sign_of_rot > 0:
112
- corner_seq = {
113
- 0: {'w_s': -1, 'h_s': -1, 'far': False}, # 'blcf'
114
- 1: {'w_s': -1, 'h_s': 1, 'far': False}, # 'brcf'
115
- 2: {'w_s': 1, 'h_s': 1, 'far': False}, # 'trcf'
116
- 3: {'w_s': 1, 'h_s': -1, 'far': False}, # 'tlcf'
117
- 4: {'w_s': -1, 'h_s': -1, 'far': True}, # bottom left corner close #'blcc'
118
- 5: {'w_s': -1, 'h_s': 1, 'far': True}, # bottom right corner close #'brcc'
119
- 6: {'w_s': 1, 'h_s': 1, 'far': True}, # top right corner close #'trcc'
120
- 7: {'w_s': 1, 'h_s': -1, 'far': True}, # top right corner close #'tlcc'
121
- }
122
- else:
123
- corner_seq = {
124
- 0: {'w_s': 1, 'h_s': 1, 'far': False}, # 'trcf'
125
- 1: {'w_s': 1, 'h_s': -1, 'far': False}, # 'brcf'
126
- 2: {'w_s': -1, 'h_s': -1, 'far': False}, # 'blcf'
127
- 3: {'w_s': -1, 'h_s': 1, 'far': False}, # 'tlcf'
128
- 4: {'w_s': 1, 'h_s': 1, 'far': True}, # top right corner close #'trcc'
129
- 5: {'w_s': 1, 'h_s': -1, 'far': True}, # bottom right corner close #'brcc'
130
- 6: {'w_s': -1, 'h_s': -1, 'far': True}, # bottom left corner close #'blcc'
131
- 7: {'w_s': -1, 'h_s': 1, 'far': True}, # top left corner close #'tlcc'
132
- }
133
- for corner_num, corner_dict in corner_seq.items():
134
- self._add_corner_points(corner_num, *self.calc_turns_corner_coords(corner_dict['w_s'], corner_dict['h_s'], far=corner_dict['far']))
135
-
136
- def calc_turns_corner_coords(self, w_s=1, h_s=1, far=False):
137
- """
138
- Based on https://doi.org/10.1016/j.cryogenics.2020.103041
139
- :param h_s: wire height sign
140
- :param w_s: wire width sign
141
- :param far: for far surface set to True, for close surface set to False. This refers to walls of hexagon.
142
- :return:
143
- """
144
- r_wm = self.cctdm.geometry.windings.r_wms[self.layer] # radius of winding layer, in the middle of the conductor groove [m]
145
- w_i = self.cctdm.geometry.windings.lps[self.layer] # layer pitch [m]
146
- n_turns = self.cctdm.geometry.windings.n_turnss[self.layer] # number of turns [-]
147
- nept = self.cctdm.geometry.windings.ndpts[self.layer] - 1 # number of elements per turn [-]
148
- alpha = self.cctdm.geometry.windings.alphas[self.layer] * np.pi / 180 # inclination of the winding [deg]
149
- theta_0 = 0 # offset start at theta_0, i.e. angular position of the beginning of the 1st turn[deg]
150
- z_0 = 0 # offset start at z_0, i.e. axial position of the beginning of the 1st turn [m]
151
- hh = 0.5 * h_s * self.cctdm.geometry.windings.wwhs[self.layer] # half of h (groove height)
152
- hw = 0.5 * w_s * self.cctdm.geometry.windings.wwws[self.layer] # half of w (groove width)
153
- tot_angle = 2 * np.pi * (n_turns + 1 / nept)
154
- tot_points = int(nept * n_turns) + 1
155
- if alpha > 0:
156
- theta = np.linspace(0, tot_angle, tot_points, endpoint=False)
157
- else:
158
- theta = np.linspace(0 + np.pi, tot_angle + np.pi, tot_points, endpoint=False) # this rotates terminals for opposite inclination to be on the opposite side
159
- if far:
160
- theta = theta[1:] # give all points but the first one, so the far surface can be created
161
- else:
162
- theta = theta[:-1] # give all the points but the last one so a near surface can be created
163
- if theta.size < 2:
164
- raise ValueError(f'Combination of number of division points per turn (ndpts) and number of turns (n_turnss) must result in at least 2 Hexahedrons, but inputs result in only {theta.size}!')
165
- C = r_wm * np.arctan(alpha) * np.cos(theta - theta_0) + w_i / (2 * np.pi)
166
- D = np.sqrt(r_wm ** 2 + np.square(C))
167
- # -- x, y, z points coordinates
168
- xs = (r_wm + hh) * np.cos(theta) - np.sin(theta) * hw * C / D
169
- ys = (r_wm + hh) * np.sin(theta) + np.cos(theta) * hw * C / D
170
- zs = r_wm * np.sin(theta - theta_0) / np.tan(alpha) + w_i * (theta - theta_0) / (2 * np.pi) + z_0 - hw * r_wm / D # original formula
171
- if alpha > 0:
172
- channel_turn = np.ceil((theta - 2 * np.pi / (nept + 1)) / (2 * np.pi))
173
- # channel_turn[0] = channel_turn[1] # make the hex at theta 0 to belong to the first turn.
174
- else:
175
- channel_turn = np.ceil((theta - np.pi - 2 * np.pi / (nept + 1)) / (2 * np.pi))
176
- channel_turn[0] = channel_turn[1] # make the hex at theta 0 to belong to the first turn.
177
- return xs.tolist(), ys.tolist(), zs.tolist(), list(map(int, channel_turn))
178
-
179
- def _center_z_and_offset(self, offset=0):
180
- z_mins = []
181
- z_maxs = []
182
- for c_val in self._corner_points.values():
183
- z_mins.append(np.min(c_val['z']))
184
- z_maxs.append(np.max(c_val['z']))
185
- z_min_wind = np.min(np.array(z_mins))
186
- z_max_wind = np.max(np.array(z_maxs))
187
- z_centre = (z_min_wind + z_max_wind) / 2
188
- if not self.post_proc:
189
- for c_key, c_val in self._corner_points.items():
190
- self._corner_points[c_key]['z'] = (c_val['z'] - z_centre + offset).tolist()
191
- z_mins = []
192
- z_maxs = []
193
- for c_val in self._corner_points.values():
194
- z_mins.append(np.min(c_val['z']))
195
- z_maxs.append(np.max(c_val['z']))
196
- self.z_min_winding_layer = np.min(np.array(z_mins))
197
- self.z_max_winding_layer = np.max(np.array(z_maxs))
198
- return z_centre + offset
199
-
200
-
201
- class FQPL(Hexahedrons):
202
- def __init__(self, cctdm, layer):
203
- """
204
- FQPL hex generator
205
- :param cctdm: cct data model object parsed from yaml input file
206
- :param layer: loop number, integer
207
- """
208
- super(FQPL, self).__init__()
209
- self.cctdm = cctdm
210
- self.layer = layer
211
- self._calc_z_unit_len()
212
- self._populate_corners()
213
- self._generate_hexes()
214
- self._generate_fqpl()
215
- self._rotate_fqpl()
216
- self._reorder_and_renumber_hexes()
217
- self._clean_attr()
218
-
219
- def _calc_z_unit_len(self):
220
- if self.cctdm.geometry.fqpls.z_starts[self.layer] == 'z_min':
221
- self.near = self.cctdm.geometry.air.z_min
222
- self.z_unit_len = (self.cctdm.geometry.fqpls.z_ends[self.layer] - self.near - self.cctdm.geometry.fqpls.fwws[self.layer]) / self.cctdm.geometry.fqpls.fndpls[self.layer]
223
-
224
- elif self.cctdm.geometry.fqpls.z_starts[self.layer] == 'z_max':
225
- self.near = self.cctdm.geometry.air.z_max
226
- self.z_unit_len = (self.near - self.cctdm.geometry.fqpls.z_ends[self.layer] - self.cctdm.geometry.fqpls.fwws[self.layer]) / self.cctdm.geometry.fqpls.fndpls[self.layer]
227
- else:
228
- raise ValueError(f'fqpl.z_starts parameter must be a string equal to z_min or z_max, but {self.cctdm.geometry.fqpls.z_starts[self.layer]} was given!')
229
-
230
- self.extrusion_sign = math.copysign(1, self.cctdm.geometry.fqpls.z_ends[self.layer] - self.near)
231
-
232
- def _calc_first_corner_coords(self, w_s, h_s, far, offset):
233
- x = [self.cctdm.geometry.fqpls.r_ins[self.layer] + self.cctdm.geometry.fqpls.fwhs[self.layer] * h_s] # not rotated coordinate
234
- y = [self.cctdm.geometry.fqpls.fwws[self.layer] * w_s] # not rotated coordinate
235
- if far:
236
- z = [self.near - self.z_unit_len + offset]
237
- else:
238
- z = [self.near + offset]
239
- return x, y, z, [1]
240
-
241
- def _populate_corners(self):
242
- r"""
243
- Generates hexahedron with 8 corners numbered 0-7. The size of hexahedron is taken from groove size of cct as specified in yaml. The length is from number of elements per turn.
244
- y (v)
245
- 3----------2
246
- |\ ^ |\
247
- | \ | | \
248
- | \ | | \
249
- | 7------+---6
250
- | | +-- |-- | -> x (u)
251
- 0---+---\--1 |
252
- \ | \ \ |
253
- \ | \ \ |
254
- \| z(w)\|
255
- 4----------5
256
-
257
- :return: none
258
- """
259
- if self.extrusion_sign > 0:
260
- offset = self.z_unit_len
261
- corner_seq = {
262
- 0: {'w_s': -0.5, 'h_s': 0, 'far': True}, # 'blcf'
263
- 1: {'w_s': -0.5, 'h_s': 1, 'far': True}, # 'tlcf'
264
- 2: {'w_s': 0.5, 'h_s': 1, 'far': True}, # 'trcf'
265
- 3: {'w_s': 0.5, 'h_s': 0, 'far': True}, # 'brcf'
266
- 4: {'w_s': -0.5, 'h_s': 0, 'far': False}, # bottom left corner close #'blcc'
267
- 5: {'w_s': -0.5, 'h_s': 1, 'far': False}, # top left corner close #'tlcc'
268
- 6: {'w_s': 0.5, 'h_s': 1, 'far': False}, # top right corner close #'trcc'
269
- 7: {'w_s': 0.5, 'h_s': 0, 'far': False}, # bottom right corner close #'brcc'
270
- }
271
- else:
272
- offset = 0
273
- corner_seq = {
274
- 0: {'w_s': -0.5, 'h_s': 0, 'far': False}, # 'blcf'
275
- 1: {'w_s': -0.5, 'h_s': 1, 'far': False}, # 'tlcf'
276
- 2: {'w_s': 0.5, 'h_s': 1, 'far': False}, # 'trcf'
277
- 3: {'w_s': 0.5, 'h_s': 0, 'far': False}, # 'brcf'
278
- 4: {'w_s': -0.5, 'h_s': 0, 'far': True}, # bottom left corner close #'blcc'
279
- 5: {'w_s': -0.5, 'h_s': 1, 'far': True}, # top left corner close #'tlcc'
280
- 6: {'w_s': 0.5, 'h_s': 1, 'far': True}, # top right corner close #'trcc'
281
- 7: {'w_s': 0.5, 'h_s': 0, 'far': True}, # bottom right corner close #'brcc'
282
- }
283
- for corner_num, corner_dict in corner_seq.items():
284
- self._add_corner_points(corner_num, *self._calc_first_corner_coords(corner_dict['w_s'], corner_dict['h_s'], corner_dict['far'], offset))
285
-
286
- def _get_ref_point(self, elem_number, surf_n_dir_str, coord, dist):
287
- """
288
- Gets center point for a surface 'pointing' in the direction surf_n_dir_str of hex with elem_number. It then moves that point into along coordinate coord and by distance dist
289
- :param elem_number: hex number to take the surface from
290
- :param surf_n_dir_str: 'direction of surface' (kind of normal), options are: 'x-', 'x+', 'y-', 'y+', 'z-', 'z+'
291
- :param coord: direction to more the point in, options are: 'x', 'y', 'z'
292
- :param dist: distance to move the point, this is positive or negative float
293
- :return:
294
- """
295
- source_points = self.vertices_to_surf[self._sur_names.index(surf_n_dir_str)] # get the indexes 4 points for the surface points in direction surf_n_dir_str
296
- points_coor = [self.hexes[elem_number][point] for point in source_points] # get the points, they contain coordinates
297
- xs = []
298
- ys = []
299
- zs = []
300
- for point in points_coor: # get coordinates into list for each axis
301
- for cor_list, coor in zip([xs, ys, zs], ['x', 'y', 'z']):
302
- cor_list.append(point[coor])
303
- p = {'x': np.mean(xs), 'y': np.mean(ys), 'z': np.mean(zs)} # get averages of each axis coordinate, giving the center of the surface
304
- p[coord] = p[coord] + dist
305
- return p
306
-
307
- def _add_elem_with_rot(self, elem_number, surf_n_dir_str, add_ax, angle, ref_p_coor):
308
- """
309
- :param elem_number: idx of start element
310
- :param surf_n_dir_str: direction of adding element, options are: 'x-', 'x+', 'y-', 'y+', 'z-', 'z+'
311
- :param ref_p_coor: length of new element in the direction specified above [m]
312
- :return: index of created element
313
- """
314
- mod_coor = [surf_n_dir_str[0], add_ax]
315
- # mod_coor.remove(surf_n_dir_str[0]) # get the other two coordinates that arenet the direction of the surface to take
316
-
317
- angle_rad = np.deg2rad(angle)
318
- op_dir = surf_n_dir_str[0] + self._op_sign[surf_n_dir_str[1]]
319
- new_elem = elem_number + math.copysign(1, elem_number)
320
- self.hexes[new_elem] = {}
321
- source_points = self.vertices_to_surf[self._sur_names.index(surf_n_dir_str)]
322
- destination_points = self.vertices_to_surf[self._sur_names.index(op_dir)]
323
- for d_p, s_p in zip(destination_points + source_points, source_points + source_points):
324
- self.hexes[new_elem][d_p] = copy.deepcopy(self.hexes[elem_number][s_p])
325
- for s_p in source_points: # modif_points:
326
- v1 = self.hexes[new_elem][s_p][mod_coor[0]]
327
- v2 = self.hexes[new_elem][s_p][mod_coor[1]]
328
- v1_ofset = ref_p_coor[mod_coor[0]]
329
- v2_ofset = ref_p_coor[mod_coor[1]]
330
- v1 = v1 - v1_ofset
331
- v2 = v2 - v2_ofset
332
- v1_new = np.cos(angle_rad) * v1 - np.sin(angle_rad) * v2
333
- v2_new = np.sin(angle_rad) * v1 + np.cos(angle_rad) * v2
334
- v1 = v1_new + v1_ofset
335
- v2 = v2_new + v2_ofset
336
- self.hexes[new_elem][s_p][mod_coor[0]] = v1
337
- self.hexes[new_elem][s_p][mod_coor[1]] = v2
338
- self.hexes[new_elem]['ct'] = self.hexes[elem_number]['ct']
339
- return new_elem
340
-
341
- def _generate_fqpl(self):
342
- new_elem = self._add_elem_to_elem(0, 'z+', self.extrusion_sign * self.z_unit_len)
343
- for _ in range(self.cctdm.geometry.fqpls.fndpls[self.layer] - 2):
344
- new_elem = self._add_elem_to_elem(new_elem, 'z+', self.extrusion_sign * self.z_unit_len)
345
- coord = 'x'
346
- ref_point = self._get_ref_point(new_elem, 'z+', coord, self.cctdm.geometry.fqpls.r_bs[self.layer]) # centre of rotation pint.
347
- n_seg = 10
348
- angle = 180 / n_seg
349
- for _ in range(n_seg):
350
- new_elem = self._add_elem_with_rot(new_elem, 'z+', coord, self.extrusion_sign * angle, ref_point)
351
- for _ in range(self.cctdm.geometry.fqpls.fndpls[self.layer]):
352
- new_elem = self._add_elem_to_elem(new_elem, 'z+', -self.extrusion_sign * self.z_unit_len)
353
-
354
- def _rotate_fqpl(self):
355
- for h_num, h_dict in self.hexes.items():
356
- channel_turn = h_dict.pop('ct', None)
357
- for p_num, p_dict in h_dict.items():
358
- xx = p_dict['x']
359
- yy = p_dict['y']
360
- x = xx * np.cos(np.deg2rad(self.cctdm.geometry.fqpls.thetas[self.layer])) - yy * np.sin(np.deg2rad(self.cctdm.geometry.fqpls.thetas[self.layer])) # rotated coordinate
361
- y = xx * np.sin(np.deg2rad(self.cctdm.geometry.fqpls.thetas[self.layer])) + yy * np.cos(np.deg2rad(self.cctdm.geometry.fqpls.thetas[self.layer])) # rotated coordinate
362
- self.hexes[h_num][p_num]['x'] = x
363
- self.hexes[h_num][p_num]['y'] = y
364
- self.hexes[h_num]['ct'] = channel_turn
365
-
366
- def _clean_attr(self):
367
- del self.cctdm
368
- del self.layer
369
- del self.near
370
- del self.z_unit_len
371
-
372
-
373
- class Generate_BREPs:
374
- def __init__(self, fdm, verbose=True):
375
- """
376
- Class to generate (build) Windings and Formers (WF) of a canted cosine theta (CCT) magnet and save them to brep files
377
- :param fdm: fiqus data model
378
- :param verbose: If True more information is printed in python console.
379
- """
380
- self.cctdm = fdm.magnet
381
- self.model_folder = os.path.join(os.getcwd())
382
- self.magnet_name = fdm.general.magnet_name
383
-
384
- self.verbose = verbose
385
- self.cctwi = WindingsInformation()
386
- self.cctwi.magnet_name = self.magnet_name
387
- self.transl_dict = {'vol': '', 'surf': '_bd', 'surf_in': '_in', 'surf_out': '_out', 'cochain': '_cut'}
388
- self.gu = GmshUtils(self.model_folder, self.verbose)
389
- self.gu.initialize()
390
- self.formers = []
391
- self.air = []
392
- gmsh.option.setString('Geometry.OCCTargetUnit', 'M')
393
-
394
- def generate_windings_or_fqpls(self, type_str):
395
- """
396
- Generates windings brep files, as many as number of entries in the yaml file for windings.
397
- :param type_str: What to generate, options are: 'windings' or 'fqpls'
398
- :return: None, saves brep to disk
399
- """
400
- if self.verbose:
401
- print(f'Generating {type_str} started')
402
- start_time = timeit.default_timer()
403
-
404
- def generate_geom(worf_in, names_in):
405
- worf_tags_dict_in = {}
406
- for worf_num, worf_in in enumerate(worf_in):
407
- worf_tags_dict_in[worf_num] = self.generate_hex_geom(worf_in)
408
- gmsh.model.occ.synchronize()
409
- gmsh.write(os.path.join(self.model_folder, f'{names_in[worf_num]}.brep'))
410
- gmsh.clear()
411
- return worf_tags_dict_in
412
-
413
- if self.cctdm.geometry.windings.names and type_str == 'windings':
414
- worf = [Winding(self.cctdm, i) for i, _ in enumerate(self.cctdm.geometry.windings.names)] # worf = winding or fqpl
415
- names = [name for name in self.cctdm.geometry.windings.names]
416
- for w in worf:
417
- if self.cctdm.geometry.air.z_min > w.z_min_winding_layer or w.z_max_winding_layer > self.cctdm.geometry.air.z_max:
418
- raise ValueError(f'{self.cctdm.geometry.air.name} region dimensions are too s mall to fit the windings turns. Please extend z_min and/or z_max')
419
- else:
420
- pass
421
- worf_tags_dict = generate_geom(worf, names)
422
-
423
- if self.cctdm.geometry.fqpls.names and type_str == 'fqpls':
424
- worf = [FQPL(self.cctdm, i) for i, _ in enumerate(self.cctdm.geometry.fqpls.names)] # worf = winding or fqpl
425
- names = [name for name in self.cctdm.geometry.fqpls.names]
426
- worf_tags_dict = generate_geom(worf, names)
427
-
428
- w_z_maxs = []
429
- w_z_mins = []
430
- if type_str == 'windings':
431
- for winding in worf:
432
- w_z_maxs.append(winding.z_max_winding_layer)
433
- w_z_mins.append(winding.z_min_winding_layer)
434
- self.cctwi.windings_avg_length = float(np.mean(w_z_maxs) - np.mean(w_z_mins))
435
- self.cctwi.windings.names = names
436
- self.cctwi.windings.t_in.vol_st = []
437
- self.cctwi.windings.t_in.surf_st = []
438
- self.cctwi.windings.t_out.vol_st = []
439
- self.cctwi.windings.t_out.surf_st = []
440
- self.cctwi.windings.t_in.vol_et = []
441
- self.cctwi.windings.t_in.surf_et = []
442
- self.cctwi.windings.t_out.vol_et = []
443
- self.cctwi.windings.t_out.surf_et = []
444
- self.cctwi.windings.t_in.lc_et = []
445
- self.cctwi.windings.t_out.lc_et = []
446
- self.cctwi.windings.t_in.ndpterms = self.cctdm.geometry.windings.ndpt_ins
447
- self.cctwi.windings.t_out.ndpterms = self.cctdm.geometry.windings.ndpt_outs
448
- self.cctwi.windings.t_in.z_air = self.cctdm.geometry.air.z_min
449
- self.cctwi.windings.t_out.z_air = self.cctdm.geometry.air.z_max
450
- for winding_num, winding_dict in worf_tags_dict.items():
451
- first_vol = list(winding_dict.keys())[0]
452
- last_vol = list(winding_dict.keys())[-1]
453
- self.cctwi.windings.t_in.vol_st.append(winding_dict[first_vol]['v_t'])
454
- self.cctwi.windings.t_in.surf_st.append(winding_dict[first_vol]['sf_ts'][0])
455
- self.cctwi.windings.t_out.vol_st.append(winding_dict[last_vol]['v_t'])
456
- self.cctwi.windings.t_out.surf_st.append(winding_dict[last_vol]['sf_ts'][2])
457
- self.cctwi.windings.t_in.vol_et.append(winding_dict[first_vol]['v_t'])
458
- self.cctwi.windings.t_in.surf_et.append(winding_dict[first_vol]['sf_ts'][0])
459
- self.cctwi.windings.t_out.vol_et.append(winding_dict[last_vol]['v_t'])
460
- self.cctwi.windings.t_out.surf_et.append(winding_dict[last_vol]['sf_ts'][2])
461
-
462
- lc_st_neg_alpha = [[1, 0], [0, 3], [3, 2], [2, 1], [6, 4], [4, 5], [5, 7], [7, 6], [1, 6], [0, 4], [3, 5], [2, 7]]
463
- lc_st_pos_alpha = [[3, 1], [1, 0], [0, 2], [2, 3], [7, 4], [4, 5], [5, 6], [6, 7], [3, 7], [1, 4], [0, 5], [2, 6]]
464
- lc_et_neg_alpha = [[5, 7], [7, 6], [6, 4], [4, 5], [1, 3], [3, 2], [2, 0], [0, 1], [5, 1], [7, 3], [6, 2], [4, 0]]
465
- lc_et_pos_alpha = [[5, 7], [7, 6], [6, 4], [4, 5], [1, 3], [3, 2], [2, 0], [0, 1], [5, 1], [7, 3], [6, 2], [4, 0]]
466
- # [[1, 2], [2, 3], [3, 4], [4, 1], [5, 6], [6, 7], [7, 8], [8, 5], [1, 5], [2, 6], [3, 7], [4, 8]]
467
- # [[2, 1], [1, 4], [4, 3], [3, 2], [6, 5], [5, 8], [8, 7], [7, 6], [2, 6], [1, 5], [4, 8], [3, 7]]
468
-
469
- lc_et_pos_corr = [[1, 0], [0, 3], [3, 2], [2, 1], [5, 4], [4, 7], [7, 6], [6, 5], [1, 5], [0, 4], [3, 7], [2, 6]]
470
-
471
- self.cctwi.windings.t_in.lc_st = []
472
- self.cctwi.windings.t_out.lc_st = []
473
- for alpha in self.cctdm.geometry.windings.alphas:
474
- if alpha > 0:
475
- self.cctwi.windings.t_in.lc_st.append(lc_st_pos_alpha)
476
- self.cctwi.windings.t_out.lc_st.append(lc_st_pos_alpha)
477
- self.cctwi.windings.t_in.lc_et.append(lc_et_pos_corr)
478
- self.cctwi.windings.t_out.lc_et.append(lc_et_pos_alpha)
479
- else:
480
- self.cctwi.windings.t_in.lc_st.append(lc_st_neg_alpha)
481
- self.cctwi.windings.t_out.lc_st.append(lc_st_neg_alpha)
482
- self.cctwi.windings.t_in.lc_et.append(lc_et_neg_alpha)
483
- self.cctwi.windings.t_out.lc_et.append(lc_et_pos_corr)
484
-
485
- if self.verbose:
486
- print(f'Generating {type_str} took {timeit.default_timer() - start_time:.2f} s')
487
-
488
- def save_volume_info(self):
489
- if self.cctdm.geometry.fqpls.names:
490
- fqpls = self.cctdm.geometry.fqpls.names
491
- else:
492
- fqpls = []
493
-
494
- self.cctwi.w_names = self.cctdm.geometry.windings.names
495
- self.cctwi.f_names = fqpls
496
- self.cctwi.formers = self.cctdm.geometry.formers.names
497
- self.cctwi.air = self.cctdm.geometry.air.name
498
-
499
- volume_info_file = os.path.join(self.model_folder, f'{self.cctwi.magnet_name}.wi')
500
- uff.write_data_to_yaml(volume_info_file, self.cctwi.dict())
501
-
502
- def generate_formers(self):
503
- if self.cctdm.geometry.formers.r_ins:
504
- if self.verbose:
505
- print('Generating Formers Started')
506
- start_time = timeit.default_timer()
507
-
508
- for f_i, _ in enumerate(self.cctdm.geometry.formers.r_ins):
509
- z = self.cctdm.geometry.formers.z_mins[f_i]
510
- dz = self.cctdm.geometry.formers.z_maxs[f_i] - self.cctdm.geometry.formers.z_mins[f_i]
511
- cylin_out = (3, gmsh.model.occ.addCylinder(0, 0, z, 0, 0, dz, self.cctdm.geometry.formers.r_outs[f_i], angle=2 * math.pi)) # add cylinder to align to existing bodies in the pro file
512
- cylin_in = (3, gmsh.model.occ.addCylinder(0, 0, z, 0, 0, dz, self.cctdm.geometry.formers.r_ins[f_i], angle=2 * math.pi)) # add another cylinder to subtract from the first one to make a tube
513
- cylinder = gmsh.model.occ.cut([cylin_out], [cylin_in], removeObject=True) # subtract cylinders to make a tube
514
- self.formers.append(cylinder[0][0]) # keep just the tag and append
515
- gmsh.model.occ.synchronize()
516
- gmsh.write(os.path.join(self.model_folder, f'{self.cctdm.geometry.formers.names[f_i]}.brep'))
517
- gmsh.clear()
518
- if self.verbose:
519
- print(f'Generating formers took {timeit.default_timer() - start_time:.2f} s')
520
-
521
- def generate_air(self):
522
- if self.cctdm.geometry.air.ar:
523
- if self.verbose:
524
- print('Generating air started')
525
- start_time = timeit.default_timer()
526
- if self.cctdm.geometry.air.sh_type == 'cylinder':
527
- self.air = [(3, gmsh.model.occ.addCylinder(0, 0, self.cctdm.geometry.air.z_min, 0, 0, self.cctdm.geometry.air.z_max - self.cctdm.geometry.air.z_min, self.cctdm.geometry.air.ar, angle=2 * math.pi))]
528
- elif self.cctdm.geometry.air.sh_type == 'cuboid':
529
- air_box_size = [-self.cctdm.geometry.air.ar / 2, -self.cctdm.geometry.air.ar / 2, self.cctdm.geometry.air.z_min, self.cctdm.geometry.air.ar, self.cctdm.geometry.air.ar,
530
- self.cctdm.geometry.air.z_max - self.cctdm.geometry.air.z_min] # list of box size with: x, y, z, dx, dy, dz
531
- self.air = [(3, gmsh.model.occ.addBox(*air_box_size))]
532
- else:
533
- raise ValueError(f'Shape type: {self.cctdm.geometry.air.sh_type} is not supported!')
534
- gmsh.model.occ.synchronize()
535
- gmsh.write(os.path.join(self.model_folder, f'{self.cctdm.geometry.air.name}.brep'))
536
- gmsh.clear()
537
- if self.verbose:
538
- print(f'Generating air took {timeit.default_timer() - start_time:.2f} s')
539
-
540
- def generate_regions_file(self):
541
- if self.verbose:
542
- print('Generating Regions File Started')
543
- start_time = timeit.default_timer()
544
- cctrm = Reg_Mod_FiQ.RegionsModel()
545
- vrt = 1000000 # volume region tag start
546
- srt = 2000000 # surface region tag start
547
- lrt = 3000000 # line region tag start
548
- # -------- powered ----------
549
- # volumes
550
- cctrm.powered['cct'] = Reg_Mod_FiQ.Powered()
551
- cctrm.powered['cct'].vol.names = [name + self.transl_dict['vol'] for name in self.cctdm.geometry.windings.names + self.cctdm.geometry.fqpls.names]
552
- cctrm.powered['cct'].vol.currents = self.cctdm.solve.windings.currents + self.cctdm.solve.fqpls.currents
553
- cctrm.powered['cct'].vol.sigmas = self.cctdm.solve.windings.sigmas + self.cctdm.solve.fqpls.sigmas
554
- cctrm.powered['cct'].vol.mu_rs = self.cctdm.solve.windings.mu_rs + self.cctdm.solve.fqpls.mu_rs
555
- reg = []
556
- for _ in self.cctdm.geometry.windings.names + self.cctdm.geometry.fqpls.names:
557
- reg.append(vrt)
558
- vrt += 1
559
- cctrm.powered['cct'].vol.numbers = reg
560
- # surfaces
561
- cctrm.powered['cct'].surf_in.names = [name + self.transl_dict['surf_in'] for name in self.cctdm.geometry.windings.names + self.cctdm.geometry.fqpls.names]
562
- cctrm.powered['cct'].surf_out.names = [name + self.transl_dict['surf_out'] for name in self.cctdm.geometry.windings.names + self.cctdm.geometry.fqpls.names]
563
- reg = []
564
- for _ in self.cctdm.geometry.windings.names + self.cctdm.geometry.fqpls.names:
565
- reg.append(srt)
566
- srt += 1
567
- cctrm.powered['cct'].surf_in.numbers = reg
568
- reg = []
569
- for _ in self.cctdm.geometry.windings.names + self.cctdm.geometry.fqpls.names:
570
- reg.append(srt)
571
- srt += 1
572
- cctrm.powered['cct'].surf_out.numbers = reg
573
-
574
- # -------- induced ----------
575
- # volumes
576
- cctrm.induced['cct'] = Reg_Mod_FiQ.Induced()
577
- cctrm.induced['cct'].vol.names = [name + self.transl_dict['vol'] for name in self.cctdm.geometry.formers.names]
578
- cctrm.induced['cct'].vol.sigmas = self.cctdm.solve.formers.sigmas
579
- cctrm.induced['cct'].vol.mu_rs = self.cctdm.solve.formers.mu_rs
580
- reg = []
581
- for _ in self.cctdm.geometry.formers.names:
582
- reg.append(vrt)
583
- vrt += 1
584
- cctrm.induced['cct'].vol.numbers = reg
585
-
586
- # -------- air ----------
587
- # volumes
588
- cctrm.air.vol.name = self.cctdm.geometry.air.name + self.transl_dict['vol']
589
- cctrm.air.vol.sigma = self.cctdm.solve.air.sigma
590
- cctrm.air.vol.mu_r = self.cctdm.solve.air.mu_r
591
- cctrm.air.vol.number = vrt
592
- vrt += 1
593
-
594
- # surface
595
- cctrm.air.surf.name = self.cctdm.geometry.air.name + self.transl_dict['surf']
596
- cctrm.air.surf.number = srt
597
- srt += 1
598
-
599
- # # center line
600
- # cctrm.air.line.name = 'Center_line'
601
- # cctrm.air.line.number = lrt
602
- # lrt += 1
603
- lrt = srt
604
-
605
- # --------- cuts -------
606
- # these need to be done at the end with the highest surface tags
607
- cctrm.powered['cct'].cochain.names = [name + self.transl_dict['cochain'] for name in self.cctdm.geometry.windings.names + self.cctdm.geometry.fqpls.names]
608
- reg = []
609
- for _ in self.cctdm.geometry.windings.names + self.cctdm.geometry.fqpls.names:
610
- reg.append(lrt)
611
- lrt += 1
612
- cctrm.powered['cct'].cochain.numbers = reg
613
- # induced cuts
614
- cctrm.induced['cct'].cochain.names = [name + self.transl_dict['cochain'] for name in self.cctdm.geometry.formers.names]
615
- reg = []
616
- for _ in self.cctdm.geometry.formers.names:
617
- reg.append(lrt)
618
- lrt += 1
619
- cctrm.induced['cct'].cochain.numbers = reg
620
-
621
- uff.write_data_to_yaml(os.path.join(self.model_folder, f'{self.cctwi.magnet_name}.regions'), cctrm.dict())
622
-
623
- if self.verbose:
624
- print(f'Generating Regions File Took {timeit.default_timer() - start_time:.2f} s')
625
-
626
- @staticmethod
627
- def generate_hex_geom(hexes_dict,
628
- cons_for_lines=[[0, 1], [1, 2], [2, 3], [3, 0], [4, 5], [5, 6], [6, 7], [7, 4], [0, 4], [1, 5], [2, 6], [3, 7]],
629
- vol_tag=-1):
630
- """
631
- Generates hexahedra volumes from data in hexes dict
632
- :param hexes_dict: dictionary of data generated by either Winding or FQPL class in Hexahedrons.py
633
- :param cons_for_lines: line connection defined as list of list in terms of point numbers to connect in the hexahedron
634
- :param vol_tag: volume tag to use for generated volume of hexahedron
635
- :return: dictionary with tags of created volumes, surface loops, surface fillings, closed loops, lines, points and channel turn (i.e. turn number of CCT winding).
636
- """
637
- tags_dict = {}
638
- for h_num, h_dict in hexes_dict.hexes.items():
639
- p_ts = [] # Point tags
640
- channel_turn = h_dict.pop('ct', None)
641
- for p_num, p_dict in h_dict.items():
642
- p_ts.append(gmsh.model.occ.addPoint(p_dict['x'], p_dict['y'], p_dict['z']))
643
-
644
- l_ts = [] # Line tags
645
- for p_c in cons_for_lines: # point connections to make a line
646
- l_ts.append(gmsh.model.occ.addLine(p_ts[p_c[0]], p_ts[p_c[1]]))
647
-
648
- cl_ts = [] # Curved Loops tags
649
- for l_c in [[0, 1, 2, 3], [4, 5, 6, 7], [3, 8, 7, 11], [1, 9, 5, 10], [0, 8, 4, 9], [2, 11, 6, 10]]: # line connections to make a curved loop
650
- cl_ts.append(gmsh.model.occ.addCurveLoop([l_ts[l_c[0]], l_ts[l_c[1]], l_ts[l_c[2]], l_ts[l_c[3]]]))
651
- sf_ts = [] # Surface Filling tags
652
- for cl_t in cl_ts:
653
- sf_ts.append(gmsh.model.occ.addSurfaceFilling(cl_t, degree=3, tol2d=0.000001, tol3d=0.00001, tolAng=0.001, tolCurv=0.05))
654
- sl_t = gmsh.model.occ.addSurfaceLoop(sf_ts, sewing=True)
655
- v_t = gmsh.model.occ.addVolume([sl_t], vol_tag)
656
- tags_dict[int(h_num)] = {'v_t': v_t, 'sl_t': sl_t, 'sf_ts': sf_ts, 'cl_ts': cl_ts, 'l_ts': l_ts, 'p_ts': p_ts, 'ct': channel_turn}
657
- return tags_dict
658
-
659
-
660
- class Prepare_BREPs:
661
- def __init__(self, fdm, verbose=True) -> object:
662
- """
663
- Class to preparing brep files by adding terminals.
664
- :param fdm: FiQuS data model
665
- :param verbose: If True more information is printed in python console.
666
- """
667
-
668
- self.cctdm = fdm.magnet
669
- self.model_folder = os.path.join(os.getcwd())
670
- self.magnet_name = fdm.general.magnet_name
671
-
672
- self.verbose = verbose
673
- self.winding_info_file = os.path.join(self.model_folder, f'{self.magnet_name}.wi')
674
- self.cctwi = uff.read_data_from_yaml(self.winding_info_file, WindingsInformation)
675
- self.gu = GmshUtils(self.model_folder, self.verbose)
676
- self.gu.initialize()
677
- self.model_file = os.path.join(self.model_folder, f'{self.magnet_name}.brep')
678
-
679
- @staticmethod
680
- def get_pts_for_sts(surface_tags):
681
- """
682
- Get point tags for surface tags
683
- :param surface_tags: list of surface tags to get point tags
684
- :return: list of point tags belonging to the surfaces in the list
685
- """
686
- vol_line_tags = []
687
- vol_line_tags_i = 0
688
- vol_line_tags_idx = []
689
- for surf_tag in surface_tags:
690
- line_tags_new = gmsh.model.getAdjacencies(2, surf_tag)[1]
691
- for line_tag in line_tags_new:
692
- vol_line_tags_i += 1
693
- if line_tag not in vol_line_tags:
694
- vol_line_tags.append(line_tag)
695
- vol_line_tags_idx.append(vol_line_tags_i)
696
- point_tags = []
697
- point_tags_i = 0
698
- point_tags_idx = []
699
- for line_tag in vol_line_tags:
700
- point_tags_new = gmsh.model.getAdjacencies(1, line_tag)[1]
701
- for point_tag in point_tags_new:
702
- point_tags_i += 1
703
- if point_tag not in point_tags:
704
- point_tags.append(point_tag)
705
- point_tags_idx.append(point_tags_i)
706
- return point_tags, vol_line_tags_idx, point_tags_idx
707
-
708
- def straighten_terminal(self, gui=False):
709
- """
710
- Extends winding geom_generators to the air region boundary by extending the geom_generators with 'terminals' geom_generators up to the air boundary surfaces
711
- By default this extends along the z axis and makes the final surface normal to the z axis.
712
- :return: None, saves breps with straighten terminals to disk
713
- """
714
-
715
- for i, name in enumerate(self.cctwi.windings.names):
716
- if self.verbose:
717
- print(f'Straightening terminals of {name} started')
718
- start_time = timeit.default_timer()
719
- gmsh.open(os.path.join(self.model_folder, f'{name}.brep'))
720
- for terminal in [self.cctwi.windings.t_in, self.cctwi.windings.t_out]:
721
- vol_tag = terminal.vol_st[i]
722
- vol_surf_tags = gmsh.model.getAdjacencies(3, vol_tag)[1]
723
- surf_tag = terminal.surf_st[i]
724
- if surf_tag not in vol_surf_tags:
725
- raise ValueError(f'Surface tag of {surf_tag} given for volume in of {vol_tag} does not belong to the volume!')
726
- # surf_point_tags = self.get_pts_for_sts([surf_tag]) # only 'powered' surface
727
- surf_point_tags, surf_line_tags_idx, surf_point_tags_idx = self.get_pts_for_sts([surf_tag]) # only 'powered' surface
728
- vol_point_tags, vol_line_tags_idx, vol_point_tags_idx = self.get_pts_for_sts(vol_surf_tags) # all tags of hex
729
- vol_point_dict = {}
730
- for p_tag in vol_point_tags:
731
- xmin, ymin, zmin, xmax, ymax, zmax = gmsh.model.occ.getBoundingBox(0, p_tag)
732
- vol_point_dict[p_tag] = {'x': (xmin + xmax) / 2, 'y': (ymin + ymax) / 2, 'z': (zmin + zmax) / 2}
733
- z = []
734
- y = []
735
- x = []
736
- for p_tag in surf_point_tags: # calculate average z position for the terminal surface
737
- xmin, ymin, zmin, xmax, ymax, zmax = gmsh.model.occ.getBoundingBox(0, p_tag)
738
- x.append((xmin + xmax) / 2)
739
- y.append((ymin + ymax) / 2)
740
- z.append((zmin + zmax) / 2)
741
- x_avg = np.mean(x)
742
- y_avg = np.mean(y)
743
- z_avg = np.mean(z)
744
- sign_x = [math.copysign(1, x_i - x_avg) for x_i in x]
745
- sign_y = [math.copysign(1, y_i - y_avg) for y_i in y]
746
- dist_x = []
747
- dist_y = []
748
- for j in range(len(x) - 1):
749
- dist_x.append(math.sqrt((x[j] - x[j + 1]) ** 2))
750
- dist_y.append(math.sqrt((y[j] - y[j + 1]) ** 2))
751
- eq_len = self.cctdm.geometry.windings.wwhs[i] / 2
752
- y_shift_sign = math.copysign(1, -terminal.z_air * self.cctdm.geometry.windings.alphas[i])
753
- new_x = [x_avg + s_x * eq_len for s_x in sign_x]
754
- new_y = [y_avg + s_y * eq_len + y_shift_sign * eq_len - y_shift_sign * dist_y[0] / 2 for s_y in sign_y]
755
- for p_tag, x_n, y_n in zip(surf_point_tags, new_x, new_y): # assign z_avg to only points on the terminal surface
756
- vol_point_dict[p_tag]['x'] = x_n
757
- vol_point_dict[p_tag]['y'] = y_n
758
- vol_point_dict[p_tag]['z'] = z_avg
759
- gmsh.model.occ.remove([(3, terminal.vol_st[i])], recursive=True)
760
- gmsh.model.occ.synchronize()
761
- hexes = Hexahedrons() # create hex class instance to be able to reuse existing code for generating hexes geom_generators
762
- hexes.hexes[0] = {} # only one winding layer is involved per iteration, so hardcoded 0 index
763
- for p_i, (_, p_dict) in enumerate(vol_point_dict.items()):
764
- hexes.hexes[0][p_i] = p_dict
765
- Generate_BREPs.generate_hex_geom(hexes, cons_for_lines=terminal.lc_st[i], vol_tag=terminal.vol_st[i])
766
- gmsh.model.occ.synchronize()
767
- gmsh.write(os.path.join(self.model_folder, f'{name}.brep'))
768
- if self.verbose:
769
- print(f'Straightening terminals of {name} took {timeit.default_timer() - start_time:.2f} s')
770
- # uff.write_data_to_yaml(self.winding_info_yaml_path, self.cctvi.dict())
771
- if gui:
772
- gmsh.model.occ.synchronize()
773
- self.gu.launch_interactive_GUI()
774
- else:
775
- gmsh.clear()
776
-
777
- def extend_terms(self, operation='extend', gui=False, ):
778
- self.cctwi = uff.read_data_from_yaml(self.winding_info_file, WindingsInformation) # this is repeated as the operation over the data for the case add, so an original data for extend operation needs to be loaded.
779
- if operation == 'add':
780
- for terminal in [self.cctwi.windings.t_in, self.cctwi.windings.t_out]:
781
- terminal.ndpterms = [1] * len(terminal.ndpterms)
782
- terminal.z_air = terminal.z_add
783
- elif operation == 'extend':
784
- pass
785
- file_in_postfix = ''
786
- file_out_postfix = ''
787
- for i, name in enumerate(self.cctwi.windings.names):
788
- if self.verbose:
789
- print(f'Extending terminals of {name} started')
790
- start_time = timeit.default_timer()
791
- gmsh.open(os.path.join(self.model_folder, f'{name}{file_in_postfix}.brep'))
792
- volumes = [vol[1] for vol in gmsh.model.getEntities(dim=3)]
793
- for oper, terminal in enumerate([self.cctwi.windings.t_in, self.cctwi.windings.t_out]):
794
- vol_tag = terminal.vol_et[i]
795
- vol_surf_tags = gmsh.model.getAdjacencies(3, vol_tag)[1]
796
- surf_tag = terminal.surf_et[i]
797
- if surf_tag not in vol_surf_tags:
798
- raise ValueError(f'Surface tag of {surf_tag} given for volume in of {vol_tag} does not belong to the volume!')
799
- surf_point_tags, surf_line_tags_idx, surf_point_tags_idx = self.get_pts_for_sts([surf_tag])
800
- surf_point_tags = sorted(surf_point_tags)
801
- vol_point_tags, vol_line_tags_idx, vol_point_tags_idx = self.get_pts_for_sts(vol_surf_tags)
802
- vol_point_tags = sorted(vol_point_tags)
803
- surf_point_tags_idx_pw = [i for i, e in enumerate(vol_point_tags) if e in set(surf_point_tags)] # powered
804
- surf_point_tags_idx_oth = [i for i, e in enumerate(vol_point_tags) if e not in set(surf_point_tags)] # other (opposite powered)
805
- vol_point_dict = {}
806
- for idx_pw, p_tag in zip(surf_point_tags_idx_pw, surf_point_tags):
807
- xmin, ymin, zmin, xmax, ymax, zmax = gmsh.model.occ.getBoundingBox(0, p_tag)
808
- vol_point_dict[idx_pw] = {'x': (xmin + xmax) / 2, 'y': (ymin + ymax) / 2, 'z': (zmin + zmax) / 2}
809
- z_ext = round((terminal.z_air - zmin) / terminal.ndpterms[i], 8)
810
- for idx_oth, p_tag in zip(surf_point_tags_idx_oth, surf_point_tags):
811
- xmin, ymin, zmin, xmax, ymax, zmax = gmsh.model.occ.getBoundingBox(0, p_tag)
812
- vol_point_dict[idx_oth] = {'x': (xmin + xmax) / 2, 'y': (ymin + ymax) / 2, 'z': (zmin + zmax) / 2 + z_ext}
813
- hexes = Hexahedrons() # create hex class instance to be able to reuse existing code for generating hexes geom_generators
814
- for h in range(terminal.ndpterms[i]):
815
- hexes.hexes[h] = {}
816
- for p_i, (_, p_dict) in enumerate(vol_point_dict.items()):
817
- hexes.hexes[h][p_i] = copy.deepcopy(p_dict)
818
- for p_t, p_dict in vol_point_dict.items():
819
- p_dict['z'] = p_dict['z'] + z_ext
820
- for idx_oth in range(4, 8):
821
- z_air = terminal.z_air
822
- hexes.hexes[h][idx_oth]['z'] = z_air
823
- tags_dict = Generate_BREPs.generate_hex_geom(hexes, cons_for_lines=terminal.lc_et[i], vol_tag=-1)
824
- new_volumes = []
825
- for _, hex_dict in tags_dict.items():
826
- new_volumes.append(hex_dict['v_t'])
827
- gmsh.model.occ.synchronize()
828
- if oper == 0: # this vol_in is needed for reordering volumes later
829
- vol_in = new_volumes
830
- renumbered_vols = []
831
- other_vols = list(set(volumes) - set(vol_in))
832
- max_tag = gmsh.model.occ.getMaxTag(3)
833
- for vol in other_vols:
834
- gmsh.model.setTag(3, vol, vol + max_tag)
835
- renumbered_vols.append(vol + max_tag)
836
- for n_tag, vol in enumerate(reversed(vol_in)):
837
- gmsh.model.setTag(3, vol, n_tag + 1)
838
- max_term_tag = len(vol_in) + 1
839
- spiral_volumes = []
840
- for n_tag, vol in enumerate(renumbered_vols):
841
- gmsh.model.setTag(3, vol, n_tag + max_term_tag)
842
- spiral_volumes.append(n_tag + max_term_tag)
843
- gmsh.write(os.path.join(self.model_folder, f'{name}{file_out_postfix}.brep'))
844
- if self.verbose:
845
- print(f'Straightening terminals of {name} took {timeit.default_timer() - start_time:.2f} s')
846
- print(f'Writing volume information file: {name}.vi ')
847
- vi = {'export': spiral_volumes, 'all': [vol[1] for vol in gmsh.model.getEntities(dim=3)]}
848
- json.dump(vi, open(f"{os.path.join(self.model_folder, name)}.vi", 'w'))
849
- if self.verbose:
850
- print(f'Done writing volume information file: {name}.vi ')
851
- if gui:
852
- self.gu.launch_interactive_GUI()
853
- else:
854
- gmsh.clear()
855
-
856
- def save_fqpl_vi(self):
857
- for f_name in self.cctdm.geometry.fqpls.names:
858
- if self.verbose:
859
- print(f'Writing volume information file: {f_name}.vi ')
860
- gmsh.open(os.path.join(self.model_folder, f'{f_name}.brep'))
861
- volumes = [vol[1] for vol in gmsh.model.getEntities(dim=3)]
862
- export = volumes
863
- vi = {'export': export, 'all': volumes}
864
- json.dump(vi, open(f"{os.path.join(self.model_folder, f_name)}.vi", 'w'))
865
- if self.verbose:
866
- print(f'Done writing volume information file: {f_name}.vi ')
867
- gmsh.clear()
868
-
869
- def fragment(self, gui=False):
870
- if self.verbose:
871
- print('Loading files in preparation for fragment operation')
872
- for f_name in self.cctwi.w_names + self.cctwi.f_names:
873
- gmsh.merge(os.path.join(self.model_folder, f'{f_name}.brep'))
874
- num_vol = np.max([vol[1] for vol in gmsh.model.getEntities(dim=3)])
875
- for f_name in self.cctwi.formers:
876
- gmsh.merge(os.path.join(self.model_folder, f'{f_name}.brep'))
877
- num_vol += 1
878
- gmsh.merge(os.path.join(self.model_folder, f'{self.cctwi.air}.brep'))
879
- num_vol += 1
880
- entities = gmsh.model.getEntities(dim=3)
881
- if len(entities) != num_vol:
882
- raise ValueError('Not consistent volumes numbers in brep and json files!')
883
- objectDimTags = [entities[-1]]
884
- toolDimTags = entities[:-1]
885
- # central_line = [(1, 3898)]
886
- # toolDimTags = toolDimTags+central_line
887
- if self.verbose:
888
- print(f'Fragmenting {self.magnet_name} started')
889
- start_time = timeit.default_timer()
890
- gmsh.model.occ.fragment(objectDimTags, toolDimTags, removeObject=True, removeTool=True)
891
- gmsh.model.occ.synchronize()
892
- if self.verbose:
893
- print(f'Fragmenting {self.magnet_name} took {timeit.default_timer() - start_time:.2f} s')
894
-
895
- gmsh.write(self.model_file)
896
- if gui:
897
- self.gu.launch_interactive_GUI()
898
- else:
899
- gmsh.clear()
900
- gmsh.finalize()
901
-
902
- def load_geometry(self, gui=False):
903
- gmsh.open(self.model_file)
904
- if gui:
905
- self.gu.launch_interactive_GUI()
1
+ import os
2
+ import copy
3
+ import math
4
+ import collections
5
+ import logging
6
+
7
+ import json
8
+ import timeit
9
+ import numpy as np
10
+ import gmsh
11
+
12
+ from fiqus.data import RegionsModelFiQuS as Reg_Mod_FiQ
13
+ from fiqus.data.DataWindingsCCT import WindingsInformation # for winding information
14
+ from fiqus.utils.Utils import FilesAndFolders as uff
15
+ from fiqus.utils.Utils import GmshUtils
16
+
17
+ logger = logging.getLogger(__name__)
18
+ class Hexahedrons:
19
+
20
+ def __init__(self):
21
+ """
22
+ Points generator for CCT windings to give hexahedron volumes for hexahedron meshing in Gmsh.
23
+ """
24
+ self.vertices_to_surf = [[0, 4, 7, 3], [1, 5, 6, 2], [1, 5, 4, 0], [2, 6, 7, 3], [0, 1, 2, 3], [4, 5, 6, 7]]
25
+ # Node ordering following Gmsh for hexahedron https://gmsh.info/doc/texinfo/gmsh.html#Legacy-formats --> node ordering
26
+ # above _vertices_to_surf is only used for manipulation of the hexahedra points on the data model level. Gmsh uses points connections and line connections for creating surfaces
27
+
28
+ self.hexes = {}
29
+ self._corner_points = {}
30
+ self._sur_names = ['x-', 'x+', 'y-', 'y+', 'z-', 'z+']
31
+ self._op_sign = {'+': '-', '-': '+'}
32
+ self._sign = {1: '+', -1: '-'}
33
+
34
+ def _add_corner_points(self, corner_num, x, y, z, ct):
35
+ self._corner_points[corner_num] = {'x': x, 'y': y, 'z': z, 'ct': ct} # ct is channel turn
36
+
37
+ def _generate_hexes(self):
38
+ for h in range(len(self._corner_points[0]['x'])):
39
+ self.hexes[h] = {}
40
+ for p_num, p_coor in self._corner_points.items():
41
+ self.hexes[h][p_num] = {}
42
+ for coor in ['x', 'y', 'z']:
43
+ self.hexes[h][p_num][coor] = p_coor[coor][h]
44
+ self.hexes[h]['ct'] = p_coor['ct'][h]
45
+ return p_coor['ct'][h]
46
+
47
+ def _add_elem_to_elem(self, start_elem, dir_str, dist):
48
+ """
49
+ :param start_elem: idx of start element
50
+ :param dir_str: direction of adding element, options are: 'x-', 'x+', 'y-', 'y+', 'z-', 'z+'
51
+ :param dist: length of new element in the direction specified above [m]
52
+ :return: index of created element
53
+ """
54
+ op_dir = dir_str[0] + self._op_sign[dir_str[1]]
55
+ new_elem = start_elem + math.copysign(1, start_elem)
56
+ self.hexes[new_elem] = {}
57
+ source_points = self.vertices_to_surf[self._sur_names.index(dir_str)]
58
+ destination_points = self.vertices_to_surf[self._sur_names.index(op_dir)]
59
+ for d_p, s_p in zip(destination_points + source_points, source_points + source_points):
60
+ self.hexes[new_elem][d_p] = copy.deepcopy(self.hexes[start_elem][s_p])
61
+ for s_p in source_points: # modif_points:
62
+ self.hexes[new_elem][s_p][dir_str[0]] = self.hexes[new_elem][s_p][dir_str[0]] + dist
63
+ self.hexes[new_elem]['ct'] = self.hexes[start_elem]['ct']
64
+ return new_elem
65
+
66
+ def _reorder_and_renumber_hexes(self):
67
+ self.hexes = collections.OrderedDict(sorted(self.hexes.items()))
68
+ first_hex_num = list(self.hexes.keys())[0]
69
+ temp_dict = {}
70
+ for h_num, h_dict in self.hexes.items():
71
+ temp_dict[h_num - first_hex_num + 1] = h_dict
72
+ self.hexes = temp_dict
73
+ for h_num, h_dict in self.hexes.items():
74
+ turn = h_dict.pop('ct', None)
75
+ self.hexes[h_num] = collections.OrderedDict(sorted(h_dict.items()))
76
+ self.hexes[h_num]['ct'] = turn
77
+
78
+
79
+ class Winding(Hexahedrons):
80
+
81
+ def __init__(self, cctdm, layer, post_proc=False):
82
+
83
+ super(Winding, self).__init__()
84
+ self.cctdm = cctdm
85
+ self.layer = layer
86
+ self.post_proc = post_proc
87
+ self.z_corr = 0
88
+ self._populate_corners()
89
+ self.z_corr = self._center_z_and_offset()
90
+ self._generate_hexes()
91
+
92
+ def _populate_corners(self):
93
+ r"""
94
+ Generates hexahedron with 8 corners numbered 0-7. The size of hexahedron is taken from groove size of cct as specified in yaml. The length is from number of elements per turn.
95
+ y (v)
96
+ 3----------2
97
+ |\ ^ |\
98
+ | \ | | \
99
+ | \ | | \
100
+ | 7------+---6
101
+ | | +-- |-- | -> x (u)
102
+ 0---+---\--1 |
103
+ \ | \ \ |
104
+ \ | \ \ |
105
+ \| z(w)\|
106
+ 4----------5
107
+
108
+ :return: none
109
+ """
110
+ self._sign_of_rot = math.copysign(1, self.cctdm.geometry.windings.alphas[self.layer])
111
+ if self._sign_of_rot > 0:
112
+ corner_seq = {
113
+ 0: {'w_s': -1, 'h_s': -1, 'far': False}, # 'blcf'
114
+ 1: {'w_s': -1, 'h_s': 1, 'far': False}, # 'brcf'
115
+ 2: {'w_s': 1, 'h_s': 1, 'far': False}, # 'trcf'
116
+ 3: {'w_s': 1, 'h_s': -1, 'far': False}, # 'tlcf'
117
+ 4: {'w_s': -1, 'h_s': -1, 'far': True}, # bottom left corner close #'blcc'
118
+ 5: {'w_s': -1, 'h_s': 1, 'far': True}, # bottom right corner close #'brcc'
119
+ 6: {'w_s': 1, 'h_s': 1, 'far': True}, # top right corner close #'trcc'
120
+ 7: {'w_s': 1, 'h_s': -1, 'far': True}, # top right corner close #'tlcc'
121
+ }
122
+ else:
123
+ corner_seq = {
124
+ 0: {'w_s': 1, 'h_s': 1, 'far': False}, # 'trcf'
125
+ 1: {'w_s': 1, 'h_s': -1, 'far': False}, # 'brcf'
126
+ 2: {'w_s': -1, 'h_s': -1, 'far': False}, # 'blcf'
127
+ 3: {'w_s': -1, 'h_s': 1, 'far': False}, # 'tlcf'
128
+ 4: {'w_s': 1, 'h_s': 1, 'far': True}, # top right corner close #'trcc'
129
+ 5: {'w_s': 1, 'h_s': -1, 'far': True}, # bottom right corner close #'brcc'
130
+ 6: {'w_s': -1, 'h_s': -1, 'far': True}, # bottom left corner close #'blcc'
131
+ 7: {'w_s': -1, 'h_s': 1, 'far': True}, # top left corner close #'tlcc'
132
+ }
133
+ for corner_num, corner_dict in corner_seq.items():
134
+ self._add_corner_points(corner_num, *self.calc_turns_corner_coords(corner_dict['w_s'], corner_dict['h_s'], far=corner_dict['far']))
135
+
136
+ def calc_turns_corner_coords(self, w_s=1, h_s=1, far=False):
137
+ """
138
+ Based on https://doi.org/10.1016/j.cryogenics.2020.103041
139
+ :param h_s: wire height sign
140
+ :param w_s: wire width sign
141
+ :param far: for far surface set to True, for close surface set to False. This refers to walls of hexagon.
142
+ :return:
143
+ """
144
+ r_wm = self.cctdm.geometry.windings.r_wms[self.layer] # radius of winding layer, in the middle of the conductor groove [m]
145
+ w_i = self.cctdm.geometry.windings.lps[self.layer] # layer pitch [m]
146
+ n_turns = self.cctdm.geometry.windings.n_turnss[self.layer] # number of turns [-]
147
+ nept = self.cctdm.geometry.windings.ndpts[self.layer] - 1 # number of elements per turn [-]
148
+ alpha = self.cctdm.geometry.windings.alphas[self.layer] * np.pi / 180 # inclination of the winding [deg]
149
+ theta_0 = 0 # offset start at theta_0, i.e. angular position of the beginning of the 1st turn[deg]
150
+ z_0 = 0 # offset start at z_0, i.e. axial position of the beginning of the 1st turn [m]
151
+ hh = 0.5 * h_s * self.cctdm.geometry.windings.wwhs[self.layer] # half of h (groove height)
152
+ hw = 0.5 * w_s * self.cctdm.geometry.windings.wwws[self.layer] # half of w (groove width)
153
+ tot_angle = 2 * np.pi * (n_turns + 1 / nept)
154
+ tot_points = int(nept * n_turns) + 1
155
+ if alpha > 0:
156
+ theta = np.linspace(0, tot_angle, tot_points, endpoint=False)
157
+ else:
158
+ theta = np.linspace(0 + np.pi, tot_angle + np.pi, tot_points, endpoint=False) # this rotates terminals for opposite inclination to be on the opposite side
159
+ if far:
160
+ theta = theta[1:] # give all points but the first one, so the far surface can be created
161
+ else:
162
+ theta = theta[:-1] # give all the points but the last one so a near surface can be created
163
+ if theta.size < 2:
164
+ raise ValueError(f'Combination of number of division points per turn (ndpts) and number of turns (n_turnss) must result in at least 2 Hexahedrons, but inputs result in only {theta.size}!')
165
+ C = r_wm * np.arctan(alpha) * np.cos(theta - theta_0) + w_i / (2 * np.pi)
166
+ D = np.sqrt(r_wm ** 2 + np.square(C))
167
+ # -- x, y, z points coordinates
168
+ xs = (r_wm + hh) * np.cos(theta) - np.sin(theta) * hw * C / D
169
+ ys = (r_wm + hh) * np.sin(theta) + np.cos(theta) * hw * C / D
170
+ zs = r_wm * np.sin(theta - theta_0) / np.tan(alpha) + w_i * (theta - theta_0) / (2 * np.pi) + z_0 - hw * r_wm / D # original formula
171
+ if alpha > 0:
172
+ channel_turn = np.ceil((theta - 2 * np.pi / (nept + 1)) / (2 * np.pi))
173
+ # channel_turn[0] = channel_turn[1] # make the hex at theta 0 to belong to the first turn.
174
+ else:
175
+ channel_turn = np.ceil((theta - np.pi - 2 * np.pi / (nept + 1)) / (2 * np.pi))
176
+ channel_turn[0] = channel_turn[1] # make the hex at theta 0 to belong to the first turn.
177
+ return xs.tolist(), ys.tolist(), zs.tolist(), list(map(int, channel_turn))
178
+
179
+ def _center_z_and_offset(self, offset=0):
180
+ z_mins = []
181
+ z_maxs = []
182
+ for c_val in self._corner_points.values():
183
+ z_mins.append(np.min(c_val['z']))
184
+ z_maxs.append(np.max(c_val['z']))
185
+ z_min_wind = np.min(np.array(z_mins))
186
+ z_max_wind = np.max(np.array(z_maxs))
187
+ z_centre = (z_min_wind + z_max_wind) / 2
188
+ if not self.post_proc:
189
+ for c_key, c_val in self._corner_points.items():
190
+ self._corner_points[c_key]['z'] = (c_val['z'] - z_centre + offset).tolist()
191
+ z_mins = []
192
+ z_maxs = []
193
+ for c_val in self._corner_points.values():
194
+ z_mins.append(np.min(c_val['z']))
195
+ z_maxs.append(np.max(c_val['z']))
196
+ self.z_min_winding_layer = np.min(np.array(z_mins))
197
+ self.z_max_winding_layer = np.max(np.array(z_maxs))
198
+ return z_centre + offset
199
+
200
+
201
+ class FQPL(Hexahedrons):
202
+ def __init__(self, cctdm, layer):
203
+ """
204
+ FQPL hex generator
205
+ :param cctdm: cct data model object parsed from yaml input file
206
+ :param layer: loop number, integer
207
+ """
208
+ super(FQPL, self).__init__()
209
+ self.cctdm = cctdm
210
+ self.layer = layer
211
+ self._calc_z_unit_len()
212
+ self._populate_corners()
213
+ self._generate_hexes()
214
+ self._generate_fqpl()
215
+ self._rotate_fqpl()
216
+ self._reorder_and_renumber_hexes()
217
+ self._clean_attr()
218
+
219
+ def _calc_z_unit_len(self):
220
+ if self.cctdm.geometry.fqpls.z_starts[self.layer] == 'z_min':
221
+ self.near = self.cctdm.geometry.air.z_min
222
+ self.z_unit_len = (self.cctdm.geometry.fqpls.z_ends[self.layer] - self.near - self.cctdm.geometry.fqpls.fwws[self.layer]) / self.cctdm.geometry.fqpls.fndpls[self.layer]
223
+
224
+ elif self.cctdm.geometry.fqpls.z_starts[self.layer] == 'z_max':
225
+ self.near = self.cctdm.geometry.air.z_max
226
+ self.z_unit_len = (self.near - self.cctdm.geometry.fqpls.z_ends[self.layer] - self.cctdm.geometry.fqpls.fwws[self.layer]) / self.cctdm.geometry.fqpls.fndpls[self.layer]
227
+ else:
228
+ raise ValueError(f'fqpl.z_starts parameter must be a string equal to z_min or z_max, but {self.cctdm.geometry.fqpls.z_starts[self.layer]} was given!')
229
+
230
+ self.extrusion_sign = math.copysign(1, self.cctdm.geometry.fqpls.z_ends[self.layer] - self.near)
231
+
232
+ def _calc_first_corner_coords(self, w_s, h_s, far, offset):
233
+ x = [self.cctdm.geometry.fqpls.r_ins[self.layer] + self.cctdm.geometry.fqpls.fwhs[self.layer] * h_s] # not rotated coordinate
234
+ y = [self.cctdm.geometry.fqpls.fwws[self.layer] * w_s] # not rotated coordinate
235
+ if far:
236
+ z = [self.near - self.z_unit_len + offset]
237
+ else:
238
+ z = [self.near + offset]
239
+ return x, y, z, [1]
240
+
241
+ def _populate_corners(self):
242
+ r"""
243
+ Generates hexahedron with 8 corners numbered 0-7. The size of hexahedron is taken from groove size of cct as specified in yaml. The length is from number of elements per turn.
244
+ y (v)
245
+ 3----------2
246
+ |\ ^ |\
247
+ | \ | | \
248
+ | \ | | \
249
+ | 7------+---6
250
+ | | +-- |-- | -> x (u)
251
+ 0---+---\--1 |
252
+ \ | \ \ |
253
+ \ | \ \ |
254
+ \| z(w)\|
255
+ 4----------5
256
+
257
+ :return: none
258
+ """
259
+ if self.extrusion_sign > 0:
260
+ offset = self.z_unit_len
261
+ corner_seq = {
262
+ 0: {'w_s': -0.5, 'h_s': 0, 'far': True}, # 'blcf'
263
+ 1: {'w_s': -0.5, 'h_s': 1, 'far': True}, # 'tlcf'
264
+ 2: {'w_s': 0.5, 'h_s': 1, 'far': True}, # 'trcf'
265
+ 3: {'w_s': 0.5, 'h_s': 0, 'far': True}, # 'brcf'
266
+ 4: {'w_s': -0.5, 'h_s': 0, 'far': False}, # bottom left corner close #'blcc'
267
+ 5: {'w_s': -0.5, 'h_s': 1, 'far': False}, # top left corner close #'tlcc'
268
+ 6: {'w_s': 0.5, 'h_s': 1, 'far': False}, # top right corner close #'trcc'
269
+ 7: {'w_s': 0.5, 'h_s': 0, 'far': False}, # bottom right corner close #'brcc'
270
+ }
271
+ else:
272
+ offset = 0
273
+ corner_seq = {
274
+ 0: {'w_s': -0.5, 'h_s': 0, 'far': False}, # 'blcf'
275
+ 1: {'w_s': -0.5, 'h_s': 1, 'far': False}, # 'tlcf'
276
+ 2: {'w_s': 0.5, 'h_s': 1, 'far': False}, # 'trcf'
277
+ 3: {'w_s': 0.5, 'h_s': 0, 'far': False}, # 'brcf'
278
+ 4: {'w_s': -0.5, 'h_s': 0, 'far': True}, # bottom left corner close #'blcc'
279
+ 5: {'w_s': -0.5, 'h_s': 1, 'far': True}, # top left corner close #'tlcc'
280
+ 6: {'w_s': 0.5, 'h_s': 1, 'far': True}, # top right corner close #'trcc'
281
+ 7: {'w_s': 0.5, 'h_s': 0, 'far': True}, # bottom right corner close #'brcc'
282
+ }
283
+ for corner_num, corner_dict in corner_seq.items():
284
+ self._add_corner_points(corner_num, *self._calc_first_corner_coords(corner_dict['w_s'], corner_dict['h_s'], corner_dict['far'], offset))
285
+
286
+ def _get_ref_point(self, elem_number, surf_n_dir_str, coord, dist):
287
+ """
288
+ Gets center point for a surface 'pointing' in the direction surf_n_dir_str of hex with elem_number. It then moves that point into along coordinate coord and by distance dist
289
+ :param elem_number: hex number to take the surface from
290
+ :param surf_n_dir_str: 'direction of surface' (kind of normal), options are: 'x-', 'x+', 'y-', 'y+', 'z-', 'z+'
291
+ :param coord: direction to more the point in, options are: 'x', 'y', 'z'
292
+ :param dist: distance to move the point, this is positive or negative float
293
+ :return:
294
+ """
295
+ source_points = self.vertices_to_surf[self._sur_names.index(surf_n_dir_str)] # get the indexes 4 points for the surface points in direction surf_n_dir_str
296
+ points_coor = [self.hexes[elem_number][point] for point in source_points] # get the points, they contain coordinates
297
+ xs = []
298
+ ys = []
299
+ zs = []
300
+ for point in points_coor: # get coordinates into list for each axis
301
+ for cor_list, coor in zip([xs, ys, zs], ['x', 'y', 'z']):
302
+ cor_list.append(point[coor])
303
+ p = {'x': np.mean(xs), 'y': np.mean(ys), 'z': np.mean(zs)} # get averages of each axis coordinate, giving the center of the surface
304
+ p[coord] = p[coord] + dist
305
+ return p
306
+
307
+ def _add_elem_with_rot(self, elem_number, surf_n_dir_str, add_ax, angle, ref_p_coor):
308
+ """
309
+ :param elem_number: idx of start element
310
+ :param surf_n_dir_str: direction of adding element, options are: 'x-', 'x+', 'y-', 'y+', 'z-', 'z+'
311
+ :param ref_p_coor: length of new element in the direction specified above [m]
312
+ :return: index of created element
313
+ """
314
+ mod_coor = [surf_n_dir_str[0], add_ax]
315
+ # mod_coor.remove(surf_n_dir_str[0]) # get the other two coordinates that arenet the direction of the surface to take
316
+
317
+ angle_rad = np.deg2rad(angle)
318
+ op_dir = surf_n_dir_str[0] + self._op_sign[surf_n_dir_str[1]]
319
+ new_elem = elem_number + math.copysign(1, elem_number)
320
+ self.hexes[new_elem] = {}
321
+ source_points = self.vertices_to_surf[self._sur_names.index(surf_n_dir_str)]
322
+ destination_points = self.vertices_to_surf[self._sur_names.index(op_dir)]
323
+ for d_p, s_p in zip(destination_points + source_points, source_points + source_points):
324
+ self.hexes[new_elem][d_p] = copy.deepcopy(self.hexes[elem_number][s_p])
325
+ for s_p in source_points: # modif_points:
326
+ v1 = self.hexes[new_elem][s_p][mod_coor[0]]
327
+ v2 = self.hexes[new_elem][s_p][mod_coor[1]]
328
+ v1_ofset = ref_p_coor[mod_coor[0]]
329
+ v2_ofset = ref_p_coor[mod_coor[1]]
330
+ v1 = v1 - v1_ofset
331
+ v2 = v2 - v2_ofset
332
+ v1_new = np.cos(angle_rad) * v1 - np.sin(angle_rad) * v2
333
+ v2_new = np.sin(angle_rad) * v1 + np.cos(angle_rad) * v2
334
+ v1 = v1_new + v1_ofset
335
+ v2 = v2_new + v2_ofset
336
+ self.hexes[new_elem][s_p][mod_coor[0]] = v1
337
+ self.hexes[new_elem][s_p][mod_coor[1]] = v2
338
+ self.hexes[new_elem]['ct'] = self.hexes[elem_number]['ct']
339
+ return new_elem
340
+
341
+ def _generate_fqpl(self):
342
+ new_elem = self._add_elem_to_elem(0, 'z+', self.extrusion_sign * self.z_unit_len)
343
+ for _ in range(self.cctdm.geometry.fqpls.fndpls[self.layer] - 2):
344
+ new_elem = self._add_elem_to_elem(new_elem, 'z+', self.extrusion_sign * self.z_unit_len)
345
+ coord = 'x'
346
+ ref_point = self._get_ref_point(new_elem, 'z+', coord, self.cctdm.geometry.fqpls.r_bs[self.layer]) # centre of rotation pint.
347
+ n_seg = 10
348
+ angle = 180 / n_seg
349
+ for _ in range(n_seg):
350
+ new_elem = self._add_elem_with_rot(new_elem, 'z+', coord, self.extrusion_sign * angle, ref_point)
351
+ for _ in range(self.cctdm.geometry.fqpls.fndpls[self.layer]):
352
+ new_elem = self._add_elem_to_elem(new_elem, 'z+', -self.extrusion_sign * self.z_unit_len)
353
+
354
+ def _rotate_fqpl(self):
355
+ for h_num, h_dict in self.hexes.items():
356
+ channel_turn = h_dict.pop('ct', None)
357
+ for p_num, p_dict in h_dict.items():
358
+ xx = p_dict['x']
359
+ yy = p_dict['y']
360
+ x = xx * np.cos(np.deg2rad(self.cctdm.geometry.fqpls.thetas[self.layer])) - yy * np.sin(np.deg2rad(self.cctdm.geometry.fqpls.thetas[self.layer])) # rotated coordinate
361
+ y = xx * np.sin(np.deg2rad(self.cctdm.geometry.fqpls.thetas[self.layer])) + yy * np.cos(np.deg2rad(self.cctdm.geometry.fqpls.thetas[self.layer])) # rotated coordinate
362
+ self.hexes[h_num][p_num]['x'] = x
363
+ self.hexes[h_num][p_num]['y'] = y
364
+ self.hexes[h_num]['ct'] = channel_turn
365
+
366
+ def _clean_attr(self):
367
+ del self.cctdm
368
+ del self.layer
369
+ del self.near
370
+ del self.z_unit_len
371
+
372
+
373
+ class Generate_BREPs:
374
+ def __init__(self, fdm, verbose=True):
375
+ """
376
+ Class to generate (build) Windings and Formers (WF) of a canted cosine theta (CCT) magnet and save them to brep files
377
+ :param fdm: fiqus data model
378
+ :param verbose: If True more information is printed in python console.
379
+ """
380
+ self.cctdm = fdm.magnet
381
+ self.model_folder = os.path.join(os.getcwd())
382
+ self.magnet_name = fdm.general.magnet_name
383
+
384
+ self.verbose = verbose
385
+ self.cctwi = WindingsInformation()
386
+ self.cctwi.magnet_name = self.magnet_name
387
+ self.transl_dict = {'vol': '', 'surf': '_bd', 'surf_in': '_in', 'surf_out': '_out', 'cochain': '_cut'}
388
+ self.gu = GmshUtils(self.model_folder, self.verbose)
389
+ self.gu.initialize()
390
+ self.formers = []
391
+ self.air = []
392
+ gmsh.option.setString('Geometry.OCCTargetUnit', 'M')
393
+
394
+ def generate_windings_or_fqpls(self, type_str):
395
+ """
396
+ Generates windings brep files, as many as number of entries in the yaml file for windings.
397
+ :param type_str: What to generate, options are: 'windings' or 'fqpls'
398
+ :return: None, saves brep to disk
399
+ """
400
+ if self.verbose:
401
+ print(f'Generating {type_str} started')
402
+ start_time = timeit.default_timer()
403
+
404
+ def generate_geom(worf_in, names_in):
405
+ worf_tags_dict_in = {}
406
+ for worf_num, worf_in in enumerate(worf_in):
407
+ worf_tags_dict_in[worf_num] = self.generate_hex_geom(worf_in)
408
+ gmsh.model.occ.synchronize()
409
+ gmsh.write(os.path.join(self.model_folder, f'{names_in[worf_num]}.brep'))
410
+ gmsh.clear()
411
+ return worf_tags_dict_in
412
+
413
+ if self.cctdm.geometry.windings.names and type_str == 'windings':
414
+ worf = [Winding(self.cctdm, i) for i, _ in enumerate(self.cctdm.geometry.windings.names)] # worf = winding or fqpl
415
+ names = [name for name in self.cctdm.geometry.windings.names]
416
+ for w in worf:
417
+ if self.cctdm.geometry.air.z_min > w.z_min_winding_layer or w.z_max_winding_layer > self.cctdm.geometry.air.z_max:
418
+ raise ValueError(f'{self.cctdm.geometry.air.name} region dimensions are too s mall to fit the windings turns. Please extend z_min and/or z_max')
419
+ else:
420
+ pass
421
+ worf_tags_dict = generate_geom(worf, names)
422
+
423
+ if self.cctdm.geometry.fqpls.names and type_str == 'fqpls':
424
+ worf = [FQPL(self.cctdm, i) for i, _ in enumerate(self.cctdm.geometry.fqpls.names)] # worf = winding or fqpl
425
+ names = [name for name in self.cctdm.geometry.fqpls.names]
426
+ worf_tags_dict = generate_geom(worf, names)
427
+
428
+ w_z_maxs = []
429
+ w_z_mins = []
430
+ if type_str == 'windings':
431
+ for winding in worf:
432
+ w_z_maxs.append(winding.z_max_winding_layer)
433
+ w_z_mins.append(winding.z_min_winding_layer)
434
+ self.cctwi.windings_avg_length = float(np.mean(w_z_maxs) - np.mean(w_z_mins))
435
+ self.cctwi.windings.names = names
436
+ self.cctwi.windings.t_in.vol_st = []
437
+ self.cctwi.windings.t_in.surf_st = []
438
+ self.cctwi.windings.t_out.vol_st = []
439
+ self.cctwi.windings.t_out.surf_st = []
440
+ self.cctwi.windings.t_in.vol_et = []
441
+ self.cctwi.windings.t_in.surf_et = []
442
+ self.cctwi.windings.t_out.vol_et = []
443
+ self.cctwi.windings.t_out.surf_et = []
444
+ self.cctwi.windings.t_in.lc_et = []
445
+ self.cctwi.windings.t_out.lc_et = []
446
+ self.cctwi.windings.t_in.ndpterms = self.cctdm.geometry.windings.ndpt_ins
447
+ self.cctwi.windings.t_out.ndpterms = self.cctdm.geometry.windings.ndpt_outs
448
+ self.cctwi.windings.t_in.z_air = self.cctdm.geometry.air.z_min
449
+ self.cctwi.windings.t_out.z_air = self.cctdm.geometry.air.z_max
450
+ for winding_num, winding_dict in worf_tags_dict.items():
451
+ first_vol = list(winding_dict.keys())[0]
452
+ last_vol = list(winding_dict.keys())[-1]
453
+ self.cctwi.windings.t_in.vol_st.append(winding_dict[first_vol]['v_t'])
454
+ self.cctwi.windings.t_in.surf_st.append(winding_dict[first_vol]['sf_ts'][0])
455
+ self.cctwi.windings.t_out.vol_st.append(winding_dict[last_vol]['v_t'])
456
+ self.cctwi.windings.t_out.surf_st.append(winding_dict[last_vol]['sf_ts'][2])
457
+ self.cctwi.windings.t_in.vol_et.append(winding_dict[first_vol]['v_t'])
458
+ self.cctwi.windings.t_in.surf_et.append(winding_dict[first_vol]['sf_ts'][0])
459
+ self.cctwi.windings.t_out.vol_et.append(winding_dict[last_vol]['v_t'])
460
+ self.cctwi.windings.t_out.surf_et.append(winding_dict[last_vol]['sf_ts'][2])
461
+
462
+ lc_st_neg_alpha = [[1, 0], [0, 3], [3, 2], [2, 1], [6, 4], [4, 5], [5, 7], [7, 6], [1, 6], [0, 4], [3, 5], [2, 7]]
463
+ lc_st_pos_alpha = [[3, 1], [1, 0], [0, 2], [2, 3], [7, 4], [4, 5], [5, 6], [6, 7], [3, 7], [1, 4], [0, 5], [2, 6]]
464
+ lc_et_neg_alpha = [[5, 7], [7, 6], [6, 4], [4, 5], [1, 3], [3, 2], [2, 0], [0, 1], [5, 1], [7, 3], [6, 2], [4, 0]]
465
+ lc_et_pos_alpha = [[5, 7], [7, 6], [6, 4], [4, 5], [1, 3], [3, 2], [2, 0], [0, 1], [5, 1], [7, 3], [6, 2], [4, 0]]
466
+ # [[1, 2], [2, 3], [3, 4], [4, 1], [5, 6], [6, 7], [7, 8], [8, 5], [1, 5], [2, 6], [3, 7], [4, 8]]
467
+ # [[2, 1], [1, 4], [4, 3], [3, 2], [6, 5], [5, 8], [8, 7], [7, 6], [2, 6], [1, 5], [4, 8], [3, 7]]
468
+
469
+ lc_et_pos_corr = [[1, 0], [0, 3], [3, 2], [2, 1], [5, 4], [4, 7], [7, 6], [6, 5], [1, 5], [0, 4], [3, 7], [2, 6]]
470
+
471
+ self.cctwi.windings.t_in.lc_st = []
472
+ self.cctwi.windings.t_out.lc_st = []
473
+ for alpha in self.cctdm.geometry.windings.alphas:
474
+ if alpha > 0:
475
+ self.cctwi.windings.t_in.lc_st.append(lc_st_pos_alpha)
476
+ self.cctwi.windings.t_out.lc_st.append(lc_st_pos_alpha)
477
+ self.cctwi.windings.t_in.lc_et.append(lc_et_pos_corr)
478
+ self.cctwi.windings.t_out.lc_et.append(lc_et_pos_alpha)
479
+ else:
480
+ self.cctwi.windings.t_in.lc_st.append(lc_st_neg_alpha)
481
+ self.cctwi.windings.t_out.lc_st.append(lc_st_neg_alpha)
482
+ self.cctwi.windings.t_in.lc_et.append(lc_et_neg_alpha)
483
+ self.cctwi.windings.t_out.lc_et.append(lc_et_pos_corr)
484
+
485
+ if self.verbose:
486
+ print(f'Generating {type_str} took {timeit.default_timer() - start_time:.2f} s')
487
+
488
+ def save_volume_info(self):
489
+ if self.cctdm.geometry.fqpls.names:
490
+ fqpls = self.cctdm.geometry.fqpls.names
491
+ else:
492
+ fqpls = []
493
+
494
+ self.cctwi.w_names = self.cctdm.geometry.windings.names
495
+ self.cctwi.f_names = fqpls
496
+ self.cctwi.formers = self.cctdm.geometry.formers.names
497
+ self.cctwi.air = self.cctdm.geometry.air.name
498
+
499
+ volume_info_file = os.path.join(self.model_folder, f'{self.cctwi.magnet_name}.wi')
500
+ uff.write_data_to_yaml(volume_info_file, self.cctwi.model_dump())
501
+
502
+ def generate_formers(self):
503
+ if self.cctdm.geometry.formers.r_ins:
504
+ if self.verbose:
505
+ print('Generating Formers Started')
506
+ start_time = timeit.default_timer()
507
+
508
+ for f_i, _ in enumerate(self.cctdm.geometry.formers.r_ins):
509
+ z = self.cctdm.geometry.formers.z_mins[f_i]
510
+ dz = self.cctdm.geometry.formers.z_maxs[f_i] - self.cctdm.geometry.formers.z_mins[f_i]
511
+ cylin_out = (3, gmsh.model.occ.addCylinder(0, 0, z, 0, 0, dz, self.cctdm.geometry.formers.r_outs[f_i], angle=2 * math.pi)) # add cylinder to align to existing bodies in the pro file
512
+ cylin_in = (3, gmsh.model.occ.addCylinder(0, 0, z, 0, 0, dz, self.cctdm.geometry.formers.r_ins[f_i], angle=2 * math.pi)) # add another cylinder to subtract from the first one to make a tube
513
+ cylinder = gmsh.model.occ.cut([cylin_out], [cylin_in], removeObject=True) # subtract cylinders to make a tube
514
+ self.formers.append(cylinder[0][0]) # keep just the tag and append
515
+ gmsh.model.occ.synchronize()
516
+ gmsh.write(os.path.join(self.model_folder, f'{self.cctdm.geometry.formers.names[f_i]}.brep'))
517
+ gmsh.clear()
518
+ if self.verbose:
519
+ print(f'Generating formers took {timeit.default_timer() - start_time:.2f} s')
520
+
521
+ def generate_air(self):
522
+ if self.cctdm.geometry.air.ar:
523
+ if self.verbose:
524
+ print('Generating air started')
525
+ start_time = timeit.default_timer()
526
+ if self.cctdm.geometry.air.sh_type == 'cylinder':
527
+ self.air = [(3, gmsh.model.occ.addCylinder(0, 0, self.cctdm.geometry.air.z_min, 0, 0, self.cctdm.geometry.air.z_max - self.cctdm.geometry.air.z_min, self.cctdm.geometry.air.ar, angle=2 * math.pi))]
528
+ elif self.cctdm.geometry.air.sh_type == 'cuboid':
529
+ air_box_size = [-self.cctdm.geometry.air.ar / 2, -self.cctdm.geometry.air.ar / 2, self.cctdm.geometry.air.z_min, self.cctdm.geometry.air.ar, self.cctdm.geometry.air.ar,
530
+ self.cctdm.geometry.air.z_max - self.cctdm.geometry.air.z_min] # list of box size with: x, y, z, dx, dy, dz
531
+ self.air = [(3, gmsh.model.occ.addBox(*air_box_size))]
532
+ else:
533
+ raise ValueError(f'Shape type: {self.cctdm.geometry.air.sh_type} is not supported!')
534
+ gmsh.model.occ.synchronize()
535
+ gmsh.write(os.path.join(self.model_folder, f'{self.cctdm.geometry.air.name}.brep'))
536
+ gmsh.clear()
537
+ if self.verbose:
538
+ print(f'Generating air took {timeit.default_timer() - start_time:.2f} s')
539
+
540
+ def generate_regions_file(self):
541
+ if self.verbose:
542
+ print('Generating Regions File Started')
543
+ start_time = timeit.default_timer()
544
+ cctrm = Reg_Mod_FiQ.RegionsModel()
545
+ vrt = 1000000 # volume region tag start
546
+ srt = 2000000 # surface region tag start
547
+ lrt = 3000000 # line region tag start
548
+ # -------- powered ----------
549
+ # volumes
550
+ cctrm.powered['cct'] = Reg_Mod_FiQ.Powered()
551
+ cctrm.powered['cct'].vol.names = [name + self.transl_dict['vol'] for name in self.cctdm.geometry.windings.names + self.cctdm.geometry.fqpls.names]
552
+ cctrm.powered['cct'].vol.currents = self.cctdm.solve.windings.currents + self.cctdm.solve.fqpls.currents
553
+ cctrm.powered['cct'].vol.sigmas = self.cctdm.solve.windings.sigmas + self.cctdm.solve.fqpls.sigmas
554
+ cctrm.powered['cct'].vol.mu_rs = self.cctdm.solve.windings.mu_rs + self.cctdm.solve.fqpls.mu_rs
555
+ reg = []
556
+ for _ in self.cctdm.geometry.windings.names + self.cctdm.geometry.fqpls.names:
557
+ reg.append(vrt)
558
+ vrt += 1
559
+ cctrm.powered['cct'].vol.numbers = reg
560
+ # surfaces
561
+ cctrm.powered['cct'].surf_in.names = [name + self.transl_dict['surf_in'] for name in self.cctdm.geometry.windings.names + self.cctdm.geometry.fqpls.names]
562
+ cctrm.powered['cct'].surf_out.names = [name + self.transl_dict['surf_out'] for name in self.cctdm.geometry.windings.names + self.cctdm.geometry.fqpls.names]
563
+ reg = []
564
+ for _ in self.cctdm.geometry.windings.names + self.cctdm.geometry.fqpls.names:
565
+ reg.append(srt)
566
+ srt += 1
567
+ cctrm.powered['cct'].surf_in.numbers = reg
568
+ reg = []
569
+ for _ in self.cctdm.geometry.windings.names + self.cctdm.geometry.fqpls.names:
570
+ reg.append(srt)
571
+ srt += 1
572
+ cctrm.powered['cct'].surf_out.numbers = reg
573
+
574
+ # -------- induced ----------
575
+ # volumes
576
+ cctrm.induced['cct'] = Reg_Mod_FiQ.Induced()
577
+ cctrm.induced['cct'].vol.names = [name + self.transl_dict['vol'] for name in self.cctdm.geometry.formers.names]
578
+ cctrm.induced['cct'].vol.sigmas = self.cctdm.solve.formers.sigmas
579
+ cctrm.induced['cct'].vol.mu_rs = self.cctdm.solve.formers.mu_rs
580
+ reg = []
581
+ for _ in self.cctdm.geometry.formers.names:
582
+ reg.append(vrt)
583
+ vrt += 1
584
+ cctrm.induced['cct'].vol.numbers = reg
585
+
586
+ # -------- air ----------
587
+ # volumes
588
+ cctrm.air.vol.name = self.cctdm.geometry.air.name + self.transl_dict['vol']
589
+ cctrm.air.vol.sigma = self.cctdm.solve.air.sigma
590
+ cctrm.air.vol.mu_r = self.cctdm.solve.air.mu_r
591
+ cctrm.air.vol.number = vrt
592
+ vrt += 1
593
+
594
+ # surface
595
+ cctrm.air.surf.name = self.cctdm.geometry.air.name + self.transl_dict['surf']
596
+ cctrm.air.surf.number = srt
597
+ srt += 1
598
+
599
+ # # center line
600
+ # cctrm.air.line.name = 'Center_line'
601
+ # cctrm.air.line.number = lrt
602
+ # lrt += 1
603
+ lrt = srt
604
+
605
+ # --------- cuts -------
606
+ # these need to be done at the end with the highest surface tags
607
+ cctrm.powered['cct'].cochain.names = [name + self.transl_dict['cochain'] for name in self.cctdm.geometry.windings.names + self.cctdm.geometry.fqpls.names]
608
+ reg = []
609
+ for _ in self.cctdm.geometry.windings.names + self.cctdm.geometry.fqpls.names:
610
+ reg.append(lrt)
611
+ lrt += 1
612
+ cctrm.powered['cct'].cochain.numbers = reg
613
+ # induced cuts
614
+ cctrm.induced['cct'].cochain.names = [name + self.transl_dict['cochain'] for name in self.cctdm.geometry.formers.names]
615
+ reg = []
616
+ for _ in self.cctdm.geometry.formers.names:
617
+ reg.append(lrt)
618
+ lrt += 1
619
+ cctrm.induced['cct'].cochain.numbers = reg
620
+
621
+ uff.write_data_to_yaml(os.path.join(self.model_folder, f'{self.cctwi.magnet_name}.regions'), cctrm.model_dump())
622
+
623
+ if self.verbose:
624
+ print(f'Generating Regions File Took {timeit.default_timer() - start_time:.2f} s')
625
+
626
+ @staticmethod
627
+ def generate_hex_geom(hexes_dict,
628
+ cons_for_lines=[[0, 1], [1, 2], [2, 3], [3, 0], [4, 5], [5, 6], [6, 7], [7, 4], [0, 4], [1, 5], [2, 6], [3, 7]],
629
+ vol_tag=-1):
630
+ """
631
+ Generates hexahedra volumes from data in hexes dict
632
+ :param hexes_dict: dictionary of data generated by either Winding or FQPL class in Hexahedrons.py
633
+ :param cons_for_lines: line connection defined as list of list in terms of point numbers to connect in the hexahedron
634
+ :param vol_tag: volume tag to use for generated volume of hexahedron
635
+ :return: dictionary with tags of created volumes, surface loops, surface fillings, closed loops, lines, points and channel turn (i.e. turn number of CCT winding).
636
+ """
637
+ tags_dict = {}
638
+ for h_num, h_dict in hexes_dict.hexes.items():
639
+ p_ts = [] # Point tags
640
+ channel_turn = h_dict.pop('ct', None)
641
+ for p_num, p_dict in h_dict.items():
642
+ p_ts.append(gmsh.model.occ.addPoint(p_dict['x'], p_dict['y'], p_dict['z']))
643
+
644
+ l_ts = [] # Line tags
645
+ for p_c in cons_for_lines: # point connections to make a line
646
+ l_ts.append(gmsh.model.occ.addLine(p_ts[p_c[0]], p_ts[p_c[1]]))
647
+
648
+ cl_ts = [] # Curved Loops tags
649
+ for l_c in [[0, 1, 2, 3], [4, 5, 6, 7], [3, 8, 7, 11], [1, 9, 5, 10], [0, 8, 4, 9], [2, 11, 6, 10]]: # line connections to make a curved loop
650
+ cl_ts.append(gmsh.model.occ.addCurveLoop([l_ts[l_c[0]], l_ts[l_c[1]], l_ts[l_c[2]], l_ts[l_c[3]]]))
651
+ sf_ts = [] # Surface Filling tags
652
+ for cl_t in cl_ts:
653
+ sf_ts.append(gmsh.model.occ.addSurfaceFilling(cl_t, degree=3, tol2d=0.000001, tol3d=0.00001, tolAng=0.001, tolCurv=0.05))
654
+ sl_t = gmsh.model.occ.addSurfaceLoop(sf_ts, sewing=True)
655
+ v_t = gmsh.model.occ.addVolume([sl_t], vol_tag)
656
+ tags_dict[int(h_num)] = {'v_t': v_t, 'sl_t': sl_t, 'sf_ts': sf_ts, 'cl_ts': cl_ts, 'l_ts': l_ts, 'p_ts': p_ts, 'ct': channel_turn}
657
+ return tags_dict
658
+
659
+
660
+ class Prepare_BREPs:
661
+ def __init__(self, fdm, verbose=True) -> object:
662
+ """
663
+ Class to preparing brep files by adding terminals.
664
+ :param fdm: FiQuS data model
665
+ :param verbose: If True more information is printed in python console.
666
+ """
667
+
668
+ self.cctdm = fdm.magnet
669
+ self.model_folder = os.path.join(os.getcwd())
670
+ self.magnet_name = fdm.general.magnet_name
671
+
672
+ self.verbose = verbose
673
+ self.winding_info_file = os.path.join(self.model_folder, f'{self.magnet_name}.wi')
674
+ self.cctwi = uff.read_data_from_yaml(self.winding_info_file, WindingsInformation)
675
+ self.gu = GmshUtils(self.model_folder, self.verbose)
676
+ self.gu.initialize()
677
+ self.model_file = os.path.join(self.model_folder, f'{self.magnet_name}.brep')
678
+
679
+ @staticmethod
680
+ def get_pts_for_sts(surface_tags):
681
+ """
682
+ Get point tags for surface tags
683
+ :param surface_tags: list of surface tags to get point tags
684
+ :return: list of point tags belonging to the surfaces in the list
685
+ """
686
+ vol_line_tags = []
687
+ vol_line_tags_i = 0
688
+ vol_line_tags_idx = []
689
+ for surf_tag in surface_tags:
690
+ line_tags_new = gmsh.model.getAdjacencies(2, surf_tag)[1]
691
+ for line_tag in line_tags_new:
692
+ vol_line_tags_i += 1
693
+ if line_tag not in vol_line_tags:
694
+ vol_line_tags.append(line_tag)
695
+ vol_line_tags_idx.append(vol_line_tags_i)
696
+ point_tags = []
697
+ point_tags_i = 0
698
+ point_tags_idx = []
699
+ for line_tag in vol_line_tags:
700
+ point_tags_new = gmsh.model.getAdjacencies(1, line_tag)[1]
701
+ for point_tag in point_tags_new:
702
+ point_tags_i += 1
703
+ if point_tag not in point_tags:
704
+ point_tags.append(point_tag)
705
+ point_tags_idx.append(point_tags_i)
706
+ return point_tags, vol_line_tags_idx, point_tags_idx
707
+
708
+ def straighten_terminal(self, gui=False):
709
+ """
710
+ Extends winding geom_generators to the air region boundary by extending the geom_generators with 'terminals' geom_generators up to the air boundary surfaces
711
+ By default this extends along the z axis and makes the final surface normal to the z axis.
712
+ :return: None, saves breps with straighten terminals to disk
713
+ """
714
+
715
+ for i, name in enumerate(self.cctwi.windings.names):
716
+ if self.verbose:
717
+ print(f'Straightening terminals of {name} started')
718
+ start_time = timeit.default_timer()
719
+ gmsh.open(os.path.join(self.model_folder, f'{name}.brep'))
720
+ for terminal in [self.cctwi.windings.t_in, self.cctwi.windings.t_out]:
721
+ vol_tag = terminal.vol_st[i]
722
+ vol_surf_tags = gmsh.model.getAdjacencies(3, vol_tag)[1]
723
+ surf_tag = terminal.surf_st[i]
724
+ if surf_tag not in vol_surf_tags:
725
+ raise ValueError(f'Surface tag of {surf_tag} given for volume in of {vol_tag} does not belong to the volume!')
726
+ # surf_point_tags = self.get_pts_for_sts([surf_tag]) # only 'powered' surface
727
+ surf_point_tags, surf_line_tags_idx, surf_point_tags_idx = self.get_pts_for_sts([surf_tag]) # only 'powered' surface
728
+ vol_point_tags, vol_line_tags_idx, vol_point_tags_idx = self.get_pts_for_sts(vol_surf_tags) # all tags of hex
729
+ vol_point_dict = {}
730
+ for p_tag in vol_point_tags:
731
+ xmin, ymin, zmin, xmax, ymax, zmax = gmsh.model.occ.getBoundingBox(0, p_tag)
732
+ vol_point_dict[p_tag] = {'x': (xmin + xmax) / 2, 'y': (ymin + ymax) / 2, 'z': (zmin + zmax) / 2}
733
+ z = []
734
+ y = []
735
+ x = []
736
+ for p_tag in surf_point_tags: # calculate average z position for the terminal surface
737
+ xmin, ymin, zmin, xmax, ymax, zmax = gmsh.model.occ.getBoundingBox(0, p_tag)
738
+ x.append((xmin + xmax) / 2)
739
+ y.append((ymin + ymax) / 2)
740
+ z.append((zmin + zmax) / 2)
741
+ x_avg = np.mean(x)
742
+ y_avg = np.mean(y)
743
+ z_avg = np.mean(z)
744
+ sign_x = [math.copysign(1, x_i - x_avg) for x_i in x]
745
+ sign_y = [math.copysign(1, y_i - y_avg) for y_i in y]
746
+ dist_x = []
747
+ dist_y = []
748
+ for j in range(len(x) - 1):
749
+ dist_x.append(math.sqrt((x[j] - x[j + 1]) ** 2))
750
+ dist_y.append(math.sqrt((y[j] - y[j + 1]) ** 2))
751
+ eq_len = self.cctdm.geometry.windings.wwhs[i] / 2
752
+ y_shift_sign = math.copysign(1, -terminal.z_air * self.cctdm.geometry.windings.alphas[i])
753
+ new_x = [x_avg + s_x * eq_len for s_x in sign_x]
754
+ new_y = [y_avg + s_y * eq_len + y_shift_sign * eq_len - y_shift_sign * dist_y[0] / 2 for s_y in sign_y]
755
+ for p_tag, x_n, y_n in zip(surf_point_tags, new_x, new_y): # assign z_avg to only points on the terminal surface
756
+ vol_point_dict[p_tag]['x'] = x_n
757
+ vol_point_dict[p_tag]['y'] = y_n
758
+ vol_point_dict[p_tag]['z'] = z_avg
759
+ gmsh.model.occ.remove([(3, terminal.vol_st[i])], recursive=True)
760
+ gmsh.model.occ.synchronize()
761
+ hexes = Hexahedrons() # create hex class instance to be able to reuse existing code for generating hexes geom_generators
762
+ hexes.hexes[0] = {} # only one winding layer is involved per iteration, so hardcoded 0 index
763
+ for p_i, (_, p_dict) in enumerate(vol_point_dict.items()):
764
+ hexes.hexes[0][p_i] = p_dict
765
+ Generate_BREPs.generate_hex_geom(hexes, cons_for_lines=terminal.lc_st[i], vol_tag=terminal.vol_st[i])
766
+ gmsh.model.occ.synchronize()
767
+ gmsh.write(os.path.join(self.model_folder, f'{name}.brep'))
768
+ if self.verbose:
769
+ print(f'Straightening terminals of {name} took {timeit.default_timer() - start_time:.2f} s')
770
+ if gui:
771
+ gmsh.model.occ.synchronize()
772
+ self.gu.launch_interactive_GUI()
773
+ else:
774
+ gmsh.clear()
775
+
776
+ def extend_terms(self, operation='extend', gui=False, ):
777
+ self.cctwi = uff.read_data_from_yaml(self.winding_info_file, WindingsInformation) # this is repeated as the operation over the data for the case add, so an original data for extend operation needs to be loaded.
778
+ if operation == 'add':
779
+ for terminal in [self.cctwi.windings.t_in, self.cctwi.windings.t_out]:
780
+ terminal.ndpterms = [1] * len(terminal.ndpterms)
781
+ terminal.z_air = terminal.z_add
782
+ elif operation == 'extend':
783
+ pass
784
+ file_in_postfix = ''
785
+ file_out_postfix = ''
786
+ for i, name in enumerate(self.cctwi.windings.names):
787
+ if self.verbose:
788
+ print(f'Extending terminals of {name} started')
789
+ start_time = timeit.default_timer()
790
+ gmsh.open(os.path.join(self.model_folder, f'{name}{file_in_postfix}.brep'))
791
+ volumes = [vol[1] for vol in gmsh.model.getEntities(dim=3)]
792
+ for oper, terminal in enumerate([self.cctwi.windings.t_in, self.cctwi.windings.t_out]):
793
+ vol_tag = terminal.vol_et[i]
794
+ vol_surf_tags = gmsh.model.getAdjacencies(3, vol_tag)[1]
795
+ surf_tag = terminal.surf_et[i]
796
+ if surf_tag not in vol_surf_tags:
797
+ raise ValueError(f'Surface tag of {surf_tag} given for volume in of {vol_tag} does not belong to the volume!')
798
+ surf_point_tags, surf_line_tags_idx, surf_point_tags_idx = self.get_pts_for_sts([surf_tag])
799
+ surf_point_tags = sorted(surf_point_tags)
800
+ vol_point_tags, vol_line_tags_idx, vol_point_tags_idx = self.get_pts_for_sts(vol_surf_tags)
801
+ vol_point_tags = sorted(vol_point_tags)
802
+ surf_point_tags_idx_pw = [i for i, e in enumerate(vol_point_tags) if e in set(surf_point_tags)] # powered
803
+ surf_point_tags_idx_oth = [i for i, e in enumerate(vol_point_tags) if e not in set(surf_point_tags)] # other (opposite powered)
804
+ vol_point_dict = {}
805
+ for idx_pw, p_tag in zip(surf_point_tags_idx_pw, surf_point_tags):
806
+ xmin, ymin, zmin, xmax, ymax, zmax = gmsh.model.occ.getBoundingBox(0, p_tag)
807
+ vol_point_dict[idx_pw] = {'x': (xmin + xmax) / 2, 'y': (ymin + ymax) / 2, 'z': (zmin + zmax) / 2}
808
+ z_ext = round((terminal.z_air - zmin) / terminal.ndpterms[i], 8)
809
+ for idx_oth, p_tag in zip(surf_point_tags_idx_oth, surf_point_tags):
810
+ xmin, ymin, zmin, xmax, ymax, zmax = gmsh.model.occ.getBoundingBox(0, p_tag)
811
+ vol_point_dict[idx_oth] = {'x': (xmin + xmax) / 2, 'y': (ymin + ymax) / 2, 'z': (zmin + zmax) / 2 + z_ext}
812
+ hexes = Hexahedrons() # create hex class instance to be able to reuse existing code for generating hexes geom_generators
813
+ for h in range(terminal.ndpterms[i]):
814
+ hexes.hexes[h] = {}
815
+ for p_i, (_, p_dict) in enumerate(vol_point_dict.items()):
816
+ hexes.hexes[h][p_i] = copy.deepcopy(p_dict)
817
+ for p_t, p_dict in vol_point_dict.items():
818
+ p_dict['z'] = p_dict['z'] + z_ext
819
+ for idx_oth in range(4, 8):
820
+ z_air = terminal.z_air
821
+ hexes.hexes[h][idx_oth]['z'] = z_air
822
+ tags_dict = Generate_BREPs.generate_hex_geom(hexes, cons_for_lines=terminal.lc_et[i], vol_tag=-1)
823
+ new_volumes = []
824
+ for _, hex_dict in tags_dict.items():
825
+ new_volumes.append(hex_dict['v_t'])
826
+ gmsh.model.occ.synchronize()
827
+ if oper == 0: # this vol_in is needed for reordering volumes later
828
+ vol_in = new_volumes
829
+ renumbered_vols = []
830
+ other_vols = list(set(volumes) - set(vol_in))
831
+ max_tag = gmsh.model.occ.getMaxTag(3)
832
+ for vol in other_vols:
833
+ gmsh.model.setTag(3, vol, vol + max_tag)
834
+ renumbered_vols.append(vol + max_tag)
835
+ for n_tag, vol in enumerate(reversed(vol_in)):
836
+ gmsh.model.setTag(3, vol, n_tag + 1)
837
+ max_term_tag = len(vol_in) + 1
838
+ spiral_volumes = []
839
+ for n_tag, vol in enumerate(renumbered_vols):
840
+ gmsh.model.setTag(3, vol, n_tag + max_term_tag)
841
+ spiral_volumes.append(n_tag + max_term_tag)
842
+ gmsh.write(os.path.join(self.model_folder, f'{name}{file_out_postfix}.brep'))
843
+ if self.verbose:
844
+ print(f'Straightening terminals of {name} took {timeit.default_timer() - start_time:.2f} s')
845
+ print(f'Writing volume information file: {name}.vi ')
846
+ vi = {'export': spiral_volumes, 'all': [vol[1] for vol in gmsh.model.getEntities(dim=3)]}
847
+ json.dump(vi, open(f"{os.path.join(self.model_folder, name)}.vi", 'w'))
848
+ if self.verbose:
849
+ print(f'Done writing volume information file: {name}.vi ')
850
+ if gui:
851
+ self.gu.launch_interactive_GUI()
852
+ else:
853
+ gmsh.clear()
854
+
855
+ def save_fqpl_vi(self):
856
+ for f_name in self.cctdm.geometry.fqpls.names:
857
+ if self.verbose:
858
+ print(f'Writing volume information file: {f_name}.vi ')
859
+ gmsh.open(os.path.join(self.model_folder, f'{f_name}.brep'))
860
+ volumes = [vol[1] for vol in gmsh.model.getEntities(dim=3)]
861
+ export = volumes
862
+ vi = {'export': export, 'all': volumes}
863
+ json.dump(vi, open(f"{os.path.join(self.model_folder, f_name)}.vi", 'w'))
864
+ if self.verbose:
865
+ print(f'Done writing volume information file: {f_name}.vi ')
866
+ gmsh.clear()
867
+
868
+ def fragment(self, gui=False):
869
+ if self.verbose:
870
+ print('Loading files in preparation for fragment operation')
871
+ for f_name in self.cctwi.w_names + self.cctwi.f_names:
872
+ gmsh.merge(os.path.join(self.model_folder, f'{f_name}.brep'))
873
+ num_vol = np.max([vol[1] for vol in gmsh.model.getEntities(dim=3)])
874
+ for f_name in self.cctwi.formers:
875
+ gmsh.merge(os.path.join(self.model_folder, f'{f_name}.brep'))
876
+ num_vol += 1
877
+ gmsh.merge(os.path.join(self.model_folder, f'{self.cctwi.air}.brep'))
878
+ num_vol += 1
879
+ entities = gmsh.model.getEntities(dim=3)
880
+ if len(entities) != num_vol:
881
+ raise ValueError('Not consistent volumes numbers in brep and json files!')
882
+ objectDimTags = [entities[-1]]
883
+ toolDimTags = entities[:-1]
884
+ # central_line = [(1, 3898)]
885
+ # toolDimTags = toolDimTags+central_line
886
+ if self.verbose:
887
+ print(f'Fragmenting {self.magnet_name} started')
888
+ start_time = timeit.default_timer()
889
+ gmsh.model.occ.fragment(objectDimTags, toolDimTags, removeObject=True, removeTool=True)
890
+ gmsh.model.occ.synchronize()
891
+ if self.verbose:
892
+ print(f'Fragmenting {self.magnet_name} took {timeit.default_timer() - start_time:.2f} s')
893
+
894
+ gmsh.write(self.model_file)
895
+ if gui:
896
+ self.gu.launch_interactive_GUI()
897
+ else:
898
+ gmsh.clear()
899
+ gmsh.finalize()
900
+
901
+ def load_geometry(self, gui=False):
902
+ gmsh.open(self.model_file)
903
+ if gui:
904
+ self.gu.launch_interactive_GUI()