Skip to content

Commit 6035de1

Browse files
committed
Anchor calcMudlineLoads & new helper function
-- Anchor.calcMudlineLoads() will apply platform loads in worst-case scenarios and pull out anchor loads. For shared anchors, will consider worst case scenarios like upwind turbine shut down -- helpers.wrap_angle_diff() calculates difference between 2 angles or 2 arrays of angles while wrapping around. For example. 355 deg - 5 deg would output 10 deg instead of 350 deg.
1 parent 971740d commit 6035de1

File tree

2 files changed

+168
-48
lines changed

2 files changed

+168
-48
lines changed

famodel/anchors/anchor.py

Lines changed: 140 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from collections import defaultdict
1111
import famodel.platform.platform
1212
import shapely as sh
13+
from famodel.helpers import wrap_angle_diff
1314

1415
class Anchor(Node):
1516

@@ -305,93 +306,184 @@ def makeMoorPyAnchor(self, ms):
305306

306307
return ms
307308

308-
"""
309-
def calcAnchorLoads(self, level=0):
309+
310+
def calcMudlineLoads(self, level=0, plot=False, mean_load_keys=None):
310311
'''Compute anchor load envelope based on available information.
311312
The 'level' input sets the type of analysis. Currently supported:
312313
level 0 : quasi-static analysis based on mean offsets and surge
313314
motion amplitudes.
314-
'''
315315
316+
level: int, optional
317+
analysis level. 0 is quasi-static. Default is 0.
318+
plot: bool, optional
319+
whether to plot the motions of the platforms. Default is False.
320+
mean_load_keys: list or str, optional
321+
key names of platform mean_load dict to describe which mean load types
322+
to sum and apply to the platform. Default is None, which will sum
323+
values of all mean_loads listed in platform.mean_loads
324+
'''
325+
316326
# Identify attached moorings and platforms
317327
moorings = []
318328
platforms = []
329+
319330
for att in self.attachments.values():
320331
if isinstance(att['obj'], Mooring):
321-
moorings.append(att)
322332

323333
# get the platform on the other end of the mooring
324-
platform = att.attached_to[1-att[['end']]
334+
platform = att['obj'].attached_to[1-att['end']]
325335

326336
# if it's a normal platform, just save it
327-
if ...
337+
if platform.entity.upper() == 'FOWT':
328338
platforms.append(platform)
339+
moorings.append(att['obj'])
329340
# if it's something else (like a buoy), get the platforms attached to it!
330-
else:
331-
for att2 in platform.attachments.values():
332-
if isinstance(att2['obj'], Mooring) and not att2==att: # look through each other mooring
333-
...
334-
335-
moorings = [att for att in self.attachments.values() if isinstance(att['obj'], Mooring)]
341+
elif platform.entity.upper() == 'BUOY':
342+
for att2 in platform.getMoorings().values():
343+
if isinstance(att2, Mooring) and not att2==att['obj']: # look through each other mooring
344+
moorings.append(att2)
345+
for att3 in att2.attached_to:
346+
if not isinstance(att3, Anchor):
347+
if att3.entity.upper() == 'FOWT':
348+
platforms.append(att3)
349+
350+
ms = self.mpAnchor.sys
351+
ms.initialize()
352+
ms.solveEquilibrium()
353+
if plot:
354+
cols = ['red','orange','yellow','lime','green','turquoise','skyblue','blue','purple','magenta','goldenrod']
355+
jj=0
356+
fig,ax = plt.subplots()
357+
ax.scatter(self.r[0],self.r[1],marker='x')
358+
ax.scatter([pf.body.r6[0] for pf in platforms],[pf.body.r6[1] for pf in platforms],c='black')
336359

337360
# Calculate the loading envelope on the anchor
338361
if level==0: # quasi-static analysis
339-
loads = []
362+
loads_h = []
363+
loads_v = []
364+
loads_env = []
365+
# store initial loads
366+
loads_env.append(self.mpAnchor.getForces())
367+
loads_h.append(np.linalg.norm(np.abs(loads_env[-1][:2])))
368+
loads_v.append(loads_env[-1][2])
340369

341370
# Look at a worst-case loads along each mooring direction
342371
for mooring in moorings:
343372
# get direction of mooring from anchor
344-
heading = mooring. ...
345-
346-
347-
# --- worst horizontal load ---
373+
heading = np.radians(90-(mooring.heading-180)) # mooring.heading is from direction of platform & compass
374+
u = [np.cos(heading), np.sin(heading)]
348375

376+
# --- worst horizontal load ---
349377
# Apply maximum steady load in that direction on each platform
350378
for platform in platforms:
351-
platform.body.f6ext = platform.maxLoad...
379+
platform.calcThrustTotal()
380+
if mean_load_keys == None:
381+
total_mean_load = np.sum([x for x in platform.mean_loads.values()])
382+
elif isinstance(mean_load_keys,str):
383+
total_mean_load = platform.mean_loads[mean_load_keys]
384+
elif isinstance(mean_load_keys,(list,np.ndarray)):
385+
total_mean_load = np.sum([platform.mean_loads[x] for x in mean_load_keys])
386+
platform.body.f6Ext = [total_mean_load*u[0],
387+
total_mean_load*u[1],
388+
0,0,0,0]
352389

353390
# Compute array MoorPy system mean offset positions due to applied loads
354-
# ms.solveEquilibrium(DOFtype='both')
355-
391+
ms.initialize()
392+
ms.solveEquilibrium(DOFtype='both')
393+
# store loads in horizontal and vertical separate
394+
loads_env.append(self.mpAnchor.getForces())
395+
loads_h.append(np.linalg.norm(np.abs(loads_env[-1][:2])))
396+
loads_v.append(loads_env[-1][2])
397+
356398
# Add motion amplitude in loading direction to the mean offsets of each platform
357399
for platform in platforms:
358400
# platform.x_amp is if we somehow stored max surge amplitude info in each platform <<<
359-
x = platform.x_amp*np.cos(heading) # displacement amplitude in x
360-
y = platform.x_amp*np.sin(heading) # displacement amplitude in y
401+
x = platform.x_ampl*u[0] # displacement amplitude in x
402+
y = platform.x_ampl*u[1] # displacement amplitude in y
361403
platform.body.r6 += np.array([ x, y, 0,0,0,0])
404+
405+
# Compute mooring tensions based on these positions, then sum up anchor tensions
406+
ms.initialize()
407+
ms.solveEquilibrium()
408+
# store loads in horizontal and vertical
409+
loads_env.append(self.mpAnchor.getForces())
410+
loads_h.append(np.linalg.norm(np.abs(loads_env[-1][:2])))
411+
loads_v.append(loads_env[-1][2])
362412

363-
# Compute mooring tensions based on these positions, the sum up anchor tensions
364-
# ms.solveEquilibrium(...
365-
loads.append(self.mpAnchor.getForces())
413+
# reset to remove added platform displacement but keep external thrust
414+
ms.solveEquilibrium(DOFtype='both')
366415

367416

368417
# --- worst vertical load (assumes one upwind platform is failed/unloaded) ---
369-
370-
# figure out which platform is most upwind and remove it's external load
371-
idle_platform = ...
372-
idle_platform.body.f6ext = np.zeros(3)
373-
374-
# Compute array MoorPy system mean offset positions due to applied loads
375-
# ms.solveEquilibrium(DOFtype='both')
376-
377-
# Add motion amplitude in spreading-out direction to the mean offsets of each platform
378-
for platform in platforms:
379-
x_amp = platform.x_amp
380-
heading_to_platform = ...
381-
x = x_amp*np.cos(heading_to_platform) # displacement amplitude in x
382-
y = x_amp*np.sin(heading_to_platform) # displacement amplitude in y
383-
platform.body.r6 += np.array([ x, y, 0,0,0,0])
384-
385-
# Compute mooring tensions based on these positions, the sum up anchor tensions
386-
# ms.solveEquilibrium(...
387-
loads.append(self.mpAnchor.getForces())
388-
389-
390-
# maybe process the loads to make an envelope of the worst horizontal-vertical loads
418+
# get heading of mooring and ensure in bounds of 0-360. Opposite of heading above.
419+
if len(moorings)>1: # this is a shared anchor
420+
mooring_heading = np.radians((90-(mooring.heading))%360)
421+
if mooring_heading <0:
422+
mooring_heading = mooring_heading+2*np.pi
423+
424+
# figure out which platform is most upwind and remove its external load
425+
pf_r_rels = [pf.r-self.r for pf in platforms] # get xy position of platform relative to anchor
426+
pf_heads = np.array([np.arctan2(pf_r[1],pf_r[0]) for pf_r in pf_r_rels]) # calculate heading from rel position
427+
# make sure heading is between 0-360
428+
for i,h in enumerate(pf_heads):
429+
if h <0:
430+
pf_heads[i] = h+2*np.pi
431+
elif h>2*np.pi:
432+
pf_heads[i] = h-2*np.pi
433+
434+
# get difference between angles, wrapping around to be between -180:180
435+
idx = np.abs(wrap_angle_diff(pf_heads,mooring_heading,degrees=False)).argmin()
436+
# pull out platform with smallest diff between and remove external force
437+
idle_platform = platforms[idx]
438+
idle_platform.body.f6Ext = np.zeros(6)
439+
440+
441+
# Compute array MoorPy system mean offset positions due to applied loads
442+
ms.initialize()
443+
ms.solveEquilibrium(DOFtype='both')
444+
445+
# store horizontal and vertical loads
446+
loads_env.append(self.mpAnchor.getForces())
447+
loads_h.append(np.linalg.norm(np.abs(loads_env[-1][:2])))
448+
loads_v.append(loads_env[-1][2])
449+
if plot:
450+
ax.scatter([pf.body.r6[0] for pf in platforms],[pf.body.r6[1] for pf in platforms],c=cols[jj])
451+
jj +=1
452+
453+
# Add motion amplitude in spreading-out direction to the mean offsets of each platform
454+
for i,platform in enumerate(platforms):
455+
x_amp = platform.x_ampl
456+
heading_to_platform = pf_heads[i]
457+
x = x_amp*np.cos(heading_to_platform) # displacement amplitude in x
458+
y = x_amp*np.sin(heading_to_platform) # displacement amplitude in y
459+
platform.body.r6 += np.array([ x, y, 0,0,0,0])
460+
461+
# Compute mooring tensions based on these positions, then sum up anchor tensions
462+
ms.initialize()
463+
ms.solveEquilibrium()
464+
if plot:
465+
ax.scatter([pf.body.r6[0] for pf in platforms],[pf.body.r6[1] for pf in platforms],c=cols[jj])
466+
jj+=1
467+
loads_env.append(self.mpAnchor.getForces())
468+
loads_h.append(np.linalg.norm(np.abs(loads_env[-1][:2])))
469+
loads_v.append(loads_env[-1][2])
470+
471+
self.loads['Hm'] = np.max(loads_h)
472+
self.loads['Vm'] = np.max(loads_v)
473+
self.loads['thetam'] = np.degrees(np.arctan2(self.loads['Vm'],
474+
self.loads['Hm']))
475+
self.loads['mudline_load_type'] = 'max_force'
476+
self.loads['info'] = 'mudline loads determined with MoorPy'
477+
self.envelope['max_loads'] = loads_env
391478

479+
# remove loads and set everything back to initial positions
480+
for pf in platforms:
481+
pf.body.f6Ext = np.array([0,0,0,0,0,0])
482+
ms.initialize()
483+
ms.solveEquilibrium(DOFtype='both')
392484
else:
393485
raise Exception('Only level 0 is supported so far')
394-
"""
486+
395487

396488
def getLineProperties(self):
397489
'''

famodel/helpers.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,34 @@ def pol2cart(rho, phi):
2222
y = rho * np.sin(phi)
2323
return(x, y)
2424

25+
def wrap_angle_diff(a, b, degrees=True):
26+
# convert to arrays
27+
a = np.array(a)
28+
b = np.array(b)
29+
# ensure in degrees
30+
if degrees==False:
31+
a = np.degrees(a)
32+
b = np.degrees(b)
33+
# ensure between 0 & 360
34+
a[a<0] += 360
35+
a[a>=360] -= 360
36+
# calc initial difference
37+
c = a-b
38+
# now wrap around if abs(difference)>180
39+
for i,val in enumerate(c):
40+
if val > 180:
41+
c[i] -= 360
42+
if val < -180:
43+
c[i] += 360
44+
if degrees==False:
45+
c = np.radians(c)
46+
47+
# if len is 1, return as a float rather than array
48+
if len(c)==1:
49+
c = float(c)
50+
return(c)
51+
52+
2553
def m2nm(data):
2654
''' Convert meters to nautical miles'''
2755
if isinstance(data,list):

0 commit comments

Comments
 (0)