diff --git a/.readthedocs.yaml b/.readthedocs.yaml index cdc80d1..21f862e 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -2,7 +2,7 @@ version: 2 sphinx: configuration: docs/conf.py python: - version: 3.7 + version: 3.8 install: - method: pip path: . diff --git a/README.md b/README.md index 74d8035..355886f 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,8 @@ MoorPy is available on PyPi via: pip install MoorPy ``` -For an editable install that relies on the local source code, first clone the repository. Then, from the command line in the main MoorPy directory, run the following commands based on your additional needs... +For an editable install that relies on the local source code, first clone the repository. Then, from the command line in the main MoorPy directory, run the following commands (with a "-e" for "editable") based on your additional needs. +The "dev", "test", and "docs" flags will install necessary packages related to development, testing, or documentation (e.g., the docs flag installs "sphinx" for documentation). #### General ```pycon diff --git a/docs/starting.rst b/docs/starting.rst index 15e8f1b..84fe788 100644 --- a/docs/starting.rst +++ b/docs/starting.rst @@ -28,13 +28,151 @@ Examples The MoorPy repository has an examples folder containing two example scripts: +- manual_system.py constructs a mooring system from scratch using functions for creating each line and attachment point. + - imported_system.py creates a mooring system by reading an included MoorDyn-style input file. -- manual_system.py constructs a mooring system from scratch using functions for creating each line and attachment point. +Running either of these scripts will produce a basic mooring system model that can be used in further analysis. + + +Creating a MoorPy System Manually +--------------------------------- + +MoorPy has internal functions to facilitate the orderly creation of a mooring system. The following +gives an example of how they work (from manual_system.py). + +.. code-block:: python + + + # ----- choose some system geometry parameters ----- + + depth = 600 # water depth [m] + angles = np.radians([60, 180, 300]) # line headings list [rad] + rAnchor = 1600 # anchor radius/spacing [m] + zFair = -21 # fairlead z elevation [m] + rFair = 20 # fairlead radius [m] + lineLength= 1800 # line unstretched length [m] + typeName = "chain1" # identifier string for the line type + + + # ----- set up the mooring system and floating body ----- + + # Create new MoorPy System and set its depth + ms = mp.System(depth=depth) + + # add a line type + ms.setLineType(dnommm=120, material='chain', name=typeName) # this would be 120 mm chain + + # Add a free, body at [0,0,0] to the system (including some properties to make it hydrostatically stiff) + ms.addBody(0, np.zeros(6), m=1e6, v=1e3, rM=100, AWP=1e3) + + # For each line heading, set the anchor point, the fairlead point, and the line itself + for i, angle in enumerate(angles): + + # create end Points for the line + ms.addPoint(1, [rAnchor*np.cos(angle), rAnchor*np.sin(angle), -depth]) # create anchor point (type 0, fixed) + ms.addPoint(1, [ rFair*np.cos(angle), rFair*np.sin(angle), zFair]) # create fairlead point (type 0, fixed) + + # attach the fairlead Point to the Body (so it's fixed to the Body rather than the ground) + ms.bodyList[0].attachPoint(2*i+2, [rFair*np.cos(angle), rFair*np.sin(angle), zFair]) + + # add a Line going between the anchor and fairlead Points + ms.addLine(lineLength, typeName, pointA=2*i+1, pointB=2*i+2) + + + + +Creating a MoorPy System for a MoorDyn Input File +------------------------------------------------- + +.. _inputfile: + +A MoorPy System can be initialized by reading in a MoorDyn-style input file. This is simply done by +passing the input file name when creating the System object (from imported_system.py): + +.. code-block:: python + + ms = mp.System(file='the MoorDyn-style input file.txt') + + +The format of the input file is expected to follow the +MoorDyn v2 style, an example of which is shown below: + + +.. code-block:: none + + MoorDyn v2 Input File + Sample for input to MoorPy + ---------------------- LINE TYPES -------------------------------------------------- + TypeName Diam Mass/m EA BA/-zeta EI Cd Ca CdAx CaAx + (name) (m) (kg/m) (N) (N-s/-) (N-m^2) (-) (-) (-) (-) + chain 0.2160 286.56 1.23e9 -1.0 0.00 1.00 1.00 0.00 0.00 + --------------------- ROD TYPES ----------------------------------------------------- + TypeName Diam Mass/m Cd Ca CdEnd CaEnd + (name) (m) (kg/m) (-) (-) (-) (-) + ----------------------- BODIES ------------------------------------------------------ + ID Attachment X0 Y0 Z0 r0 p0 y0 Mass CG* I* Volume CdA* Ca* + (#) (-) (m) (m) (m) (deg) (deg) (deg) (kg) (m) (kg-m^2) (m^3) (m^2) (-) + 1 coupled 0.00 0.00 -0.75 0.00 0.00 0.00 1.0e6 0.00 0.00 1.0e3 0.00 0.00 + ---------------------- RODS --------------------------------------------------------- + ID RodType Attachment Xa Ya Za Xb Yb Zb NumSegs RodOutputs + (#) (name) (#/key) (m) (m) (m) (m) (m) (m) (-) (-) + ---------------------- POINTS ------------------------------------------------------- + ID Attachment X Y Z Mass Volume CdA Ca + (#) (-) (m) (m) (m) (kg) (mˆ3) (m^2) (-) + 1 Fixed 800.00 1385.64 -600.00 0.00 0.00 0.00 0.00 + 2 Body1 10.00 17.32 -21.00 0.00 0.00 0.00 0.00 + 3 Fixed -1600.00 0.00 -600.00 0.00 0.00 0.00 0.00 + 4 Body1 -20.00 0.00 -21.00 0.00 0.00 0.00 0.00 + 5 Fixed 800.00 -1385.64 -600.00 0.00 0.00 0.00 0.00 + 6 Body1 10.00 -17.32 -21.00 0.00 0.00 0.00 0.00 + ---------------------- LINES -------------------------------------------------------- + ID LineType AttachA AttachB UnstrLen NumSegs LineOutputs + (#) (name) (#) (#) (m) (-) (-) + 1 chain 1 2 1800.000 40 p + 2 chain 3 4 1800.000 40 p + 3 chain 5 6 1800.000 40 p + ---------------------- OPTIONS ------------------------------------------------------ + 600.0 depth + --------------------- need this line ------------------------------------------------ + + +Note that some parameters are only applicable to a dynamic model like MoorDyn, and are ignored by MoorPy. +Conversely, some Body parameters used by MoorPy for hydrostatics are not captured in a MoorDyn-style file. + + + +Running the MoorPy Model +------------------------ + +Once the MoorPy System is set up, it can be analyzed, viewed, and manipulated using a handful of main +functions, as well as a variety of additional helper functions for more specialized tasks. + +Here is an example showing one of the possible functions to analyze a mooring system: + + +.. code-block:: python + + ms.initialize() # make sure everything's connected + + ms.solveEquilibrium() # equilibrate + fig, ax = ms.plot() # plot the system in original configuration + ms.unload("sample.txt") # export to MD input file + + ms.bodyList[0].f6Ext = np.array([3e6, 0, 0, 0, 0, 0]) # apply an external force on the body + ms.solveEquilibrium() # equilibrate + fig, ax = ms.plot(ax=ax, color='red') # plot the system in displaced configuration (on the same plot, in red) + + + +**Documentation Overview** + +An overview of how a mooring system is represented in MoorPy can be found in :ref:`The Model Structure page`. + +More documentation and examples of other functions that can be applied to a MoorPy mooring system can be +found in :ref:`The Usage page`. + +Detailed theory "under the hood" of the functions in MoorPy can be found in :ref:`The Theory Page`. + +Detailed inputs and outputs of MoorPy classes and functions can be found in :ref:`The API Page`. -Both of these example scripts should run successfully after MoorPy has been installed. -They will produce a plot showing the mooring system in the calculated equilibrium state. -:ref:`The Model Structure page` gives an overview of how a mooring system -is represented in MoorPy. :ref:`The MoorPy Usage page` gives more information -about the function calls used to make these examples work, as well as other available -methods and outputs of MoorPy. \ No newline at end of file diff --git a/docs/structure.rst b/docs/structure.rst index 29d8f40..a9c146e 100644 --- a/docs/structure.rst +++ b/docs/structure.rst @@ -83,6 +83,7 @@ accessed afterward are as follows: - **f6Ext**: applied external forces and moments vector in global orientation (not including weight/buoyancy) [N]. The default is np.zeros(6). + The MoorPy System Object ------------------------ @@ -91,3 +92,36 @@ The System object in MoorPy is used to hold all the objects being simulated as w information like the line types dictionary, water density, and water depth. Most user interactions with the mooring system are supported through methods of the System class. These methods are listed on the :ref:`API` page. More description to be added here in future... + +Bathymetry +^^^^^^^^^^ + +MoorPy supports three different approaches to the seabed surface, which +are defined by the seabedMod flag: + +0. Uniform flat seabed specified by a depth value. +1. Uniform sloped seabed specified by a depth value at x,y = 0,0 along + with xSlope and ySlope valus that specify the slope (rise/run). If + only one of these values, the other is assumed to be zero. +2. A bathymetry grid where the seabed depth is interpolated as a function + of x and y coorinates based on bilinear interpolation from a rectangular + grid of depth data. This grid is usually read in from a specially formatted + file similar to MoorDyn (link to be added). + +Further usage of these features will be described in (link to usage section to be added). + +Current +^^^^^^^ + +The effect of current in terms of drag forces on the mooring lines can be +included, as controlled by the currentMod flag: + +0. No current is considered. +1. A steady uniform current is considered, specified by the System current + vector. The drag force from this current will be added to the weight + vector each time the catenary equations are used to solve the mooring + line profiles. +2. XX A steady current that is uniform in the horizontal direction but can + change with depth according to a lookup table (typically from an input + file following the same format as in MoorDyn. This feature is NOT YET + IMPLEMENTED. \ No newline at end of file diff --git a/docs/usage.rst b/docs/usage.rst index 496766b..ab05a2c 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -14,145 +14,40 @@ MoorPy Usage .. role:: stnd -Setting up a Mooring System ---------------------------- - -In MoorPy, the full moored floating system is contained in a System object, which includes -lists of Body, Point, and Line objects that make up the full assembly. This collection of -objects and their linkages can be set up manually via function calls, or they can be -generated based on reading in a MoorDyn-style input file. - - -Creating a MoorPy System Manually -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -MoorPy has functions to facilitate the orderly creation of a mooring system. The following -gives an example of how they work. - -.. code-block:: python - - - # ----- choose some system geometry parameters ----- - - depth = 600 # water depth [m] - angles = np.radians([60, 180, 300]) # line headings list [rad] - rAnchor = 1600 # anchor radius/spacing [m] - zFair = -21 # fairlead z elevation [m] - rFair = 20 # fairlead radius [m] - lineLength= 1800 # line unstretched length [m] - typeName = "chain1" # identifier string for the line type - - - # ----- set up the mooring system and floating body ----- - - # Create new MoorPy System and set its depth - ms = mp.System(depth=depth) - - # add a line type - ms.setLineType(dnommm=120, material='chain', name=typeName) # this would be 120 mm chain - - # Add a free, body at [0,0,0] to the system (including some properties to make it hydrostatically stiff) - ms.addBody(0, np.zeros(6), m=1e6, v=1e3, rM=100, AWP=1e3) - - # For each line heading, set the anchor point, the fairlead point, and the line itself - for i, angle in enumerate(angles): - - # create end Points for the line - ms.addPoint(1, [rAnchor*np.cos(angle), rAnchor*np.sin(angle), -depth]) # create anchor point (type 0, fixed) - ms.addPoint(1, [ rFair*np.cos(angle), rFair*np.sin(angle), zFair]) # create fairlead point (type 0, fixed) - - # attach the fairlead Point to the Body (so it's fixed to the Body rather than the ground) - ms.bodyList[0].attachPoint(2*i+2, [rFair*np.cos(angle), rFair*np.sin(angle), zFair]) - - # add a Line going between the anchor and fairlead Points - ms.addLine(lineLength, typeName, pointA=2*i+1, pointB=2*i+2) - - - - -Creating a MoorPy System for a MoorDyn Input File -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -.. _inputfile: - -A MoorPy System can be initialized by reading in a MoorDyn-style input file. This is simply done by -passing the input file name when creating the System object: - -.. code-block:: python - - ms = mp.System(file='the MoorDyn-style input file.txt') - - -The format of the input file is expected to follow the -MoorDyn v2 style, an example of which is shown below: - - -.. code-block:: none - - MoorDyn v2 Input File - Sample for input to MoorPy - ---------------------- LINE TYPES -------------------------------------------------- - TypeName Diam Mass/m EA BA/-zeta EI Cd Ca CdAx CaAx - (name) (m) (kg/m) (N) (N-s/-) (N-m^2) (-) (-) (-) (-) - chain 0.2160 286.56 1.23e9 -1.0 0.00 1.00 1.00 0.00 0.00 - --------------------- ROD TYPES ----------------------------------------------------- - TypeName Diam Mass/m Cd Ca CdEnd CaEnd - (name) (m) (kg/m) (-) (-) (-) (-) - ----------------------- BODIES ------------------------------------------------------ - ID Attachment X0 Y0 Z0 r0 p0 y0 Mass CG* I* Volume CdA* Ca* - (#) (-) (m) (m) (m) (deg) (deg) (deg) (kg) (m) (kg-m^2) (m^3) (m^2) (-) - 1 coupled 0.00 0.00 -0.75 0.00 0.00 0.00 1.0e6 0.00 0.00 1.0e3 0.00 0.00 - ---------------------- RODS --------------------------------------------------------- - ID RodType Attachment Xa Ya Za Xb Yb Zb NumSegs RodOutputs - (#) (name) (#/key) (m) (m) (m) (m) (m) (m) (-) (-) - ---------------------- POINTS ------------------------------------------------------- - ID Attachment X Y Z Mass Volume CdA Ca - (#) (-) (m) (m) (m) (kg) (mˆ3) (m^2) (-) - 1 Fixed 800.00 1385.64 -600.00 0.00 0.00 0.00 0.00 - 2 Body1 10.00 17.32 -21.00 0.00 0.00 0.00 0.00 - 3 Fixed -1600.00 0.00 -600.00 0.00 0.00 0.00 0.00 - 4 Body1 -20.00 0.00 -21.00 0.00 0.00 0.00 0.00 - 5 Fixed 800.00 -1385.64 -600.00 0.00 0.00 0.00 0.00 - 6 Body1 10.00 -17.32 -21.00 0.00 0.00 0.00 0.00 - ---------------------- LINES -------------------------------------------------------- - ID LineType AttachA AttachB UnstrLen NumSegs LineOutputs - (#) (name) (#) (#) (m) (-) (-) - 1 chain 1 2 1800.000 40 p - 2 chain 3 4 1800.000 40 p - 3 chain 5 6 1800.000 40 p - ---------------------- OPTIONS ------------------------------------------------------ - 600.0 depth - --------------------- need this line ------------------------------------------------ - - -Note that some parameters are only applicable to a dynamic model like MoorDyn, and are ignored by MoorPy. -Conversely, some Body parameters used by MoorPy for hydrostatics are not captured in a MoorDyn-style file. - - - -Running the MoorPy Model ------------------------- - -Once the MoorPy System is set up, it can be analyzed, viewed, and manipulated using a handful of main -functions, as well as a variety of additional helper functions for more specialized tasks. - -Here is an example showing the most important functions: - - -.. code-block:: python - - ms.initialize() # make sure everything's connected - - ms.solveEquilibrium() # equilibrate - fig, ax = ms.plot() # plot the system in original configuration - ms.unload("sample.txt") # export to MD input file - - ms.bodyList[0].f6Ext = np.array([3e6, 0, 0, 0, 0, 0]) # apply an external force on the body - ms.solveEquilibrium() # equilibrate - fig, ax = ms.plot(ax=ax, color='red') # plot the system in displaced configuration (on the same plot, in red) +Advanced Features +^^^^^^^^^^^^^^^^^ (A list of key functions to be added here) +Variable Rope Stiffness Behavior +-------------------------------- + +MoorPy supports separate static and dynamic stiffness coefficients, +to approximate the variable stiffness characterisics of synthetic +fiber ropes. For using this capability, the mooring line type +information must include a static stiffness value (EAs), and a +dynamic stiffness value (EAd). An additional factor on EAd that +scales with mean tension (EAd_Lm) can also be included. When using +a mooring line properties library (i.e., a yaml file), these values +should be specified by the EA_MBL, EAd_MBL, and EAd_MBL_Lm keywords, +respectively. See the (moorprops library section to be added) for +more information. + +Two System methods control switching between static and +dynamic stiffness properties: + +activateDynamicStiffness switches the mooring system model to dynamic +line stiffness values. It also adjusts the unstretched line lengths +to maintain the same tension at the current system state. If EAd has +not been set, it will not change anything (call it with display > 0 +to display a warning when this occurs). + +:func:`.system.activateDynamicStiffness` + +revertToStaticStiffness resets the mooring lines to use the static +stiffness values and return to their original unstretched lenths. + Additional Parameters in MoorPy ------------------------------- diff --git a/examples/.DS_Store b/examples/.DS_Store new file mode 100644 index 0000000..84271a1 Binary files /dev/null and b/examples/.DS_Store differ diff --git a/examples/MoordynSemiTautUpd.dat b/examples/MoordynSemiTautUpd.dat new file mode 100644 index 0000000..b025ba3 --- /dev/null +++ b/examples/MoordynSemiTautUpd.dat @@ -0,0 +1,63 @@ +MoorDyn v2 Input File +Generated by MoorPy +---------------------- LINE TYPES -------------------------------------------------- +TypeName Diam Mass/m EA BA/-zeta EI Cd Ca CdAx CaAx +(name) (m) (kg/m) (N) (N-s/-) (N-m^2) (-) (-) (-) (-) +0 0.2791 480.93 2.058e+09 -1.000e+00 0.000e+00 1.200 1.000 0.20 0.00 +1 0.1438 22.42 1.424e+08|1.586e+08 4E9|11e6 0.000e+00 1.200 1.000 0.20 0.00 +--------------------- ROD TYPES ----------------------------------------------------- +TypeName Diam Mass/m Cd Ca CdEnd CaEnd +(name) (m) (kg/m) (-) (-) (-) (-) +----------------------- BODIES ------------------------------------------------------ +ID Attachment X0 Y0 Z0 r0 p0 y0 Mass CG* I* Volume CdA* Ca* +(#) (-) (m) (m) (m) (deg) (deg) (deg) (kg) (m) (kg-m^2) (m^3) (m^2) (-) +---------------------- RODS --------------------------------------------------------- +ID RodType Attachment Xa Ya Za Xb Yb Zb NumSegs RodOutputs +(#) (name) (#/key) (m) (m) (m) (m) (m) (m) (-) (-) +---------------------- POINTS ------------------------------------------------------- +ID Attachment X Y Z Mass Volume CdA Ca +(#) (-) (m) (m) (m) (kg) (m^3) (m^2) (-) +1 Fixed 350.00 606.22 -200.00 0.00 0.00 0.00 0.00 +2 Free 108.24 187.48 -138.42 0.00 0.00 0.00 0.00 +3 Coupled 29.00 50.23 -14.00 0.00 0.00 0.00 0.00 +4 Fixed -700.00 0.00 -200.00 0.00 0.00 0.00 0.00 +5 Free -216.49 0.00 -138.42 0.00 0.00 0.00 0.00 +6 Coupled -58.00 0.00 -14.00 0.00 0.00 0.00 0.00 +7 Fixed 350.00 -606.22 -200.00 0.00 0.00 0.00 0.00 +8 Free 108.24 -187.48 -138.42 0.00 0.00 0.00 0.00 +9 Coupled 29.00 -50.23 -14.00 0.00 0.00 0.00 0.00 +---------------------- LINES -------------------------------------------------------- +ID LineType AttachA AttachB UnstrLen NumSegs LineOutputs +(#) (name) (#) (#) (m) (-) (-) +1 0 1 2 497.702 20 p +2 1 2 3 199.800 6 p +3 0 4 5 497.702 20 p +4 1 5 6 199.800 6 p +5 0 7 8 497.702 20 p +6 1 8 9 199.800 6 p +---------------------- OPTIONS ------------------------------------------------------ +0.001 dtM +3000000.0 kb +300000.0 cb +60 TmaxIC +9.81 g +200 depth +1025 rho +----------------------- OUTPUTS ----------------------------------------------------- +FairTen1 +FairTen2 +FairTen3 +FairTen4 +FairTen5 +FairTen6 +Point1Fx +Point1Fy +Point1Fz +Point4Fx +Point4Fy +Point4Fz +Point7Fx +Point7Fy +Point7Fz +END +--------------------- need this line ------------------------------------------------ diff --git a/examples/manual_subsystem.py b/examples/manual_subsystem.py new file mode 100644 index 0000000..23fb7e7 --- /dev/null +++ b/examples/manual_subsystem.py @@ -0,0 +1,116 @@ +# MoorPy Example Script: +# Example of manually setting up a mooring system in MoorPy and solving equilibrium. +# This example features a subsystem to make sure it works. This will become a test after. + +import numpy as np +import matplotlib.pyplot as plt +import moorpy as mp +from moorpy.MoorProps import getLineProps +from moorpy.subsystem import Subsystem + + +# ----- choose some system geometry parameters ----- + +depth = 200 # water depth [m] +angles = np.radians([60, 300]) # line headings list [rad] +rAnchor = 600 # anchor radius/spacing [m] +zFair = -21 # fairlead z elevation [m] +rFair = 20 # fairlead radius [m] +lineLength= 650 # line unstretched length [m] +typeName = "chain1" # identifier string for the line type + + +# ===== First a simple Subsystem by itself ===== + +# create a subsystem +ss = Subsystem(depth=depth, spacing=rAnchor, rBfair=[10,0,-20]) + +# set up the line types +ss.setLineType(180, 'chain', name='one') +ss.setLineType( 50, 'chain', name='two') + +# set up the lines and points and stuff +lengths = [350, 300] +types = ['one', 'two'] +ss.makeGeneric(lengths, types) + +# plotting examples +ss.setEndPosition([0 ,-40,-200], endB=0) +ss.setEndPosition([300,400, -10], endB=1) +ss.staticSolve() +# 3D plot in the Subsystem's local frame +fig, ax = ss.plot() +# 2D view of the same +ss.plot2d() +# Line-like plot (in global reference frame) +fig = plt.figure() +ax = plt.axes(projection='3d') +ss.drawLine(0, ax, color='r') + + +# ===== Now a Subsystem in a larger System with a floating body ===== + +# Create new MoorPy System and set its depth +ms = mp.System(depth=depth) + +# add a line type +ms.setLineType(dnommm=120, material='chain', name=typeName) # this would be 120 mm chain + +# Add a free, body at [0,0,0] to the system (including some properties to make it hydrostatically stiff) +ms.addBody(0, np.zeros(6), m=1e6, v=1e3, rM=100, AWP=1e3) + +# For each line heading, set the anchor point, the fairlead point, and the line itself +for i, angle in enumerate(angles): + + # create end Points for the line + ms.addPoint(1, [rAnchor*np.cos(angle), rAnchor*np.sin(angle), -depth]) # create anchor point (type 0, fixed) + ms.addPoint(1, [ rFair*np.cos(angle), rFair*np.sin(angle), zFair]) # create fairlead point (type 0, fixed) + + # attach the fairlead Point to the Body (so it's fixed to the Body rather than the ground) + ms.bodyList[0].attachPoint(2*i+2, [rFair*np.cos(angle), rFair*np.sin(angle), zFair]) + + # add a Line going between the anchor and fairlead Points + ms.addLine(lineLength, typeName, pointA=2*i+1, pointB=2*i+2) + +# ----- Now add a SubSystem line! ----- +ss = Subsystem(mooringSys=ms, depth=depth, spacing=rAnchor, rBfair=[10,0,-20]) + +# set up the line types +ms.setLineType(180, 'chain', name='one') +ms.setLineType( 50, 'chain', name='two') + +# set up the lines and points and stuff +ls = [350, 300] +ts = ['one', 'two'] +ss.makeGeneric(lengths=ls, types=ts) + +# add points that the subSystem will attach to... +ms.addPoint(1, [-rAnchor, 100, -depth]) # Point 5 - create anchor point (type 0, fixed) +ms.addPoint(1, [ -rFair , 0, zFair]) # Point 6 - create fairlead point (type 0, fixed) +ms.bodyList[0].attachPoint(6, [-rFair, 0, zFair]) # attach the fairlead Point to the Body + +# string the Subsystem between the two points! +ms.lineList.append(ss) # add the SubSystem to the System's lineList +ss.number = 3 +ms.pointList[4].attachLine(3, 0) # attach it to the respective points +ms.pointList[5].attachLine(3, 1) # attach it to the respective points + + +# ----- run the model to demonstrate ----- + +ms.initialize() # make sure everything's connected + +ms.solveEquilibrium() # equilibrate +fig, ax = ms.plot() # plot the system in original configuration +#ms.unload("sample_from_manual.txt") # export to MD input file + +ms.bodyList[0].f6Ext = np.array([3e6, 0, 0, 0, 0, 0]) # apply an external force on the body +ms.solveEquilibrium3() # equilibrate +fig, ax = ms.plot(ax=ax, color='red') # plot the system in displaced configuration (on the same plot, in red) + +print(f"Body offset position is {ms.bodyList[0].r6}") + +plt.show() + + + diff --git a/examples/sample.txt b/examples/sample.txt index ff73b5e..b9dc6ae 100644 --- a/examples/sample.txt +++ b/examples/sample.txt @@ -16,7 +16,7 @@ ID RodType Attachment Xa Ya Za Xb Yb Zb NumSegs RodOutputs (#) (name) (#/key) (m) (m) (m) (m) (m) (m) (-) (-) ---------------------- POINTS ------------------------------------------------------- ID Attachment X Y Z Mass Volume CdA Ca -(#) (-) (m) (m) (m) (kg) (mˆ3) (m^2) (-) +(#) (-) (m) (m) (m) (kg) (m^3) (m^2) (-) 1 Fixed 800.00 1385.64 -600.00 0.00 0.00 0.00 0.00 2 Body1 10.00 17.32 -21.00 0.00 0.00 0.00 0.00 3 Fixed -1600.00 0.00 -600.00 0.00 0.00 0.00 0.00 diff --git a/moorpy/Catenary.py b/moorpy/Catenary.py index c7fda8d..ed5d027 100644 --- a/moorpy/Catenary.py +++ b/moorpy/Catenary.py @@ -8,7 +8,8 @@ -def catenary(XF, ZF, L, EA, W, CB=0, HF0=0, VF0=0, Tol=0.000001, nNodes=20, MaxIter=100, plots=0): +def catenary(XF, ZF, L, EA, W, CB=0, alpha=0, HF0=0, VF0=0, Tol=0.000001, + nNodes=20, MaxIter=100, plots=0, depth=0): ''' The quasi-static mooring line solver. Adapted from catenary subroutine in FAST v7 by J. Jonkman. Note: this version is updated Oct 7 2020 to use the dsolve solver. @@ -25,14 +26,18 @@ def catenary(XF, ZF, L, EA, W, CB=0, HF0=0, VF0=0, Tol=0.000001, nNodes=20, MaxI Extensional stiffness of line [N] W : float Weight of line in fluid per unit length [N/m] + alpha : float + seabed incline angle along line from end A to B [deg] CB : float, optional - If positive, coefficient of seabed static friction drag. If negative, no seabed contact and the value is the distance down from end A to the seabed in m\ - NOTE: friction (CV > 0) should only be applied when end A of the line is at an anchor, otherwise assumptions are violated. + If positive, coefficient of seabed static friction drag. If negative, + no seabed contact and the value is the distance down from end A to + the seabed in m. + NOTE: friction (CV > 0) should only be applied when end A of the line + is at an anchor, otherwise assumptions are violated. HF0 : float, optional Horizontal fairlead tension. If zero or not provided, a guess will be calculated. VF0 : float, optional - Vertical fairlead tension. If zero or not provided, a guess will be calculated. - + Vertical fairlead tension. If zero or not provided, a guess will be calculated. Tol : float, optional Convergence tolerance within Newton-Raphson iteration specified as an absolute displacement error nNodes : int, optional @@ -46,16 +51,32 @@ def catenary(XF, ZF, L, EA, W, CB=0, HF0=0, VF0=0, Tol=0.000001, nNodes=20, MaxI Returns ------- : tuple - (end 1 horizontal tension, end 1 vertical tension, end 2 horizontal tension, end 2 vertical tension, info dictionary) [N] (positive up) + (end 1 horizontal tension, end 1 vertical tension, end 2 horizontal + tension, end 2 vertical tension, info dictionary) [N] (positive up). + Info dictionary contains the following: + HF and VF - horizontal and vertical tension components of end B [N]. + stiffnessA - 2D stiffness matrix for end A [N/m]. + stiffnessB - 2D stiffness matrix for end B [N/m]. + stiffnessBA - 2D stiffness matrix for force at B due to movement of A [N/m]. + LBot - length of line section laying on the seabed [m]. + ProfileType + Zextreme - extreme z coordinate of the line section (in direction of + wet weight), relative to height of end A [m]. ''' vertical_threshold = 0.0001 # the XF/ZF ratio below which the line will be approximated as vertical to avoid catenary errors (this could become an input parameter) + # Set a very small value to use as the horizontal stiffness in slack cases + # instead of to avoid indeterminant stiffness matrices and to allow the + # solver to move toward equilibrium even when all mooring lines may be + # slack. Set as a fraction of W, which is roughly kz for slack cases. + slack_kx = 0.01*abs(W) + # make info dict to contain any additional outputs info = dict(error=False) - info['call'] = f"catenary({XF}, {ZF}, {L}, {EA}, {W}, CB={CB}, HF0={HF0}, VF0={VF0}, Tol={Tol}, MaxIter={MaxIter}, plots=1)" + info['call'] = f"catenary({XF}, {ZF}, {L}, {EA}, {W}, CB={CB}, alpha={alpha}, HF0={HF0}, VF0={VF0}, Tol={Tol}, MaxIter={MaxIter}, depth={depth}, plots=1)" # make some arrays if needed for plotting each node @@ -65,23 +86,52 @@ def catenary(XF, ZF, L, EA, W, CB=0, HF0=0, VF0=0, Tol=0.000001, nNodes=20, MaxI Zs= np.zeros(nNodes) # Vertical locations of each line node relative to the anchor (meters) Te= np.zeros(nNodes) # Effective line tensions at each node (N) + # compute height of each end off seabed + hA = max(0, -CB) + hB = ZF + hA - XF*np.tan(np.radians(alpha)) + if abs(alpha) > 1 and abs(hB) < 0.001*L: + hB = 0 # small adjustment to allow margin for error when end B is on the seabed + if hB < 0: + breakpoint() + raise CatenaryError("End B is below the seabed.") + # >>> from this point on in the code, CB will only be used for friction + # and hA or hB will be used for height off seabed <<< + if CB < 0: + CB = 0.0 # flip line in the solver if it is buoyant if W < 0: W = -W ZF = -ZF - CB = -10000. # <<< TODO: set this to the distance to sea surface <<< + CB = 0 + # Set hA, hB to distances to sea surface + if depth > 0: + hB = depth - hB + hA = depth - hA flipFlag = True else: flipFlag = False # reverse line in the solver if end A is above end B - if ZF < 0: + if ZF < 0 and (hA > 0 or flipFlag): # and if end A isn't on the seabed (unless it's a buoyant line) + #CB = -max(0, -CB) - ZF + XF*np.tan(np.radians(alpha)) + hA, hB = hB, hA ZF = -ZF + alpha = -alpha reverseFlag = True else: reverseFlag = False + # If an end is "below the bottom" (likely it means a buoyant line cross + # the sea surface), ignore the crossing. + if hA < 0: + hB = hB - hA + hA = 0 + + # avoid issue with tolerance on the margin of full seabed contact + if abs(ZF <= Tol) and alpha==0: + ZF = 0 + # ensure the input variables are realistic if XF < 0.0: raise CatenaryError("XF is negative!") @@ -91,12 +141,12 @@ def catenary(XF, ZF, L, EA, W, CB=0, HF0=0, VF0=0, Tol=0.000001, nNodes=20, MaxI if EA <= 0.0: raise CatenaryError("EA is zero or negative!") - # Solve for the horizontal and vertical forces at the fairlead (HF, VF) and at the anchor (HA, VA) # There are many "ProfileTypes" of a mooring line and each must be analyzed separately (1-3 are consistent with FAST v7) # ProfileType=0: Entire line is on seabed # ProfileType=1: No portion of the line rests on the seabed + # ProfileType=-1: Approximation for a taut line if the normal catenary algorithm failed. # ProfileType=2: A portion of the line rests on the seabed and the anchor tension is nonzero # ProfileType=3: A portion of the line must rest on the seabed and the anchor tension is zero # ProfileType=4: The line is negatively buoyant, seabed interaction is enabled, and the line @@ -106,27 +156,35 @@ def catenary(XF, ZF, L, EA, W, CB=0, HF0=0, VF0=0, Tol=0.000001, nNodes=20, MaxI # double-back on itself; the line forms an "L" between the anchor and fairlead. Then it # models it as bunched up on the seabed (instead of throwing an error) # ProfileType=5: Similar to above but both ends are off seabed, so it's U shaped and fully slack - # ProfileType=6: Completely vertical line that is off the seabed (on the seabed is handled by 4 and 5) + # ProfileType=6: Completely vertical line without seabed contact (on the seabed is handled by 4 and 5) + # ProfileType = 7: Portion of the line is resting on the seabed, and the seabed has a slope EA_W = EA/W # calculate the unstretched length that would be hanging if the line was fully slack (vertical down to flat on the seabed) + ''' if CB < 0: # free floating (potentially U shaped case) LHanging1 = np.sqrt(2.0*( -CB)*EA_W + EA_W*EA_W) - EA_W # unstretched hanging length at end A LHanging2 = np.sqrt(2.0*(ZF-CB)*EA_W + EA_W*EA_W) - EA_W # unstretched hanging length at end B LHanging = LHanging1+LHanging2 else: # at least one end on seabed LHanging = np.sqrt(2.0*ZF*EA_W + EA_W*EA_W) - EA_W # unstretched length of line hanging vertically to seabed - + ''' + LHanging1 = np.sqrt(2.0*hA*EA_W + EA_W*EA_W) - EA_W # unstretched hanging length at end A + LHanging2 = np.sqrt(2.0*hB*EA_W + EA_W*EA_W) - EA_W # unstretched hanging length at end B + LHanging = LHanging1 + LHanging2 # calculate a vertical stiffness estimate for an end lifting off the seabed def dV_dZ_s(z0, H): # height off seabed to evaluate at (infinite if 0), horizontal tension #return W*(z0*W/H + 1)/np.sqrt( (z0*W/H + 1)**2 - 1) # inelastic apprxoimation return W # returning a fully slack line approximation, # because a large value here risks adding a bad cross coupling term in the system stiffness matrix - + # ProfileType 0 case - entirely along seabed - if ZF==0.0 and CB >= 0.0 and W > 0: + if ZF==0.0 and hA == 0.0 and W > 0: + + # >>> this case should be extended to support sloped seabeds <<< + if not alpha == 0: raise CatenaryError("Line along seabed but seabed is sloped - not yet supported") ProfileType = 0 @@ -137,6 +195,8 @@ def dV_dZ_s(z0, H): # height off seabed to evaluate at (infinite if 0), horizo HF = (XF/L -1.0)*EA + 0.5*CB*W*L HA = np.max([0.0, HF - CB*W*L]) else: # case 3: seabed friction and zero anchor tension + if EA*CB*W*(XF-L) < 0: # something went wrong + breakpoint() HF = np.sqrt(2*EA*CB*W*(XF-L)) HA = 0.0 @@ -155,7 +215,7 @@ def dV_dZ_s(z0, H): # height off seabed to evaluate at (infinite if 0), horizo info["VF"] = 0.0 info["stiffnessB"] = np.array([[ dHF_dXF, 0.0], [0.0, dVF_dZF]]) info["stiffnessA"] = np.array([[ dHF_dXF, 0.0], [0.0, dVF_dZF]]) - info["stiffnessAB"] = np.array([[-dHF_dXF, 0.0], [0.0, 0.0]]) + info["stiffnessBA"] = np.array([[-dHF_dXF, 0.0], [0.0, 0.0]]) info["LBot"] = L info['ProfileType'] = 0 info['Zextreme'] = 0 @@ -183,12 +243,12 @@ def dV_dZ_s(z0, H): # height off seabed to evaluate at (infinite if 0), horizo else: # the tension is nonzero Xs[I] = s[I] + CB*W/EA*(s[I] - xB)**2 Te[I] = HF - CB*W*(L-s[I]) - + # ProfileType 4 case - fully slack - elif (W > 0.0) and (L >= XF + LHanging): + elif (W > 0.0) and (L >= XF/np.cos(np.radians(alpha)) + LHanging): - if CB >= 0.0: # one end on seabed + if hA == 0.0: # one end on seabed ProfileType = 4 # this is a special case that requires no iteration @@ -201,9 +261,9 @@ def dV_dZ_s(z0, H): # height off seabed to evaluate at (infinite if 0), horizo info["HF"] = HF # solution to be used to start next call (these are the solved variables, may be for anchor if line is reversed) info["VF"] = VF - info["stiffnessB"] = np.array([[0.0, 0.0], [0.0, dVF_dZF]]) - info["stiffnessA"] = np.array([[0.0, 0.0], [0.0, W]]) - info["stiffnessAB"] = np.array([[0.0, 0.0], [0.0, 0.0]]) + info["stiffnessB"] = np.array([[slack_kx, 0.0], [0.0, dVF_dZF]]) + info["stiffnessA"] = np.array([[slack_kx, 0.0], [0.0, W]]) + info["stiffnessBA"] = np.array([[0.0, 0.0], [0.0, 0.0]]) info["LBot"] = L - LHanging info['ProfileType'] = 4 info['Zextreme'] = 0 @@ -211,6 +271,9 @@ def dV_dZ_s(z0, H): # height off seabed to evaluate at (infinite if 0), horizo if plots > 0: + cos_alpha = np.cos(np.radians(alpha)) + tan_alpha = np.tan(np.radians(alpha)) + for I in range(nNodes): if s[I] > L-LHanging: # this node is on the suspended/hanging portion of the line @@ -220,13 +283,16 @@ def dV_dZ_s(z0, H): # height off seabed to evaluate at (infinite if 0), horizo else: # this node is on the seabed - Xs[I] = np.min([s[I], XF]) - Zs[I] = 0.0 + Xs[I] = np.min([s[I]*cos_alpha, XF]) + Zs[I] = Xs[I]*tan_alpha Te[I] = 0.0 else: # U shaped ProfileType = 5 + + # >>> this case should be extended to support sloped seabeds <<< + if not alpha == 0: raise CatenaryError("Skack U profile along seabed but seabed is sloped - not yet supported") HF = 0.0 VF = W*LHanging2 @@ -237,12 +303,12 @@ def dV_dZ_s(z0, H): # height off seabed to evaluate at (infinite if 0), horizo info["HF"] = HF # solution to be used to start next call (these are the solved variables, may be for anchor if line is reversed) info["VF"] = VF - info["stiffnessB"] = np.array([[0.0, 0.0], [0.0, W / np.sqrt(2.0*(ZF-CB)/EA_W + 1.0)]]) - info["stiffnessA"] = np.array([[0.0, 0.0], [0.0, W / np.sqrt(2.0*( -CB)/EA_W + 1.0)]]) - info["stiffnessAB"] = np.array([[0.0, 0.0], [0.0, 0.0]]) + info["stiffnessB"] = np.array([[slack_kx, 0.0], [0.0, W / np.sqrt(2.0*hB/EA_W + 1.0)]]) + info["stiffnessA"] = np.array([[slack_kx, 0.0], [0.0, W / np.sqrt(2.0*hA/EA_W + 1.0)]]) + info["stiffnessBA"] = np.array([[0.0, 0.0], [0.0, 0.0]]) info["LBot"] = L - LHanging info['ProfileType'] = 5 - info['Zextreme'] = CB + info['Zextreme'] = -hA if plots > 0: @@ -255,7 +321,7 @@ def dV_dZ_s(z0, H): # height off seabed to evaluate at (infinite if 0), horizo elif s[I] <= L-LHanging2: # the middle portion of the line, slack along the seabed Xs[I] = (s[I]-LHanging1)*XF/(L-LHanging1-LHanging2) - Zs[I] = CB + Zs[I] = -hA Te[I] = 0.0 else: # the 2nd suspended/hanging portion of the line @@ -263,7 +329,6 @@ def dV_dZ_s(z0, H): # height off seabed to evaluate at (infinite if 0), horizo Xs[I] = XF Zs[I] = ZF - Lms - W/EA*(LHanging2*Lms - 0.5*Lms**2 ) Te[I] = W*Lms - # ProfileType 6 case - vertical line without seabed contact @@ -290,7 +355,7 @@ def dV_dZ_s(z0, H): # height off seabed to evaluate at (infinite if 0), horizo info["VF"] = VF info["stiffnessB"] = np.array([[ VF/ZF, 0.0], [0.0, 0.5*W]]) info["stiffnessA"] = np.array([[ VF/ZF, 0.0], [0.0, 0.5*W]]) - info["stiffnessAB"] = np.array([[-VF/ZF, 0.0], [0.0,-0.5*W]]) + info["stiffnessBA"] = np.array([[-VF/ZF, 0.0], [0.0,-0.5*W]]) info["LBot"] = 0.0 info['ProfileType'] = 6 info['Zextreme'] = -hA @@ -330,7 +395,7 @@ def dV_dZ_s(z0, H): # height off seabed to evaluate at (infinite if 0), horizo info["VF"] = VF info["stiffnessB"] = np.array([[ VF/ZF, 0.0], [0.0, EA/L]]) info["stiffnessA"] = np.array([[ VF/ZF, 0.0], [0.0, EA/L]]) - info["stiffnessAB"] = np.array([[-VF/ZF, 0.0], [0.0,-EA/L]]) + info["stiffnessBA"] = np.array([[-VF/ZF, 0.0], [0.0,-EA/L]]) info["LBot"] = 0.0 info['ProfileType'] = 6 info['Zextreme'] = 0 @@ -384,22 +449,22 @@ def dV_dZ_s(z0, H): # height off seabed to evaluate at (infinite if 0), horizo # make sure required values are non-zero HF = np.max([ HF, Tol ]) XF = np.max([ XF, Tol ]) - ZF = np.max([ ZF, Tol ]) + if ZF > -Tol: # allow negative values + ZF = np.max([ ZF, Tol ]) # some initial values just for printing before they're filled in EXF=0 EZF=0 - + # Solve the analytical, static equilibrium equations for a catenary (or taut) mooring line with seabed interaction: X0 = [HF, VF] Ytarget = [0,0] - args = dict(cat=[XF, ZF, L, EA, W, CB, WL, WEA, L_EA, CB_EA], step=[0.15,1.0,1.5]) + args = dict(cat=[XF, ZF, L, EA, W, CB, hA, hB, alpha, WL, WEA, L_EA, CB_EA]) #, step=[0.15,1.0,1.5]) # call the master solver function #X, Y, info2 = msolve.dsolve(eval_func_cat, X0, Ytarget=Ytarget, step_func=step_func_cat, args=args, tol=Tol, maxIter=MaxIter, a_max=1.2) X, Y, info2 = dsolve2(eval_func_cat, X0, Ytarget=Ytarget, step_func=step_func_cat, args=args, ytol=Tol, stepfac=1, maxIter=MaxIter, a_max=1.2) - # retry if it failed if info2['iter'] >= MaxIter-1 or info2['oths']['error']==True or np.linalg.norm(info2['err']) > 10*Tol: # ! Perhaps we failed to converge because our initial guess was too far off. @@ -422,91 +487,133 @@ def dV_dZ_s(z0, H): # height off seabed to evaluate at (infinite if 0), horizo HF = np.max([ abs( 0.5*W* XF/ Lamda0 ), Tol ]) # As above, set the lower limit of the guess value of HF to the tolerance VF = 0.5*W*( ZF/np.tanh(Lamda0) + L ) - + X0 = [HF, VF] Ytarget = [0,0] - args = dict(cat=[XF, ZF, L, EA, W, CB, WL, WEA, L_EA, CB_EA], step=[0.1,0.8,1.5]) # step: alpha_min, alpha0, alphaR + args = dict(cat=[XF, ZF, L, EA, W, CB, hA, hB, alpha, WL, WEA, L_EA, CB_EA]) #, step=[0.1,0.8,1.5]) # step: alpha_min, alpha0, alphaR # call the master solver function #X, Y, info3 = msolve.dsolve(eval_func_cat, X0, Ytarget=Ytarget, step_func=step_func_cat, args=args, tol=Tol, maxIter=MaxIter, a_max=1.1) #, dX_last=info2['dX']) X, Y, info3 = dsolve2(eval_func_cat, X0, Ytarget=Ytarget, step_func=step_func_cat, args=args, ytol=Tol, stepfac=1, maxIter=MaxIter, a_max=1.2) - + # retry if it failed if info3['iter'] >= MaxIter-1 or info3['oths']['error']==True: - X0 = X Ytarget = [0,0] - args = dict(cat=[XF, ZF, L, EA, W, CB, WL, WEA, L_EA, CB_EA], step=[0.1,1.0,2.0]) + args = dict(cat=[XF, ZF, L, EA, W, CB, hA, hB, alpha, WL, WEA, L_EA, CB_EA]) #, step=[0.05,1.0,1.0]) # call the master solver function #X, Y, info4 = msolve.dsolve(eval_func_cat, X0, Ytarget=Ytarget, step_func=step_func_cat, args=args, tol=Tol, maxIter=10*MaxIter, a_max=1.15) #, dX_last=info3['dX']) X, Y, info4 = dsolve2(eval_func_cat, X0, Ytarget=Ytarget, step_func=step_func_cat, args=args, ytol=Tol, stepfac=1, maxIter=MaxIter, a_max=1.2) # check if it failed - if info4['iter'] >= 10*MaxIter-1 or info4['oths']['error']==True: - - print("catenary solve failed on all 3 attempts.") - print(f"catenary({XF}, {ZF}, {L}, {EA}, {W}, CB={CB}, HF0={HF0}, VF0={VF0}, Tol={Tol}, MaxIter={MaxIter}, plots=1)") - - print("First attempt's iterations are as follows:") - for i in range(info2['iter']+1): - print(f" Iteration {i}: HF={info2['Xs'][i,0]: 8.4e}, VF={info2['Xs'][i,1]: 8.4e}, EX={info2['Es'][i,0]: 6.2e}, EZ={info2['Es'][i,1]: 6.2e}") - - print("Second attempt's iterations are as follows:") - for i in range(info3['iter']+1): - print(f" Iteration {i}: HF={info3['Xs'][i,0]: 8.4e}, VF={info3['Xs'][i,1]: 8.4e}, EX={info3['Es'][i,0]: 6.2e}, EZ={info3['Es'][i,1]: 6.2e}") - - - print("Last attempt's iterations are as follows:") - for i in range(info4['iter']+1): - print(f" Iteration {i}: HF={info4['Xs'][i,0]: 8.4e}, VF={info4['Xs'][i,1]: 8.4e}, EX={info4['Es'][i,0]: 6.2e}, EZ={info4['Es'][i,1]: 6.2e}") - - - ''' - # plot solve performance - fig, ax = plt.subplots(4,1, sharex=True) - ax[0].plot(np.hstack([info2['Xs'][:,0], info3['Xs'][:,0], info4['Xs'][:,0]])) - ax[1].plot(np.hstack([info2['Xs'][:,1], info3['Xs'][:,1], info4['Xs'][:,1]])) - ax[2].plot(np.hstack([info2['Es'][:,0], info3['Es'][:,0], info4['Es'][:,0]])) - ax[3].plot(np.hstack([info2['Es'][:,1], info3['Es'][:,1], info4['Es'][:,1]])) - ax[0].set_ylabel("HF") - ax[1].set_ylabel("VF") - ax[2].set_ylabel("X err") - ax[3].set_ylabel("Z err") - - # plot solve path - plt.figure() - - #c = np.hypot(info2['Es'][:,0], info2['Es'][:,1]) + if info4['iter'] >= MaxIter-1 or info4['oths']['error']==True: - c = np.arange(info2['iter']+1) - c = cm.jet((c-np.min(c))/(np.max(c)-np.min(c))) + # ----- last-ditch attempt for a straight line ----- + d = np.sqrt(XF*XF+ZF*ZF) - for i in np.arange(info2['iter']): - plt.plot(info2['Xs'][i:i+2,0], info2['Xs'][i:i+2,1],":", c=c[i]) - plt.plot(info2['Xs'][0,0], info2['Xs'][0,1],"o") - - c = np.arange(info3['iter']+1) - c = cm.jet((c-np.min(c))/(np.max(c)-np.min(c))) - - for i in np.arange(info3['iter']): - plt.plot(info3['Xs'][i:i+2,0], info3['Xs'][i:i+2,1], c=c[i]) - plt.plot(info3['Xs'][0,0], info3['Xs'][0,1],"*") - - c = np.arange(info4['iter']+1) - c = cm.jet((c-np.min(c))/(np.max(c)-np.min(c))) + # if it's taut -- let's do an approximation that ignores weight + if d/L > 1: + + # tension, assuming a straight line and ignoring weight, is + T0 = (d/L - 1) * EA + + # solve the horizontal equivalent using a parabola approximation! + theta = np.arctan2(ZF,XF) # rotation angle + W2 = W*np.cos(theta) # transformed vertical weight (i.e. transverse distributed load) + + # Get line profile for future plotting. The quadratic term is just w/T0. + # Figure out the right z = w/T0 x^2 + bx + c + c = 0 + # z(d) = 0 = w/T0 d^2 + b d + b = -W2/T0 * d # slope at x=0 + X2 = np.linspace(0, d, nNodes) + Z2 = W2/T0*X2**2 + b*X2 + + Xs_parabola = X2*np.cos(theta) - Z2*np.sin(theta) # put in global frame + Zs_parabola = X2*np.sin(theta) + Z2*np.cos(theta) + + # Now get the forces and stiffnesses, again assuming it's just based on elasticity + # This is back in the regular orientation - should we add weight in? + HF = T0*np.cos(theta) + VF = T0*np.sin(theta) + HA = T0*np.cos(theta) + VA = T0*np.sin(theta) + R = np.array([[np.cos(theta), -np.sin(theta)],[np.sin(theta), np.cos(theta)]]) # rotation matrix + Kl = EA/L # inline stiffness + Kt = T0/d # transverse stiffness + K = np.matmul(R, np.matmul( np.array([[Kl,0],[0,Kt]]), R.T) ) # stiffness matrix (should check some of the math) + + # put things in the format expected for later parts of the code + X = np.zeros(2) + X[0] = HF + X[1] = VF + + info4['oths']['HF'] = HF + info4['oths']['VF'] = VF + info4['oths']['HA'] = HA + info4['oths']['VA'] = VA + info4['oths']['LBot'] = 0 + + info4['oths']["stiffnessA"] = np.array(K) + info4['oths']["stiffnessB"] = np.array(K) + info4['oths']["stiffnessBA"] = np.array(-K) + + info4['oths']['ProfileType'] = -1 # flag for the parabolic solution for the coordinates... + info4['oths']['error'] = False + info4['oths']['message'] = 'Approximated as a straight massless spring as a last resort.' + + + info.update(info4['oths']) # <<< hopefully I modified enough in info4 for this to work + info['catenary'] = info4 - for i in np.arange(info4['iter']): - plt.plot(info4['Xs'][i:i+2,0], info4['Xs'][i:i+2,1], c=c[i]) - plt.plot(info4['Xs'][0,0], info4['Xs'][0,1],"*") + # if it's slightly slack and mostly on the seabed, make a bilinear approximation + elif d/L > 0.9: + L_bot = (d**2 - L**2)/(2*(XF-L)) # length on seabed + Ls = L - L_bot + VF = Ls*W # weight of suspended length + HF = (XF - L_bot)/ZF * VF - plt.title("catenary solve path for troubleshooting") - plt.show() - - #breakpoint() - ''' - raise CatenaryError("catenary solver failed.") + # put things in the format expected for later parts of the code + X = np.zeros(2) + X[0] = HF + X[1] = VF + + info4['oths']['HF'] = HF + info4['oths']['VF'] = VF + info4['oths']['HA'] = HA + info4['oths']['VA'] = VA + info4['oths']['LBot'] = 0 + + info4['oths']["stiffnessA"] = np.array(K) + info4['oths']["stiffnessB"] = np.array(K) + info4['oths']["stiffnessBA"] = np.array(-K) + + info4['oths']['ProfileType'] = -2 # flag for the bilinear solution for the coordinates... + info4['oths']['error'] = False + info4['oths']['message'] = 'Approximated as bilinear with seabed contact as a last resort.' + + # Otherwise, we'd need an iterative solve of tension, curvature, and distributed load. + # It could potentially be with a parabola. We don't have that yet, so fail. + else: + print("catenary solve failed on all 3 attempts.") + print(f"catenary({XF}, {ZF}, {L}, {EA}, {W}, CB={CB}, HF0={HF0}, VF0={VF0}, Tol={Tol}, MaxIter={MaxIter}, plots=1)") + + print("First attempt's iterations are as follows:") + for i in range(info2['iter']+1): + print(f" Iteration {i}: HF={info2['Xs'][i,0]: 8.4e}, VF={info2['Xs'][i,1]: 8.4e}, EX={info2['Es'][i,0]: 6.2e}, EZ={info2['Es'][i,1]: 6.2e}") + + print("Second attempt's iterations are as follows:") + for i in range(info3['iter']+1): + print(f" Iteration {i}: HF={info3['Xs'][i,0]: 8.4e}, VF={info3['Xs'][i,1]: 8.4e}, EX={info3['Es'][i,0]: 6.2e}, EZ={info3['Es'][i,1]: 6.2e}") + + + print("Last attempt's iterations are as follows:") + for i in range(info4['iter']+1): + print(f" Iteration {i}: HF={info4['Xs'][i,0]: 8.4e}, VF={info4['Xs'][i,1]: 8.4e}, EX={info4['Es'][i,0]: 6.2e}, EZ={info4['Es'][i,1]: 6.2e}") + + raise CatenaryError("catenary solver failed. "+info4['oths']['message']) else: # if the solve was successful, info.update(info4['oths']) # copy info from last solve into existing info dictionary @@ -521,7 +628,7 @@ def dV_dZ_s(z0, H): # height off seabed to evaluate at (infinite if 0), horizo # check for errors ( WOULD SOME NOT ALREADY HAVE BEEN CAUGHT AND RAISED ALREADY?) if info['error']==True: - #breakpoint() + breakpoint() # >>>> what about errors for which we can first plot the line profile?? <<<< raise CatenaryError("Error in catenary computations: "+info['message']) @@ -537,7 +644,7 @@ def dV_dZ_s(z0, H): # height off seabed to evaluate at (infinite if 0), horizo # --- now that the iterative solve is over, check some things on the results, handle plotting, etc. --- # compute the Zextreme value - for a freely suspended line, if necessary, check to ensure the line doesn't droop and hit the seabed - if info['ProfileType']==1 and CB < 0 and VF-WL < 0.0: # only need to do this if the line is slack (has zero slope somewhere) + if info['ProfileType']==1 and hA > 0 and VF-WL < 0.0: # only need to do this if the line is slack (has zero slope somewhere) VFMinWL = VF - WL; LBot = L - VF/W; # unstretched length of line resting on seabed (Jonkman's PhD eqn 2-38), LMinVFOVrW @@ -559,10 +666,10 @@ def dV_dZ_s(z0, H): # height off seabed to evaluate at (infinite if 0), horizo info["Sextreme"] = 0.0 info["Zextreme"] = 0.0 info["Xextreme"] = 0.0 - - + + # handle special case of a U-shaped line that has seabed contact (using 2 new catenary solves) - if info['ProfileType']==1 and info["Zextreme"] < min(CB, 0): + if info['ProfileType']==1 and info["Zextreme"] < -hA: # we will solve this as two separate lines to form the U shape info['ProfileType'] = 'U' @@ -572,11 +679,10 @@ def dV_dZ_s(z0, H): # height off seabed to evaluate at (infinite if 0), horizo X2_0 = XF - X1_0 L1 = info['Sextreme'] L2 = L-L1 - Z1 = CB # negative of height from seabed to original 'anchor' end [m] + Z1 = -hA # negative of height from seabed to original 'anchor' end [m] Z2 = -Z1 + ZF # height from seabed to fairlead end # set up a 1D solve for the correct choice of the anchor point so that horizontal tensions balance - def eval_func_U(X, args): info = dict(error=False) @@ -585,8 +691,8 @@ def eval_func_U(X, args): X2 = XF-X1 # note: reducing tolerances for these sub-calls <<< how much is good? <<< - (fAH1, fAV1, fBH1, fBV1, info1) = catenary(X1, Z1, L1, EA, W, CB=0, Tol=0.5*Tol, MaxIter=MaxIter) - (fAH2, fAV2, fBH2, fBV2, info2) = catenary(X2, Z2, L2, EA, W, CB=0, Tol=0.5*Tol, MaxIter=MaxIter) + (fAH1, fAV1, fBH1, fBV1, info1) = catenary(X1, Z1, L1, EA, W, CB=Z1, alpha=0, Tol=0.5*Tol, MaxIter=MaxIter) + (fAH2, fAV2, fBH2, fBV2, info2) = catenary(X2, Z2, L2, EA, W, CB=0 , alpha=0, Tol=0.5*Tol, MaxIter=MaxIter) Himbalance = fBH2 - fBH1 @@ -614,18 +720,19 @@ def step_func_U(X, args, Y, info, Ytarget, err, tols, iter, maxIter): X, Y, infoU = dsolve2(eval_func_U, [X1_0], step_func=step_func_U, ytol=0.25*Tol, stepfac=1, maxIter=20, a_max=1.2, display=0) X1 = X[0] X2 = XF-X1 - nNodes1 = int(L1/L*nNodes + 0.5) # set number of nodes in the first line proportionally to its length, rounded. + nNodes1 = int(L1/L*(nNodes-1) + 0.5) + 1 # set number of nodes in the first line by trying to keep original segment length # call one more time to get final values - (fAH1, fAV1, fBH1, fBV1, info1) = catenary(X1, Z1, L1, EA, W, CB=0, Tol=0.5*Tol, MaxIter=MaxIter, plots=plots, nNodes=nNodes1) - (fAH2, fAV2, fBH2, fBV2, info2) = catenary(X2, Z2, L2, EA, W, CB=0, Tol=0.5*Tol, MaxIter=MaxIter, plots=plots, nNodes=nNodes-nNodes1) - + (fAH1, fAV1, fBH1, fBV1, info1) = catenary(X1, Z1, L1, EA, W, CB=Z1, alpha=0, Tol=0.5*Tol, MaxIter=MaxIter, plots=plots, nNodes=nNodes1) + (fAH2, fAV2, fBH2, fBV2, info2) = catenary(X2, Z2, L2, EA, W, CB=0, alpha=0, Tol=0.5*Tol, MaxIter=MaxIter, plots=plots, nNodes=nNodes-nNodes1+1) + if plots > 0 or (info1['error'] and info2['error']): - s = np.hstack([ info1["s" ] , info2["s" ]+L1 ]) - Xs = np.hstack([ info1["X" ] , info2["X" ]+X1 ]) - Zs = np.hstack([ info1["Z" ] , info2["Z" ]+Z1 ]) - Te = np.hstack([ info1["Te"] , info2["Te"] ]) + # concatenate nodal values (removing duplicate at the end nodes where they connect) + s = np.hstack([ info1["s" ] , info2["s" ][1:]+L1 ]) + Xs = np.hstack([ info1["X" ] , info2["X" ][1:]+X1 ]) + Zs = np.hstack([ info1["Z" ] , info2["Z" ][1:]+Z1 ]) + Te = np.hstack([ info1["Te"] , info2["Te"][1:] ]) # re-reverse line distributed data back to normal if applicable ''' @@ -636,8 +743,8 @@ def step_func_U(X, args, Y, info, Ytarget, err, tols, iter, maxIter): info['Te'] = info['Te'][::-1] ''' - if flipFlag: - raise Exception("flipFlag connot be True for the case of a U shaped line with seabed contact. Something must be wrong.") + #if flipFlag: + # raise Exception("flipFlag connot be True for the case of a U shaped line with seabed contact. Something must be wrong.") @@ -655,7 +762,7 @@ def step_func_U(X, args, Y, info, Ytarget, err, tols, iter, maxIter): info['stiffnessB'] = np.array([[ dH_dX , K2[0,1] *K1[0,0]*dxdH ], [K2[1,0] *K1[0,0]*dxdH, K2[1,1] -K2[1,0]*dxdH*K2[0,1]]]) - info['stiffnessAB']= np.array([[-K1[0,0] *K2[0,0]*dxdH, K1[0,1] *K2[0,0]*dxdH ], # this is the lower-left submatrix, A motions, B reaction forces (corrected/flipped sign of entry 0,1) + info['stiffnessBA']= np.array([[-K1[0,0] *K2[0,0]*dxdH, K1[0,1] *K2[0,0]*dxdH ], # this is the lower-left submatrix, A motions, B reaction forces (corrected/flipped sign of entry 0,1) [-K1[0,0] *dxdH*K2[1,0], -K1[0,1] *dxdH*K2[1,0] ]]) @@ -709,7 +816,7 @@ def step_func_U(X, args, Y, info, Ytarget, err, tols, iter, maxIter): info['LBot'] = info1['LBot'] + info2['LBot'] # not very useful outputs for this case: info["Sextreme"] = L1 - info1['LBot'] - info["Zextreme"] = CB + info["Zextreme"] = -hA info["Xextreme"] = X1 - info1['LBot'] #FxA = fAH1 @@ -761,7 +868,13 @@ def step_func_U(X, args, Y, info, Ytarget, err, tols, iter, maxIter): Xs[I] = ( np.log( VFMinWLs_HF + SQRT1VFMinWLs_HF2 ) - np.log( VFMinWL_HF + SQRT1VFMinWL_HF2 ) )*HF_W + s_EA* HF; Zs[I] = ( SQRT1VFMinWLs_HF2 - SQRT1VFMinWL_HF2 )*HF_W + s_EA*( VFMinWL + 0.5*Ws ); Te[I] = np.sqrt( HF*HF + VFMinWLs*VFMinWLs ); - + + if ProfileType==-1: # new last-ditch solution attempt for taut lines + Xs[I] = Xs_parabola[I] + Zs[I] = Zs_parabola[I] + Te[I] = T0 + + # A portion of the line must rest on the seabed elif ProfileType in [2,3]: @@ -788,6 +901,51 @@ def step_func_U(X, args, Y, info, Ytarget, err, tols, iter, maxIter): Xs[I] = LBot + HF_W*np.log( VFMinWLs_HF + SQRT1VFMinWLs_HF2 ) + HF*s_EA + 0.5*CB*W/EA *(-LBot*LBot + xB*xBlim); Zs[I] = ( -1.0 + SQRT1VFMinWLs_HF2)*HF_W + s_EA*(VFMinWL + 0.5*Ws ) + 0.5* VFMinWL*VFMinWL/WEA; Te[I] = np.sqrt( HF*HF + VFMinWLs*VFMinWLs ) + # No portion of the line rests on the seabed + + + # Line is paritally in contact with a sloped seabed + elif ProfileType==7: + + cos_alpha = np.cos(np.radians(alpha)) + sin_alpha = np.sin(np.radians(alpha)) + tan_alpha = sin_alpha/cos_alpha + + LBot = L - (VF - HF * tan_alpha)/W # Length of line on the seafloor + + VTD = VF - W*(L-LBot) #Vertical Force at the touchdownpoint (last point in contact with (sloped) seabed + + TTD = np.sqrt(VTD * VTD + HF * HF) #Tension at the Touchdown Point (HF is the same at fairlead as it is at touchdownpoint + + TA = TTD - W*(sin_alpha + CB)*LBot #Tension at the anchor + + X_TD = (LBot+(TA*LBot)/EA+(W*(sin_alpha + CB)*LBot*LBot)/(2*EA))*cos_alpha # X excursion from anchor to touchdown point + Z_TD = (LBot+(TA*LBot)/EA+(W*(sin_alpha + CB)*LBot*LBot)/(2*EA))*sin_alpha # Z excursion from anchor to the touchdown point + + if CB > 0: + xB = LBot - TTD/(W*(sin_alpha + CB)) # location of point at which line tension reaches zero (WWest Check this!!!!) + else: + xB = 0.0 + + xBlim = max(xB, 0.0) + + if s[I] <= xB and CB > 0: # (aka Lbot - s > HF/(CB*W) ) if this node rests on the seabed and the tension is zero + + Xs[I] = cos_alpha*s[I]; + Zs[I] = sin_alpha*s[I]; + Te[I] = 0.0; + + elif( s[I] <= LBot ): # // .TRUE. if this node rests on the seabed and the tension is nonzero + + Xs[I] = (s[I]+(TA*s[I])/EA+(W*(sin_alpha + CB)*s[I]*s[I])/(2*EA))*cos_alpha + Zs[I] = (s[I]+(TA*s[I])/EA+(W*(sin_alpha + CB)*s[I]*s[I])/(2*EA))*sin_alpha + Te[I] = TA + W*(sin_alpha + CB)*s[I]; + + else: # // LBot < s <= L ! This node must be above the seabed + + Xs[I] = X_TD + HF_W*(np.arcsinh((VTD+W*(s[I]-LBot))/HF)-np.arcsinh(VTD/HF))+(HF*(s[I]-LBot))/EA; + Zs[I] = Z_TD + HF_W*(np.sqrt(1+((VTD+W*(s[I]-LBot))/HF)*((VTD+W*(s[I]-LBot))/HF))-np.sqrt(1+(VTD/HF)*(VTD/HF)))+(1/EA)*(VTD*(s[I]-LBot)+(W*(s[I]-LBot)*(s[I]-LBot))/2); + Te[I] = np.sqrt( HF*HF + VFMinWLs*VFMinWLs ) if plots > 0: # re-reverse line distributed data back to normal if applicable @@ -815,15 +973,15 @@ def step_func_U(X, args, Y, info, Ytarget, err, tols, iter, maxIter): # get A and AB stiffness matrices for catenary profiles here based on fairlead (B) stiffness matrix if ProfileType == 1: info['stiffnessA'] = np.array(info['stiffnessB']) - info['stiffnessAB'] = -info['stiffnessB'] + info['stiffnessBA'] = -info['stiffnessB'] - elif ProfileType in [2,3]: + elif ProfileType in [2,3,7]: if CB == 0.0: info['stiffnessA'] = np.array([[info['stiffnessB'][0,0], 0], [0, dV_dZ_s(Tol, HF)]]) # vertical term is very approximate - info['stiffnessAB'] = np.array([[-info['stiffnessB'][0,0], 0], [0, 0]]) # note: A and AB stiffnesses for this case only valid if zero friction + info['stiffnessBA'] = np.array([[-info['stiffnessB'][0,0], 0], [0, 0]]) # note: A and AB stiffnesses for this case only valid if zero friction else: info['stiffnessA'] = np.ones([2,2]) * np.nan # if friction, flag to ensure users don't use this - info['stiffnessAB'] = np.ones([2,2]) * np.nan # if friction, flag to ensure users don't use this + info['stiffnessBA'] = np.ones([2,2]) * np.nan # if friction, flag to ensure users don't use this # un-swap line ends if they've been previously swapped, and apply global sign convention # (vertical force positive-up, horizontal force positive from A to B) @@ -842,8 +1000,10 @@ def step_func_U(X, args, Y, info, Ytarget, err, tols, iter, maxIter): info["stiffnessA"][1,0] = -info["stiffnessA"][1,0] info["stiffnessB"][0,1] = -info["stiffnessB"][0,1] info["stiffnessB"][1,0] = -info["stiffnessB"][1,0] - info["stiffnessAB"][0,1] = -info["stiffnessAB"][0,1] # for cross coupling matrix could also maybe transpose? but should be symmetrical so no need - info["stiffnessAB"][1,0] = -info["stiffnessAB"][1,0] + info["stiffnessBA"][0,1] = -info["stiffnessBA"][0,1] # for cross coupling matrix could also maybe transpose? but should be symmetrical so no need + info["stiffnessBA"][1,0] = -info["stiffnessBA"][1,0] + + info['Zextreme'] = info['Zextreme'] + ZF else: FxA = HA @@ -865,8 +1025,10 @@ def step_func_U(X, args, Y, info, Ytarget, err, tols, iter, maxIter): info["stiffnessA"][1,0] = -info["stiffnessA"][1,0] info["stiffnessB"][0,1] = -info["stiffnessB"][0,1] info["stiffnessB"][1,0] = -info["stiffnessB"][1,0] - info["stiffnessAB"][0,1] = -info["stiffnessAB"][0,1] - info["stiffnessAB"][1,0] = -info["stiffnessAB"][1,0] + info["stiffnessBA"][0,1] = -info["stiffnessBA"][0,1] + info["stiffnessBA"][1,0] = -info["stiffnessBA"][1,0] + + info['Zextreme'] = -info['Zextreme'] # TODO <<< should add more info <<< @@ -879,13 +1041,13 @@ def step_func_U(X, args, Y, info, Ytarget, err, tols, iter, maxIter): def eval_func_cat(X, args): '''returns target outputs and also secondary outputs for constraint checks etc.''' - info = dict(error=False) # a dict of extra outputs to be returned + info = dict(error=False, message='') # a dict of extra outputs to be returned ## Step 1. break out design variables and arguments into nice names HF = X[0] VF = X[1] - [XF, ZF, L, EA, W, CB, WL, WEA, L_EA, CB_EA] = args['cat'] + [XF, ZF, L, EA, W, CB, hA, hB, alpha, WL, WEA, L_EA, CB_EA] = args['cat'] ## Step 2. do the evaluation (this may change mutable things in args) @@ -906,17 +1068,28 @@ def eval_func_cat(X, args): SQRT1VF_HF2 = np.sqrt( 1.0 + VF_HF2 ) SQRT1VFMinWL_HF2 = np.sqrt( 1.0 + VFMinWL_HF2 ) - + # simple inelastic approximation of arc lenght of a parabola with exactly zero laid length + s = np.sqrt(4*XF**2 + 16*ZF**2) # some constanst + lp = s/4 + XF**2/4/ZF*np.log((4*ZF + s)/2/ZF) # length of parabola (slight underestimate vs lenght of catenary) + d = np.linalg.norm([XF, ZF]) # distance from points + + alpha_rad = np.radians(alpha) # convert to radians for convenience + sin_alpha = np.sin(alpha_rad) + cos_alpha = np.cos(alpha_rad) + + # this is a crude check that nothing should be laying along the seabed:ZF/HF >> 0 and L-d << (lp-d) # determine line profile type - if(( CB < 0.0) or ( W < 0.0) or ( VFMinWL > 0.0 ) ): # no portion of the line rests on the seabed + if (hA > 0.0 or W < 0.0 or VFMinWL > sin_alpha/cos_alpha + or (ZF/XF > 0.1 and L-d < 0.001*(lp-d)) ): # no portion of the line rests on the seabed ProfileType = 1 - elif( -CB*VFMinWL < HF ): # a portion of the line rests on the seabed and the anchor tension is nonzero + elif not alpha==0: # a portion of line rests along a *sloped* seabed + ProfileType = 7 + elif -CB*VFMinWL < HF: # a portion of the line rests on the seabed and the anchor tension is nonzero ProfileType = 2 else: # must be 0.0 < HF <= -CB*VFMinWL, meaning a portion of the line must rest on the seabed and the anchor tension is zero ProfileType = 3 - - + # Compute the error functions (to be zeroed) and the Jacobian matrix # (these depend on the anticipated configuration of the mooring line): @@ -952,7 +1125,8 @@ def eval_func_cat(X, args): dZFdVF = ( VF_HF /SQRT1VF_HF2 - VFMinWL_HF /SQRT1VFMinWL_HF2 )/ W + L_EA #dZFdVF = ( np.sign(VF)*VF_HF /SQRT1VF_HF2 - VFMinWL_HF /SQRT1VFMinWL_HF2 )/ W + L_EA - + + elif ProfileType==27: if (VF_HF + SQRT1VF_HF2 <= 0): @@ -1004,7 +1178,82 @@ def eval_func_cat(X, args): dZFdHF = ( SQRT1VF_HF2 - 1.0 - VF_HF2 /SQRT1VF_HF2 )/ W dZFdVF = ( VF_HF /SQRT1VF_HF2 )/ W + VF_WEA + + + ## Line Profile Type 7: Line partially on a sloped seabed + elif ProfileType==7: + + if (VF_HF + SQRT1VF_HF2 <= 0): + info['error'] = True + info['message'] = "ProfileType 7: VF_HF + SQRT1VF_HF2 <= 0" + + elif HF*1000000 < VF: + info['error'] = True + info['message'] = "ProfileType 7: HF << VF, line is slack, not supported yet" + breakpoint() + + else: + + LBot = L - (VF - HF * sin_alpha/cos_alpha)/W # Lb on the seafloor + + # Practical limits on LBot, because the estimates can be way off + if LBot > XF/cos_alpha: # if LBot is too large (if true, this would be profileType 4) + LBot = XF/cos_alpha # limit it to stop under end B + elif LBot < 0: + LBot = max(LBot, 0) # Ensure LBot is not less than zero + + VTD = VF - W*(L-LBot) # Vertical Force at the touchdownpoint + + TTD = np.sqrt(VTD * VTD + HF * HF) # Tension at the Touchdown Point + + TA = TTD - W*(sin_alpha+CB)*LBot # Tension at the anchor + + if CB > 0: + xB = LBot - TTD/(W*(sin_alpha+CB)) # location of point at which line tension reaches zero + else: + xB = 0.0 + xBlim = max(xB, 0.0) + + TA = max(0,TA) #Anchor Tension Cannot be Negative + + #X and Z Excursions along the sloped seabed + X_TD = (LBot+(TA*LBot)/EA+(W*(sin_alpha+CB)*LBot*LBot)/(2*EA))*cos_alpha + Z_TD = (LBot+(TA*LBot)/EA+(W*(sin_alpha+CB)*LBot*LBot)/(2*EA))*sin_alpha + + # WWest Comment: Could clean this up for readibility (Will do at somepoint) + EXF = HF_W*(np.arcsinh((VTD+W*(L-LBot))/HF)-np.arcsinh(VTD/HF))+(HF*(L-LBot))/EA + X_TD - XF # error in horizontal distance + + EZF = HF_W*(np.sqrt(1+((VTD+W*(L-LBot))/HF)*((VTD+W*(L-LBot))/HF))-np.sqrt(1+(VTD/HF)*(VTD/HF)))+(1/EA)*(VTD*(L-LBot)+(W*(L-LBot)*(L-LBot))/2) + Z_TD - ZF # error in vertical distance + + #Line stiffness values + #Re-assign some helpful values + VFMinWL = VF - W*(L-LBot) # = VTD Point, the vertical anchor load (positive-up, but VF is positive-down) + L_EA = (L-LBot)/EA + VFMinWL_HF = VFMinWL/HF + VFMinWL_HF2 = VFMinWL_HF*VFMinWL_HF + SQRT1VFMinWL_HF2 = np.sqrt( 1.0 + VFMinWL_HF2 ) + + #Line stiffness values + dXFdHF = (( np.log( VF_HF + SQRT1VF_HF2 ) - np.log( VFMinWL_HF + SQRT1VFMinWL_HF2 ) )/ W - + ( ( VF_HF + VF_HF2 /SQRT1VF_HF2 )/( VF_HF + SQRT1VF_HF2 ) + - ( VFMinWL_HF + VFMinWL_HF2/SQRT1VFMinWL_HF2 )/( VFMinWL_HF + SQRT1VFMinWL_HF2 ) )/ W + L_EA) + + dXFdVF = (( ( 1.0 + VF_HF /SQRT1VF_HF2 )/( VF_HF + SQRT1VF_HF2 ) + - ( 1.0 + VFMinWL_HF /SQRT1VFMinWL_HF2 )/( VFMinWL_HF + SQRT1VFMinWL_HF2 ) )/ W) + + dZFdHF = ( SQRT1VF_HF2 - SQRT1VFMinWL_HF2 )/ W - ( VF_HF2 /SQRT1VF_HF2 - VFMinWL_HF2/SQRT1VFMinWL_HF2 )/ W + + dZFdVF = ( VF_HF /SQRT1VF_HF2 - VFMinWL_HF /SQRT1VFMinWL_HF2 )/ W + L_EA + + + #print(f"\n{dXFdHF:10.3e} {dXFdVF:10.3e}\n{dZFdHF:10.3e} {dZFdVF:10.3e}") + + # if there was an error, send the stop signal + if info['error']==True: + #breakpoint() + return np.zeros(2), info, True + # Now compute the tensions at the anchor if ProfileType==1: # No portion of the line rests on the seabed @@ -1019,12 +1268,10 @@ def eval_func_cat(X, args): HA = 0.0 VA = 0.0 + elif ProfileType==7: # A portion of the line must rest on the seabed and the anchor tension is zero or non-zero + HA = TA*cos_alpha + VA = TA*sin_alpha - # if there was an error, send the stop signal - if info['error']==True: - #breakpoint() - return np.zeros(2), info, True - ## Step 3. group the outputs into objective function value and others Y = np.array([EXF, EZF]) # objective function @@ -1054,30 +1301,21 @@ def step_func_cat(X, args, Y, info, Ytarget, err, tols, iter, maxIter): info - the info dict created by the main catenary function ''' - [XF, ZF, L, EA, W, CB, WL, WEA, L_EA, CB_EA] = args['cat'] - - #if abs( err[1] + ZF ) < 0.0001: - # breakpoint() + [XF, ZF, L, EA, W, CB, hA, hB, alpha, WL, WEA, L_EA, CB_EA] = args['cat'] - - - [alpha_min, alpha0, alphaR] = args['step'] # get minimum alpha, initial alpha, and alpha reduction rate from passed arguments - - #J = info['jacobian'] - #dX = -np.matmul(np.linalg.inv(J), err) dX = -np.matmul(info['stiffnessB'], err) - # ! Reduce dHF by factor (between 1 at I = 1 and 0 at I = MaxIter) that reduces linearly with iteration count # to ensure that we converge on a solution even in the case were we obtain a nonconvergent cycle about the # correct solution (this happens, for example, if we jump to quickly between a taut and slack catenary) - alpha = 1.0 #M<<<<<<<< np.max([alpha_min, alpha0*(1.0 - alphaR*iter/maxIter)]) - - #exponential approach alpha = alpha0 * np.exp( iter/maxIter * np.log(alpha_min/alpha0 ) ) - - dX[0] = dX[0]*alpha #dHF*( 1.0 - Tol*I ) - dX[1] = dX[1]*alpha #dVF*( 1.0 - Tol*I ) + # options for adaptive step size: + if 'step' in args: + [alpha_min, alpha0, alphaR] = args['step'] # get minimum alpha, initial alpha, and alpha reduction rate from passed arguments + alpha = np.max([alpha_min, alpha0*(1.0 - alphaR*iter/maxIter)]) + alpha = alpha0 * np.exp( iter/maxIter * np.log(alpha_min/alpha0 ) ) + dX[0] = dX[0]*alpha #dHF*( 1.0 - Tol*I ) + dX[1] = dX[1]*alpha #dVF*( 1.0 - Tol*I ) # To avoid an ill-conditioned situation, make sure HF does not go less than or equal to zero by having a lower limit of Tol*HF # [NOTE: the value of dHF = ( Tol - 1.0 )*HF comes from: HF = HF + dHF = Tol*HF when dHF = ( Tol - 1.0 )*HF] @@ -1146,12 +1384,23 @@ def step_func_cat(X, args, Y, info, Ytarget, err, tols, iter, maxIter): #(fAH1, fAV1, fBH1, fBV1, info1) = catenary(0.00040612281105723014, 391.558570722038, 400.0, 3300142385.3140063, 6555.130220040344, CB=-287.441429277962, HF0=2127009.4122708915, VF0=10925834.69512347, Tol=4.000000000000001e-06, MaxIter=100, plots=1) #(fAH1, fAV1, fBH1, fBV1, info1) = catenary(0.0004959907076624859, 69.87150531147275, 110.89397565668423, 80297543.26800226, 146.26820268238743, CB=-509.12849468852727, HF0=1322712.3676957292, VF0=1045583.1849462093, Tol=4.000000000000001e-06, MaxIter=50, plots=1) - (fAH1, fAV1, fBH1, fBV1, info1) = catenary(2.203138228651369e-05, 378.2807133834522, 383.34011790636976, 260525391.7172965, 474.5672065262173, CB=-7.719286616547777, - HF0=0.012438428537800724, VF0=179721.2163869108, Tol=4.000000000000001e-06, MaxIter=100, plots=1) + #(fAH1, fAV1, fBH1, fBV1, info1) = catenary(2.203138228651369e-05, 378.2807133834522, 383.34011790636976, 260525391.7172965, 474.5672065262173, CB=-7.719286616547777, + # HF0=0.012438428537800724, VF0=179721.2163869108, Tol=4.000000000000001e-06, MaxIter=100, plots=1) + """ + (fAH1, fAV1, fBH1, fBV1, info1) = catenary(406.77, -21.22, 410, 854000000.00, 1820.205, CB=0, alpha=-26.7, + HF0=1300157.2, VF0=931582.2, Tol=1e-06, MaxIter=100, plots=1) + plt.plot(info1['X'], info1['Z'] ) #plt.plot(infoU['X'], infoU['Z'] ) plt.axis('equal') + + XF = 406.77 + ZF = -21.22 + alpha = -26.7 + plt.plot([0, XF], [0, XF*np.tan(np.radians(alpha))], 'g--') + plt.plot(XF, ZF, 'b*') + ''' plt.figure() plt.plot(info1['s'], info1['Te'] ) @@ -1165,5 +1414,120 @@ def step_func_cat(X, args, Y, info, Ytarget, err, tols, iter, maxIter): printMat(info1['stiffnessA']) print('') printMat(info1['stiffnessB']) + """ + ''' + # sloped case + (fAH1, fAV1, fBH1, fBV1, info1) = catenary(147.0, -25.8, 160.0, 854e7, 6523.0, + CB=0.0, alpha=-27.73, HF0=725968.57, VF0=667765.24, + Tol=0.0001, MaxIter=100, plots=1) + ''' + # simple U case (fAH1, fAV1, fBH1, fBV1, info1) = catenary(200, 30, 260.0, 8e9, 6500, CB=-50, nNodes=21, plots=1) + + #tricky cable + ''' + (fAH1, fAV1, fBH1, fBV1, info1) = catenary(266.66666666666674, 195.33333333333337, + 400.0, 700000.0, -15.807095300087719, CB=0.0, alpha=-0.0, + #HF0=3815675.5094567537, VF0=-1038671.8978986739, + Tol=0.0001, MaxIter=100, plots=1) + ''' + #fAH1, fAV1, fBH1, fBV1, info1) = catenary(231.8516245613182, 248.3746210557402, 339.7721054751881, 70000000000.0, 34.91060469991227, + #(fAH1, fAV1, fBH1, fBV1, info1) = catenary(231.8, 248.3, 339.7, 70000000000.0, 34.91060469991227, + # CB=0.0, HF0=2663517010.1, VF0=2853338140.1, Tol=0.0001, MaxIter=100, plots=2) + + #(fAH1, fAV1, fBH1, fBV1, info1) = catenary(246.22420940032947, 263.55766330843164, 360.63566262396927, 700000000.0, 3.087350845602259, + # CB=0.0, HF0=9216801.959569097, VF0=9948940.060081193, Tol=2e-05, MaxIter=100, plots=1) + #catenary(400.0000111513176, 2e-05, 400.0, 70000000000.0, 34.91060469991227, + # CB=0.0, HF0=4224.074763303775, VF0=0.0, Tol=2e-05, MaxIter=100, plots=1) + # tricky near-taut case with starting point + #catenary(119.3237383002058, 52.49668849178113, 130.36140355177318, 700000000.0, 34.91060469991227, CB=0.0, HF0=9298749.157482728, VF0=4096375.3052922436, Tol=2e-05, MaxIter=100, plots=1) + #(fAH1, fAV1, fBH1, fBV1, info1) = catenary(829.0695733253751, 2.014041774600628e-05, 829.0695771203765, 700000000.0, 34.91060469991227, CB=0.0, HF0=0.0, VF0=0.0, Tol=2e-05, MaxIter=100, plots=1) + #(fAH1, fAV1, fBH1, fBV1, info1) = catenary(829.0695733253751, 2.014041774600628e-05, + # 829.0695771203765, 700000000.0, 34.91060469991227, Tol=2e-05, MaxIter=100, plots=1) + + + # Tricky case for sloped seabed (prone to overestimating LBot unless trapped corrected in the solve) + #(fAH1, fAV1, fBH1, fBV1, info1) = catenary(121.5, 17.5, 138.5, 1232572089, 2456.8, CB=0.0, alpha=-2.6, HF0=428113, VF0=396408, Tol=0.0005, nNodes=41, plots=1) + + # case from Emma + #(fAH1, fAV1, fBH1, fBV1, info1) = catenary(0.006002402242302196, 52.088735921752004, 105, 440229677.8451713, 362.2187847407355, CB=0, HF0=0.0, VF0=30883.56235473299, Tol=5.000000000000001e-05, MaxIter=100, plots=1) + + # cable case + #(fAH1, fAV1, fBH1, fBV1, info1) = catenary(39., 3.675, 37., 528., -5152., + # CB=0.0, alpha=-0.0, HF0=0.0, VF0=0.0, Tol=2e-05, MaxIter=100, plots=1, depth=200) + + + #(fAH1, fAV1, fBH1, fBV1, info1) = catenary(274.9, 15.6, 328.5, 528887323., -121., + # CB=-48., alpha=0, Tol=2e-05, MaxIter=100, plots=1, depth=200) + + #(fAH1, fAV1, fBH1, fBV1, info1) = catenary(267.60271224572784, 11.629753388110558, 383.52733838297667, 528887323.6999998, -121.77472470613236, + # CB=-81.79782970374734, alpha=0, HF0=13369.512495199759, VF0=24221.869928543758, Tol=2e-05, + # MaxIter=100, depth=180, plots=1, nNodes=100) + + + #(fAH1, fAV1, fBH1, fBV1, info1) = catenary(0.16525209064463553, 1000.1008645356192, 976.86359774357, 861890783.955385, -5.663702119364548, 0.0, -0.0, 0.0, 17325264.3383085, 5.000000000000001e-05, 101, 1, 1000) + #(fAH1, fAV1, fBH1, fBV1, info1) = catenary(0.0, 997.8909672113768, 1006.0699998926483, 361256000.00000006, 2.4952615384615333, 0, 0.0, 0, 0, 0.0001, 101, 100, 1, 1000) + #(fAH1, fAV1, fBH1, fBV1, info1) = catenary(148.60942473375343, 1315.624259105325, 1279.7911844766932, 17497567581.802254, -81.8284437736437, CB=0.0, alpha=-0.0, HF0=795840.9590915733, VF0=6399974.444658389, Tol=0.0005, MaxIter=100, depth=1300.0, plots=1) + (fAH1, fAV1, fBH1, fBV1, info1) = catenary(2887.113193120885, -838.1577017360617, 2979.7592390255295, 41074520109.87981, 1104.878355564403, CB=0.0, alpha=81.41243787249468, HF0=0.0, VF0=3564574.0622291695, Tol=0.0005, MaxIter=100, depth=3000.0, plots=1) + + #(fAH1, fAV1, fBH1, fBV1, info1) = catenary(274.9, 15.6, 328.5, 528887323., -121., + # CB=-48., alpha=0, HF0=17939., VF0=20596., Tol=2e-05, MaxIter=100, plots=1) + + + # First attempt's iterations are as follows: + # Iteration 0: HF= 5.0000e-05, VF= 2.8450e+04, EX= 0.00e+00, EZ= 0.00e+00 + # Second attempt's iterations are as follows: + # Iteration 0: HF= 5.0000e-05, VF= 2.8450e+04, EX= 0.00e+00, EZ= 0.00e+00 + # Last attempt's iterations are as follows: + # Iteration 0: HF= 5.0000e-05, VF= 2.8450e+04, EX= 0.00e+00, EZ= 0.00e+00 + + """ + Tol =2e-05 + + for dl in [-1, -0.1, -0.01, 0, 0.01, 0.1, 1]: # for exploring line length sensitivity + + ''' + XF= 119.3237383002058 + ZF= 52.49668849178113 + L = 130.36140355177318 + dl + EA= 700000000.0 + W = 34.91060469991227 + ''' + XF=246.22420940032947 + ZF=263.55766330843164 + L =360.63566262396927 + dl + EA=700000000.0 + W =3.08735 + ''' + XF=231.8516245613182 + ZF=248.3746210557402 + L = 339.7721054751881 #- Tol + EA=7e8 + W =34.91 + + XF=40.0 + ZF=30.0 + L = 50 - 0.001 + EA=7e9 + W=3 + ''' + d = np.sqrt(XF*XF+ZF*ZF) + sin_angle = XF/d + F_lateral = sin_angle*np.abs(W)*L + F_EA = (d/L-1)*EA + + #L = d + + #print(f" F_lateral/F_EA is {F_lateral/F_EA:6.2e} !!!! and strain is {d/L-1 : 7.2e}.") + #print(f" F_lateral / ((strain+tol)EA) is {F_lateral/(((d+Tol)/L-1)*EA):6.2e} !!!!!!") + #print(f" F_lateral / ((strain-tol)EA) is {F_lateral/(((d-Tol)/L-1)*EA):6.2e} !!!!!!") + + (fAH1, fAV1, fBH1, fBV1, info1) = catenary(XF, ZF, L, EA, W, CB=0, Tol=Tol, MaxIter=40, plots=2) + print("{:10.1f} {:10.1f} {:10.1f} {:10.1f}".format(fAH1, fAV1, fBH1, fBV1)) + #print("{:10.1f} {:10.1f} {:10.1f} {:10.1f}".format(*info1['stiffnessB'].ravel().tolist() )) + + """ + plt.plot(info1['X'], info1['Z'] ) + #plt.axis('equal') + #plt.close('all') plt.show() diff --git a/moorpy/MoorProps.py b/moorpy/MoorProps.py index 0becfb0..97b86e0 100644 --- a/moorpy/MoorProps.py +++ b/moorpy/MoorProps.py @@ -8,6 +8,7 @@ import numpy as np import moorpy as mp +import math def getLineProps(dmm, type="chain", stud="studless", source="Orcaflex-altered", name=""): @@ -195,12 +196,318 @@ def getLineProps(dmm, type="chain", stud="studless", source="Orcaflex-altered", return mp.LineType(typestring, d_vol, massden, EA, MBL=MBL, cost=cost, notes=notes, input_type=type, input_d=dmm) +#----NOTES---- +#def getAnchorProps(value, anchor="drag-embedment", soil_type='medium clay',uhc_mode = True, method = 'static', display=0): + # need to make 'value' input all the same as either Mass or UHC + #'value' can be both capacity or mass, 'uhc_mode' defines what it is. However, this method does not take into account fx, fz loading directions or safety factor +# applied safety factors make input UHC smaller resulting in smaller mass > uhc_mode = True, calulating for capacity | INPUT: mass | does not account for this + #added 'capacity_sf' variable: outputs capacity for applied safety factor, while showing original input capacities (fx,fz) + +# VLA calculation for thickness may be wrong: may not need to divide by 4. When we dont it matched STEVMANTA table (in excel sheet) + +# Do i want to scale fx,fz by 20% when considering dynamic loads like in Moorpy 'capacity_x = 1,2*fx ; capacity_z = 1.2*fz + +# Need to validate suction curves and compare to Ahmed's curves. His is pure horizontal while these ones are for both horizontal and vertical + +#-------------- + +def getAnchorMass( uhc_mode = True, fx=0, fz=0, mass_int=0, anchor="drag-embedment", soil_type='medium clay', method = 'static', display=0): + '''Calculates anchor required capacity, mass, and cost based on specified loadings and anchor type + + Parameters + ---------- + uhc_mode: boolean + True: INPUT Mass to find UHC + False: INPUT fx,fz to find Mass and UHC + fx : float + horizontal maximum anchor load [N] + fz : float + vertical maximum anchor load [N] + massint : float + anchor mass [kg] + anchor : string + anchor type name + soil_type: string + soil type name + method: string + anchor analysis method for applied safety factors + + Returns + ------- + UHC: float + required anchor ultimate holding capacity [N] + mass: float + anchor mass [kg] + info: float + dictionary + ''' + + #--MAGIC NUMBERS -- assign varibles to values + density_st = 7.85*1000 #[kg/m^3] + gravity = 9.81 #[m/sec^2]? or [N/kg], if so may need to add conversion *1000 so [kN/mT] + info = {} + + + # Define anchor type + if anchor == "drag-embedment": + + # Define soil type (coefficients are from Vryhof for Stevpris MK6) + if soil_type == 'soft clay': + a, b = 509.96, 0.93 + elif soil_type == 'medium clay': + a, b = 701.49, 0.93 + elif soil_type == 'hard clay' or soil_type == 'sand': + a, b = 904.21, 0.92 + else: + raise ValueError('Error: Invalid soil type') + + # Define mode + # Calculate capacity | INPUT: Mass in kg + if uhc_mode: + uhc = a * (mass_int/1000)**b*1000 #[N] (from Vryhof eqn) + # y = m*(x)^b + + print(f"Mass input: {mass_int} -- UHC: {uhc}") + info["Mass"] = mass_int #[kg] + info["UHC"] = uhc #[N] + + + # Calculate mass | INPUT: UHC + else: + # Define method for safety factor + if method == 'static': + uhc = 1.8*fx #[N] + elif method == 'dynamic': + uhc = 1.5*fx #[N] + else: + raise Exception("Error - invalid method") + + mass = (uhc /1000 /a)**(1/b)*1000 # [kg] (from Vryhof eqn) + + print(f"UHC input: {fx} -- Mass: {mass}") + info["UHC"] = uhc #[N] + info["Mass"] = mass #[kg] + + elif anchor == "VLA": + + # Vryhof coefficients + if soil_type == 'soft clay': + c, d = 0.003, -0.2 + elif soil_type == 'medium clay': + c, d = 0.0017, -0.32 + else: + raise ValueError('Error: Invalid soil type') + + if uhc_mode: # Calculate capacity | INPUT: Mass + #t2_m_ratio = 20933.3 #[m^2] + + #Ericka note - I don't know where this ratio is coming from. Matt may have estimated from Vryhof manual + t2_m_ratio = 1308.33 + + #Ericka note -not sure about this equation - should it be to the 1/2 power? + thickness = (mass_int/1000 / t2_m_ratio)**(1/3) #[m] #DONE: magic number - assign to variable (think its from t^2/A = 6000; thickness ratio) + area = mass_int/ (thickness * density_st) #[m^2] Area (1-30) + uhc = (area - d) / c * 1000 #[N] Vryhof equation + + + print(f"Mass input: {mass_int} -- UHC: {uhc}, Area: {area}") + info["Mass"] = mass_int #[kg] + info["UHC"] = uhc #[N] + info["Area"] = area #[m^2] + + else: # Calculate mass | INPUT: UHC + capacity_x = 2.0 * fx # N + capacity_z = 2.0 * fz # N + uhc = np.linalg.norm([capacity_x,capacity_z]) + + #Vryhof equation + area = c * uhc/1000 + d #[m^2] Vryhof equation + # y = m*x + b + + #Ericka note - I don't know where this ratio is coming from. Matt may have estimated from Vryhof manual + t2_a_ratio = 0.006 #[m^2] + if area < 0: + thickness = 0 + raise ValueError ("Error: Negative area)") + else: + #thickness = math.sqrt(t2_a_ratio * area)/4 #[m] #### why divide by 4 - looking at this in exel sheet + thickness = math.sqrt(t2_a_ratio * area) + mass = area * thickness * density_st #[kg] + + + print(f"UHC input: fx:{fx} fz:{fz} -- Mass: {mass}, Area: {area}") + info["UHC"] = uhc #[N] + info["Mass"] = mass #[kg] + info["Area"] = area #[m^2] + + + ###### VERIFICATION REQUIRED, THESE CURVES CONSIDER INCLINED LOADS -> While Ahmed's is pure horizontal + elif anchor == "suction": + + # c and d numbers are from ABS FOWT Anchor Report + # m and b numbers are from Brian's excel sheet curve fit to Mass vs UHC graph + if soil_type == 'soft clay': + m = 88.152 + b = 1.1058 + c_L, d_L = 1.1161, 0.3442 + c_D, d_D = 0.3095, 0.2798 + c_T, d_T = 2.058, 0.2803 + + elif soil_type == 'medium clay': + m = 384.15 + b = 0.8995 + c_L, d_L = 0.5166, 0.3995 + c_D, d_D = 0.126, 0.3561 + c_T, d_T = 0.8398, 0.3561 + + else: + raise ValueError("Invalid Soil Type") + + + if uhc_mode: # Calculate capacity | INPUT: Mass + + ### ABS (Matts curves) slightly off (from Brian's excel sheet curve fit for Mass vs UHC) + uhc = m * (mass_int/1000) ** b * 1000 # [N] + + + print(f"Mass INPUT: {mass_int} -- UHC: {uhc}") + info["Mass"] = mass_int #[kg] + info["UHC"] = uhc #[N] + + else: # Calculate mass | INPUT: UHC + + capacity_x = 1.6*fx #[N] + #capacity_x = fx + capacity_z = 2.0*fz #[N] + uhc = np.linalg.norm([capacity_x, capacity_z]) #[kN] + + # Equations from ABS Anchors Report + L = c_L * (uhc/1000) **d_L #[m] + D = c_D * (uhc/1000) **d_D #[m] + T = (c_T * (uhc/1000) **d_T) /1000 #[m] + + # y = m*(x)^b + + # volume of one end + open cylinder wall + volume = (math.pi/4 * D ** 2 + math.pi * D * L) * T #[m^3] + mass = volume * density_st #[kg] + + + print(f"UHC input: fx:{fx} fz:{fz} -- Mass: {mass}") + info["UHC"] = uhc #[N] + info["Mass"] = mass #[kg] + #info["Length"] = L + + + + + #-----IN PROGRESS ----------------------------------- + + elif anchor == "SEPLA": + # Ericka note - we think these coefficients come from Ahmed's intermediate model + if soil_type == 'soft clay': + c_B, d_B = 0.0225, 0.5588 + c_L, d_L = 0.0450, 0.5588 + c_T, d_T = 0.0006, 0.5588 + + else: + raise ValueError("Invalid Soil Type") + + if uhc_mode: # Calculate UHC | INPUT: Mass ### A little bit off from uhc = FALSE + + # from excel + m = 1557.4 + b = 0.5956 + uhc = m *(mass_int/1000) ** b *10000 # [N] + + + print(f'Work in progress -- Mass input: {mass_int} -- UHC {uhc}') + info["Mass"] = mass_int + info["UHC"] = uhc + + else: # Calculate Mass | INPUT: UHC + capacity_x = 2.0*fx + uhc = capacity_x + + B = c_B * (uhc/1000) ** d_B #[m] + L = c_L * (uhc/1000) ** d_L #[m] + T = c_T * (uhc/1000) ** d_T #[m] + # y = m*(x)^b + + area = B*L #[m^2] + volume = area * T #[m^3] + mass = volume * density_st #[mT] + + + print(f"UHC input: fx:{fx} fz:{fz} -- Mass {mass}") + info["UHC"] = uhc #[N] + info["Mass"] = mass #[kg] + info["Area"] = area #[m^2] + + elif anchor == "micropile": + raise ValueError ("Not supported yet") + + + else: + return Exception("Error - invalid anchor type") + + + + if uhc_mode: + mass = mass_int + + return uhc, mass, info + #return info + + +def getAnchorCost(fx, fz, type="drag-embedment",soil_type='medium clay', method = 'static'): + ''' applies factors to material cost ''' + + + uhc, mass, info = getAnchorMass( uhc_mode = False, fx=fx, fz=fz, anchor= type, soil_type=soil_type, method = method) + euros2dollars = 1.18 # the number of dollars there currently are in a euro (3-31-21) + + if type == "drag-embedment": + + + anchorMatCost = 6.7*mass # $ per kg mass + anchorInstCost = 163548*euros2dollars # installation cost + anchorDecomCost = 228967*euros2dollars # decommissioning cost + + elif type == "suction": + + anchorMatCost = 10.25 *mass # $ per kg mass + anchorInstCost = 179331*euros2dollars # installation cost + anchorDecomCost = 125532*euros2dollars # decommissioning cost + + + elif type == "micropile": + + raise ValueError('Micropile material costs are not yet supported') + #anchorMatCost = 0.48 * mass # $ per kg mass + anchorInstCost = 0 # installation cost + anchorDecomCost = 0 # decommissioning cost + + elif type == "plate": # cross between suction and plate + + raise ValueError('Plate material costs are not yet supported') + #anchorMatCost = 0.45 * mass# $ per kg mass + anchorInstCost = 0 # installation cost + anchorDecomCost = 0 # decommissioning cost + + else: + raise ValueError(f'getAnchorProps received an unsupported anchor type ({type})') + + # mooring line sizing: Tension limit for QS: 50% MBS. Or FOS = 2 + + + return anchorMatCost, anchorInstCost, anchorDecomCost, info # [USD] def getAnchorProps(fx, fz, type="drag-embedment", display=0): - ''' Calculates anchor required capacity and cost based on specified loadings and anchor type''' + ''' ****OLD VERSION**** Calculates anchor required capacity and cost based on specified loadings and anchor type''' # for now this is based on API RP-2SK guidance for static analysis of permanent mooring systems # fx and fz are horizontal and vertical load components assumed to come from a dynamic (or equivalent) analysis. @@ -210,8 +517,8 @@ def getAnchorProps(fx, fz, type="drag-embedment", display=0): # coefficients in front of fx and fz in each anchorType are the SF for that anchor for quasi-static (pages 30-31 of RP-2SK) # scale QS loads by 20% to approximate dynamic loads - fx = 1.2*fx - fz = 1.2*fz + # fx = 1.2*fx + # fz = 1.2*fz # note: capacity is measured here in kg force diff --git a/moorpy/MoorProps_default.yaml b/moorpy/MoorProps_default.yaml index a150c59..4215107 100644 --- a/moorpy/MoorProps_default.yaml +++ b/moorpy/MoorProps_default.yaml @@ -3,31 +3,24 @@ # lineProps -> [line material name] -> [line material coefficients] # All possible lineProps coefficients are as follow: -# mass_0 : # linear mass density offset [kg/m] -# mass_d : # linear mass density per diameter [kg/m^2] # mass_d2 : # linear mass density per diameter^2 [kg/m^3] -# mass_d3 : # linear mass density per diameter^3 [kg/m^4] -# -# EA_0 : # stiffness offset [N] -# EA_d : # stiffness per diameter [N/m] -# EA_d2 : # stiffness per diameter^2 [N/m^2] -# EA_d3 : # stiffness per diameter^3 [N/m^3] -# EA_MBL : # stiffness per MBL [N/N] - -NEW: -# Krs or EAs_MBL : # quasi-static stiffness per MBL [N/N] -# Krd_alpha or EAd_MBL : # dynamic stiffness per MBL [N/N] -# Krd_beta or EAd_MBL_PML : # dynamic stiffness per MBL per % mean load [N/N] -# (% mean load will be input later in the modeling code) - - # # MBL_0 : # minimum breaking load offset [N] # MBL_d : # minimum breaking load per diameter [N/m] # MBL_d2 : # minimum breaking load per diameter^2 [N/m^2] # MBL_d3 : # minimum breaking load per diameter^3 [N/m^3] # +# EA_0 : # stiffness offset [N] +# EA_d : # stiffness per diameter [N/m] +# EA_d2 : # stiffness per diameter^2 [N/m^2] +# EA_d3 : # stiffness per diameter^3 [N/m^3] +# EA_MBL : # (quasi-static) stiffness per MBL [N/N] (aka Kr, Krs) +# EAd_MBL : # dynamic stiffness per MBL [N/N] (aka Krd or Krd_alpha) +# EAd_MBL_Lm: # dynamic stiffness per MBL per fraction of mean load (not %) [N/N] (aka or Krd_beta) +# # dvol_dnom : # volume-equivalent diameter per nominal diameter [-] +# density : # density of the line material [kg/m^3] (e.g., chain density = 7850 kg/m^3) +# NOTE: Only one of the above three variables can be used as input! # # cost_0 : # cost offset [$/m] # cost_d : # cost per diameter [$/m^2] @@ -36,50 +29,96 @@ NEW: # cost_mass : # cost per mass [$/kg] # cost_EA : # cost per stiffness [$/m/N] # cost_MBL : # cost per MBL [$/m/N] +# +# Cd : # drag coefficient based on DNV-OS-E301 adjusted for use with volumetric diameter +# Cd_ax : # axial drag coefficient based on DNV-OS-E301 adjusted for use with volumetric diameter +# Ca : # added mass coefficient based on Bureau Veritas 493-NR_2021-07 +# Ca_ax : # axial added mass coefficient based on Bureau Veritas 493-NR_2021-07 + +# Chain Notes +# - The MBLs between studless and studlink chains are the same, for every grade +# - The masses between different grades of chain are the same (for now...different grades might correspond to different material densities) +# - If the user needs a different grade of chain, they will have to add another section and include those properties here. This default file only considers R4 chain +# - This default yaml uses R4 studless chain as the default style of chain +# - The chain MBL uses a cubic function, so be aware if you are researching theoretical chains with diameters greater than about 360mm, as the MBL will then decrease +# - Chain EA values not provided in manufacturer catalogs, so the below coefficients are taken from DNV-OS-E301 2013 + lineProps: - chain : # R4 grade studless chain - mass_d2 : 19.9e3 # linear mass density per diameter^2 [kg/m^3] - EA_d2 : 85.4e9 # stiffness per diameter^2 [N/m^2] - MBL_d : 66.72e6 # minimum breaking load per diameter [N/m] - MBL_d2 : 482.2e6 # minimum breaking load per diameter^2 [N/m^2] - dvol_dnom : 1.8 # volume-equivalent diameter per nominal diameter [-] + chain : # R4 grade studless chain (the default chain that will be used when linetypename='chain') + mass_d2 : 20.0e3 # linear mass density per diameter^2 [kg/m/m^2] + EA_d3 : -3.93e7 # stiffness per diameter^3 [N/m^3] + EA_d2 : 85.6e9 # stiffness per diameter^2 [N/m^2] + MBL_d3 : -2.19e9 # minimum breaking load per diameter^3 [N/m^3] + MBL_d2 : 1.21e9 # minimum breaking load per diameter^2 [N/m^2] + MBL_d : 9.11e2 # minimum breaking load per diameter [N/m] + dvol_dnom : 1.80 # volume-equivalent diameter per nominal diameter [-] (assumes 7,850 kg/m^3 material density) cost_mass : 2.585 # cost per mass [$/kg] + Cd : 1.333 # drag coefficient based on DNV-OS-E301 adjusted for use with volumetric diameter + Cd_ax : 0.639 # axial drag coefficient based on DNV-OS-E301 adjusted for use with volumetric diameter + Ca : 1.0 # added mass coefficient based on Bureau Veritas 493-NR_2021-07 + Ca_ax : 0.5 # axial added mass coefficient based on Bureau Veritas 493-NR_2021-07 - - chain_studlink : # R4 grade studlink chain - mass_d2 : 19.9e3 # linear mass density per diameter^2 [kg/m^3] - EA_d2 : 85.4e9 # stiffness per diameter^2 [N/m^2] - MBL_d : 66.72e6 # minimum breaking load per diameter [N/m] - MBL_d2 : 482.2e6 # minimum breaking load per diameter^2 [N/m^2] - dvol_dnom : 1.89 # volume-equivalent diameter per nominal diameter [-] - cost_mass : 2.585 # cost per mass [$/kg] - - - polyester : # polyester synthetic rope - mass_d2 : 797.8 # linear mass density per diameter^2 [kg/m^3] - EA_d2 : 1.09e9 # stiffness per diameter^2 [N/m^2] - MBL_d2 : 170.5e6 # minimum breaking load per diameter^2 [N/m^2] - dvol_dnom : 0.86 # volume-equivalent diameter per nominal diameter [-] - cost_MBL : 1.65e-05 # cost per MBL [$/m/N] - - nylon : # nylon synthetic rope - mass_d2 : 647.6 # linear mass density per diameter^2 [kg/m^3] - EA_d2 : 1.18e8 # stiffness per diameter^2 [N/m^2] - MBL_d2 : 139.4e6 # minimum breaking load per diameter^2 [N/m^2] - dvol_dnom : 0.85 # volume-equivalent diameter per nominal diameter [-] - cost_MBL : 4.29e-05 # cost per MBL [$/m/N] + chain_studlink : # R4 grade studlink chain + mass_d2 : 21.9e3 # linear mass density per diameter^2 [kg/m/m^2] + EA_d2 : 88.0e9 # stiffness per diameter^2 [N/m^2] + MBL_d3 : -2.19e9 # minimum breaking load per diameter^3 [N/m^3] + MBL_d2 : 1.21e9 # minimum breaking load per diameter^2 [N/m^2] + MBL_d : 9.11e2 # minimum breaking load per diameter [N/m] + dvol_dnom : 1.89 # volume-equivalent diameter per nominal diameter [-] (assumes 7,850 kg/m^3 material density) + cost_mass : 2.585 # cost per mass [$/kg] + Cd : 1.376 # drag coefficient based on DNV-OS-E301 adjusted for use with volumetric diameter + Cd_ax : 0.741 # axial drag coefficient based on DNV-OS-E301 adjusted for use with volumetric diameter + Ca : 1.0 # added mass coefficient based on Bureau Veritas 493-NR_2021-07 + Ca_ax : 0.5 # axial added mass coefficient based on Bureau Veritas 493-NR_2021-07 - wire : - mass_d2 : 3989.7 # linear mass density per diameter^2 [kg/m^3] - EA_d2 : 4.04e10 # stiffness per diameter^2 [N/m^2] - MBL_d2 : 633.36e6 # minimum breaking load per diameter^2 [N/m^2] - dvol_dnom : 0.8 # volume-equivalent diameter per nominal diameter [-] - cost_d2 : 0.04542e6 # cost per diameter^2 with d in meters [$/m] + wire : # wire rope + mass_d2 : 5293 # linear mass density per diameter^2 [kg/m/m^2] + dvol_dnom : 1.18 # volume-equivalent diameter per nominal diameter [-] (assumes 4,875.5 kg/m^3 material density) + MBL_d2 : 1022e6 # minimum breaking load per diameter^2 [N/m^2] + EA_d2 : 97.1e9 # stiffness per diameter^2 [N/m^2] + Cd : 1.021 # drag coefficient (transverse) based on DNV-OS-E301 adjusted for volumetric diameter; longitudinal (Cd_ax) is neglected + cost_MBL : 1.2e-05 # cost per N of MBL + polyester : # polyester synthetic rope + mass_d2 : 679 # linear mass density per diameter^2 [kg/m/m^2] + MBL_d2 : 308e6 # minimum breaking load per diameter^2 [N/m^2] + EA_MBL : 14 # quasi-static stiffness per MBL [N/N] + EAd_MBL : 11.6 # dynamic stiffness per MBL [N/N] + EAd_MBL_Lm : 40.0 # dynamic stiffness per MBL per fraction of mean load (not %) [N/N] (beta term to be multiplied with the absolute mean tension) + density : 1380 # density of the polyester material [kg/m^3] (taken from specific gravity of 1.38, relative to 1000 kg/m^3) + cost_MBL : 1.65e-05 # cost per MBL [$/m/N] + Cd : 2.021 # drag coefficient based on DNV-OS-E301 adjusted for use with volumetric diameter + Ca : 1.1 # added mass coefficient based on Bureau Veritas 493-NR_2021-07 + Ca_ax : 0.15 # axial added mass coefficient based on Bureau Veritas 493-NR_2021-07 + nylon : # nylon synthetic rope + mass_d2 : 585 # linear mass density per diameter^2 [kg/m/m^2] + MBL_d3 : 230e6 # minimum breaking load per diameter^3 [N/m^2] + MBL_d2 : 207e6 # minimum breaking load per diameter^2 [N/m^2] + EA_MBL : 5 # quasi-static stiffness per MBL [N/N] (can range from 1 to 10) + EAd_MBL : 2.08 # dynamic stiffness per MBL [N/N] + EAd_MBL_Lm : 39.0 # dynamic stiffness per MBL per fraction of mean load (not %) [N/N] (beta term to be multiplied with the absolute mean tension) + density : 1140 # density of the nylon material [kg/m^3] (taken from specific gravity of 1.14, relative to 1000 kg/m^3) + cost_MBL : 4.29e-05 # cost per MBL [$/m/N] + Cd : 1.979 # drag coefficient based on DNV-OS-E301 adjusted for use with volumetric diameter + Ca : 1.1 # added mass coefficient based on Bureau Veritas 493-NR_2021-07 + Ca_ax : 0.15 # axial added mass coefficient based on Bureau Veritas 493-NR_2021-07 + + hmpe : # high modulus polyethylene synthetic rope + mass_d2 : 496 # linear mass density per diameter^2 [kg/m/m^2] + MBL_d3 : 651e6 # minimum breaking load per diameter^3 [N/m^2] + MBL_d2 : 580e6 # minimum breaking load per diameter^2 [N/m^2] + EA_MBL : 56 # quasi-static stiffness per MBL [N/N] + EAd_MBL : 59 # dynamic stiffness per MBL [N/N] + EAd_MBL_Lm : 54.0 # dynamic stiffness per MBL per fraction of mean load (not %) [N/N] (beta term to be multiplied with the absolute mean tension) + density : 975 # density of the hmpe/dyneema material [kg/m^3] (taken from specific gravity of 0.975, relative to 1000 kg/m^3 + Cd : 1.988 # drag coefficient based on DNV-OS-E301 adjusted for use with volumetric diameter + Ca : 1.1 # added mass coefficient based on Bureau Veritas 493-NR_2021-07 + Ca_ax : 0.15 # axial added mass coefficient based on Bureau Veritas 493-NR_2021-07 + cost_MBL : 1.4e-04 # cost per N of MBL [$/m/N] diff --git a/moorpy/__init__.py b/moorpy/__init__.py index 6fde5e0..ef6e014 100644 --- a/moorpy/__init__.py +++ b/moorpy/__init__.py @@ -8,6 +8,7 @@ from moorpy.body import Body from moorpy.lineType import LineType from moorpy.system import System +from moorpy.subsystem import Subsystem from moorpy.helpers import * from moorpy.Catenary import catenary diff --git a/moorpy/body.py b/moorpy/body.py index 1b714a3..e4c44f5 100644 --- a/moorpy/body.py +++ b/moorpy/body.py @@ -8,8 +8,9 @@ class Body(): '''A class for any object in the mooring system that will have its own reference frame''' - def __init__(self, mooringSys, num, type, r6, m=0, v=0, rCG=np.zeros(3), AWP=0, rM=np.zeros(3), - f6Ext=np.zeros(6), I=np.zeros(3), CdA=np.zeros(3), Ca=np.zeros(3)): + def __init__(self, mooringSys, num, type, r6, m=0, v=0, rCG=np.zeros(3), + AWP=0, rM=np.zeros(3), f6Ext=np.zeros(6), I=np.zeros(3), + CdA=np.zeros(3), Ca=np.zeros(3), DOFs=[0,1,2,3,4,5]): '''Initialize Body attributes Parameters @@ -40,10 +41,9 @@ def __init__(self, mooringSys, num, type, r6, m=0, v=0, rCG=np.zeros(3), AWP=0, Product of drag coefficient and frontal area in three directions [m^2]. Ca : array, optional Added mass coefficient in three directions. - attachedP: list, int - list of ID numbers of any Points attached to the Body - rPointRel: list, float - list of coordinates of each attached Point relative to the Body reference frame [m] + DOFs: list, optional + list of the DOFs for this body (0=surge,1=sway,...5=yaw). Any not + listed will be held fixed. E.g. [0,1,5] for 2D horizontal motion. Returns ------- @@ -55,15 +55,15 @@ def __init__(self, mooringSys, num, type, r6, m=0, v=0, rCG=np.zeros(3), AWP=0, self.number = num self.type = type # 0 free to move, or -1 coupled externally - self.r6 = np.array(r6, dtype=np.float_) # 6DOF position and orientation vector [m, rad] + self.r6 = np.array(r6, dtype=float) # 6DOF position and orientation vector [m, rad] self.m = m # mass, centered at CG [kg] self.v = v # volume, assumed centered at reference point [m^3] - self.rCG = np.array(rCG, dtype=np.float_) # center of gravity position in body reference frame [m] + self.rCG = np.array(rCG, dtype=float) # center of gravity position in body reference frame [m] self.AWP = AWP # waterplane area - used for hydrostatic heave stiffness if nonzero [m^2] if np.isscalar(rM): - self.rM = np.array([0,0,rM], dtype=np.float_) # coordinates of body metacenter relative to body reference frame [m] + self.rM = np.array([0,0,rM], dtype=float) # coordinates of body metacenter relative to body reference frame [m] else: - self.rM = np.array(rM, dtype=np.float_) + self.rM = np.array(rM, dtype=float) # >>> should streamline the below <<< if np.isscalar(I): @@ -83,6 +83,8 @@ def __init__(self, mooringSys, num, type, r6, m=0, v=0, rCG=np.zeros(3), AWP=0, self.f6Ext = np.array(f6Ext, dtype=float) # for adding external forces and moments in global orientation (not including weight/buoyancy) + self.DOFs = DOFs + self.nDOF = len(DOFs) self.attachedP = [] # ID numbers of any Points attached to the Body self.rPointRel = [] # coordinates of each attached Point relative to the Body reference frame @@ -103,11 +105,6 @@ def attachPoint(self, pointID, rAttach): The identifier ID number of a point rAttach : array The position of the point relative to the body's frame [m] - - Returns - ------- - None. - ''' self.attachedP.append(pointID) @@ -116,6 +113,23 @@ def attachPoint(self, pointID, rAttach): if self.sys.display > 1: print("attached Point "+str(pointID)+" to Body "+str(self.number)) + """ + def dettachPoint(self, pointID): + '''Removes a Point from the Body. + + Parameters + ---------- + pointID : int + The identifier ID number of a point + rAttach : array + The position of the point relative to the body's frame [m] + ''' + + find pointID index in self.attachedP + delete entry from self.attachedP and rPointRel + + set point type to free + """ def attachRod(self, rodID, endCoords): '''Adds a Point to the Body, at the specified relative position on the body. @@ -160,10 +174,13 @@ def setPosition(self, r6): ''' + # update the position/orientation of the body if len(r6)==6: - self.r6 = np.array(r6, dtype=np.float_) # update the position of the Body itself + self.r6 = np.array(r6, dtype=float) # update the position of the Body itself + elif len(r6) == self.nDOF: + self.r6[self.DOFs] = r6 # mapping to use only some DOFs else: - raise ValueError(f"Body setPosition method requires an argument of size 6, but size {len(r6):d} was provided") + raise ValueError(f"Body setPosition method requires an argument of size 6 or nDOF, but size {len(r6):d} was provided") self.R = rotationMatrix(self.r6[3], self.r6[4], self.r6[5]) # update body rotation matrix @@ -185,7 +202,7 @@ def setPosition(self, r6): - def getForces(self, lines_only=False): + def getForces(self, lines_only=False, all_DOFs=False): '''Sums the forces and moments on the Body, including its own plus those from any attached objects. Forces and moments are aligned with global x/y/z directions but are relative to the body's local reference point. @@ -193,13 +210,14 @@ def getForces(self, lines_only=False): Parameters ---------- lines_only : boolean, optional - An option for calculating forces from just the mooring lines or not. The default is False. + If true, the Body's contribution to the forces is ignored. + all_DOFs : boolean, optional + True: return all forces/moments; False: only those in DOFs list. Returns ------- f6 : array The 6DOF forces and moments applied to the body in its current position [N, Nm] - ''' f6 = np.zeros(6) @@ -239,11 +257,14 @@ def getForces(self, lines_only=False): moment_about_body_ref = np.matmul(rotMat.T, f6[3:]) # transform moments so that they are about the body's local/rotated axes f6[3:] = moment_about_body_ref # use these moments ''' - return f6 - + + if all_DOFs: + return f6 + else: + return f6[self.DOFs] - def getStiffness(self, X = [], tol=0.0001, dx = 0.1): + def getStiffness(self, X = [], tol=0.0001, dx = 0.1, all_DOFs=False): '''Gets the stiffness matrix of a Body due only to mooring lines with all other objects free to equilibriate. The rotational indices of the stiffness matrix correspond to the global x/y/z directions. @@ -253,6 +274,8 @@ def getStiffness(self, X = [], tol=0.0001, dx = 0.1): The position vector (6DOF) of the main axes of the Body at which the stiffness matrix is to be calculated. dx : float, optional The change in displacement to be used for calculating the change in force. The default is 0.01. + all_DOFs : boolean, optional + True: return all forces/moments; False: only those in DOFs list. Returns ------- @@ -295,18 +318,27 @@ def getStiffness(self, X = [], tol=0.0001, dx = 0.1): self.sys.solveEquilibrium3(tol=tol) # find equilibrium of mooring system given this Body in current position self.type = type0 # restore the Body's type to its original value - return K - + # Return stiffness matrix + if all_DOFs: + return K + else: # only return rows/columns of active DOFs + return K[:,self.DOFs][self.DOFs,:] - def getStiffnessA(self, lines_only=False): + def getStiffnessA(self, lines_only=False, all_DOFs=False): '''Gets the analytical stiffness matrix of the Body with other objects fixed. + Parameters + ---------- + lines_only : boolean, optional + If true, the Body's contribution to its stiffness is ignored. + all_DOFs : boolean, optional + True: return all forces/moments; False: only those in DOFs list. + Returns ------- K : matrix 6x6 analytic stiffness matrix. - ''' K = np.zeros([6,6]) @@ -345,8 +377,11 @@ def getStiffnessA(self, lines_only=False): K[3:,3:] += Kw + Kb K[2 ,2 ] += Kwp - - return K + # Return stiffness matrix + if all_DOFs: + return K + else: # only return rows/columns of active DOFs + return K[:,self.DOFs][self.DOFs,:] diff --git a/moorpy/helpers.py b/moorpy/helpers.py index 519c192..80a3025 100644 --- a/moorpy/helpers.py +++ b/moorpy/helpers.py @@ -3,7 +3,7 @@ import time import yaml import os - +import re # base class for MoorPy exceptions @@ -72,6 +72,63 @@ def printVec(vec): print( "\t".join(["{:+9.4e}"]*len(vec)).format( *vec )) + +def unitVector(r): + '''Returns the unit vector along the direction of input vector r.''' + + L = np.linalg.norm(r) + + return r/L + + + +def getInterpNums(xlist, xin, istart=0): # should turn into function in helpers + ''' + Paramaters + ---------- + xlist : array + list of x values + xin : float + x value to be interpolated + istart : int + first lower index to try + + Returns + ------- + i : int + lower index to interpolate from + fout : float + fraction to return such that y* = y[i] + fout*(y[i+1]-y[i]) + ''' + + nx = len(xlist) + + if xin <= xlist[0]: # below lowest data point + i = 0 + fout = 0.0 + + elif xlist[-1] <= xin: # above highest data point + i = nx-1 + fout = 0.0 + + else: # within the data range + + # if istart is below the actual value, start with it instead of + # starting at 0 to save time, but make sure it doesn't overstep the array + if xlist[min(istart,nx)] < xin: + i1 = istart + else: + i1 = 0 + + for i in range(i1, nx-1): + if xlist[i+1] > xin: + fout = (xin - xlist[i] )/( xlist[i+1] - xlist[i] ) + break + + return i, fout + + + def getH(r): '''function gets the alternator matrix, H, that when multiplied with a vector, returns the cross product of r and that vector @@ -300,7 +357,7 @@ def dsolve2(eval_func, X0, Ytarget=[], step_func=None, args=[], tol=0.0001, ytol start_time = time.time() # process inputs and format as arrays in case they aren't already - X = np.array(X0, dtype=np.float_) # start off design variable + X = np.array(np.atleast_1d(X0), dtype=float) # start off design variable N = len(X) Xs = np.zeros([maxIter,N]) # make arrays to store X and error results of the solve @@ -313,9 +370,9 @@ def dsolve2(eval_func, X0, Ytarget=[], step_func=None, args=[], tol=0.0001, ytol # check the target Y value input if len(Ytarget)==N: - Ytarget = np.array(Ytarget, dtype=np.float_) + Ytarget = np.array(Ytarget, dtype=float) elif len(Ytarget)==0: - Ytarget = np.zeros(N, dtype=np.float_) + Ytarget = np.zeros(N, dtype=float) else: raise TypeError("Ytarget must be of same length as X0") @@ -323,7 +380,7 @@ def dsolve2(eval_func, X0, Ytarget=[], step_func=None, args=[], tol=0.0001, ytol if ytol==0: # if not using ytol if np.isscalar(tol) and tol <= 0.0: raise ValueError('tol value passed to dsovle2 must be positive') - elif not np.isscalar(tol) and any(tol <= 0): + elif not np.isscalar(tol) and any(np.array(tol) <= 0): raise ValueError('every tol entry passed to dsovle2 must be positive') # if a step function wasn't provided, provide a default one @@ -362,14 +419,14 @@ def step_func(X, args, Y, oths, Ytarget, err, tols, iter, maxIter): if len(Xmin)==0: Xmin = np.zeros(N)-np.inf elif len(Xmin)==N: - Xmin = np.array(Xmin, dtype=np.float_) + Xmin = np.array(Xmin, dtype=float) else: raise TypeError("Xmin must be of same length as X0") if len(Xmax)==0: Xmax = np.zeros(N)+np.inf elif len(Xmax)==N: - Xmax = np.array(Xmax, dtype=np.float_) + Xmax = np.array(Xmax, dtype=float) else: raise TypeError("Xmax must be of same length as X0") @@ -378,7 +435,7 @@ def step_func(X, args, Y, oths, Ytarget, err, tols, iter, maxIter): if len(dX_last)==0: dX_last = np.zeros(N) else: - dX_last = np.array(dX_last, dtype=np.float_) + dX_last = np.array(dX_last, dtype=float) if display>0: print(f"Starting dsolve iterations>>> aiming for Y={Ytarget}") @@ -422,7 +479,7 @@ def step_func(X, args, Y, oths, Ytarget, err, tols, iter, maxIter): m,b = np.polyfit(Es[int(0.7*iter):iter,0], Xs[int(0.7*iter):iter,0], 1) X = np.array([b]) Y = np.array([0.0]) - print(f"Using linaer fit to estimate solution at X={b}") + if display>0: print(f"dsolve is using linear fit to estimate solution at X={b}") break @@ -537,6 +594,11 @@ def step_func(X, args, Y, oths, Ytarget, err, tols, iter, maxIter): X = X + dX + # truncate empty parts of these arrays + Xs = Xs [:iter+1] + Es = Es [:iter+1] + dXlist = dXlist [:iter+1] + dXlist2 = dXlist2[:iter+1] return X, Y, dict(iter=iter, err=err, dX=dX_last, oths=oths, Xs=Xs, Es=Es, success=success, dXlist=dXlist, dXlist2=dXlist2) @@ -567,28 +629,13 @@ def dsolvePlot(info): def getLineProps(dnommm, material, lineProps=None, source=None, name="", rho=1025.0, g=9.81, **kwargs): '''Sets up a dictionary that represents a mooring line type based on the - specified diameter and material type. The - - - This function requires at least one input: the line diameter in millimeters. - - The rest of the inputs are optional: describe the desired type of line (chain, polyester, wire, etc.), - the type of chain (studless or studlink), the source of data (Orcaflex-original or altered), or a name identifier - - The function will output a MoorPy linetype object - - # support options for : - - # 1. dictionary passed in (this is what will be done when called from a System method) - - (a) by default, System will load in MoorPy's default line property information YAML - - (b) or user specify's alternate YAML file when making the system - - - # 2. yaml filename pass in or use default moorpy yaml and load yaml (used when called independently) - - System.linePropsDatabase will be a dictionary of stored YAML coefficients - - System.lineTypes will become a dictionary of LineType DICTIONARIES - + specified diameter and material type. The returned dictionary can serve as + a MoorPy line type. Data used for determining these properties is a MoorPy + lineTypes dictionary data structure, created by loadLineProps. This data + can be passed in via the lineProps parameter, or a new data set can be + generated based on a YAML filename or dictionary passed in via the source + parameter. The lineProps dictionary should be error-checked at creation, + so it is not error check in this function for efficiency. Parameters ---------- @@ -596,10 +643,16 @@ def getLineProps(dnommm, material, lineProps=None, source=None, name="", rho=102 nominal diameter [mm]. material : string string identifier of the material type be used. + lineProps : dictionary + A MoorPy lineProps dictionary data structure containing the property scaling coefficients. source : dict or filename (optional) YAML file name or dictionary containing line property scaling coefficients name : any dict index (optional) - Identifier for the line type (otherwise will be generated automatically). + Identifier for the line type (otherwise will be generated automatically). + rho : float (optional) + Water density used for computing apparent (wet) weight [kg/m^3]. + g : float (optional) + Gravitational constant used for computing weight [m/s^2]. ''' if lineProps==None and source==None: @@ -607,9 +660,9 @@ def getLineProps(dnommm, material, lineProps=None, source=None, name="", rho=102 # deal with the source (is it a dictionary, or reading in a new yaml?) if not source==None: - lineProps = loadLineProps(source) if not lineProps==None: print('Warning: both lineProps and source arguments were passed to getLineProps. lineProps will be ignored.') + lineProps = loadLineProps(source) # raise an error if the material isn't in the source dictionary if not material in lineProps: @@ -617,26 +670,53 @@ def getLineProps(dnommm, material, lineProps=None, source=None, name="", rho=102 # calculate the relevant properties for this specific line type mat = lineProps[material] # shorthand for the sub-dictionary of properties for the material in question - d = dnommm*0.001 # convert nominal diameter from mm to m - d_vol = mat['dvol_dnom']*d - mass = mat['mass_0'] + mat['mass_d']*d + mat['mass_d2']*d**2 + mat['mass_d3']*d**3 + d = dnommm*0.001 # convert nominal diameter from mm to m + mass = mat['mass_d2']*d**2 MBL = mat[ 'MBL_0'] + mat[ 'MBL_d']*d + mat[ 'MBL_d2']*d**2 + mat[ 'MBL_d3']*d**3 EA = mat[ 'EA_0'] + mat[ 'EA_d']*d + mat[ 'EA_d2']*d**2 + mat[ 'EA_d3']*d**3 + mat['EA_MBL']*MBL cost =(mat['cost_0'] + mat['cost_d']*d + mat['cost_d2']*d**2 + mat['cost_d3']*d**3 + mat['cost_mass']*mass + mat['cost_EA']*EA + mat['cost_MBL']*MBL) + # add in drag and added mass coefficients if available, if not, use defaults + if 'Cd' in mat: + Cd = mat['Cd'] + else: + Cd = 1.2 + if 'Cd_ax' in mat: + CdAx = mat['Cd_ax'] + else: + CdAx = 0.2 + if 'Ca' in mat: + Ca = mat['Ca'] + else: + Ca = 1.0 + if 'Ca_ax' in mat: + CaAx = mat['Ca_ax'] + else: + CaAx = 0.0 + + # internally calculate the volumetric diameter using a ratio + d_vol = mat['dvol_dnom']*d # [m] + + # use the volumetric diameter to calculate the apparent weight per unit length w = (mass - np.pi/4*d_vol**2 *rho)*g + # stiffness values for viscoelastic approach + EAd = mat['EAd_MBL']*MBL # dynamic stiffness constant: Krd alpha term x MBL [N] + EAd_Lm = mat['EAd_MBL_Lm'] # dynamic stiffness Lm slope: Krd beta term (to be multiplied by mean load) [-] + # Set up a main identifier for the linetype unless one is provided if name=="": - typestring = f"{type}{dnommm:.0f}" + typestring = f"{material}{dnommm:.0f}" # note: previously was type instead of material, undefined else: typestring = name notes = f"made with getLineProps" lineType = dict(name=typestring, d_vol=d_vol, m=mass, EA=EA, w=w, - MBL=MBL, cost=cost, notes=notes, input_type=type, input_d=d, material=material) - + MBL=MBL, EAd=EAd, EAd_Lm=EAd_Lm, input_d=d, + cost=cost, notes=notes, material=material, + Cd=Cd, CdAx=CdAx, Ca=Ca, CaAx=CaAx) + lineType.update(kwargs) # add any custom arguments provided in the call to the lineType's dictionary return lineType @@ -651,10 +731,14 @@ def loadLineProps(source): Parameters ---------- - source : dict or filename YAML file name or dictionary containing line property scaling coefficients - + + Returns + ------- + dictionary + LineProps dictionary listing each supported mooring line type and + subdictionaries of scaling coefficients for each. ''' if type(source) is dict: @@ -682,22 +766,37 @@ def loadLineProps(source): output = dict() # output dictionary combining default values with loaded coefficients # combine loaded coefficients and default values into dictionary that will be saved for each material - for mat, props in lineProps.items(): + for mat, props in lineProps.items(): output[mat] = {} - output[mat]['mass_0' ] = getFromDict(props, 'mass_0' , default=0.0) - output[mat]['mass_d' ] = getFromDict(props, 'mass_d' , default=0.0) - output[mat]['mass_d2' ] = getFromDict(props, 'mass_d2' , default=0.0) - output[mat]['mass_d3' ] = getFromDict(props, 'mass_d3' , default=0.0) + output[mat]['mass_d2' ] = getFromDict(props, 'mass_d2') # mass must scale with d^2 output[mat]['EA_0' ] = getFromDict(props, 'EA_0' , default=0.0) output[mat]['EA_d' ] = getFromDict(props, 'EA_d' , default=0.0) output[mat]['EA_d2' ] = getFromDict(props, 'EA_d2' , default=0.0) output[mat]['EA_d3' ] = getFromDict(props, 'EA_d3' , default=0.0) output[mat]['EA_MBL' ] = getFromDict(props, 'EA_MBL' , default=0.0) + output[mat]['EAd_MBL' ] = getFromDict(props, 'EAd_MBL' , default=0.0) + output[mat]['EAd_MBL_Lm']= getFromDict(props, 'EAd_MBL_Lm',default=0.0) + output[mat]['Cd' ] = getFromDict(props, 'Cd' , default=0.0) + output[mat]['Cd_ax' ] = getFromDict(props, 'Cd_ax' , default=0.0) + output[mat]['Ca' ] = getFromDict(props, 'Ca' , default=0.0) + output[mat]['Ca_ax' ] = getFromDict(props, 'Ca_ax' , default=0.0) + output[mat]['MBL_0' ] = getFromDict(props, 'MBL_0' , default=0.0) output[mat]['MBL_d' ] = getFromDict(props, 'MBL_d' , default=0.0) output[mat]['MBL_d2' ] = getFromDict(props, 'MBL_d2' , default=0.0) output[mat]['MBL_d3' ] = getFromDict(props, 'MBL_d3' , default=0.0) output[mat]['dvol_dnom'] = getFromDict(props, 'dvol_dnom', default=1.0) + + # special handling if material density is provided + if 'density' in props: + if 'dvol_dnom' in props: + raise ValueError("Only one parameter can be specified to calculate the volumetric diameter. Choose either 'dvol_dnom' or 'density'.") + else: + mass_d2 = output[mat]['mass_d2'] + material_density = getFromDict(props, 'density') + output[mat]['dvol_dnom'] = np.sqrt((mass_d2/material_density)*(4/np.pi)) + + # cost coefficients output[mat]['cost_0' ] = getFromDict(props, 'cost_0' , default=0.0) output[mat]['cost_d' ] = getFromDict(props, 'cost_d' , default=0.0) output[mat]['cost_d2' ] = getFromDict(props, 'cost_d2' , default=0.0) @@ -705,10 +804,77 @@ def loadLineProps(source): output[mat]['cost_mass'] = getFromDict(props, 'cost_mass', default=0.0) output[mat]['cost_EA' ] = getFromDict(props, 'cost_EA' , default=0.0) output[mat]['cost_MBL' ] = getFromDict(props, 'cost_MBL' , default=0.0) + + return output + + + + +def getPointProps(weight, rho=1025.0, g=9.81, **kwargs): + '''for now this is just getClumpMV put in a place where it could grow + into a fully versatile equivalent to getMoorProps. + ''' + + '''A function to provide a consistent scheme for converting a clump weight/float magnitude to the + mass and volume to use in a MoorPy Point.''' + + if weight >= 0: # if the top point of the intermediate line has a clump weight + pointvol = 0.0 + pointmass = weight*1000.0 # input variables are in units of tons (1000 kg), convert to kg + else: + pointvol = -weight*1200.0/rho # input variables are still in tons. Assume additional 20% of BM mass + pointmass = -weight*200.0 + + return dict(m=pointmass, v=pointvol) + + +def loadPointProps(source): + '''Loads a set of MoorPy point property scaling coefficients from + a specified YAML file or passed dictionary. + + Parameters + ---------- + source : dict or filename + YAML file name or dictionary containing line property scaling coefficients + + Returns + ------- + dictionary + PointProps dictionary listing each supported mooring line type and + subdictionaries of scaling coefficients for each. + ''' + + ''' + if type(source) is dict: + source = source + + elif source is None or source=="default": + import os + mpdir = os.path.dirname(os.path.realpath(__file__)) + with open(os.path.join(mpdir,"PointProps_default.yaml")) as file: + source = yaml.load(file, Loader=yaml.FullLoader) + + elif type(source) is str: + with open(source) as file: + source = yaml.load(file, Loader=yaml.FullLoader) + + else: + raise Exception("loadLineProps supplied with invalid source") + + if 'lineProps' in source: + lineProps = source['lineProps'] + else: + raise Exception("YAML file or dictionary must have a 'lineProps' field containing the data") + ''' + + output = dict() # output dictionary combining default values with loaded coefficients + + #output['generic'] = dict(rho = , m_v = , ) return output + def getFromDict(dict, key, shape=0, dtype=float, default=None): ''' Function to streamline getting values from design dictionary from YAML file, including error checking. @@ -772,6 +938,56 @@ def getFromDict(dict, key, shape=0, dtype=float, default=None): return np.tile(default, shape) +def addToDict(dict1, dict2, key1, key2, default=None): + ''' + Function to streamline getting values from one dictionary and + putting them in another dictionary (potentially under a different key), + including error checking. + + Parameters + ---------- + dict1 : dict + the input dictionary + dict2 : dict + the output dictionary + key1 : string + the key in the input dictionary + key2 : string + the key in the output dictionary + default : number, optional + The default value to fill in if the item isn't in the input dictionary. + Otherwise will raise error if the key doesn't exist. + ''' + + if key1 in dict1: + val = dict1[key1] + else: + if default == None: + raise ValueError(f"Key '{key1}' not found in input dictionary...") + else: + val = default + + dict2[key2] = val + + +def drawBox(ax, r1, r2, color=[0,0,0,0.2]): + '''Draw a box along the x-y-z axes between two provided corner points.''' + + + ax.plot([r1[0], r2[0]], [r1[1], r1[1]], [r1[2], r1[2]], color=color) # along x + ax.plot([r1[0], r2[0]], [r2[1], r2[1]], [r1[2], r1[2]], color=color) + ax.plot([r1[0], r2[0]], [r1[1], r1[1]], [r2[2], r2[2]], color=color) + ax.plot([r1[0], r2[0]], [r2[1], r2[1]], [r2[2], r2[2]], color=color) + ax.plot([r1[0], r1[0]], [r1[1], r2[1]], [r1[2], r1[2]], color=color) # along y + ax.plot([r2[0], r2[0]], [r1[1], r2[1]], [r1[2], r1[2]], color=color) + ax.plot([r1[0], r1[0]], [r1[1], r2[1]], [r2[2], r2[2]], color=color) + ax.plot([r2[0], r2[0]], [r1[1], r2[1]], [r2[2], r2[2]], color=color) + ax.plot([r1[0], r1[0]], [r1[1], r1[1]], [r1[2], r2[2]], color=color) # along z + ax.plot([r1[0], r1[0]], [r2[1], r2[1]], [r1[2], r2[2]], color=color) + ax.plot([r2[0], r2[0]], [r1[1], r1[1]], [r1[2], r2[2]], color=color) + ax.plot([r2[0], r2[0]], [r2[1], r2[1]], [r1[2], r2[2]], color=color) + + def makeTower(twrH, twrRad): '''Sets up mesh points for visualizing a cylindrical structure (should align with RAFT eventually.''' @@ -802,6 +1018,501 @@ def makeTower(twrH, twrRad): return Xs, Ys, Zs +def lines2ss(ms): + ''' + This function automatically detects multi-segmented + mooring lines in the MoorPy system object, convert + them into subsystems, and updates the MoorPy system. + It detects whether the line is suspended or anchored, + as well as whether it is in the right order (if not + it will re-oreder). + + Parameters + ---------- + ms (object): + MoorPy system object + + Returns + ---------- + ms : object + an updated MoorPy system object with the replaced + multi-segmented mooring lines with subsystems. + + ''' + + i = 0 + while True: + subsys_line_id = [] + subsys_point_id = [] + line_ID_of_interest = [] + point_ID_of_interest = [] + pointi = ms.pointList[i] + if len(pointi.attached) > 2: + raise ValueError("f point number {pointi.number} branches out.") + # 1) define the connected lines if any + subsys_line_id.append(pointi.attached[0]) + subsys_point_id.append(pointi.number) + # 2) check where the line with line_ID has been repeated in other points + while True: + for line_id in subsys_line_id: + for pointj in ms.pointList: + if line_id in pointj.attached: + line_ID_of_interest.append(pointj.attached) + point_ID_of_interest.append(pointj.number) + # if len(pointj.attached) > 2: # this is the case where we end the subsystem chain if the subsystem line is branching + # continue + old_subsys_line = subsys_line_id + old_subsys_point = subsys_point_id + # 3) get the unique values + subsys_line_id = np.unique(np.concatenate(line_ID_of_interest)) + subsys_point_id = np.unique(point_ID_of_interest) + if len(subsys_line_id) == len(old_subsys_line) and len(subsys_point_id) == len(old_subsys_point): + break + + # 4) check if the subsystem is at its lowest state (line and two points), in that case, move to the next point. + if len(subsys_line_id) == 1 and len(subsys_point_id) == 2: + i += 1 + if i >= len(ms.pointList): + break + continue + # 5) define the case for this subsys: (case=0: anchored, case=1: suspended) + ends_z = [] + for pointk in subsys_point_id: + ends_z.append(ms.pointList[pointk - 1].r[-1]) + + dist_to_seabed = np.abs(np.min(ends_z)) - ms.depth # distance from the lowest point to the seabed + # if the abs(distance) is below 20%, and it is of fixed type, we consider it anchored (seabed can be variant and anchor might not be at exact ms.seabed) + if np.abs(dist_to_seabed) < 0.2 * ms.depth and ms.pointList[np.argmin(ends_z)].type == 1: + case = 0 + else: + case = 1 # suspended + + # 6) rearrange lines. + if case==0: + anchored_point = ms.pointList[subsys_point_id[np.argmin(ends_z)]-1].r + # find the distance between the anchored point and the middle of each line: + dist_to_anchor = [] + + for line_id in subsys_line_id: + rA = ms.lineList[line_id-1].rA + rB = ms.lineList[line_id-1].rB + rAdist_to_anchor = np.linalg.norm(rA - anchored_point) + rBdist_to_anchor = np.linalg.norm(rB - anchored_point) + dist_to_anchor.append(np.mean([rAdist_to_anchor, rBdist_to_anchor])) + if rAdist_to_anchor > rBdist_to_anchor: # Find a way to switch rA and rB so that rA is the point closer to the anchor + pass + + subsys_lines = subsys_line_id[np.argsort(dist_to_anchor)] - 1 + + # find the distance between the anchored point and the mooring points + pdist_to_anchor = [] + for point_id in subsys_point_id: + pdist_to_anchor.append(np.linalg.norm(ms.pointList[point_id - 1].r - anchored_point)) + + subsys_points = subsys_point_id[np.argsort(pdist_to_anchor)] - 1 + else: + subsys_lines = subsys_line_id - 1 # no need to rearrange because it could work from either end + subsys_points = subsys_point_id - 1 + + + lines = list(subsys_lines) + points = list(subsys_points) + ms = lines2subsystem(lines, ms, span=None, case=case) + ms.initialize() + ms.solveEquilibrium() + i += 1 + if i >= len(ms.pointList): + break + + return ms + +def lines2subsystem(lines,ms,span=None,case=0): + '''Takes a set of connected lines (in order from rA to rB) in a moorpy system and creates a subsystem equivalent. + The original set of lines are then removed from the moorpy system and replaced with the + subsystem. + + Parameters + ---------- + lines : list + List of indices in the ms.lineList to replace. + ms : object + MoorPy system object the lines are part of + span : float (optional) + Span of the total line (from start to end of subsystem) + case : int (optional) + 0 = end A on seabed + 1 = suspended line with end A at another floater + 2 = suspended line is symmetric, end A is assumed the midpoint + + Returns + ------- + ms : object + MoorPy system object with new subsystem line + + ''' + from moorpy.subsystem import Subsystem + from copy import deepcopy + # save a deepcopy of the line list to delete + originalList = deepcopy(lines) + + # # check that all lines connect (all are sections of one full mooring line) + # for i in range(0,len(lines)): + # if i>0: + # if not all(b==0 for b in ms.lineList[lines[i]].rB == ms.lineList[lines[i-1]].rA): + # raise Exception('Lines indices must be provided in order from rA to rB.') + # get the span of the subsystem line + if not span: + span = np.sqrt((ms.lineList[lines[0]].rA[0]-ms.lineList[lines[-1]].rB[0])**2+(ms.lineList[lines[0]].rA[1]-ms.lineList[lines[-1]].rB[1])**2) + # make a subsystem object + ss = Subsystem(depth=ms.depth, span=span,rBFair=ms.lineList[lines[-1]].rB) + lengths = [] + types = [] + pt = [] # list of points that + # ptB = [] + + # go through each line + for i in lines: + # see which point is connected to end A and end B + for j in range(0,len(ms.pointList)): + for k in range(0,len(ms.pointList[j].attached)): + if ms.pointList[j].attached[k] == i+1: #and ms.pointList[j].attachedEndB[k] == 0: + if not j in pt: + pt.append(j) + # elif ms.pointList[j].attached[k] == i+1 and ms.pointList[j].attachedEndB[k] == 1: + # ptB.append(j) + + # collect line lengths and types + lengths.append(ms.lineList[i].L) + types.append(ms.lineList[i].type['name']) + ss.lineTypes[types[-1]] = ms.lineTypes[types[-1]] + + # use makeGeneric to build the subsystem line + ss.makeGeneric(lengths,types,suspended=case) + ss.setEndPosition(ms.lineList[lines[0]].rA,endB=0) + ss.setEndPosition(ms.lineList[lines[-1]].rB,endB=1) + + # add in any info on the points connected to the lines + # currently, mass, volume, and diameter but others could be added + for i in range(0,len(pt)): + ss.pointList[i].m = ms.pointList[pt[i]].m + ss.pointList[i].v = ms.pointList[pt[i]].v + ss.pointList[i].d = ms.pointList[pt[i]].d + # ss.pointList[i].m = ms.pointList[ptB[i]].m + # ss.pointList[i].v = ms.pointList[ptB[i]].v + # ss.pointList[i].d = ms.pointList[ptB[i]].d + + from moorpy import helpers + # delete old line + for i in range(0,len(lines)): + decB = 0 # boolean to check if ptB has been decreased already for this line + decA = 0 # boolean to check if ptA has been decreased already for this line + if i == 0 and i < len(lines) - 1: + # first line of multiple (keep only point A) + delpts = 2 + for j in range(0,len(ms.pointList)): + if lines[i]+1 in ms.pointList[j].attached: + if pt[-1]>j and decB == 0: + pt[-1] -= 1 + decB = 1 + if pt[0]>j and decA == 0: + pt[0] -= 1 + decA = 1 + elif i == 0 and i == len(lines) - 1: + # first line, only line (keep point A and B) + delpts = 0 + elif i == len(lines) - 1: + # last line, keep point A because already been deleted, and keep point B (fairlead) + delpts = 0 + else: + # not beginning or end line, point A (previous line pointB) will have already been deleted so don't delete point A + delpts = 2 + # reduce index of last point B in ptB list and first point A in ptA list (only care about last ptB and first ptA now) by one + for j in range(0,len(ms.pointList)): + if lines[i]+1 in ms.pointList[j].attached: + if pt[-1]>j and decB == 0: + pt[-1] -= 1 + decB = 1 + if pt[0]>j and decA == 0: + pt[0] -= 1 + decA = 1 + # adjust index of any lines that have a higher index than the line to delete + for j in range(0,len(lines)): + if lines[i] ln + 1: + ms.lineList[i].number = ms.lineList[i].number - 1 + + # adjust attached line index number in any point that is attached to a line index after the deleted line index + numpts = len(ms.pointList) + i=0 + reset = 0 + while i < numpts: + j = 0 + while j < len(ms.pointList[i].attached): + Bbool = ms.pointList[i].attachedEndB[j] + reset = 0 # turn off boolean to reset i + if ms.pointList[i].attached[j] > ln+1 : + ms.pointList[i].attached[j] = ms.pointList[i].attached[j] - 1 # new index will be one less + # delete points if wanted + elif ms.pointList[i].attached[j] == ln+1: + # remove line number from attached list + ms.pointList[i].attached.pop(j) + if delpts == 0: + # keep the point but delete the line from the attachedEndB list + ms.pointList[i].attachedEndB.pop(j) + if delpts == 1 or delpts == 3: + if Bbool == 0: + # if ms.pointList[i].attachedEndB[j] == 0: + # reduce number of times through the loop + numpts = numpts-1 + # reduce point.number for each point after deleted point + for k in range(0,len(ms.pointList)): + if ms.pointList[k].number > i + 1: + ms.pointList[k].number = ms.pointList[k].number - 1 + # lower index of any body attached points after deleted point, remove deleted point from body attached points + for k in range(0,len(ms.bodyList)): + ii = 0 + while ii < len(ms.bodyList[k].attachedP): + if ms.bodyList[k].attachedP[ii] == i+1 : + # remove point + ms.bodyList[k].attachedP.pop(ii) + # remove relative points from list + ms.bodyList[k].rPointRel.pop(ii) + ii = ii - 1 # reduce iter because attachedP[1] is now attachedP[0] so need to get that one + elif ms.bodyList[k].attachedP[ii] > i+1 : + # reduce index by one + ms.bodyList[k].attachedP[ii] = ms.bodyList[k].attachedP[ii] - 1 + ii += 1 + # remove point + ms.pointList.pop(i) + # trigger boolean to reset i and j back one (since now point x+1 will be point x) + reset = 1 + + if delpts == 2 or delpts == 3: + if Bbool == 1: + # if ms.pointList[i].attachedEndB[j] == 1: + # reduce number of times through the loop + numpts = numpts-1 + # reduce point.number for each point after deleted point + for k in range(0,len(ms.pointList)): + if ms.pointList[k].number > i + 1: + ms.pointList[k].number = ms.pointList[k].number - 1 + # lower index of any body attached points after deleted point, remove deleted point from body attached points + for k in range(0,len(ms.bodyList)): + ii = 0 + while ii < len(ms.bodyList[k].attachedP): + if ms.bodyList[k].attachedP[ii] == i+1 : + # remove relative points from list + ms.bodyList[k].rPointRel.pop(ii) + # remove point + ms.bodyList[k].attachedP.pop(ii) + ii = ii - 1 # reduce iter because attachedP[1] is now attachedP[0] so need to get that one + elif ms.bodyList[k].attachedP[ii] > i+1 : + # reduce index by one + ms.bodyList[k].attachedP[ii] = ms.bodyList[k].attachedP[ii] - 1 + ii += 1 + # remove point + ms.pointList.pop(i) + # trigger boolean to reset i and j back one (since now point x+1 will be point x) + reset = 1 + + j += 1 + if reset: + j -= 1 + # need to get out of inner loop if the last point in the list was deleted (for statement for inner loop would throw an error otherwise) + if i >= len(ms.pointList): + break + # reset i if any points were removed + if reset: + i -= 1 + # increment i + i += 1 + return(ms) + +def subsystem2Line(ms,ssNum,nsegs=10): + '''Replace a subsystem with equivalent set of lines + + Parameters + ---------- + ms : system object + MoorPy system object that contains the subsystem + ssNum : int + index in the lineList of ms which points to the subsystem object to replace + nsegs : list OR int, optional + Number of segments per line for each line. Can be an integer (all line sections have the same # of segments) + OR can be a list (# of segments for each section of line in order from A to B) + + Returns + ------- + None. + + ''' + # get subsystem object + ss = ms.lineList[ssNum] + types = [] + lengths = [] + points = [] + # record line types, lines, and points in the subsystem + for i in range(0,len(ss.lineList)): + types.append(ss.lineList[i].type) + lengths.append(ss.lineList[i].L) + if not types[-1]['name'] in ms.lineTypes: + # add type to lineTypes list + ms.lineTypes[types[-1]['name']] = types[-1] + for i,spt in enumerate(ss.pointList): + # gather all info about the points in the subsystem + points.append({'r':spt.r,'m':spt.m,'v':spt.v,'CdA':spt.CdA,'d':spt.d,'type':spt.type,'Ca':spt.Ca}) + # points[0]['r'] = ss.rA + # points[-1]['r'] = ss.rB + if spt.attachedEndB[-1]: + endB = i + points[endB]['r'] = ss.rB + if spt.attachedEndB[0] == 0: + endA = i + points[endA]['r'] = ss.rA + # get actual r of end points (r in subsystem is not true location) + for i in range(0,len(ms.pointList)): + # check if point is attached to the subsystem line + for j in range(0,len(ms.pointList[i].attached)): + if ms.pointList[i].attached[j] == ssNum+1: + if ms.pointList[i].attachedEndB[j]: + # for k in range(0,len(ms.bodyList)): + # if i+1 in ms.bodyList[k].attachedP: + # points[-1]['body'] = k + # update end B r + points[endB]['r'] = ms.pointList[i].r + points[endB]['type'] = ms.pointList[i].type + # check if end points are attached to a body + for k in range(0,len(ms.bodyList)): + if i+1 in ms.bodyList[k].attachedP: + points[endB]['body'] = k + else: + # update end A r + points[endA]['r'] = ms.pointList[i].r + points[endA]['type'] = ms.pointList[i].type + # check if end points are attached to a body + for k in range(0,len(ms.bodyList)): + if i+1 in ms.bodyList[k].attachedP: + points[endA]['body'] = k + # approximate midpoint r with depth of subsystem point r and angle from two end points + aang = np.arctan2(points[0]['r'][1] - points[-1]['r'][1],points[0]['r'][0] - points[-1]['r'][0]) + # update x-y location of any midpoints if they exist + if len(points)>2: + for i in range(1,len(points)-1): + ll = np.sqrt(points[i]['r'][0]**2+points[i]['r'][1]**2) + points[i]['r'][0] = (ll)*np.cos(aang)+points[-1]['r'][0]# poits[-1]['r][0] + points[i]['r'][1] = (ll)*np.sin(aang)+points[-1]['r'][1] + #points[i]['r'][0] = (ll+np.abs(points[-1]['r'][0]))*np.cos(aang) + #points[i]['r'][1] = (ll+np.abs(points[-1]['r'][1]))*np.sin(aang) + + from moorpy import helpers + # remove subsystem line, delete all associated points + helpers.deleteLine(ms,ssNum,delpts=3) + # add in new lines to replace subsystem + for i in range(0,len(types)): + # determine # of segments for this line section + if isinstance(nsegs,list): + NSegs = nsegs[i] + elif isinstance(nsegs,int): + NSegs = nsegs + else: + raise Exception('Input nsegs must be either a list or an integer') + # add point A + ms.addPoint(points[i]['type'], points[i]['r'], points[i]['m'], points[i]['v'], d=points[i]['d']) + ms.pointList[-1].CdA = points[i]['CdA'] + ms.pointList[-1].Ca = points[i]['Ca'] + # add line + ms.addLine(lengths[i], types[i]['name'], nSegs=NSegs) + # attach new line to its point A + ms.pointList[-1].attachLine(len(ms.lineList),endB=0) # end A for line just created + # attach to any bodies the point was originally attached to + if 'body' in points[i]: + ms.bodyList[points[i]['body']].attachPoint(len(ms.pointList),[ms.pointList[-1].r[0]-ms.bodyList[points[i]['body']].r6[0],ms.pointList[-1].r[1]-ms.bodyList[points[i]['body']].r6[1],ms.pointList[-1].r[2]]) + if i>0: + # new point is end B for previous line, attach as point B + ms.pointList[-1].attachLine(len(ms.lineList)-1,endB=1) + + # add last point (should be the fairlead) + ms.addPoint(points[-1]['type'], points[-1]['r'], points[-1]['m'], points[-1]['v'], d=points[-1]['d']) + ms.pointList[-1].CdA = points[-1]['CdA'] + ms.pointList[-1].Ca = points[-1]['Ca'] + # attach to last line as point B + ms.pointList[-1].attachLine(len(ms.lineList),endB=1) + # attach to a body if applicable + if 'body' in points[-1]: + ms.bodyList[points[-1]['body']].attachPoint(len(ms.pointList),[ms.pointList[-1].r[0]-ms.bodyList[points[-1]['body']].r6[0],ms.pointList[-1].r[1]-ms.bodyList[points[-1]['body']].r6[1],ms.pointList[-1].r[2]]) + + + + + +def readBathymetryFile(filename): + '''Read a MoorDyn-style bathymetry input file (rectangular grid of depths) + and return the lists of x and y coordinates and the matrix of depths. + ''' + f = open(filename, 'r') + + # skip the header + line = next(f) + # collect the number of grid values in the x and y directions from the second and third lines + line = next(f) + nGridX = int(line.split()[1]) + line = next(f) + nGridY = int(line.split()[1]) + # allocate the Xs, Ys, and main bathymetry grid arrays + bathGrid_Xs = np.zeros(nGridX) + bathGrid_Ys = np.zeros(nGridY) + bathGrid = np.zeros([nGridY, nGridX]) # MH swapped order June 30 + # read in the fourth line to the Xs array + line = next(f) + bathGrid_Xs = [float(line.split()[i]) for i in range(nGridX)] + # read in the remaining lines in the file into the Ys array (first entry) and the main bathymetry grid + for i in range(nGridY): + line = next(f) + entries = line.split() + bathGrid_Ys[i] = entries[0] + bathGrid[i,:] = entries[1:] + + return bathGrid_Xs, bathGrid_Ys, bathGrid + def read_mooring_file(dirName,fileName): # Taken from line system.... maybe should be a helper function? @@ -851,4 +1562,82 @@ def read_mooring_file(dirName,fileName): data3 = data2.astype(float) return data3, ch, channels, units - \ No newline at end of file + +def read_output_file(dirName,fileName, skiplines=-1, hasunits=1, chanlim=999, dictionary=True): + + # load data from FAST output file + # looks for channel names, then units (if hasunits==1), then data lines after first skipping [skiplines] lines. + # skiplines == -1 signals to search for first channel names line based on starting channel "Time". + +# print('attempting to load '+dirName+fileName) + f = open(dirName+fileName, 'r') + + channels = [] + units = [] + data = [] + i=0 + + for line in f: # loop through lines in file + + if (skiplines == -1): # special case signalling to search for "Time" at start of channel line + entries = line.split() # split elements by whitespace + print(entries) + if entries[0].count('Time') > 0 or entries[0].count('time') > 0: # if we find the time keyword + skiplines = i + print("got skiplines="+str(i)) + else: + pass + + if (i < skiplines or skiplines < 0): # if we haven't gotten to the first channel line or we're in search mode, skip + pass + + elif (i == skiplines): + for entry in line.split(): # loop over the elemets, split by whitespace + channels.append(entry) # append to the last element of the list + + elif (i == skiplines+1 and hasunits == 1): + for entry in line.split(): # loop over the elemets, split by whitespace + if entry.count('kN') > 0 and entry.count('m') > 0: # correct for a possible weird character + entry = '(kN-m)' + + units.append(entry) # append to the last element of the list + + elif len(line.split()) > 0: + data.append([]) # add a new sublist to the data matrix + + r = re.compile(r"(?<=\d)\-(?=\d)") # catch any instances where a large negative exponent has been written with the "E" + line2 = r.sub("E-",line) # and add in the E + + j=0 + for entry in line2.split(): # loop over the elements, split by whitespace + if j > chanlim: + break + j+=1 + data[-1].append(entry) # append to the last element of the list + + else: + break + + i+=1 + + f.close() # close data file + + + # use a dictionary for convenient access of channel columns (eg. data[t][ch['PtfmPitch'] ) + ch = dict(zip(channels, range(len(channels)))) + + #print ch['WindVxi'] + + data2 = np.array(data) + + data3 = data2.astype(float) + + if dictionary: + dataDict = {} + unitDict = {} + for i in range(len(channels)): + dataDict[channels[i]] = data3[:,i] + unitDict[channels[i]] = units[i] + return dataDict, unitDict + else: + return data3, ch, channels, units \ No newline at end of file diff --git a/moorpy/line.py b/moorpy/line.py index df295cf..9772173 100644 --- a/moorpy/line.py +++ b/moorpy/line.py @@ -1,10 +1,12 @@ - +import pdb import numpy as np from matplotlib import cm from moorpy.Catenary import catenary from moorpy.nonlinear import nonlinear -from moorpy.helpers import LineError, CatenaryError, rotationMatrix, makeTower, read_mooring_file, quiver_data_to_segments +from moorpy.helpers import (unitVector, LineError, CatenaryError, + rotationMatrix, makeTower, read_mooring_file, + quiver_data_to_segments, printVec, printMat) from os import path @@ -30,8 +32,6 @@ def __init__(self, mooringSys, num, L, lineType, nSegs=100, cb=0, isRod=0, attac line seabed friction coefficient (will be set negative if line is fully suspended). The default is 0. isRod : boolean, optional determines whether the line is a rod or not. The default is 0. - attachments : TYPE, optional - ID numbers of any Points attached to the Line. The default is [0,0]. << consider removing Returns ------- @@ -44,36 +44,40 @@ def __init__(self, mooringSys, num, L, lineType, nSegs=100, cb=0, isRod=0, attac self.number = num self.isRod = isRod - self.L = L # line unstretched length + self.L = L # line unstretched length (may be modified if using nonlinear elasticity) [m] + self.L0 = L # line reference unstretched length [m] self.type = lineType # dictionary of a System.lineTypes entry + self.cost = {} # empty dictionary to contain cost information + + self.EA = self.type['EA'] # use the default stiffness value for now (may be modified if using nonlinear elasticity) [N] self.nNodes = int(nSegs) + 1 self.cb = float(cb) # friction coefficient (will automatically be set negative if line is fully suspended) + self.sbnorm = [] # Seabed Normal Vector (to be filled with a 3x1 normal vector describing seabed orientation) self.rA = np.zeros(3) # end coordinates self.rB = np.zeros(3) self.fA = np.zeros(3) # end forces self.fB = np.zeros(3) + self.TA = 0 # end tensions [N] + self.TB = 0 + self.KA = np.zeros([3,3]) # 3D stiffness matrix of end A [N/m] + self.KB = np.zeros([3,3]) # 3D stiffness matrix of end B + self.KBA= np.zeros([3,3]) # 3D stiffness matrix of cross coupling between ends - #Perhaps this could be made less intrusive by defining it using a line.addpoint() method instead, similar to point.attachline(). - self.attached = attachments # ID numbers of the Points at the Line ends [a,b] >>> NOTE: not fully supported <<<< - self.th = 0 # heading of line from end A to B self.HF = 0 # fairlead horizontal force saved for next solve self.VF = 0 # fairlead vertical force saved for next solve - self.KA = [] # to be filled with the 2x2 end stiffness matrix from catenary - self.KB = [] # to be filled with the 2x2 end stiffness matrix from catenary self.info = {} # to hold all info provided by catenary self.qs = 1 # flag indicating quasi-static analysis (1). Set to 0 for time series data self.show = True # a flag that will be set to false if we don't want to show the line (e.g. if results missing) - #print("Created Line "+str(self.number)) self.color = 'k' self.lw=0.5 - - + self.fCurrent = np.zeros(3) # total current force vector on the line [N] + - def loadData(self, dirname, rootname, sep='.MD.'): + def loadData(self, dirname, rootname, sep='.MD.', id=0): '''Loads line-specific time series data from a MoorDyn output file''' self.qs = 0 # signals time series data @@ -82,8 +86,11 @@ def loadData(self, dirname, rootname, sep='.MD.'): strtype='Rod' elif self.isRod==0: strtype='Line' - - filename = dirname+rootname+sep+strtype+str(self.number)+'.out' + + if id==0: + id = self.number + + filename = dirname+rootname+sep+strtype+str(id)+'.out' if path.exists(filename): @@ -158,12 +165,16 @@ def loadData(self, dirname, rootname, sep='.MD.'): # --- Read in additional data if available --- # segment tension <<< to be changed to nodal tensions in future MD versions - #if "Seg1Te" in ch if "Seg1Ten" in ch: self.Tendata = True - self.Ten = np.zeros([nT,self.nNodes-1]) + self.Te = np.zeros([nT,self.nNodes-1]) for i in range(self.nNodes-1): - self.Ten[:,i] = data[:, ch['Seg'+str(i+1)+'Ten']] + self.Te[:,i] = data[:, ch['Seg'+str(i+1)+'Ten']] + elif "Seg1Te" in ch: + self.Tendata = True + self.Te = np.zeros([nT,self.nNodes-1]) + for i in range(self.nNodes-1): + self.Te[:,i] = data[:, ch['Seg'+str(i+1)+'Te']] else: self.Tendata = False @@ -268,13 +279,22 @@ def loadData(self, dirname, rootname, sep='.MD.'): # print(e) # self.show = False - + + def setL(self, L): + '''Sets the line unstretched length [m], and saves it for use with + static-dynamic stiffness adjustments. Also reverts to static + stiffness to avoid an undefined state of having changing the line + length in a state with adjusted dynamic EA and L values.''' + self.L = L + self.L0 = L + self.revertToStaticStiffness() + def getTimestep(self, Time): '''Get the time step to use for showing time series data''' if Time < 0: - ts = np.int_(-Time) # negative value indicates passing a time step index + ts = int(-Time) # negative value indicates passing a time step index else: # otherwise it's a time in s, so find closest time step if len(self.Tdata) > 0: for index, item in enumerate(self.Tdata): @@ -291,10 +311,10 @@ def getTimestep(self, Time): - def getLineCoords(self, Time, n=0): # formerly UpdateLine + def getLineCoords(self, Time, n=0, segmentTensions=False): '''Gets the updated line coordinates for drawing and plotting purposes.''' - if n==0: n = self.nNodes + if n==0: n = self.nNodes # <<< not used! # special temporary case to draw a rod for visualization. This assumes the rod end points have already been set somehow if self.qs==1 and self.isRod > 0: @@ -319,47 +339,17 @@ def getLineCoords(self, Time, n=0): # formerly UpdateLine # if a quasi-static analysis, just call the catenary function to return the line coordinates elif self.qs==1: - - depth = self.sys.depth - - dr = self.rB - self.rA - LH = np.hypot(dr[0], dr[1]) # horizontal spacing of line ends - LV = dr[2] # vertical offset from end A to end B - if LH >0: - cosBeta = dr[0]/LH # cos of line heading - sinBeta = dr[1]/LH # sin of line heading - self.th = np.arctan2(dr[1],dr[0]) # line heading - else: # special case of vertical line: line heading is undefined - use zero as default - cosBeta = 0.0 - sinBeta = 0.0 - self.th = 0.0 - if np.min([self.rA[2],self.rB[2]]) > -depth: - self.cb = -depth - np.min([self.rA[2],self.rB[2]]) # if this line's lower end is off the seabed, set cb negative and to the distance off the seabed - elif self.cb < 0: # if a line end is at the seabed, but the cb is still set negative to indicate off the seabed - self.cb = 0.0 # set to zero so that the line includes seabed interaction. - - # ----- check for linear vs nonlinear line elasticity ----- - - #If EA is found in the line properties we will run the original catenary function - if 'EA' in self.type: - - try: - (fAH, fAV, fBH, fBV, info) = catenary(LH, LV, self.L, self.type['EA'], self.type['w'], - self.cb, HF0=self.HF, VF0=self.VF, nNodes=n, plots=1) - except CatenaryError as error: - raise LineError(self.number, error.message) - - #(fAH, fAV, fBH, fBV, info) = catenary(LH, LV, self.L, self.type['EA'], self.type['w'], CB=self.cb, HF0=self.HF, VF0=self.VF, nNodes=n, plots=1) # call line model - - #If EA isnt found then we will use the ten-str relationship defined in the input file - else: - (fAH, fAV, fBH, fBV, info) = nonlinear(LH, LV, self.L, self.type['Str'], self.type['Ten'],self.type['w']) + self.staticSolve(profiles=1) # call with flag to tell Catenary to return node info - Xs = self.rA[0] + info["X"]*cosBeta - Ys = self.rA[1] + info["X"]*sinBeta - Zs = self.rA[2] + info["Z"] - Ts = info["Te"] + #Xs = self.rA[0] + self.info["X"]*self.cosBeta + #Ys = self.rA[1] + self.info["X"]*self.sinBeta + #Zs = self.rA[2] + self.info["Z"] + #Ts = self.info["Te"] + Xs = self.Xs + Ys = self.Ys + Zs = self.Zs + Ts = self.Ts return Xs, Ys, Zs, Ts # otherwise, count on read-in time-series data @@ -395,10 +385,13 @@ def getLineCoords(self, Time, n=0): # formerly UpdateLine # handle whether or not there is tension data try: # use average to go from segment tension to node tensions <<< can skip this once MD is updated to output node tensions - Te = 0.5*(np.append(self.Te[ts,0], self.Te[ts,:]) +np.append(self.Te[ts,:], self.Te[ts,-1])) + if segmentTensions: + Te = self.Te[ts,:] # return tensions of segments rather than averaging to get tensions of nodes + else: + Te = 0.5*(np.append(self.Te[ts,0], self.Te[ts,:]) +np.append(self.Te[ts,:], self.Te[ts,-1])) except: # otherwise return zeros to avoid an error (might want a warning in some cases?) Te = np.zeros(self.nNodes) - + return self.xp[ts,:], self.yp[ts,:], self.zp[ts,:], Te @@ -411,16 +404,18 @@ def getCoordinate(self, s, n=100): Ss = np.linspace(0, self.L, n) Xs, Ys, Zs, Ts = self.getLineCoords(0.0, n=n) - X = np.interp(s, Ss, Xs)*dr[0]/LH - Y = np.interp(s, Ss, Ys)*dr[1]/LH + X = np.interp(s, Ss, Xs)*dr[0]/LH #? + Y = np.interp(s, Ss, Ys)*dr[1]/LH #? Z = np.interp(s, Ss, Zs) T = np.interp(s, Ss, Ts) + # <<< is this function used for anything? Does it make sense? + return X, Y, Z, T - def drawLine2d(self, Time, ax, color="k", Xuvec=[1,0,0], Yuvec=[0,0,1], Xoff=0, Yoff=0, colortension=False, cmap='rainbow', plotnodes=[], plotnodesline=[], label="", alpha=1.0): + def drawLine2d(self, Time, ax, color="k", Xuvec=[1,0,0], Yuvec=[0,0,1], Xoff=0, Yoff=0, colortension=False, cmap='rainbow', plotnodes=[], plotnodesline=[], label="", alpha=1.0,linewidth=1): '''Draw the line on 2D plot (ax must be 2D) Parameters @@ -439,6 +434,8 @@ def drawLine2d(self, Time, ax, color="k", Xuvec=[1,0,0], Yuvec=[0,0,1], Xoff=0, toggle to plot the lines in a colormap based on node tensions. The default is False cmap : string, optional colormap string type to plot tensions when colortension=True. The default is 'rainbow' + linewidth: float, optional + sets the mooring lines linewidth in matplotlib. default is 1 Returns ------- @@ -449,10 +446,10 @@ def drawLine2d(self, Time, ax, color="k", Xuvec=[1,0,0], Yuvec=[0,0,1], Xoff=0, linebit = [] # make empty list to hold plotted lines, however many there are + Xs, Ys, Zs, Ts = self.getLineCoords(Time) + if self.isRod > 0: - Xs, Ys, Zs, Te = self.getLineCoords(Time) - # apply any 3D to 2D transformation here to provide desired viewing angle Xs2d = Xs*Xuvec[0] + Ys*Xuvec[1] + Zs*Xuvec[2] Ys2d = Xs*Yuvec[0] + Ys*Yuvec[1] + Zs*Yuvec[2] @@ -464,28 +461,23 @@ def drawLine2d(self, Time, ax, color="k", Xuvec=[1,0,0], Yuvec=[0,0,1], Xoff=0, # drawing lines... else: - # >>> can probably streamline the next bit of code a fair bit <<< - if self.qs==1: - Xs, Ys, Zs, tensions = self.getLineCoords(Time) - elif self.qs==0: - Xs, Ys, Zs, Ts = self.getLineCoords(Time) + if self.qs==0: self.rA = np.array([Xs[0], Ys[0], Zs[0]]) self.rB = np.array([Xs[-1], Ys[-1], Zs[-1]]) - tensions = self.getLineTens() # apply any 3D to 2D transformation here to provide desired viewing angle Xs2d = Xs*Xuvec[0] + Ys*Xuvec[1] + Zs*Xuvec[2] + Xoff Ys2d = Xs*Yuvec[0] + Ys*Yuvec[1] + Zs*Yuvec[2] + Yoff if colortension: # if the mooring lines want to be plotted with colors based on node tensions - maxt = np.max(tensions); mint = np.min(tensions) + maxT = np.max(Ts); minT = np.min(Ts) for i in range(len(Xs)-1): # for each node in the line - color_ratio = ((tensions[i] + tensions[i+1])/2 - mint)/(maxt - mint) # ratio of the node tension in relation to the max and min tension + color_ratio = ((Ts[i] + Ts[i+1])/2 - minT)/(maxT - minT) # ratio of the node tension in relation to the max and min tension cmap_obj = cm.get_cmap(cmap) # create a cmap object based on the desired colormap rgba = cmap_obj(color_ratio) # return the rbga values of the colormap of where the node tension is linebit.append(ax.plot(Xs2d[i:i+2], Ys2d[i:i+2], color=rgba)) else: - linebit.append(ax.plot(Xs2d, Ys2d, lw=1, color=color, label=label, alpha=alpha)) # previously had lw=1 (linewidth) + linebit.append(ax.plot(Xs2d, Ys2d, lw=linewidth, color=color, label=label, alpha=alpha)) # previously had lw=1 (linewidth) if len(plotnodes) > 0: for i,node in enumerate(plotnodes): @@ -532,18 +524,17 @@ def drawLine(self, Time, ax, color="k", endpoints=False, shadow=True, colortensi if color == 'self': color = self.color # attempt to allow custom colors lw = self.lw + elif color == None: + color = [0.3, 0.3, 0.3] # if no color, default to grey + lw = 1 else: lw = 1 linebit = [] # make empty list to hold plotted lines, however many there are - + + Xs, Ys, Zs, tensions = self.getLineCoords(Time) + if self.isRod > 0: - - if color==None: - color = [0.3, 0.3, 0.3] # if no color provided, default to dark grey rather than rainbow rods - - Xs, Ys, Zs, Ts = self.getLineCoords(Time) - for i in range(int(len(Xs)/2-1)): linebit.append(ax.plot(Xs[2*i:2*i+2],Ys[2*i:2*i+2],Zs[2*i:2*i+2] , color=color)) # side edges linebit.append(ax.plot(Xs[[2*i,2*i+2]],Ys[[2*i,2*i+2]],Zs[[2*i,2*i+2]] , color=color)) # end A edges @@ -555,19 +546,14 @@ def drawLine(self, Time, ax, color="k", endpoints=False, shadow=True, colortensi # drawing lines... else: - # >>> can probably streamline the next bit of code a fair bit <<< - if self.qs==1: # returns the node positions and tensions of the line, doesn't matter what time - Xs, Ys, Zs, tensions = self.getLineCoords(Time) - elif self.qs==0: # returns the node positions and time data at the given time - Xs, Ys, Zs, Ts = self.getLineCoords(Time) + if self.qs==0: self.rA = np.array([Xs[0], Ys[0], Zs[0]]) self.rB = np.array([Xs[-1], Ys[-1], Zs[-1]]) - tensions = self.getLineTens() if colortension: # if the mooring lines want to be plotted with colors based on node tensions - maxt = np.max(tensions); mint = np.min(tensions) + maxT = np.max(tensions); minT = np.min(tensions) for i in range(len(Xs)-1): # for each node in the line - color_ratio = ((tensions[i] + tensions[i+1])/2 - mint)/(maxt - mint) # ratio of the node tension in relation to the max and min tension + color_ratio = ((tensions[i] + tensions[i+1])/2 - minT)/(maxT - minT) # ratio of the node tension in relation to the max and min tension cmap_obj = cm.get_cmap(cmap_tension) # create a cmap object based on the desired colormap rgba = cmap_obj(color_ratio) # return the rbga values of the colormap of where the node tension is linebit.append(ax.plot(Xs[i:i+2], Ys[i:i+2], Zs[i:i+2], color=rgba, zorder=100)) @@ -607,9 +593,6 @@ def drawLine(self, Time, ax, color="k", endpoints=False, shadow=True, colortensi return linebit - - - def redrawLine(self, Time, colortension=False, cmap_tension='rainbow', drawU=True): #, linebit): '''Update 3D line drawing based on instantaneous position''' @@ -636,12 +619,11 @@ def redrawLine(self, Time, colortension=False, cmap_tension='rainbow', drawU=Tru if colortension: self.rA = np.array([Xs[0], Ys[0], Zs[0]]) # update the line ends based on the MoorDyn data self.rB = np.array([Xs[-1], Ys[-1], Zs[-1]]) - tensions = self.getLineTens() # get the tensions of the line calculated quasi-statically - maxt = np.max(tensions); mint = np.min(tensions) + maxT = np.max(Ts); minT = np.min(Ts) cmap_obj = cm.get_cmap(cmap_tension) # create the colormap object for i in range(len(Xs)-1): # for each node in the line, find the relative tension of the segment based on the max and min tensions - color_ratio = ((tensions[i] + tensions[i+1])/2 - mint)/(maxt - mint) + color_ratio = ((Ts[i] + Ts[i+1])/2 - minT)/(maxT - minT) rgba = cmap_obj(color_ratio) linebit[i][0]._color = rgba # set the color of the segment to a new color based on its updated tension linebit[i][0].set_data(Xs[i:i+2],Ys[i:i+2]) # set the x and y coordinates @@ -700,9 +682,9 @@ def setEndPosition(self, r, endB): ''' if endB == 1: - self.rB = np.array(r, dtype=np.float_) + self.rB = np.array(r, dtype=float) elif endB == 0: - self.rA = np.array(r, dtype=np.float_) + self.rA = np.array(r, dtype=float) else: raise LineError("setEndPosition: endB value has to be either 1 or 0") @@ -732,215 +714,198 @@ def staticSolve(self, reset=False, tol=0.0001, profiles=0): ''' - depth = self.sys.depth - - dr = self.rB - self.rA - LH = np.hypot(dr[0], dr[1]) # horizontal spacing of line ends - LV = dr[2] # vertical offset from end A to end B - if LH >0: - cosBeta = dr[0]/LH # cos of line heading - sinBeta = dr[1]/LH # sin of line heading - self.th = np.arctan2(dr[1],dr[0]) # line heading - else: # special case of vertical line: line heading is undefined - use zero as default - cosBeta = 0.0 - sinBeta = 0.0 - self.th = 0.0 - - if self.rA[2] < -depth: - raise LineError("Line {} end A is lower than the seabed.".format(self.number)) - elif self.rB[2] < -depth: - raise LineError("Line {} end B is lower than the seabed.".format(self.number)) - elif np.min([self.rA[2],self.rB[2]]) > -depth: - self.cb = -depth - np.min([self.rA[2],self.rB[2]]) # if this line's lower end is off the seabed, set cb negative and to the distance off the seabed - elif self.cb < 0: # if a line end is at the seabed, but the cb is still set negative to indicate off the seabed - self.cb = 0.0 # set to zero so that the line includes seabed interaction. - - - if self.HF < 0: # or self.VF < 0: <<<<<<<<<<< it shouldn't matter if VF is negative - this could happen for buoyant lines, etc. + # deal with horizontal tension starting point + if self.HF < 0: raise LineError("Line HF cannot be negative") # this could be a ValueError too... if reset==True: # Indicates not to use previous fairlead force values to start catenary self.HF = 0 # iteration with, and insteady use the default values. - - # ----- get line results for linear or nonlinear elasticity ----- - #If EA is found in the line properties we will run the original catenary function - if 'EA' in self.type: - try: - (fAH, fAV, fBH, fBV, info) = catenary(LH, LV, self.L, self.type['EA'], self.type['w'], CB=self.cb, Tol=tol, HF0=self.HF, VF0=self.VF, plots=profiles) # call line model - - except CatenaryError as error: - raise LineError(self.number, error.message) - #If EA isnt found then we will use the ten-str relationship defined in the input file + + # ensure line profile information is computed if needed for computing current loads + if self.sys.currentMod == 1 and profiles == 0: + profiles = 1 + + # get seabed depth and slope under each line end + depthA, nvecA = self.sys.getDepthFromBathymetry(self.rA[0], self.rA[1]) + depthB, nvecB = self.sys.getDepthFromBathymetry(self.rB[0], self.rB[1]) + + # deal with height off seabed issues + if self.rA[2] < -depthA: + self.rA[2] = -depthA + self.cb = 0 + #raise LineError("Line {} end A is lower than the seabed.".format(self.number)) <<< temporarily adjust to seabed depth + elif self.rB[2] < -depthB: + raise LineError("Line {} end B is lower than the seabed.".format(self.number)) else: - (fAH, fAV, fBH, fBV, info) = nonlinear(LH, LV, self.L, self.type['Str'], self.type['Ten'],self.type['w']) - - self.HF = info["HF"] - self.VF = info["VF"] - self.KA2 = info["stiffnessA"] - self.KB2 = info["stiffnessB"] - self.LBot = info["LBot"] - self.info = info - - self.fA[0] = fAH*cosBeta - self.fA[1] = fAH*sinBeta - self.fA[2] = fAV - self.fB[0] = fBH*cosBeta - self.fB[1] = fBH*sinBeta - self.fB[2] = fBV - self.TA = np.sqrt(fAH*fAH + fAV*fAV) # end tensions - self.TB = np.sqrt(fBH*fBH + fBV*fBV) + self.cb = -depthA - self.rA[2] # when cb < 0, -cb is defined as height of end A off seabed (in catenary) + - # ----- compute 3d stiffness matrix for both line ends (3 DOF + 3 DOF) ----- + # ----- Perform rotation/transformation to 2D plane of catenary ----- - # solve for required variables to set up the perpendicular stiffness. Keep it horizontal - #L_xy = np.linalg.norm(self.rB[:2] - self.rA[:2]) - #T_xy = np.linalg.norm(self.fB[:2]) + dr = self.rB - self.rA - # create the rotation matrix based on the heading angle that the line is from the horizontal - R = rotationMatrix(0,0,self.th) + # if a current force is present, include it in the catenary solution + if np.sum(np.abs(self.fCurrent)) > 0: - # initialize the line's analytic stiffness matrix in the "in-line" plane then rotate the matrix to be about the global frame [K'] = [R][K][R]^T - def from2Dto3Drotated(K2D, F, L): - if L > 0: - Kt = F/L # transverse stiffness term - else: - Kt = 0.0 + # total line exernal force per unit length vector (weight plus current drag) + w_vec = self.fCurrent/self.L + np.array([0, 0, -self.type["w"]]) + w = np.linalg.norm(w_vec) + w_hat = w_vec/w - K2 = np.array([[K2D[0,0], 0 , K2D[0,1]], - [ 0 , Kt, 0 ], - [K2D[1,0], 0 , K2D[1,1]]]) - return np.matmul(np.matmul(R, K2), R.T) + # >>> may need to adjust to handle case of buoyant vertical lines <<< + # get rotation matrix from gravity down to w_vec being down + if w_hat[0] == 0 and w_hat[1] == 0: + if w_hat[2] < 0: + R_curr = np.eye(3,3) + else: + R_curr = -np.eye(3,3) + else: + R_curr = RotFrm2Vect(w_hat, np.array([0, 0, -1])) # rotation matrix to make w vertical - self.KA = from2Dto3Drotated(info['stiffnessA'], -fBH, LH) # stiffness matrix describing reaction force on end A due to motion of end A - self.KB = from2Dto3Drotated(info['stiffnessB'], -fBH, LH) # stiffness matrix describing reaction force on end B due to motion of end B - self.KAB = from2Dto3Drotated(info['stiffnessAB'], fBH, LH) # stiffness matrix describing reaction force on end B due to motion of end A - - #self.K6 = np.block([[ from2Dto3Drotated(self.KA), from2Dto3Drotated(self.KAB.T)], - # [ from2Dto3Drotated(self.KAB), from2Dto3Drotated(self.KB) ]]) + # vector from A to B needs to be put into the rotated frame + dr = np.matmul(R_curr, dr) + # if no current force, things are simple + else: + R_curr = np.eye(3,3) + w = self.type["w"] - if profiles > 1: - import matplotlib.pyplot as plt - plt.plot(info['X'], info['Z']) - plt.show() + # apply a rotation about Z' to align the line profile with the X'-Z' plane + theta_z = -np.arctan2(dr[1], dr[0]) + R_z = rotationMatrix(0, 0, theta_z) - + # overall rotation matrix (global to catenary plane) + R = np.matmul(R_z, R_curr) - def getEndForce(self, endB): - '''Returns the force of the line at the specified end based on the endB value - - Parameters - ---------- - endB : boolean - An indicator of which end of the line is the force wanted - - Raises - ------ - LineError - If the given endB value is not a 1 or 0 - - Returns - ------- - fA or fB: array - The force vector at the end of the line - - ''' + # figure out slope in plane (only if contacting the seabed) + if self.rA[2] <= -depthA or self.rB[2] <= -depthB: + nvecA_prime = np.matmul(R, nvecA) - if endB == 1: - return self.fB - elif endB == 0: - return self.fA - else: - raise LineError("getEndForce: endB value has to be either 1 or 0") - - - - def getStiffnessMatrix(self): - '''Returns the stiffness matrix of a line derived from analytic terms in the jacobian of catenary - - Raises - ------ - LineError - If a singluar matrix error occurs while taking the inverse of the Line's Jacobian matrix. - - Returns - ------- - K2_rot : matrix - the analytic stiffness matrix of the line in the rotated frame. - - ''' - - # take the inverse of the Jacobian to get the starting analytic stiffness matrix - ''' - if np.isnan(self.jacobian[0,0]): #if self.LBot >= self.L and self.HF==0. and self.VF==0. << handle tricky cases here? - K = np.array([[0., 0.], [0., 1.0/self.jacobian[1,1] ]]) + dz_dx = -nvecA_prime[0]*(1.0/nvecA_prime[2]) # seabed slope components + dz_dy = -nvecA_prime[1]*(1.0/nvecA_prime[2]) # seabed slope components + # we only care about dz_dx since the line is in the X-Z plane in this rotated situation + alpha = np.degrees(np.arctan(dz_dx)) + cb = self.cb else: - try: - K = np.linalg.inv(self.jacobian) - except: - raise LineError(self.number, f"Check Line Length ({self.L}), it might be too long, or check catenary ProfileType") - ''' - - # solve for required variables to set up the perpendicular stiffness. Keep it horizontal - L_xy = np.linalg.norm(self.rB[:2] - self.rA[:2]) - T_xy = np.linalg.norm(self.fB[:2]) - Kt = T_xy/L_xy + if np.sum(np.abs(self.fCurrent)) > 0 or nvecA[2] < 1: # if there is current or seabed slope + alpha = 0 + cb = min(0, dr[2]) - 100 # put the seabed out of reach (model limitation) + else: # otherwise proceed as usual (this is the normal case) + alpha = 0 + cb = self.cb - # initialize the line's analytic stiffness matrix in the "in-line" plane - KA = np.array([[self.KA2[0,0], 0 , self.KA2[0,1]], - [ 0 , Kt, 0 ], - [self.KA2[1,0], 0 , self.KA2[1,1]]]) - - KB = np.array([[self.KB2[0,0], 0 , self.KB2[0,1]], - [ 0 , Kt, 0 ], - [self.KB2[1,0], 0 , self.KB2[1,1]]]) + # horizontal and vertical dimensions of line profile (end A to B) + LH = np.linalg.norm(dr[:2]) + LV = dr[2] - # create the rotation matrix based on the heading angle that the line is from the horizontal - R = rotationMatrix(0,0,self.th) - # rotate the matrix to be about the global frame [K'] = [R][K][R]^T - KA_rot = np.matmul(np.matmul(R, KA), R.T) - KB_rot = np.matmul(np.matmul(R, KB), R.T) - - return KA_rot, KB_rot - - - def getLineTens(self): - '''Calls the catenary function to return the tensions of the Line for a quasi-static analysis''' - - # >>> this can probably be done using data already generated by static Solve <<< - - depth = self.sys.depth - - dr = self.rB - self.rA - LH = np.hypot(dr[0], dr[1]) # horizontal spacing of line ends - LV = dr[2] # vertical offset from end A to end B + # ----- call catenary function or alternative and save results ----- - if np.min([self.rA[2],self.rB[2]]) > -depth: - self.cb = -depth - np.min([self.rA[2],self.rB[2]]) # if this line's lower end is off the seabed, set cb negative and to the distance off the seabed - elif self.cb < 0: # if a line end is at the seabed, but the cb is still set negative to indicate off the seabed - self.cb = 0.0 # set to zero so that the line includes seabed interaction. - - tol = 0.0001 - #If EA is found in the line properties we will run the original catenary function if 'EA' in self.type: try: - tol = 0.000001 #TODO figure out why tol and profiles are not defined. These values are hardcoded from defaults in other function calls - profiles = 1 - (fAH, fAV, fBH, fBV, info) = catenary(LH, LV, self.L, self.type['EA'], self.type['w'], CB=self.cb, Tol=tol, HF0=self.HF, VF0=self.VF, plots=profiles) # call line model - + (fAH, fAV, fBH, fBV, info) = catenary(LH, LV, self.L, self.EA, + w, CB=cb, alpha=alpha, HF0=self.HF, VF0=self.VF, Tol=tol, + nNodes=self.nNodes, plots=profiles, depth=self.sys.depth) + except CatenaryError as error: - raise LineError(self.number, error.message) + raise LineError(self.number, error.message) #If EA isnt found then we will use the ten-str relationship defined in the input file else: - (fAH, fAV, fBH, fBV, info) = nonlinear(LH, LV, self.L, self.type['Str'], self.type['Ten'],self.type['w']) + (fAH, fAV, fBH, fBV, info) = nonlinear(LH, LV, self.L, self.type['Str'], self.type['Ten'],np.linalg.norm(w)) + + + # save line profile coordinates in global frame (involves inverse rotation) + if profiles > 0: + # note: instantiating new arrays rather than writing directly to self.Xs + # seems to be necessary to avoid plots auto-updating to the current + # profile of the Line object. + Xs = np.zeros(self.nNodes) + Ys = np.zeros(self.nNodes) + Zs = np.zeros(self.nNodes) + # apply inverse rotation to node positions + for i in range(0,self.nNodes): + temp_array = np.array([info['X'][i], 0 ,info['Z'][i]]) + unrot_pos = np.matmul(temp_array, R) + + Xs[i] = self.rA[0] + unrot_pos[0] + Ys[i] = self.rA[1] + unrot_pos[1] + Zs[i] = self.rA[2] + unrot_pos[2] - Ts = info["Te"] - return Ts + self.Xs = Xs + self.Ys = Ys + self.Zs = Zs + self.Ts = info["Te"] + + # save fairlead tension components for use as ICs next iteration + self.HF = info["HF"] + self.VF = info["VF"] + + # save other important info + self.LBot = info["LBot"] + self.z_extreme = self.rA[2] + info["Zextreme"] + self.info = info + + # save forces in global reference frame + self.fA = np.matmul(np.array([fAH, 0, fAV]), R) + self.fB = np.matmul(np.array([fBH, 0, fBV]), R) + self.TA = np.linalg.norm(self.fA) # end tensions + self.TB = np.linalg.norm(self.fB) + + # Compute transverse (out-of-plane) stiffness term + if LH < 0.01*abs(LV): # if line is nearly vertical (note: this theshold is unverified) + Kt = 0.5*(fAV-fBV)/LV # compute Kt based on vertical tension/span + else: # otherwise use the classic horizontal approach + Kt = -fBH/LH + + + # save 3d stiffness matrix in global orientation for both line ends (3 DOF + 3 DOF) + self.KA = from2Dto3Drotated(info['stiffnessA'], Kt, R.T) # reaction at A due to motion of A + self.KB = from2Dto3Drotated(info['stiffnessB'], Kt, R.T) # reaction at B due to motion of B + self.KBA = from2Dto3Drotated(info['stiffnessBA'],-Kt, R.T) # reaction at B due to motion of A + + + # ----- calculate current loads if applicable, for use next time ----- + + if self.sys.currentMod == 1: + + U = self.sys.current # 3D current velocity [m/s] (could be changed to depth-dependent profile) + + fCurrent = np.zeros(3) # total current force on line in x, y, z [N] + + # Loop through each segment along the line and add up the drag forces. + # This is in contrast to MoorDyn calculating for nodes. + for i in range(self.nNodes-1): + #For each segment find the tangent vector and then calculate the current loading + dr_seg = np.array([self.Xs[i+1] - self.Xs[i], + self.Ys[i+1] - self.Ys[i], + self.Zs[i+1] - self.Zs[i]]) # segment vector + ds_seg = np.linalg.norm(dr_seg) + + if ds_seg > 0: # only include if segment length > 0 + q = dr_seg/ds_seg + # transverse and axial current velocity components + Uq = np.dot(U, q) * q + Up = U - Uq + # transverse and axial drag forces on segment + dp = 0.5*self.sys.rho*self.type["Cd"] *self.type["d_vol"]*ds_seg*np.linalg.norm(Up)*Up + dq = 0.5*self.sys.rho*self.type["CdAx"]*np.pi*self.type["d_vol"]*ds_seg*np.linalg.norm(Uq)*Uq + # add to total current force on line + fCurrent += dp + dq + + self.fCurrent = fCurrent # save for use next call + else: + self.fCurrent = np.zeros(3) # if no current, ensure this force is zero + + + # ----- plot the profile if requested ----- + if profiles > 1: + import matplotlib.pyplot as plt + plt.plot(self.info['X'], self.info['Z']) + plt.show() def getTension(self, s): @@ -1001,9 +966,89 @@ def getPosition(self, s): Zs = self.rA[2] + z return np.vstack([ Xs, Ys, Zs]) + + + def getCost(self): + '''Fill in a cost dictionary and return the total cost for this Line object.''' + self.cost = {} # clear any old cost numbers + self.cost['material'] = self.type['cost']*self.L0 + total_cost = sum(self.cost.values()) + return total_cost + def attachLine(self, lineID, endB): pass + + def activateDynamicStiffness(self, display=0): + '''Switch mooring line model to dynamic line stiffness value, + including potential unstretched line length adjustment, taking the + current state as the mean/static offset position to work from. + This only works when dynamic line properties are used.''' + + if self.type['EAd'] > 0: + # switch to dynamic stiffness value + EA_old = self.type['EA'] + EA_new = self.type['EAd'] + self.type['EAd_Lm']*np.mean([self.TA, self.TB]) # this implements the sloped Krd = alpha + beta*Lm + self.EA = np.max([EA_new, EA_old]) # only if the dynamic stiffness is higher than the static stiffness, activate the dynamic stiffness + + # adjust line length to maintain current tension (approximate) + self.L = self.L0 * (1 + self.TB/EA_old)/(1 + self.TB/EA_new) + + else: + if display > 0: + print(f'Line {self.number} has zero dynamic stiffness coefficient so activateDynamicStiffness does nothing.') + + def revertToStaticStiffness(self): + '''Switch mooring line model to dynamic line stiffness + values, including potential unstretched line length + adjustment. This only works when dynamic line properties + are used.''' + + # switch to static/default stiffness value + self.EA = self.type['EA'] + + # revert to original line length + self.L = self.L0 + + +def from2Dto3Drotated(K2D, Kt, R): + '''Initialize a line end's analytic stiffness matrix in the + plane of the catenary then rotate the matrix to be about the + global frame using [K'] = [R][K][R]^T + Parameters + ---------- + K2D : 2x2 matrix + Planar stiffness matrix of line end [N/m] + Kt : float + Transverse (out-of-plane) stiffness term [N/m]. + R : 3x3 matrix + Rotation matrix from global frame to the local + X-Z plane of the line + + Returns + ------- + 3x3 stiffness matrix in global orientation [N/m]. + ''' + K2 = np.array([[K2D[0,0], 0 , K2D[0,1]], + [ 0 , Kt, 0 ], + [K2D[1,0], 0 , K2D[1,1]]]) + + return np.matmul(np.matmul(R, K2), R.T) + + +def RotFrm2Vect( A, B): + '''Rodriguez rotation function, which returns the rotation matrix + that transforms vector A into Vector B. + ''' + + v = np.cross(A,B) + ssc = np.array([[0, -v[2], v[1]], + [v[2], 0, -v[0]], + [-v[1], v[0], 0]]) + + R = np.eye(3,3) + ssc + np.matmul(ssc,ssc)*(1-np.dot(A,B))/(np.linalg.norm(v)*np.linalg.norm(v)) + + return R \ No newline at end of file diff --git a/moorpy/point.py b/moorpy/point.py index ad88529..3032a8b 100644 --- a/moorpy/point.py +++ b/moorpy/point.py @@ -2,8 +2,6 @@ import numpy as np - - class Point(): '''A class for any object in the mooring system that can be described by three translational coorindates''' @@ -49,8 +47,11 @@ def __init__(self, mooringSys, num, type, r, m=0, v=0, fExt=np.zeros(3), DOFs=[0 self.number = num self.type = type # 1: fixed/attached to something, 0 free to move, or -1 coupled externally - self.r = np.array(r, dtype=np.float_) - + self.r = np.array(r, dtype=float) + self.entity = {type:''} # dict for entity (e.g. anchor) info + self.cost = {} # empty dictionary to contain cost info + self.loads = {} # empty dictionary to contain load info + self.m = float(m) self.v = float(v) self.CdA= float(CdA) @@ -67,6 +68,8 @@ def __init__(self, mooringSys, num, type, r, m=0, v=0, fExt=np.zeros(3), DOFs=[0 self.attached = [] # ID numbers of any Lines attached to the Point self.attachedEndB = [] # specifies which end of the line is attached (1: end B, 0: end A) + + self.cable = False # specifies whether the point should be modeled as a Rod (for dynamic cables) or not if len(zSpan)==2: self.zSpan = np.array(zSpan, dtype=float) @@ -85,11 +88,6 @@ def attachLine(self, lineID, endB): The identifier ID number of a line endB : boolean Determines which end of the line is attached to the point - - Returns - ------- - None. - ''' self.attached.append(lineID) @@ -105,15 +103,16 @@ def detachLine(self, lineID, endB): The identifier ID number of a line endB : boolean Determines which end of the line is to be detached from the point - - Returns - ------- - None. - ''' - self.attached.pop(self.attached.index(lineID)) - self.attachedEndB.pop(self.attachedEndB.index(endB)) + # get attachment index + i1 = self.attached.index(lineID) + i2 = self.attachedEndB.index(endB) + if not i1==i2: + raise Exception("issue with the right end of the line to detach...") + + self.attached.pop(i1) + self.attachedEndB.pop(i1) print("detached Line "+str(lineID)+" from Point "+str(self.number)) @@ -145,8 +144,10 @@ def setPosition(self, r): raise ValueError(f"Point setPosition method requires an argument of size 3 or nDOF, but size {len(r):d} was provided") # update the point's depth and position based on relation to seabed - self.zSub = np.max([-self.zTol, -self.r[2] - self.sys.depth]) # depth of submergence in seabed if > -zTol - self.r = np.array([self.r[0], self.r[1], np.max([self.r[2], -self.sys.depth])]) # don't let it sink below the seabed + depth, _ = self.sys.getDepthFromBathymetry(self.r[0], self.r[1]) + + self.zSub = np.max([-self.zTol, -self.r[2] - depth]) # depth of submergence in seabed if > -zTol + self.r = np.array([self.r[0], self.r[1], np.max([self.r[2], -depth])]) # don't let it sink below the seabed # update the position of any attached Line ends for LineID,endB in zip(self.attached,self.attachedEndB): @@ -208,7 +209,11 @@ def getForces(self, lines_only=False, seabed=True, xyz=False): # add forces from attached lines for LineID,endB in zip(self.attached,self.attachedEndB): - f += self.sys.lineList[LineID-1].getEndForce(endB) + # f += self.sys.lineList[LineID-1].getEndForce(endB) + if endB: + f += self.sys.lineList[LineID-1].fB + else: + f += self.sys.lineList[LineID-1].fA if xyz: return f @@ -326,13 +331,38 @@ def getStiffnessA(self, lines_only=False, xyz=False): # if on seabed, apply a large stiffness to help out system equilibrium solve (if it's transitioning off, keep it a small step to start with) if self.r[2] == -self.sys.depth: K[2,2] += 1.0e12 - + if sum(np.isnan(K).ravel()) > 0: breakpoint() if xyz: # if asked to output all DOFs, do it return K else: # otherwise only return rows/columns of active DOFs return K[:,self.DOFs][self.DOFs,:] + + def getCost(self): + '''Fill in and returns a cost dictionary for this Point object. + So far it only applies for if the point is an anchor.''' + + from moorpy.MoorProps import getAnchorCost + + self.cost = {'material':0} # clear any old cost numbers and start with 0 + + # figure out if it should be an anchor if it isn't already defined + if self.entity['type'] == '': + depth, _ = self.sys.getDepthFromBathymetry(self.r[0], self.r[1]) + if self.r[3] == depth and self.type==1: # if it's fixed on the seabed + self.entity['type'] = 'anchor' # assume it's an anchor + if self.FA[2] == 0: + self.entity['anchor_type'] = 'drag-embedment' + else: + self.entity['anchor_type'] = 'suction' + + # calculate costs if it's an anchor (using simple model) + if self.entity['type'] == 'anchor': + self.cost['material'] = getAnchorCost(self.loads['fx_max'], + self.loads['fz_max'], + type=self.entity['anchor_type']) + return cost diff --git a/moorpy/subsystem.py b/moorpy/subsystem.py new file mode 100644 index 0000000..a38f614 --- /dev/null +++ b/moorpy/subsystem.py @@ -0,0 +1,730 @@ +import numpy as np +import yaml + +from moorpy.system import System +from moorpy.body import Body +from moorpy.point import Point +from moorpy.line import Line, from2Dto3Drotated +from moorpy.lineType import LineType +from moorpy.helpers import (rotationMatrix, rotatePosition, getH, printVec, + set_axes_equal, dsolve2, SolveError, MoorPyError, + loadLineProps, getLineProps, read_mooring_file, + printMat, printVec, getInterpNums, unitVector, + getFromDict, addToDict) + + + +class Subsystem(System, Line): + '''A class for a mooring line or dynamic cable subsystem. + It includes Line sections but also can fit into a larger System + the same way a Line can. + + A subsystem tracks its own objects in its local coordinate system. + This local coordinate system puts the x axis along the line heading, + from anchor to fairlead, with the anchor at x=0. + + For a multi-section line or cable (the main reason to use a SubSystem), + the free DOFs of the points are contained in the SubSystem and are not + seen by the parent System. Solving their equilibrium is an internal + solve nested within the larger System's equilibrium solve. + + The first and last points in the subsystem are set as coupled so that + their stiffness matrices can be computed and passed back the same way as + those of a line's two ends. + + Line types should be referenced from the overall System... [how?] + + Key adjustments: replace staticSolve from Line to do an internal + equilibrium solve using System.solveEquilibrium. + ''' + + + def __init__(self, mooringSys=None, num=0, depth=0, rho=1025, g=9.81, + lineProps=None, **kwargs): + '''Shortened initializer for just the SubSystem aspects. + + How should this be initialized??? + ''' + + # some basics for compatibility with Line objects when used in a System lineList + self.sys = mooringSys # store a reference to the overall mooring system (instance of System class) + self.number = num + self.isRod = False + self.type = {} # because Line objects have a type dict... + + # lists to hold mooring system objects + self.bodyList = [] + self.rodList = [] + self.pointList = [] + self.lineList = [] + self.lineTypes = {} # dict indexed as a list [0:n-1] corresponding to lineList. Contains *references* to lineType dicts. + self.rodTypes = {} + + # some basic subsystem things + self.rA = np.zeros(3) + self.rB = np.zeros(3) + self.LH = 0 + self.th = 0 + self.sin_th = 0 + self.cos_th = 1 + + # load mooring line property scaling coefficients for easy use when creating line types + self.lineProps = loadLineProps(lineProps) + + # the ground body (number 0, type 1[fixed]) never moves but is the parent of all anchored things + self.groundBody = Body(self, 0, 1, np.zeros(6)) + + # constants used in the analysis + self.depth = depth # water depth [m] + self.rho = rho # water density [kg/m^3] + self.g = g # gravitational acceleration [m/s^2] + + # Set position tolerance to use in equilibrium solves [m] + self.eqtol = getFromDict(kwargs, 'eqtol', default=0.01) + + # water current - currentMod 0 = no current; 1 = steady uniform current + self.currentMod = 0 # flag for current model to use + self.current = np.zeros(3) # current velocity vector [m/s] + if 'current' in kwargs: + self.currentMod = 1 + self.current = getFromDict(kwargs, 'current', shape=3) + + # flag to indicate shared line or suspended cable being modeled as symmetric + self.shared = getFromDict(kwargs, 'shared', dtype=bool, default=False) + + self.span = getFromDict(kwargs, 'span', default=0) # horizontal end-end distance [m] + self.rad_fair = getFromDict(kwargs, 'rad_fair', default=0) # [m] fairlead radius [m] + self.z_fair = getFromDict(kwargs, 'z_fair' , default=0) # [m] fairlead z coord [m] + + # the old rAFair input for the position of a second fairlead of a shaerd mooring line is gone + # currently we assume rad_fair and z_fair are same for both ends of a shared line... + + # seabed bathymetry - seabedMod 0 = flat; 1 = uniform slope, 2 = grid + self.seabedMod = 0 + + if 'xSlope' in kwargs or 'ySlope' in kwargs: + self.seabedMod = 1 + self.xSlope = getFromDict(kwargs, 'xSlope', default=0) + self.ySlope = getFromDict(kwargs, 'ySlope', default=0) + + if 'bathymetry' in kwargs: + self.seabedMod = 2 + self.bathGrid_Xs, self.bathGrid_Ys, self.bathGrid = self.readBathymetryFile(kwargs['bathymetry']) + # Note, System and Subsystem bathymetry can be set after creation + # by setBathymetry, which uses link to existing data, for efficiency. + + # initializing variables and lists + self.nDOF = 0 # number of (free) degrees of freedom of the mooring system (needs to be set elsewhere) + self.freeDOFs = [] # array of the values of the free DOFs of the system at different instants (2D list) + + self.nCpldDOF = 0 # number of (coupled) degrees of freedom of the mooring system (needs to be set elsewhere) + self.cpldDOFs = [] # array of the values of the coupled DOFs of the system at different instants (2D list) + + self.display = 0 # a flag that controls how much printing occurs in methods within the System (Set manually. Values > 0 cause increasing output.) + + self.MDoptions = {} # dictionary that can hold any MoorDyn options read in from an input file, so they can be saved in a new MD file if need be + self.qs = 1 # flag that it's a MoorPy analysis, so System methods don't complain. One day should replace this <<< + + + def initialize(self, daf_dict={}): + '''Initialize the Subsystem including DAFs.''' + + # Count number of line sections and total number of nodes + self.nLines = len(self.lineList) + self.nNodes = np.sum([l.nNodes for l in self.lineList]) - self.nLines + 1 + + # Use the System initialize method + System.initialize(self) + + # dynamic amplication factor for each line section, and anchor forces + # (DAFS[-2] is for vertical load, DAFS[-1] is for horizontal load) + self.DAFs = getFromDict(daf_dict, 'DAFs', shape=self.nLines+2, default=1.0) + + # mean tension [N] of each line section end [section #, end A/B] for any given mean offset + self.Te0 = np.zeros([self.nLines,2]) # undispalced values + self.TeM = np.zeros([self.nLines,2]) # mean offset values + self.TeD = np.zeros([self.nLines,2]) # dynamic values + + # adjustment on laylength... positive means that the dynamic lay length is greater than linedesign laylength + self.LayLen_adj = getFromDict(daf_dict, 'LayLen_adj', shape=0, default=0.0) + + # Fatigue damage over all nodes (placeholder for now) + self.damage = np.zeros(self.nNodes) + + + def makeGeneric(self, lengths, types, connectors=[], suspended=0): + + '''Creates a cable of n components going between an anchor point and + a floating body (or a bridle point). If shared, it goes between two + floating bodies. + + Parameters + ---------- + lengths : list of floats + List of each section's length. This also implies the number of + sections. + types : list of strings or dicts + List of lineType names or dicts for each section. If strings, + these names must match keys in the parent system lineTypes + dictionary or the subsystem's lineTypes dictionary. If dicts, + these dicts are referred to for each lineType (by reference). + connectors : list of dicts + List of length nLines-1 with dicts of optional properties for + any interior points (between sections). + suspended : int + Selector shared/suspended cases: + - 0 (default): end A is on the seabed, + - 1: the assembly is suspended and end A is at another floating system, + - 2: the assembly is suspended and assumed symmetric, end A is the midpoint. + ''' + + # some initialization steps. + self.nLines = len(lengths) + if len(connectors) == 0: + connectors = [{}]*(self.nLines - 1) + elif not len(connectors) == self.nLines - 1: + raise Exception('Length of connectors must be nLines - 1') + + if not len(types)==self.nLines: + raise Exception("The specified number of lengths and types is inconsistent.") + + # get cumulative sum of line lengths, starting from anchor segment + Lcsum = np.cumsum(np.array(lengths)) + + # set end A location depending on whether configuration is suspended/symmetrical + if suspended==2: # symmetrical suspended case + rA = np.array([-0.5*self.span-self.rad_fair, 0, -1]) # shared line midpoint coordinates + self.shared = True # flag that it's being modeled as symmetric + elif suspended==1: # general suspended case + rA = np.array([-self.span-self.rad_fair, 0, self.z_fair]) # other suspended end + else: # normal anchored line case + rA = np.array([-self.span-self.rad_fair, 0, -self.depth]) # anchor coordinates + rB = np.array([-self.rad_fair, 0, self.z_fair]) # fairlead coordinates + + self.rA = rA + self.rB = rB + + if suspended==2: + self.addPoint(0, rA, DOFs=[2]) # add shared line point, free only to move in z + else: + self.addPoint(-1, rA, DOFs=[0,2]) # add anchor point + + # Go through each line segment and add its upper point, add the line, and connect the line to the points + for i in range(self.nLines): + + # find the specified lineType dict and save a reference to it + if type(types[i]) == dict: # if it's a dictionary, just point to it + self.lineTypes[i] = types[i] + # otherwise we're assuming it's a string of the lineType name + elif types[i] in self.lineTypes: # first look for the name in the subsystem + self.lineTypes[i] = self.lineTypes[types[i]] + elif self.sys: # otherwise look in the parent system, if there is one + if types[i] in self.sys.lineTypes: # first look for the name in the subsystem + self.lineTypes[i] = self.sys.lineTypes[types[i]] + else: + raise Exception(f"Can't find lineType '{types[i]}' in the SubSystem or parent System.") + else: + raise Exception(f"Can't find lineType '{types[i]}' in the SubSystem.") + + # add the line segment using the reference to its lineType dict + self.addLine(lengths[i],self.lineTypes[i]) + + # add the upper end point of the segment + if i==self.nLines-1: # if this is the upper-most line + self.addPoint(-1, rB, DOFs=[0,2]) # add the fairlead point (make it coupled) + #self.bodyList[0].attachPoint(i+2, rB) # attach the fairlead point to the body (two points already created) + else: # if this is an intermediate line + m = connectors[i].get('m', 0) + v = connectors[i].get('v', 0) + # add the point, initializing linearly between anchor and fairlead/midpoint + self.addPoint(0, rA + (rB-rA)*Lcsum[i]/Lcsum[-1], m=m, v=v, DOFs=[0,2]) + + # attach the line to the points + self.pointList[-2].attachLine(i+1, 0) # attach end A of the line + self.pointList[-1].attachLine(i+1, 1) # attach end B of the line + + + def setEndPosition(self, r, endB, sink=False): + '''Sets either end position of the subsystem in the global/system + reference frame. This is included mainly to mimic the Line method. + + Parameters + ---------- + r : array + x,y,z coorindate position vector of the line end [m]. + endB : boolean + An indicator of whether the r array is at the end or beginning of the line + sink : bool + If true, and if there is a subsystem, the z position will be on the seabed. + + Raises + ------ + LineError + If the given endB value is not a 1 or 0 + ''' + + if sink: # set z coordinate on seabed + z, _ = self.getDepthFromBathymetry(r[0], r[1]) + r = np.array([r[0], r[1], z]) # (making a copy of r to not overwrite it) + + # set end coordinates in global frame just like for a Line + if endB == 1: + self.rB = np.array(r, dtype=float) + elif endB == 0: + self.rA = np.array(r, dtype=float) + else: + raise LineError("setEndPosition: endB value has to be either 1 or 0") + + + def staticSolve(self, reset=False, tol=0, profiles=0): + '''Solve internal equilibrium of the Subsystem and saves the forces + and stiffnesses at the ends in the global reference frame. All the + This method mimics the behavior of the Line.staticSolve method and + the end coordinates rA and rB must be set beforehand. The solve + equilibrium happens in the local 2D plane. Values in this local + frame are also saved. + ''' + + if tol==0: + tol=self.eqtol + + # transform end positions to SubSystem internal coordinate system + # inputs are self.rA, rB in global frame + # outputs should be pointList[0] and [N] .r + dr = self.rB - self.rA + LH = np.hypot(dr[0], dr[1]) # horizontal spacing of line ends + LV = dr[2] # vertical rise from end A to B + if self.shared: + self.pointList[ 0].setPosition([ -self.span/2 , 0, self.rA[2]]) + self.pointList[-1].setPosition([ -self.span/2+LH, 0, self.rB[2]]) + else: + self.pointList[ 0].setPosition([ -self.span , 0, self.rA[2]]) + self.pointList[-1].setPosition([ -self.span+LH, 0, self.rB[2]]) + + # get equilibrium + self.solveEquilibrium(tol=tol) + + # get 2D stiffness matrices of end points + K = self.getCoupledStiffnessA() + + # transform coordinates and forces back into global frame + if LH > 0: + self.cos_th = dr[0]/LH # cos of line heading + self.sin_th = dr[1]/LH # sin of line heading + th = np.arctan2(dr[1],dr[0]) # heading (from A to B) [rad] + self.R = rotationMatrix(0,0,th) # rotation matrix about z that goes from +x direction to heading + + else: # special case of vertical line: line heading is undefined - use zero as default + self.cos_th = 1.0 + self.sin_th = 0.0 + self.R = np.eye(3) + + # save end forces and stiffness matrices (first in local frame) + self.fA_L = self.pointList[ 0].getForces(xyz=True) # force at end A + self.fB_L = self.pointList[-1].getForces(xyz=True) # force at end B + + # Compute transverse (out-of-plane) stiffness term + if LH < 0.01*abs(LV): # if line is nearly vertical (note: this theshold is unverified) + Kt = 0.5*(self.fA_L[2] - self.fB_L[2])/LV # compute Kt based on vertical tension/span + else: # otherwise use the classic horizontal approach + Kt = -self.fB_L[0]/LH + + # expand to get 3D stiffness matrices + ''' + R = np.eye(3) + self.KA_L = from2Dto3Drotated(K[:2,:2], Kt, R.T) # reaction at A due to motion of A + self.KB_L = from2Dto3Drotated(K[2:,2:], Kt, R.T) # reaction at B due to motion of B + self.KBA_L = from2Dto3Drotated(K[2:,:2], -Kt, R.T) # reaction at B due to motion of A + ''' + self.KA_L = np.array([[K[0,0], 0 , K[0,1]], + [ 0 , Kt, 0 ], + [K[1,0], 0 , K[1,1]]]) + + # If symmetrical model, ignore midpoint stiffness and force + if self.shared: + self.KB_L = np.array(self.KA_L) # same stiffness as A + self.fB_L = np.array([-self.fA_L[0], 0, self.fA_L[1]]) # mirror of fA + else: + self.KB_L = np.array([[K[2,2], 0 , K[2,3]], + [ 0 , Kt, 0 ], + [K[3,2], 0 , K[3,3]]]) + + self.KBA_L = -self.KB_L + + # Save tension magnitudes + self.TA = np.linalg.norm(self.fA_L) # tensions [N] + self.TB = np.linalg.norm(self.fB_L) + + # Save rotated quantities for larger system in global reference frame + self.fA = np.matmul(self.R, self.fA_L) + self.fB = np.matmul(self.R, self.fB_L) + + self.KA = np.matmul(np.matmul(self.R, self.KA_L ), self.R.T) + self.KB = np.matmul(np.matmul(self.R, self.KB_L ), self.R.T) + self.KBA = np.matmul(np.matmul(self.R, self.KBA_L), self.R.T) + + #self.LBot = info["LBot"] <<< this should be calculated considering all lines + + # ----- plot the profile if requested ----- + if profiles > 1: + import matplotlib.pyplot as plt + self.plot2d() + plt.show() + + + def drawLine2d(self, Time, ax, color="k", endpoints=False, Xuvec=[1,0,0], Yuvec=[0,0,1], Xoff=0, Yoff=0, colortension=False, plotnodes=[], plotnodesline=[],label="",cmap='rainbow', alpha=1.0): + '''wrapper to System.plot2d with some transformation applied''' + + for i, line in enumerate(self.lineList): + + # color and width settings + if color == 'self': + colorplot = line.color # attempt to allow custom colors + lw = line.lw + elif color == None: + colorplot = [0.3, 0.3, 0.3] # if no color, default to grey + lw = 1 + else: + colorplot = color + lw = 1 + + # get the Line's local coordinates + Xs0, Ys0, Zs, tensions = line.getLineCoords(Time) + + # transform to global coordinates (Ys0 should be zero for now) + Xs = self.rA[0] + (Xs0 + self.span)*self.cos_th - Ys0*self.sin_th + Ys = self.rA[1] + (Xs0 + self.span)*self.sin_th + Ys0*self.cos_th + + # apply 3D to 2D transformation to provide desired viewing angle + Xs2d = Xs*Xuvec[0] + Ys*Xuvec[1] + Zs*Xuvec[2] + Xoff + Ys2d = Xs*Yuvec[0] + Ys*Yuvec[1] + Zs*Yuvec[2] + Yoff + + + if colortension: # if the mooring lines want to be plotted with colors based on node tensions + maxT = np.max(tensions); minT = np.min(tensions) + for i in range(len(Xs)-1): # for each node in the line + color_ratio = ((tensions[i] + tensions[i+1])/2 - minT)/(maxT - minT) # ratio of the node tension in relation to the max and min tension + cmap_obj = cm.get_cmap(cmap_tension) # create a cmap object based on the desired colormap + rgba = cmap_obj(color_ratio) # return the rbga values of the colormap of where the node tension is + ax.plot(Xs2d[i:i+2], Ys2d[i:i+2], color=rgba, zorder=100) + else: + ax.plot(Xs2d, Ys2d, color=colorplot, lw=lw, zorder=100, label=label, alpha=alpha) + + if len(plotnodes) > 0: + for i,node in enumerate(plotnodes): + if self.number==plotnodesline[i]: + ax.plot(Xs2d[node], Ys2d[node], 'o', color=colorplot, markersize=5) + + if endpoints == True: + ax.scatter([Xs2d[0], Xs2d[-1]], [Ys2d[0], Ys2d[-1]], color = colorplot) + + + def drawLine(self, Time, ax, color="k", endpoints=False, shadow=True, colortension=False, cmap_tension='rainbow'): + '''wrapper to System.plot with some transformation applied''' + + for i, line in enumerate(self.lineList): + + # color and width settings + if color == 'self': + color = line.color # attempt to allow custom colors + lw = line.lw + elif color == None: + color = [0.3, 0.3, 0.3] # if no color, default to grey + lw = 1 + else: + lw = 1 + + # get the Line's local coordinates + Xs0, Ys0, Zs, tensions = line.getLineCoords(Time) + + # transform to global coordinates (Ys0 should be zero for now) + Xs = self.rA[0] + (Xs0 + self.span)*self.cos_th - Ys0*self.sin_th + Ys = self.rA[1] + (Xs0 + self.span)*self.sin_th + Ys0*self.cos_th + + if colortension: # if the mooring lines want to be plotted with colors based on node tensions + maxT = np.max(tensions); minT = np.min(tensions) + for i in range(len(Xs)-1): # for each node in the line + color_ratio = ((tensions[i] + tensions[i+1])/2 - minT)/(maxT - minT) # ratio of the node tension in relation to the max and min tension + cmap_obj = cm.get_cmap(cmap_tension) # create a cmap object based on the desired colormap + rgba = cmap_obj(color_ratio) # return the rbga values of the colormap of where the node tension is + #linebit.append(ax.plot(Xs[i:i+2], Ys[i:i+2], Zs[i:i+2], color=rgba, zorder=100)) + ax.plot(Xs[i:i+2], Ys[i:i+2], Zs[i:i+2], color=rgba, zorder=100) + else: + #linebit.append(ax.plot(Xs, Ys, Zs, color=color, lw=lw, zorder=100)) + ax.plot(Xs, Ys, Zs, color=color, lw=lw, zorder=100) + + if shadow: + ax.plot(Xs, Ys, np.zeros_like(Xs)-self.depth, color=[0.5, 0.5, 0.5, 0.2], lw=lw, zorder = 1.5) # draw shadow + + if endpoints == True: + #linebit.append(ax.scatter([Xs[0], Xs[-1]], [Ys[0], Ys[-1]], [Zs[0], Zs[-1]], color = color)) + ax.scatter([Xs[0], Xs[-1]], [Ys[0], Ys[-1]], [Zs[0], Zs[-1]], color = color) + + + def setOffset(self, offset, z=0): + '''Moves end B of the Subsystem to represent an offset from the + undisplaced position of the endpoint. End A is set based on the + 'span' (shouldn't change), and B is set based on offset and the + rad_fair/z_fair setting. Optional argument z can be added for a z offset. + ''' + + # Use static EA values and unstretched lengths + self.revertToStaticStiffness() + + # Ensure end A position and set end B position to offset values + if self.shared: + self.rA = np.array([-self.span/2-self.rad_fair, 0, self.rA[2]]) + self.rB = np.array([-self.rad_fair + offset/2, 0, self.z_fair+z]) + + else: + self.rA = np.array([-self.span-self.rad_fair, 0, self.rA[2]]) + self.rB = np.array([-self.rad_fair + offset, 0, self.z_fair+z]) + + self.staticSolve(tol=self.eqtol) # solve the subsystem + + # Store some values at this offset position that may be used later + for i, line in enumerate(self.lineList): + self.TeM[i,0] = np.linalg.norm(line.fA) + self.TeM[i,1] = np.linalg.norm(line.fB) + + self.anchorFx0 = self.lineList[0].fA[0] + self.anchorFz0 = self.lineList[0].fA[2] + + self.TeD = np.copy(self.TeM) # set the dynamic values as well, in case they get queried right away + + + def setDynamicOffset(self, offset, z=0): + '''Moves end B of the Subsystem to represent a dynamic offset from + the previous offset location. Uses dynamic stiffness values and also + applies dynamic amplification factors (DAFs) on the difference from + the mean tensions (which would have been calculated in getOffset). + End A is set based on the 'span' (shouldn't change), + and B is set based on offset and the rad_fair/z_fair setting. + Optional argument z can be added for a z offset. + ''' + + if not self.dynamic_stiffness_activated: # if not already using them, + System.activateDynamicStiffness(self) # switch to dynamic EA values + + # adjust end B to the absolute offsets specified + self.rB = np.array([-self.rad_fair + offset, 0, self.z_fair+z]) + + self.staticSolve(tol=self.eqtol) # solve the subsystem + + # Store dynamic values at this offset position that may be used later + for i, line in enumerate(self.lineList): + self.TeD[i,:] = self.TeM[i,:] + self.DAFs[i]*( np.array([line.TA, line.TB]) - self.TeM[i,:] ) + + + + def activateDynamicStiffness(self, display=0): + '''Calls the dynamic stiffness method from System rather than from Line.''' + System.activateDynamicStiffness(self, display=display) + + + def revertToStaticStiffness(self): + '''Calls the static stiffness method from System rather than from Line.''' + System.revertToStaticStiffness(self) + + + # ---- Extra convenience functions (subsystem should be in equilibrium) ----- + + + def getLayLength(self, iLine=0): + '''Computes how much of a line is on the seabed. + Finds the total LBot of the whole Mooring looking at all lines in the Mooring + ''' + + uplift_ang = np.degrees(np.arctan2(self.lineList[iLine].fA[2], self.lineList[iLine].fA[0])) + + LBot = sum([line.LBot for line in self.lineList if line.number <= self.nLines]) + + if LBot == 0.0: # if there is zero lay length, enter a negative value equal to the negative sign of the anchor inclination + #LBot = -self.lineList[iLine].fA[2]/np.linalg.norm(self.lineList[iLine].fA) # this is to give the optimizer a gradient to help GET out of the constraint violation + + LBot = -uplift_ang + # UPDATED: calculate the angle of uplift when LBot is negative to keep constraint 'continuous' + # If input constraint value is negative, that's the highest angle (degrees) of vertical uplift allowed + + return LBot + + + def getPointHeight(self, iPoint): + '''Computes how high a point is off the seabed. + Default is assuming point 1. + ''' + + if iPoint==0: # if the input index of the point that needs to be constrained is the anchor point (0) + Xs, Ys, Zs, Ts = self.lineList[iPoint].getLineCoords(0) # get the current coordinates of the nodes in the bottom line + height_off_seabed = Zs[1] + self.depth # the rope point that needs to be constrained is the first node of the line away from the anchor + else: + height_off_seabed = self.pointList[iPoint].r[2] + self.depth # measure the height of the rope-chain connecting point + + # if it's on the seabed, include some other factor to avoid a zero jacobian + if height_off_seabed <= 0: + fudge_factor = -self.lineList[iPoint].LBot # add the negative of the contact length of the next line + else: + fudge_factor = 0.0 + + if height_off_seabed < 0: + print("Something is wrong") + breakpoint() + + return height_off_seabed + fudge_factor + + + def getHog(self, iLine): + '''Compute the z elevation of the highest point along a line section.''' + line = self.lineList[iLine] + return max([line.z_extreme, line.rA[2], line.rB[2]]) + + def getSag(self, iLine): + '''Compute the z elevation of the lowest point along a line section.''' + line = self.lineList[iLine] + return min([line.z_extreme, line.rA[2], line.rB[2]]) + + def getMinSag(self): + '''Compute the z elevation of the lowest point of the whole subsystem.''' + return min([ min([line.z_extreme, line.rA[2], line.rB[2]]) for line in self.lineList ]) + + + def getTen(self, iLine): + '''Compute the end (maximum) tension for a specific line, including + a dynamic amplification factor.''' + line = self.lineList[iLine] + ''' + dynamicTe = max([ self.Te0[iLine,0] + self.DAFs[iLine]*(np.linalg.norm(line.fA) - self.Te0[iLine,0]), + self.Te0[iLine,1] + self.DAFs[iLine]*(np.linalg.norm(line.fB) - self.Te0[iLine,1])]) + ''' + dynamicTe = max( self.TeD[iLine,:] ) + + return dynamicTe + + + def getTenSF(self, iLine): + '''Compute MBL/tension for a specific line.''' + return self.lineList[iLine].type['MBL'] / self.getTen(iLine) + + + def getMinTenSF(self, display=0): + '''Compute minimum MBL/tension across all line sections.''' + return min([self.getTenSF(i) for i in range(len(self.lineList))]) + + + def getCurv(self, iLine): + '''Compute the maximum curvature (minimum bending radius) of a line + section. In the future this could include a DAF.''' + + # For now, this gets the max of the nodal curvatures for this line + # which should already be calculated by calcCurvature (Is==iLine is + # a mask). In future this could be replaced with an analytic calc. + return max(self.Ks[self.Is==iLine]) + + def getCurvSF(self, iLine): + '''Compute minimum allowable bending radius / actual bend radius for + a line/cable section.''' + return 1 /( self.lineList[iLine].type['MBR'] * self.getCurv(iLine) ) + + def getMinCurvSF(self, display=0): + '''Compute minimum MBL/tension across all line sections.''' + return min([self.getCurvSF(i) for i in range(len(self.lineList))]) + + + def getYawStiffness(self): + '''Compute the contribution to platform yaw stiffness. Should already + be in equilibrium.''' + + tau0 = -self.fB[0] # horizontal tension component [N] + + yaw_stiff = (tau0/l)*self.rad_fair**2 + tau0*self.rad_fair # [N-m] + + return yaw_stiff + + + def calcCurvature(self): + '''Get the curvature [1/m] at each node of a Subsystem. Should already + be in equilibrium. Also populates nodal values for S, X, Y, Z, T, and K + along the full length.''' + + n = self.nNodes + + self.Ss = np.zeros(n) # arc length along line assembly (unstretched) [m] + self.Xs = np.zeros(n) + self.Ys = np.zeros(n) + self.Zs = np.zeros(n) + self.Ts = np.zeros(n) # tensions [N] + self.Ks = np.zeros(n) # curvatures [1/m] + self.Is = np.zeros(n, dtype=int) # what line ID this node corresponds to (end A inclusive) + + ia = 0 # index of a Line's first node, as counted along the Subsystem + La = 0 # length from end A of the Subsystem to end A of the Line + + for i, line in enumerate(self.lineList): + ni = line.nNodes # number of nodes in this Line + Ss = np.linspace(0, line.L, ni) + La + Xs, Ys, Zs, Ts = line.getLineCoords(0) + + # paste in values for this line into the full array + self.Ss[ia:ia+ni] = Ss + self.Xs[ia:ia+ni] = Xs + self.Ys[ia:ia+ni] = Ys + self.Zs[ia:ia+ni] = Zs + self.Ts[ia:ia+ni] = Ts + self.Is[ia:ia+ni] = i + + # increment the starting index and length for the next line + ia += ni-1 # one point will be overwritten since lines share end points + La += line.L + + + #ds = np.diff(self.Ss) # unstretched length of each segment + ds = np.zeros(n-1) # stretched length of each segment + + q = np.zeros([n-1, 3]) # unit vector of each segment + + + # get length and direction of each segment + for i in range(n-1): + + dr = np.array([self.Xs[i+1]-self.Xs[i], self.Ys[i+1]-self.Ys[i], self.Zs[i+1]-self.Zs[i]]) + + ds[i] = np.linalg.norm(dr) # stretched length of each segment [m] + + q[i,:] = dr/ds[i] # unit direction vector of the segment + + + # now go through and get curvature at each node + curvature = np.zeros(n) # curvature at each node [1/m] + for i in range(1,n-1): + dl = (ds[i-1] + ds[i])/2 # average of two segment lengths + + curvature[i] = (2 / dl) * np.sqrt((1 - np.dot(q[i-1,:], q[i,:])) / 2) + + # assume the curvature of the end nodes is the same as the adjacent nodes + curvature[ 0] = curvature[ 1] + curvature[-1] = curvature[-2] + + # save values + self.Ks = curvature + + return curvature + + + def loadData(self, dirname, rootname, line_IDs): + '''Load data from MoorDyn into the Subsystem lineList. + + dirname + folder where output files are located + rootname + root name of the OpenFAST primary file (.fst) + line_IDs : list + list of the line numbers (of MoorDyn) that correspond to the + lineList. These are the numbers of the line.out file to load. + + ''' + + self.qs = 0 # indicate this Subsystem is reading in MoorDyn data + + for i, line in enumerate(self.lineList): + line.loadData(dirname, rootname, line_IDs[i]) + diff --git a/moorpy/system.py b/moorpy/system.py index 803ebc4..ecbc215 100644 --- a/moorpy/system.py +++ b/moorpy/system.py @@ -20,7 +20,11 @@ from moorpy.lineType import LineType import matplotlib as mpl #import moorpy.MoorSolve as msolve -from moorpy.helpers import rotationMatrix, rotatePosition, getH, printVec, set_axes_equal, dsolve2, SolveError, MoorPyError, loadLineProps, getLineProps, read_mooring_file, printMat, printVec +from moorpy.helpers import (rotationMatrix, rotatePosition, getH, printVec, + set_axes_equal, dsolve2, SolveError, MoorPyError, + loadLineProps, getLineProps, read_mooring_file, + printMat, printVec, getInterpNums, unitVector, + getFromDict, addToDict, readBathymetryFile) @@ -30,7 +34,7 @@ class System(): # >>> note: system module will need to import Line, Point, Body for its add/creation routines # (but line/point/body modules shouldn't import system) <<< - def __init__(self, file="", dirname="", rootname="", depth=0, rho=1025, g=9.81, qs=1, Fortran=True, lineProps=None): + def __init__(self, file="", dirname="", rootname="", depth=0, rho=1025, g=9.81, qs=1, Fortran=True, lineProps=None, **kwargs): '''Creates an empty MoorPy mooring system data structure and will read an input file if provided. Parameters @@ -43,6 +47,8 @@ def __init__(self, file="", dirname="", rootname="", depth=0, rho=1025, g=9.81, Water density of the system. The default is 1025. g : float, optional Gravity of the system. The default is 9.81. + bathymetry : filename, optional + Filename for MoorDyn-style bathymetry input file. Returns ------- @@ -70,6 +76,27 @@ def __init__(self, file="", dirname="", rootname="", depth=0, rho=1025, g=9.81, self.rho = rho # water density [kg/m^3] self.g = g # gravitational acceleration [m/s^2] + # water current - currentMod 0 = no current; 1 = steady uniform current + self.currentMod = 0 # flag for current model to use + self.current = np.zeros(3) # current velocity vector [m/s] + if 'current' in kwargs: + self.currentMod = 1 + self.current = getFromDict(kwargs, 'current', shape=3) + + # seabed bathymetry - seabedMod 0 = flat; 1 = uniform slope, 2 = grid + self.seabedMod = 0 + + if 'xSlope' in kwargs or 'ySlope' in kwargs: + self.seabedMod = 1 + self.xSlope = getFromDict(kwargs, 'xSlope', default=0) + self.ySlope = getFromDict(kwargs, 'ySlope', default=0) + + if 'bathymetry' in kwargs: + self.seabedMod = 2 + self.bathGrid_Xs, self.bathGrid_Ys, self.bathGrid = readBathymetryFile(kwargs['bathymetry']) + + + # initializing variables and lists self.nDOF = 0 # number of (free) degrees of freedom of the mooring system (needs to be set elsewhere) self.freeDOFs = [] # array of the values of the free DOFs of the system at different instants (2D list) @@ -79,6 +106,8 @@ def __init__(self, file="", dirname="", rootname="", depth=0, rho=1025, g=9.81, self.display = 0 # a flag that controls how much printing occurs in methods within the System (Set manually. Values > 0 cause increasing output.) self.MDoptions = {} # dictionary that can hold any MoorDyn options read in from an input file, so they can be saved in a new MD file if need be + + self.dynamic_stiffness_activated = False # flag turned on when dynamic EA values are activate # read in data from an input file if a filename was provided if len(file) > 0: @@ -115,7 +144,8 @@ def __init__(self, file="", dirname="", rootname="", depth=0, rho=1025, g=9.81, rod.loadData(dirname, rootname, sep='_') - def addBody(self, mytype, r6, m=0, v=0, rCG=np.zeros(3), AWP=0, rM=np.zeros(3), f6Ext=np.zeros(6)): + def addBody(self, mytype, r6, m=0, v=0, rCG=np.zeros(3), AWP=0, + rM=np.zeros(3), f6Ext=np.zeros(6), DOFs=[0,1,2,3,4,5]): '''Convenience function to add a Body to a mooring system Parameters @@ -143,7 +173,8 @@ def addBody(self, mytype, r6, m=0, v=0, rCG=np.zeros(3), AWP=0, rM=np.zeros(3), ''' - self.bodyList.append( Body(self, len(self.bodyList)+1, mytype, r6, m=m, v=v, rCG=rCG, AWP=AWP, rM=rM, f6Ext=f6Ext) ) + self.bodyList.append( Body(self, len(self.bodyList)+1, mytype, r6, m=m, + v=v, rCG=rCG, AWP=AWP, rM=rM, f6Ext=f6Ext, DOFs=DOFs) ) # handle display message if/when MoorPy is reorganized by classes @@ -258,7 +289,39 @@ def addLine(self, lUnstr, lineType, nSegs=40, pointA=0, pointB=0, cb=0): #print("Created Line "+str(self.lineList[-1].number)) # handle display message if/when MoorPy is reorganized by classes + + """ + def addSubsystem(self, lengths, lineTypes, pointA=0, pointB=0): + '''Convenience function to add a Subsystem to a mooring system. IN PROGRESS. + + Parameters + ---------- + pointA int, optional + Point number to attach end A of the Subsystem to. + pointB int, optional + Point number to attach end B of the Subsystem to. + ''' + + if not isinstance(lineType, dict): # If lineType is not a dict, presumably it is a key for System.LineTypes. + if lineType in self.lineTypes: # So make sure it matches up with a System.LineType + lineType = self.lineTypes[lineType] # in which case that entry will get passed to Line.init + else: + raise ValueError(f"The specified lineType name ({lineType}) does not correspond with any lineType stored in this MoorPy System") + + self.lineList.append( Subsystem(self, ...) ) + if pointA > 0: + if pointA <= len(self.pointList): + self.pointList[pointA-1].attachLine(self.lineList[-1].number, 0) + else: + raise Exception(f"Provided pointA of {pointA} exceeds number of points.") + if pointB > 0: + if pointB <= len(self.pointList): + self.pointList[pointB-1].attachLine(self.lineList[-1].number, 1) + else: + raise Exception(f"Provided pointB of {pointB} exceeds number of points.") + """ + """ def removeLine(self, lineID): '''Removes a line from the system.''' @@ -279,7 +342,36 @@ def removeLine(self, lineID): raise Exception("Invalid line number") """ + + def disconnectLineEnd(self, lineID, endB): + '''Disconnects the specified end of a Line object from whatever point + it's attached to, and instead attaches it to a new free point. + ''' + + # for now assume the line is at the expected index + line = self.lineList[lineID-1] + + if not line.number == lineID: + raise Exception(f"Error lineID {lineID} isn't at the corresponding lineList index.") + # detach line from whatever point it's attached to + for point in self.pointList: + if lineID in point.attached: + if point.attachedEndB[point.attached.index(lineID)] == endB: + point.detachLine(lineID, endB) + break + + # create new free point to attach the line end to + if endB: + r = line.rB + else: + r = line.rA + + self.addPoint(0, r) + + self.pointList[-1].attachLine(lineID, endB) + + def addLineType(self, type_string, d, mass, EA, name=""): '''Convenience function to add a LineType to a mooring system or adjust the values of an existing line type if it has the same name/key. @@ -533,7 +625,6 @@ def load(self, filename, clear=True): entries = line.split() # entries: ID Attachment X0 Y0 Z0 r0 p0 y0 M CG* I* V CdA* Ca* num = int(entries[0]) entry0 = entries[1].lower() - #num = np.int_("".join(c for c in entry0 if not c.isalpha())) # remove alpha characters to identify Body # if ("fair" in entry0) or ("coupled" in entry0) or ("ves" in entry0): # coupled case bodyType = -1 @@ -549,8 +640,8 @@ def load(self, filename, clear=True): r6 = np.array(entries[2:8], dtype=float) # initial position and orientation [m, rad] r6[3:] = r6[3:]*np.pi/180.0 # convert from deg to rad #rCG = np.array(entries[7:10], dtype=float) # location of body CG in body reference frame [m] - m = np.float_(entries[8]) # mass, centered at CG [kg] - v = np.float_(entries[11]) # volume, assumed centered at reference point [m^3] + m = float(entries[8]) # mass, centered at CG [kg] + v = float(entries[11]) # volume, assumed centered at reference point [m^3] # process CG strings_rCG = entries[ 9].split("|") # split by braces, if any @@ -643,7 +734,7 @@ def load(self, filename, clear=True): entry1 = entries[1].lower() - num = np.int_("".join(c for c in entry0 if not c.isalpha())) # remove alpha characters to identify Point # + num = int("".join(c for c in entry0 if not c.isalpha())) # remove alpha characters to identify Point # if ("anch" in entry1) or ("fix" in entry1): @@ -701,10 +792,10 @@ def load(self, filename, clear=True): while line.count('---') == 0: entries = line.split() # entries: ID LineType AttachA AttachB UnstrLen NumSegs Outputs - num = np.int_(entries[0]) - lUnstr = np.float_(entries[4]) + num = int(entries[0]) + lUnstr = float(entries[4]) lineType = self.lineTypes[entries[1]] - nSegs = np.int_(entries[5]) + nSegs = int(entries[5]) #lineList.append( Line(dirName, num, lUnstr, dia, nSegs) ) self.lineList.append( Line(self, num, lUnstr, lineType, nSegs=nSegs)) #attachments = [int(entries[4]), int(entries[5])]) ) @@ -825,14 +916,20 @@ def parseYAML(self, data): # line types for d in data['line_types']: + name = d['name'] dia = float(d['diameter'] ) - w = float(d['mass_density'])*self.g + m = float(d['mass_density']) + w = (m - np.pi/4*dia**2 *self.rho)*self.g EA = float(d['stiffness'] ) - if d['breaking_load']: - MBL = float(d['breaking_load']) - else: - MBL = 0 - self.lineTypes[d['name']] = dict(name=d['name'], d_vol=dia, w=w, EA=EA, MBL=MBL) + self.lineTypes[name] = dict(name=name, d_vol=dia, m=m, w=w, EA=EA) + + addToDict(d, self.lineTypes[name], 'breaking_load' , 'MBL' , default=0) + addToDict(d, self.lineTypes[name], 'cost' , 'cost', default=0) + addToDict(d, self.lineTypes[name], 'transverse_drag' , 'Cd' , default=0) + addToDict(d, self.lineTypes[name], 'tangential_drag' , 'CdAx', default=0) + addToDict(d, self.lineTypes[name], 'transverse_added_mass', 'Ca' , default=0) + addToDict(d, self.lineTypes[name], 'tangential_added_mass', 'CaAx', default=0) + # rod types TBD @@ -851,7 +948,7 @@ def parseYAML(self, data): entry0 = d['name'].lower() entry1 = d['type'].lower() - #num = np.int_("".join(c for c in entry0 if not c.isalpha())) # remove alpha characters to identify Point # + #num = int("".join(c for c in entry0 if not c.isalpha())) # remove alpha characters to identify Point # num = i+1 # not counting on things being numbered in YAML files if ("anch" in entry1) or ("fix" in entry1): @@ -886,12 +983,12 @@ def parseYAML(self, data): r = np.array(d['location'], dtype=float) if 'mass' in d: - m = np.float_(d['mass']) + m = float(d['mass']) else: m = 0.0 if 'volume' in d: - v = np.float_(d['volume']) + v = float(d['volume']) else: v = 0.0 @@ -903,7 +1000,7 @@ def parseYAML(self, data): num = i+1 - lUnstr = np.float_(d['length']) + lUnstr = float(d['length']) self.lineList.append( Line(self, num, lUnstr, self.lineTypes[d['type']]) ) @@ -911,36 +1008,9 @@ def parseYAML(self, data): self.pointList[pointDict[d['endA']]].attachLine(num, 0) self.pointList[pointDict[d['endB']]].attachLine(num, 1) - - def readBathymetryFile(self, filename): - f = open(filename, 'r') - - # skip the header - line = next(f) - # collect the number of grid values in the x and y directions from the second and third lines - line = next(f) - nGridX = int(line.split()[1]) - line = next(f) - nGridY = int(line.split()[1]) - # allocate the Xs, Ys, and main bathymetry grid arrays - bathGrid_Xs = np.zeros(nGridX) - bathGrid_Ys = np.zeros(nGridY) - bathGrid = np.zeros([nGridX, nGridY]) - # read in the fourth line to the Xs array - line = next(f) - bathGrid_Xs = [float(line.split()[i]) for i in range(nGridX)] - # read in the remaining lines in the file into the Ys array (first entry) and the main bathymetry grid - for i in range(nGridY): - line = next(f) - entries = line.split() - bathGrid_Ys[i] = entries[0] - bathGrid[i,:] = entries[1:] - - return bathGrid_Xs, bathGrid_Ys, bathGrid - - - - def unload(self, fileName, MDversion=2, line_dL=0, rod_dL=0, flag='p', outputList=[]): + + def unload(self, fileName, MDversion=2, line_dL=0, rod_dL=0, flag='p', + outputList=[], Lm=0, T_half=42): '''Unloads a MoorPy system into a MoorDyn-style input file Parameters @@ -953,7 +1023,12 @@ def unload(self, fileName, MDversion=2, line_dL=0, rod_dL=0, flag='p', outputLis Optional specified for target segment length when discretizing Rods outputList : list of strings, optional Optional list of additional requested output channels - + Lm : float + Mean load on mooring line as FRACTION of MBL, used for dynamic stiffness calculation. Only used if line type has a nonzero EAd + T_half : float, optional + For tuning response of viscoelastic model, the period when EA is + half way between the static and dynamic values [s]. Default is 42. + Returns ------- None. @@ -973,7 +1048,7 @@ def unload(self, fileName, MDversion=2, line_dL=0, rod_dL=0, flag='p', outputLis # Some default settings to fill in if coefficients aren't set #lineTypeDefaults = dict(BA=-1.0, EI=0.0, Cd=1.2, Ca=1.0, CdAx=0.2, CaAx=0.0) - lineTypeDefaults = dict(BA=-1.0, cIntDamp=-0.8, EI=0.0, Can=1.0, Cat=1.0, Cdn=1.0, Cdt=0.5) + lineTypeDefaults = dict(BA=-1.0, EI=0.0, Ca=1.0, CaAx=1.0, Cd=1.0, CdAx=0.5) rodTypeDefaults = dict(Cd=1.2, Ca=1.0, CdEnd=1.0, CaEnd=1.0) # bodyDefaults = dict(IX=0, IY=0, IZ=0, CdA_xyz=[0,0,0], Ca_xyz=[0,0,0]) @@ -1021,8 +1096,8 @@ def unload(self, fileName, MDversion=2, line_dL=0, rod_dL=0, flag='p', outputLis #L.append("{:<12} {:7.4f} {:8.2f} {:7.3e} {:7.3e} {:7.3e} {:<7.3f} {:<7.3f} {:<7.2f} {:<7.2f}".format( #key, di['d_vol'], di['m'], di['EA'], di['cIntDamp'], di['EI'], di['Can'], di['Cat'], di['Cdn'], di['Cdt'])) L.append("{:<12} {:7.4f} {:8.2f} {:7.3e} {:7.3e} {:<7.3f} {:<7.3f} {:<7.2f} {:<7.2f}".format( - key, di['d_vol'], di['m'], di['EA'], di['BA'], di['Can'], di['Cat'], di['Cdn'], di['Cdt'])) - + # key, di['d_vol'], di['m'], di['EA'], di['BA'], di['Can'], di['Cat'], di['Cdn'], di['Cdt'])) + key, di['d_vol'], di['m'], di['EA'], di['BA'], di['Ca'], di['CaAx'], di['Cd'], di['CdAx'])) #L.append("---------------------- POINTS ---------------------------------------------------------") L.append("---------------------- NODE PROPERTIES ---------------------------------------------------------") @@ -1132,17 +1207,45 @@ def unload(self, fileName, MDversion=2, line_dL=0, rod_dL=0, flag='p', outputLis # Figure out mooring line attachments (Create a ix2 array of connection points from a list of m points) connection_points = np.empty([len(self.lineList),2]) #First column is Anchor Node, second is Fairlead node + rod_ends = np.empty([len(self.lineList),2]) for point_ind,point in enumerate(self.pointList,start = 1): #Loop through all the points for (line,line_pos) in zip(point.attached,point.attachedEndB): #Loop through all the lines #s connected to this point if line_pos == 0: #If the A side of this line is connected to the point - connection_points[line -1,0] = point_ind #Save as as an Anchor Node + if point.cable == True: + connection_points[line -1,0] = -point_ind #Save as as an Anchor Node + + #determine which end of rod to connect to + F = point.getForces(lines_only = True, xyz = True) + if F[0] > 0: + rod_ends[line - 1, 0] = 1 + else: + rod_ends[line - 1, 0] = 0 + else: + connection_points[line -1,0] = point_ind #Save as as an Anchor Node #connection_points[line -1,0] = self.pointList.index(point) + 1 elif line_pos == 1: #If the B side of this line is connected to the point - connection_points[line -1,1] = point_ind #Save as a Fairlead node + if point.cable == True: + connection_points[line -1,1] = -point_ind #Save as a Fairlead node + + #determine which end of rod to connect to + F = point.getForces(lines_only = True, xyz = True) + if F[0] > 0: + rod_ends[line - 1, 1] = 0 + else: + rod_ends[line - 1, 1] = 1 + else: + connection_points[line -1,1] = point_ind #Save as a Fairlead node #connection_points[line -1,1] = self.pointList.index(point) + 1 + #check if any points have cable = True, this determines if rods are needed + cable = False + for point in self.pointList: + if point.cable == True: + cable = True + break + #Line Properties - flag = "p" # "-" + #flag = "p" # "-" # "flag" is already set as an input to self.unload(), meaning you can set it in the function call #Outputs List Outputs = [f"FairTen{i+1}" for i in range(len(self.lineList))] # for now, have a fairlead tension output for each line @@ -1170,20 +1273,52 @@ def unload(self, fileName, MDversion=2, line_dL=0, rod_dL=0, flag='p', outputLis for key, lineType in self.lineTypes.items(): di = lineTypeDefaults.copy() # start with a new dictionary of just the defaults di.update(lineType) # then copy in the lineType's existing values - L.append("{:<12} {:7.4f} {:8.2f} {:7.3e} {:7.3e} {:7.3e} {:<7.3f} {:<7.3f} {:<7.2f} {:<7.2f}".format( - key, di['d_vol'], di['m'], di['EA'], di['BA'], di['EI'], di['Cd'], di['Ca'], di['CdAx'], di['CaAx'])) + if 'EAd' in di.keys() and di['EAd'] > 0: + if Lm > 0: + print('Calculating dynamic stiffness with Lm = ' + str(Lm)+'* MBL') + # Get dynamic stiffness including mean load dependence + EAd = di['EAd'] + di['EAd_Lm']*Lm*di['MBL'] + # This damping value is chosen for critical damping of a 10 m segment + c2 = 10 * np.sqrt(di['EA'] * di['m']) + # or use c1 = di['BA'] ? + # This damping value is chosen to get the desired + # half-way period between static and dynamic stiffnesses + frac=0.5 + EAs = di['EA'] + K1 = EAs*EAd/(EAd-EAs) + K1= EAs + K2 = EAd + + c1 = (K1+K2)/(2*np.pi/T_half) * np.sqrt(((K1+frac*K2)**2 - K1**2)/((K1+K2)**2 - (K1+frac*K2)**2)) + + + L.append("{:<12} {:7.4f} {:8.2f} {:7.3e}|{:7.3e} {:7.3e}|{:7.3e} {:7.3e} {:<7.3f} {:<7.3f} {:<7.2f} {:<7.2f}".format( + key, di['d_vol'], di['m'], di['EA'], EAd, c1, c2, di['EI'], di['Cd'], di['Ca'], di['CdAx'], di['CaAx'])) + else: + print('No mean load provided!!! using the static EA value ONLY') + L.append("{:<12} {:7.4f} {:8.2f} {:7.3e} {:7.3e} {:7.3e} {:<7.3f} {:<7.3f} {:<7.2f} {:<7.2f}".format( + key, di['d_vol'], di['m'], di['EA'], di['BA'], di['EI'], di['Cd'], di['Ca'], di['CdAx'], di['CaAx'])) + else: + L.append("{:<12} {:7.4f} {:8.2f} {:7.3e} {:7.3e} {:7.3e} {:<7.3f} {:<7.3f} {:<7.2f} {:<7.2f}".format( + key, di['d_vol'], di['m'], di['EA'], di['BA'], di['EI'], di['Cd'], di['Ca'], di['CdAx'], di['CaAx'])) L.append("--------------------- ROD TYPES -----------------------------------------------------") L.append("TypeName Diam Mass/m Cd Ca CdEnd CaEnd") L.append("(name) (m) (kg/m) (-) (-) (-) (-)") + for key, rodType in self.rodTypes.items(): di = rodTypeDefaults.copy() di.update(rodType) L.append("{:<15} {:7.4f} {:8.2f} {:<7.3f} {:<7.3f} {:<7.3f} {:<7.3f}".format( key, di['d_vol'], di['m'], di['Cd'], di['Ca'], di['CdEnd'], di['CaEnd'])) + # add arbitrary rod for cable connections if cable is True + if cable: + L.append("{:<15} {:7.4f} {:8.2f} {:<7.3f} {:<7.3f} {:<7.3f} {:<7.3f}".format( + 'connector', 0.2, 0.0 , 0.0, 0.0, 0.0, 0.0)) + L.append("----------------------- BODIES ------------------------------------------------------") L.append("ID Attachment X0 Y0 Z0 r0 p0 y0 Mass CG* I* Volume CdA* Ca*") @@ -1209,30 +1344,61 @@ def unload(self, fileName, MDversion=2, line_dL=0, rod_dL=0, flag='p', outputLis # Rod Properties Table TBD <<< + #add zero length rods for dynamic cables + rod_id = 0 + for point in self.pointList: + point_pos = point.r + if point.cable == True: + + rod_id = rod_id + 1 + if point.type == 1: # point is fixed or attached (anch, body, fix) + point_type = 'Fixed' + + #Check if the point is attached to body + for body in self.bodyList: + for attached_Point in body.attachedP: + if attached_Point == point.number: + point_type = "Body" + str(body.number) + point_pos = body.rPointRel[body.attachedP.index(attached_Point)] # get point position in the body reference frame + + elif point.type == 0: # point is coupled externally (con, free) + point_type = 'Free' + + elif point.type == -1: # point is free to move (fair, ves) + point_type = 'Coupled' + + f = point.getForces(lines_only = True, xyz = True) + f_unit = f / np.linalg.norm(f) + rA = [point_pos[0] - f_unit[0], point_pos[1] - f_unit[1], point_pos[2] - f_unit[2]] + rB = [point_pos[0] + f_unit[0], point_pos[1] + f_unit[1], point_pos[2] + f_unit[2]] + L.append("{:<4d} {:9} {:9} {:8.2f} {:8.2f} {:9.2f} {:6.2f} {:6.2f} {:6.2f} {} {}".format( + rod_id, 'connector', point_type, rA[0], rA[1], rA[2], rB[0], rB[1], rB[2], 0, '-')) + L.append("---------------------- POINTS -------------------------------------------------------") L.append("ID Attachment X Y Z Mass Volume CdA Ca") - L.append("(#) (-) (m) (m) (m) (kg) (mˆ3) (m^2) (-)") + L.append("(#) (-) (m) (m) (m) (kg) (m^3) (m^2) (-)") for point in self.pointList: - point_pos = point.r # get point position in global reference frame to start with - if point.type == 1: # point is fixed or attached (anch, body, fix) - point_type = 'Fixed' - - #Check if the point is attached to body - for body in self.bodyList: - for attached_Point in body.attachedP: - if attached_Point == point.number: - point_type = "Body" + str(body.number) - point_pos = body.rPointRel[body.attachedP.index(attached_Point)] # get point position in the body reference frame - - elif point.type == 0: # point is coupled externally (con, free) - point_type = 'Free' + if point.cable == False: + point_pos = point.r # get point position in global reference frame to start with + if point.type == 1: # point is fixed or attached (anch, body, fix) + point_type = 'Fixed' - elif point.type == -1: # point is free to move (fair, ves) - point_type = 'Coupled' - - L.append("{:<4d} {:9} {:8.2f} {:8.2f} {:8.2f} {:9.2f} {:6.2f} {:6.2f} {:6.2f}".format( - point.number,point_type, point_pos[0],point_pos[1],point_pos[2], point.m, point.v, point.CdA, point.Ca)) + #Check if the point is attached to body + for body in self.bodyList: + for attached_Point in body.attachedP: + if attached_Point == point.number: + point_type = "Body" + str(body.number) + point_pos = body.rPointRel[body.attachedP.index(attached_Point)] # get point position in the body reference frame + + elif point.type == 0: # point is coupled externally (con, free) + point_type = 'Free' + + elif point.type == -1: # point is free to move (fair, ves) + point_type = 'Coupled' + + L.append("{:<4d} {:9} {:8.2f} {:8.2f} {:8.2f} {:9.2f} {:6.2f} {:6.2f} {:6.2f}".format( + point.number,point_type, point_pos[0],point_pos[1],point_pos[2], point.m, point.v, point.CdA, point.Ca)) L.append("---------------------- LINES --------------------------------------------------------") @@ -1241,10 +1407,15 @@ def unload(self, fileName, MDversion=2, line_dL=0, rod_dL=0, flag='p', outputLis for i,line in enumerate(self.lineList): nSegs = int(np.ceil(line.L/line_dL)) if line_dL>0 else line.nNodes-1 # if target dL given, set nSegs based on it instead of line.nNodes - - L.append("{:<4d} {:<15} {:^5d} {:^5d} {:8.3f} {:4d} {}".format( - line.number, line.type['name'], int(connection_points[i,0]), int(connection_points[i,1]), line.L, nSegs, flag)) - + if connection_points[i,0] < 0: + attach = ['A', 'B'] + L.append("{:<4d} {:<15} {} {} {:8.3f} {:4d} {}".format( + line.number, line.number - 1, 'R'+str(int(-connection_points[i,0]))+attach[int(rod_ends[i,0])], 'R' +str(int(-connection_points[i,1])) +attach[int(rod_ends[i,1])], line.L, nSegs, flag)) + + else: + L.append("{:<4d} {:<15} {:^5d} {:^5d} {:8.3f} {:4d} {}".format( + line.number, line.type['name'], int(connection_points[i,0]), int(connection_points[i,1]), line.L, nSegs, flag)) + L.append("---------------------- OPTIONS ------------------------------------------------------") @@ -1294,11 +1465,11 @@ def getDOFs(self): for body in self.bodyList: if body.type == 0: - nDOF += 6 - DOFtypes += [0]*6 + nDOF += body.nDOF + DOFtypes += [0]*body.nDOF if body.type ==-1: - nCpldDOF += 6 - DOFtypes += [-1]*6 + nCpldDOF += body.nDOF + DOFtypes += [-1]*body.nDOF for point in self.pointList: if point.type == 0: @@ -1428,15 +1599,17 @@ def getPositions(self, DOFtype="free", dXvals=[]): else: raise ValueError("getPositions called with invalid DOFtype input. Must be free, coupled, or both") - X = np.array([], dtype=np.float_) + X = np.array([], dtype=float) if len(dXvals)>0: dX = [] # gather DOFs from bodies for body in self.bodyList: if body.type in types: - X = np.append(X, body.r6) - if len(dXvals)>0: dX += 3*[dXvals[-2]] + 3*[dXvals[-1]] - + X = np.append(X, body.r6[body.DOFs]) # only include active DOFs + if len(dXvals) > 0: # set translation and rotational dx vals + dX += [dXvals[-2]] * sum(i in body.DOFs for i in [0,1,2]) + dX += [dXvals[-1]] * sum(i in body.DOFs for i in [3,4,5]) + # gather DOFs from points for point in self.pointList: if point.type in types: @@ -1444,7 +1617,7 @@ def getPositions(self, DOFtype="free", dXvals=[]): if len(dXvals)>0: dX += point.nDOF*[dXvals[0]] if len(dXvals)>0: - dX = np.array(dX, dtype=np.float_) + dX = np.array(dX, dtype=float) return X, dX else: return X @@ -1488,8 +1661,8 @@ def setPositions(self, X, DOFtype="free"): # update positions of bodies for body in self.bodyList: if body.type in types: - body.setPosition(X[i:i+6]) - i += 6 + body.setPosition(X[i:i+body.nDOF]) + i += body.nDOF # update position of Points for point in self.pointList: @@ -1537,8 +1710,8 @@ def getForces(self, DOFtype="free", lines_only=False): # gather net loads from bodies for body in self.bodyList: if body.type in types: - f[i:i+6] = body.getForces(lines_only=lines_only) - i += 6 + f[i:i+body.nDOF] = body.getForces(lines_only=lines_only) + i += body.nDOF # gather net loads from points for point in self.pointList: @@ -1569,6 +1742,15 @@ def getTensions(self): return T + def saveMaxTensions(self, T): + '''Store max tensions at each line computed elsewhere for later + use in MoorPy (e.g., for computing anchor costs).''' + + n = len(self.lineList) + + for i, line in enumerate(self.lineList): + self.lineList[i].loads['tenA_max'] = T[ i] + self.lineList[i].loads['tenB_max'] = T[n+i] def mooringEq(self, X, DOFtype="free", lines_only=False, tol=0.001, profiles=0): @@ -1658,8 +1840,8 @@ def solveEquilibrium(self, DOFtype="free", plots=0, rmsTol=10, maxIter=200): X0 += [*Point.r ] # add free Point position to vector db += [ 5., 5., 5.] # specify maximum step size for point positions - X0 = np.array(X0, dtype=np.float_) - db = np.array(db, dtype=np.float_) + X0 = np.array(X0, dtype=float) + db = np.array(db, dtype=float) ''' X0, db = self.getPositions(DOFtype=DOFtype, dXvals=[5.0, 0.3]) @@ -1802,25 +1984,12 @@ def solveEquilibrium(self, DOFtype="free", plots=0, tol=0.05, rmsTol=0.0, maxIte # create arrays for the initial positions of the objects that need to find equilibrium, and the max step sizes X0, db = self.getPositions(DOFtype=DOFtype, dXvals=[30, 0.02]) - # temporary for backwards compatibility <<<<<<<<<< - ''' - if rmsTol != 0.0: - tols = np.zeros(len(X0)) + rmsTol - print("WHAT IS PASSING rmsTol in to solveEquilibrium?") - breakpoint() - elif np.isscalar(tol): - if tol < 0: - tols = -tol*db # tolerances set relative to max step size - lineTol = 0.05*tols[0] # hard coding a tolerance for catenary calcs <<<<<<<<<<< - else: - tols = 1.0*tol # normal case, passing dsovle(2) a scalar for tol - else: - tols = np.array(tol) # assuming tolerances are passed in for each free variable - ''' - # store z indices for later seabed contact handling, and create vector of tolerances + # ----- Store z indices and create vector of tolerances ----- + zInds = [] tols = [] + i = 0 # index to go through system DOF vector if DOFtype == "free": types = [0] @@ -1828,19 +1997,23 @@ def solveEquilibrium(self, DOFtype="free", plots=0, tol=0.05, rmsTol=0.0, maxIte types = [-1] elif DOFtype == "both": types = [0,-1] - + + # For bodies and points, only include active DOFs for body in self.bodyList: if body.type in types: - zInds.append(i+2) - i+=6 + if 2 in body.DOFs: # note the z index if it's an active DOF + zInds.append(i + body.DOFs.index(2)) + i+=body.nDOF # advance the index to the start of the next object + rtol = tol/max([np.linalg.norm(rpr) for rpr in body.rPointRel]) # estimate appropriate body rotational tolerance based on attachment point radii - tols += 3*[tol] + 3*[rtol] + tols += [tol ] * sum(i in body.DOFs for i in [0,1,2]) + tols += [rtol] * sum(i in body.DOFs for i in [3,4,5]) for point in self.pointList: if point.type in types: - if 2 in point.DOFs: - zInds.append(i + point.DOFs.index(2)) # may need to check this bit <<<< - i+=point.nDOF # note: only including active DOFs of the point (z may not be one of them) + if 2 in point.DOFs: # note the z index if it's an active DOF + zInds.append(i + point.DOFs.index(2)) + i+=point.nDOF # advance the index to the start of the next object tols += point.nDOF*[tol] @@ -1855,6 +2028,8 @@ def solveEquilibrium(self, DOFtype="free", plots=0, tol=0.05, rmsTol=0.0, maxIte print("There are no DOFs so solveEquilibrium is returning without adjustment.") return True + # ----- Set up the equilibrium solver functions ----- + # clear some arrays to log iteration progress self.freeDOFs.clear() # clear stored list of positions, so it can be refilled for this solve process self.Xs = [] # for storing iterations from callback fn @@ -2040,7 +2215,7 @@ def step_func_equil(X, args, Y, oths, Ytarget, err, tol_, iter, maxIter): # check sign for backward result (potentially a result of bad numerics?) and strengthen diagonals if so to straighten it out for iTry in range(10): if sum(dX2*Y2) < 0: - print("sum(dX2*Y2) is negative so enlarging the diagonals") + #print("sum(dX2*Y2) is negative so enlarging the diagonals") for i in range(n2): K2[i,i] += 0.1*abs(K2[i,i]) # double the diagonal entries as a hack @@ -2137,6 +2312,9 @@ def step_func_equil(X, args, Y, oths, Ytarget, err, tol_, iter, maxIter): #if iter > 100: # print(iter) # breakpoint() + + + if np.sum(np.isnan(dX)) > 0: breakpoint() return dX @@ -2275,7 +2453,7 @@ def getSystemStiffness(self, DOFtype="free", dx=0.1, dth=0.1, solveOption=1, lin for i in range(n): # loop through each DOF - X2 = np.array(X1, dtype=np.float_) + X2 = np.array(X1, dtype=float) X2[i] += dX[i] # perturb positions by dx in each DOF in turn F2p = self.mooringEq(X2, DOFtype=DOFtype, lines_only=lines_only, tol=lineTol) # system net force/moment vector from positive perturbation @@ -2298,7 +2476,7 @@ def getSystemStiffness(self, DOFtype="free", dx=0.1, dth=0.1, solveOption=1, lin # potentially iterate with smaller step sizes if we're at a taut-slack transition (but don't get too small, or else numerical errors) for j in range(nTries): if self.display > 2: print(" ") - X2 = np.array(X1, dtype=np.float_) + X2 = np.array(X1, dtype=float) X2[i] += dXi # perturb positions by dx in each DOF in turn F2p = self.mooringEq(X2, DOFtype=DOFtype, lines_only=lines_only, tol=lineTol) # system net force/moment vector from positive perturbation if self.display > 2: @@ -2415,7 +2593,7 @@ def getCoupledStiffness(self, dx=0.1, dth=0.1, solveOption=1, lines_only=False, for i in range(self.nCpldDOF): # loop through each DOF - X2 = np.array(X1, dtype=np.float_) + X2 = np.array(X1, dtype=float) X2[i] += dX[i] # perturb positions by dx in each DOF in turn self.setPositions(X2, DOFtype="coupled") # set the perturbed coupled DOFs self.solveEquilibrium() # let the system settle into equilibrium (note that this might prompt a warning if there are no free DOFs) @@ -2442,7 +2620,7 @@ def getCoupledStiffness(self, dx=0.1, dth=0.1, solveOption=1, lines_only=False, for j in range(nTries): #print(f'-------- nTries = {j+1} --------') - X2 = np.array(X1, dtype=np.float_) + X2 = np.array(X1, dtype=float) X2[i] += dXi # perturb positions by dx in each DOF in turn self.setPositions(X2, DOFtype="coupled") # set the perturbed coupled DOFs #print(f'solving equilibrium {i+1}+_{self.nCpldDOF}') @@ -2512,14 +2690,12 @@ def getCoupledStiffness(self, dx=0.1, dth=0.1, solveOption=1, lines_only=False, - def getCoupledStiffnessA(self, dx=0.1, dth=0.1, solveOption=1, lines_only=False, tensions=False, nTries=3, plots=0): + def getCoupledStiffnessA(self, lines_only=False, tensions=False): '''Calculates the stiffness matrix for coupled degrees of freedom of a mooring system with free uncoupled degrees of freedom equilibrated - analytical appraoch. Parameters ---------- - plots : boolean, optional - Determines whether the stiffness calculation process is plotted and/or animated or not. The default is 0. lines_only : boolean Whether to consider only line forces and ignore body/point properties. tensions : boolean @@ -2542,10 +2718,17 @@ def getCoupledStiffnessA(self, dx=0.1, dth=0.1, solveOption=1, lines_only=False, # get full system stiffness matrix K_all = self.getSystemStiffnessA(DOFtype="both", lines_only=lines_only) + # If there are no free DOFs, then K_all is K_coupled, so return it + if self.nDOF == 0: + return K_all + # invert matrix - K_inv_all = np.linalg.inv(K_all) + try: + K_inv_all = np.linalg.inv(K_all) + except: + K_inv_all = np.linalg.pinv(K_all) - # remove free DOFs (this corresponds to saying that the same of forces on these DOFs will remain zero) + # remove free DOFs (this corresponds to saying that the sum of forces on these DOFs will remain zero) #indices = list(range(n)) # list of DOF indices that will remain active for this step mask = [True]*n # this is a mask to be applied to the array K indices @@ -2557,7 +2740,10 @@ def getCoupledStiffnessA(self, dx=0.1, dth=0.1, solveOption=1, lines_only=False, K_inv_coupled = K_inv_all[mask,:][:,mask] # invert reduced matrix to get coupled stiffness matrix (with free DOFs assumed to equilibrate linearly) - K_coupled = np.linalg.inv(K_inv_coupled) + try: + K_coupled = np.linalg.inv(K_inv_coupled) + except: + K_coupled = np.linalg.pinv(K_inv_coupled) #if tensions: # return K_coupled, J @@ -2631,7 +2817,7 @@ def getSystemStiffnessA(self, DOFtype="free", lines_only=False, rho=1025, g=9.81 # get body's self-stiffness matrix (now only cross-coupling terms will be handled on a line-by-line basis) K6 = body1.getStiffnessA(lines_only=lines_only) - K[i:i+6,i:i+6] += K6 + K[i:i+body1.nDOF, i:i+body1.nDOF] += K6 # go through each attached point @@ -2645,21 +2831,14 @@ def getSystemStiffnessA(self, DOFtype="free", lines_only=False, rho=1025, g=9.81 for lineID in point1.attached: # go through each attached line to the Point, looking for when its other end is attached to something that moves endFound = 0 # simple flag to indicate when the other end's attachment has been found - j = i+6 # first index of the DOFs this line is attached to. Start it off at the next spot after body1's DOFs + j = i + body1.nDOF # first index of the DOFs this line is attached to. Start it off at the next spot after body1's DOFs # get cross-coupling stiffness of line: force on end attached to body1 due to motion of other end if point1.attachedEndB == 1: - KB = self.lineList[lineID-1].KAB - else: - KB = self.lineList[lineID-1].KAB.T - ''' - KA, KB = self.lineList[lineID-1].getStiffnessMatrix() - # flip sign for coupling - if point1.attachedEndB == 1: # assuming convention of end A is attached to the first point, so if not, - KB = -KA # swap matrices of ends A and B + KB = self.lineList[lineID-1].KBA else: - KB = -KB - ''' + KB = self.lineList[lineID-1].KBA.T + # look through Bodies further on in the list (coupling with earlier Bodies will already have been taken care of) for body2 in self.bodyList[self.bodyList.index(body1)+1: ]: if body2.type in d: @@ -2675,18 +2854,20 @@ def getSystemStiffnessA(self, DOFtype="free", lines_only=False, rho=1025, g=9.81 H2 = getH(r2) # loads on body1 due to motions of body2 - K66 = np.block([[ KB , np.matmul(KB, H1)], - [np.matmul(H2.T, KB), np.matmul(np.matmul(H2, KB), H1.T)]]) + K66 = np.block([[ KB , np.matmul(KB, H2)], + [np.matmul(H1.T, KB), np.matmul(np.matmul(H2, KB), H1.T)]]) + + # Trim for only enabled DOFs of the two bodies + K66 = K66[body1.DOFs,:][:,body2.DOFs] - K[i:i+6, j:j+6] += K66 - K[j:j+6, i:i+6] += K66.T # mirror + K[i:i+body1.nDOF, j:j+body2.nDOF] += K66 # f on B1 due to x of B2 + K[j:j+body2.nDOF, i:i+body1.nDOF] += K66.T # mirror # note: the additional rotational stiffness due to change in moment arm does not apply to this cross-coupling case - endFound = 1 # signal that the line has been handled so we can move on to the next thing break - j += 6 # if this body has DOFs we're considering, then count them + j += body2.nDOF # if this body has DOFs we're considering, then count them # look through free Points @@ -2696,13 +2877,13 @@ def getSystemStiffnessA(self, DOFtype="free", lines_only=False, rho=1025, g=9.81 if lineID in point2.attached: # the line is also attached to it # only add up one off-diagonal sub-matrix for now, then we'll mirror at the end - #K[i :i+3, j:j+3] += K3 - #K[i+3:i+6, j:j+3] += np.matmul(H1.T, K3) K63 = np.vstack([KB, np.matmul(H1.T, KB)]) - K63 = K63[:,point2.DOFs] # trim the matrix to only use the enabled DOFs of each point - K[i:i+6 , j:j+point2.nDOF] += K63 - K[j:j+point2.nDOF, i:i+6 ] += K63.T # mirror + # Trim for only enabled DOFs of the point and body + K63 = K63[body1.DOFs,:][:,point2.DOFs] + + K[i:i+ body1.nDOF, j:j+point2.nDOF] += K63 # f on B1 due to x of P2 + K[j:j+point2.nDOF, i:i+ body1.nDOF] += K63.T # mirror break @@ -2710,7 +2891,7 @@ def getSystemStiffnessA(self, DOFtype="free", lines_only=False, rho=1025, g=9.81 # note: No cross-coupling with fixed points. The body's own stiffness matrix is now calculated at the start. - i += 6 # moving along to the next body... + i += body1.nDOF # moving along to the next body... # go through each movable point in the system @@ -2733,34 +2914,27 @@ def getSystemStiffnessA(self, DOFtype="free", lines_only=False, rho=1025, g=9.81 # go through movable points to see if one is attached for point2 in self.pointList[self.pointList.index(point)+1: ]: if point2.type in d: - if lineID in point2.attached: # if this point is at the other end of the line - ''' - KA, KB = self.lineList[lineID-1].getStiffnessMatrix() # get full 3x3 stiffness matrix of the line that attaches them - # flip sign for coupling - if point.attachedEndB == 1: # assuming convention of end A is attached to the first point, so if not, - KB = -KA # swap matrices of ends A and B - else: - KB = -KB - ''' + if lineID in point2.attached: # if this point is at the other end of the line + # get cross-coupling stiffness of line: force on end attached to point1 due to motion of other end if point.attachedEndB == 1: - KB = self.lineList[lineID-1].KAB + KB = self.lineList[lineID-1].KBA else: - KB = self.lineList[lineID-1].KAB.T + KB = self.lineList[lineID-1].KBA.T - KB = KB[point.DOFs,:][:,point2.DOFs] # trim the matrix to only use the enabled DOFs of each point + # Trim stiffness matrix to only use the enabled DOFs of each point + KB = KB[point.DOFs,:][:,point2.DOFs] - K[i:i+n , j:j+point2.nDOF] += KB - K[j:j+point2.nDOF, i:i+n ] += KB.T # mirror + K[i:i+n , j:j+point2.nDOF] += KB # force on P1 due to movement of P2 + K[j:j+point2.nDOF, i:i+n ] += KB.T # mirror (f on P2 due to x of P1) j += point2.nDOF # if this point has DOFs we're considering, then count them i += n - - return K + def getAnchorLoads(self, sfx, sfy, sfz, N): ''' Calculates anchor loads Parameters @@ -2790,6 +2964,13 @@ def getAnchorLoads(self, sfx, sfy, sfz, N): anchorloads.append(max(convec)) return(anchorloads) + + def getCost(self): + '''Fill in and return a cost dictionary for the System.''' + pass + # sume up quantities across line cots dict + + def ropeContact(self, lineNums, N): ''' Determines whether Node 1 is off the ground for lines in lineNums Parameters @@ -2873,7 +3054,128 @@ def checkTensions(self, N = None): print('Line does not hold tension data') return return(ratios) - + + + def activateDynamicStiffness(self, display=0): + '''Switch mooring system model to dynamic line stiffness values and + adjust the unstretched line lengths to maintain the same tensions. + If dynamic stiffnesses are already activated, it does nothing. + This only has an effect when dynamic line properties are used. ''' + + if not self.dynamic_stiffness_activated: + for line in self.lineList: + line.activateDynamicStiffness(display=display) + + self.dynamic_stiffness_activated = True + + + def revertToStaticStiffness(self): + '''Revert mooring system model back to the static stiffness + values and the original unstretched lenths.''' + + for line in self.lineList: + line.revertToStaticStiffness() + + self.dynamic_stiffness_activated = False + + + def setBathymetry(self, x, y, depth): + '''Provide a System with existing bathymetry data, rather than having + it read in its own data and store it locally. This allows reuse of + bathymetry grid data that may be stored elsewhere. It saves a ref. + + Parameters + ---------- + x : list or array + X coordinates of rectangular bathymetry grid [m]. + y : list or array + Y coordinates of rectangular bathymetry grid [m]. + depth : 2D array + Matrix of depths at x and y locations [m]. Positive depths. + Shape should be [len(x), len(y)] + ''' + # Save references to the passed data (not copy or create new arrays) + self.bathGrid_Xs = x + self.bathGrid_Ys = y + self.bathGrid = depth + self.seabedMod = 2 + + + def getDepthFromBathymetry(self, x, y): #BathymetryGrid, BathGrid_Xs, BathGrid_Ys, LineX, LineY, depth, nvec) + ''' interpolates local seabed depth and normal vector + + Parameters + ---------- + x, y : float + x and y coordinates to find depth and slope at [m] + + Returns + ------- + depth : float + local seabed depth (positive down) [m] + nvec : array of size 3 + local seabed surface normal vector (positive out) + ''' + + # if no bathymetry info stored, just return uniform depth + if self.seabedMod == 0: + return self.depth, np.array([0,0,1]) + + if self.seabedMod == 1: + depth = self.depth - self.xSlope*x - self.ySlope*y + nvec = unitVector([-self.xSlope, -self.ySlope, 1]) + return depth, nvec + + if self.seabedMod == 2: + # get interpolation indices and fractions for the relevant grid panel + ix0, fx = getInterpNums(self.bathGrid_Xs, x) + iy0, fy = getInterpNums(self.bathGrid_Ys, y) + + + # handle end case conditions + if fx == 0: + ix1 = ix0 + else: + ix1 = min(ix0+1, self.bathGrid.shape[1]) # don't overstep bounds + + if fy == 0: + iy1 = iy0 + else: + iy1 = min(iy0+1, self.bathGrid.shape[0]) # don't overstep bounds + + + # get corner points of the panel + c00 = self.bathGrid[iy0, ix0] + c01 = self.bathGrid[iy1, ix0] + c10 = self.bathGrid[iy0, ix1] + c11 = self.bathGrid[iy1, ix1] + + # get interpolated points and local value + cx0 = c00 *(1.0-fx) + c10 *fx + cx1 = c01 *(1.0-fx) + c11 *fx + c0y = c00 *(1.0-fy) + c01 *fy + c1y = c10 *(1.0-fy) + c11 *fy + depth = cx0 *(1.0-fy) + cx1 *fy + + # get local slope + dx = self.bathGrid_Xs[ix1] - self.bathGrid_Xs[ix0] + dy = self.bathGrid_Ys[iy1] - self.bathGrid_Ys[iy0] + + if dx > 0.0: + dc_dx = (c1y-c0y)/dx + else: + dc_dx = 0.0 # maybe this should raise an error + + if dy > 0.0: + dc_dy = (cx1-cx0)/dy + else: + dc_dy = 0.0 # maybe this should raise an error + + nvec = unitVector([dc_dx, dc_dy, 1.0]) # compute unit vector + + return depth, nvec + + def loadData(self, dirname, rootname, sep='.MD.'): '''Loads time series data from main MoorDyn output file (for example driver.MD.out) Parameters @@ -2914,8 +3216,6 @@ def plot(self, ax=None, bounds='default', rbound=0, color=None, **kwargs): Adds point numbers to plot in text. Default is False. endpoints: bool, optional Adds visible end points to lines. Default is False. - bathymetry: bool, optional - Creates a bathymetry map of the seabed based on an input file. Default is False. Returns ------- @@ -2934,9 +3234,12 @@ def plot(self, ax=None, bounds='default', rbound=0, color=None, **kwargs): draw_body = kwargs.get("draw_body" , True ) # toggle to draw the Bodies or not draw_clumps = kwargs.get('draw_clumps' , False ) # toggle to draw clump weights and float of the mooring system draw_anchors = kwargs.get('draw_anchors' , False ) # toggle to draw the anchors of the mooring system or not - bathymetry = kwargs.get("bathymetry" , False ) # toggle (and string) to include bathymetry or not. Can do full map based on text file, or simple squares + draw_seabed = kwargs.get('draw_seabed' , True ) # toggle to draw the seabed bathymetry or not + args_bath = kwargs.get("args_bath" , {} ) # dictionary of optional plot_surface arguments + ''' cmap_bath = kwargs.get("cmap" , 'ocean' ) # matplotlib colormap specification alpha = kwargs.get("opacity" , 1.0 ) # the transparency of the bathymetry plot_surface + ''' rang = kwargs.get('rang' , 'hold' ) # colorbar range: if range not used, set it as a placeholder, it will get adjusted later cbar_bath = kwargs.get('cbar_bath' , False ) # toggle to include a colorbar for a plot or not colortension = kwargs.get("colortension" , False ) # toggle to draw the mooring lines in colors based on node tensions @@ -3054,12 +3357,14 @@ def plot(self, ax=None, bounds='default', rbound=0, color=None, **kwargs): else: j = j + 1 if color==None and 'material' in line.type: - if 'chain' in line.type['material'] or 'Cadena80' in line.type['material']: + if 'chain' in line.type['material']: line.drawLine(time, ax, color=[.1, 0, 0], endpoints=endpoints, shadow=shadow, colortension=colortension, cmap_tension=cmap_tension) - elif 'rope' in line.type['material'] or 'polyester' in line.type['material'] or 'Dpoli169' in line.type['material']: + elif 'rope' in line.type['material'] or 'polyester' in line.type['material']: line.drawLine(time, ax, color=[.3,.5,.5], endpoints=endpoints, shadow=shadow, colortension=colortension, cmap_tension=cmap_tension) elif 'nylon' in line.type['material']: line.drawLine(time, ax, color=[.8,.8,.2], endpoints=endpoints, shadow=shadow, colortension=colortension, cmap_tension=cmap_tension) + elif 'buoy' in line.type['material']: + line.drawLine(time, ax, color=[.6,.6,.0], endpoints=endpoints, shadow=shadow, colortension=colortension, cmap_tension=cmap_tension) else: line.drawLine(time, ax, color=[0.5,0.5,0.5], endpoints=endpoints, shadow=shadow, colortension=colortension, cmap_tension=cmap_tension) else: @@ -3070,8 +3375,8 @@ def plot(self, ax=None, bounds='default', rbound=0, color=None, **kwargs): ax.text((line.rA[0]+line.rB[0])/2, (line.rA[1]+line.rB[1])/2, (line.rA[2]+line.rB[2])/2, j) if cbar_tension: - maxten = max([max(line.getLineTens()) for line in self.lineList]) # find the max tension in the System - minten = min([min(line.getLineTens()) for line in self.lineList]) # find the min tension in the System + maxten = max([max(line.Ts) for line in self.lineList]) # find the max tension in the System + minten = min([min(line.Ts) for line in self.lineList]) # find the min tension in the System bounds = range(int(minten),int(maxten), int((maxten-minten)/256)) norm = mpl.colors.BoundaryNorm(bounds, 256) # set the bounds in a norm object, with 256 being the length of all colorbar strings fig.colorbar(cm.ScalarMappable(norm=norm, cmap=cmap_tension), label='Tension (N)') # add the colorbar @@ -3084,8 +3389,8 @@ def plot(self, ax=None, bounds='default', rbound=0, color=None, **kwargs): i = i + 1 if pointlabels == True: ax.text(point.r[0], point.r[1], point.r[2], i, c = 'r') - - if bathymetry==True: # if bathymetry is true, then make squares at each anchor point + ''' MH: This needs to be way more generalized/versatile if it's going to be included! + if draw_seabed: # if bathymetry is true, then make squares at each anchor point if point.attachedEndB[0] == 0 and point.r[2] < -400: points.append([point.r[0]+250, point.r[1]+250, point.r[2]]) points.append([point.r[0]+250, point.r[1]-250, point.r[2]]) @@ -3094,32 +3399,19 @@ def plot(self, ax=None, bounds='default', rbound=0, color=None, **kwargs): Z = np.array(points) verts = [[Z[0],Z[1],Z[2],Z[3]]] - ax.add_collection3d(Poly3DCollection(verts, facecolors='limegreen', linewidths=1, edgecolors='g', alpha=alpha)) - - if isinstance(bathymetry, str): # or, if it's a string, load in the bathymetry file + ax.add_collection3d(Poly3DCollection(verts, facecolors='limegreen', linewidths=1, edgecolors='g', alpha=1.0)) + ''' + + # draw the seabed if requested (only works for full bathymetries so far) + if draw_seabed and self.seabedMod == 2: - # parse through the MoorDyn bathymetry file - bathGrid_Xs, bathGrid_Ys, bathGrid = self.readBathymetryFile(bathymetry) if rang=='hold': - rang = (np.min(-bathGrid), np.max(-bathGrid)) - ''' - # First method: plot nice 2D squares using Poly3DCollection - nX = len(bathGrid_Xs) - nY = len(bathGrid_Ys) - # store a list of points in the grid - Z = [[bathGrid_Xs[j],bathGrid_Ys[i],-bathGrid[i,j]] for i in range(nY) for j in range(nX)] - # plot every square in the grid (e.g. 16 point grid yields 9 squares) - verts = [] - for i in range(nY-1): - for j in range(nX-1): - verts.append([Z[j+nX*i],Z[(j+1)+nX*i],Z[(j+1)+nX*(i+1)],Z[j+nX*(i+1)]]) - ax.add_collection3d(Poly3DCollection(verts, facecolors='limegreen', linewidths=1, edgecolors='g', alpha=0.5)) - verts = [] - ''' - # Second method: plot a 3D surface, plot_surface - X, Y = np.meshgrid(bathGrid_Xs, bathGrid_Ys) + rang = (np.min(-self.bathGrid), np.max(-self.bathGrid)) + + X, Y = np.meshgrid(self.bathGrid_Xs, self.bathGrid_Ys) - bath = ax.plot_surface(X,Y,-bathGrid, cmap=cmap_bath, vmin=rang[0], vmax=rang[1], alpha=alpha) + bath = ax.plot_surface(X,Y,-self.bathGrid, vmin=rang[0], vmax=rang[1], + rstride=1, cstride=1, **args_bath) if cbar_bath_size!=1.0: # make sure the colorbar is turned on just in case it isn't when the other colorbar inputs are used cbar_bath=True @@ -3133,7 +3425,6 @@ def plot(self, ax=None, bounds='default', rbound=0, color=None, **kwargs): waterX, waterY = np.meshgrid(waterXs, waterYs) ax.plot_surface(waterX, waterY, np.array([[-50,-50],[-50,-50]]), alpha=0.5) - fig.suptitle(title) set_axes_equal(ax) @@ -3179,7 +3470,7 @@ def plot2d(self, Xuvec=[1,0,0], Yuvec=[0,0,1], ax=None, color=None, **kwargs): pointlabels = kwargs.get('pointlabels' , False ) # toggle to include point number labels in the plot draw_body = kwargs.get("draw_body" , False ) # toggle to draw the Bodies or not draw_anchors = kwargs.get('draw_anchors' , False ) # toggle to draw the anchors of the mooring system or not - bathymetry = kwargs.get("bathymetry" , False ) # toggle (and string) to include bathymetry contours or not based on text file + draw_seabed = kwargs.get('draw_seabed' , True ) # toggle to draw the seabed bathymetry or not cmap_bath = kwargs.get("cmap_bath" , 'ocean' ) # matplotlib colormap specification alpha = kwargs.get("opacity" , 1.0 ) # the transparency of the bathymetry plot_surface rang = kwargs.get('rang' , 'hold' ) # colorbar range: if range not used, set it as a placeholder, it will get adjusted later @@ -3196,6 +3487,8 @@ def plot2d(self, Xuvec=[1,0,0], Yuvec=[0,0,1], ax=None, color=None, **kwargs): plotnodesline = kwargs.get('plotnodesline' , [] ) # the list of line numbers that match up with the desired node to be plotted label = kwargs.get('label' , "" ) # the label/marker name of a line in the System draw_fairlead = kwargs.get('draw_fairlead' , False ) # toggle to draw large points for the fairleads + line_width = kwargs.get('linewidth' , 1 ) # toggle to set the mooring line width in "drawLine2d + @@ -3252,13 +3545,13 @@ def plot2d(self, Xuvec=[1,0,0], Yuvec=[0,0,1], ax=None, color=None, **kwargs): j = j + 1 if color==None and 'material' in line.type: if 'chain' in line.type['material']: - line.drawLine2d(time, ax, color=[.1, 0, 0], Xuvec=Xuvec, Yuvec=Yuvec, colortension=colortension, cmap=cmap_tension, plotnodes=plotnodes, plotnodesline=plotnodesline, label=label, alpha=alpha) + line.drawLine2d(time, ax, color=[.1, 0, 0], Xuvec=Xuvec, Yuvec=Yuvec, colortension=colortension, cmap=cmap_tension, plotnodes=plotnodes, plotnodesline=plotnodesline, label=label, alpha=alpha, linewidth=line_width) elif 'rope' in line.type['material'] or 'polyester' in line.type['material']: - line.drawLine2d(time, ax, color=[.3,.5,.5], Xuvec=Xuvec, Yuvec=Yuvec, colortension=colortension, cmap=cmap_tension, plotnodes=plotnodes, plotnodesline=plotnodesline, label=label, alpha=alpha) + line.drawLine2d(time, ax, color=[.3,.5,.5], Xuvec=Xuvec, Yuvec=Yuvec, colortension=colortension, cmap=cmap_tension, plotnodes=plotnodes, plotnodesline=plotnodesline, label=label, alpha=alpha, linewidth=line_width) else: - line.drawLine2d(time, ax, color=[0.3,0.3,0.3], Xuvec=Xuvec, Yuvec=Yuvec, colortension=colortension, cmap=cmap_tension, plotnodes=plotnodes, plotnodesline=plotnodesline, label=label, alpha=alpha) + line.drawLine2d(time, ax, color=[0.3,0.3,0.3], Xuvec=Xuvec, Yuvec=Yuvec, colortension=colortension, cmap=cmap_tension, plotnodes=plotnodes, plotnodesline=plotnodesline, label=label, alpha=alpha, linewidth=line_width) else: - line.drawLine2d(time, ax, color=color, Xuvec=Xuvec, Yuvec=Yuvec, colortension=colortension, cmap=cmap_tension, plotnodes=plotnodes, plotnodesline=plotnodesline, label=label, alpha=alpha) + line.drawLine2d(time, ax, color=color, Xuvec=Xuvec, Yuvec=Yuvec, colortension=colortension, cmap=cmap_tension, plotnodes=plotnodes, plotnodesline=plotnodesline, label=label, alpha=alpha, linewidth=line_width) # Add Line labels if linelabels == True: @@ -3267,8 +3560,8 @@ def plot2d(self, Xuvec=[1,0,0], Yuvec=[0,0,1], ax=None, color=None, **kwargs): ax.text(xloc,yloc,j) if cbar_tension: - maxten = max([max(line.getLineTens()) for line in self.lineList]) # find the max tension in the System - minten = min([min(line.getLineTens()) for line in self.lineList]) # find the min tension in the System + maxten = max([max(line.Ts) for line in self.lineList]) # find the max tension in the System + minten = min([min(line.Ts) for line in self.lineList]) # find the min tension in the System bounds = range(int(minten),int(maxten), int((maxten-minten)/256)) norm = mpl.colors.BoundaryNorm(bounds, 256) # set the bounds in a norm object, with 256 being the length of all colorbar strings fig.colorbar(cm.ScalarMappable(norm=norm, cmap=cmap_tension), label='Tension (N)') # add the colorbar @@ -3283,13 +3576,11 @@ def plot2d(self, Xuvec=[1,0,0], Yuvec=[0,0,1], ax=None, color=None, **kwargs): yloc = np.dot([point.r[0], point.r[1], point.r[2]], Yuvec) ax.text(xloc, yloc, i, c = 'r') - if isinstance(bathymetry, str): # or, if it's a string, load in the bathymetry file - - # parse through the MoorDyn bathymetry file - bathGrid_Xs, bathGrid_Ys, bathGrid = self.readBathymetryFile(bathymetry) + # draw the seabed if requested (only works for full bathymetries so far) + if draw_seabed and self.seabedMod == 2: - X, Y = np.meshgrid(bathGrid_Xs, bathGrid_Ys) - Z = -bathGrid + X, Y = np.meshgrid(self.bathGrid_Xs, self.bathGrid_Ys) + Z = -self.bathGrid if rang=='hold': rang = (np.min(Z), np.max(Z)) diff --git a/pyproject.toml b/pyproject.toml index bd63e44..70c1105 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "MoorPy" -version = "1.0.2" +version = "1.1.0" description = "A design-oriented mooring system library for Python" readme = "README.md" requires-python = ">=3.8" diff --git a/tests/case1.dat b/tests/case1.dat new file mode 100644 index 0000000..496c78f --- /dev/null +++ b/tests/case1.dat @@ -0,0 +1,35 @@ +MoorDyn Input File +single catenary line +----------------------- LINE TYPES ------------------------------------------ +Name Diam MassDen EA BA/-zeta EI Cd Ca CdAx CaAx +(-) (m) (kg/m) (N) (N-s/-) (-) (-) (-) (-) (-) +main 0.2 500.00 2.0E+09 -1 0 1 0.27 1 0.20 +---------------------- POINTS -------------------------------- +Node Type X Y Z M V CdA CA +(-) (-) (m) (m) (m) (kg) (m^3) (m^2) (-) +1 Fixed -800.0 0.0 -300.0 0 0 0 0 +2 Coupled 0.0 0.0 0.0 0 0 0 0 +---------------------- LINES -------------------------------------- +Line LineType NodeAnch NodeFair UnstrLen NumSegs Flags/Outputs +(-) (-) (-) (-) (m) (-) (-) +1 main 1 2 900.00 40 pt +---------------------- SOLVER OPTIONS --------------------------------------- +300 WtrDpth +0.001 dtM - time step to use in mooring integration (s) +3.0e6 kbot - bottom stiffness (Pa/m) +3.0e5 cbot - bottom damping (Pa-s/m) +1.0 dtIC - time interval for analyzing convergence during IC gen (s) +50.0 TmaxIC - max time for ic gen (s) +4.0 CdScaleIC - factor by which to scale drag coefficients during dynamic relaxation (-) +0.001 threshIC - threshold for IC convergence (-) +------------------------ OUTPUTS -------------------------------------------- +FairTen1 +AnchTen1 +Con1fx +Con1fy +Con1fz +Con2fx +Con2fy +Con2fz +------------------------- need this line -------------------------------------- + diff --git a/tests/case2.dat b/tests/case2.dat new file mode 100644 index 0000000..349c13f --- /dev/null +++ b/tests/case2.dat @@ -0,0 +1,38 @@ +MoorDyn Input File +catenary plus rope +----------------------- LINE TYPES ------------------------------------------ +Name Diam MassDen EA BA/-zeta EI Cd Ca CdAx CaAx +(-) (m) (kg/m) (N) (N-s/-) (-) (-) (-) (-) (-) +main 0.2 200.00 2.0E+09 -1 0 4 0.27 4 0.20 +polyester 0.15 25 30e6 -0.8 0 4 1.0 4 0.0 +---------------------- POINTS -------------------------------- +Node Type X Y Z M V CdA CA +(-) (-) (m) (m) (m) (kg) (m^3) (m^2) (-) +1 Fixed -800.0 0.0 -300.0 0 0 0 0 +2 Free -400.0 0.0 -100.0 0 0 0 0 +3 Coupled 0.0 0.0 0.0 0 0 0 0 +---------------------- LINES -------------------------------------- +Line LineType NodeAnch NodeFair UnstrLen NumSegs Flags/Outputs +(-) (-) (-) (-) (m) (-) (-) +1 main 1 2 500.00 25 pt +2 polyester 2 3 380.00 15 pt +---------------------- SOLVER OPTIONS --------------------------------------- +300 WtrDpth +0.001 dtM - time step to use in mooring integration (s) +3.0e6 kbot - bottom stiffness (Pa/m) +3.0e5 cbot - bottom damping (Pa-s/m) +1.0 dtIC - time interval for analyzing convergence during IC gen (s) +50.0 TmaxIC - max time for ic gen (s) +4.0 CdScaleIC - factor by which to scale drag coefficients during dynamic relaxation (-) +0.001 threshIC - threshold for IC convergence (-) +------------------------ OUTPUTS -------------------------------------------- +FairTen1 +AnchTen1 +Con1fx +Con1fy +Con1fz +Con2fx +Con2fy +Con2fz +------------------------- need this line -------------------------------------- + diff --git a/tests/case3.dat b/tests/case3.dat new file mode 100644 index 0000000..fe8856d --- /dev/null +++ b/tests/case3.dat @@ -0,0 +1,40 @@ +MoorDyn Input File +single catenary line from VolturnUS-S +----------------------- LINE TYPES ------------------------------------------ +Name Diam MassDen EA BA/-zeta EI Cd Ca CdAx CaAx +(-) (m) (kg/m) (N) (N-s/-) (-) (-) (-) (-) (-) +main 0.2 500.00 2.0E+09 -1 0 1 0.27 1 0.20 +---------------------- POINTS -------------------------------- +Node Type X Y Z M V CdA CA +(-) (-) (m) (m) (m) (kg) (m^3) (m^2) (-) +1 Fixed -800.0 0.0 -300.0 0 0 0 0 +2 Free -400.0 0.0 -100.0 0 200 0 0 +3 Free -200.0 0.0 -100.0 1e5 0 0 0 +4 Coupled 0.0 0.0 0.0 0 0 0 0 +---------------------- LINES -------------------------------------- +Line LineType NodeAnch NodeFair UnstrLen NumSegs Flags/Outputs +(-) (-) (-) (-) (m) (-) (-) +1 main 1 2 400.00 20 pt +2 main 2 3 250.00 10 pt +3 main 3 4 250.00 10 pt +---------------------- SOLVER OPTIONS --------------------------------------- +2 writeLog +300 WtrDpth +0.001 dtM - time step to use in mooring integration (s) +3.0e6 kbot - bottom stiffness (Pa/m) +3.0e5 cbot - bottom damping (Pa-s/m) +1.0 dtIC - time interval for analyzing convergence during IC gen (s) +0.0 TmaxIC - max time for ic gen (s) +4.0 CdScaleIC - factor by which to scale drag coefficients during dynamic relaxation (-) +0.001 threshIC - threshold for IC convergence (-) +------------------------ OUTPUTS -------------------------------------------- +FairTen1 +AnchTen1 +Con1fx +Con1fy +Con1fz +Con2fx +Con2fy +Con2fz +------------------------- need this line -------------------------------------- + diff --git a/tests/case4.dat b/tests/case4.dat new file mode 100644 index 0000000..148c264 --- /dev/null +++ b/tests/case4.dat @@ -0,0 +1,32 @@ +MoorDyn input file for matching RM3 WEC umbilical cable at 50 m +NOTE: Re-running this file in MD_C using go.py gives different results each run!! +---------------------- LINE DICTIONARY ----------------------------------------------------- +LineType Diam MassDenInAir EA cIntDamp EI Cd Ca CdAx CaAx +(-) (m) (kg/m) (N) (Pa-s) (N-m^2) (-) (-) (-) (-) +cable 0.2 200 2e9 -0.8 0 1.0 0.0 1.2 0.008 +buoyancy 0.7 200 2e9 -0.8 0 1.0 0.0 1.2 0.008 +---------------------- POINTS ----------------------------------------------------- +Node Type X Y Z M V CdA Ca +(-) (-) (m) (m) (m) (kg) (m^3) (m2) () +1 Fixed -800 0 -300 0 0 0 0 +2 Free -300 0 -200 0 0 0 0 +3 Free -100 0 -200 0 0 0 0 +4 Fixed 0 0 0 0 0 0 0 # NOTE: this was coupled before +---------------------- LINES ----------------------------------------------------- +Line LineType NodeA NodeB UnstrLen NumSegs Flags/Outputs +(-) (-) (-) (-) (m) (-) (-) +1 cable 1 2 360 18 pt +2 buoyancy 2 3 240 12 pt +3 cable 3 4 360 18 pt +---------------------- SOLVER OPTIONS ---------------------------------------- +2 writeLog +0.001 dtM - time step to use in mooring integration +0.5 dtOut +1.5e5 kb - bottom stiffness Senu used 1.5e6 but for MD this causes pretty bad bumps on seabed contact +1e4 cb - bottom damping Senu used zero +300 WtrDpth - water depth +5.0 ICDfac - factor by which to scale drag coefficients during dynamic relaxation IC gen +0.0001 ICthresh - threshold for IC convergence +0 TmaxIC - threshold for IC convergence +---------------------- OUTPUT ----------------------------------------- +--------------------- need this line ------------------ \ No newline at end of file diff --git a/tests/case5.dat b/tests/case5.dat new file mode 100644 index 0000000..763c7bb --- /dev/null +++ b/tests/case5.dat @@ -0,0 +1,35 @@ +MoorDyn Input File +single catenary line from VolturnUS-S +----------------------- LINE TYPES ------------------------------------------ +Name Diam MassDen EA BA/-zeta EI Cd Ca CdAx CaAx +(-) (m) (kg/m) (N) (N-s/-) (-) (-) (-) (-) (-) +main 0.2 500.0 2E+09 -1 0 1 0.27 1 0.20 +---------------------- POINTS -------------------------------- +Node Type X Y Z M V CdA CA +(-) (-) (m) (m) (m) (kg) (m^3) (m^2) (-) +1 Fixed -400.0 0.0 -300.0 0 0 0 0 +2 Coupled 0.0 0.0 0.0 0 0 0 0 +---------------------- LINES -------------------------------------- +Line LineType NodeAnch NodeFair UnstrLen NumSegs Flags/Outputs +(-) (-) (-) (-) (m) (-) (-) +1 main 1 2 890.00 40 pt +---------------------- SOLVER OPTIONS --------------------------------------- +300 WtrDpth +0.001 dtM - time step to use in mooring integration (s) +3.0e6 kbot - bottom stiffness (Pa/m) +3.0e5 cbot - bottom damping (Pa-s/m) +1.0 dtIC - time interval for analyzing convergence during IC gen (s) +50.0 TmaxIC - max time for ic gen (s) +4.0 CdScaleIC - factor by which to scale drag coefficients during dynamic relaxation (-) +0.001 threshIC - threshold for IC convergence (-) +------------------------ OUTPUTS -------------------------------------------- +FairTen1 +AnchTen1 +Con1fx +Con1fy +Con1fz +Con2fx +Con2fy +Con2fz +------------------------- need this line -------------------------------------- + diff --git a/tests/case6.dat b/tests/case6.dat new file mode 100644 index 0000000..c14870d --- /dev/null +++ b/tests/case6.dat @@ -0,0 +1,39 @@ +MoorDyn Input File +W shape with seabed contact +----------------------- LINE TYPES ------------------------------------------ +Name Diam MassDen EA BA/-zeta EI Cd Ca CdAx CaAx +(-) (m) (kg/m) (N) (N-s/-) (-) (-) (-) (-) (-) +main 0.2 500.00 2.0E+09 -1 0 1 0.27 1 0.20 +---------------------- POINTS -------------------------------- +Node Type X Y Z M V CdA CA +(-) (-) (m) (m) (m) (kg) (m^3) (m^2) (-) +1 Fixed -400.0 0.0 0.0 0 0 0 0 +2 Free 0.0 0.0 -100.0 0 200 0 0 +3 Free 0.0 200.0 -200.0 0 0 0 0 +4 Fixed 0.0 400.0 0.0 0 0 0 0 +---------------------- LINES -------------------------------------- +Line LineType NodeAnch NodeFair UnstrLen NumSegs Flags/Outputs +(-) (-) (-) (-) (m) (-) (-) +1 main 1 2 660.00 20 pt +2 main 2 3 330.00 10 pt +3 main 3 4 330.00 10 pt +---------------------- SOLVER OPTIONS --------------------------------------- +300 WtrDpth +0.001 dtM - time step to use in mooring integration (s) +3.0e6 kbot - bottom stiffness (Pa/m) +3.0e5 cbot - bottom damping (Pa-s/m) +1.0 dtIC - time interval for analyzing convergence during IC gen (s) +50.0 TmaxIC - max time for ic gen (s) +4.0 CdScaleIC - factor by which to scale drag coefficients during dynamic relaxation (-) +0.001 threshIC - threshold for IC convergence (-) +------------------------ OUTPUTS -------------------------------------------- +FairTen1 +AnchTen1 +Con1fx +Con1fy +Con1fz +Con2fx +Con2fy +Con2fz +------------------------- need this line -------------------------------------- + diff --git a/tests/case7a.dat b/tests/case7a.dat new file mode 100644 index 0000000..2e466a2 --- /dev/null +++ b/tests/case7a.dat @@ -0,0 +1,48 @@ +MoorDyn Input File +Case 7: Single catenary line attached to body to test 6DOF stifness +----------------------- LINE TYPES ------------------------------------------ +Name Diam MassDen EA BA/-zeta EI Cd Ca CdAx CaAx +(-) (m) (kg/m) (N) (N-s/-) (-) (-) (-) (-) (-) +main 0.2 200.00 2.0E+09 -1 0 4 0.27 4 0.20 +----------------------- BODIES ------------------------------------------------------ +ID Attachment X0 Y0 Z0 r0 p0 y0 Mass CG* I* Volume CdA* Ca* +(#) (-) (m) (m) (m) (deg) (deg) (deg) (kg) (m) (kg-m^2) (m^3) (m^2) (-) +1 coupled 0.00 0.00 0.00 0.00 0.00 0.00 0.0 0.0 0.000e+00 0.0 0.00 0.00 +---------------------- POINTS -------------------------------- +Node Type X Y Z M V CdA CA +(-) (-) (m) (m) (m) (kg) (m^3) (m^2) (-) +1 Fixed 100.0 0.0 -50.0 0 0 0 0 +2 Body1 0.0 0.0 -10.0 0 0 0 0 +---------------------- LINES -------------------------------------- +Line LineType EndA EndB UnstrLen NumSegs Flags/Outputs +(-) (-) (-) (-) (m) (-) (-) +1 main 1 2 112.00 80 pt +---------------------- SOLVER OPTIONS --------------------------------------- +50 WtrDpth +0.0002 dtM - time step to use in mooring integration (s) +3.0e6 kbot - bottom stiffness (Pa/m) +3.0e5 cbot - bottom damping (Pa-s/m) +1.0 dtIC - time interval for analyzing convergence during IC gen (s) +60.0 TmaxIC - max time for ic gen (s) +4.0 CdScaleIC - factor by which to scale drag coefficients during dynamic relaxation (-) +0.001 threshIC - threshold for IC convergence (-) +------------------------ OUTPUTS -------------------------------------------- +FairTen1 +AnchTen1 +Con1fx +Con1fy +Con1fz +Con2fx +Con2fy +Con2fz +Con2px +Con2py +Con2pz +Body1fx +Body1fy +Body1fz +Body1mx +Body1my +Body1mz +------------------------- need this line -------------------------------------- + diff --git a/tests/case7b.dat b/tests/case7b.dat new file mode 100644 index 0000000..e01f402 --- /dev/null +++ b/tests/case7b.dat @@ -0,0 +1,48 @@ +MoorDyn Input File +Case 7: Single catenary line attached to body to test 6DOF stifness - case b: 3Doffset +----------------------- LINE TYPES ------------------------------------------ +Name Diam MassDen EA BA/-zeta EI Cd Ca CdAx CaAx +(-) (m) (kg/m) (N) (N-s/-) (-) (-) (-) (-) (-) +main 0.2 200.00 2.0E+09 -1 0 4 0.27 4 0.20 +----------------------- BODIES ------------------------------------------------------ +ID Attachment X0 Y0 Z0 r0 p0 y0 Mass CG* I* Volume CdA* Ca* +(#) (-) (m) (m) (m) (deg) (deg) (deg) (kg) (m) (kg-m^2) (m^3) (m^2) (-) +1 coupled 0.00 0.00 0.00 0.00 0.00 0.00 0.0 0.0 0.000e+00 0.0 0.00 0.00 +---------------------- POINTS -------------------------------- +Node Type X Y Z M V CdA CA +(-) (-) (m) (m) (m) (kg) (m^3) (m^2) (-) +1 Fixed 100.0 0.0 -50.0 0 0 0 0 +2 Body1 5.0 3.0 -10.0 0 0 0 0 +---------------------- LINES -------------------------------------- +Line LineType EndA EndB UnstrLen NumSegs Flags/Outputs +(-) (-) (-) (-) (m) (-) (-) +1 main 1 2 107.00 80 pt +---------------------- SOLVER OPTIONS --------------------------------------- +50 WtrDpth +0.0002 dtM - time step to use in mooring integration (s) +3.0e6 kbot - bottom stiffness (Pa/m) +3.0e5 cbot - bottom damping (Pa-s/m) +1.0 dtIC - time interval for analyzing convergence during IC gen (s) +60.0 TmaxIC - max time for ic gen (s) +4.0 CdScaleIC - factor by which to scale drag coefficients during dynamic relaxation (-) +0.001 threshIC - threshold for IC convergence (-) +------------------------ OUTPUTS -------------------------------------------- +FairTen1 +AnchTen1 +Con1fx +Con1fy +Con1fz +Con2fx +Con2fy +Con2fz +Con2px +Con2py +Con2pz +Body1fx +Body1fy +Body1fz +Body1mx +Body1my +Body1mz +------------------------- need this line -------------------------------------- + diff --git a/tests/case8.dat b/tests/case8.dat new file mode 100644 index 0000000..592b994 --- /dev/null +++ b/tests/case8.dat @@ -0,0 +1,59 @@ +Shared Moorings Project - DTU 10MW Turbine - Hywind-like Spar - Baseline Design +---------------------- LINE TYPES ----------------------------------------------------- +Name Diam MassDen EA BA/-zeta EI Cd Ca CdAx CaAx +(-) (m) (kg/m) (N) (N-s/-) (-) (-) (-) (-) (-) +polyester 0.15 25 30e6 -1 0 4 1.0 4 0.0 +----------------------- BODIES ------------------------------------------------------ +ID Attachment X0 Y0 Z0 r0 p0 y0 Mass CG* I* Volume CdA* Ca* +(#) (-) (m) (m) (m) (deg) (deg) (deg) (kg) (m) (kg-m^2) (m^3) (m^2) (-) +1 coupled 0.00 0.00 0.00 0.00 0.00 0.00 0.0 0.0 0.0 0.0 0.00 0.00 +---------------------- POINTS -------------------------------- +Node Type X Y Z M V CdA CA +(-) (-) (m) (m) (m) (kg) (m^3) (m^2) (-) +1 Fixed 172.7 -100.0 -100.0 0 0 0 0 +2 Body1 6.82 3.9375 -21.0 0 0 0 0 +3 Body1 0.00 -7.875 -21.0 0 0 0 0 +4 Connect 34.64 -20.00 -50.0 0 0 0 0 +5 Fixed 0.0 200.0 -100.0 0 0 0 0 +6 Body1 -6.82 3.9375 -21.0 0 0 0 0 +7 Body1 6.82 3.9375 -21.0 0 0 0 0 +8 Connect 0.0 40.00 -50.0 0 0 0 0 +9 Fixed -172.7 -100.0 -100.0 0 0 0 0 +10 Body1 0.00 -7.875 -21.0 0 0 0 0 +11 Body1 -6.82 3.9375 -21.0 0 0 0 0 +12 Connect -34.64 -20.00 -50.0 0 0 0 0 +---------------------- LINES ----------------------------------------------------- +Line LineType EndA EndB UnstrLen NumSegs Flags/Outputs +(-) (-) (-) (-) (m) (-) (-) +1 polyester 4 2 45.36 4 - +2 polyester 4 3 45.36 4 - +3 polyester 1 4 160 10 - +4 polyester 8 6 45.36 4 - +5 polyester 8 7 45.36 4 - +6 polyester 5 8 160 10 - +7 polyester 12 10 45.36 4 - +8 polyester 12 11 45.36 4 - +9 polyester 9 12 160 10 - +---------------------- SOLVER OPTIONS ---------------------------------------- +100 depth +0.001 dtM - time step to use in mooring integration (s) +3e6 kbot - bottom stiffness (Pa/m) +3e5 cbot - bottom damping (Pa-s/m) +2 dtIC - time interval for analyzing convergence during IC gen (s) +600 TmaxIC - max time for ic gen (s) +10 CdScaleIC - factor by which to scale drag coefficients during dynamic relaxation (-) +0.01 threshIC - threshold for IC convergence (-) +----------------------------OUTPUTS-------------------------------------------- +FairTen2 +FairTen3 +FairTen5 +FairTen6 +FairTen8 +FairTen9 +Body1fx +Body1fy +Body1fz +Body1mx +Body1my +Body1mz +--------------------- need this line ------------------ \ No newline at end of file diff --git a/tests/case9.dat b/tests/case9.dat new file mode 100644 index 0000000..d18c6bb --- /dev/null +++ b/tests/case9.dat @@ -0,0 +1,61 @@ +test case for two bodies with one shared mooring line between them +---------------------- LINE TYPES ----------------------------------------------------- +Name Diam MassDen EA BA/-zeta EI Cd Ca CdAx CaAx +(-) (m) (kg/m) (N) (N-s/-) (-) (-) (-) (-) (-) +main 0.2 200.00 2.0E+09 -1 0 30 0.27 30 0.20 +----------------------- BODIES ------------------------------------------------------ +ID Attachment X0 Y0 Z0 r0 p0 y0 Mass CG* I* Volume CdA* Ca* +(#) (-) (m) (m) (m) (deg) (deg) (deg) (kg) (m) (kg-m^2) (m^3) (m^2) (-) +1 coupled 0.00 0.00 0.00 0.00 0.00 0.00 0.0 0.0 0.0 0.0 0.00 0.00 +2 coupled 200.00 0.00 0.00 0.00 0.00 0.00 0.0 0.0 0.0 0.0 0.00 0.00 +---------------------- POINTS -------------------------------- +Node Type X Y Z M V CdA CA +(-) (-) (m) (m) (m) (kg) (m^3) (m^2) (-) +1 Fixed -141.4 -141.4 -100.0 0 0 0 0 +2 Fixed -141.4 141.4 -100.0 0 0 0 0 +3 Fixed 341.4 -141.4 -100.0 0 0 0 0 +4 Fixed 341.4 141.4 -100.0 0 0 0 0 +5 Body1 -14.14 -14.14 -20.0 0 0 0 0 +6 Body1 -14.14 14.14 -20.0 0 0 0 0 +7 Body2 14.14 -14.14 -20.0 0 0 0 0 +8 Body2 14.14 14.14 -20.0 0 0 0 0 +9 Body1 20.0 0.0 -20.0 0 0 0 0 +10 Body2 -20.0 0.0 -20.0 0 0 0 0 +---------------------- LINES ----------------------------------------------------- +Line LineType EndA EndB UnstrLen NumSegs Flags/Outputs +(-) (-) (-) (-) (m) (-) (-) +1 main 1 5 210.0 40 - +2 main 2 6 210.0 40 - +3 main 3 7 210.0 40 - +4 main 4 8 210.0 40 - +5 main 9 10 168.2 40 - +---------------------- SOLVER OPTIONS ---------------------------------------- +100 depth +0.0004 dtM - time step to use in mooring integration (s) +3e6 kbot - bottom stiffness (Pa/m) +3e5 cbot - bottom damping (Pa-s/m) +2 dtIC - time interval for analyzing convergence during IC gen (s) +600 TmaxIC - max time for ic gen (s) +4 CdScaleIC - factor by which to scale drag coefficients during dynamic relaxation (-) +0.01 threshIC - threshold for IC convergence (-) +0.1 dtOut +----------------------------OUTPUTS-------------------------------------------- +FairTen1 +FairTen2 +FairTen3 +FairTen4 +FairTen5 +AnchTen5 +Body1fx +Body1fy +Body1fz +Body1mx +Body1my +Body1mz +Body2fx +Body2fy +Body2fz +Body2mx +Body2my +Body2mz +--------------------- need this line ------------------ \ No newline at end of file diff --git a/tests/test_catenary.py b/tests/test_catenary.py index c872775..ef33f6d 100644 --- a/tests/test_catenary.py +++ b/tests/test_catenary.py @@ -13,7 +13,11 @@ [ 400, 200, 500.0, 7510000000000.0, 800.0 , 0.1 , 0, 0], [ 400, 200, 500.0, 7510000000000.0, 200.0 , -372.7 , 0, 0], [ 89.9, 59.2, 130.0, 751000000.0, 881.05, -372.7 , 0, 0], - [ 37.96888656874307, 20.49078283711694, 100.0, 751000000.0, -881.0549577007893, -1245.2679469540894, 63442.20077641379, -27995.71383270186]] + [ 37.96888656874307, 20.49078283711694, 100.0, 751000000.0, -881.0549577007893, -1245.2679469540894, 63442.20077641379, -27995.71383270186], + [ 246.22420940032947, 263.55766330843164, 360.63566262396927-0.01, 700e6, 3.08735, 0, 0, 0], # near-taut tests + [ 246.22420940032947, 263.55766330843164, 360.63566262396927 , 700e6, 3.08735, 0, 0, 0], # near-taut tests (hardest one) + [ 246.22420940032947, 263.55766330843164, 360.63566262396927+0.01, 700e6, 3.08735, 0, 0, 0], # near-taut tests + [ 119.3237383002058, 52.49668849178113, 130.36140355177318, 700000000.0, 34.91060469991227, 0, 9298749.157482728, 4096375.3052922436]] # hard starting point near-taut case # desired results of: fAH, fAV, fBH, fBV, LBot desired = [[0.0, 0.0, -96643.43501616362, -237751.75997440016, 202.81030003199982], @@ -21,7 +25,11 @@ [80418.60434855078, 0.0, -96643.42877136687, -237751.75577183915, 202.81030528520108], [43694.580989249596, -22365.599350216216, -43694.580989249596, -77634.40064978378, 0], [31373.229006023103, -26650.341270116318, -31373.229006023103, -87886.15872988368, 0], - [6428.437434537766, 53178.729367882406, -6428.437434537766, 34926.76640219652, 0]] + [6428.437434537766, 53178.729367882406, -6428.437434537766, 34926.76640219652, 0], + [71116.8, 75567.3, -71116.8, -76680.6, 0], + [56804.6, 60803.4, -56804.6, -60803.4, 0], + [46077.1, 48765.2, -46077.1, -49878.6, 0], + [72694.145, 29715.173, -72694.145, -34266.168, 0]] @pytest.mark.parametrize('index', range(len(indata))) @@ -43,10 +51,22 @@ def test_catenary_symmetricU(): (fAHU, fAVU, fBHU, fBVU, infoU) = catenary(100, 0, 130, 1e12, 100.0, CB=-20, Tol=0.00001, MaxIter=50) assert_allclose([fAHU, fAVU, fBHU, fBVU], [-fBH1, fBV1, fBH1, fBV1], rtol=1e-05, atol=0, verbose=True) + + +def test_sloped_mirror(): + '''Tests two mirror-image sloped seabed scenarios''' + + # seabed slope is 10 deg. dz = (60 m)*tan(alpha) = 10.5796 + hB = 40 - 10.5796 + + (fAH1, fAV1, fBH1, fBV1, info1) = catenary(60, 40, 80, 1e12, 100.0, CB= 0, alpha= 10, Tol=0.00001, MaxIter=50) + (fAH2, fAV2, fBH2, fBV2, info2) = catenary(60, -40, 80, 1e12, 100.0, CB=-hB, alpha=-10, Tol=0.00001, MaxIter=50) + + assert_allclose([fAH1, fAV1, fBH1, fBV1], [-fBH2, fBV2, -fAH2, fAV2], rtol=1e-05, atol=0, verbose=True) + if __name__ == '__main__': for i in range(len(indata)): test_catenary_solutions(i) - - #catenary(0.007335040615956245, 46.969250518704726, 100.0, 257826627.22942558, 512.1255141001664, CB=-532.0307494812953, HF0=2169047.825684437, VF0=1165782.713912318, Tol=2.0000000000000003e-06, MaxIter=50, plots=1) + \ No newline at end of file diff --git a/tests/test_line.py b/tests/test_line.py new file mode 100644 index 0000000..c9b0ea6 --- /dev/null +++ b/tests/test_line.py @@ -0,0 +1,55 @@ +# tests MoorPy Line functionality and results (work in progres) + +import pytest + +from numpy.testing import assert_allclose + +import numpy as np +import moorpy as mp +#from moorpy.MoorProps import getLineProps +from moorpy.helpers import getLineProps + +import matplotlib.pyplot as plt + + +inCBs = [0, 1.0, 10.0] # friction coefficients as inputs for test_seabed + + + +def test_line_stiffness(): + '''Checks stiffness of mooring lines.''' + + + +if __name__ == '__main__': + + import moorpy as mp + import matplotlib.pyplot as plt + + ms = mp.System(depth=100) + ms.setLineType(100, 'chain', name='chain') + + ms.addPoint(1, [1, 0, -100]) # anchor point + ms.addPoint(-1, [0, 0, 0]) # moving point + + ms.addLine(99, 'chain', pointA=1, pointB=2) + + ms.initialize() + + fig, ax = ms.plot() + + ms.solveEquilibrium() + f0 = ms.pointList[1].getForces() + print(f0) + print(ms.lineList[0].KA[1,1]) + + ms.pointList[1].setPosition([0,0.1,0]) + ms.solveEquilibrium() + f1 = ms.pointList[1].getForces() + print(f1) + print(ms.lineList[0].KA[1,1]) + + ms.plot(ax=ax, color='red') + + plt.show() + \ No newline at end of file diff --git a/tests/test_subsystem.py b/tests/test_subsystem.py new file mode 100644 index 0000000..7f3f2ef --- /dev/null +++ b/tests/test_subsystem.py @@ -0,0 +1,153 @@ +# tests MoorPy Subsystem functionality and results + +import pytest + +from numpy.testing import assert_allclose + +import numpy as np +import moorpy as mp +#from moorpy.MoorProps import getLineProps +from moorpy.helpers import getLineProps + +import matplotlib.pyplot as plt + + +""" +def test_tensions_swap(): + '''Compares two equivalent catenary mooring lines that are defined in opposite directions.''' + + ms = mp.System(depth=60) + + #ms.lineTypes['chain'] = getLineProps(120, name='chain') # add a line type + ms.setLineType(120, 'chain', name='chain') # add a line type + + ms.addPoint(1, [ 0, 0, -60]) + ms.addPoint(1, [100, 10, -30]) + + # line sloping up from A to B, and another in the opposite order + ms.addLine(120, 'chain', pointA=1, pointB=2) + ms.addLine(120, 'chain', pointA=2, pointB=1) + + ms.initialize() + + # compare tensions + assert_allclose(np.hstack([ms.lineList[0].fA, ms.lineList[0].fB]), + np.hstack([ms.lineList[1].fB, ms.lineList[1].fA]), rtol=0, atol=10.0, verbose=True) +""" + +# TO DO: make some test functions like the above one to test out some simple Subsystems for code bugs +# One key test is a 2-line subsystem that is fully slack with the middle point on the seabed, as in the below code. + + +""" + +import numpy as np +import matplotlib.pyplot as plt +import moorpy as mp +from moorpy.MoorProps import getLineProps +from moorpy.subsystem import Subsystem + + +# ----- choose some system geometry parameters ----- + +depth = 200 # water depth [m] +angles = np.radians([60, 300]) # line headings list [rad] +rAnchor = 600 # anchor radius/spacing [m] +zFair = -21 # fairlead z elevation [m] +rFair = 20 # fairlead radius [m] +lineLength= 650 # line unstretched length [m] +typeName = "chain1" # identifier string for the line type + + +# ===== First a simple Subsystem by itself ===== + +# create a subsystem +ss = Subsystem(depth=depth, spacing=rAnchor, rBfair=[10,0,-20]) + +# set up the line types +ss.setLineType(180, 'chain', name='one') +ss.setLineType( 50, 'chain', name='two') + +# set up the lines and points and stuff +lengths = [350, 400] +types = ['one', 'two'] +ss.makeGeneric(lengths, types) + +# plotting examples +ss.setEndPosition([0 ,-40,-200], endB=0) +ss.setEndPosition([300,400, -10], endB=1) +ss.staticSolve() +# 3D plot in the Subsystem's local frame +fig, ax = ss.plot() +# 2D view of the same +ss.plot2d() +# Line-like plot (in global reference frame) +fig = plt.figure() +ax = plt.axes(projection='3d') +ss.drawLine(0, ax, color='r') + + +# ===== Now a Subsystem in a larger System with a floating body ===== + +# Create new MoorPy System and set its depth +ms = mp.System(depth=depth) + +# add a line type +ms.setLineType(dnommm=120, material='chain', name=typeName) # this would be 120 mm chain + +# Add a free, body at [0,0,0] to the system (including some properties to make it hydrostatically stiff) +ms.addBody(0, np.zeros(6), m=1e6, v=1e3, rM=100, AWP=1e3) + +# For each line heading, set the anchor point, the fairlead point, and the line itself +for i, angle in enumerate(angles): + + # create end Points for the line + ms.addPoint(1, [rAnchor*np.cos(angle), rAnchor*np.sin(angle), -depth]) # create anchor point (type 0, fixed) + ms.addPoint(1, [ rFair*np.cos(angle), rFair*np.sin(angle), zFair]) # create fairlead point (type 0, fixed) + + # attach the fairlead Point to the Body (so it's fixed to the Body rather than the ground) + ms.bodyList[0].attachPoint(2*i+2, [rFair*np.cos(angle), rFair*np.sin(angle), zFair]) + + # add a Line going between the anchor and fairlead Points + ms.addLine(lineLength, typeName, pointA=2*i+1, pointB=2*i+2) + +# ----- Now add a SubSystem line! ----- +ss = Subsystem(mooringSys=ms, depth=depth, spacing=rAnchor, rBfair=[10,0,-20]) + +# set up the line types +ms.setLineType(180, 'chain', name='one') +ms.setLineType( 50, 'chain', name='two') + +# set up the lines and points and stuff +ls = [350, 300] +ts = ['one', 'two'] +ss.makeGeneric(lengths=ls, types=ts) + +# add points that the subSystem will attach to... +ms.addPoint(1, [-rAnchor, 100, -depth]) # Point 5 - create anchor point (type 0, fixed) +ms.addPoint(1, [ -rFair , 0, zFair]) # Point 6 - create fairlead point (type 0, fixed) +ms.bodyList[0].attachPoint(6, [-rFair, 0, zFair]) # attach the fairlead Point to the Body + +# string the Subsystem between the two points! +ms.lineList.append(ss) # add the SubSystem to the System's lineList +ss.number = 3 +ms.pointList[4].attachLine(3, 0) # attach it to the respective points +ms.pointList[5].attachLine(3, 1) # attach it to the respective points + + +# ----- run the model to demonstrate ----- + +ms.initialize() # make sure everything's connected + +ms.solveEquilibrium() # equilibrate +fig, ax = ms.plot() # plot the system in original configuration +#ms.unload("sample_from_manual.txt") # export to MD input file + +ms.bodyList[0].f6Ext = np.array([3e6, 0, 0, 0, 0, 0]) # apply an external force on the body +ms.solveEquilibrium3() # equilibrate +fig, ax = ms.plot(ax=ax, color='red') # plot the system in displaced configuration (on the same plot, in red) + +print(f"Body offset position is {ms.bodyList[0].r6}") + +plt.show() +""" diff --git a/tests/test_system.py b/tests/test_system.py index 571a2ea..434b863 100644 --- a/tests/test_system.py +++ b/tests/test_system.py @@ -11,6 +11,8 @@ import matplotlib.pyplot as plt +import os.path + inCBs = [0, 1.0, 10.0] # friction coefficients as inputs for test_seabed @@ -56,8 +58,8 @@ def test_stiffnesses_swap(): ms.initialize() # compare stiffnesses - assert_allclose(np.hstack([ms.lineList[0].KA, ms.lineList[0].KB, ms.lineList[0].KAB]), - np.hstack([ms.lineList[1].KB, ms.lineList[1].KA, ms.lineList[1].KAB]), rtol=0, atol=10.0, verbose=True) + assert_allclose(np.hstack([ms.lineList[0].KA, ms.lineList[0].KB, ms.lineList[0].KBA]), + np.hstack([ms.lineList[1].KB, ms.lineList[1].KA, ms.lineList[1].KBA]), rtol=0, atol=10.0, verbose=True) def test_stiffness_body(): @@ -82,8 +84,8 @@ def test_stiffness_body(): ms.initialize() # compare stiffnesses - assert_allclose(np.hstack([ms.lineList[0].KA, ms.lineList[0].KB, ms.lineList[0].KAB]), - np.hstack([ms.lineList[1].KB, ms.lineList[1].KA, ms.lineList[1].KAB]), rtol=0, atol=10.0, verbose=True) + assert_allclose(np.hstack([ms.lineList[0].KA, ms.lineList[0].KB, ms.lineList[0].KBA]), + np.hstack([ms.lineList[1].KB, ms.lineList[1].KA, ms.lineList[1].KBA]), rtol=0, atol=10.0, verbose=True) @@ -340,6 +342,47 @@ def test_seabed(CB): np.hstack([ms2.pointList[0].getForces(), ms2.pointList[-1].getForces()]), rtol=0, atol=10.0, verbose=True) + +def test_6DOF_stiffness(): + '''Tests 6x6 stiffness matrix of a complex system to check off diagonals.''' + + dir = os.path.abspath(os.path.dirname(__file__)) + file = os.path.join(dir, 'case8.dat') + + # Create new MoorPy System and set its depth + ms = mp.System(file=file) + + # ----- run the model to demonstrate ----- + + ms.initialize() # make sure everything's connected + + # find equilibrium of just the mooring lines (the body is 'coupled' and will by default not be moved) + ms.solveEquilibrium(tol=0.0001) # equilibrate + + + #Kmp = ms.getSystemStiffness( DOFtype='both', dx=0.02, dth=0.001) + #KmpA = ms.getSystemStiffnessA(DOFtype='both', ) + + Kn = ms.getCoupledStiffness() + Ka = ms.getCoupledStiffnessA() + + + #Kn = Kmp + #Ka = KmpA + #Kn = ms.bodyList[0].getStiffness(tol=0.00001, dx = 0.001) + #Ka = ms.bodyList[0].getStiffnessA(lines_only=True) + + print(Kn) + print(Ka) + print(Ka-Kn) + #print(Ka/Kn) + + assert_allclose(Ka[:3,:3], Kn[:3,:3], rtol=0.02, atol=5e3, verbose=True) # translational + assert_allclose(Ka[3:,:3], Kn[3:,:3], rtol=0.02, atol=5e4, verbose=True) # + assert_allclose(Ka[:3,3:], Kn[:3,3:], rtol=0.02, atol=5e4, verbose=True) # + assert_allclose(Ka[3:,3:], Kn[3:,3:], rtol=0.02, atol=2e6, verbose=True) # rotational + + if __name__ == '__main__': #test_basic()