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.
- fiqus/MainFiQuS.py +24 -17
- fiqus/data/DataConductor.py +9 -1
- fiqus/data/DataFiQuS.py +3 -3
- fiqus/data/DataFiQuSConductorAC_Rutherford.py +569 -0
- fiqus/data/DataFiQuSHomogenizedConductor.py +478 -0
- fiqus/geom_generators/GeometryConductorAC_Rutherford.py +706 -0
- fiqus/geom_generators/GeometryConductorAC_Strand_RutherfordCopy.py +1848 -0
- fiqus/geom_generators/GeometryHomogenizedConductor.py +183 -0
- fiqus/getdp_runners/RunGetdpConductorAC_Rutherford.py +200 -0
- fiqus/getdp_runners/RunGetdpHomogenizedConductor.py +178 -0
- fiqus/mains/MainConductorAC_Rutherford.py +76 -0
- fiqus/mains/MainHomogenizedConductor.py +112 -0
- fiqus/mesh_generators/MeshConductorAC_Rutherford.py +235 -0
- fiqus/mesh_generators/MeshConductorAC_Strand_RutherfordCopy.py +718 -0
- fiqus/mesh_generators/MeshHomogenizedConductor.py +229 -0
- fiqus/post_processors/PostProcessAC_Rutherford.py +142 -0
- fiqus/post_processors/PostProcessHomogenizedConductor.py +114 -0
- fiqus/pro_templates/combined/ConductorACRutherford_template.pro +1742 -0
- fiqus/pro_templates/combined/HomogenizedConductor_template.pro +1663 -0
- {fiqus-2025.10.0.dist-info → fiqus-2025.12.0.dist-info}/METADATA +6 -5
- {fiqus-2025.10.0.dist-info → fiqus-2025.12.0.dist-info}/RECORD +27 -11
- tests/test_geometry_generators.py +20 -0
- tests/test_mesh_generators.py +38 -0
- tests/test_solvers.py +100 -0
- {fiqus-2025.10.0.dist-info → fiqus-2025.12.0.dist-info}/LICENSE.txt +0 -0
- {fiqus-2025.10.0.dist-info → fiqus-2025.12.0.dist-info}/WHEEL +0 -0
- {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()
|