• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

WISDEM / WEIS / 17924227114

22 Sep 2025 06:04PM UTC coverage: 58.0% (-0.6%) from 58.572%
17924227114

Pull #430

github

dzalkind
Activate test mode for overrides example
Pull Request #430: Support for WindIO 2.0 and WISDEM 4.0

674 of 767 new or added lines in 17 files covered. (87.87%)

125 existing lines in 8 files now uncovered.

7913 of 13643 relevant lines covered (58.0%)

0.58 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

77.85
/weis/visualization/meshRender.py
1
import numpy as np
1✔
2
from dash_vtk.utils import to_mesh_state
1✔
3
import pyvista as pv
1✔
4

5
try:
1✔
6
    from vtkmodules.util.numpy_support import numpy_to_vtk
1✔
7
except:
×
NEW
8
    from vtk.util.numpy_support import numpy_to_vtk
×
9
import weis.inputs as sch
1✔
10

11

12
def render_blade(turbineData, local=True):
1✔
13

14
    # fetch airfoil names from array of airfoils
15
    airfoils_by_names = {}
1✔
16
    for a in turbineData['airfoils']:
1✔
17
        airfoils_by_names[a['name']] = a
1✔
18

19
    points = bladeMesh(turbineData['components']['blade'], airfoils_by_names)
1✔
20

21
    if local:
1✔
22
        mesh_state, mesh = render_our_own_delaunay(points)
×
23

24
    else:
25
        ## transforming the blade to be in the global coordinate system
26

27
        # Rotate about z-axis by 90-deg so that x is from HP to LP surface
28
        points = rotation_transformation(points, [0, 0, np.pi/2])
1✔
29

30
        # Translate to the hub position
31
        points = translation_transformation(points, [0, 0, turbineData['components']['hub']['diameter'] / 2])
1✔
32

33
        # if turbineData['assembly']['rotor_orientation'] has downwind (case insensitive) then rotate set downwindScalar to -1
34
        downwindScalar = -1 if 'downwind' in turbineData['assembly']['rotor_orientation'].lower() else 1
1✔
35

36
        # rotate about y-axis by the cone angle
37
        coneAngle = np.deg2rad(turbineData['components']['hub']['cone_angle']) * -1 * downwindScalar # in rads is about x-axis
1✔
38
        blade_1 = rotation_transformation(points, [0,           coneAngle,  0])
1✔
39
        # blade_2 = rotation_transformation(blade_1, [2*np.pi/3,   0,  0])
40
        # blade_3 = rotation_transformation(blade_1, [4*np.pi/3,   0,  0])
41
        # This achieves the symmetry of the rotor.
42

43
        _, mesh_1 = render_our_own_delaunay(blade_1)
1✔
44
        mesh_2 = mesh_1.rotate_x( 2*np.pi/3 * 180/np.pi, point=(0,0,0), inplace=False)
1✔
45
        mesh_3 = mesh_1.rotate_x( 4*np.pi/3 * 180/np.pi, point=(0,0,0), inplace=False)
1✔
46

47

48
        # angle by the tilt of the rotor
49
        tiltAngle = np.deg2rad(turbineData['components']['drivetrain']['outer_shape']['uptilt']) * downwindScalar # in rads is about y-axis
1✔
50
        # blade_1 = rotation_transformation(blade_1, [0, tiltAngle, 0])
51
        # blade_2 = rotation_transformation(blade_2, [0, tiltAngle, 0])
52
        # blade_3 = rotation_transformation(blade_3, [0, tiltAngle, 0])
53

54
        mesh_1 = mesh_1.rotate_y(tiltAngle * 180/np.pi, point=(0,0,0), inplace=False)
1✔
55
        mesh_2 = mesh_2.rotate_y(tiltAngle * 180/np.pi, point=(0,0,0), inplace=False)
1✔
56
        mesh_3 = mesh_3.rotate_y(tiltAngle * 180/np.pi, point=(0,0,0), inplace=False)
1✔
57

58
        # Tanslation along the z-axis to the hub height
59
        zTranslation = turbineData['assembly']['hub_height']
1✔
60
        # Translation in the x-axis
61
        xTranslation = turbineData['components']['drivetrain']['outer_shape']['distance_tt_hub'] * downwindScalar * np.cos(tiltAngle) * -1
1✔
62

63
        # blade_1 = translation_transformation(blade_1, [xTranslation, 0, zTranslation])
64
        # blade_2 = translation_transformation(blade_2, [xTranslation, 0, zTranslation])
65
        # blade_3 = translation_transformation(blade_3, [xTranslation, 0, zTranslation])
66

67
        mesh_1 = mesh_1.translate((xTranslation, 0, zTranslation), inplace=False)
1✔
68
        mesh_2 = mesh_2.translate((xTranslation, 0, zTranslation), inplace=False)
1✔
69
        mesh_3 = mesh_3.translate((xTranslation, 0, zTranslation), inplace=False)
1✔
70

71
        # _, mesh_1 = render_our_own_delaunay(blade_1)
72
        # _, mesh_2 = render_our_own_delaunay(blade_2)
73
        # _, mesh_3 = render_our_own_delaunay(blade_3)
74

75
        mesh = mesh_1.merge(mesh_2).merge(mesh_3)
1✔
76

77
        mesh_state = to_mesh_state(mesh)
1✔
78

79
    # extracting all the points from the 
80
    rotor = mesh.points
1✔
81

82
    extremes = extractExtremes(rotor)
1✔
83

84
    return mesh_state, mesh, extremes
1✔
85

86
def render_hub(turbineData, local=True):
1✔
87

88
    points = hubMesh(turbineData['components']['hub'])
1✔
89

90
    if local:
1✔
91
        mesh_state, mesh = render_our_own_delaunay(points)
×
92
    
93
    else:
94
        # if turbineData['assembly']['rotor_orientation'] has downwind (case insensitive) then rotate set downwindScalar to -1
95
        downwindScalar = -1 if 'downwind' in turbineData['assembly']['rotor_orientation'].lower() else 1
1✔
96

97
        # First rotate hub to align with x-axis (90 degrees about y), 
98
        # with additional 180 deg rotation for downwind configuration
99
        points = rotation_transformation(points, [0, np.pi/2 + np.pi*int(downwindScalar==-1), 0])
1✔
100

101
        # Apply tilt angle rotation
102
        tiltAngle = np.deg2rad(turbineData['components']['drivetrain']['outer_shape']['uptilt']) # in rads is about y-axis
1✔
103
        points = rotation_transformation(points, [0, tiltAngle, 0])
1✔
104

105
        # For downwind configuration, rotate 180 degrees about z-axis
106
        if downwindScalar == -1:
1✔
107
            points = rotation_transformation(points, [0, 0, np.pi])
×
108

109
        # Tanslation along the z-axis to the hub height
110
        zTranslation = turbineData['assembly']['hub_height']
1✔
111
        # Translation in the x-axis (account for downwind configuration)
112
        xTranslation = turbineData['components']['drivetrain']['outer_shape']['distance_tt_hub'] * downwindScalar * np.cos(tiltAngle) * -1
1✔
113

114
        points = translation_transformation(points, [xTranslation, 0, zTranslation])
1✔
115

116
        mesh_state, mesh = render_our_own_delaunay(points)
1✔
117

118
    extremes = extractExtremes(points)
1✔
119

120
    return mesh_state, mesh, extremes
1✔
121

122
def render_Tower(turbineData, local=True):
1✔
123

124
    points = towerMesh(turbineData['components']['tower'])
1✔
125

126
    if local:
1✔
127
        mesh_state, mesh = render_our_own_delaunay(points)
1✔
128
    else:
129
        # Tower does not have any orientation or tilt or translations
130
        mesh_state, mesh = render_our_own_delaunay(points)
1✔
131

132
    extremes = extractExtremes(points)
1✔
133

134
    return mesh_state, mesh, extremes
1✔
135

136

137
def render_nacelle(turbineData, local=True):
1✔
138

139
    points = nacelleMesh(turbineData['components']['drivetrain'],turbineData['components']['hub'])
1✔
140

141
    if local:
1✔
142
        mesh_state, mesh = render_our_own_delaunay(points)
×
143
    else:
144

145
        # if turbineData['assembly']['rotor_orientation'] has downwind (case insensitive) then rotate set downwindScalar to -1
146
        downwindScalar = -1 if 'downwind' in turbineData['assembly']['rotor_orientation'].lower() else 1
1✔
147

148
        # need to translate it above the tower, the nacelle is always above the tower
149
        zTranslation = turbineData['assembly']['hub_height']
1✔
150

151
        # move the nacelle so that the -x face is touching the hub
152
        xTranslation = turbineData['components']['drivetrain']['outer_shape']['overhang']  * 0.5
1✔
153

154
        # If downwind, rotate nacelle 180 degrees about z-axis
155
        if downwindScalar == -1:
1✔
156
            points = rotation_transformation(points, [0, 0, np.pi])
×
157

158
            xTranslation = -xTranslation  # Reverse the x-translation for downwind configuration
×
159

160
        points = translation_transformation(np.array(points), [xTranslation, 0, zTranslation])
1✔
161

162
        mesh_state, mesh = render_our_own_delaunay(points)
1✔
163

164
    extremes = extractExtremes(points)
1✔
165

166
    return mesh_state, mesh, extremes
1✔
167

168
def render_monopile(turbineData, local=True):
1✔
169

170
    points = monopileMesh(turbineData['components']['monopile'])
1✔
171

172
    if local:
1✔
173
        mesh_state, mesh = render_our_own_delaunay(points)
×
174
    else:
175
        # Monopile does not have any orientation or tilt or translations
176
        mesh_state, mesh = render_our_own_delaunay(points)
1✔
177

178
    extremes = extractExtremes(points)
1✔
179

180
    return mesh_state, mesh, extremes
1✔
181

182
def render_floatingPlatform(turbineData, local=True):
1✔
183

184
    mesh = floatingPlatformMesh(turbineData['components']['floating_platform'],
1✔
185
                                turbineData['components']['mooring']) # this function unlike other components returns a mesh object
186

187
    if local:
1✔
188
        mesh_state = to_mesh_state(mesh)
×
189
    else:
190
        # Floating platform does not have any orientation or tilt or translations
191
        mesh_state = to_mesh_state(mesh)
1✔
192

193
    extremes = extractExtremes(mesh.points)
1✔
194

195
    return mesh_state, mesh, extremes
1✔
196

197

198
def render_turbine(turbineData, components):
1✔
199

200
    mesh_state_all = []
×
201
    extremes_all = []
×
202

203
    for idx, component in enumerate(components):
×
204
        if component == 'blade':
×
205
            mesh_state, mesh, extreme = render_blade(turbineData, local=False)
×
206
        elif component == 'hub':
×
207
            mesh_state, mesh, extreme = render_hub(turbineData, local=False)
×
208
        elif component == 'tower':
×
209
            mesh_state, mesh, extreme = render_Tower(turbineData, local=False)
×
NEW
210
        elif component == 'drivetrain':
×
211
            mesh_state, mesh, extreme = render_nacelle(turbineData, local=False)
×
212
        elif component == 'monopile':
×
213
            mesh_state, mesh, extreme = render_monopile(turbineData, local=False)
×
214
        elif component == 'floating_platform':
×
215
            mesh_state, mesh, extreme = render_floatingPlatform(turbineData, local=False)
×
216
        else:
217
            raise ValueError(f"Component {component} not recognized")
×
218
        
219
        mesh_state_all.append(mesh_state)
×
220
        extremes_all.append(extreme)
×
221
        if idx == 0:
×
222
            mesh_all = mesh
×
223
        else:
224
            mesh_all = mesh_all.merge(mesh)
×
225

226
    return mesh_state_all, mesh_all, extremes_all
×
227

228

229

230
def rotation_transformation(coords, angles):
1✔
231
    '''
232
    Apply rotation transformation to a set of coordinates
233
    here, angle(1) is the rotation about x-axis, angle(2) is the rotation about y-axis, angle(3) is the rotation about z-axis
234
    angles are in radians
235
    '''
236
    # Create rotation matrices for each angle
237
    rot_x = np.array([[1, 0, 0], [0, np.cos(angles[0]), -np.sin(angles[0])], [0, np.sin(angles[0]), np.cos(angles[0])]])
1✔
238
    rot_y = np.array([[np.cos(angles[1]), 0, np.sin(angles[1])], [0, 1, 0], [-np.sin(angles[1]), 0, np.cos(angles[1])]])
1✔
239
    rot_z = np.array([[np.cos(angles[2]), -np.sin(angles[2]), 0], [np.sin(angles[2]), np.cos(angles[2]), 0], [0, 0, 1]])
1✔
240

241
    # Apply rotation
242
    coords = np.dot(rot_x, coords)
1✔
243
    coords = np.dot(rot_y, coords)
1✔
244
    coords = np.dot(rot_z, coords)
1✔
245

246
    return coords
1✔
247
    
248

249
def translation_transformation(coords, translation):
1✔
250
    '''
251
    Apply translation transformation to a set of coordinates
252
    '''
253
    coords[0, :] += translation[0]
1✔
254
    coords[1, :] += translation[1]
1✔
255
    coords[2, :] += translation[2]
1✔
256

257
    return coords
1✔
258

259

260
def extractExtremes(points):
1✔
261
    '''
262
    Extract the minimum and maximum values of the points
263
    '''
264
    # no matter the shape, transform to 3xN
265
    points = np.array(points)
1✔
266
    if points.shape[0] != 3:
1✔
267
        points = points.T
1✔
268

269
    x_min = np.min(points[0])
1✔
270
    x_max = np.max(points[0])
1✔
271
    y_min = np.min(points[1])
1✔
272
    y_max = np.max(points[1])
1✔
273
    z_min = np.min(points[2])
1✔
274
    z_max = np.max(points[2])
1✔
275

276
    return (x_min, x_max, y_min, y_max, z_min, z_max)
1✔
277

278
###################################################
279
# Mesh generation for different turbine components
280
###################################################
281

282
def bladeMesh(bladeData, airfoils):
1✔
283
    '''
284
    Generate mesh for blade component
285
    '''
286
   # Clear previous arrays
287
    x = np.array([])
1✔
288
    y = np.array([])
1✔
289
    z = np.array([]) 
1✔
290
    
291
    # interpolate the chord, twist, pitch axis and reference axis to the locations of the airfoils
292
    s_airfoil = [bladeData['outer_shape']['airfoils'][i]['spanwise_position'] for i in range(len(bladeData['outer_shape']['airfoils']))]
1✔
293
    chord = np.interp(s_airfoil,
1✔
294
                      bladeData['outer_shape']['chord']['grid'],
295
                      bladeData['outer_shape']['chord']['values'])
296
    
297
    twist = np.interp(s_airfoil,
1✔
298
                      bladeData['outer_shape']['twist']['grid'],
299
                      bladeData['outer_shape']['twist']['values'])
300
    
301
    section_offset_x = np.zeros(len(s_airfoil)) #np.interp(s_airfoil,
1✔
302
                           #bladeData['outer_shape']['section_offset_x']['grid'],
303
                           #bladeData['outer_shape']['section_offset_x']['values'])
304
    
305
    section_offset_y = np.interp(s_airfoil,
1✔
306
                           bladeData['outer_shape']['section_offset_y']['grid'],
307
                           bladeData['outer_shape']['section_offset_y']['values'])
308
    
309
    ref_axis_x = np.interp(s_airfoil,
1✔
310
                           bladeData['reference_axis']['x']['grid'],
311
                           bladeData['reference_axis']['x']['values'])
312
    
313
    ref_axis_y = np.interp(s_airfoil,
1✔
314
                           bladeData['reference_axis']['y']['grid'],
315
                           bladeData['reference_axis']['y']['values'])
316
    
317
    ref_axis_z = np.interp(s_airfoil,
1✔
318
                           bladeData['reference_axis']['z']['grid'],
319
                           bladeData['reference_axis']['z']['values'])
320
    
321

322
    # extract the airfoil names at the grid locations 
323
    airfoil_names = [bladeData['outer_shape']['airfoils'][i]['name'] for i in range(len(bladeData['outer_shape']['airfoils']))]
1✔
324

325
    # Arrays to store points
326
    x = np.array([])
1✔
327
    y = np.array([])
1✔
328
    z = np.array([])
1✔
329

330
    # For each blade station, we transform the coordinates and store
331
    for i in range(len(ref_axis_z)):
1✔
332
        # Get the airfoil coordinates for this section
333
        af_coordinates = np.array([airfoils[airfoil_names[i]]['coordinates']['x'], airfoils[airfoil_names[i]]['coordinates']['y']]).T
1✔
334
        
335
        # Scale by chord and adjust by section offsets x and y 
336
        # Note that section_offsets are in the blade coordinate system, which is flipped compared to the local airfoil coordinate system
337
        # Also, section_offset_x must be summed, whereas section_offset_y must be subtracted
338
        af_coordinates[:, 0] = af_coordinates[:, 0] * chord[i] - section_offset_y[i]
1✔
339
        af_coordinates[:, 1] = af_coordinates[:, 1] * chord[i] + section_offset_x[i]
1✔
340
        
341
        # Create rotation matrix for twist angle
342
        cos_t = np.cos(np.radians(twist[i]))
1✔
343
        sin_t = np.sin(np.radians(twist[i]))
1✔
344
        rot_matrix = np.array([[cos_t, -sin_t], [sin_t, cos_t]])
1✔
345
        
346
        # Apply rotation
347
        af_coordinates = np.dot(af_coordinates, rot_matrix.T)
1✔
348
        
349
        # Translate to reference axis position
350
        af_coordinates_3d = np.zeros((af_coordinates.shape[0], 3))
1✔
351
        af_coordinates_3d[:, 0] = af_coordinates[:, 0] + ref_axis_y[i]
1✔
352
        af_coordinates_3d[:, 1] = af_coordinates[:, 1] + -ref_axis_x[i] 
1✔
353
        af_coordinates_3d[:, 2] = np.full(af_coordinates.shape[0], ref_axis_z[i])
1✔
354

355
        # Append points
356
        x = np.append(x, af_coordinates_3d[:,0])
1✔
357
        y = np.append(y, af_coordinates_3d[:,1]) 
1✔
358
        z = np.append(z, af_coordinates_3d[:,2])
1✔
359

360
    points = (x, y, z)
1✔
361

362
    return points
1✔
363

364

365
def towerMesh(towerData):
1✔
366

367
    # Clear previous arrays
368
    x = np.array([])
1✔
369
    y = np.array([])
1✔
370
    z = np.array([]) 
1✔
371
    
372
    towerGrid = np.interp(towerData['outer_shape']['outer_diameter']['grid'],
1✔
373
                            towerData['reference_axis']['z']['grid'],
374
                            towerData['reference_axis']['z']['values'])
375
    TowerOD = towerData['outer_shape']['outer_diameter']['values']
1✔
376

377
    # Create points for each cylinder segment
378
    for i in range(len(towerGrid)-1):
1✔
379
        # Get parameters for this segment
380
        h_start = towerGrid[i]
1✔
381
        h_end = towerGrid[i+1] 
1✔
382
        r_start = TowerOD[i]/2
1✔
383
        r_end = TowerOD[i+1]/2
1✔
384
        
385
        # Create points along height
386
        h_points = np.linspace(h_start, h_end, 20)
1✔
387
        
388
        # For each height, create a circle of points
389
        for h in h_points:
1✔
390
            # Calculate interpolated radius at this height
391
            r = r_start + (r_end - r_start) * (h - h_start)/(h_end - h_start)
1✔
392
            
393
            # Create circle of points at this height
394
            theta = np.linspace(0, 2*np.pi, 36)
1✔
395
            x = np.append(x, r * np.cos(theta))
1✔
396
            y = np.append(y, r * np.sin(theta))
1✔
397
            z = np.append(z, np.full_like(theta, h))
1✔
398

399

400
    points = (x, y, z)
1✔
401
    return points
1✔
402

403
# def hubMesh(hubData):
404

405
#     # Clear previous arrays
406
#     x = np.array([])
407
#     y = np.array([])
408
#     z = np.array([]) 
409
    
410
#     dia = hubData['diameter']
411
#     h = 0.5 * dia
412
#     '''
413
#     let the height of the hub be 0.5 of the diameter
414
#     The radius of the hub along the height varies from diameter to zero at the top and follows a parabolic function
415
#     '''
416
#     numPoints = 100
417
#     theta = np.linspace(0, 2*np.pi, numPoints)
418
#     z_dists = np.linspace(0, dia/2, 20)
419

420
#     for z_dist in z_dists:
421
#         r = dia/2 * np.sqrt(1 - (2*z_dist/dia)**2)
422
#         x = np.append(x, r * np.cos(theta))
423
#         y = np.append(y, r * np.sin(theta))
424
#         z = np.append(z, np.full_like(theta, z_dist))
425

426
#     points = (x, y, z)
427

428
#     return points
429

430
def hubMesh(hubData):
1✔
431
    # Clear previous arrays
432
    x = np.array([])
1✔
433
    y = np.array([])
1✔
434
    z = np.array([]) 
1✔
435
    
436
    dia = 1.25 * hubData['diameter']  # Increased diameter multiplier
1✔
437
    total_length = dia #* 1.2  # Increased total length for better coverage
1✔
438
    offset = -0.2 * dia  # Offset to move the hub backwards
1✔
439
    
440
    '''
1✔
441
    Hub consists of a parabolic section that:
442
    1. Starts slightly behind z=0 to better cover blade roots
443
    2. Has maximum radius at z=0.3*length 
444
    3. Tapers to a point at the end
445
    '''
446
    
447
    numPoints = 100
1✔
448
    theta = np.linspace(0, 2*np.pi, numPoints)
1✔
449
    z_points = np.linspace(offset, total_length + offset, 30)
1✔
450
    
451
    for z_dist in z_points:
1✔
452
        # Modified parabolic profile 
453
        normalized_z = (z_dist - offset)/(total_length)
1✔
454
        # Profile peaks at z=0.3*length and tapers at both ends
455
        r = dia/2 * (1 - ((normalized_z - 0.3)/0.7)**2)
1✔
456
        r = max(0, r)  # Ensure radius doesn't go negative
1✔
457
        
458
        x = np.append(x, r * np.cos(theta))
1✔
459
        y = np.append(y, r * np.sin(theta))
1✔
460
        z = np.append(z, np.full_like(theta, z_dist))
1✔
461

462
    points = (x, y, z)
1✔
463
    return points
1✔
464

465

466
def monopileMesh(monopileData):
1✔
467

468
    # Reusing the tower mesh function for monopile
469
    points = towerMesh(monopileData)
1✔
470

471
    return points
1✔
472
    
473
def nacelleMesh(nacelleData, hubData):
1✔
474

475
    # Clear previous arrays
476
    x = np.array([])
1✔
477
    y = np.array([])
1✔
478
    z = np.array([]) 
1✔
479
    
480
    # Add an offset for better visual alignment
481
    xOffset = -0.2  # Shift nacelle back by to better align with hub
1✔
482
    
483
    # if the nacelle dict has length, width and height, we can create a box
484
    if 'length' in nacelleData['outer_shape'].keys():
1✔
NEW
485
        length = nacelleData['outer_shape']['length']
×
NEW
486
        width = nacelleData['outer_shape']['width']
×
NEW
487
        height = nacelleData['outer_shape']['height']
×
488
        x = np.array([length/2, length/2, -length/2, -length/2, length/2, length/2, -length/2, -length/2]).T + xOffset * length
×
489
        y = np.array([width/2, -width/2, -width/2, width/2, width/2, -width/2, -width/2, width/2]).T
×
490
        z = np.array([-height/2, -height/2, -height/2, -height/2, height/2, height/2, height/2, height/2]).T
×
491
    else:
492
        # if not, we use the overhang, distance from tower top to hub, and hub diameter to create the box such that
493
        # height = 1.5 * distance from tower top to hub
494
        # length = 1 * overhang
495
        # width = 1 * hub diameter
496
        height = 1.5 * nacelleData['outer_shape']['distance_tt_hub']
1✔
497
        length = 1 * nacelleData['outer_shape']['overhang']
1✔
498
        width = 1 * hubData['diameter']
1✔
499
        x = np.array([length/2, length/2, -length/2, -length/2, length/2, length/2, -length/2, -length/2]).T + xOffset * length
1✔
500
        y = np.array([width/2, -width/2, -width/2, width/2, width/2, -width/2, -width/2, width/2]).T
1✔
501
        z = np.array([-height/2, -height/2, -height/2, -height/2, height/2, height/2, height/2, height/2]).T
1✔
502
    
503
    points = (x, y, z)
1✔
504

505
    return points
1✔
506

507
def floatingPlatformMesh(semisubData, mooringData=None):
1✔
508

509
    # convert joints from array to dictionary
510
    semisubData['joints'] = {joint['name']: joint for joint in semisubData['joints']}
1✔
511

512
    # creating a few sub functions
513
    def cylind2cart(p):
1✔
514
        # r, theta, z
515
        x = np.array([0, 0, 0])
1✔
516
        x[0] = p[0] * np.cos(np.deg2rad(p[1]))
1✔
517
        x[1] = p[0] * np.sin(np.deg2rad(p[1]))
1✔
518
        x[2] = p[2]
1✔
519
        return x
1✔
520

521
    def getJointPoint(jointName):
1✔
522

523
        joint = semisubData['joints'][jointName]
1✔
524

525
        # search if joint has cylindrical key
526
        if 'cylindrical' in joint.keys():
1✔
527
            if joint['cylindrical']:
1✔
528
                return cylind2cart(joint['location'])
1✔
529
            else:
530
                return joint['location']
1✔
531
        else:
532
            return joint['location']
×
533

534
    # def meshCone(joint1, joint2, grid, value):
535
    #     grid = np.interp(grid, [0, 1], [joint1, joint2])
536

537

538
    # looping through the members
539
    for idx, member in enumerate(semisubData['members']):
1✔
540
        # fetch the joints
541
        joint1 = np.array(getJointPoint(member['joint1']))
1✔
542
        joint2 = np.array(getJointPoint(member['joint2']))
1✔
543

544
        # direction vector normalized
545
        direction = (joint2 - joint1) / np.linalg.norm(joint2 - joint1)
1✔
546

547

548
        if member['outer_shape']['shape'] == 'circular':
1✔
549
            # create a cylinder, if last and first diameters are the same, then use pv.Cylinder
550
            # else use towerMesh to create a cylinder
551
            if member['outer_shape']['outer_diameter']['values'][0] == member['outer_shape']['outer_diameter']['values'][-1]:
1✔
552
                center = joint1 + 0.5 * (joint2 - joint1)  # Calculate midpoint
1✔
553
                memberMesh = pv.Cylinder(center=center,
1✔
554
                                        direction=direction, 
555
                                        height=np.linalg.norm(joint2 - joint1), 
556
                                        radius=member['outer_shape']['outer_diameter']['values'][0]/2)
557
            else:
558
                # Create a tapered cylinder using towerMesh-like approach
559
                towerData = {
×
560
                    'outer_shape': {
561
                        'outer_diameter': {
562
                            'grid': member['outer_shape']['outer_diameter']['grid'],
563
                            'values': member['outer_shape']['outer_diameter']['values']
564
                        },
565
                        'reference_axis': {
566
                            'z': {
567
                                'grid': member['outer_shape']['outer_diameter']['grid'],
568
                                'values': np.linspace(0, np.linalg.norm(joint2 - joint1), len(member['outer_shape']['outer_diameter']['grid']))
569
                            }
570
                        }
571
                    }
572
                }
573
                
574
                x, y, z = towerMesh(towerData)
×
575
                
576
                # Rotate and translate the points to align with member direction
577
                points = np.vstack((x, y, z))
×
578
                
579
                # Calculate rotation matrix from [0,0,1] to direction vector
580
                v = np.array([0, 0, 1])
×
581
                rot_axis = np.cross(v, direction)
×
582
                rot_angle = np.arccos(np.dot(v, direction))
×
583
                
584
                if np.linalg.norm(rot_axis) > 0:
×
585
                    rot_axis = rot_axis / np.linalg.norm(rot_axis)
×
586
                    rotMatrix = pv.transformations.axis_angle_rotation(rot_axis, np.degrees(rot_angle))
×
587
                    points = np.dot(rotMatrix[:3, :3], points)
×
588
                
589
                # Translate to joint1
590
                points = points + joint1.reshape(3, 1)
×
591
                
592
                memberMesh = pv.PolyData(points.T)
×
593
                memberMesh = memberMesh.delaunay_3d()
×
594

595
        elif member['outer_shape']['shape'] == 'rectangular':
×
596
            '''
×
597
            Definition of Rectangular member:
598
            shape: rectangular
599
                side_length_a:
600
                    grid: [0.0, 1.0]
601
                    values: [12.5, 12.5]
602
                side_length_b:
603
                    grid: [0.0, 1.0]
604
                    values: [7.0, 7.0]
605
            '''
606
            # For rectangular members, create a box using the side lengths
607
            # Get side lengths at grid points
608
            side_a = member['outer_shape']['side_length_a']['values']
×
609
            side_b = member['outer_shape']['side_length_b']['values']
×
610

611
            # Calculate the member direction and perpendicular vectors
612
            direction = (joint2 - joint1) / np.linalg.norm(joint2 - joint1)
×
613
            perpendicular1 = np.array([-direction[1], direction[0], 0])
×
614
            if np.linalg.norm(perpendicular1) < 1e-10:
×
615
                perpendicular1 = np.array([1, 0, 0])
×
616
            perpendicular1 = perpendicular1 / np.linalg.norm(perpendicular1)
×
617
            perpendicular2 = np.cross(direction, perpendicular1)
×
618
            
619
            # Create corners based on joint points
620
            corners = np.array([
×
621
                # Bottom face
622
                joint1 + (-side_a[0]/2 * perpendicular1) + (-side_b[0]/2 * perpendicular2),
623
                joint1 + (side_a[0]/2 * perpendicular1) + (-side_b[0]/2 * perpendicular2),
624
                joint1 + (side_a[0]/2 * perpendicular1) + (side_b[0]/2 * perpendicular2),
625
                joint1 + (-side_a[0]/2 * perpendicular1) + (side_b[0]/2 * perpendicular2),
626
                # Top face 
627
                joint2 + (-side_a[1]/2 * perpendicular1) + (-side_b[1]/2 * perpendicular2),
628
                joint2 + (side_a[1]/2 * perpendicular1) + (-side_b[1]/2 * perpendicular2),
629
                joint2 + (side_a[1]/2 * perpendicular1) + (side_b[1]/2 * perpendicular2),
630
                joint2 + (-side_a[1]/2 * perpendicular1) + (side_b[1]/2 * perpendicular2)
631
            ])
632

633
            # Create mesh from vertices
634
            faces = np.array([
×
635
                [4, 0, 1, 2, 3],  # bottom
636
                [4, 4, 5, 6, 7],  # top
637
                [4, 0, 1, 5, 4],  # front
638
                [4, 1, 2, 6, 5],  # right
639
                [4, 2, 3, 7, 6],  # back
640
                [4, 3, 0, 4, 7]   # left
641
            ])
642
            memberMesh = pv.PolyData(corners, faces)
×
643

644

645

646

647
        else:
648
            # warning that the shape is not recognized
649
            print(f"Shape {member['outer_shape']['shape']} not recognized")
×
650
            
651

652
        # if the member has a key named 'axial_joints', then calculate its location in space and add it to the list of joints for future reference
653
        if 'axial_joints' in member.keys():
1✔
654
            for axial_joint in member['axial_joints']:
1✔
655
                axial_joint_location = joint1 + axial_joint['grid'] * (joint2 - joint1)
1✔
656
                semisubData['joints'][axial_joint['name']] = {'name': axial_joint['name'],'location': axial_joint_location, 'cylindrical': False}
1✔
657

658

659
        if idx == 0:
1✔
660
            floatingMesh = memberMesh
1✔
661
        else:
662
            floatingMesh = floatingMesh.merge(memberMesh)
1✔
663

664
        # floatingMesh.plot(show_edges=True)
665

666

667
    # if the semisub has a mooring system, then we need to add the mooring lines
668
    if mooringData:
1✔
669
        
670
        # convert nodes from array to dictionary
671
        mooringData['nodes'] = {node['name']: node for node in mooringData['nodes']}
1✔
672

673
        # Looping over the mooring lines
674
        for line in mooringData['lines']:
1✔
675
            # fetch the nodes
676
            np.array(getJointPoint(member['joint1']))
1✔
677
            node1 = np.array(getJointPoint(mooringData['nodes'][line['node1']]['joint']))
1✔
678
            node2 = np.array(getJointPoint(mooringData['nodes'][line['node2']]['joint']))
1✔
679

680
            # check if the distance between the nodes is equal to the unstretched length of the mooring line
681
            if np.linalg.norm(node2 - node1) == line['unstretched_length']:
1✔
682
                mooringLineMesh = pv.Line(pointa=node1, pointb=node2)
×
683
            else:
684
                # Create catenary mooring line using parametric equations
685
                num_points = 50
1✔
686
                t = np.linspace(0, 1, num_points)
1✔
687
                # Calculate catenary parameter (a) based on endpoints and length
688
                dx = node2[0] - node1[0]
1✔
689
                dy = node2[1] - node1[1]
1✔
690
                dz = node2[2] - node1[2]
1✔
691
                L = line['unstretched_length']
1✔
692
                a = np.sqrt(L**2 - dz**2) / 2  # Approximate catenary parameter
1✔
693
                
694
                # Generate points along catenary
695
                x = node1[0] + dx * t
1✔
696
                y = node1[1] + dy * t
1✔
697
                # Changed the sign before 'a' to make the catenary curve downward
698
                z = node1[2] + dz * t + a * (np.cosh(t - 0.5) - np.cosh(-0.5))
1✔
699
                
700
                points = np.column_stack((x, y, z))
1✔
701
                mooringLineMesh = pv.lines_from_points(points)
1✔
702

703
            floatingMesh = floatingMesh.merge(mooringLineMesh)
1✔
704

705
    return floatingMesh
1✔
706

707
def render_our_own_delaunay(points):
1✔
708
    '''
709
    Create and fill the VTK Data Object with your own data using VTK library and pyvista high level api
710

711
    Reference: https://tutorial.pyvista.org/tutorial/06_vtk/b_create_vtk.html
712
    https://docs.pyvista.org/examples/00-load/create-tri-surface
713
    https://docs.pyvista.org/api/core/_autosummary/pyvista.polydatafilters.reconstruct_surface#pyvista.PolyDataFilters.reconstruct_surface
714
    '''
715

716
    # Join the points
717
    x, y, z = points
1✔
718
    values = np.c_[x.ravel(), y.ravel(), z.ravel()]     # (6400, 3) where each column is x, y, z coords
1✔
719
    coords = numpy_to_vtk(values)
1✔
720
    cloud = pv.PolyData(coords)
1✔
721
    # mesh = cloud.delaunay_2d()          # From point cloud, apply a 2D Delaunary filter to generate a 2d surface from a set of points on a plane.
722
    mesh = cloud.delaunay_3d()
1✔
723

724
    mesh_state = to_mesh_state(mesh)
1✔
725

726
    return mesh_state, mesh
1✔
727

728

729
def main():    
1✔
730
    # fetch the turbine data from the yaml file
NEW
731
    file_name = '../../examples/00_setup/ref_turbines/IEA-15-240-RWT_VolturnUS-S.yaml'
×
732
    # with open('../../examples/00_setup/ref_turbines/IEA-15-240-RWT.yaml') as file:
NEW
733
    turbine_data = sch.load_geometry_yaml(file_name)
×
734

735
    # render the turbine components
736
    # mesh_state, meshTurbine, extremes_all = render_turbine(turbine_data, ['blade', 'hub', 'tower', 'drivetrain', 'monopile'])
NEW
737
    mesh_state, meshTurbine, extremes_all = render_turbine(turbine_data, ['blade', 'hub', 'tower', 'drivetrain', 'floating_platform'])
×
738
    # mesh_state, meshTurbine, extremes_all = render_turbine(turbine_data, ['floating_platform'])
739
    meshTurbine.plot(show_edges=False)
×
740

741

742
# call main if this script is run
743
if __name__ == "__main__":
1✔
744
    main()
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc