fiqus 2025.10.0__py3-none-any.whl → 2025.12.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 (27) hide show
  1. fiqus/MainFiQuS.py +24 -17
  2. fiqus/data/DataConductor.py +9 -1
  3. fiqus/data/DataFiQuS.py +3 -3
  4. fiqus/data/DataFiQuSConductorAC_Rutherford.py +569 -0
  5. fiqus/data/DataFiQuSHomogenizedConductor.py +478 -0
  6. fiqus/geom_generators/GeometryConductorAC_Rutherford.py +706 -0
  7. fiqus/geom_generators/GeometryConductorAC_Strand_RutherfordCopy.py +1848 -0
  8. fiqus/geom_generators/GeometryHomogenizedConductor.py +183 -0
  9. fiqus/getdp_runners/RunGetdpConductorAC_Rutherford.py +200 -0
  10. fiqus/getdp_runners/RunGetdpHomogenizedConductor.py +178 -0
  11. fiqus/mains/MainConductorAC_Rutherford.py +76 -0
  12. fiqus/mains/MainHomogenizedConductor.py +112 -0
  13. fiqus/mesh_generators/MeshConductorAC_Rutherford.py +235 -0
  14. fiqus/mesh_generators/MeshConductorAC_Strand_RutherfordCopy.py +718 -0
  15. fiqus/mesh_generators/MeshHomogenizedConductor.py +229 -0
  16. fiqus/post_processors/PostProcessAC_Rutherford.py +142 -0
  17. fiqus/post_processors/PostProcessHomogenizedConductor.py +114 -0
  18. fiqus/pro_templates/combined/ConductorACRutherford_template.pro +1742 -0
  19. fiqus/pro_templates/combined/HomogenizedConductor_template.pro +1663 -0
  20. {fiqus-2025.10.0.dist-info → fiqus-2025.12.0.dist-info}/METADATA +6 -5
  21. {fiqus-2025.10.0.dist-info → fiqus-2025.12.0.dist-info}/RECORD +27 -11
  22. tests/test_geometry_generators.py +20 -0
  23. tests/test_mesh_generators.py +38 -0
  24. tests/test_solvers.py +100 -0
  25. {fiqus-2025.10.0.dist-info → fiqus-2025.12.0.dist-info}/LICENSE.txt +0 -0
  26. {fiqus-2025.10.0.dist-info → fiqus-2025.12.0.dist-info}/WHEEL +0 -0
  27. {fiqus-2025.10.0.dist-info → fiqus-2025.12.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,706 @@
1
+ import os
2
+ import math
3
+ import logging
4
+ import json
5
+ import shutil
6
+ import timeit
7
+ import pickle
8
+
9
+ import numpy as np
10
+ import gmsh
11
+ from itertools import zip_longest
12
+
13
+ import fiqus.data.DataFiQuSConductorAC_Strand as geom
14
+ from fiqus.utils.Utils import GmshUtils
15
+ from fiqus.utils.Utils import FilesAndFolders
16
+ from fiqus.parsers.ParserCOND import ParserCOND
17
+ from fiqus.data import RegionsModelFiQuS as Reg_Mod_FiQ
18
+ from fiqus.utils.Utils import FilesAndFolders as UFF
19
+ from abc import ABC, abstractmethod
20
+ from pydantic import BaseModel
21
+ from typing import (Dict, List)
22
+
23
+ import matplotlib.pyplot as plt
24
+ import matplotlib.patches as patches
25
+
26
+ import fiqus.geom_generators.GeometryConductorAC_Strand_RutherfordCopy as StrandGeom
27
+
28
+ logger = logging.getLogger('FiQuS')
29
+
30
+ class RoundedQuadrilateral(StrandGeom.Surface):
31
+ def __init__(self, corner_points, corner_arc_rad):
32
+ super().__init__()
33
+ self.corner_points = corner_points
34
+ self.r = corner_arc_rad
35
+ self.boundary_curves = self.generate_boundary_curves()
36
+
37
+ def generate_boundary_curves(self):
38
+ if self.r == 0: # If the trapezoid is not rounded, return the edge lines.
39
+ return [StrandGeom.Line.create_or_get(self.corner_points[i], self.corner_points[(i+1)%4]) for i in range(4)]
40
+ corner_circle_arcs = []
41
+ boundary_curves = []
42
+
43
+ for i, p in enumerate(self.corner_points): # Create the corner circle arcs
44
+ p_prev = self.corner_points[(i-1)%4] # Previous point in the list of corner points
45
+ p_next = self.corner_points[(i+1)%4] # Next point in the list of corner points
46
+ v1 = (p_prev - p) / np.linalg.norm(p_prev - p) # Unit vector from p to p_prev
47
+ v2 = (p_next - p) / np.linalg.norm(p_next - p) # Unit vector from p to p_next
48
+ angle = np.arccos(np.dot(v1, v2)) # Angle between v1 and v2
49
+ circle_arc_center = p + self.r / np.sin(angle/2) * (v1 + v2) / np.linalg.norm(v1 + v2) # Center of the circle arc
50
+ arc_p1 = p + v1*np.dot(v1, circle_arc_center - p) # First point on the circle arc
51
+ arc_p2 = p + v2*np.dot(v2, circle_arc_center - p) # Second point on the circle arc
52
+ # The rounded corners should be represented as circle-arcs, but must be ellipse-arcs to allow for snapping close points together.
53
+ # Circle-arcs would need the points to be exact.
54
+ arc = StrandGeom.EllipseArc(arc_p1, circle_arc_center, arc_p1, arc_p2) # Create the rounded corner ellipse-arc
55
+ if np.linalg.norm(arc.P1.pos-arc.C.pos) < np.linalg.norm(arc.P2.pos-arc.C.pos):# Make sure that the major axis is greater than the minor axis
56
+ arc.M = arc.P2
57
+ arc.points[-1] = arc.M
58
+
59
+ if i > 0:
60
+ if corner_circle_arcs[i-1].P2 is not arc.P1:
61
+ # If the subsequent circle-arcs are not connected we want to connect them with a line element
62
+ line = StrandGeom.Line.create_or_get(corner_circle_arcs[i-1].P2, arc.P1) # Create the edge line
63
+ boundary_curves.append(line)
64
+
65
+ boundary_curves.append(arc)
66
+ corner_circle_arcs.append(arc)
67
+
68
+ if corner_circle_arcs[-1].P2 is not corner_circle_arcs[0].P1:
69
+ line = StrandGeom.Line.create_or_get(corner_circle_arcs[-1].P2, corner_circle_arcs[0].P1)
70
+ boundary_curves.append(line)
71
+
72
+ return boundary_curves
73
+
74
+ @classmethod
75
+ def get_max_arc_radius(cls, corner_points):
76
+ """
77
+ Returns the maximum radius of the circle arc that can be used to round the corners.
78
+ """
79
+ # With increasing radius of the circle arc, the center of a circle arc will move along the bisector of the angle between the two lines that meet at the corner.
80
+ # At some maximum radius, two or more circle arc centers will coincide, and at any larger radius, the circle arcs will overlap, rendering the quadrilateral invalid.
81
+ # The maximum radius of the circle-arcs is therefore the minimum radius where two or more circle arc centers coincide.
82
+
83
+ # 1) Find the bisectors of the angles between the lines that meet at the corners.
84
+ bisectors = []
85
+ bisector_angles = []
86
+ for i, p in enumerate(corner_points):
87
+ p_prev = corner_points[(i-1)%4] # Previous point in the list of corner points
88
+ p_next = corner_points[(i+1)%4] # Next point in the list of corner points
89
+ v1 = (p_prev - p) / np.linalg.norm(p_prev - p) # Unit vector from p to p_prev
90
+ v2 = (p_next - p) / np.linalg.norm(p_next - p) # Unit vector from p to p_next
91
+ angle = np.arccos(np.dot(v1, v2)) # Angle between v1 and v2
92
+ bisector_angles.append(angle/2)
93
+ bisector = (v1 + v2) / np.linalg.norm(v1 + v2) # unit vector from p towards the circle arc center
94
+ bisectors.append(bisector)
95
+
96
+ # 2) Find the radii for which the circle-arc centers will lie at the points where the bisectors intersect
97
+ # The smallest such radius is the maximum radius of the circle arc that can be used to round the corners of the quadrilateral.
98
+ r = []
99
+ for i, v1 in enumerate(bisectors):
100
+ for j, v2 in enumerate(bisectors[i+1:]):
101
+ j = j+i+1
102
+ p1 = corner_points[i]
103
+ angle1 = bisector_angles[i]
104
+
105
+ p2 = corner_points[j]
106
+ angle2 = bisector_angles[j]
107
+
108
+ r_max = np.linalg.norm(p2-p1) / np.linalg.norm(v1/np.sin(angle1) - v2/np.sin(angle2)) # The radius for which two circle arc centers coincide at the intersection of the bisectors
109
+ r.append(r_max)
110
+ return min(r) # The smallest radius at which two or more circle arc centers coincide
111
+
112
+ @classmethod
113
+ def get_area(cls, corner_points, r):
114
+ """
115
+ Returns the area of a rounded quadrilateral with the given corner points and corner arc radius.
116
+ """
117
+ corner_points = corner_points[:, :2] # Remove the z-coordinate from the corner points
118
+ # Area of the quadrilateral without rounded corners is calculated using the shoelace formula:
119
+ quadrilateral_area = 0.5 * np.abs(np.dot(corner_points[:, 0], np.roll(corner_points[:, 1], -1)) - np.dot(corner_points[:, 1], np.roll(corner_points[:, 0], -1)))
120
+
121
+ # When the corners are rounded, the area of the quadrilateral is reduced by r^2 * k(angle/2) (for each corner),
122
+ # where r is the radius of the circle arcs and k(angle/2) is a factor that depends on the angle between the two lines that meet at the corner.
123
+ k = lambda angle: np.pi/2 - angle - 1/np.tan(angle)
124
+ area = quadrilateral_area
125
+ for i, p in enumerate(corner_points):
126
+ p_prev = corner_points[(i-1)%4] # Previous point in the list of corner points
127
+ p_next = corner_points[(i+1)%4] # Next point in the list of corner points
128
+ v1 = (p_prev - p) / np.linalg.norm(p_prev - p) # Unit vector from p to p_prev
129
+ v2 = (p_next - p) / np.linalg.norm(p_next - p) # Unit vector from p to p_next
130
+ angle = np.arccos(np.dot(v1, v2)) # Angle between v1 and v2
131
+ area += r**2 * k(angle/2)
132
+
133
+ return area
134
+
135
+ @classmethod
136
+ def get_arc_radius(cls, desired_area, corner_points):
137
+ """
138
+ Returns the radius of the circle arc that is required to create a rounded quadrilateral with the desired area.
139
+ """
140
+ corner_points = corner_points[:, :2]
141
+ # Area of the quadrilateral without rounded corners is calculated using the shoelace formula:
142
+ quadrilateral_area = 0.5 * np.abs(np.dot(corner_points[:, 0], np.roll(corner_points[:, 1], -1)) - np.dot(corner_points[:, 1], np.roll(corner_points[:, 0], -1)))
143
+
144
+ # When the corners are rounded, the area of the quadrilateral is reduced by r^2 * k(angle/2) for each corner,
145
+ # where r is the radius of the circle arcs and k(angle/2) is a factor that depends on the angle between the two lines that meet at the corner.
146
+ k = lambda angle: np.pi/2 - angle - 1/np.tan(angle)
147
+ angles = []
148
+ for i, p in enumerate(corner_points):
149
+ p_prev = corner_points[(i-1)%4] # Previous point in the list of corner points
150
+ p_next = corner_points[(i+1)%4] # Next point in the list of corner points
151
+ v1 = (p_prev - p) / np.linalg.norm(p_prev - p) # Unit vector from p to p_prev
152
+ v2 = (p_next - p) / np.linalg.norm(p_next - p) # Unit vector from p to p_next
153
+ angle = np.arccos(np.dot(v1, v2)) # Angle between v1 and v2
154
+ angles.append(angle)
155
+ r = np.sqrt( (desired_area - (quadrilateral_area+1e-10) )/ sum([k(angle/2) for angle in angles]) ) # Radius of the circle arc that is required to create a rounded trapezoid with the desired area.
156
+ return r
157
+
158
+ class RutherfordCable:
159
+ def __init__(self) -> None:
160
+ self.strands : List[StrandGeom.Surface] = []
161
+ self.air : List[StrandGeom.Surface] = [] # TODO: This needs to be a list of all the air-surfaces (including the air between the strands). They must all be added to the physical group 'Air'.
162
+ self.coating : StrandGeom.Surface = None
163
+
164
+ self.excitation_coils : List[StrandGeom.Surface] = []
165
+
166
+ self.cable_boundary_physical_group_tag = None
167
+ self.cable_boundary_physical_group_name = None
168
+ self.cable_boundary_curve_tags = []
169
+ self.cable_boundary_curve_loop_tag = None
170
+
171
+ # Coating
172
+ # self.coating_curve_tags = []
173
+ # self.coating_surface_tag = None
174
+ # self.coating_curve_loop_tag = None
175
+ # self.coating_physical_group_tag = None
176
+
177
+ def create_geometry(self, N_strands_per_layer, cable_width, cable_height_min, cable_height_max, strand_area, min_roundness_fraction, coating_thickness, coating_corner_arc_radius, air_radius, keep_strand_area, coil_center_points, coil_widths, coil_heights):
178
+ """
179
+ Creates the geometry of the Rutherford cable.
180
+ """
181
+
182
+ strand_height_min = cable_height_min / 2
183
+ strand_height_max = cable_height_max / 2
184
+
185
+ if not keep_strand_area:
186
+ avg_height = (strand_height_min + strand_height_max) / 2
187
+ strand_width = cable_width / N_strands_per_layer
188
+ max_arc_rad = RoundedQuadrilateral.get_max_arc_radius(np.array([[0, 0, 0], [strand_width, 0, 0], [strand_width, avg_height, 0], [0, avg_height, 0]]))
189
+ strand_area = RoundedQuadrilateral.get_area(np.array([[0, 0, 0], [strand_width, 0, 0], [strand_width, avg_height, 0], [0, avg_height, 0]]), min_roundness_fraction * max_arc_rad)
190
+
191
+ # The rutherford cable strands will now be created as a series of trapezoids.
192
+ height = lambda x: strand_height_min + (strand_height_max-strand_height_min)/cable_width*x # Height of the trapezoids as a function of the x-coordinate
193
+ w1 = cable_width/(N_strands_per_layer) # Strand width (unaltered)
194
+ delta_w = 0
195
+
196
+ #1) Check if the first strand can be created without altering the width.
197
+ # If the first strand, with minimum arc-radius, has an area below the specified area, the width of the first strand must be increased.
198
+ s1_max_arc_rad = RoundedQuadrilateral.get_max_arc_radius(np.array([[0, 0, 0], [w1, 0, 0], [w1, height(w1), 0], [0, height(0), 0]]))
199
+ s1_min_arc_rad = min_roundness_fraction*s1_max_arc_rad
200
+ s1_area_squared = RoundedQuadrilateral.get_area(np.array([[0, 0, 0], [w1, 0, 0], [w1, height(w1), 0], [0, height(0), 0]]), s1_min_arc_rad)
201
+
202
+ if s1_area_squared < strand_area - 1e-10:
203
+ # print("The first strand cannot be created without altering the width.")
204
+ # If the first strand cannot be created without altering the width, the width must be adjusted
205
+ # by finding the width that gives the specified area for the first strand, given that it is fully squared.
206
+ w1 = cable_width*(np.sqrt(strand_height_min**2 + 2*(strand_area+s1_min_arc_rad**2*(np.pi-2))*(strand_height_max-strand_height_min)/cable_width) - strand_height_min)/(strand_height_max-strand_height_min) # Width of the first strand
207
+ delta_w = 2*(cable_width-N_strands_per_layer*w1)/(N_strands_per_layer*(N_strands_per_layer-1)) # The remaining strand widths will be incremented by delta_w to keep the total width constant.
208
+
209
+ #2) Check if the area of the first strand, when fully rounded, is greater than the specified area.
210
+ # If it is, then the geometry can not be made since the smallest strand is greater than the specified area.
211
+ s1_rounded_arc_radius = RoundedQuadrilateral.get_max_arc_radius(np.array([[0, 0, 0], [w1, 0, 0], [w1, height(w1), 0], [0, height(0), 0]]))
212
+ s1_area_rounded = RoundedQuadrilateral.get_area(np.array([[0, 0, 0], [w1, 0, 0], [w1, height(w1), 0], [0, height(0), 0]]), s1_rounded_arc_radius)
213
+ if s1_area_rounded > strand_area:
214
+ raise Exception(f"The area of the smallest possible strand is greater than the specified area by a factor of {s1_area_rounded/strand_area}.")
215
+
216
+ x = lambda n: n*w1 + n*(n-1)/2*delta_w # Function for the x-coordinate of the leftmost point of strand n (the width of strand n is x(n+1)-x(n))
217
+
218
+ #3) Check if the last strand, when fully squared, is smaller than the specified area.
219
+ # If it is, then the geometry can not be made since the largest strand is smaller than the specified area.
220
+ sN_area_squared = RoundedQuadrilateral.get_area(np.array([[x(N_strands_per_layer-1), 0, 0], [x(N_strands_per_layer), 0, 0], [x(N_strands_per_layer), height(x(N_strands_per_layer)), 0], [x(N_strands_per_layer-1), height(x(N_strands_per_layer-1)), 0]]), 0)
221
+ if sN_area_squared < strand_area - 1e-10:
222
+ raise Exception(f"The area of the last strand is less than the specified area by a factor of {sN_area_squared/strand_area}.")
223
+
224
+ #4) Check if the last strand, when fully rounded, has an area greater than the specified area.
225
+ # If it does, then the geometry can not be made since the last strand is larger than the specified area.
226
+ # This problem should only arise if the last strand is elongated in the y-direction. In which case the cable is 'pulled', not compressed.
227
+ sN_rounded_arc_radius = RoundedQuadrilateral.get_max_arc_radius(np.array([[x(N_strands_per_layer-1), 0, 0], [x(N_strands_per_layer), 0, 0], [x(N_strands_per_layer), height(x(N_strands_per_layer)), 0], [x(N_strands_per_layer-1), height(x(N_strands_per_layer-1)), 0]]))
228
+ sN_area_rounded = RoundedQuadrilateral.get_area(np.array([[x(N_strands_per_layer-1), 0, 0], [x(N_strands_per_layer), 0, 0], [x(N_strands_per_layer), height(x(N_strands_per_layer)), 0], [x(N_strands_per_layer-1), height(x(N_strands_per_layer-1)), 0]]), sN_rounded_arc_radius)
229
+ if sN_area_rounded > strand_area:
230
+ raise Exception(f"The minimum area of the last strand is greater than the specified area by a factor of {sN_area_rounded/strand_area}.")
231
+
232
+ #5) Create the strands
233
+ for layer in range(2):
234
+ for n in range(N_strands_per_layer):
235
+ sign = 1 if layer == 0 else -1 # Sign of the height of the strand (the second layer is inverted)
236
+ strand_corner_points = np.array([[x(n), 0, 0], [x(n+1), 0, 0], [x(n+1), sign*height(x(n+1)), 0], [x(n), sign*height(x(n)), 0]])
237
+ if layer == 1:
238
+ strand_corner_points = strand_corner_points[::-1] # Reverse the order of the corner points in the second layer
239
+ r = RoundedQuadrilateral.get_arc_radius(strand_area, strand_corner_points) # Radius of the circle arc that is required to create a rounded trapezoid with the desired area.
240
+ if r <= max((x(n+1) - x(n))/1e4, StrandGeom.Point.point_snap_tolerance):
241
+ r = 0 # If the radius is very small, it is likely due to numerical errors and should be set to zero.
242
+ strand = RoundedQuadrilateral(strand_corner_points, r)
243
+ self.strands.append(strand)
244
+
245
+ # 6) Reverse the second half of the list of strands to make the second layer of strands inverted. This is to have the strands in the correct counter-clockwise order.
246
+ self.strands = self.strands[:N_strands_per_layer] + self.strands[N_strands_per_layer:][::-1]
247
+
248
+ StrandGeom.Surface.replace_overlapping_edges() # Replace overlapping edges of the strands
249
+
250
+ # 7) Create a region representing a coating around the cable.
251
+ # a) Create the corner points of the trapezoid
252
+ corner_points = np.array([[-coating_thickness, strand_height_min+coating_thickness, 0], [-coating_thickness, -strand_height_min-coating_thickness, 0], [cable_width+coating_thickness, -strand_height_max-coating_thickness, 0], [cable_width+coating_thickness, strand_height_max+coating_thickness, 0]])
253
+ # b) Create the trapezoid
254
+ self.coating = RoundedQuadrilateral(corner_points, coating_corner_arc_radius)
255
+ # c) Get the combined boundary curves of the cable
256
+ cable_boundary_curves = sum([strand.boundary_curves for strand in self.strands], [])
257
+ # Subtract all curves that are shared between multiple strands
258
+ shared_curves = set([curve for curve in cable_boundary_curves if cable_boundary_curves.count(curve) > 1])
259
+ cable_boundary_curves = list(set(cable_boundary_curves) - shared_curves)
260
+
261
+ cable_boundary_loops = StrandGeom.Curve.get_closed_loops(cable_boundary_curves) # Get the closed loops that make up the boundaries between the air region and the strands.
262
+ outer_cable_boundary = max(cable_boundary_loops, key=len) # The outer cable-air boundary is the longest closed loop.
263
+
264
+ # d) Set the outer cable boundary as the inner boundary of the coating
265
+ # self.coating.inner_boundary_curves.append(outer_cable_boundary) # Do not append! Mysterious error will occur...
266
+ self.coating.inner_boundary_curves = [outer_cable_boundary]
267
+
268
+ # 8) Create optional excitation coils with source current
269
+ if coil_center_points:
270
+ n_coils = len(coil_center_points)
271
+ else:
272
+ n_coils = 0
273
+ for n in range(n_coils):
274
+ excitation_coil = StrandGeom.Rectangle(coil_center_points[n], coil_widths[n], coil_heights[n])
275
+ self.excitation_coils.append(excitation_coil)
276
+
277
+ # 9) Create the air region
278
+ # a) Create a disk that represents the outer air region
279
+ outer_air_region = StrandGeom.Disk([cable_width/2,0,0], air_radius)
280
+ # b) Set the inner boundary of the air region
281
+ outer_air_region.inner_boundary_curves.append(self.coating.boundary_curves)
282
+ for n in range(n_coils):
283
+ outer_air_region.inner_boundary_curves.append(self.excitation_coils[n].boundary_curves)
284
+ self.air.append(outer_air_region)
285
+ # c) Create the inner air regions (the air between the strands)
286
+ air_boundaries = cable_boundary_loops
287
+ air_boundaries.remove(outer_cable_boundary) # Remove the outer cable boundary from the list of air boundaries.
288
+ for air_boundary in air_boundaries:
289
+ air_region = StrandGeom.Surface(air_boundary)
290
+ self.air.append(air_region)
291
+
292
+ def write_geom_to_yaml(self, file_path):
293
+ # This function writes the geometry to a yaml file.
294
+ # The yaml file contains the coordinates of the points, the type of the curves and the indices of the points that make up the curves and the indices of the curves that make up the areas.
295
+ # Note: Only the strands are written to the yaml file. The air region is not included.
296
+ strands = self.strands
297
+ strand_curves = [curve for strand in strands for curve in strand.boundary_curves]
298
+ strand_points = [point for curve in strand_curves for point in curve.points]
299
+
300
+ points = {}
301
+ curves = {}
302
+ areas = {}
303
+
304
+ for p, point in enumerate(StrandGeom.Point.points_registry):
305
+ if point in strand_points:
306
+ points[p] = {"Coordinates": point.pos.tolist()}
307
+
308
+ for c, curve in enumerate(StrandGeom.Curve.curves_registry):
309
+ if curve in strand_curves:
310
+ curves[c] = {"Type": curve.__class__.__name__, "Points": []}
311
+ for point in curve.points:
312
+ point_id = StrandGeom.Point.points_registry.index(point)
313
+ curves[c]["Points"].append(point_id)
314
+
315
+ for a, area in enumerate(StrandGeom.Surface.surfaces_registry):
316
+ if area in strands:
317
+ areas[a] = {"Boundary": []}
318
+ for curve in area.boundary_curves:
319
+ curve_id = StrandGeom.Curve.curves_registry.index(curve)
320
+ areas[a]["Boundary"].append(curve_id)
321
+
322
+ geom_dict = {"Points": points, "Curves": curves, "Areas": areas}
323
+
324
+ UFF.write_data_to_yaml(file_path, geom_dict)
325
+
326
+ @classmethod
327
+ def read_geom_from_yaml(cls, file_path):
328
+ # This function loads the geometry from a yaml file.
329
+ # The yaml file contains the coordinates of the points, the type of the curves and the indices of the points that make up the curves
330
+ # and the indices of the curves that make up the strands.
331
+ geom_data = UFF.read_data_from_yaml(file_path, StrandGeom.Geom)
332
+ point_coords = [p.Coordinates for p in geom_data.Points.values()]
333
+ for pos in point_coords:
334
+ StrandGeom.Point.create_or_get(pos)
335
+
336
+ for curve in geom_data.Curves.values():
337
+ curve_type = curve.Type
338
+ points = [StrandGeom.Point.points_registry[p] for p in curve.Points]
339
+ globals()[curve_type].create_or_get(*points) # Creates a curve-entity of the specified type (which is added to the curve-registry).
340
+
341
+ rutherford_cable = cls()
342
+ for area in geom_data.Areas.values():
343
+ boundary_curves = [StrandGeom.Curve.curves_registry[c] for c in area.Boundary]
344
+ a = StrandGeom.Surface(boundary_curves)
345
+ rutherford_cable.strands.append(a)
346
+
347
+ return rutherford_cable
348
+
349
+ def create_gmsh_instance(self):
350
+
351
+ # When creating the gmsh instances of the strands, curve loops are created for each strand.
352
+ # The curve loops are oriented according to the orientation of the first curve in the loop and determine the orientation of the surface.
353
+ # We want all the strands to have the same orientation, so we need to make sure that the curve loops are oriented in the same direction.
354
+ # To do this, we will check the orientation of the first curve in each strand boundary.
355
+ # If the orientation is not counter-clockwise, we will 'roll' the curves in the boundary until the first curve is counter-clockwise.
356
+
357
+ surfaces = self.strands + [self.coating] + self.air + self.excitation_coils
358
+ # StrandGeom.Surface.set_correct_boundary_orientation(surfaces)
359
+
360
+ for s in surfaces:
361
+ # Determine orientation by:
362
+ # 1) Find the centroid of the boundary curves.
363
+ # 2) Look at the cross-product of the vectors from the centroid to the start and end points of the first curve.
364
+ # 3) If the cross-product is positive, the orientation is counter-clockwise. If it is negative, the orientation is clockwise.
365
+ # centroid = np.mean(sum([[curve.P1.pos, curve.P2.pos] for curve in s.boundary_curves], []), axis=0)
366
+ # first_curve = s.boundary_curves[0]
367
+ # v1 = first_curve.P1.pos - centroid
368
+ # v2 = first_curve.P2.pos - centroid
369
+ # orientation = np.sign(np.cross(v1, v2)[2])
370
+
371
+ # if orientation < 0:
372
+ # print("here for surface ", s)
373
+ # If the orientation is not counter-clockwise, check the next curve in the boundary.
374
+ # for i in range(1, len(s.boundary_curves)):
375
+ # next_curve = s.boundary_curves[i]
376
+ # v1 = next_curve.P1.pos - centroid
377
+ # v2 = next_curve.P2.pos - centroid
378
+ # orientation = np.sign(np.cross(v1, v2)[2])
379
+ # if orientation > 0:
380
+ # s.boundary_curves = s.boundary_curves[i:] + s.boundary_curves[:i] # Roll the curves in the boundary until the first curve is counter-clockwise.
381
+ # break
382
+
383
+ s.create_gmsh_instance()
384
+
385
+ def add_coating(self):
386
+ """
387
+ This function adds a coating around the entire cable. The coating is added as a surface that surrounds the cable.
388
+ We can add the coating by getting the combined outer boundary of the cable, scaling it up while keeping the center fixed, and creating a surface from the scaled boundary.
389
+ Alternatively we can simply add a trapezoid that surrounds the cable.
390
+
391
+ """
392
+ gmsh.model.occ.synchronize() # Synchronize the OCC model with the GMSH model.
393
+
394
+ # 1) Get the curves that make up the boundary of the cable.
395
+ strands = [strand.surface_tag for strand in self.strands]
396
+ strand_comnbined_boundary_tags = [tag for dim, tag in gmsh.model.get_boundary([(2, strand) for strand in strands], combined=True)]
397
+ strand_combined_boundary_curves = [StrandGeom.Curve.get_curve_from_tag(tag) for tag in strand_comnbined_boundary_tags]
398
+
399
+ air_boundaries = StrandGeom.Curve.get_closed_loops(strand_combined_boundary_curves) # Get the closed loops that make up the boundaries between the air region and the strands.
400
+ outer_cable_air_boundary = max(air_boundaries, key=len) # The outer cable-air boundary is the longest closed loop.
401
+
402
+ self.cable_boundary_curve_tags = [curve.tag for curve in outer_cable_air_boundary]
403
+ self.cable_boundary_curve_loop_tag = gmsh.model.occ.addCurveLoop(self.cable_boundary_curve_tags)
404
+
405
+
406
+ # 1.1) Find the strand which each curve in the boundary belongs to and store this in a dictionary.
407
+ curve_strand_dict = {}
408
+ for curve in outer_cable_air_boundary:
409
+ for strand in self.strands:
410
+ if curve in strand.boundary_curves:
411
+ curve_strand_dict[curve] = strand
412
+ break
413
+
414
+ # 2) Create copies of each curve in the boundary
415
+ cable_outline_curves = gmsh.model.occ.copy([(1, curve.tag) for curve in outer_cable_air_boundary])
416
+
417
+
418
+ # 3) Scale the curves individually by a scaling factor, around the center of mass of the strand they belong to.
419
+ scaling_factor = 1.1
420
+
421
+ for new_curve, curve in zip(cable_outline_curves, outer_cable_air_boundary):
422
+ strand = curve_strand_dict[curve]
423
+ center_of_mass = gmsh.model.occ.get_center_of_mass(2, strand.surface_tag)
424
+ gmsh.model.occ.dilate([(1, new_curve[1])], center_of_mass[0], center_of_mass[1], center_of_mass[2], scaling_factor, scaling_factor, scaling_factor)
425
+
426
+ # 4) Fuse the scaled curves
427
+ fused_curve = gmsh.model.occ.fuse([curve for curve in cable_outline_curves[:len(cable_outline_curves)//2]], [curve for curve in cable_outline_curves[len(cable_outline_curves)//2:]])[0]
428
+ logger.info(f"Fused curve: {fused_curve}")
429
+
430
+ gmsh.model.occ.synchronize()
431
+ # 5) Now we remove curves which do not belong to the fused curve.
432
+ # These curves have one point which is shared by three curves and one point which is only on one curve.
433
+ # 5.1) Create a dictionary with the points and the curves they belong to.
434
+ point_curves_dict = {}
435
+ for curve in fused_curve:
436
+ logger.info(f"Curve: {curve}, Boundary points: {gmsh.model.getBoundary([curve])}")
437
+ for point in gmsh.model.getBoundary([curve]):
438
+ if point not in point_curves_dict:
439
+ point_curves_dict[point] = []
440
+ point_curves_dict[point].append(curve)
441
+ logger.info(f"Point curves dict: {point_curves_dict}")
442
+ # 5.2) Find the curves corresponding to points which lie only on one curve.
443
+ single_point_curves = [curve for point, curves in point_curves_dict.items() if len(curves) == 1 for curve in curves]
444
+ logger.info(f"Single point curves: {single_point_curves}")
445
+ # 5.3) Remove the single point curves
446
+ gmsh.model.occ.remove([curve for curve in single_point_curves])
447
+
448
+ # 6) Create a surface from the fused curve
449
+ # Remove the deleted curves from the list of curves
450
+ self.coating_curve_tags = [curve[1] for curve in fused_curve if curve not in single_point_curves]
451
+ self.coating_curve_loop_tag = gmsh.model.occ.addCurveLoop(self.coating_curve_tags)
452
+ self.coating_surface_tag = gmsh.model.occ.add_plane_surface([self.coating_curve_loop_tag, self.cable_boundary_curve_loop_tag])
453
+ logger.info(f"Coating surface: {self.coating_surface_tag}")
454
+
455
+ def add_coating2(self, cable_width, cable_height_min, cable_height_max, coating_thickness):
456
+ gmsh.model.occ.synchronize() # Synchronize the OCC model with the GMSH model.
457
+ # Create a trapezoidal surface that surrounds the cable.
458
+ strand_height_min = cable_height_min / 2
459
+ strand_height_max = cable_height_max / 2
460
+ # 1) Create the corner points of the trapezoid
461
+ corner_points = np.array([[-coating_thickness, strand_height_min+coating_thickness, 0], [-coating_thickness, -strand_height_min-coating_thickness, 0], [cable_width+coating_thickness, -strand_height_max-coating_thickness, 0], [cable_width+coating_thickness, strand_height_max+coating_thickness, 0]])
462
+ # 2) Create the trapezoid
463
+ coating = RoundedQuadrilateral(corner_points, 0)
464
+ # for c in coating.boundary_curves: # Add the boundary curves gmsh instances to the coating.
465
+ # c.create_gmsh_instance()
466
+
467
+ # 3) Get the outer boundary of the cable
468
+ strands = [strand.surface_tag for strand in self.strands]
469
+ strand_comnbined_boundary_tags = [tag for dim, tag in gmsh.model.get_boundary([(2, strand) for strand in strands], combined=True)]
470
+ strand_combined_boundary_curves = [StrandGeom.Curve.get_curve_from_tag(tag) for tag in strand_comnbined_boundary_tags]
471
+
472
+ air_boundaries = StrandGeom.Curve.get_closed_loops(strand_combined_boundary_curves) # Get the closed loops that make up the boundaries between the air region and the strands.
473
+ outer_cable_boundary = max(air_boundaries, key=len) # The outer cable-air boundary is the longest closed loop.
474
+
475
+ # 4) Add the boundary curves of the cable to the coating inner boundary
476
+ # coating.inner_boundary_curves.append([curve for curve in outer_cable_boundary])
477
+
478
+
479
+ # 5) Create the curve loops of the coating
480
+ # coating.curve_loop_tag = StrandGeom.Surface.create_or_get_curve_loop(coating.boundary_curves) #gmsh.model.occ.addCurveLoop([c.tag for c in coating.boundary_curves])
481
+ # coating.inner_curve_loop_tags.append(StrandGeom.Surface.create_or_get_curve_loop(outer_cable_boundary)) #(gmsh.model.occ.addCurveLoop([c.tag for c in outer_cable_boundary]))
482
+ coating.create_gmsh_instance()
483
+
484
+ # 6) Create the surface of the coating
485
+ # coating.surface_tag = gmsh.model.occ.addPlaneSurface([coating.curve_loop_tag, coating.inner_curve_loop_tags[0]])
486
+
487
+ self.coating = coating
488
+
489
+
490
+
491
+ # def add_air(self, cable_width, air_radius):
492
+ # # Note: This must be done after the strand gmsh instances have been created.
493
+ # gmsh.model.occ.synchronize() # Synchronize the OCC model with the GMSH model.
494
+
495
+ # # Get the curves that make up the boundary of the 'fused' strands. This is the boundary separating the strands from the air.
496
+ # strands = [strand.surface_tag for strand in self.strands]
497
+ # strand_comnbined_boundary_tags = [tag for dim, tag in gmsh.model.get_boundary([(2, strand) for strand in strands], combined=True)]
498
+ # strand_combined_boundary_curves = [StrandGeom.Curve.get_curve_from_tag(tag) for tag in strand_comnbined_boundary_tags]
499
+
500
+ # air_boundaries = StrandGeom.Curve.get_closed_loops(strand_combined_boundary_curves) # Get the closed loops that make up the boundaries between the air region and the strands.
501
+ # outer_cable_air_boundary = max(air_boundaries, key=len) # The outer cable-air boundary is the longest closed loop.
502
+ # air_boundaries.remove(outer_cable_air_boundary) # Remove the outer air boundary from the list of air boundaries.
503
+
504
+ # outer_air_region = StrandGeom.Disk([cable_width/2,0,0], air_radius) # Create the body of the outer air region.
505
+ # for c in outer_air_region.boundary_curves: # Add the boundary curves gmsh instances to the outer air region.
506
+ # c.create_gmsh_instance()
507
+ # outer_air_region.inner_boundary_curves.append([curve for curve in outer_cable_air_boundary])
508
+
509
+ # outer_air_region.curve_loop_tag = gmsh.model.occ.addCurveLoop([c.tag for c in outer_air_region.boundary_curves]) # Create the outer curve loop of the outer air region.
510
+ # outer_air_region.inner_curve_loop_tags.append(gmsh.model.occ.addCurveLoop([c.tag for c in outer_cable_air_boundary])) # Create the inner curve loop of the outer air region. This is the outline of the entire cable.
511
+ # outer_air_region.surface_tag = gmsh.model.occ.addPlaneSurface([outer_air_region.curve_loop_tag, outer_air_region.inner_curve_loop_tags[0]])
512
+
513
+ # self.air.append(outer_air_region) # Add the outer air region to the list of air regions.
514
+
515
+ # # Create the inner air regions:
516
+ # for air_boundary in air_boundaries:
517
+ # air_region = StrandGeom.Surface(air_boundary)
518
+ # air_region.create_gmsh_instance()
519
+ # self.air.append(air_region)
520
+
521
+ def add_air2(self, cable_width, air_radius):
522
+ # Note: This must be done after the strand gmsh instances have been created.
523
+ gmsh.model.occ.synchronize() # Synchronize the OCC model with the GMSH model.
524
+
525
+ # Get the curves that make up the boundary of the 'fused' strands. This is the boundary separating the strands from the air.
526
+ strands = [strand.surface_tag for strand in self.strands]
527
+ strand_comnbined_boundary_tags = [tag for dim, tag in gmsh.model.get_boundary([(2, strand) for strand in strands], combined=True)]
528
+ strand_combined_boundary_curves = [StrandGeom.Curve.get_curve_from_tag(tag) for tag in strand_comnbined_boundary_tags]
529
+
530
+ air_boundaries = StrandGeom.Curve.get_closed_loops(strand_combined_boundary_curves) # Get the closed loops that make up the boundaries between the air region and the strands.
531
+ outer_cable_air_boundary = max(air_boundaries, key=len) # The outer cable-air boundary is the longest closed loop.
532
+ air_boundaries.remove(outer_cable_air_boundary) # Remove the outer air boundary from the list of air boundaries.
533
+
534
+ outer_air_region = StrandGeom.Disk([cable_width/2,0,0], air_radius) # Create the body of the outer air region.
535
+ for c in outer_air_region.boundary_curves: # Add the boundary curves gmsh instances to the outer air region.
536
+ c.create_gmsh_instance()
537
+ # outer_air_region.inner_boundary_curves.append([curve for curve in outer_cable_air_boundary])
538
+ outer_air_region.inner_boundary_curves.append(self.coating.boundary_curves)
539
+
540
+ outer_air_region.curve_loop_tag = gmsh.model.occ.addCurveLoop([c.tag for c in outer_air_region.boundary_curves]) # Create the outer curve loop of the outer air region.
541
+ # outer_air_region.inner_curve_loop_tags.append(gmsh.model.occ.addCurveLoop([c.tag for c in outer_cable_air_boundary])) # Create the inner curve loop of the outer air region. This is the outline of the entire cable.
542
+ outer_air_region.inner_curve_loop_tags.append(self.coating.curve_loop_tag)
543
+ outer_air_region.surface_tag = gmsh.model.occ.addPlaneSurface([outer_air_region.curve_loop_tag, outer_air_region.inner_curve_loop_tags[0]])
544
+
545
+ self.air.append(outer_air_region) # Add the outer air region to the list of air regions.
546
+
547
+ # Create the inner air regions:
548
+ for air_boundary in air_boundaries:
549
+ air_region = StrandGeom.Surface(air_boundary)
550
+ air_region.create_gmsh_instance()
551
+ self.air.append(air_region)
552
+
553
+
554
+ # def add_air(self, cable_width, air_radius):
555
+ # # Note: This must be done after the strand gmsh instances have been created.
556
+ # gmsh.model.occ.synchronize() # Synchronize the OCC model with the GMSH model.
557
+
558
+ # # # Get the curves that make up the boundary of the 'fused' strands. This is the boundary separating the strands from the air.
559
+ # strands = [strand.surface_tag for strand in self.strands]
560
+ # strand_comnbined_boundary_tags = [tag for dim, tag in gmsh.model.get_boundary([(2, strand) for strand in strands], combined=True)]
561
+ # strand_combined_boundary_curves = [StrandGeom.Curve.get_curve_from_tag(tag) for tag in strand_comnbined_boundary_tags]
562
+
563
+ # air_boundaries = StrandGeom.Curve.get_closed_loops(strand_combined_boundary_curves) # Get the closed loops that make up the boundaries between the air region and the strands.
564
+ # outer_cable_air_boundary = max(air_boundaries, key=len) # The outer cable-air boundary is the longest closed loop.
565
+ # air_boundaries.remove(outer_cable_air_boundary) # Remove the outer air boundary from the list of air boundaries.
566
+
567
+ # outer_air_region = StrandGeom.Disk([cable_width/2,0,0], air_radius) # Create the body of the outer air region.
568
+ # for c in outer_air_region.boundary_curves: # Add the boundary curves gmsh instances to the outer air region.
569
+ # c.create_gmsh_instance()
570
+ # # outer_air_region.inner_boundary_curves.append([curve for curve in outer_cable_air_boundary])
571
+
572
+ # outer_air_region.curve_loop_tag = gmsh.model.occ.addCurveLoop([c.tag for c in outer_air_region.boundary_curves]) # Create the outer curve loop of the outer air region.
573
+ # outer_air_region.inner_curve_loop_tags.append(self.coating_curve_loop_tag) # Create the inner curve loop of the outer air region. This is the outline of the entire cable.
574
+ # outer_air_region.surface_tag = gmsh.model.occ.addPlaneSurface([outer_air_region.curve_loop_tag, outer_air_region.inner_curve_loop_tags[0]])
575
+
576
+ # self.air.append(outer_air_region) # Add the outer air region to the list of air regions.
577
+
578
+ # # Create the inner air regions:
579
+ # for air_boundary in air_boundaries:
580
+ # air_region = StrandGeom.Surface(air_boundary)
581
+ # air_region.create_gmsh_instance()
582
+ # self.air.append(air_region)
583
+
584
+ def update_tags(self):
585
+ # for strand in self.strands:
586
+ # strand.update_tags()
587
+
588
+ # for air in self.air:
589
+ # air.update_tags()
590
+ surfaces = self.strands + self.air + [self.coating] + self.excitation_coils
591
+ StrandGeom.Surface.update_tags(surfaces)
592
+
593
+ def add_physical_groups(self):
594
+ # Add physical groups for the strands and the air regions.
595
+ # The strands each have a physical boundary and a physical surface.
596
+ for i, strand in enumerate(self.strands):
597
+ strand.add_physical_boundary(name=f"Boundary: Strand {i}")
598
+ strand.add_physical_surface(name=f"Surface: Strand {i}")
599
+
600
+ strand.physical_edge_point_tag = gmsh.model.addPhysicalGroup(0, [strand.boundary_curves[0].P1.tag])
601
+
602
+ # The air region is composed of multiple surfaces: The outer air region and the inner air regions between strands.
603
+ # The air has an outer physical boundary and all the air regions share a physical surface.
604
+ self.air[0].add_physical_boundary(f"Outer boundary: Air")
605
+ self.air[0].physical_surface_tag = gmsh.model.add_physical_group(2, [air.surface_tag for air in self.air], name = "Surface: Air")
606
+ self.air[0].physical_surface_name = "Surface: Air"
607
+ for i, air in enumerate(self.air[1:]):
608
+ # air.add_physical_boundary(f"Boundary: Inner air-region {i}")
609
+ air.physical_surface_tag = self.air[0].physical_surface_tag
610
+ air.physical_surface_name = "Surface: Air"
611
+ air.add_physical_boundary(f"Boundary: Inner air-region {i}")
612
+
613
+ # Add a physical boundary for the entire cable boundary
614
+ # self.cable_boundary_physical_group_tag = gmsh.model.addPhysicalGroup(1, , name = "Boundary: Cable")
615
+ # self.cable_boundary_physical_group_name = "Boundary: Cable"
616
+
617
+ # if self.coating:
618
+ # Add a physical surface and boundary for the coating
619
+ self.coating.add_physical_surface("Surface: Cable coating")
620
+ self.coating.add_physical_boundary("Boundary: Cable coating")
621
+ self.coating.physical_edge_point_tag = gmsh.model.addPhysicalGroup(0, [self.coating.boundary_curves[0].P1.tag])
622
+
623
+ # self.cable_boundary_physical_group_tag = gmsh.model.addPhysicalGroup(1, self.coating_curve_tags, name = "Boundary: Cable coating")
624
+ # self.cable_boundary_physical_group_name = "Boundary: Cable coating"
625
+
626
+ # # Add a physical surface for the coating
627
+ # self.coating_physical_group_tag = gmsh.model.addPhysicalGroup(2, [self.coating_surface_tag], name = "Surface: Cable coating")
628
+
629
+ # Add physical groups for the excitation coil regions
630
+ for i, coil in enumerate(self.excitation_coils):
631
+ coil.add_physical_boundary(name=f"Boundary: Excitation coil {i}")
632
+ coil.add_physical_surface(name=f"Surface: Excitation coil {i}")
633
+
634
+
635
+ def save(self, save_file):
636
+ with open(save_file, "wb") as geom_save_file:
637
+ pickle.dump(self, geom_save_file)
638
+ logger.info(f"Geometry saved to file {save_file}")
639
+
640
+ class Geometry(StrandGeom.Geometry):
641
+ def generate_cable_geometry(self, gui=False):
642
+ """
643
+ Generates the geometry of a Rutherford cable.
644
+ """
645
+ # gmsh.option.setNumber("Geometry.Tolerance", 1e-12)
646
+ conductor_name = self.cacdm.solve.conductor_name
647
+ cable_width = self.fdm.conductors[conductor_name].cable.bare_cable_width
648
+ cable_height_min = self.fdm.conductors[conductor_name].cable.bare_cable_height_low
649
+ cable_height_max = self.fdm.conductors[conductor_name].cable.bare_cable_height_high
650
+ cable_strands_per_layer = self.fdm.conductors[conductor_name].cable.n_strands_per_layers
651
+ strand_diameter = self.fdm.conductors[conductor_name].strand.diameter
652
+ strand_area = (strand_diameter/2)**2 * np.pi
653
+ air_radius = self.cacdm.geometry.air_radius
654
+ coating_thickness = self.cacdm.geometry.coating_thickness
655
+ coating_corner_arc_radius = self.cacdm.geometry.coating_corner_arc_radius
656
+ keep_strand_area = self.cacdm.geometry.keep_strand_area
657
+ coil_center_points = self.cacdm.geometry.excitation_coils.centers
658
+ coil_widths = self.cacdm.geometry.excitation_coils.widths
659
+ coil_heights = self.cacdm.geometry.excitation_coils.heights
660
+ StrandGeom.Point.point_snap_tolerance = strand_diameter*self.cacdm.geometry.point_snap_tolerance_relative_to_strand_diameter
661
+ min_roundness_factor = self.cacdm.geometry.min_roundness_factor
662
+
663
+
664
+ if self.cacdm.geometry.io_settings.load.load_from_yaml:
665
+ filename = self.cacdm.geometry.io_settings.load.filename
666
+ RC = RutherfordCable.read_geom_from_yaml(os.path.join(self.inputs_folder, filename))
667
+ else:
668
+ RC = RutherfordCable()
669
+ RC.create_geometry(cable_strands_per_layer, cable_width, cable_height_min, cable_height_max, strand_area+1e-10, min_roundness_factor, coating_thickness, coating_corner_arc_radius, air_radius, keep_strand_area, coil_center_points, coil_widths, coil_heights)
670
+
671
+
672
+ RC.create_gmsh_instance()
673
+
674
+ # Cut the coating to remove overlap with the strands
675
+ # gmsh.model.occ.cut([(2, RC.coating.surface_tag)], [(2, strand.surface_tag) for strand in RC.strands]+[(2, air.surface_tag) for air in RC.air[1:]], removeTool=False)
676
+
677
+ # RC.add_coating2(cable_width, cable_height_min, cable_height_max, 1e-5)
678
+
679
+ # RC.add_coating()
680
+
681
+ # RC.add_air(cable_width, air_radius)
682
+ # RC.add_air2(cable_width, air_radius)
683
+
684
+ # for strand in RC.strands + RC.air:
685
+ # strand.update_tags()
686
+
687
+
688
+ if self.cacdm.geometry.io_settings.save.save_to_yaml:
689
+ filename = self.cacdm.geometry.io_settings.save.filename
690
+ RC.write_geom_to_yaml(os.path.join(self.geom_folder, filename))
691
+
692
+
693
+ gmsh.model.occ.synchronize()
694
+
695
+
696
+ RC.add_physical_groups()
697
+ # RC.add_physical_groups2()
698
+ gmsh.write(self.geom_file)
699
+ RC.save(os.path.join(self.geom_folder, f'{self.magnet_name}.pkl'))
700
+
701
+ if gui:
702
+ self.gu.launch_interactive_GUI()
703
+ else:
704
+ if gmsh.isInitialized():
705
+ gmsh.clear()
706
+ gmsh.finalize()