diff --git a/README.md b/README.md index 02a13d22..1d45b7ca 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,12 @@ -# FAModel +# Floating Array Design Toolset -The FAModel (or Floating Array Model) package serves as a high-level library for +The Floating Array Design (FAD) Toolset is a collection of tools for +modeling and designing arrays of floating offshore structures. It was +originally designed for floating wind systems but has applicability +for many offshore applications. + +A core part of the FAD Toolset is the Floating Array Model (FAModel), +which serves as a high-level library for efficiently modeling a floating wind array. It combines site condition information and a description of the floating array design, and contains functions for evaluating the array's behavior considering the site conditions. For example, it combines @@ -10,27 +16,37 @@ estimate the holding capacity of each anchor. The library works in conjunction with the tools RAFT, MoorPy, and FLORIS to model floating wind turbines, mooring systems, and array wakes respectively. -In addition to the code, this repository defines a -[Floating Array Ontology](https://github.com/FloatingArrayDesign/FAModel/tree/main/famodel/ontology), -which provides a standardized description format for floating wind farms. +Layered on top of the floating array model is a set of design tools that can +be used for algorithmically adjusting or optimizing parts of the a floating +array. Specific tools existing for mooring lines, shared mooring systems, +dynamic power cables, static power cable routing, and overall array layout. +These capabilities work with the design representation and evaluation functions +in FAModel, and they can be applied by users in various combinations to suit +different purposes. -An example of use of these tools to model three mooring lines over the bathymetry -of the Humboldt lease area is shown below. +In addition to standalone uses of the FAD Toolset, a coupling has been made with +[Ard](https://github.com/WISDEM/Ard), a sophisticated and flexible wind farm +optimization tool. This coupling allows Ard to use certain mooring system +capabilities from FAD to perform layout optimization of floating wind farms +with Ard's more advanced layout optimization capabilities. -![Humboldt](famodel/seabed/images/slopeview4.PNG) +The FAD Toolset works with the [IEA Wind Task 49 Ontology](https://github.com/IEAWindTask49/Ontology), +which provides a standardized format for describing floating wind farm sites +and designs. -See example use cases in our [examples](https://github.com/FloatingArrayDesign/FAModel/tree/main/examples/README.md) folder. +See example use cases in our [examples](./examples/README.md) folder. ## Pre-installation Requirements -The FAModel package is built entirely in Python. It is recommended that users familiarize themselves with basic Python commands before use. -It is important to understand the general structure of FAModel and how to access models and stored information. Please see the model structure -document (./famodel/README.md). +The FAD Toolset is built entirely in Python. It is recommended that users +familiarize themselves with basic Python commands before use. +For working with the library, it is important to understand the floating array +model structure, which is described more [here](./famodel/README.md). ## Installation -To install FAModel itself, first clone the FAModel repository. +To install the FAD Toolset itself, first clone this FAD-Toolset repository. -The dependencies required by FAModel depend on how it is used. To install all +The dependencies required by FAD depend on how it is used. To install all possible required dependencies, you can create a new python virtual environment based on the included yaml listing the required dependencies. @@ -41,24 +57,25 @@ run the following command: conda env create -f famodel-env.yaml -This command will install all the dependencies required to run FAModel. -Activate your virtual environment before using FAModel with ```conda activate famodel-env``` +This command will install all the dependencies required to run FAD. +Activate your virtual environment before using FAD with ```conda activate famodel-env``` -To install the FAModel package in your environment, enter the -following in the command line from the FAModel directory. +To install the FAD Toolset package in your environment, enter the +following in the command line from the FAD-Toolset directory. For development use: -run ```python setup.py develop``` or ```pip install -e .``` from the command line in the main FAModel directory. +run ```python setup.py develop``` or ```pip install -e .``` from the command +line in the main FAD-Toolset directory. For non-development use: -run ```python setup.py``` or ```pip install .``` from the command line in the main FAModel directory. +run ```python setup.py``` or ```pip install .``` from the command line in +the main FAD-Toolset directory. -** At this time, FAModel requires the latest MoorPy development branch version to be used. ** -Therefore, you must install MoorPy with ```git clone https://github.com/NREL/MoorPy.git``` -then navigate to the MoorPy folder and checkout the development branch with ```git checkout dev``` -Finally, install this version into your environment with ```pip install -e .```. +FAD requires MoorPy and we currently install it separately. If you don't already have it, +you can install MoorPy with ```git clone https://github.com/NREL/MoorPy.git``` +then navigate to the MoorPy folder and install with ```pip install .```. Make sure your virtual enviroment is activated before installing MoorPy. @@ -68,19 +85,30 @@ The library has a core Project class for organizing information, classes for eac collection of subpackages for specific functions. The current subpackages are: - anchors: contains modules for anchor capacity calculations, in addition to the anchor class -- failures: contains modules for failure modeling with graph theory, and allows for enactment of a failure mode in integrated FAModel tools such as MoorPy and RAFT. +- failures: contains modules for failure modeling with graph theory, and allows for enactment of a failure mode. - seabed: contains modules for seabed bathymetry and boundary information +- design: contains various tools for performing design steps. Please navigate into the subfolders above for additional information. ## Getting Started -The easiest way to create an FAModel project is to provide the array information in an ontology yaml file. FAModel has been designed to work with a specific ontology yaml setup, which is described in detail in the [Ontology ReadMe](./famodel/ontology/README.md). - -The [example driver file](./famodel/example_driver.py) creates an FAModel project from a pre-set ontology file and shows the syntax and outputs of various capabilities. For guidance on creating your own ontology yaml file, it is recommended to read through the [Ontology ReadMe](./famodel/ontology/README.md), then either adapt one of the ontology samples or fill in the ontology template. - -The [FAModel core readme](./famodel/README.md) describes the FAModel class structure, as well as the properties and methods of each component class. - -There are some limited helper functions to auntomatically fill in sections of a yaml from a moorpy system or a list of platform locations. See [FAModel helpers](./famodel/helpers.py) for the full list of yaml writing capabilities. Many of these are a work in progress. +The easiest way to create a FAD project is to provide the array +information in an ontology yaml file. FAD has been designed +to work with a specific ontology yaml setup, which is described +in detail in the [Ontology ReadMe](./famodel/ontology/README.md). + +The [example driver file](./famodel/example_driver.py) creates a FAD Project +object from a pre-set ontology file and shows the syntax and outputs of +various capabilities. For guidance on creating your own ontology yaml file, +it is recommended to read through the [Ontology ReadMe](./famodel/ontology/README.md), +then either adapt one of the ontology samples or fill in the ontology template. + +The [core model readme](./famodel/README.md) describes the Project class structure, +as well as the properties and methods of each component class. + +There are some limited helper functions to automatically fill in sections +of a yaml from a MoorPy system or a list of platform locations. +See [helpers](./famodel/helpers.py) for the full list of yaml writing capabilities. ## Authors diff --git a/examples/01_Visualization/02_visual_moorings.yaml b/examples/01_Visualization/02_visual_moorings.yaml index 51dd390f..30d86fae 100644 --- a/examples/01_Visualization/02_visual_moorings.yaml +++ b/examples/01_Visualization/02_visual_moorings.yaml @@ -30,11 +30,11 @@ mooring_systems: ms1: name: 2-line semi-taut polyester mooring system with a third line shared - keys: [MooringConfigID, heading, anchorType, lengthAdjust] + keys: [MooringConfigID, heading, anchorType] data: - - [ semitaut-poly_1, 150 , drag-embedment1, 0 ] - - [ semitaut-poly_1, 270 , drag-embedment1, 0 ] - - [ semitaut-poly_1, 30 , drag-embedment1, 0 ] + - [ semitaut-poly_1, 150 , drag-embedment1] + - [ semitaut-poly_1, 270 , drag-embedment1] + - [ semitaut-poly_1, 30 , drag-embedment1] # Mooring line configurations diff --git a/examples/01_Visualization/03_visual_cables.py b/examples/01_Visualization/03_visual_cables.py index 806164f8..769c2fb5 100644 --- a/examples/01_Visualization/03_visual_cables.py +++ b/examples/01_Visualization/03_visual_cables.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- """ Simple driver file to create a 2d plot of an platform locations with -mooring lines in an array. +cables in an array. The input file only contains the bare minimum information to build a 2d plot -of the turbine locations and moorings (no cables, platform design, turbines, +of the turbine locations and cables (no moorings, platform design, turbines, site condition information, etc.) """ diff --git a/examples/01_Visualization/03_visual_cables.yaml b/examples/01_Visualization/03_visual_cables.yaml index 4963f67b..aeee9e05 100644 --- a/examples/01_Visualization/03_visual_cables.yaml +++ b/examples/01_Visualization/03_visual_cables.yaml @@ -35,7 +35,6 @@ dynamic_cable_configs: A: 300 # cable conductor area [mm^2] cable_type: dynamic_cable_66 # ID of a cable section type from famodel/cables/cableProps_default.yaml. Cable props loaded automatically from this! length: 353.505 # [m] length (unstretched) - rJTube : 5 # [m] radial distance from center of platform that J-tube is located sections: - type: Buoyancy_750m # name of buoy type from famodel/cables/cableProps_default.yaml - buoy design info read in automatically from this! @@ -51,8 +50,7 @@ dynamic_cable_configs: span: 1512 # [m] cable_type: dynamic_cable_66 # ID of a cable section type from famodel/cables/cableProps_default.yaml. Cable props loaded automatically from this! A: 300 # cable conductor area [mm^2] - length: 1550 # [m] length (unstretched) - rJTube : 58 # [m] radial distance from center of platform that J-tube is located + length: 1650 # [m] length (unstretched) sections: - type: Buoyancy_750m diff --git a/examples/08_Design_Adjustment/01_Fairleads.yaml b/examples/08_Design_Adjustment/01_Fairleads.yaml new file mode 100644 index 00000000..4fa235bd --- /dev/null +++ b/examples/08_Design_Adjustment/01_Fairleads.yaml @@ -0,0 +1,68 @@ + +# ----- Array-level inputs ----- + +# Wind turbine array layout +array: + keys : [ID, topsideID, platformID, mooringID, x_location, y_location, heading_adjust] + data : # ID# ID# ID# [m] [m] [deg] + - [fowt0, 0, 1, ms1, -1600, -1600, 180 ] + - [fowt1, 0, 1, ms1, 0, -1600, 0 ] + - [fowt2, 0, 1, ms1, 1600, -1600, 0 ] + - [fowt3, 0, 1, ms1, -1600, 0, 0 ] + - [fowt4, 0, 1, ms1, 0, 0, 45 ] + - [fowt5, 0, 1, ms1, 1600, 0, 0 ] + - [fowt6, 0, 1, ms1, -1600, 1600, 0 ] + - [fowt7, 0, 1, ms1, 0, 1600, 0 ] + - [fowt8, 0, 1, ms1, 1600, 1600, 0 ] + +platform: + type : FOWT + fairleads : # list of fairlead coordinates for the platform relative to platform coordinate and 0-degree heading + - name : fairlead1 + r_rel : [58, 0, -14] + headings : [30, 150, 270] + + + + +# ----- Mooring system ----- + +# Mooring system descriptions (each for an individual FOWT with no sharing) +mooring_systems: + + ms1: + name: 2-line semi-taut polyester mooring system with a third line shared + + keys: [MooringConfigID, heading, anchorType, fairlead] + data: + - [ semitaut-poly_1, 135 , drag-embedment1, 2] + - [ semitaut-poly_1, 270 , drag-embedment1, 3] + - [ semitaut-poly_1, 45 , drag-embedment1, 1] + + +# Mooring line configurations +mooring_line_configs: + + semitaut-poly_1: # mooring line configuration identifier, matches MooringConfigID + + name: Semitaut polyester configuration 1 # descriptive name + + span: 642 # 2D x-y distance from fairlead to anchor + + sections: #in order from anchor to fairlead + - mooringFamily: chain # ID of a mooring line section type + d_nom: .1549 # nominal diameter of material [m] + length: 497.7 # [m] usntretched length of line section + - mooringFamily: polyester # ID of a mooring line section type + d_nom: .182 # nominal diameter of material [m] + length: 199.8 # [m] length (unstretched) + + + +# Anchor type properties +anchor_types: + + drag-embedment1: + type : DEA # type of anchor (drag-embedment anchor) + + diff --git a/examples/08_Design_Adjustment/02_Jtubes.py b/examples/08_Design_Adjustment/02_Jtubes.py new file mode 100644 index 00000000..dd00f9cf --- /dev/null +++ b/examples/08_Design_Adjustment/02_Jtubes.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +""" +Simple driver file to create a 2d plot of an platform locations with +cables in an array using Jtubes to define platform connection location. +The input file only contains the bare minimum information to build a 2d plot +of the turbine locations and cables connected to Jtubes (no moorings, platform design, turbines, + site condition information, etc.) +""" + +from famodel import Project +import matplotlib.pyplot as plt + +# define name of ontology input file +input_file = '02_Jtubes.yaml' + +# initialize Project class with input file, we don't need RAFT for this so mark False +project = Project(file=input_file,raft=False) + +# plot +project.plot2d() + +# to plot cables in 3d, we'll need to add depth and create a moorpy model of the system +project.depth = 200 # depth added because we did not include the site conditions section of the yaml +project.getMoorPyArray() +project.plot3d() + +plt.show() diff --git a/examples/08_Design_Adjustment/02_Jtubes.yaml b/examples/08_Design_Adjustment/02_Jtubes.yaml new file mode 100644 index 00000000..95d926fa --- /dev/null +++ b/examples/08_Design_Adjustment/02_Jtubes.yaml @@ -0,0 +1,71 @@ + +# ----- Array-level inputs ----- + +# Wind turbine array layout +array: + keys : [ID, topsideID, platformID, mooringID, x_location, y_location, heading_adjust] + data : # ID# ID# ID# [m] [m] [deg] + - [fowt0, 0, 1, 0, -1600, -1600, 0 ] + - [fowt1, 0, 1, 0, 0, -1600, 0 ] + - [fowt2, 0, 1, 0, 1600, -1600, 0 ] + - [fowt3, 0, 1, 0, -1600, 0, 0 ] + - [fowt4, 0, 1, 0, 0, 0, 0 ] + - [fowt5, 0, 1, 0, 1600, 0, 0 ] + - [fowt6, 0, 1, 0, -1600, 1600, 0 ] + - [fowt7, 0, 1, 0, 0, 1600, 0 ] + - [fowt8, 0, 1, 0, 1600, 1600, 0 ] + +platform: + type : FOWT + Jtubes : + - name: Jtube1 + r : 5 + z : -20 + headings : [90, 210, 330] # headings in degrees for the Jtube (if multiple headings, the Jtube will be repeated for each heading) + +# Array cables +array_cables: + keys: [ AttachA, AttachB, DynCableA, DynCableB, headingA, headingB, JtubeA, JtubeB, cableType] + data: + - [ fowt0, fowt1, suspended_1, None, 90, 270, 1, 2, None] # suspended cable, so only one dynamic cable configuration, no static cable + - [ fowt1, fowt2, lazy_wave1, lazy_wave1, 90, 270, 1, 3, static_cable_66] + +# Dynamic and cable configurations +dynamic_cable_configs: +# contains the subsections that make up each section of the subsea cable (i.e., what sections make up the lazywave cable in array_cable_1) + lazy_wave1: + name: Lazy wave configuration 1 (simpler approach) + voltage: 66 # [kV] + span : 195 # [m] horizontal distance to end of dynamic cable from attachment point + A: 300 # cable conductor area [mm^2] + cable_type: dynamic_cable_66 # ID of a cable section type from famodel/cables/cableProps_default.yaml. Cable props loaded automatically from this! + length: 353.505 # [m] length (unstretched) + + sections: + - type: Buoyancy_750m # name of buoy type from famodel/cables/cableProps_default.yaml - buoy design info read in automatically from this! + L_mid: 200 # [m] from platform connection + N_modules: 6 + spacing: 11.23 # [m] + V: 1 # [m^3] + + + suspended_1: + name: Dynamic suspended cable configuration 1 + voltage: 33 # [kV] + span: 1512 # [m] + cable_type: dynamic_cable_66 # ID of a cable section type from famodel/cables/cableProps_default.yaml. Cable props loaded automatically from this! + A: 300 # cable conductor area [mm^2] + length: 1650 # [m] length (unstretched) + + sections: + - type: Buoyancy_750m + L_mid: 510 # [m] from end A + N_modules: 6 + spacing: 18 # [m] + V: 2 # [m^3] + + - type: Buoyancy_750m + L_mid: 1040 # [m] from end A + N_modules: 6 + spacing: 18 # [m] + V: 2 # [m^3] \ No newline at end of file diff --git a/examples/08_Design_Adjustment/02_fairleads.py b/examples/08_Design_Adjustment/02_fairleads.py new file mode 100644 index 00000000..d1f65f08 --- /dev/null +++ b/examples/08_Design_Adjustment/02_fairleads.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +""" +Simple driver file to create an array showing moorings attached to fairelead objects. + +This allows you to connect moorings to platforms at a specific point and then run +mooring headings independent of the heading of this connection point. +The input file only contains the bare minimum information to build a 2d plot +of the turbine locations and moorings with fairleads (no cables, platform design, turbines, + site condition information, etc.) +""" + +from famodel import Project +import matplotlib.pyplot as plt + +# define name of ontology input file +input_file = '01_Fairleads.yaml' + +# initialize Project class with input file, we don't need RAFT for this so mark False +project = Project(file=input_file,raft=False) + +# plot +project.plot2d() + + +# to moorings plot in 3d, we'll need to add depth and create a moorpy model of the system +project.depth = 200 # depth added because we did not include the site conditions section of the yaml +project.getMoorPyArray() +project.plot3d() + +plt.show() + diff --git a/examples/08_Design_Adjustment/02_rotations.py b/examples/08_Design_Adjustment/02_rotations.py new file mode 100644 index 00000000..23bc0026 --- /dev/null +++ b/examples/08_Design_Adjustment/02_rotations.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +""" +Simple driver file to create an array with moorings and rotate the platforms and array. + +This allows you to rotate platforms including all of their moorings, anchors, and fairleads +The input file only contains the bare minimum information to build a 2d plot +of the turbine locations and moorings with fairleads (no cables, platform design, turbines, + site condition information, etc.) +""" + +from famodel import Project +import matplotlib.pyplot as plt + +# define name of ontology input file +input_file = '01_Fairleads.yaml' + +# initialize Project class with input file, we don't need RAFT for this so mark False +project = Project(file=input_file,raft=False) + +# plot +project.plot2d() + +# let's rotate a platform but keep in the same x,y position +pf_loc = project.platformList['fowt0'].r +new_heading = 143 # [deg] +project.platformList['fowt0'].setPosition(r=pf_loc, heading=new_heading, + degrees=True, project=project) + +# plot again to see the difference +project.plot2d() + +# let's now change the platform's position +new_r = [-2000, -2200] +project.platformList['fowt0'].setPosition(r=new_r, project=project) + +# plot again +project.plot2d() + + diff --git a/examples/Inputs/output_MD.dat b/examples/Inputs/output_MD.dat deleted file mode 100644 index 38578132..00000000 --- a/examples/Inputs/output_MD.dat +++ /dev/null @@ -1,55 +0,0 @@ -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_chain 0.2788 479.88 2.054e+09 -1.000e+00 0.000e+00 1.333 1.000 0.64 0.50 -1_polyester 0.1441 22.49 1.428e+08 -1.000e+00 0.000e+00 2.021 1.100 0.00 0.15 ---------------------- 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 free -1499.91 1499.96 0.00 0.00 0.00 -0.00 1.9911e+07 0.00|0.00|-2.54 0.000e+00 19480.10 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 -1150.00 893.78 -204.21 0.00 0.00 0.00 0.00 -2 Free -1391.15 1311.56 -137.43 140.00 0.13 0.00 0.00 -3 Coupled -1470.91 1449.73 -14.00 0.00 0.00 0.00 0.00 -4 Fixed -2200.00 1500.00 -203.86 0.00 0.00 0.00 0.00 -5 Free -1717.38 1499.97 -137.30 140.00 0.13 0.00 0.00 -6 Coupled -1557.91 1499.96 -14.00 0.00 0.00 0.00 0.00 -7 Fixed -1150.00 2106.22 -204.07 0.00 0.00 0.00 0.00 -8 Free -1391.18 1688.34 -137.38 140.00 0.13 0.00 0.00 -9 Coupled -1470.91 1550.19 -14.00 0.00 0.00 0.00 0.00 ----------------------- LINES -------------------------------------------------------- -ID LineType AttachA AttachB UnstrLen NumSegs LineOutputs -(#) (name) (#) (#) (m) (-) (-) -1 0_chain 1 2 497.700 10 p -2 1_polyester 2 3 199.800 10 p -3 0_chain 4 5 497.700 10 p -4 1_polyester 5 6 199.800 10 p -5 0_chain 7 8 497.700 10 p -6 1_polyester 8 9 199.800 10 p ----------------------- OPTIONS ------------------------------------------------------ -0.001 dtM -3000000.0 kb -300000.0 cb -60 TmaxIC -9.81 g -0 depth -1025 rho ------------------------ OUTPUTS ----------------------------------------------------- -FairTen1 -FairTen2 -FairTen3 -FairTen4 -FairTen5 -FairTen6 -END ---------------------- need this line ------------------------------------------------ diff --git a/examples/Inputs/Moordyn_semitaut200m.dat b/examples/Moordyn_semitaut200m.dat similarity index 100% rename from examples/Inputs/Moordyn_semitaut200m.dat rename to examples/Moordyn_semitaut200m.dat diff --git a/examples/Inputs/OntologySample200m.yaml b/examples/OntologySample200m.yaml similarity index 95% rename from examples/Inputs/OntologySample200m.yaml rename to examples/OntologySample200m.yaml index a59e0a1c..ee348243 100644 --- a/examples/Inputs/OntologySample200m.yaml +++ b/examples/OntologySample200m.yaml @@ -30,7 +30,7 @@ site: -[x2, y2] bathymetry: - file: './bathymetry200m_sample.txt' + file: 'bathymetry200m_sample.txt' seabed: x : [-10901, 0, 10000] @@ -112,12 +112,6 @@ array: - [FOWT1, 1, 1, ms3, -1600, -1600, 0, 180 ] # 2 turbine array - [FOWT2, 1, 1, ms3, 0, -1600, 0, 0 ] - [OSS1, 2, 2, ms3, 1600, -1600, 0, 180 ] # substation - # - [FOWT3, 1, 1, ms3, -1600, 0, 0, 0 ] - # - [FOWT5, 1, 1, ms3, 0, 0, 0, 45 ] - # - [FOWT6, 1, 1, ms3, 1600, 0, 0, 0 ] - # - [FOWT7, 1, 1, ms3, -1600, 1600, 0, 0 ] - # - [FOWT8, 1, 1, ms3, 0, 1600, 0, 0 ] - # - [FOWT9, 1, 1, ms3, 1600, 1600, 0, 0 ] # Array-level mooring system (in addition to any per-turbine entries later) @@ -127,16 +121,17 @@ array_mooring: anchor_data : line_keys : - [MooringConfigID , endA, endB, headingA, headingB, lengthAdjust] + [MooringConfigID , endA, endB, fairleadA, fairleadB] line_data : - # - [ rope_shared , FOWT1, FOWT2, 270, 270, 0] + # - [ rope_shared , FOWT1, FOWT2, 0, 0, None] # shared mooring doesn't need a heading specification + # - [ semitaut-poly_1, FOWT1, Anchor1, None, 0, 30] # shared anchor doesn't need a fairleadA specification # Array cables array_cables: - keys: [ AttachA, AttachB, DynCableA, DynCableB, headingA, headingB, cableType] + keys: [ AttachA, AttachB, DynCableA, DynCableB, JTubeA, JTubeB, headingA, headingB, cableType] data: - - [ FOWT2, OSS1, suspended_1, None, 90, 90, None] # suspended cable, so only one dynamic cable configuration, no static cable + - [ FOWT2, OSS1, suspended_1, None, 2, 2, 90, 90, None] # suspended cable, so only one dynamic cable configuration, no static cable # ----- Mooring system ----- @@ -147,11 +142,11 @@ mooring_systems: ms3: name: 3-line semi-taut polyester mooring system - keys: [MooringConfigID, heading, anchorType, lengthAdjust] + keys: [MooringConfigID, heading, anchorType, fairlead, lug] # fairlead and lug listings are optional; if not specified, fairlead list follows order of fairleads in the platform definition data: - - [ semitaut-poly_1, 150 , drag-embedment1, 0 ] - - [ semitaut-poly_1, 30 , drag-embedment1, 0 ] - - [ semitaut-poly_1, 270, drag-embedment1, 0 ] + - [ semitaut-poly_1, 30 , drag-embedment1, 1, 1 ] + - [ semitaut-poly_1, 150 , drag-embedment1, 2, 1 ] + - [ semitaut-poly_1, 270, drag-embedment1, 3, 1 ] # Mooring line configurations @@ -164,23 +159,39 @@ mooring_line_configs: sections: + - subsections: # subsections are used to define segments of mooring that run in parallel, such as a bridle + - - connectorType: h_link # end A connector to the fairlead A0 + - type: chain_185 # each outer list is a separate subsection, each inner list is the segments of the subsection that are connected in series + length: 40 + - - connectorType: h_link # end A connector to the fairlead A1 + - type: chain_185 + length: 40 - connectorType: h_link - type: rope - length: 150 + length: 110 - connectorType: clump_weight_80 - type: rope - length: 1172 + length: 500 + - connectorType: triplate + - subsections: # a subsection that is not on the end of a mooring connects back to the connector on either side + - - type: chain_185 + length: 180 + - - type: chain_185 + length: 180 + - connectorType: triplate + - type: rope + length: 500 - connectorType: clump_weight_80 - type: rope - length: 150 + length: 110 - connectorType: h_link - - bridle_sections: - bridle_radius: - - type: - - connectorType: - - type: - length: + - subsections: + - - type: chain_185 + length: 40 + - connectorType: h_link # end B connector to the fairlead B0 + - - type: chain_185 + length: 40 + - connectorType: h_link # end B connector to the fairlead B1 semitaut-poly_1: # mooring line configuration identifier @@ -199,6 +210,29 @@ mooring_line_configs: d_nom: .182 length: 199.8 # [m] length (unstretched) + semitaut-poly_1_bridle: # mooring line configuration identifier + + name: Semitaut polyester configuration 1 # descriptive name + + span: 642 + + sections: #in order from anchor to fairlead + - mooringFamily: chain # ID of a mooring line section type + d_nom: .1549 + length: 497.7 # [m] usntretched length of line section + adjustable: True # flags that this section could be adjusted to accommodate different spacings... + - connectorType: h_link + - mooringFamily: polyester # ID of a mooring line section type + d_nom: .182 + length: 119.8 # [m] length (unstretched) + - subsections: + - - mooringFamily: polyester # ID of a mooring line section type + d_nom: .182 + length: 80 # [m] length (unstretched) + - - mooringFamily: polyester # ID of a mooring line section type + d_nom: .182 + length: 80 # [m] length (unstretched) + # Mooring line cross-sectional properties @@ -349,7 +383,6 @@ dynamic_cable_configs: A: 300 cable_type: dynamic_cable_66_1 # ID of a cable section type1 length: 353.505 # [m] length (unstretched) - rJTube : 5 # [m] radial distance from center of platform that J-tube is located sections: - type: buoyancy_module_1 #_w_buoy # (section properties including averaged effect of buoyancy modules) @@ -364,11 +397,10 @@ dynamic_cable_configs: suspended_1: name: Dynamic suspended cable configuration 1 voltage: 33 - span: 1512 + span: 1590 cable_type: dynamic_cable_66 # ID of a cable section type1 A: 300 - length: 1550 # [m] length (unstretched) - rJTube : 58 # [m] radial distance from center of platform that J-tube is located + length: 1610 # [m] length (unstretched) sections: - type: Buoyancy_750m #_w_buoy # (section properties including averaged effect of buoyancy modules) @@ -391,11 +423,13 @@ cables: attachID: FOWT1 # FOWT/substation/junction ID heading: 260 # [deg] heading of attachment at end A dynamicID: lazy_wave1 # ID of dynamic cable configuration at this end + JTube: 1 # index of Jtube list in platform definition endB: attachID: FOWT2 # FOWT/substation/junction ID heading: 280 # [deg] heading of attachment at end B dynamicID: lazy_wave1 # ID of dynamic cable configuration at this end + JTube: 1 # index of Jtube list in platform definition routing_x_y_r: # optional vertex points along the cable route. Nonzero radius wraps around a point at that radius. - [-900, -1450, 20] @@ -1529,8 +1563,14 @@ platforms: - potModMaster : 1 # [int] master switch for potMod variables; 0=keeps all member potMod vars the same, 1=turns all potMod vars to False (no HAMS), 2=turns all potMod vars to True (no strip) dlsMax : 5.0 # maximum node splitting section amount for platform members; can't be 0 qtfPath : 'IEA-15-240-RWT-UMaineSemi.12d' # path to the qtf file for the platform - rFair : 58 - zFair : -14 + fairleads : # list of fairlead coordinates for the platform relative to platform coordinate and 0-degree heading + - name: fairlead1 + r_rel: [58, 0, -14] + headings: [30, 150, 270, 35] # headings in degrees for the fairlead (if multiple headings, the fairlead will be repeated for each heading) + Jtubes : # list of Jtube coordinates for the platform relative to platform coordinate and 0-degree heading + - name: Jtube1 + r_rel: [5, 0, -20] + headings: [90, 210, 330] # headings in degrees for the Jtube (if multiple headings, the Jtube will be repeated for each heading) type : FOWT z_location : 0 # optional to put the depth of this platform type @@ -1628,6 +1668,14 @@ platforms: qtfPath : 'IEA-15-240-RWT-UMaineSemi.12d' # path to the qtf file for the platform rFair : 58 zFair : -15 + fairleads : # list of fairlead coordinates for the platform relative to platform coordinate and 0-degree heading + - name: fairlead1 + r_rel: [58, 0, -14] + headings: [30, 150, 270, 35] # headings in degrees for the fairlead (if multiple headings, the fairlead will be repeated for each heading) + Jtubes: + - name: Jtube1 + r_rel: [5, 0, -20] + headings: [90, 210, 330] # headings in degrees for the Jtube (if multiple headings, the Jtube will be repeated for each heading) type : Substation members: # list all members here diff --git a/examples/Inputs/OntologySample200m_1turb.yaml b/examples/OntologySample200m_1turb.yaml similarity index 100% rename from examples/Inputs/OntologySample200m_1turb.yaml rename to examples/OntologySample200m_1turb.yaml diff --git a/examples/Inputs/OntologySample200m_uniformArray.yaml b/examples/OntologySample200m_uniformArray.yaml similarity index 100% rename from examples/Inputs/OntologySample200m_uniformArray.yaml rename to examples/OntologySample200m_uniformArray.yaml diff --git a/examples/Inputs/OntologySample600m_shared.yaml b/examples/OntologySample600m_shared.yaml similarity index 96% rename from examples/Inputs/OntologySample600m_shared.yaml rename to examples/OntologySample600m_shared.yaml index 675d2f94..33def23b 100644 --- a/examples/Inputs/OntologySample600m_shared.yaml +++ b/examples/OntologySample600m_shared.yaml @@ -103,8 +103,8 @@ array: data : # ID# ID# ID# [m] [m] [deg] - [FOWT1, 1, 1, ms3, 0, 0, 180 ] # 2 array, shared moorings - [FOWT2, 1, 1, ms2, 1600, 0, 0 ] - - [FOWT3, 1, 1, ms1, 0, 1656, 180 ] - - [FOWT4, 1, 1, ms4, 1600, 1656, 180] + - [FOWT3, 1, 1, ms1, 0, 1700.4577, 180 ] + - [FOWT4, 1, 1, ms4, 1600, 1700.4577, 180] # - [4, 1, 2, ms4, -1200, 0, 0 ] # - [5, 1, 1, ms5, 0, 0, 0 ] # - [6, 1, 1, ms6, 1200, 0, 0 ] @@ -118,18 +118,18 @@ array_mooring: anchor_keys : [ID, type, x, y, embedment ] anchor_data : - - [ Anch1, suction_pile1, -828 , 828 , 2 ] + - [ Anch1, suction_pile1, -829 , 850.22887 , 2 ] # - [ 2, suction1, -1900 , -1200 , 2 ] # - [ 3, suction1, -850 , -1806 , 2 ] # - [ 4, suction1, -850 , 600 , 2 ] # - [ 5, suction1, -1900 , 0 , 2 ] line_keys : - [MooringConfigID , endA, endB, headingA, headingB, lengthAdjust] + [MooringConfigID , endA, endB, fairleadA, fairleadB, lengthAdjust] line_data : - - [ rope_shared , FOWT1, FOWT2, 270, 270, 0] - - [ rope_1 , Anch1, FOWT1, NONE, 135, 0] - - [ rope_1 , Anch1, FOWT3, NONE, 45, 0] + - [ rope_shared_bridle , FOWT1, FOWT2, [4,5], [4,5], 0] + - [ rope_1 , Anch1, FOWT1, NONE, 2, 0] + - [ rope_1 , Anch1, FOWT3, NONE, 1, 0] # - [ shared-2-clump , FOWT 2, FOWT 3, 0, 0, 0] @@ -1203,6 +1203,18 @@ platforms: rFair : 40.5 # platform fairlead radius zFair : -20 # platform fairlead z-location type : FOWT + fairleads : + # list of fairlead coordinates for the platform relative to platform coordinate and 0-degree heading + - name: fairlead1 + r_rel: [58, 0, -14] # relative coordinates of fairlead to platform center + headings: [30, 150, 270] # headings in degrees for the fairlead (if multiple headings, the fairlead will be repeated for each heading) + - name: fairleads2 + r_rel: [-57.779,-5.055, -14] + - name: fairleads3 + r_rel: [-57.779, 5.055, -14] + rFair : 40.5 # platform fairlead radius + zFair : -20 # platform fairlead z-location + type : FOWT members: # list all members here @@ -1302,10 +1314,10 @@ mooring_systems: ms1: name: 3-line semi-taut polyester mooring system with one line shared anchor - keys: [MooringConfigID, heading, anchorType, lengthAdjust] + keys: [MooringConfigID, heading, anchorType, fairlead] data: - - [ rope_1, 270 , suction_pile1, 0 ] - - [ rope_1, 135 , suction_pile1, 0 ] + - [ rope_1_bridle, 270 , suction_pile1, [4,5] ] + - [ rope_1, 135 , suction_pile1, 2 ] ms2: name: 2-line semitaut with a third shared line @@ -1318,18 +1330,18 @@ mooring_systems: ms3: name: 3-line semi-taut polyester mooring system with one line shared anchor and one shared line - keys: [MooringConfigID, heading, anchorType, lengthAdjust] + keys: [MooringConfigID, heading, anchorType, fairlead] data: - - [ rope_1, 45 , suction_pile1, 0 ] + - [ rope_1, 45 , suction_pile1, 1 ] ms4: name: 3 line taut poly mooring system - keys: [MooringConfigID, heading, anchorType, lengthAdjust] + keys: [MooringConfigID, heading, anchorType, fairlead] data: - - [ rope_1, 45 , suction_pile1, 0 ] - - [ rope_1, 135 , suction_pile1, 0 ] - - [ rope_1, 270 , suction_pile1, 0 ] + - [ rope_1, 45 , suction_pile1, 1 ] + - [ rope_1, 135 , suction_pile1, 2 ] + - [ rope_1, 270 , suction_pile1, 3 ] # Mooring line configurations @@ -1349,6 +1361,67 @@ mooring_line_configs: length: 1170 # [m] usntretched length of line section adjustable: True # flags that this section could be adjusted to accommodate different spacings... + rope_1_bridle: # mooring line configuration identifier + + name: rope configuration 1 with a bridle # descriptive name + + span: 1131.37 + + + sections: #in order from anchor to fairlead + - type: chain_155mm + length: 20 + - type: rope # ID of a mooring line section type + length: 500 # [m] usntretched length of line section + - connectorType: triplate + - subsections: # double chain section + - - mooringFamily: chain + d_nom: 0.1 + length: 120 + - - mooringFamily: chain + d_nom: 0.1 + length: 120 + - connectorType: triplate + - type: rope # ID of a mooring line section type + length: 500 # [m] usntretched length of line section + - subsections: # bridle sections for end B + - - type: rope + length: 50 + - connectorType: shackle + - - type: rope + length: 50 + - connectorType: shackle + + rope_shared_bridle: + name: shared rope with a bridle on each end + + span: 1484 + + + sections: + - subsections: # bridle sections for end B + - - connectorType: shackle + - type: rope + length: 50 + - - connectorType: shackle + - type: rope + length: 50 + - type: rope + length: 100 + - connectorType: clump_weight_80 + - type: rope + length: 1172 + - connectorType: clump_weight_80 + - type: rope + length: 100 + - subsections: # bridle sections for end B + - - type: rope + length: 50 + - connectorType: shackle + - - type: rope + length: 50 + - connectorType: shackle + rope_shared: name: shared rope @@ -1480,6 +1553,11 @@ mooring_connector_types: shackle: m : 200 v : .2 + + triplate: + m : 1000 + v : 0.1 + # note: triplate is a connector that connects three mooring lines together, e.g., for a double chain section # Anchor type properties anchor_types: diff --git a/examples/Inputs/bathymetry200m_Array.txt b/examples/bathymetry200m_Array.txt similarity index 100% rename from examples/Inputs/bathymetry200m_Array.txt rename to examples/bathymetry200m_Array.txt diff --git a/examples/Inputs/bathymetry200m_sample.txt b/examples/bathymetry200m_sample.txt similarity index 100% rename from examples/Inputs/bathymetry200m_sample.txt rename to examples/bathymetry200m_sample.txt diff --git a/examples/Inputs/boundary_sample.csv b/examples/boundary_sample.csv similarity index 100% rename from examples/Inputs/boundary_sample.csv rename to examples/boundary_sample.csv diff --git a/examples/create_platform_from_ms.py b/examples/create_platform_from_ms.py index ab085379..a4f3b955 100644 --- a/examples/create_platform_from_ms.py +++ b/examples/create_platform_from_ms.py @@ -15,15 +15,12 @@ import matplotlib.pyplot as plt #### INPUTS #### -input_directory = 'Inputs/' # relative location of directory for input files (yaml, bath files, etc) -filename = 'MoorDyn_semitaut200m.dat' # moordyn file to create a moorpy system +dir = os.path.dirname(os.path.realpath(__file__)) +filename = dir+'\MoorDyn_semitaut200m.dat' # moordyn file to create a moorpy system rep_pf_name = 'FOWT1' # platform to replicate (look at yaml file array data table to get platform names) new_pf_loc = [-100,-1500,0] -# change to input directory -os.chdir(input_directory) - # create moorpy system ms = mp.System(file=filename) ms.initialize() diff --git a/examples/duplicate_platform.py b/examples/duplicate_platform.py index e40b77c9..b78644a1 100644 --- a/examples/duplicate_platform.py +++ b/examples/duplicate_platform.py @@ -9,13 +9,11 @@ import matplotlib.pyplot as plt #### INPUTS #### -input_directory = 'Inputs/' # relative location of directory for input files (yaml, bath files, etc) -filename = 'OntologySample200m.yaml' # yaml file to make initial platform(s) +dir = os.path.dirname(os.path.realpath(__file__)) +filename = dir+'\OntologySample200m.yaml' # yaml file to make initial platform(s) rep_pf_name = 'FOWT1' # platform to replicate (look at yaml file array data table to get platform names) new_pf_loc = [0,0] -# switch to directory of input files -os.chdir(input_directory) # first load in single platform from yaml project = Project(file=filename) diff --git a/examples/example_anchors.py b/examples/example_anchors.py index d496b11f..95bd77ed 100644 --- a/examples/example_anchors.py +++ b/examples/example_anchors.py @@ -7,10 +7,10 @@ from famodel.project import Project import os -os.chdir('./Inputs/') +dir = os.path.dirname(os.path.realpath(__file__)) # set yaml file location and name -ontology_file = 'OntologySample200m_1turb.yaml' +ontology_file = dir+'\OntologySample200m_1turb.yaml' # create project class project = Project(file=ontology_file) diff --git a/examples/example_driver.py b/examples/example_driver.py index ad1ac38b..c8d043cc 100644 --- a/examples/example_driver.py +++ b/examples/example_driver.py @@ -22,18 +22,16 @@ import os import matplotlib.pyplot as plt -os.chdir('./Inputs/') - # set yaml file location and name -ontology_file = 'OntologySample200m.yaml' +ontology_file = "OntologySample200m.yaml" #%% Section 1: Project without RAFT print('Creating project without RAFT\n') -print(os.getcwd()) + # create project object -project = Project(file=ontology_file,raft=False) +project = Project(file=ontology_file, raft=False) # create moorpy system of the array, include cables in the system -project.getMoorPyArray(cables=1) +project.getMoorPyArray(cables=True) # plot in 3d, using moorpy system for the mooring and cable plots project.plot2d() project.plot3d() diff --git a/examples/example_manual.py b/examples/example_manual.py index 7a0eed62..7fe34235 100644 --- a/examples/example_manual.py +++ b/examples/example_manual.py @@ -9,16 +9,14 @@ import matplotlib.pyplot as plt ### INPUTS ### -inputfolder = './Inputs/' -bathfile = 'bathymetry200m_Array.txt' -soilfile = 'soil_sample.txt' -boundfile = 'boundary_sample.csv' -moordynfile = 'Moordyn_semitaut200m.dat' +dir = os.path.dirname(os.path.realpath(__file__)) +bathfile = dir+'\bathymetry200m_Array.txt' +soilfile = dir+'\soil_sample.txt' +boundfile = dir+'\boundary_sample.csv' +moordynfile = dir+'\Moordyn_semitaut200m.dat' n_pfs = 4 pf_spacing = 1600 -# change to input directory -os.chdir(inputfolder) # create empty project project = Project() diff --git a/examples/example_sharedmoorings.py b/examples/example_sharedmoorings.py index 3e4d156b..b2f40083 100644 --- a/examples/example_sharedmoorings.py +++ b/examples/example_sharedmoorings.py @@ -4,21 +4,19 @@ a shared anchor. The purpose of this example is to show the Ontology inputs for shared systems and how these systems might appear in FAModel """ -import os import famodel from famodel import Project import matplotlib.pyplot as plt +import os # point to location of yaml file with uniform array info -input_directory = 'Inputs/' # relative location of directory for input files (yaml, bath files, etc) -filename = 'OntologySample600m_shared.yaml' # yaml file for project +dir = os.path.dirname(os.path.realpath(__file__)) +filename = '\OntologySample600m_shared.yaml' # yaml file for project -# switch to directory of input files -os.chdir(input_directory) - # load in yaml -project = Project(file=filename,raft=True) +project = Project(file=dir+filename, raft=True) + # plot in 2d and 3d project.plot2d() diff --git a/examples/Inputs/gch.yaml b/examples/gch.yaml similarity index 100% rename from examples/Inputs/gch.yaml rename to examples/gch.yaml diff --git a/examples/Inputs/iea_15MW.yaml b/examples/iea_15MW.yaml similarity index 100% rename from examples/Inputs/iea_15MW.yaml rename to examples/iea_15MW.yaml diff --git a/examples/Inputs/maine_rose.csv b/examples/maine_rose.csv similarity index 100% rename from examples/Inputs/maine_rose.csv rename to examples/maine_rose.csv diff --git a/examples/platform_ms.py b/examples/platform_ms.py index 07a96df8..97671673 100644 --- a/examples/platform_ms.py +++ b/examples/platform_ms.py @@ -9,13 +9,11 @@ from moorpy.helpers import subsystem2Line # get locations of files -cwd = os.getcwd() -input_dir = cwd+'/Inputs/' -yaml_file = 'OntologySample200m_uniformArray.yaml' +dir = os.path.dirname(os.path.realpath(__file__)) +yaml_file = '\OntologySample200m_uniformArray.yaml' -os.chdir(input_dir) # create project -project = Project(file=yaml_file) +project = Project(file=dir+yaml_file) # create a moorpy system for a single platform project.platformList['fowt0'].mooringSystem(project=project) diff --git a/examples/Inputs/soil_sample.txt b/examples/soil_sample.txt similarity index 100% rename from examples/Inputs/soil_sample.txt rename to examples/soil_sample.txt diff --git a/examples/uniformArray.py b/examples/uniformArray.py index 9f69a4d4..8b185179 100644 --- a/examples/uniformArray.py +++ b/examples/uniformArray.py @@ -12,17 +12,15 @@ import matplotlib.pyplot as plt # point to location of yaml file with uniform array info -input_directory = 'Inputs/' # relative location of directory for input files (yaml, bath files, etc) -filename = 'OntologySample200m_uniformArray.yaml' # yaml file to make initial platform(s) +dir = os.path.dirname(os.path.realpath(__file__)) +filename = '\OntologySample200m_uniformArray.yaml' # yaml file to make initial platform(s) # This yaml file does not contain explicit locations of each platform in the array table, # but rather has a 'uniform_array' section that describes # of rows, cols, spacing, etc. # This info is then used to automatically make a uniform array when the yaml file is loaded -# switch to directory of input files -os.chdir(input_directory) # load in yaml -project = Project(file=filename,raft=True) +project = Project(file=dir+filename,raft=True) project.plot2d() # plot the system project.plot3d(fowt=True) diff --git a/famodel/anchors/anchor.py b/famodel/anchors/anchor.py index 847c6263..28c2671a 100644 --- a/famodel/anchors/anchor.py +++ b/famodel/anchors/anchor.py @@ -123,10 +123,9 @@ def makeMoorPyAnchor(self, ms): MoorPy system ''' - # create anchor as a fixed point in MoorPy system - ms.addPoint(1,self.r) - # assign this point as mpAnchor in the anchor class instance - self.mpAnchor = ms.pointList[-1] + # create anchor as a fixed body in MoorPy system and assign to mpAnchor property + r6 = [self.r[0],self.r[1],self.r[2],0,0,0] + self.mpAnchor = ms.addBody(1,r6) # add mass if available if 'm' in self.dd['design'] and self.dd['design']['m']: @@ -550,14 +549,14 @@ def makeEqual_TaTm(mudloads): # get line type for att in self.attachments.values(): if isinstance(att['obj'],Mooring): - mtype = att['obj'].dd['sections'][0]['type']['material'] + mtype = att['obj'].sections()[0]['type']['material'] if not 'chain' in mtype: print('No chain on seafloor, setting Ta=Tm') nolugload = True break else: - md = att['obj'].dd['sections'][0]['type']['d_nom'] - mw = att['obj'].dd['sections'][0]['type']['w'] + md = att['obj'].sections()[0]['type']['d_nom'] + mw = att['obj'].sections()[0]['type']['w'] soil = next(iter(self.soilProps.keys()), None) ground_conds = self.soilProps[soil] # update soil conds as needed to be homogeneous diff --git a/famodel/cables/cable.py b/famodel/cables/cable.py index 21ca3ada..bbbe540c 100644 --- a/famodel/cables/cable.py +++ b/famodel/cables/cable.py @@ -8,8 +8,9 @@ from famodel.cables.dynamic_cable import DynamicCable from famodel.cables.static_cable import StaticCable -from famodel.cables.components import Joint +from famodel.cables.components import Joint, Jtube from famodel.famodel_base import Edge +from famodel.helpers import cableDesignInterpolation class Cable(Edge): @@ -109,8 +110,7 @@ def __init__(self, id, d=None): self.r = [] # get cable length - self.getL() - + self.getL() # failure probability self.failure_probability = {} @@ -122,7 +122,7 @@ def reposition(self,headings=None,project=None,rad_fair=[]): Parameters ---------- headings : list, optional - List of headings associated with the platform/substation attached + List of absolute compass headings associated with the platform/substation attached to each end of the cable. The default is None. project : FAModel project object, optional FAModel project object associated with this cable, only used if @@ -139,24 +139,43 @@ def reposition(self,headings=None,project=None,rad_fair=[]): ''' # reposition cable and set end points for the first and last cable sections (or the dynamic cable for a suspended cable) if not headings: - headingA = self.subcomponents[0].headingA - self.attached_to[0].phi - headingB = self.subcomponents[-1].headingB - self.attached_to[1].phi + # convert headings to unit circle + headingA = np.pi/2 - self.subcomponents[0].headingA + headingB = np.pi/2 - self.subcomponents[-1].headingB else: - headingA = headings[0] - headingB = headings[1] + # convert headings to unit circle + headingA = np.pi/2 - headings[0] + self.subcomponents[0].headingA = headings[0] + headingB = np.pi/2 - headings[1] + self.subcomponents[-1].headingB = headings[1] - if not rad_fair: - rad_fair = [self.attached_to[x].rFair if self.attached_to[x].rFair else 0 for x in range(2)] - else: - for i,r in enumerate(rad_fair): - if r==None: - rad_fair[i] = self.attached_to[i].rFair if self.attached_to[i] else 0 + if not isinstance(self.subcomponents[0].attached_to[0], Jtube): + if not rad_fair: + rf = self.attached_to[0].rFair if self.attached_to[0] else 0 + else: + if rad_fair[0] == None: + rf = self.attached_to[0].rFair if self.attached_to[0] else 0 + else: + rf = rad_fair[0] + + # calculate fairlead locations + Aloc = [self.attached_to[0].r[0]+np.cos(headingA)*rf, + self.attached_to[0].r[1]+np.sin(headingA)*rf, + self.attached_to[0].zFair+self.attached_to[0].r[2]] + self.subcomponents[0].rA = Aloc; self.rA = Aloc + if not isinstance(self.subcomponents[-1].attached_to[-1], Jtube): + if not rad_fair: + rf = self.attached_to[1].rFair if self.attached_to[1] else 0 + else: + if rad_fair[1] == None: + rf = self.attached_to[1].rFair if self.attached_to[1] else 0 + else: + rf = rad_fair[1] - # calculate fairlead locations (can't use reposition method because both ends need separate repositioning) - Aloc = [self.attached_to[0].r[0]+np.cos(headingA)*rad_fair[0], self.attached_to[0].r[1]+np.sin(headingA)*rad_fair[0], self.attached_to[0].zFair] - Bloc = [self.attached_to[1].r[0]+np.cos(headingB)*rad_fair[1], self.attached_to[1].r[1]+np.sin(headingB)*rad_fair[1], self.attached_to[1].zFair] - self.subcomponents[0].rA = Aloc; self.rA = Aloc - self.subcomponents[-1].rB = Bloc; self.rB = Bloc + Bloc = [self.attached_to[1].r[0]+np.cos(headingB)*rf, + self.attached_to[1].r[1]+np.sin(headingB)*rf, + self.attached_to[1].zFair+self.attached_to[1].r[2]] + self.subcomponents[-1].rB = Bloc; self.rB = Bloc if project: # set end points of subcomponents @@ -166,14 +185,25 @@ def reposition(self,headings=None,project=None,rad_fair=[]): if lensub > 1: for i,sub in enumerate(self.subcomponents): if i == 0: + oldz = project.getDepthAtLocation(sub.rB[0], + sub.rB[1]) # get depth at location of rB xy = [sub.rA[0]+np.cos(headingA)*sub.span, sub.rA[1]+np.sin(headingA)*sub.span] z = project.getDepthAtLocation(xy[0],xy[1]) + # adjust design if applicable + if sub.alternate_designs is not None and oldz!=z: + sub.dd = cableDesignInterpolation( + sub.dd, + sub.alternate_designs, + z) + self.reposition() # recursively call reposition # set the end B of the first subsection sub.rB = [xy[0],xy[1],-z] sub.z_anch = -z sub.depth = z + + # set joint self.subcomponents[i+1]['r'] = sub.rB # set rA of next cable section @@ -182,6 +212,13 @@ def reposition(self,headings=None,project=None,rad_fair=[]): xy = [sub.rB[0]+np.cos(headingB)*sub.span, sub.rB[1]+np.sin(headingB)*sub.span] z = project.getDepthAtLocation(xy[0],xy[1]) + # adjust design if applicable + if sub.alternate_designs is not None and oldz!=z: + sub.dd = cableDesignInterpolation( + sub.dd, + sub.alternate_designs, + z) + self.reposition() # re-call reposition # set the end A of the last subsection sub.rA = [xy[0],xy[1],-z] # update z_anch and depth of the subsection @@ -236,7 +273,7 @@ def getL(self): self.L = L def makeLine(self,buff_rad=20,include_dc=True): - + '''Make a 2D shapely linestring of the cable''' coords = [] for sub in self.subcomponents: if isinstance(sub,Joint): @@ -253,6 +290,67 @@ def makeLine(self,buff_rad=20,include_dc=True): return(line) + def dynamicCables(self): + """ Return list of dynamic cables in this cable object """ + return [a for a in self.subcomponents if isinstance(a, DynamicCable)] + + def updateTensions(self, DAF=1): + """ + Update the tensions stored in dynamic cable load dictionaries + + Returns + ------- + None. + + """ + for dc in self.dynamicCables(): + if not 'Tmax' in dc.loads: + dc.loads['Tmax'] = 0 + if dc.ss: + for line in dc.ss.lineList: + Tmax = max([abs(line.TA), abs(line.TB)]) + if Tmax*DAF > dc.loads['Tmax']: + dc.loads['Tmax'] = deepcopy(Tmax)*DAF + return(dc.loads['Tmax']) + + # def updateCurvature(self): + # for dc in self.dyamicCables(): + # if not dc.curvature: + # dc.curvature = np.inf + # if dc.ss: + # curv = dc.ss.calcCurvature() + # if curv > dc.curvature: + # dc.curvature = curv + # mCSF = dc.ss.getMinCurvSF() + + def updateSafetyFactors(self, key='tension', load='Tmax', prop='MBL', + info={}): + """ + Update the safety factor dictionaries stored in dynamic cable objects + + Parameters + ---------- + key : str/int, optional + key in safety factor dictionary of dynamic cables. + The default is 'tension'. + load : str, optional + Key in load dictionary of dynamic cables. The default is 'Tmax'. + prop : str, optional + Key in dynamic cable properties dictionary to compare to load. + The default is 'MBL'. + info : dict, optional + Information dictionary to add in the safety_factors dict for context + + Returns + ------- + None. + + """ + for dc in self.dynamicCables(): + dc.safety_factors[key] = dc.dd['cable_type'][prop]/dc.loads[load] + dc.safety_factors['info'] = info + + def updateSpan(self,newSpan): ''' Change the lengths of subcomponents based on the new total span diff --git a/famodel/cables/components.py b/famodel/cables/components.py index 9f108339..5844e3ce 100644 --- a/famodel/cables/components.py +++ b/famodel/cables/components.py @@ -61,6 +61,11 @@ def makeMoorPyConnector(self, ms): return(ms) +class Jtube(Node,dict): + def __init__(self,id, r=None,**kwargs): + dict.__init__(self, **kwargs) # initialize dict base class (will put kwargs into self dict) + Node.__init__(self, id) # initialize Node base class + """ class Cable(Edge, dict): '''A length of a subsea power cable product (i.e. same cross section of diff --git a/famodel/cables/dynamic_cable.py b/famodel/cables/dynamic_cable.py index 022a836f..59f6f9e3 100644 --- a/famodel/cables/dynamic_cable.py +++ b/famodel/cables/dynamic_cable.py @@ -79,6 +79,7 @@ def __init__(self, id, dd=None, subsystem=None, rA=[0,0,0], rB=[0,0,0], self.headingB = 0 elif 'headingB' in self.dd: self.headingB = self.dd['headingB'] # <<< ?? + # if there's no headingA, likely a suspended cable - headingA = headingB+180 degrees self.headingA = 0 else: @@ -109,6 +110,9 @@ def __init__(self, id, dd=None, subsystem=None, rA=[0,0,0], rB=[0,0,0], self.rho = rho self.g = g + # alternate designs to interpolate between when depth changes + self.alternate_designs = None + # Dictionaries for addition information self.loads = {} self.safety_factors = {} # calculated safety factor diff --git a/famodel/cables/static_cable.py b/famodel/cables/static_cable.py index 76a7bd47..055e2eab 100644 --- a/famodel/cables/static_cable.py +++ b/famodel/cables/static_cable.py @@ -181,13 +181,12 @@ def updateRouting(self,coords=None): ''' - if coords: + if coords is not None and len(coords)>0: self.x = [coord[0] for coord in coords] # cable route vertex global x coordinate [m] self.y = [coord[1] for coord in coords] # cable route vertex global y coordinate [m] # Check if radius available if len(coords[0]) == 3: self.r = [coord[2] for coord in coords] # cable route vertex corner radius [m] - # update static cable length self.getLength() diff --git a/famodel/design/CableDesign.py b/famodel/design/CableDesign.py new file mode 100644 index 00000000..cd182529 --- /dev/null +++ b/famodel/design/CableDesign.py @@ -0,0 +1,1312 @@ +# Draft Cable Design class adapted from LineDesign - R + +import moorpy as mp +from moorpy.subsystem import Subsystem + +from famodel.cables.dynamic_cable import DynamicCable +import famodel.cables.cable_properties as cprops + +from fadesign.fadsolvers import dopt2, doptPlot +from moorpy.helpers import getLineProps, getFromDict +from copy import deepcopy + +import numpy as np +import matplotlib.pyplot as plt +import yaml +import time + + +class CableDesign(DynamicCable): + ''' + The Dynamic Cable class inherits the properties of MoorPy's Subsystem class + (i.e. solving for equilibrium) for the purpose of quasi-static design and + analysis. Eventually the DynamicCable class will live in FAModel, and this + code will be streamlined to inherit it and just add design methods. + + Example allVars vector: X = [span, L, ...] + where < > section repeats and is composed of + B - net buoyancy provided by all modules on this section [kN] + Lmid - the buoyancy section midpoint along the cable arc length [m] + Ls - the length of this buoyancy section (centered about the midpoint) [m] + Xindices + specify the design variable number, or optional key characters: + c - constant, will not be changed + r - the AllVars value will be interpreted as a ratio to the total length + In other words, the actual value will be the specified value times L. + ''' + + def __init__(self, depth, cableType, buoyType, n=3, i_buoy=[1], mgdict = None, **kwargs): + '''Creates a DynamicCable object to be used for evaluating or + optimizing a dynamic cable design. + + Parameters + ---------- + depth : float + Water depth + span : float + Horizontal distance of dynamic cable [m]. + n : int + Number of sections (typically alternating: cable, cable+buoyancy, ...) + i_buoy : list + List of section indices that can have buoyancy modules. + cableType : dict + Dictionary of bare cable properties. + buoyType : dict + Dictionary of buoyancy module properties. + name : string + Name of dictionary entry in cableProps yaml file to get data from. + X0 : array + Initial design variable values (length n). + offset : float + Maximum mean/steady offset in surge [m]. + ''' + + self.display = getFromDict(kwargs, 'display', default=0) + + # add the parameters set by the input settings dictionary + self.name = getFromDict(kwargs, 'name', dtype=str, default='no name provided') + + # set up the mooring system object with the basics from the System class + rho = getFromDict(kwargs, 'rho', default=1025.0) + g = getFromDict(kwargs, 'g' , default=9.81) + + # ----- set model-specific parameters ----- + + self.shared = getFromDict(kwargs, 'shared', default=0) # flag to indicate shared line + self.rBFair = getFromDict(kwargs, 'rBFair', shape=-1, default=[0,0,0]) # [m] end coordinates relative to attached body's ref point + self.nLines = n # number of sections + self.i_buoy = i_buoy # index of any sections that have buoyancy modules + self.bs = [0 for i in range(0,len(i_buoy))] + + #-------set marine growth parameters--------------------- + self.MG = getFromDict(kwargs, 'MG', default = False) + if self.MG: + if mgdict == None: + raise Exception('mgdict must be provied if MG == True') + else: + self.mgdict = mgdict + + # ============== set the design variable list ============== + self.ignore_static = getFromDict(kwargs, 'ignore_static', default=False) + + self.allVars = getFromDict(kwargs, 'allVars' , shape=2 + 3*len(self.i_buoy)) + + # set the design variable type list + if 'Xindices' in kwargs: + self.Xindices = list(kwargs['Xindices']) + if not len(self.Xindices)==len(self.allVars): + raise Exception("Xindices must be the same length as allVars") + else: + raise Exception("Xindices must be provided.") + + + # number of design variables (the design vector is the length of each + # find the largest integer to determine the number of desired design variables + self.nX = 1 + max([ix for ix in self.Xindices if isinstance(ix, int)]) + + + # check for errors in Xindices + for i in range(self.nX): + if not i in self.Xindices: + raise Exception(f"Design variable number {i} is missing from Xindices.") + # entries must be either design variable index or constant/solve/ratio flags + valid = list(range(self.nX))+['c','r'] + for xi in self.Xindices: + if not xi in valid: + raise Exception(f"The entry '{xi}' in Xindices is not valid. Must be a d.v. index, 'c', or 'r'.") + + # check for 'r' variable option + self.rInds = [i for i,xi in enumerate(self.Xindices) if xi=='r'] + for i in range(len(self.rInds)): + if self.allVars[self.rInds[i]] >= 1.0 or self.allVars[self.rInds[i]] <= 0.0: + raise Exception("The ratio variable needs to be between 1 and 0") + + + # ----- Initialize some objects ----- + + self.span = self.allVars[0] + self.L = self.allVars[1] + + # Store the bare cable type by itself for easy access (TODO: reduce redundancy) + self.cableType = cableType + self.buoyType = buoyType + + # make a dummy design dictionary for Mooring to make a Subsystem with??? + dd = {} + + # The bare cable properties dict + dd['cable_type'] = cableType + + #length properties + dd['length'] = self.L + + #span + dd['span'] = self.span + + # Buoyancy section properties + + for i in range(len(i_buoy)): + + # Net buoyancy per buoyancy module [N] + F_buoy = (rho - buoyType['density'])*g*buoyType['volume'] + + # Buoyancy + if self.shared == 2 and i ==0: + N_modules = 1000*self.allVars[3*i+2] / (F_buoy) / 2 # split buoyancy force across the full length + else: + N_modules = 1000*self.allVars[3*i+2] / F_buoy # my not be an integer, that's okay + + + # L_mid (position along cable) + if self.Xindices[3*i + 3] == 'r': + + #set equal to ratio * cable length + L_mid = self.allVars[3*i+3] * self.L + else: + L_mid = self.allVars[3*i+3] + + # Spacing + spacing = self.allVars[3*i+4] / (N_modules - 1) + + if N_modules > 0: + if not 'buoyancy_sections' in dd: + dd['buoyancy_sections'] = [] + dd['buoyancy_sections'].append(dict(L_mid=L_mid, + module_props=buoyType, + N_modules = N_modules, + spacing = spacing)) + + # Call Mooring init function (parent class) + if self.shared == 1: + + DynamicCable.__init__(self, 'designed cable', dd=dd, + rA=[self.span,0,self.rBFair[2]], rB=self.rBFair, + rad_anch=self.span, rad_fair=self.rBFair[0], z_anch=-depth, + z_fair=self.rBFair[2], rho=rho, g=g, span=self.span, length=self.L, shared = self.shared) # arbitrary initial length + + elif self.shared == 2: + DynamicCable.__init__(self, 'designed cable', dd=dd, + rA=[-0.5*self.span-self.rBFair[0], 0, -1], rB=self.rBFair, + rad_anch=self.span, rad_fair=self.rBFair[0], z_anch=-depth, + z_fair=self.rBFair[2], rho=rho, g=g, span=self.span, length=self.L, shared = self.shared) # arbitrary initial length + + else: + DynamicCable.__init__(self, 'designed cable', dd=dd, + rA=[self.span,0,-depth], rB=self.rBFair, + rad_anch=self.span, rad_fair=self.rBFair[0], z_anch=-depth, + z_fair=self.rBFair[2], rho=rho, g=g, span=self.span, length=self.L, shared = self.shared) # arbitrary initial length + + + + # now make Subsystem, self.ss + self.createSubsystem(case=int(self.shared)) + self.ss.eqtol= 0.05 # position tolerance to use in equilibrium solves [m] + + # amplification factors etc. + self.DAFs = getFromDict(kwargs, 'DAFs', shape=self.nLines+2, default=1.0) # dynamic amplication factor for each line section, and anchor forces (DAFS[-2] is for vertical load, DAFS[-1] is for horizontal load) + self.Te0 = np.zeros([self.nLines,2]) # undisplaced tension [N] of each line section end [section #, end A/B] + self.LayLen_adj = getFromDict(kwargs, 'LayLen_adj', shape=0, default=0.0) # adjustment on laylength... positive means that the dynamic lay length is greater than linedesign laylength + self.damage = getFromDict(kwargs, 'damage', shape = -1, default = 0.0) #Lifetime fatigue damage from previous iteration for wind/wave headings in self.headings and the 180 degree reverse of self.headings + #self.unload(f'{configuration}.dat') + + + # ----- Set solver and optimization settings ----- + self.x_mean = getFromDict(kwargs, 'offset', default=0) + self.x_ampl = getFromDict(kwargs, 'x_ampl', default=10) # [m] expected wave-frequency motion amplitude about mean + + self.eqtol = 0.002 # position tolerance to use in equilibrium solves [m] + self.noFail = False # can be set to True for some optimizers to avoid failing on errors + + self.iter = -1 # iteration number of a given optimization run (incremented by updateDesign) + self.log = dict(x=[], f=[], g=[]) # initialize a log dict with empty values + + + # ----- optimization stuff ----- + # get design variable bounds and last step size + self.Xmin = getFromDict(kwargs, 'Xmin' , shape=self.nX, default=np.zeros(self.nX)) # minimum bounds on each design variable + self.Xmax = getFromDict(kwargs, 'Xmax' , shape=self.nX, default=np.zeros(self.nX)+1000) # maximum bounds on each design variable + self.dX_last = getFromDict(kwargs, 'dX_last', shape=self.nX, default=[]) # 'last' step size for each design variable + + if len(self.Xmin) != self.nX or len(self.Xmax) != self.nX or len(self.dX_last) != self.nX: + raise Exception("The size of Xmin/Xmax/dX_last does not match the number of design variables") + + #set up initial design variable values from allVars input + self.X0 = np.array([self.allVars[self.Xindices.index(i)] for i in range(self.nX)]) + + # initialize the vector of the last design variables, which each iteration will compare against + self.Xlast = np.zeros(self.nX) + + self.X_denorm = np.ones(self.nX) # normalization factor for design variables + self.obj_denorm = 1.0 # normalization factor for objective function + + + # ----- set up the constraint functions and lists ----- + + if 'constraints' in kwargs: + self.constraints = kwargs['constraints'] + else: + self.constraints = {} + #raise ValueError('A constraints dictionary must be passed when initializing a new Mooring') + + # a hard-coded dictionary that points to all of the possible constraint functions by name + self.confundict = {"max_total_length" : self.con_total_length, # maximum total length of combined line sections + "min_lay_length" : self.con_lay_length, # minimum length of a line section on the seabed + "max_lay_length" : self.con_max_lay_length, # minimum length of a line section on the seabed + "tension_safety_factor" : self.con_strength, # minimum ratio of MBL/tension for a section + "overall_tension_safety_factor" : self.con_overall_strength, # minimum ratio of MBL/tension for all sections + "curvature_safety_factor":self.con_curvature, # minimum ratio of curvature_limit/curvature for a section + "max_curvature" :self.con_max_curvature, # minimum ratio of curvature_limit/curvature for a section + "min_sag" : self.con_min_sag, # minimum for the lowest point of a section + "max_sag" : self.con_max_sag, # maximum for the lowest point of a section + "max_hog" : self.con_max_hog, # maximum for the highest point of a section + "max_touchdown_range" : self.con_max_td_range, # maximum for the lowest point of a section + } + + # set up list of active constraint functions + self.conList = [] + self.convals = np.zeros(len(self.constraints)) # array to hold constraint values + self.con_denorm = np.ones(len(self.constraints)) # array to hold constraint normalization constants + self.con_denorm_default = np.ones(len(self.constraints)) # default constraint normalization constants + + for i, con in enumerate(self.constraints): # for each list (each constraint) in the constraint dictionary + + # ensure each desired constraint name matches an included constraint function + if con['name'] in self.confundict: + + # the constraint function for internal use (this would be called in UpdateDesign) + def internalConFun(cc, ii): # this is a closure so that Python doesn't update index and threshold + def conf_maker(X): + def func(): + # compute the constraint value using the specified function + val = self.confundict[cc['name']](X, cc['index'], cc['threshold']) + + # record the constraint value in the list + self.convals[ii] = val / self.con_denorm[ii] # (normalized) + self.constraints[ii]['value'] = val # save to dict (not normalized) + + return val + return func() + return conf_maker + + # make the internal function and save it in the constraints dictionary + con['fun'] = internalConFun(con, i) + + # the externally usable constraint function maker + def externalConFun(name, ii): # this is a closure so that Python doesn't update index and threshold + def conf_maker(X): + def func(): + # Call the updatedesign function (internally avoids redundancy) + self.updateDesign(X) + + # get the constraint value from the internal list + val = self.convals[ii] + + return val + return func() + return conf_maker + + # add the conf function to the conList + self.conList.append(externalConFun(con['name'], i)) + + # Save the default/recommended normalization constant + + if con['name'] in ['max_total_length']: + self.con_denorm_default[i] = con['threshold'] # sum([line.L for line in self.ss.lineList]) + + elif con['name'] in ['tension_safety_factor', 'curvature_safety_factor']: + self.con_denorm_default[i] = 4*con['threshold'] + + elif con['name'] in ['max_curvature']: + self.con_denorm_default[i] = 4*con['threshold'] + + elif con['name'] in ['min_lay_length', 'min_sag', 'max_sag', 'max_hog', 'max_touchdown_range']: + self.con_denorm_default[i] = depth + + else: + raise ValueError("Constraint parameter "+con['name']+" is not a supported constraint type.") + + + + # ----- Set up the cable properties ----- + ''' + # For now, this will load the cable properties YAML and manually add + # the selected cable type to the MoorPy system. + with open(cableProps) as file: + source = yaml.load(file, Loader=yaml.FullLoader) + + # Get dictionary of the specified cable type from the yaml + di = source['cable_types'][name] + + cableType = self.makeCableType(di, name) # Process/check it into a new dict + # ^^^ I forget why this is done + + # Save some constants for use when computing buoyancy module stuff + + self.d0 = cableType['d_vol'] # diameter of bare dynamic cable + self.m0 = cableType['m'] # mass/m of bare dynamic cable + self.w0 = cableType['w'] # weight/m of bare dynamic cable + + #self.rho_buoy = cableType['rho_buoy'] # aggregate density of buoyancy modules [kg/m^3] + ''' + + ''' + # ----- set up the dynamic cable in MoorPy ----- + + lengths = self.allVars[:self.nLines] # Length of each section [m] (first n entries of allVars) + types = [] + + # Set up line types list + for i in range(self.nLines): + # Give the buoyancy sections their own type so they can be adjusted independently + if i in self.i_buoy: + types.append(deepcopy(cableType)) + + else: # All bare cable sections can reference the same type + types.append(cableType) + + # call to the Subsystem method to put it all together + if self.shared == True: + + # set second platform connection at the same coordinates as the first platform connection + self.rAFair = self.rBFair + self.makeGeneric(lengths, types, suspended = 1) + else: + self.makeGeneric(lengths, types) + ''' + # initialize and equilibrate this initial cable + self.ss.initialize() + self.ss.maxIter = 5000 + self.ss.setOffset(0) + self.updateDesign(self.X0, normalized=False) # assuming X0/AllVars is not normalized + + + + def updateDesign(self, X, display=0, display2=0, normalized=True): + '''updates the design with the current design variables (X). + + Example allVars vector: X = [span, L, ...] + where < > section repeats and is composed of + B - net buoyancy provided by all modules on this section [N] + Lmid - the buoyancy section midpoint along the cable arc length + Ls - the length of this buoyancy section (centered about the midpoint) + Xindices + specify the design variable number, or optional key characters: + c - constant, will not be changed + r - the AllVars value will be interpreted as a ratio to the total length + In other words, the actual value will be the specified value times L. + + If self.shared==2, then the buoyancy sections are measured from the + center point of the cable, and are assumed to be mirrored on both sides. + ''' + + # Design vector error checks + if len(X)==0: # if any empty design vector is passed (useful for checking constraints quickly) + return + elif not len(X)==self.nX: + raise ValueError(f"DynamicCable.updateDesign passed design vector of length {len(X)} when expecting length {self.nX}") + elif any(np.isnan(X)): + raise ValueError("NaN value found in design vector") + + # If X is normalized, denormalize (scale) it up to the full values + if normalized: + X = X*self.X_denorm + + + # If any design variable has changed, update the design and the metrics + if not all(X == self.Xlast): + + self.Xlast = np.array(X) # record the current design variables + + if self.display > 1: + print("Updated design") + print(X) + + self.iter += 1 + + # ----- Apply the design variables to update the design ----- + + # Update span + dvi = self.Xindices[0] # design variable index - will either be an integer or a string + if dvi in range(self.nX): # only update if it's tied to a design variable (if it's an integer) + self.span = X[dvi] + self.dd['span'] = self.span + + # Update total cable length + dvi = self.Xindices[1] + if dvi in range(self.nX): + self.L = X[dvi] + self.dd['length'] = X[dvi] # some redundancy - need to streamline DynamicCable + + + # Update each buoyancy section + for i in range(len(self.i_buoy)): + + bs = self.dd['buoyancy_sections'][i] # shorthand for the buoyancy section dict + + # Net buoyancy per buoyancy module [N] + F_buoy = (self.rho - bs['module_props']['density'])*self.g*bs['module_props']['volume'] + + + # Buoyancy + dvi = self.Xindices[3*i+2] # buoyancy design variable index + if dvi in range(self.nX): # only update if it's tied to a design variable + if self.shared == 2 and i == 0: + bs['N_modules'] = 1000*X[dvi] / F_buoy / 2 # my not be an integer, that's okay + else: + bs['N_modules'] = 1000*X[dvi] / F_buoy # my not be an integer, that's okay + + + # L_mid (position along cable) + dvi = self.Xindices[3*i+3] # buoyancy design variable index + if dvi in range(self.nX): # only update if it's tied to a design variable + bs['L_mid'] = X[dvi] + elif dvi == 'r': + bs['L_mid'] = self.allVars[3*i+3] * self.L + + # Spacing + dvi = self.Xindices[3*i+4] # buoyancy design variable index + if dvi in range(self.nX): # only update if it's tied to a design variable + length = X[dvi] + else: + length = self.allVars[3*i+4] + + bs['spacing'] = length / (bs['N_modules'] - 1) + + #store the buoyancy module spacing + self.bs[i] = bs['spacing'] + + + + # get these design dictionary changes applied in DynamicCable + if len(self.i_buoy) > 0: + self.updateSubsystem() + else: + self.ss.lineList[0].setL(self.L) + + ''' + for i in range(self.nLines): # go through each section + + # update the section length from the design variables + dvi = self.Xindices[i] #design variable index + + # only update if design variable is in list (not constant) + if dvi in range(self.nX): + L=X[dvi] + self.ss.lineList[i].setL(L) + + # if the line has buoyancy, apply the buoyancy design variable + if i in self.i_buoy: + + # check if design variable is in list (not constant) + dvi = self.Xindices[self.nLines + self.i_buoy.index(i)] + if dvi in range(self.nX): + B = X[dvi] # take the buoyancy per unit length design variable [N/m] + + #handle cases where buoyancy is fixed + else: + B = self.allVars[self.nLines + i - 1] + + # compute what diameter of buoyancy module is needed to achieve this buoyancy per unit length + d_inner = self.d0 # inner diameter for buoyancy module [m] + rho_buoy = self.rho_buoy # constant density of buoyancy modules + + d_outer = np.sqrt(((4*B)/((self.rho-self.rho_buoy)*np.pi*self.g))+d_inner**2) # required outer diameter of buoyancy modules (assuming spread over section length) [m] + m_buoy = rho_buoy*(np.pi/4*(d_outer**2 - d_inner**2)) # mass per meter of spread buoyancy module [kg/m] + m = self.m0 + m_buoy # mass per unit length of combined cable + spread buoyancy modules [kg/m] + w = m*self.g - self.rho*(np.pi/4*(d_outer**2))*self.g # weight per unit length [N/m] + + # update line properties + self.ss.lineTypes[i]['m'] = m + self.ss.lineTypes[i]['w'] = w + self.ss.lineTypes[i]['d_vol'] = d_outer + ''' + + + # ----- evaluate constraints ----- + # Evaluate any constraints in the list, at the appropriate displacements. + # The following function calls will fill in the self.convals array. + + #increase solveEquilibrium tolerance + self.ss.eqtol = 0.05 + + + if self.MG: + self.addMarineGrowth(self.mgdict) + self.ss_mod.eqtol = 0.05 + self.ss_mod.maxIter = 5000 + + + # ZERO OFFSET: + self.ss.setOffset(0) + self.ss.calcCurvature() + + if self.MG: + self.ss_mod.setOffset(0) + self.ss_mod.calcCurvature() + + # Save tensions # these aren't used anywhere... not saving the MG tensions + for i, line in enumerate(self.ss.lineList): + self.Te0[i,0] = np.linalg.norm(line.fA) + self.Te0[i,1] = np.linalg.norm(line.fB) + + # Call any constraints that evaluate at the undisplaced position + for con in self.constraints: + if con['offset'] == 'zero': + con['fun'](X) + + # MAX OFFSET: + self.ss.setOffset(self.x_mean+self.x_ampl) # apply static + dynamic offsets + self.ss.calcCurvature() + + if self.MG: + self.ss_mod.setOffset(self.x_mean+self.x_ampl) + self.ss_mod.calcCurvature() + + # Call any constraints needing a positive displacement + for con in self.constraints: + if con['offset'] == 'max': + con['fun'](X) + + self.min_lay_length = self.ss.getLayLength() # record minimum lay length + + if self.MG: + self.min_lay_length = min([self.ss.getLayLength(), self.ss_mod.getLayLength()]) + + # MIN OFSET: + self.ss.setOffset(-self.x_mean-self.x_ampl) # apply static + dynamic offsets + self.ss.calcCurvature() + + if self.MG: + self.ss_mod.setOffset(-self.x_mean-self.x_ampl) + self.ss_mod.calcCurvature() + + # Call any constraints needing a negative displacement + for con in self.constraints: + if con['offset'] == 'min': + con['fun'](X) + + self.max_lay_length = self.ss.getLayLength() # record maximum lay length + if self.MG: + self.max_lay_length = max([self.ss.getLayLength(), self.ss_mod.getLayLength()]) + + # OTHER: + self.ss.setOffset(0) # restore to zero offset and static EA + if self.MG: + self.ss_mod.setOffset(0) + + # or at least set back to static states + + # Call any constraints that depend on results across offsets + for con in self.constraints: + if con['offset'] == 'other': + con['fun'](X) + + + # --- evaluate objective function --- + + # calculate the cost of each section + self.cost = {} + + if self.ignore_static: # option to ignore static portion of cable in cost calcs + L = self.L - self.min_lay_length + else: + L = self.L + + self.cost['cable'] = L*self.cableType['cost'] + + self.cost['buoyancy'] = 0 + + if 'buoyancy_sections' in self.dd: + for bs in self.dd['buoyancy_sections']: + self.cost['buoyancy'] += bs['N_modules']*self.buoyType['cost'] + + self.cost['total'] = self.cost['cable'] + self.cost['buoyancy'] + + self.obj_val = self.cost['total'] / self.obj_denorm # normalize objective function value + + # could also add a cost for touchdown protection sleeve based on + # the touchdown point range of motion + # e.g. c_touchdwown_protection = (self.max_lay_length - self.min_lay_length) * cost_factor + + + # ----- write to log ----- + + # log the iteration number, design variables, objective, and constraints + self.log['x'].append(list(X)) + self.log['f'].append(list([self.obj_val])) + self.log['g'].append(list(self.convals)) + + + # provide some output? + if display > 5: + f = self.objectiveFun(X, display=1) + print("Line lengths are ") + for line in self.ss.lineList: + print(line.L) + + print(f"Cost is {f}") + self.evaluateConstraints(X, display=1) + self.ss.plotProfile() + plt.show() + + + def objectiveFun(self, X, display=0, normalized=True): + '''Update the design (if necessary) and return the objective function + (cost) value.''' + + self.updateDesign(X, display=display, normalized=normalized) + + if display > 1: + print(f"Cost is {self.cost['total']:.1f} and objective value is {self.obj_val:.3f}.") + + return self.obj_val + + + def evaluateConstraints(self, X, display=0, normalized=True): + '''Update the design (if necessary) and display the constraint + values.''' + + self.updateDesign(X, display=display, normalized=normalized) + + if display > 1: + for i, con in enumerate(self.constraints): + print(f" Constraint {i:2d} value of {con['value']:8.2f} " + +f"for {con['name']}: {con['threshold']} of {con['index']} at {con['offset']} displacement.") + + return self.convals + + + def setNormalization(self): + '''Set normalization factors for optimization + (based on initial design state).''' + + # design variables + self.X_denorm = np.array(self.Xlast) + # objective + self.obj_denorm = self.cost['total'] + # constraints + self.con_denorm = self.con_denorm_default + + + def clearNormalization(self): + '''Clear any normalization constants to unity so no scaling is done.''' + self.X_denorm = np.ones(self.nX) + self.obj_denorm = 1.0 + self.con_denorm = np.ones(len(self.constraints)) + + + def optimize(self, gtol=0.03, maxIter=40, nRetry=0, plot=False, display=0, stepfac=4, method='dopt'): + '''Optimize the design variables according to objectve, constraints, bounds, etc. + ''' + + # reset iteration counter + self.iter = -1 + + # clear optimization progress tracking lists + self.log['x'] = [] + self.log['f'] = [] + self.log['g'] = [] + + # set combined objective+constraints function for dopt + def eval_func(X): + '''DynamicCable object evaluation function''' + + self.updateDesign(X) + f = self.obj + g = np.array(self.convals) # needs to be a copy to not pass by ref + oths = dict(status=1) + + return f, g, [], [], oths, False + + # set the display value to use over the entire process + self.display = display + self.method = method + + # Set starting point to normalized value + X0 = self.X0 / self.X_denorm + dX_last = self.dX_last / self.X_denorm + Xmax = self.Xmax / self.X_denorm + Xmin = self.Xmin / self.X_denorm + + # call optimizer to perform optimization + if method=='dopt': + + if display > 0: print("\n --- Beginning CableDesign2 optimize iterations using DOPT2 ---") + + X, min_cost, infodict = dopt2(eval_func, X0, tol=0.001, a_max=1.4, maxIter=maxIter, stepfac=stepfac, + Xmin=Xmin, Xmax=Xmax, dX_last=dX_last, display=self.display) + + elif method in ['COBYLA', 'SLSQP']: + + from scipy.optimize import minimize + + if self.display > 0: print("\n --- Beginning CableDesign2 optimize iterations using COBYLA ---") + + condict = [dict(type="ineq", fun=con) for con in self.conList] + cons_tuple = tuple(condict) + + if method=='COBYLA': + result = minimize(self.objectiveFun, X0, constraints=cons_tuple, method="COBYLA", + options={'maxiter':maxIter, 'disp':True, 'rhobeg':0.1}) + #options={'maxiter':maxIter, 'disp':True, 'rhobeg':10.0}) + + elif method=='SLSQP': + result = minimize(self.objectiveFun, X0, constraints=cons_tuple, method='SLSQP', + bounds = list(zip(Xmin, Xmax)), + options={'maxiter':maxIter, 'eps':0.02,'ftol':1e-6, 'disp': True, 'iprint': 99}) + + X = result.x + + #elif method=='CMNGA': + # from cmnga import cmnga + + # bounds = np.array([[self.Xmin[i], self.Xmax[i]] for i in range(len(self.Xmin))]) + + # X, min_cost, infoDict = cmnga(self.objectiveFun, bounds, self.conList, dc=0.2, nIndivs=12, nRetry=100, maxGens=20, maxNindivs=500 ) + + #, maxIter=maxIter, stepfac=stepfac, Xmin=self.Xmin, Xmax=self.Xmax, dX_last=self.dX_last, display=self.display) + + else: + raise Exception('Optimization method unsupported.') + + # make sure it's left at the optimized state + self.updateDesign(X) + + # plot + if plot: + self.plotOptimization() + + return X, self.cost['total'] # , infodict + + + def plotOptimization(self): + + if len(self.log['x']) == 0: + print("No optimization trajectory saved (log is empty). Nothing to plot.") + return + + fig, ax = plt.subplots(len(self.X0)+1+len(self.constraints),1, sharex=True, figsize=[6,8]) + fig.subplots_adjust(left=0.4) + Xs = np.array(self.log['x']) + Fs = np.array(self.log['f']) + Gs = np.array(self.log['g']) + + for i in range(len(self.X0)): + ax[i].plot(Xs[:,i]) + #ax[i].axhline(self.Xmin[i], color=[0.5,0.5,0.5], dashes=[1,1]) + #ax[i].axhline(self.Xmax[i], color=[0.5,0.5,0.5], dashes=[1,1]) + + ax[len(self.X0)].plot(Fs) + ax[len(self.X0)].set_ylabel("cost", rotation='horizontal') + + for i, con in enumerate(self.constraints): + j = i+1+len(self.X0) + ax[j].axhline(0, color=[0.5,0.5,0.5]) + ax[j].plot(Gs[:,i]) + ax[j].set_ylabel(f"{con['name']}({con['threshold']})", + rotation='horizontal', labelpad=80) + + ax[j].set_xlabel("function evaluations") + + # ::::::::::::::::::::::::::::::: constraint functions ::::::::::::::::::::::::::::::: + + # Each should return a scalar C where C >= 0 means valid and C < 0 means violated. + + + def con_total_length(self, X, index, threshold): + '''This ensures that the total length of the Mooring does not result in a fully slack Mooring + (ProfileType=4) in its negative extreme mean position''' + # ['max_line_length', index, threshold] # index and threshold are completely arbitrary right now + + Lmax = (self.span-self.rBFair[0]-self.x_mean + self.depth+self.ss.rBFair[2]) # (3-14-23) this method might now be deprecated with more recent updates to ensure the combined line lengths aren't too large + total_linelength = sum([self.ss.lineList[i].L for i in range(self.nLines)]) + c = Lmax-total_linelength + + return c + + + def con_lay_length(self, X, index, threshold, display=0): + '''This ensures there is a minimum amount of line on the seabed at the +extreme displaced position.''' + + if self.MG: + minlaylength = min([self.ss.getLayLength(iLine=index),self.ss_mod.getLayLength(iLine=index)]) + else: + minlaylength = self.ss.getLayLength(iLine=index) + + return minlaylength - threshold + self.LayLen_adj + + def con_max_lay_length(self, X, index, threshold, display=0): + '''This ensures there is a minimum amount of line on the seabed at the +extreme displaced position.''' + + if self.MG: + minlaylength = min([self.ss.getLayLength(iLine=index),self.ss_mod.getLayLength(iLine=index)]) + else: + minlaylength = self.ss.getLayLength(iLine=index) + + return threshold - minlaylength + + def con_max_td_range(self, X, index, threshold, display=0): + '''Ensures the range of motion of the touchdown point betweeen the + range of offsets is less then a certain distance. + This constraint is for the system as a whole (index is ignored) and + must have offset='other' so that it's evaluated at the end.''' + return threshold - (self.max_lay_length - self.min_lay_length) + + """ + def con_buoy_contact(self, X, index, threshold, display=0): + '''This ensures the first line node doesn't touch the seabed by some minimum clearance in the +extreme displaced position.''' + return self.getPointHeight(index) - threshold + <<<< seems funny <<< + """ + + def con_strength(self, X, index, threshold, display=0): + '''This ensures the MBL of the line is always greater than the maximum + tension the line feels times a safety factor.''' + if self.MG: + minsf = min([self.ss.getTenSF(index),self.ss_mod.getTenSF(index)]) + else: + minsf = self.ss.getTenSF(index) + return minsf - threshold + + def con_overall_strength(self, X, index, threshold, display=0): + '''This ensures the MBL of the line is always greater than the maximum + tension the line feels times a safety factor. *** checks all line sections ***''' + + sfs = [] + + #check both ss_mod and ss if there's marine growth + if self.MG: + + #iterate through linelist and append safety factors + for index in range(0, len(self.ss_mod.lineList)): + minsf = self.ss_mod.getTenSF(index) + sfs.append(minsf - threshold) + + for index in range(0, len(self.ss.lineList)): + minsf = self.ss.getTenSF(index) + sfs.append(minsf - threshold) + + return min(sfs) + + + + def con_curvature(self, X, index, threshold, display=0): + '''Ensure that the MBR of the cable is always greater than the maximum + actual curvature times a safety factor.''' + if self.MG: + mincsf = min([ self.ss.getCurvSF(index), self.ss_mod.getCurvSF(index)]) + else: + mincsf = self.ss.getCurvSF(index) + return mincsf - threshold + + def con_max_curvature(self, x, index, threshold, display=0): + '''Ensures that the MBR divided by the maximum curvature over the + entire cable is greater than a threshold safety factor. + + >>> make a single set of cable props for the line overall + >>> then there will be more for the buoyancy sections ''' + if self.MG: + maxks = max([max(self.ss.Ks), max(self.ss_mod.Ks)]) + else: + maxks = max(self.ss.Ks) + return 1 /( self.cableType['MBR'] * maxks ) - threshold + + + def con_min_sag(self, X, index, threshold, display=0): + '''Ensure the lowest point of a line section is below + a minimum depth.''' + if self.MG: + minsag = min([self.ss.getSag(index), self.ss_mod.getSag(index)]) + else: + minsag = self.ss.getSag(index) + return threshold - minsag + + def con_max_sag(self, X, index, threshold, display=0): + '''Ensures the lowest point of a line section is above + a certain maximum depth.''' + if self.MG: + maxsag = max([self.ss.getSag(index), self.ss_mod.getSag(index)]) + else: + maxsag = self.ss.getSag(index) + return maxsag - threshold + + def con_max_hog(self, X, index, threshold, display=0): + '''Ensures the highest point of a line section is below + a certain maximum depth ''' + if self.MG: + maxhog = max([self.ss.getHog(index), self.ss_mod.getHog(index)]) + else: + maxhog = self.ss.getHog(index) + return threshold - maxhog + + # ----- utility functions ----- + + def plotProfile(self, iPoint=1, Xuvec=[1,0,0], Yuvec=[0,0,1], ax=None, color=None, title="", slack=False, displaced=True, figsize=(6,4)): + '''Plot the mooring profile in undisplaced and extreme displaced positions + + Parameters + ---------- + Xuvec : list, optional + plane at which the x-axis is desired. The default is [1,0,0]. + Yuvec : lsit, optional + plane at which the y-axis is desired. The default is [0,0,1]. + ax : axes, optional + Plot on an existing set of axes + color : string, optional + Some way to control the color of the plot ... TBD <<< + title : string, optional + A title of the plot. The default is "". + slack : bool, optional + If false, equal axis aspect ratios are not enforced to allow compatibility in subplots with axis constraints. + displaced : bool, optional + If true (default), displaced line profiles are also plotted. + + Returns + ------- + fig : figure object + To hold the axes of the plot + ax: axis object + To hold the points and drawing of the plot + + ''' + + # if axes not passed in, make a new figure + if ax == None: + fig, ax = plt.subplots(1,1, figsize=figsize) + ax.set_xlabel('Horizontal distance (m)') + ax.set_ylabel('Depth (m)') + + if self.MG: + fig1, ax1 = plt.subplots(1,1, figsize=figsize) + ax1.set_xlabel('Horizontal distance (m)') + ax1.set_ylabel('Depth (m)') + else: + fig = plt.gcf() # will this work like this? <<< + + + if displaced: + offsets = [0, self.x_mean+self.x_ampl, -self.x_mean-self.x_ampl] + else: + offsets = [0] + + for x in offsets: + alph = 1 if x==0 else 0.5 # make semi-transparent for offset profiles + + self.ss.setOffset(x) + + ax.plot(x, 0,'ko',markersize = 2) # draw platform reference point + + if self.shared == 2: # plot other half too if it's a shared line where only half is modeled <<< + for i, line in enumerate(self.ss.lineList): + if i in self.i_buoy: + self.ss.lineList[i].color = [.6,.6,.0] + else: + self.ss.lineList[i].color = [.3,.5,.5] + self.ss.drawLine2d(0, ax, color = 'self', Xoff = -self.ss.span/2) + + #store ss cos_th before plotting the flipped half cable + self.ss.cos_th = -self.ss.cos_th + self.ss.drawLine2d(0, ax, color = 'self', Xoff = self.ss.span/2) + self.ss.cos_th = -self.ss.cos_th + + else: + for i, line in enumerate(self.ss.lineList): + if color==None: # alternate colors so the segments are visible + if i in self.i_buoy: + line.drawLine2d(0, ax, color=[.6,.6,.0], alpha=alph, Xuvec=Xuvec, Yuvec=Yuvec) + else: + line.drawLine2d(0, ax, color=[.3,.5,.5], alpha=alph, Xuvec=Xuvec, Yuvec=Yuvec) + else: + line.drawLine2d(0, ax, color=color, alpha=alph, Xuvec=Xuvec, Yuvec=Yuvec) + + if self.MG: + alph = 1 if x==0 else 0.5 # make semi-transparent for offset profiles + + self.ss_mod.setOffset(x) + + ax1.plot(x, 0,'ko',markersize = 2) # draw platform reference point + + if self.shared == 2: # plot other half too if it's a shared line where only half is modeled <<< + for i, line in enumerate(self.ss_mod.lineList): + + #check if linetype name has buoy in it (**** this is highly dependent on naming convention) + if line.type['name'].split("_")[-1][:4] == 'buoy': + self.ss_mod.lineList[i].color = [.6,.6,.0] + else: + self.ss_mod.lineList[i].color = [.3,.5,.5] + self.ss_mod.drawLine2d(0, ax1, color = 'self', Xoff = -self.ss.span/2) + + #store ss cos_th before plotting the flipped half cable + self.ss_mod.cos_th = -self.ss_mod.cos_th + self.ss_mod.drawLine2d(0, ax1, color = 'self', Xoff = self.ss_mod.span/2) + self.ss_mod.cos_th = -self.ss_mod.cos_th + + else: + for i, line in enumerate(self.ss_mod.lineList): + if color==None: # alternate colors so the segments are visible + + #check if linetype name has buoy in it (**** this is highly dependent on naming convention) + if line.type['name'].split("_")[-1][:4] == 'buoy': + line.drawLine2d(0, ax1, color=[.6,.6,.0], alpha=alph, Xuvec=Xuvec, Yuvec=Yuvec) + else: + line.drawLine2d(0, ax1, color=[.3,.5,.5], alpha=alph, Xuvec=Xuvec, Yuvec=Yuvec) + else: + line.drawLine2d(0, ax1, color=color, alpha=alph, Xuvec=Xuvec, Yuvec=Yuvec) + + # make legend entries available + if displaced: + if not color==None: + ax.plot(np.nan, np.nan, color=color, alpha=1, label="undisplaced") + ax.plot(np.nan, np.nan, color=color, alpha=0.5, label="displaced") + + #ax.plot([self.ss.lineList[0].rA[0], 0], [-self.depth, -self.depth], color='k') + # only force equal aspect ratio if "slack" keyword isn't specified (so that sharex=True, sharey-True plots are possible) + if not slack: + ax.axis("equal") + + ax.set_title(title) + #ax.set_ylim(-1,1) + + + self.ss.setOffset(0) # set back to its neutral position + + if self.MG: + + if not slack: + ax1.axis("equal") + + ax1.set_title(title + " Marine Growth") + + self.ss_mod.setOffset(0) + + return fig, ax, fig1, ax1 # return the figure and axis object in case it will be used later to update the plot + + else: + return fig, ax + + def plotCurves(self, ax=[], color="k", title=""): + '''Plot key performance curves for the cable as a function of offset + + Parameters + ---------- + ax : axes, optional + Plot on an existing set of axes + title : string, optional + A title of the plot. The default is "". + + Returns + ------- + fig : figure object + To hold the axes of the plot + ax: axis object + To hold the points and drawing of the plot + + ''' + + # if axes not passed in, make a new figure + if len(ax) == 0: + fig, ax = plt.subplots(2,1, sharex=True) + newFig=True + else: + if not len(ax) == 2: + raise Exception("ax provided to plotCurves must be a list of 2 axes.") + fig = plt.gcf() + newFig = False + + x = np.linspace(-self.x_mean_high-self.x_ampl, self.x_mean_low+self.x_ampl, 50) + + Fx = np.zeros(len(x)) + Ts = np.zeros([len(x), len(self.ss.lineList)]) + + # calculate values at each offset point + for i in range(len(x)): # go through each offset point + + self.ss.setOffset(x[i]) # offset the desired amount + + Fx[i] = self.ss.fB_L[0] # get horizontal mooring force + + for j in range(len(self.ss.lineList)): # get upper end tension of each line segment + Ts[i,j] = self.ss.lineList[j].TB + + # plots + ax[0].plot(x, -Fx/1e3, c=color) + + for j in range(len(self.ss.lineList)): + ax[1].plot(x, Ts[:,j]/1e3, c=color, dashes=[5-0.5*j, 0.5*j], label=f"segment {j+1}") + + ax[0].set_ylabel("Fx (kN)") + ax[1].set_ylabel("Tension (kN)") + if newFig: ax[1].legend() + ax[1].set_xlabel("Offset (m)") + #fig.set_title(title) + + self.ss.setOffset(0) # restore to undisplaced position + + return fig, ax # return the figure and axis object in case it will be used later to update the plot + + """ + def makeCableType(self, di, name): + '''sets up a cableType dictinoary by reading in from a dictionary a + a specified name entry.''' + + # a few calculations + d = float(di['d']) # [m] + m = float(di['m']) # [kg/m] + w = (m - np.pi/4*d**2 *self.rho)*self.g + + # make and fill in a cableType dictionary, which will go in MoorPy's lineTypes dictionary + cableType = dict(name=name) + cableType['d_vol'] = float(di['d']) # [m] + cableType['m'] = m # [kg/m + cableType['w'] = w # [N/m] wet weight per unit length] + cableType['EA'] = getFromDict(di, 'EA') # [N] axial stiffness + cableType['EI'] = getFromDict(di, 'EI' , default=0) # [N-m^2] bending stiffness + cableType['MBL'] = getFromDict(di, 'MBL', default=0) # [N] minimum breaking load + cableType['MBR'] = getFromDict(di, 'MBR', default=0) # [m] minimum bend radius + cableType['A_con'] = getFromDict(di, 'A' , default=0) # [mm^2] conductor area + cableType['dynamic'] = getFromDict(di, 'dynamic', dtype=bool, default=True) + cableType['DC'] = getFromDict(di, 'DC' , dtype=bool, default=False) + cableType['cable_cost'] = getFromDict(di, 'cable_cost', default=0) # $/m dynamic cable cost + cableType['buoy_cost'] = getFromDict(di, 'buoy_cost', default=0) # cost of each module + cableType['buoy_length'] = getFromDict(di, 'buoy_length', default=0) # meters for each buoyancy module + cableType['L_BM'] = getFromDict(di, 'L_BM', default=0) # [m] center to center spacing between two buoyancy modules + cableType['D_BM'] = getFromDict(di, 'D_BM', default=0) # [m] Diameter of buoyancy module + cableType['V_BM'] = getFromDict(di, 'V_BM', default=0) # [m] volume of buoyancy module + cableType['rho_buoy'] = getFromDict(di, 'rho_buoy', default=500) # [kg/m^3] aggregate density of buoyancy module + if cableType['V_BM'] <= 0: + raise Exception("Volume of buoyancy module must be greater than zero") + + return cableType + """ + def updateHyroCoeffs(self, C_dnc = 1.2, C_dnb = 1.2, C_dab1 = 1, C_dab2 = 0, C_dac = 0, C_anb = 1, C_anc = 1, C_aab = 0.5 , C_aac = 0): + ''' + + + Parameters + ---------- + C_dnc : Normal drag coeff for the cable. The default is 1.2. + C_dnb : Normal drag coeff for the buoyancy module. The default is 1.2. + C_dab1 : Drag coefficient for exposed ends of buoyancy module. The default is 1. + C_dab2 : Axial drag coefficient for buoyancy module (skin friction). The default is 0. + C_dac : Axial drag coefficient for cable (skin friction). The default is 0. + C_anb : Normal added mass coefficient for buoyancy module. The default is 1. + C_anc : Normal added mass coefficient for cable. The default is 1. + C_aab : Axial added mass coefficient for buoyancy module. The default is 0.5 (assumed sphere added mass coeff). + C_aac : Axial added mass coefficient for cable. The default is 0. + + Returns + ------- + None. + + ''' + #iterate through list of line properties + buoycount = -1 + for i in (range(0, len(self.ss.lineTypes))): + linetype = self.ss.lineTypes[i] + if linetype['name'].split("_")[-1][:4] == 'buoy': + buoycount += 1 + deq = linetype['d_vol'] # volume equiv diameter for buoy section + dc = self.cableType['d_vol'] # diameter of cable + db = self.buoyType['d'] # diameter of buoy + Lbs = self.bs[buoycount] + if Lbs == 0: + ValueError('Buoyancy module spacing is zero') + Lb = self.buoyType['l'] + + self.ss.lineTypes[i]['Cd'] = 1/(Lbs * deq)*(C_dnc * dc * (Lbs - Lb) + C_dnb * db * Lb) + self.ss.lineTypes[i]['CdAx'] = 1 / (Lbs * deq) *(C_dab1 * (db**2 - dc**2)/4 + C_dab2 * db * Lb + C_dac * dc * (Lbs - Lb)) + self.ss.lineTypes[i]['Ca'] = 1 / (Lbs * deq**2) * (C_anb * db**2 * Lb + C_anc * dc**2 *(Lbs - Lb)) + self.ss.lineTypes[i]['CaAx'] = 1 / (Lbs * deq**2) * (C_aab * db**2 * Lb + C_aac * dc**2 *(Lbs - Lb)) + else: + self.ss.lineTypes[i]['CdAx'] = 0.0 + self.ss.lineTypes[i]['Ca'] = 1.0 + if self.MG: + for i in (range(0, len(self.ss_mod.lineTypes))): + linetype = self.ss_mod.lineTypes[i] + if linetype['name'].split("_")[-1][:4] == 'buoy': + buoycount += 1 + deq = linetype['d_vol'] # volume equiv diameter for buoy section + dc = self.cableType['d_vol'] # diameter of cable + db = self.buoyType['d'] # diameter of buoy + Lbs = self.bs[buoycount] + if Lbs == 0: + ValueError('Buoyancy module spacing is zero') + Lb = self.buoyType['l'] + + self.ss_mod.lineTypes[i]['Cd'] = 1/(Lbs * deq)*(C_dnc * dc * (Lbs - Lb) + C_dnb * db * Lb) + self.ss_mod.lineTypes[i]['CdAx'] = 1 / (Lbs * deq) *(C_dab1 * (db**2 - dc**2)/4 + C_dab2 * db * Lb + C_dac * dc * (Lbs - Lb)) + self.ss_mod.lineTypes[i]['Ca'] = 1 / (Lbs * deq**2) * (C_anb * db**2 * Lb + C_anc * dc**2 *(Lbs - Lb)) + self.ss_mod.lineTypes[i]['CaAx'] = 1 / (Lbs * deq**2) * (C_aab * db**2 * Lb + C_aac * dc**2 *(Lbs - Lb)) + else: + self.ss_mod.lineTypes[i]['CdAx'] = 0.0 + self.ss_mod.lineTypes[i]['Ca'] = 1.0 + + +# ----- Main Script ----- +if __name__ == '__main__': + + # EXAMPLE + + depth = 800 + configuration = 'Humboldt' + + settings = {} + settings['rBFair'] = [0,0,-14] # relative attachment coordinate on FOWT [m] + settings['span'] = 950 # relative attachment coordinate on FOWT [m] + + settings['offset'] = 80 # mean surge offsets in either direction [m] + settings['x_ampl'] = 5 # additional dynamic surge amplitude about the mean [m] + + + # design variables: initial values, min and max bounds + settings['Xindices'] = ['c', 0, 1, 2, 'c'] # order of design variables. multiple line segments can have the same design variable. 'c' flag means that it stays constant + # span L B1[kN] Lmid1 Spread + settings['allVars'] = [950, 1100, 100, 613, 300] # must be the same length as Xindices + settings['Xmin'] = [100, 100, 100] # must be same length as # of design variables + settings['Xmax'] = [1200, 800, 1000] # must be same length as # of design variables + settings['dX_last'] = [10, 10, 10] # must be same length as # of design variables + + # set up constraints + settings['constraints'] = [dict(name='min_lay_length', index=0, threshold= 80, offset='max'), # ensure there is at least 20 m of cable along the seabed + dict(name='max_sag', index=1, threshold=5-depth, offset='min')] # ensure the start of the buoyancy section stays 5 m off the seabed + + # also add a tension safety factor constraint for each section + + for i in range(3): + settings['constraints'].append(dict(name='tension_safety_factor', index=i, threshold=2.0, offset='max')) + + # add a curvature safety factor constraint for each offset of the cable or section of the cable + for i in range(3): + settings['constraints'].append(dict(name='curvature_safety_factor', index=i, threshold=2.0, offset='min')) + + # add a maximum touchdown point range of motion constraint + settings['constraints'].append(dict(name='max_touchdown_range', index=0, threshold=50.0, offset='other')) + + # load property coefficients + cable_props = cprops.loadCableProps(None) # load default property scaling coefficients + cableType = cprops.getCableProps(400, 'dynamic_cable_66', cableProps=cable_props) + + buoy_props = cprops.loadBuoyProps(None) # load default property scaling coefficients + buoyType = cprops.getBuoyProps(1, 'Buoyancy_750m', buoyProps=buoy_props) + + + #set up the object + dc = CableDesign(depth, cableType, buoyType, n=3, i_buoy=[1], **settings) + + #plot initial design + dc.plotProfile(title='initial (X0)') + dc.setNormalization() + + X, min_cost = dc.optimize(maxIter=3, plot=False, display=2, stepfac=4, method='COBYLA') + #X, min_cost = dc.optimize(maxIter=8, plot=False, display=1, stepfac=4, method='SLSQP') + #X, min_cost = dc.optimize(maxIter=2, plot=False, display=1, stepfac=4, method='dopt') + + dc.objectiveFun(X, display=2) + dc.evaluateConstraints(X, display=2) + dc.updateDesign(X, display=0) + dc.plotProfile(title= 'dopt') + dc.plotOptimization() + #dc.unload('Humboldt.dat') + + + plt.show() diff --git a/famodel/design/CableLayout_functions.py b/famodel/design/CableLayout_functions.py new file mode 100644 index 00000000..cdc40be1 --- /dev/null +++ b/famodel/design/CableLayout_functions.py @@ -0,0 +1,1034 @@ +# -*- coding: utf-8 -*- +import os +import numpy as np +from sklearn.cluster import SpectralClustering +# from sklearn.cluster import SpectralClustering +from scipy.spatial.distance import cdist, pdist, squareform +import networkx as nx +import math +import pandas as pd +import matplotlib.pyplot as plt +from famodel.cables.cable_properties import * +from shapely.geometry import Point, LineString, MultiPoint +import shapely as sh +from copy import deepcopy + + +# TODO: rename and reorder inputs + +def getCableLayout(turb_coords, subs_coords, conductor_sizes, + cableProps_type, turb_rating_MW, turb_cluster_id=[], turb_subs_id=[], + n_cluster_sub=0, n_tcmax=8, plot=False, oss_rerouting=False, + substation_id=None): + ''' Function creating the cable layout of whole wind farm, including + estimation of cable conductor sizes. It currently supports a single + substation. + + Parameters + ---------- + turb_coords : 2D array + Coordinates of each turbine, provided as an N-by-2 array of [x,y] values [m]. + subs_coords : list or array + Substation [x,y] coordinates [m]. + conductor_sizes ; list + Conductor sizes to be allowed when sizing cables [mm^2]. + cableProps_type : string + Name of cable type in cableProps property scaling coefficients yaml. + turb_rating_MW : float + Turbine rated power [MW] + turb_cluster_id : list (optional) + The index of the cluster (integers starting from zero) that each + turbine belongs to. This is specified to determines the clusters. + turb_subs_id : list (optional) + The index of the substation (integers starting from zero) that each + turbine should feed to. + n_cluster_sub : int (optional) + Then number of clusters per substation to create if clustering automatically + (turb_cluster_id should not be specified in this case). + n_tcmax : int (optional) + Then number of clusters to create if clustering automatically + (turb_cluster_id should not be specified in this case). + plot : bool (optional, default False) + Displays a plot of the array cable network if True. + + Returns + ------- + iac_dic : list of dicts + List of an array cable information dictionary for each cable. + ''' + + # Handle if coordinates are inputted as lists rather than arrays + if type(turb_coords) == list: + turb_coords = np.array(turb_coords) + n_turb = turb_coords.shape[0] # number of turbines + + if type(subs_coords) == list: + subs_coords = np.array(subs_coords) + + if subs_coords.shape == (2,): # if just an x,y pair, put in a 2D array + subs_coords = np.array([subs_coords]) + n_subs = subs_coords.shape[0] # number of substations + + + # Get cable properties + iac_props = [] + for A in conductor_sizes: + cprops = getCableProps(A, cableProps_type, cableProps=None, source='default', name="", rho=1025.0, g=9.81) + iac_props.append(cprops) + + + # ----- Divide turbines among substations ----- + + if len(turb_subs_id) > 0: # if substation assignment indices are provided + + subs_labels_unique, subs_labels_counts = np.unique(turb_subs_id,return_counts=True) + if not n_subs == len(subs_labels_unique): + raise Exception("There are more unique entries in turb_subs_id than number of subs_coords provided.") + turb_subs_id = np.array(turb_subs_id) + + # Check that substation labels are integers counting up from 0 + for i in range(n_subs): + if not i in subs_labels_unique: + raise Exception(f"provided substation assignment labels must be integers counting up from 0. Integer {i} was not found.") + + else: # If no substation assignments are provided, divide the turbines by distance + # determine max # of turbines allowed per substation + max_turbs_per_substation = n_cluster_sub*n_tcmax + 5 # num clusters x num turbs per cluster + a few extra + turb_subs_id = assignSubstationTurbines(turb_coords, subs_coords, max_turbs_per_substation) + + + # ----- Handle turbine clustering ----- + + if len(turb_cluster_id) > 0: # if cluster indices are provided + + cluster_labels_unique, cluster_labels_counts = np.unique(turb_cluster_id,return_counts=True) + n_cluster = len(cluster_labels_unique) + turb_cluster_id = np.array(turb_cluster_id) + + + # Check that cluster labels are integers counting up from 0 + cluster_subs_id = [] + for i in range(n_cluster): + cluster_subs_id.append(int(np.unique(turb_subs_id[turb_cluster_id==i]))) + if not i in cluster_labels_unique: + raise Exception(f"provided cluster labels must be integers counting up from 0. Integer {i} was not found.") + + # TODO: figure out how to deal with inconsistencies between turbine cluster vs substation assignments + + else: # If no clusters are provided, create clusters + + n_cluster = 0 + if n_cluster_sub == 0: # if number of clusters (per substation) not specified, use default + n_cluster_sub = int(np.ceil(n_turb/n_tcmax/n_subs)) + + # cluster turbines (for each substation if multiple) + turb_cluster_id = [None]*n_turb # cluster ID of each turbine + cluster_labels_counts = [] # the number of turbines in each cluster + cluster_subs_id = [] # substation ID of each cluster + for i in range(n_subs): + turbs = np.where(turb_subs_id==i)[0] + cluster_id, labels_counts = clusteringSpectral(turb_coords[turbs], + subs_coords[i,:], n_cluster_sub, n_tcmax) + + # Store each turbine's cluster ID (adjusting IDs for multiple substations) + for ii,cid in enumerate(cluster_id): + turb_cluster_id[turbs[ii]] = int(cid + n_cluster) + cluster_subs_id += list([int(x) for x in np.zeros(len(labels_counts)) + i]) + + cluster_labels_counts += list(labels_counts) + + n_cluster += len(labels_counts) # tally up actual number of clusters + + + + # ----- Figure out cable connections for each cluster ----- + if not substation_id: + substation_id = [] + for i in range(n_subs): + substation_id.append(n_turb + i) + + index_map = [] # maps local turbine id within each cluster to the global turbine list index + + # The main outputs of this part of the code (one entry per cable) + global_edge_list = [] # global end connection ids of each cable (a, b) + upstreamturb_count = [] # number of turbines upstream of each cable + cable_cluster_id = [] # id number of the cluster each cable belongs to + + cable_types = [] # list of the cable type dict for each cable + + for ic in range(n_cluster): # for each cluster + # Select indices of points per cluster + cluster_ind = np.where(np.array(turb_cluster_id) == ic)[0] + + # Index of the substation for this cluster + #isubs = turb_subs_id[cluster_ind[0]] + isubs = cluster_subs_id[ic] + + # ----- Make coordinate lists for each cluster, and index map ----- + + # Make array of just the coordinates in the cluster + cluster_coords = turb_coords[cluster_ind,:] + + #cluster_arrays.append(cluster_coords) + # Make list of global turbine indicies that are within this cluster + index_map.append(np.arange(n_turb)[cluster_ind]) + + + # Distances from substation to turbine locations for cluster + distances = np.linalg.norm(cluster_coords - subs_coords[isubs,:], axis=1) + # Find the index of the closest turbine to substation + gate_index0 = np.argmin(distances) + + # Calculate minimum spanning tree for the cluster + cluster_edge_list = minimum_spanning_tree(cluster_coords, gate_index0) + # This is a list of [a, b] pairs of turbine indices where, within each + # pair, the power flow is from b to a, and a is closer to the substation. + + # Get number of upstream turbines per turbine, counting th + iac_upstreamturb_count_ic = getUpstreamTurbines(cluster_edge_list) + # iac_upstreamturb_count_ic is now a list giving the number of + # upstream turbines for each cable, with the same indexing as + # cluster_edge_list. + + # Convert cluster edge list into global turbine IDs + for ia, ib in cluster_edge_list: + global_edge_list.append([index_map[ic][ia], + index_map[ic][ib]]) + cable_cluster_id.append(ic) + + upstreamturb_count.append(iac_upstreamturb_count_ic[ib] + 1) + + # determine which substation this cable goes to based on cluster to substation index mapping + subid = substation_id[isubs] + + # Add the cable that goes from the substation to the cluster gate + global_edge_list.append([subid, index_map[ic][gate_index0]]) + cable_cluster_id.append(ic) + upstreamturb_count.append(cluster_labels_counts[ic]) # (cable to substation) + + # Get cable id and assign cable to turbine + #iac_cab2turb_ic2 = getCableID(cluster_coords, gate_coords[ic], + # cluster_edge_list, iac_upstreamturb_count_ic) + # iac_cab2turb_ic = [[el[1], i, el[0]] for i, el in enumerate(cluster_edge_list)] + # Above is no longer used <<< + + + # ----- Size cables and generate dictionary of cable information ----- + + # results of the previous stage are stored in + # - global_edge_list + # - upstreamturb_count + # - cable_cluster_id + + # combine coordinates for easy plotting of everything + coords = np.vstack([turb_coords, subs_coords]) + + iac_dic = [] # list of dictionaries for each cable's information + + # loop through ALL cables + for i in range(len(global_edge_list)): + + # Size cable to support cumulative power up to this point + required_rated_power = turb_rating_MW * upstreamturb_count[i] + selected_cable = selectCable(required_rated_power, iac_props) + + cable_types.append(selected_cable) + + # note: turb_id_A/B is currently opposite of cluster_edge_list [a,b] <<< + turb_id_A = global_edge_list[i][1] + turb_id_B = global_edge_list[i][0] + + coordinates = [[coords[turb_id_A][0], coords[turb_id_A][1]], + [coords[turb_id_B][0], coords[turb_id_B][1]]] + + iac_dic.append({'cluster_id': cable_cluster_id[i], + 'turbineA_glob_id': turb_id_A, # row_id_A, + 'turbineB_glob_id': turb_id_B, # row_id_B, + 'cable_id': i, # this is the global id + 'upstream_turb_count': upstreamturb_count[i], + '2Dlength': np.linalg.norm(coords[turb_id_A] - coords[turb_id_B]), + 'coordinates': coordinates, # end/turbine coordinates: [[xA,yA],[xB,yB]] + 'conductor_area': selected_cable['A'], + 'cable_costpm': selected_cable['cost']}) + + + """ + + # >>> This section has draft rerouting capability for cable to substation. <<< + # oss_rerouting : cable rerouting to avoid intersections of cables between clusters and substation. True = on, False = off + intersection_join = False # ? + + # GATE ROUTING + # Create a list to store the connections + gate_connections = [] + gate_line = LineString(gate_coords) + # Connect OSS coordinates to each point along the gate line + for point in gate_line.coords: + connection_line = LineString([subs_coords, point]) + gate_connections.append(connection_line) + + # Loop over each cluster + for ic in range(n_cluster): + + # Check for intersection + # Overwrite new path when there is an intersection + # Define the first connection + connection = gate_connections[ic] + # Find the intersection between the first connection and the gate line + intersection = connection.intersection(gate_line) + + # Check if there is an intersection + # Multipoint means, there is another intersection, except the target gate + if intersection_join: + if intersection.geom_type == "MultiPoint": + # Create new path + if ic == (len(gate_coords)) and oss_rerouting == 1: + # If last gate leads to an intersection + connection_new = [subs_coords, gate_coords[ic-1], gate_coords[ic]] + + elif ic >= len(gate_coords) - 1: + connection_new = [subs_coords, gate_coords[ic-1], gate_coords[ic]] + + else: + connection_new = [subs_coords, gate_coords[ic+1], gate_coords[ic]] + # Create a new LineString with the updated coordinates + new_line = LineString(connection_new) + + ''' + # Second interation - check if new line is also intersecting + intersection = connection.intersection(new_line) + if intersection.geom_type == "MultiPoint": + # Create new path + if ic == range(len(gate_coords)): + # If last gate leads to an intersection + connection_new = [subs_coords, gate_coords[ic-2], gate_coords[ic-1], gate_coords[ic]] + else: + connection_new = [subs_coords, gate_coords[ic+2], gate_coords[ic+1], gate_coords[ic]] + # Create a new LineString with the updated coordinates + new_line = LineString(connection_new) + ''' + + + # Replace gate connection with new line + gate_connections[ic] = new_line + """ + + + # Make cable layout plot + if plot == 1: + plotCableLayout(iac_dic, turb_coords, subs_coords, save=False) + + # cable_id = np.array([a['cable_id'] for a in iac_dic]) + # ia = np.array([a['turbineA_glob_id'] for a in iac_dic]) + # ib = np.array([a['turbineB_glob_id'] for a in iac_dic]) + # cid =np.array([a['cluster_id'] for a in iac_dic]) + + return iac_dic, global_edge_list, cable_types + + +# ----- Cluster turbines ----- +def clusteringSpectral(turb_coords, subs_coords, n_cluster, n_tcmax): + ''' Clustering wind turbines based on their angles from a single + substation using Spectral Clustering. + + Input: + self.turb_coords : turbines coordinates + self.subs_coords : offshore substation coordinates + self.n_cluster : amount of clusters + n_tcmax : max amount of turbines per cluster + + Output: + self.cluster_arrays : list with turbine coordinates per cluster + self.turb_cluster_id : array with cluster ID per turbine location + + https://scikit-learn.org/stable/modules/clustering.html#spectral-clustering + ''' + # ----- Clustering with Spectral clustering + # Output: labels + # Calculate vectors from root to each point + vectors = turb_coords - subs_coords + # Calculate angles (in radians) between vectors and x-axis + angles = np.arctan2(vectors[:, 1], vectors[:, 0]) + # Rescale angles to [0, 2*pi] + angles[angles < 0] += 2 * np.pi + # Reshape angles to column vector for clustering + angles = angles.reshape(-1, 1) + + # Calculate Euclidean distance from each point to the root + # Clustering using spectral with angles as features + spectral_clustering = SpectralClustering(n_clusters=n_cluster, random_state = 0, affinity='nearest_neighbors', n_neighbors=n_tcmax) + spectral_clustering.fit(angles) + + # ----- Cluster labels + turb_cluster_id = spectral_clustering.labels_ + # ----- Number of turbines per cluster + cluster_labels_unique, cluster_labels_counts = np.unique(turb_cluster_id, return_counts=True) + ''' + # ----- Cluster locations array + cluster_arrays = [] + + for name in cluster_labels_unique: + #name=0 + # Select indices of points per cluster + cluster_ind = np.where(turb_cluster_id == name)[0] + cluster_points = turb_coords[cluster_ind] + cluster_arrays.append(cluster_points) + ''' + return turb_cluster_id, cluster_labels_counts + + +def getclusterGates(turb_coords, subs_coords, turb_cluster_id): + ''' Get gates of turbines cluster, meaning the closest turbines to oss from each cluster. + Input: + turb_coords : turbine coordinates - list of x,y pairs + turb_cluster_id : cluster ID of each turbine + subs_coords : substation coordinates - list of x,y pairs + + Output: + gate_coords : list of gate coordinates per cluster + gate_index : index of the turbine that is the gate per cluster + ''' + cluster_names = np.unique(turb_cluster_id) + gate_coords = np.zeros((len(cluster_names),2)) + gate_index = np.zeros(len(cluster_names)) + + for i in cluster_names : + # Get locations in current cluster + cluster_ind = np.where(turb_cluster_id == i)[0] + cluster_points = turb_coords[cluster_ind] + # Calculate distances from OSS to Turb locations for cluster + distances = np.linalg.norm(cluster_points - subs_coords, axis=1) + # Find the index of the turbine with the minimum distance to OSS + gate_index0 = np.argmin(distances) + # Get the closest location to OSS for current cluster + gate_coords[i,:] = cluster_points[gate_index0] + gate_index[i] = gate_index0 + return gate_coords, gate_index + + +def minimum_spanning_tree(points, start_index): + '''Find edges that form a minimum spanning tree of the provided node + points, starting from a specified node. + X are edge weights of fully connected graph. + This function is adapted from the 'Simplistic Minimum Spanning Tree in Numpy' + from Andreas Mueller, 2012. + https://peekaboo-vision.blogspot.com/2012/02/simplistic-minimum-spanning-tree-in.html + If only one point is provided, an empty list will be returned. + + Input: + points : List of turbine coordinate x,y pairs + start_index : index of which point to start at, which corresponds to + the turbine that will be attached to the substation. + + Output: + spanning_edges : list of lists + Collection of node pairs for each edge, where in each [a,b] pair, a + is the ID of the node closer to the substation. + ''' + + X = squareform(pdist(points)) + + n_vertices = X.shape[0] + spanning_edges = [] + + # initialize with start_index: + visited_vertices = [start_index] + num_visited = 1 + # exclude self connections: + diag_indices = np.arange(n_vertices) + X[diag_indices, diag_indices] = np.inf # set self-distances to infinite to exclude them + + while num_visited != n_vertices: + # define new edge as shortest distance between visited vertices and others + new_edge = np.argmin(X[visited_vertices], axis=None) + # 2d encoding of new_edge from flat, get correct indices + new_edge = divmod(new_edge, n_vertices) + new_edge = [visited_vertices[new_edge[0]], new_edge[1]] + # add edge to tree + spanning_edges.append(new_edge) + visited_vertices.append(new_edge[1]) + # remove all edges inside current tree so they aren't considered for the next new_edge + X[tuple(visited_vertices), new_edge[1]] = np.inf + X[new_edge[1], tuple(visited_vertices)] = np.inf + num_visited += 1 + + return spanning_edges + + +def selectCable(required_rated_power, cableTypes): + '''Selected the cable type from a list that is the smallest option to + exceed the required rated power.''' + + closest_rated_power = float('inf') # Initialize with positive infinity to find the closest power + selected_cable = None + + # Iterate through the list and find the closest power that is >= required_rated_power + for cable_props_dict in cableTypes: + if cable_props_dict['power'] >= required_rated_power and cable_props_dict['power'] < closest_rated_power: + + closest_rated_power = cable_props_dict['power'] + selected_cable = cable_props_dict + + if not selected_cable: + raise Exception(f"No cable provided meets the required rated power of {required_rated_power}.") + breakpoint() + + return selected_cable + +def assignSubstationTurbines(turb_coords, sub_coords, max_turbines): + ''' + Function to split turbines between substations based on which substation a turbine is closest to. + + Parameters + ---------- + turb_coords : array + Array of turbine x,y coordinates + sub_coords : array + Array of substation x,y coordinates + max_turbines : int + Maximum number of turbines allowed per substation + + Returns + ------- + turb_subs_id : list + The index of substation that each turbine should feed to + ''' + turb_subs_id = np.zeros((len(turb_coords[:,0]))) # array of substations associated with each turbine + turbs_for_oss = [] # list of turbine ids for each substation + distlist = [] # list of distances for each turbine from each substation + noss = len(sub_coords[:,0]) # number of substations + + # create list where each entry is an array of distances from turbine coords to a specific oss coord + for oo in range(noss): + turbs_for_oss.append([]) + distlist.append(np.linalg.norm(turb_coords - sub_coords[oo], axis=1)) + + # find which oss is closest to each point + for idx in range(len(distlist[0])): + turb_subs_id[idx] = int(np.argmin([dist[idx] for dist in distlist])) + # list of turbine ids broken out by substation + turbs_for_oss = [list(np.where(turb_subs_id==subid)[0]) for subid in range(noss)] + + rturbs_for_oss = deepcopy(turbs_for_oss) + # if an oss has too many turbines, need to switch some turbines to another oss + overfilled_oss = [oo for oo in range(noss) if len(turbs_for_oss[oo])>max_turbines] + if len(overfilled_oss)>0: + # find oss with least number of turbines + uoss = np.argmin([len(turbs_for_oss[oo]) for oo in range(noss)]) # underfilled oss + + # for each overfilled oss, switch some turbines to the underfilled oss + for ooss in overfilled_oss: + turbine_ids = np.array(turbs_for_oss[ooss]) # ids of turbines currently associated with overfilled oss + # find difference in distance between each turbine and the over- and under-filled oss + dist_disparity_margin = [distlist[uoss][tidx]-distlist[ooss][tidx] for tidx in turbine_ids] + # sort list of indices by decreasing distance difference + sorted_dist_disp = np.flip(np.argsort(dist_disparity_margin)) + rturbs_for_oss[ooss] = list(turbine_ids[sorted_dist_disp[:max_turbines]]) # update overfilled oss turb list with turbines of largest distance disparity + rturbs_for_oss[uoss].extend(list(turbine_ids[sorted_dist_disp[max_turbines:]])) # add remaining turbines to underfilled oss + + # update turb_subs_id + for oo,ossid in enumerate(rturbs_for_oss): + for tid in ossid: # tid is the turbine index/id number + turb_subs_id[tid] = int(oo) + + # return vals + return(turb_subs_id) + + +""" + +# IN WORK => BACKLOG! +# ----- Advanced routing ----- +def advancedCableRouting(iac_edges, cluster_arrays, exclusion_coords): + '''Wrapping method to perform advanced cable routing, considering obstacles + iac_edges : list of array with edge IDs + cluster_arrays : List of arrays with turbine coordinates per cluster + exclusion_coords : List of arrays with exclusion zone coordinates + ''' + + # Check cable intersection + intersecting_lines, lines = checkCableIntersections(iac_edges, cluster_arrays, exclusion_coords) + + nearby_lines = getObstacles(intersecting_lines, lines, buffer_distance=2000) + + obstacles_list = [nearby_lines, lines, exclusion_polygons_sh] + + + +def checkCableIntersections(iac_edges, cluster_arrays, exclusion_coords): + '''Wrapping method to perform advanced cable routing, considering obstacles + Input: + iac_edges : list of array with edge IDs + cluster_arrays : List of arrays with turbine coordinates per cluster + exclusion_coords : List of arrays with exclusion zone coordinates + + Output: + intersecting_indices : list of array with edge IDs + + ''' + # Exclusion zones + exclusion = exclusion_coords + exclusion_polygons_sh = [] # List to store polygons + + # Create exclusion zone polygons + for ie in range(len(exclusion)): + exclusion_polygon = sh.Polygon(exclusion[ie]) + exclusion_polygons_sh.append(exclusion_polygon) + + # Convert iac_edges and respective coordinates into Shapely LineString objects and identify intersecting lines + intersecting_indices = [] + # Loop over clusters + for ic in range(len(iac_edges)): + edges = iac_edges[ic] + coords = cluster_arrays[ic] + + # Loop over cables in cluster + for ie in range(len(edges)): + start, end = edges[ie] + line = LineString([coords[start], coords[end]]) + + # Check if the line intersects the exclusion polygon and get iac_edge index + if line.intersects(exclusion_polygon): + intersecting_indices.append((ic, ie)) + + + + + + # Get insecting edges + iac_edges[intersecting_indices[0][0]][intersecting_indices[0][1]] + + + cluster_arrays[intersecting_indices[0][0],iac_edges[intersecting_indices[0][0]][intersecting_indices[0][1]]] + + # Convert iac_edges and respective coordinates into Shapely LineString objects + lines = [] + for edges, coords in zip(iac_edges, cluster_arrays): + for edge in edges: + start, end = edge + line = LineString([coords[start], coords[end]]) + lines.append(line) + + # Identify lines that intersect with the exclusion polygon + intersecting_lines = [line for line in lines if line.intersects(exclusion_polygons_sh[0])] + + return intersecting_lines, lines + + +def getObstacles(intersecting_lines, lines, buffer_distance): + '''Wrapping method to perform advanced cable routing, considering obstacles + intersecting_lines : list of array shapely lines + buffer_distance : distance, integer + ''' + combined_buffer = intersecting_lines[0].buffer(buffer_distance) + for intersecting_line in intersecting_lines[1:]: + combined_buffer = combined_buffer.union(intersecting_line.buffer(buffer_distance)) + + # Identify lines that intersect with the buffer + # Currently cables only, later include mooring lines as well + nearby_lines = [line for line in lines if line.intersects(combined_buffer) and line not in intersecting_lines] + + return nearby_lines + + + #x,y = nearby_lines[0].coords.xy + + + # Plotting - with nearby lines + plt.figure(figsize=(10, 10)) + + # Plot all lines in blue + for line in lines: + x, y = line.xy + plt.plot(x, y, marker='o', color='blue') + + # Plot intersecting lines in red + for intersecting_line in intersecting_lines: + x, y = intersecting_line.xy + plt.plot(x, y, marker='o', color='red') + + # Plot nearby lines in orange + for line in nearby_lines: + x, y = line.xy + plt.plot(x, y, marker='o', color='orange') + + # Plot the combined buffer + #x, y = combined_buffer.exterior.xy + #plt.plot(x, y, color='green', linestyle='--') + + plt.xlabel('X') + plt.ylabel('Y') + plt.title('Lines and Buffer Around Intersecting Line') + plt.grid(True) + plt.show() + + + + + # Plotting - different lines only + plt.figure(figsize=(10, 10)) + for line in lines: + x, y = line.xy + plt.plot(x, y, marker='o') + + plt.xlabel('X') + plt.ylabel('Y') + plt.title('Shapely Lines from Edges and Coordinates') + plt.grid(True) + plt.show() + + # Display the results + for line in lines: + print(line) + + # Plotting - insecting lines + plt.figure(figsize=(10, 10)) + + # Plot all lines + for line in lines: + x, y = line.xy + plt.plot(x, y, marker='o', color='blue') + + # Plot intersecting lines in red + for line in intersecting_lines: + x, y = line.xy + plt.plot(x, y, marker='o', color='red') + + # Plot the exclusion polygon + for polygon in exclusion_polygons_sh: + x, y = polygon.exterior.xy + plt.plot(x, y, color='green') + + plt.xlabel('X') + plt.ylabel('Y') + plt.title('Shapely Lines and Exclusion Polygon') + plt.grid(True) + plt.show() + +""" + + +def getUpstreamTurbines(edge_list): + '''Calculate the number of turbines upstream of each turbine. + Input: + edge_list : list of list pairs + List of the object ids at the ends of each cable [a ,b], where power + flows from b to a. + + Output: + self.iac_upstreamturb : upstream turbines per cable + self.iac_upstreamturb_count : amount of upstream turbines per cable + ''' + + if len(edge_list) == 0: + return [] # if there is only one turbine in the cluster + + # Create a directed graph from the iac edge list + G = nx.DiGraph() + G.add_edges_from(edge_list) + + # Initialize a list to store neighbors (turbines) of each point + neighbors_list = [] + # Iterate over each point and find its neighbors until a point has no neighbors + for point in range(np.max(edge_list) + 1): + neighbors = bfs_neighbors(G, point) + # Directly append neighbors which might be a set or None + neighbors_list.append(neighbors if neighbors is not None else None) + # Neighbor count + neighbor_count = [len(neighbors) if neighbors is not None else 0 for neighbors in neighbors_list] + iac_upstreamturb_count = [nc for nc in neighbor_count] + + return iac_upstreamturb_count + + +# Function to perform BFS traversal +def bfs_neighbors(graph, start_point): + '''Breadth-First Search. It's a algorithm for searching or traversing tree or graph data structures. + The algorithm starts at a chosen node of a graph and explores all of the neighbor nodes at the present + depth prior to moving on to the nodes at the next depth level. + + Input: + graph : network graph + start_point : start point + + Output: + neighbors : list of neighbors of each point + ''' + + neighbors = set() # Set to store neighbors + visited = set() # Set to store visited points + queue = [start_point] # Initialize queue with start point + + while queue: + # Dequeue a point from the queue + current_point = queue.pop(0) + # Check if current point has neighbors + if current_point not in visited: + visited.add(current_point) + current_neighbors = set(graph.neighbors(current_point)) + neighbors |= current_neighbors # Union operation to add neighbors + queue.extend(current_neighbors - visited) # Add unvisited neighbors to the queue + + # Replace empty set with None + if not neighbors: + neighbors = None + + return neighbors + + +""" +#Seems like the below function is equivalent to +#iac_cab2turb_ic = [[el[1], i, el[0]] for i, el in enumerate(cluster_edge_list)] +#but should double check if it has additional functionality: +def getCableID(coords, gate_coord, edge_list, iac_upstreamturb_count): + '''Identify cable (edge) number related to turbines. + Input: + self.iac_edges : list of inter array cable edges per cluster + self.iac_upstreamturb_count : amount of upstream turbines per cable + gate_coord : list of gate coordinates per cluster + + Output: + self.iac_ID : Downflow cable ID at each wind turbine + self.iac_cab2turb : List with cables and turbines, without 999 (gates) + + ''' + + # Identify cable (edge) number related to turbines + # These cables are in flow direction of the respective wind turbine + #breakpoint() + iac_ID = [] + TurbB_ID = [] + + + cab_id = np.zeros(len(iac_upstreamturb_count), dtype=int) + turb_id_B = np.zeros(len(iac_upstreamturb_count), dtype=int) + + gate_index = np.where((coords == gate_coord).all(axis=1))[0][0] + + # Iterate through the range of points + for turb_id_A in range(np.min(edge_list), np.max(edge_list) + 1): + # If gate point, then skip, because a gate point does not have a inner cluster cable + if turb_id_A == gate_index: + cab_id[turb_id_A] = 999 + turb_id_B[turb_id_A] = 200 # Index for substation + else: + connected_edges = [] + edge_neighbor_counts = [] + edge_points = [] + + # Find edges connected to the point and their respective neighbor counts + for edge_index, (start, end) in enumerate(edge_list): + if start == turb_id_A or end == turb_id_A: + connected_edges.append(edge_index) + + # Add neighbor count for the opposite end of the edge + target_point = end if start == turb_id_A else start + edge_neighbor_counts.append(iac_upstreamturb_count[target_point]) + + # Select the edge (cable) that leads to the turbine with the most neighbors + if edge_neighbor_counts: + max_neighbors_index = np.argmax(edge_neighbor_counts) + selected_edge_index = connected_edges[max_neighbors_index] + cab_id[turb_id_A] = selected_edge_index + + # Get Index of turbine B + cable = edge_list[selected_edge_index] + turb_id_B[turb_id_A] = cable[cable != turb_id_A] + + #iac_cab2turb = relateCab2Turb(iac_ID, TurbB_ID) + + #iac_edges[iac_ID[ic]] + array = np.column_stack((np.arange(len(cab_id)), cab_id, turb_id_B)) + iac_cab2turb = array[array[:, 1] != 999] + + return iac_cab2turb +""" + + +# ----- Plot wind farm layout ----- +def plotCableLayout(iac_dic, turb_coords, subs_coords, gate_connections=[], exclusion_coords=[], save=False): + '''Plot wind farm Cable layout. + + ''' + + # combine coordinates for easy plotting of everything + coords = np.vstack([turb_coords, subs_coords]) + + # Exclusion zones + if len(exclusion_coords) > 0: + exclusion = exclusion_coords + exclusion_polygons_sh = [] # List to store polygons + + # Create exclusion polygons + for ie in range(len(exclusion)): + exclusion_polygon = sh.Polygon(exclusion[ie]) + exclusion_polygons_sh.append(exclusion_polygon) + + + # Set font sizes + #fsize_legend = 12 # Legend + #fsize_ax_label = 12 # Ax Label + #fsize_ax_ticks = 12 # Ax ticks + #fsize_title = 16 # Title + + # Create a colormap and a legend entry for each unique cable section + # Find unique values + # Convert dictionary into data frame + iac_df=pd.DataFrame(iac_dic) + + unique_cables = np.unique([a['conductor_area'] for a in iac_dic]) + colors = plt.cm.viridis(np.linspace(0, 1, len(unique_cables))) # Create a colormap based on the number of unique sections + section_to_color = {sec: col for sec, col in zip(unique_cables, colors)} + + + plt.figure(figsize=(10, 6)) + + # ----- Lease area boundary + #shape_polygon = sh.Polygon(self.boundary) + #x, y = self.boundary_sh.exterior.xy + #plt.plot(x, y, label='Boundary', linestyle='dashed', color='black') + + # Plot Turbines + plt.scatter(coords[:-1, 0], coords[:-1, 1], color='red', label='Turbines') + + # Annotate each point with its index + for i in range(coords.shape[0]-1): #, point in enumerate(cluster_arrays[ic]): + plt.annotate(str(i), coords[i,:], textcoords="offset points", xytext=(0, 10), ha='center') + + # Loop over edges / cable ids + for i in range(len(iac_dic)): + + # Cable selection + color = section_to_color[iac_dic[i]['conductor_area']] + label = f"Section {int(iac_dic[i]['conductor_area'])} mm²" if int(iac_dic[i]['conductor_area']) not in plt.gca().get_legend_handles_labels()[1] else "" + + ia = iac_dic[i]['turbineA_glob_id'] + ib = iac_dic[i]['turbineB_glob_id'] + + plt.plot( coords[[ia,ib], 0], coords[[ia,ib], 1], color=color, label=label) + + plt.text( np.mean(coords[[ia,ib], 0]), np.mean(coords[[ia,ib], 1]), str(i), fontsize=9, color='black') + + + # Turbines + # plt.scatter(cluster_arrays[ic][:, 0], cluster_arrays[ic][:, 1], color='red', label='Turbines') + # Plot gate as a diamond marker + #plt.scatter(self.gate_coords[ic][0], self.gate_coords[ic][1], marker='D', color='green', label='Gate') + + """ + ## ----- Cables Gates to OSS + # TODO: updated cable_id below >>> + iac_oss = iac_df[iac_df['cable_id'] >= 100] + iac_array_oss = iac_oss.values + + for i in range(n_cluster): + cable_section_size = int(iac_array_oss[i, 9]) # Assuming cable section size is in the 7th column + color = section_to_color.get(cable_section_size, 'black') # Default to black if section size not found + connection = gate_connections[i] + x_connection, y_connection = connection.xy + label = f'Section {cable_section_size} mm²' if cable_section_size not in plt.gca().get_legend_handles_labels()[1] else "" + plt.plot(x_connection, y_connection, color=color, label=label) + + #plt.plot([gate_coords[i][0], x_oss], [gate_coords[i][1], y_oss], color=color, label=f'Section {cable_section_size} mm²' if cable_section_size not in plt.gca().get_legend_handles_labels()[1] else "") + """ + + if len(exclusion_coords) > 0: + for ie in range(len(exclusion)): + shape_polygon = exclusion_polygons_sh[ie]#sh.Polygon(self.exclusion[i]) + x, y = shape_polygon.exterior.xy + plt.plot(x, y, linestyle='dashed', color='orange', label='Exclusion Zone') + #ax.plot([], [], linestyle='dashed', color='orange', label='Exclusion Zone') + + # turbine locations + #ax.scatter(x0, y0, c='black', s=12, label='Turbines') + + + # ----- OSS + plt.scatter(subs_coords[:,0], subs_coords[:,1], label='substation', marker='*', color='black', s=100) + + + # Set plot title and labels + plt.title('Wind Turbine Cluster - Cable Conductor Sizes') + plt.xlabel('X (m)') + plt.ylabel('Y (m)') + + # Create a custom legend for the unique cable sections + handles, labels = plt.gca().get_legend_handles_labels() + by_label = dict(zip(labels, handles)) # Removing duplicate labels + plt.legend(by_label.values(), by_label.keys(),loc='upper left', fancybox=True, ncol=2) + plt.gca().set_aspect('equal', adjustable='box') # Set aspect ratio to be equal + + # Create a custom legend for the unique cable sections + #handles, labels = plt.gca().get_legend_handles_labels() + #by_label = dict(zip(labels, handles)) # Removing duplicate labels + #sorted_labels = sorted(by_label.keys()) # Sort the labels alphabetically + #sorted_handles = [by_label[label] for label in sorted_labels] # Get handles corresponding to sorted labels + #plt.legend(sorted_handles, sorted_labels, loc='upper center', bbox_to_anchor=(0.5, -0.1), fancybox=True, ncol=2) + #plt.gca().set_aspect('equal', adjustable='box') # Set aspect ratio to be equal + + + plt.grid(True) + + # ----- Save plot with an incremented number if it already exists + if save: + counter = 1 + output_filename = f'wind farm layout_{counter}.png' + while os.path.exists(output_filename): + counter += 1 + output_filename = f'wind farm layout_{counter}.png' + + # Increase the resolution when saving the plot + plt.savefig(output_filename, dpi=300, bbox_inches='tight') # Adjust the dpi as needed + + +# Test Script +if __name__ == '__main__': + + + turb_coords = [[ 0, 1000], + [ 0, 2000], + [ 0, 3000], + [ 0, 4000], + [ 0, 5000], + [ 1000, 0], + [ 1000, 1000], + [ 1000, 2000], + [ 1000, 3000], + [ 2000, 2000], + [ 2000, 3000], + [ 2000, 4000], + [ 2000, 5000]] + + cluster_id = [ 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, + 1, 1, 2] + + subs_coords = [ 1400, 200] + + conductor_sizes = np.array([300, 630, 1000]) + + cableProps_type = 'dynamic_cable_66' + turb_rating_MW = 15 + + + #iac_dic = getCableLayout(conductor_sizes, cableProps_type, turb_rating_MW, turb_coords, subs_coords, plot=1) + iac_dic, connections, types = getCableLayout(turb_coords, subs_coords, conductor_sizes, cableProps_type, turb_rating_MW, turb_cluster_id=[], plot=1) + + cable_id = np.array([a['cable_id'] for a in iac_dic]) + ia = np.array([a['turbineA_glob_id'] for a in iac_dic]) + ib = np.array([a['turbineB_glob_id'] for a in iac_dic]) + cid =np.array([a['cluster_id'] for a in iac_dic]) + + + # set up a CableSystem!! + from famodel.cables.cable_system import CableSystem + cs = CableSystem(turb_coords) + + cs.update(connections, types, coords=turb_coords, + powers=[15]*len(turb_coords), + subcoords=subs_coords) + + cs.checkConnectivity() + + plt.show() + \ No newline at end of file diff --git a/famodel/design/LineDesign.py b/famodel/design/LineDesign.py new file mode 100644 index 00000000..4b51d0e5 --- /dev/null +++ b/famodel/design/LineDesign.py @@ -0,0 +1,2181 @@ +# New version of LineDesign that uses Subsystem + +import moorpy as mp # type: ignore +#import moordesign.MoorSolve as msolve +from fadesign.fadsolvers import dsolve2, dopt2, doptPlot +from moorpy.MoorProps import getAnchorProps # type: ignore +from moorpy.helpers import (loadLineProps, getLineProps, # type: ignore + rotationMatrix, getFromDict) + +from famodel.mooring.mooring import Mooring + +import numpy as np +import matplotlib.pyplot as plt +import yaml +import time + + + +class LineDesign(Mooring): + ''' + The LineDesign class inherits from Mooring, which includes a Subsystem. For some cases where offsets + need to be computed, this class will also add a System to look at N line case. + + - The dynamic component of the design process will utilize various dynamic amplification factors (DAFs) + that will be used an input into this class to design the Moorings + - Design variables are imported through the 'allVars' variable,designated by the 'Xindices' variable, + and stored in the "X" variable + - The objective is calculated using the method self.objectiveFun to evaluate the cost of the Mooring + - Constraints are initialized by a global dictionary of all possible constraints: self.confundict + - Each constraint in confundict has a corresponding function (method) to evaluate that constraint + - The key is a string (e.g. "min_lay_length") that the user can pass in through a list, with a corresponding + number to designate the Line in the Mooring for the constraint to apply to, and its quantitative limit + - There is a member function (e.g. con_lay_length) that pertains to each constraint. It accepts the design + vector X, updates the design to ensure the mooring system properties are updated, and evaluates the constraint. + It returns a negative scalar if the constraint is not met by the quantity specified by the user + + Other notable capabilities + - Shared vs anchored moorings designated by "shared" parameter + - Anchor spacing can be a design variable. Turbine spacing for a shared line needs to be defined. + - rBFair is the fairlead coordinates relative to the attached body's reference point for an anchored line in Quadrant 1. + For example, rBFair can be something like [7.875,0,-21] or [5.57,5.57,-21] or [3.93,6.82,-21] + For a shared line, the fairlead coordinates are assumed to be the same for both bodies but flipped + - Design variables can be given one of four designations in Xindices: an integer, 'c' (constant), + 's' (to be solved for), or 'r' (like a constant, but can be set as a ratio wrt another variable) + + Example allVars vector: X = [A or W0, L1, D1, ...] where < > section repeats + For anchor lines, the first entry is anchor spacing. For shared lines, the first entry is midpoint weight. + Example Mooring: (anchor at spacing A)---- L1,D1-----(W1)------L2,D2------(W2)------L3,D3------(end B) + Clump weights and buoyancy floats are not specified directly. They are both 'weights' and can have either a postitive or negative value + + ''' + + def __init__(self, depth, lineProps=None, **kwargs): + '''Creates a LineDesign Mooring object to be used for evaluating or optimizing a mooring line design. + + Parameters + ---------- + depth : float + Water depth + + Keyword Arguments + ----------------- + solve_for : string + Keyword indicating which built-in design algorithm to use, if any. Options are: + 'tension' - adjusts a line section length to achieve a target horizontal tension on the line. + 'offset' - adjusts a line section length to achieve a target mean offset considering all lines. + 'stiffness' - adjusts a line section length to achieve a target undisplaced surge stiffness considering all lines. + 'ghost' - adjusts anchor spacing to achieve a target minimum laid length - IN PROGRESS. + 'fancy' - adjusts a line section length to ensure mean offset is less than a target value - IN PROGRESS. + 'none' - makes no adjustment. + All options except none require that one of the line section lengths be set as solved ('s') rather than fixed/variable. + DAFs : float or float array, optional + Dynamic amplification factors to use to scale up quasi-static predicted deviations from mean + values to approximate dynamic ones. Provide a scalar or an n+1 array where n is the number + of line sections and the last entry is the DAF to be used for anchor loads. Default is 1. + + ''' + + self.display = getFromDict(kwargs, 'display', default=0) + + # add the parameters set by the input settings dictionary + self.name = getFromDict(kwargs, 'name', dtype=str, default='no name provided') + lineTypeNames = getFromDict(kwargs, 'lineTypeNames' , dtype=str, shape=-1, default=[]) + + + # set up the mooring system object with the basics from the System class + rho = getFromDict(kwargs, 'rho', default=1025.0) + g = getFromDict(kwargs, 'g' , default=9.81) + self.depth = depth # used? + + # ----- Set properties for Mooring object and its Subsystem ----- + # set model-specific parameters + self.shared = getFromDict(kwargs, 'shared', dtype=bool, default=False) + self.span = getFromDict(kwargs, 'span', default=0) # [m] horizontal extent of mooring (formerly "spacing") + + # set remaining Mooring-specific parameters + self.rBFair = getFromDict(kwargs, 'rBFair', shape=-1, default=[0,0,0]) # [m] end coordinates relative to attached body's ref point + self.nLines = len(lineTypeNames) # number of sections in the mooring line + + + + # ============== set the design variable list ============== + self.solve_for = getFromDict(kwargs, 'solve_for', dtype=str, default='offset') # whether to solve for offsets assuming 3 lines, or solve for mean horizontal tension of this line (for use with shared array design tools) + + self.allVars = getFromDict(kwargs, 'allVars' , shape=3*len(lineTypeNames)) + + # set the design variable type list + if 'Xindices' in kwargs: + self.Xindices = list(kwargs['Xindices']) + if not len(self.Xindices)==len(self.allVars): + raise Exception("Xindices must be the same length as allVars") + else: + raise Exception("Xindices must be provided.") + + # find the largest integer to determine the number of desired design variables + self.nX = 1 + max([ix for ix in self.Xindices if isinstance(ix, int)]) + + # check for errors in Xindices + for i in range(self.nX): + if not i in self.Xindices: + raise Exception(f"Design variable number {i} is missing from Xindices.") + valid = list(range(self.nX))+['c','s','r','g'] # entries must be either design variable index or constant/solve/ratio flags + for xi in self.Xindices: + if not xi in valid: + raise Exception(f"The entry '{xi}' in Xindices is not valid. Must be a d.v. index, 'c', 's', or 'r'.") + + # find the length solve index 's' and make sure it's valid + sInds = [i for i,xi in enumerate(self.Xindices) if xi=='s'] + if len(sInds) == 1: + if (sInds[0]-1)%3 == 0: + self.iL = int((sInds[0]-1)/3) # this is the line index whose length will be adjusted in the dsolve inner loop + else: + raise Exception("The 's' flag in Xindices must be at a line length (i.e. the 2nd, 5th, 8th...) position.") + elif len(sInds) == 0: + if self.solve_for in ['none', 'ghost']: + self.iL = 0 # arbitrary line index. The index won't matter when solve_for = 'none' + else: + raise Exception("A single 's' flag for line length solving must be provided in Xindices") + else: + raise Exception("A single 's' flag for line length solving must be provided in Xindices") + + # check for 'r' variable option + self.rInds = [i for i,xi in enumerate(self.Xindices) if xi=='r'] + for i in range(len(self.rInds)): + if self.allVars[self.rInds[i]] >= 1.0 or self.allVars[self.rInds[i]] <= 0.0: + raise Exception("The ratio variable needs to be between 1 and 0") + + + # set up the mooring system for the specific configuration type + ''' + Just makes the connections, sizing happens later. + + Example allVars vector: X = [A or W0, L1, D1, ...] where < > section repeats + For anchor lines, the first entry is anchor spacing. For shared lines, the first entry is midpoint weight. + Example Mooring: (anchor at spacing A)---- L1,D1-----(W1)------L2,D2------(W2)------L3,D3------(end B) + Clump weights and buoyancy floats are not specified directly. They are both 'weights' and can have either a postitive or negative value + ''' + + # first set the weight, length, and diameter lists based on the allVars inputs. Don't worry about design variables yet + if self.shared==1: + if self.span == 0: raise Exception("For shared arrangements, a span must be provided to the Mooring object.") + Ws = self.allVars[0::3].tolist() + else: + self.span = self.allVars[0]*10 - self.rBFair[0] # in tens of meters + Ws = self.allVars[3::3].tolist() + Ls = self.allVars[1::3].tolist() + Ds = self.allVars[2::3].tolist() + + # if any of the input lengths are in ratio form, convert them to real value form + # (this can currently only handle 1 ration variable per Mooring) + if len(self.rInds) > 0: + self.nsll_ratio = self.allVars[self.rInds[0]] + self.allVars[self.rInds[0]] = self.nsll_ratio*self.span + Ls = self.allVars[1::3].tolist() # reset the Ls variable + + # ==================================================================== + + + + # ----- Initialize some objects ----- + if self.shared==1: + shared=1 + shareCase=2 # assumed symmetric and we model half the shared line. + elif self.shared==0: + shared=0 + shareCase=0 + # make a dummy design dictionary for Mooring to make a Subsystem with??? + dd = dict(sections={}, connectors={}) + dd['sections'] = [{} for i in range(self.nLines)] + dd['connectors'] = [{} for i in range(self.nLines + 1)] + + # the sizing function coefficients to use in the design + self.lineProps = loadLineProps(lineProps) + + # Assign section properties for use in Mooring's Subsystem.makeGeneric call + for i in range(self.nLines): + dd['sections'][i]['type'] = getLineProps(Ds[i], + material=lineTypeNames[i], name=i, lineProps=self.lineProps) + dd['sections'][i]['L'] = Ls[i] + + # Assign props of intermediate point if shared + if self.shared==1: + pointDict = self.getClumpMV(Ws[0]) + + dd['connectors'][0]['m'] = pointDict['m'] + dd['connectors'][0]['v'] = pointDict['v'] + + # Assign props for intermediate points/connectors + for i in range(self.nLines-1): + # if this is an intermediate line + pointDict = self.getClumpMV(Ws[ i + 1*(shared==1)]) + + dd['connectors'][i+1]['m'] = pointDict['m'] + dd['connectors'][i+1]['v'] = pointDict['v'] + # CdA? + + # General mooring dimension info + dd['span' ] = self.span + dd['zAnchor' ] = -self.depth + dd['rad_fair'] = np.abs(self.rBFair[0]) + dd['z_fair' ] = self.rBFair[2] + + # super().__init__(depth=depth, rho=rho, g=g, lineProps=lineProps) # if we're a subsystem + + # Call Mooring init function (parent class) + + + Mooring.__init__(self, dd=dd, rho=rho, g=g, shared=shared) + # The above will also create Mooring self parameters like self.rad_anch + + # Save a copy of the original anchoring radius to use with the + # solve_for=ghost option to adjust the chain length. + self.rad_anch0 = float(self.rad_anch) + + self.createSubsystem(case=int(shareCase)) + if self.shared==1: + self.ss.rA[2] = self.rBFair[2] + + # HARDCODING THIS FOR NOW (MIDPOINT WEIGHT MUST BE UPDATED) + pointDict = self.getClumpMV(.5*Ws[0]) + + self.dd['connectors'][0]['m'] = pointDict['m'] + self.dd['connectors'][0]['v'] = pointDict['v'] + + self.ss.pointList[0].m = pointDict['m'] + self.ss.pointList[0].v = pointDict['v'] + + self.ss.eqtol= 0.002 # position tolerance to use in equilibrium solves [m] + + # load a custom line props scaling dict if provided ?? + #self.ss.lineProps = lineProps + + + # identify number of line sections and initialize dynamic amplification factors + self.DAFs = getFromDict(kwargs, 'DAFs', shape=self.nLines+2, default=1.0) # dynamic amplication factor for each line section, and anchor forces (DAFS[-2] is for vertical load, DAFS[-1] is for horizontal load) + self.Te0 = np.zeros([self.nLines,2]) # undisplaced tension [N] of each line section end [section #, end A/B] + self.LayLen_adj = getFromDict(kwargs, 'LayLen_adj', shape=0, default=0.0) # adjustment on laylength... positive means that the dynamic lay length is greater than linedesign laylength + self.damage = getFromDict(kwargs, 'damage', shape = -1, default = 0.0) #Lifetime fatigue damage *(MBL/dT/dx)^m in list with same order as fatigue_headings + self.fatigue_headings = getFromDict(kwargs, 'fatigue_headings', shape = -1, default = [0]) #loading directions for fatigue damage, same order as self.damage + self.ms_fatigue_index = int(getFromDict(kwargs, 'ms_fatigue_index', shape = 0, default = 1)) #index of line in full moorpy system for fatigue damage evaluation. linelist follows the order in headings + self.corrosion_mm = getFromDict(kwargs, 'corrosion_mm', default=0) # [mm] the corrosion of line material over a 25 year lifetime + + # ----- Set solver and optimization settings ----- + + self.x_target = getFromDict(kwargs, 'x_target', default=0) # [m] target mean offset at rated load (e.g. from LinearSystem) - only used in solve_for offset or ghost + self.x_mean_in = getFromDict(kwargs, 'x_mean_in', default=0) + self.x_mean_out = getFromDict(kwargs, 'x_mean_out', default=0) + #self.x_mean_max = getFromDict(kwargs, 'x_mean_max', default=self.x_mean) # set the maximum tolerable mean offset to match the initial target mean offset << appears no longer really used + self.x_ampl = getFromDict(kwargs, 'x_ampl' , default=10) # [m] expected wave-frequency motion amplitude about mean + #self.x_extreme = getFromDict(kwargs, 'xextreme' , default=self.xmax) # >>> same as below, but leaving for now for backward compatibility <<< + #self.x_extr_pos = getFromDict(kwargs, 'x_extr_pos', default=self.xmax) # [m] expected maximum extreme offset (mean + dynamics) + #self.x_extr_neg = getFromDict(kwargs, 'x_extr_neg', default=-self.x_extr_pos) # [m] expected maximum extreme negative offset (negative of xextreme unless provided separately) + self.fx_target = getFromDict(kwargs, 'fx_target') # [N] the expected thrust force or target horizontal line tension + self.kx_target = getFromDict(kwargs, 'kx_target', default=0) # [N/m] the target horizontal line stiffness + if self.solve_for == 'ghost': + self.lay_length_target = getFromDict(kwargs, 'lay_target') # [m] Target laid length - required when solve_for is ghost + + self.headings = getFromDict(kwargs, 'headings' , shape=-1, default=[60, 180, 300]) # [deg] headings of the mooring lines (used only when solve_for is 'offset', 'stiffness', or 'fancy') + + # >>> TODO: add something that adjusts headings to give min/max offsets in -/+ x direction <<< + + self.noFail = False # can be set to True for some optimizers to avoid failing on errors + + self.iter = -1 # iteration number of a given optimization run (incremented by updateDesign) + self.log = dict(x=[], f=[], g=[], time=[], xe=[], a=[]) # initialize a log dict with empty values + + # set the anchor type and initialize the horizontal and vertical capacities of the anchor + self.anchorType = getFromDict(kwargs, 'anchorType', dtype=str, default='drag-embedment') + self.anchorFx = 0.0 + self.anchorFz = 0.0 + self.anchorFx0 = 0.0 + self.anchorFz0 = 0.0 + + + # ----- optimization stuff ----- + # get design variable bounds and last step size + self.Xmin = getFromDict(kwargs, 'Xmin' , shape=self.nX) # minimum bounds on each design variable + self.Xmax = getFromDict(kwargs, 'Xmax' , shape=self.nX) # maximum bounds on each design variable + self.dX_last = getFromDict(kwargs, 'dX_last', shape=self.nX, default=[]) # 'last' step size for each design variable + if len(self.Xmin) != self.nX or len(self.Xmax) != self.nX or len(self.dX_last) != self.nX: + raise Exception("The size of Xmin/Xmax/dX_last does not match the number of design variables") + + + # initialize the vector of the last design variables, which each iteration will compare against + self.Xlast = np.zeros(self.nX) + + # fill in the X0 value (initial design variable values) based on provided allVars and Xindices (uses first value if a DV has multiple in allVars) + self.X0 = np.array([self.allVars[self.Xindices.index(i)] for i in range(self.nX)]) + + self.X_denorm = np.ones(self.nX) # normalization factor for design variables + self.obj_denorm = 1.0 # normalization factor for objective function + + + # ----- Set up the constraint functions and lists ----- + + if 'constraints' in kwargs: + self.constraints = kwargs['constraints'] + else: + self.constraints = [] + + # a hard-coded dictionary that points to all of the possible constraint functions by name + self.confundict = {"min_Kx" : self.con_Kx, # a minimum for the effective horizontal stiffness of the mooring + "max_offset" : self.con_offset, # a maximum for the horizontal offset in the extreme loaded position + "min_lay_length" : self.con_lay_length, # a minimum for the length of Line 1 on the seabed at x=x_extr_pos (replaces anchor_uplift) + "rope_contact" : self.con_rope_contact, # a margin off the seabed for Point 2 (bottom of Line 2) at x=x_extr_neg + "tension_safety_factor" : self.con_strength, # a minimum ratio of MBL/tension for all lines in the Mooring at x=x_extr_pos + "min_sag" : self.con_min_sag, # a minimum for the lowest point's depth at x=x_extr_pos + "max_sag" : self.con_max_sag, # a maximum for the lowest point's depth at x=x_extr_neg + "max_total_length" : self.con_total_length, # a maximum line length + "min_yaw_stiff" : self.con_yaw_stiffness, # a minimum yaw stiffness for the whole system about the extreme negative position + "max_damage" : self.con_damage, # a maximum fatigue damage for a specified mooring line (scales from provided damage from previous iteration) + "min_tension" : self.con_min_tension # a minimum line tension + } + + # set up list of active constraint functions + self.conList = [] + self.convals = np.zeros(len(self.constraints)) # array to hold constraint values + self.con_denorm = np.ones(len(self.constraints)) # array to hold constraint normalization constants + self.con_denorm_default = np.ones(len(self.constraints)) # default constraint normalization constants + + for i, con in enumerate(self.constraints): # for each list (each constraint) in the constraint dictionary + + # ensure each desired constraint name matches an included constraint function + if con['name'] in self.confundict: + + # the constraint function for internal use (this would be called in UpdateDesign) + def internalConFun(cc, ii): # this is a closure so that Python doesn't update index and threshold + def conf_maker(X): + def func(): + # compute the constraint value using the specified function + val = self.confundict[cc['name']](X, cc['index'], cc['threshold']) + + # record the constraint value in the list + self.convals[ii] = val / self.con_denorm[ii] # (normalized) + self.constraints[ii]['value'] = val # save to dict (not normalized) + + return val + return func() + return conf_maker + + # make the internal function and save it in the constraints dictionary + con['fun'] = internalConFun(con, i) + + # the externally usable constraint function maker + def externalConFun(name, ii): # this is a closure so that Python doesn't update index and threshold + def conf_maker(X): + def func(): + # Call the updatedesign function (internally avoids redundancy) + try: + self.updateDesign(X) + + # get the constraint value from the internal list + val = self.convals[ii] + except: + val = -1000 + + return val + return func() + return conf_maker + + # add the conf function to the conList + self.conList.append(externalConFun(con['name'], i)) + + # Save the default/recommended normalization constant + + if con['name'] in ['max_total_length']: + self.con_denorm_default[i] = con['threshold'] # sum([line.L for line in self.ss.lineList]) + + elif con['name'] in ['min_Kx', 'tension_safety_factor', 'min_yaw_stiff', 'max_damage']: + self.con_denorm_default[i] = con['threshold'] + + elif con['name'] in ['max_offset', 'min_lay_length', 'rope_contact', 'min_sag', 'max_sag', 'max_touchdown_range']: + self.con_denorm_default[i] = depth + + else: + raise ValueError("Constraint parameter "+con['name']+" is not a supported constraint type.") + + + + # ensure each constraint is applicable for the type of mooring + if self.shared==1: + if any([con['name'] in ["min_lay_length", "rope_contact"] for con in self.constraints]): + raise ValueError("You are using a constraint that will not work for a shared mooring line") + else: + if any([con['name'] in ["min_sag", "max_sag"] for con in self.constraints]): + raise ValueError("You are using a sag constraint that will not work for an anchored mooring line") + + if not self.solve_for in ['none', 'ghost']: + if any([con['name'] == "max_offset" for con in self.constraints]): + raise ValueError("The offset constraints should only be used when solve_for is none or ghost") + + if self.solve_for == 'ghost': + if any([con['name'] == "min_lay_length" for con in self.constraints]): + print('Warning: having a min_lay_length cosntraint may conflict with lay_length_target in solve_for ghost.') + #ind = [con['name'] for con in self.constraints].index('min_lay_length') + #self.lay_length_target = self.constraints[ind]['threshold'] + if shared: + raise Exception("solve_for ghost can't be used for shared lines") + if not self.Xindices[0] == 'c': + raise Exception("solve_for ghost requires the Xindices[0] to be 'c'.") + + + # ============================================================= + + # make the mooring system + # self.makeGenericMooring( Ls, Ds, lineTypeNames, Ws, suspended=int(self.shared)) + + if self.solve_for in ['none', 'offset'] and len(self.headings) == 0: + raise Exception('When solve_for is none or offset, line headings must be provided.') + + # If needed, make a MoorPy System to use for determining offsets + self.ms = None + + + # These options require forces/stiffnesses of the whole mooring system + if self.solve_for in ['none', 'offset', 'ghost']: + + self.ms = mp.System(depth=self.depth, rho=self.rho, g=self.g) + #lineProps=lineProps) + + # Add a coupled body to represent the platform + self.ms.addBody(-1, np.zeros(6), DOFs=[0,1]) + + # Set up Subsystems at the headings + for i, heading in enumerate(self.headings): + + rotMat = rotationMatrix(0, 0, np.radians(heading)) + + # create end Points for the line + self.ms.addPoint(1, np.matmul(rotMat, [self.rad_anch, 0, -self.depth])) + self.ms.addPoint(1, np.matmul(rotMat, [self.rad_fair, 0, self.z_fair]), body=1) + + # Make subsystem and attach it + ss = mp.Subsystem(mooringSys=self.ms, depth=self.depth, + span=self.span, rBfair=[-self.rad_fair, 0, self.z_fair]) + + # set up the Subsystem design, with references to the types in dd + types = [sec['type'] for sec in self.dd['sections']] + ss.makeGeneric(lengths=Ls, types=types) + self.ms.lineList.append(ss) # add the SubSystem to the System's lineList + ss.number = i+1 + + # attach it to the respective points + self.ms.pointList[2*i+0].attachLine(i+1, 0) + self.ms.pointList[2*i+1].attachLine(i+1, 1) + + self.ms.initialize() + + # initialize the created mooring system + self.ss.initialize() + self.ss.setOffset(0) + self.updateDesign(self.X0, normalized=False) # assuming X0/AllVars is not normalized + + + def updateDesign(self, X, display=0, display2=0, normalized=True): + '''updates the design with the current design variables using improved Fx/Kx solver methods + ''' + start_time = time.time() + + # Design vector error checks + if len(X)==0: # if any empty design vector is passed (useful for checking constraints quickly) + return + elif not len(X)==self.nX: + raise ValueError(f"LineDesign.updateDesign passed design vector of length {len(X)} when expecting length {self.nX}") + elif any(np.isnan(X)): + raise ValueError("NaN value found in design vector") + + # If X is normalized, denormalize (scale) it up to the full values + if normalized: + X = X*self.X_denorm + + # If any design variable has changed, update the design and the metrics + if not all(X == self.Xlast): + + self.Xlast = np.array(X) # record the current design variables + + self.iter += 1 + + if self.display > 2: + print(f"Iteration {self.iter}") + + if self.display > 1: + print("Updated design") + print(X) + + + # ----- Apply the design variables to update the design ----- + + # update anchor spacing + dvi = self.Xindices[0] # design variable index - will either be an integer or a string + if dvi in range(self.nX): # only update if it's tied to a design variable (if it's an integer) + if self.shared==1: # if it's a shared line, this would be the midpoint weight (we divide by two because we're simulating half the line) + + pointDict = self.getClumpMV(.5*X[dvi]) + + self.dd['connectors'][0]['m'] = pointDict['m'] + self.dd['connectors'][0]['v'] = pointDict['v'] + + self.ss.pointList[0].m = pointDict['m'] + self.ss.pointList[0].v = pointDict['v'] + + # arbitrary depth of self.depth/2. Will be equilibrated soon + #self.ss.pointList[0].setPosition([ -0.5*self.span, 0, -0.5*self.depth]) + + else: + # if it's an anchor line, this would be the anchor spacing + + dv = X[dvi]*10 + self.ss.span = dv - np.abs(self.rBFair[0]) # update the span of the ld.ss subsystem + self.dd['span'] = dv - np.abs(self.rBFair[0]) # can update the ld.subsystem's design dictionary too + + self.ss.pointList[0].setPosition([-self.ss.span, 0, -self.depth]) + + self.setAnchoringRadius(dv) + + + # Update section lengths or diameters + for i in range(self.nLines): + + # length + dvi = self.Xindices[3*i+1] # design variable index + if dvi in range(self.nX): # only update if it's tied to a design variable + + # Modify section 1 length (if using ghost option) + if i==0 and self.solve_for=='ghost': + L_new = X[dvi] + (self.rad_anch - self.rad_anch0) + else: + L_new = X[dvi] + + self.setSectionLength(L_new, i) + if self.ms: + for ss in self.ms.lineList: + ss.lineList[i].setL(L_new) + + #elif dvi=='r': # if the line length is a ratio variable, update it to stay the same proportion of the updated anchor spacing + # self.ss.lineList[i].setL(self.nsll_ratio*self.span) + + # diameter + dvi = self.Xindices[3*i+2] # design variable index + if dvi in range(self.nX): # only update if it's tied to a design variable + lineType = getLineProps(X[dvi], + material=self.dd['sections'][i]['type']['material'], + name=i, lineProps=self.lineProps) + # use the update method to preserve refs to the original dict - this 'points'/connects to the subsystem object too! + self.dd['sections'][i]['type'].update(lineType) + + # apply corrosion to the mooring's MBL dictionary (which gets references in the getTenSF constraint in subsystem) + self.addCorrosion(corrosion_mm=self.corrosion_mm) + + # update the intermediate points if they have any weight or buoyancy + for i in range(self.nLines-1): + dvi = self.Xindices[3*i+3] # design variable index + if dvi in range(self.nX): # only update if it's tied to a design variable + + pointDict = self.getClumpMV(X[dvi]) + + + self.dd['connectors'][i+1]['m'] = pointDict['m'] + self.dd['connectors'][i+1]['v'] = pointDict['v'] + + self.ss.pointList[i+1].m = pointDict['m'] # update clump buoyancy + self.ss.pointList[i+1].v = pointDict['v'] # update clump mass + + if self.ms: # also update things in the ms if there is one + for ss in self.ms.lineList: + ss.pointList[i+1].m = pointDict['m'] + ss.pointList[i+1].v = pointDict['v'] + + + # ----- Screen design to make sure it's physically feasible ----- + + # >>> TODO: check for negative line lengths that somehow get set <<< + + try: + Lmax = 0.95*(self.ss.span + self.depth+self.rBFair[2]) + if sum([self.ss.lineList[i].L for i in range(self.nLines)]) > Lmax: # check to make sure the total length of line is less than the maximum L shape (helpful for GA optimizations) + + if self.solve_for=='none': + self.x_mean_in = -1e3 + self.x_mean_out = 1e3 + self.x_mean_eval = 1e3 # arbitrary high number to set the offset (and constraints) + + for i,con in enumerate(self.constraints): + val = -1e3 + self.convals[i] = val / self.con_denorm[i] # (normalized) + self.constraints[i]['value'] = val # save to dict (not normalized) + + + else: + + # ----- Length adjustment (seems sketchy) ----- + # print(self.ms.bodyList[0].r6[2]) + # set x0 as a 1D list of the line length to be solved for + x0 = [self.ss.lineList[self.iL].L] + + # maximum length of the segment being sized to avoid fully slack + Lmax = 0.99*(self.ss.span + self.depth+self.rBFair[2]) - sum([self.ss.lineList[i].L for i in range(self.nLines) if i != self.iL]) + + # >>> may need a different Lmax for shared lines <<< + + if x0[0] >= Lmax: + x0[0] = 0.8*Lmax + + + # ----- Solver process ----- + + # call dsolve2 to tune line length - eval function depends on solve_for + # note: use a high stepfac so that dsolve2 detects a nonzero slope even when the slope is quite shallow + if self.solve_for == "tension": + x, y, info = dsolve2(self.func_TH_L, x0, tol=[0.4*self.ss.eqtol], args=dict(direction='horizontal'), + Xmin=[10], Xmax=[Lmax], dX_last=[10], maxIter=40, + stepfac=100, display=self.display-1) + elif self.solve_for == "offset": + + args = dict(xOffset=self.x_target, display=self.display-1) + + x, y, info = dsolve2(self.func_fx_L, x0, args=args, + tol=[0.4*self.ss.eqtol], Xmin=[10], Xmax=[Lmax], + dX_last=[10], stepfac=100, maxIter=40, + display=self.display-1) + + elif self.solve_for == "none": + pass + # >>> can remove this from if else block once solve_for error check is done in init func <<< + + elif self.solve_for == 'stiffness': + + x, y, info = dsolve2(self.func_kx_L, x0, args=dict(display=display2), + tol=[0.4*self.ss.eqtol], Xmin=[10], Xmax=[Lmax], + dX_last=[10], stepfac=100, display=self.display-1) + + # this solves for the line length to meet a stiffness equality constraint + # which means that we can still have an offset constraint since the line + # length isn't being solved for to meet a certain offset + + + elif self.solve_for == 'fancy': # a new option to allow lower mean offsets (need to rename!) + # Outer loop determines offset that gives target tension SF, inner loop adjusts line length to achieve said offset + def tuneLineLengthsForOffset(xCurrent, args): # this function does the standard "offset"-mode solve, but now it can be done in the loop of another solve + + args = dict(xOffset=xCurrent[0], fx_target=self.fx_target) + + # tune line length until thrust force is balanced at this mean offset + x, y, info = dsolve2(self.func_fx_L, x0, args=args, + tol=[0.4*self.ss.eqtol], Xmin=[10], Xmax=[Lmax], + dX_last=[10], stepfac=100, display=0) + + stopFlag = False if info['success'] else True # if the line length solve was unsuccessful, set the flat to stop the mean offset solve + + # check strength constraint at this offset + some dynamic additional offset + # (doing this manually here for now, and avoiding the strength constaint at higher levels >>> do not use tension_safety_factor! <<<) + '''This ensures the MBL of the line is always greater than the maximum tension the line feels times a safety factor''' + self.ss.lineList[self.iL].setL(x[0]) # make sure the design is up to date (in terms of tuned line length) + self.ss.setOffset(xCurrent[0] + 10) # offset the body the desired amount (current mean offset + wave offset) + cMin = self.ss.getMinSF(display=display) - 2.0 # compute the constraint value + + print(f" xmax={xCurrent[0]:8.2f} L={x[0]:8.3f} dFx={y[0]:8.0f} minSF={self.getMinSF():7.3f}") + #breakpoint() + + return np.array([cMin]), dict(status=1), stopFlag # return the constraint value - we'll actually solve for this to be zero - finding the offset that just barely satisifes the SFs + + # solve for mean offset that will satisfy tension safety factor constraint (after dynamic wave offset is added) + x, y, info = dsolve2(tuneLineLengthsForOffset, [5], tol=[4*self.ss.eqtol], Xmin=[1], Xmax=[4*self.x_target], dX_last=[5], stepfac=10, display=1) + + + elif self.solve_for=='ghost': + '''Use very large anchor spacing and compute an imaginary + anchor spacing and line length based on the desired lay + length.''' + + # Compute the offset with the adjusted design variables + self.x_mean_out = self.getOffset(self.fx_target) + self.ms.bodyList[0].setPosition([0,0,0,0,0,0]) # ensure body is re-centered + + # self.span and self.ss.span seems redundant. Does LD/Mooring need it?? <<< + + # figure out tension in least laid length scenario... + self.ss.setOffset(self.x_mean_out) # apply max static offsets + self.ss.setDynamicOffset(self.x_mean_out + self.x_ampl) # move to dynamic offset + max_anchor_tension = self.ss.TeD[0,0] # save tension at anchor + + # Set anchoring radius a bit larger than needed, and evaluate once (ss only) + length_to_add = 0.2 * self.rad_anch + new_length = self.dd['sections'][0]['L'] + length_to_add/(1 + max_anchor_tension/self.ss.lineList[0].EA) + self.rad_anch = float(self.rad_anch + length_to_add) + self.ss.span = self.rad_anch - self.rBFair[0] + self.ss.setEndPosition([-self.rad_anch, 0, -self.depth], endB=False) + Mooring.setSectionLength(self, new_length, 0) # ss only, skip ms + + # Figure out lay length + self.ss.setOffset(self.x_mean_out) # apply max static offsets + self.ss.setDynamicOffset(self.x_mean_out + self.x_ampl) # move to dynamic offset + max_anchor_tension = self.ss.TeD[0,0] # save tension at anchor + min_lay_length = self.ss.getLayLength() # record minimum lay length + + # Adjust anchor positions to hit target + unused_length = min_lay_length - self.lay_length_target + new_length = self.dd['sections'][0]['L'] - unused_length + new_spacing = self.rad_anch - unused_length*(1 + max_anchor_tension/self.ss.lineList[0].EA) + self.setAnchoringRadius(new_spacing) + self.setSectionLength(new_length, 0) + + # Update the Subsystem solutions after the adjustments + self.ss.staticSolve() + for ss in self.ms.lineList: + ss.staticSolve() + + #print(f"{self.iter} {self.ss.offset:6.2f}m offset, {self.rad_anch:6.2f} rad_anch, {self.ss.lineList[0].L:6.2f} L") + + else: + raise Exception("solve_for must one of 'offset', 'tension', 'none', 'stiffness, 'fancy', or 'ghost'") + + + if not self.solve_for in ['none', 'ghost']: + if info["success"] == False: + print("Warning: dsolve2 line length tuning solve did not converge.") + #breakpoint() # <<<< handle non convergence <<< + else: + #>>>>> deal with nonzero y - penalize it somehow - for optimizer <<<<< + + # ensure system uses latest tuned line length + #self.ss.lineList[self.iL].setL(x[0]) + self.setSectionLength(x[0], self.iL) + + + # ----- Compute (or set) high and low mean offsets ----- + # (solve for the offsets at which the horizontal mooring reactions balance with fx_target) + if self.solve_for in ['none', 'ghost']: + self.x_mean_out = self.getOffset(self.fx_target) + self.x_mean_in = -self.getOffset(-self.fx_target) + if display > 1: print(f" Found offsets x_mean_out: {self.x_mean_out:.2f}, x_mean_in: {self.x_mean_in:.2f}") + self.ms.bodyList[0].setPosition([0,0,0,0,0,0]) # ensure body is re-centered + + # x_mean_in is the offset when the input headings are flipped, representing the opposite loading direction. + # This will only be worst-case/best-case offsets when one of the input headings is either completely upwind or completely downwind. + + + # ----- Evaluate system state and constraint values at offsets ----- + + # Evaluate any constraints in the list, at the appropriate displacements. + # The following function calls will fill in the self.convals array. + + + # ZERO OFFSET: + self.ss.setOffset(0) + + # get undisplaced tensions of each line section and anchors + for i, line in enumerate(self.ss.lineList): + self.Te0[i,0] = np.linalg.norm(line.fA) + self.Te0[i,1] = np.linalg.norm(line.fB) + + self.anchorFx0 = self.ss.lineList[0].fA[0] + self.anchorFz0 = self.ss.lineList[0].fA[2] + + # Call any constraints that evaluate at the undisplaced position + #self.calcCurvature() + for con in self.constraints: + if con['offset'] == 'zero': + con['fun'](X) + + + # MAX OFFSET: + self.ss.setOffset(self.x_mean_out) # apply static offsets + self.ss.setDynamicOffset(self.x_mean_out + self.x_ampl) # move to dynamic offset + # save worst-case anchor tensions for use in cost calculations (includes DAF) + self.anchorFx = self.ss.anchorFx0 + self.ss.DAFs[-1]*(self.ss.lineList[0].fA[0] - self.ss.anchorFx0) + self.anchorFz = self.ss.anchorFz0 + self.ss.DAFs[-2]*(self.ss.lineList[0].fA[2] - self.ss.anchorFz0) + + self.min_lay_length = self.ss.getLayLength() # record minimum lay length + #print(f"{self.iter} {self.ss.offset:6.2f}m offset, {self.rad_anch:6.2f} rad_anch, {self.ss.lineList[0].L:6.2f} L") + #print(f"Min lay length is {self.min_lay_length}") + self.x_mean_eval = float(self.x_mean_out) # the x_mean value to evaluate if there's an offset constraint + + # Call any constraints needing a positive displacement + for con in self.constraints: + if con['offset'] == 'max': + con['fun'](X) + + + # MIN OFFSET: + self.ss.setOffset(-self.x_mean_in) # apply static offset + self.ss.setDynamicOffset(-self.x_mean_in + -self.x_ampl) # peak offset + + self.max_lay_length = self.ss.getLayLength() # record maximum lay length + + self.x_mean_eval = float(self.x_mean_in) # the x_mean value to evaluate if there's an offset constraint + + # Call any constraints needing a negative displacement + for con in self.constraints: + if con['offset'] == 'min': + con['fun'](X) + + + # OTHER: + self.ss.setOffset(0) # restore to zero offset and static EA + # or at least set back to static states + + # Call any constraints that depend on results across offsets + for con in self.constraints: + if con['offset'] == 'other' or con['offset'] == 'zero': + con['fun'](X) + + ############################################################ + + except: + + if self.solve_for=='none': + self.x_mean_in = -1e3 + self.x_mean_out = 1e3 + self.x_mean_eval = 1e3 # arbitrary high number to set the offset (and constraints) + + for i,con in enumerate(self.constraints): + val = -1e3 + self.convals[i] = val / self.con_denorm[i] # (normalized) + self.constraints[i]['value'] = val # save to dict (not normalized) + + + # ----- Evaluate objective function ----- + + # Calculate the cost from all components in the Mooring + self.lineCost = 0.0 + for line in self.ss.lineList: + self.lineCost += line.L*line.type['cost'] + + # Adjust cost for active length in case of ghost option + if self.solve_for == 'ghost': + + # the length beyond the minimum lay length is not used + unused_length = self.min_lay_length - self.lay_length_target + + # just adjust costs from first section + self.lineCost -= unused_length * self.ss.lineList[0].type['cost'] + + # also adjust and record anchor position ??? (would be nice to show on plots) + + + # calculate anchor cost (using anchor forces calculated when the mooring's constraints were analyzed) + if self.shared==1: + self.anchorCost = 0.0 + self.anchorMatCost = 0.0 + self.anchorInstCost = 0.0 + self.anchorDecomCost = 0.0 + else: + self.anchorMatCost, self.anchorInstCost, self.anchorDecomCost = getAnchorProps(self.anchorFx, self.anchorFz, type=self.anchorType) + self.anchorCost = self.anchorMatCost + self.anchorInstCost + self.anchorDecomCost + + # calculate weight/float cost + self.wCost = 0.0 + self.WF = 1.0 # weight factor: a multiplier for the weight cost per unit mass (kg) + for point in self.ss.pointList: + if point.number > 1 and point.number < self.nLines+1: + self.wCost += abs(point.m + point.v*self.rho)*self.WF + + # if it's shared, we need to double the line costs since it's mirrored + if self.shared==1: + self.lineCost = self.lineCost*2 + self.wCost = self.wCost*2 + + # total cost for all 3 moorings + self.cost = self.lineCost + self.anchorCost + self.wCost + + if self.display > 1: + print(' Cost is ',self.cost) + + + # >>> dynamic_L = self.ss.lineList[0].L - self.min_lay_length #(for line [0] only...) + + self.obj_val = self.cost / self.obj_denorm # normalize objective function value + + + # ----- write to log ----- + + # log the iteration number, design variables, objective, and constraints + self.log['x'].append(list(X)) + self.log['f'].append(list([self.obj_val*self.obj_denorm])) + self.log['g'].append(list(self.convals*self.con_denorm)) + self.log['time'].append(time.time() - start_time) + self.log['xe'].append(self.ss.lineList[self.iL].L) + self.log['a'].append((self.ss.span + self.rBFair[0])/10) + + # TODO: log relevant tuned values (line length, lay length, etc.) for each solve_for option <<< + + # provide some output + if self.display > 5: + f = self.objectiveFun(X, display=1) + + Fx = self.fB_L[0] # get horizontal force from mooring on body + + print(f"Fx: {Fx/1e3:7.1f} vs target of {self.fx_target/1e3:7.1f}") + + print("Line lengths are ") + for line in self.ss.lineList: + print(line.L) + + print("Line input diameters are ") + for lineType in self.lineTypes.values(): + print(lineType['input_d']) + + print(f"Cost is {f}") + + self.evaluateConstraints(X, normalized=False, display=1) + + self.plotProfile() + plt.show() + + + def objectiveFun(self, X, display=0, normalized=True): + '''objective of the optimization. Set to minimize cost''' + + self.updateDesign(X, display=display, normalized=normalized) + + if display > 1: + print(f"Cost is {self.cost:.1f} and objective value is {self.obj_val:.3f}.") + + return float(self.obj_val) # return a copy + + + def evaluateConstraints(self, X, display=0, normalized=True): + '''Update the design (if necessary) and display the constraint + values.''' + + self.updateDesign(X, display=display, normalized=normalized) + + if display > 1: + for i, con in enumerate(self.constraints): + print(f" Constraint {i:2d} value of {con['value']:8.2f} " + +f"for {con['name']}: {con['threshold']} of {con['index']} at {con['offset']} displacement.") + + return np.array(self.convals) # return a copy + + + def setNormalization(self): + '''Set normalization factors for optimization + (based on initial design state).''' + + # design variables + self.X_denorm = np.array(self.Xlast) + # objective + self.obj_denorm = self.cost + # constraints + self.con_denorm = self.con_denorm_default + + + def clearNormalization(self): + '''Clear any normalization constants to unity so no scaling is done.''' + self.X_denorm = np.ones(self.nX) + self.obj_denorm = 1.0 + self.con_denorm = np.ones(len(self.constraints)) + + + def optimize(self, gtol=0.03, maxIter=40, nRetry=0, plot=False, display=0, stepfac=4, method='dopt'): + '''Optimize the design variables according to objectve, constraints, bounds, etc. + ''' + + # set the display value to use over the entire process + self.display = display + + # reset iteration counter + self.iter = -1 + + # clear optimization progress tracking lists + self.log['x'] = [] + self.log['f'] = [] + self.log['g'] = [] + self.log['time'] = [] + self.log['xe'] = [] + self.log['a'] = [] + + def eval_func(X, args): + '''Mooring object evaluation function condusive with MoorSolve.dopt2''' + + f = self.objectiveFun(X, display=display) + g = self.evaluateConstraints(X, display=display) + oths = dict(status=1) + Fx = self.ss.fB_L[0] + + return f, g, [self.ss.lineList[self.iL].L], Fx, oths, False + + + # set noFail for GAs in case they come up with crazy designs (avoids exceptions) + if 'GA' in method: + self.noFail = True + else: + self.noFail = False + + # Set starting point to normalized value + X0 = self.X0 / self.X_denorm + dX_last = self.dX_last / self.X_denorm + Xmax = self.Xmax / self.X_denorm + Xmin = self.Xmin / self.X_denorm + + + # call optimizer to perform optimization + # --------- dopt method: Newton Iteration ----------- + if method=='dopt': + + if display > 0: print("\n --- Beginning LineDesign2 optimize iterations using DOPT2 ---") + + X, min_cost, infodict = dopt2(eval_func, X0, tol=4*self.ss.eqtol, a_max=1.4, maxIter=maxIter, stepfac=stepfac, + Xmin=Xmin, Xmax=Xmax, dX_last=dX_last, display=4) #self.display) + + + # Retry procedure if things don't work + for i in range(nRetry): + print(f" Mooring optimization attempt {i} was UNSUCCESFUL") + print(f" Message from dopt on attempt {i}: {infodict['message']}") + + self.updateDesign(X) # update the mooring using the optimized design variables + G = self.evaluateConstraints(X) # evaluate the constraints of the mooring + + # check how far the constraints are off + c_rel = G / np.array([con[2] for con in self.constraints], dtype=float) # get relative value of constraints (denominator converts dict values to a np.array) + i_kx = [i for i,con in enumerate(self.constraints) if con=='min_Kx'][0] # index of Kx constraint + + if display > 1: print(f' stiffness is {c_rel[i_kx]*100+100.:5.1f}% of target') + c_rel[i_kx] = 0.0 + # zero the kx constraint since it's okay to break it (that's why we iterate with LinearSystem) + + + + if np.min(c_rel) < -gtol: + + # try to catch some known problem cases + if stepfac==10: + print(' retrying optimization with step size (stepfac) boosted from 10 to 100') + stepfac = 100 + + else: + self.updateDesign(X) # make sure it's left at the optimized state + break # out of ideas, so that's the best we can do with this design problem + + # rerun the optimizer with modified settings + X, min_cost, infodict = dopt2(eval_func, X0, tol=0.001, a_max=1.4, maxIter=maxIter, + Xmin=Xmin, Xmax=Xmax, dX_last=dX_last) + + + else: # if successful + self.updateDesign(X) # make sure it's left at the optimized state + break # exit the retry loop + + if i==nRetry-1: # re-check the design if we did all retries, since otherwise it won't be done by the loop above + self.updateDesign(X) # update the mooring using the optimized design variables + G = self.evaluateConstraints(X, display=0) # evaluate the constraints of the mooring + + # check how far the constraints are off + #c_rel = G / np.fromiter(self.constraints.values(), dtype=float) # get relative value of constraints (denominator converts dict values to a np.array) + c_rel = G / np.array([con[2] for con in self.constraints], dtype=float) # get relative value of constraints (denominator converts dict values to a np.array) + i_kx = [i for i,con in enumerate(self.constraints) if con=='min_Kx'][0] # index of Kx constraint + if display > 1: print(f' stiffness is {c_rel[i_kx]*100+100.:5.1f}% of target') + c_rel[i_kx] = 0.0 + + + # --------- COBYLA method ----------- + elif method in ['COBYLA', 'SLSQP']: + + from scipy.optimize import minimize + + if self.display > 0: print("\n --- Beginning LineDesign2 optimize iterations using COBYLA ---") + + condict = [dict(type="ineq", fun=con) for con in self.conList] + cons_tuple = tuple(condict) + + if method=='COBYLA': + result = minimize(self.objectiveFun, X0, constraints=cons_tuple, + method="COBYLA", options={'maxiter':maxIter, + 'disp':True, 'rhobeg':0.1, 'catol':0.001}) # 'rhobeg':10.0 + + elif method=='SLSQP': + result = minimize(self.objectiveFun, X0, constraints=cons_tuple, + method='SLSQP', bounds = list(zip(Xmin, Xmax)), + options={'maxiter':maxIter, 'eps':0.02, + 'ftol':1e-6, 'disp': True, 'iprint': 99}) + + X = result.x + + + # --------- Bayesian method ----------- + elif method == 'bayesian': + + from bayes_opt import BayesianOptimization + from scipy.optimize import NonlinearConstraint + + if self.display > 0: print("\n --- Beginning LineDesign2 optimize iterations using Bayesian Optimization ---") + + # --- make list of decision variable names --- + # design parameter names [A or W0, L1, D1, ...] + ''' + param_names = [] + for i in range( (2+self.nX)//3): + param_names = param_names + [f'W{i}', f'L{i+1}', f'D{i+1}'] + if not self.shared: + param_names[0] = 'A' + ''' + dvnames = [str(i) for i in range(self.nX)] + + # --- set up constraints --- + def constraint_function(**kwargs): + + # Reconstruct decision variable vector + X = np.zeros(self.nX) + for i in range(self.nX): + X[i] = kwargs[dvnames[i]] + + # Call and evaluate each constraint? + G = self.evaluateConstraints(X, display=0) + + return G + + # Make constraint objects (valid values are from zero to infinity) + zeros = np.zeros(len(self.conList)) + constraint = NonlinearConstraint(constraint_function, zeros, zeros+np.inf) + + # Make objective function to maximize + def negativeObjectiveFun(**kwargs): + + # Reconstruct decision variable vector + X = np.zeros(self.nX) + for i in range(self.nX): + X[i] = kwargs[dvnames[i]] + + # Negative version of objective function + return -1*self.objectiveFun(X, display=0) + + # Bounded region of parameter space + pbounds = {} + for i in range(self.nX): + pbounds[dvnames[i]] = (Xmin[i], Xmax[i]) + + # Set up optimizer + optimizer = BayesianOptimization( + f=negativeObjectiveFun, constraint=constraint, + pbounds=pbounds, verbose=2, random_state=1) + + # Find some valid starting points using a random search + pts = 0 # how many valid starting points found so far + for i in range(1000): + x = optimizer.space.random_sample() + print(x) + if optimizer.constraint.allowed(optimizer.space.probe(x)[1]): + print('Registering start point') + print(x) + optimizer.space.probe(x) + pts += 1 + + if pts > 3: # <<< How many valid starting points to ensure + break + + # Do the optimization + optimizer.maximize( + init_points=4, # <<< Total number of start points before iterating + n_iter=maxIter) # <<< Number of points to iterate through + + print(optimizer.max) + + X = np.array(list(optimizer.max['params'].values())) + + + # ----- CMNGA ----- + elif method=='CMNGA': + from cmnga import cmnga # type: ignore + + bounds = np.array([[Xmin[i], Xmax[i]] for i in range(len(Xmin))]) + + X, min_cost, infoDict = cmnga(self.objectiveFun, bounds, self.conList, + dc=0.03, nIndivs=14, nRetry=500, maxGens=20, maxNindivs=600 ) + + + # --------- Genetic Algorithm ---------- + elif method=='GA': + + # import the GA from scipy to save from importing if other optimizers are selected + from scipy.optimize import differential_evolution, NonlinearConstraint + + # initialize storage variables for the GA, including an iterator variable to track through LineDesign + n = 100000 + self.XsGA = np.zeros([self.nX, n]) + self.CsGA = np.zeros([len(self.constraints), n]) + self.FsGA = np.zeros([3,n]) + + # initialize some GA parameters + self.popsize = 2 + self.maxiter = 40 + + self.popsize = 15 + self.maxiter = 1000 + + # bounds + bounds = [(Xmin[i], Xmax[i]) for i in range(len(Xmin))] + # constraints + constraints = tuple([ NonlinearConstraint(self.conList[i], 0, np.inf) for i in range(len(self.conList)) ]) + + # run the GA + result = differential_evolution(self.objectiveFun, bounds, maxiter=self.maxiter, + constraints=constraints, popsize=self.popsize, tol=0.1, disp=True, polish=False) + + # this doesn't require the initial design variable vector, it searches the whole design space initially + + # set the number of individuals in the population (for some reason, it means NP = popsize*N, where N is # of DV's) + if self.popsize==1: + self.NP = self.popsize*self.nX + 1 + else: + self.NP = self.popsize*self.nX + + # organize the stored variables better (trim the excess zeros) + XsGA = np.zeros([len(self.XsGA), len(np.trim_zeros(self.XsGA[0,:]))]) + for i in range(len(self.XsGA)): + XsGA[i,:] = np.trim_zeros(self.XsGA[i,:]) + self.XsGA = np.array(XsGA) + maxCsGA = 0 + for i in range(len(self.CsGA)): + if len(np.trim_zeros(self.CsGA[i,:])) > maxCsGA: + maxCsGA = len(np.trim_zeros(self.CsGA[i,:])) + CsGA = np.zeros([len(self.CsGA), maxCsGA]) + for i in range(len(self.CsGA)): + CsGA[i,:len(np.trim_zeros(self.CsGA[i,:]))] = np.trim_zeros(self.CsGA[i,:]) + self.CsGA = np.array(CsGA) + FsGA = np.zeros([len(self.FsGA),len(self.CsGA[0])]) + for i in range(len(FsGA)): + FsGA[i,:] = np.array(self.FsGA[i,0:len(self.CsGA[0])]) + self.FsGA = FsGA + + X = result.x + + + # --------- Particle Swarm Algorithm ---------- + elif method=='PSO': + + from pyswarm import pso + + xopt, fopt = pso(self.objectiveFun, Xmin, Xmax, f_ieqcons=self.getCons4PSO, + maxiter=50, swarmsize=1000, debug=True) + + # TODO: Either implement a change in the pyswarm.pso function to rerun if a feasible design isn't found in the first generation OR + # implement a try/except statement in updateDesign for when solveEquilibrium errors occur (which will happen if a feasible design isn't found) + + X = xopt + + + else: + raise Exception('Specified optimization method not recognized.') + + # make sure it's left at the optimized state + self.updateDesign(X) + + # save a couple extra metrics + #self.infodict['weight'] = -self.fB_L[2] + + # check whether optimization passed or failed based on constraint values + self.evaluateConstraints(X, display = 5) + if min(self.convals) > -0.01: + self.success = True + else: + self.success = False + + if plot: + self.plotOptimization() + + return X, self.cost #, infodict + + + def getCons4PSO(self, X): + conList = [] + for con in self.constraints: + conList.append(con['value']) + return conList + + + def plotOptimization(self): + + if len(self.log['x']) == 0: + print("No optimization trajectory saved (log is empty). Nothing to plot.") + return + + fig, ax = plt.subplots(len(self.X0)+1+len(self.constraints),1, sharex=True, figsize=[6,8]) + fig.subplots_adjust(left=0.4) + Xs = np.array(self.log['x']) + Fs = np.array(self.log['f']) + Gs = np.array(self.log['g']) + + for i in range(len(self.X0)): + ax[i].plot(Xs[:,i]) + #ax[i].axhline(self.Xmin[i], color=[0.5,0.5,0.5], dashes=[1,1]) + #ax[i].axhline(self.Xmax[i], color=[0.5,0.5,0.5], dashes=[1,1]) + + ax[len(self.X0)].plot(Fs) + ax[len(self.X0)].set_ylabel("cost", rotation='horizontal') + + for i, con in enumerate(self.constraints): + j = i+1+len(self.X0) + ax[j].axhline(0, color=[0.5,0.5,0.5]) + ax[j].plot(Gs[:,i]) + ax[j].set_ylabel(f"{con['name']}({con['threshold']})", + rotation='horizontal', labelpad=80) + + ax[j].set_xlabel("function evaluations") + + def plotGA(self): + '''A function dedicated to plotting relevant GA outputs''' + + # determine how many "generations" the GA went through + gens = [0]; m=3 + gens.append(self.NP*m) + feasible=False + while len(gens) < self.maxiter+1: + while not feasible and len(gens) < self.maxiter+1: + m=2 + nextgen = gens[-1] + (self.NP*m) + gens.append(nextgen) + for f in self.FsGA[0,gens[-2]:nextgen]: + if f>0: + feasible=True + m=1 + if len(gens) < self.maxiter+1: + nextgen = gens[-1] + (self.NP*m) + gens.append(nextgen) + if len(gens) != self.maxiter+1: raise ValueError('Something is not right') + + + #Ls = self.allVars[1::3].tolist() + #Ds = self.allVars[2::3].tolist() + #Ws = self.allVars[3::3].tolist() + + # set the x-axis vector of each individual that was evaluated + iters = np.arange(1, self.iter+1 + 1, 1) + + # plot the change in design variables across each individual + chainL = [self.XsGA[0,i] for i in range(len(self.XsGA[0]))] + chainD = [self.XsGA[1,i] for i in range(len(self.XsGA[1]))] + #polyL = [self.XsGA[2,i] for i in range(len(self.XsGA[2]))] + #polyD = [self.XsGA[3,i] for i in range(len(self.XsGA[3]))] + + fig, ax = plt.subplots(2,1, sharex=True) + ax[0].plot(iters, chainL, label='chain') + #ax[0].plot(iters, polyL, label='polyester') + ax[1].plot(iters, chainD, label='chain') + #ax[1].plot(iters, polyD, label='polyester') + ax[1].set_xlabel('individual evaluated') + ax[1].set_ylabel('line diameter (mm)') + ax[0].set_ylabel('line length (m)') + ax[0].legend() + ax[1].legend() + #for i in range(len(gens)): + #ax[0].axvline(x=gens[i], color='k') + + # plot the change in each constraint of each individual across the optimization + Cnames = ['lay_length','rope_contact','offset','strength0','strength1'] + Cline = np.zeros_like(self.CsGA) + fig, ax = plt.subplots(len(self.CsGA), 1, sharex=True) + for i in range(len(self.CsGA)): + for j in range(len(self.CsGA[i])): + if self.CsGA[i,j] < -9000: + ax[i].plot(iters[j], 0, 'rx') + Cline[i,j] = 0 + else: + ax[i].plot(iters[j], self.CsGA[i,j], 'bo') + Cline[i,j] = self.CsGA[i,j] + ax[i].set_ylabel(f'{Cnames[i]}') + ax[i].plot(iters, Cline[i,:], 'g') + ax[i].plot(iters, np.zeros(len(iters)), 'r') + ax[-1].set_xlabel('individual evaluated') + + # plot the change in objective (cost) of each individual across the optimization + Fnames = ['Line Cost', 'Anchor Cost', 'Total Cost'] + fig, ax = plt.subplots(1, 1, sharex=True) + ax.plot(iters, self.FsGA[0,:], label='Line Cost') + ax.plot(iters, self.FsGA[1,:], label='Anchor Cost') + ax.plot(iters, self.FsGA[2,:], label='Total Cost') + ax.set_ylabel('Cost ($)') + ax.set_xlabel('individual evaluated') + ax.legend() + ''' + # to calculate all the iterations (individuals) that had all nonzero constraints + a=[] + for j in range(len(ld.CsGA[0])): + if np.all(ld.CsGA[:,j]>0): + a.append(j) + + # attempting to plot only the nonzero points on the plot + for i in range(len(ld.FsGA)): + for j in range(len(ld.FsGA[i])): + if ld.FsGA[i,j]==np.nan: + ld.FsGA[i,j] = None + ''' + + + + def storeGA(self, val, i, type='X', name='', index=0): + '''function to store the design variable vector, constraint values, and objective results for each iteration, based on self.iter, + where self.iter is updated every time updateDesign is called''' + + #if method=='GA': + if type=='X': + self.XsGA[:,i] = val + + elif type=='C': + confunnames = [self.confundict[con[0]].__name__ for con in self.constraints] + for c in range(len(np.unique(confunnames))): + if name==confunnames[c]: + self.CsGA[c+index,i] = val + + elif type=='F': + self.FsGA[:,i] = val + + + """ + def checkGA(self, type='normal'): + '''function to check the feasibility of a design, mostly used in a GA, to ensure that LineDesign can even run it. + More specifically, if the GA comes up with a design with sum of line lengths longer than span+depth, it will return False''' + + total_linelength = sum([self.ss.lineList[i].L for i in range(self.nLines)]) + + if type=='normal': + Lmax0 = self.span-self.rBFair[0] + self.depth+self.rBFair[2] # maximum possible line length allowable in equilibrium position + if total_linelength > Lmax0: + return False + else: + return True + + elif type=='offset': + Lmax1 = self.span-self.rBFair[0]-self.x_mean_out-self.x_ampl + self.depth+self.rBFair[2] # maximum possible line length allowable in offset position + if total_linelength > Lmax1: + return False + else: + return True + """ + + + # :::::::::: solver functions :::::::::: + + # the original function from LineDesign, for tuning the line's horizontal tension + def func_TH_L(self, Xl, args): + '''Apply specified section L, return the horizontal pretension error.''' + self.setSectionLength(Xl[0], self.iL) + # option to setOffset? + self.ss.staticSolve() + if args['direction']=='horizontal': + Fx = abs(self.ss.fB_L[0]) # horizontal fairlead tension + elif args['direction']=='norm': + Fx = np.linalg.norm(self.ss.fB_L) + + return np.array([Fx - self.fx_target]), dict(status=1) , False + + + def func_kH_L(self, Xl, args): + '''Apply specified section L, return the horizontal stiffness error.''' + self.ss.lineList[self.iL].setL(Xl[0]) + # option to setOffset? + self.staticSolve() + Kx = self.KB_L[0,0] # horizontal inline stiffness + + return np.array([Kx - self.kx_target]), dict(status=1) , False + + + def func_fx_L(self, Xl, args): + '''Apply specified section L, return the Fx error when system is offset.''' + '''Function for solving line length that achieves equilibrium at a specified offset. + Expects xOffset, fx_target, and angles as keys in args dictionary. + Receives line length and returns net force at xOffset.''' + + if self.ms: + for ss in self.ms.lineList: + ss.lineList[self.iL].setL(Xl[0]) + self.ms.bodyList[0].setPosition([args['xOffset'], 0,0,0,0,0]) + self.ms.solveEquilibrium() + Fx = -self.ms.bodyList[0].getForces()[0] + else: + self.ss.lineList[self.iL].setL(Xl[0]) + self.ss.setOffset(args['xOffset']) + Fx = np.abs(self.ss.fB_L[0]) # horizontal fairlead tension. + + if 'display' in args: + if args['display'] > 2: + print(f" Xl is {Xl[0]:6.3f} and Fx is {Fx/1e3:10.0f} kN so error is {(Fx+self.fx_target)/1e3:8.0f} kN") + + return np.array([Fx - self.fx_target]), dict(status=1), False + + + def func_kx_L(self, Xl, args): # evaluate how close the system horizontal stiffness is compared to the kx_target + + for ss in self.ms.lineList: # go through each Subsystem + ss.lineList[self.iL].setL(Xl[0]) # update the section length + + # option to setOffset? + self.ms.bodyList[0].setPosition([0, 0,0,0,0,0]) # apply offset + self.ms.solveEquilibrium() + Kx = self.ms.getCoupledStiffness()[0,0] # mooring system stiffness in x + + if 'display' in args: + if args['display'] > 1: + print(f" Xl is {Xl[0]:6.3f} and Kx is {Kx/1e3:10.0f} kN/m so error is {(Kx+self.kx_target)/1e3:8.0f} kN/m") + + return np.array([Kx - self.kx_target]), dict(status=1), False + + + + def func_fx_x(self, X, args): + + self.ms.bodyList[0].setPosition([X[0], 0,0,0,0,0]) # apply offset + self.ms.solveEquilibrium() + FxMoorings = self.ms.bodyList[0].getForces()[0] # net mooring force in x + FxApplied = args['FxApplied'] + + return np.array([FxApplied + FxMoorings]), dict(status=1), False + + + + def step_fx_x(self, X, args, Y, oths, Ytarget, err, tols, iter, maxIter): + ''' this now assumes tols passed in is a vector''' + + FxMoorings = self.ms.bodyList[0].getForces()[0] # net mooring force in x + FxApplied = args['FxApplied'] + + dY = FxApplied + FxMoorings + + Kx = self.ms.bodyList[0].getStiffnessA(lines_only=True)[0,0] + + if Kx > 0: + dX = dY/Kx + + else: # backup case, just move 10 m + + dX = np.sign(dY)*10 + + return np.array([dX]) + + + + + def setAnchoringRadius(self, a): + '''Sets the anchoring radius, including of any LineDesign MoorPy + System. Input is the anchoring radius from platform centerline [m]. + ''' + + if a < 0: + raise Exception("The value passed to setAnchoringRadius must be positive.") + + self.rad_anch = float(a) + + self.dd['span'] = self.rad_anch - self.rBFair[0] + self.ss.span = float(self.dd['span']) + + self.ss.setEndPosition([-self.rad_anch, 0, -self.depth], endB=False) + + # Now handle the MoorPy system, if there is one, moving the anchor points + if self.ms: + for i, heading in enumerate(self.headings): + rotMat = rotationMatrix(0, 0, np.radians(heading)) + self.ms.pointList[2*i].setPosition(np.matmul(rotMat, [self.rad_anch, 0, -self.depth])) + + # set subsystem span if needed... <<< + self.ms.lineList[i].span = float(self.dd['span']) + + def setSectionLength(self, L, i): + '''Sets the length of a section, including in the MoorPy System if there + is one. Overrides Mooring.setSectionLength''' + + # First call the Mooring version of this method, which handles the subsystem + Mooring.setSectionLength(self, L, i) + + # Now handle the MoorPy system, if there is one + if self.ms: + for ss in self.ms.lineList: + ss.lineList[i].setL(L) + + + # ::::::::::::::::::::::::::::::: constraint functions ::::::::::::::::::::::::::::::: + + # Each should return a scalar C where C >= 0 is valid and C < 0 is violated. + + def con_Kx(self, X, index, value, display=0): + '''This ensures Kx, the effective horizontal stiffness, is greater than a given value. + Note: this constraint doesn't use the index input.''' + + Kx = self.ss.KB_L[0,0] # get effective horizontal stiffness at current/undisplaced position + c = Kx - value + + return c + + + def con_total_length(self, X, index, value): + '''This ensures that the total length of the Mooring does not result in a fully slack Mooring + (ProfileType=4) in its negative extreme mean position''' + # ['max_line_length', index, value] # index and value are completely arbitrary right now + + Lmax = (self.span-self.ss.rBFair[0]-self.x_mean_out + self.depth+self.rBFair[2]) # (3-14-23) this method might now be deprecated with more recent updates to ensure the combined line lengths aren't too large + + total_linelength = sum([self.ss.lineList[i].L for i in range(self.nLines)]) + c = Lmax-total_linelength + + return c + + # ----- offset constraints ----- + + def getOffset(self, FxApplied, headings=[]): + '''Computes the horizontal offset of the body in response to an + applied horizontal force, considering all mooring lines, by solving + for offset at which mooring reaction force equals FxApplied.''' + + # Ensure everything is switched back to status stiffnesses + self.ms.revertToStaticStiffness() + + # Solve for the surge offset that matches the applied force + ''' + x, y, info = dsolve2(self.func_fx_x, [0], step_func=self.step_fx_x, + args=dict(FxApplied=FxApplied, + heading=headings), tol=[0.01], Xmin=[-1e5], + Xmax=[1e5], dX_last=[10], stepfac=4, display=0) + + return x[0] + ''' + self.ms.bodyList[0].f6Ext = np.array([FxApplied, 0,0, 0,0,0]) + try: + self.ms.solveEquilibrium(DOFtype='both') + return self.ms.bodyList[0].r6[0] + #offset = self.ms.bodyList[0].r6[0] + #self.ms.bodyList[0].f6Ext = [0,0,0,0,0,0] + #self.ms.bodyList[0].setPosition([0,0,0,0,0,0]) + #self.ms.solveEquilibrium() + #return offset + except: + return 1e3 + + + def con_offset0(self, X, index, value): + '''This ensures that the system does not offset by a certain amount in its unloaded position''' + + # placeholder, this method may not make sense as-is + return value - self.getOffset(0) + + + def con_offset(self, X, index, value): + '''This ensures that the system does not offset by a certain amount in its fully loaded position''' + + return value - abs(self.x_mean_eval) + + # ----- lay length constraints ----- + + def con_lay_length(self, X, index, threshold, display=0): + '''This ensures there is a minimum amount of line on the seabed at the +extreme displaced position.''' + return self.ss.getLayLength(iLine=index) - threshold + self.ss.LayLen_adj + + def con_max_td_range(self, X, index, threshold, display=0): + '''Ensures the range of motion of the touchdown point betweeen the + range of offsets is less then a certain distance. + This constraint is for the system as a whole (index is ignored) and + must have offset='other' so that it's evaluated at the end.''' + return threshold - (self.max_lay_length - self.min_lay_length) + + + # ----- rope contact constraints ----- + + def con_rope_contact(self, X, index, threshold, display=0): + '''Ensures the first line node doesn't touch the seabed by some + minimum clearance.''' + + return self.ss.getPointHeight(index) - threshold # compute the constraint value + + + # ----- strength constraints ----- + + def con_strength(self, X, index, threshold, display=0): + '''This ensures the MBL of the line is always greater than the maximum + tension the line feels times a safety factor.''' + return self.ss.getTenSF(index) - threshold + + def con_min_tension(self, X, index, threshold, display = 0): + '''Ensures that the minimum line tension is above a threshold''' + return self.ss.getMinTen(index) - threshold + + def con_curvature(self, X, index, threshold, display=0): + '''Ensure that the MBR of the cable is always greater than the maximum + actual curvature times a safety factor.''' + return self.ss.getCurvSF(index) - threshold + + + def getDamage(self, index, display=0): + ''' method to predict fatigue damage based on previous iteration''' + + damage = self.damage + + if sum(damage) == 0: + raise ValueError("Fatigue damage from previous iteration was not provided") + + + sumdamage = 0 + + + #fatigue_headings are loading direction for fatigue dynamic factor calculation. must match order of damage in self.damage + for i, ang in enumerate(self.fatigue_headings): + + #apply fx_target at direction in fatigue_headings + self.ms.bodyList[0].f6Ext = np.array([self.fx_target*np.cos(np.radians(ang)), self.fx_target*np.sin(np.radians(ang)),0, 0,0,0]) + self.ms.solveEquilibrium(DOFtype='both') + + #store offset + offsetx = self.ms.bodyList[0].r6[0] + offsety = self.ms.bodyList[0].r6[1] + + #tension 1 + Ten1 = max(np.linalg.norm(self.ms.lineList[self.ms_fatigue_index].lineList[index].fA),np.linalg.norm(self.ms.lineList[self.ms_fatigue_index].lineList[index].fB)) + + #set force back to zero + self.ms.bodyList[0].f6Ext = [0,0,0,0,0,0] + + #add dx to previous offset to get dtdx (slope of tension-displacement curve) + dx = 0.5 + self.ms.bodyList[0].setPosition(np.array([offsetx + dx*np.cos(np.radians(ang)),offsety+dx*np.sin(np.radians(ang)),0,0,0,0])) # move the body by the change in distance + self.ms.solveEquilibrium() + + #tension 1 + Ten2 = max(np.linalg.norm(self.ms.lineList[self.ms_fatigue_index].lineList[index].fA),np.linalg.norm(self.ms.lineList[self.ms_fatigue_index].lineList[index].fB)) + + #slope of tension-displacement curve at fx_target applied at ang + dTdx = (Ten2 - Ten1)/dx + + #ratio is based on fatigue damage equation (Tension/MBL)^m, where m = 3 for chain + MBL_corroded = self.ms.lineList[self.ms_fatigue_index].lineList[index].type['MBL'] * ( (self.ms.lineList[self.ms_fatigue_index].lineList[index].type['d_nom'] - (self.corrosion_mm/1000)) / self.ms.lineList[self.ms_fatigue_index].lineList[index].type['d_nom'] )**2 + ratio = (dTdx/ MBL_corroded)**3 + + #ratio is multipled by the inputted previous iteration damage*MBL1/dTdx1 + sumdamage = sumdamage + ratio * damage[i] + + + return sumdamage + + + def con_damage(self, X, index, threshold, display=0): + '''constraint method to ensure the scaled fatigue damage meets required fatigue damage''' + + return threshold - self.getDamage(index, display=display) + + + def getYawStiffness(self, x_offset, display=0): + '''method to calculate the yaw stiffness of the whole mooring system using an analytical equation''' + + yawstiff = 0 + # calculate stiffness in different situations + for i, ang in enumerate(self.headings): + spacing_x = self.span*np.cos(np.radians(ang)) - x_offset # x distance from offset fairlead to anchor point + spacing_y = self.span*np.sin(np.radians(ang)) # y distance from offset fairlead to anchor point + spacing_xy= np.linalg.norm([spacing_x, spacing_y]) # radial distance from offset fairlead to anchor point + self.setPosition(spacing_xy-self.span) + tau0 = self.ss.fB_L[0] # calculate the horizontal tension on the body from the 1 line + + # analytic equation for yaw stiffness for each mooring line heading + yawstiff += (-tau0/spacing_xy)*self.ss.rBFair[0]**2 + -tau0*self.ss.rBFair[0] + + self.ss.setOffset(0) # restore to zero offset and static EA + + return yawstiff + + + def con_yaw_stiffness0(self, X, value, display=0): + '''constraint method to ensure the yaw stiffness of the mooring system represented by this line design meets a certain yaw stiffness requirement, + quasi-statically, and in the undisplaced position''' + + c = self.getYawStiffness(x_offset=0, display=display) - value # compute the constraint value + + return c + + def con_yaw_stiffness(self, X, index, value, display=0): + '''constraint method to ensure the yaw stiffness of the mooring system represented by this line design meets a certain yaw stiffness requirement, + quasi-statically, and in the extreme displaced position''' + + try: + bodyPosition = np.array([-self.x_mean_in-self.x_ampl, 0,0,0,0,0]) + c = self.getYawStiffness(x_offset=bodyPosition[0], display=display) - value # compute the constraint value + + except Exception as e: + if self.noFail: + c = -60000 + else: + raise(e) + + return c + + + # ----- shared line sag constraints ----- + + def con_min_sag(self, X, index, threshold, display=0): + '''Ensure the lowest point of a line section is below + a minimum depth.''' + return threshold - self.ss.getSag(index) + + def con_max_sag(self, X, index, threshold, display=0): + '''Ensures the lowest point of a line section is above + a certain maximum depth.''' + return self.ss.getSag(index) - threshold + + + + # ----- utility functions ----- + + def plotProfile(self, Xuvec=[1,0,0], Yuvec=[0,0,1], ax=None, color=None, title="", slack=False, displaced=True, figsize=(6,4), label=None): + '''Plot the mooring profile in undisplaced and extreme displaced positions + + Parameters + ---------- + Xuvec : list, optional + plane at which the x-axis is desired. The default is [1,0,0]. + Yuvec : lsit, optional + plane at which the y-axis is desired. The default is [0,0,1]. + ax : axes, optional + Plot on an existing set of axes + color : string, optional + Some way to control the color of the plot ... TBD <<< + title : string, optional + A title of the plot. The default is "". + slack : bool, optional + If false, equal axis aspect ratios are not enforced to allow compatibility in subplots with axis constraints. + dispalced : bool, optional + If true (default), displaced line profiles are also plotted. + + Returns + ------- + fig : figure object + To hold the axes of the plot + ax: axis object + To hold the points and drawing of the plot + + ''' + + # if axes not passed in, make a new figure + if ax == None: + fig, ax = plt.subplots(1,1, figsize=figsize) + ax.set_xlabel('Horizontal distance (m)') + ax.set_ylabel('Depth (m)') + else: + fig = plt.gcf() # will this work like this? <<< + + + if displaced: + offsets = [0, self.x_mean_out+self.x_ampl, -self.x_mean_in-self.x_ampl] + else: + offsets = [0] + + for x in offsets: + + alph = 1 if x==0 else 0.5 # make semi-transparent for offset profiles + + self.ss.setOffset(x) + + #ax.plot(self.rB[0], self.rB[2],'ko',markersize = 2) # fairlead location + ax.plot(x, 0,'ko',markersize = 2) # platform ref point location + # self.ss.drawLine2d(0,ax) + for i, line in enumerate(self.ss.lineList): + if i != 0: + label = None + if color==None: # alternate colors so the segments are visible + if line.type['material'][0]=='c': + line.drawLine2d(0, ax, color=[.1, 0, 0], alpha=alph, Xuvec=Xuvec, Yuvec=Yuvec, Xoff=-self.rad_fair, label=label) + if self.shared==1: # plot other half too if it's a shared line where only half is modeled <<< + line.drawLine2d(0, ax, color=[.1, 0, 0], alpha=alph, Xuvec=-np.array(Xuvec), Yuvec=Yuvec, Xoff=-self.span-self.rad_fair, label=label) + elif 'nylon' in line.type['material']: + line.drawLine2d(0, ax, color=[.8,.8,.2], alpha=alph, Xuvec=Xuvec, Yuvec=Yuvec,Xoff=-self.rad_fair, label=label) + else: + line.drawLine2d(0, ax, color=[.3,.5,.5], alpha=alph, Xuvec=Xuvec, Yuvec=Yuvec,Xoff=-self.rad_fair, label=label) + if self.shared==1: # plot other half too if it's a shared line where only half is modeled <<< + line.drawLine2d(0, ax, color=[.3,.5,.5], alpha=alph, Xuvec=-np.array(Xuvec), Yuvec=Yuvec, Xoff=-self.span-self.rad_fair, label=label) + else: + line.drawLine2d(0, ax, color=color, alpha=alph, Xuvec=Xuvec, Yuvec=Yuvec,Xoff=-self.rad_fair, label=label) + if self.shared==1: # plot other half too if it's a shared line where only half is modeled <<< + line.drawLine2d(0, ax, color=color, alpha=alph, Xuvec=-np.array(Xuvec), Yuvec=Yuvec, Xoff=-self.span-self.rad_fair, label=label) + + ''' + # plot points/weights/floats along the line >>> needs to be updated to account for Xuvec and Yuvec <<< + for point in self.pointList: + if point.number > 1 and point.number < self.nLines+1: + if point.v > 0: + ax.plot(point.r[0],point.r[2],'yo',markersize=5) + elif point.m > 0: + ax.plot(point.r[0],point.r[2],'ko',markersize=5) + else: + ax.plot(point.r[0],point.r[2],'bo',markersize=1) + ''' + + # make legend entries available + if displaced: + if not color==None: + ax.plot(np.nan, np.nan, color=color, alpha=1, label="undisplaced") + ax.plot(np.nan, np.nan, color=color, alpha=0.5, label="displaced") + + #ax.plot([self.ss.lineList[0].rA[0], 0], [-self.depth, -self.depth], color='k') + # only force equal aspect ratio if "slack" keyword isn't specified (so that sharex=True, sharey-True plots are possible) + if not slack: + ax.axis("equal") + + ax.set_title(title) + #ax.set_ylim(-1,1) + + self.ss.setOffset(0) # return to undisplaced position + self.ss.solveEquilibrium(tol=self.ss.eqtol) + + return fig, ax # return the figure and axis object in case it will be used later to update the plot + + + def plotCurves(self, ax=[], color="k", title=""): + '''Plot key performance curves for the mooring as a function of offset + + Parameters + ---------- + ax : axes, optional + Plot on an existing set of axes + title : string, optional + A title of the plot. The default is "". + + Returns + ------- + fig : figure object + To hold the axes of the plot + ax: axis object + To hold the points and drawing of the plot + + ''' + + # if axes not passed in, make a new figure + if len(ax) == 0: + fig, ax = plt.subplots(2,1, sharex=True) + newFig=True + else: + if not len(ax) == 2: + raise Exception("ax provided to plotCurves must be a list of 2 axes.") + fig = plt.gcf() + newFig = False + + x = np.linspace(-self.x_mean_in-self.x_ampl, self.x_mean_out+self.x_ampl, 50) + + Fx = np.zeros(len(x)) + Ts = np.zeros([len(x), len(self.ss.lineList)]) + + # calculate values at each offset point + for i in range(len(x)): # go through each offset point + + self.ss.setOffset(x[i]) # offset the desired amount + + Fx[i] = self.ss.fB_L[0] # get horizontal mooring force + + for j in range(len(self.ss.lineList)): # get upper end tension of each line segment + Ts[i,j] = self.ss.lineList[j].TB + + # plots + ax[0].plot(x, -Fx/1e3, c=color) + + for j in range(len(self.ss.lineList)): + ax[1].plot(x, Ts[:,j]/1e3, c=color, dashes=[5-0.5*j, 0.5*j], label=f"segment {j+1}") + + ax[0].set_ylabel("Fx (kN)") + ax[1].set_ylabel("Tension (kN)") + if newFig: ax[1].legend() + ax[1].set_xlabel("Offset (m)") + #fig.set_title(title) + + self.ss.setOffset(0) # restore to undisplaced position + + return fig, ax # return the figure and axis object in case it will be used later to update the plot + + + def dump(self): + '''Puts info about the mooring into a dictionary and returns it.''' + + self.objectiveFun([]) # ensure things are calculated + + info = dict(arrangement={}, design={}, performance={}, cost={}) # the dictionary and its top-level entries + + info['arrangement']['name'] = self.name + ''' + info['design']['X' ] = self.Xlast # design variables + info['design']['Gdict' ] = self.evaluateConstraints([])[1] # dict of constraint names and values of evaluated constraint functions + info['design']['Ls' ] = [line.L for line in self.ss.lineList] # length of each segment + info['design']['Ds' ] = [line.type['input_d'] for line in self.ss.lineList] # *input* diameter of each segment + info['design']['lineTypes' ] = [line.type['name'] for line in self.ss.lineList] # line type of each segment (may be redundant with what's in arrangement) + info['design']['anchorType'] = self.anchorType # (may be redundant with what's in arrangement) + info['design']['span' ] = self.span # platform-platform of platfom-anchor horizontal span just in case it's changed + info['design']['Ltot' ] = sum([line.L for line in self.ss.lineList]) # total mooring length + + info['performance']['Fx'] = self.fB_L[0] + info['performance']['Kx'] = self.bodyList[0].getStiffness(tol=self.eqtol)[0,0] + + info['cost']['total' ] = self.cost + info['cost']['line' ] = self.lineCost + if not self.shared: + info['cost']['anchor' ] = self.anchorMatCost + info['cost']['install'] = self.anchorInstCost # eventually should sort out if this represents the total installation cost + info['cost']['decom' ] = self.anchorDecomCost + ''' + + # this version converts out of numpy format for yaml export (should make a better system for this) + info['design']['X' ] = self.Xlast.tolist() # design variables + #info['design']['Gdict' ] = self.evaluateConstraints([])[1] # dict of constraint names and values of evaluated constraint functions + info['design']['Ls' ] = [float(line.L ) for line in self.ss.lineList] # length of each segment + info['design']['Ds' ] = [float(line.type['input_d']) for line in self.ss.lineList] # *input* diameter of each segment + info['design']['lineTypes' ] = [str(line.type['material']) for line in self.ss.lineList] # line type of each segment (may be redundant with what's in arrangement) + info['design']['anchorType'] = self.anchorType # (may be redundant with what's in arrangement) + info['design']['span' ] = float(self.span) # platform-platform of platfom-anchor horizontal span just in case it's changed + info['design']['Ltot' ] = float(sum([line.L for line in self.ss.lineList])) # total mooring length + + info['performance']['Fx'] = float(self.fB_L[0] ) + info['performance']['Kx'] = float(self.KB_L[0,0]) + + info['cost']['total' ] = float(self.cost ) + info['cost']['line' ] = float(self.lineCost) + if not self.shared==1: + info['cost']['anchor' ] = float(self.anchorMatCost ) + info['cost']['install'] = float(self.anchorInstCost ) # eventually should sort out if this represents the total installation cost + info['cost']['decom' ] = float(self.anchorDecomCost) + + + return info + + + def adjustConstraint(self, key, value): + '''Modifies the value of an existing constraint''' + for con in self.constraints: + if con[0] == key: + con[2] = value + + @staticmethod + def getClumpMV(weight, rho=1025.0, g=9.81, **kwargs): + + '''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) + + + +if __name__ == '__main__': + + # Example case from Stein + ''' + settings = {} + settings['rBFair'] = [58,0,-14] + settings['x_ampl'] = 10 # xmax value is designed to be the "target" offset, used for solve_for = 'tension' + settings['fx_target'] = 1.95e6 + settings['solve_for'] = 'none' + settings['headings'] = [60, 180, 300] + + settings['name'] = 'chain-poly-chain' + settings['lineTypeNames'] = ['chain','polyester','chain'] + settings['anchorType'] = 'suction' + settings['allVars'] = [1000/10, 100, 120, 0, 800, 200, 0, 100, 120] + settings['Xindices'] = ['c', 0, 'c', 'c', 1, 2, 'c', 'c', 'c'] + settings['Xmin'] = [10, 10, 10] + settings['Xmax'] = [500, 10000, 500] + settings['dX_last'] = [10, 10, 10] + + settings['constraints'] = [dict(name='rope_contact' , index=1, threshold=5 , offset='min'), + dict(name='max_offset' , index=0, threshold=60, offset='max')] + + for j in range(len(settings['lineTypeNames'])): + settings['constraints'].append(dict(name='tension_safety_factor', index=j, threshold=2.0, offset='max')) + + depth = 766.765 + ld = LineDesign(depth, **settings) + + ld.setNormalization() # turn on normalization (important for COBYLA etc) + + start_time = time.time() + #X, min_cost = ld.optimize(maxIter=12, plot=False, display=3, stepfac=4, method='dopt') + X, min_cost = ld.optimize(maxIter=10, plot=True, display=3, stepfac=4, method='COBYLA') + print("optimize time: {:8.2f} seconds".format((time.time() - start_time))) + ld.objectiveFun(X, display=2) + ld.evaluateConstraints(X, display=0) + ld.updateDesign(X, display=0) + ld.plotProfile() + plt.show() + ''' + + + depth = 200 + + settings = {} + settings['rBFair'] = [58,0,-14] + settings['x_ampl'] = 10 + settings['fx_target'] = 1.95e6 + settings['headings'] = [60, 180, 300] + + settings['solve_for'] = 'none' + #settings['solve_for'] = 'ghost' + + settings['name'] = 'DEA-chain-poly' # <<< semitaut option + settings['lineTypeNames'] = ['chain','polyester'] + settings['anchorType'] = 'drag-embedment' + ''' + settings['allVars'] = [800/10, 400, 120, 0, 400, 200,] + settings['Xindices'] = ['c', 0, 1, 'c', 2, 3] + settings['Xmin'] = [10, 10, 10, 10] + settings['Xmax'] = [10000, 500, 800, 500] + settings['dX_last'] = [10, 10, 10, 10] + ''' + settings['allVars'] = [1000/10, 800, 120, 0, 80, 200,] + settings['Xindices'] = ['c', 0, 1, 'c', 'c', 2] + settings['Xmin'] = [400, 10, 10] + settings['Xmax'] = [2000, 500, 500] + settings['dX_last'] = [10, 10, 10] + + ''' + settings['name'] = 'DEA-chain' # <<< catenary option + settings['lineTypeNames'] = ['chain'] + settings['anchorType'] = 'drag-embedment' + settings['lay_target'] = 200 + settings['allVars'] = [1000/10, 1000, 120] + settings['Xindices'] = ['c', 0, 1] + settings['Xmin'] = [500, 50] + settings['Xmax'] = [1500, 300] + settings['dX_last'] = [10, 10] + + + #settings['solve_for'] = 'offset' + settings['solve_for'] = 'tension' + settings['Xindices'] = ['c', 's', 0] + settings['Xmin'] = [10] + settings['Xmax'] = [500] + settings['dX_last'] = [10] + settings['x_target'] = 34.560922734165835 + settings['x_mean_out'] = 34.560922734165835 + settings['x_mean_in'] = 60 + ''' + settings['constraints'] = [dict(name='min_lay_length', index=0, threshold=20, offset='max'), + dict(name='max_offset' , index=0, threshold=25, offset='max'), + dict(name='rope_contact' , index=1, threshold=5 , offset='min')] + + for j in range(len(settings['lineTypeNames'])): + settings['constraints'].append(dict(name='tension_safety_factor', index=j, threshold=2.0, offset='max')) + + + + + ld = LineDesign(depth, **settings) + + ld.setNormalization() # turn on normalization (important for COBYLA etc) + + start_time = time.time() + #X, min_cost = ld.optimize(maxIter=20, plot=False, display=3, stepfac=4, method='dopt') + #X, min_cost = ld.optimize(maxIter=40, plot=True, display=3, stepfac=4, method='COBYLA') + #X, min_cost = ld.optimize(maxIter=40, plot=True, display=3, stepfac=4, method='CMNGA') + #X, min_cost = ld.optimize(maxIter=40, plot=True, display=3, stepfac=4, method='PSO') + X, min_cost = ld.optimize(maxIter=40, plot=True, display=0, stepfac=4, method='bayesian') + + print('') + print('Analyzing Results:') + print( " optimize time: {:8.2f} seconds".format((time.time() - start_time))) + print( ' design variables (normalized): ', [f"{x:8.3f}" for x in X]) + print( ' design variables (denormalized): ', [f"{x:8.2f}" for x in X*ld.X_denorm]) + print(f' solved line length: {ld.ss.lineList[ld.iL].L:8.2f} m') + print('') + + ld.objectiveFun(X, display=2) + ld.evaluateConstraints(X, display=2) + ld.updateDesign(X, display=0) + ld.plotProfile() + plt.show() + + a = 2 diff --git a/famodel/design/LinearSystem.py b/famodel/design/LinearSystem.py new file mode 100644 index 00000000..e150eb90 --- /dev/null +++ b/famodel/design/LinearSystem.py @@ -0,0 +1,2083 @@ +import moorpy as mp # type: ignore +import fadesign.MoorSolve as msolve +import numpy as np +import scipy +import matplotlib as mpl +import matplotlib.pyplot as plt +import fadesign.conceptual.graph_helpers as gh +# Old shared moorings linear modeling code from 2021 / updated in 2024 by Rudy Alkarem + +def getUnitAndLength( rA, rB ): + + dr = rB-rA + l = np.linalg.norm(dr) + u = dr/l + + return u, l + + +class LinearSystem(): + '''2D array representation with linear mooring properties for optimization. + + + some conventions/flags: + - shared (formerly profile) + - net: False for normal/shared line; True for >2 interconnected lines + + + ''' + + + def __init__(self, coords, intraMat, nPtfm, interMats=[], + interCoords=None, inits=None, profileMap=None, intersectZ=None, + rFair=0, zFair=0, depth=600., fmax=1e6, xmax=40.0, plots=0, nonlin=1.0, + center=True, old_mode=True): + '''Formerly makeSimpleSystem in Array.py, this sets up a LinearSystem + from a coordinates list and adjacency matrix. + + Parameters + ---------- + coords : 2D list + intra-cell coordinates + intraMat : 2D array + Adjacency matrix for intra-cell lines (within)... + nPtfm : int + Number of intra-cell platforms or turbines + interMats : list of 2D arrays + Adjacency matrix for inter-cell lines (between)... + interCoords : list of 2D lists + inter-cell spacing (coordinates of center of neighboring cell w.r.t. the center of the unit cell) + inits: dictionary + initial tension and stiffness values and hybrid line-related characteristics. + In the "old_mode", this requires mooring groups to be provided with the following: + - tenA # [N] Initial tension(Anchored) + - tenS # [N] Initial tension(Shared) + - klA # [N/m] Initial stiffness (Anchored) + - klS # [N/m] Initial stiffness (Shared) + - tenTen # [N] Initial tendon tension (Hybrid) + - w + In the newer more general mode, this requires mooring groups to be provided with: + - kl + - kt + - + profileMap: 2D list + allows the user to manually define the profiles of each mooring group (0: anchored, 1: shared, 2: hybrid) + intersectZ: 2D list + depth of midpoint for all anchors (if the z value is not zero, it is a hybrid system) + rFair : float + Radius of fairleads from platform centerline (just a uniform value for now..) + zFair : float + Z elevation of fairleads (just a uniform value for now..) + depth : float, optional + Depth of the water in the system. The default is 600 + nonlin : float + A multiplier to compensate for nonlinearitize in a real mooring system. 1: linear, >1: add some margin from the watch circles + center : bool + If true, the system will be shifted so that its center is at 0,0 + old_mode : bool + If true (default) functions the old way with assumed weight-tension + -stiffness relations. If False, uses a more generic approach. + ''' + + self.old_mode = bool(old_mode) + + # key array layout info + self.coords = np.array(coords) + self.intraMat = np.array(intraMat) + self.interMats = np.array(interMats) + self.nPtfm = nPtfm + if np.any(intersectZ==None): + self.intersectZ = np.zeros(len(coords)) + else: + self.intersectZ = intersectZ + self.interCoords = interCoords + if inits: + self.inits = inits + + self.profileMap = profileMap + # lists of objects to be created + #self.bodyList = [] # one for each FOWT + #self.pointList = [] # for each anchor and also each attachment point (on the FOWT bodies) + self.mooringGroups = [] # a list of dictionaries for the linear properties of each mooring group in the array + + # arrays for each actual mooring line (not groups), i.e. each anchor or shared line + self.u = [] # list of line unit vectors + self.l = [] # list of line horiztonal spacings + self.group = [] # line group index + self.endA = [] # id of what the line is connected to (this corresponds to the row or column index in the adjacency matrix) + self.endB = [] + self.rA = [] # coordinates of the connected lines at end A + self.rB = [] # coordinates of the connected lines at end B + self.angA = [] # offset angles (deg about yaw axis) of fairlead attachment on platform [deg] + self.angB = [] + self.boundary = [] # a boolean to check if the line is a boundary (inter-cell) line + # parameters + self.depth = depth + self.fmax = fmax + + self.xmax = xmax # max offset (watch circle radius) constraint + self.nonlin = nonlin + self.angles = np.arange(0,360, 15) # wind angles to consider + + self.anchor_cost_factor = 1.0 # this factor is used to scale the cost of anchor lines (whereas shared line costs aren't scaled). Can be adjusted externally. + + + # shift the coordinates of all the bodies and anchors so that the center of the plot is ideally at (0,0) + if center: + cx = np.mean([self.coords[i,0] for i in range(len(self.coords))]) + cy = np.mean([self.coords[i,1] for i in range(len(self.coords))]) + #mss.transform(trans=[-cx, -cy]) + self.coords = self.coords - [cx,cy] + + self.center = [np.mean([self.coords[i,0] for i in range(len(self.coords))]), + np.mean([self.coords[i,1] for i in range(len(self.coords))])] + + # also add some generic line types (which map to LineDesigns), + # one for each line grouping as defined in the entries of intraMat + maxGroupNumber = np.max(intraMat) + netGroup = 0 + if interMats: + for interMat in interMats: + maxGroupNumber = np.max([maxGroupNumber, np.max(interMat)]) + + if self.old_mode: # make mooring groups using the old approach (compatible with conceptDesign) + + for i in range(maxGroupNumber): # loop up to maximum number in adjacency matrix (one for each line grouping) + #if profileMap: # MH: not needed <<< + # shared = self.profileMap[i] + #else: + if interMats: + shared = i+1 in intraMat[:nPtfm, :nPtfm] or np.any([i+1 in interMat for interMat in interMats]) + else: + shared = i+1 in intraMat[:nPtfm, :nPtfm] + + if inits: + + if not np.all(self.intersectZ==0) and i+1 in intraMat[:nPtfm, nPtfm:] and shared==1: + percent_droop = 100 - self.intersectZ[np.where(i+1==intraMat[:nPtfm, :])[1][0]]/self.depth * 100 # Check the depth of that anchor point to determine the percent_droop, if any + intersectDeg = np.sum(intraMat[self.intersectZ > 0, :] > 0, axis=1)[netGroup] + net = True + tendON = True + netGroup += 1 + if percent_droop <= 20: # if the line has low drop, a tendon is too expensive + tendON = False + else: + net = False + percent_droop = 50 + + if shared==0: # anchored + self.mooringGroups.append(dict(type=i+1, ten=self.inits['tenA'], kl=self.inits['klA'], w=self.inits['w'], shared=shared, net=False, cost=1)) + elif shared==1: # shared or hybrid (net) + if net: + self.mooringGroups.append(dict(type=i+1, ten=self.inits['tenS'], kl=self.inits['klS'], w=self.inits['w'], shared=shared, percent_droop=percent_droop, net=net, tendON=tendON, tenTen=self.inits['tenTen'], intersectDeg=intersectDeg, cost=1)) + else: + self.mooringGroups.append(dict(type=i+1, ten=self.inits['tenS'], kl=self.inits['klS'], w=self.inits['w'], shared=shared, percent_droop=percent_droop, net=net, cost=1)) + else: + self.mooringGroups.append(dict(type=i+1, kl=100, ten=1000, w=1500, tenTen=1000, shared=shared, net=False, cost=1)) + + else: # new more general/simple approach for mooring groups + # note: inits must be provided as an input in this case (defining each mooring group) + + # figure out shared from place in intraMat and interMats... + groupNumbers = [] + profileMap = [] # says whether each group is anchored or shared line + n = self.intraMat.shape[0] + for i in range(n): + for j in range(n): + a = self.intraMat[i,j] + # if the entry is nonzero and not already stored, add it + if a > 0: + if not a in groupNumbers: + groupNumbers.append(a) + if i < self.nPtfm and j < self.nPtfm: + profileMap.append(1) # flag as shared line + shared = 1 + elif i > self.nPtfm and j > self.nPtfm: + raise Exception("This appears to be an anchor-anchor mooring!") + else: + profileMap.append(0) # flag as anchored line + shared = 0 + + # set up the mooring Group + self.mooringGroups.append(dict(type=a, shared=shared, net=False, cost=1)) + self.mooringGroups[-1].update(self.inits[a-1]) # add in input info on the mooring group + + + # now go through interMat (intercell matrices) and do similarly + for interMat in self.interMats: + n = interMat.shape[0] + for i in range(n): + for j in range(n): + a = interMat[i,j] + # if the entry is nonzero and not already stored, add it + if a > 0: + if not a in groupNumbers: + groupNumbers.append(a) + if i < self.nPtfm and j < self.nPtfm: + profileMap.append(1) # flag as shared line + shared = 1 + elif i > self.nPtfm and j > self.nPtfm: + raise Exception("This appears to be an anchor-anchor mooring!") + else: + profileMap.append(0) # flag as anchored line + shared = 0 + + # set up the mooring Group + self.mooringGroups.append(dict(type=a, shared=shared, net=False, cost=1)) + self.mooringGroups[-1].update(self.inits[a-1]) # add in input info on the mooring group + + # not sure what to do about nets in the above!! + + + # make lines using adjacency matrix (and add corresponding points if they're on a platform) + #linenum = 1 + for iA in range(len(coords)): + for iB in range(iA): + k = int(intraMat[iA,iB]) # the number (if positive) indicates the lineDesign type or grouping + if k > 0: + dr = self.coords[iB,:] - self.coords[iA,:] + l = np.linalg.norm(dr) + self.l.append(l) # store length <<<<<<<< need to subtract fairlead radii... ? + self.u.append(np.round(dr/l, 2)) # store unit vector + self.group.append(k) # store lineDesign type (starts at 1, need to subtract 1 for index) + + self.endA.append(iA) # end A attachment object index + self.endB.append(iB) + self.rA.append(self.coords[iA, :]) + self.rB.append(self.coords[iB, :]) + self.angA.append(0.0) # to be handled later <<<<<<< + self.angB.append(0.0) + self.boundary.append(False) + # fill in ratios in the corresponding line design for convenience - eventually need to check/enforce consistency <<<< + mg = self.mooringGroups[k-1] + shared = mg['shared'] + + if self.old_mode: + mg['ten__w'] = mg['ten']/mg['w'] + mg['kl__w' ] = mg['kl' ]/mg['w'] + mg['kt__kl'] = mg['ten']/l/mg['kl'] # kt/ k = ten/l/k + else: + pass # <<< nothing needed here?? + + mg['l'] = l # store a copy so it's acccessible in the mooringGroups list + mg['dl_max'] = xmax # maximum extension of this mooring group (initial value equal to xmax) + mg['dl_min'] = -xmax # maximum compression of this mooring group (must be negative, initial value equal to -xmax) + + # Add inter-cell lines: + if interMats: + for interMat, interCoord in zip(interMats, interCoords): + interMat = np.array(interMat) + interCoord = np.array(interCoord) + for iA in range(self.nPtfm): + for iB in range(iA): + if iA < self.nPtfm and iB < self.nPtfm: + b = int(interMat[iA, iB]) + if b > 0: + intercenter = self.center + interCoord + rotMat = np.array([[np.cos(np.pi), -np.sin(np.pi)], + [np.sin(np.pi), np.cos(np.pi)]]) + intercenterp = np.matmul(rotMat, intercenter) + intercoordsiB = intercenter + (self.coords[iB, :] - self.center) + intercoordsiA = intercenter + (self.coords[iA, :] - self.center) + intercoordsiBp = intercenterp + (self.coords[iB, :] - self.center) + intercoordsiAp = intercenterp + (self.coords[iA, :] - self.center) + # compute both distances and choose the smallest one + drBA = intercoordsiB - self.coords[iA,:] + lBA = np.linalg.norm(drBA) + drBAp = intercoordsiBp - self.coords[iA,:] + lBAp = np.linalg.norm(drBAp) + if lBAp > lBA: + dr = drBA + # not that it matters but for consistency, choose the right A and B: + self.endA.append(iA) + self.endB.append(iB) + self.rA.append(self.coords[iA, :]) + self.rB.append(intercoordsiB) + + self.endA.append(iB) + self.endB.append(iA) + self.rA.append(self.coords[iB, :]) + self.rB.append(intercoordsiAp) + else: + dr = drBAp + self.endA.append(iA) + self.endB.append(iB) + self.rA.append(self.coords[iA, :]) + self.rB.append(intercoordsiBp) + + self.endA.append(iB) + self.endB.append(iA) + self.rA.append(self.coords[iB, :]) + self.rB.append(intercoordsiA) + l = np.linalg.norm(dr) + self.l.append(l) + self.l.append(l) + self.u.append(dr/l) + self.u.append(-dr/l) + self.group.append(b) + self.group.append(b) + self.angA.append(0.0) + self.angB.append(0.0) + self.angA.append(0.0) + self.angB.append(0.0) + self.boundary.append(True) + self.boundary.append(True) + # not sure about this part: + mg = self.mooringGroups[b-1] + shared = mg['shared'] + if self.old_mode: + mg['ten__w'] = mg['ten']/mg['w'] + mg['kl__w' ] = mg['kl' ]/mg['w'] + mg['kt__kl'] = mg['ten']/l/mg['kl'] # kt/ k = ten/l/k + else: + pass # <<< nothing needed here? + mg['l'] = l + mg['dl_max'] = xmax + mg['dl`_min'] = -xmax + + + print("end of intermat setup?") + + self.nLines = len(self.l) + + # should make some error checks for consistent properties (spacing, shared) in mooring groups <<< + + # now also make the structure matrix + self.StructureMatrix = np.zeros([2*self.nPtfm, self.nLines]) # rows: DOFs; columns: lines + + for j in range(self.nLines): + if self.endA[j] < self.nPtfm: # only if not an anchor + self.StructureMatrix[self.endA[j]*2 , j] = self.u[j][0] + self.StructureMatrix[self.endA[j]*2 + 1, j] = self.u[j][1] + + if self.endB[j] < self.nPtfm: # only if not an anchor + self.StructureMatrix[self.endB[j]*2 , j] = -self.u[j][0] + self.StructureMatrix[self.endB[j]*2 + 1, j] = -self.u[j][1] + + + # remember, you may want to call calibrate (or similar) to set up better + # values for ten__w, kl__w, and kt__kl for each mooring object assuming a continuous catenary line + + + def preprocess(self, plots=0, display=0): + '''Initializes things... + + Does all the things that can be done once the lineDesign characteristics are set (f/l/k and f/w) + + the mooring system objects to their initial positions if applicable? + + contents of former getTensionMatrix and getKnobs are now here''' + + # ensure the points (and line ends) are in the right positions + #for point in self.pointList: + # point.setPosition(Point.r) + + # update line properties so that initial values are in place + #self.calibrate_kt_over_k(plots=plots) + + # fill in the initial line stiffnesses and generate the system stiffness matrix so it's in place + #self.updateStiffnesses(np.ones(self.nLines)) + + # draw initial mooring system if desired + #if plots==1: + # self.plot(title="Mooring system at initialization") + + + #def getTensionMatrix(self): + '''Tension matrix defines the contribution of each line group's weight to each line's tension + + Essentially it is just a mapping from each line group's weight to each individual line's tension. + There is only one nonzero entry per row - i.e. each line's tension is just based on a single group's stiffness. + This seems simple and maybe doesn't need to be a matrix?? + ''' + + self.TensionMatrix = np.zeros([self.nLines, len(self.mooringGroups)]) + + for j in range(self.nLines): + + i = self.group[j]-1 + + if self.old_mode: + self.TensionMatrix[j, i] = self.mooringGroups[i]['ten__w'] + else: # NEW - USING TENSIONS DIRECTLY RATHER THAN WEIGHT RATIOS + self.TensionMatrix[j, i] = self.mooringGroups[i]['ten'] + + if self.boundary[j]: + self.TensionMatrix[j, i] *= 0.5 #MH: this seems suspect <<< + + + + #def getKnobs(self): + '''based on structure and tension matrices, calculatesd self.Knobs_k, which is used by c_to_k when optimizing stiffness.''' + + # Null space of Structure Matrix + N1 = scipy.linalg.null_space(self.StructureMatrix)#, rcond = 0.0001) + + # null space of N1 augmented with tension matrix + N2 = scipy.linalg.null_space(np.hstack([N1, -self.TensionMatrix])) #, rcond = 0.0001) + #N2 = scipy.linalg.null_space(np.append(N1, -self.TensionMatrix,1))#, rcond = 0.0001) + + # nullspace matrix containing basis vectors of valid line weight solutions for equilibrium given line groupings + # (we skip the top part of the vectors--the coefficients for feasible tension combinations--because tensions can be calculated directly from weights) + self.wBasis = N2[-len(self.mooringGroups):] + + # self.getSystemStiffness() + # print(self.SystemStiffness) + + # check to make sure there is at least on design variable + if np.prod(self.wBasis.shape)==0: + raise Exception("No knobs available") + + # normalize each weight basis vector and flip signs on any that are mostly negative + self.nBasis = self.wBasis.shape[1] # store the number of vectors + #print(self.nKnobs) + + for i in range(self.nBasis): + self.wBasis[:,i] = self.wBasis[:,i] / np.linalg.norm(self.wBasis[:,i]) * np.sign(np.sum(self.wBasis[:,i])) + #self.Knobs_k[:,i] = self.Knobs_k[:,i] / np.linalg.norm(self.Knobs_k[:,i]) + + + #Create Initial guess for q (the knob values multiplied by the weight basis vectors) + self.q0 = np.zeros(self.nBasis)+100 + + # lower any initial knob values for basis vectors that contain negatives to avoid any negative weights to start with + #for j in range(len(self.mooringGroups)): + # for i in range(self.nBasis): + # wtemp = np.sum(self.wBasis[:,i]*self.q0 + # if any(self.wBasis[:,i] < 0): + # self.q0[i] = 0.0 + # raise the knob of the all-positive basis vector if needed to make all weights positive + for i in range(self.nBasis): + w = np.matmul(self.wBasis, self.q0) # initial weights per unit length of each line group + if any(w < 0) and all(self.wBasis[:,i] > 0): # if there's a negative line weight and this basis vector is all positive + + q_i_add = np.max(-w/self.wBasis[:,i]) + if display > 0: + print(f' to resolve negative initial line tension ({w}), increasing q0[{i}] by {2*q_i_add}') + + self.q0[i] += 2*q_i_add + + + + + def getSystemStiffness(self): + '''Calculates the stiffness matrix of the SimpleSystem, based on current positions and LineType info''' + + #If were to generalize so each point has 3 or more degrees of freedom, replace each 2 with a 3 + #If we were to further generalize to consider points and bodies, we have to put more carefull thought into the indexing + + + # self.SystemStiffness = np.zeros([2*len(self.coords), 2*len(self.coords)]) + # MH: Changed back to nPtfm x nPtfm. Not sure why anchors were included. Maybe for hybrid? <<< + self.SystemStiffness = np.zeros([2*self.nPtfm, 2*self.nPtfm]) + + for j in range(self.nLines): + + # first get the line's stiffness matrix (xA, yA, xB, yB) + cc = self.u[j][0]*self.u[j][0] # cos(theta)*cos(theta) + ss = self.u[j][1]*self.u[j][1] + cs = self.u[j][0]*self.u[j][1] + + # Find Transformation Matrices: + transMat_inline = np.array([ [ cc, cs,-cc,-cs], + [ cs, ss,-cs,-ss], + [-cc,-cs, cc, cs], + [-cs,-ss, cs, ss]]) + + transMat_perpendicular = np.array([ [ ss,-cs,-ss, cs ], + [-cs, cc, cs,-cc ], + [-ss, cs, ss,-cs ], + [ cs,-cc,-cs, cc ]]) + # Lookup inline and perpendicular stiffness values for this line type (assumes a certain line spacing, etc.) + mg = self.mooringGroups[self.group[j]-1] + if self.old_mode: + kl = mg['kl__w']*mg['w'] + kt = mg['kt__kl']*kl + mg['kl'] = kl + else: # the new mode uses stiffness values that are already provided + kl = mg['kl'] + kt = mg['kt'] + + # Multiply stiffness values by transformation matrix + K_inline = kl * transMat_inline + + #Force in y direction from displacement in y direction caused by tension in x direction + K_perpendicular = kt * transMat_perpendicular + + # Note: Force in x direction from displacement in y direction caused by tension in x direction is neglected as second-order + K_sum = K_inline + K_perpendicular + + # now apply to the appropriate spots in the system stiffness matrix + iA = self.endA[j] + iB = self.endB[j] + + # MH: re-adding the old logic here >>> + if self.endA[j]>> MH: this part may be for hybrid shared moorings >>> + boundaryLineCounti = np.sum(self.boundary[:j]) + if boundaryLineCounti % 2 == 0: # only count the stiffness of the boundary line once + self.SystemStiffness[iA*2:iA*2+2, iA*2:iA*2+2] += K_sum[:2,:2] + self.SystemStiffness[iB*2:iB*2+2, iB*2:iB*2+2] += K_sum[2:,2:] + self.SystemStiffness[iA*2:iA*2+2, iB*2:iB*2+2] += K_sum[:2,2:] + self.SystemStiffness[iB*2:iB*2+2, iA*2:iA*2+2] += K_sum[2:,:2] + if iA >= self.nPtfm: + tau = mg.get('tenTen', 1000) + tau__L = tau/self.intersectZ[iA] # intentionally will be set to inf if it's an anchor + # Only apply if there is a tendon + if mg.get('net', False) and not mg.get('tendON', False): + tau__L = 0 + + self.SystemStiffness[iA*2:iA*2+2, iA*2:iA*2+2] += (np.array([[1, 0],[0, 1]]) * tau__L) + ''' + """ + >>> MH: maybe this was a clever approach to remove anchor row/columns? >>> + # remove any rows and columns in the stiffness matrix with infinity values + finite_mask = np.isfinite(self.SystemStiffness).all(axis=1) + self.SystemStiffness = self.SystemStiffness[np.ix_(finite_mask, finite_mask)] + self.nAnch = int(np.sum(finite_mask==False)/2) + + >>> RA: This works for non-hybrid designs but need to think of a new way to include + net/hybrid designs. + """ + self.nAnch = len(self.coords) - self.nPtfm #MH: temporarily filling this in here <<< + + # self.SystemStiffness[np.abs(self.SystemStiffness) < 1e-5] = 0 + # calculate inverse of stiffness matrix to avoid solving multiple times later + self.K_inverse = np.linalg.inv(self.SystemStiffness) + + + def get_x(self, f, theta=0, heading=0, removeHybrid=True): + '''Get displacement in all dof using linear model. Nonlinear factor included. + This assumes system in equilibrium when no external force is applied. + + theta is wind directions in radians, heading is the wind direction in degrees + ''' + + #if watch_circles > 0: + if not hasattr(self, 'K_inverse'): + raise Exception("In a Linear System, getSystemStiffness must be called before calling get_x.") + + + if theta==0: + theta = np.radians(heading) + + if np.isscalar(f): # thrust force and direction + F = [f*np.cos(theta), f*np.sin(theta)]*(len(self.coords)-self.nAnch) + F[2*self.nPtfm:] = [0, 0]*(len(self.coords) - self.nPtfm - self.nAnch) + + elif len(f)==2: # x and y force components to be applied to each turbine + F = [f[0], f[1]]*(len(self.coords)-self.nAnch) + F[2*self.nPtfm:] = [0, 0]*(len(self.coords) - self.nPtfm - self.nAnch) + + elif len(f)==2*(len(self.coords)-self.nAnch): # full vector of forces + F = f + + else: + raise ValueError("Invalid format of f provided to get_x") + + + #Use linear algebra to solve for x vector (solve for offsets based on stiffness matrix and force vector) Nonlinear factor included here. + xi = np.matmul(self.K_inverse, F)*self.nonlin + + # also figure out peak tensions etc here? <<<< + + + if removeHybrid: + # remove hybrid and anchor points + xi = xi[:2*self.nPtfm] + return xi + + + + def windsweep(self, f=0): + ''' gets offsets and changes in line spacing across wind directions. + ''' + + if f == 0: + f=self.fmax + + self.xi_sweep = np.zeros([len(self.angles), 2*self.nPtfm]) # holds displacement vector (x and y of each platform) for each wind direction + self.dl_sweep = np.zeros([len(self.angles), self.nLines]) # change in each line's spacing for each wind direction + + for i,angle in enumerate(self.angles): + + xi = self.get_x(f, heading=angle) # Get the offsets in each DOF + self.xi_sweep[i,:] = xi + + for il in range(self.nLines): # loop through line indices + dl = 0.0 + iA = self.endA[il] + iB = self.endB[il] + if iA < self.nPtfm: # if this end is attached to a platform + dl += np.sum( xi[2*iA : 2*iA+2] * self.u[il]) # calculate extension as dot product of displacement and line unit vector + if iB < self.nPtfm: # if this end is attached to a platform + dl -= np.sum( xi[2*iB : 2*iB+2] * self.u[il]) # calculate extension as -dot product of displacement and line unit vector + + self.dl_sweep[i, il] = dl + + # also compute watch circle area (by summation of areas of triangles) + self.areas = np.zeros(self.nPtfm) + for i in range(self.nPtfm): + + for j in range(-1, len(self.angles)-1): + self.areas[i] += self.xi_sweep[j,2*i] * (self.xi_sweep[j+1,2*i+1] - self.xi_sweep[j-1,2*i+1]) * 0.5 + + return + + def getCost(self): + '''Calculate the cost function of the line for the spring model''' + #Assume cost is proportional to line length and mass + + #self.line_cost = [self.l[i]*self.mooringGroups[self.group[i]-1]['w'] for i in range(self.nLines)] <<< this was double counting + self.line_cost = [] + + for i in range(self.nLines): + + self.line_cost.append(self.l[i]*self.mooringGroups[self.group[i]-1]['w']) # Cost Function for each line [kg m] + + if self.mooringGroups[self.group[i]-1]['shared'] != 1: # If it's an anchor mooring + self.line_cost[-1] = self.line_cost[-1]*self.anchor_cost_factor # include an anchorcost factor + elif self.mooringGroups[self.group[i]-1]['net'] and self.mooringGroups[self.group[i]-1]['tendON']: # if there's an anchor in a hybrid system + self.line_cost[-1] = self.line_cost[-1]*self.anchor_cost_factor/self.mooringGroups[self.group[i]-1]['intersectDeg'] # include a shared anchorcost factor + # sloppily store the cost in the mooring group as well for use in vizualization + self.mooringGroups[self.group[i]-1]['cost'] = self.line_cost[-1] + + self.cost = np.sum(np.array(self.line_cost)) # save total cost + + return(self.cost) + + + def getWeight(self): + '''Calculate the total system mooring line (wet) weight''' + + return sum([self.l[i]*self.mooringGroups[self.group[i]-1]['w'] for i in range(self.nLines)]) + + + def optimize(self, display=0): + '''solve the cheapeast system for a given input force, wind angle, and maximum displacement''' + + if not self.old_mode: + raise Exception("LinearSystem.optimize only works when old_mode = True") + + if display > 1: + print(f'Beginning LinearSystem optimization.') + + print('weight basis vectors are:') + print(self.wBasis) + + def dopt_fun(q): + '''evaluation function for the dopt solver. This function includes both + design variables and constraints. This function inputs q which is an array of knob values. + ''' + + # ----- calculate line weight from q, and upate line types and system stiffness matrix ------------ + w = np.matmul(self.wBasis, q) # weights per unit length of each line group + + for i, mg in enumerate(self.mooringGroups): + mg['w'] = w[i] # update wet weight per unit length [N/m] + + self.getSystemStiffness() # need this for evaluating constraints + + # ---- objective function - mooring system mass or cost ----- + f = self.getCost() + + + # ----- constraint values - margin from max offsets, and line weights (must be positive) ----- + ''' + Finds how much a certain stiffness will undershoot the maximum design displacement + This function returns a list of undershoots for each degree of freedom. + ''' + + self.windsweep() # update offset and line extension numbers for each wind direction + + self.offsets = np.zeros([len(self.angles), self.nPtfm]) # store offset distance (radius) for each direction + + for i,angle in enumerate(self.angles): # Calculate the hypotenuse of each platform's offset + self.offsets[i,:] = [np.linalg.norm(self.xi_sweep[i, 2*k:2*k+2]) for k in range(self.nPtfm)] + + peak_offsets = np.max(self.offsets, axis=0) # get largest displacement of each platform (over the range of wind directions) + + offset_margin = [self.xmax - peak_offset for peak_offset in peak_offsets] # substract maximum from resultant values to get undershoot + + g = np.hstack([offset_margin, w.tolist()]) + + + # return the objective and constraint values + return f, g, w, 0, 0, False + + + dX_last = np.zeros(self.nBasis)+1e5 + + + # call the optimizer (set display=2 for a desecription of what's going on) + #q, f, res = msolve.dopt(dopt_fun, q0, tol=0.001, maxIter=70, a_max=1.5, dX_last=dX_last, display=max(display-1,0)) + #q, f, res = msolve.dopt(dopt_fun, self.q0, tol=0.002, stepfac=100, maxIter=100, a_max=1.5, dX_last=dX_last, display=display-1) + q, f, res = msolve.dopt2(dopt_fun, self.q0, tol=0.002, stepfac=100, maxIter=100, a_max=1.5, dX_last=dX_last, display=display-1) + + + # check the results - and retry up to 4 times + for i in range(4): + if display > 0: print(f" Message from dopt: {res['message']}") + + f, g, w, _, _, _ = dopt_fun(q) # get latest objective and constraint values + + #if display>0: + # if res['success'] == False: + # print('LinearSystem Mooring optimization was UNSUCCESSFUL: '+res['message']) + # else: + # print(f"LinearSystem Mooring optimization was successful after {res['iter']} iterations.") + + # check for overly stiff or soft solution (and the rerun with better starting points) + if self.nPtfm==1: + offset_margin = g[0] # <<< can this be simplified? + else: + offset_margin = np.min(g[:-len(q)-1]) # this is the closest any watch circle comes to the limit + + if offset_margin > 0.05*self.xmax: # if the closest it gets to the target watch circles is more than 5% short + if display > 0: print(f' LinearSystem optimization attempt {i} detected overly small watch circles (largest is {offset_margin:5.1f} m from the limit).') + if display > 1: print(' Retrying the optimization with lighter starting points (q0)') + self.q0 = 0.3*self.q0 + + + elif offset_margin < -0.1*self.xmax: # if it overshoots the target watch circles by more than 10% + if display > 0: print(f' LinearSystem optimization attempt {i} detected extreme watch circles (largest is {-offset_margin:5.1f} m over the limit).') + if display > 1: print(' Retrying the optimization with heavier starting points (q0)') + self.q0 = 10.0*self.q0 + + else: # otherwise, call it succsessful + if display>0: print(f" LinearSystem optimization attempt {i} was successful after {res['iter']} iterations.") + break + + # this is where we rerun dopt with the modified settings + q, f, res = msolve.dopt(dopt_fun, self.q0, tol=0.002, stepfac=100, maxIter=100, a_max=1.5, dX_last=dX_last, display=display-1) + + + if display>1: + + if res['success'] == False: + print('Final LinearSystem Mooring optimization was UNSUCCESSFUL: '+res['message']) + else: + print(f"Final LinearSystem Mooring optimization was successful after {res['iter']} iterations.") + + + + # plotting + + if ((res['success'] == False and display >0) or display > 1) and self.nPtfm>1: # plot the optimization if it's likely desired + + n = len(q) + fig, ax = plt.subplots(n+3, 1, sharex=True) + Xs = np.array(res["Xs"]) + Fs = np.array(res["Fs"]) + Gs = np.array(res["Gs"]) + iter = res["iter"] + + for i in range(n): + ax[i].plot(Xs[:iter+1,i]) + ax[i].set_ylabel(f"q{i}") + + ax[n].plot(Xs[:iter+1,n:]) + ax[n].set_ylabel("weights") + + ax[n+1].plot(Fs[:iter+1]) + ax[n+1].set_ylabel("cost") + + #m = len(self.Knobs_k) + ax[n+2].plot(Gs[:iter+1,:-n-1]) + #ax[n+3].plot(Gs[:iter+1,-n-1:]) + ax[n+2].set_ylabel("g offsets") + #ax[n+3].set_ylabel("g weights") + ax[n+2].set_xlabel("iteration") + + #breakpoint() + if display > 1: + plt.show() + breakpoint() + + + #For debugging purposes: + self.res = res + self.q = q + + + # get max extension of each line group's spacing and store it in the mooring group + dl_max = np.max(self.dl_sweep, axis=0) + dl_min = np.min(self.dl_sweep, axis=0) + #print((" group: "+"".join([" {:6d}"]*self.nLines)).format(*self.group )) + #print((" dl_max: "+"".join([" {:6.1f}"]*self.nLines)).format(*dl_max.tolist() )) + #print((" dl_min: "+"".join([" {:6.1f}"]*self.nLines)).format(*dl_min.tolist() )) + for i, mg in enumerate(self.mooringGroups): + mg['dl_max'] = np.mean(dl_max[[j for j, k in enumerate(self.group) if k==i+1]]) # take the mean from any lines in this mooring group (group i+1) + mg['dl_min'] = np.mean(dl_min[[j for j, k in enumerate(self.group) if k==i+1]]) + + # update each mooring group's weight and tension values + for i, mg in enumerate(self.mooringGroups): + mg['w'] = w[i] # update wet weight per unit length [N/m] + mg['ten'] = mg['ten__w']*mg['w'] # update line tension [N] + + if np.round(w[i], 3) == 0: + w[i] = 0.0 + + if w[i] < 0: + raise ValueError("breakpoint due to negative weight") + + self.getSystemStiffness() # need this for evaluating constraints + + return q + + + + def optimize2(self, display=0): + '''NEW: Figure out what mooringGroup stiffnesses will achieve the + desired watch circles, for a given input force, wind angle, and + maximum displacement''' + + if self.old_mode: + raise Exception("LinearSystem.optimize2 only works when old_mode = False") + + if display > 1: + print(f'Beginning LinearSystem optimization2.') + + print('tension basis vectors are:') + print(self.wBasis) + + + self.iter=0 # reset iteration counter + + def dopt_fun(kls): + '''evaluation function for the dopt solver. This function includes + both design variables and constraints. This function inputs kls, + inline stiffness values. + ''' + + # ----- upate line types and system stiffness matrix ------------ + + for i, mg in enumerate(self.mooringGroups): + mg['kl'] = kls[i] # update inline stiffness [N/m] + + self.getSystemStiffness() # need this for evaluating constraints + + # ---- objective function - mooring system mass or cost ----- + # approximate cost as product of line length and stiffness + line_stiffness_costs = [self.l[i]*self.mooringGroups[self.group[i]-1]['kl'] for i in range(self.nLines)] + f = sum(line_stiffness_costs) + + + # ----- constraint values - margin from max offsets, and line weights (must be positive) ----- + ''' + Finds how much a certain stiffness will undershoot the maximum design displacement + This function returns a list of undershoots for each degree of freedom. + ''' + + self.windsweep() # update offset and line extension numbers for each wind direction + + # store offset distance (radius) for each direction + self.offsets = np.zeros([len(self.angles), self.nPtfm]) + for i,angle in enumerate(self.angles): + self.offsets[i,:] = [np.linalg.norm(self.xi_sweep[i, 2*k:2*k+2]) for k in range(self.nPtfm)] + + peak_offsets = np.max(self.offsets, axis=0) # get largest displacement of each platform (over the range of wind directions) + + offset_margin = [self.xmax - peak_offset for peak_offset in peak_offsets] # substract maximum from resultant values to get undershoot + + g = np.hstack([offset_margin, kls.tolist()]) # constraints are offset and positive stiffness + + self.iter = self.iter + 1 # update counter (this isn't actually iterations, it's function calls) + if display > 3 and self.iter%20 == 0: + sys.plot2d(watch_circles=4, line_val="stiffness") + plt.show() + + + # return the objective and constraint values + return f, g, [], 0, 0, False + + n = len(self.mooringGroups) + kls0 = np.array([mg['kl'] for mg in self.mooringGroups]) # starting point + dX_last = np.zeros(n) + 0.01*np.mean(kls0) # step size + + + # call the optimizer (set display=2 for a desecription of what's going on) + #q, f, res = msolve.dopt(dopt_fun, q0, tol=0.001, maxIter=70, a_max=1.5, dX_last=dX_last, display=max(display-1,0)) + #q, f, res = msolve.dopt(dopt_fun, self.q0, tol=0.002, stepfac=100, maxIter=100, a_max=1.5, dX_last=dX_last, display=display-1) + kls, f, res = msolve.dopt2(dopt_fun, kls0, tol=0.002, stepfac=20, maxIter=40, a_max=1.4, dX_last=dX_last, display=display-1) + + + # check the results - and retry up to 4 times + for i in range(4): + if display > 0: print(f" Message from dopt: {res['message']}") + + f, g, _, _, _, _ = dopt_fun(kls) # get latest objective and constraint values + + #if display>0: + # if res['success'] == False: + # print('LinearSystem Mooring optimization was UNSUCCESSFUL: '+res['message']) + # else: + # print(f"LinearSystem Mooring optimization was successful after {res['iter']} iterations.") + + # check for overly stiff or soft solution (and the rerun with better starting points) + if self.nPtfm==1: + offset_margin = g[0] # <<< can this be simplified? + else: + offset_margin = np.min(g[:-len(kls)-1]) # this is the closest any watch circle comes to the limit + + if offset_margin > 0.05*self.xmax: # if the closest it gets to the target watch circles is more than 5% short + if display > 0: print(f' LinearSystem optimization attempt {i} detected overly small watch circles (largest is {offset_margin:5.1f} m from the limit).') + if display > 1: print(' Retrying the optimization with lighter starting points (q0)') + self.q0 = 0.3*self.q0 + + + elif offset_margin < -0.1*self.xmax: # if it overshoots the target watch circles by more than 10% + if display > 0: print(f' LinearSystem optimization attempt {i} detected extreme watch circles (largest is {-offset_margin:5.1f} m over the limit).') + if display > 1: print(' Retrying the optimization with heavier starting points (q0)') + self.q0 = 10.0*self.q0 + + else: # otherwise, call it succsessful + if display>0: print(f" LinearSystem optimization attempt {i} was successful after {res['iter']} iterations.") + break + + # this is where we rerun dopt with the modified settings + q, f, res = msolve.dopt(dopt_fun, kls0, tol=0.002, stepfac=100, maxIter=100, a_max=1.5, dX_last=dX_last, display=display-1) + + + if display>1: + + if res['success'] == False: + print('Final LinearSystem Mooring optimization was UNSUCCESSFUL: '+res['message']) + else: + print(f"Final LinearSystem Mooring optimization was successful after {res['iter']} iterations.") + + + + # plotting + + if True: #((res['success'] == False and display >0) or display > 1) and self.nPtfm>1: # plot the optimization if it's likely desired + + n = len(kls) + fig, ax = plt.subplots(n+3, 1, sharex=True) + Xs = np.array(res["Xs"]) + Fs = np.array(res["Fs"]) + Gs = np.array(res["Gs"]) + iter = res["iter"] + + for i in range(n): + ax[i].plot(Xs[:iter+1,i]) + ax[i].set_ylabel(f"kls{i}") + + ax[n].plot(Xs[:iter+1,n:]) + ax[n].set_ylabel("weights") + + ax[n+1].plot(Fs[:iter+1]) + ax[n+1].set_ylabel("cost") + + #m = len(self.Knobs_k) + ax[n+2].plot(Gs[:iter+1,:-n-1]) + #ax[n+3].plot(Gs[:iter+1,-n-1:]) + ax[n+2].set_ylabel("g offsets") + #ax[n+3].set_ylabel("g weights") + ax[n+2].set_xlabel("iteration") + + + + #For debugging purposes: + self.res = res + self.kls = kls + + + # get max extension of each line group's spacing and store it in the mooring group + dl_max = np.max(self.dl_sweep, axis=0) + dl_min = np.min(self.dl_sweep, axis=0) + #print((" group: "+"".join([" {:6d}"]*self.nLines)).format(*self.group )) + #print((" dl_max: "+"".join([" {:6.1f}"]*self.nLines)).format(*dl_max.tolist() )) + #print((" dl_min: "+"".join([" {:6.1f}"]*self.nLines)).format(*dl_min.tolist() )) + for i, mg in enumerate(self.mooringGroups): + mg['dl_max'] = np.mean(dl_max[[j for j, k in enumerate(self.group) if k==i+1]]) # take the mean from any lines in this mooring group (group i+1) + mg['dl_min'] = np.mean(dl_min[[j for j, k in enumerate(self.group) if k==i+1]]) + + # update each mooring group's kl + for i, mg in enumerate(self.mooringGroups): + mg['kl'] = kls[i] + + if np.round(kls[i], 3) == 0: + kls[i] = 0.0 + + if kls[i] < 0: + raise ValueError("breakpoint due to negative kl") + + self.getSystemStiffness() # need this for evaluating constraints + + return kls + + + def plot2d(self, ax=None, **kwargs): + '''Plots 2d view of simple system mooring configuration, optionally including additional properties + + Parameters + ---------- + ax : matplotlib axes + The axes to draw the plot on. A new figure is created and returned if this is not provided. + show_lines : string + Specifies whether to show lines: none, anch, shared, all (default) + watch_circles: float + Specifies whether to draw watch circles (and at what scale, >0) or not to draw them (0) + line_val : string + Specifies what to show for the lines: uniform, two, groups (default), stiffness, cost, weight, tension + colormap : int or string + Specifies what colormap to use. + colorbar : int + 0 - don't draw one, 1 - draw as normal, 2 - draw on seperate axes specified in kwarg cbax. + colorscale : string + Specify linear or log (for logarithmic) + cbax : plt.Axes + Only used if colorbar=2 + show_axes : bool + Whether to show the axes of the figure or not (hide them). + labels : string + Whether to label lines (l), points (p or t for turbine, a for anchor), etc. '' means no labels. + title : string + Text to add above the figure (otherwise default text will be shown). + line_color + line_style + line_width + ''' + + + #plt.ion() #Turn on interactive mode + + # initialize some plotting settings + n = self.nLines + + # some optional argument processing and setting default values if not supplied + + line_val = kwargs.get("line_val" , "groups" ) # get the input value, or use "groups" as default + show_lines = kwargs.get("show_lines" , "all" ) # get the input value, or use "groups" as default + watch_circles = kwargs.get("watch_circles", 0 ) # + colormap = kwargs.get("colormap" , 0 ) # + colorbar = kwargs.get("colorbar" , 1 ) # + colorscale = kwargs.get("colorscale" , "linear" ) # + show_axes = kwargs.get("show_axes" , True ) # + labels = kwargs.get("labels" , '' ) # + title = kwargs.get("title" , [] ) # + figsize = kwargs.get("figsize" , (5,5) ) # + wea = kwargs.get("wea" , None ) # + #center = kwargs.get("center" , 1 ) # turns on and off whether the plot is centered or not + + # receive or use default uniform line color/style/width (may be overriden by non-uniform color coding options in line_val) + colors = [kwargs.get("line_color", 'black')]*n + styles = [kwargs.get("line_style", 'solid')]*n + thicks = [kwargs.get("line_width", 2)]*n + + + + # set up colormap + if colormap == 0 or colormap == "rainbow" or colormap == "jet": + #Create Rainbow colormap (I still incorrectly use 'jet' sometimes when I want a rainbow colormap, so I will keep support for that keyworkd) + cmap = mpl.cm.rainbow + + elif colormap == 1 or colormap == 'aut': #Create autumn colormap + cmap = mpl.cm.autumn + + else: + raise ValueError("invalide colormap input provided to plot2d.") + + + # set whether colormap will be linear of logarithmic + if colorscale == "linear": + normalizer = mpl.colors.Normalize + elif colorscale == "log" or colorscale == "logarithmic": + normalizer = mpl.colors.LogNorm + else: + raise ValueError("colorscale must be 'linear' or 'log'.") + + # set up color map bounds if provided + if "val_lim" in kwargs: + + def getLineColors(values): + norm = normalizer(vmin=kwargs["val_lim"][0], vmax=kwargs["val_lim"][1]) #Scale to min and max values + s_m = mpl.cm.ScalarMappable(norm=norm, cmap = cmap) # create Scalar Mappable for colormapping + return s_m.to_rgba(values), s_m + + else: + + def getLineColors(values): + if min(values) == max(values): + norm = normalizer(vmin=0, vmax=max(values)) # if only one value, start scale at zero + else: + norm = normalizer(vmin=min(values), vmax=max(values)) # set scaling to min and max values + s_m = mpl.cm.ScalarMappable(norm = norm, cmap = cmap) # create Scalar Mappable for colormapping + return s_m.to_rgba(values), s_m + + ''' + # get arrays of all the values of interest up-front + line_k = [0.001*self.lineTypes[key].k for key in self.lineTypes] + line_c = [0.001*self.lineTypes[key].cost for key in self.lineTypes] # note: this is when the cost parameter has been repurposed from $/m to $/line + line_m = [ self.lineTypes[key].mlin for key in self.lineTypes] + line_w = [ self.lineTypes[key].w for key in self.lineTypes] + line_t = [0.001*self.lineTypes[key].t for key in self.lineTypes] + line_kt_k = [ self.lineTypes[key].kt_over_k for key in self.lineTypes] + line_MBL = [0.001*self.lineTypes[key].MBL for key in self.lineTypes] + line_MSF = [ MBL/t for MBL,t in zip(line_MBL,line_t)] # safety factor + ''' + + line_k = [self.mooringGroups[self.group[i]-1]['kl' ]/1e3 for i in range(n)] # stiffness in kN/m + line_t = [self.mooringGroups[self.group[i]-1]['ten']/1e6 for i in range(n)] # tension in MN + line_w = [self.mooringGroups[self.group[i]-1]['w'] for i in range(n)] # wet weight per length in N/m + line_m = [self.mooringGroups[self.group[i]-1]['w']/9.81 for i in range(n)] # wet weight per length in kg/m + #line_t_w = [self.mooringGroups[self.group[i]-1]['ten__w'] for i in range(n)] # can add this in later + #line_k_w = [self.mooringGroups[self.group[i]-1]['kl__w'] for i in range(n)] # can add this in later + if self.old_mode: + line_kt_k = [self.mooringGroups[self.group[i]-1]['kt__kl'] for i in range(n)] + line_cost = [self.mooringGroups[self.group[i]-1]['cost'] for i in range(n)] + + + clist = ['tab:blue','tab:cyan','tab:green','tab:olive','tab:brown','tab:purple', + 'tab:red','tab:orange','tab:blue','tab:pink','tab:gray'] + + + # set up line data display - Detetermine which line variable we are using + if line_val == 'uniform': # all lines drawn black and solid + pass + + # elif line_val == 'two': # distinguishes shared vs. anchor lines + # for i in range(n): + # if self.mooringGroups[i]['shared']: + # colors[i] = "blue" + # styles[i] = "solid" + # else: + # colors[i] = "black" + # styles[i] = "dashed" + + elif line_val == 'groups': + for i in range(n): + ii = self.group[i]-1 + colors[i] = clist[ii] + + elif line_val == 'shared': + for i in range(n): + ii = self.group[i]-1 + if self.mooringGroups[ii]['shared']: + colors[i] = 'tab:cyan' + else: + colors[i] = 'tab:pink' + colorbar = 0 + + elif line_val == 'stiffness': + colors, s_m = getLineColors(line_k) # get colors corresponding to each line type + colorbar_label = 'Effective stiffness (kN/m)' + line_var = 'k' + + + elif line_val == 'weight': + colors, s_m = getLineColors(line_w) + colorbar_label = 'Wet weight (N/m)' + line_var = 'weight' + + elif line_val == 'mass': + colors, s_m = getLineColors(line_m) + colorbar_label = 'Wet mass (kg/m)' + line_var = 'weight' + + elif line_val == 'tension': + colors, s_m = getLineColors(line_t) + colorbar_label = 'Horizontal tension (MN)' + line_var = 'T' + + elif line_val == 'kt_over_k': + colors, s_m = getLineColors(line_kt_k) + colorbar_label = 'Line kt/k (-)' + line_var = 'kt/k' + + elif line_val == 'cost': + colors, s_m = getLineColors(line_cost) + colorbar_label = 'Line cost [?]' + line_var = 'cost' + + + else: + raise ValueError('Incorrect line_val given') + + + # set up axes + if ax == None: # if no axes passed in, make a new figure + fig, ax = plt.subplots(1,1, figsize=figsize, constrained_layout=True) + else: + fig = ax.get_figure() # otherwise plot on the axes passed in + + + + # # plot each mooring line, colored differently for each line type + for i in range(n): + + # shousner: I don't understand how the j var found an integer + #j = int(Line.type[4:])-1 # index of LineType + ii = self.group[i]-1 + + shared = self.mooringGroups[ii]['shared']==1 + + rA = self.rA[i] + rB = self.rB[i] + + + + if not (show_lines=="none" or (show_lines=="anch" and shared) or (show_lines=="shared" and not shared)): + if self.boundary[i]: + l, = ax.plot([rA[0], rB[0]],[rA[1], rB[1]], color=colors[i], linestyle='--', lw=thicks[i]) + else: + l, = ax.plot([rA[0], rB[0]],[rA[1], rB[1]], color=colors[i], linestyle=styles[i], lw=thicks[i]) + if 'l' in labels: + coord = 0.5*(rA + rB) # position label at midpoint between line ends + ax.text(coord[0], coord[1], f"{i+1}", bbox=dict(facecolor='none', edgecolor='k')) + + + # display colorbar + if not line_val in ["uniform", "two", "groups"]: + if colorbar==2: + if 'cbax' in kwargs: + #if isinstance(colorbar, plt.Axes): # if an axes has been passed in via colorbar + plt.gcf().colorbar(s_m, label=colorbar_label, ax=kwargs["cbax"], shrink=0.4, aspect=12) # put the colorbar on that axes + else: + raise ValueError("An axes to put the colorbar beside must be provided (as 'cbax') when colorbar=2") + + elif colorbar == 1: # make a regular colorbar on the current axes + cax = plt.gca().inset_axes([1.1, 0, 0.05, 1]) + plt.gcf().colorbar(s_m, label=colorbar_label, cax=cax) + elif colorbar == 0: # don't make a colorbar + pass + else: + raise ValueError("Unrecognized entry for colorbar when calling plot2d.") + + + #plot each platform and anchor + #for i in range(self.coords.shape()[0]): # loop through each platform or anchor + for i in range(len(self.intraMat)): + + # platform + if i < self.nPtfm: + ax.plot(self.coords[i,0], self.coords[i,1], 'ko', markersize = 6) + + # plot watch circles if requested + if watch_circles > 0: + if not hasattr(self, 'xi_sweep'): + raise Exception("In a Linear System, windsweep must be called before trying to plot watch circles.") + + scale = watch_circles + + center_x = self.coords[i,0] + center_y = self.coords[i,1] + + # plot calculated displacement envelopes + #disps_x = self.xi_sweep[:,2*i] * scale + #disps_y = self.xi_sweep[:,2*i+1] * scale + #ax.plot(center_x + disps_x, center_y + disps_y,'k',lw=1.5, alpha = 0.6) + + watch_circle_coords = np.column_stack([self.xi_sweep[:,2*i ]*scale + center_x, + self.xi_sweep[:,2*i+1]*scale + center_y]) + + # ax.add_patch(mpl.patches.Polygon(watch_circle_coords, lw=1, ec=[(self.depth - np.max(self.intersectZ))/self.depth,0,0,1.0], fc=[0,0,0,0])) + ax.add_patch(mpl.patches.Polygon(watch_circle_coords, lw=1, ec=[0,0,0,0.6], fc=[0,0,0,0])) + + # Plot the boundaries + r = self.xmax * scale + + thetas = np.linspace(0, 2 * np.pi, 201) + xs, ys = (np.array(()),np.array(())) + for theta in thetas: + xs = np.append(xs,r * np.cos(theta) + center_x) + ys = np.append(ys,r * np.sin(theta) + center_y) + ax.plot(xs,ys,'r--', lw=1, alpha = 0.5) + + + if 't' in labels: + coord = np.array([self.coords[i,0], self.coords[i,1],0]) + np.array([250, 150,0]) + ax.text(coord[0], coord[1], f"T{i+1}", fontweight='bold')#, bbox=dict(facecolor='none', edgecolor='k', boxstyle='circle,pad=0.3')) + elif 'p' in labels: + coord = np.array([self.coords[i,0], self.coords[i,1],0]) + np.array([200, 200,0]) + ax.text(coord[0], coord[1], str(i+1))#, bbox=dict(facecolor='none', edgecolor='k', boxstyle='circle,pad=0.3')) + + # anchor + elif i >= self.nPtfm and self.intersectZ[i] > 0: + ax.plot(self.coords[i,0], self.coords[i,1], 'ko', markersize=6, mfc='cyan') + if 'h' in labels: + coord = np.array([self.coords[i,0], self.coords[i,1],0]) + np.array([200, 200,0]) + ax.text(coord[0], coord[1], "Hbrd"+str(i+1-self.nPtfm), bbox=dict(facecolor='none', edgecolor='c', boxstyle='circle,pad=0.3')) + else: + ax.plot(self.coords[i,0], self.coords[i,1], 'ko', markersize=6, mfc='none') + if 'a' in labels: + coord = np.array([self.coords[i,0], self.coords[i,1],0]) + np.array([200, 200,0]) + ax.text(coord[0], coord[1], "Anch"+str(i+1-self.nPtfm), bbox=dict(facecolor='none', edgecolor='k', boxstyle='circle,pad=0.3')) + + + + + #Uncomment to hard code labels + #plt.legend(shadow=True, loc="upper left") <<< still need to sort out legend + if line_val == 'groups': + + from matplotlib.lines import Line2D + handles = [] + for i in range(len(self.mooringGroups)): + handles.append(Line2D([0], [0], label=f'Group {i}', color=clist[i])) + + plt.legend(handles=handles) + + ax.set_aspect('equal') + + if not show_axes: + ax.axis('off') + + if len(title) > 0: + plt.title(title) + + # if this made a new figure, return its handles + #if axes == None: + + # Plot the WEA boundaries if given: + if wea: + x, y = wea.exterior.xy + plt.plot(x, y, color='green', linestyle='--') + + return fig, ax + + + + + + #note: method analyzeWind(self) made some nice plots with binning offsets by direction according to severity + # See code prior to March 20 for this capability. + + + + def eigenAnalysis(self, plot=0, M=1e6, deg=0): + ''' + deg + first desired direction of turbine 1 for organizing eigenmodes. Default 0 (deg) + ''' + + v1 = [[ np.cos(np.radians(deg))], [np.sin(np.radians(deg))]] + v2 = [[-np.sin(np.radians(deg))], [np.cos(np.radians(deg))]] + + + #Take code and ideas from patrick and run eigen analysis + + #Define and Populate Mass Matrix + if np.isscalar(M): + self.MassMatrix = np.zeros((2*self.nPtfm, 2*self.nPtfm)) + np.fill_diagonal(self.MassMatrix, M) + else: # if it's a full matrix + if M.shape != self.SystemStiffness.shape: + S = self.SystemStiffness + raise ValueError(f'The mass matrix is of size {M.shape[0]}x{M.shape[1]} and needs to be of size {S.shape[0]}x{S.shape[1]}') + else: + self.MassMatrix = M + + + #Calculate eigenvalues and eigenvectors (note: eigenvectors or mode shapes are *columns*) + self.eigenvalues, self.eigenvectors = np.linalg.eig(np.matmul(np.linalg.inv(self.MassMatrix), self.SystemStiffness)) + + #Calculate Natrual Frequency + self.nat_freqs = np.real(np.sqrt(self.eigenvalues)) + + #Find Indicies to sort from smallest to largest + sort_indices = np.argsort(self.nat_freqs) + + + #Use sort_indices to sort natrual frequency, eigenvalues and eigenvectors + self.nat_freqs = np.array([self.nat_freqs[i] for i in sort_indices]) + self.eigenvalues = np.array([self.eigenvalues[i] for i in sort_indices]) + self.eigenvectors = np.transpose([self.eigenvectors[:,i] for i in sort_indices]) + self.periods = np.pi * 2 / self.nat_freqs + + #Round periods to 5 decimals + self.periods = np.round(self.periods,5) + + #Pretty plots + #Loop through each eigen vector + #Re-orient eigenvector pairs to look nice + for period in set(self.periods): + count = np.count_nonzero(self.periods == period) + + #If there are duplicates, re-order them so that they are orthogonal + if count == 2: + #print('re-normalizing modes for period {}'.format(period)) + + #Get indicies + ind = [i for i in range(len(self.periods)) if self.periods[i] == period] + eigs = np.empty([len(self.periods),0]) + for i in ind: + #eigs is a nxc matrix where n is the number of DOF and c is the period count + eigs = np.column_stack((eigs,self.eigenvectors[:,i])) + #print(eigs) + + #Make orthogonal + eigs = scipy.linalg.orth(eigs) + + #A elegant bit of linear algebra used to get desired directions + #desired directions + dir1 = np.array(v1) + dir2 = np.array(v2) + dirs = np.append(dir1,dir2,axis = 1) + + #current directions + current = eigs[:2,:2] + + #get the weights needed + #weights1 = np.matmul(np.linalg.inv(current),dir1) + #weights2 = np.matmul(np.linalg.inv(current),dir2) + weights = np.matmul(np.linalg.inv(current),dirs) + + #trasform eigs using weights + eigs = np.matmul(eigs,weights) + + #Update variables + for i in ind: + self.eigenvectors[:,i] = eigs[:,0] + eigs = np.delete(eigs, 0, axis = 1) + + + #Plot Things + if plot == 1: + + def closestDivisors(n): + a = round(np.sqrt(n)) + while n%a > 0: a -= 1 + return int(a),int(n//a) + + rows, cols = closestDivisors(len(self.eigenvalues)) + + fig,ax = plt.subplots(rows,cols) + #Loop through each eigen values + for ind in range(len(self.eigenvalues)): + # np.unravel_index() allows linear indexing of a 2D array, like in matlab + if len(np.shape(ax)) == 2: + plt_ind = np.unravel_index(ind,[rows,cols],'F') + else: + plt_ind = ind + + self.eigenPlot(ind, ax=ax[plt_ind]) + + ''' + #size eigenvector #TODO: Change this based on the size of the plot + eigenvector = self.eigenvectors[:,ind] * 1500 + + + #Loop through each point + for i in range(self.nPtfm): + r = np.array([self.coords[i,0], self.coords[i,1]]) + + ax[plt_ind].plot(r[0], r[1], 'ko', markersize = 2) + + ax[plt_ind].quiver(np.array(r[0]), + np.array(r[1]), + np.array(eigenvector[2*i]), + np.array(eigenvector[2*i+1]) + ,units='xy', scale=1) + + ax[plt_ind].set_aspect('equal') + ax[plt_ind].set_xticks([]) + ax[plt_ind].set_yticks([]) + ax[plt_ind].set_axis_off() + ax[plt_ind].set_xlim(ax[plt_ind].get_xlim()[0] - 1000, ax[plt_ind].get_xlim()[1] + 1000) + ax[plt_ind].set_ylim(ax[plt_ind].get_ylim()[0] - 1000, ax[plt_ind].get_ylim()[1] + 1000) + ax[plt_ind].set_title('T = {:.3f}s'.format(self.periods[ind])) + ''' + #TODO + #Nice way to make them perpindicular + #Animation of them rotating + + #Collective Mode + #is there a slick way to add the two together + #anti-collective mode + + #kx collective + #kt collective + + #kx anti-collective + #kt anti-collective + + + #1. Singlue turbine - 2 modes, same period + #2. 2 turbines - 4 modes, all permuatataions of kt/kx, col/anti-col + # no two modes have the same peiord. + #3. 4 turbine square - 8 modes. 2 copies of all perumatations of kt/kx + #4 3 turbines triangle - 6 modes. Things get weird because all motion + # includes combinations of kt and kx + #5 6 turbine hexagon - 12 modes. Theres 120 deg symmetry so I expect + # atleast 3 copies of all permutations. I expect these permutations + # to look similar to the triangle + #6 7 turbine hexagon - 14 modes. There are 120 deg symmetry so I again + # expect 3 copies of all permutations. Things get weird because 14 + # is not divisable by 3, so I don't quite know where that leads + + + # new method to plot any given eigenmode + def eigenPlot(self, ind, ax=None, period=True, figsize=(5,5), length=800, color='k', line_width=4, head_size=3): + '''Plot an eigenmode of a Linear System. i is the mode index. eigenAnalysis must be called first.''' + + if ax == None: + fig, ax = plt.subplots(1,1, figsize=figsize) + else: + fig = ax.get_figure() + + # get largest length of an eigenvector horizontal motion vector + maxLength = max(np.hypot(self.eigenvectors[0::2,ind], self.eigenvectors[1::2,ind])) + + # scale eigenvector to desired length + eigenvector = self.eigenvectors[:,ind] * length/maxLength + + #Loop through each point + for i in range(self.nPtfm): + r = np.array([self.coords[i,0], self.coords[i,1]]) + + ax.plot(r[0], r[1], 'ko', markersize=2) + + ax.quiver(np.array(r[0]), + np.array(r[1]), + np.array(eigenvector[2*i]), + np.array(eigenvector[2*i+1]), + units='dots', width=line_width, color=color, zorder=10, + headwidth=head_size, headlength=head_size, headaxislength=head_size, + angles='xy', scale_units='xy', scale=1) + #units='xy', scale=1) + + ax.set_aspect('equal') + ax.set_xticks([]) + ax.set_yticks([]) + ax.set_axis_off() + #ax.set_xlim(ax.get_xlim()[0] - length, ax.get_xlim()[1] + length) + #ax.set_ylim(ax.get_ylim()[0] - length, ax.get_ylim()[1] + length) + if period: + ax.set_title('T = {:.3f}s'.format(self.periods[ind])) + + return fig, ax + + # :::::::::::: methods below here to eventually be moved to separate code ::::::::::::::: + + def calibrate(self, percent_droop=50, percent_drag=60, plots=0): + + + def laylength_eval(X, args): + '''Function to be solved for lay length target''' + + # Step 1. break out design variables and arguments into nice names + L = X[0] + [Xf,Zf,EA,W] = args + + # Step 2. do the evaluation (this may change mutable things in args) + (Fx1, Fy1, Fx2, Fy2, info) = mp.catenary(Xf, Zf, L, EA, W) + + # Step 3. group the outputs into objective function value and others + Y = info["LBot"] # objective function + oths = dict(message="none") # other outputs - returned as dict for easy use + + return np.array([Y]), oths, False + + + def laylength_step(X, args, Y, oths, Ytarget, err, tols, iter, maxIter): + '''Stepping functions for achieving lay length target''' + + L = X[0] + [Xf,Zf,EA,W] = args + LBot = Y[0] + + if LBot <= 0: # if no seabed contact, increase line length by 10% of spacing + dL = 0.1*Xf + + else: # get numerical derivative + deltaL = 2*tols[0] # step size + (Fx1, Fy1, Fx2, Fy2, info) = mp.catenary(Xf, Zf, L+deltaL, EA, W) # evaluate LBot in perturbed case + LBot2 = info["LBot"] + dLBot_dL = (LBot2-LBot)/deltaL # derivative + + # adjust as per Netwon's method + dL = -err[0]/dLBot_dL + + return np.array([dL]) # returns dX (step to make) + + + def droop_eval(X, args): + '''Function to be solved for shared droop target''' + + # Step 1. break out design variables and arguments into nice names + L = X[0] + [Xf,Zf,EA,W,cb] = args + + # Step 2. do the evaluation (this may change mutable things in args) + (Fx1, Fy1, Fx2, Fy2, info) = mp.catenary(Xf, Zf, L, EA, W, cb) + + # Step 3. group the outputs into objective function value and others + Y = info["Zextreme"] # objective function + oths = dict(message="none") # other outputs - returned as dict for easy use + + return np.array([Y]), oths, False + + + def droop_step(X, args, Y, oths, Ytarget, err, tols, iter, maxIter): + '''Stepping functions for achieving shared droop target''' + + L = X[0] + [Xf,Zf,EA,W,cb] = args + Zmin = Y[0] + + if Zmin >= -tols[0]: # if nearly no droop at all (in which case derivative will be near zero), add length + dL = 0.1*Xf + + else: # get numerical derivative + deltaL = 2*tols[0] # step size + (Fx1, Fy1, Fx2, Fy2, info) = mp.catenary(Xf, Zf, L+deltaL, EA, W, cb) # evaluate droop in perturbed case + Zmin2 = info["Zextreme"] + dZmin_dL = (Zmin2-Zmin)/deltaL # derivative + + # adjust as per Netwon's method + dL = -err[0]/dZmin_dL + + return np.array([dL]) # returns dX (step to make) + + + # initialize 3D coordinates (can probably go in init) + coords = np.zeros([len(self.coords),3]) + for j in range(len(self.coords)): + if j < self.nPtfm: + coords[j,:] = np.array([self.coords[j][0], self.coords[j][1], 0]) + else: + coords[j,:] = np.array([self.coords[j][0], self.coords[j][1], -self.depth+self.intersectZ[j]]) + + # Just need to get an initial Fx to send to LineDesign -> assume it's a simple catenary for now + + # Loop through each mooring line and update its properties + for ii in range(np.max(self.intraMat)): + mg = self.mooringGroups[ii] # the mooring group shortcut + i = self.group.index(ii+1) # the index of endA/endB where this mooring line object occurs first + + rA = coords[self.endA[i]] # starting coordinates of the line + rB = coords[self.endB[i]] # ending coordinates of the line + + # initialize line parameters + Xf = np.linalg.norm((rA - rB)[0:2]) # horizontal distance (a.k.a. L_xy) + Zf = np.linalg.norm((rA - rB)[2 ]) # vertical distance (aka depth) + L = 1.2*np.hypot(Xf, Zf) # unstretched line length (design variable) + EA = 1232572089.6 # EA value of 120mm chain + W = 2456.820077481978 # W value of 120mm chain + cb = -self.depth # the distance down from end A to the seabed + + + # if anchored, adjust line length to have line on seabed for percent_drag of spacing + if mg['shared']==0: # anchored line + X0 = [L] + LBotTarget = [percent_drag/100*Xf] # target lay length is percent_drag of horizontal anchor spacing + args = [Xf,Zf,EA,W] # the other (constant) arguments needed by catenary + X, Y, info = msolve.dsolve2(laylength_eval, X0, Ytarget=LBotTarget, step_func=laylength_step, args=args, maxIter=20) + + # set line length to the solved value + L = X[0] + # Call catenary function with resized line length + (Fx1, Fy1, Fx2, Fy2, info) = mp.catenary(Xf, Zf, L, EA, W, plots=plots) + + # if shared, adjust the line length to have the line droop for percent_droop of spacing + elif mg['shared']==1 and not mg['net']: # shared line + X0 = [L] + DroopTarget = [-percent_droop/100*self.depth - rB[2]] # target droop elevation relative to fairlead (from percent_droop of depth) + args = [Xf,Zf,EA,W,cb] # the other (constant) arguments needed by catenary + X, Y, info = msolve.dsolve2(droop_eval, X0, Ytarget=DroopTarget, step_func=droop_step, args=args, maxIter=20) + + # set line length to the solved value + L = X[0] + + # Call catenary function with resized line length + (Fx1, Fy1, Fx2, Fy2, info) = mp.catenary(Xf, Zf, L, EA, W, cb, plots = plots) + + elif mg['net'] and not mg.get('tendON', False): # hybrid line (without tendon) + Xf *= 2 + Zf *= 0 + X0 = [2*L] + DroopTarget = [rA[2]] + args = [Xf,Zf,EA,W,cb] + X, Y, info = msolve.dsolve2(droop_eval, X0, Ytarget=DroopTarget, step_func=droop_step, args=args, maxIter=20) + + # set line length to the solved value + L = X[0] + + # Call catenary function with resized line length + (Fx1, Fy1, Fx2, Fy2, info) = mp.catenary(Xf, Zf, L, EA, W, cb, plots = plots) + + elif mg['net'] and mg.get('tendON', False): # hybrid line (with tendon) - assume shared with zero droop + X0 = [L] + DroopTarget = [0] # target droop elevation relative to fairlead (from percent_droop of depth)== + args = [Xf,Zf,EA,W,cb] # the other (constant) arguments needed by catenary + X, Y, info = msolve.dsolve2(droop_eval, X0, Ytarget=DroopTarget, step_func=droop_step, args=args, maxIter=20) + + # set line length to the solved value + L = X[0] + + # Call catenary function with resized line length + (Fx1, Fy1, Fx2, Fy2, info) = mp.catenary(Xf, Zf, L, EA, W, cb, plots = plots) + + Fx = np.abs(info['HF']) # horizontal tension component at fairlead [N] + Kx = info['stiffnessB'][0,0] # effective horizontal stiffness at fairlead [N/m] + kt_over_k = Fx / Kx / Xf # kt/Kx = Fx/L_xy / Kx + + + if plots == 2: + plt.title('Catenary Line Profile for Mooring Line: {}'.format(ii+1)) + + print('Force for Mooring Line {}: {}'.format(ii, Fx)) + print('Stiffness for Mooring Line {}: {}'.format(ii, Kx)) + print('\n{} '.format('kt/k for Mooring Line {}: {}'.format(ii, kt_over_k))) + + + # Update mooringGroups dictionary values with + mg['ten__w'] = Fx/W + mg['kl__w'] = Kx/W + mg['kt__kl'] = kt_over_k + mg['L'] = L # <<<< this is a shortcut that should be done outside of LinearSystem in future + + + + def makeMoorPySystem(self): + ''' sets up a *very simple* MoorPy system using points for the FOWTS, and no bodies ''' + + ms = mp.System(depth=self.depth) + + + # make free points for platforms (assumed at z=0) and fixed points for anchors + for i in range(len(self.coords)): + if i < self.nPtfm: + #ms.addPoint(0, np.hstack([self.coords[i][:2], 0]), m=1e9, v=2e9*ms.rho) # add buoyancy as double the mass - this should make it equilibrate at z=0 + ms.addPoint(0, np.hstack([self.coords[i][:2], 0]), DOFs=[0,1]) # specify as free to move in x and y only + else: + ms.addPoint(1, np.hstack([self.coords[i][:2], -self.depth])) + + + # also add some generic line types, one for each line grouping as defined in the entries of intraMat + for i in range(np.max(self.intraMat)): + shared = i+1 in self.intraMat[:self.nPtfm, :self.nPtfm] # detect if it's a shared line (True) or not (False, assumed anchored) + massden = self.mooringGroups[i]['w']/9.81 + + ms.lineTypes[f"type{i+1}"] = mp.LineType(f"type{i+1}", 0.0, massden, 1.0e15) #, shared=shared) + + + # make lines using adjacency matrix + linenum = 1 + for i in range(len(self.coords)): + for j in range(i): + k = np.int(self.intraMat[i,j]) # the entry in the intraMat corresponds to the line type number (starting from 1) + if k > 0: + ml = self.mooringGroups[k-1] + ms.addLine(ml['L'], f'type{k}') + ms.pointList[i].attachLine(linenum, 1) + ms.pointList[j].attachLine(linenum, 0) + linenum = linenum + 1 + ''' this should be done to coords if it happens anywhere + if self.center: + cx = np.mean([point.r[0] for point in ms.pointList]) + cy = np.mean([point.r[1] for point in ms.pointList]) + ms.transform(trans=[-cx, -cy]) + ''' + + ms.initialize() + + return ms + + + def getYawStiffness(self, rf): + yawStiffness = np.zeros(self.nPtfm) + for i in range(self.nPtfm): + connectedLines = np.where(np.abs(self.StructureMatrix[i*2:i*2+1, :]) > 0)[1] + for j in connectedLines: + if self.boundary[j]: + lineYawStiffness = self.mooringGroups[self.group[j] - 1]['ten']/(2*self.l[j]) * rf**2 + else: + lineYawStiffness = self.mooringGroups[self.group[j] - 1]['ten']/self.l[j] * rf**2 + yawStiffness[i] += lineYawStiffness + return yawStiffness + + + def removeRedundantGroups(self): + ''' + this method: + 1) removes any zero weight groups (lower than 1% of mean weight) + 2) merge mooring groups that are similar to one another (mooring groups that their w are 5% different from the w range), and + 3) reformulates the system and its mooring groups + ''' + # calculate the mean weight for the LinearSystem: + wSum = sum(mg['w'] for mg in self.mooringGroups) + wMax = max(mg['w'] for mg in self.mooringGroups) + wMin = min(mg['w'] for mg in self.mooringGroups) + wMean = wSum / len(self.mooringGroups) + + # find the indices of mooring groups to keep and the ones to remove + remove_indices = [i for i, mg in enumerate(self.mooringGroups) if mg['w'] < 0.01 * wMean] + keep_indices = list(set(range(len(self.mooringGroups))) - set(remove_indices)) + + # Update mooringGroups and profileMap + self.mooringGroups = [self.mooringGroups[i] for i in keep_indices] + self.profileMap = [self.profileMap[i] for i in keep_indices] + + # create a mapping from old indices to new indices + index_mapping = {old: new for new, old in enumerate(keep_indices)} + + # remove lines that belong to redundant mooring groups + new_l, new_u, new_endA, new_endB, new_rA, new_rB, new_angA, new_angB, new_boundary, new_group = [], [], [], [], [], [], [], [], [], [] + + for i in range(len(self.l)): + if self.group[i] - 1 in keep_indices: # subtract 1 because group starts at 1, not 0 + new_group.append(index_mapping[self.group[i] - 1] + 1) + new_l.append(self.l[i]) + new_u.append(self.u[i]) + new_endA.append(self.endA[i]) + new_endB.append(self.endB[i]) + new_rA.append(self.rA[i]) + new_rB.append(self.rB[i]) + new_angA.append(self.angA[i]) + new_angB.append(self.angB[i]) + new_boundary.append(self.boundary[i]) + + # remove from intra-cell adjacency matrix + for remIdx in remove_indices: + for i, mg in enumerate(self.group): + if mg == remIdx + 1: + remA = self.endA[i] + remB = self.endB[i] + self.intraMat[remA, remB], self.intraMat[remB, remA] = 0, 0 + + unique_intra_groups = sorted(np.unique(self.intraMat[self.intraMat > 0])) + intra_group_mapping = {old: new for new, old in enumerate(unique_intra_groups, start=1)} + for old, new in intra_group_mapping.items(): + self.intraMat[self.intraMat == old] = new + # update the properties with the new filtered lists + self.l = new_l + self.u = new_u + self.endA = new_endA + self.endB = new_endB + self.rA = new_rA + self.rB = new_rB + self.angA = new_angA + self.angB = new_angB + self.boundary = new_boundary + self.group = new_group + self.nLines = len(self.l) + self.StructureMatrix = np.zeros([2*self.nPtfm, self.nLines]) # rows: DOFs; columns: lines + + # merge mooring Groups + removeIndex = [] + for i, mg1 in enumerate(self.mooringGroups[:-1]): + for j in range(i+1, len(self.mooringGroups)): + mg2 = self.mooringGroups[j] + # Check following conditions: + # if the difference in weight is minimial, + # if they both have the same shared map, + # and if they have the same length: + con1 = np.abs(mg2['w'] - mg1['w'])/(wMax - wMin) < 0.05 + con2 = self.profileMap[i]==self.profileMap[i+1] + con3 = np.round(mg1['l'], 2)==np.round(mg2['l'], 2) + if con1 and con2 and con3: + self.mooringGroups[i]['w'] = (mg1['w'] + mg2['w']) / 2 + removeIndex.append(j) + self.group = [i+1 if g==j+1 else g for g in self.group] + idx1, idx2 = np.where(self.intraMat==j+1) + self.intraMat[idx1, idx2] = i+1 + + if removeIndex: + for idx in sorted(np.unique(removeIndex), reverse=True): + del self.mooringGroups[idx] + del self.profileMap[idx] + + unique_groups = sorted(np.unique(self.group)) + unique_intra_groups = sorted(np.unique(self.intraMat[self.intraMat > 0])) + group_mapping = {old: new for new, old in enumerate(unique_groups, start=1)} + intra_group_mapping = {old: new for new, old in enumerate(unique_intra_groups, start=1)} + self.group = [group_mapping[g] for g in self.group] + + for old, new in intra_group_mapping.items(): + self.intraMat[self.intraMat == old] = new + + for j in range(self.nLines): + if self.endA[j] < self.nPtfm: # only if not an anchor + self.StructureMatrix[self.endA[j]*2 , j] = self.u[j][0] + self.StructureMatrix[self.endA[j]*2 + 1, j] = self.u[j][1] + + if self.endB[j] < self.nPtfm: # only if not an anchor + self.StructureMatrix[self.endB[j]*2 , j] = -self.u[j][0] + self.StructureMatrix[self.endB[j]*2 + 1, j] = -self.u[j][1] + + # Check if any row/column in the intraMat is empty, delete it, and delete the corresponding self.coords: + empty_rows = np.where(~self.intraMat.any(axis=1))[0] + empty_cols = np.where(~self.intraMat.any(axis=0))[0] + empty_indices = np.unique(np.concatenate((empty_rows, empty_cols))) + if len(empty_indices) > 0: + # Remove empty rows and columns from intraMat + self.intraMat = np.delete(self.intraMat, empty_indices, axis=0) + self.intraMat = np.delete(self.intraMat, empty_indices, axis=1) + self.coords = np.delete(self.coords, empty_indices, axis=0) + self.intersectZ = np.delete(self.intersectZ, empty_indices, axis=0) + + # Reassign the 'type' of each mooring group after deletion + for i, group in enumerate(self.mooringGroups): + group['type'] = i + 1 # Reassign types from 1 to len(mooringGroups) + + self.preprocess() + self.optimize() + +# ------- test script + +if __name__ == '__main__': + + import Array as array + + from moorpy.helpers import printVec, printMat + + # specify the array layouts and their parameters + T = 2000. + A = 1200. + depth = 600. + + ''' + # ----- old examples ----- + + #coords, intraMat, nPtfm, name = array.layout_pair_4_anchs(T, A, deg=120) + #coords, intraMat, nPtfm, name = array.layout_triangle_3_anchs(T, A) + coords, intraMat, nPtfm, name = array.layout_1_square_8_anchs(T, A) + + sys = LinearSystem(coords, intraMat, nPtfm, depth=600., fmax=1e6, + xmax=0.1*min(T,A)) + + + + sys.preprocess() + + q= sys.optimize() + print(q) + sys.plot2d(watch_circles=1, line_val="stiffness") + + #sys.eigenAnalysis(plot=1) + + #sys.updateDesign() + + + # ----- newer more advanced example ----- + ''' + print("New LinearSystem example") + # def __init__(self, coords, intraMat, nPtfm, interMats=None, + # interCoords=None, inits=None, profileMap=None, intersectZ=None, + # rFair=0, zFair=0, depth=600., fmax=1e6, xmax=40.0, plots=0, nonlin=1.0, + # center=True, old_mode=True): + + #coords, intraMat, nPtfm, name = array.Grid3x3(T, A) + #coords, intraMat, nPtfm, name = array.Fat_Hexagon(T, A, fathexagontype='min_linetypes') + coords, intraMat, nPtfm, name = array.Square(T, A, type='water-strider') + + + mooringGroupDict = [ + dict(w=1500, ten=100000, kl=10000, kt=50, shared=False), + #dict(w=1500, ten=100000, kl=10000, kt=50, shared=True), + dict(w=1500, ten=100000, kl=10000, kt=50, shared=True)] #, + ''' + dict(w=1500, ten=100000, kl=10000, kt=50, shared=True), + dict(w=1500, ten=100000, kl=10000, kt=50, shared=True), + dict(w=1500, ten=100000, kl=10000, kt=50, shared=True), + dict(w=1500, ten=100000, kl=10000, kt=50, shared=True), + dict(w=1500, ten=100000, kl=10000, kt=50, shared=True), + dict(w=1500, ten=100000, kl=10000, kt=50, shared=True), + dict(w=1500, ten=100000, kl=10000, kt=50, shared=True), + dict(w=1500, ten=100000, kl=10000, kt=50, shared=True), + dict(w=1500, ten=100000, kl=10000, kt=50, shared=True)] + ''' + + sys = LinearSystem(coords, intraMat, nPtfm, depth=600., fmax=1e6, + xmax=0.1*min(T,A), inits=mooringGroupDict, old_mode=False) + + + sys.preprocess() + + + sys.getSystemStiffness() + sys.windsweep() # figure out watch circles + + sys.plot2d(watch_circles=4, line_val="stiffness") + + + # now try a stiffness optimization + + sys.optimize2(display=2) + sys.plot2d(watch_circles=4, line_val="stiffness") + + ''' + + print("New LinearSystem example - with inter-array shared lines") + # def __init__(self, coords, intraMat, nPtfm, interMats=None, + # interCoords=None, inits=None, profileMap=None, intersectZ=None, + # rFair=0, zFair=0, depth=600., fmax=1e6, xmax=40.0, plots=0, nonlin=1.0, + # center=True, old_mode=True): + + coords, intraMat, nPtfm, name = array.Grid3x3(T, A) + + # remove anchored lines on side E-W turbines (turbine 3 and 5) + intraMat[20,3] = 0 + intraMat[14,5] = 0 + + # make interMats + interMats = [] + a = np.zeros([9,9]) + a[5,3] = 3 # connect turbines 3 and 5 with a shared line + print(a) + interMats.append(np.array(a)) + + # specify a lateral pattern spaced 6 km apart so shared lines all have same length + interCoords = [[600,0]] + + mooringGroupDict = [ + dict(w=1500, ten=100000, kl=10000, kt=500, shared=False), + dict(w=1500, ten=100000, kl=10000, kt=500, shared=True), + dict(w=1500, ten=100000, kl=10000, kt=500, shared=True)] + + + sys = LinearSystem(coords, intraMat, nPtfm, depth=600., fmax=1e6, + xmax=0.1*min(T,A), + interMats = interMats, interCoords = interCoords, + inits=mooringGroupDict, old_mode=False) + + sys.getSystemStiffness() + sys.windsweep() # figure out watch circles + + sys.plot2d(watch_circles=1, line_val="stiffness") + ''' + + # ----- example with inter-array shared lines! ----- + + + ''' + + import fadesign.conceptual.Cell as cell + #coords, intraMat, nPtfm, interMats, interCoords = cell.Grid3x3(T, A) # no inter shared lines! + #coords, intraMat, nPtfm, interMats, interCoords = cell.honeycombPattern(T) # + coords, intraMat, nPtfm, interMats, interCoords = cell.grid() # + + breakpoint() + + mooringGroupDict = [ + dict(w=1500, ten=100000, kl=10000, kt=50, shared=False), + dict(w=1500, ten=100000, kl=10000, kt=50, shared=True), + dict(w=1500, ten=100000, kl=10000, kt=50, shared=True)] + + + sys = LinearSystem(coords, intraMat, nPtfm, interCoords=interCoords, + depth=600., fmax=1e6, + xmax=0.1*min(T,A), inits=mooringGroupDict, old_mode=False) + + sys.getSystemStiffness() + sys.windsweep() # figure out watch circles + + sys.plot2d(watch_circles=1, line_val="stiffness") + + ''' + + + plt.show() \ No newline at end of file diff --git a/famodel/design/fadsolvers.py b/famodel/design/fadsolvers.py new file mode 100644 index 00000000..a9f7d436 --- /dev/null +++ b/famodel/design/fadsolvers.py @@ -0,0 +1,1857 @@ +# a file to hold the custom solvers used in FAD + +import numpy as np +import matplotlib.pyplot as plt +import time +#from scipy.optimize import fsolve +#import scipy.optimize + + +# ================================ original above / modified below =========================================== + + +""" +def eval_func1(X, args): + '''returns target outputs and also secondary outputs for constraint checks etc.''' + + # Step 1. break out design variables and arguments into nice names + + # Step 2. do the evaluation (this may change mutable things in args) + + # Step 3. group the outputs into objective function value and others + + return Y, oths + + + +def step_func1(X, args, Y, oths, Ytarget, err, tol, iter, maxIter): + '''General stepping functions, which can also contain special condition checks or other adjustments to the process + + ''' + + # step 1. break out variables as needed + + # do stepping, as well as any conditional checks + + return dL # returns dX (step to make) +""" + + + +def dsolve1D(eval_func, step_func, X0, Ytarget, args, tol=0.0001, maxIter=20, Xmin=-np.inf, Xmax=np.inf): + ''' + Assumes the function is positive sloped (so use -X if negative-sloped) + + tol - relative convergence tolerance (relative to step size, dX) + Xmin, Xmax - bounds. by default start bounds at infinity + ''' + + X = 1*X0 # start off design variable + + + print(f"Starting dsolve1D iterations>>> aiming for Y={Ytarget}") + + for iter in range(maxIter): + + + # call evaluation function + Y, oths = eval_func(X, args) + + # compute error + err = Y - Ytarget + + print(f" new iteration with X={X:6.2f} and Y={Y:6.2f}") + + # update/narrow the bounds (currently this part assumes that the function is positively sloped) << any N-D equivalent? + if err > 0:# and L < LUpper: # + Xmax = 1.0*X + elif err < 0:# and L > LLower: # + Xmin = 1.0*X + + if iter==maxIter-1: + print("Failed to find solution after "+str(iter)+" iterations, with error of "+str(err)) + breakpoint() + break + + #>>>> COULD ALSO HAVE AN ITERATION RESTART FUNCTION? >>> + # that returns a restart boolean, as well as what values to use to restart things if true. How? + + else: + dX = step_func(X, args, Y, oths, Ytarget, err, tol, iter, maxIter) + + + # check for convergence + if np.abs(dX) < tol*(np.abs(X)+tol): + print("Equilibrium solution completed after "+str(iter)+" iterations with error of "+str(err)+" and dX of "+str(dX)) + print("solution X is "+str(X)) + break + + + # Make sure we're not diverging by keeping things within narrowing bounds that span the solution. + # I.e. detect potential for oscillation and avoid bouncing out and then back in to semi-taut config + # Use previous values to bound where the correct soln is, and if an iteration moves beyond that, + # stop it and put it between the last value and where the bound is (using golden ratio, why not). + if dX > 0 and X+dX >= Xmax: # if moving up and about to go beyond previous too-high value + X = X + 0.62*(Xmax-X) # move to mid point between current value and previous too-high value, rather than overshooting + print("<--|") + elif dX < 0 and X+dX <= Xmin: # if moving down and about to go beyond previous too-low value + X = X + 0.62*(Xmin-X) #0.5*(L+LLower) # move to mid point between current value and previous too-low value, rather than overshooting + print("|-->") + else: + X = X+dX + + + return X, Y, dict(iter=iter, err=err) + + + +# X, Y, info = dsolve1D(eval_func1, step_func1, X0, Ytarget, args, tol=tol, maxIter=maxIter) + + + + +# TODO: add default step_func (finite differencer), Ytarget, and args + +def dsolve(eval_func, X0, Ytarget=[], step_func=None, args=[], tol=0.0001, maxIter=20, + Xmin=[], Xmax=[], a_max=2.0, dX_last=[], display=0): + ''' + PARAMETERS + ---------- + eval_func : function + function to solve (will be passed array X, and must return array Y of same size) + X0 : array + initial guess of X + Ytarget : array (optional) + target function results (Y), assumed zero if not provided + stp_func : function (optional) + function use for adjusting the variables (computing dX) each step. + If not provided, Netwon's method with finite differencing is used. + args : list + A list of variables (e.g. the system object) to be passed to both the eval_func and step_func + tol : float + *relative* convergence tolerance (applied to step size components, dX) + Xmin, Xmax + Bounds. by default start bounds at infinity + a_max + maximum step size acceleration allowed + dX_last + Used if you want to dictate the initial step size/direction based on a previous attempt + ''' + success = False + + # process inputs and format as arrays in case they aren't already + + X = np.array(X0, dtype=np.float_) # start off design variable + N = len(X) + + Xs = np.zeros([maxIter,N]) # make arrays to store X and error results of the solve + Es = np.zeros([maxIter,N]) + dXlist = np.zeros([maxIter,N]) + dXlist2 = np.zeros([maxIter,N]) + + + # check the target Y value input + if len(Ytarget)==N: + Ytarget = np.array(Ytarget, dtype=np.float_) + elif len(Ytarget)==0: + Ytarget = np.zeros(N, dtype=np.float_) + else: + raise TypeError("Ytarget must be of same length as X0") + + + # if a step function wasn't provided, provide a default one + if step_func==None: + if display>1: + print("Using default finite difference step func") + + def step_func(X, args, Y, oths, Ytarget, err, tol, iter, maxIter): + + J = np.zeros([N,N]) # Initialize the Jacobian matrix that has to be a square matrix with nRows = len(X) + + for i in range(N): # Newton's method: perturb each element of the X variable by a little, calculate the outputs from the + X2 = np.array(X) # minimizing function, find the difference and divide by the perturbation (finding dForce/d change in design variable) + deltaX = tol*(np.abs(X[i])+tol) + X2[i] += deltaX + Y2, _, _ = eval_func(X2, args) # here we use the provided eval_func + + J[:,i] = (Y2-Y)/deltaX # and append that column to each respective column of the Jacobian matrix + + if N > 1: + dX = -np.matmul(np.linalg.inv(J), Y-Ytarget) # Take this nth output from the minimizing function and divide it by the jacobian (derivative) + else: + + dX = np.array([-(Y[0]-Ytarget[0])/J[0,0]]) + + if display > 1: + print(f" step_func iter {iter} X={X[0]:9.2e}, error={Y[0]-Ytarget[0]:9.2e}, slope={J[0,0]:9.2e}, dX={dX[0]:9.2e}") + + return dX # returns dX (step to make) + + + + # handle bounds + if len(Xmin)==0: + Xmin = np.zeros(N)-np.inf + elif len(Xmin)==N: + Xmin = np.array(Xmin, dtype=np.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_) + else: + raise TypeError("Xmax must be of same length as X0") + + + + if len(dX_last)==0: + dX_last = np.zeros(N) + else: + dX_last = np.array(dX_last, dtype=np.float_) + + if display>1: + print(f"Starting dsolve iterations>>> aiming for Y={Ytarget}") + + + for iter in range(maxIter): + + + # call evaluation function + Y, oths, stop = eval_func(X, args) + + # compute error + err = Y - Ytarget + + if display>1: + print(f" new iteration #{iter} with X={X} and Y={Y}") + + Xs[iter,:] = X + Es[iter,:] = err + + # stop if commanded by objective function + if stop: + break + + + if iter==maxIter-1: + if display>0: + print("Failed to find solution after "+str(iter)+" iterations, with error of "+str(err)) + breakpoint() + break + + #>>>> COULD ALSO HAVE AN ITERATION RESTART FUNCTION? >>> + # that returns a restart boolean, as well as what values to use to restart things if true. How? + + else: + dX = step_func(X, args, Y, oths, Ytarget, err, tol, iter, maxIter) + + + #if display>2: + # breakpoint() + + # Make sure we're not diverging by keeping things from reversing too much. + # Track the previous step (dX_last) and if the current step reverses too much, stop it part way. + # Stop it at a plane part way between the current X value and the previous X value (using golden ratio, why not). + + # get the point along the previous step vector where we'll draw the bounding hyperplane (could be a line, plane, or more in higher dimensions) + Xlim = X - 0.62*dX_last + + # the equation for the plane we don't want to recross is then sum(X*dX_last) = sum(Xlim*dX_last) + if np.sum((X+dX)*dX_last) < np.sum(Xlim*dX_last): # if we cross are going to cross it + + alpha = np.sum((Xlim-X)*dX_last)/np.sum(dX*dX_last) # this is how much we need to scale down dX to land on it rather than cross it + + if display > 2: + print(" limiting oscillation with alpha="+str(alpha)) + print(f" dX_last was {dX_last}, dX was going to be {dX}, now it'll be {alpha*dX}") + print(f" dX_last was {dX_last/1000}, dX was going to be {dX/1000}, now it'll be {alpha*dX/1000}") + + dX = alpha*dX # scale down dX + + # also avoid extreme accelerations in the same direction + if np.linalg.norm(dX_last) > tol: # only worry about accelerations if the last step was non-negligible + for i in range(N): + + if abs(dX_last[i]) < tol: # set the maximum permissible dx in each direction based an an acceleration limit + dX_max = a_max*10*tol*np.sign(dX[i]) + else: + dX_max = a_max*dX_last[i] + + if dX_max == 0.0: # avoid a divide-by-zero case (if dX[i] was zero to start with) + dX[i] = 0.0 + else: + a_i = dX[i]/dX_max # calculate ratio of desired dx to max dx + + if a_i > 1.0: + + if display > 2: + print(f" limiting acceleration ({1.0/a_i:6.4f}) for axis {i}") + print(f" dX_last was {dX_last}, dX was going to be {dX}") + + #dX = dX*a_max/a_i # scale it down to the maximum value + dX[i] = dX[i]/a_i # scale it down to the maximum value (treat each DOF individually) + + if display > 2: + print(f" now dX will be {dX}") + + dXlist[iter,:] = dX + if iter==196: + breakpoint() + # enforce bounds + for i in range(N): + + if X[i] + dX[i] < Xmin[i]: + dX[i] = Xmin[i] - X[i] + + elif X[i] + dX[i] > Xmax[i]: + dX[i] = Xmax[i] - X[i] + + dXlist2[iter,:] = dX + # check for convergence + if all(np.abs(dX) < tol*(np.abs(X)+tol)): + + if display>0: + print(f"dsolve converged. iter={iter}, X={X}, error={err} and dX={dX}") + + #if abs(err) > 10: + # breakpoint() + + if any(X == Xmin) or any(X == Xmax): + success = False + print("Warning: dsolve ended on a bound.") + else: + success = True + + break + + dX_last = 1.0*dX # remember this current value + + + X = X + dX + + + return X, Y, dict(iter=iter, err=err, dX=dX_last, oths=oths, Xs=Xs, Es=Es, success=success, dXlist=dXlist, dXlist2=dXlist2) + + +def dsolve2(eval_func, X0, Ytarget=[], step_func=None, args=[], tol=0.0001, maxIter=20, + Xmin=[], Xmax=[], a_max=2.0, dX_last=[], stepfac=4, display=0): + ''' + PARAMETERS + ---------- + eval_func : function + function to solve (will be passed array X, and must return array Y of same size) + X0 : array + initial guess of X + Ytarget : array (optional) + target function results (Y), assumed zero if not provided + stp_func : function (optional) + function use for adjusting the variables (computing dX) each step. + If not provided, Netwon's method with finite differencing is used. + args : list + A list of variables (e.g. the system object) to be passed to both the eval_func and step_func + tol : float or array + If scalar, the*relative* convergence tolerance (applied to step size components, dX). + If an array, must be same size as X, and specifies an absolute convergence threshold for each variable. + Xmin, Xmax + Bounds. by default start bounds at infinity + a_max + maximum step size acceleration allowed + dX_last + Used if you want to dictate the initial step size/direction based on a previous attempt + ''' + success = False + 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 + N = len(X) + + Xs = np.zeros([maxIter,N]) # make arrays to store X and error results of the solve + Es = np.zeros([maxIter,N]) + dXlist = np.zeros([maxIter,N]) + dXlist2 = np.zeros([maxIter,N]) + + + # check the target Y value input + if len(Ytarget)==N: + Ytarget = np.array(Ytarget, dtype=np.float_) + elif len(Ytarget)==0: + Ytarget = np.zeros(N, dtype=np.float_) + else: + raise TypeError("Ytarget must be of same length as X0") + + # ensure all tolerances are positive + 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([toli <= 0 for toli in tol]): + raise ValueError('every tol entry passed to dsovle2 must be positive') + + + # handle bounds + if len(Xmin)==0: + Xmin = np.zeros(N)-np.inf + elif len(Xmin)==N: + Xmin = np.array(Xmin, dtype=np.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_) + else: + raise TypeError("Xmax must be of same length as X0") + + + # if a step function wasn't provided, provide a default one + if step_func==None: + if display>1: + print("Using default finite difference step func") + + def step_func(X, args, Y, oths, Ytarget, err, tols, iter, maxIter): + ''' this now assumes tols passed in is a vector''' + J = np.zeros([N,N]) # Initialize the Jacobian matrix that has to be a square matrix with nRows = len(X) + + for i in range(N): # Newton's method: perturb each element of the X variable by a little, calculate the outputs from the + X2 = np.array(X) # minimizing function, find the difference and divide by the perturbation (finding dForce/d change in design variable) + deltaX = stepfac*tols[i] # note: this function uses the tols variable that is computed in dsolve based on the tol input + X2[i] += deltaX + Y2, _, _ = eval_func(X2, args) # here we use the provided eval_func + + J[:,i] = (Y2-Y)/deltaX # and append that column to each respective column of the Jacobian matrix + + if N > 1: + dX = -np.matmul(np.linalg.inv(J), Y-Ytarget) # Take this nth output from the minimizing function and divide it by the jacobian (derivative) + else: + # if the result of the eval_func did not change, increase the stepfac parameter by a factor of 10 and calculate the Jacobian again + if J[0,0] == 0.0: + + stepfacb = stepfac*10 + + J = np.zeros([N,N]) # Initialize the Jacobian matrix that has to be a square matrix with nRows = len(X) + for i in range(N): # Newton's method: perturb each element of the X variable by a little, calculate the outputs from the + X2b = np.array(X) # minimizing function, find the difference and divide by the perturbation (finding dForce/d change in design variable) + deltaXb = stepfacb*tols[i] # note: this function uses the tols variable that is computed in dsolve based on the tol input + X2b[i] += deltaXb + Y2b, _, _ = eval_func(X2b, args) # here we use the provided eval_func + J[:,i] = (Y2b-Y)/deltaXb # and append that column to each respective column of the Jacobian matrix + + if J[0,0] == 0.0: # if the Jacobian is still 0, maybe increase the stepfac again, but there might be a separate issue + #breakpoint() + raise ValueError('dsolve2 found a zero gradient - maybe a larger stepfac is needed.') + + # if the Jacobian is all good, then calculate the dX + dX = np.array([-(Y[0]-Ytarget[0])/J[0,0]]) + + if display > 1: + print(f" step_func iter {iter} X={X[0]:9.2e}, error={Y[0]-Ytarget[0]:9.2e}, slope={J[0,0]:9.2e}, dX={dX[0]:9.2e}") + + return dX # returns dX (step to make) + + + if len(dX_last)==0: + dX_last = np.zeros(N) + else: + dX_last = np.array(dX_last, dtype=np.float_) + + if display>0: + print(f"Starting dsolve iterations>>> aiming for Y={Ytarget}") + + + for iter in range(maxIter): + + + # call evaluation function + Y, oths, stop = eval_func(X, args) + + # compute error + err = Y - Ytarget + + if display>2: + print(f" new iteration #{iter} with X={X} and Y={Y}") + + Xs[iter,:] = X + Es[iter,:] = err + + # stop if commanded by objective function + if stop: + break + + # handle tolerances input + if np.isscalar(tol): + tols = tol*(np.abs(X)+tol) + else: + tols = np.array(tol) + + # check maximum iteration + if iter==maxIter-1: + if display>0: + print("Failed to find solution after "+str(iter)+" iterations, with error of "+str(err)) + + # looks like things didn't converge, so if N=1 do a linear fit on the last 30% of points to estimate the soln + if N==1: + + 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]) + if display>1: + print(f"Using linear fit to estimate solution at X={b}") + + break + + #>>>> COULD ALSO HAVE AN ITERATION RESTART FUNCTION? >>> + # that returns a restart boolean, as well as what values to use to restart things if true. How? + + else: + dX = step_func(X, args, Y, oths, Ytarget, err, tols, iter, maxIter) + + + #if display>2: + # breakpoint() + + # Make sure we're not diverging by keeping things from reversing too much. + # Track the previous step (dX_last) and if the current step reverses too much, stop it part way. + # Stop it at a plane part way between the current X value and the previous X value (using golden ratio, why not). + + # get the point along the previous step vector where we'll draw the bounding hyperplane (could be a line, plane, or more in higher dimensions) + Xlim = X - 0.62*dX_last + + # the equation for the plane we don't want to recross is then sum(X*dX_last) = sum(Xlim*dX_last) + if np.sum((X+dX)*dX_last) < np.sum(Xlim*dX_last): # if we cross are going to cross it + + alpha = np.sum((Xlim-X)*dX_last)/np.sum(dX*dX_last) # this is how much we need to scale down dX to land on it rather than cross it + + if display > 2: + print(" limiting oscillation with alpha="+str(alpha)) + print(f" dX_last was {dX_last}, dX was going to be {dX}, now it'll be {alpha*dX}") + print(f" dX_last was {dX_last/1000}, dX was going to be {dX/1000}, now it'll be {alpha*dX/1000}") + + dX = alpha*dX # scale down dX + + # also avoid extreme accelerations in the same direction + for i in range(N): + + if abs(dX_last[i]) > tols[i]: # only worry about accelerations if the last step was non-negligible + + dX_max = a_max*dX_last[i] # set the maximum permissible dx in each direction based an an acceleration limit + + if dX_max == 0.0: # avoid a divide-by-zero case (if dX[i] was zero to start with) + breakpoint() + dX[i] = 0.0 + else: + a_i = dX[i]/dX_max # calculate ratio of desired dx to max dx + + if a_i > 1.0: + + if display > 2: + print(f" limiting acceleration ({1.0/a_i:6.4f}) for axis {i}") + print(f" dX_last was {dX_last}, dX was going to be {dX}") + + #dX = dX*a_max/a_i # scale it down to the maximum value + dX[i] = dX[i]/a_i # scale it down to the maximum value (treat each DOF individually) + + if display > 2: + print(f" now dX will be {dX}") + + dXlist[iter,:] = dX + #if iter==196: + #breakpoint() + + # enforce bounds + for i in range(N): + + if X[i] + dX[i] < Xmin[i]: + dX[i] = Xmin[i] - X[i] + + elif X[i] + dX[i] > Xmax[i]: + dX[i] = Xmax[i] - X[i] + + dXlist2[iter,:] = dX + # check for convergence + if all(np.abs(dX) < tols): + + if display>0: + print("Iteration converged after "+str(iter)+" iterations with error of "+str(err)+" and dX of "+str(dX)) + print("Solution X is "+str(X)) + + #if abs(err) > 10: + # breakpoint() + + if display > 0: + print("Total run time: {:8.2f} seconds = {:8.2f} minutes".format((time.time() - start_time),((time.time() - start_time)/60))) + + + if any(X == Xmin) or any(X == Xmax): + success = False + print("Warning: dsolve ended on a bound.") + else: + success = True + + break + + dX_last = 1.0*dX # remember this current value + + + X = X + dX + + + return X, Y, dict(iter=iter, err=err, dX=dX_last, oths=oths, Xs=Xs, Es=Es, success=success, dXlist=dXlist, dXlist2=dXlist2) + + +def dsolvePlot(info): + '''Plots dsolve or dsolve solution process based on based dict of dsolve output data''' + + n = info['Xs'].shape[1] # number of variables + + if n < 8: + fig, ax = plt.subplots(2*n, 1, sharex=True) + for i in range(n): + ax[ i].plot(info['Xs'][:info['iter']+1,i]) + ax[n+i].plot(info['Es'][:info['iter']+1,i]) + ax[-1].set_xlabel("iteration") + else: + fig, ax = plt.subplots(n, 2, sharex=True) + for i in range(n): + ax[i,0].plot(info['Xs'][:info['iter']+1,i]) + ax[i,1].plot(info['Es'][:info['iter']+1,i]) + ax[-1,0].set_xlabel("iteration, X") + ax[-1,1].set_xlabel("iteration, Error") + plt.show() + + +def dopt(eval_func, X0, tol=0.0001, maxIter=20, Xmin=[], Xmax=[], a_max=1.2, dX_last=[], display=0, stepfac=10): + ''' + Multi-direction Newton's method solver. + + tol - *relative* convergence tolerance (applied to step size components, dX) + Xmin, Xmax - bounds. by default start bounds at infinity + a_max - maximum step size acceleration allowed + stepfac - factor to increase step size to relative to tol*X0 + ''' + start_time = time.time() + + success = False + lastConverged = False # flag for whether the previous iteration satisfied the convergence criterion + + # process inputs and format as arrays in case they aren't already + if len(X0) == 0: + raise ValueError("X0 cannot be empty") + + X = np.array(X0, dtype=np.float_) # start off design variable (optimized) + + # do a test call to see what size the results are + f, g, Xextra, Yextra, oths, stop = eval_func(X) #, XtLast, Ytarget, args) + + N = len(X) # number of design variables + Nextra = len(Xextra) # additional relevant variables calculated internally and passed out, for tracking + m = len(g) # number of constraints + + Xs = np.zeros([maxIter, N + Nextra]) # make arrays to store X and error results of the solve + Fs = np.zeros([maxIter]) # make arrays to store objective function values + Gs = np.zeros([maxIter, m]) # make arrays to store constraint function values + + + + if len(Xmin)==0: + Xmin = np.zeros(N)-np.inf + elif len(Xmin)==N: + Xmin = np.array(Xmin, dtype=np.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_) + else: + raise TypeError("Xmax must be of same length as X0") + + + if len(dX_last)==N: + dX_last = np.array(dX_last, dtype=np.float_) + elif len(dX_last)==0: + dX_last = np.zeros(N) + else: + raise ValueError("dX_last input must be of same size as design vector, if provided") + #XtLast = 1.0*Xt0 + + # set finite difference step size + #dX_fd = 4.0 #0.5# 1.0*dX[i] # this is gradient finite difference step size, not opto step size + dX_fd = stepfac*X*tol # set dX_fd as function of tolerance and initial values + + + + if display > 0: + print("Starting dopt iterations>>>") + + for iter in range(maxIter): + iter_start_time = time.time() + + # call evaluation function (returns objective val, constrain vals, tuned variables, tuning results) + f, g, Xextra, Yextra, oths, stop = eval_func(X) #, XtLast, Ytarget, args) + + if display > 1: print("") + if display > 0: + + if isinstance(Xextra, list): + XextraDisp = Xextra + else: + XextraDisp = Xextra.tolist() + + print((" >> Iteration {:3d}: f={:8.2e} X="+"".join([" {:9.2f}"]*len(X))+" Xe="+"".join([" {:9.2f}"]*len(Xextra))).format(*( + [ iter , f ] + X.tolist() + XextraDisp) )) + + + if display > 1: print(f"\n Constraint values: {g}") + + Xs[iter,:] = np.hstack([X, Xextra]) + Fs[iter] = f + Gs[iter,:] = g + + + # stop if commanded by objective function + if stop: + message = 'Received stop command from objective function' + break + + # temporarily display output + #print(np.hstack([X,Y])) + + + if iter==maxIter-1: + + print("Failed to converge after "+str(iter)+" iterations") + + if any(X == Xmin) or any(X == Xmax) or any(g < 0.0): + for i in range(N): + if X[i] == Xmin[i] : print(f" Warning: Design variable {i} ended on minimum bound {Xmin[i]}.") + if X[i] == Xmax[i] : print(f" Warning: Design variable {i} ended on maximum bound {Xmax[i]}.") + + for j in range(m): # go through each constraint + if g[j] < 0: # if a constraint will be violated + print(f" Warning: Constraint {j} was violated by {-g[j]}.") + else: + print(" No constraint or bound issues.") + + success = False + break + + #>>>> COULD ALSO HAVE AN ITERATION RESTART FUNCTION? >>> + # that returns a restart boolean, as well as what values to use to restart things if true. How? + + else: # this is where we get derivatives and then take a step + + #dX = step_func(X, args, Y, oths, Ytarget, err, tol, iter, maxIter) + # hard coding a generic approach for now + + dX = np.zeros(N) # optimization step size to take + + X2 = np.array(X, dtype=np.float_) + + Jf = np.zeros([N]) + Jg = np.zeros([N,m]) + Hf = np.zeros([N]) # this is just the diagonal of the Hessians + Hg = np.zeros([N,m]) + + for i in range(N): # loop through each variable + + # could do repetition to hone in when second derivative is large, but not going to for now + # or if first derivative is zero (in which case take a larger step size) + + X2[i] += dX_fd[i] # perturb + + fp, gp, Xtp, Yp, othsp, stopp = eval_func(X2) + X2[i] -= 2.0*dX_fd[i] # perturb - + fm, gm, Xtm, Ym, othsm, stopm = eval_func(X2) + X2[i] += dX_fd[i] # restore to original + + # for objective function and constraints (note that g may be multidimensional), + # fill in diagonal values of Jacobian and Hession (not using off-diagonals for now) + Jf[i] = (fp-fm) /(2*dX_fd[i]) + Jg[i,:] = (gp-gm) /(2*dX_fd[i]) + Hf[i] = (fm-2.0*f+fp) /dX_fd[i]**2 + Hg[i,:] = (gm-2.0*g+gp) /dX_fd[i]**2 + + #breakpoint() + + # If we're currently violating a constraint, fix it rather than worrying about the objective function + # This step is when new gradients need to be calculated at the violating point + # e.g. in cases where the constraint functions are flat when not violated + if any(g < 0.0): + + if display > 3: + print(" CONSTRAINT HANDLING SECTION") + for i in range(len(Jg)): + print(f" Jg[{i}] = {np.round(Jg[i],5)}") + #print((" Jg[{:3d}] = "+"".join([" {:6.2f}"]*m).format(*([i]+Jg[i].tolist())))) + + g0 = [] + gradg = [] + #sqg = [] + + # first get the gradient of each active constraint + stepdir = np.zeros(N) # this is the direction we will step in + + for j in range(m): # go through each constraint + if g[j] < 0: # if a constraint will be violated + if np.sum(np.abs(Jg[:,j])) == 0.0: + print(f"dopt error, zero Jacobian for constraint {j}. g(X) may be flat or dX_fd may be too small") + stop=True # set flag to exit iteration + message = f"Error, zero Jacobian for constraint {j}. g(X) may be flat or dX_fd may be too small" + break + + g0.append( g[j]) # constraint value at the current location + gradg.append(Jg[:,j]) # gradient for each active constraint <<< doesn't work so well + #sqg.append( np.sum(Jg[:,j]*Jg[:,j])) # gradient dotted with itself (i.e. sum of squares) + + + # OG output for comparison + stepdir_i = 1.0*Jg[:,j] # default is to assume we're moving in the same direction as the gradient since that's most efficient + for i in range(N): + if (X[i]==Xmin[i] and Jg[i,j]<0) or (X[i]==Xmax[i] and Jg[i,j]>0): # but if any dimension is on its bound, and the gradient is to move in that direction + stepdir_i[i] = 0.0 # set its component to zero instead (other dimensions will now have to move farther) + alph = (0.0-g[j])/np.sum(Jg[:,j]*stepdir_i) # for our selected step direction, find how far to move to get to zero + if np.sum(Jg[:,j]*stepdir_i) == 0.0: + print('NaN isue') + + dXcon = stepdir_i*alph *1.1 # step is step direction vector (possibly gradient) times alpha (plus a little extra for margin) - add the step command from each violated constraint + + if display > 3: + print(f' - Looking at g[{j}]') + print(" stepdir_i = "+"".join([" {:.5f}"]*len(stepdir_i)).format(*(stepdir_i.tolist()))) + print(" alph = ",alph) + print(" g0 = ",g0) + print(" gradg = ",gradg) + + if display > 1: + print((" Con {:3d} OG correction"+"".join([" {:9.2f}"]*N)).format(*( [j]+ dXcon.tolist()) )) + + + # now zero any dimensions that are about to cross a bound (if we're already at the bound) + for i in range(N): + + for j in range(len(g0)): # look through each active constraint (but apply zeroing to all active constraints for now) + if (X[i]==Xmin[i] and gradg[j][i]<0) or (X[i]==Xmax[i] and gradg[j][i]>0): # but if any dimension is on its bound, and the gradient is to move in that direction + for k in range(len(g0)): + gradg[k][i] = 0.0 # set its component to zero instead (other dimensions will now have to move farther) + if display > 3: print('gradg',gradg) + if display > 3: print(' - No bounds issues') + sqg = [ np.sum(jac*jac) for jac in gradg] # update the gradient dotted with itself (i.e. sum of squares) + + + if display > 3: print(' - Find stepdir') + # now sort out a combined step direction depending on the active constraints + if len(g0) == 2 and np.sum(gradg[0]*gradg[1]) < 0 and N>1: # if two active constraints in opposing directions + c1 = g0[0]/sqg[0] * ( np.sum(gradg[0]*gradg[1]) * gradg[1]/sqg[1] - gradg[0] ) + + c2 = g0[1]/sqg[1] * ( np.sum(gradg[0]*gradg[1]) * gradg[0]/sqg[0] - gradg[1] ) + stepdir = c1 + c2 + if display > 3: print(f' A: c1={c1}, c2={c2}') + + else: # all other cases - assume we're moving in the same direction as the gradient since that's most efficient + #c2 = [(-g0[j])/np.sum(gradg[j]*gradg[j])*gradg[j] for j in range(len(g0))] # compute step directions that will zero each constraint + + c = np.zeros([len(g0), N]) + for j in range(len(g0)): # compute step directions that will zero each constraint + if np.sum(gradg[j]*gradg[j]) > 0: # just leave it as zero if any direction has a zero derivative + c[j,:] = -g0[j] / np.sum(gradg[j]*gradg[j]) * gradg[j] + if display > 3: print(f' B: c={c}') + else: + if display > 0: + print(f' dopt warning: zero gradient squared for active constraint {j} at iter={iter} and X={X}') + + #stepdir=sum(c2) + stepdir = np.sum(c, axis=0) # step is step direction vector (possibly gradient) times alpha (plus a little extra for margin) - add the step command from each violated constraint + if display > 3: print(' stepdir = ',stepdir) + + + if np.linalg.norm(stepdir)==0: + stop = True + break + + + if display > 3: print(' - Find alpha') + # now find how large the step needs to be to satisfy each active constraint + alpha = 0.0 # this is the scalar that determines how far we will step in the direction + for j in range(m): # go through each constraint + if g[j] < 0: # if a constraint will be violated + alpha_i = (0.0-g[j])/np.sum(Jg[:,j]*stepdir)# for this constraint, find how far to move along the step direction to get to zero + + alpha = np.max([alpha, alpha_i]) + if display > 3: print(' alpha_i =',alpha_i) + # if an acceleration limit will be applied in some dimension, it'd be nice to revise the direction and recompute <<< + + #dXcon = stepdir*alpha *1.1 # step is step direction vector (possibly gradient) times alpha (plus a little extra for margin) - add the step command from each violated constraint + + #if display > 1: + #print(f" Constraint {j:3d} active).") + #print((" Con {:3d} correction: "+"".join([" {:9.2f}"]*N)).format(*( [j]+ dXcon.tolist()) )) + #if display > 2: + # print((" J = "+"".join([" {:9.2e}"]*m)).format(*Jg[:,j].tolist() )) + # print((" H = "+"".join([" {:9.2e}"]*m)).format(*Hg[:,j].tolist() )) + + dX = stepdir*alpha *1.1 # step is step direction vector (possibly gradient) times alpha (plus a little extra for margin) + + + if display > 1: + print((" Total constraint step (dX) :"+"".join([" {:9.2f}"]*N)).format(*dX.tolist()) ) + + #if iter==4 or iter==5: + # breakpoint() + + # if the above fails, we could try backtracking along dX_last until the constriant is no longer violated... + + # at the end of this, the step will be a summation of the steps estimated to resolve each constraint - good idea? + + # otherwise make an optimization step + else: + if display > 3: print(" OPTIMIZATION STEP SECTION") + + # figure out step size in each dimension + dxType = ['none']*N + for i in range(N): + if Hf[i] <= 0.1*abs(Jf[i])/np.linalg.norm(dX_last): # if the hessian is very small or negative, just move a fixed step size + #dX[i] = -Jf[i]/np.linalg.norm(Jf) * np.abs(dX_last[i]) * a_max*0.9 + dX[i] = -Jf[i]/np.linalg.norm(Jf) * np.linalg.norm(dX_last) * a_max + if display > 3: print(Jf[i], np.linalg.norm(Jf), np.linalg.norm(dX_last), dX_fd[i]) + # but make sure the step size is larger than the convergence tolerance + if abs(dX[i]) <= tol*(np.abs(X[i])+tol): + dX[i] = np.sign(dX[i])*tol*(np.abs(X[i])+tol)*1.1 + + dxType[i] = 'fixed' + else: + dX[i] = -Jf[i]/Hf[i] + + dxType[i] = 'hessian' + + #dX[i] = -Jf[i]/np.linalg.norm(Jf) * np.linalg.norm(dX_last) * a_max # << trying a fixed step size approach (no hessian) + + if display > 1: + print((" Minimization step, dX = "+"".join([" {:9.2f}"]*N)).format(*dX.tolist() )) + if display > 2: + print((" step type "+"".join([" {:9}"]*N)).format(*dxType )) + if display > 2: + print((" J = "+"".join([" {:9.2f}"]*N)).format(*Jf.tolist() )) + print((" H = "+"".join([" {:9.2f}"]*N)).format(*Hf.tolist() )) + #breakpoint() + + if any(np.isnan(dX)): + breakpoint() + + dX_min0 = np.array(dX) + + # respect bounds (handle each dimension individually) + for i in range(N): + if X[i] + dX[i] < Xmin[i]: + dX[i] = Xmin[i] - X[i] + elif X[i] + dX[i] > Xmax[i]: + dX[i] = Xmax[i] - X[i] + + dX_minB = np.array(dX) + + # deal with potential constraint violations in making the step (based on existing gradients) + # respect constraints approximately (ignore cross-couplings...for now) + X2 = X + dX # save jump before constraint correction + for j in range(m): # go through each constraint + g2j = g[j] + np.sum(Jg[:,j]*dX) # estimate constraint value after planned step + if g2j < 0: # if the constraint will be violated + + # option 1: assume we complete the step, then follow the constraint gradient up to resolve the constraint violation + alpha = -g2j / np.sum(Jg[:,j]*Jg[:,j]) # assuming we follow the gradient, finding how far to move to get to zero + + if display > 2 and alpha > 2000: + breakpoint() + + dX = dX + alpha*Jg[:,j]*1.05 # step size is gradient times alpha (adding a little extra for margin) + + + # option 2: just stop short of where the constraint would be violated along the original dX path (inferior because it gets bogged up when against a constraint) + #alpha = -g[j] / np.sum(Jg[:,j]*dX) + #dX = alpha * dX * 0.95 + + if display > 1: + print((" trimin step: (j={:2d},{:6.3f}):"+"".join([" {:9.2f}"]*N)).format( + *( [j, alpha] + dX.tolist()))) + + + # this is how to stop the dX vector at the approximate constraint boundary (not good for navigation) + #for j in len(g): # go through each constraint + # if g[j] + np.sum(Jg[:,j]*dX) < 0: # if a constraint will be violated + # alpha = -g[j]/np.sum(Jg[:,j]*dX) # find where the constraint boundary is (linear approximation) + # dX = dX*alpha # shrink the step size accordingly (to stop at edge of constraint) + + if stop: + break + + + # Make sure we're not diverging by keeping things from reversing too much. + # Track the previous step (dX_last) and if the current step reverses too much, stop it part way. + + ''' + # Original approach: Stop it at a plane part way between the current X value and the previous X value (using golden ratio, why not). + # This means scaling down the full vector (while preserving its direction). The downside is this limits all dimensions. + # get the point along the previous step vector where we could draw a bounding hyperplane (could be a line, plane, or more in higher dimensions) + Xlim = X - 0.62*dX_last + # the equation for the plane we don't want to recross is then sum(X*dX_last) = sum(Xlim*dX_last) + if np.sum((X+dX)*dX_last) < np.sum(Xlim*dX_last): # if we cross are going to cross it + alpha = np.sum((Xlim-X)*dX_last)/np.sum(dX*dX_last) # this is how much we need to scale down dX to land on it rather than cross it + dX = alpha*dX # scale down dX + if display > 1: + print((" (alpha={:9.2e}) to "+"".join([" {:8.2e}"]*N)).format( + ''' + # Revised approach: only scale down the directions that have reversed sign from the last step. + for i in range(N): + if np.sign(dX[i])==-np.sign(dX_last[i]) and abs(dX_last[i]) > tol: # if this dimension is reversing direction + + ratio = np.abs(dX[i]/dX_last[i]) # if it's reversing by more than 62% of the last step in this dimension, limit it + if ratio > 0.62: + dX[i] = dX[i]*0.62/ratio # scale down dX so that it is just 62% of the dX_last + + if display > 1: + print((" oscil limit: (i={:2d},{:6.3f},{:7.3f}):"+"".join([" {:9.2f}"]*N)).format( + *( [i, 0.62/ratio, ratio] + dX.tolist() ))) + + + # also avoid extreme accelerations in the same direction + if np.linalg.norm(dX_last) > tol: # only worry about accelerations if the last step was non-negligible + for i in range(N): + + # set the maximum permissible dx in each direction based an an acceleration limit + if abs(dX_last[i]) < tol: + dX_max = a_max*10*tol*np.sign(dX[i]) + #if abs(dX_last[i]) < tol*(np.abs(X[i])+tol): + # dX_max = a_max*tol*(np.abs(X[i])+tol)*np.sign(dX[i]) + else: + dX_max = a_max*dX_last[i] + #print('dX_max',dX_max, dX_last, tol, dX) + if dX_max == 0.0: # avoid a divide-by-zero case (if dX[i] was zero to start with) + dX[i] = 0.0 + else: + a_i = dX[i]/dX_max # calculate ratio of desired dx to max dx + #print('a_i',a_i, i, dX[i]) + if a_i > 1.0: + + #dX = dX/a_i # scale it down to the maximum value <<< this needs to be in the conditional if X[i] > Xmin[i] and X[i] < Xmax[i]: # limit this direction if it exceeds the limit and if it's not on a bound (otherwise things will get stuck) + # NOTE: if this has problems with making the dX too small, triggering convergence, try the individual approach below <<< + dX[i] = dX[i]/a_i # scale it down to the maximum value (treat each DOF individually) + #print(dX[i]) + if display > 1: + print((" accel limit: (i={:2d},{:6.3f},{:7.3f}):"+"".join([" {:9.2f}"]*N)).format( + *( [i, 1.0/a_i, a_i ] + dX.tolist()))) + + + + # enforce bounds + for i in range(N): + if X[i] + dX[i] < Xmin[i]: + dX[i] = Xmin[i] - X[i] + if display > 3: print(f" Minimum bounds adjustment for dX[{i}]") + elif X[i] + dX[i] > Xmax[i]: + dX[i] = Xmax[i] - X[i] + if display > 3: print(f" Maximum bounds adjustment for dX[{i}]") + + + # check for convergence + if all(np.abs(dX) < tol*(np.abs(X)+tol)): + + if lastConverged: # only consider things converged if the last iteration also satisfied the convergence criterion + + if display>0: + print(f"Optimization converged after {iter} iterations with dX of {dX}") + print(f"Solution X is "+str(X)) + print(f"Constraints are "+str(g)) + + if any(X == Xmin) or any(X == Xmax): + if display>0: + for i in range(N): + if X[i] == Xmin[i] : print(f" Warning: Design variable {i} ended on minimum bound {Xmin[i]}.") + if X[i] == Xmax[i] : print(f" Warning: Design variable {i} ended on maximum bound {Xmax[i]}.") + + success = True + message = "converged on one or more bounds" + + elif any(X == Xmin) or any(X == Xmax) or any(g < 0.0): + if display>0: + for j in range(m): # go through each constraint + if g[j] < 0: # if a constraint will be violated + print(f" Warning: Constraint {j} was violated by {-g[j]}.") + + success = False + message = f"converged with one or more constraints violated (by max {-min(g):7.1e})" + + else: + success = True + message = "converged with no constraint violations or active bounds" + break + + else: + lastConverged = True # if this is the first time the convergence criterion has been met, note it and keep going + message = "convergence criteria only met once (need twice in a row)" + else: + lastConverged = False + message = "not converged" + + if display > 2: + print(f" Convergence message: {message}") + + dX_last = 1.0*dX # remember this current value + #XtLast = 1.0*Xt + #if iter==3: + #breakpoint() + X = X + dX + + if display > 1: + + print((" dopt iteration finished. dX= "+"".join([" {:9.2f}"]*N)).format(*(dX.tolist()))) + print(" iteration run time: {:9.2f} seconds".format(time.time() - iter_start_time)) + + + if display > 2: + print(f" Convergence message: {message}") + + if display > 0: + print(" total run time: {:8.2f} seconds = {:8.2f} minutes".format((time.time() - start_time),((time.time() - start_time)/60))) + + return X, f, dict(iter=iter, dX=dX_last, oths=oths, Xs=Xs, Fs=Fs, Gs=Gs, Xextra=Xextra, g=g, Yextra=Yextra, + success=success, message=message) + + + +def dopt2(eval_func, X0, tol=0.0001, maxIter=20, Xmin=[], Xmax=[], a_max=1.2, dX_last=[], display=0, stepfac=10, args=[]): + ''' + Gradient descent solver with some line search capability + + tol - *relative* convergence tolerance (applied to step size components, dX) + Xmin, Xmax - bounds. by default start bounds at infinity + a_max - maximum step size acceleration allowed + stepfac - factor to increase step size to relative to tol*X0 + ''' + start_time = time.time() + + success = False + lastConverged = False # flag for whether the previous iteration satisfied the convergence criterion + + # process inputs and format as arrays in case they aren't already + if len(X0) == 0: + raise ValueError("X0 cannot be empty") + + X = np.array(X0, dtype=np.float_) # start off design variable (optimized) + + # do a test call to see what size the results are + f, g, Xextra, Yextra, oths, stop = eval_func(X, args) #, XtLast, Ytarget, args) + + N = len(X) # number of design variables + Nextra = len(Xextra) # additional relevant variables calculated internally and passed out, for tracking + m = len(g) # number of constraints + + Xs = np.zeros([maxIter, N + Nextra]) # make arrays to store X and error results of the solve + Fs = np.zeros([maxIter]) # make arrays to store objective function values + Gs = np.zeros([maxIter, m]) # make arrays to store constraint function values + + + + if len(Xmin)==0: + Xmin = np.zeros(N)-np.inf + elif len(Xmin)==N: + Xmin = np.array(Xmin, dtype=np.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_) + else: + raise TypeError("Xmax must be of same length as X0") + + + if len(dX_last)==N: + dX_last = np.array(dX_last, dtype=np.float_) + elif len(dX_last)==0: + dX_last = np.zeros(N) + else: + raise ValueError("dX_last input must be of same size as design vector, if provided") + #XtLast = 1.0*Xt0 + + # set finite difference step size + #dX_fd = 4.0 #0.5# 1.0*dX[i] # this is gradient finite difference step size, not opto step size + dX_fd = stepfac*X*tol # set dX_fd as function of tolerance and initial values + dX_fd0 = np.array(dX_fd) + + if display > 3: print(f" dX_fd is {dX_fd}") + + if display > 0: + print("Starting dopt iterations>>>") + + for iter in range(maxIter): + iter_start_time = time.time() + + Xsave = np.array(X) + + if any(X == Xmin): + Xbadj = np.array(X) + for ixmin in np.where(X==Xmin)[0]: + Xbadj[ixmin] = X[ixmin]*(1+tol) # badj = bound adjustment + elif any(X == Xmax): + Xbadj = np.array(X) + for ixmax in np.where(X==Xmax)[0]: + Xbadj[ixmax] = X[ixmax]*(1-tol) + else: + Xbadj = np.array(X) + + X = np.array(Xbadj) + + # call evaluation function (returns objective val, constrain vals, tuned variables, tuning results) + f, g, Xextra, Yextra, oths, stop = eval_func(X, args) #, XtLast, Ytarget, args) + + if display > 1: print("") + if display > 0: + + if isinstance(Xextra, list): + XextraDisp = Xextra + else: + XextraDisp = Xextra.tolist() + + print((" >> Iteration {:3d}: f={:8.2e} X="+"".join([" {:9.2f}"]*len(X))+" Xe="+"".join([" {:9.2f}"]*len(Xextra))).format(*( + [ iter , f ] + X.tolist() + XextraDisp) )) + + + if display > 1: + print(f"\n Constraint values: {g}") + elif display > 0 and any(g < 0.0): + print(f" Constraint values: {g}") + + Xs[iter,:] = np.hstack([X, Xextra]) + Fs[iter] = f + Gs[iter,:] = g + + + # stop if commanded by objective function + if stop: + message = 'Received stop command from objective function' + break + + # temporarily display output + #print(np.hstack([X,Y])) + + + if iter==maxIter-1: + + print("Failed to converge after "+str(iter)+" iterations") + + if any(X == Xmin) or any(X == Xmax) or any(g < 0.0): + for i in range(N): + if X[i] == Xmin[i] : print(f" Warning: Design variable {i} ended on minimum bound {Xmin[i]}.") + if X[i] == Xmax[i] : print(f" Warning: Design variable {i} ended on maximum bound {Xmax[i]}.") + + for j in range(m): # go through each constraint + if g[j] < 0: # if a constraint will be violated + print(f" Warning: Constraint {j} was violated by {-g[j]}.") + else: + print(" No constraint or bound issues.") + + success = False + break + + #>>>> COULD ALSO HAVE AN ITERATION RESTART FUNCTION? >>> + # that returns a restart boolean, as well as what values to use to restart things if true. How? + + else: # this is where we get derivatives and then take a step + + #dX = step_func(X, args, Y, oths, Ytarget, err, tol, iter, maxIter) + # hard coding a generic approach for now + + dX = np.zeros(N) # optimization step size to take + + X2 = np.array(X, dtype=np.float_) + + Jf = np.zeros([N]) + Jg = np.zeros([N,m]) + Hf = np.zeros([N]) # this is just the diagonal of the Hessians + Hg = np.zeros([N,m]) + + for i in range(N): # loop through each variable + + # could do repetition to hone in when second derivative is large, but not going to for now + # or if first derivative is zero (in which case take a larger step size) + + dX_fd = np.array(dX_fd0) # make a copy of the original dX_fd to store temporary values + + X2[i] += dX_fd0[i] # perturb + by original dX_fd0 + if X2[i] > Xmax[i]: # if the perturbed+ X2 value goes above the bounds + X2[i] = Xmax[i] # set the perturbed+ X2 value to the max bound + dX_fd[i] = Xmax[i] - X[i] # and set the temp dX_fdi value to how much that new perturbation is + + fp, gp, Xtp, Yp, othsp, stopp = eval_func(X2, args) # evaluate at the proper X2 position + + X2[i] -= 2.0*dX_fd[i] # perturb - by updated dX_fd + if X2[i] < Xmin[i]: # if the perturbed- X2 value goes under the bounds + X2[i] = Xmin[i] # set the perturbed- X2 value to the min bound + dX_fd[i] = X[i] - Xmin[i] # and set the temp dX_fd value to how much that new perturbation is + fm, gm, Xtm, Ym, othsm, stopm = eval_func(X2, args) # evaluate at the proper X2 position + + X2[i] += dX_fd[i] # restore to original + + # for objective function and constraints (note that g may be multidimensional), + # fill in diagonal values of Jacobian and Hessian (not using off-diagonals for now) + Jf[i] = (fp-fm) /(2*dX_fd[i]) + Jg[i,:] = (gp-gm) /(2*dX_fd[i]) + Hf[i] = (fm-2.0*f+fp) /dX_fd[i]**2 + Hg[i,:] = (gm-2.0*g+gp) /dX_fd[i]**2 + #if i==0: print(fp, fm, dX_fd[i], Jf[i]) + + #breakpoint() + + # If we're currently violating a constraint, fix it rather than worrying about the objective function + # This step is when new gradients need to be calculated at the violating point + # e.g. in cases where the constraint functions are flat when not violated + if any(g < 0.0): + + if display > 3: + print(" Constraint step") + for i in range(len(Jg)): + print(f" Jg[{i}] = {np.round(Jg[i],5)}") + #print((" Jg[{:3d}] = "+"".join([" {:6.2f}"]*m).format(*([i]+Jg[i].tolist())))) + + g0 = [] + gradg = [] + #sqg = [] + + # first get the gradient of each active constraint + stepdir = np.zeros(N) # this is the direction we will step in + + for j in range(m): # go through each constraint + if g[j] < 0: # if a constraint will be violated + if np.sum(np.abs(Jg[:,j])) == 0.0: + print(f"dopt error, zero Jacobian for constraint {j}. g(X) may be flat or dX_fd may be too small") + stop=True # set flag to exit iteration + message = f"Error, zero Jacobian for constraint {j}. g(X) may be flat or dX_fd may be too small" + break + + g0.append( g[j]) # constraint value at the current location + gradg.append(Jg[:,j]) # gradient for each active constraint <<< doesn't work so well + #sqg.append( np.sum(Jg[:,j]*Jg[:,j])) # gradient dotted with itself (i.e. sum of squares) + + + # OG output for comparison + stepdir_i = 1.0*Jg[:,j] # default is to assume we're moving in the same direction as the gradient since that's most efficient + for i in range(N): + if (X[i]==Xmin[i] and Jg[i,j]<0) or (X[i]==Xmax[i] and Jg[i,j]>0): # but if any dimension is on its bound, and the gradient is to move in that direction + stepdir_i[i] = 0.0 # set its component to zero instead (other dimensions will now have to move farther) + alph = (0.0-g[j])/np.sum(Jg[:,j]*stepdir_i) # for our selected step direction, find how far to move to get to zero + if np.sum(Jg[:,j]*stepdir_i) == 0.0: + print('NaN isue') + + dXcon = stepdir_i*alph *1.1 # step is step direction vector (possibly gradient) times alpha (plus a little extra for margin) - add the step command from each violated constraint + + if display > 3: + print(f' - Looking at g[{j}]') + print(" stepdir_i = "+"".join([" {:.5f}"]*len(stepdir_i)).format(*(stepdir_i.tolist()))) + print(" alph = ",alph) + print(" g0 = ",g0) + print(" gradg = ",gradg) + + if display > 1: + print((" Con {:3d} OG correction"+"".join([" {:9.2f}"]*N)).format(*( [j]+ dXcon.tolist()) )) + + + # now zero any dimensions that are about to cross a bound (if we're already at the bound) + for i in range(N): + + for j in range(len(g0)): # look through each active constraint (but apply zeroing to all active constraints for now) + if (X[i]==Xmin[i] and gradg[j][i]<0) or (X[i]==Xmax[i] and gradg[j][i]>0): # but if any dimension is on its bound, and the gradient is to move in that direction + for k in range(len(g0)): + gradg[k][i] = 0.0 # set its component to zero instead (other dimensions will now have to move farther) + if display > 3: print('gradg',gradg) + if display > 3: print(' - No bounds issues') + sqg = [ np.sum(jac*jac) for jac in gradg] # update the gradient dotted with itself (i.e. sum of squares) + + + if display > 3: print(' - Find stepdir') + # now sort out a combined step direction depending on the active constraints + if len(g0) == 2 and np.sum(gradg[0]*gradg[1]) < 0 and N>1: # if two active constraints in opposing directions + c1 = g0[0]/sqg[0] * ( np.sum(gradg[0]*gradg[1]) * gradg[1]/sqg[1] - gradg[0] ) + + c2 = g0[1]/sqg[1] * ( np.sum(gradg[0]*gradg[1]) * gradg[0]/sqg[0] - gradg[1] ) + stepdir = c1 + c2 + if display > 3: print(f' A: c1={c1}, c2={c2}') + + else: # all other cases - assume we're moving in the same direction as the gradient since that's most efficient + #c2 = [(-g0[j])/np.sum(gradg[j]*gradg[j])*gradg[j] for j in range(len(g0))] # compute step directions that will zero each constraint + + c = np.zeros([len(g0), N]) + for j in range(len(g0)): # compute step directions that will zero each constraint + if np.sum(gradg[j]*gradg[j]) > 0: # just leave it as zero if any direction has a zero derivative + c[j,:] = -g0[j] / np.sum(gradg[j]*gradg[j]) * gradg[j] + if display > 3: print(f' B: c={c}') + else: + if display > 0: + print(f' dopt warning: zero gradient squared for active constraint {j} at iter={iter} and X={X}') + + #stepdir=sum(c2) + stepdir = np.sum(c, axis=0) # step is step direction vector (possibly gradient) times alpha (plus a little extra for margin) - add the step command from each violated constraint + if display > 3: print(' stepdir = ',stepdir) + + + if np.linalg.norm(stepdir)==0: + stop = True + break + + + if display > 3: print(' - Find alpha') + # now find how large the step needs to be to satisfy each active constraint + alpha = 0.0 # this is the scalar that determines how far we will step in the direction + for j in range(m): # go through each constraint + if g[j] < 0: # if a constraint will be violated + alpha_i = (0.0-g[j])/np.sum(Jg[:,j]*stepdir)# for this constraint, find how far to move along the step direction to get to zero + + alpha = np.max([alpha, alpha_i]) + if display > 3: print(' alpha_i =',alpha_i) + # if an acceleration limit will be applied in some dimension, it'd be nice to revise the direction and recompute <<< + + #dXcon = stepdir*alpha *1.1 # step is step direction vector (possibly gradient) times alpha (plus a little extra for margin) - add the step command from each violated constraint + + #if display > 1: + #print(f" Constraint {j:3d} active).") + #print((" Con {:3d} correction: "+"".join([" {:9.2f}"]*N)).format(*( [j]+ dXcon.tolist()) )) + #if display > 2: + # print((" J = "+"".join([" {:9.2e}"]*m)).format(*Jg[:,j].tolist() )) + # print((" H = "+"".join([" {:9.2e}"]*m)).format(*Hg[:,j].tolist() )) + + dX = stepdir*alpha *1.1 # step is step direction vector (possibly gradient) times alpha (plus a little extra for margin) + + + if display > 1: + print((" Total constraint step (dX) :"+"".join([" {:9.2f}"]*N)).format(*dX.tolist()) ) + + #if iter==4 or iter==5: + # breakpoint() + + # if the above fails, we could try backtracking along dX_last until the constriant is no longer violated... + + # at the end of this, the step will be a summation of the steps estimated to resolve each constraint - good idea? + + # otherwise (no constraints violated) make an optimization step + else: + + # start by doing a line search down the slope + + dir = -Jf/np.linalg.norm(Jf) # direction to move along + if display > 1: print(f" beginning line search in steepest descent direction, u={dir}") + step = dir * tol*np.linalg.norm(X) * 2 + #print('dir',dir,'step',step) + j_active = -1 # index of constraint that is limiting the step + + dX = np.zeros_like(X) + X2 = X + dX + flast = 1.0*f + glast = 1.0*g + step2 = 1.0*step + for k in range(100): # now do a line search + + step2 = step*(2**k) # double step size each time + + dX = dX + step2 # <<< looks like I'm actually trippling the step - need to fix at some point <<< + + + # check for bound violation + if any(X2 + dX < Xmin) or any(X2 + dX > Xmax): + dX = dX - step2 + if display > 3: + print(f" ----- next step will cross bounds, so will use X={X+dX} and dX={dX}") + break + + # evaluate the function + fl, gl, Xtl, Yl, othsl, stopl = eval_func(X + dX, args) + if display > 3: + print((" line searching: k={:2d} f={:6.3f}):"+"".join([" {:9.2f}"]*N)).format( + *( [k, fl] + (X+dX).tolist()))) + + # check for increasing f + if fl > flast: + dX = dX - step2 + if display > 3: + print(f" ----- f increasing ----- so will use X={X+dX} and dX={dX}") + break + + # check for constraint violation + if any(gl < 0): + + frac = -glast/(gl-glast) # how much of the step (fraction) could be taken until each constraint is violated + backfrac = (1.0-frac)*(frac > 0) # how much of the step to backtrace (with filtering to exclude negatives from constraints that are decreasing) + j_active = np.argmax(backfrac) + + dXog = 1.0*dX + #breakpoint() + + dX = dX - backfrac[j_active]*step2 - 2*dir*tol*np.linalg.norm(X) # back up, and also keep a 2*tol margin from the boundary + + # normal case: # find -Jf component tangent with constraint surface to move along + tandir = -Jf + np.sum(Jg[:,j_active]*Jf)*Jg[:,j_active]/np.sum(Jg[:,j_active]*Jg[:,j_active]) + + ''' ...not sure this next part really works/helps at all... + # >>>>>>> make it so that if we've backed up further from the constraint than the last X, + #then move along direction of previous dX! (to avoid potential sawtooth bouncing along constraint boundary) + # IF the constraint direction points more toward the constraint than the previous dX direction. + if iter > 0 and np.sum(dX*step) < 0: # line search has us moving further away from constraint boundary + + tandir = tandir/np.linalg.norm(tandir) + lastdir = dX_last/np.linalg.norm(dX_last) + + if np.sum(Jg[:,j_active]*lastdir) > np.sum(Jg[:,j_active]*tandir): + print(f"special case. Using {lastdir} rather than {tandir}") + tandir = lastdir + ''' + + + if display > 3: + print(f" ----- constraint violated ----- {gl} ") + print(f" will back up to X={X+dX} and do line search along constraint") + + print(dXog) + print(dX) + print(gl) + fl2, gl2, Xtl2, Yl2, othsl2, stopl2 = eval_func(X + dX, args) + if display > 3: print(gl2) + + break + + + flast = 1.0*fl + glast = 1.0*gl + + #for i in range(N): + #dX[i] = -Jf[i]/np.linalg.norm(Jf) * np.linalg.norm(dX_last) * a_max # << trying a fixed step size approach (no hessian) + + if display > 1: + print((" Minimization step, dX = "+"".join([" {:9.2f}"]*N)).format(*dX.tolist() )) + if display > 2: + print((" J = "+"".join([" {:9.2f}"]*N)).format(*Jf.tolist() )) + + if any(np.isnan(dX)): + breakpoint() + + dX_min0 = np.array(dX) + + # respect bounds (handle each dimension individually) <<< + for i in range(N): + if X[i] + dX[i] < Xmin[i]: + dX[i] = Xmin[i] - X[i] + elif X[i] + dX[i] > Xmax[i]: + dX[i] = Xmax[i] - X[i] + + dX_minB = np.array(dX) + + + # but do a line search tangent to whatever constraint boundary is limiting if applicable (and if tangent is clear) + if j_active >= 0 and N > 1 and not any(np.isnan(tandir)): + #tandir = -Jf + np.sum(Jg[:,j_active]*Jf)*Jg[:,j_active]/np.sum(Jg[:,j_active]*Jg[:,j_active]) # find -Jf component tangent with constraint surface to move along + step = tandir/np.linalg.norm(tandir) * tol*np.linalg.norm(X) + if display > 3: + print(f"Constraint normal vector is {Jg[:,j_active]/np.linalg.norm(Jg[:,j_active])}") + print(f" beginning line search along constraint {j_active} boundary, u={tandir/np.linalg.norm(tandir)}") + + X3 = X + dX + step3=0 + for k in range(100): # now do a line search + + # evaluate the function + fl, gl, Xtl, Yl, othsl, stopl = eval_func(X3, args) + if display > 3: + print((" line searching: k={:2d} f={:6.3f}):"+"".join([" {:9.2f}"]*N)).format( + *( [k, fl] + X3.tolist()))) + + # check for increasing f + if k>0: + if fl > flast: + X3 = X3 - step3 + if display > 3: + print(f" ----- f increasing ----- so will use previous X={X3} and dX={X3 - X}") + break + + # check for constraint violation + if any(gl < 0): + X3 = X3 - step3 + # could instead back up to an intermediate point, and offset by the 2*tol margin too + if display > 3: + print(f" ----- constraint violated ----- {gl} --- so will use previous") + break + + flast = 1.0*fl + step3 = step*(1.6**k) # increase step size each time + + # check for bound violation + if any(X3 + step3 < Xmin) or any(X3 + step3 > Xmax): + if display > 3: + print(f" ----- next step will cross bounds, so stopping here") + break + + X3 = X3 + step3 + + dX = X3 - X # undo the last step (which was bad) and calculated overall effective dX + + + # this is how to stop the dX vector at the approximate constraint boundary (not good for navigation) + #for j in len(g): # go through each constraint + # if g[j] + np.sum(Jg[:,j]*dX) < 0: # if a constraint will be violated + # alpha = -g[j]/np.sum(Jg[:,j]*dX) # find where the constraint boundary is (linear approximation) + # dX = dX*alpha # shrink the step size accordingly (to stop at edge of constraint) + + if stop: + break + + + # Make sure we're not diverging by keeping things from reversing too much. + # Track the previous step (dX_last) and if the current step reverses too much, stop it part way. + + + # Original approach: Stop it at a plane part way between the current X value and the previous X value (using golden ratio, why not). + # This means scaling down the full vector (while preserving its direction). The downside is this limits all dimensions. + # get the point along the previous step vector where we could draw a bounding hyperplane (could be a line, plane, or more in higher dimensions) + Xlim = X - 0.62*dX_last + # the equation for the plane we don't want to recross is then sum(X*dX_last) = sum(Xlim*dX_last) + if np.sum((X+dX)*dX_last) < np.sum(Xlim*dX_last): # if we cross are going to cross it + ratio = np.sum((Xlim-X)*dX_last)/np.sum(dX*dX_last) # this is how much we need to scale down dX to land on it rather than cross it + dX = ratio*dX # scale down dX + if display > 1: + print((" oscil limit: ( reducing by factor {:6.3f} :"+"".join([" {:9.2f}"]*N)).format( + *( [ratio] + dX.tolist() ))) + + # Revised approach: only scale down the directions that have reversed sign from the last step. + ''' + for i in range(N): + if np.sign(dX[i])==-np.sign(dX_last[i]) and abs(dX_last[i]) > tol: # if this dimension is reversing direction + + ratio = np.abs(dX[i]/dX_last[i]) # if it's reversing by more than 62% of the last step in this dimension, limit it + if ratio > 0.62: + dX[i] = dX[i]*0.62/ratio # scale down dX so that it is just 62% of the dX_last + + if display > 1: + print((" oscil limit: (i={:2d},{:6.3f},{:7.3f}):"+"".join([" {:9.2f}"]*N)).format( + *( [i, 0.62/ratio, ratio] + dX.tolist() ))) + ''' + + # also avoid extreme accelerations in the same direction + if np.linalg.norm(dX_last) > tol: # only worry about accelerations if the last step was non-negligible + for i in range(N): + + # set the maximum permissible dx in each direction based an an acceleration limit + #if abs(dX_last[i]) < tol: + # dX_max = a_max*10*tol*np.sign(dX[i]) + if abs(dX_last[i]) < tol*(np.abs(X[i])+tol): + dX_max = a_max*tol*(np.abs(X[i])+tol)*np.sign(dX[i]) + else: + dX_max = a_max*dX_last[i] + #print('dX_max',dX_max, dX_last, tol, dX) + if dX_max == 0.0: # avoid a divide-by-zero case (if dX[i] was zero to start with) + dX[i] = 0.0 + else: + a_i = dX[i]/dX_max # calculate ratio of desired dx to max dx + #print('a_i',a_i, i, dX[i]) + if a_i > 1.0: + + # Option 1. the directoin-preserving approach: (could have problems with making the dX too small, triggering convergence) + dX = dX/a_i # scale it down to the maximum value <<< this needs to be in the conditional if X[i] > Xmin[i] and X[i] < Xmax[i]: # limit this direction if it exceeds the limit and if it's not on a bound (otherwise things will get stuck) + # Option 2. the individual approach below <<< + #dX[i] = dX[i]/a_i # scale it down to the maximum value (treat each DOF individually) + #print(dX[i]) + if display > 1: + print((" accel limit: (i={:2d},by {:6.3f} :"+"".join([" {:9.2f}"]*N)).format( + *( [i, 1.0/a_i] + dX.tolist()))) + + + # enforce bounds + for i in range(N): + if X[i] + dX[i] < Xmin[i]: + dX[i] = Xmin[i] - X[i] + #dX[i] = Xmin[i]*(1+tol) - X[i] + if display > 2: print(f" Minimum bounds adjustment for dX[{i}]") + elif X[i] + dX[i] > Xmax[i]: + dX[i] = Xmax[i] - X[i] + #dX[i] = Xmax[i]*(1-tol) - X[i] + if display > 2: print(f" Maximum bounds adjustment for dX[{i}]") + + + # check for convergence + if all(np.abs(dX) < tol*(np.abs(X)+tol)): + + if lastConverged: # only consider things converged if the last iteration also satisfied the convergence criterion + + if display>0: + print(f"Optimization converged after {iter} iterations with dX of {dX}") + print(f"Solution X is "+str(X)) + print(f"Constraints are "+str(g)) + + if any(X == Xmin) or any(X == Xmax): + if display>0: + for i in range(N): + if X[i] == Xmin[i] : print(f" Warning: Design variable {i} ended on minimum bound {Xmin[i]}.") + if X[i] == Xmax[i] : print(f" Warning: Design variable {i} ended on maximum bound {Xmax[i]}.") + + success = True + message = "converged on one or more bounds" + + elif any(X == Xmin) or any(X == Xmax) or any(g < 0.0): + if display>0: + for j in range(m): # go through each constraint + if g[j] < 0: # if a constraint will be violated + print(f" Warning: Constraint {j} was violated by {-g[j]}.") + + success = False + message = f"converged with one or more constraints violated (by max {-min(g):7.1e})" + + else: + success = True + message = "converged with no constraint violations or active bounds" + break + + else: + lastConverged = True # if this is the first time the convergence criterion has been met, note it and keep going + message = "convergence criteria only met once (need twice in a row)" + else: + lastConverged = False + message = "not converged" + + if display > 2: + print(f" Convergence message: {message}") + + dX_last = 1.0*dX # remember this current value + #XtLast = 1.0*Xt + #if iter==3: + #breakpoint() + X = X + dX + + if display > 0: + print((" dopt iteration finished. dX= "+"".join([" {:9.2f}"]*N)).format(*(dX.tolist()))) + if display > 2: + print(" iteration run time: {:9.2f} seconds".format(time.time() - iter_start_time)) + + + if display > 2: + print(f" Convergence message: {message}") + + if display > 0: + print(" total run time: {:8.2f} seconds = {:8.2f} minutes".format((time.time() - start_time),((time.time() - start_time)/60))) + + runtime = time.time() - start_time #seconds + + return X, f, dict(iter=iter, dX=dX_last, oths=oths, Xs=Xs, Fs=Fs, Gs=Gs, Xextra=Xextra, g=g, Yextra=Yextra, + success=success, message=message, time=runtime) + + +def doptPlot(info): + + n = info['Xs'].shape[1] # number of DVs + m = info['Gs'].shape[1] # number of constraints + + fig, ax = plt.subplots(n+1+m,1, sharex=True) + Xs = np.array(info["Xs"]) + Fs = np.array(info["Fs"]) + Gs = np.array(info["Gs"]) + iter = info["iter"] + + for i in range(n): + ax[i].plot(Xs[:iter+1,i]) + + ax[n].plot(Fs[:iter+1]) + ax[n].set_ylabel("objective") + + for i in range(Gs.shape[1]): + j = i+1+n + ax[j].axhline(0, color=[0.5,0.5,0.5]) + ax[j].plot(Gs[:iter+1,i]) + ax[j].set_ylabel(f'con {i}') + + ax[j].set_xlabel("iteration") + + plt.show() + + + +# ------------------------------ sample functions ---------------------------- + + +def eval_func1(X, args): + '''returns target outputs and also secondary outputs for constraint checks etc.''' + + # Step 1. break out design variables and arguments into nice names + + # Step 2. do the evaluation (this may change mutable things in args) + y1 = (X[0]-2)**2 + X[1] + y2 = X[0] + X[1] + + # Step 3. group the outputs into objective function value and others + Y = np.array([y1, y2]) # objective function + oths = dict(status=1) # other outputs - returned as dict for easy use + + return Y, oths, False + + + +def step_func1(X, args, Y, oths, Ytarget, err, tol, iter, maxIter): + '''General stepping functions, which can also contain special condition checks or other adjustments to the process + + ''' + + # get numerical derivative + J = np.zeros([len(X),len(X)]) # Initialize the Jacobian matrix that has to be a square matrix with nRows = len(X) + + for i in range(len(X)): # Newton's method: perturb each element of the X variable by a little, calculate the outputs from the + X2 = np.array(X) # minimizing function, find the difference and divide by the perturbation (finding dForce/d change in design variable) + deltaX = tol*(np.abs(X[i])+tol) + X2[i] += deltaX + Y2, extra = eval_func1(X2, args) + + J[:,i] = (Y2-Y)/deltaX # and append that column to each respective column of the Jacobian matrix + + dX = -np.matmul(np.linalg.inv(J), Y) # Take this nth output from the minimizing function and divide it by the jacobian (derivative) + + return dX # returns dX (step to make) + + + + + +## ============================== below is a new attempt at the catenary solve ====================================== +# <<< moved to Catenary.py >>> + + + + + + + + + + + + +''' + +# test run + + +#Catenary2(100, 50, 130, 1e8, 100, plots=1) + +print("\nTEST 1") +catenary(576.2346666666667, 514.6666666666666, 800, 4809884.623076923, -2.6132152062554828, CB=-64.33333333333337, HF0=0, VF0=0, Tol=1e-05, MaxIter=50, plots=2) +print("\nTEST 2") +catenary(88.91360441490338, 44.99537159734132, 100.0, 854000000.0000001, 1707.0544275185273, CB=0.0, HF0=912082.6820817506, VF0=603513.100376363, Tol=1e-06, MaxIter=50, plots=1) +print("\nTEST 3") +catenary(99.81149090002897, 0.8459770263789324, 100.0, 854000000.0000001, 1707.0544275185273, CB=0.0, HF0=323638.97834178555, VF0=30602.023233123222, Tol=1e-06, MaxIter=50, plots=1) +print("\nTEST 4") +catenary(99.81520776134033, 0.872357398602503, 100.0, 854000000.0000001, 1707.0544275185273, CB=0.0, HF0=355255.0943810993, VF0=32555.18285808794, Tol=1e-06, MaxIter=50, plots=1) +print("\nTEST 5") +catenary(99.81149195956499, 0.8459747131565791, 100.0, 854000000.0000001, 1707.0544275185273, CB=0.0, HF0=323645.55876751675, VF0=30602.27072107738, Tol=1e-06, MaxIter=50, plots=1) +print("\nTEST 6") +catenary(88.91360650151807, 44.99537139684605, 100.0, 854000000.0000001, 1707.0544275185273, CB=0.0, HF0=912082.6820817146, VF0=603513.100376342, Tol=1e-06, MaxIter=50, plots=1) +''' +''' +maxIter = 10 +# call the master solver function +X0 = [2,2] +Ytarget = [0,0] +args = [] +X, Y, info = dsolve(eval_func1, step_func1, X0, Ytarget, args, maxIter=maxIter) +''' diff --git a/famodel/design/layout.py b/famodel/design/layout.py new file mode 100644 index 00000000..7ef553cb --- /dev/null +++ b/famodel/design/layout.py @@ -0,0 +1,2485 @@ +import os +import moorpy as mp +from moorpy.helpers import getFromDict +import numpy as np +import math +import pandas as pd +import matplotlib.pyplot as plt +from matplotlib.animation import FuncAnimation +from matplotlib.colors import LogNorm +from matplotlib.collections import PolyCollection +from matplotlib.ticker import AutoLocator, AutoMinorLocator +from matplotlib.ticker import FuncFormatter +from scipy.interpolate import NearestNDInterpolator +from scipy import interpolate, optimize +from scipy.optimize import minimize, differential_evolution, NonlinearConstraint +from scipy.spatial.distance import cdist, pdist, squareform +from scipy.spatial import distance +from scipy import optimize +from sklearn.cluster import SpectralClustering # KMeans + +import random +import csv +# from moorpy.helpers import getFromDict +# from shapely import Point, Polygon +import shapely as sh +from shapely.geometry import Point, LineString, MultiLineString, Polygon, MultiPolygon +from shapely.ops import unary_union, nearest_points + +import shapely.geometry +import shapely.affinity +from shapely.affinity import translate +#import shapely.affinity as sa +import networkx as nx + +import yaml +#import raft +from copy import deepcopy + +from moorpy.helpers import set_axes_equal + +import fadesign + +from famodel.project import Project +from famodel.mooring.mooring import Mooring +from famodel.anchors.anchor import Anchor +from famodel.platform.platform import Platform +from famodel.cables.cable import Cable +from famodel.cables.cable_properties import loadCableProps, getCableProps +from famodel.substation.substation import Substation + +from fadesign.layout_helpers import getLower, makeMooringListN +from fadesign.CableLayout_functions import getCableLayout + +import floris +from floris import FlorisModel + +# from floris.turbine_library import TurbineInterface, TurbineLibrary + +from pyswarm import pso + +# Import PySwarms +#import pyswarms as pso +#from pyswarms.utils.functions import single_obj as fx +#from pyswarms.utils.plotters import (plot_cost_history, plot_contour, plot_surface) +#from pyswarms.utils.plotters.formatters import Mesher + + +# SPYDER +# Interactive plots on +#%matplotlib qt +# Interactive plots off +#%matplotlib inline + + +class Layout(Project): + '''A class to store and work with the layout information of a wind farm.''' + + def __init__(self, X, Xu, Xdb =[], wind_rose = [], ss = None, mooringAdjuster = None, **kwargs): + + '''Create a new Layout object that can be used for optimization. + The Layout class allows storage of various data for the layout design + problem, such as the boundaries, the seabed bathymetry, and wind rose. + This initialization function sets those data. Many of the data inputs + are optional and default to not being used. + + For FREE LAYOUT OPTIMIZATION: X = [x,y,phi], Xu = [] + For UNIFORM GRID LAYOUT OPTIMIZATION: X = [], + Xu = [ spacing_x, spacing_y, trans_x, trans_y, rotang, skew] + + + Parameters + ---------- + X : 1D array + Design vector considered within the optimization [x,y,phi] + x, y : turbine positions in (m) + phi : turbine heading in (deg) + Xu : 1D array + Design vector considered within the uniform grid optimization + [grid_spacing_x,grid_spacing_y,grid_trans_x, grid_trans_y, grid_rotang, grid_skew] + grid_spacing_x, y : x,y turbine spacing in (m) + grid_trans_x, y : x,y translation of entire grid in (m) + grid_rotang : rotation of grid around centroid of lease area (deg) + grid_skew : skew angle of grid (deg) + Xdb : + ??? + nTurbines : int + Number of turbines to work with. + boundary_coords : 2D array + List of x coordinates of lease area boundary vertices (m). + List of y coordinates of lease area boundary vertices (m). + + grid_x : 1D array + List of x coordinates of bathymetry grid (km). + grid_y : 1D array + List of y coordinates of bathymetry grid (km). + grid_depth : 2D array + Matrix of depth values corresponding to x,y coordinates (m). + + wind_rose : FLORIS wind rose + A wind rose of wind speeds, direction, frequency and TI + ss : MoorPy Subsystem, optional + A MoorPy Subsystem to adapt for a 3D representation of each mooring line. + mode : string + 'LCOE', 'AEP' or 'CAPEX'. + rotation_mode : Bool + True : considering rotation as part of the design vector as design variable + False: not considering rotation as design variable + + turb_minrad=360 + Radius of turbine buffer zone. + moor_minrad=50 + Radius of mooring buffer zone. + moorOpt_mode : string + 'basic' : Basic mooring layout, without MoorPy + 'advanced' : Mooring layout considers MoorPy input + + + + ''' + # Initialize Project aspects to start with + super().__init__() + + self.display = getFromDict(kwargs, 'display', default=0) + + # add seabed bathymetry (based on file for now) + self.bathymetry_file = getFromDict(kwargs, 'bathymetry_file', dtype=str, default = '') + self.loadBathymetry(self.bathymetry_file) + self.soil_file = getFromDict(kwargs, 'soil_file', dtype=str, default = '') + self.loadSoil(self.soil_file) + self.cable_mode= getFromDict(kwargs, 'cable_mode', default = True) + + + # ----- Optimization modes ----- + self.optimizer = getFromDict(kwargs, 'optimizer', dtype=str, default = '') # Optimizer + self.obj_penalty = getFromDict(kwargs, 'obj_penalty', default = True) # Use penalty factor in objective function yes (1) or no (0) + self.mode = getFromDict(kwargs, 'mode', dtype=str, default = 'LCOE') # Optimization mode + self.rotation_mode = getFromDict(kwargs, 'rotation_mode', default = True) # Rotation included as Design Variable or not + self.alternate_rows = getFromDict(kwargs, 'alternate_rows', default = False ) + self.log = dict(x=[], f=[], g=[]) # initialize a log dict with empty values + self.iter = -1 # iteration number of a given optimization run (incremented by updateDesign) + self.parallel = getFromDict(kwargs, 'parallel', default = False) + self.infeasible_obj_update = getFromDict(kwargs, 'infeasible_obj_update', default = False) # set True to update objective function even when layout violates constraints + + + # ----- Turbine quantity and positions ----- + self.nt = int(getFromDict(kwargs, 'n_turbines', default = 67)) # Number of turbines + self.turb_coords= np.zeros((self.nt,2)) # Turbine positions (x,y) [m] + self.turb_depth = np.zeros(self.nt) + + self.turb_minrad = getFromDict(kwargs, 'turb_minrad', default = 200) + self.moor_minrad = getFromDict(kwargs, 'moor_minrad', default = 20) + self.anchor_minrad = getFromDict(kwargs, 'anchor_minrad', default = 50) + + self.turb_mindist_m = np.zeros(self.nt) # currently inactive + self.con_turb_turb = np.zeros(self.nt) # distance to turbine + # distance to boundary - considering anchor radius + self.con_turb_boundary = np.zeros(self.nt) + # distance to boundary - center of WTG + self.turb_dist_tb2_m = np.zeros(self.nt) + + if np.size(Xu) != 0: + self.Xlast = np.zeros((self.nt)*2) # X vector containting X and Y coordinates + else: + self.Xlast = np.zeros_like(X) + + self.obj_value = 0 + self.turb_rating_MW = getFromDict(kwargs, 'turb_rating_MW', default = 15) # Rating of each turbine in MW + # IAC System parameters + self.iac_voltage_kV = getFromDict(kwargs, 'iac_voltage_kV', default = 66) # Voltage level in kV + self.iac_type = 'dynamic_cable_66' # Cable type, as defined in cable properties YAML + + # Turbine electrical current in ampere A + self.turb_I = (self.turb_rating_MW * 1e6) / (self.iac_voltage_kV * 1e3) + + # Cable conductor sizes for 66 kV transmission system + # List to be further specified + + #dir = os.path.dirname(os.path.realpath(__file__)) + #with open(os.path.join(dir,"CableProps_default.yaml")) as file: + # source = yaml.load(file, Loader=yaml.FullLoader) + #As = source['conductor_size']['size_A_df'] + self.iac_typical_conductor = getFromDict(kwargs, 'iac_typical_conductor',shape=-1, default = [0]) + if len(self.iac_typical_conductor)==1 and self.iac_typical_conductor[0] == 0: + self.iac_typical_conductor = np.array([ 70, 95, 120, 150, 185, 240, 300, 400, 500, 630, + 800, 1000, 1200,1400,1600,200,2500,3000,4000]) + + + # ----- Offshore Substation ----- + self.noss = int(getFromDict(kwargs,'noss', default = 1)) + self.oss_coords_initial = getFromDict(kwargs, 'oss_coords', shape=-1, default = np.zeros((self.noss,2))) # initial OSS coordinates + # adjust to a nested list [[x,y]] if given oss coords as [x,y] for compatibility with situations with multiple oss + if self.oss_coords_initial.shape == (2,): + self.oss_coords_initial = np.array([self.oss_coords_initial]) + # for now we'll set oss_coords = oss_coords_initial but this could change in generateGridPoints if using uniform grid layout + self.oss_coords = deepcopy(self.oss_coords_initial) + self.oss_minrad = getFromDict(kwargs, 'oss_minrad', default = self.turb_minrad*2) + self.static_substations = getFromDict(kwargs, 'static_substations', dtype=bool, default = False) + + # create substation platform object + for oo in range(self.noss): + r = [self.oss_coords[oo][0], self.oss_coords[oo][1], 0] + self.platformList[self.nt+oo] = Platform(id=self.nt+oo,r=r,rFair=ss.rad_fair,zFair=ss.z_fair) + self.platformList[self.nt+oo].entity = 'Substation' + + + + + # ----- Turbine Cluster ----- + self.n_cluster = int(getFromDict(kwargs, 'n_cluster', default = 9))# Amount of turbine cluster for cable routing + #self.n_tcmax = (np.ceil(self.nt/self.n_cluster)).astype(int) + self.n_tcmax = round(self.nt/(self.n_cluster*self.noss)) + + # ----- set default obj. values ----- + self.aep = 0 + self.obj_value = None + + + print("setting up areas/geometry") + # ----- Lease area boundary polygon ----- + #self.boundary = boundary_coords#list(zip(boundary_x, boundary_y)) + self.boundary_coords = getFromDict(kwargs, 'boundary_coords', shape = -1, default = np.array([(0, 0),(10000, 0),(10000, 10000), (0,10000) ])) + self.setBoundary(self.boundary_coords[:,0], self.boundary_coords[:,1]) + self.boundary_sh = sh.Polygon(self.boundary) + + # set up any interior sub boundaries (useful for mulitple separate uniform grids) + self.sub_boundary_coords = getFromDict(kwargs, 'sub_boundary_coords', shape=-1, + default = []) + self.sub_boundary = [] + self.sub_boundary_sh = [] + self.sub_boundary_centroid = [] + self.sub_boundary_centroid_x = [] + self.sub_boundary_centroid_y = [] + for subb in self.sub_boundary_coords: + subb = np.array(subb) + # save as project sub boundaries + self.sub_boundary.append(np.vstack([[subb[i,0],subb[i,1]] for i in range(len(subb))])) + + # if the boundary doesn't repeat the first vertex at the end, add it + if not all(subb[0,:] == subb[-1,:]): + self.sub_boundary[-1] = np.vstack([self.sub_boundary[-1], subb[0,:]]) + # create sub boundary shapely polygon + self.sub_boundary_sh.append(sh.Polygon(self.sub_boundary[-1])) + # create sub boundary centroid and store centroid coords + self.sub_boundary_centroid.append(self.sub_boundary_sh[-1].centroid) + self.sub_boundary_centroid_x.append(self.sub_boundary_centroid[-1].x) + self.sub_boundary_centroid_y.append(self.sub_boundary_centroid[-1].y) + + # trim the bathymetry grid to avoid excess + self.trim_grids = getFromDict(kwargs,'trimGrids',default=True) + if self.trim_grids: + self.trimGrids() + + # Get centroid of lease area + self.boundary_centroid = self.boundary_sh.centroid + self.boundary_centroid_x, self.boundary_centroid_y = self.boundary_centroid.x, self.boundary_centroid.y + + # Safety margin + self.boundary_margin = getFromDict(kwargs, 'boundary_margin', default = 0) #margin applied to exterior boundary + # Calculate the buffered polygon with safety margin + # internal: safety margin, to ensure that there is enough space for mooring system without crossing lease area boundaries + # idea: Safety margin dependent on water depth? + self.boundary_sh_int = self.boundary_sh.buffer(-self.boundary_margin) + #self.grid_x_min, self.grid_y_min, self.grid_x_max, self.grid_y_max = self.boundary_sh_ext.bounds + + ''' + # Calculate the total area of the buffered polygon + self.total_area_ext = self.boundary_sh_ext.area + self.total_area_int = self.boundary_sh_int.area + self.a_f=round(self.total_area_ext/self.total_area_int) + + # Maximum x and y distance in boundary shape + def max_distance(x_values): + if len(x_values) < 2: + return 0 + x_max = max(x_values) + x_min = min(x_values) + return abs(x_max - x_min) + + self.bound_dist_x = max_distance(self.boundary[:,0]) + self.bound_dist_y = max_distance(self.boundary[:,1]) + ''' + + # INTIAL: Parse the design vector and store updated positions internally + #x_pos, y_pos = X[:len(X)//2], X[len(X)//2:] + # ONLY FOR FREE OPTIMIZATION + if np.size(Xu) == 0 and np.size(Xdb) == 0: + if self.rotation_mode: + x_pos, y_pos, rot_rad = X[:self.nt], X[self.nt:2*self.nt], X[2*self.nt:] + #self.turb_rot_deg = rot_deg + self.turb_rot= rot_rad#np.radians(rot_deg) + else: + x_pos, y_pos = X[:len(X)//2], X[len(X)//2:] + + self.turb_rot = getFromDict(kwargs, 'turb_rot', shape = self.nt, default = np.zeros(self.nt))#rot_rad#np.radians(turb_rot) + + # Turbine positons: INPUT in [m] + self.turb_coords[:,0]= x_pos + self.turb_coords[:,1]= y_pos + # UNIFORM GRID LAYOUT OPTIMIZATION + else: + self.turb_rot = getFromDict(kwargs, 'turb_rot', shape = self.nt, default = np.zeros(self.nt)) + self.turb_coords = np.zeros((self.nt,2)) + + # X = self.generateGridPoints(Xu) + #x_pos, y_pos = X[:len(X)//2], X[len(X)//2:] + #self.turb_coords[:,0]= x_pos + #self.turb_coords[:,1]= y_pos + + # ----- Exclusion zone polygons ----- + # Turbine distances to exclusion zones (if any) + self.exclusion = getFromDict(kwargs, 'exclusion_coords', shape = -1, default = []) + self.turb_dist_tez1_m = np.zeros((self.nt*len(self.exclusion))) + self.turb_dist_tez2_m = np.zeros((self.nt*len(self.exclusion))) + self.exclusion_polygons_sh = [] # List to store polygons + + # Create exclusion polygons + for ie in range(len(self.exclusion)): + exclusion_polygon = sh.Polygon(self.exclusion[ie]) + self.exclusion_polygons_sh.append(exclusion_polygon) + + + # ----- Wind data ----- + self.wind_rose = wind_rose + + + # ----- Mooring system variables ----- + print("setting up mooringList") + self.mooringList = makeMooringListN(ss, 3*self.nt) # make Moorings + + for mooring in self.mooringList.values(): # hackily set them up + mooring.dd['sections'] = [] + mooring.dd['connectors'] = [] + for i,sec in enumerate(mooring.ss.lineList): + mooring.dd['connectors'].append({'CdA':0,'m':0,'v':0}) + mooring.dd['sections'].append({'type':mooring.ss.lineList[i].type, + 'L':mooring.ss.lineList[i].L}) + mooring.dd['connectors'].append({'CdA':0,'m':0,'v':0}) + mooring.adjuster = mooringAdjuster # set the designer/adjuster function + + + # ----- Platforms ----- + for i in range(self.nt): + r = [self.turb_coords[i][0],self.turb_coords[i][1],0] + self.platformList[i] = Platform(id=i, r=r, heading=0, mooring_headings=[0,120,240],rFair=ss.rad_fair,zFair=ss.z_fair) + self.platformList[i].entity = 'FOWT' + + for j in range(3): + self.platformList[i].attach(self.mooringList[i*3+j], end='b') + + + # ---- Anchors ---- + self.anchorList = {} + if 'anchor_settings' in kwargs: + anchor_settings = True + else: + anchor_settings = False + # set up anchor design dictionary + ad = {'design':{}, 'cost':{}} + if anchor_settings and 'anchor_design' in kwargs['anchor_settings']: + anchor_design_initial = kwargs['anchor_settings']['anchor_design'] + ad['type'] = kwargs['anchor_settings']['anchor_type'] + else: + print('No anchor type given, defaulting to suction bucket anchor.') + anchor_design_initial = {'D':3.0,'L':16.5,'zlug':10} + ad['type'] = 'suction_pile' + ad['design'] = anchor_design_initial # INPUT or not??? + for i, moor in enumerate(self.mooringList.values()): + if self.soil_x is not None: # get soil conditions at anchor location if soil info available + name, props = self.getSoilAtLocation(moor.rA[0], moor.rA[1]) + + # create anchor object + anch = Anchor(dd=ad,aNum=i,id=moor.id) + anch.soilProps = {name:props} + self.anchorList[anch.id] = anch + # attach to mooring line + moor.attachTo(anch,end='a') + if 'mass' in ad: + anch.mass = ad['mass'] + elif anchor_settings and 'mass' in kwargs['anchor_settings']: + anch.mass = kwargs['anchor_settings']['mass'] + + + # --- develop anchor types --- + self.anchorTypes = {} + self.anchorMasses = {} + self.anchorCosts = {} + # pull out mean depth + meandepth = np.mean(-self.grid_depth) + pf = self.platformList[0] + # artificially set platform at 0,0 + pf.setPosition([0,0],project=self) # put in a random place and reposition moorings + # create ms for this platform + msPF = pf.mooringSystem() + # set depth artificially to mean depth + msPF.depth = -meandepth + # set mooring object depth artificially for now + for moor in pf.getMoorings().values(): + moor.dd['zAnchor'] = meandepth + moor.z_anch = meandepth + moor.ss.depth = -meandepth + moor.rad_fair = 58 + moor.z_fair = -14 + # call set position function again to use adjuster function on all moorings + pf.setPosition([0,0]) + + # get anchors connected to this platform + anchors = pf.getAnchors() + # choose one (all should be same) + anch = anchors[0] + + # keep zlug constant? + if anchor_settings and 'fix_zlug' in kwargs['anchor_settings']: + fix_zlug=kwargs['anchor_settings']['fix_zlug'] + else: + fix_zlug=False + # set minimum allowable FS + if anchor_settings and 'FS_min' in kwargs['anchor_settings']: + minfs = kwargs['anchor_settings']['FS_min'] + else: + minfs = {'Ha':2,'Va':2} + # set FSdiff_max if provided + if anchor_settings and 'FSdiff_max' in kwargs['anchor_settings']: + FSdiff_max = kwargs['anchor_settings']['FSdiff_max'] + else: + FSdiff_max = None + # create anchor for each soil type + for name, soil in self.soilProps.items(): + if anchor_settings and 'anchor_resize' in kwargs['anchor_settings'] and kwargs['anchor_settings']['anchor_resize']: + # get anchor forces from array watch circle + pf.getWatchCircle(ms = msPF) + # get loads dictionary but get rid of any Ha Va loads that might already be there + anch.loads = {'Hm':anch.loads['Hm'],'Vm':anch.loads['Vm'], + 'thetam':anch.loads['thetam'], 'mudline_load_type':'max'} + # update soil type for anchor + anch.soilProps = {name:soil} + geom = [val for val in anch.dd['design'].values()] + geomKeys = [key for key in anch.dd['design'].keys()] + anch.getSize(geom,geomKeys, FSdiff_max=FSdiff_max, + fix_zlug=fix_zlug, minfs=minfs) + + self.anchorTypes[name] = deepcopy(anch.dd) if anch.dd else {} + self.anchorMasses[name] = deepcopy(anch.mass) if anch.mass else 0 + try: + self.anchorCosts[name] = deepcopy(anch.getCost()) + except: + self.anchorCosts[name] = 0 + + + + self.ms_na = 3 # Number of anchors per turbine. For now ONLY 3 point mooring system. + #self.ms_anchor_depth = np.zeros((self.nt*self.ms_na)) # depths of anchors + self.anchor_coords= np.zeros((self.nt*self.ms_na,2)) # anchor x-y coordinate list [m] + self.ms_bufferzones_pos = np.zeros((self.nt,), dtype=object) # Buffer zones for moorign system + self.ms_bufferzones_rout = np.zeros((self.nt,), dtype=object) + self.ms_bufferzones_rout_points = np.zeros((self.nt,), dtype=object) + + + # ----- Initialize the FLORIS interface fi ----- + self.use_FLORIS = getFromDict(kwargs,'use_FLORIS', default = False) + if self.use_FLORIS: # If using FLORIS, initialize it + print("initializing FLORIS") + # How to do this more elegant? + dirname = '' #'./_input/' + #flName = 'gch_floating.yaml' + if self.parallel: + from floris import ParFlorisModel + self.floris_file = getFromDict(kwargs, 'floris_file', dtype = str, default = '') + self.flow = ParFlorisModel(self.floris_file) + + else: + self.floris_file = getFromDict(kwargs, 'floris_file', dtype = str, default = '') + self.flow = FlorisModel(self.floris_file) #FlorisInterface + + # FLORIS inputs x y positions in m + self.flow.set(layout_x=self.turb_coords[:,0], + layout_y=self.turb_coords[:,1], + wind_data = self.wind_rose + ) + #run floris simulation + # self.flow.run() + + # # SAVE INITIAL AEP + # self.aep0 = self.flow.get_farm_AEP() + + # ----- Wind Turbine Data ----- + # https://nrel.github.io/floris/turbine_interaction.html + # self.ti = TurbineInterface.from_internal_library("iea_15MW.yaml") + + if self.display > 0: + self.plotWakes(wind_spd = 10, wind_dir = 270, ti = 0.06) + + else: # if not using FLORIS, indicate it with a None + self.flow = None + + print("updating layout") + if np.size(Xu) != 0: + self.updateLayoutUG(Xu) + elif np.size(Xdb) != 0: + self.db_ext_spacing = getFromDict(kwargs, 'db_ext_spacing', default = [0, 1, 0, 1]) + self.updateLayoutDB(Xdb) + else: + self.updateLayoutOPT(X) + + + + + + def generateGridPoints(self, Xu, trans_mode, boundary_index=-1): + ''' Generate uniform grid points and save resulting coordinates into vector X. + This transforms the uniform grid (UG) design variables into the design variables of + the free layout optimization. + + trans_mode = 'x': Shear transformation in x direction only + trans_mode = 'xy': Shear transformation in x and y direction + ''' + grid_spacing_x = Xu[0] + grid_spacing_y = Xu[1] + grid_trans_x = Xu[2] + grid_trans_y = Xu[3] + grid_rotang = Xu[4] + grid_skew = Xu[5] + + if boundary_index >= 0: + boundary = self.sub_boundary_sh[boundary_index] + bound_centroid_y = self.sub_boundary_centroid_y[boundary_index] + bound_centroid_x = self.sub_boundary_centroid_x[boundary_index] + + else: + boundary = self.boundary_sh + bound_centroid_y = self.boundary_centroid_y + bound_centroid_x = self.boundary_centroid_x + + if self.rotation_mode: + if len(Xu) != 7: + raise ValueError('If rotation mode is True, Xu[6] is turbine rotation') + self.turb_rot = np.radians(Xu[6]) + + # Check if self.grid_spacing_x/y is equal to 0, if so, set it to 1000 m + if grid_spacing_x == 0: + grid_spacing_x = self.turb_minrad*0.5 + if grid_spacing_y == 0: + grid_spacing_y = self.turb_minrad*0.5 + + # Shear transformation + # Calculate trigonometric values + cos_theta = np.cos(np.radians(grid_rotang)) + sin_theta = np.sin(np.radians(grid_rotang)) + tan_phi = np.tan(np.radians(grid_skew)) + + # Transmoration matrix, considering shear transformatio and rotation + # Default: shear direction in x direction only + # xy: shear direction in x and direction + if trans_mode == 'xy': + # Compute combined x and y shear + transformation_matrix = np.array([[cos_theta-sin_theta*tan_phi, -sin_theta + tan_phi * cos_theta], + [sin_theta+cos_theta*tan_phi, sin_theta*tan_phi+cos_theta]]) + else: + # default transformation: x shear only + transformation_matrix = np.array([[cos_theta, -sin_theta + tan_phi * cos_theta], + [sin_theta, sin_theta*tan_phi+cos_theta]]) + + # Generate points in the local coordinate system + points = [] + + # Lease area shape: Get min and max xy coordinates and calculate width + min_x, min_y, max_x, max_y = boundary.bounds # self.boundary_sh.bounds + xwidth = abs(max_x-min_x) + ywidth = abs(max_y-min_y) + + + # LOCAL COORDINATE SYSTEM WITH (0,0) LEASE AREA CENTROID + # Therefore, +/- self.boundary_centroid_y/x cover the entire area + # Loop through y values within the boundary_centroid_y range with grid_spacing_y increments + column_count = 0 + rotations = [] + grid_position =[] + for y in np.arange(-bound_centroid_y-ywidth, bound_centroid_y+ywidth, grid_spacing_y): + column_count += 1 + row_count = 0 + # Loop through x values within the boundary_centroid_x range with grid_spacing_x increments + for x in np.arange(-bound_centroid_x-xwidth, bound_centroid_x+xwidth, grid_spacing_x): + + row_count += 1 + # Apply transformation matrix to x, y coordinates + local_x, local_y = np.dot(transformation_matrix, [x, y]) + # Add grid translation offsets to local coordinates + local_x += grid_trans_x + local_y += grid_trans_y + # Create a Point object representing the transformed coordinates + # Transform back into global coordinate system with by adding centroid to local coordinates + point = Point(local_x + bound_centroid_x, local_y + bound_centroid_y) + points.append(point) + + if self.alternate_rows: + rotations.append(self.turb_rot + np.radians(180 * (column_count % 2))) + #store column, row for each turbine + grid_position.append([column_count, row_count]) + + + # remove points that are not in boundaries + bound_lines = boundary.boundary # get boundary lines for shapely analysis + out_lines = [bound_lines] + # keep only points inside bounds + points_ib = [pt for pt in points if (boundary.contains(pt))] + if self.alternate_rows: + self.turb_rot = [rotations[ind] for ind in range(0, len(points)) if boundary.contains(points[ind])] + self.grid_positions = [grid_position[ind] for ind in range(0, len(points)) if boundary.contains(points[ind])] + + points_ibe = points_ib + # remove points in exclusion zones + if self.exclusion_polygons_sh: + for ie in range(len(self.exclusion)): + points_ibe = [pt for pt in points_ibe if not self.exclusion_polygons_sh[ie].contains(pt)] + out_lines.append(self.exclusion_polygons_sh[ie].boundary) # get boundary lines for exclusion zones + + return(points_ibe) + + + def pareGridPoints(self,points_ibe): + ''' + Function to pare number of grid points down to desired amount, place oss + at closest grid points (if substations allowed to move) and return + array of x, y(, rotation) values. Sorts points by distance from all borders + (lease boundary, inner boundaries, exclusion zones) and keeps the nt points + furthest from all boundaries + + Parameters + ---------- + points_ibe : list + List of shapely point objects that are inside all boundaries and outside all exclusion zones + + Returns + ------- + X : np.ndarray + 1D array of concatenated x, y(, rotation) for each turbine + + ''' + # determine number of points to keep (usually # turbines + # substations) + if self.static_substations: + # in this case, keep substations where they are + nt = self.nt + else: + nt = self.nt + self.noss + + # create list of boundary lines from outside boundary, exclusion zones, and inner boundaries + out_lines = [self.boundary_sh.boundary] + if len(self.sub_boundary_sh) > 0: + for sub in self.sub_boundary_sh: + out_lines.append(sub.boundary) + for ie in range(len(self.exclusion)): + out_lines.append(self.exclusion_polygons_sh[ie].boundary) + + lines = MultiLineString(out_lines) + point_dists = [pt.distance(lines) for pt in points_ibe] # get min dist between bounds and each point + points_ibe = np.array(points_ibe) + # get indices of sorting by descending minimum distance + points_sorted_idx = [int(ind) for ind in np.flip(np.argsort(point_dists,kind='stable'))] + furthest_points = list([points_ibe[i] for i in range (0, len(points_ibe)) if i in points_sorted_idx[:nt]]) # pull out the points that are furthest from bounds + self.grid_positions = list(self.grid_positions[i] for i in range (0, len(points_ibe)) if i in points_sorted_idx[:nt]) + if self.alternate_rows: + furthest_rotations = list(self.turb_rot[i] for i in range (0, len(points_ibe)) if i in points_sorted_idx[:nt]) + + + # add points outside lease area if more points are needed + min_x, min_y, max_x, max_y = self.boundary_sh.bounds + if len(points_sorted_idx)< nt: + # determine remaining number of turbines to add + leftover = nt-len(points_sorted_idx) + # choose point outside bounds for leftovers + leftover_loc = Point(min_x-1,min_y-1) + furthest_points.extend([leftover_loc]*leftover) + if self.alternate_rows: + furthest_rotations.extend([0]*leftover) + + # put substation(s) in place closest to oss_coords if substations can move + if not self.static_substations: + for oo in range(self.noss): + # make a multipoint fro + turb_multipoint = sh.MultiPoint(furthest_points) + oss_point_start = Point(self.oss_coords_initial[oo]) + # find point closest to initial oss coord & set as new oss position + oss_point = nearest_points(turb_multipoint,oss_point_start)[0] + # remove turbine from new oss position (extra turbines have been placed already) + if oss_point in furthest_points: + if self.alternate_rows: + del furthest_rotations[furthest_points.index(oss_point)] + + index = furthest_points.index(oss_point) + furthest_points.remove(oss_point) + self.grid_positions.remove(self.grid_positions[index]) + self.oss_coords[oo] = [oss_point.x, oss_point.y] + else: + print('Could not find nearby point for oss, setting oss to initial coords') + self.oss_coords[oo] = self.oss_coords_initial[oo] + + # save points furthest from bounds into turb_coords + x_coords = np.array([point.x for point in furthest_points])#/1000 + y_coords = np.array([point.y for point in furthest_points])#/1000 + for i,coord in enumerate(self.turb_coords): + coord[0] = x_coords[i] + coord[1] = y_coords[i] + + #update grid_positions row and column coordinates based on minimum + self.grid_positions = np.array(self.grid_positions) + self.grid_positions[:,0] = self.grid_positions[:,0] - min(self.grid_positions[:,0]) + self.grid_positions[:,1] = self.grid_positions[:,1] - min(self.grid_positions[:,1]) + + # Return Design Vector X with x,y coordinates, same as used for the free layout optimization. + # Coordinates in (km) + # This completes the interface + if self.rotation_mode: + + if self.alternate_rows: + self.turb_rot = furthest_rotations + X = np.concatenate((self.turb_coords[:,0], self.turb_coords[:,1], self.turb_rot)) + else: + X = np.concatenate((self.turb_coords[:,0], self.turb_coords[:,1], self.turb_rot*np.ones((nt)))) + else: + X = np.concatenate((self.turb_coords[:,0], self.turb_coords[:,1])) + + return X + + + def updateLayout(self, X, level=0, refresh=False): + '''Update the layout based on the specified design vector, X. This + will adjust the turbine positions stored in the Layout object as + well as those in the FLORIS and any other sub-objects. + + Parameters + ---------- + X + Design vector. + level + Analysis level to use. Simplest is 0. + refresh : bool + If true, forces a re-analysis, even if this design vector is old. + ''' + if len(X)==0: # if any empty design vector is passed (useful for checking constraints quickly) + if refresh: + X = np.array(self.Xlast) + else: + return + if np.array_equal(X, self.Xlast) and not refresh: + #if all(X == self.Xlast) and not refresh: # if X is same as last time + #breakpoint() + pass # just continue, skip the update steps + + + elif any(np.isnan(X)): + raise ValueError("NaN value found in design vector") + + else: # Update things iff the design vector is valid and has changed + if self.display > 1: + print("Updated design") + + self.iter += 1 # update internal iteration counter + + # Parse the design vector and store updated positions internally + if self.rotation_mode: + x_pos, y_pos, rot_rad = X[:self.nt], X[self.nt:2*self.nt], X[2*self.nt:] + #self.turb_rot = np.radians(rot_deg) + self.turb_rot = rot_rad + else: + x_pos, y_pos = X[:len(X)//2], X[len(X)//2:] + #self.turb_rot = self.turb_rot_const + + self.turb_coords[:,0]= x_pos + self.turb_coords[:,1]= y_pos + + # Update things for each turbine + #breakpoint() + #print(self.nt, len(self.turb_depth), X) + #print(self.turb_coords) + + # Update Paltform class + for i in range(self.nt): + self.platformList[i].setPosition(self.turb_coords[i], heading=self.turb_rot[i], degrees=False, project = self) + # switch anchor type + anchs = self.platformList[i].getAnchors() + for anch in anchs.values(): + name, props = self.getSoilAtLocation(anch.r[0],anch.r[1]) + atype = self.anchorTypes[name] + anch.dd.update(atype) + anch.mass = self.anchorMasses[name] + anch.cost['materials'] = self.anchorCosts[name] + anch.soilProps = {name:props} + + # Get depth at turbine postion + self.turb_depth[i] = -self.getDepthAtLocation( + self.turb_coords[i,0], self.turb_coords[i,1]) + # update substation platform location(s) + for oo in range(self.noss): + self.platformList[self.nt+oo].setPosition(self.oss_coords[oo], + heading=self.turb_rot[i], + degrees=False,project=self) + + + # Anchor locations - to be repalced when integration is further advanced + #for j in range(3): + # im = i*3 + j # index in mooringList + # self.ms_anchor_depth[im] = self.mooringList[im].z_anch#self.mooringList[im].rA[2] OLD, not needed anymore + # self.anchor_coords[im,:] = self.mooringList[im].rA[:2] + + ''' + # Calculate anchor position based on headings + + theta = self.turb_rot[i] # turbine heading + #R = np.array([[np.cos(theta), -np.sin(theta)],[np.sin(theta), np.cos(theta)]]) + + headings = np.radians([60,180,300]) + + for j in range(len(headings)): + + im = i*3 + j # index in mooringList + + # heading of the mooring line + heading_i = headings[j] + theta + + # adjust the whole Mooring + #self.mooringList[im].reposition(self.turb_coords[i,:], + # heading=heading_i, project=self, level=level) + #self.mooringList[im].reposition(r_center=self.turb_coords[i,:], + # heading=heading_i, project=self, level=level) + ''' + # get the anchor location from the mooring + #self.anchor_coords[im,:] = self.mooringList[im].rA[:2] + #self.ms_anchor_depth[im] = self.mooringList[im].rA[2] + #self.mooringList[0].z_anch + #self.mooringList[0].rA + #self.mooringList[0].rA + + + + # ----- evaluate constraints ----- + + # ----- Calculate buffer zone shape around mooring lines and anchors. ----- + # ISO 19901-7: 100 m safety zone to other offshore assets, therefore 50 m per mooring line is recommended + + # SAFE BUFFERZONES IN PLATFORM OBJECT? + + + + + # Create LineString geometries and buffer them + for i in range(self.nt): + # Buffer group for turbine positioning + buffer_group_pos = [] + # Buffer group for cable routing + buffer_group_rout = [] + + for j in range(self.ms_na): + im = 3*i + j # global index of mooring/anchor + + moor_bf_start = get_point_along_line(self.turb_coords[i,:], self.mooringList[im].rA[:2], self.turb_minrad) + # Buffer zone mooring line + #line = LineString([self.turb_coords[i,:], self.mooringList[im].rA[:2]]) + line = LineString([moor_bf_start, self.mooringList[im].rA[:2]]) + mooringline_buffer = line.buffer(self.moor_minrad) + + # Buffer zone anchor + # Create a point at coordinates (x, y) + point = Point(self.mooringList[im].rA[:2]) + # Create a buffer around the anchor with a radius of X + anchor_buffer = point.buffer(self.anchor_minrad) + + # Buffer zone turbine + # Create a point at coordinates (x, y) + point = Point(self.turb_coords[i,:],) + # Create a buffer around the anchor with a radius of X + turb_buffer = point.buffer(self.turb_minrad) + + # Buffer group for turbine positioning + buffer_group_pos.append(mooringline_buffer) + buffer_group_pos.append(anchor_buffer) + buffer_group_pos.append(turb_buffer) + + # Buffer group for cable routing + buffer_group_rout.append(mooringline_buffer) + buffer_group_rout.append(anchor_buffer) + + # Combine the buffered lines connected to the same turbine into one polygon + polygon = unary_union(buffer_group_pos) # Combine buffers for each turbine + if isinstance(polygon, MultiLineString): + # Convert MultiLineString to Polygon + polygon = Polygon(polygon) + self.ms_bufferzones_pos[i] = polygon + + polygon = unary_union(buffer_group_rout) # Combine buffers for each turbine + if isinstance(polygon, MultiLineString): + # Convert MultiLineString to Polygon + polygon = Polygon(polygon) + self.ms_bufferzones_rout[i] = polygon + + #envelopes['buffer_zones']['shape'] + + + + # ----- Overlap between mooring zones ----- + # Create an empty 2D array to store the areas of intersection + intersection_areas = np.zeros((self.nt, self.nt)) + # Calculate and fill the array with the areas of intersection + for i in range(self.nt): + for j in range(i + 1, self.nt): + polygon1 = self.ms_bufferzones_pos[i] + polygon2 = self.ms_bufferzones_pos[j] + # Calculate intersection + intersection = polygon1.intersection(polygon2) + # Fill the array with the area of intersection + intersection_areas[i, j] = intersection.area*(-1) + intersection_areas[j, i] = intersection.area*(-1) + + self.con_moor_moor = getLower(intersection_areas) # get lower diagonal + + + # ----- Overlap between mooring zones and boundary ----- + # Calculate areas of the parts of polygons outside the boundary + self.con_moor_boundary = np.zeros(self.nt) + # Iterate over polygons and fill the array with areas + for i, polygon in enumerate(self.ms_bufferzones_pos): + if isinstance(polygon, (Polygon, MultiPolygon)) and polygon.intersects( self.boundary_sh): + # Calculate the intersection with the boundary polygon + intersection = polygon.intersection( self.boundary_sh) + # Calculate the area of the parts outside the boundary + outside_area = polygon.difference(intersection).area + # Fill the array with the area + self.con_moor_boundary[i] = -outside_area + + + # ----- Between exclusion zones and turbines ----- + self.con_moor_ez_m2 = np.zeros((self.nt*len(self.exclusion))) + r = 0 + for ie in range(len(self.exclusion)): + #exclusion_polygon = sh.Polygon(self.exclusion[ie]) + # Iterate over polygons and fill the array with areas + for i, polygon in enumerate(self.ms_bufferzones_pos): + if isinstance(polygon, (Polygon, MultiPolygon)) and polygon.intersects(self.exclusion_polygons_sh[ie]): + # Calculate the intersection with the boundary polygon + intersection = polygon.intersection(self.exclusion_polygons_sh[ie]) + # Calculate the area of the parts inside exclusion areas + inside_area = polygon.difference(intersection).area + # Fill the array with the area + self.con_moor_ez_m2[r] = -inside_area + r += 1 + + + # ----- Margin between turbines ----- + + # Distance matrix between turbines + distances = cdist(self.turb_coords, self.turb_coords) + + dists = distances [np.tril_indices_from( distances , k=-1)] # get lower diagonal + + # Reduce by buffer radius (for each turbine) then store + self.con_turb_turb = dists - 2*self.turb_minrad + + # ----- Margin between turbines and OSS ----- + # Distance matrix between turbines + #distances = cdist(self.turb_coords,self.oss_coords) + #print(self.oss_coords) + #print(self.turb_coords) + #dists = distance.cdist(self.oss_coords, self.turb_coords, 'euclidean') + #dists = distances [np.tril_indices_from( distances , k=-1)] # get lower diagonal + dists = [] + for oo in range(self.noss): + dists.extend(np.linalg.norm(self.turb_coords - self.oss_coords[oo], axis=1)) + # Reduce by buffer radius (for each turbine) then store + self.con_turb_oss = np.array(dists) - self.oss_minrad + + # ----- margin between turbines and lease area boundary ----- + r = 0 + self.con_turb_ez_m = np.zeros((self.nt*len(self.exclusion))) + self.con_oss_boundary = np.zeros(self.noss) + self.con_oss_ez_m = np.zeros((self.noss*len(self.exclusion))) + coords = np.zeros((self.nt+self.noss,2)) + coords[:self.nt] = self.turb_coords + coords[self.nt:] = self.oss_coords + isturb=True + for i in range(self.nt+self.noss): + if i>=self.nt: + isturb = False + # Create a Shapely Point for the given xy of turbine or oss + p_turb = Point(coords[i,0], coords[i,1]) + + #breakpoint() + # Find the nearest point on the shape to the given point + p_bound = nearest_points(self.boundary_sh.exterior, p_turb)[0] + + # Calculate the Euclidean distance between point and nearest point on boundary + distance_within = p_turb.distance(p_bound) + + # If point is outside boundary, give the distance a negative sign + if not p_turb.within(self.boundary_sh): + distance_within = -abs(distance_within) + + # Reduce by buffer radius, then add to constraint list + if isturb: + self.con_turb_boundary[i] = distance_within - self.turb_minrad + else: + self.con_oss_boundary[i-self.nt] = distance_within - self.oss_minrad + + for ie in range(len(self.exclusion)): + p_exclusion = nearest_points(self.exclusion_polygons_sh[ie].exterior, p_turb)[0] + dist_outside = p_turb.distance(p_exclusion) + # if turbine is inside exclusion zone, give distance - sign + if p_turb.within(self.exclusion_polygons_sh[ie]): + dist_outside = -abs(dist_outside) + + if isturb: + self.con_turb_ez_m[r] = dist_outside + else: + self.con_oss_ez_m[r-self.nt*len(self.exclusion)] = dist_outside + r += 1 + + # could handle exclusion zones in this same loop + # # ----- margin between turbines and exclusion zones ----- + # # Optimize: creat point once, together with above + # if len(self.exclusion) > 0: + # r = 0 + # for ie in range(len(self.exclusion)): + # #exclusion_polygon = sh.Polygon(self.exclusion[ie]) + # #breakpoint() + + # for i in range(self.nt): + # # Create a Shapely Point for the given xy + # #point = Point(x_pos[i], y_pos[i]) + # point = Point(self.turb_coords[i,0], self.turb_coords[i,1]) + # # Find the nearest point on the shape to the given point + # nearest_point = nearest_points(self.exclusion_polygons_sh[ie].exterior, point)[0] + # # Calculate the Euclidean distance between WTG anchor radius and nearest point on shape + # # Reduce distance by radius (distance has to be equal or greater than anchor radius) + # self.turb_dist_tez1_m[r] = point.distance( + # nearest_point) - self.turb_minrad + # # Calculate the Euclidean distance between WTG center and shape + # self.turb_dist_tez2_m[r] = point.distance(nearest_point) + # # Check if turbine is outside the boundary + # # Ensure if point is outside shape, distance is always negative + # if point.within(self.exclusion_polygons_sh[ie]): + # self.turb_dist_tez1_m[r] = -abs(self.turb_dist_tez1_m[r]) + # # Weight the contraints so that the turbines stay within the specifified area + # self.turb_dist_tez2_m[r] = -abs(self.turb_dist_tez2_m[r]) + # r =+1 + + + + + + + # ----- Concatenate constraints vector ----- + + # Note: exclusions are temporarily skipped, but can be added back in to the below + + #!! QUESTION MB: Should this be considered at all as a constraint? I think it is more important that + # anchor buffer zones do not exceed the lease boundaries, but not a wind turbine spacing parameter. + + # distances + constraint_vals_m = np.concatenate([self.con_turb_turb, self.con_turb_boundary, + self.con_turb_oss, self.con_turb_ez_m, + self.con_oss_boundary, self.con_turb_ez_m]) + constraint_vals_km = constraint_vals_m/1000 + + # areas + constraint_vals_m2 = np.concatenate([self.con_moor_moor, self.con_moor_boundary, self.con_moor_ez_m2]) + constraint_vals_km2 = constraint_vals_m2/(1000**2) + # Combine constraint values (scaling to be around 1) + self.con_vals = 10*np.concatenate([constraint_vals_km, constraint_vals_km2]) + + + # Sum of Constraint values + negative_values = [val for val in self.con_vals if val < 0] + + + if not negative_values: + self.con_sum = 0 + # ----- Cable Layout - ONLY FOR FEASIBLE LAYOUT + if self.cable_mode: + + self.iac_dic,_,_ = getCableLayout(self.turb_coords, self.oss_coords, self.iac_typical_conductor, + self.iac_type, self.turb_rating_MW, turb_cluster_id=[], + n_cluster_sub=self.n_cluster, n_tcmax=self.n_tcmax, plot=False, oss_rerouting=1) + + # Save cables in cable objects + self.addCablesConnections(self.iac_dic,cableType_def=self.iac_type) + + else: + self.con_sum = sum(negative_values) # sum of all values below zero + + + if self.optimizer == 'PSO': + # PSO constraints only + # Constraints above zero 0: satisfied (often it is g < 0 for satisfied constraints for a PSO) + # Solution: Sum of negative constraint values, because it has to be one value only + self.con_vals = self.con_sum + + + # Penalty factor: (1+abs(self.con_vals)) or 1 + if self.obj_penalty == 1: # penalty ON + f_pentalty = (1+abs(self.con_sum)) + else: # penalty OFF + f_pentalty = 1 + + + # ----- evaluate objective function ----- + # compute the objective function value + # objective function includes a constraint term, leading to a penalty when constraints are not satisfied + # (1+abs(self.con_vals)) + # objective funciton + if not negative_values or self.infeasible_obj_update or not self.obj_value: + if self.mode == 'LCOE': # minimize LCOE (this LCOE version focuses on mooring and cable costs/AEP) + self.getLCOE() + #self.constraintFuns_penalty(X) + self.obj_value = self.lcoe*1e5*f_pentalty #(1+abs(self.con_vals)) #+ self.cost_penalty / self.aep#self.getLCOE() #+ self.constraintFuns_penalty(X)/self.aep + elif self.mode == 'LCOE2': # minimize LCOE (this LCOE version includes opex estimates and platform/turbine cost estimates) + self.getLCOE2() + self.obj_value = self.lcoe*f_pentalty + elif self.mode == 'AEP': # maximize AEP + self.getAEP(display = self.display) + self.obj_value = -self.aep/1e12/f_pentalty #-self.getAEP() #+ self.constraintFuns_penalty(X) # minus, because algorithm minimizes the objective function + elif self.mode == 'CAPEX': # maximize AEP + self.getCost() + #self.constraintFuns_penalty(X) + self.obj_value = (self.cost_total/1e7)*f_pentalty #+ self.cost_penalty#+ abs(self.con_vals)#self.constraintFuns_penalty#(X)/1e7 #self.getCAPEX() #+ self.constraintFuns_penalty(X) + + else: + raise Exception( + "The layout 'mode' must be either LCOE, AEP or CAPEX.") + + ''' + # ----- write to log ----- + # only log if the design has significantly changed + if np.linalg.norm(X - self.Xlast) > 100: # <<< threshold should be customized + # log the iteration number, design variables, objective, and constraints + self.log['x'].append(list(X)) + self.log['f'].append(list([self.obj_value])) + # Check if self.con_vals is an integer - Different optimizer require different constraints + if isinstance(self.con_vals, int): + # Convert self.con_vals to a list before appending to self.log['g'] + self.log['g'].append([self.con_vals]) + else: + # If self.con_vals is already iterable, directly append it to self.log['g'] + self.log['g'].append(list(self.con_vals)) + ''' + + self.Xlast = np.array(X) # record the current design variables + + + def updateLayoutUG(self, Xu, level=0, refresh=False): + '''Interface from uniform grid design variables to turbine coordinates.''' + + X_points = [] + # create grid points + if len(self.sub_boundary_sh) > 0: + # determine # of grid variables per sub boundary + nXu = 7 if self.rotation_mode else 6 + + # create grid points for each sub grid + for ind in range(len(self.sub_boundary_sh)): + # pull out relevant design variables + Xus = Xu[nXu*ind:nXu*(ind+1)] + # convert km to m for first 4 variables + Xum = np.hstack([[x*1000 for x in Xus[0:4]], Xus[4:]]) + # generate grid points + X_points.extend(self.generateGridPoints(Xum,trans_mode='x',boundary_index=ind)) + else: + # create grid points for entire grid + Xum = np.hstack([[x*1000 for x in Xu[0:4]], Xu[4:]]) # convert first 4 entries from km to m + # generate grid points + X_points.extend(self.generateGridPoints(Xum,trans_mode='x')) + + # pare down grid points to those furthest from boundaries & optionally add substation(s) in grid + X = self.pareGridPoints(X_points) + + self.updateLayout(X, level, refresh) # update each turbine's position + + #def updateLayoutOPTUG(self, Xu): + # '''Interface from uniform grid design variables to turbine coordinates.''' + # X = self.generateGridPoints(Xu) + # X2 = np.array(X) # make a copy of the design vector + # X2[:2*self.nt] = X[:2*self.nt]#*1000 # convert coordinates from km to m + # self.updateLayout(X2) + + + def updateLayoutDB(self, Xdb, level=0, refresh=False): + '''Interface for Dogger Bank style layouts.''' + + ### Xdb[0] and Xdb[1] are exterior spacings. db_ext_spacing allows the user to set what spacing each side uses (in order of coordinates) + interior =self.boundary_sh.buffer(-self.mooringList[0].rad_anch - self.anchor_minrad) ### this buffer should ensure anchor stays within boundary --- need to check + coords = list(interior.exterior.coords) + + from shapely.geometry import LineString + + #iterate through boundaries + points = [] + for i in range(0, len(coords) - 1): + + # connect exterior coordinates in order + line = LineString([coords[i], coords[i+1]]) + + # db_ext_spacing input allows the user to set which boundaries use which outer spacing + # determine number of turbines that will fit + num = math.floor(line.length/Xdb[self.db_ext_spacing[i]]) + + #interpolate along the side for the num turbines + if i == 0: + points.extend([line.interpolate(i/num , normalized = True) for i in range(num)]) + else: + + #after the first side, start turbines at +spacing so there isn't overlap at the corner + points.extend([line.interpolate(i/num , normalized = True) for i in range(1, num)]) + + + xs = [point.coords[0][0] for point in points] + ys = [point.coords[0][1] for point in points] + + + + #fill the interio using generateGridPoints + interiorinterior = interior.buffer(-self.mooringList[0].rad_anch - self.anchor_minrad) ### again this buffer needs to be checked + + #store original exterior boundary and numturbines + boundary_sh_int = self.boundary_sh_int + nt = self.nt + + interior_nt = nt - len(xs) + if interior_nt < 0: + interior_nt = 0 + + self.boundary_sh_int = interiorinterior + self.nt = interior_nt + + if self.nt > 0: + + X = self.generateGridPoints(Xdb[2:],trans_mode='x') + #combined exterior and interior turbines into X vector + Xall = list(X[:self.nt]) + xs + list(X[self.nt:]) + ys + x_coords = list(X[:self.nt]) + xs + y_coords = list(X[self.nt:]) + ys + else: + print('Exterior coords filled the required number of turbines') + + xs = xs[:nt] + ys = ys[:nt] + + Xall = xs + ys + x_coords = xs + y_coords = ys + + #revert boundary and nt + self.boundary_sh_int = boundary_sh_int + self.nt = nt + self.turb_coords = np.zeros((self.nt,2)) + + + #create buffers for exterior points (generateGridPoints did this for interior already) + for i in range(0, len(xs)): + point = Point(xs[i], ys[i]) + self.platformList[interior_nt + i].setPosition([point.x,point.y], heading=None, degrees=False, project = self) + atts = [x['obj'] for x in self.platformList[interior_nt + i].attachments.values()] + mList = [x for x in atts if type(x)==Mooring] + + # switch anchor type + anchs = self.platformList[i].getAnchors() + for anch in anchs.values(): + name, props = self.getSoilAtLocation(anch.r[0],anch.r[1]) + atype = self.anchorTypes[name] + anch.dd.update(atype) + anch.mass = self.anchorMasses[name] + anch.cost['materials'] = self.anchorCosts[name] + anch.soilProps = {name:props} + + # Get depth at turbine postion + self.turb_depth[interior_nt + i] = -self.getDepthAtLocation( + point.x, point.y) + buffer_group_pos = [] + + + for j in range(self.ms_na): + # im = 3*len(points) + j # global index of mooring/anchor + moor_bf_start = get_point_along_line([point.x, point.y], mList[j].rA[:2],self.turb_minrad) + # Buffer zone mooring line + #line = LineString([self.turb_coords[i,:], self.mooringList[im].rA[:2]]) + line = LineString([moor_bf_start, mList[j].rA[:2]]) + mooringline_buffer = line.buffer(self.moor_minrad) + + # Buffer zone anchor + # Create a point at coordinates (x, y) + point1 = Point(mList[j].rA[:2]) + # Create a buffer around the anchor with a radius of X + anchor_buffer = point1.buffer(self.anchor_minrad) + + # Buffer zone turbine + # Create a buffer around the anchor with a radius of X + turb_buffer = point.buffer(self.turb_minrad) + + # Buffer group for turbine positioning + buffer_group_pos.append(mooringline_buffer) + buffer_group_pos.append(anchor_buffer) + buffer_group_pos.append(turb_buffer) + polygon = unary_union(buffer_group_pos) # Combine buffers for each turbine + + if self.boundary_sh_int.contains(polygon): + # If the point is within the shape, append it to the list of bufferzones + + self.ms_bufferzones_pos[interior_nt + i] = polygon + + + + if len(x_coords) < self.nt: + for i in range(len(points)): + self.turb_coords[i,0] = x_coords[i] + self.turb_coords[i,1] = y_coords[i] + else: + self.turb_coords[:,0] = x_coords + self.turb_coords[:,1] = y_coords + + self.updateLayout(Xall, level, refresh) + + + + def updateLayoutOPT(self, X): + '''Wrapper for updateLayout that uses km instead of m.''' + X2 = np.array(X) # make a copy of the design vector + X2[:2*self.nt] = X[:2*self.nt]*1000 # convert coordinates from km to m + self.updateLayout(X2) + + + + # ----- OBJECTIVE FUNCTION ----- + def objectiveFunUG(self, Xu): + '''The general objective function. Will behave differently depending + on settings. Only input is the design variable vector, Xu.''' + # print('Xu in objective function: ',Xu) + # X = self.generateGridPoints(Xu,trans_mode='x') + + # update the layout with the specified design vector + # self.updateLayoutUG(X) + #Xum = np.hstack([[x*1000 for x in Xu[0:4]], Xu[4:]]) # convert first 4 entries from km to m + self.updateLayoutUG(Xu) + #self.updateLayoutOPTUG(X) + return self.obj_value + + + def objectiveFunDB(self, Xdb): + '''The general objective function. Will behave differently depending + on settings. Only input is the design variable vector, Xu.''' + + # update the layout with the specified design vector + # self.updateLayoutUG(X) + + self.updateLayoutDB(Xdb) + + #self.updateLayoutOPTUG(X) + + return self.obj_value + + + def objectiveFun(self, X): + '''The general objective function. Will behave differently depending + on settings. Only input is the design variable vector, X.''' + + # update the layout with the specified design vector + self.updateLayoutOPT(X) + + return self.obj_value + + + # ----- ANCHORS ----- + + + + # ----- AEP / FLORIS ----- + def getAEP(self, display = 0): + '''Compute AEP using FLORIS, based on whatever data and turbine + positions are already stored in the Layout object. + (updateLayout should have been called before this method.''' + + # FLORIS inputs positions in m + self.flow.set(layout_x=self.turb_coords[:,0], + layout_y=self.turb_coords[:,1] ) + + #run floris simulation + self.flow.set(wind_data = self.wind_rose) + self.flow.run() + + #frequencies must be in list + self.aep = self.flow.get_farm_AEP() + + if display > 0: + self.plotWakes(wind_spd = 10, wind_dir = 270, ti = 0.06) + #return self.aep + """ + # ----- CAPEX ----- + def getCAPEXMooring(self): + '''Compute CAPEX of mooring systems. Currently test function only''' + self.capex_mooring = sum(abs(self.ms_anchor_depth))*1000 + #capex_mooring = sum(self.turb_depth**2) + #return self.capex_mooring + + # ----- TOTAL CAPEX function + def getCAPEX(self): + '''Compute TOTAL CAPEX, adding sub-methods together. Currently test function only''' + self.getCAPEXMooring() + self.capex_total = self.capex_mooring + """ + # ----- TOTAL CAPEX function + def getCost(self): + '''Compute TOTAL CAPEX, adding sub-methods together. Currently test function only''' + + CapEx_mooring = 0 + for mooring in self.mooringList.values(): + CapEx_mooring += mooring.getCost() + + CapEx_anchors = 0 + for anchor in self.anchorList.values(): + CapEx_anchors += anchor.getCost() + + CapEx_cables = 0 + for cable in self.cableList.values(): + CapEx_cables += cable.getCost() + + + + # Include cable costs for feasible layouts + #if self.con_sum == 0: + #self.cost_cable = sum(self.iac_cost) + # self.cost_total = CapEx+self.cost_cable + #else: + self.cost_total = CapEx_mooring + CapEx_cables + CapEx_anchors + return self.cost_total + + + # ----- LCOE ----- + def getLCOE(self): + '''Compute LCOE = CAPEX / AEP. Currently test function and based on CAPEX only.''' + self.getAEP(display = self.display) + self.getCost() + self.lcoe = self.cost_total/self.aep#self.getCOST()/(self.getAEP()/ 1.0e6) # [$ / MWh]self.getAEP() + #return self.cost + + # ----- LCOE ----- + def getLCOE2(self): + '''updated LCOE function using capex, opex, and fcr assumptions from previous projects''' + + farm_capacity = self.turb_rating_MW * self.nt * 1000 # kW + + capex = 3748.8 * farm_capacity # $ does NOT include moorings/cables. + #from DeepFarm LCOE report GW scale individual wind farm, substracted mooring system and array system costs + opex = 62.51 *farm_capacity # $ annually. from DeepFarm LCOE report + fcr = 5.82/100 # fixed charge rate %. from DeepFarm LCOE report + + self.getAEP() + self.getCost() + self.lcoe = ((self.cost_total+capex)*fcr+ opex)/self.aep*1e6 # [$ / MWh] + + #return self.cost + + # ----- PENALTY FUNCTION ----- + def constraintFuns_penalty(self, X): + '''Penalty function to better guide the optimization. Only input is the design variable vector, X.''' + self.getCost() + self.constraintFuns(X) + + #con_vals = self.con_vals#self.constraintFuns(X) + # Get the indices of negative values + #negative_indices = np.where(con_vals < 0)[0] + #return self.getCAPEX()*0.1*abs(np.sum(con_vals[negative_indices]))#*1e3*self.nt**2 + #self.cost_penalty = self.cost_total*0.5*abs(np.sum(con_vals[negative_indices]))#*1e3*self.n + + self.cost_penalty = self.con_vals + + #return self.cost_penalty + + + + # ----- CONSTRAINTS FUNCTION ----- + # -------------------------------- + def constraintFunsUG(self, Xu): + '''The general constraints function. Will behave differently depending + on settings. Only input is the design variable vector, X.''' + + #X = self.generateGridPoints(Xu) + #Xum = np.hstack([[x*1000 for x in Xu[0:4]], Xu[4:]]) # convert first 4 entries from km to m + # print(Xu) + # if any([x>2500 for x in Xu]): + # breakpoint() + # update the layout with the specified design vector + self.updateLayoutUG(Xu) + #self.updateLayoutOPTUG(Xu) + return self.con_vals + + def constraintFunsDB(self, Xdb): + '''The general constraints function. Will behave differently depending + on settings. Only input is the design variable vector, X.''' + + # update the layout with the specified design vector + self.updateLayoutDB(Xdb) + + return self.con_vals + + + def constraintFuns(self, X): + '''The general constraints function. Will behave differently depending + on settings. Only input is the design variable vector, X.''' + + # update the layout with the specified design vector + self.updateLayoutOPT(X) + + return self.con_vals + + + def calcDerivatives(self): + '''Compute the derivatives about the current state of the layout, + for use with optimizers that accept a Jacobian function. + This is explicitly designed for when variables are x, y, and h. + >>> PLACEHOLDER <<< + ''' + + nDOF = 3*self.nt + ''' + # Perturb each DOF in turn and compute AEP results + J_AEP = np.zeros([nt,nt]) + + # Perturp each turbine and figure out the effects on cost and constraint + for i in range(nt): + J_CONS_i = np.zeros([ng, 3]) # fill in each row of this (or each column?) + + + # then combine them into overall matrices + + J_cost + + J_constraints.... + + # >>>> need to straighten out constraint vectors... + ''' + + def saveLOG(self, filename): + + with open(filename, 'w', newline='') as csvfile: + writer = csv.writer(csvfile) + # Write header + writer.writerow(['it','x', 'f', 'g']) + # Write data + for i in range(len(self.log['x'])): + it = [i] + x_values = self.log['x'][i] # Design variables + f_value = self.log['f'][i] # Result of objective function + g_values = self.log['g'][i] # Scalar, either -1 or 1 + + writer.writerow([it, x_values, f_value, g_values]) + + + # ----- Plot wind farm layout ----- + def plotLayout(self, ax=None, bare=False, save=False): + '''Plot wind farm layout.''' + + # if axes not passed in, make a new figure + if ax == None: + fig, ax = plt.subplots(1,1, figsize=[6,6]) + else: + fig = ax.get_figure() + + # Set font sizes + fsize_legend = 12 # Legend + fsize_ax_label = 12 # Ax Label + fsize_ax_ticks = 12 # Ax ticks + fsize_title = 16 # Title + + x0 = self.turb_coords[:,0] + y0 = self.turb_coords[:,1] + + + # Plot the layout, using the internally stored information. + + #breakpoint() + # ----- Bathymetry / contourf + + #num_levels = 10 # Adjust this value as needed + X, Y = np.meshgrid(self.grid_x, self.grid_y) + #breakpoint() + depth_min =np.min(self.grid_depth) + depth_min=math.floor(depth_min / 10) * 10 + depth_max =np.max(self.grid_depth) + depth_max=math.ceil(depth_min / 10) * 10 + + depth_range = depth_max- depth_min + + if depth_range < 100: + steps_m = 10 + else: + steps_m = 100 + + num_levels = round((depth_max- depth_min)/steps_m) + + + if depth_min != depth_max: + contourf = ax.contourf(X, Y, self.grid_depth, num_levels, cmap='Blues', vmin=depth_min, vmax=depth_max) + #contourf = ax.contourf(X, Y, self.grid_depth[x_indices, y_indices], num_levels, cmap='Blues', vmin=0, vmax=1000) + #contourf.norm.autoscale([0,1]) + + #contourf.set_clim(0, 1000) + + # Add colorbar with label + if not bare: + cbar = plt.colorbar(contourf, ax=ax, fraction=0.04, label='Water Depth (m)') + # Set the font size for the colorbar label and ticks + #cbar.ax.yaxis.label.set_fontsize(fsize_ax_label) + #cbar.ax.tick_params(axis='y', labelsize=fsize_ax_ticks) + + + # seabed + X, Y = np.meshgrid(self.soil_x, self.soil_y) + ax.scatter(X, Y, s=4, cmap='cividis_r', vmin=-0.5, vmax=1.5) + + # ----- OSS + for oo in self.oss_coords: + ax.scatter(oo[0],oo[1], color='red', marker='*', label='OSS', s=100) + circle = plt.Circle((oo[0], oo[1]), self.oss_minrad, edgecolor=[.5,0,0,.8], + facecolor='none', linestyle='dashed', lw=0.8) + + # (AEP: {aep / 1.0e9:.2f} GWh,\n CAPEX: M$ {cost/1.0e6:.2f},\n LCOE: {lcoe:.2f} $/MWh)' + + # plt.scatter(x, y, color='blue', marker='D') + # plt.scatter(optimized_x_pos, optimized_y_pos, label=f'Optimized Positions (AEP: {optimized_aep / 1.0e9:.2f} GWh)', color='red', marker='D') + + # Anchors + #plt.scatter(self.anchor_coords[:,0], self.anchor_coords[:,1], + # label='Anchor Positions', color='red', marker='.') + + # Plot mooring buffer zones + for i, polygon in enumerate(self.ms_bufferzones_pos): + if isinstance(polygon, MultiPolygon): + for poly in polygon: + x, y = poly.exterior.xy + ax.plot(x, y,color='red') + else: + x, y = polygon.exterior.xy + #ax.plot(x, y,color='red') + ax.fill(x, y,color=[.6,.3,.3,.6]) + # Add a single legend entry outside the loop + if not bare: + legend_entry = ax.fill([], [], color=[.6,.3,.3,.6], label='Mooring Buffer Zone') + + # Add a legend with fontsize + if not bare: + ax.legend(handles=legend_entry) #, fontsize=fsize_legend) + + # ----- mooring lines + for i in range(self.nt): + for j in range(3): + plt.plot([self.turb_coords[i,0], self.mooringList[3*i+j].rA[0]], + [self.turb_coords[i,1], self.mooringList[3*i+j].rA[1]], 'k', lw=0.5) + + # plt.plot([self.turb_coords[i,0], self.anchor_coords[3*i+j,0]], + # [self.turb_coords[i,1], self.anchor_coords[3*i+j,1]], 'k', lw=0.5) + + + + # ----- Minimum distance + i = 0 + for x, y in zip(x0, y0): + if i == 0: + circle = plt.Circle((x, y), self.turb_minrad, edgecolor=[.5,0,0,.8], + facecolor='none', linestyle='dashed', label='Turbine Buffer Zone', lw=0.8) + else: + circle = plt.Circle((x, y), self.turb_minrad, edgecolor=[.5,0,0,.8], + facecolor='none', linestyle='dashed', lw=0.8) + i =+ 1 + ax.add_patch(circle) + # Add a legend to the axes with fontsize + if not bare: + ax.legend() #fontsize=fsize_legend) + # plt.gca().add_patch(circle) + + # ----- Lease area boundary + #shape_polygon = sh.Polygon(self.boundary) + x, y = self.boundary_sh.exterior.xy + ax.plot(x, y, label='Boundary', linestyle='dashed', color='black') + + # ----- Sub boundaries + for subb in self.sub_boundary_sh: + x,y = subb.exterior.xy + ax.plot(x,y, label='Sub-boundary', linestyle=':', color='blue') + + + # ----- Exclusion zones + if len(self.exclusion) !=0: + for ie in range(len(self.exclusion)): + shape_polygon = self.exclusion_polygons_sh[ie]#sh.Polygon(self.exclusion[i]) + x, y = shape_polygon.exterior.xy + ax.plot(x, y, linestyle='dashed', color='orange', label='Exclusion Zone') + #ax.plot([], [], linestyle='dashed', color='orange', label='Exclusion Zone') + + # turbine locations + ax.scatter(x0, y0, c='black', s=12, label='Turbines') + + + + if self.cable_mode: + # ----- Cables + # Create a colormap and a legend entry for each unique cable section + # Find unique values + unique_cables = np.unique([x['conductor_area'] for x in self.iac_dic]) #(self.iac_dic['minimum_con'].values) + colors = plt.cm.viridis(np.linspace(0, 1, len(unique_cables))) # Create a colormap based on the number of unique sections + section_to_color = {sec: col for sec, col in zip(unique_cables, colors)} + + + # ----- Cables in Cluster + # Cable array + iac_array = self.iac_dic + count = 0 + # Loop over each cluster + for ic in range(self.n_cluster*self.noss): + # Plot vertices + #plt.scatter(self.cluster_arrays[ic][:, 0], self.cluster_arrays[ic][:, 1], color='red', label='Turbines') + + # Annotate each point with its index + #for i, point in enumerate(self.cluster_arrays[ic]): + #plt.annotate(str(i), (point[0], point[1]), textcoords="offset points", xytext=(0, 10), ha='center') + + # Get index of cluster + #ind_cluster = np.where(iac_array[:, 0] == 0)[0] + # Loop over edges / cable ids + len_cluster = len(np.where(np.array([x['cluster_id']==ic for x in iac_array]))[0]) + for i in range(len_cluster): + ix = np.where((np.array([x['cluster_id']== ic for x in iac_array])) & (np.array([y['cable_id']== count for y in iac_array]) ))[0] + if len(ix)<1: + breakpoint() + ind = ix[0] + #ind = np.where((iac_array[:, 0] == ic) & (iac_array[:, 2] == i))[0][0] + # Plot edge + #edge = self.iac_edges[ic][i] + start = iac_array[ind]['coordinates'][0]#self.cluster_arrays[ic][edge[0]] + end = iac_array[ind]['coordinates'][1] + # Cable selection + color = section_to_color[iac_array[ind]['conductor_area']] + ax.plot([start[0], end[0]], [start[1], end[1]], color=color, label=f'Section {int(iac_array[ind]["conductor_area"])} mm²' if int(iac_array[ind]["conductor_area"]) not in plt.gca().get_legend_handles_labels()[1] else "") + #plt.text((start[0] + end[0]) / 2, (start[1] + end[1]) / 2, str(i), fontsize=9, color='black') + # for sid in oss_ids: + # if iac_array[ix]['turbineA_glob_id'] == sid or iac_array[ix]['turbineB_glob_id'] == sid: + # iac_array_oss.append(iac_array[ix]) + # iac_oss_id.append(sid) + + count += 1 + + # Plot gate as a diamond marker + #plt.scatter(self.gate_coords[ic][0], self.gate_coords[ic][1], marker='D', color='green', label='Gate') + + + ## ----- Cables Gates to OSS + + # for i in range(self.n_cluster): + # cable_section_size = int(iac_array_oss[i]['conductor_area']) # Assuming cable section size is in the 7th column + # color = section_to_color.get(cable_section_size, 'black') # Default to black if section size not found + # oss_coord = self.substationList[iac_oss_id[i]].r + # ax.plot([iac_array_oss[i]['coordinates'][1][0], oss_coord[0]], [iac_array_oss[i]['coordinates'][1][1],oss_coord[1]], color=color, label=f'Section {cable_section_size} mm²' if cable_section_size not in plt.gca().get_legend_handles_labels()[1] else "") + + + + ''' + # NEW: TURBINE CLUSTER AND CABLES + # Plot turbines by cluster + for label in set(self.cluster_labels): + cluster_turbines = [self.turb_coords[i] for i, lbl in enumerate(self.cluster_labels) if lbl == label] + if cluster_turbines: # Check if list is not empty + x, y = zip(*cluster_turbines) + ax.scatter(x, y, label=f'Cluster {label}') + + # Plot edges + for i in range(len(self.cluster_edges)): + P = self.cluster_arrays[i] + for edge in self.cluster_edges[i]: + i, j = edge + plt.plot([P[i, 0], P[j, 0]], [P[i, 1], P[j, 1]], color ='black') + + # Plot OSS and gates + ax.scatter(*self.oss_coords, color='red', marker='*', label='OSS') + ax.scatter(self.gate_coords[:, 0], self.gate_coords[:, 1], color='black', marker='d', label='Gates') + + # Legend adjustment might be needed depending on the number of elements + #ax.legend(loc='upper center', fancybox=True, ncol=2) + ax.legend(loc='upper center', bbox_to_anchor=(0.5, -0.05), fancybox=True, ncol=2) + ''' + + + + # ----- Additional plot customization + # Set x- and y-axis ticks fontsize + if not bare: + ax.set_xticks(ax.get_xticks()) + ax.set_yticks(ax.get_yticks()) + ax.set_xticklabels(ax.get_xticklabels()) #, fontsize=fsize_ax_ticks) + ax.set_yticklabels(ax.get_yticklabels()) #, fontsize=fsize_ax_ticks) + + # # Define a custom formatter to divide ticks by 1000 + # def divide_by_1000(value, tick_number): + # return f'{value/1000:.0f}' + + # # Apply the custom formatter to the x and y axis ticks + # ax.xaxis.set_major_formatter(FuncFormatter(divide_by_1000)) + # ax.yaxis.set_major_formatter(FuncFormatter(divide_by_1000)) + + #ax.axis("equal") + ax.set_aspect('equal') + + # # Use AutoLocator for major ticks + # ax.xaxis.set_major_locator(AutoLocator()) + # ax.yaxis.set_major_locator(AutoLocator()) + # # Use AutoMinorLocator for minor ticks + # ax.xaxis.set_minor_locator(AutoMinorLocator()) + # ax.yaxis.set_minor_locator(AutoMinorLocator()) + + # ax.set_xlim([self.grid_x[0], self.grid_x[-1]]) + # ax.set_ylim([self.grid_y[0], self.grid_y[-1]]) + #ax.set_xlim([x_min_bounds-1000, x_max_bounds+1000]) + #ax.set_ylim([y_min_bounds-1000, y_max_bounds+1000]) + + + #plt.title('Optimized Wind Farm Layout',fontsize=fsize_title) + plt.xlabel('x (km)') #,fontsize=fsize_ax_label) + plt.ylabel('y (km)') #,fontsize=fsize_ax_label) + #plt.legend(loc='upper center', bbox_to_anchor=( + # 0.5, -0.2), fancybox=True, ncol=3) + #plt.legend(loc='upper center', fancybox=True, ncol=2) + handles, labels = plt.gca().get_legend_handles_labels() + unique_labels = list(set(labels)) # Get unique labels + unique_labels.sort() # Sort the unique labels alphabetically + unique_handles = [handles[labels.index(label)] for label in unique_labels] # Get handles corresponding to unique labels + plt.legend(unique_handles, unique_labels, loc='upper center', bbox_to_anchor=(0.5, -0.1), fancybox=True, ncol=2) + plt.gca().set_aspect('equal', adjustable='box') # Set aspect ratio to be equal + #ax.legend(loc='upper center', bbox_to_anchor=(0.5, -0.1), fancybox=True, ncol=2) + + #breakpoint() + # Set plot area to around the lease area + + + # Calc plot bounds + #offset = 1000 + #offset_polygon = translate(self.boundary_sh, xoff=offset, yoff=offset) + # Get bounds + #x_min_bounds, y_min_bounds, x_max_bounds, y_max_bounds = offset_polygon.bounds + # Round to next 100 + #x_min_bounds, y_min_bounds = [math.floor(v / 1000) * 1000 for v in (x_min, y_min)] + #x_max_bounds, y_max_bounds = [math.ceil(v / 1000) * 1000 for v in (x_max, y_max)] + + + + # ----- Save plot with an incremented number if it already exists + if save: + counter = 1 + output_filename = f'wind farm layout_{counter}.png' + while os.path.exists(output_filename): + counter += 1 + output_filename = f'wind farm layout_{counter}.png' + + # Increase the resolution when saving the plot + plt.savefig(output_filename, dpi=300, bbox_inches='tight') # Adjust the dpi as needed + + # also print some output + + if self.flow: # if FLORIS + print('AEP:', self.aep) + + self.getCost() + print('Cost:', self.cost_total) + + # for mooring in self.mooringList.values(): + # print(mooring.cost) + + """ + def plot3d(self, ax=None, figsize=(10,8), fowt=None, save=False, + draw_boundary=True, boundary_on_bath=True, args_bath={}, draw_axes=True): + '''Plot aspects of the Project object in matplotlib in 3D. + + TODO - harmonize a lot of the seabed stuff with MoorPy System.plot... + + Parameters + ---------- + ... + ''' + + # color map for soil plotting + import matplotlib.cm as cm + from matplotlib.colors import Normalize + cmap = cm.cividis_r + norm = Normalize(vmin=-0.5, vmax=1.5) + #print(cmap(norm(np.array([0,1])))) + + + # if axes not passed in, make a new figure + if ax == None: + fig = plt.figure(figsize=figsize) + ax = plt.axes(projection='3d') + else: + fig = ax.get_figure() + + # try icnraesing dpeht grid density for nicer plot + xs = np.arange(-1000,8000,500) + ys = np.arange(-1000,9500,500) + #self.setGrid(xs, ys) + zs = np.zeros([len(ys), len(xs)]) + for i in range(len(ys)): + for j in range(len(xs)): + zs[i,j] = self.getDepthAtLocation(xs[j], ys[i]) + X, Y = np.meshgrid(xs, ys) # 2D mesh of seabed grid + + + # plot the bathymetry in matplotlib using a plot_surface + #X, Y = np.meshgrid(self.grid_x, self.grid_y) # 2D mesh of seabed grid + ax.plot_surface(X, Y, -zs, **args_bath) + ''' + # interpolate soil rockyness factor onto this grid + xs = self.grid_x + ys = self.grid_y + rocky = np.zeros([len(ys), len(xs)]) + for i in range(len(ys)): + for j in range(len(xs)): + rocky[i,j], _,_,_,_ = sbt.interpFromGrid(xs[j], ys[i], + self.soil_x, self.soil_y, self.soil_rocky) + # apply colormap + rc = cmap(norm(rocky)) + bath = ax.plot_surface(X, Y, -self.grid_depth, facecolors=rc, **args_bath) + ''' + #bath = ax.plot_surface(X, Y, -self.grid_depth, **args_bath) + # + + + # also if there are rocky bits... (TEMPORARY) + ''' + X, Y = np.meshgrid(self.soil_x, self.soil_y) + Z = np.zeros_like(X) + xs = self.soil_x + ys = self.soil_y + for i in range(len(ys)): + for j in range(len(xs)): + Z[i,j] = -self.getDepthAtLocation(xs[j], ys[i]) + ax.scatter(X, Y, Z+5, c=self.soil_rocky, s=6, cmap='cividis_r', vmin=-0.5, vmax=1.5, zorder=0) + ''' + + # plot the project boundary + if draw_boundary: + boundary = np.vstack([self.boundary, self.boundary[0,:]]) + ax.plot(boundary[:,0], boundary[:,1], np.zeros(boundary.shape[0]), + 'b--', zorder=100, lw=1, alpha=0.5) + + # plot the projection of the boundary on the seabed, if desired + if boundary_on_bath: + boundary_z = self.projectAlongSeabed(boundary[:,0], boundary[:,1]) + ax.plot(boundary[:,0], boundary[:,1], -boundary_z, 'k--', zorder=10, lw=1, alpha=0.7) + + # plot the Moorings + for mooring in self.mooringList: + #mooring.subsystem.plot(ax = ax, draw_seabed=False) + if mooring.subsystem: + mooring.subsystem.drawLine(0, ax, shadow=False) + + # plot the FOWTs using a RAFT FOWT if one is passed in (TEMPORARY) + if fowt: + for i in range(self.nt): + xy = self.turb_coords[i,:] + fowt.setPosition([xy[0], xy[1], 0,0,0,0]) + fowt.plot(ax, zorder=20) + + # Show full depth range + ax.set_zlim([-np.max(self.grid_depth), 0]) + + set_axes_equal(ax) + if not draw_axes: + ax.axis('off') + + ax.view_init(20, -130) + ax.dist -= 3 + fig.tight_layout() + + # ----- Save plot with an incremented number if it already exists + if save: + counter = 1 + output_filename = f'wind farm 3d_{counter}.png' + while os.path.exists(output_filename): + counter += 1 + output_filename = f'wind farm 3d_{counter}.png' + + # Increase the resolution when saving the plot + plt.savefig(output_filename, dpi=300, bbox_inches='tight') # Adjust the dpi as needed + """ + + def playOptimization(self): + '''A very slow clunky way to animate the optimization''' + fig, ax = plt.subplots(1,1) + + self.updateLayout(self.log['x'][0]) + self.plotLayout(ax=ax) + + def animate(i): + ax.clear() + self.updateLayout(self.log['x'][i]) + self.plotLayout(ax=ax, bare=True) + + ani = FuncAnimation(fig, animate, frames=len(self.log['x']), + interval=500, repeat=True) + + return ani + + + + + + + + + + + + + + + + + + + + + + def plotOptimization(self): + + if len(self.log['x']) == 0: + print("No optimization trajectory saved (log is empty). Nothing to plot.") + return + + + + fig, ax = plt.subplots(5,1, sharex=True, figsize=[6,8]) + fig.subplots_adjust(left=0.4) + + X = np.array(self.log['x']) + Fs = np.array(self.log['f']) + Gs = np.array(self.log['g']) + + + if self.rotation_mode: + x_pos, y_pos, rot_rad = X[:,:self.nt], X[:,self.nt:2*self.nt], X[:,2*self.nt:] + else: + x_pos, y_pos = X[:,:len(X)//2], X[:,len(X)//2:] + rot_rad = np.zeros_like(x_pos) + + for i in range(self.nt): + ax[0].plot(x_pos[:,i]) + ax[1].plot(y_pos[:,i]) + ax[2].plot(rot_rad[:,i]) + + ax[3].plot(Fs) + ax[3].set_ylabel("cost", rotation='horizontal') + + Gs_neg = Gs*(Gs < 0) + ax[4].plot(np.sum(Gs_neg, axis=1)) + ax[4].set_ylabel("constaint violation sum", rotation='horizontal') + ''' + for i, con in enumerate(self.constraints): + j = i+1+len(X) + ax[j].axhline(0, color=[0.5,0.5,0.5]) + ax[j].plot(Gs[:,i]) + ax[j].set_ylabel(f"{con['name']}({con['threshold']})", + rotation='horizontal', labelpad=80) + ''' + ax[-1].set_xlabel("iteration roughly") + + + + """ + nX = len(self.log['x'][0]) + fig, ax = plt.subplots(nX+1+1,1, sharex=True, figsize=[6,8]) + fig.subplots_adjust(left=0.4) + Xs = np.array(self.log['x']) + Fs = np.array(self.log['f']) + Gs = np.array(self.log['g']) + + for i in range(nX): + ax[i].plot(Xs[:,i]) + #ax[i].axhline(self.Xmin[i], color=[0.5,0.5,0.5], dashes=[1,1]) + #ax[i].axhline(self.Xmax[i], color=[0.5,0.5,0.5], dashes=[1,1]) + + ax[nX].plot(Fs) + ax[nX].set_ylabel("cost", rotation='horizontal') + + Glist = Gs.ravel() + + ax[nX+1].plot(np.sum(Glist[Glist<0])) + ax[nX+1].set_ylabel("constaint violation sum", rotation='horizontal') + ''' + for i, con in enumerate(self.constraints): + j = i+1+len(X) + ax[j].axhline(0, color=[0.5,0.5,0.5]) + ax[j].plot(Gs[:,i]) + ax[j].set_ylabel(f"{con['name']}({con['threshold']})", + rotation='horizontal', labelpad=80) + ''' + ax[-1].set_xlabel("iteration roughly") + """ + + def plotCost(self): + '''Makes a bar chart of the cost breakdown.''' + + + def plotWakes(self, wind_spd, wind_dir, ti): + '''uses floris tools to plot wakes''' + import floris.layout_visualization as layoutviz + from floris.flow_visualization import visualize_cut_plane + + fmodel = self.flow + + # Create the plotting objects using matplotlib + fig, ax = plt.subplots() + + + layoutviz.plot_turbine_points(fmodel, ax=ax) + layoutviz.plot_turbine_labels(fmodel, ax=ax) + ax.set_title("Turbine Points and Labels") + ax.set_xlabel('X (m)') + ax.set_ylabel('Y (m)') + + + + fmodel.set(wind_speeds=[wind_spd], wind_directions=[wind_dir], turbulence_intensities=[ti]) + horizontal_plane = fmodel.calculate_horizontal_plane( + x_resolution=200, + y_resolution=100, + height=90.0, + ) + + # Plot the flow field with rotors + fig, ax = plt.subplots() + visualize_cut_plane( + horizontal_plane, + ax=ax, + label_contours=False, + title="Horizontal Flow with Turbine Rotors and labels", + ) + ax.set_xlabel('X (m)') + ax.set_ylabel('Y (m)') + + # Plot the turbine rotors + layoutviz.plot_turbine_rotors(fmodel, ax=ax) + + plt.show() + +# Calculate offset from the turbine to create buffer zones for cable routing +def get_point_along_line(start, end, diste): + # Convert inputs to numpy arrays + start = np.array(start) + end = np.array(end) + # Calculate the direction vector from start to end + direction = end - start + # Normalize the direction vector + length = np.linalg.norm(direction) + unit_direction = direction / length + # Calculate the new point at the specified distance along the direction vector + new_point = start + unit_direction * diste + return new_point + +# def mooringAdjuster1(mooring, project, r, u, level=0): +# '''Custom function to adjust a mooring, called by +# Mooring.adjust. Fairlead point should have already +# been adjusted.''' + +# ss = mooring.ss # shorthand for the mooring's subsystem + +# T_target = 1e6 # target mooring line pretension [N] (hardcoded example) +# i_line = 0 # line section to adjust (if multiple) (hardcoded example) + +# #>>> pit in better prpfile <<< + +# # Find anchor location based on desired relation +# r_i = np.hstack([r + 58*u, -14]) # fairlead point +# slope = 0.58 # slope from horizontal +# u_a = np.hstack([u, -slope]) # direct vector from r_i to anchor +# r_anch = project.seabedIntersect(r_i, u_a) # seabed intersection + +# # save some stuff for the heck of it +# mooring.z_anch = r_anch[2] +# mooring.anch_rad = np.linalg.norm(r_anch[:2]-r) + +# mooring.setEndPosition(r_anch, 'a') # set the anchor position + +# # Estimate the correct line length to start with +# ss.lineList[0].setL(np.linalg.norm(mooring.rB - mooring.rA)) + +# # Next we could adjust the line length/tension (if there's a subsystem) +# if level==1: # level 1 analysis (static solve to update node positions) +# ss.staticSolve() + +# elif level==2: # adjust pretension (hardcoded example method for now) + +# def eval_func(X, args): +# '''Tension evaluation function for different line lengths''' +# ss.lineList[i_line].L = X[0] # set the first line section's length +# ss.staticSolve(tol=0.0001) # solve the equilibrium of the subsystem +# return np.array([ss.TB]), dict(status=1), False # return the end tension + +# # run dsolve2 solver to solve for the line length that matches the initial tension +# X0 = [ss.lineList[i_line].L] # start with the current section length +# L_final, T_final, _ = dsolve2(eval_func, X0, Ytarget=[T_target], +# Xmin=[1], Xmax=[1.1*np.linalg.norm(ss.rB-ss.rA)], +# dX_last=[1], tol=[0.1], maxIter=50, stepfac=4) +# ss.lineList[i_line].L = L_final[0] + + +# # Compute anchor size and cost +# soilr = project.getSoilAtLocation(*r_anch[:2]) +# if 'rock' in soilr: +# rocky = 1 +# else: +# rocky = 0 + +# anchor_cost = 300e3 + rocky*200e3 +# mooring.cost['anchor'] = anchor_cost + + + # getWatchCircle() method + # getMudlineForces(, max_forces=True) + + + + + +if __name__ == '__main__': + + # Wind rose + from floris import WindRose + wind_rose = WindRose.read_csv_long( + 'humboldt_rose.csv', wd_col="wd", ws_col="ws", freq_col="freq_val", ti_col_or_value=0.06) + + + # ----- LEASE AREA BOUNDARIES ----- + WestStart = 10000 + NorthStart = 10000 + boundary_coords = np.array([ + (0, 0), + (WestStart, 0), + (WestStart, NorthStart), + (0,NorthStart) + ]) + + + + # Make a sample Subsystem to hold the mooring design (used for initialization) + print("Making subsystem") + newFile = '..\scripts\input_files\GoMxOntology.yaml' + project = Project(file=newFile,raft=0) + project.getMoorPyArray() + ss = deepcopy(project.ms.lineList[0]) + + # ----- Set optimization mode + opt_mode = 'CAPEX' + #opt_mode = 'AEP' + #opt_mode = 'LCOE' + # remember to set use_FLORIS accordingly when initializing Layout + + # set substation location + oss_coords = np.array([0, 0]) + + + + #layouttype = 'freelayout' + layouttype = 'uniformgridlayout' + + # ----- UNIFORM GRID ----- + if layouttype == 'uniformgridlayout': + + + + #[grid_spacing_x, grid_spacing_y, grid_trans_x, grid_trans_y, grid_rotang, grid_skew, optional turb_rotation] + Xu = [1000/1000, 1000/1000, 500/1000, 150/1000, 45, 0, 0] + + + + # Amount of wind turbines + nt = 20 + + #rotation mode and turbine rotation + rotation_mode = True + rot_rad=np.zeros((nt)) + + + # Boundaries of design variables for PSO + boundaries_UG=np.array([[0.5, 3],[0.5, 3], [-.5, .5], [-.5, .5], [0, 180], [0,0.2],[0, 180] ]) + + #cable routing + cable_mode = True + + # ----- FREE LAYOUT ----- + elif layouttype == 'freelayout': + + #first iteration turbine coordinates + gulfofmaine_int = np.array([ + [3000, 2000], + [2000, 2000], + [0, 2000], + [1000, 2000], + [2000, 2000], + [1000, 100], + [2000, 100], + [0, 4000], + [1000, 4000], + [2000, 4000] + ]) + + + x_coords = gulfofmaine_int[:, 0] # x coordinates + y_coords = gulfofmaine_int[:, 1] # y coordinates + + nt = len(x_coords) # Number of wind turbines + + #first iteration rotations + rot_deg = np.zeros((nt)) + + + # ----- Bounds vectors for design variables ----- + # FOR OPTIMIZER ONLY + # Lease area boundaries + boundaries_x = np.tile([(min(boundary_coords[:,0]), max(boundary_coords[:,0]))], (nt, 1)) + boundaries_y = np.tile([(min(boundary_coords[:,1]), max(boundary_coords[:,1]))], (nt, 1)) + # Rotation in rad 0 - 360*pi/180 + boundaries_rot = np.tile([(0.001, 6.283)], (nt, 1)) + # Combine into one array + + + + # ----- Set rotation mode + # If True, rotations are considered as design variable, therefore included + # into same vector as x and y. Otherwise not. + rotation_mode = True + rot_rad = np.deg2rad(rot_deg) # Rotations need to be in rad for the optimization + x = np.array(x_coords/1000) #km + y = np.array(y_coords/1000) #km + + # Create flattened array xy for initial positions for Layout [km, rad] + if rotation_mode: + xy = np.concatenate((x, y, rot_rad)) + boundary_xy = np.concatenate((boundaries_x/1000, boundaries_y/1000, boundaries_rot)) + else: + xy = np.concatenate((x, y)) + boundary_xy = np.concatenate((boundaries_x/1000, boundaries_y/1000)) + + # cable routing + cable_mode = True + + + + # ----- Initialize LAYOUT class ----- + print("Initializing Layout") + + settings = {} + settings['n_turbines'] = nt + settings['turb_rot'] = rot_rad + settings['rotation_mode'] = rotation_mode + settings['cable_mode'] = cable_mode + settings['oss_coords'] = oss_coords + settings['boundary_coords'] = boundary_coords + settings['bathymetry_file'] = '..\scripts\input_files\GulfOfMaine_bathymetry_100x100.txt' + settings['soil_file'] = '..\scripts\input_files\soil_sample.txt' + settings['floris_file']='gch_floating.yaml' + #settings['exclusion_coords'] = exclusion_coords + settings['use_FLORIS'] = False + settings['mode'] = opt_mode + settings['optimizer'] ='PSO' + settings['obj_penalty'] = 1 + settings['parallel'] = False + settings['n_cluster'] = 3 + + # set up anchor dictionary + anchor_settings = {} + anchor_settings['anchor_design'] = {'L':20,'D':4.5,'zlug':13.3} # geometry of anchor + anchor_settings['anchor_type'] = 'suction' # anchor type + anchor_settings['anchor_resize'] = True # bool to resize the anchor or not + anchor_settings['fix_zlug'] = False # bool to keep zlug the same when resizing anchor + anchor_settings['FSdiff_max'] = {'Ha':.2,'Va':.2} # max allowed difference between FS and minimum FS + anchor_settings['FS_min'] = {'Ha':2,'Va':2} # horizontal and vertical minimum safety factors + + settings['anchor_settings'] = anchor_settings + + + if layouttype == 'freelayout': + layout1 = Layout(X=xy, Xu=[], wind_rose = wind_rose, ss=ss, **settings) + elif layouttype == 'uniformgridlayout': + layout1 = Layout(X=[], Xu=Xu, wind_rose = wind_rose, ss=ss, **settings) + + + + ''' + # ----- Sequential Least Squares Programming (SLSQP) + if layouttype == 'freelayout': + res = minimize(fun=layout1.objectiveFun, x0=xy, method='SLSQP', + bounds = boundary_xy, + constraints={'type': 'ineq', 'fun': layout1.constraintFuns}, + options={'maxiter':100, 'eps':0.02,'ftol':1e-6, 'disp': True, 'iprint': 99}) + #options={'maxiter': 5000,'eps': 0.2, 'finite_diff_rel_step': '2-point', 'ftol': 1e-6, 'disp': True, 'iprint': 99}) + elif layouttype == 'uniformgridlayout': + res = minimize(fun=layout1.objectiveFunUG, x0=Xu, method='SLSQP', + bounds = boundaries_UG, + constraints={'type': 'ineq', 'fun': layout1.constraintFunsUG}, + options={'maxiter':1, 'eps':0.02,'ftol':1e-6, 'disp': True, 'iprint': 99}) + ''' + + ''' + # ----- Constrained Optimization BY Linear Approximation (COBYLA) + res = minimize(fun=layout1.objectiveFun, x0=xy, method='COBYLA', + constraints={'type': 'ineq', 'fun': layout1.constraintFuns}, + options={'maxiter':2000,'catol': 1e-6, 'tol': 1e-6, 'disp': True}) + ''' + + + ''' + # ----- Differential Evolution (DE) + # NonlinearConstraint + # https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.NonlinearConstraint.html#scipy.optimize.NonlinearConstraint + #cons_fun = NonlinearConstraint(fun, lb, ub, jac='2-point', hess=, keep_feasible=False, finite_diff_rel_step=None, finite_diff_jac_sparsity=None) + cons_fun = NonlinearConstraint(layout1.constraintFuns, lb = 0, ub = np.inf) + + # takes FOREVER and is not the best solution + res = differential_evolution(func=layout1.objectiveFun, bounds=boundary_xy, args=(), strategy='best1bin', + maxiter=1000, popsize=25, tol=0.01, mutation=(0.5, 1.0), + recombination=0.7, seed=None, callback=None, disp=True, polish=True, + init='latinhypercube', atol=0, updating='immediate', workers=1, + constraints=cons_fun, x0=xy, integrality=None, vectorized=False) + ''' + + + # ----- Particle Swarm Optimization + + # Other PSO (PSO with Scipy interface, but not that elaborated?) + # https://github.com/jerrytheo/psopy?tab=readme-ov-file + + + # Pyswarm (NOT pyswarms) + # https://pythonhosted.org/pyswarm/ + + if layouttype == 'freelayout': + res, fopt = pso(layout1.objectiveFun, lb=boundary_xy[:,0], ub=boundary_xy[:,1], f_ieqcons=layout1.constraintFuns, + swarmsize=20, omega=0.72984, phip=0.6, phig=0.8, maxiter=20, minstep=1e-8, minfunc=1e-8, debug=True) + elif layouttype == 'uniformgridlayout': + res, fopt = pso(layout1.objectiveFunUG, lb=boundaries_UG[:,0], ub=boundaries_UG[:,1], f_ieqcons=layout1.constraintFunsUG, + swarmsize=20, omega=0.72984, phip=0.6, phig=0.8, maxiter=20, minstep=1e-8, minfunc=1e-8, debug=True) + + + if layouttype == 'freelayout': + layout1.updateLayoutOPT(res) # make sure it is using the optimized layout => ONLY NEEDED WHEN INPUT WAS in km + layout1.updateLayout(X=[], level=2, refresh=True) # do a higher-fidelity update + layout1.plotLayout(save=True) + + elif layouttype == 'uniformgridlayout': + + #optimized_xy_m = [1400, 1400, 500, 1000, 45, 0] + + layout1.updateLayoutUG(Xu=res, level=2, refresh=True) # do a higher-fidelity update + layout1.plotLayout(save=True) + + + + plt.show() + + + + + +########################## END +# ARCHIVE diff --git a/famodel/design/layout_helpers.py b/famodel/design/layout_helpers.py new file mode 100644 index 00000000..64f5f8c7 --- /dev/null +++ b/famodel/design/layout_helpers.py @@ -0,0 +1,1177 @@ +import moorpy as mp +from moorpy.helpers import dsolve2 +import numpy as np +import matplotlib.pyplot as plt +from shapely import Point, Polygon +from numpy import random +from copy import deepcopy +import time + + +from famodel.mooring.mooring import Mooring +from famodel.seabed.seabed_tools import getDepthFromBathymetry +from famodel.project import Project +from fadesign.fadsolvers import dsolve2 + + +def create_initial_layout(lease_xs, lease_ys, ms, grid_x, grid_y, grid_depth, update_ms=True, display=0): + ''' + The first iteration of a layout generator function based off of Katherine's previous work in summer 2023. + The idea is to come up with turbine locations within a lease area boundary that can be oriented various directions, + not overlap other mooring systems, and not extend outside of a lease area boundary. + In reality, I'd imagine this function would become obsolete, as we could populate a lease area with random points and + then optimize their positions, but the capabilities within this function can be used as a starting point to + incorporate into the optimization process. + + Right now, it loops through a "grid" of x and y positions, spaced relatively close together and calculates the anchor positions + of that turbine (using an adjustable mooring system orientation) by extending or shortening the mooring line until it + contacts the seabed bathymetry along that path. Using these new anchor positions, the function then checks each turbine + position on whether 1) it, and the anchor x/y positions are within the lease area bounds and 2) the given triangle that + connects the anchor points overlaps any other existing triangles (i.e., footprints). If it satisfies those two criteria, + then it appends that turbine x/y position to the list of valid points. + + Parameters + ---------- + lease_xs : float, array + The x coordinates of coordinate pairs defining a layout boundary, relative to a certain point (usually a centroid) + lease_ys : float, array + The y coordinates of coordinate pairs defining a layout boundary, relative to a certain point (usually a centroid) + ms : MoorPy System object + A MoorPy System object defining a mooring system + grid_x : float, array + The x coordinates of coordinate pairs defining a bathymetry grid + grid_y : float, array + The y coordinates of coordinate pairs defining a bathymetry grid + grid_depth: float, 2D matrix + The depth (z coordinates) of the grid defined by grid_x and grid_y + + Returns + ------- + xs : float, array + A list of the x coordinates of turbine locations within the array + ys : float, array + A list of the y coordinates of turbine locations within the array + footprintList : list, Polygon objects + A list of shapely Polygon objects of each mooring system footprint based on anchor locations + ''' + + coords = [] + + area = Polygon([(lease_xs[i], lease_ys[i]) for i in range(len(lease_xs))]) + + # Brainstorm different initialization approaches + # - Placing one at a starting point and filling in from there + # - Choosing a predetermined number of turbine and making them fit + # - Placing anchors and working backwards + # not sure which one is the best right now; will stick with the first one of choosing a starting point and filling in around + + # Placing approaches: + # make a very fine xlocs and ylocs grid + # loop through and find the first point in the lease area that has all 3 anchors in the lease area + # - the next point that's tested will obviously be way too close to the first point, but this will allow for better placement + # make an "orientation" variable in case we want to switch the orientations (ms.transform) (typically either 180 or 0) (later todo item) + + xlocs = np.arange(np.min(lease_xs), np.max(lease_xs)+1, 1000) # set a really small spacing between + ylocs = np.arange(np.min(lease_ys), np.max(lease_ys)+1, 1000) + + # if you want to change the "starting" point, you will need to rearrange the xlocs and ylocs variables + # something like "xlocs = np.hstack([(xlocs[int(len(xlocs))/2):], xlocs[:int(len(xlocs))/2)]])" + + # count how many anchor points there are + anchors = [point.number for point in ms.pointList if point.r[2]==-ms.depth] # <<<<< might need to change this assumption later on checking if it's on the seabed + fairleads = ms.bodyList[0].attachedP + + # initialize a couple storage/bool variables + invalid = False + footprintList = [] + msList = [] + counter = 0 + + # placeholder to deal with the mooring system orientation + orientation = -180 + + # loop through the xlocs and ylocs variables to test x/y positions to place turbines + for ix in range(len(xlocs)): + for iy in range(len(ylocs)): + anchorGlobalTempList = [] + anchorLocalTempList = [] + + # set the x/y position of a point to test + point = [xlocs[ix], ylocs[iy]] + + # orient the mooring system around that point by a certain amount + orientation = 0 + #orientation += 180 + #orientation = random.choice([0,90,180,270]) + ms.transform(rot=orientation) + + # reinitialize the mooring system after reorientation + ms.initialize() + ms.solveEquilibrium() + + # loop through the anchors in the mooring system and evaluate whether they meet the criteria + for i,anchornum in enumerate(anchors): + + old_anchor_point = ms.pointList[anchornum-1].r + np.array([point[0], point[1], 0]) + fairlead_point = ms.pointList[fairleads[i]-1].r + np.array([point[0], point[1], 0]) + # update the anchor point based on how close it is to the bathymetry + new_anchor_point = getUpdatedAnchorPosition(old_anchor_point, fairlead_point, grid_x, grid_y, grid_depth) + + # check to make sure the updated anchor point is within the lease area boundary + if not area.contains(Point(new_anchor_point[0], new_anchor_point[1])): + invalid = True + + # save the anchor point for later, regardless of whether it fits or not + anchorGlobalTempList.append(new_anchor_point) + anchorLocalTempList.append(new_anchor_point - np.array([point[0], point[1], 0])) + + + # create new lists/polygons of using the anchor positions of this one turbine point + #anchorList.append([anchor_point for anchor_point in anchorGlobalTempList]) + + # create a shapely polygon made up of the anchor points + new_boundary = Polygon( [(anchor_point[0], anchor_point[1]) for anchor_point in anchorGlobalTempList] ) + + # check to make sure that the newly created polygon does not intersect any other polygons + for moor_sys in footprintList: + if moor_sys.intersects(new_boundary): + invalid = True + + + + + + # if all checks pass, then include this point to the list of coordinates of the farm and include the boundary polygon to reference later + if invalid==False: + + # save the point to a list of "coords" + coords.append(point) + if display > 0: print(f"Appending Point ({point[0]:6.1f}, {point[1]:6.1f}) to the coords list") + + # add to the counter for the number of turbines that meet criteria + counter += 1 + if display > 0: print(f'nTurbines = {counter}') + + # save the polygon footprint of the anchor points + footprintList.append(Polygon( [(anchor_point[0], anchor_point[1]) for anchor_point in anchorGlobalTempList] ) ) + + + # if you want to update the mooring system line lengths to match pretension + if update_ms: + if display > 0: print(f"Updating the mooring system at Point ({point[0]:6.1f}, {point[1]:6.1f}) ") + + # create a copy of the MoorPy System and of the anchor point list + mscopy = deepcopy(ms) + + # adjust the MoorPy System line lengths to keep the same pretension (calls another internal function) + #ms_new = adjustMS4Pretension(mscopy, anchorLocalTempList) + ms_new = adjustMS4Bath(mscopy, point, grid_x, grid_y, grid_depth, display=display) + + else: + ms_new = deepcopy(ms) + + # save the adjusted (or not adjusted mooring system) + msList.append(ms_new) + + + + # reset the invalid flag variable in case it was changed to true + invalid = False + + + # extract the x and y variables from the list of points + xs = [xy[0] for xy in coords] + ys = [xy[1] for xy in coords] + + return xs, ys, footprintList, msList + + + +def create_layout(bound_xs, bound_ys, subsystem, grid_x, grid_y, grid_depth, + spacing_x, spacing_y, headings=[60, 180, 300]): + ''' + Create a rectangular grid layout. + + Parameters + ---------- + lease_xs : float, array + The x coordinates of coordinate pairs defining a layout boundary, relative to a certain point (usually a centroid) + lease_ys : float, array + The y coordinates of coordinate pairs defining a layout boundary, relative to a certain point (usually a centroid) + subsystem : MoorPy Subsystem object + A MoorPy Subsystem object defining the mooring configuration to be used. + grid_x : float, array + The x coordinates of coordinate pairs defining a bathymetry grid + grid_y : float, array + The y coordinates of coordinate pairs defining a bathymetry grid + grid_depth: float, 2D matrix + The depth (z coordinates) of the grid defined by grid_x and grid_y + spacing_x : float + The x spacing between turbines [m]. + spacing_y : float + The y spacing between turbines [m]. + + Returns + ------- + xs : float, array + A list of the x coordinates of turbine locations within the array + ys : float, array + A list of the y coordinates of turbine locations within the array + footprintList : list, Polygon objects + A list of shapely Polygon objects of each mooring system footprint based on anchor locations + ''' + + + # make sure the subsystem is initialized + subsystem.initialize() + + # save dimensions from the subsystem + rad_anch = np.linalg.norm(subsystem.rA[:2]) + rad_fair = np.linalg.norm(subsystem.rB[:2]) + z_anch = subsystem.rA[2] + z_fair = subsystem.rB[2] + + # initialize some lists + coords = [] + mooringList = [] + footprintList = [] + + # make the bounds into a shapely Polygon + area = Polygon([(bound_xs[i], bound_ys[i]) for i in range(len(bound_xs))]) + + # Grid of turbine locations (only those in the boundaries will be kept) + xlocs = np.arange(np.min(lease_xs), np.max(lease_xs)+1, spacing_x) + ylocs = np.arange(np.min(lease_ys), np.max(lease_ys)+1, spacing_y) + + mooring_count = 0 + + # loop through the xlocs and ylocs variables to test x/y positions to place turbines + for ix in range(len(xlocs)): + for iy in range(len(ylocs)): + + valid = True # flag for whether the turbine position satisfies requirements + + # set the x/y position of a point to test + point = [xlocs[ix], ylocs[iy]] + + # make sure the turbine location is in the boundary + if not area.contains(Point(point)): + valid = False + + # assume "orientation" is always 0 + # initialize a list + anchorlist = [] + ssList = [] + + # at the current grid point, set the anchor and fairlead points of the subsystem using a list of line heading angles and adjust for bathymetry + for ang in headings: + + if not valid: + break + + th = np.radians(ang) + + # set the local anchor and fairlead points + r_anch = np.hstack([rad_anch*np.array([np.cos(th), np.sin(th)])+point, z_anch]) + r_fair = np.hstack([rad_fair*np.array([np.cos(th), np.sin(th)])+point, z_fair]) + + mooring_count += 1 + print(f"Mooring count is {mooring_count}.") + + ss = deepcopy(subsystem) # make a copy from the original since we'll be iterating on this object + + # set the anchor and fairlead points of the subsystem + #subsystem_copy.pointList[0].setPosition(r_anch) + ss.setEndPosition(r_anch, endB=0) + #subsystem_copy.pointList[-1].setPosition(r_fair) + ss.setEndPosition(r_fair, endB=1) + ss.staticSolve() + + + # adjust subsystem for bathymetry (adjusting anchor points and line lengths) + adjustSS4Bath(ss, grid_x, grid_y, grid_depth, display=0) + + new_anchor_point = ss.rA + anchorlist.append(new_anchor_point) + + ssList.append(ss) # add it to a temporary list for just this turbine + + # if any new anchor point is outside the bounds of the Polygon area, then this point is invalid + if not area.contains(Point(new_anchor_point)): + valid = False + + # if not valid, skip the rest of this point in the for loop + if not valid: + continue + + # after checking all new anchor points for each line heading, check to make sure the new footprint doesn't overlap with any others + new_footprint = Polygon( [(anchor_point[0], anchor_point[1]) for anchor_point in anchorlist] ) + + # check to make sure that the newly created polygon does not intersect any other polygons + for footprint in footprintList: + if footprint.intersects(new_footprint): + valid = False + + # make the moorings and add to the master lists if valid + if valid: + + for i, ss in enumerate(ssList): + mooringList.append(Mooring(subsystem=ss, rA=ss.rA, rB=ss.rB)) + + coords.append(point) + + footprintList.append( Polygon( [(anchor_point[0], anchor_point[1]) for anchor_point in anchorlist] ) ) + + + return np.array(coords), mooringList, footprintList + + +def create_rotated_layout(bound_xs, bound_ys, spacing_x, spacing_y, grid_rotang, grid_skew_x, grid_skew_y, grid_trans_x, grid_trans_y, fullMPsystem = True, ms = None, rad_anch = None, rotations=None, center=None): + ''' + Create a rectangular grid layout. + + Parameters + ---------- + bound_xs : list + The x coordinates of coordinate pairs defining a layout boundary, relative to a certain point (usually a centroid) + bound_ys : list + The y coordinates of coordinate pairs defining a layout boundary, relative to a certain point (usually a centroid) + spacing_x : float + The x spacing between turbines [m]. + spacing_y : float + The y spacing between turbines [m]. + grid_rotang : float + Rotation of y axis in deg (0 deg is due North, 90 deg is due West) + grid_skew_x : float + Angle of parallelogram between adjacent rows in deg + grid_skew_y : float + Angle of parallelogram between adjacent columns in deg + grid_trans_x : float + x offset to add to all turbine positions + grid_trans_y : float + y offset to add to all turbine positions + fullMPsystem : bool + if True, create/rotation full moorpy systems (slower). if False, use rad_anch as circular buffer zone + ms : MoorPy system + mooring system to rotate, need to input if fullMPsystem = True + rad_anch : float + mooring anchoring radius. need to input if fullMPsystem = False + rotations: list + list of two mooring orientations in deg relative to the rotated y axis (used for every other row). not used if fullMPsystem = False + center: list + the coordinate of the center of the layout. Default: the midpoint of the x and y bounds + Returns + ------- + x_coords : array + array of turbine x coordinates + y_coords : array + array of turbine y coordinates + moorings : list + list of mooring systems + area : shapely polygon + polygon of boundary + ''' + + #boundary of area + area = Polygon([(bound_xs[i], bound_ys[i]) for i in range(len(bound_xs))]) + + # Shear transformation in X + # Calculate trigonometric values + cos_theta = np.cos(np.radians(-grid_rotang)) + sin_theta = np.sin(np.radians(-grid_rotang)) + tan_phi_x = np.tan(np.radians(grid_skew_x)) + tan_phi_y = np.tan(np.radians(grid_skew_y)) + + # Compute combined rotation and skew transformation matrix + transformation_matrix = np.array([[cos_theta - sin_theta*tan_phi_y, cos_theta*tan_phi_x - sin_theta], + [sin_theta + cos_theta*tan_phi_y, sin_theta*tan_phi_x + cos_theta]]) + + # Generate points in the local coordinate system + points = [] + moorings = [] + labels_list = [] # list with grid labels, so that each point now to which horizontal or vertical line it belongs + break_flag = False + # LOCAL COORDINATE SYSTEM WITH (0,0) LEASE AREA CENTROID + # Therefore, +/- self.boundary_centroid_y/x cover the entire area + # Loop through y values within the boundary_centroid_y range with grid_spacing_y increments + iy = 0 + + ywidth = np.max(bound_ys) - np.min(bound_ys) + xwidth = np.max(bound_xs) - np.min(bound_xs) + if center==None: + ycenter = (np.max(bound_ys) + np.min(bound_ys))/2 + xcenter = (np.max(bound_xs) + np.min(bound_xs))/2 + else: + xcenter = center[0] + ycenter = center[1] + + for y in np.arange(np.min(bound_ys) - ywidth*1.0, np.max(bound_ys) + ywidth*1.0, spacing_y): # extending by 1.0*width in x and y to make sure rotations include everything + # Loop through x values within the boundary_centroid_x range with grid_spacing_x increments + ix = 0 + for x in np.arange(np.min(bound_xs) - xwidth*1.0, np.max(bound_xs) + xwidth*1.0, spacing_x): + # Apply transformation matrix to x, y coordinates + local_x, local_y = np.dot(transformation_matrix, [x - xcenter, y - ycenter]) + # Add grid translation offsets to local coordinates + local_x += grid_trans_x + local_y += grid_trans_y + + + # Create a Point object representing the transformed coordinates + # Transform back into global coordinate system with by adding centroid to local coordinates + #point = Point(local_x + np.min(bound_xs), local_y + np.min(bound_ys)) + point = Point(local_x + xcenter, local_y + ycenter) + + if fullMPsystem: + + if ms == None: + raise ValueError('NEED TO INPUT MOORPY SYSTEM') + + # Check if the point lies within the specified shape (boundary_sh_int) + + # deep copy of mooring system to apply translations and rotation + mss = deepcopy(ms) + + # select every other column for rotation and add to farm rotation (mooring rotation is relative to y') + rot = rotations[iy % 2] + grid_rotang + + mss.transform(trans = [point.x, point.y], rot = -rot) #moorpy rotation convention is opposite + mss.initialize() + mss.solveEquilibrium() + + contained = True + for l in mss.lineList: + anchor = Point(l.rA[0], l.rA[1]) + + if not area.contains_properly(anchor): + contained = False + + else: + if rad_anch == None: + raise ValueError('NEED TO INPUT RAD_ANCH') + buff = point.buffer(rad_anch) + contained = True + + if not area.contains_properly(buff): + contained = False + + if contained: + # If the point is within the shape, append it to the list of points + points.append(point) + + if fullMPsystem: + moorings.append(mss) + # Save grid label + labels_list.append([ix,iy-1]) # y -1 so that labels are again starting at 0 + # If the number of points collected reaches the desired threshold (nt), set break_flag to True and exit the loop + # if len(points) >= self.nt: + # break_flag = True + # break + ix += 1 + iy += 1 + # If break_flag is True, exit the outer loop as well + if break_flag: + break + + x_coords = np.array([point.x for point in points])#/1000 + y_coords = np.array([point.y for point in points])#/1000 + + return(x_coords, y_coords, moorings, area) + + + +def getUpdatedAnchorPosition(old_anchor_point, fairlead_point, grid_x, grid_y, grid_depth, ratio=1000): + ''' + Compute a new anchor position for a taut mooring line by looking along the + a line from old anchor to fairlead and seeing where it intersects the seabed. + + Paramaters + ---------- + old_anchor_point : float, array + list of a xyz coordinate of an anchor point + fairlead_point : float, array + list of a xyz coordinate of a fairlead point + grid_x : float, array + The x coordinates of coordinate pairs defining a bathymetry grid + grid_y : float, array + The y coordinates of coordinate pairs defining a bathymetry grid + grid_depth: float, 2D matrix + The depth (z coordinates) of the grid defined by grid_x and grid_y + ratio: int or float (optional) + the value of how far to extend a mooring line until it intersects the bathymetry grid plane + + Returns + ------- + new_anchor_point : float, array + list of a xyz coordinate of the updated anchor point so that it intersects the local bathymetry grid plane + ''' + + # calculate the actual depth based on bathymetry of the x/y coordinates of the anchor + x = old_anchor_point[0] + y = old_anchor_point[1] + #depth, nvec, ix0, iy0 = getDepthFromBathymetry(x, y, grid_x, grid_y, grid_depth) # needed to adjust gDFB function <<<<<< can change later + depth, nvec = getDepthFromBathymetry(x, y, grid_x, grid_y, grid_depth) # needed to adjust gDFB function <<<<<< can change later + + # create points of a line that connect the fairlead to the anchor + p0 = fairlead_point + p1 = old_anchor_point + + ''' + # but adjust the "anchor" point to way below the bathymetry, if it is found that the initial anchor position is above the bathymetry + if p1[2] > -depth: + p1 = np.array([p0[0]+ratio*(p1[0]-p0[0]), p0[1]+ratio*(p1[1]-p0[1]), p0[2]+ratio*(p1[2]-p0[2])]) + ''' + + # Find the intersection point between the mooring Line (assumed straight) + # and the bathymetry grid panel + u = p1 - p0 # vector from fairlead to original anchor + w = np.array([x, y, -depth]) - p0 # vector from fairlead to a point on the grid panel of the original anchor + + fac = np.dot(nvec, w) / np.dot(nvec, u) # fraction along u where it crosses the seabed (can be greater than 1) + + new_anchor_point = p0 + u*fac + + return new_anchor_point + + + +""" +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 (optional) + 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 getDepthFromBathymetry(x, y, bathGrid_Xs, bathGrid_Ys, bathGrid, point_on_plane=False): #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] + bathGrid_Xs, bathGrid_Ys: float, array + The x and y coordinates defining a bathymetry grid + bathGrid: float, 2D matrix + The depth (z coordinates) of the grid defined by bathGrid_Xs and bathGrid_Ys + point_on_plane: bool (optional): + determines whether to return the indices that go with the bathGrid arrays to return a point on the bathymetry grid plane + + Returns + ------- + depth : float + local seabed depth (positive down) [m] + nvec : array of size 3 + local seabed surface normal vector (positive out) + ix0 : int + index of the point on the bathymetry grid plane that goes with bathGrid_Xs + iy0 : int + index of the point on the bathymetry grid plane that goes with bathGrid_Xs + ''' + + # get interpolation indices and fractions for the relevant grid panel + ix0, fx = getInterpNums(bathGrid_Xs, x) + iy0, fy = getInterpNums(bathGrid_Ys, y) + + + # handle end case conditions + if fx == 0: + ix1 = ix0 + else: + ix1 = min(ix0+1, bathGrid.shape[1]) # don't overstep bounds + + if fy == 0: + iy1 = iy0 + else: + iy1 = min(iy0+1, bathGrid.shape[0]) # don't overstep bounds + + + # get corner points of the panel + c00 = bathGrid[iy0, ix0] + c01 = bathGrid[iy1, ix0] + c10 = bathGrid[iy0, ix1] + c11 = 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 = bathGrid_Xs[ix1] - bathGrid_Xs[ix0] + dy = bathGrid_Ys[iy1] - bathGrid_Ys[iy0] + + if dx > 0.0: + dc_dx = (c1y-c0y)/dx + else: + dc_dx = 0.0 # maybe this should raise an error + + if dx > 0.0: + dc_dy = (cx1-cx0)/dy + else: + dc_dy = 0.0 # maybe this should raise an error + + nvec = np.array([dc_dx, dc_dy, 1.0])/np.linalg.norm([dc_dx, dc_dy, 1.0]) # compute unit vector + + if not point_on_plane: + return depth, nvec + else: + return depth, nvec, ix0, iy0 +""" + + + +def adjustMS4Bath(ms, ms_xy, grid_x, grid_y, grid_depth, iLine=-1, nLines_in_ms=3, nLines_in_line=3, display=0, extend=True): + '''Function that updates a MoorPy System object's anchor positions in response to bathymetry and then updates + the line lengths to keep the same pretension that was there before the bathymetry adjustments + + Parameters + ---------- + ms : MoorPy System object + A MoorPy System object defining a mooring system + ms_xy: float, array + The 2D x/y position of the system's coordinate system relative to a reference point (e.g., a centroid) + to reference the proper bathymetry location + grid_x : float, array + The x coordinates of coordinate pairs defining a bathymetry grid + grid_y : float, array + The y coordinates of coordinate pairs defining a bathymetry grid + grid_depth: float, 2D matrix + The depth (z coordinates) of the grid defined by grid_x and grid_y + iLine: int, optional + the index of the line object that is to be adjusted (among the indices of line objects from one anchor to one fairlead, like a subsystem) + nLines: int, optional + the number of mooring lines that surround the MoorPy Body + display: int, optional + an option for print statement outputting + extend: boolean, optional + True for updating anchor positions to bathymetry along the vector of the mooring line, or False for dropping/lifting the anchor at the same x/y position + + Returns + ------- + ms: MoorPy System object + The updated MoorPy System object with new anchor positions and line lengths that match the initial pretensions + ''' + + # NOTE: This function can probably be put in system.py as a method since it adjusts a System object + if np.any([isinstance(line, mp.Subsystem) for line in ms.lineList]): + subsystem_flag = True + else: + subsystem_flag = False + + ### COLLECT INFORMATION ABOUT THE INPUT MOORING SYSTEM (OR SUBSYSTEMS(S)) ### + + T_init_list = [np.linalg.norm(ss.fB_L[0:2]) for ss in ms.lineList] + + # collect point numbers for all anchor points + anchors = [point.number for point in ms.pointList if point.type==1 and point.number not in ms.bodyList[0].attachedP] + # collect point numbers for all anchor points if there are any subsystems in the lineList + #anchors_subsystem = [anchornum for anchornum in anchors for line in ms.lineList for linenum in ms.pointList[anchornum-1].attached if isinstance(line, mp.Subsystem) and linenum==line.number] + # split anchor point numbers up based on whether they are attached to a subsystem or just a line + #anchors_lines = list(set(anchors).difference(anchors_subsystem)) + + # collect point numbers for all "fairleads" + # (fairleads are defined as the points attached to the body where "upper_points" are the points that the lines that are to be adjusted are attached to at the top) + if not subsystem_flag: + iLines = np.arange(iLine, 1e3, nLines_in_line, dtype=int)[:nLines_in_ms] # create a list of the indices of all lines in a mooring system to vary (doesn't always need to be line connected to the fairlead) + upper_points = np.sort([point.number for point in ms.pointList for iL in iLines if all(point.r==ms.lineList[iL].rB)]) # collect the numbers of the points where the lines of interest are attached to at the top + + fairleads = [point.number for point in ms.pointList if point.type==1 and point.number in ms.bodyList[0].attachedP] # collect the numbers of the points that are fairleads + # collect point numbers for all "upper_points" if there are any subsystems in the list + #upper_points_subsystem = [fairleadnum for fairleadnum in upper_points for line in ms.lineList for linenum in ms.pointList[fairleadnum-1].attached if isinstance(line, mp.Subsystem) and linenum==line.number] + # split the upper_points list up based on whether they are attached to a subsystem or not + #upper_points_lines = np.sort(list(set(upper_points).difference(upper_points_subsystem))) + + if not subsystem_flag: + # collect line numbers that are attached to the points of interest + #lower_lines = [line.number for line in ms.lineList if not isinstance(line, mp.Subsystem) for point in ms.pointList if point.number in anchors if all(line.rA==point.r)] + upper_lines = [line.number for line in ms.lineList if not isinstance(line, mp.Subsystem) for point in ms.pointList if point.number in upper_points if all(line.rB==point.r)] + fairleads_lines = [line.number for line in ms.lineList if not isinstance(line, mp.Subsystem) for point in ms.pointList if point.number in fairleads if all(line.rB==point.r)] + # collect the upper tensions of each line attached to the points of interest + upper_lines_TB = [ms.lineList[linenum-1].TB for linenum in upper_lines] + fairleads_lines_TB = [ms.lineList[linenum-1].TB for linenum in fairleads_lines] + + # separate the subsystem objects from the rest to use later, separately from the Line objects + subsystems = [line for line in ms.lineList if isinstance(line, mp.Subsystem)] + + ### CALCULATE AND SET NEW ANCHOR POSITIONS FOR ONLY LINE OBJECTS ### + for i,anchornum in enumerate(anchors): + anchor_point_local = ms.pointList[anchornum-1].r + anchor_point_global = anchor_point_local + np.array([ms_xy[0], ms_xy[1], 0]) + fairlead_point_local = ms.pointList[fairleads[i]-1].r + fairlead_point_global = fairlead_point_local + np.array([ms_xy[0], ms_xy[1], 0]) + + if extend: # if you wish to "extend" or "retract" the anchor point along the vector of the mooring line + new_anchor_point_global = getUpdatedAnchorPosition(anchor_point_global, fairlead_point_global, grid_x, grid_y, grid_depth) + if new_anchor_point_global[2] < anchor_point_global[2]: + if display > 0: print("'Extending' the anchor point to the bathymetry, along the vector of the mooring line") + elif new_anchor_point_global[2] > anchor_point_global[2]: + if display > 0: print("'Retracting' the anchor point to the bathymetry, along the vector of the mooring line") + else: + if display > 0: print("No change in the anchor depth") + else: # if you wish to "drop" or "lift" the anchor point at the same x/y position + new_depth, _ = ms.getDepthFromBathymetry(anchor_point_global[0], anchor_point_global[1]) + new_anchor_point_global = np.array([anchor_point_global[0], anchor_point_global[1], -new_depth]) + if new_anchor_point_global[2] < anchor_point_global[2]: + if display > 0: print("'Dropping' the anchor point to the bathymetry, at the same x/y position") + elif new_anchor_point_global[2] > anchor_point_global[2]: + if display > 0: print("'Lifting' the anchor point to the bathymetry, at the same x/y position") + else: + if display > 0: print("No change in the anchor depth") + + new_anchor_point_local = new_anchor_point_global - np.array([ms_xy[0], ms_xy[1], 0]) + ms.pointList[anchornum-1].setPosition(new_anchor_point_local) + # setPosition sets the point.r value to the input, and also updates the end position of the line object + # setPosition also doesn't allow the input position to be less than ms.depth (which shouldn't matter if the input ms to this function is already at seabedMod=2) + if subsystem_flag: + ms.lineList[i].setEndPosition(new_anchor_point_local, endB=0) + ms.lineList[i].depth = -new_anchor_point_local[2] + ms.lineList[i].pointList[0].setPosition(np.array([ms.lineList[i].pointList[0].r[0], 0, new_anchor_point_local[2]])) + + # resolve for equilibrium + ms.solveEquilibrium() + + if subsystem_flag: + for i,ss in enumerate(subsystems): + L = adjustSS4Pretension(ss, i_line=1, T_init=T_init_list[i], horizontal=True, display=3, tol=0.001) + ss.lineList[1].setL(L) + ms.solveEquilibrium() + + else: + ## update line lengths to match pretension ## + def eval_func(X, args): + '''Tension evaluation function for different line lengths''' + L = X[0] # extract the solver variable + # set args variables + ms_copy = args['ms'] #ms_copy = deepcopy(args['ms']) + iLineX = args['iLineX'] + iLineFair = args['iLineFair'] + # set System variables and solve for new tension + ms_copy.lineList[iLineX].L = L + ms_copy.solveEquilibrium() + T = np.linalg.norm(ms_copy.lineList[iLineFair].fB) + return np.array([T]), dict(status=1), False + + #upper_lines_byL = [ upper_lines[i] for i in np.flip(np.argsort([ms.lineList[ul-1].L for ul in upper_lines])) ] + #fairleads_lines_byL = [ fairleads_lines[i] for i in np.flip(np.argsort([ms.lineList[ul-1].L for ul in upper_lines])) ] + + # loop through the upper lines and run dsolve2 solver to solve for the line length that matches that initial tension + for i,upper_linenum in enumerate(upper_lines): + # set initial variables + T_init = fairleads_lines_TB[i] #T_init = upper_lines_TB[i] + EA = ms.lineList[upper_linenum-1].type['EA'] + L_init = ms.lineList[upper_linenum-1].L + X0 = [ 10 ] #X0 = [ L_init/(T_init/EA+1) ] # setting to start at 10 to start from really taut and extend to longer, always + if display > 0: print(f" Updating Line {upper_linenum} length to match pretension") + + # run dsolve2 to solve for the line length that produces the same initial tension + L_final, T_final, _ = dsolve2(eval_func, X0, Ytarget=[T_init], args=dict(ms=ms, iLineX=upper_linenum-1, iLineFair=fairleads_lines[i]-1), maxIter=200, stepfac=4, display=display, tol=1e-4) + # has the option to solve for an intermediate line length that results in the same tension on the fairlead line (different line) + + # set the new line length into the ms System + ms.lineList[upper_linenum-1].L = L_final[0] + if display > 0: print(f' L0 = {X0[0]:6.1f}, LF = {L_final[0]:6.1f}') + if display > 0: print(f' T0 = {T_init:8.2e}, TF = {T_final[0]:8.2e}') + + + ms.solveEquilibrium() + + return ms + + +def adjustSS4Pretension(ssin, i_line=0, T_init=None, horizontal=False, display=0, tol=0.01): + + ss = deepcopy(ssin) + + if T_init==None: + if horizontal: + T_init = np.linalg.norm(ss.fB_L[0:2]) + else: + T_init = ss.TB # save the initial pretension + + # can update the subsystem initially if need be (Subsystem.staticSolve is the equivalent to System.solveEquilibrium) + #ss.staticSolve() + #T0 = ss.TB + + # update line lengths to match pretension + def eval_func(X, args): + '''Tension evaluation function for different line lengths''' + L = X[0] + ss.lineList[i_line].L = L + ss.staticSolve() + if horizontal: + T = np.linalg.norm(ss.fB_L[0:2]) + else: + T = ss.TB + return np.array([T]), dict(status=1), False + + # run dsolve2 solver to solve for the upper line length that matches the initial tension + L_init = ss.lineList[i_line].L + if display > 0: print(f" Updating Subsystem {ss.number}'s Line {ss.lineList[i_line].number} length to match pretension") + + #X0 = [L_init] + X0 = [10] + + # run dsolve2 to solve for the line length that sets the same pretension + L_final, T_final, _ = dsolve2(eval_func, X0, Ytarget=[T_init], + Xmin=[1], Xmax=[1.1*np.linalg.norm(ss.rB-ss.rA)], + dX_last=[1], tol=[tol], + maxIter=200, stepfac=100, display=display) + + ss.lineList[i_line].setL(L_final[0]) # assign the solved_for length + if display > 0: print(f' L_init = {L_init:6.1f}, LF = {L_final[0]:6.1f}') + if display > 0: print(f' T_init = {T_init:8.2e}, TF = {T_final[0]:8.2e}') + + ss.staticSolve() # reset the subsystem + + return L_final[0] + + + + + +def adjustMooring(mooring, layout, r_fair, r_anch, adjust={}): + '''Adjust a Mooring object for change in layout considering the seabed, + which is contained in Project object that is also passed in. + The Mooring adjustment should work regardless of whether the mooring + is only 2D or also includes a 3D representation via MoorPy Subsystem. + + When a subsystem is involved, a dictionary can be past via 'adjust' + to ask for the pretension to be adjusted to a desired value. + + Parameters + ---------- + mooring : Mooring object + The Mooring to be adjusted. + layout : Layout object + An object of the Layout class that contains seabed information. + r_fair : float, array + Absolute xyz coordinate of a fairlead point [m]. + r_anch : float, array + Absolute xyz coordinate of the anchor point (guess to be adjusted) [m]. + adjust : dict + Dictionary specifying a method of adjusting the mooring to maintain a + desired characteristic. Currently only pretension is supported: + {'pretension' : {'target' : XXX N, 'i': line index to adjust}}. + ''' + + # Update the anchor position if it isn't already on the seabed + if not np.isclose(r_anch[2], layout.getDepthAtLocation(*r_anch[:2])): + r_anch = layout.getUpdatedAnchorPosition(r_fair, r_anch) + + # Set the mooring end positions (this will update any Subsystem too) + mooring.setEndPosition(r_anch, 'a') + mooring.setEndPosition(r_fair, 'b') + + # If requested, update the line lengths to maintain pretension + if mooring.subsystem and 'pretension' in adjust: + target = adjust['pretension']['target'] + i_line = int(adjust['pretension']['i']) + + ss = mooring.subsystem # shorthand + + # update line lengths to match pretension + def eval_func(X, args): + '''Tension evaluation function for different line lengths''' + ss.lineList[i_line].L = X[0] # set specified line section's length + ss.staticSolve(tol=0.0001) # solve the equilibrium of the subsystem + return np.array([ss.TB]), dict(status=1), False # return the end tension + + # run dsolve2 solver to solve for the upper line length that matches the initial tension + X0 = [ss.lineList[i_line].L] # initial value is current section length + + L_final, T_final, _ = dsolve2(eval_func, X0, Ytarget=[target], + Xmin=[1], Xmax=[1.1*np.linalg.norm(ss.rB-ss.rA)], + dX_last=[1], tol=[0.1], + maxIter=200, stepfac=4, display=2) + + ss.lineList[i_line].L = L_final[0] + + print(f"Adjusted mooring to pretension of {T_final[0]:.0f} N and {L_final[0]:.2f}-m section length.") + + +def makeMooringListN(subsystem0, N): + '''Simple function for making a mooringList of N mooring objects, by + by duplication one provided subsystem. They can be positioned later. + ''' + + #mooringList = [] + # Initialize empty list + #mooringList = [None] * N + mooringList = {} + + for i in range(N): + + # Make a copy from the original + ss = deepcopy(subsystem0) + + #mooringList.append(Mooring(subsystem=ss,id=i)) + mooringList[i] = Mooring(subsystem=ss,id=i) + #mooringList[i].rA = ss.rA + #mooringList[i].rB = ss.rB + # Make a new mooring object to hold the copied subsystem + #mooringList.append(Mooring(subsystem=ss, rA=ss.rA, rB=ss.rB)) + + return mooringList + + +def getLower(A): + '''Return a vector of the serialized lower-triangular elements of matrix A''' + return A[np.tril_indices_from(A , k=-1)] + +if __name__ == '__main__': + + + # initialize the area bounds + lease_xs = np.array([ 2220.61790941, 2220.61787966, 3420.61793096, 3420.61791961, + 3420.61801382, 3420.61803977, 3420.61807596, 3420.61811471, + 3420.61822821, 4620.61806679, 4620.61816982, 4620.6182304 , + 4620.61832506, 4620.61827356, 4620.61840937, 4620.61846802, + 5820.61842928, 5820.61837593, 5820.61846352, 5820.61851395, + 5820.61860812, 7020.61852522, 7020.61862679, 7020.61856713, + 5820.61872489, 5820.61881681, 5820.61883604, 4620.61889509, + 4620.61904038, 4620.61903256, 3420.619089 , 3420.61917709, + 3420.61920431, 2220.61926344, 2220.61944036, 2220.61939526, + 1020.61946852, -179.38041543, -179.38053362, -179.38049454, + -179.38063026, -179.38059316, -179.38071736, -179.38082411, + -179.38081907, -179.38086534, -179.38087289, -179.38099606, + -179.38098055, -1379.38097106, -1379.38100559, -1379.38111504, + -2579.38099329, -2579.38099701, -2579.38112028, -2579.38119101, + -3779.38113702, -3779.38108251, -3779.38118818, -4979.38109205, + -4979.38117059, -4979.38120063, -6179.38111446, -6179.38120826, + -7379.38107666, -7379.38119559, -8579.38111169, -8579.38121027, + -7379.38119975, -7379.38133324, -7379.38133537, -7379.38136608, + -7379.3815011 , -7379.38148217, -6179.38158388, -6179.38169844, + -4979.38166381, -4979.38179166, -3779.38188846, -2579.38201223, + -1379.38197896, -1379.38206961, -179.38223745, -179.38215968, + 1020.61770826, 2220.61761805, 2220.61770748, 2220.61772366, + 2220.6178225 , 2220.61783243, 2220.61790941]) + + lease_ys = np.array([ 10997.93747912, 9797.9374786 , 9797.93762135, 8597.93777362, + 7397.93779263, 6197.93779256, 4997.9378827 , 3797.93806299, + 2597.93810968, 2597.93821722, 1397.93821124, 197.93829277, + -1002.06153985, -2202.06150557, -3402.0613888 , -4602.0614069 , + -4602.06132321, -5802.06115355, -7002.06101498, -8202.06101547, + -9402.06093466, -9402.06087921, -10602.06081225, -11802.06066334, + -11802.06074832, -13002.06064456, -14202.06056929, -14202.06071115, + -15402.06052442, -16602.06047346, -16602.0605191 , -17802.06042198, + -19002.060349 , -19002.06047655, -20202.06042422, -21402.06027959, + -21402.06033391, -21402.06033968, -20202.06050384, -19002.06057741, + -17802.06066473, -16602.06066129, -15402.06078244, -14202.06092037, + -13002.06096561, -11802.06102581, -10602.06121353, -9402.06119237, + -8202.0613002 , -8202.06134893, -7002.06145163, -5802.06156475, + -5802.06157941, -4602.06171868, -3402.06175304, -2202.06190779, + -2202.0618502 , -1002.06194537, 197.93795779, 197.93795451, + 1397.93778023, 2597.93771457, 2597.93761946, 3797.93767253, + 3797.93747917, 4997.93739719, 4997.93747426, 6197.9373365 , + 6197.93731811, 7397.93724719, 8597.93717933, 9797.93711807, + 10997.93706522, 12197.93690464, 12197.93699804, 13397.93696353, + 13397.93699185, 14597.93692719, 14597.93693641, 14597.93707605, + 14597.93701544, 15797.93706205, 15797.93707378, 16997.93692194, + 16997.93704183, 16997.93701933, 15797.93709866, 14597.93727451, + 13397.93732149, 12197.93746482, 10997.93747912]) + + # initialize dummy mooring system to use to organize turbines within a layout + ms_type = 1 + + if ms_type==1: + ms = mp.System(file='sample_deep.txt') + + elif ms_type==2: + 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 + + 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(1, 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 = mp.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 + + #ms.addLine(length, type, pointA, pointB) + + # 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 + + #ms.bodyList[0].type = 1 + + + + ms.initialize() # make sure everything's connected + ms.solveEquilibrium() # equilibrate + + + + + + + # create a subsystem + ss = mp.Subsystem(depth=2000, spacing=2000, rBfair=[10,0,-20]) + + # set up the line types + ss.setLineType(180, 'polyester', name='one') + + # set up the lines and points and stuff + lengths = [2000] + types = ['one'] + ss.makeGeneric(lengths, types) + + # plotting examples + ss.setEndPosition([-2000 , 0,-2000], endB=0) + ss.setEndPosition([-10, 0, -20], endB=1) + ss.staticSolve() + + #ss.pointList[0].setPosition(np.array([-2000, 0, -2000])) + #ss.pointList[-1].setPosition(np.array([-10, 0, -20])) + #ss.solveEquilibrium() + + + + + + + # initialize dummy bathymetry variables to check anchor depths + grid_x = np.array([-65000, 65000]) + grid_y = np.array([-65000, 65000]) + grid_depth = np.array([[2367, -211], + [3111, -338]]) + + start_time = time.time() + + coords, mooringList, footprintList = create_layout(lease_xs, lease_ys, ss, grid_x, grid_y, grid_depth, + spacing_x=8400, spacing_y=8600) + + end_time = time.time() - start_time + print(end_time, end_time/60) + # create a layout of turbine positions + #xs, ys, footprintList, msList = create_initial_layout(lease_xs, lease_ys, ms, grid_x, grid_y, grid_depth, display=1) + + # plot the result + fig, ax = plt.subplots(1,1) + ax.plot(coords[:,0], coords[:,1], color='k', marker='o', linestyle='') + ax.plot(lease_xs, lease_ys, color='r') + for polygon in footprintList: + x, y = polygon.exterior.coords.xy + ax.plot(x, y, color='b', alpha=0.5) + ax.set_aspect('equal') + + + # try making a Project with the above + + project = Project() + + project.loadBathymetry('bathymetry_sample.txt') + + project.mooringList = mooringList + + project.plot3d() + + plt.show() + + + a = 2 + + + + + # Next Steps: + # - be able to adjust the starting point to see if there are any other arrangements that can fit more turbines + # - be able to adjust the "bounds" on each turbine (i.e., change from a circle around each turbine to maybe a triangle) + # - be able to account for bathymetry for each anchor point (will likely need FAModel integration, as this is already set up to do this) + + + +def convertm2km(coords): + above_1000 = any(coord > 1000 for coord in coords) + if above_1000: + coords = [coord / 1000 for coord in coords] + return coords + + + + + + + + + + + diff --git a/famodel/famodel_base.py b/famodel/famodel_base.py index 808bbfae..c21d014c 100644 --- a/famodel/famodel_base.py +++ b/famodel/famodel_base.py @@ -1,5 +1,5 @@ import numpy as np - +from famodel.helpers import calc_midpoint ''' famodel_base contains base classes that can be used for various classes in the floating array model that relate to entities that act like either @@ -22,10 +22,53 @@ Linear assemblies of edges and nodes can be grouped into a higher-level edge object. This allows mooring line sections and connectors to be part of an overall mooring line object, which is itself an edge. These -relationships are tracked with sub_edge and sub_node lists in the +relationships are tracked with subcomponents lists in the higher-level Edge object, and part_of entries in the lower-level node or edge objects. +NEW: +Edge subcomponents can now be in any arrangement +The subcomponent(s) at the end of an edge can attach to the node +the edge is attached to, or a sub-node of the node. +If the super-edge end is attached to a node, +the sub-objects could be automatically attached to the node, or +left to be manually attached <<<<< which one?? +If a super-edge is disconnected, the sub-objects should be disconnected, right? + +Is there some overriding logic of the above?? + +How do we identify/track the sub-objects? +How do we identify/track the attacheble end sub-objets? +Dicts??? + + +The current approach is not top-down. If you connect higher-level objects, +the lower level objects aren't automatically connected. Instead, if you connect +lower objects (including specifying particular positions), the higher-level +connections are also made. + + +Nodes can attach to nodes as either subortinately or equally... + + +when attached equally, they reference each other (mirror) in self.attachments +but not in self.attached_to. The latter is used only for subordinate +connections (i.e. to a higher node, and only to one). +Mutual/equal node attachments are done with the join method, and undone with +the separate method. + + +When a node or edge of a higher edge is attached to a node of a higher node, +the following rules apply: +The higher level objects can be attached regardless of sub-object attachment. +If any sub-objects are attached, the higher level objects must be attached. +- Detaching the higher level objects must detach the sub objects in the process. +- When all the sub objects are detached, it would be convenient to detach the higher objects. + +Can an end of a higher level edge attach to multiple nodes? +(using a list, corresponding to multiple sub objects at the end) + + ''' class Node(): @@ -47,16 +90,16 @@ def __init__(self, id): self.id = id # id number or string, used as the key when attached to things - self.attachments = {} # dictionary listing attached edges + self.attachments = {} # dictionary listing attached edges or nodes # (key is the id, value is attachment object ref and other info) - self.attached_to = None # whether this object is bound to another object + self.attached_to = None # whether this object is subordinately bound to another object self.part_of = None # whether this object is part of an Edge group # position/orientation variables self.r = np.zeros(2) # position [m] - self.theta = 0 # heading [rad] + self.theta = 0 # heading [rad] CCW+ self.R = np.eye(2) # rotation matrix # installation dictionary [checks for installation status] @@ -65,40 +108,123 @@ def __init__(self, id): def isAttached(self, object, end=None): - '''Check if something is attached to this node, even if it's part of - a higher-level edge. + '''Check if something is attached to this node. This works for both + attached edges (the end can be specified), or nodes that are joined + with this node. Parameters ---------- object The Node or Edge object being attached to this one. end - If an Edge is being attached, which end of it, 'a' or 'b'. + If an Edge is being considered, which end of it, 'a' or 'b' (optional). ''' - # find top-level edge end if applicable - if isinstance(object, Node): - object2, i_end = object.getTopLevelEdge() - elif isinstance(object, Edge): - i_end = endToIndex(end, estr='when checking if an edge is attached to a node.') - object2, _ = object.getTopLevelEdge(i_end) - else: - raise Exception('Provided object is not an Edge or Node.') + if object.id in self.attachments: # See if it's attached - # See if it's attached (might be a higher-level edge) - # if object2.id in self.attachments: - if object2.id in self.attachments: - - if isinstance(object2, Node): # if it's a node, it's simple + if isinstance(object, Node): # if it's a node, it's simple return True - elif isinstance(object2, Edge): # if it's an edge, end matters - if endToIndex(self.attachments[object2.id]['end']) == i_end: - return True # the end in question is attached - else: - return False + elif isinstance(object, Edge): + if end == None: # if end not specified, it's simple + return True + else: # otherwise check which end + if endToIndex(self.attachments[object.id]['end']) == endToIndex(end): + return True # the end in question is attached + else: + return False + else: + raise Exception('Provided object is not an Edge or Node.') else: return False + + + def join(self, object): + '''Join another node ot this node, in a mutual way. + This could be multiple connectors within a higher level edge, + or one connector in a higher level edge (potentially connecting + to a connector in a higher level node). + ''' + + if not isinstance(object, Node): + raise Exception('Provided object is not a Node.') + + + # Make sure they're not already attached + if object.id in self.attachments: + raise Exception(f"Object {object.id} is already attached to {self.id}") + if self.id in object.attachments: + raise Exception(f"{self.id} is already attached to {object.id}") + + + # make sure there isn't some incompatibility in joining these nodes? + if isinstance(self.part_of, Edge) and isinstance(object.part_of, Edge): + if not self.part_of == object.part_of: + raise Exception("Cannot join two nodes that are each part of a different edge") + + # do the mutual joining + self.attachments[object.id] = dict(obj=object, id=object.id, + r_rel=np.array([0,0]), type='node') + + object.attachments[self.id] = dict(obj=self, id=self.id, + r_rel=np.array([0,0]), type='node') + + # Register the attachment in higher level objects if applicable + if isinstance(self.part_of, Edge) and isinstance(object.part_of, Edge): + raise Exception("This attachment would directly connect two higher-level edges to each other, which is not allowed.") + + elif isinstance(self.part_of, Edge) and isinstance(object.attached_to, Node): + end = self.part_of.findEnd(self) + object.attached_to.attach(self.part_of, end=end, + r_rel=object.attached_to.attachments[object.id]['r_rel']) + #self.part_of._attach_to(object.part_of, end=end) + + elif isinstance(self.attached_to, Node) and isinstance(object.part_of, Edge): + end = object.part_of.findEnd(object) + self.attached_to.attach(object.part_of, end=end, + r_rel=self.attached_to.attachments[self.id]['r_rel']) + + elif isinstance(self.attached_to, Node) and isinstance(object.attached_to, Node): + raise Exception("This would attach two higher-level nodes, which is not supported.") + + + def isJoined(self): + '''Check if this node is joined to anything else.''' + + for att in self.attachments.values(): # Look through everything attached + object = att['obj'] + if isinstance(object, Node): # Only another node could be joined + if self.id in object.attachments: + return True + + # If we've gotten this far, it's not joined with anything + return False + + + def separate(self, object): + '''Opposite of join''' + + if not isinstance(object, Node): + raise Exception('Provided object is not a Node.') + + # Make sure they're already attached + if not object.id in self.attachments: + raise Exception(f"Object {object.id} is not attached to {self.id}") + if not self.id in object.attachments: + raise Exception(f"{self.id} is not attached to {object.id}") + + # do the mutual separating + del self.attachments[object.id] + del object.attachments[self.id] + + # Register the separation in higher level objects if applicable + if isinstance(self.part_of, Edge) and isinstance(object.attached_to, Node): + end = self.part_of.findEnd(self) + object.attached_to.dettach(self.part_of, end=end) + + elif isinstance(self.attached_to, Node) and isinstance(object.part_of, Edge): + end = object.part_of.findEnd(object) + self.attached_to.detach(object.part_of, end=end) def attach(self, object, r_rel=[0,0], end=None): @@ -115,6 +241,11 @@ def attach(self, object, r_rel=[0,0], end=None): end If an Edge is being attached, which end of it, 'a' or 'b'. ''' + + #object_parent + #self_parent + + ''' # find top-level edge end if applicable if isinstance(object, Node): object2, i_end = object.getTopLevelEdge() @@ -123,45 +254,107 @@ def attach(self, object, r_rel=[0,0], end=None): object2, _ = object.getTopLevelEdge(i_end) else: raise Exception('Provided object is not an Edge or Node.') - - + ''' # Make sure it's not already attached (note this doesn't distinguish end A/B) - if object2.id in self.attachments: - raise Exception(f"Object {object.id} is already attached to {self.id}") - - - # Attach the object (might be a higher-level edge) - if isinstance(object2, Node): - self.attachments[object2.id] = dict(obj=object2, id=object2.id, + if object.id in self.attachments: + # for bridles, the mooring will already be attached to platform + # for second bridle section + # need to calculate new r_rel that is average of end points + if isinstance(object, Edge): + # pull out relative dist of each point on end to self + r_rel = self.calculate_r_rel(object,end=end) + self.attachments[object.id]['r_rel'] = r_rel + # update end position + Node.setPosition(self, r=self.r,theta=self.theta) + # don't need to attach, already attached- just return + return + else: + raise Exception(f"Object {object.id} is already attached to {self.id}") + + + # Attach the object + if isinstance(object, Node): # (object is a node) + + if object.attached_to: # object is already attached to something + raise Exception("The object being attached is already attached to a higher node - it needs to be detached first.") + + self.attachments[object.id] = dict(obj=object, id=object.id, r_rel=np.array(r_rel), type='node') - #object2._attach_to(self) # tell it it's attached to this Node - object2.attachments[self.id] = dict(obj=self,id=self.id,r_rel=np.array(r_rel),type='node') + object._attach_to(self) # tell it it's attached to this Node - elif isinstance(object2, Edge): - self.attachments[object2.id] = dict(obj=object2, id=object2.id, - r_rel=np.array(r_rel), type='edge', - end=['a', 'b'][i_end]) - object2._attach_to(self, i_end) # tell it it's attached to this Node - ''' - if end in ['a', 'A', 0]: - new_entry['end'] = 'a' - object._attach_to(self, 0) + elif isinstance(object, Edge): # (object is an edge) + i_end = endToIndex(end, estr='when attaching an edge to a node.') - elif end in ['b', 'B', 1]: - new_entry['end'] = 'b' - object._attach_to(self, 1) + if object.attached_to[i_end]: # object is already attached to something + raise Exception("The object being attached is already attached to a higher node - it needs to be detached first.") - else: - raise Exception('End A or B must be specified when attaching an edge.') - ''' + self.attachments[object.id] = dict(obj=object, id=object.id, + r_rel=np.array(r_rel), type='edge', + end=i_end) + + object._attach_to(self, i_end) # tell it it's attached to this Node + else: raise Exception('Unrecognized object type') + + # See about attaching higher-level objects (new) (note: r_rel will be neglected at higher level) + + if isinstance(object.part_of, Edge): # attached object is part of an edge + + # figure out which end of the edge object corresponds to + if object in object.part_of.subcons_A and object in object.part_of.subcons_B: + # there is only one subcomponent, keep end that was passed in + i_end = end + elif object in object.part_of.subcons_A: + end = 0 + elif object in object.part_of.subcons_B: + end = 1 + else: + end = -1 # object isn't at the end of the higher level edge so do nothing + if not self.part_of == object.part_of: + raise Exception("Cannot attach two non-end subcomponents of different edges.") + + if self.part_of == None and end > -1: # this node isn't part of an edge + if self.attached_to: # if self is part of a higher node + # attach higher edge to higher node + self.attached_to.attach(object.part_of, end=end) + else: + # attach higher edge to this node + self.attach(object.part_of, r_rel=r_rel, end=end) + + else: # if self is part of a higher level edge + raise Exception("This attachment would directly connect two higher-level edges to each other, which is not allowed.") + ''' + elif isinstance(object.attached_to, Node): # attached object is attached to a higher node + + raise Exception("The object being attached is part of a higher node - this operation is not supported.") + + if self.part_of: # self node is part of a higher level edge + + # figure out which end of the edge object corresponds to + if self in self.part_of.subcons_A: + end = 0 + elif self in self.part_of.subcons_B: + end = 1 + + # attach higher node and edge + object.attached_to.attach(self.part_of, end=end) + + # attach higher edge to this node + self.attach(object.part_of, r_rel=r_rel, end=end) + + elif if self.attached_to: # if self is part of a higher node + Exception("This attachment would directly connect two higher-level nodes to each other, which is not allowed.") + + else: # self has nothing higher, so attach the object's higher level thing to self as well + self.attach(object.attached_to, r_rel=r_rel, end=end) XXXX + ''' + def detach(self, object, end=None): '''Detach the specified object from this node. - Note that this method doesn't search for highest-level edge - attachment because it should already be attached that way. + Will also detach any attached sub-objects Parameters ---------- @@ -176,54 +369,45 @@ def detach(self, object, end=None): raise Exception(f"Object {object.id} is not attached to {self.id}") # this exception could be optionally disabled - # Remove it from the attachment registry - del self.attachments[object.id] - # Handle attachment type and the end if applicable, and record # the detachment in the subordinate object. if isinstance(object, Node): - object._detach_from() # tell the attached object to record things - + object._detach_from() # tell the attached object to record that it's detached + elif isinstance(object, Edge): - - i_end = endToIndex(end, estr='when detaching an edge from a node.') - + if end: + i_end = endToIndex(end, estr='when detaching an edge from a node.') + else: # otherwise figure out which end is attached + i_end = self.attachments[object.id]['end'] + + # Detach this edge from self + # This will also detach end subcomponents of the edge from anything. object._detach_from(i_end) - ''' - if end in ['a', 'A', 0]: - object._detach_from(0) - - elif end in ['b', 'B', 1]: - object._detach_from(1) - - else: - raise Exception('End A or B must be specified when detaching an edge.') - ''' + else: raise Exception('Provided object is not an Edge or Node.') - - + + # Remove it from the attachment registry + del self.attachments[object.id] + - def _attach_to(self, object,sub=0): + def _attach_to(self, object): '''Internal method to update the Node's attached_to registry when - requested by a node. + requested by a node. With an error check. Parameters ---------- object The Node object to attach to. - sub - Boolean to mark if this node is subordinately attached to the object ''' - if not sub: - if isinstance(object, Node): - raise Exception('Node objects can only be attached subordinately to Node objects.') + # Make sure it's not already attached to something else if self.attached_to: # True if populated, False if empty dict # self.detach() # commented out because I don't think this would work for shared anchors # could optionally have a warning or error here print(f'Warning: node {self.id} is attached to 2 objects') - + breakpoint() + print("fix up this scenario in the code somewhere...") # Add it to the attached_to registry self.attached_to = object @@ -236,10 +420,10 @@ def _detach_from(self): self.attached_to = None - - def getTopLevelEdge(self): - '''If this node is part of a higher-level edge group, and the request - corresponds to an end of that higher-level group, return the higher edge, + """ + def getTopLevelObject(self): + '''If this node is part of a higher-level object, and the request + corresponds to an end of that higher-level group, return the higher object, otherwise return this same object. Can be recursive. A similar method exists for edges.''' @@ -247,13 +431,13 @@ def getTopLevelEdge(self): supe = self.part_of # shorthand for the super edge if supe.subcomponents[0] == self: # if this node is at end A of supe return supe.getTopLevelEdge(0) # return supe, and which end - elif supe.subcomponents[-1] == self: # if this node is at end B of supe + >> elif supe.subcomponents[-1] == self: # if this node is at end B of supe return supe.getTopLevelEdge(1) else: return self, -1 # if not part of something bigger, just return self + """ - - def setPosition(self, r, theta=0): + def setPosition(self, r, theta=0, force=False): '''Set the position of the node, as well as any attached objects. Parameters @@ -262,27 +446,117 @@ def setPosition(self, r, theta=0): x and y coordinates to position the node at [m]. theta, float (optional) The heading of the object [rad]. + force : bool (optional) + When false (default) it will not allow movement of subordinate objects. ''' + # Don't allow this if this is part of another object + if self.attached_to and not force: + raise Exception("Can't setPosition of an object that's attached to a higher object unless force=True.") + # Store updated position and orientation - self.r = np.array(r) + if len(r) >= len(self.r): # default r is 2D, but can be adjusted to 3D + self.r = np.array(r) + else: # if just a portion of r is being adjusted, only change up to length of initial r + self.r[:len(r)] = r + self.theta = theta # Get rotation matrix... - self.R = np.array([[np.cos(theta), -np.sin(theta)],[np.sin(theta), np.cos(theta)]]) + if len(self.r) == 2: + self.R = np.array([[np.cos(theta), -np.sin(theta)],[np.sin(theta), np.cos(theta)]]) + elif len(self.r) == 3: + if np.isscalar(theta): + angles = np.array([ 0, 0, theta]) + elif len(theta)==1: + angles = np.array([0, 0, theta[0]]) + elif len(theta)==3: + angles = np.array(theta) + else: + raise Exception("theta needs to be length 1 or 3.") + self.R = rotationMatrix(*angles) + else: + raise Exception("Length of r must be 2 or 3.") # Update the position of any attach objects for att in self.attachments.values(): + if len(self.r) == len(att['r_rel']): + pass # all good + elif len(self.r) == 3 and len(att['r_rel']) == 2: # pad r_rel with z=0 if needed + att['r_rel'] = np.hstack([att['r_rel'], [0]]) + else: + raise Exception("r and attachment r_rel values have mismatched dimensions.") + # Compute the attachment's position r_att = self.r + np.matmul(self.R, att['r_rel']) - + # set position of any attached node that isn't subordinate to another node + # (prevents infinite loop of setPositioning for nodes) if isinstance(att['obj'], Node): - att['obj'].setPosition(r_att) + if not isinstance(att['obj'].attached_to, Node) or att['obj'].attached_to == self: + att['obj'].setPosition(r_att, theta=theta, force=True) elif isinstance(att['obj'], Edge): att['obj'].setEndPosition(r_att, att['end']) + + def calculate_r_rel(self,object, end=None): + '''Calculate the relative distance between node and object + based on the combined relative distances of the subordinate/ + subcomponent nodes connecting them''' + if isinstance(object,Edge): + # pull out subcomponent(s) attached to self at the correct end + end = endToIndex(end) # find end + subs = object.subcons_A if end==0 else object.subcons_B + + # go through all end subcomponents of edge at the correct end + rel_locs = [] # relative location list (in case multiple points at end) + for sub in subs: + # first check if subordinate/subcomponent joined + att = [att for att in sub.attachments.values() if att['obj'].attached_to==self] + if len(att)>0: + # find attachment of sub that is subordinately connected to self (Node) + att = att[0] # just need 1st entry + r_rel_att_self = self.attachments[att['id']]['r_rel'] + r_rel_att_sub = att['obj'].attachments[sub.id]['r_rel'] + # r_rel of sub to self is r_rel of attachment to self + r_rel of sub to attachment + if len(r_rel_att_self) < 3: # pad as needed + r_rel_att_self = np.hstack([r_rel_att_self,[0]]) + if len(r_rel_att_sub) < 3: # pad as needed + r_rel_att_sub = np.hstack([r_rel_att_sub,[0]]) + rel_locs.append(r_rel_att_self + r_rel_att_sub) + # otherwise, check if directly connected + elif self.isAttached(object): + # if no subordinate/subcomponent connection, should be + # only 1 attachment point at this end + return(self.attachments[object.id]['r_rel']) + else: + raise Exception(f'Cannot determine how {self.id} and {object.id} are connected') + return calc_midpoint(rel_locs) + elif isinstance(object, Node): + # node to node - check if 2 subordinates connected + att = [att for att in object.attachments.values() if self.isAttached(att['obj'])] + if len(att)>0: + att = att[0] # just need 1st entry + # get relative distance of subordinately attached nodes + r_rel_att_self = self.attachments[att['id']]['r_rel'] + r_rel_att_obj = object.attachments[att['id']]['r_rel'] + # r_rel of obj to self is r_rel of attachment to self + r_rel of obj to attachment + if len(r_rel_att_self) < 3: # pad as needed + r_rel_att_self = np.hstack(r_rel_att_self,[0]) + if len(r_rel_att_obj) < 3: # pad as needed + r_rel_att_sub = np.hstack(r_rel_att_sub,[0]) + return(r_rel_att_self + r_rel_att_sub) + # otherwise see if they are directly attached and return r_rel + elif self.isattached(object): + return self.attachments[object.id]['r_rel'] + else: + raise Exception(f'Cannot determine how {self.id} and {object.id} are connected') + else: + raise Exception(f'{object} is not a Node or Edge') + + + @@ -305,19 +579,22 @@ def __init__(self, id): self.id = id # id number or string, used as the key when attached to things - self.attached_to = [None, None] # whether either end [A, B] of this object is bound to another object + self.attached_to = [None, None] # object end [A, B] of this edge is attached to # End A and B locations - self.rA = [0,0] - self.rB = [0,0] + self.rA = np.zeros(2) + self.rB = np.zeros(2) # Some attributes related to super-edges used to group things self.part_of = None # whether this object is part of an Edge group self.subcomponents = [] # chain of edges and nodes that make up this edge # (e.g. sections of a mooring line, and connetors between them) - - whole = True # false if there are sub edges/nodes that aren't connected + + self.subcons_A = [] # subcomponent for end A (can be multiple) + self.subcons_B = [] # subcomponent for end B (can be multiple) + + whole = True # false if there is a disconnect among the sub edges/nodes # installation dictionary [checks for installation status] self.inst = {'mobilized': False, @@ -343,27 +620,7 @@ def isAttachedTo(self, query, end=None): return query.isAttached(self, end=end) else: raise Exception('Edges can only be attached to Nodes.') - - ''' - # old version where query can be a reference to the object itself, or the object ID. - # If query is a reference to an object, get its ID - if isinstance(query, Node) or isinstance(query, Edge): - id = query.id - else: - id = query - - if end == None: - answer = bool(id == self.attached_to[0]['id'] or id == self.attached_to[1]['id']) - else: - if end in ['a', 'A', 0]: - answer = bool(id == self.attached_to[0]['id']) - elif end in ['b', 'B', 1]: - answer = bool(id == self.attached_to[1]['id']) - else: - raise Exception("If an 'end' parameter is provided, it must be one of a,b,A,B,0,1,False,True.") - ''' - # return answer - + def attachTo(self, object, r_rel=[0,0], end=None): '''Attach an end of this edge to some node. @@ -392,35 +649,11 @@ def attachTo(self, object, r_rel=[0,0], end=None): # Tell the Node in question to initiate the attachment # (this will also call self._attach_to) object.attach(self, r_rel=r_rel, end=i_end) - - ''' - # Add it to the attachment registry - new_entry = dict(ref=object, r_rel=np.array(r_rel)) - - # Handle attachment type and the end if applicable - if type(object) == Node: - new_entry['type'] = 'Node' - - elif type(object) == Edge: - new_entry['type'] = 'Edge' - if not end: - raise Exception('End A or B must be specified when attaching an edge.') - if end.lower() in ['a', 'b']: - new_entry['end'] = end.lower() - else: - raise Exception('End A or B must be specified when attaching an edge.') - else: - raise Exception('Provided object is not an Edge or Node.') - - # Since things have worked, add it to the list - self.attachments[object['id']] = new_entry - ''' def _attach_to(self, object, end): '''Internal method to update the edge's attached_to registry when - requested by a node. In nested edges, expects to be called for the - highest-level one first, then it will recursively call any lower ones. + requested by a node. This doesn't do higher level attachments. Parameters ---------- @@ -434,33 +667,373 @@ def _attach_to(self, object, end): if not isinstance(object, Node): raise Exception('Edge objects can only be attached to Node objects.') + # Could potentially check if it's already attached to something <<< + # Add it to the attached_to registry self.attached_to[i_end] = object + ''' # Recursively attach any subcomponent at the end - if len(self.subcomponents) > 0: - subcon = self.subcomponents[-i_end] # index 0 for A, -1 for B + >>> do we really want to do this still? <<< + if len(self.subcomponents) > 0: # this edge has subcomponents + for i_sub_end in self.end_inds[i_end]: # go through each subcomponent associated with this end + subcon = self.subcomponents[i_sub_end] + if isinstance(subcon, Node): # if the end subcomponent is a node + subcon._attach_to(object) + + elif isinstance(subcon, Edge): # if it's an edge + subcon._attach_to(object, i_end) # i_end will tell it whether to use end A or B + ''' + + + + def detachFrom(self, end): + '''Detach the specified end of the edge from whatever it's attached to. + + Parameters + ---------- + end + Which end of the line is being dettached, 'a' or 'b'. + ''' + # Determine which end to detach + i_end = endToIndex(end, estr='when detaching an edge.') + + # Tell the Node the end is attached to to initiate detachment + # (this will then also call self._detach_from) + self.attached_to[i_end].detach(self, end=i_end) + + + def _detach_from(self, end): + '''Internal method to update the edge's attached_to registry when + requested by a node. In nested edges, it will recursively call any + lower objects. + + Parameters + ---------- + end + Which end of the line is being detached, a-false, b-true. + ''' + i_end = endToIndex(end) + # Delete the attachment(s) of the edge end (there should only be one) + self.attached_to[i_end] = None + + # Go deeper >..... >>> + if i_end == 0: + end_subcons = self.subcons_A + elif i_end == 1: + end_subcons = self.subcons_B + + for subcon in end_subcons: if isinstance(subcon, Node): - # will be attaching a node to a node, tell _attach_to it is attaching the subcon as a subordinate node we don't throw an error - #print(f'attaching {object.id} to {subcon.id}') - subcon._attach_to(object,sub=1) - # object._attach_to(subcon,sub=1) + if subcon.attached_to: + subcon.attached_to.detach(subcon) + # the above will then call subcon._detach_from() elif isinstance(subcon, Edge): - #print(f'attaching {object.id} to {subcon.id}') - subcon._attach_to(object, i_end) - # object._attach_to(subcon) + if subcon.attached_to[i_end]: + subcon.detachFrom(end=i_end) + # the above will eventually call subcon._detach_from(i_end) + + # could add a check that if it isn't the highest left edge and if it + # isn't called from another edge's _detach_from method, then error, + # or go up a level... + + + def addSubcomponents(self, items, iA=[0], iB=[-1]): + '''If items is a list: Adds a sequences of nodes and edges + (alternating) as subcomponents of this edge. It also connects + the sequence in the process, and saves it as a dict rather than list.?? + If items is a list: Adds them, assuming they are already assembled.?? + iA, iB : index of the end subcomponent(s) - provided as lists + ''' + + # Attach the sequency of nodes and edges to each other + assemble(items) + + # Store them as subcomponents of this edge + self.subcomponents = items # dict(enumerate(items)) + for item in items: + if isinstance(item, list): + for branch in item: + for subitem in branch: + subitem.part_of = self + else: + item.part_of = self + # Assign ends + if isinstance(items[0],list): + self.subcons_A = [it[0] for it in items[0]] # subcomponent for end A (can be multiple) + else: + self.subcons_A = list([items[0]]) # subcomponents for end A (can be multiple) + if isinstance(items[-1],list): + self.subcons_B = [it[-1] for it in items[-1]] # subcomponent for end B (can be multiple) + else: + self.subcons_B = list([items[-1]]) # subcomponent for end B (can be multiple) + + + ''' + # Make sure the subcomponents ends are connected appropriately + # to whatever this Edge might be attached to + >>> this seems like it shouldn't be done anymore! <<< + for i in [0,1]: + if self.attached_to[end_inds[i]]: + if isinstance(self.attached_to[end_inds[i], Node): + self._attach_to(self.attached_to[end_inds[i]]) + else: # it's an edge, so also tell it which end should be attached + self._attach_to(self.attached_to[end_inds[i]], end_ends[i]) + + if self.attached_to[0]: + for i in self.end_inds[0]: + + if isinstance(self.attached_to[end_inds[i], Node): + self._attach_to(self.attached_to[end_inds[i]]) + else: # it's an edge, so also tell it which end should be attached + self._attach_to(self.attached_to[end_inds[i]], end_ends[i]) + #self._attach_to(self.attached_to[0], 0) + if self.attached_to[1]: + self._attach_to(self.attached_to[1], 1) + ''' + """ + def getTopLevelObject(self, end): + '''If this edge is part of a higher-level object, and the request + corresponds to an end of that higher-level group, return the higher object, + otherwise return this same object. Can be recursive. + A similar method exists for nodes.''' + + i_end = endToIndex(end) + + if self.part_of: # if part of something higher + supe = self.part_of # shorthand for the super edge + if supe.subcomponents[-i_end] == self: # if we're at an end of supe + return supe.getTopLevelEdge(i_end), i_end # return supe, and which end + + else: + return self, i_end # if not part of something bigger, just return self + """ + + def findEnd(self, object): + '''Checks if object is a subcomponent of self and which end it's at.''' + + if not object in self.subcomponents: + obj_in = False + for sub in self.subcomponents: + if isinstance(sub,list): + for subsub in sub: + if object in subsub: + obj_in = True + break + if not obj_in: + raise Exception("This object is not a subcomponent of this edge!") + + if any([object is con for con in self.subcons_A]): + end = 0 + elif any([object is con for con in self.subcons_B]): + end = 1 + else: + end = -1 # object isn't at the end of the higher level edge so do nothing + + return end + + + def getSubcomponent(self, index): + '''Returns the subcomponent of the edge corresponding to the provided + index. An index with multiple entries can be used to refer to parallel + subcomponents. + + Parameters + ---------- + index: list + The index of the subcomponent requested to be returned. Examples: + [2]: return the third subcomponent in the series (assuming there + are no parallel subcomponents). + [2,1,0]: Return the first (or only) object along the second + parallel string at the third serial position. Same as [2,1]. + [1,0,2]: Return the third object along the first paralle string + at the first serial position. + ''' + + if np.isscalar(index): + index = [index] # put into a common list format if not already + + if len(index) == 2: # assume length 2 is for a parallel branch with + index.append(0) # with just one edge object, so add that 0 index. + + # Only one index means the simple case without a parallel string here + if len(index) == 1: + if isinstance(self.subcomponents[index[0]], list): + raise Exception('There is a parallel string at the requested index.') + + object = self.subcomponents[index[0]] + + # Three indices means an object along a parallel string + elif len(index) == 3: + if not isinstance(self.subcomponents[index[0]], list): + raise Exception('There is not a parallel string at the requested index.') + if len(self.subcomponents[index[0]]) < index[1]+1: + raise Exception('The number of parallel strings is less than the requested index.') + if len(self.subcomponents[index[0]][index[1]]) < index[2]+1: + raise Exception('The number of objects along the parallel string is less than the requested index.') + object = self.subcomponents[index[0]][index[1]][index[2]] + + else: # other options are not yet supported + raise Exception('Index must be length 1 or 3.') + + return object + + + def setEndPosition(self, r, end): + '''Set the position of an end of the edge. This method should only be + called by a Node's setPosition method if the edge end is attached to + the node. + + Parameters + ---------- + r : list + x and y coordinates to set the end at [m]. + end + Which end of the edge is being positioned, 'a' or 'b'. ''' - if end: - self.sub_edges[-1]._attach_to(object, end) + + if end in ['a', 'A', 0]: + self.rA = np.array(r) + elif end in ['b', 'B', 1]: + self.rB = np.array(r) else: - self.sub_edges[0]._attach_to(object, end) + raise Exception('End A or B must be specified with either the letter, 0/1, or False/True.') + + + def delete(self): + '''Detach the point from anything it's attached to, then delete the + object (if such a thing is possible?).''' + + self.detachFrom(0) # detach end A + self.detachFrom(1) # detach end B + + # next step would just be to separately remove any other references + # to this object... + + + +class Poly(): + '''A base class for objects that run between two OR MORE Node-type objects. + + It has attached_to dictionaries for ends 0-N, with entries formatted as: + id : { 'ref' : object ) # simply a reference to the object it's attached to + + With a Poly, end is no longer A/B (0/1) but instead 0:N where N is the + number of exposed ends of the poly. + + >>>> + For grouped/multilevel edges, connections are stored at the highest level + in the nodes they are attached to. But each edge object will store what it's + attached to in its attached_to dict, even though that's repeated info between + the levels. + In general, edge methods will worry about their internal subcomponents, but won't + "look up" to see if they are part of something bigger. Except getTopLevelEdge(). <<<< revise + ''' + + def __init__(self, id): + + self.id = id # id number or string, used as the key when attached to things + + self.attached_to = [None, None] # whether either end [A, B] of this object is bound to another object + + # End locations + self.r = [[0,0]] + + # Some attributes related to super-polies used to group things + self.part_of = None # whether this object is part of a Poly group + + self.subcomponents = [] # collection of edges and nodes that make up this Poly + + self.sub_end_indices = [] # subcomponent index of each attachable end of this edge + self.sub_end_ends = [] # if the subcomponent is an edge, which end corresponds to self Edge's end + + whole = True # false if there are sub edges/nodes that aren't connected + + + def isAttachedTo(self, query, end=None): + '''Checks if this poly is attached to the Node object 'query'. + It uses the node's isAttached method since that will also check + if this poly in question is part of a higher-level poly that + might be what is stored in the attachments list. + + Parameters + ---------- + query + The Node we might be attached to. + end + Which end of the poly being asked about, 0:N. + ''' + + if isinstance(query, Node): # call the node's method; it's easy + return query.isAttached(self, end=end) + else: + raise Exception('Polies can only be attached to Nodes.') + + + def attachTo(self, object, r_rel=[0,0], end=None): + '''Attach an end of this poly to some node. + + Parameters + ---------- + object + The Node object to attach to. + r_rel : list + x and y coordinates of the attachment point [m]. + end + Which end of the line is being attached, 0:N. ''' + # Determine which end to attach + if end == None: + raise Exception("Poly end must be given...") + else: + i_end = int(end) # <<>> endToIndex(end, estr='when attaching an edge to something.') + + # Make sure this end isn't already attached to something + if self.attached_to[i_end]: + self.detachFrom(end=i_end) + + # Tell the Node in question to initiate the attachment + # (this will also call self._attach_to) + object.attach(self, r_rel=r_rel, end=i_end) + + + def _attach_to(self, object, end): + '''Internal method to update the poly's attached_to registry when + requested by a node. In nested polies, expects to be called for the + highest-level one first, then it will recursively call any lower ones. + + Parameters + ---------- + object + The Node object to attach to. + end + Which end of the Poly is being attached, 0:N. + ''' + i_end = endToIndex(end) # <<< (all these are redundant for polies?) <<< + + if not isinstance(object, Node): + raise Exception('Poly objects can only be attached to Node objects.') + + # Add it to the attached_to registry + self.attached_to[i_end] = object + + # Recursively attach any subcomponent at the end + if len(self.subcomponents) > 0: + subcon = self.subcomponents[-i_end] + + if isinstance(subcon, Node): # if the end subcomponent is a node + subcon._attach_to(object) + + elif isinstance(subcon, Edge): # if it's an edge + subcon._attach_to(object, i_end) + + def detachFrom(self, end): - '''Detach the specified end of the edge from whatever it's attached to. + '''Detach the specified end of the poly from whatever it's attached to. Parameters ---------- @@ -468,7 +1041,7 @@ def detachFrom(self, end): Which end of the line is being dettached, 'a' or 'b'. ''' # Determine which end to detach - i_end = endToIndex(end, estr='when detaching an edge.') + #i_end = endToIndex(end, estr='when detaching a poly.') # Tell the Node the end is attached to to initiate detachment # (this will then also call self._detach_from) @@ -476,9 +1049,9 @@ def detachFrom(self, end): def _detach_from(self, end): - '''Internal method to update the edge's attached_to registry when - requested by a node. In nested edges, expects to be called for the - highest-level on first, then it will recursively call any lower ones. + '''Internal method to update the poly's attached_to registry when + requested by a node. In nested polies, expects to be called for the + highest-level one first, then it will recursively call any lower ones. Parameters ---------- @@ -490,6 +1063,9 @@ def _detach_from(self, end): self.attached_to[i_end] = None # Recursively detach the ends of any sub-edges + + #>>> need to figure out which subcomponent would correspond to the requested end of the poly <<< + if len(self.subcomponents) > 0: subcon = self.subcomponents[-i_end] # index 0 for A, -1 for B @@ -512,29 +1088,33 @@ def _detach_from(self, end): def addSubcomponents(self, items): - '''Adds a sequences of nodes and edges (alternating) as subcomponents - of this edge. It also connects the sequence in the process.''' + '''Adds a collection of nodes and edges as subcomponents of this Poly. + These subcomponents should already be attached to each other.''' + # >>> check if the items are already assembled? <<< - assemble(items) + # Store and register them as subcomponents of this Poly self.subcomponents = items for item in items: item.part_of = self + # Make sure the subcomponents ends are connected appropriately # to whatever this Edge might be attached to - if self.attached_to[0]: - self._attach_to(self.attached_to[0], 0) - if self.attached_to[1]: - self._attach_to(self.attached_to[1], 1) - - + for i in range(len(end_inds)): + if self.attached_to[end_inds[i]]: + if isinstance(self.attached_to[end_inds[i]], Node): + self._attach_to(self.attached_to[end_inds[i]]) + else: # it's an edge, so also tell it which end should be attached + self._attach_to(self.attached_to[end_inds[i]], end_ends[i]) + + """ def getTopLevelEdge(self, end): '''If this edge is part of a higher-level edge group, and the request corresponds to an end of that higher-level group, return the higher edge, otherwise return this same object. Can be recursive. A similar method exists for nodes.''' - + >>> i_end = endToIndex(end) if self.part_of: # if part of something higher @@ -567,7 +1147,7 @@ def setEndPosition(self, r, end): end Which end of the edge is being positioned, 'a' or 'b'. ''' - + >>> if end in ['a', 'A', 0]: self.rA = np.array(r) elif end in ['b', 'B', 1]: @@ -579,17 +1159,18 @@ def setEndPosition(self, r, end): def delete(self): '''Detach the point from anything it's attached to, then delete the object (if such a thing is possible?).''' - + >>>> self.detachFrom(0) # detach end A self.detachFrom(1) # detach end B # next step would just be to separately remove any other references # to this object... + """ # general functions -def are_attached(object1, object2): +def areAttached(object1, object2): '''Check if two objects are attached to each other. If both are nodes, checks if one is in the attached list of the other. If both are edges, checks if both are attached to the same node. @@ -606,7 +1187,7 @@ def detach(object1, object2): def attach(self, object1, object2, r_rel=[0,0], end=None):#end1=None, end2=None): '''Attached 1 to object2, as long as the object types are compatible. - Ends can to be specified if either object is an end type, otherwise + Ends can to be specified if either object is an edge type, otherwise if not specified then an available end will be connected. ''' @@ -626,25 +1207,71 @@ def attach(self, object1, object2, r_rel=[0,0], end=None):#end1=None, end2=None) # lower-level utility functions -def endToIndex(end, estr=''): +def endToIndex(end, estr='', n=2): '''Converts an end specifier (a, b, A, B, 0, 1) to just 0, 1 for use in the attached_to indexing of an Edge-type object.''' - - if end in ['a', 'A', 0]: - return 0 - elif end in ['b', 'B', 1]: - return 1 - else: - raise Exception('End A/B must be specified (with a/b or 0/1) '+estr) + + if type(end) == str: + if len(end) == 1: + end = ord(end.lower())-97 # convert letter to integer (A=0, b=1, etc) + else: + raise Exception("When providing 'end' as a string, it must be a single letter.") + + if not type(end) == int: + raise Exception('End must be provided as a character or integer.') + + if end < 0: + raise Exception('End must be positive.') + elif end > n-1: + if n==2: + raise Exception('End A/B must be specified (with a/b or 0/1) '+estr) + else: + raise Exception(f'The specified end value exceeds the limit of {n} values from 0/A' +estr) + + return end def assemble(items): '''Strings together a sequence of nodes and edges''' - n = len(items) + # >>> TODO: adjust this so it can connect parallel elements .eg. for bridles <<< + ''' + # If the provided items isn't a list, there's nothing to assemble so return + if not isinstance(items[i], List): + print('Not a list - returning!') + return + ''' + n = len(items) for i in range(n-1): - if isinstance(items[i], Node) and isinstance(items[i+1], Edge): + if isinstance(items[i], list): + for subitem in items[i]: # go through each parallel subitem + if isinstance(subitem, list): # if it's a concatenation of multiple things + + assemble(subitem) # make sure that any sublist is assembled + + # attach the end objects of the subitem to the nodes before and after + if i > 0 and isinstance(items[i-1], Node): # attach to previous node + items[i-1].attach(subitem[0], end='a') + if i < n-1 and isinstance(items[i+1], Node): # attach to next node + items[i+1].attach(subitem[-1], end='b') + # note: this requires the end objects to be edges + + elif isinstance(subitem, Edge): # if the subitem is just one edge + print("THIS CASE SHOULDN'T HAPPEN - the list should be nested more") + breakpoint() + if i > 0 and isinstance(items[i-1], Node): # attach to previous node + items[i-1].attach(subitem, end='a') + if i < n-1 and isinstance(items[i+1], Node): # attach to next node + items[i+1].attach(subitem, end='b') + else: + raise Exception("Unsupported situation ... parallel subitems must be edges or concatenations") + + elif isinstance(items[i], Node) and isinstance(items[i+1], list): + pass # this node connects to a bridle or doubled section, + # so it will be hooked up in the next step + + elif isinstance(items[i], Node) and isinstance(items[i+1], Edge): items[i].attach(items[i+1], end='a') elif isinstance(items[i], Edge) and isinstance(items[i+1], Node): @@ -652,9 +1279,63 @@ def assemble(items): else: raise Exception('sequences is not alternating between nodes and edges') + # check if last item in items is a list (if length of items>1) + # if it is a list, it won't have been attached/assembled previously, so + # attach and assemble now + if n-1>0: + if isinstance(items[i+1], list): + for subitem in items[i+1]: # go through each parallel subitem + if isinstance(subitem, list): # if it's a concatenation of multiple things + assemble(subitem) # make sure that any sublist is assembled + + # attach the end objects of the subitem to the nodes before and after + if i > 0 and isinstance(items[i], Node): # attach to previous node + items[i].attach(subitem[0], end='a') + if i < n-1 and isinstance(items[i+1], Node): # attach to next node + items[i+1].attach(subitem[-1], end='b') + # note: this requires the end objects to be edges + + elif isinstance(subitem, Edge): # if the subitem is just one edge + print("THIS CASE SHOULDN'T HAPPEN - the list should be nested more") + breakpoint() + if i > 0 and isinstance(items[i], Node): # attach to previous node + items[i].attach(subitem, end='a') + if i < n-1 and isinstance(items[i+1], Node): # attach to next node + items[i+1].attach(subitem, end='b') + else: + raise Exception("Unsupported situation ... parallel subitems must be edges or concatenations") + +def rotationMatrix(x3,x2,x1): + '''Calculates a rotation matrix based on order-z,y,x instrinsic (tait-bryan?) angles, meaning + they are about the ROTATED axes. (rotation about z-axis would be (0,0,theta) ) + (Copied from MoorPy) + Parameters + ---------- + x3, x2, x1: floats + The angles that the rotated axes are from the nonrotated axes. Normally roll,pitch,yaw respectively. [rad] + + Returns + ------- + R : matrix + The rotation matrix + ''' + # initialize the sines and cosines + s1 = np.sin(x1) + c1 = np.cos(x1) + s2 = np.sin(x2) + c2 = np.cos(x2) + s3 = np.sin(x3) + c3 = np.cos(x3) + # create the rotation matrix + R = np.array([[ c1*c2, c1*s2*s3-c3*s1, s1*s3+c1*c3*s2], + [ c2*s1, c1*c3+s1*s2*s3, c3*s1*s2-c1*s3], + [ -s2, c2*s3, c2*c3]]) + return R + + # test script if __name__ == '__main__': @@ -679,7 +1360,7 @@ def assemble(items): edge1.attachTo(node2, end='a') - # ----- make a test for super edges... ----- + # ----- a test for super edges... ----- e0 = Edge(id='e0') e1 = Edge(id='e1') @@ -696,6 +1377,126 @@ def assemble(items): assemble([A, E, B]) - E.addSubcomponents([e0,n0,e1,n1,e2]) + E.addSubcomponents([e0,n0,e1,n1,e2]) + + + # ----- a test for bridles etc ----- + + e0 = Edge(id='e0') + n0 = Node(id='n0') + e1 = Edge(id='e1') + n1 = Node(id='n1') + e2a1 = Edge(id='e2a') + e2a2 = Node(id='e2a') + e2a3 = Edge(id='e2a') + e2b = Edge(id='e2b') + + + thelist = [e0, n0, e1, n1, [[e2a1, e2a2, e2a3], [e2b]]] + E = Edge(id='big edge') + + E.addSubcomponents(thelist) + + s = E.getSubcomponent([4,0,2]) + + # ----- try joining two nodes ----- + """ + A = Node(id='Node A') + B = Node(id='Node B') + A.join(B) + + + # ----- tests connecting multi-level node and edge objects ---- + + # --- Test 1 --- + # platform and fairlead + n1 = Node(id='n1') + n2 = Node(id='n2') + n1.attach(n2, r_rel=[20,0,-10]) + # mooring and contents + e1 = Edge(id='e1') + e1_e1 = Edge(id='e1_e1') + e1_n2 = Node(id='e1_n2') + e1_e3 = Edge(id='e1_e3') + e1.addSubcomponents([e1_e1, e1_n2, e1_e3]) + # attach mooring to platfrom (by lower objects, then upper will be automatic) + #n2.attach(e1_e1, end='A') + n2.attach(e1_e3, end='B') + + # --- Test 2 --- + # platform and fairlead + n1 = Node(id='n1') + n2 = Node(id='n2') + n1.attach(n2, r_rel=[20,0,-10]) + # mooring and contents + e1 = Edge(id='e1') + e1_n1 = Node(id='e1_n1') + e1_e2 = Edge(id='e1_e2') + e1_n3 = Node(id='e1_n3') + e1.addSubcomponents([e1_n1, e1_e2, e1_n3]) + # attach mooring to platfrom (by lower objects, then upper will be automatic) + n2.attach(e1_n1) + #n2.attach(e1_n3) + #n2.join(e1_n1) + #n2.join(e1_n3) + + # --- Test 3 --- + # platform and fairlead + n1 = Node(id='n1') + # mooring and contents + e1 = Edge(id='e1') + e1_e1 = Edge(id='e1_e1') + e1_n2 = Node(id='e1_n2') + e1_e3 = Edge(id='e1_e3') + e1.addSubcomponents([e1_e1, e1_n2, e1_e3]) + # attach mooring to platfrom (by lower objects, then upper will be automatic) + #n1.attach(e1_e1, r_rel=[20,0,-10], end='A') + n1.attach(e1_e3, r_rel=[20,0,-10], end='B') + + # --- Test 4 --- + # platform and fairlead + n1 = Node(id='n1') + # mooring and contents + e1 = Edge(id='e1') + e1_n1 = Node(id='e1_n1') + e1_e2 = Edge(id='e1_e2') + e1_n3 = Node(id='e1_n3') + e1.addSubcomponents([e1_n1, e1_e2, e1_n3]) + # attach mooring to platfrom (by lower objects, then upper will be automatic) + #n1.attach(e1_n1, r_rel=[20,0,-10]) + n1.attach(e1_n3, r_rel=[20,0,-10]) + + # --- Test 5 --- + # platform and fairlead + n1 = Node(id='n1') + n2 = Node(id='n2') + n1.attach(n2, r_rel=[20,0,-10]) + # mooring and contents + e1_e1 = Edge(id='e1_e1') + e1_n2 = Node(id='e1_n2') + e1_n2.attach(e1_e1, end='B') + # attach mooring to platfrom (by lower objects, then upper will be automatic) + n2.attach(e1_n2) + + # --- Test 6 --- + # platform and fairlead + n1 = Node(id='n1') + n2 = Node(id='n2') + n1.attach(n2, r_rel=[20,0,-10]) + # mooring and contents + e1_n1 = Node(id='e1_n1') + e1_e2 = Edge(id='e1_e2') + e1_n1.attach(e1_e2, end='A') + # attach mooring to platfrom (by lower objects, then upper will be automatic) + n2.attach(e1_e2, end='B') + # --- done tests --- + n1.setPosition(r=[0,0,0], theta=0) + #print(n1.attachments) + #print(e1.attached_to) + """ + + + + \ No newline at end of file diff --git a/famodel/helpers.py b/famodel/helpers.py index cfaf1f18..55f9b243 100644 --- a/famodel/helpers.py +++ b/famodel/helpers.py @@ -232,12 +232,16 @@ def head_adjust(att,heading,rad_buff=np.radians(30),endA_dir=1, adj_dir=1): att : list list of objects to attach to. 1 object if only concerned about the attached object associated with that side heading : float - Cable heading at attachment to att in radians + Cable compass heading at attachment to att in radians rad_buff : float Buffer angle in radians endA_dir : float, optional Either 1 or -1, controls sign of new heading for end B. Only altered to -1 if dynamic cable from end A will get close to end B moorings. Default is 1. + adj_dir : float, optional + Either 1 or -1, default is 1. If -1, adjusts direction heading is altered + to avoid mooring lines, can be used if that heading direction is more natural. + This is a manual input to the main function adjusting cables. Returns ------- @@ -249,7 +253,7 @@ def head_adjust(att,heading,rad_buff=np.radians(30),endA_dir=1, adj_dir=1): headnew = np.pi*2 + heading else: headnew = heading - attheadings = [] + attheadings = [] # complete list of mooring headings to avoid, from all platforms flipheads = False # whether to flip headings ( for if you are looking at mooring headings of platform on the other end) for at in att: mhs = np.radians([m.heading for m in at.getMoorings().values()]) @@ -260,11 +264,11 @@ def head_adjust(att,heading,rad_buff=np.radians(30),endA_dir=1, adj_dir=1): if a>2*np.pi: atmh[j] = a-2*np.pi else: - atmh = np.array(mhs) #at.mooring_headings + at.phi + atmh = np.array(mhs) #attached platform mooring headings array #attheadings.extend(atmh) - attheadings.extend(np.pi/2 - atmh) # convert to 0 rad at East going CCW + attheadings.extend(atmh) # keep in compass heading flipheads = True - + interfere_h = check_headings(attheadings,headnew,rad_buff) # if the headings interfere, adjust them by angle buffer for mhead in interfere_h: @@ -286,6 +290,71 @@ def head_adjust(att,heading,rad_buff=np.radians(30),endA_dir=1, adj_dir=1): return(headnew) +def cableDesignInterpolation(dd, cables, depth): + '''Interpolates between dynamic cable designs for different depths to produce + a design for the given depth + + Parameters + ---------- + dd : dict + Design dictionary of cable object before interpolation + cables : list + List of dictionaries of cable designs to interpolate between + depth : float + Depth (abs val) of cable to interpolate design for + ''' + # grab list of values for all cables + + n_bs = len(dd['buoyancy_sections']) + cabdesign = {'n_buoys':[[] for _ in range(n_bs)], + 'spacings':[[] for _ in range(n_bs)], + 'L_mids':[[] for _ in range(n_bs)], + 'span': [], + 'L': []} + depths = [] + for cab in cables: + if len(cab['buoyancy_sections'])==n_bs: + for ii in range(n_bs): + cabdesign['n_buoys'][ii].append( + cab['buoyancy_sections'][ii]['N_modules']) + cabdesign['spacings'][ii].append( + cab['buoyancy_sections'][ii]['spacing']) + cabdesign['L_mids'][ii].append( + cab['buoyancy_sections'][ii]['L_mid']) + + cabdesign['L'].append(cab['L']) + depths.append(cab['depth']) + cabdesign['span'].append(cab['span']) + + # sort and interp all lists by increasing depths + sorted_indices = np.argsort(depths) + depths_sorted = [depths[i] for i in sorted_indices] + newdd = deepcopy(dd) + if depth > depths_sorted[-1]: + # depth outside range, can't interpolate - just adjust length + newdd['L'] = cabdesign['L'][sorted_indices[-1]] + depth-depths_sorted[-1] + elif depth < depths_sorted[0]: + # depth outside range, can't interpolate - just adjust length + newdd['L'] = cabdesign['L'][sorted_indices[0]] - depth-depths_sorted[0] + else: + # interpolate designs + newdd['span'] = np.interp(depth,depths_sorted, + [cabdesign['span'][i] for i in sorted_indices]) + for i,bs in enumerate(newdd['buoyancy_sections']): + bs['N_modules'] = np.interp(depth, depths_sorted, + [cabdesign['n_buoys'][i][j] for j in sorted_indices]) + bs['spacing'] = np.interp(depth,depths_sorted, + [cabdesign['spacings'][i][j] for j in sorted_indices]) + bs['L_mid'] = np.interp(depth,depths_sorted, + [cabdesign['L_mids'][i][j] for j in sorted_indices]) + newdd['L'] = np.interp(depth,depths_sorted, + [cabdesign['L'][j] for j in sorted_indices]) + newdd['depth'] = depth + newdd['z_anch'] = -depth + + + return(newdd) + def getCableDD(dd,selected_cable,cableConfig,cableType_def,connVal): ''' get cable design dictionary from a cableConfig yaml. Primarily used for project.addCablesConnections() @@ -317,11 +386,11 @@ def getCableDD(dd,selected_cable,cableConfig,cableType_def,connVal): # get connector and joint costs if they were given dd['connector_cost'] = getFromDict(selected_cable,'connector_cost',default=0) joint_cost = getFromDict(selected_cable,'joint_cost',default=0) - + depth = cableConfig['cableTypes'][selected_cable['sections'][0]]['depth'] for j in range(len(selected_cable['sections'])): dd['cables'].append(deepcopy(cableConfig['cableTypes'][selected_cable['sections'][j]])) cd = dd['cables'][j] - cd['z_anch'] = -selected_cable['depth'] + cd['z_anch'] = -depth # cd['cable_type'] = cableConfig['cableTypes'][selected_cable['sections'][j]] # assign info in selected cable section dict to cd cd['A'] = selected_cable['A'] cd['voltage'] = cableType_def[-2:] @@ -354,73 +423,140 @@ def getCableDD(dd,selected_cable,cableConfig,cableType_def,connVal): return(dd) +def getCandidateCableDesigns(cable_reqs, cable_configs): + ''' + Returns list of cable designs that meet requirements + + Parameters + ---------- + cable_reqs : TYPE + DESCRIPTION. + cable_configs : TYPE + DESCRIPTION. + + Returns + ------- + None. + + ''' + candidate_cables = [] + for config in cable_configs: + if np.allclose( + np.array([cable_reqs[key] for key in cable_reqs.keys()]), + np.array([config[key] for key in cable_reqs.keys()]) + ): + candidate_cables.append(config) + + return(candidate_cables) + def getCableDesign(connVal, cableType_def, cableConfig, configType, depth=None): # go through each index in the list and create a cable, connect to platforms dd = {} dd['cables'] = [] # collect design dictionary info on cable - - # create reference cables (these are not saved into the cableList, just used for reference) + if connVal['cable_id']>100: + # connected to substation, overwrite cable type + ctype = 0 + else: + ctype=configType + + cable_reqs = {'A': connVal['conductor_area'], + 'type': ctype } - # find associated cable in cableConfig dict - cableAs = [] - cableDs = [] - cable_selection = [] - for cabC in cableConfig['configs']: - if connVal['conductor_area'] == cabC['A']: - cableAs.append(cabC) - if not cableAs: - raise Exception('Cable configs provided do not match required conductor area') - elif len(cableAs) == 1: - cable_selection = cableAs - else: - for cabA in cableAs: - # only check distance if the cable is NOT connected to substation - if 'dist' in cabA and connVal['cable_id']<100: - if abs(connVal['2Dlength'] - cabA['dist']) < 0.1: - cableDs.append(cabA) + if ctype>0: + cable_reqs['dist'] = connVal['2Dlength'] - #if there's no matching distance, assume the nonsuspended cables - if cableDs == []: - for cabA in cableAs: - if cabA['type'] == 0: - cableDs.append(cabA) + cable_candidates = getCandidateCableDesigns(cable_reqs, + cableConfig['configs']) + if not cable_candidates: + # change type to dynamic-static-dynamic and try again + cable_reqs['type']=0 + cable_candidates = getCandidateCableDesigns(cable_reqs, + cableConfig['configs']) + + if len(cable_candidates)> 1: + # downselect by depth + depthdiff = np.array([x['depth']-depth for x in cable_candidates]) + selected_cable = cable_candidates[np.argmin(depthdiff)] + elif len(cable_candidates) == 1: + # found the correct cable + selected_cable = cable_candidates[0] + else: + raise Exception(f"No cable matching the selection criteria found for cable {connVal['cable_id']}") + + # # create reference cables (these are not saved into the cableList, just used for reference) + + # # find associated cable in cableConfig dict + # cableAs = [] + # cableDs = [] + # cable_selection = [] + # for cabC in cableConfig['configs']: + # if connVal['conductor_area'] == cabC['A']: + # cableAs.append(cabC) + # if not cableAs: + # raise Exception('Cable configs provided do not match required conductor area') + # elif len(cableAs) == 1: + # cable_selection = cableAs + # cableDs = cableAs # needed for interpolation procedure + # else: + # for cabA in cableAs: + # # only check distance if the cable is NOT connected to substation + # if 'dist' in cabA and connVal['cable_id']<100: + # if abs(connVal['2Dlength'] - cabA['dist']) < 0.1: + # cableDs.append(cabA) + + # #if there's no matching distance, assume the nonsuspended cables + # if cableDs == []: + # for cabA in cableAs: + # if cabA['type'] == 0: + # cableDs.append(cabA) - for cabD in cableDs: - if connVal['cable_id']>=100 and cabD['type']==0: - # connected to a substation, use a dynamic-static-dynamic configuration - cable_selection.append(cabD) + # for cabD in cableDs: + # if connVal['cable_id']>=100 and cabD['type']==0: + # # connected to a substation, use a dynamic-static-dynamic configuration + # cable_selection.append(cabD) - elif connVal['cable_id']<100 and cabD['type']==configType: - # not connected to substation, use default config type - cable_selection.append(cabD) + # elif connVal['cable_id']<100 and cabD['type']==configType: + # # not connected to substation, use default config type + # cable_selection.append(cabD) - # if no cables are found to match, override the configType + # # if no cables are found to match, override the configType - if cable_selection == []: - for cabD in cableDs: - if connVal['cable_id']<100: - cable_selection.append(cabD) + # if cable_selection == []: + # for cabD in cableDs: + # if connVal['cable_id']<100: + # cable_selection.append(cabD) - if len(cable_selection)> 1: - # downselect by depth - depthdiff = np.array([x['depth']-depth for x in cable_selection]) - selected_cable = cable_selection[np.argmin(depthdiff)] - # else: - # raise Exception(f"Multiple cables match selection criteria for cable {connDict[i]['cable_id']}") - elif len(cable_selection) == 1: - # found the correct cable - selected_cable = cable_selection[0] + # if len(cable_selection)> 1: + # # downselect by depth + # depthdiff = np.array([x['depth']-depth for x in cable_selection]) + # selected_cable = cable_selection[np.argmin(depthdiff)] + # # else: + # # raise Exception(f"Multiple cables match selection criteria for cable {connDict[i]['cable_id']}") + # elif len(cable_selection) == 1: + # # found the correct cable + # selected_cable = cable_selection[0] - else: - raise Exception(f"No cable matching the selection criteria found for cable {connVal['cable_id']}") - - dd = getCableDD(dd,selected_cable,cableConfig,cableType_def,connVal) + # else: + # raise Exception(f"No cable matching the selection criteria found for cable {connVal['cable_id']}") + dd = getCableDD(dd,selected_cable,cableConfig,cableType_def,connVal) + i_dc = [i for i,sec in enumerate(dd['cables']) if sec['type']=='dynamic'] dd['name'] = cableType_def + dc_cands = [] + # pull out the dc definitions of candidate cables + for cand in cable_candidates: + cand = dict(cand) + for sec in cand['sections']: + typedef = cableConfig['cableTypes'][sec] + if typedef['type']=='dynamic' and typedef not in dc_cands: + dc_cands.append(cableConfig['cableTypes'][sec]) + for i in i_dc: + dd['cables'][i] = cableDesignInterpolation( + dd['cables'][i], dc_cands, depth) - return(selected_cable,deepcopy(dd)) + return(selected_cable,deepcopy(dd), cable_candidates) def getDynamicCables(cable_config, cable_types, cable_appendages, depth, rho_water=1025, g=9.81): @@ -659,6 +795,7 @@ def MooringProps(mCon, lineTypes, rho_water, g, checkType=1): # else: # d_vol = dd['d'] dd['w'] = (dd['m']-np.pi/4*d_vol**2*rho_water)*g + dd['MBL'] = float(dd['MBL']) if 'mooringFamily' in mCon: raise Exception('type and moorFamily listed in yaml - use type to reference a mooring type in the mooring_line_types section of the yaml and mooringFamily to obtain mooring properties from MoorProps_default.yaml') elif 'mooringFamily' in mCon: @@ -670,6 +807,7 @@ def MooringProps(mCon, lineTypes, rho_water, g, checkType=1): dd = mProps dd['name'] = mCon['mooringFamily'] dd['d_nom'] = mCon['d_nom'] + dd['MBL'] = float(dd['MBL']) elif 'type' in mCon and not mCon['type'] in lineTypes: raise Exception(f'Type {mCon["type"]} provided in mooring_line_config {mCon} is not found in mooring_line_types section. Check for errors.') @@ -693,20 +831,20 @@ def getMoorings(lcID, lineConfigs, connectorTypes, pfID, proj): Returns ------- - m_config : dict - mooring configuration dictionary - c_config : dict - connector configuration dictionary + dd : dict + mooring design dictionary ''' # set up dictionary of information on the mooring configurations - m_config = {'sections':[],'anchor':{},'span':{},'zAnchor':{}}#,'EndPositions':{}} + dd = {'span':{},'zAnchor':{}}#,'EndPositions':{}} # set up connector dictionary c_config = [] + config = [] # mooring and connector combined configuation list lineLast = 1 # boolean whether item with index k-1 is a line. Set to 1 for first run through of for loop ct = 0 # counter for number of line types - for k in range(0,len(lineConfigs[lcID]['sections'])): # loop through each section in the line + nsec = len(lineConfigs[lcID]['sections']) # number of sections + for k in range(0,nsec): # loop through each section in the line lc = lineConfigs[lcID]['sections'][k] # set location for code clarity later # determine if it's a line type or a connector listed @@ -714,19 +852,20 @@ def getMoorings(lcID, lineConfigs, connectorTypes, pfID, proj): # this is a line if lineLast: # previous item in list was a line (or this is the first item in a list) # no connector was specified for before this line - add an empty connector + config.append({}) c_config.append({}) # set line information lt = MooringProps(lc, proj.lineTypes, proj.rho_water, proj.g) # lt = self.lineTypes[lc['type']] # set location for code clarity and brevity later # set up sub-dictionaries that will contain info on the line type - m_config['sections'].append({'type':lt})# {'name':str(ct)+'_'+lc['type'],'d_nom':lt['d_nom'],'material':lt['material'],'d_vol':lt['d_vol'],'m':lt['m'],'EA':float(lt['EA'])}}) - m_config['sections'][ct]['type']['name'] = str(ct)+'_'+str(lt['name']) + config.append({'type':lt})# {'name':str(ct)+'_'+lc['type'],'d_nom':lt['d_nom'],'material':lt['material'],'d_vol':lt['d_vol'],'m':lt['m'],'EA':float(lt['EA'])}}) + config[-1]['type']['name'] = str(ct)+'_'+str(lt['name']) # make EA a float not a string - m_config['sections'][ct]['type']['EA'] = float(lt['EA']) + config[-1]['type']['EA'] = float(lt['EA']) + config[-1]['type']['MBL'] = float(lt['MBL']) # set line length - m_config['sections'][ct]['L'] = lc['length'] - # update counter for line types - ct = ct + 1 + config[-1]['L'] = lc['length'] + # update line last boolean lineLast = 1 @@ -740,56 +879,107 @@ def getMoorings(lcID, lineConfigs, connectorTypes, pfID, proj): # last item in list was a line if cID in connectorTypes: - c_config.append(connectorTypes[cID]) # add connector to list - c_config[-1]['type'] = cID + config.append(connectorTypes[cID]) # add connector to list + config[-1]['type'] = cID else: # try pointProps try: props = loadPointProps(None) design = {f"num_c_{cID}":1} - c_config.append(getPointProps(design, Props=props)) + config.append(getPointProps(design, Props=props)) + except Exception as e: raise Exception(f"Connector type {cID} not found in connector_types dictionary, and getPointProps raised the following exception:",e) # update lineLast boolean lineLast = 0 + elif 'subsections' in lc: + # TODO: LHS: ERROR CHECKING FOR ORDER OF COMPONENTS PROVIDED WITHIN SUBSECTIONS, ADD IN NEEDED CONNECTORS!! + + if lineLast and k != 0: + # if this is not the first section AND last section was a line, add a empty connector first + config.append({}) + lineLast = 0 + config.append([]) + sublineLast = [lineLast]*len(lc['subsections']) # to check if there was a connector provided before this + for ii,sub in enumerate(lc['subsections']): + config[-1].append([]) + for jj,subsub in enumerate(sub): + if 'connectorType' in subsub and sublineLast[ii]: + cID = subsub['connectorType'] + if cID in connectorTypes: + cID = subsub['connectorType'] + config[-1][-1].append(connectorTypes[cID]) + else: + # try pointProps + try: + props = loadPointProps(None) + design = {f"num_c_{cID}":1} + config[-1][-1].append(getPointProps(design, Props=props)) + except Exception as e: + raise Exception(f"Connector type {cID} not found in connector_types dictionary, and getPointProps raised the following exception:",e) + sublineLast[ii] = 0 + elif 'connectorType' in subsub and not sublineLast[ii]: + raise Exception('Previous section had a connector, two connectors cannot be listed in a row') + elif 'type' or 'mooringFamily' in subsub: + if sublineLast[ii]: + # add empty connector + config[-1][-1].append({}) + lt = MooringProps(subsub,proj.lineTypes, proj.rho_water, proj.g) + config[-1][-1].append({'type':lt, + 'L': subsub['length']}) + # make EA a float not a string + config[-1][-1][-1]['type']['EA'] = float(lt['EA']) + config[-1][-1][-1]['type']['MBL'] = float(lt['MBL']) + sublineLast[ii] = 1 + else: + raise Exception(f"keys in subsection line definitions must either be 'type', 'mooringFamily', or 'connectorType'") + # if this is the last section and the last part of the subsection in the section, it needs to end on a connector + # so, add a connector if last part of subsection was a line! + if sublineLast[ii] and k==nsec-1 and jj==len(sub)-1: + # end bridle needs connectors added + config[-1][-1].append({}) + sublineLast[ii] = 0 + + lineLast = sublineLast[-1] # TODO: LHS: think how to handle this situation for error checking... else: # not a connector or a line - raise Exception(f"Please make sure that all section entries for line configuration '{lcID}' are either line sections (which must have a 'type' key) or connectors (which must have a 'connectorType' key") + raise Exception(f"Please make sure that all section entries for line configuration '{lcID}' are either line sections (which must have a 'type' key), connectors (which must have a 'connectorType' key, or subsections") # check if line is a shared symmetrical configuration if 'symmetric' in lineConfigs[lcID] and lineConfigs[lcID]['symmetric']: if not lineLast: # check if last item in line config list was a connector - for ii in range(0,ct): + for ii in range(len()): # set mooring configuration - m_config['sections'].append(m_config['sections'][-1-2*ii]) + config.append(config[-1-2*ii]) # set connector (since it's mirrored, connector B becomes connector A) - c_config.append(c_config[-2-2*ii]) + config.append(config[-2-2*ii]) else: # double the length of the end line - m_config['sections'][-1]['L'] = m_config['sections'][-1]['L']*2 + config[-1]['L'] =config[-1]['L']*2 # set connector B for line same as previous listed connector - c_config.append(c_config[-1]) + config.append(config[-1]) for ii in range(0,ct-1): # go through every line config except the last (since it was doubled already) # set mooring configuration - m_config['sections'].append(m_config['sections'][-2-2*ii]) + config.append(config[-2-2*ii]) # set connector - c_config.append(c_config[-3-2*ii]) + config.append(config[-3-2*ii]) else: # if not a symmetric line, check if last item was a line (if so need to add another empty connector) if lineLast: # add an empty connector object - c_config.append({}) + config.append({}) # set general information on the whole line (not just a section/line type) # set to general depth first (will adjust to depth at anchor location after repositioning finds new anchor location) - m_config['zAnchor'] = -proj.depth - m_config['span'] = lineConfigs[lcID]['span'] - m_config['name'] = lcID + dd['subcomponents'] = config + dd['zAnchor'] = -proj.depth + dd['span'] = lineConfigs[lcID]['span'] + dd['name'] = lcID # add fairlead radius and depth to dictionary - m_config['rad_fair'] = proj.platformList[pfID].rFair - m_config['z_fair'] = proj.platformList[pfID].zFair + dd['rad_fair'] = proj.platformList[pfID].rFair + dd['z_fair'] = proj.platformList[pfID].zFair - m_config['connectors'] = c_config # add connectors section to the mooring dict - return(m_config) #, c_config) + return(dd) #, c_config) + def getConnectors(c_config, mName, proj): @@ -850,8 +1040,96 @@ def getAnchors(lineAnch, arrayAnchor, proj): return(ad, mass) -def route_around_anchors(proj, anchor=True, cable=True, padding=50): +def attachFairleads(moor, end, platform, fair_ID_start=None, fair_ID=None, fair_inds=None): + ''' + helper function for loading, attaches fairleads to mooring objects + and runs some error checks + + Parameters + ---------- + fair_inds : int/list + Fairlead index/indices to attach to mooring line + moor : Mooring class instance + Mooring that will attach to fairlead(s) + end : int or str + must be in [0,a,A] for end A or [1,b,B] for end B + platform : Platform class instance + Platform that is associated with the fairlead + fair_ID_start : str, optional + start of fairlead ID, the index will be appended to this. Not needed if fair_ID provided + fair_ID : list, optional + fairlead ID list for each fairlead. If fair_ID_start is not provided, fair_ID must be provided + + + Returns + ------- + None. + + ''' + # convert to list if needed + if fair_inds is not None : + if not isinstance(fair_inds,list): + fair_inds = list([fair_inds]) + # check lengths are the same + if not len(moor.subcons_B)==len(fair_inds): + raise Exception(f'Number of fairleads must equal number of parallel sections at end {end}') + elif fair_ID is not None: + if not isinstance(fair_ID, list): + fair_ID = list([fair_ID]) + # check lengths are the same + if not len(moor.subcons_B)==len(fair_ID): + raise Exception(f'Number of fairleads must equal number of parallel sections at end {end}') + else: + raise Exception('Either fairlead indices or fairlead IDs must be provided') + # grab correct end + end_subcons = moor.subcons_B if end in [1,'b','B'] else moor.subcons_A + + # put together fairlead ids as needed + if fair_ID_start != None and fair_inds != None: + fair_ID = [] + for i in fair_inds: + fair_ID.append(fair_ID_start+str(i)) + # attach each fairlead to the end subcomponent + fairs = [] + for ii,con in enumerate(end_subcons): + fairs.append(platform.attachments[fair_ID[ii]]['obj']) + end_subcons[ii].join(fairs[-1]) + + return(fairs) + +def calc_heading(pointA, pointB): + '''calculate a compass heading from points, if pointA or pointB is a list of points, + the average of those points will be used for that end''' + # calculate the midpoint of the point(s) on each end first + pointAmid = calc_midpoint(pointA) + pointBmid = calc_midpoint(pointB) + dists = np.array(pointAmid) - np.array(pointBmid) + headingB = np.pi/2 - np.arctan2(dists[1], dists[0]) + + return(headingB) + +def calc_midpoint(point): + '''Calculates the midpoint of a list of points''' + if isinstance(point[0],list) or isinstance(point[0],np.ndarray): + pointx = sum([x[0] for x in point])/len(point) + pointy = sum([x[1] for x in point])/len(point) + # add z component if needed + if len(point[0])==3: + pointz = sum([x[2] for x in point])/len(point) + return([pointx,pointy,pointz]) + else: + pointx = point[0] + pointy = point[1] + # add z component if needed + if len(point)==3: + pointz = point[2] + return([pointx,pointy,pointz]) + + return([pointx,pointy]) + +def route_around_anchors(proj, anchor=True, cable=True, padding=50): + '''check if static cables hit anchor buffer, if so reroute cables around anchors''' # make anchor buffers with 50m radius if anchor: anchor_buffs = [] @@ -873,15 +1151,16 @@ def angle(pt): for anch in anchor_buffs: if cab.intersects(anch): # Get the start and end of the detour (the two closest points to the buffer) - segments = [] + new_points = [] # make additional points on the line on either side of anchor dist_to_anch = cab.line_locate_point(anch.centroid) if dist_to_anch > 100: - segments.append(cab.interpolate(dist_to_anch - 100)) + new_points.append(cab.interpolate(dist_to_anch - 100)) if cab.length - dist_to_anch > 100: - segments.append(cab.interpolate(dist_to_anch + 100)) + new_points.append(cab.interpolate(dist_to_anch + 100)) - start = np.array(segments[0].coords[-1]) + # pull out the coordinates of the first new point + start = np.array(new_points[0].coords[-1]) # Get buffer center and radius center = np.array(anch.centroid.coords[0]) @@ -893,8 +1172,20 @@ def angle(pt): # Generate point along the arc (detour) arc_point = [center[0] + radius * np.cos(angle_start+np.pi/2), center[1] + radius * np.sin(angle_start+np.pi/2)] + # determine relative positions of new routing points among other routing points + rel_dist = [] + orig_coords = [] + for i,x in enumerate(proj.cableList[name].subcomponents[2].x): + y = proj.cableList[name].subcomponents[2].y[i] + rel_dist.append(cab.line_locate_point(sh.Point([x,y]))) + orig_coords.append([x,y]) + all_dists = np.hstack((rel_dist,dist_to_anch-100, dist_to_anch+100, dist_to_anch)) + all_points = np.vstack((orig_coords,[coord.coords[0] for coord in new_points],arc_point)) + sorted_idxs = np.argsort(all_dists) + final_points = all_points[sorted_idxs] + # add new routing point in cable object - proj.cableList[name].subcomponents[2].updateRouting([list(segments[0].coords[1:]) + arc_point + list(segments[1].coords[:-1])]) + proj.cableList[name].subcomponents[2].updateRouting(final_points) @@ -1057,7 +1348,8 @@ def eval_func(X, args): Xmin=[1], Xmax=[1.1*np.linalg.norm(ss.rB-ss.rA)], dX_last=[1], tol=[0.01], maxIter=50, stepfac=4) ss.lineList[i_line].L = L_final[0] - mooring.dd['sections'][i_line]['L'] = L_final[0] + sec = mooring.getSubcomponent(i_line) + sec['L'] = L_final[0] mooring.dd['span'] = span mooring.span = span @@ -1083,6 +1375,8 @@ def func_TH_L(X, args): args=dict(direction='horizontal'), Xmin=[10], Xmax=[2000], dX_last=[10], maxIter=50, stepfac=4) + # update design dictionary L + mooring.setSectionLength(ss.lineList[i_line].L,i_line) else: print('Invalid method. Must be either pretension or horizontal') @@ -1178,6 +1472,28 @@ def gothroughlist(dat): # return cleaned dictionary return(info) + +def createRAFTDict(project): + from famodel.turbine.turbine import Turbine + # Create a RAFT dictionary from a project class to create RAFT model + rd = {'array':{'keys':['ID', 'turbineID', 'platformID', 'mooringID', 'x_location', 'y_location', 'heading_adjust'], + 'data':[]}} + turb = 0 + for pf in project.platformList.values(): + for att in pf.attachments.values(): + if isinstance(att['obj'],Turbine): + turb = att['obj'].dd['type'] + break + rd['array']['data'].append([pf.id, turb, pf.dd['type'], 0, pf.r[0], pf.r[1],np.degrees(pf.phi)]) + rd['site'] = {'water_depth':project.depth,'rho_water':project.rho_water,'rho_air':project.rho_air,'mu_air':project.mu_air} + rd['site']['shearExp'] = .12 + + rd['turbines'] = project.turbineTypes + rd['platforms'] = project.platformTypes + + return rd + + def getFromDict(dict, key, shape=0, dtype=float, default=None, index=None): ''' Function to streamline getting values from design dictionary from YAML file, including error checking. diff --git a/famodel/images/fairleads_and_jtubes.png b/famodel/images/fairleads_and_jtubes.png new file mode 100644 index 00000000..a9289fbf Binary files /dev/null and b/famodel/images/fairleads_and_jtubes.png differ diff --git a/famodel/images/parallel_sections.png b/famodel/images/parallel_sections.png new file mode 100644 index 00000000..332ddaa1 Binary files /dev/null and b/famodel/images/parallel_sections.png differ diff --git a/famodel/mooring/README.md b/famodel/mooring/README.md index 1e0b38fa..6e06022a 100644 --- a/famodel/mooring/README.md +++ b/famodel/mooring/README.md @@ -22,18 +22,23 @@ includes a design dictionary with the following details: - rad_fair : Fairlead radius - z_fair : Fairlead depth - span : 2D distance from fairlead to anchor (or fairlead to fairlead) -- Sections: List of line segment detail dictionaries, becomes list of section objects - - type : Line section type dictionary - - d_nom, d_vol : diameter (nominal and volume-equivalent) [m] - - material - - cost [USD] - - m : linear mass [g/m] - - w : weight [N/m] - - MBL : minimum breaking load [N] - - EA : stiffness coefficient [N] - - L : Line section length [m] +- Subcomponents: List of line sections and connectors in order from end A to end B + The values in each subcomponent type vary depending on if it is a section or connector. For sections: + - type : material property dictionary + - d_nom, d_vol : diameter (nominal and volume-equivalent) [m] + - material + - cost [USD] + - m : linear mass [g/m] + - w : weight [N/m] + - MBL : minimum breaking load [N] + - EA : stiffness coefficient [N] + - L : Line section length [m] + For connectors: + - m : mass [kg] + - v : volume [kg/m^3] + - CdA -The Mooring object contains subcomponent objects that represent each component of the full mooring line. Line segments are Section objects, while connectors between segments and at the ends of the lines are Connector objects. These segments alternate. +The Mooring object contains subcomponent objects that represent each component of the full mooring line. Line segments are Section objects, while connectors between segments and at the ends of the lines are Connector objects. These segments alternate, and are listed in the subcomponents section of the design dictionary in order from end A to end B. If there are parallel sections, such as in the case of a bridle, the parallel sections are described with nested lists. ## Mooring Properties - dd @@ -55,13 +60,15 @@ The Mooring object contains subcomponent objects that represent each component o - rA : end A absolute coordinates - rB : end B absolute coordinates - heading : compass heading from B to A +- ss : MoorPy subsystem representation of this Mooring, pristine +- ss_mod : modified MoorPy subsystem of thie Mooring, could have marine growth etc +- span : 2D (x-y) distance from fairlead to anchor or fairlead to fairlead. If bridles, the distance is calculated from the midpoint of all bridle fairlead points - adjuster : custom function that can adjust mooring - shared : int for anchored line (0), shared line (1) or half of a shared line (2) - symmetric : boolean for if the mooring line is symmetric shared line - rho : water density - g : acceleration due to gravity - envelopes : 2D motion envelopes, buffers, etc. -- loads : dictionary of loads on the mooring line - reliability : dictionary of reliability information on the line - cost : dictionary of line costs - failure_probability : dictionary of failure probabilities @@ -88,7 +95,7 @@ Set the position of an end of the mooring Finds the cost based on the MoorPy subsystem cost estimates ### updateTensions -Gets tensions from subsystem and updates the max tensions dictionary if it is larger than a previous tension +Gets tensions from subsystem and updates the max tensions dictionaries of each Section object if it is larger than a previous tension ### createSubsystem @@ -123,7 +130,7 @@ the following details: - volume - CdA -The connector class also contains an xyz location of the connector, and a connector object in MoorPy. +The connector class also contains an xyz location of the connector, and a connector object in MoorPy (mpConn). ## Connector methods @@ -135,7 +142,11 @@ Create a MoorPy connector object in a MoorPy system. Mass, volume, and CdA are a The Section class provides a data structure for the mooring line section material and length. The Section class inherits from dict and Edge. -The line material properties (linear mass, material, MBL, Cd, etc) are stored in the type dictionary of the Section class. +The line material properties (linear mass, material, MBL, Cd, etc) are stored in the type dictionary of the Section class. If a moorpy system is developed, the the line object representing this section is listed in the mpLine parameter. Loads are stored in the loads dictionary, and safety factors are stored in the safety_factors dictionary property. + +### Section methods +- makeMoorPyLine +Create a moorpy line object in a moorpy system [Back to Top](#moorings-sections-and-connectors) diff --git a/famodel/mooring/connector.py b/famodel/mooring/connector.py index 72620e98..5bf5eec6 100644 --- a/famodel/mooring/connector.py +++ b/famodel/mooring/connector.py @@ -38,14 +38,19 @@ def __init__(self,id, r=[0,0,0], **kwargs): # MoorPy Point Object for Connector self.mpConn = None + # dictionary of loads + self.loads = {} + # dictionary of failure probabilities self.failure_probability = {} # cost dictionary self.cost = {} - self.getProps() + self.getProps() + + def makeMoorPyConnector(self, ms): '''Create a MoorPy connector object in a MoorPy system Parameters @@ -57,10 +62,15 @@ def makeMoorPyConnector(self, ms): ------- ms : class instance MoorPy system - ''' + + if self.isJoined(): # if the connector is joined to something + pointType = 1 # make it a fixed point + else: + pointType = 0 # otherwise a free point + # create connector as a point in MoorPy system - ms.addPoint(0,self.r) + ms.addPoint(pointType, self.r) # assign this point as mpConn in the anchor class instance self.mpConn = ms.pointList[-1] @@ -71,11 +81,10 @@ def makeMoorPyConnector(self, ms): # set point type in ms self.getProps() - - return(ms) + def getProps(self): ''' Wrapper function to get moorpy point props dictionary @@ -94,8 +103,11 @@ def getProps(self): if self['CdA']>0: details['CdA'] = self['CdA'] if self.mpConn: + # add point type to system and assign to point entity if mp connector point exists pt = self.mpConn.sys.setPointType(design,**details) + self.mpConn.entity = pt else: + # otherwise, jsut get the point properties dict and return props = loadPointProps(None) pt = getPointProps(design, Props=props, **details) self.required_safety_factor = pt['FOS'] @@ -104,17 +116,26 @@ def getProps(self): return(pt) + def getCost(self,update=True, fx=0.0, fz=0.0, peak_tension=None, MBL=None): '''Get cost of the connector from MoorPy pointProps. Wrapper for moorpy's getCost_and_MBL helper function''' if update: - if self.mpConn: - # use pointProps to update cost - try: - self.getProps() - self.cost['materials'], MBL, info = self.mpConn.getCost_and_MBL(fx=fx, fz=fz, peak_tension=peak_tension) - except: - print('Warning: unable to find cost from MoorPy pointProps, cost dictionary not updated') + # use pointProps to update cost + try: + # get point properties from wrapper function + ptype = self.getProps() + if self.mpConn: + point = self.mpConn + else: + from moorpy import System + ms = System() # create a blank moorpy system + point = ms.addPoint(0, r=[0,0]) # add a dummy point + point.entity = ptype # get the point properties and assign to point entity + # calculate cost with any given forces/tensions + self.cost['materials'], MBL, info = point.getCost_and_MBL(fx=fx, fz=fz, peak_tension=peak_tension) + except: + print('Warning: unable to find cost from MoorPy pointProps, cost dictionary not updated') # if update == False, just return existing costs return sum(self.cost.values()) @@ -139,4 +160,40 @@ def __init__(self,id, **kwargs): # if the type dict wasn't provided, set as none to start with if not 'type' in self: - self['type'] = None \ No newline at end of file + self['type'] = None + + # MoorPy Line object for the section + self.mpLine = None + + # dictionary of loads on section + self.loads = {} + + # dictionary of safety factors + self.safety_factors = {} + + + def makeMoorPyLine(self, ms): + '''Create a MoorPy Line object in a MoorPy system. + If this section is attached to connectors that already have associated + MoorPy point objects, then those attachments will also be made in + MoorPy. + + Parameters + ---------- + ms : MoorPy System object + The MoorPy system to create the Line object in. + ''' + + # See if this section is attached to any already-created MoorPy Points + pointA = 0 + if self.attached_to[0]: # if an end A attachment + if self.attached_to[0].mpConn: # if it has a MoorPy point object + pointA = self.attached_to[0].mpConn.number # get its number + pointB = 0 + if self.attached_to[1]: + if self.attached_to[1].mpConn: + pointB = self.attached_to[1].mpConn.number + + # Create a Line for the section in MoorPy system + self.mpLine = ms.addLine(self['L'], self['type'], pointA=pointA, pointB=pointB) + diff --git a/famodel/mooring/mooring.py b/famodel/mooring/mooring.py index 7692d9bc..e90860f6 100644 --- a/famodel/mooring/mooring.py +++ b/famodel/mooring/mooring.py @@ -5,7 +5,8 @@ from moorpy.subsystem import Subsystem from moorpy import helpers from famodel.mooring.connector import Connector, Section -from famodel.famodel_base import Edge +from famodel.famodel_base import Edge, Node +from famodel.helpers import calc_midpoint class Mooring(Edge): ''' @@ -23,15 +24,19 @@ def __init__(self, dd=None, subsystem=None, anchor=None, dd: dictionary Design dictionary that contains all information on a mooring line needed to create a MoorPy subsystem Layout: { - sections: + subcomponents: # always starts and ends with connectors even if connector dict is blank { - 0 - { + 0 + { # connector + type: {m, v, CdA} + } + 1 + { # section type: { name, d_nom, material, d_vol, m, EA, EAd, EAd_Lm, MBL, cost, weight } - L + L # length in [m] } } connectors: @@ -42,17 +47,7 @@ def __init__(self, dd=None, subsystem=None, anchor=None, zAnchor z_fair rad_fair - EndPositions: - { - endA, endB - } } - Initialize an empty object for a mooring line. - Eventually this will fully set one up from ontology inputs. - - >>> This init method is a placeholder that currently may need - some additional manual setup of the mooring object after it is - called. <<< ''' Edge.__init__(self, id) # initialize Edge base class @@ -74,35 +69,80 @@ def __init__(self, dd=None, subsystem=None, anchor=None, if not 'z_fair' in self.dd: self.dd['z_fair'] = self.ss.z_fair - self.dd['sections'] = [] - self.dd['connectors'] = [] - for ls in self.ss.lineList: - self.dd['sections'].append({'type':ls.type,'L':ls.L}) + self.dd['subcomponents'] = [] + # find the starting point index for lp in self.ss.pointList: - self.dd['connectors'].append({'CdA':lp.CdA, 'm':lp.m, 'v':lp.v}) + if not any(lp.attachedEndB): + # this is the starting point at end A - add this point first + self.dd['subcomponents'].append({'CdA':lp.CdA, 'm':lp.m, 'v':lp.v}) + break + # now iterate through and add the line and point B + for s in range(len(self.ss.lineList)): + # find what entry in the attached list is for a line attached at end A + endA = [lp.attachedEndB[i] for i in lp.attachedEndB if i==0][0] + # save the line number attached to the point at its end A + line_num = lp.attached[endA] + # pull out the line section and save it to subcomponents + ls = self.ss.lineList[line_num-1] + self.dd['subcomponents'].append({'type':ls.type, 'L':ls.L}) + # go through the point list again and pull out the point attached to end B of the line + for lb in self.ss.pointList: + if line_num in lb.attached and lb != lp: + lp = lb + # save end B point to subcomponents + self.dd['subcomponents'].append({'CdA':lp.CdA, 'm':lp.m, 'v':lp.v}) + + + self.parallels = False # True if there are any parallel sections in the mooring # let's turn the dd into something that holds subdict objects of connectors and sections if self.dd: # >>> list of sections ? And optional of section types (e,g, chian, poly) # and dict of scaling props (would be used by linedesign) ? - self.n_sec = len(self.dd['sections']) - + # self.n_sec = len(self.dd['sections']) + self.i_con = [] self.i_sec = [] - # Turn what's in dd and turn it into Sections and Connectors - for i, con in enumerate(self.dd['connectors']): - if con and 'type' in con: - Cid = con['type']+str(i) - else: - Cid = None - self.addConnector(con, i, id=Cid, insert=False) + + # # Turn what's in dd and turn it into Sections and Connectors + # con_i = 0 + # for i, con in enumerate(self.dd['connectors']): + # if isinstance(self.dd['connectors'][i],list): + # for j,subcon in enumerate(self.dd['connectors'][i]): + # if isinstance(self.dd['connectors'][i][j],list): + # if con and 'type' in con: + # Cid = con['type']+str(con_i) + # else: + # Cid = None + # self.addConnector(con, con_i, id=Cid, insert=False) + # con_i += 1 + # else: + # if con and 'type' in con: + # Cid = con['type']+str(con_i) + # else: + # Cid = None + # self.addConnector(con, con_i, id=Cid, insert=False) + # con_i += 1 + # sub_i = 0 + # for i, sec in enumerate(self.dd['sections']): + # if isinstance(self.dd['sections'][i],list): + # for j,subcon in enumerate(self.dd['sections'][i]): + # if isinstance(self.dd['sections'][i][j],list): + # self.addSection(sec['L'],sec['type'],sub_i, insert=False) + # sub_i += 1 + # else: + # self.addSection(sec['L'],sec['type'],sub_i, insert=False) + # sub_i += 1 + + # convert subcomponents list into actual objects + self.convertSubcomponents(self.dd['subcomponents']) - for i, sec in enumerate(self.dd['sections']): - self.addSection(sec['L'],sec['type'],i, insert=False) + # connect subcomponents + self.addSubcomponents(self.dd['subcomponents']) - # connect subcomponents and update i_sec and i_conn lists - self.connectSubcomponents() + # point dd['subcomponents'] list to self.subcomponents + self.dd['subcomponents'] = self.subcomponents @@ -148,9 +188,13 @@ def update(self, dd=None): if not dd == None: # if dd passed in self.dd.update(dd) # move contents of dd into Mooring.dd - + self.convertSubcomponents(dd['subcomponents']) + self.addSubcomponents(self.dd['subcomponents']) + # Update section lengths and types - for i, sec in enumerate(dd['sections']): + for i in range(len(self.i_sec)): + sec = self.getSubcomponent(self.i_sec[i]) + if self.ss: self.ss.lineList[i].setL(sec[i]['L']) self.ss.lineTypes[i] = sec[i]['type'] @@ -161,8 +205,8 @@ def update(self, dd=None): def setSectionLength(self, L, i): '''Sets length of section, including in the subdsystem if there is one.''' - - self.dd['sections'][i]['L'] = L # set length in dd (which is also Section/subcomponent) + sec = self.getSubcomponent(self.i_sec[i]) + sec['L'] = L # set length in dd (which is also Section/subcomponent) if self.ss: # is Subsystem exists, adjust length there too self.ss.lineList[i].setL(L) @@ -179,9 +223,9 @@ def setSectionDiameter(self, d, i): def setSectionType(self, lineType, i): '''Sets lineType of section, including in the subdsystem if there is one.''' - + sec = self.getSubcomponent(self.i_sec[i]) # set type dict in dd (which is also Section/subcomponent) - self.dd['sections'][i]['type'] = lineType + sec['type'] = lineType if self.ss: # is Subsystem exists, adjust length there too self.ss.lineTypes[i] = lineType @@ -239,18 +283,22 @@ def reposition(self, r_center=None, heading=None, project=None, r_centerA = self.attached_to[0].r r_centerB = self.attached_to[1].r - # create fairlead radius list for end A and end B if needed - if not rad_fair: - rad_fair = [self.attached_to[x].rFair if (hasattr(self.attached_to[x],'rFair') and self.attached_to[x].rFair) else 0 for x in range(2)] - # create fairlead depth list for end A and end B if needed - if not z_fair: - z_fair = [self.attached_to[x].zFair if (hasattr(self.attached_to[x],'zFair') and self.attached_to[x].zFair) else 0 for x in range(2)] + # check if there are fairlead objects attached to end connectors + fairs = True if len(self.subcons_B[0].attachments)>1 else False + # if there is no fairlead object, use traditional method to determine new fairlead location and set it, otherwise end B should be set already + if not fairs: + # create fairlead radius list for end A and end B if needed + if not rad_fair: + rad_fair = [self.attached_to[x].rFair if (hasattr(self.attached_to[x],'rFair') and self.attached_to[x].rFair) else 0 for x in range(2)] + # create fairlead depth list for end A and end B if needed + if not z_fair: + z_fair = [self.attached_to[x].zFair if (hasattr(self.attached_to[x],'zFair') and self.attached_to[x].zFair) else 0 for x in range(2)] + + # Set the updated end B location + self.setEndPosition(np.hstack([r_centerB[:2] + rad_fair[1]*u, z_fair[1] + r_centerB[2]]), 'b') - # Set the updated end B location - self.setEndPosition(np.hstack([r_centerB[:2] + rad_fair[1]*u, z_fair[1] + r_centerB[2]]), 'b') # Run custom function to update the mooring design (and anchor position) - # this would also szie the anchor maybe? if self.adjuster and adjust: #if i_line is not defined, assumed segment 0 will be adjusted @@ -264,27 +312,31 @@ def reposition(self, r_center=None, heading=None, project=None, else: #move anchor based on set spacing then adjust line length - xy_loc = r_centerB[:2] + (self.span + rad_fair[1])*u + xy_loc = self.rB[:2] + self.span*u #r_centerB[:2] + (self.span + rad_fair[1])*u if project: self.dd['zAnchor'] = -project.getDepthAtLocation(xy_loc[0],xy_loc[1]) self.z_anch = self.dd['zAnchor'] else: print('Warning: depth of mooring line, anchor, and subsystem must be updated manually.') - self.setEndPosition(np.hstack([r_centerB[:2] + (self.span + rad_fair[1])*u, self.z_anch]), 'a', sink=True) + self.setEndPosition(np.hstack([self.rB[:2] + self.span*u, self.z_anch]), 'a', sink=True) self.adjuster(self, method = 'horizontal', r=r_centerB, project=project, target = self.target, i_line = self.i_line) - elif self.shared == 1: # set position of end A at platform end A - self.setEndPosition(np.hstack([r_centerA[:2] - rad_fair[0]*u, z_fair[0] + r_centerA[2]]),'a') + elif self.shared == 1: # set position of end A at platform end A if no fairlead objects + if not len(self.subcons_A[0].attachments) > 1: + self.setEndPosition(np.hstack([r_centerA[:2] - rad_fair[0]*u, z_fair[0] + r_centerA[2]]),'a') else: # otherwise just set the anchor position based on a set spacing (NEED TO UPDATE THE ANCHOR DEPTH AFTER!) - xy_loc = r_centerB[:2] + (self.span + rad_fair[1])*u + if not fairs: + xy_loc = self.rB[:2] + self.span*u #r_centerB[:2] + (self.span + rad_fair[1])*u + else: + xy_loc = calc_midpoint([sub.r[:2] for sub in self.subcons_B]) + self.span*u if project: self.dd['zAnchor'] = -project.getDepthAtLocation(xy_loc[0],xy_loc[1]) self.z_anch = self.dd['zAnchor'] else: print('Warning: depth of mooring line, anchor, and subsystem must be updated manually.') - self.setEndPosition(np.hstack([r_centerB[:2] + (self.span + rad_fair[1])*u, self.z_anch]), 'a', sink=True) + self.setEndPosition(np.hstack([xy_loc, self.z_anch]), 'a', sink=True) # Update the mooring profile given the repositioned ends if self.ss: @@ -300,6 +352,9 @@ def reposition(self, r_center=None, heading=None, project=None, att.r = iend if att.mpAnchor: att.mpAnchor.r = att.r + + # reposition the subcomponents + self.positionSubcomponents() @@ -391,24 +446,66 @@ def getCost(self,from_ss=True): # sum up the costs in the dictionary and return return sum(self.cost.values()) - def updateTensions(self): + def updateTensions(self, DAF=1): ''' Gets tensions from subsystem and updates the max tensions dictionary if it is larger than a previous tension ''' - if not 'TAmax' in self.loads: - self.loads['TAmax'] = 0 - if not 'TBmax' in self.loads: - self.loads['TBmax'] = 0 - # get anchor tensions - if abs(self.ss.TA) > self.loads['TAmax']: - self.loads['TAmax'] = deepcopy(self.ss.TA) - # get TB tensions - if abs(self.ss.TB) > self.loads['TBmax']: - self.loads['TBmax'] = deepcopy(self.ss.TB) + Ts = [] + Tc = [] + # get tensions for each section + for sec in self.sections(): + if not 'Tmax' in sec.loads: + sec.loads['Tmax'] = 0 + Tmax = max([abs(sec.mpLine.TA), abs(sec.mpLine.TB)]) + if Tmax*DAF > sec.loads['Tmax']: + sec.loads['Tmax'] = deepcopy(Tmax)*DAF + Ts.append(sec.loads['Tmax']) + for conn in self.connectors(): + if not 'Tmax' in conn.loads: + conn.loads['Tmax'] = 0 + Tmax = np.linalg.norm(conn.mpConn.getForces()) + if Tmax*DAF > conn.loads['Tmax']: + conn.loads['Tmax'] = deepcopy(Tmax)*DAF + Tc.append(conn.loads['Tmax']) - return(self.loads['TAmax'],self.loads['TBmax']) + return max(Ts) + def updateSafetyFactors(self,key='tension',load='Tmax', prop='MBL', + sections=True, connectors=True, info={}): + """Update safety factors for desired factor type, load type, and property + + Parameters + --------- + key: str/int, optional + safety_factor dictionary key. Default is 'tension'. + load: str, optional + key in loads dictionary. Default is 'Tmax' + prop: str, optional + key in line type dictionary. Default is 'MBL' + info: str, optional + information string to add to safety_factors dictionary + + Returns + ------- + Minimum safety factor for the given key across all sections in the mooring line + """ + + # get safety factors for each section + if sections: + for sec in self.sections(): + if prop in sec['type']: + sec.safety_factors[key] = sec['type'][prop]/sec.loads[load] + sec.safety_factors['info'] = info + if connectors: + for con in self.connectors(): + if 'type' in con and prop in con['type']: + con.safety_factors[key] = con['type'][prop]/con.loads[load] + sec.safety_factors['info'] = info + + + - def createSubsystem(self, case=0,pristine=True,dd=None, mooringSys=None): + + def createSubsystem(self, case=0, pristine=True, dd=None, ms=None): ''' Create a subsystem for a line configuration from the design dictionary Parameters @@ -422,67 +519,194 @@ def createSubsystem(self, case=0,pristine=True,dd=None, mooringSys=None): - 2: the assembly is suspended and assumed symmetric, end A is the midpoint dd : dict, optional Dictionary describing the design - mooringSys : MoorPy System, optional - MoorPy system this subsystem is a part of + ms : MoorPy System, optional + MoorPy system this subsystem is a part of. Necessary if ''' + # set design dictionary as self.dd if none given, same with connectorList if not dd: dd = self.dd - - ss=Subsystem(mooringSys=mooringSys, depth=-dd['zAnchor'], rho=self.rho, g=self.g, + + # get list of sections and connectors, send in dd in case it is not from self.dd + secs = self.sections(dd) + conns = self.connectors(dd) + + if self.parallels: # make parts of a MoorPy system + + if not ms: + raise Exception('A MoorPy system (ms) must be provided for a Mooring with parallel/bridle parts.') + # Make Points + for con in conns: + # >>> leah had some checks here that I didn't understand <<< + con.makeMoorPyConnector(ms) + + # Make Lines + for sec in secs: + sec.makeMoorPyLine(ms) # this also will connect the Lines to Points + + else: + ss=Subsystem(mooringSys=ms, depth=-dd['zAnchor'], rho=self.rho, g=self.g, span=dd['span'], rad_fair=self.rad_fair, z_fair=self.z_fair)#, bathymetry=dict(x=project.grid_x, y=project.grid_y, depth=project.grid_depth)) # don't necessarily need to import anymore - #ss.setSSBathymetry(project.grid_x, project.grid_y, project.grid_depth) - - - - - - lengths = [] - types = [] - # run through each line section and collect the length and type - for i, sec in enumerate(dd['sections']): - lengths.append(sec['L']) - # points to existing type dict in self.dd for now - types.append(sec['type']) # list of type names - #types.append(sec['type']['name']) # list of type names - #self.ss.lineTypes[i] = sec['type'] + + lengths = [] + types = [] + # run through each line section and collect the length and type + for sec in secs: + lengths.append(sec['L']) + types.append(sec['type']) # list of type names + + + # make the lines and set the points + ss.makeGeneric(lengths, types, + connectors=[conns[ic+1] for ic in range(len(conns)-2)], + suspended=case) + ss.setEndPosition(self.rA,endB=0) + ss.setEndPosition(self.rB,endB=1) + + for i,sec in enumerate(self.sections(dd)): + sec.mpLine = ss.lineList[i] + for i,con in enumerate(self.connectors(dd)): + con.mpConn = ss.pointList[i] + + # add in connector info to subsystem points + if case == 0: # has an anchor - need to ignore connection for first point because anchor is a point itself so can't have a point attached to a point + startNum = 1 + else: # no anchor - need to include all connections + startNum = 0 + for i in range(startNum,len(ss.pointList)): + conn = conns[i] + conn.mpConn = ss.pointList[i] + conn.mpConn.CdA = conns[i]['CdA'] + conn.getProps() + + # solve the system + ss.initialize() + ss.staticSolve() + + + # add to the parent mooring system if applicable + if ms: + ms.lineList.append(ss) + ss.number = len(ms.lineList) + + # save ss to the correct Mooring variable + if pristine: + # save to ss + self.ss = ss + return(self.ss) + else: + # save to modified ss (may have marine growth, corrosion, etc) + self.ss_mod = ss + return(self.ss_mod) + + + def positionSubcomponents(self): + '''Puts any subcomponent connectors/nodes along the mooring in + approximate positions relative to the endpoints based on the + section lengths.''' - # make the lines and set the points - ss.makeGeneric(lengths, types, - connectors=[dd['connectors'][ic+1] for ic in range(len(dd['connectors'])-2)], - suspended=case) - ss.setEndPosition(self.rA,endB=0) - ss.setEndPosition(self.rB,endB=1) - # note: next bit has similar code/function as Connector.makeMoorPyConnector <<< + # Tabulate the section lengths + L = [] + n_serial_nodes = 0 # number of serial nodes, including first and last + + # ----- First pass, going through each section in series ----- + + # Figure out lengths + for item in self.subcomponents: + + if isinstance(item, list): # indicates there are parallel sections here + pLtot = [] # total length of each parallel string + for j, parallel in enumerate(item): # go through each parallel string + if isinstance(parallel, list): # if it's a concatenation of multiple things + pLtot.append(0) + # go through each item along the parallel path + for subitem in parallel: + if isinstance(subitem, Edge): + pLtot[j] += subitem['L'] # add the L of each edge + else: + raise Exception("Unsupported situation ... parallel subitems must be lists") + + L.append(min(pLtot)) # save minimum parallel string length + + elif isinstance(item, Node): + n_serial_nodes += 1 + + elif isinstance(item, Edge): + L.append(item['L']) # save length of section - # add in connector info to subsystem points - if case == 0: # has an anchor - need to ignore connection for first point because anchor is a point itself so can't have a point attached to a point - startNum = 1 - else: # no anchor - need to include all connections - startNum = 0 + + # Position nodes along main serial string between rA and rB + Lsum = np.cumsum(np.array(L)) + j = 0 # index of node along serial string (at A is 0) - for i in range(startNum,len(ss.pointList)): - dd['connectors'][i].mpConn = ss.pointList[i] - dd['connectors'][i].mpConn.CdA = dd['connectors'][i]['CdA'] - dd['connectors'][i].getProps() + for i, item in enumerate(self.subcomponents): + if isinstance(item, list) or isinstance(item, Edge): + j = j+1 # note that we're moving a certain length along the string - # solve the system - ss.initialize() - ss.staticSolve() - - # save ss to the correct Mooring variable - if pristine: - # save to ss - self.ss = ss - return(self.ss) - else: - # save to modified ss (may have marine growth, corrosion, etc) - self.ss_mod = ss - return(self.ss_mod) + # if it's a node, but not the first or last one + elif isinstance(item, Node) and i > 0 and i < len(self.subcomponents)-1: + r = self.rA + (self.rB-self.rA)*Lsum[j-1]/Lsum[-1] + item.setPosition(r) + + + # ----- Second pass, to position any nodes that are along parallel sections ----- + for i, item in enumerate(self.subcomponents): + + if isinstance(item, list): # indicates there are parallel sections here + for j, parallel in enumerate(item): # go through each parallel string + + # --- go through each item along the parallel path --- + # Note: this part repeats some logic, could be replaced with recursive fn + L = [] + n_serial_nodes = 0 + + for subitem in parallel: + if isinstance(item, Node): + n_serial_nodes += 1 + + elif isinstance(subitem, Edge): + L.append(subitem['L']) # save length of section + + # --- Figure out the end points of this paralle string --- + + # if this parallel is on the first section, then it's a bridle at A + if i == 0: + if isinstance(parallel[0], Edge): # if first object is an Edge + rA = parallel[0].rA + else: + rA = parallel[0].r + else: + rA = self.subcomponents[i-1].r + + # if this parallel is on the last section, then it's a bridle at B + if i == len(self.subcomponents)-1: + if isinstance(parallel[-1], Edge): # if last object is an Edge + rB = parallel[-1].rB + else: + rB = parallel[-1].r + else: + rB = self.subcomponents[i+1].r + + # --- Do the positioning --- + Lsum = np.cumsum(np.array(L)) + + for subitem in parallel: + if isinstance(subitem, Edge): + j = j+1 # note that we're moving a certain length along the string + + # if it's a node, but not the first or last one + elif isinstance(item, Node): + if j > 0 and j < n_serial_nodes-1: + r = rA + (rB-rA)*Lsum[j]/Lsum[-1] + item.setPosition(r) + else: + print('end of parallel') + breakpoint() + def mirror(self,create_subsystem=True): ''' Mirrors a half design dictionary. Useful for symmetrical shared mooring lines where only half of the line is provided @@ -494,9 +718,11 @@ def mirror(self,create_subsystem=True): Default is True ''' # disconnect all sections and connectors - for i, sec in enumerate(self.dd['sections']): - self.dd['sections'][i].detachFrom(end='a') - self.dd['sections'][i].detachFrom(end='b') + # TODO: update to work with dd['subcomponents'] + for i in self.i_sec: + sec = self.getSubcomponent(i) + sec.detachFrom(end='a') + sec.detachFrom(end='b') # find out if the connector at end A (center of line) is empty if not self.dd['connectors'][0] or self.dd['connectors'][0]['m']==0: # do not double the middle section length @@ -626,7 +852,8 @@ def addMarineGrowth(self, mgDict, project=None, idx=None): # oldLine = project.mooringListPristine[idx] # else: # use current mooring object # oldLine = self - + if self.parallels: + raise Exception('addMarineGrowth not set up to work with parallels at this time') # create a reference subsystem if it doesn't already exist if not self.ss: self.createSubsystem() @@ -643,7 +870,7 @@ def addMarineGrowth(self, mgDict, project=None, idx=None): changeDepths = [] # index of list that has the corresponding changeDepth # set first connector - connList.append(self.dd['connectors'][0]) + connList.append(self.connectors()[0]) # go through each line section for i in range(0,len(oldLine.lineList)): slthick = [] # mg thicknesses for the section (if rA is above rB, needs to be flipped before being added to full subsystem list LThick) @@ -749,7 +976,7 @@ def addMarineGrowth(self, mgDict, project=None, idx=None): Lengths.append(ssLine.L) # add connector at end of section to list - connList.append(self.dd['connectors'][i+1]) + connList.append(self.connectors()[i+1]) # Set up list variables for pristine line info EA = [] @@ -871,8 +1098,11 @@ def addMarineGrowth(self, mgDict, project=None, idx=None): # fill out rest of new design dictionary nd1 = deepcopy(self.dd) - nd1['sections'] = nd - nd1['connectors'] = connList + nd1['subcomponents'] = [None]*(len(nd)*2+1) + for i in range(len(nd)): + nd1['subcomponents'][2*i] = Connector('C'+str(i),**connList[i]) + nd1['subcomponents'][2*i+1] = Section('S'+str(i),**nd[i]) + nd1['subcomponents'][2*i+2] = Connector('C'+str(i),**connList[i+1]) # call createSubsystem() to make moorpy subsystem with marine growth if self.shared: @@ -897,7 +1127,8 @@ def addCorrosion(self,corrosion_mm=10): None. ''' - for i,sec in enumerate(self.dd['sections']): + for i in self.i_sec: + sec = self.getSubcomponent[i] if sec['type']['material']=='chain': MBL_cor = sec['type']['MBL']*( (sec['type']['d_nom']-(corrosion_mm/1000))/sec['type']['d_nom'] )**2 # corroded MBL else: @@ -971,27 +1202,53 @@ def addSection(self, section_length, section_type, index, id=None, insert=True): Length of new section in [m] section_type : dict Dictionary of section properties - index : int + index : list New index of section in the mooring design dictionary sections list + List of length 1 or 3 depending on if part of a subsection or not id : str/int, optional Id of section ''' if not id: if insert: - for i,sec in enumerate(self.dd['sections']): - # update ids of sections - if i>=index and isinstance(sec, Section): - sec.id = 'Section'+str(i+1) - id='Section'+str(index) + for i in self.i_sec: + # update ids of subcomponents after this in series + # first check if the i_sec index i is the same level as index to add in + # and the final entry in i is greater than the index to add in + if len(i)==len(index) and i[-1]>index[-1]: + # check if all indices within the index list are less + # than or equal to the i_con index, i, list + if np.all([i[j]>=index[j] for j in range(len(i))]) and i[-1]>index[-1]: + sec = self.getSubcomponent(i) + sec.id = '_'.join(['S',*[str(j) for j in i]]) + # make the id start with S and add each component of the index separated by _ + id='_'.join(['S',*[str(j) for j in index]]) newsection_dd = {'type':section_type,'L':section_length} - newsection = Section(id,**newsection_dd) + # create section object + newsec = Section(id,**newsection_dd) if insert: - self.dd['sections'].insert(index, newsection) + if len(index)==1: + self.dd['subcomponents'].insert(index[0], + newsec) + elif len(index)==2: + self.dd['subcomponents'][index[0]][index[1]].insert(0, + newsec) + elif len(index)==3: + self.dd['subcomponents'][index[0]][index[1]].insert(index[2], + newsec) + else: + raise Exception('Length of index must be 1 or 3') else: - self.dd['sections'][index] = newsection + if len(index)==1: + self.dd['subcomponents'][index[0]] = newsec + elif len(index)==2: + self.dd['subcomponents'][index[0]][index[1]][0] = newsec + elif len(index)==3: + self.dd['subcomponents'][index[0]][index[1]][index[2]] = newsec + else: + raise Exception('Length of index must be 1 or 3') - return(newsection) + return(newsec) def addConnector(self, conn_dd, index, id=None, insert=True): ''' @@ -1002,7 +1259,7 @@ def addConnector(self, conn_dd, index, id=None, insert=True): conn_dd : dict Connector design dictionary index : int - New index of connector in the mooring design dictionary connectors list + New index of connector in the mooring design dictionary subcomponents list id : str or int, optional ID of new connector insert : bool, optional @@ -1016,47 +1273,179 @@ def addConnector(self, conn_dd, index, id=None, insert=True): ''' if not id: if insert: - for i,conn in enumerate(self.dd['connectors']): - # update ids of connectors - if i>=index and isinstance(conn, Connector): - conn.id = 'Conn'+str(i+1) - id = 'Conn'+str(index) + for i in self.i_con: + # update ids of subcomponents after this in series + # first check if the i_con index i is the same level as index to add in + # and the final entry in i is greater than the index to add in + if len(i)==len(index) and i[-1]>index[-1]: + # check if all indices within the index list are less + # than or equal to the i_con index, i, list + if np.all([i[j]>=index[j] for j in range(len(i))]): + conn = self.getSubcomponent(i) + conn.id = '_'.join(['C',*[str(j) for j in i]]) + # make the id start with C and add each component of the index separated by _ + id = '_'.join(['C',*[str(j) for j in index]]) + # create connector object newconn = Connector(id, **conn_dd) + # insert it in self.dd['subcompoents'] list or replace entry in list as needed if insert: - self.dd['connectors'].insert(index, newconn) + if len(index)==1: + self.dd['subcomponents'].insert(index[0], + newconn) + elif len(index)==2: + self.dd['subcomponents'][index[0]][index[1]][0].insert(0, + newconn) + elif len(index)==3: + self.dd['subcomponents'][index[0]][index[1]].insert(index[2], + newconn) + else: + raise Exception('Length of index must be 1 or 3') else: - self.dd['connectors'][index] = newconn + if len(index)==1: + self.dd['subcomponents'][index[0]] = newconn + elif len(index)==2: + self.dd['subcomponents'][index[0]][index[1]][0] = newconn + elif len(index)==3: + self.dd['subcomponents'][index[0]][index[1]][index[2]] = newconn + else: + raise Exception('Length of index must be 1 or 3') return(newconn) - def connectSubcomponents(self): + # def connectSubcomponents(self, subcons=None): - # first disconnect any current subcomponents - for ii in self.i_sec: - self.subcomponents[ii].detachFrom('A') - self.subcomponents[ii].detachFrom('B') + # # first disconnect any current subcomponents + # for ii in self.i_sec: + # self.subcomponents[ii].detachFrom('A') + # self.subcomponents[ii].detachFrom('B') - # detach end connectors from platforms/anchors just in case - if len(self.subcomponents)>0: - endattsA = [att['obj'] for att in self.subcomponents[0].attachments.values()] - endattsB = [att['obj'] for att in self.subcomponents[-1].attachments.values()] - for att in endattsA: - self.subcomponents[0].detach(att) - for att in endattsB: - self.subcomponents[-1].detach(att) + # # # detach end connectors from platforms/anchors just in case + # # if len(self.subcomponents)>0: + # # endattsA = [att['obj'] for att in self.subcomponents[0].attachments.values()] + # # endattsB = [att['obj'] for att in self.subcomponents[-1].attachments.values()] + # # for att in endattsA: + # # self.subcomponents[0].detach(att) + # # for att in endattsB: + # # self.subcomponents[-1].detach(att) - # Now connect the new set of subcomponents and store them in self(Edge).subcomponents! - subcons = [] # temporary list of node-edge-node... to pass to the function - for i in range(self.n_sec): - subcons.append(self.dd['connectors'][i]) - subcons.append(self.dd['sections'][i]) - subcons.append(self.dd['connectors'][-1]) - self.addSubcomponents(subcons) # Edge method to connect and store em - - # Indices of connectors and sections in self.subcomponents list - self.i_con = list(range(0, 2*self.n_sec+1, 2)) - self.i_sec = list(range(1, 2*self.n_sec+1, 2)) + # # Now connect the new set of subcomponents and store them in self(Edge).subcomponents! + # if subcons is None: + # subcons = [] # temporary list of node-edge-node... to pass to the function + # for i in range(self.n_sec): + # subcons.append(self.dd['connectors'][i]) + # subcons.append(self.dd['sections'][i]) + # subcons.append(self.dd['connectors'][-1]) + # self.addSubcomponents(subcons) # Edge method to connect and store em + + def convertSubcomponents(self, subs_list): + '''Create section and connector objects from the subcomponents dicts. + ''' + # go through each entry in subcomponents list + for i,sub in enumerate(subs_list): + # if this entry is a list, go through each entry in that + if isinstance(sub,list): + self.parallels = True # flag there is at least one parallel section + for j,subsub in enumerate(sub): + # if this is a list (3rd level), make sections and connectors from entries + if isinstance(subsub, list): + for k, subsubsub in enumerate(subsub): + if 'L' in subsubsub: + id = '_'.join(['S',*[str(l) for l in [i,j,k]]]) + # this is a section + subs_list[i][j][k] = self.addSection(subsubsub['L'], + subsubsub['type'], + [i,j,k], + id=id, + insert=False) + self.i_sec.append([i, j, k]) + else: + # this should be a connector (no length provided) + id = '_'.join(['C',*[str(l) for l in [i,j,k]]]) + subs_list[i][j][k] = self.addConnector(subsubsub, + [i,j,k], + id=id, + insert=False) + self.i_con.append([i, j, k]) + else: + raise Exception('subcomponent list entry must be length 1 or 3') + elif 'L' in sub: + # this is a section + id = 'S'+str(i) + subs_list[i] = self.addSection(sub['L'], + sub['type'], + [i], + id=id, + insert=False) + self.i_sec.append([i]) + else: + # this is a connector + id = 'C'+str(i) + subs_list[i] = self.addConnector(sub, [i], id=id, insert=False) + self.i_con.append([i]) + + def sections(self, dd=None): + ''' + returns list of sections in the mooring + ''' + secs = [] + # allow option to input dict of subcomponents and pull sections from that + if dd: + for sub in dd['subcomponents']: + if 'L' in sub: + secs.append(sub) + elif isinstance(sub, list): + for subsub in sub: + if isinstance(subsub, list): + for sss in subsub: + if 'L' in sss: + secs.append(sss) + elif 'L' in sss: + secs.append(subsub) + else: + for i in self.i_sec: + secs.append(self.getSubcomponent(i)) + + return secs + + def connectors(self, dd=None): + ''' + returns list of connectors in the mooring + ''' + conns = [] + # allow option to input dict of subcomponents and pull sections from that + if dd: + for sub in dd['subcomponents']: + if not 'L' in sub and isinstance(sub, dict): + conns.append(sub) + elif isinstance(sub, list): + for subsub in sub: + if isinstance(subsub, list): + for sss in subsub: + if not 'L' in sss and isinstance(sss, dict): + conns.append(sss) + elif not 'L' in sss and isinstance(sss, dict): + conns.append(subsub) + else: + for i in self.i_con: + conns.append(self.getSubcomponent(i)) + + return conns + # def convertSubcomponents(self,subs_list, level=0, index=[0]): + # ind = index + # for i,sub in enumerate(subs_list): + # if isinstance(sub, list): + # lvl = level+1 + # if len + # ind.append(0) + # self.convertSubcomponents(sub,level=lvl, index=ind) + # elif 'L' in sub: + # # this is a section + # id = '_'.join([str(j) for j in ind]) + # self.addSection(sub['L'], sub['type'], ind, id=id) + # ind[level] += 1 + # else: + # self.addConnector(sub, ind) + # ind[level] += 1 - \ No newline at end of file diff --git a/famodel/ontology/README.md b/famodel/ontology/README.md index 33de9515..e06b70a4 100644 --- a/famodel/ontology/README.md +++ b/famodel/ontology/README.md @@ -168,7 +168,6 @@ csv filename. TI: [ , , ] Shear: [ , , ] CS: [ , , ] - 8.5 9.8, 10.4, 11.8, 12.4, 13.7, 16.8, 18.1, 18.6, 19.8, 20.3, 21.4 WS_2_4 : # conditional values from wind speed range of 2 - 4 m/s @@ -307,7 +306,10 @@ Additionally, a list of mooring lines can be input in the line_data table with s If there is an anchor connected to this line, it must be listed in end A, not end B. All anchors listed in line_data end A must have a matching ID in the anchor_data table, and all FOWTs listed in line_data end A or end B must have a matching ID in the array_data table. The anchor and fowt IDs must all be unique. The mooring lines each have a mooring configuration ID which links to the [Mooring Line Configs](#mooring-line-configurations) section. -There is also an option to adjust the length of the line, depending on the spacing. + +The fairleadA and fairleadB keys are optional; if not provided, the moorings will attach to the platform at the platform's fairlead radius and depth based on the angle of the mooring line. If none is provided, the fairlead radius and depth of the platform(s) connected to the mooring line is used to determine the fairlead point. If you choose to specify fairlead points, they are represented with an integer value or list of integers in fairleadA and fairleadB respectively. This integer value maps to an index (starting at 1) in the list of fairleads within the platform definition of the associated platform type in [Platforms](#platforms). For lines with shared anchors, the fairleadA is listed as None. + +If a mooring line has multiple connection points to a platform, such as in the case of a bridle, a list of integers (starting at 1) is used to specify the fairlead connection points of each section. The order that the fairlead points are listed here corresponds to the order of parallel sections provided in the mooring configuration from the [Mooring Line Configs](#mooring-line-configurations) section. ```yaml array_mooring: @@ -318,11 +320,11 @@ array_mooring: - [ anch2, suction1, , , ] line_keys : - [MooringConfigID , end A, end B, lengthAdjust] + [MooringConfigID , endA, endB, fairleadA, fairleadB] line_data : - - [ semitaut-poly_1 , anch1, fowt1, 0] - - [ semitaut-poly_1 , anch2, fowt1, 0] - - [ semitaut-poly_2 , fowt1, f2, 0] + - [ semitaut-poly_1 , anch1, fowt1, None, 1] + - [ semitaut-poly_1 , anch2, fowt3, None, 2] + - [ semitaut-poly_bridle , fowt1, f2, 2, [3,4]] ``` ### Array Cables @@ -330,7 +332,7 @@ There are two locations to list cables, either or both may be used. Cables liste This section provides a straightforward and compact way to define the power cables in the array. The CableID refers to an entry in the [Top Level Cables](#top-level-cables) section. For each end (A and B) of the cable, it specifies the -platform (matching an ID in the [array table](#array-layout)) it is attached to, the dynamic cable attached at either end (matching an ID in the [Dynamic Cable Configurations](#dynamic-cable-configurations) section), and the heading of the cable at the attachment of each end, using headings relative to the heading of the platform or substation it is connected to, running clockwise. +platform (matching an ID in the [array table](#array-layout)) it is attached to, the dynamic cable attached at either end (matching an ID in the [Dynamic Cable Configurations](#dynamic-cable-configurations) section), the heading of the dynamic cable at the attachment of each end, using headings relative to the heading of the platform or substation it is connected to, running clockwise, and the index in the platform J-tube list (starting at 1) each end is attached to. The JtubeA and JtubeB keys in the table are optional to implement; if not provided it is assumed that the cable attaches to the platform at a j-tube radius specified in the dynamic cable configuration (if provided) or at the fairlead radius of the platform. The static cable type is listed under 'cableType', referencing either an entry in the [Cable Cross Sectional Properties](#cable-cross-sectional-properties) section or a cable type name in the FAModel CableProps yaml. Length adjustment information is also included. @@ -339,10 +341,10 @@ If a cable does not have a feature (for example, a suspended cable would not hav ```yaml array_cables: - keys: [ AttachA, AttachB, DynCableA, DynCableB, headingA, headingB, cableType, lengthAdjust] + keys: [ AttachA, AttachB, DynCableA, DynCableB, headingA, headingB, JtubeA, JtubeB, cableType ] data: - - [ f2, substation1, lazy_wave1, lazy_wave2, 270, 270, static_cable_66, 0 ] - - [ fowt1, f2, suspended_1, None, 90, 270, None, 0 ] + - [ f2, substation1, lazy_wave1, lazy_wave2, 270, 270, 1, 1, static_cable_66 ] + - [ fowt1, f2, suspended_1, None, 90, 270, 2, 3, None ] ``` ## Topside(s) @@ -389,8 +391,15 @@ by [WEIS](https://weis.readthedocs.io). This section defines the floating support structures used in the design. As in the previous section, it can contain a single platform or a list of platforms. By default, the format here follows that used by -[RAFT](https://openraft.readthedocs.io) input files, with the addition of 'rFair', 'zFair', and 'type' entries to the -dictionary for each platform in the first level of each platform listed. In this case, rFair is the fairlead radius, zFair is the fairlead depth with respect to the platform depth, and type describes the kind of platform (i.e. FOWT for a floating wind turbine, Substation, WEC). An optional input is z_location, which describes the nominal depth of the platform. If the z_location is not provided here or as a column in the array table, it is assumed to be 0. +[RAFT](https://openraft.readthedocs.io) input files, with the addition of 'type', and the optional entries of 'fairleads', 'Jtubes', 'rFair' and 'zFair' to the +dictionary for each platform in the first level of each platform listed. In this case, type describes the kind of platform (i.e. FOWT for a floating wind turbine, Substation, WEC). + +Optional entries include: + - *fairleads* : a list of dictionaries providing information on the relative fairlead locations of the platform. The exact relative positions can be listed for each fairlead, or a relative position and headings (relative to 0 platform heading) can be provided. If a list of fairlead headings is provided in one fairlead entry, the heading list indices are added to the list of fairlead indices referenced in mooring_systems or array_mooring. For the case below, the 30 degree heading would be index 1, 150 degree heading would be index 2, 270 degree heading would be index 3, and the 'fairleads2' entry (with relative position [-57.779,-5.055, -14]) would be index 4) + - *Jtubes*: a list of dictionaries providing information on the relative J-tube locations on the platform. Like fairleads, the exact relative position can be provided or a relative position and list of headings (relative to 0 platform heading) can also be provided. If a list of j-tube headings is provided in one j-tube entry, the heading list indices are added to the list of j-tube indices referenced in cables or array_cables (just like fairleads). +- *rFair*: fairlead radius. MUST be provided if fairleads and Jtubes not provided. +- *zFair*: fairlead depth with respect to the platform depth. MUST be provided if fairleads and Jtubes not provided. +- *z_location*: nominal depth of the platform. If the z_location is not provided here or as a column in the array table, it is assumed to be 0. However, support will be added for also linking to platform descriptions that follow the [WindIO](https://windio.readthedocs.io) ontology format, which is also used @@ -401,6 +410,17 @@ platform: potModMaster : 1 # [int] master switch for potMod variables; 0=keeps all member potMod vars the same, 1=turns all potMod vars to False (no HAMS), 2=turns all potMod vars to True (no strip) dlsMax : 5.0 # maximum node splitting section amount for platform members; can't be 0 + fairleads : + # list of fairlead coordinates for the platform relative to platform coordinate and 0-degree heading + - name: fairlead1 + r_rel: [58, 0, -14] # relative coordinates of fairlead to platform center + headings: [30, 150, 270] # headings in degrees for the fairlead (if multiple headings, the fairlead will be repeated for each heading) + - name: fairleads2 + r_rel: [-57.779,-5.055, -14] + Jtubes : # list of Jtube coordinates for the platform relative to platform coordinate and 0-degree heading + - name: Jtube1 + r_rel: [5, 0, -20] + headings: [90, 210, 330] # headings in degrees for the Jtube (if multiple headings, the Jtube will be repeated for each heading) rFair : 58 zFair : -15 type : FOWT # floating wind turbine platform @@ -441,29 +461,30 @@ anchor characteristics. ### Mooring Systems This section describes the mooring systems that could be used for individual turbines and repeated throughout the array. Each mooring system contains a -list of mooring lines, which contains the mooring configuration ID, the heading, the anchor type, and a possible length adjustment. The +list of mooring lines, which contains the mooring configuration ID, the heading, the anchor type, and optionally, fairlead connection index for end B. The mooring configuration ID links to the details about the segments lengths and types in the [mooring line configurations](#mooring-line-configurations) section. The heading refers to the angle of the mooring line and it rotates clockwise from North, relative to the heading of the platform. The anchor type links to details about the anchor -size and dimensions in the [anchor types section](#anchor-types). The length adjustment -is an optional parameter that can adjust the mooring line length for a shallower or deeper depth, for example. +size and dimensions in the [anchor types section](#anchor-types). The fairlead index +is an optional parameter that can specify the relative fairlead position point on a platform. +This index (which starts at 1) refers to the list of fairlead relative positions specified in the [Platforms](#platforms) section. If a list of indices is provided for a mooring line entry, this mooring line has a bridle and each entry in the list refers to a fairlead index in the platform definition. ```yaml mooring_systems: ms1: - name: 3-line taut polyester mooring system + name: 3-line taut polyester mooring system with 3rd line bridle - keys: [MooringConfigID, heading, anchorType, lengthAdjust] + keys: [MooringConfigID, heading, anchorType, fairlead] data: - - [ semitaut-poly_1, 30 , suction 1, 0 ] - - [ semitaut-poly_1, 150 , suction 1, 0 ] - - [ semitaut-poly_1, 270 , suction 1, 0 ] + - [ semitaut-poly_1, 30 , suction 1, 1 ] + - [ semitaut-poly_1, 150 , suction 1, 2 ] + - [ semitaut-poly_1, 270 , suction 1, [4,5] ] ``` ### Mooring Line Configurations The mooring line configurations lists the segment lengths and line types that make up each mooring line. Each line has a name that can then be specified as the MooringConfigID in the [mooring systems](#mooring-systems) section. The span is specified for each configuration, which represents the distance in the x-y plane between -the two connection points of the line - i.e. between fairlead and anchor, or for shared lines, fairlead and fairlead. +the two connection points of the line - i.e. between fairlead and anchor, or for shared lines, fairlead and fairlead. If there is a bridle, the fairlead position used to calculate span is the midpoint of the bridle fairlead locations. Fairlead radius and fairlead depth are specified in the [Platform](#platforms) section. Each line contains a list of sections that details the line section type and length. The line type name @@ -482,6 +503,9 @@ the middle line (last line given in the list) is doubled in length in the mirror For example, the 'rope_shared' config in the yaml below would produce a symmetric shared line with sections in the following order a 150 m section of rope, a clump weight, a 1172 m section of rope (note the doubled length), a clump weight, and finally a 150 m section of rope. +A section of line that has multiple parallel sections, such as a bridle or double chain section bounded by triplates, is specified with the 'subsections' key. Each list within the subsections key describes the sections of line connected in series for that individual line within the parallel. The following image visualizes the mooring configuration described in 'taut_bridle_double_chain': +![Mooring configuration with a bridle and double chain section](../images/parallel_sections.png) + ```yaml @@ -512,9 +536,40 @@ a 150 m section of rope, a clump weight, a 1172 m section of rope (note the doub - type: polyester_182mm # ID of a mooring line section type length: 199.8 # [m] length (unstretched) + taut_bridle_double_chain: # mooring line configuration identifier + + name: rope configuration 1 with a bridle # descriptive name + + span: 1131.37 + + + sections: #in order from anchor to fairlead + - type: chain_155mm + length: 20 + - type: rope # ID of a mooring line section type + length: 500 # [m] usntretched length of line section + - connectorType: triplate + - subsections: # double chain section + - - mooringFamily: chain + d_nom: 0.1 + length: 120 + - - mooringFamily: chain + d_nom: 0.1 + length: 120 + - connectorType: triplate + - type: rope # ID of a mooring line section type + length: 500 # [m] usntretched length of line section + - subsections: # bridle sections for end B + - - type: rope + length: 50 + - connectorType: shackle + - - type: rope + length: 50 + - connectorType: shackle + rope_shared: - name: shared rope + name: shared rope line shown symmetrically symmetric: True span: 1484 @@ -663,6 +718,8 @@ section, including joints, buoyancy module layout and other appendages. A top-level cable is defined as the full assembly of electrical connection equipment between two turbines or a turbine and a substation. 'type' links to the static cable property description, either in the [Cable Cross-Sectional Properties](#cable-cross-sectional-properties) section or in the cableProps_default yaml. Each cable end (A and B) is defined by the attached FOWT/substation/junction ID linking to the associated ID in the array table (attachID), the heading of the cable, and the dynamic cable configuration ID linking to a key in the [Dynamic Cable Configurations](#cable-configurations) section (dynamicID). +Jtube is an optional parameter defined for both endA and endB. If used, it refers to an index (starting at 1) in the Jtube list of the platform definition, which defines relative Jtube positions on a platform. If not provided, either the rJtube (optional) listed in the dynamic cable configuration, or the fairlead radius will be used to determine the platform connection point. + Routing can be added as an option, described in a list of coordinates for x,y, and radius values. Burial can also be included, with the station describing the normalized length along the cable, and depth describing the cable burial depth below the mudline. ```yaml @@ -674,11 +731,13 @@ Routing can be added as an option, described in a list of coordinates for x,y, a attachID: fowt1 # FOWT/substation/junction ID heading: 270 # [deg] heading of attachment at end A dynamicID: lazy_wave1 # ID of dynamic cable configuration at this end + Jtube: 1 endB: attachID: f2 # FOWT/substation/junction ID heading: 270 # [deg] heading of attachment at end B dynamicID: lazy_wave1 # ID of dynamic cable configuration at this end + Jtube: 2 routing_x_y_r: # optional vertex points along the cable route. Nonzero radius wraps around a point at that radius. - [-900, -1450, 20] @@ -700,6 +759,8 @@ and the volume of a single buoyancy module. The volume is only needed if the buo from the cableProps_defaul yaml. As with the cable properties, the 'type' in the sections list must refer to an entry in either the [Cable Appendages](#cable-appendages) section or in the FAModel cableProps_default.yaml. +rJtube is an optional parameter; if provided, and a Jtube relative position is not provided, it defines the radial distance of the connection point for the cable to the platform. + Similar to mooring lines, the span refers to the end to end distance of the line in the x-y plane. ```yaml @@ -713,6 +774,7 @@ dynamic_cable_configs: A: 300 cable_type: dynamic_cable_66_1 # ID of a cable section type length: 353.505 # [m] length (unstretched) + rJtube: 5 #[m] radial distance from platform center of J-tube sections: - type: buoyancy_module_1 diff --git a/famodel/platform/fairlead.py b/famodel/platform/fairlead.py new file mode 100644 index 00000000..2861a500 --- /dev/null +++ b/famodel/platform/fairlead.py @@ -0,0 +1,12 @@ +from famodel.famodel_base import Node + +class Fairlead(Node): + + def __init__(self,id): + + # Initialize as a node + Node.__init__(self,id) + + + + \ No newline at end of file diff --git a/famodel/platform/platform.py b/famodel/platform/platform.py index 157c9679..c4ae0739 100644 --- a/famodel/platform/platform.py +++ b/famodel/platform/platform.py @@ -9,6 +9,7 @@ from famodel.cables.cable import Cable from famodel.anchors.anchor import Anchor from famodel.cables.cable import DynamicCable +from famodel.famodel_base import Node, Edge class Platform(Node): ''' @@ -61,6 +62,7 @@ def __init__(self, id, r=[0,0,0], heading=0, mooring_headings=[60,180,300],rFair self.failure_probability = {} self.raftResults = {} + def setPosition(self, r, heading=None, degrees=False,project=None): ''' Set the position/orientation of the platform as well as the associated @@ -74,31 +76,20 @@ def setPosition(self, r, heading=None, degrees=False,project=None): x and y coordinates to position the node at [m]. heading, float (optional) The heading of the platform [deg or rad] depending on - degrees parameter (True or False). + degrees parameter (True or False) in compass direction ''' - ''' - # Future approach could be + # first call the Node method to take care of the platform and what's directly attached - Node.setPosition(self, r, heading=heading) - # then also adjust the anchor points - ''' - - # Store updated position and orientation - for i,ri in enumerate(r): - self.r[i] = np.array(ri) - - if not heading == None: - if degrees: + if heading: # save compass heading in radians + if degrees == True: self.phi = np.radians(heading) else: self.phi = heading - - # correction for xy coords - corr = np.radians(90) - - # Get 2D rotation matrix - self.R = np.array([[np.cos(corr-self.phi), -np.sin(corr-self.phi)],[np.sin(corr-self.phi), np.cos(corr-self.phi)]]) + # send in cartesian heading to node.setPosition (+ rotations CCW here) + Node.setPosition(self, r, theta=-self.phi) + # then also adjust the anchor points + # Update the position of any Moorings count = 0 # mooring counter (there are some attachments that aren't moorings) @@ -117,11 +108,12 @@ def setPosition(self, r, heading=None, degrees=False,project=None): cab = self.attachments[att]['obj'] - # update headings stored in subcomponents - headings = [cab.subcomponents[0].headingA + self.phi, cab.subcomponents[-1].headingB + self.phi] + # update heading stored in subcomponent for attached end + # pf_phis = [cab.attached_to[0].phi, cab.attached_to[1].phi] + # headings = [cab.subcomponents[0].headingA + pf_phis[0], cab.subcomponents[-1].headingB + pf_phis[1]] # reposition the cable - cab.reposition(headings=headings,project=project) + cab.reposition(project=project) def mooringSystem(self,rotateBool=0,mList=None,bodyInfo=None, project=None): @@ -147,23 +139,20 @@ def mooringSystem(self,rotateBool=0,mList=None,bodyInfo=None, project=None): # check if the subsystems were passed in from the function call if not mList: - mList = [] - for i in self.attachments: - if isinstance(self.attachments[i]['obj'],Mooring): - mList.append(self.attachments[i]['obj']) + mList = [moor for moor in self.getMoorings().values()] if project and len(project.grid_depth) > 1: - # calculate the maximum anchor spacing - anchor_spacings = [np.linalg.norm(mooring.rA[0:2] - self.r[:2]) for mooring in mList] - # get the bathymetry range that is related to this platform - margin = 1.2 - small_grid_x = np.linspace((self.r[0] - np.max(anchor_spacings)*margin), (self.r[0] + np.max(anchor_spacings)*margin), 10) - small_grid_y = np.linspace((self.r[1] - np.max(anchor_spacings)*margin), (self.r[1] + np.max(anchor_spacings)*margin), 10) - # interpolate the global bathymetry - small_grid_depths = np.zeros([len(small_grid_y), len(small_grid_x)]) - for i,x in enumerate(small_grid_x): - for j,y in enumerate(small_grid_y): - small_grid_depths[j,i] = project.getDepthAtLocation(x, y) + # # calculate the maximum anchor spacing + # anchor_spacings = [np.linalg.norm(mooring.rA[0:2] - self.r[:2]) for mooring in mList] + # # get the bathymetry range that is related to this platform + # margin = 1.2 + # small_grid_x = np.linspace((self.r[0] - np.max(anchor_spacings)*margin), (self.r[0] + np.max(anchor_spacings)*margin), 10) + # small_grid_y = np.linspace((self.r[1] - np.max(anchor_spacings)*margin), (self.r[1] + np.max(anchor_spacings)*margin), 10) + # # interpolate the global bathymetry + # small_grid_depths = np.zeros([len(small_grid_y), len(small_grid_x)]) + # for i,x in enumerate(small_grid_x): + # for j,y in enumerate(small_grid_y): + # small_grid_depths[j,i] = project.getDepthAtLocation(x, y) #self.ms = mp.System(bathymetry=dict(x=small_grid_x, y=small_grid_y, depth=small_grid_depths)) self.ms = mp.System(bathymetry=dict(x=project.grid_x, y=project.grid_y, depth=project.grid_depth)) @@ -176,47 +165,113 @@ def mooringSystem(self,rotateBool=0,mList=None,bodyInfo=None, project=None): r6 = [self.r[0],self.r[1],self.r[2],0,0,0] # create body if bodyInfo: - self.ms.addBody(0,r6,m=bodyInfo['m'],v=bodyInfo['v'],rCG=np.array(bodyInfo['rCG']),rM=np.array(bodyInfo['rM']),AWP=bodyInfo['AWP']) + body = self.ms.addBody(0,r6,m=bodyInfo['m'],v=bodyInfo['v'],rCG=np.array(bodyInfo['rCG']),rM=np.array(bodyInfo['rM']),AWP=bodyInfo['AWP']) else: - self.ms.addBody(0,r6,m=19911423.956678286,rCG=np.array([ 1.49820657e-15, 1.49820657e-15, -2.54122031e+00]),v=19480.104108645974,rM=np.array([2.24104273e-15, 1.49402849e-15, 1.19971829e+01]),AWP=446.69520543229874) + body = self.ms.addBody(0,r6,m=19911423.956678286,rCG=np.array([ 1.49820657e-15, 1.49820657e-15, -2.54122031e+00]),v=19480.104108645974,rM=np.array([2.24104273e-15, 1.49402849e-15, 1.19971829e+01]),AWP=446.69520543229874) if rotateBool: # rotation self.setPosition(self.r) # make mooring system from subsystems - for i,attID in enumerate(self.attachments): - - # only process moorings that have subsystems for now + for i,mooring in enumerate(mList): - if type(self.attachments[attID]['obj']) == Mooring: - mooring = self.attachments[attID]['obj'] - if mooring.ss: - ssloc = mooring.ss - else: - ssloc = mooring.createSubsystem() + if mooring.ss and not mooring.parallels: + ssloc = mooring.ss + self.ms.lineList.append(ssloc) + else: + ssloc = mooring.createSubsystem(ms=self.ms) + + if ssloc: # only proceed it's not None + ''' + # add subsystem as a line to the linelist + self.ms.lineList.append(ssloc) + ssloc.number = i+1 + ''' + for j,att in enumerate(mooring.attached_to): + if isinstance(att,Anchor): + # check whether a moorpy anchor object exists for this mooring line + # if not att.mpAnchor: + # create anchor moorpy object + att.makeMoorPyAnchor(self.ms) + if mooring.parallels: + subcom = mooring.subcomponents[j] # check what's on the end of the mooring + + if isinstance(subcom, list): # bridle case + print('This case not implemented yet') + breakpoint() + elif isinstance(subcom, Node): + # TODO: get rel dist from connector to anchor + # for now, just assume 0 rel dist until anchor lug objects introduced + r_rel = [0,0,0] + # attach anchor body to subcom connector point + subcom.mpConn.type = 1 + att.mpAnchor.attachPoint(subcom.mpConn.number,r_rel) + else: + # need to create "dummy" point to connect to anchor body + point = self.ms.addPoint(1,att.r) + # attach dummy point to anchor body + att.mpAnchor.attachPoint(point.number,[0,0,0]) + # now attach dummy point to line + point.attachLine(ssloc.number, j) + + elif isinstance(att,Platform): + # attach rB point to platform + if mooring.parallels: # case with paralles/bridles + + # Look at end B object(s) + subcom = mooring.subcomponents[-1] + + if isinstance(subcom, list): # bridle case + for parallel in subcom: + subcom2 = parallel[-1] # end subcomponent of the parallel path + + # Code repetition for the moment: + if isinstance(subcom2, Edge): + r = subcom2.attached_to[1].r # approximate end point...? + point = self.ms.addPoint(1, r) + body.attachPoint(point.number, r-att.r) + point.attachLine(subcom2.mpLine.number, 1) # attach the subcomponent's line object end B + + elif isinstance(subcom2, Node): + r = subcom2.r # approximate end point...? + subcom2.mpConn.type = 1 + pnum = subcom2.mpConn.number + body.attachPoint(pnum, r-att.r) + + elif isinstance(subcom, Edge): + r = subcom.attached_to[1].r # approximate end point...? + point = self.ms.addPoint(1, r) + body.attachPoint(point.number, r-att.r) + point.attachLine(subcom.mpLine.number, 1) # attach the subcomponent's line object end B + + elif isinstance(subcom, Node): + r = subcom.r # approximate end point...? + subcom.mpConn.type = 1 + pnum = subcom.mpConn.number + body.attachPoint(pnum, r-att.r) + # (the section line object(s) should already be attached to this point) + + + else: # normal serial/subsystem case + # add fairlead point + point = self.ms.addPoint(1,ssloc.rB) + # add connector info for fairlead point + # >>> MH: these next few lines might result in double counting <<< + point.m = ssloc.pointList[-1].m + point.v = ssloc.pointList[-1].v + point.CdA = ssloc.pointList[-1].CdA + # attach the line to point + point.attachLine(ssloc.number,j) + body.attachPoint(point.number, ssloc.rB-att.r) # attach to fairlead (need to subtract out location of platform from point for subsystem integration to work correctly) - if ssloc: # only proceed it's not None - # add subsystem as a line to the linelist - self.ms.lineList.append(ssloc) - ssloc.number = i+1 - for att in mooring.attached_to: - if isinstance(att,Anchor): - # check whether a moorpy anchor object exists for this mooring line - # if not att.mpAnchor: - # create anchor moorpy object - att.makeMoorPyAnchor(self.ms) - # else: - # # add anchor point from anchor class and fairlead point adjusted to include location offsets, attach subsystem - # self.ms.pointList.append(att.mpAnchor) # anchor - # attach subsystem line to the anchor point - self.ms.pointList[-1].attachLine(i,0) - # add fairlead point as a coupled point - self.ms.addPoint(1,ssloc.rB) - # attach subsystem line to the fairlead point - self.ms.pointList[-1].attachLine(i,1) - # attach fairlead point to body - self.ms.bodyList[0].attachPoint(len(self.ms.pointList),self.ms.pointList[-1].r-np.append(self.r[:2], [0])) + + # # add fairlead point as a coupled point + # self.ms.addPoint(1,ssloc.rB) + # # attach subsystem line to the fairlead point + # self.ms.pointList[-1].attachLine(i,1) + # # attach fairlead point to body + # self.ms.bodyList[0].attachPoint(len(self.ms.pointList),self.ms.pointList[-1].r-np.append(self.r[:2], [0])) # initialize and plot self.ms.initialize() self.ms.solveEquilibrium() @@ -226,7 +281,8 @@ def mooringSystem(self,rotateBool=0,mList=None,bodyInfo=None, project=None): def getWatchCircle(self, plot=0, ang_spacing=45, RNAheight=150, - shapes=True,Fth=None,SFs=True,ms=None): + shapes=True,Fth=None,SFs=True,ms=None, DAF=1, + moor_seabed_disturbance=False): ''' Compute watch circle of platform, as well as mooring and cable tension safety factors and cable sag safety factors based on rated thrust. @@ -269,6 +325,7 @@ def getWatchCircle(self, plot=0, ang_spacing=45, RNAheight=150, moorings = [] # list of mooring lines attached cables = [] # list of cables attached dcs = [] + lBots = [0]*len(self.mooring_headings) # find turbines, cables, and mooorings attached to platform moorings = self.getMoorings().values() @@ -320,27 +377,36 @@ def getWatchCircle(self, plot=0, ang_spacing=45, RNAheight=150, ms.solveEquilibrium3(DOFtype='both') # equilibrate (specify 'both' to enable both free and coupled DOFs) if SFs: + # get loads on anchors (may be shared) + for j,anch in enumerate(anchors): + F2 = anch.mpAnchor.getForces()*DAF # add up all forces on anchor body + H = np.hypot(F2[0],F2[1]) # horizontal force + T = np.sqrt(F2[0]**2+F2[1]**2+F2[2]**2) # total tension force + if F[j] is None or T>np.sqrt(F[j][0]**2+F[j][1]**2+F[j][2]**2): + F[j] = F2 # max load on anchor + # save anchor load information + anch.loads['Hm'] = H + anch.loads['Vm'] = F[j][2] + anch.loads['thetam'] = np.degrees(np.arctan(anch.loads['Vm']/anch.loads['Hm'])) #[deg] + anch.loads['mudline_load_type'] = 'max' + anch.loads['info'] = f'determined from arrayWatchCircle() with DAF of {DAF}' # get tensions on mooring line for j,moor in enumerate(moorings): - MBLA = float(moor.ss.lineList[0].type['MBL']) - MBLB = float(moor.ss.lineList[-1].type['MBL']) - # print(MBLA,MBLB,moor.ss.TA,moor.ss.TB,MBLA/moor.ss.TA,MBLB/moor.ss.TB,abs(MBLA/moor.ss.TA),abs(MBLB/moor.ss.TB)) - MTSF = min([abs(MBLA/moor.ss.TA),abs(MBLB/moor.ss.TB)]) - # atenMax[j], btenMax[j] = moor.updateTensions() - if not minTenSF[j] or minTenSF[j]>MTSF: - minTenSF[j] = deepcopy(MTSF) - if not moor.shared: - if self.attachments[moor.id]['end'] == 'a': - # anchor attached to end B - F[j] = moor.ss.fB - else: - F[j] = moor.ss.fA + lBot = 0 + moor.updateTensions(DAF=DAF) + info = {'analysisType': 'quasi-static (MoorPy)', + 'info': f'determined from platform.getWatchCircle() with DAF of {DAF}'} + moor.updateSafetyFactors(info=info) + if moor_seabed_disturbance: + for sec in moor.sections(): + lBot += sec.mpLine.LBot + lBots[j] = max(lBots[j], lBot) # get tensions, sag, and curvature on cable for j,cab in enumerate(dcs): MBLA = cab.ss.lineList[0].type['MBL'] MBLB = cab.ss.lineList[-1].type['MBL'] - CMTSF = min([abs(MBLA/cab.ss.TA),abs(MBLB/cab.ss.TB)]) + CMTSF = min([abs(MBLA/(cab.ss.TA*DAF)),abs(MBLB/(cab.ss.TB*DAF))]) if not CminTenSF[j] or CminTenSF[j]>CMTSF: CminTenSF[j] = deepcopy(CMTSF) # CatenMax[j], CbtenMax[j] = cab.updateTensions() @@ -380,16 +446,7 @@ def getWatchCircle(self, plot=0, ang_spacing=45, RNAheight=150, ms.solveEquilibrium3(DOFtype='both') - if SFs: - # save anchor loads - for j,moor in enumerate(moorings): - for att3 in moor.attached_to: - if isinstance(att3,Anchor): - att3.loads['Hm'] = np.sqrt(F[j][0]**2+F[j][1]**2) - att3.loads['Vm'] = F[j][2] - att3.loads['thetam'] = np.degrees(np.arctan(att3.loads['Vm']/att3.loads['Hm'])) #[deg] - att3.loads['mudline_load_type'] = 'max' - + if SFs: maxVals = {'minTenSF':minTenSF,'minTenSF_cable':CminTenSF,'minCurvSF':minCurvSF,'minSag':minSag,'maxF':F}# np.vstack((minTenSF,CminTenSF,minCurvSF,minSag)) return(x,y,maxVals) else: diff --git a/famodel/project.py b/famodel/project.py index f73a5e1c..ad6b1dc7 100644 --- a/famodel/project.py +++ b/famodel/project.py @@ -21,21 +21,23 @@ from famodel.mooring.mooring import Mooring from famodel.platform.platform import Platform from famodel.anchors.anchor import Anchor -from famodel.mooring.connector import Connector +from famodel.mooring.connector import Connector, Section from famodel.substation.substation import Substation from famodel.cables.cable import Cable from famodel.cables.dynamic_cable import DynamicCable from famodel.cables.static_cable import StaticCable from famodel.cables.cable_properties import getCableProps, getBuoyProps, loadCableProps,loadBuoyProps -from famodel.cables.components import Joint +from famodel.cables.components import Joint, Jtube +from famodel.platform.fairlead import Fairlead from famodel.turbine.turbine import Turbine -from famodel.famodel_base import Node +from famodel.famodel_base import Node, Edge, rotationMatrix # Import select required helper functions from famodel.helpers import (check_headings, head_adjust, getCableDD, getDynamicCables, getMoorings, getAnchors, getFromDict, cleanDataTypes, getStaticCables, getCableDesign, m2nm, loadYAML, - configureAdjuster, route_around_anchors) + configureAdjuster, route_around_anchors, attachFairleads, + calc_heading, calc_midpoint) class Project(): @@ -105,8 +107,8 @@ def __init__(self, lon=0, lat=0, file=None, depth=202,raft=1): # Seabed grid self.grid_x = np.array([0]) # coordinates of x grid lines [m] self.grid_y = np.array([0]) # coordinates of y grid lines [m] - self.grid_depth = np.array([[depth]]) # depth at each grid point [iy, ix] self.depth = depth + self.grid_depth = np.array([[self.depth]]) # depth at each grid point [iy, ix] self.seabed_type = 'clay' # switch of which soil property set to use ('clay', 'sand', or 'rock') @@ -152,13 +154,18 @@ def load(self, info, raft=True): # if not project: # raise Exception(f'File {file} does not exist or cannot be read. Please check filename.') project = loadYAML(info) + + # save directory of main yaml for use when reading linked files + dir = os.path.dirname(os.path.abspath(info)) + else: project = info + dir = '' # look for site section # call load site method if 'site' in project: - self.loadSite(project['site']) + self.loadSite(project['site'], dir=dir) # look for design section # call load design method @@ -377,11 +384,23 @@ def loadDesign(self, d, raft=True): self.turbineTypes = turbines # ----- set up dictionary for each individual mooring line, create anchor, mooring, and platform classes ---- - # make platforms first if they exist, as there may be no moorings called out + + # check that all necessary sections of design dictionary exist if arrayInfo: - for i in range(len(arrayInfo)): + + mct = 0 # counter for number of mooring lines + import string + alph = list(string.ascii_lowercase) + jtube_by_platform = {} + fairlead_by_platform = {} # dict of platform ids as keys and fairlead objects list as values + + + for i in range(0, len(arrayInfo)): # loop through each platform in array + + + # get index of platform from array table pfID = int(arrayInfo[i]['platformID']-1) - # create platform instance (even if it only has shared moorings / anchors), store under name of ID for that row + # - - - create platform instance (even if it only has shared moorings / anchors), store under name of ID for that row if 'z_location' in arrayInfo[i]: r = [arrayInfo[i]['x_location'],arrayInfo[i]['y_location'],arrayInfo[i]['z_location']] @@ -396,25 +415,66 @@ def loadDesign(self, d, raft=True): hydrostatics = {} # add platform - self.addPlatform(r=r, id=arrayInfo[i]['ID'], phi=arrayInfo[i]['heading_adjust'], + platform = self.addPlatform(r=r, id=arrayInfo[i]['ID'], phi=arrayInfo[i]['heading_adjust'], entity=platforms[pfID]['type'], rFair=platforms[pfID].get('rFair',0), zFair=platforms[pfID].get('zFair',0),platform_type=pfID, hydrostatics=hydrostatics) - - # check that all necessary sections of design dictionary exist - if arrayInfo and lineConfigs: - - mct = 0 # counter for number of mooring lines - # set up a list of the alphabet for assigning names purposes - import string - alph = list(string.ascii_lowercase) - - for i in range(0, len(arrayInfo)): # loop through each platform in array - - # get index of platform from array table - pfID = int(arrayInfo[i]['platformID']-1) - # get platform object - platform = self.platformList[arrayInfo[i]['ID']] + + # add fairleads + pf_fairs = [] + fct = 1 # start at 1 because using indices starting at 1 in ontology + if 'fairleads' in platforms[pfID]: + for fl in platforms[pfID]['fairleads']: + # if headings provided, adjust r_rel with headings + if 'headings' in fl: + + + for head in fl['headings']: + # get rotation matrix of heading + R = rotationMatrix(0,0,np.radians(90-head)) + # apply to unrotated r_rel + r_rel = np.matmul(R, fl['r_rel']) + # r_rel = [fl['r_rel'][0]*np.cos(np.radians(90-head)), + # fl['r_rel'][1]*np.sin(np.radians(90-head)), + # fl['r_rel'][2]] + pf_fairs.append(self.addFairlead(id=platform.id+'_F'+str(fct), + platform=platform, + r_rel=r_rel)) + fct += 1 + # otherwise, just use r_rel as-is + elif 'r_rel' in fl: + pf_fairs.append(self.addFairlead(id=platform.id+'_F'+str(fct), + platform=platform, + r_rel=fl['r_rel'])) + fct += 1 + + fairlead_by_platform[platform.id] = pf_fairs + + # add J-tubes + pf_jtubes = [] + jct = 1 + if 'Jtubes' in platforms[pfID]: + for jt in platforms[pfID]['Jtubes']: + if 'headings' in jt: + for head in jt['headings']: + # get rotation matrix of heading + R = rotationMatrix(0,0,np.radians(90-head)) + # apply to unrotated r_rel + r_rel = np.matmul(R, jt['r_rel']) + pf_jtubes.append(self.addJtube(id=platform.id+'_J'+str(jct), + platform=platform, + r_rel=r_rel)) + jct += 1 + elif 'r_rel' in jt: + pf_jtubes.append(self.addJtube(id=platform.id+'_J'+str(jct), + platform=platform, + r_rel=jt['r_rel'])) + jct += 1 + jtube_by_platform[platform.id] = pf_jtubes + + + # # get platform object + # platform = self.platformList[arrayInfo[i]['ID']] # remove pre-set headings (need to append to this list so list should start off empty) platform.mooring_headings = [] @@ -445,7 +505,7 @@ def loadDesign(self, d, raft=True): node.dd = topside_dd platform.attach(node) - if mSystems and not arrayInfo[i]['mooringID'] == 0: #if not fully shared mooring on this platform + if lineConfigs and mSystems and not arrayInfo[i]['mooringID'] == 0: #if not fully shared mooring on this platform m_s = arrayInfo[i]['mooringID'] # get mooring system ID # # sort the mooring lines in the mooring system by heading from 0 (North) mySys = [dict(zip(d['mooring_systems'][m_s]['keys'], row)) for row in d['mooring_systems'][m_s]['data']] @@ -472,27 +532,43 @@ def loadDesign(self, d, raft=True): lineconfig = mySys[j]['MooringConfigID'] # create mooring and connector dictionary - m_config = getMoorings(lineconfig, lineConfigs, connectorTypes, arrayInfo[i]['ID'], self) + mdd = getMoorings(lineconfig, lineConfigs, connectorTypes, arrayInfo[i]['ID'], self) # create mooring object, attach ends, reposition moor = self.addMooring(id=name, heading=headings[j]+platform.phi, - dd=m_config, reposition=False) + dd=mdd, + reposition=False) anch = self.addAnchor(id=name, dd=ad, mass=mass) - + # attach ends moor.attachTo(anch, end='A') - moor.attachTo(platform, end='B') + if 'fairlead' in mySys[j]: + attachFairleads(moor, + 1, + platform, + fair_ID_start=platform.id+'_F', + fair_inds=mySys[j]['fairlead']) + + elif pf_fairs: + attachFairleads(moor, + 1, + platform, + fair_ID = pf_fairs[j].id) + + else: + moor.attachTo(platform, r_rel=[platform.rFair,0,platform.zFair], end='b') - # reposition mooring - moor.reposition(r_center=platform.r, heading=headings[j]+platform.phi, project=self) - # update anchor depth and soils - self.updateAnchor(anch=anch) - + # Position the subcomponents along the Mooring + moor.positionSubcomponents() + # update counter mct += 1 + + # update position of platform, moorings, anchors + platform.setPosition(r=platform.r, project=self) # ----- set up dictionary for each shared mooring line or shared anchor, create mooring and anchor classes ---- @@ -502,63 +578,81 @@ def loadDesign(self, d, raft=True): # get mooring line info for all lines for j in range(0, len(arrayMooring)): # run through each line - PFNum = [] # platform ID(s) connected to the mooring line + PF = [] # platforms connected to the mooring line # Error check for putting an anchor (or something else) at end B if not any(ids['ID'] == arrayMooring[j]['endB'] for ids in arrayInfo): raise Exception("Input for end B must match an ID from the array table.") if any(ids['ID'] == arrayMooring[j]['endB'] for ids in arrayAnchor): raise Exception(f"input for end B of line_data table row '{j}' in array_mooring must be an ID for a FOWT from the array table. Any anchors should be listed as end A.") + # Make sure no anchor IDs in arrayAnchor table are the same as IDs in array table for k in range(0,len(arrayInfo)): if any(ids['ID'] == arrayInfo[k] for ids in arrayAnchor): raise Exception(f"ID for array table row {k} must be different from any ID in anchor_data table in array_mooring section") + # determine if end A is an anchor or a platform if any(ids['ID'] == arrayMooring[j]['endA'] for ids in arrayInfo): # shared mooring line (no anchor) # get ID of platforms connected to line - PFNum.append(arrayMooring[j]['endB']) - PFNum.append(arrayMooring[j]['endA']) + PF.append(self.platformList[arrayMooring[j]['endB']]) + PF.append(self.platformList[arrayMooring[j]['endA']]) # find row in array table associated with these platform IDs and set locations for k in range(0, len(arrayInfo)): - if arrayInfo[k]['ID'] == PFNum[0]: + if arrayInfo[k]['ID'] == PF[0]: rowB = arrayInfo[k] - elif arrayInfo[k]['ID'] == PFNum[1]: + elif arrayInfo[k]['ID'] == PF[1]: rowA = arrayInfo[k] - # get headings (mooring heading combined with platform heading) - headingB = np.radians(arrayMooring[j]['headingB']) + self.platformList[PFNum[0]].phi + # # get headings (mooring heading combined with platform heading) + # headingB = np.radians(arrayMooring[j]['headingB']) + self.platformList[PFNum[0]].phi # get configuration for the line lineconfig = arrayMooring[j]['MooringConfigID'] # create mooring and connector dictionary for that line - m_config = getMoorings(lineconfig, lineConfigs, connectorTypes, self.platformList[PFNum[0]].id, self) + mdd = getMoorings(lineconfig, lineConfigs, connectorTypes, PF[0].id, self) # create mooring class instance - moor = self.addMooring(id=str(PFNum[1])+'-'+str(PFNum[0]), - heading=headingB, dd=m_config, shared=1) + moor = self.addMooring(id=str(PF[1].id)+'-'+str(PF[0].id), + dd=mdd, + shared=1) # attach ends - moor.attachTo(self.platformList[PFNum[1]],end='A') - moor.attachTo(self.platformList[PFNum[0]],end='B') + fairsB = attachFairleads(moor, + 1, + PF[0], + fair_ID_start=PF[0].id+'_F', + fair_inds=arrayMooring[j]['fairleadB']) + fairsA = attachFairleads(moor, + 0, + PF[1], + fair_ID_start=PF[1].id+'_F', + fair_inds=arrayMooring[j]['fairleadA']) + - # reposition - moor.reposition(r_center=[self.platformList[PFNum[1]].r, - self.platformList[PFNum[0]].r], + # determine heading + points = [[f.r[:2] for f in fairsA], + [f.r[:2] for f in fairsB]] + headingB = calc_heading(points[0], points[1]) + moor.reposition(r_center=[PF[1].r, + PF[0].r], heading=headingB, project=self) + + # Position the subcomponents along the Mooring + moor.positionSubcomponents() elif any(ids['ID'] == arrayMooring[j]['endA'] for ids in arrayAnchor): # end A is an anchor # get ID of platform connected to line - PFNum.append(arrayMooring[j]['endB']) + PF.append(self.platformList[arrayMooring[j]['endB']]) # get configuration for that line lineconfig = arrayMooring[j]['MooringConfigID'] # create mooring and connector dictionary for that line - m_config = getMoorings(lineconfig, lineConfigs, connectorTypes, self.platformList[PFNum[0]].id, self) + mdd = getMoorings(lineconfig, lineConfigs, connectorTypes, PF[0].id, self) # get letter number for mooring line - ind = len(self.platformList[PFNum[0]].getMoorings()) - # create mooring class instance, attach to end A and end B objects, reposition - moor = self.addMooring(id=str(PFNum[0])+alph[ind], - heading=np.radians(arrayMooring[j]['headingB'])+self.platformList[PFNum[0]].phi, - dd=m_config) + ind = len(PF[0].getMoorings()) + + # create mooring class instance + moor = self.addMooring(id=str(PF[0].id)+alph[ind], + dd=mdd) # check if anchor instance already exists if any(tt == arrayMooring[j]['endA'] for tt in self.anchorList): # anchor name exists already in list @@ -582,37 +676,58 @@ def loadDesign(self, d, raft=True): # attach anchor moor.attachTo(anchor,end='A') # attach platform - moor.attachTo(self.platformList[PFNum[0]],end='B') + fairsB = attachFairleads(moor, + 1, + PF[0], + fair_ID_start=PF[0].id+'_F', + fair_inds=arrayMooring[j]['fairleadB']) + + # determine heading + headingB = calc_heading(anchor.r[:2],[f.r[:2] for f in fairsB]) + + # re-determine span as needed from anchor loc and end B midpoint + # this is to ensure the anchor location does not change from that specified in the ontology + moor.span = np.linalg.norm(anchor.r[:2]- + np.array(calc_midpoint([f.r[:2] for f in fairsB]))) + # reposition mooring - moor.reposition(r_center=self.platformList[PFNum[0]].r, heading=np.radians(arrayMooring[j]['headingB'])+self.platformList[PFNum[0]].phi, project=self) + moor.reposition(r_center=PF[0].r, heading=headingB, project=self) # update depths zAnew, nAngle = self.getDepthAtLocation(aloc[0],aloc[1], return_n=True) moor.dd['zAnchor'] = -zAnew moor.z_anch = -zAnew - moor.rA = [aloc[0],aloc[1],-zAnew] + moor.setEndPosition([aloc[0],aloc[1],-zAnew], 0) + + # Position the subcomponents along the Mooring + moor.positionSubcomponents() - # update anchor depth and soils - self.updateAnchor(anchor, update_loc=False) + # # update anchor depth and soils + # self.updateAnchor(anchor, update_loc=False) else: # error in input raise Exception(f"end A input in array_mooring line_data table line '{j}' must be either an ID from the anchor_data table (to specify an anchor) or an ID from the array table (to specify a FOWT).") - # add heading - self.platformList[PFNum[0]].mooring_headings.append(np.radians(arrayMooring[j]['headingB'])) - if len(PFNum)>1: # if shared line - self.platformList[PFNum[1]].mooring_headings.append(np.radians(arrayMooring[j]['headingA'])) # add heading + # add heading to platform headings list + PF[0].mooring_headings.append(headingB-PF[0].phi)#np.radians(arrayMooring[j]['headingB'])) + PF[0].setPosition(r=PF[0].r, project=self) + if len(PF)>1: # if shared line + headingA = headingB - np.pi + PF[1].mooring_headings.append(headingA-PF[1].phi) # add heading + PF[1].setPosition(r=PF[1].r, project=self) # increment counter mct += 1 + # update all anchors + self.updateAnchor() + # ===== load Cables ====== # load in array cables from table (no routing, assume dynamic-static-dynamic or dynamic suspended setup) if arrayCableInfo: for i,cab in enumerate(arrayCableInfo): A=None - rJTubeA = None; rJTubeB = None # create design dictionary for subsea cable dd = {'cables':[],'joints':[]} @@ -620,22 +735,31 @@ def loadDesign(self, d, raft=True): dyn_cabA = cab['DynCableA'] if not 'NONE' in cab['DynCableA'].upper() else None dyn_cabB = cab['DynCableB'] if not 'NONE' in cab['DynCableB'].upper() else None stat_cab = cab['cableType'] if not 'NONE' in cab['cableType'].upper() else None + JtubeA = cab['JtubeA'] if ('JtubeA' in cab) else None + JtubeB = cab['JtubeB'] if ('JtubeB' in cab) else None + rJTubeA = None # if Jtube rel position not provided, this is the radial Jtube position + rJTubeB = None + + A_phi = self.platformList[cab['AttachA']].phi # end A platform phi + B_phi = self.platformList[cab['AttachB']].phi # end B platform phi if dyn_cabA: dyn_cab = cab['DynCableA'] Acondd, jAcondd = getDynamicCables(dyn_cable_configs[dyn_cab], cable_types, cable_appendages, self.depth, rho_water=self.rho_water, g=self.g) - Acondd['headingA'] = np.radians(90-cab['headingA']) + # only add a joint if there's a cable section after this if stat_cab or dyn_cabB: dd['joints'].append(jAcondd) + Acondd['headingA'] = np.radians(cab['headingA']) + A_phi # heading only if not suspended else: # this is a suspended cable - add headingB - Acondd['headingB'] = np.radians(90-cab['headingB']) + Acondd['headingB'] = np.radians(cab['headingB']) + B_phi - rJTubeA = dyn_cable_configs[dyn_cabA]['rJTube'] - Acondd['rJTube'] = rJTubeA + if 'rJTube' in dyn_cable_configs[dyn_cabA]: + rJTubeA = dyn_cable_configs[dyn_cabA]['rJTube'] + Acondd['rJTube'] = rJTubeA dd['cables'].append(Acondd) # get conductor area to send in for static cable A = Acondd['A'] @@ -653,11 +777,11 @@ def loadDesign(self, d, raft=True): cable_types, cable_appendages, self.depth, rho_water=self.rho_water, g=self.g) - - rJTubeB = dyn_cable_configs[dyn_cabB]['rJTube'] - Bcondd['rJTube'] = rJTubeB + if 'rJTube' in dyn_cable_configs[dyn_cabB]: + rJTubeB = dyn_cable_configs[dyn_cabB]['rJTube'] + Bcondd['rJTube'] = rJTubeB # add heading for end A to this cable - Bcondd['headingB'] = np.radians(90-arrayCableInfo[i]['headingB']) + Bcondd['headingB'] = np.radians(arrayCableInfo[i]['headingB']) + B_phi dd['cables'].append(Bcondd) # add joint (even if empty) dd['joints'].append(jBcondd) @@ -669,15 +793,22 @@ def loadDesign(self, d, raft=True): self.cableList[cableID] = Cable(cableID,d=dd) # attach ends if cab['AttachA'] in self.platformList.keys(): - # connect to platform - self.cableList[cableID].attachTo(self.platformList[cab['AttachA']],end='A') + # attach cable subcomponent to Jtube if it exists (higher level objs will automatically connect) + if jtube_by_platform[cab['AttachA']] and JtubeA: + self.cableList[cableID].subcomponents[0].attachTo(jtube_by_platform[cab['AttachA']][JtubeA-1], end='A') + else: + self.cableList[cableID].attachTo(self.platformList[cab['AttachA']],end='A') elif cab['AttachA'] in cable_appendages: pass else: raise Exception(f'AttachA {arrayCableInfo[i]["AttachA"]} for array cable {i} does not match any platforms or appendages.') if cab['AttachB'] in self.platformList.keys(): - # connect to platform - self.cableList[cableID].attachTo(self.platformList[cab['AttachB']],end='B') + # attach cable subcomponent to Jtube if it exists (higher level objs will automatically connect) + if jtube_by_platform[cab['AttachB']] and JtubeB: + self.cableList[cableID].subcomponents[-1].attachTo(jtube_by_platform[cab['AttachB']][JtubeB-1], end='B') + else: + self.cableList[cableID].attachTo(self.platformList[cab['AttachB']],end='B') + elif cab['AttachB'] in cable_appendages: pass else: @@ -692,6 +823,8 @@ def loadDesign(self, d, raft=True): for cab in cableInfo: rJTubeA = None; rJTubeB = None + JtubeA = cab['endA']['Jtube'] if ('Jtube' in cab['endA']) else None + JtubeB = cab['endB']['Jtube'] if ('Jtube' in cab['endB']) else None # create design dictionary for subsea cable dd = {'cables':[],'joints':[]} @@ -699,7 +832,10 @@ def loadDesign(self, d, raft=True): # pull out cable sections (some may be 'NONE') dyn_cabA = cab['endA']['dynamicID'] if not 'NONE' in cab['endA']['dynamicID'].upper() else None dyn_cabB = cab['endB']['dynamicID'] if not 'NONE' in cab['endB']['dynamicID'].upper() else None - stat_cab = cab['type'] if not 'NONE' in cab['type'].upper() else None + stat_cab = cab['type'] if not 'NONE' in cab['type'].upper() else None + + A_phi = self.platformList[cab['endA']['attachID']].phi # end A platform phi + B_phi = self.platformList[cab['endB']['attachID']].phi # end B platform phi # load in end A cable section type if dyn_cabA: @@ -707,17 +843,20 @@ def loadDesign(self, d, raft=True): cable_types, cable_appendages, self.depth, rho_water=self.rho_water, g=self.g) + # only add a joint if there's a cable section after this if stat_cab or dyn_cabB: dd['joints'].append(jAcondd) else: # this is a suspended cable - add headingB - Acondd['headingB'] = np.radians(90-cab['endB']['heading']) + Acondd['headingB'] = np.radians(cab['endB']['heading']) + B_phi + # add headingA - Acondd['headingA'] = np.radians(90-cab['endA']['heading']) - rJTubeA = dyn_cable_configs[dyn_cabA]['rJTube'] - Acondd['rJTube'] = rJTubeA + Acondd['headingA'] = np.radians(cab['endA']['heading']) + A_phi + if 'rJTube' in dyn_cable_configs[dyn_cabA]: + rJTubeA = dyn_cable_configs[dyn_cabA]['rJTube'] + Acondd['rJTube'] = rJTubeA # append to cables list dd['cables'].append(Acondd) @@ -745,10 +884,10 @@ def loadDesign(self, d, raft=True): self.depth, rho_water=self.rho_water, g=self.g) # add headingB - Bcondd['headingB'] = np.radians(90-cab['endB']['heading']) - - rJTubeB = dyn_cable_configs[dyn_cabB]['rJTube'] - Bcondd['rJTube'] = rJTubeB + Bcondd['headingB'] = np.radians(cab['endB']['heading']) + B_phi + if 'rJTube' in dyn_cable_configs[dyn_cabB]: + rJTubeB = dyn_cable_configs[dyn_cabB]['rJTube'] + Bcondd['rJTube'] = rJTubeB # append to cables list dd['cables'].append(Bcondd) # append to joints list @@ -762,16 +901,22 @@ def loadDesign(self, d, raft=True): # attach end A if cab['endA']['attachID'] in self.platformList.keys(): - # connect to platform - self.cableList[cableID].attachTo(self.platformList[cab['endA']['attachID']],end='A') + if jtube_by_platform[cab['endA']['attachID']] and JtubeA: + self.cableList[cableID].subcomponents[0].attachTo(jtube_by_platform[cab['endA']['attachID']][JtubeA], end='A') + else: + # connect to platform + self.cableList[cableID].attachTo(self.platformList[cab['endA']['attachID']],end='A') elif cab['endA']['attachID'] in cable_appendages: pass else: raise Exception(f"AttachA {cab['endA']['attachID']} for cable {cab['name']} does not match any platforms or appendages.") # attach end B if cab['endB']['attachID'] in self.platformList.keys(): - # connect to platform - self.cableList[cableID].attachTo(self.platformList[cab['endB']['attachID']],end='B') + if jtube_by_platform[cab['endB']['attachID']] and JtubeB: + self.cableList[cableID].subcomponents[-1].attachTo(jtube_by_platform[cab['endB']['attachID']][JtubeB], end='B') + else: + # connect to platform + self.cableList[cableID].attachTo(self.platformList[cab['endB']['attachID']],end='B') elif cab['endB']['attachID'] in cable_appendages: pass else: @@ -780,7 +925,8 @@ def loadDesign(self, d, raft=True): # reposition the cable self.cableList[cableID].reposition(project=self, rad_fair=[rJTubeA,rJTubeB]) - + for pf in self.platformList.values(): + pf.setPosition(pf.r, project=self) # ===== load RAFT model parts ===== # load info into RAFT dictionary and create RAFT model if raft: @@ -825,10 +971,13 @@ def loadDesign(self, d, raft=True): # ----- Site conditions processing functions ----- - def loadSite(self, site): + def loadSite(self, site, dir=''): '''Load site information from a dictionary or YAML file (specified by input). This should be the site portion of - the floating wind array ontology.''' + the floating wind array ontology. + + site : portion of project dict + dir : optional directory of main yaml file''' # standard function to load dict if input is yaml # load general information @@ -840,7 +989,13 @@ def loadSite(self, site): # load bathymetry information, if provided if 'bathymetry' in site and site['bathymetry']: if 'file' in site['bathymetry'] and site['bathymetry']['file']: # make sure there was a file provided even if the key is there - self.loadBathymetry(site['bathymetry']['file']) + filename = site['bathymetry']['file'] + + # if it's a relative file location, specify the root directory + if not os.path.isabs(filename): + filename = os.path.join(dir, filename) + self.loadBathymetry(filename) + elif 'x' in site['bathymetry'] and 'y' in site['bathymetry']: self.grid_x = np.array(site['bathymetry']['x']) self.grid_y = np.array(site['bathymetry']['y']) @@ -1494,11 +1649,40 @@ def addPlatform(self,r=[0,0,0], id=None, phi=0, entity='', platform.dd = dd self.platformList[id] = platform # also save in RAFT, in its MoorPy System(s) + return(platform) + + def addFairlead(self, id=None, platform=None, r_rel=[0,0,0], + mooring=None, end='b'): + ''' + Function to create a Fairlead object and attach it to a platform''' + # create an id if needed + if id == None: + if platform != None: + id = platform.id + str(len(platform.attachments)) + + # create fairlead object + fl = Fairlead(id=id) + + # attach subordinately to platform and provide relative location + if platform: + platform.attach(fl, r_rel=r_rel) + + # attach equally to mooring end connector + if mooring: + if end in ['a','A',0]: + mooring.subcomponents[0].join(fl) + elif end in ['b','B',1]: + mooring.subcomponents[-1].join(fl) + + # return fairlead object + return(fl) + + def addMooring(self, id=None, endA=None, endB=None, heading=0, dd={}, section_types=[], section_lengths=[], connectors=[], span=0, shared=0, reposition=False, subsystem=None, - **adjuster_settings): + subcons=None, **adjuster_settings): # adjuster=None, # method = 'horizontal', target = None, i_line = 0, ''' @@ -1696,36 +1880,32 @@ def addSubstation(self, id=None, platform=None, dd={}): self.substationList[id] = Substation(dd, id) if platform != None: platform.attach(self.substationList[id]) - - - def cableDesignInterpolation(self,depth,cables): - '''Interpolates between dynamic cable designs for different depths to produce - a design for the given depth + + def addJtube(self, id=None, platform=None, r_rel=[0,0,0], + cable=None, end='b'): ''' - # grab list of values for all cables - cabdesign = {} - cabdesign['span'] = [x.dd['span'] for x in cables] - depths = [-x.z_anch for x in cables] - cabdesign['n_buoys'] = [x.dd['buoyancy_sections']['N_modules'] for x in cables] - cabdesign['spacings'] = [x.dd['buoyancy_sections']['spacing'] for x in cables] - cabdesign['L_mids'] = [x.dd['buoyancy_sections']['L_mid'] for x in cables] - cabdesign['L'] = [x.dd['L'] for x in cables] - - # sort and interp all lists by increasing depths - sorted_indices = np.argsort(depths) - depths_sorted = [depths[i] for i in sorted_indices] - newdd = deepcopy(cable[0].dd) - newdd['span'] = np.interp(depth,depths_sorted,[cabdesign['span'][i] for i in sorted_indices]) - newdd['buoyancy_sections']['N_modules'] = np.interp(depth, depths_sorted, - [cabdesign['n_buoys'][i] for i in sorted_indices]) - newdd['buoyancy_sections']['spacing'] = np.interp(depth,depths_sorted, - [cabdesign['spacings'][i] for i in sorted_indices]) - newdd['buoyancy_sections']['L_mids'] = np.interp(depth,depths_sorted, - [cabdesign['L_mids'][i] for i in sorted_indices]) - newdd['L'] = np.interp(depth,depths,[cabdesign['L'][i] for i in sorted_indices]) - + Function to create a Jtube object and attach it to a platform''' + # create an id if needed + if id == None: + if platform != None: + id = platform.id + len(platform.attachments) + + # create J-tube object + jt = Jtube(id=id) - return(newdd) + # attach subordinately to platform and provide relative location + if platform: + platform.attach(jt, r_rel=r_rel) + + # attach equally to mooring end connector + if cable: + if end in ['a','A',0]: + cable.subcomponents[0].attachTo(jt) + elif end in ['b','B',1]: + cable.subcomponents[-1].attachTo(jt) + + # return fairlead object + return(jt) @@ -1736,6 +1916,7 @@ def addCablesConnections(self,connDict,cableType_def='dynamic_cable_66',oss=Fals cableConfig=None, configType=0,heading_buffer=30, route_anchors=False, adj_dir=1, consider_alternate_side=False): + '''Adds cables and connects them to existing platforms/substations based on info in connDict Designed to work with cable optimization output designed by Michael Biglu @@ -1827,12 +2008,11 @@ def addCablesConnections(self,connDict,cableType_def='dynamic_cable_66',oss=Fals attB = pf # update platform location pf.r[:2] = connDict[i]['coordinates'][-1] - - # get heading of cable from attached object coordinates - headingA = np.radians(90) - np.arctan2((connDict[i]['coordinates'][-1][0]-connDict[i]['coordinates'][0][0]), - (connDict[i]['coordinates'][-1][1]-connDict[i]['coordinates'][0][1])) - headingB = np.radians(90) - np.arctan2((connDict[i]['coordinates'][0][0]-connDict[i]['coordinates'][-1][0]), - (connDict[i]['coordinates'][0][1]-connDict[i]['coordinates'][-1][1])) + + # get heading of cable from attached object coordinates (compass heading) + headingA = calc_heading(connDict[i]['coordinates'][-1], + connDict[i]['coordinates'][0]) + headingB = headingA + np.pi # figure out approx. depth at location initial_depths = [] @@ -1844,11 +2024,12 @@ def addCablesConnections(self,connDict,cableType_def='dynamic_cable_66',oss=Fals # get depth at these locs initial_depths.append(self.getDepthAtLocation(*endLocA)) initial_depths.append(self.getDepthAtLocation(*endLocB)) - # select cable and collect design dictionary info on cable - selected_cable, dd = getCableDesign(connDict[i], cableType_def, - cableConfig, configType, - depth=np.mean(initial_depths)) + selected_cable, dd, cable_candidates = getCableDesign( + connDict[i], cableType_def, + cableConfig, configType, + depth=np.mean(initial_depths) + ) else: dd = {} dd['cables'] = [] @@ -1888,6 +2069,9 @@ def addCablesConnections(self,connDict,cableType_def='dynamic_cable_66',oss=Fals cab.attachTo(attB,end='b') if cableConfig: + if cable_candidates: + cab.subcomponents[0].alternate_cables=cable_candidates + cab.subcomponents[-1].alternate_cables=cable_candidates if 'head_offset' in selected_cable: headingA += np.radians(selected_cable['head_offset']) headingB -= np.radians(selected_cable['head_offset']) @@ -1899,6 +2083,7 @@ def addCablesConnections(self,connDict,cableType_def='dynamic_cable_66',oss=Fals msp = list(moors.values())[0].span + attA.rFair + 200 # add a bit extra # consider mooring headings from both ends if close enough pfsp = np.linalg.norm(attA.r-attB.r) + if consider_alternate_side and pfsp-2*attA.rFair < msp+dc0s: headingA = head_adjust([attA,attB], headingA, @@ -1941,8 +2126,8 @@ def addCablesConnections(self,connDict,cableType_def='dynamic_cable_66',oss=Fals stat_cable = cab.subcomponents[ind+ind_of_stat] # get new coordinate routing point stat_cable_end = stat_cable.rA if ind==0 else stat_cable.rB - coord = [stat_cable_end[0] + np.cos(heads[ii])*spandiff, - stat_cable_end[1] + np.sin(heads[ii])*spandiff] + coord = [stat_cable_end[0] + np.cos(np.pi/2-heads[ii])*spandiff, + stat_cable_end[1] + np.sin(np.pi/2-heads[ii])*spandiff] # append it to static cable object coordinates coords.append(coord) @@ -2073,20 +2258,36 @@ def plot2d(self, ax=None, plot_seabed=False,draw_soil=False,plot_bathymetry=True # Plot moorings one way or another (eventually might want to give Mooring a plot method) for mooring in self.mooringList.values(): - + lineList = [] if mooring.ss: # plot with Subsystem if available - labs = [] - for line in mooring.ss.lineList: - if 'chain' in line.type['material']: - line.color = 'k' - elif 'polyester' in line.type['material']: - line.color = [.3,.5,.5] - else: - line.color = [0.5,0.5,0.5] - labs.append(line.type['material'][0].upper()+ - line.type['material'][1:]+' Mooring') + lineList = mooring.ss.lineList + + elif mooring.parallels: + for i in mooring.i_sec: + sec = mooring.getSubcomponent(i) + if hasattr(sec,'mpLine'): + lineList.append(sec.mpLine) + line = sec.mpLine + + labs = [] + for line in lineList: + if 'chain' in line.type['material']: + line.color = 'k' + elif 'polyester' in line.type['material']: + line.color = [.3,.5,.5] + else: + line.color = [0.5,0.5,0.5] + labs.append(line.type['material'][0].upper()+ + line.type['material'][1:]+' Mooring') + + if mooring.ss: mooring.ss.drawLine2d(0, ax, color="self", endpoints=False, - Xuvec=[1,0,0], Yuvec=[0,1,0],label=labs) + Xuvec=[1,0,0], Yuvec=[0,1,0],label=labs) + elif mooring.parallels: + for i,line in enumerate(lineList): + line.drawLine2d(0, ax, color="self", + Xuvec=[1,0,0], Yuvec=[0,1,0],label=labs[i]) + else: # simple line plot ax.plot([mooring.rA[0], mooring.rB[0]], [mooring.rA[1], mooring.rB[1]], 'k', lw=0.5, label='Mooring Line') @@ -2413,6 +2614,19 @@ def plot3d(self, ax=None, figsize=(10,8), fowt=False, save=False, line.color = [0.5,0.5,0.5] line.lw = lw mooring.ss.drawLine(0, ax, color='self') + elif mooring.parallels: + for i in mooring.i_sec: + sec = mooring.getSubcomponent(i) + if hasattr(sec,'mpLine'): + line = sec.mpLine + if 'chain' in line.type['material']: + line.color = 'k' + elif 'polyester' in line.type['material']: + line.color = [.3,.5,.5] + else: + line.color = [0.5,0.5,0.5] + line.lw = lw + line.drawLine(0,ax,color='self') # plot the FOWTs using a RAFT FOWT if one is passed in (TEMPORARY) if fowt: @@ -2503,90 +2717,209 @@ def getMoorPyArray(self, plt=0, pristineLines=True, cables=True): else: self.ms.addBody(-1,r6,m=19911423.956678286,rCG=np.array([ 1.49820657e-15, 1.49820657e-15, -2.54122031e+00]),v=19480.104108645974,rM=np.array([2.24104273e-15, 1.49402849e-15, 1.19971829e+01]),AWP=446.69520543229874) body.body = self.ms.bodyList[-1] + # create anchor points and all mooring lines connected to the anchors (since all connected to anchors, can't be a shared mooring) - for i in self.anchorList: # i is key (name) of anchor - ssloc = [] - for j in self.anchorList[i].attachments: # j is key (name) of mooring object in anchor i + for anchor in self.anchorList.values(): # Go through each anchor + + # Create it's MoorPy Point object + if anchor.mpAnchor: # If anchor already exists in MoorPy + print("Why does this anchor already have a MoorPy Point?") + breakpoint() + + anchor.makeMoorPyAnchor(self.ms) + num = anchor.mpAnchor.number + + # Go through each thing/mooring attached to the anchor + for j, att in anchor.attachments.items(): + + mooring = att['obj'] + # create subsystem if pristineLines: - - self.anchorList[i].attachments[j]['obj'].createSubsystem(pristine=1, mooringSys=self.ms) + mooring.createSubsystem(pristine=True, ms=self.ms) # set location of subsystem for simpler coding - ssloc.append(self.anchorList[i].attachments[j]['obj'].ss) + ssloc = mooring.ss else: - self.anchorList[i].attachments[j]['obj'].createSubsystem(mooringSys=self.ms) + mooring.createSubsystem(pristine=False, ms=self.ms) # set location of subsystem for simpler coding - ssloc.append(self.anchorList[i].attachments[j]['obj'].ss_mod) - self.ms.lineList.append(ssloc[-1]) - ssloc[-1].number = len(self.ms.lineList) - # create anchor point if it doesn't already exist - if self.anchorList[i].mpAnchor: - # get point number of anchor - num = self.anchorList[i].mpAnchor.number - # attach line to anchor point - self.ms.pointList[num-1].attachLine(ssloc[-1].number,0) - else: - self.anchorList[i].makeMoorPyAnchor(self.ms) - # attach line to anchor point - self.ms.pointList[-1].attachLine(ssloc[-1].number,0) + ssloc = mooring.ss_mod + + # (ms.lineList.append is now done in Mooring.createSubsystem) + + # Attach the Mooring to the anchor + if mooring.parallels: # the case with parallel sections, multiple MoorPy objects + + # note: att['end'] should always be 0 in this part of the + # code, but keeping the end variable here in case it opens + # up ideas for code consolidation later. + + subcom = mooring.subcomponents[-att['end']] # check what's on the end of the mooring + + if isinstance(subcom, list): # bridle case + print('This case not implemented yet') + breakpoint() + elif isinstance(subcom, Node): + # TODO: get rel dist from connector to anchor + # for now, just assume 0 rel dist until anchor lug objects introduced + r_rel = [0,0,0] + subcom.mpConn.type = 1 + # attach anchor body to subcom connector point + anchor.mpAnchor.attachPoint(subcom.mpConn.number,r_rel) + # (the section line object(s) should already be attached to this point) + #TODO >>> still need to handle possibility of anchor bridle attachment, multiple anchor lugs, etc. <<< + + else: # Original case with Subsystem + # need to create "dummy" point to connect to anchor body + point = self.ms.addPoint(1,anchor.r) + # attach dummy point to anchor body + anchor.mpAnchor.attachPoint(point.number,[0,0,0]) + # now attach dummy point to line + point.attachLine(ssloc.number, att['end']) + + # Check for fancy case of any lugs (nodes) attached to the anchor + if any([ isinstance(a['obj'], Node) for a in anchor.attachments.values()]): + print('Warning: anchor lugs are not supported yet') + breakpoint() # find associated platform and attach body to point (since not a shared line, should only be one platform with this mooring object) - for ii,k in enumerate(self.platformList): # ii is index in dictionary, k is key (name) of platform - if j in self.platformList[k].attachments: # j is key (name) of mooring object in anchor i checking if that same mooring object name is attached to platform k - PF = self.platformList[k] # platform object associated with mooring line j and anchor i - body = PF.body + for platform in self.platformList.values(): # ii is index in dictionary, k is key (name) of platform + if j in platform.attachments: # j is key (name) of mooring object in anchor i checking if that same mooring object name is attached to platform k + PF = platform # platform object associated with mooring line j and anchor i + break + # attach rB point to platform - # add fairlead point - self.ms.addPoint(1,ssloc[-1].rB) - # add connector info for fairlead point - self.ms.pointList[-1].m = self.ms.lineList[-1].pointList[-1].m - self.ms.pointList[-1].v = self.ms.lineList[-1].pointList[-1].v - self.ms.pointList[-1].CdA = self.ms.lineList[-1].pointList[-1].CdA - # attach the line to point - self.ms.pointList[-1].attachLine(ssloc[-1].number,1) - body.attachPoint(len(self.ms.pointList),[ssloc[-1].rB[0]-PF.r[0],ssloc[-1].rB[1]-PF.r[1],ssloc[-1].rB[2]-PF.r[2]]) # attach to fairlead (need to subtract out location of platform from point for subsystem integration to work correctly) - - - check = np.ones((len(self.mooringList),1)) - # now create and attach any shared lines or hybrid lines attached to buoys - for ii,i in enumerate(self.mooringList): # loop through all lines - ii is index of mooring object in dictionary, i is key (name) of mooring object + if mooring.parallels: # case with paralles/bridles + + # Look at end B object(s) + subcom = mooring.subcomponents[-1] + + if isinstance(subcom, list): # bridle case + for parallel in subcom: + subcom2 = parallel[-1] # end subcomponent of the parallel path + + # Code repetition for the moment: + if isinstance(subcom2, Edge): + r = subcom2.attached_to[1].r # approximate end point...? + point = self.ms.addPoint(1, r) + PF.body.attachPoint(point.number, r-PF.r) + point.attachLine(subcom2.mpLine.number, 1) # attach the subcomponent's line object end B + + elif isinstance(subcom2, Node): + r = subcom2.r # approximate end point...? + pnum = subcom2.mpConn.number + PF.body.attachPoint(pnum, r-PF.r) + + elif isinstance(subcom, Edge): + r = subcom.attached_to[1].r # approximate end point...? + point = self.ms.addPoint(1, r) + PF.body.attachPoint(point.number, r-PF.r) + point.attachLine(subcom.mpLine.number, 1) # attach the subcomponent's line object end B + + elif isinstance(subcom, Node): + r = subcom.r # approximate end point...? + pnum = subcom.mpConn.number + PF.body.attachPoint(pnum, r-PF.r) + # (the section line object(s) should already be attached to this point) + + + else: # normal serial/subsystem case + # add fairlead point + point = self.ms.addPoint(1,ssloc.rB) + # add connector info for fairlead point + # >>> MH: these next few lines might result in double counting <<< + point.m = self.ms.lineList[-1].pointList[-1].m + point.v = self.ms.lineList[-1].pointList[-1].v + point.CdA = self.ms.lineList[-1].pointList[-1].CdA + # attach the line to point + point.attachLine(ssloc.number,1) + PF.body.attachPoint(point.number, ssloc.rB-PF.r) # attach to fairlead (need to subtract out location of platform from point for subsystem integration to work correctly) + + + # Create and attach any shared lines or hybrid lines attached to buoys + for mkey, mooring in self.mooringList.items(): # loop through all lines + check = 1 # temporary approach to identify shared lines <<< for j in self.anchorList: # j is key (name) of anchor object - if i in self.anchorList[j].attachments: # check if line has already been put in ms - check[ii] = 0 - if check[ii] == 1: # mooring object not in any anchor lists + if mkey in self.anchorList[j].attachments: # check if line has already been put in ms + check = 0 + break + if check == 1: # mooring object not in any anchor lists # new shared line # create subsystem for shared line - if hasattr(self.mooringList[i],'shared'): - self.mooringList[i].createSubsystem(case=self.mooringList[i].shared,pristine=pristineLines, mooringSys=self.ms) + if hasattr(mooring, 'shared'): # <<< + mooring.createSubsystem(case=mooring.shared, + pristine=pristineLines, ms=self.ms) else: - self.mooringList[i].createSubsystem(case=1,pristine=pristineLines, mooringSys=self.ms) # we doubled all symmetric lines so any shared lines should be case 1 + mooring.createSubsystem(case=1,pristine=pristineLines, + ms=self.ms) # we doubled all symmetric lines so any shared lines should be case 1 # set location of subsystem for simpler coding if pristineLines: - ssloc = self.mooringList[i].ss + ssloc = mooring.ss else: - ssloc = self.mooringList[i].ss_mod - # add subsystem as a line in moorpy system - self.ms.lineList.append(ssloc) - ssloc.number = len(self.ms.lineList) + ssloc = mooring.ss_mod + + # (ms.lineList.append is now done in Mooring.createSubsystem) # find associated platforms/ buoys - att = self.mooringList[i].attached_to + att = mooring.attached_to # connect line ends to the body/buoy - ends = [ssloc.rA,ssloc.rB] - for ki in range(0,2): - if isinstance(att[ki],Platform): - if att[ki]: - # add fairlead point and attach the line to it - self.ms.addPoint(1,ends[ki]) - self.ms.pointList[-1].attachLine(ssloc.number,ki) - att[ki].body.attachPoint(len(self.ms.pointList),[ends[ki][0]-att[ki].r[0],ends[ki][1]-att[ki].r[1],ends[ki][2]-att[ki].r[2]]) - else: - # this end is unattached - pass - + for ki in range(0,2): # for each end of the mooring + if isinstance(att[ki],Platform): # if it's attached to a platform + + platform = att[ki] + if mooring.parallels: # case with paralles/bridles + + # Look at end object(s) + subcom = mooring.subcomponents[-ki] + + if isinstance(subcom, list): # bridle case + for parallel in subcom: + subcom2 = parallel[-ki] # end subcomponent of the parallel path + + # Code repetition for the moment: + if isinstance(subcom2, Edge): + r = subcom2.attached_to[ki].r # approximate end point...? + point = self.ms.addPoint(1, r) + platform.body.attachPoint(point.number, r-platform.r) + point.attachLine(subcom2.mpLine.number, ki) # attach the subcomponent's line object end + + elif isinstance(subcom2, Node): + r = subcom2.r # approximate end point...? + pnum = subcom2.mpConn.number + platform.body.attachPoint(pnum, r-platform.r) + + elif isinstance(subcom, Edge): + r = subcom.attached_to[ki].r # approximate end point...? + point = self.ms.addPoint(1, r) + platform.body.attachPoint(point.number, r-platform.r) + point.attachLine(subcom.mpLine.number, ki) # attach the subcomponent's line object end + + elif isinstance(subcom, Node): + r = subcom.r # approximate end point...? + pnum = subcom.mpConn.number + platform.body.attachPoint(pnum, r-platform.r) + # (the section line object(s) should already be attached to this point) + + + else: # normal serial/subsystem case + + if ki==0: + rEnd = mooring.rA + else: + rEnd = mooring.rB + + # add fairlead point A and attach the line to it + point = self.ms.addPoint(1, rEnd) + point.attachLine(ssloc.number, ki) + platform.body.attachPoint(point.number, rEnd-platform.r) + + else: + # this end is unattached + pass + + # add in cables if desired if cables: @@ -2912,6 +3245,7 @@ def getRAFT(self,RAFTDict,pristine=1): See RAFT documentation for requirements for each sub-dictionary ''' print('Creating RAFT object') + # create RAFT model if necessary components exist if 'platforms' in RAFTDict or 'platform' in RAFTDict: # set up a dictionary with keys as the table names for each row (ease of use later) @@ -3328,47 +3662,135 @@ def duplicate(self,pf, r=None,heading=None): self.platformList[newid] = pf2 count = 0 - for att in pf.attachments.values(): - if isinstance(att['obj'],Mooring): - if att['end'] == 'a': - endB = 0 - else: - endB = 1 - # grab all info from mooring object - md = deepcopy(att['obj'].dd) - mhead = att['obj'].heading - # detach mooring object from platform - pf2.detach(att['obj'],end=endB) - # create new mooring object - newm = Mooring(dd=md,id=newid+alph[count]) - self.mooringList[newm.id] = newm - newm.heading = mhead - # attach to platform - pf2.attach(newm,end=endB) - # grab info from anchor object and create new one - ad = deepcopy(att['obj'].attached_to[1-endB].dd) - newa = Anchor(dd=ad,id=newid+alph[count]) - self.anchorList[newa.id] = newa - # attach anchor to mooring - newm.attachTo(newa,end=1-endB) - newm.reposition(r_center=r,project=self) - zAnew, nAngle = self.getDepthAtLocation(newm.rA[0], newm.rA[1], return_n=True) - newm.rA[2] = -zAnew - newm.dd['zAnchor'] = -zAnew - newa.r = newm.rA - - count += 1 - - elif isinstance(att['obj'],Turbine): - pf2.detach(att['obj']) - turb = deepcopy(att['obj']) - turb.id = newid+'turb' - self.turbineList[turb.id] = turb - pf2.attach(turb) + # first check for fairlead objects + fairs = True if any([isinstance(att['obj'],Fairlead) for att in pf.attachments.values()]) else False + if fairs: + for att in pf.attachments.values(): + if isinstance(att['obj'],Fairlead): + r_rel = att['r_rel'] + if att['obj'].attachments: + for val in att['obj'].attachments.values(): + moor = val['obj'].part_of + endB = 1 + # grab all info from mooring object + md = deepcopy(moor.dd) + mhead = moor.heading + # detach mooring object from platform + pf2.detach(moor,end=endB) + pf2.detach(att['obj']) + # create new mooring object + newm = Mooring(dd=md,id=newid+alph[count]) + self.mooringList[newm.id] = newm + newm.heading = mhead + # check if fairlead + # for con in newm.subcons_B: + # if + # attach to platform + fl = self.addFairlead(platform=pf2,r_rel=r_rel,mooring=newm,id=att['obj'].id) + # grab info from anchor object and create new one + ad = deepcopy(moor.attached_to[1-endB].dd) + newa = Anchor(dd=ad,id=newid+alph[count]) + self.anchorList[newa.id] = newa + # attach anchor to mooring + newm.attachTo(newa,end=1-endB) + pf2.setPosition(r,heading=heading,project=self) + zAnew, nAngle = self.getDepthAtLocation(newm.rA[0], newm.rA[1], return_n=True) + newm.rA[2] = -zAnew + newm.dd['zAnchor'] = -zAnew + newa.r = newm.rA + + count += 1 + + else: + moor=None + + + # for att in pf.attachments.values(): + # if isinstance(att['obj'],Mooring): + # if att['end'] == 'a': + # endB = 0 + # else: + # endB = 1 + # # grab all info from mooring object + # md = deepcopy(att['obj'].dd) + # mhead = att['obj'].heading + # # detach mooring object from platform + # pf2.detach(att['obj'],end=endB) + # # create new mooring object + # newm = Mooring(dd=md,id=newid+alph[count]) + # self.mooringList[newm.id] = newm + # newm.heading = mhead + # # check if fairlead + # # for con in newm.subcons_B: + # # if + # # attach to platform + # pf2.attach(newm,end=endB) + # # grab info from anchor object and create new one + # ad = deepcopy(att['obj'].attached_to[1-endB].dd) + # newa = Anchor(dd=ad,id=newid+alph[count]) + # self.anchorList[newa.id] = newa + # # attach anchor to mooring + # newm.attachTo(newa,end=1-endB) + # newm.reposition(r_center=r,project=self) + # zAnew, nAngle = self.getDepthAtLocation(newm.rA[0], newm.rA[1], return_n=True) + # newm.rA[2] = -zAnew + # newm.dd['zAnchor'] = -zAnew + # newa.r = newm.rA + + # count += 1 + + elif isinstance(att['obj'],Turbine): + pf2.detach(att['obj']) + turb = deepcopy(att['obj']) + turb.id = newid+'turb' + self.turbineList[turb.id] = turb + pf2.attach(turb) + + elif isinstance(att['obj'],Cable): + # could be cable, just detach for now + pf2.detach(att['obj'],att['end']) + else: + for att in pf.attachments.values(): + if isinstance(att['obj'],Mooring): + if att['end'] == 'a': + endB = 0 + else: + endB = 1 + # grab all info from mooring object + md = deepcopy(att['obj'].dd) + mhead = att['obj'].heading + # detach mooring object from platform + pf2.detach(att['obj'],end=endB) + # create new mooring object + newm = Mooring(dd=md,id=newid+alph[count]) + self.mooringList[newm.id] = newm + newm.heading = mhead + pf2.attach(newm,end=endB) + # grab info from anchor object and create new one + ad = deepcopy(att['obj'].attached_to[1-endB].dd) + newa = Anchor(dd=ad,id=newid+alph[count]) + self.anchorList[newa.id] = newa + # attach anchor to mooring + newm.attachTo(newa,end=1-endB) + newm.reposition(r_center=r,project=self) + zAnew, nAngle = self.getDepthAtLocation(newm.rA[0], newm.rA[1], return_n=True) + newm.rA[2] = -zAnew + newm.dd['zAnchor'] = -zAnew + newa.r = newm.rA + + count += 1 + + elif isinstance(att['obj'],Turbine): + pf2.detach(att['obj']) + turb = deepcopy(att['obj']) + turb.id = newid+'turb' + self.turbineList[turb.id] = turb + pf2.attach(turb) + + elif isinstance(att['obj'],Cable): + # could be cable, just detach for now + pf2.detach(att['obj'],att['end']) - else: - # could be cable, just detach for now - pf2.detach(att['obj'],att['end']) # reposition platform as needed pf2.setPosition(r,heading=heading,project=self) @@ -3419,7 +3841,7 @@ def addPlatformMS(self,ms,r=[0,0,0]): alph = list(string.ascii_lowercase) for point in ms.bodyList[0].attachedP: for j,line in enumerate(ms.pointList[point-1].attached): - md = {'sections':[],'connectors':[]} # start set up of mooring design dictionary + md = {'subcomponents':[]} # start set up of mooring design dictionary rA = ms.lineList[line-1].rA rB = ms.lineList[line-1].rB pfloc = ms.bodyList[0].r6 @@ -3442,53 +3864,18 @@ def addPlatformMS(self,ms,r=[0,0,0]): md['zAnchor'] = -self.getDepthAtLocation(rA[0],rA[1]) else: md['zAnchor'] = -self.getDepthAtLocation(rB[0],rB[1]) - - # # add section and connector info - # md['sections'].append({'type':line.type}) - # md['sections'][-1]['L'] = line.L - # md['connectors'].append({'m':point.m,'v':point.v,'Ca':point.Ca,'CdA':point.CdA}) - - # anline = True - # for pt in ms.pointList: - # if line in pt.attached and pt != point: - # n_att = len(pt.attached) - # nextloc = np.where([x!=line for x in pt.attached])[0][0] - # if n_att == 1: - # # this is the anchor point - # ad = {'design':{}} - # ad['design']['m'] = pt.m - # ad['design']['v'] = pt.v - # ad['design']['CdA'] = pt.CdA - # ad['design']['Ca'] = pt.Ca - # if 'anchor_type' in pt.entity: - # ad['type'] = pt.entity['anchor_type'] - # self.anchorList[mList[-1].id] = Anchor(dd=ad,r=pt.r,id=mList[-1].id) - # self.anchorList[mList[-1].id].attach(mList[-1],end=1-endB[-1]) - # # reposition mooring and anchor - # mList[-1].reposition(r_center=r) - # zAnew = self.getDepthAtLocation(mList[-1].rA[0], - # mList[-1].rA[1]) - # mList[-1].rA[2] = -zAnew - # mList[-1].dd['zAnchor'] = -zAnew - # self.anchorList[mList[-1].id].r = mList[-1].rA - # anline = False - # else: - # # add section and connector info - # md['sections'].append({'type':sline.type}) - # md['sections'][-1]['L'] = sline.L - # spt = ms.lineList[line-1].pointList[k] - # md['connectors'].append({'m':spt.m,'v':spt.v,'Ca':spt.Ca,'CdA':spt.CdA}) for k,sline in enumerate(ms.lineList[line-1].lineList): # add section and connector info - md['sections'].append({'type':sline.type}) - md['sections'][-1]['L'] = sline.L spt = ms.lineList[line-1].pointList[k] - md['connectors'].append({'m':spt.m,'v':spt.v,'Ca':spt.Ca,'CdA':spt.CdA}) + md['subcomponents'].append({'m':spt.m,'v':spt.v,'Ca':spt.Ca,'CdA':spt.CdA}) + md['subcomponents'].append({'type':sline.type}) + md['subcomponents'][-1]['L'] = sline.L + spt = ms.lineList[line-1].pointList[k+1] - md['connectors'].append({'m':spt.m,'v':spt.v,'Ca':spt.Ca,'CdA':spt.CdA}) + md['subcomponents'].append({'m':spt.m,'v':spt.v,'Ca':spt.Ca,'CdA':spt.CdA}) mhead.append(90 - np.degrees(np.arctan2(vals[1],vals[0]))) mList.append(Mooring(dd=md,id=pfid+alph[count])) mList[-1].heading = mhead[-1] @@ -3595,7 +3982,7 @@ def addPlatformConfig(self,configDict,r=[0,0]): # create mooring objects for i in range(len(pfinfo['mooring_headings'])): head = pfinfo['mooring_headings'][i]+pfinfo['platform_heading'] - md = {'span':minfo['span'],'sections':[],'connectors':[]} + md = {'span':minfo['span'],'subcomponents':[]} def arrayWatchCircle(self,plot=False, ang_spacing=45, RNAheight=150, shapes=True,thrust=1.95e6,SFs=True,moor_envelopes=True, @@ -3635,20 +4022,22 @@ def arrayWatchCircle(self,plot=False, ang_spacing=45, RNAheight=150, dictionary of safety factors for mooring line tensions for each turbine ''' - + # get angles to iterate over angs = np.arange(0,360+ang_spacing,ang_spacing) n_angs = len(angs) - # lists to save info in - minSag = [None]*len(self.cableList) - minCurvSF = [None]*len(self.cableList) - CminTenSF = [None]*len(self.cableList) + # lists to save info in minTenSF = [None]*len(self.mooringList) + CminTenSF = [None]*len(self.cableList) + minCurvSF = [None]*len(self.cableList) F = [None]*len(self.anchorList) x = np.zeros((len(self.platformList),n_angs)) y = np.zeros((len(self.platformList),n_angs)) + info = {'analysisType': 'quasi-static (MoorPy)', + 'info': f'determined from arrayWatchCircle() with DAF of {DAF}'} + lBots = np.zeros(len(self.mooringList)) # initialize for maximum laid length per mooring if not self.ms: self.getMoorPyArray() @@ -3671,82 +4060,45 @@ def arrayWatchCircle(self,plot=False, ang_spacing=45, RNAheight=150, if SFs: # get loads on anchors (may be shared) for j,anch in enumerate(self.anchorList.values()): - atts = [att['obj'] for att in anch.attachments.values()] - F1 = [None]*len(atts) - for jj,moor in enumerate(atts): - if isinstance(moor.attached_to[0],Anchor): - # anchor attached to end A - F1[jj] = moor.ss.fA*DAF - else: - F1[jj] = moor.ss.fB*DAF - # add up all tensions on anchor in each direction (x,y,z) - F2 = [sum([a[0] for a in F1]),sum([a[1] for a in F1]),sum([a[2] for a in F1])] + F2 = anch.mpAnchor.getForces()*DAF # add up all forces on anchor body H = np.hypot(F2[0],F2[1]) # horizontal force T = np.sqrt(F2[0]**2+F2[1]**2+F2[2]**2) # total tension force - if not F[j] or T>np.sqrt(F[j][0]**2+F[j][1]**2+F[j][2]**2): + if F[j] is None or T>np.sqrt(F[j][0]**2+F[j][1]**2+F[j][2]**2): F[j] = F2 # max load on anchor # save anchor load information anch.loads['Hm'] = H anch.loads['Vm'] = F[j][2] anch.loads['thetam'] = np.degrees(np.arctan(anch.loads['Vm']/anch.loads['Hm'])) #[deg] anch.loads['mudline_load_type'] = 'max' - anch.loads['info'] = f'determined from arrayWatchCircle() with DAF of {DAF}' + anch.loads.update(info) # get tensions on mooring line for j, moor in enumerate(self.mooringList.values()): - MBLA = float(moor.ss.lineList[0].type['MBL']) - MBLB = float(moor.ss.lineList[-1].type['MBL']) - # print(MBLA,MBLB,moor.ss.TA,moor.ss.TB,MBLA/moor.ss.TA,MBLB/moor.ss.TB,abs(MBLA/moor.ss.TA),abs(MBLB/moor.ss.TB)) - MTSF = min([abs(MBLA/(moor.ss.TA*DAF)),abs(MBLB/(moor.ss.TB*DAF))]) - # atenMax[j], btenMax[j] = moor.updateTensions() - if not minTenSF[j] or minTenSF[j]>MTSF: - minTenSF[j] = deepcopy(MTSF) - moor.loads['TAmax'] = moor.ss.TA*DAF - moor.loads['TBmax'] = moor.ss.TB*DAF - moor.loads['info'] = f'determined from arrayWatchCircle() with DAF of {DAF}' - moor.safety_factors['tension'] = minTenSF[j] - moor.safety_factors['analysisType'] = 'quasi-static (MoorPy)' - - # store max. laid length of the mooring lines + lBot = 0 + moor.updateTensions(DAF=DAF) + moor.updateSafetyFactors(info=info) if moor_seabed_disturbance: - lBot = 0 - for line in moor.ss.lineList: - lBot += line.LBot + for sec in moor.sections(): + lBot += sec.mpLine.LBot lBots[j] = max(lBots[j], lBot) + # get tensions and curvature on cables for j,cab in enumerate(self.cableList.values()): dcs = [a for a in cab.subcomponents if isinstance(a,DynamicCable)] # dynamic cables in this cable - ndc = len(dcs) # number of dynamic cable objects in this single cable object - CminTenSF[j] = [None]*ndc - minCurvSF[j] = [None]*ndc - minSag[j] = [None]*ndc + cab.updateTensions(DAF=DAF) + cab.updateSafetyFactors(info=info) + minCurvSF[j] = [None]*len(dcs) + CminTenSF[j] = [None]*len(dcs) if dcs[0].ss: - for jj,dc in enumerate(dcs): - MBLA = dc.ss.lineList[0].type['MBL'] - MBLB = dc.ss.lineList[-1].type['MBL'] - CMTSF = min([abs(MBLA/dc.ss.TA),abs(MBLB/dc.ss.TB)]) - if not CminTenSF[j][jj] or CminTenSF[j][jj]>CMTSF: - CminTenSF[j][jj] = deepcopy(CMTSF) - dc.loads['TAmax'] = dc.ss.TA*DAF - dc.loads['TBmax'] = dc.ss.TB*DAF - dc.loads['info'] = f'determined from arrayWatchCircle() with DAF of {DAF}' - dc.safety_factors['tension'] = CminTenSF[j][jj] - # CatenMax[j], CbtenMax[j] = cab.updateTensions() + + for jj,dc in enumerate(dcs): + CminTenSF[j][jj] = dc.safety_factors['tension'] dc.ss.calcCurvature() mCSF = dc.ss.getMinCurvSF() if not minCurvSF[j][jj] or minCurvSF[j][jj]>mCSF: minCurvSF[j][jj] = mCSF dc.safety_factors['curvature'] = minCurvSF[j][jj] - # # determine number of buoyancy sections - # nb = len(dc.dd['buoyancy_sections']) - # m_s = [] - # for k in range(0,nb): - # m_s.append(dc.ss.getSag(2*k)) - # mS = min(m_s) - # if not minSag[j][jj] or minSag[j][jj]1 for at in atts[is_anch]]): # we have a shared anchor here, put mooring in array_mooring - headA = 'None' # no heading at end A because it's an anchor - # append mooring line to array_moor section - arrayMoor.append([current_config, atts[0].id, atts[1].id, headA,headB,int(0)]) + if fairleads: + # append mooring line to array_moor section + arrayMoor.append([current_config, + atts[0].id, + atts[1].id, + 'None', + flB]) + else: + # append mooring line to array_moor section + arrayMoor.append([current_config, + atts[0].id, + atts[1].id]) else: # not shared anchor or shared mooring, add line to mooring system - msys.append([current_config, - np.round(headB,2), - mapAnchNames[atts[is_anch][0].id], - 0]) + if fairleads: + msys.append([current_config, + np.round(headB,2), + mapAnchNames[atts[is_anch][0].id], + flB]) + else: + msys.append([current_config, + np.round(headB,2), + mapAnchNames[atts[is_anch][0].id]]) # check if an existing mooring system matches the current if len(msys)>0: @@ -4004,19 +4406,7 @@ def unload(self,file='project.yaml'): else: mname = 0 - if not 'type' in pf.dd: - pf_type_info = [pf.rFair, pf.zFair, pf.entity] - if not pf_type_info in pf_types: - pf_types.append(pf_type_info) - if not self.platformTypes: - self.platformTypes = [] - pf.dd['type'] = len(self.platformTypes) - self.platformTypes.append({'rFair': pf.rFair, - 'zFair': pf.zFair, - 'type': pf.entity}) - else: - tt = [n for n,ps in enumerate(pf_types) if ps==pf_type_info] - pf.dd['type'] = tt[0] + @@ -4062,12 +4452,12 @@ def unload(self,file='project.yaml'): # # build out mooring and anchor sections anchKeys = ['ID','type','x','y','embedment'] - lineKeys = ['MooringConfigID','endA','endB','headingA','headingB','lengthAdjust'] + lineKeys = ['MooringConfigID','endA','endB','fairleadA','fairleadB'] - msyskeys = ['MooringConfigID','heading','anchorType','lengthAdjust'] + msyskeys = ['MooringConfigID','heading','anchorType','fairlead'] moor_systems = {} for name,sys in mscs.items(): - moor_systems[name] = {'keys':msyskeys, + moor_systems[name] = {'keys':msyskeys[:len(sys[0])], 'data':sys} # set up mooring configs, connector and section types dictionaries @@ -4078,48 +4468,72 @@ def unload(self,file='project.yaml'): sUnique = [] for j,conf in enumerate(allconfigs): sections = [] - # iterate through sections - for i in range(len(conf['sections'])): - # add connector if it isn't empty - if not conf['connectors'][i]['m'] == 0 or not conf['connectors'][i]['CdA'] == 0 or not conf['connectors'][i]['v'] == 0: - # this is not an empty connector - if not 'type' in conf['connectors'][i]: - # make a new connector type - connTypes[str(int(len(connTypes)))] = dict(conf['connectors'][i]) - ctn = str(int(len(connTypes)-1)) # connector type name - else: - ctn = str(conf['connectors'][i]['type']) - connTypes[ctn] = dict(conf['connectors'][i]) - - sections.append({'connectorType':ctn}) - # add section info - stm = conf['sections'][i]['type']['material'] # section type material - stw = conf['sections'][i]['type']['w'] # section type weight - - sKey = (stm, stw) - if sKey not in sUnique: - sUnique.append(sKey) - conf['sections'][i]['type']['name'] = sIdx - stn = conf['sections'][i]['type']['name'] # section type name - secTypes[stn] = dict(conf['sections'][i]['type']) - #secTypes[stn] = cleanDataTypes(secTypes[stn]) - sIdx += 1 - - stn = sUnique.index(sKey) - sections.append({'type':stn,'length':float(conf['sections'][i]['L'])}) - - # add last connector if needed - if not conf['connectors'][i+1]['m'] == 0 or not conf['connectors'][i+1]['CdA'] == 0 or not conf['connectors'][i+1]['v'] == 0: - # this is not an empty connector - if not 'type' in conf['connectors'][i+1]: - # make a new connector type - #conf['connectors'][i+1] = cleanDataTypes(conf['connectors'][i+1]) - connTypes[str(len(connTypes))] = conf['connectors'][i+1] - ctn = str(int(len(connTypes)-1)) + # iterate through subcomponents + for comp in conf['subcomponents']: + if isinstance(comp,list): + sections.append({'subsections':[]}) + for subcomp in comp: + if isinstance(subcomp,list): + sections[-1]['subsections'].append([]) + for sc in subcomp: + if 'L' in sc: + # add section info + stm = sc['type']['material'] # section type material + stw = sc['type']['w'] # section type weight + + sKey = (stm, stw) + if sKey not in sUnique: + sUnique.append(sKey) + sc['type']['name'] = sIdx + stn = sc['type']['name'] # section type name + secTypes[stn] = dict(sc['type']) + sIdx += 1 + + stn = sUnique.index(sKey) + sections[-1]['subsections'][-1].append({'type':stn,'length':float(sc['L'])}) + else: + if not sc['m'] == 0 or not sc['CdA'] == 0 or not sc['v'] == 0: + # this is not an empty connector + if not 'type' in sc: + # make a new connector type + connTypes[str(int(len(connTypes)))] = dict(sc) + ctn = str(int(len(connTypes)-1)) # connector type name + else: + ctn = str(sc['type']) + connTypes[ctn] = dict(sc) + sections[-1]['subsections'][-1].append({'connectorType':ctn}) + else: - ctn = conf['connectors'][i+1]['type'] - connTypes[ctn] = dict(conf['connectors'][i+1]) - sections.append({'connectorType':ctn}) + if 'L' in comp: + # add section info + stm = comp['type']['material'] # section type material + stw = comp['type']['w'] # section type weight + + sKey = (stm, stw) + if sKey not in sUnique: + sUnique.append(sKey) + comp['type']['name'] = sIdx + stn = comp['type']['name'] # section type name + secTypes[stn] = dict(comp['type']) + sIdx += 1 + + stn = sUnique.index(sKey) + sections.append({'type':stn,'length':float(comp['L'])}) + else: + # add connector if it isn't empty + if not comp['m'] == 0 or not comp['CdA'] == 0 or not comp['v'] == 0: + # this is not an empty connector + if not 'type' in comp: + # make a new connector type + connTypes[str(int(len(connTypes)))] = dict(comp) + ctn = str(int(len(connTypes)-1)) # connector type name + else: + ctn = str(comp['type']) + connTypes[ctn] = dict(comp) + + sections.append({'connectorType':ctn}) + + # put mooring config dictionary together mooringConfigs[str(j)] = {'name':str(j),'span':float(conf['span']),'sections':sections} @@ -4144,6 +4558,10 @@ def unload(self,file='project.yaml'): statcab = 'None' dynCabs = [None,None] burial = None + jA = None + jB = None + jtubesA = [att['obj'].id for att in endA.attachments.values() if isinstance(att['obj'], Jtube)] + jtubesB = [att['obj'].id for att in endB.attachments.values() if isinstance(att['obj'], Jtube)] for kk,sub in enumerate(cab.subcomponents): currentConfig = {} @@ -4187,6 +4605,15 @@ def unload(self,file='project.yaml'): elif isinstance(sub,DynamicCable): + jtube = [att.id for att in sub.attached_to if isinstance(att,Jtube)] + # grab index of fairlead list from end B + + for jj in jtube: + if jj.attached_to == endA: + jA = jtubesA.index(jj)+1 + + elif jj.attached_to == endB: + jB = jtubesB.index(jj)+1 # pull out cable config and compare it to existing cableConfigs ct = sub.dd['type'] # static or dynamic ctw = sub.dd['cable_type']['w'] @@ -4252,9 +4679,11 @@ def unload(self,file='project.yaml'): jtn = 'joint_'+str(jIdx) bs.append({'type':jtn}) # create current cable config dictionary - currentConfig = {ctk:ctn,'A':ctA,'rJTube':sub.dd['rJTube'], + currentConfig = {ctk:ctn,'A':ctA, 'span':sub.dd['span'],'length':sub.L, 'voltage':sub.dd['cable_type']['voltage'],'sections':bs} + if 'rJTube' in sub.dd: + currentConfig['rJTube'] = sub.dd['rJTube'] # check if current cable config already exists in cable configs dictionary if currentConfig in cableConfigs.values(): ccn = [key for key,val in cableConfigs.items() if val==currentConfig][0] # get cable config key @@ -4275,9 +4704,13 @@ def unload(self,file='project.yaml'): endAdict = {'attachID':endA.id, 'heading':headA, 'dynamicID':dynCabs[0] if dynCabs[0] else 'None'} + if jA: + endAdict['JTube'] = jA endBdict = {'attachID':endB.id, 'heading':headB, 'dynamicID':dynCabs[1] if dynCabs[1] else 'None'} + if jB: + endBdict['JTube'] = jB cables.append({'name':cid,'endA':endAdict,'endB':endBdict,'type':statcab}) @@ -4293,11 +4726,15 @@ def unload(self,file='project.yaml'): # create master output dictionary for yaml + if arrayMoor: + arrayMooring = {'anchor_keys':anchKeys, 'anchor_data':arrayAnch, + 'line_keys':lineKeys[:len(arrayMoor[0])], 'line_data':arrayMoor} + else: + arrayMooring = {} output = {'site':site, 'array':{'keys':arrayKeys,'data':arrayData}, pfkey:pfTypes, 'topsides': topList, - 'array_mooring':{'anchor_keys':anchKeys, 'anchor_data':arrayAnch, - 'line_keys':lineKeys, 'line_data':arrayMoor}, + 'array_mooring':arrayMooring, 'mooring_systems':moor_systems, 'mooring_line_configs':mooringConfigs, 'mooring_line_types':secTypes, @@ -5006,7 +5443,7 @@ def style_it(sheet, row, col_start, col_end, fill_color="FFFF00"): ''' if __name__ == '__main__': - + ''' project = Project() project.loadSoil(filename='tests/soil_sample.txt') # create project class instance from yaml file @@ -5022,4 +5459,25 @@ def style_it(sheet, row, col_start, col_end, fill_color="FFFF00"): project.plot2d(plot_boundary=False) # this should also plot the watch circles/envelopes! + ''' + + + # point to location of yaml file with uniform array info + filename = '../Examples/OntologySample600m_shared.yaml' # yaml file for project + + # load in yaml + project = Project(file=filename,raft=False) + + + project.getMoorPyArray() + + # plot in 2d and 3d + #project.plot2d() + #project.plot3d(fowt=True) + + #plt.show() + + + # ---- + plt.show() \ No newline at end of file diff --git a/famodel/turbine/turbine.py b/famodel/turbine/turbine.py index 6267aec2..00138376 100644 --- a/famodel/turbine/turbine.py +++ b/famodel/turbine/turbine.py @@ -117,4 +117,3 @@ def getForces(self, yaw, pitch=0): self.grid_pitch, self.grid_f6) return f6 - \ No newline at end of file diff --git a/tests/mooring_ontology.yaml b/tests/mooring_ontology.yaml new file mode 100644 index 00000000..aa06dd1d --- /dev/null +++ b/tests/mooring_ontology.yaml @@ -0,0 +1,158 @@ +type: draft/example of floating array ontology under construction +name: +comments: +# Site condition information +site: + general: + water_depth : 600 # [m] uniform water depth + rho_water : 1025.0 # [kg/m^3] water density + rho_air : 1.225 # [kg/m^3] air density + mu_air : 1.81e-05 # air dynamic viscosity + #... + + +# ----- Array-level inputs ----- + +# Wind turbine array layout +array: + keys : [ID, topsideID, platformID, mooringID, x_location, y_location, heading_adjust] + data : # ID# ID# ID# [m] [m] [deg] + - [FOWT1, 0, 1, ms3, 0, 0, 180 ] # 2 array, shared moorings + - [FOWT2, 0, 1, ms2, 1600, 0, 0 ] + - [FOWT3, 0, 1, ms1, 0, 1656, 180 ] + - [FOWT4, 0, 1, ms4, 1600, 1600, 180] + + + +# Array-level mooring system (in addition to any per-turbine entries later) +array_mooring: + anchor_keys : + [ID, type, x, y, embedment ] + anchor_data : + - [ Anch1, suction_pile1, -828 , 828 , 2 ] + + line_keys : + [MooringConfigID , endA, endB, fairleadA, fairleadB] + line_data : + - [ rope_shared , FOWT1, FOWT2, 3, 3] + - [ rope_1 , Anch1, FOWT1, NONE, 2, NONE] + - [ rope_1 , Anch1, FOWT3, NONE, 1, NONE] + +platforms: + + - fairleads : + # list of fairlead coordinates for the platform relative to platform coordinate and 0-degree heading + - name: fairlead1 + r_rel: [58,0,-14] + headings: [30, 150, 270] # headings in degrees for the fairlead (if multiple headings, the fairlead will be repeated for each heading) + type : FOWT + + +# ----- Mooring system ----- + +# Mooring system descriptions (each for an individual FOWT with no sharing) +mooring_systems: + + ms1: + name: 3-line semi-taut polyester mooring system with one line shared anchor + + keys: [MooringConfigID, heading, anchorType, fairlead] + data: + - [ rope_1, 270 , suction_pile1, 3 ] + - [ rope_1, 135 , suction_pile1, 2 ] + + ms2: + name: 2-line semitaut with a third shared line + + keys: [MooringConfigID, heading, anchorType, fairlead] + data: + - [ rope_1, 45 , suction_pile1, 1 ] + - [ rope_1, 135 , suction_pile1, 2 ] + + ms3: + name: 3-line semi-taut polyester mooring system with one line shared anchor and one shared line + + keys: [MooringConfigID, heading, anchorType, fairlead] + data: + - [ rope_1, 45 , suction_pile1, 1 ] + + ms4: + name: 3 line taut poly mooring system + + keys: [MooringConfigID, heading, anchorType, fairlead] + data: + - [ rope_1, 45 , suction_pile1, 1 ] + - [ rope_1, 135 , suction_pile1, 2 ] + - [ rope_1, 270 , suction_pile1, 3 ] + + +# Mooring line configurations +mooring_line_configs: + + rope_1: # mooring line configuration identifier + + name: rope configuration 1 # descriptive name + + span: 1131.37 + + + sections: #in order from anchor to fairlead + - type: chain_155mm + length: 20 + - type: rope # ID of a mooring line section type + length: 1170 # [m] usntretched length of line section + adjustable: True # flags that this section could be adjusted to accommodate different spacings... + + rope_shared: + name: shared rope + + span: 1484 + + + sections: + - type: rope + length: 150 + - connectorType: clump_weight_80 + - type: rope + length: 1172 + - connectorType: clump_weight_80 + - type: rope + length: 150 + + +# Mooring line cross-sectional properties +mooring_line_types: + + rope: + d_nom: 0.2246 + d_vol: 0.1797 + m: 34.85 + EA: 4.761e7 + MBL: 11.75e6 + material: rope + + chain_155mm: + d_nom: 0.155 # [m] nominal diameter + d_vol: 0.279 # [m] volume-equivalent diameter + m: 480.9 # [kg/m] mass per unit length (linear density) + EA: 2058e6 # [N] quasi-static stiffness + MBL: 25.2e6 # [N] minimum breaking load + cost: 1486 # [$/m] cost per unit length + material: chain # [-] material composition descriptor + material details: R3 studless + +# Mooring connector properties +mooring_connector_types: + + clump_weight_80: + m : 80000 # [kg] + v : 0.0 # [m^3] + + +# Anchor type properties +anchor_types: + suction_pile1: + type : suction_pile + L : 16.4 # length of pile [m] + D : 5.45 # diameter of pile [m] + zlug : 9.32 # embedded depth of padeye [m] \ No newline at end of file diff --git a/tests/mooring_ontology_parallels.yaml b/tests/mooring_ontology_parallels.yaml new file mode 100644 index 00000000..e98647ae --- /dev/null +++ b/tests/mooring_ontology_parallels.yaml @@ -0,0 +1,176 @@ +type: draft/example of floating array ontology under construction +name: +comments: +# Site condition information +site: + general: + water_depth : 600 # [m] uniform water depth + rho_water : 1025.0 # [kg/m^3] water density + rho_air : 1.225 # [kg/m^3] air density + mu_air : 1.81e-05 # air dynamic viscosity + #... + + +# ----- Array-level inputs ----- + +# Wind turbine array layout +array: + keys : [ID, topsideID, platformID, mooringID, x_location, y_location, heading_adjust] + data : # ID# ID# ID# [m] [m] [deg] + - [FOWT1, 0, 1, ms3, 0, 0, 180 ] # 2 array, shared moorings + - [FOWT2, 0, 1, ms2, 1600, 0, 0 ] + - [FOWT3, 0, 1, ms1, 0, 1656, 180 ] + - [FOWT4, 0, 1, ms4, 1600, 1600, 180] + + + +# Array-level mooring system (in addition to any per-turbine entries later) +array_mooring: + anchor_keys : + [ID, type, x, y, embedment ] + anchor_data : + - [ Anch1, suction_pile1, -828 , 828 , 2 ] + + line_keys : + [MooringConfigID , endA, endB, fairleadA, fairleadB] + line_data : + - [ rope_shared , FOWT1, FOWT2, 3, 3] + - [ rope_1 , Anch1, FOWT1, NONE, 2, NONE] + - [ rope_1 , Anch1, FOWT3, NONE, 1, NONE] + +platforms: + + - fairleads : + # list of fairlead coordinates for the platform relative to platform coordinate and 0-degree heading + - name: fairlead1 + r_rel: [58,0,-14] + headings: [30, 150, 270, 25, 35] # headings in degrees for the fairlead (if multiple headings, the fairlead will be repeated for each heading) + + type : FOWT + + +# ----- Mooring system ----- + +# Mooring system descriptions (each for an individual FOWT with no sharing) +mooring_systems: + + ms1: + name: 3-line semi-taut polyester mooring system with one line shared anchor + + keys: [MooringConfigID, heading, anchorType, fairlead] + data: + - [ rope_1, 270 , suction_pile1, 3 ] + - [ rope_1, 135 , suction_pile1, 2 ] + + ms2: + name: 2-line semitaut with a third shared line + + keys: [MooringConfigID, heading, anchorType, fairlead] + data: + - [ rope_1_bridle, 45 , suction_pile1, [4,5] ] + - [ rope_1, 135 , suction_pile1, 2 ] + + ms3: + name: 3-line semi-taut polyester mooring system with one line shared anchor and one shared line + + keys: [MooringConfigID, heading, anchorType, fairlead] + data: + - [ rope_1, 45 , suction_pile1, 1 ] + + ms4: + name: 3 line taut poly mooring system + + keys: [MooringConfigID, heading, anchorType, fairlead] + data: + - [ rope_1, 45 , suction_pile1, 1 ] + - [ rope_1, 135 , suction_pile1, 2 ] + - [ rope_1, 270 , suction_pile1, 3 ] + + +# Mooring line configurations +mooring_line_configs: + + rope_1_bridle: # mooring line configuration identifier + + name: rope configuration 1 # descriptive name + + span: 1131.37 + + + sections: #in order from anchor to fairlead + - type: rope # ID of a mooring line section type + length: 1170 # [m] usntretched length of line section + adjustable: True # flags that this section could be adjusted to accommodate different spacings... + - subsections: + - - type: chain_155mm + length: 20 + - - type: chain_155mm + length: 20 + + rope_1: # mooring line configuration identifier + + name: rope configuration 1 # descriptive name + + span: 1131.37 + + + sections: #in order from anchor to fairlead + - type: chain_155mm + length: 20 + - type: rope # ID of a mooring line section type + length: 1170 # [m] usntretched length of line section + adjustable: True # flags that this section could be adjusted to accommodate different spacings... + + rope_shared: + name: shared rope + + span: 1484 + + + sections: + - type: rope + length: 150 + - connectorType: clump_weight_80 + - type: rope + length: 1172 + - connectorType: clump_weight_80 + - type: rope + length: 150 + + +# Mooring line cross-sectional properties +mooring_line_types: + + rope: + d_nom: 0.2246 + d_vol: 0.1797 + m: 34.85 + EA: 4.761e7 + MBL: 11.75e6 + material: rope + + chain_155mm: + d_nom: 0.155 # [m] nominal diameter + d_vol: 0.279 # [m] volume-equivalent diameter + m: 480.9 # [kg/m] mass per unit length (linear density) + EA: 2058e6 # [N] quasi-static stiffness + MBL: 25.2e6 # [N] minimum breaking load + cost: 1486 # [$/m] cost per unit length + material: chain # [-] material composition descriptor + material details: R3 studless + +# Mooring connector properties +mooring_connector_types: + + clump_weight_80: + m : 80000 # [kg] + v : 0.0 # [m^3] + + +# Anchor type properties +anchor_types: + suction_pile1: + type : suction_pile + L : 16.4 # length of pile [m] + D : 5.45 # diameter of pile [m] + zlug : 9.32 # embedded depth of padeye [m] \ No newline at end of file diff --git a/tests/platform_ontology.yaml b/tests/platform_ontology.yaml new file mode 100644 index 00000000..0dae6a53 --- /dev/null +++ b/tests/platform_ontology.yaml @@ -0,0 +1,1245 @@ +type: draft/example of floating array ontology under construction +name: +comments: +# Site condition information +site: + general: + water_depth : 600 # [m] uniform water depth + rho_water : 1025.0 # [kg/m^3] water density + rho_air : 1.225 # [kg/m^3] air density + mu_air : 1.81e-05 # air dynamic viscosity + #... + +# ----- Array-level inputs ----- + +# Wind turbine array layout +array: + keys : [ID, topsideID, platformID, mooringID, x_location, y_location, heading_adjust] + data : # ID# ID# ID# [m] [m] [deg] + - [FOWT1, 0, 1, 0, 0, 0, 180 ] # 2 array, shared moorings + - [FOWT2, 1, 1, ms1, 1600, 0, 0 ] + + + +# ----- turbines and platforms ----- + +topsides: + + - type : Turbine + mRNA : 991000 # [kg] RNA mass + IxRNA : 0 # [kg-m2] RNA moment of inertia about local x axis (assumed to be identical to rotor axis for now, as approx) [kg-m^2] + IrRNA : 0 # [kg-m2] RNA moment of inertia about local y or z axes [kg-m^2] + xCG_RNA : 0 # [m] x location of RNA center of mass [m] (Actual is ~= -0.27 m) + hHub : 150.0 # [m] hub height above water line [m] + Fthrust : 1500.0E3 # [N] temporary thrust force to use + + I_drivetrain: 318628138.0 # full rotor + drivetrain inertia as felt on the high-speed shaft + + nBlades : 3 # number of blades + Zhub : 150.0 # hub height [m] + Rhub : 3.97 # hub radius [m] + precone : 4.0 # [deg] + shaft_tilt : 6.0 # [deg] + overhang : -12.0313 # [m] + aeroServoMod : 2 # 0 aerodynamics off; 1 aerodynamics on (no control); 2 aerodynamics and control on + + blade: + precurveTip : -3.9999999999999964 # + presweepTip : 0.0 # + Rtip : 120.96999999936446 # rotor radius + + # r chord theta precurve presweep + geometry: + - [ 8.004, 5.228, 15.474, 0.035, 0.000 ] + - [ 12.039, 5.321, 14.692, 0.084, 0.000 ] + - [ 16.073, 5.458, 13.330, 0.139, 0.000 ] + - [ 20.108, 5.602, 11.644, 0.192, 0.000 ] + - [ 24.142, 5.718, 9.927, 0.232, 0.000 ] + - [ 28.177, 5.767, 8.438, 0.250, 0.000 ] + - [ 32.211, 5.713, 7.301, 0.250, 0.000 ] + - [ 36.246, 5.536, 6.232, 0.246, 0.000 ] + - [ 40.280, 5.291, 5.230, 0.240, 0.000 ] + - [ 44.315, 5.035, 4.348, 0.233, 0.000 ] + - [ 48.349, 4.815, 3.606, 0.218, 0.000 ] + - [ 52.384, 4.623, 2.978, 0.178, 0.000 ] + - [ 56.418, 4.432, 2.423, 0.100, 0.000 ] + - [ 60.453, 4.245, 1.924, 0.000, 0.000 ] + - [ 64.487, 4.065, 1.467, -0.112, 0.000 ] + - [ 68.522, 3.896, 1.056, -0.244, 0.000 ] + - [ 72.556, 3.735, 0.692, -0.415, 0.000 ] + - [ 76.591, 3.579, 0.355, -0.620, 0.000 ] + - [ 80.625, 3.425, 0.019, -0.846, 0.000 ] + - [ 84.660, 3.268, -0.358, -1.080, 0.000 ] + - [ 88.694, 3.112, -0.834, -1.330, 0.000 ] + - [ 92.729, 2.957, -1.374, -1.602, 0.000 ] + - [ 96.763, 2.800, -1.848, -1.895, 0.000 ] + - [ 100.798, 2.637, -2.136, -2.202, 0.000 ] + - [ 104.832, 2.464, -2.172, -2.523, 0.000 ] + - [ 108.867, 2.283, -2.108, -2.864, 0.000 ] + - [ 112.901, 2.096, -1.953, -3.224, 0.000 ] + - [ 116.936, 1.902, -1.662, -3.605, 0.000 ] + # station(rel) airfoil name + airfoils: + - [ 0.00000, circular ] + - [ 0.02000, circular ] + - [ 0.15000, SNL-FFA-W3-500 ] + - [ 0.24517, FFA-W3-360 ] + - [ 0.32884, FFA-W3-330blend ] + - [ 0.43918, FFA-W3-301 ] + - [ 0.53767, FFA-W3-270blend ] + - [ 0.63821, FFA-W3-241 ] + - [ 0.77174, FFA-W3-211 ] + - [ 1.00000, FFA-W3-211 ] + + + airfoils: + - name : circular # + relative_thickness : 1.0 # + data: # alpha c_l c_d c_m + - [ -179.9087, 0.00010, 0.35000, -0.00010 ] + - [ 179.9087, 0.00010, 0.35000, -0.00010 ] + - name : SNL-FFA-W3-500 # + relative_thickness : 0.5 # + data: # alpha c_l c_d c_m + - [ -179.9660, 0.00000, 0.08440, 0.00000 ] + - [ -170.0000, 0.44190, 0.08440, 0.31250 ] + - [ -160.0002, 0.88370, 0.12680, 0.28310 ] + - [ -149.9998, 0.96740, 0.29270, 0.26320 ] + - [ -139.9999, 0.78010, 0.49700, 0.20480 ] + - [ -130.0001, 0.62930, 0.71610, 0.19320 ] + - [ -120.0003, 0.47850, 0.92460, 0.20080 ] + - [ -109.9999, 0.31890, 1.09850, 0.21360 ] + - [ -100.0000, 0.15530, 1.21820, 0.22210 ] + - [ -90.0002, 0.00000, 1.27070, 0.21980 ] + - [ -79.9998, -0.15530, 1.21820, 0.19600 ] + - [ -70.0000, -0.31890, 1.09850, 0.16350 ] + - [ -60.0001, -0.47840, 0.92460, 0.12850 ] + - [ -49.9997, -0.62930, 0.71610, 0.09650 ] + - [ -39.9999, -0.78010, 0.49700, 0.07160 ] + - [ -30.0001, -0.96740, 0.29270, 0.05220 ] + - [ -20.0002, -1.02810, 0.14990, -0.00630 ] + - [ -19.7499, -1.02430, 0.14720, -0.00890 ] + - [ -19.2502, -1.00520, 0.14470, -0.00990 ] + - [ -18.9999, -0.99710, 0.14330, -0.01050 ] + - [ -18.7500, -1.00520, 0.14030, -0.01100 ] + - [ -18.5002, -0.99950, 0.13860, -0.01160 ] + - [ -18.2499, -0.99080, 0.13730, -0.01200 ] + - [ -18.0000, -0.98150, 0.13600, -0.01260 ] + - [ -17.4998, -0.97640, 0.13220, -0.01350 ] + - [ -17.2500, -0.97050, 0.13060, -0.01390 ] + - [ -17.0002, -0.96550, 0.12900, -0.01430 ] + - [ -16.7498, -0.96620, 0.12680, -0.01470 ] + - [ -16.5000, -0.95440, 0.12580, -0.01510 ] + - [ -16.2502, -0.94440, 0.12460, -0.01550 ] + - [ -15.9998, -0.94050, 0.12290, -0.01580 ] + - [ -15.7500, -0.94330, 0.12060, -0.01610 ] + - [ -15.5002, -0.93300, 0.11950, -0.01640 ] + - [ -15.2498, -0.92110, 0.11850, -0.01680 ] + - [ -14.7502, -0.91580, 0.11500, -0.01730 ] + - [ -14.4998, -0.90700, 0.11380, -0.01750 ] + - [ -14.2500, -0.89590, 0.11270, -0.01780 ] + - [ -14.0002, -0.89260, 0.11100, -0.01810 ] + - [ -13.7498, -0.88080, 0.11000, -0.01840 ] + - [ -13.5000, -0.87220, 0.10890, -0.01860 ] + - [ -13.2502, -0.86600, 0.10750, -0.01880 ] + - [ -12.9998, -0.86260, 0.10590, -0.01880 ] + - [ -12.7500, -0.84890, 0.10510, -0.01920 ] + - [ -12.5002, -0.83630, 0.10420, -0.01940 ] + - [ -12.2498, -0.83630, 0.10230, -0.01940 ] + - [ -12.0000, -0.82710, 0.10130, -0.01960 ] + - [ -11.7502, -0.81410, 0.10040, -0.01980 ] + - [ -11.4998, -0.80040, 0.09970, -0.02000 ] + - [ -11.0002, -0.78900, 0.09710, -0.01990 ] + - [ -10.7498, -0.78620, 0.09560, -0.01960 ] + - [ -10.5000, -0.77470, 0.09480, -0.01940 ] + - [ -10.2502, -0.77010, 0.09400, -0.01840 ] + - [ -9.9998, -0.76740, 0.09250, -0.01830 ] + - [ -9.7500, -0.75060, 0.09170, -0.01920 ] + - [ -9.5002, -0.72900, 0.09120, -0.02050 ] + - [ -9.2498, -0.70950, 0.09020, -0.02240 ] + - [ -9.0000, -0.68550, 0.08950, -0.02470 ] + - [ -8.7502, -0.65900, 0.08910, -0.02670 ] + - [ -8.4998, -0.63190, 0.08870, -0.02870 ] + - [ -8.2500, -0.60190, 0.08790, -0.03200 ] + - [ -8.0002, -0.57180, 0.08750, -0.03450 ] + - [ -7.7498, -0.54240, 0.08730, -0.03670 ] + - [ -7.5000, -0.50980, 0.08680, -0.03990 ] + - [ -7.2502, -0.47670, 0.08640, -0.04300 ] + - [ -6.9998, -0.44540, 0.08620, -0.04530 ] + - [ -6.7500, -0.41420, 0.08600, -0.04760 ] + - [ -6.5002, -0.37910, 0.08560, -0.05100 ] + - [ -6.2498, -0.34600, 0.08530, -0.05380 ] + - [ -6.0000, -0.31440, 0.08520, -0.05600 ] + - [ -5.7502, -0.28170, 0.08500, -0.05860 ] + - [ -5.4998, -0.24610, 0.08470, -0.06190 ] + - [ -5.2500, -0.21330, 0.08460, -0.06440 ] + - [ -5.0002, -0.18270, 0.08450, -0.06630 ] + - [ -4.7498, -0.14940, 0.08430, -0.06880 ] + - [ -4.5000, -0.11580, 0.08420, -0.07150 ] + - [ -4.2502, -0.08370, 0.08400, -0.07370 ] + - [ -3.9998, -0.05290, 0.08400, -0.07560 ] + - [ -3.7500, -0.02250, 0.08390, -0.07740 ] + - [ -3.5002, 0.00890, 0.08380, -0.07930 ] + - [ -3.2498, 0.03920, 0.08380, -0.08110 ] + - [ -3.0000, 0.06860, 0.08380, -0.08260 ] + - [ -2.7502, 0.09740, 0.08380, -0.08380 ] + - [ -2.4998, 0.12600, 0.08380, -0.08520 ] + - [ -2.2500, 0.15550, 0.08380, -0.08670 ] + - [ -2.0002, 0.18530, 0.08380, -0.08830 ] + - [ -1.7498, 0.21460, 0.08370, -0.08970 ] + - [ -1.5000, 0.24300, 0.08370, -0.09100 ] + - [ -1.2502, 0.27130, 0.08380, -0.09210 ] + - [ -0.9998, 0.30060, 0.08380, -0.09360 ] + - [ -0.7500, 0.32950, 0.08380, -0.09490 ] + - [ -0.5002, 0.35780, 0.08380, -0.09610 ] + - [ -0.2498, 0.38570, 0.08380, -0.09720 ] + - [ 0.0000, 0.41350, 0.08380, -0.09830 ] + - [ 0.2298, 0.44250, 0.08390, -0.09950 ] + - [ 0.4698, 0.47150, 0.08390, -0.10080 ] + - [ 0.7002, 0.50030, 0.08390, -0.10190 ] + - [ 0.9402, 0.52860, 0.08400, -0.10290 ] + - [ 1.1700, 0.55670, 0.08400, -0.10400 ] + - [ 1.3997, 0.58500, 0.08410, -0.10500 ] + - [ 1.6398, 0.61350, 0.08410, -0.10610 ] + - [ 1.8701, 0.64170, 0.08420, -0.10720 ] + - [ 2.1102, 0.66970, 0.08420, -0.10820 ] + - [ 2.3400, 0.69750, 0.08430, -0.10910 ] + - [ 2.5697, 0.72510, 0.08430, -0.11000 ] + - [ 2.8098, 0.75280, 0.08440, -0.11090 ] + - [ 3.0401, 0.78070, 0.08450, -0.11190 ] + - [ 3.2802, 0.80830, 0.08460, -0.11280 ] + - [ 3.5099, 0.83580, 0.08460, -0.11370 ] + - [ 3.7403, 0.86310, 0.08470, -0.11460 ] + - [ 3.9798, 0.89020, 0.08470, -0.11530 ] + - [ 4.2101, 0.91730, 0.08480, -0.11610 ] + - [ 4.4502, 0.94440, 0.08490, -0.11700 ] + - [ 4.6799, 0.97130, 0.08500, -0.11780 ] + - [ 4.9102, 0.99810, 0.08510, -0.11850 ] + - [ 5.1497, 1.02490, 0.08520, -0.11920 ] + - [ 5.3801, 1.05150, 0.08530, -0.11990 ] + - [ 5.6201, 1.07790, 0.08530, -0.12060 ] + - [ 5.8499, 1.10410, 0.08540, -0.12120 ] + - [ 6.0802, 1.13020, 0.08560, -0.12180 ] + - [ 6.3197, 1.15600, 0.08570, -0.12240 ] + - [ 6.5501, 1.18180, 0.08580, -0.12300 ] + - [ 6.7901, 1.20760, 0.08590, -0.12350 ] + - [ 7.0199, 1.23340, 0.08600, -0.12400 ] + - [ 7.2502, 1.25890, 0.08610, -0.12450 ] + - [ 7.4903, 1.28410, 0.08620, -0.12500 ] + - [ 7.7200, 1.30880, 0.08640, -0.12540 ] + - [ 7.9601, 1.33310, 0.08650, -0.12570 ] + - [ 8.1899, 1.35700, 0.08670, -0.12590 ] + - [ 8.4202, 1.38100, 0.08690, -0.12620 ] + - [ 8.6603, 1.40540, 0.08700, -0.12650 ] + - [ 8.8900, 1.42950, 0.08710, -0.12670 ] + - [ 9.1198, 1.45310, 0.08730, -0.12700 ] + - [ 9.8801, 1.51540, 0.08790, -0.12650 ] + - [ 10.6398, 1.57490, 0.08860, -0.12560 ] + - [ 11.4001, 1.61510, 0.08950, -0.12140 ] + - [ 12.1501, 1.64430, 0.09120, -0.11630 ] + - [ 12.9099, 1.68240, 0.09300, -0.11330 ] + - [ 13.6702, 1.71460, 0.09540, -0.11070 ] + - [ 14.4202, 1.73620, 0.09890, -0.10800 ] + - [ 15.1799, 1.76270, 0.10240, -0.10630 ] + - [ 15.9403, 1.77060, 0.10760, -0.10420 ] + - [ 16.6903, 1.76390, 0.11440, -0.10250 ] + - [ 17.4500, 1.76040, 0.12110, -0.10130 ] + - [ 18.2097, 1.72510, 0.13100, -0.10010 ] + - [ 18.9701, 1.70350, 0.13990, -0.09980 ] + - [ 19.7201, 1.67840, 0.14920, -0.10010 ] + - [ 20.4798, 1.65050, 0.15910, -0.10160 ] + - [ 21.2401, 1.62270, 0.16910, -0.10360 ] + - [ 21.9901, 1.60670, 0.17780, -0.10640 ] + - [ 22.7499, 1.59720, 0.18580, -0.10990 ] + - [ 23.5102, 1.58920, 0.19370, -0.11360 ] + - [ 24.2602, 1.58150, 0.20140, -0.11800 ] + - [ 25.0199, 1.55630, 0.21350, -0.12490 ] + - [ 25.7802, 1.52720, 0.22670, -0.13250 ] + - [ 26.5302, 1.49820, 0.23990, -0.14000 ] + - [ 27.2900, 1.46910, 0.25310, -0.14760 ] + - [ 28.0497, 1.44010, 0.26630, -0.15510 ] + - [ 28.8100, 1.41100, 0.27950, -0.16270 ] + - [ 29.5600, 1.38200, 0.29270, -0.17030 ] + - [ 30.3198, 1.36220, 0.30780, -0.17400 ] + - [ 31.0801, 1.34240, 0.32300, -0.17770 ] + - [ 31.8301, 1.32250, 0.33810, -0.18150 ] + - [ 32.5898, 1.30270, 0.35320, -0.18520 ] + - [ 33.3502, 1.28290, 0.36840, -0.18890 ] + - [ 34.1002, 1.26310, 0.38350, -0.19260 ] + - [ 34.8599, 1.24330, 0.39870, -0.19640 ] + - [ 35.6202, 1.22340, 0.41380, -0.20010 ] + - [ 36.3800, 1.20360, 0.42890, -0.20390 ] + - [ 37.1300, 1.18380, 0.44410, -0.20760 ] + - [ 37.8903, 1.16400, 0.45920, -0.21130 ] + - [ 38.6500, 1.14420, 0.47430, -0.21500 ] + - [ 39.4000, 1.12430, 0.48950, -0.21880 ] + - [ 40.1598, 1.10640, 0.50520, -0.22180 ] + - [ 40.9201, 1.09050, 0.52140, -0.22420 ] + - [ 41.6701, 1.07450, 0.53760, -0.22660 ] + - [ 42.4298, 1.05860, 0.55380, -0.22890 ] + - [ 43.1901, 1.04260, 0.57010, -0.23130 ] + - [ 43.9401, 1.02670, 0.58630, -0.23370 ] + - [ 44.6999, 1.01070, 0.60250, -0.23610 ] + - [ 45.4602, 0.99480, 0.61880, -0.23840 ] + - [ 46.2199, 0.97880, 0.63500, -0.24080 ] + - [ 46.9699, 0.96280, 0.65120, -0.24320 ] + - [ 47.7302, 0.94690, 0.66750, -0.24550 ] + - [ 48.4900, 0.93090, 0.68370, -0.24790 ] + - [ 49.2400, 0.91500, 0.69990, -0.25030 ] + - [ 49.9997, 0.89900, 0.71610, -0.25270 ] + - [ 60.0001, 0.68360, 0.92460, -0.28330 ] + - [ 70.0000, 0.45560, 1.09850, -0.31560 ] + - [ 79.9998, 0.22190, 1.21820, -0.34820 ] + - [ 90.0002, 0.00000, 1.27070, -0.37730 ] + - [ 100.0000, -0.15530, 1.21820, -0.38770 ] + - [ 109.9999, -0.31890, 1.09850, -0.38650 ] + - [ 120.0003, -0.47840, 0.92460, -0.38060 ] + - [ 130.0001, -0.62930, 0.71610, -0.38030 ] + - [ 139.9999, -0.78010, 0.49700, -0.40320 ] + - [ 149.9998, -0.96740, 0.29270, -0.48540 ] + - [ 160.0002, -0.88370, 0.12680, -0.53250 ] + - [ 170.0000, -0.44180, 0.08440, -0.39060 ] + - [ 179.9660, 0.00000, 0.08440, 0.00000 ] + - name : FFA-W3-211 # + relative_thickness : 0.211 # + data: # alpha c_l c_d c_m + - [ -179.9087, 0.00000, 0.02464, 0.00000 ] + - [ -177.7143, 0.05403, 0.02534, 0.09143 ] + - [ -175.4286, 0.10805, 0.02742, 0.18286 ] + - [ -173.1429, 0.16208, 0.03088, 0.27429 ] + - [ -170.8572, 0.21610, 0.03570, 0.36571 ] + - [ -168.5716, 0.27013, 0.05599, 0.39192 ] + - [ -166.2857, 0.32415, 0.08143, 0.37898 ] + - [ -164.0000, 0.37818, 0.11112, 0.36605 ] + - [ -161.7145, 0.43220, 0.14485, 0.35312 ] + - [ -159.4284, 0.48623, 0.18242, 0.34768 ] + - [ -157.1428, 0.54025, 0.22359, 0.36471 ] + - [ -154.8573, 0.59428, 0.26810, 0.38175 ] + - [ -152.5714, 0.64830, 0.31566, 0.39878 ] + - [ -150.2857, 0.70233, 0.36597, 0.41581 ] + - [ -148.0000, 0.75635, 0.41871, 0.41955 ] + - [ -143.8571, 0.73188, 0.51941, 0.42287 ] + - [ -139.7143, 0.70655, 0.62488, 0.42632 ] + - [ -135.5714, 0.67760, 0.73293, 0.43163 ] + - [ -131.4286, 0.64333, 0.84130, 0.43694 ] + - [ -127.2857, 0.60277, 0.94773, 0.44389 ] + - [ -123.1429, 0.55550, 1.05001, 0.45171 ] + - [ -119.0000, 0.50156, 1.14600, 0.45897 ] + - [ -114.8571, 0.44131, 1.23371, 0.46448 ] + - [ -110.7143, 0.37542, 1.31129, 0.46998 ] + - [ -106.5714, 0.30482, 1.37714, 0.47096 ] + - [ -102.4286, 0.23063, 1.42988, 0.47101 ] + - [ -98.2857, 0.15413, 1.46842, 0.46824 ] + - [ -94.1429, 0.07675, 1.49196, 0.46149 ] + - [ -90.0000, 0.00000, 1.50000, 0.45474 ] + - [ -85.8571, -0.07675, 1.49196, 0.44026 ] + - [ -81.7143, -0.15413, 1.46842, 0.42578 ] + - [ -77.5714, -0.23063, 1.42988, 0.40821 ] + - [ -73.4286, -0.30482, 1.37714, 0.38846 ] + - [ -69.2857, -0.37542, 1.31129, 0.36815 ] + - [ -65.1429, -0.44131, 1.23371, 0.34519 ] + - [ -61.0000, -0.50156, 1.14600, 0.32223 ] + - [ -56.8571, -0.55550, 1.05001, 0.29864 ] + - [ -52.7143, -0.60277, 0.94773, 0.27486 ] + - [ -48.5714, -0.64333, 0.84130, 0.25128 ] + - [ -44.4286, -0.67760, 0.73293, 0.22810 ] + - [ -40.2857, -0.70655, 0.62488, 0.20491 ] + - [ -36.1429, -0.73188, 0.51941, 0.15416 ] + - [ -32.0000, -0.75635, 0.41871, 0.10137 ] + - [ -28.0000, -0.85636, 0.28691, 0.06527 ] + - [ -24.0000, -1.18292, 0.13960, 0.01647 ] + - [ -20.0000, -1.23596, 0.08345, -0.00352 ] + - [ -18.0000, -1.22536, 0.06509, -0.00672 ] + - [ -16.0000, -1.20476, 0.04888, -0.00881 ] + - [ -14.0000, -1.18332, 0.03417, -0.01101 ] + - [ -12.0000, -1.10093, 0.02132, -0.02269 ] + - [ -10.0000, -0.88209, 0.01386, -0.04397 ] + - [ -8.0000, -0.62981, 0.01075, -0.05756 ] + - [ -6.0000, -0.37670, 0.00882, -0.06747 ] + - [ -4.0000, -0.12177, 0.00702, -0.07680 ] + - [ -2.0000, 0.12810, 0.00663, -0.08283 ] + - [ -1.0000, 0.25192, 0.00664, -0.08534 ] + - [ 0.0000, 0.37535, 0.00670, -0.08777 ] + - [ 1.0000, 0.49828, 0.00681, -0.09011 ] + - [ 2.0000, 0.62052, 0.00698, -0.09234 ] + - [ 3.0000, 0.74200, 0.00720, -0.09447 ] + - [ 4.0000, 0.86238, 0.00751, -0.09646 ] + - [ 5.0000, 0.98114, 0.00796, -0.09828 ] + - [ 6.0000, 1.09662, 0.00872, -0.09977 ] + - [ 7.0000, 1.20904, 0.00968, -0.10095 ] + - [ 8.0000, 1.31680, 0.01097, -0.10163 ] + - [ 9.0000, 1.42209, 0.01227, -0.10207 ] + - [ 10.0000, 1.52361, 0.01369, -0.10213 ] + - [ 11.0000, 1.61988, 0.01529, -0.10174 ] + - [ 12.0000, 1.70937, 0.01717, -0.10087 ] + - [ 13.0000, 1.78681, 0.01974, -0.09936 ] + - [ 14.0000, 1.84290, 0.02368, -0.09720 ] + - [ 15.0000, 1.85313, 0.03094, -0.09410 ] + - [ 16.0000, 1.80951, 0.04303, -0.09144 ] + - [ 18.0000, 1.66033, 0.07730, -0.09242 ] + - [ 20.0000, 1.56152, 0.11202, -0.09871 ] + - [ 24.0000, 1.43327, 0.18408, -0.11770 ] + - [ 28.0000, 1.29062, 0.27589, -0.14566 ] + - [ 32.0000, 1.08050, 0.41871, -0.18266 ] + - [ 36.1429, 1.04554, 0.51941, -0.20913 ] + - [ 40.2857, 1.00936, 0.62488, -0.23534 ] + - [ 44.4286, 0.96801, 0.73293, -0.25784 ] + - [ 48.5714, 0.91904, 0.84130, -0.28035 ] + - [ 52.7143, 0.86109, 0.94773, -0.30163 ] + - [ 56.8571, 0.79357, 1.05001, -0.32226 ] + - [ 61.0000, 0.71651, 1.14600, -0.34247 ] + - [ 65.1429, 0.63044, 1.23371, -0.36135 ] + - [ 69.2857, 0.53632, 1.31129, -0.38024 ] + - [ 73.4286, 0.43546, 1.37714, -0.39704 ] + - [ 77.5714, 0.32947, 1.42988, -0.41341 ] + - [ 81.7143, 0.22019, 1.46842, -0.42844 ] + - [ 85.8571, 0.10965, 1.49196, -0.44159 ] + - [ 90.0000, 0.00000, 1.50000, -0.45474 ] + - [ 94.1429, -0.07675, 1.49196, -0.46149 ] + - [ 98.2857, -0.15413, 1.46842, -0.46824 ] + - [ 102.4286, -0.23063, 1.42988, -0.47101 ] + - [ 106.5714, -0.30482, 1.37714, -0.47096 ] + - [ 110.7143, -0.37542, 1.31129, -0.46998 ] + - [ 114.8571, -0.44131, 1.23371, -0.46448 ] + - [ 119.0000, -0.50156, 1.14600, -0.45897 ] + - [ 123.1429, -0.55550, 1.05001, -0.45171 ] + - [ 127.2857, -0.60277, 0.94773, -0.44389 ] + - [ 131.4286, -0.64333, 0.84130, -0.43694 ] + - [ 135.5714, -0.67760, 0.73293, -0.43163 ] + - [ 139.7143, -0.70655, 0.62488, -0.42632 ] + - [ 143.8571, -0.73188, 0.51941, -0.42287 ] + - [ 148.0000, -0.75635, 0.41871, -0.41955 ] + - [ 150.2857, -0.70233, 0.36597, -0.41581 ] + - [ 152.5714, -0.64830, 0.31566, -0.39878 ] + - [ 154.8571, -0.59428, 0.26810, -0.38175 ] + - [ 157.1429, -0.54025, 0.22359, -0.36471 ] + - [ 159.4286, -0.48623, 0.18242, -0.34768 ] + - [ 161.7143, -0.43220, 0.14485, -0.37026 ] + - [ 164.0000, -0.37818, 0.11112, -0.40605 ] + - [ 166.2857, -0.32415, 0.08143, -0.44184 ] + - [ 168.5714, -0.27013, 0.05599, -0.47763 ] + - [ 170.8571, -0.21610, 0.03570, -0.45714 ] + - [ 173.1429, -0.16208, 0.03088, -0.34286 ] + - [ 175.4286, -0.10805, 0.02742, -0.22857 ] + - [ 177.7143, -0.05403, 0.02534, -0.11429 ] + - [ 179.9087, 0.00000, 0.02464, 0.00000 ] + - name : FFA-W3-241 # + relative_thickness : 0.241 # + data: # alpha c_l c_d c_m + - [ -179.9087, 0.00000, 0.01178, 0.00000 ] + - [ -177.7143, 0.05818, 0.01248, 0.09143 ] + - [ -175.4286, 0.11636, 0.01460, 0.18286 ] + - [ -173.1429, 0.17453, 0.01811, 0.27429 ] + - [ -170.8572, 0.23271, 0.02300, 0.36571 ] + - [ -168.5716, 0.29089, 0.02922, 0.39568 ] + - [ -166.2857, 0.34907, 0.05382, 0.38876 ] + - [ -164.0000, 0.40725, 0.08379, 0.38184 ] + - [ -161.7145, 0.46542, 0.11786, 0.37492 ] + - [ -159.4284, 0.52360, 0.15581, 0.37408 ] + - [ -157.1428, 0.58178, 0.19740, 0.39148 ] + - [ -154.8573, 0.63996, 0.24237, 0.40888 ] + - [ -152.5714, 0.69814, 0.29043, 0.42628 ] + - [ -150.2857, 0.75631, 0.34128, 0.44368 ] + - [ -148.0000, 0.81449, 0.39460, 0.44537 ] + - [ -143.8571, 0.77925, 0.49645, 0.44436 ] + - [ -139.7143, 0.74511, 0.60319, 0.44360 ] + - [ -135.5714, 0.70881, 0.71263, 0.44609 ] + - [ -131.4286, 0.66835, 0.82249, 0.44858 ] + - [ -127.2857, 0.62253, 0.93051, 0.45370 ] + - [ -123.1429, 0.57080, 1.03447, 0.46020 ] + - [ -119.0000, 0.51307, 1.13222, 0.46633 ] + - [ -114.8571, 0.44965, 1.22176, 0.47130 ] + - [ -110.7143, 0.38115, 1.30123, 0.47627 ] + - [ -106.5714, 0.30846, 1.36903, 0.47705 ] + - [ -102.4286, 0.23266, 1.42376, 0.47695 ] + - [ -98.2857, 0.15503, 1.46433, 0.47409 ] + - [ -94.1429, 0.07698, 1.48990, 0.46732 ] + - [ -90.0000, 0.00000, 1.50000, 0.46055 ] + - [ -85.8571, -0.07698, 1.48990, 0.44509 ] + - [ -81.7143, -0.15503, 1.46433, 0.42964 ] + - [ -77.5714, -0.23266, 1.42376, 0.41125 ] + - [ -73.4286, -0.30846, 1.36903, 0.39081 ] + - [ -69.2857, -0.38115, 1.30123, 0.36988 ] + - [ -65.1429, -0.44965, 1.22176, 0.34663 ] + - [ -61.0000, -0.51307, 1.13222, 0.32339 ] + - [ -56.8571, -0.57080, 1.03447, 0.29984 ] + - [ -52.7143, -0.62253, 0.93051, 0.27618 ] + - [ -48.5714, -0.66835, 0.82249, 0.25280 ] + - [ -44.4286, -0.70881, 0.71263, 0.22992 ] + - [ -40.2857, -0.74511, 0.60319, 0.20705 ] + - [ -36.1429, -0.77925, 0.49645, 0.14561 ] + - [ -32.0000, -0.81449, 0.39460, 0.08131 ] + - [ -28.0000, -1.07781, 0.22252, 0.04592 ] + - [ -24.0000, -1.12692, 0.15159, 0.01901 ] + - [ -20.0000, -1.14480, 0.09699, 0.00063 ] + - [ -18.0000, -1.12797, 0.07744, -0.00342 ] + - [ -16.0000, -1.09392, 0.06122, -0.00587 ] + - [ -14.0000, -1.05961, 0.04667, -0.00652 ] + - [ -12.0000, -1.03121, 0.03302, -0.00755 ] + - [ -10.0000, -0.93706, 0.02027, -0.02243 ] + - [ -8.0000, -0.67380, 0.01168, -0.05583 ] + - [ -6.0000, -0.40391, 0.00918, -0.07159 ] + - [ -4.0000, -0.14226, 0.00839, -0.08123 ] + - [ -2.0000, 0.11580, 0.00810, -0.08892 ] + - [ -1.0000, 0.24382, 0.00808, -0.09235 ] + - [ 0.0000, 0.37113, 0.00813, -0.09556 ] + - [ 1.0000, 0.49766, 0.00824, -0.09857 ] + - [ 2.0000, 0.62334, 0.00842, -0.10139 ] + - [ 3.0000, 0.74798, 0.00867, -0.10403 ] + - [ 4.0000, 0.87137, 0.00901, -0.10645 ] + - [ 5.0000, 0.99320, 0.00945, -0.10863 ] + - [ 6.0000, 1.11325, 0.00998, -0.11057 ] + - [ 7.0000, 1.23037, 0.01070, -0.11214 ] + - [ 8.0000, 1.34496, 0.01153, -0.11337 ] + - [ 9.0000, 1.45407, 0.01269, -0.11396 ] + - [ 10.0000, 1.55911, 0.01396, -0.11403 ] + - [ 11.0000, 1.65779, 0.01545, -0.11336 ] + - [ 12.0000, 1.74834, 0.01724, -0.11187 ] + - [ 13.0000, 1.82666, 0.01961, -0.10935 ] + - [ 14.0000, 1.88831, 0.02293, -0.10606 ] + - [ 15.0000, 1.92579, 0.02795, -0.10238 ] + - [ 16.0000, 1.92722, 0.03609, -0.09887 ] + - [ 18.0000, 1.80055, 0.06534, -0.09497 ] + - [ 20.0000, 1.63088, 0.10459, -0.09996 ] + - [ 24.0000, 1.43345, 0.19148, -0.12589 ] + - [ 28.0000, 1.28805, 0.28629, -0.15453 ] + - [ 32.0000, 1.16356, 0.39460, -0.18396 ] + - [ 36.1429, 1.11321, 0.49645, -0.21099 ] + - [ 40.2857, 1.06444, 0.60319, -0.23768 ] + - [ 44.4286, 1.01259, 0.71263, -0.25992 ] + - [ 48.5714, 0.95478, 0.82249, -0.28216 ] + - [ 52.7143, 0.88932, 0.93051, -0.30323 ] + - [ 56.8571, 0.81542, 1.03447, -0.32368 ] + - [ 61.0000, 0.73296, 1.13222, -0.34380 ] + - [ 65.1429, 0.64236, 1.22176, -0.36292 ] + - [ 69.2857, 0.54450, 1.30123, -0.38204 ] + - [ 73.4286, 0.44065, 1.36903, -0.39944 ] + - [ 77.5714, 0.33237, 1.42376, -0.41648 ] + - [ 81.7143, 0.22148, 1.46433, -0.43231 ] + - [ 85.8571, 0.10997, 1.48990, -0.44643 ] + - [ 90.0000, 0.00000, 1.50000, -0.46055 ] + - [ 94.1429, -0.07698, 1.48990, -0.46732 ] + - [ 98.2857, -0.15503, 1.46433, -0.47409 ] + - [ 102.4286, -0.23266, 1.42376, -0.47695 ] + - [ 106.5714, -0.30846, 1.36903, -0.47705 ] + - [ 110.7143, -0.38115, 1.30123, -0.47627 ] + - [ 114.8571, -0.44965, 1.22176, -0.47130 ] + - [ 119.0000, -0.51307, 1.13222, -0.46633 ] + - [ 123.1429, -0.57080, 1.03447, -0.46020 ] + - [ 127.2857, -0.62253, 0.93051, -0.45370 ] + - [ 131.4286, -0.66835, 0.82249, -0.44858 ] + - [ 135.5714, -0.70881, 0.71263, -0.44609 ] + - [ 139.7143, -0.74511, 0.60319, -0.44360 ] + - [ 143.8571, -0.77925, 0.49645, -0.44436 ] + - [ 148.0000, -0.81449, 0.39460, -0.44537 ] + - [ 150.2857, -0.75631, 0.34128, -0.44368 ] + - [ 152.5714, -0.69814, 0.29043, -0.42628 ] + - [ 154.8571, -0.63996, 0.24237, -0.40888 ] + - [ 157.1429, -0.58178, 0.19740, -0.39148 ] + - [ 159.4286, -0.52360, 0.15581, -0.37408 ] + - [ 161.7143, -0.46542, 0.11786, -0.39207 ] + - [ 164.0000, -0.40725, 0.08379, -0.42184 ] + - [ 166.2857, -0.34907, 0.05382, -0.45162 ] + - [ 168.5714, -0.29089, 0.02922, -0.48139 ] + - [ 170.8571, -0.23271, 0.02300, -0.45714 ] + - [ 173.1429, -0.17453, 0.01811, -0.34286 ] + - [ 175.4286, -0.11636, 0.01460, -0.22857 ] + - [ 177.7143, -0.05818, 0.01248, -0.11429 ] + - [ 179.9087, 0.00000, 0.01178, 0.00000 ] + - name : FFA-W3-270blend # + relative_thickness : 0.27 # + data: # alpha c_l c_d c_m + - [ -179.9087, 0.00000, 0.01545, 0.00000 ] + - [ -177.7143, 0.06213, 0.01611, 0.09143 ] + - [ -175.4286, 0.12426, 0.01807, 0.18286 ] + - [ -173.1429, 0.18639, 0.02133, 0.27429 ] + - [ -170.8572, 0.24852, 0.02587, 0.36571 ] + - [ -168.5716, 0.31064, 0.03289, 0.39874 ] + - [ -166.2857, 0.37277, 0.05681, 0.39672 ] + - [ -164.0000, 0.43490, 0.08471, 0.39470 ] + - [ -161.7145, 0.49703, 0.11643, 0.39268 ] + - [ -159.4284, 0.55916, 0.15176, 0.39544 ] + - [ -157.1428, 0.62129, 0.19048, 0.41254 ] + - [ -154.8573, 0.68342, 0.23234, 0.42964 ] + - [ -152.5714, 0.74555, 0.27708, 0.44674 ] + - [ -150.2857, 0.80768, 0.32441, 0.46384 ] + - [ -148.0000, 0.86981, 0.37404, 0.46186 ] + - [ -143.8571, 0.81660, 0.46882, 0.45335 ] + - [ -139.7143, 0.76812, 0.56814, 0.44523 ] + - [ -135.5714, 0.72040, 0.66995, 0.44237 ] + - [ -131.4286, 0.67095, 0.77214, 0.43951 ] + - [ -127.2857, 0.61828, 0.87258, 0.44072 ] + - [ -123.1429, 0.56158, 0.96921, 0.44407 ] + - [ -119.0000, 0.50057, 1.06002, 0.44739 ] + - [ -114.8571, 0.43540, 1.14315, 0.45063 ] + - [ -110.7143, 0.36655, 1.21688, 0.45387 ] + - [ -106.5714, 0.29475, 1.27969, 0.45377 ] + - [ -102.4286, 0.22098, 1.33030, 0.45298 ] + - [ -98.2857, 0.14639, 1.36768, 0.44973 ] + - [ -94.1429, 0.07227, 1.39107, 0.44302 ] + - [ -90.0000, 0.00000, 1.40000, 0.43630 ] + - [ -85.8571, -0.07227, 1.39107, 0.42180 ] + - [ -81.7143, -0.14639, 1.36768, 0.40730 ] + - [ -77.5714, -0.22098, 1.33030, 0.39020 ] + - [ -73.4286, -0.29475, 1.27969, 0.37125 ] + - [ -69.2857, -0.36655, 1.21688, 0.35190 ] + - [ -65.1429, -0.43540, 1.14315, 0.33068 ] + - [ -61.0000, -0.50057, 1.06002, 0.30945 ] + - [ -56.8571, -0.56158, 0.96921, 0.28815 ] + - [ -52.7143, -0.61828, 0.87258, 0.26684 ] + - [ -48.5714, -0.67095, 0.77214, 0.24576 ] + - [ -44.4286, -0.72040, 0.66995, 0.22512 ] + - [ -40.2857, -0.76812, 0.56814, 0.20447 ] + - [ -36.1429, -0.81660, 0.46882, 0.13957 ] + - [ -32.0000, -0.86981, 0.37404, 0.07138 ] + - [ -28.0000, -1.09837, 0.21880, 0.04400 ] + - [ -24.0000, -1.08339, 0.15982, 0.02166 ] + - [ -20.0000, -1.06990, 0.10744, 0.00422 ] + - [ -18.0000, -1.05454, 0.08690, -0.00035 ] + - [ -16.0000, -1.03432, 0.06844, -0.00334 ] + - [ -14.0000, -1.08360, 0.04733, -0.00283 ] + - [ -12.0000, -1.09489, 0.03085, -0.00556 ] + - [ -10.0000, -0.92665, 0.01984, -0.02952 ] + - [ -8.0000, -0.69676, 0.01439, -0.04822 ] + - [ -6.0000, -0.43628, 0.01155, -0.06483 ] + - [ -4.0000, -0.16252, 0.01026, -0.07919 ] + - [ -2.0000, 0.10709, 0.00976, -0.09041 ] + - [ -1.0000, 0.23993, 0.00967, -0.09517 ] + - [ 0.0000, 0.37158, 0.00968, -0.09953 ] + - [ 1.0000, 0.50210, 0.00976, -0.10355 ] + - [ 2.0000, 0.63139, 0.00993, -0.10725 ] + - [ 3.0000, 0.75951, 0.01016, -0.11068 ] + - [ 4.0000, 0.88638, 0.01045, -0.11385 ] + - [ 5.0000, 1.01172, 0.01082, -0.11673 ] + - [ 6.0000, 1.13430, 0.01140, -0.11923 ] + - [ 7.0000, 1.25536, 0.01198, -0.12145 ] + - [ 8.0000, 1.37379, 0.01267, -0.12328 ] + - [ 9.0000, 1.48841, 0.01353, -0.12460 ] + - [ 10.0000, 1.59782, 0.01460, -0.12526 ] + - [ 11.0000, 1.70005, 0.01597, -0.12505 ] + - [ 12.0000, 1.79190, 0.01777, -0.12370 ] + - [ 13.0000, 1.86782, 0.02035, -0.12093 ] + - [ 14.0000, 1.92687, 0.02385, -0.11725 ] + - [ 15.0000, 1.90901, 0.03236, -0.10931 ] + - [ 16.0000, 1.88548, 0.04259, -0.10525 ] + - [ 18.0000, 1.72106, 0.07672, -0.10292 ] + - [ 20.0000, 1.54737, 0.11914, -0.11017 ] + - [ 24.0000, 1.37176, 0.20189, -0.13431 ] + - [ 28.0000, 1.33611, 0.27981, -0.15777 ] + - [ 32.0000, 1.24258, 0.37404, -0.18432 ] + - [ 36.1429, 1.16657, 0.46882, -0.21002 ] + - [ 40.2857, 1.09731, 0.56814, -0.23531 ] + - [ 44.4286, 1.02914, 0.66995, -0.25508 ] + - [ 48.5714, 0.95850, 0.77214, -0.27485 ] + - [ 52.7143, 0.88325, 0.87258, -0.29346 ] + - [ 56.8571, 0.80225, 0.96921, -0.31145 ] + - [ 61.0000, 0.71510, 1.06002, -0.32925 ] + - [ 65.1429, 0.62200, 1.14315, -0.34641 ] + - [ 69.2857, 0.52364, 1.21688, -0.36357 ] + - [ 73.4286, 0.42107, 1.27969, -0.37949 ] + - [ 77.5714, 0.31569, 1.33030, -0.39517 ] + - [ 81.7143, 0.20913, 1.36768, -0.40983 ] + - [ 85.8571, 0.10324, 1.39107, -0.42306 ] + - [ 90.0000, 0.00000, 1.40000, -0.43630 ] + - [ 94.1429, -0.07227, 1.39107, -0.44302 ] + - [ 98.2857, -0.14639, 1.36768, -0.44973 ] + - [ 102.4286, -0.22098, 1.33030, -0.45298 ] + - [ 106.5714, -0.29475, 1.27969, -0.45377 ] + - [ 110.7143, -0.36655, 1.21688, -0.45387 ] + - [ 114.8571, -0.43540, 1.14315, -0.45063 ] + - [ 119.0000, -0.50057, 1.06002, -0.44739 ] + - [ 123.1429, -0.56158, 0.96921, -0.44407 ] + - [ 127.2857, -0.61828, 0.87258, -0.44072 ] + - [ 131.4286, -0.67095, 0.77214, -0.43951 ] + - [ 135.5714, -0.72040, 0.66995, -0.44237 ] + - [ 139.7143, -0.76812, 0.56814, -0.44523 ] + - [ 143.8571, -0.81660, 0.46882, -0.45335 ] + - [ 148.0000, -0.86981, 0.37404, -0.46186 ] + - [ 150.2857, -0.80768, 0.32441, -0.46384 ] + - [ 152.5714, -0.74555, 0.27708, -0.44674 ] + - [ 154.8571, -0.68342, 0.23234, -0.42964 ] + - [ 157.1429, -0.62129, 0.19048, -0.41254 ] + - [ 159.4286, -0.55916, 0.15176, -0.39544 ] + - [ 161.7143, -0.49703, 0.11643, -0.40982 ] + - [ 164.0000, -0.43490, 0.08471, -0.43470 ] + - [ 166.2857, -0.37277, 0.05681, -0.45958 ] + - [ 168.5714, -0.31064, 0.03289, -0.48445 ] + - [ 170.8571, -0.24852, 0.02587, -0.45714 ] + - [ 173.1429, -0.18639, 0.02133, -0.34286 ] + - [ 175.4286, -0.12426, 0.01807, -0.22857 ] + - [ 177.7143, -0.06213, 0.01611, -0.11429 ] + - [ 179.9087, 0.00000, 0.01545, 0.00000 ] + - name : FFA-W3-301 # + relative_thickness : 0.301 # + data: # alpha c_l c_d c_m + - [ -179.9087, 0.00000, 0.02454, 0.00000 ] + - [ -177.7143, 0.06508, 0.02514, 0.09143 ] + - [ -175.4286, 0.13016, 0.02694, 0.18286 ] + - [ -173.1429, 0.19525, 0.02993, 0.27429 ] + - [ -170.8572, 0.26033, 0.03408, 0.36571 ] + - [ -168.5716, 0.32541, 0.03938, 0.40085 ] + - [ -166.2857, 0.39049, 0.05910, 0.40220 ] + - [ -164.0000, 0.45557, 0.08495, 0.40356 ] + - [ -161.7145, 0.52066, 0.11433, 0.40492 ] + - [ -159.4284, 0.58574, 0.14704, 0.41010 ] + - [ -157.1428, 0.65082, 0.18290, 0.42678 ] + - [ -154.8573, 0.71590, 0.22166, 0.44345 ] + - [ -152.5714, 0.78098, 0.26309, 0.46013 ] + - [ -150.2857, 0.84607, 0.30692, 0.47680 ] + - [ -148.0000, 0.91115, 0.35287, 0.47162 ] + - [ -143.8571, 0.84257, 0.44061, 0.45656 ] + - [ -139.7143, 0.78187, 0.53255, 0.44202 ] + - [ -135.5714, 0.72448, 0.62677, 0.43452 ] + - [ -131.4286, 0.66755, 0.72131, 0.42701 ] + - [ -127.2857, 0.60928, 0.81421, 0.42483 ] + - [ -123.1429, 0.54868, 0.90355, 0.42544 ] + - [ -119.0000, 0.48530, 0.98748, 0.42634 ] + - [ -114.8571, 0.41915, 1.06425, 0.42813 ] + - [ -110.7143, 0.35056, 1.13227, 0.42992 ] + - [ -106.5714, 0.28017, 1.19015, 0.42916 ] + - [ -102.4286, 0.20881, 1.23669, 0.42788 ] + - [ -98.2857, 0.13754, 1.27093, 0.42444 ] + - [ -94.1429, 0.06751, 1.29218, 0.41794 ] + - [ -90.0000, 0.00000, 1.30000, 0.41144 ] + - [ -85.8571, -0.06751, 1.29218, 0.39804 ] + - [ -81.7143, -0.13754, 1.27093, 0.38464 ] + - [ -77.5714, -0.20881, 1.23669, 0.36892 ] + - [ -73.4286, -0.28017, 1.19015, 0.35157 ] + - [ -69.2857, -0.35056, 1.13227, 0.33391 ] + - [ -65.1429, -0.41915, 1.06425, 0.31474 ] + - [ -61.0000, -0.48530, 0.98748, 0.29557 ] + - [ -56.8571, -0.54868, 0.90355, 0.27653 ] + - [ -52.7143, -0.60928, 0.81421, 0.25754 ] + - [ -48.5714, -0.66755, 0.72131, 0.23873 ] + - [ -44.4286, -0.72448, 0.62677, 0.22027 ] + - [ -40.2857, -0.78187, 0.53255, 0.20181 ] + - [ -36.1429, -0.84257, 0.44061, 0.13644 ] + - [ -32.0000, -0.91115, 0.35287, 0.06760 ] + - [ -28.0000, -1.10349, 0.21721, 0.04231 ] + - [ -24.0000, -1.10737, 0.15629, 0.02026 ] + - [ -20.0000, -1.11815, 0.10335, 0.00407 ] + - [ -18.0000, -1.12332, 0.08180, 0.00017 ] + - [ -16.0000, -1.11865, 0.06331, -0.00167 ] + - [ -14.0000, -1.11620, 0.04718, -0.00120 ] + - [ -12.0000, -1.09588, 0.03280, -0.00463 ] + - [ -10.0000, -0.91767, 0.02351, -0.02494 ] + - [ -8.0000, -0.69311, 0.01793, -0.04304 ] + - [ -6.0000, -0.45396, 0.01431, -0.05868 ] + - [ -4.0000, -0.17779, 0.01242, -0.07601 ] + - [ -2.0000, 0.10480, 0.01160, -0.09121 ] + - [ -1.0000, 0.24383, 0.01143, -0.09763 ] + - [ 0.0000, 0.38111, 0.01138, -0.10341 ] + - [ 1.0000, 0.51660, 0.01143, -0.10861 ] + - [ 2.0000, 0.65044, 0.01156, -0.11333 ] + - [ 3.0000, 0.78267, 0.01177, -0.11762 ] + - [ 4.0000, 0.91326, 0.01204, -0.12154 ] + - [ 5.0000, 1.04207, 0.01239, -0.12510 ] + - [ 6.0000, 1.16873, 0.01283, -0.12828 ] + - [ 7.0000, 1.29296, 0.01338, -0.13104 ] + - [ 8.0000, 1.41390, 0.01406, -0.13332 ] + - [ 9.0000, 1.53088, 0.01488, -0.13503 ] + - [ 10.0000, 1.64208, 0.01592, -0.13599 ] + - [ 11.0000, 1.74568, 0.01726, -0.13605 ] + - [ 12.0000, 1.83887, 0.01908, -0.13514 ] + - [ 13.0000, 1.91764, 0.02169, -0.13322 ] + - [ 14.0000, 1.97413, 0.02572, -0.13020 ] + - [ 15.0000, 1.99916, 0.03222, -0.12641 ] + - [ 16.0000, 1.99377, 0.04157, -0.12265 ] + - [ 18.0000, 1.91720, 0.06731, -0.11675 ] + - [ 20.0000, 1.73683, 0.10526, -0.11652 ] + - [ 24.0000, 1.47321, 0.19229, -0.13790 ] + - [ 28.0000, 1.36017, 0.27449, -0.16242 ] + - [ 32.0000, 1.30164, 0.35287, -0.18463 ] + - [ 36.1429, 1.20367, 0.44061, -0.20894 ] + - [ 40.2857, 1.11695, 0.53255, -0.23276 ] + - [ 44.4286, 1.03498, 0.62677, -0.25011 ] + - [ 48.5714, 0.95364, 0.72131, -0.26746 ] + - [ 52.7143, 0.87040, 0.81421, -0.28365 ] + - [ 56.8571, 0.78383, 0.90355, -0.29923 ] + - [ 61.0000, 0.69329, 0.98748, -0.31472 ] + - [ 65.1429, 0.59878, 1.06425, -0.32988 ] + - [ 69.2857, 0.50080, 1.13227, -0.34505 ] + - [ 73.4286, 0.40024, 1.19015, -0.35942 ] + - [ 77.5714, 0.29831, 1.23669, -0.37363 ] + - [ 81.7143, 0.19648, 1.27093, -0.38702 ] + - [ 85.8571, 0.09644, 1.29218, -0.39923 ] + - [ 90.0000, 0.00000, 1.30000, -0.41144 ] + - [ 94.1429, -0.06751, 1.29218, -0.41794 ] + - [ 98.2857, -0.13754, 1.27093, -0.42444 ] + - [ 102.4286, -0.20881, 1.23669, -0.42788 ] + - [ 106.5714, -0.28017, 1.19015, -0.42916 ] + - [ 110.7143, -0.35056, 1.13227, -0.42992 ] + - [ 114.8571, -0.41915, 1.06425, -0.42813 ] + - [ 119.0000, -0.48530, 0.98748, -0.42634 ] + - [ 123.1429, -0.54868, 0.90355, -0.42544 ] + - [ 127.2857, -0.60928, 0.81421, -0.42483 ] + - [ 131.4286, -0.66755, 0.72131, -0.42701 ] + - [ 135.5714, -0.72448, 0.62677, -0.43452 ] + - [ 139.7143, -0.78187, 0.53255, -0.44202 ] + - [ 143.8571, -0.84257, 0.44061, -0.45656 ] + - [ 148.0000, -0.91115, 0.35287, -0.47162 ] + - [ 150.2857, -0.84607, 0.30692, -0.47680 ] + - [ 152.5714, -0.78098, 0.26309, -0.46013 ] + - [ 154.8571, -0.71590, 0.22166, -0.44345 ] + - [ 157.1429, -0.65082, 0.18290, -0.42678 ] + - [ 159.4286, -0.58574, 0.14704, -0.41010 ] + - [ 161.7143, -0.52066, 0.11433, -0.42206 ] + - [ 164.0000, -0.45557, 0.08495, -0.44356 ] + - [ 166.2857, -0.39049, 0.05910, -0.46506 ] + - [ 168.5714, -0.32541, 0.03938, -0.48656 ] + - [ 170.8571, -0.26033, 0.03408, -0.45714 ] + - [ 173.1429, -0.19525, 0.02993, -0.34286 ] + - [ 175.4286, -0.13016, 0.02694, -0.22857 ] + - [ 177.7143, -0.06508, 0.02514, -0.11429 ] + - [ 179.9087, 0.00000, 0.02454, 0.00000 ] + - name : FFA-W3-330blend # + relative_thickness : 0.33 # + data: # alpha c_l c_d c_m + - [ -179.9087, 0.00000, 0.03169, 0.00000 ] + - [ -177.7143, 0.06960, 0.03228, 0.09143 ] + - [ -175.4286, 0.13920, 0.03406, 0.18286 ] + - [ -173.1429, 0.20880, 0.03702, 0.27429 ] + - [ -170.8572, 0.27841, 0.04114, 0.36571 ] + - [ -168.5716, 0.34801, 0.04638, 0.40308 ] + - [ -166.2857, 0.41761, 0.05732, 0.40801 ] + - [ -164.0000, 0.48721, 0.08319, 0.41294 ] + - [ -161.7145, 0.55681, 0.11258, 0.41788 ] + - [ -159.4284, 0.62641, 0.14533, 0.42586 ] + - [ -157.1428, 0.69601, 0.18121, 0.44302 ] + - [ -154.8573, 0.76562, 0.22000, 0.46017 ] + - [ -152.5714, 0.83522, 0.26146, 0.47732 ] + - [ -150.2857, 0.90482, 0.30532, 0.49447 ] + - [ -148.0000, 0.97442, 0.35131, 0.48743 ] + - [ -143.8571, 0.89412, 0.43913, 0.46839 ] + - [ -139.7143, 0.82382, 0.53115, 0.44996 ] + - [ -135.5714, 0.75845, 0.62546, 0.43985 ] + - [ -131.4286, 0.69477, 0.72010, 0.42974 ] + - [ -127.2857, 0.63079, 0.81310, 0.42589 ] + - [ -123.1429, 0.56532, 0.90255, 0.42535 ] + - [ -119.0000, 0.49783, 0.98659, 0.42528 ] + - [ -114.8571, 0.42823, 1.06348, 0.42673 ] + - [ -110.7143, 0.35680, 1.13162, 0.42817 ] + - [ -106.5714, 0.28412, 1.18963, 0.42745 ] + - [ -102.4286, 0.21103, 1.23629, 0.42628 ] + - [ -98.2857, 0.13851, 1.27067, 0.42303 ] + - [ -94.1429, 0.06775, 1.29204, 0.41683 ] + - [ -90.0000, 0.00000, 1.30000, 0.41063 ] + - [ -85.8571, -0.06775, 1.29204, 0.39752 ] + - [ -81.7143, -0.13851, 1.27067, 0.38441 ] + - [ -77.5714, -0.21103, 1.23629, 0.36905 ] + - [ -73.4286, -0.28412, 1.18963, 0.35212 ] + - [ -69.2857, -0.35680, 1.13162, 0.33491 ] + - [ -65.1429, -0.42823, 1.06348, 0.31634 ] + - [ -61.0000, -0.49783, 0.98659, 0.29777 ] + - [ -56.8571, -0.56532, 0.90255, 0.27947 ] + - [ -52.7143, -0.63079, 0.81310, 0.26125 ] + - [ -48.5714, -0.69477, 0.72010, 0.24322 ] + - [ -44.4286, -0.75845, 0.62546, 0.22556 ] + - [ -40.2857, -0.82382, 0.53115, 0.20789 ] + - [ -36.1429, -0.89412, 0.43913, 0.13731 ] + - [ -32.0000, -0.97442, 0.35131, 0.06280 ] + - [ -28.0000, -1.16308, 0.20648, 0.03905 ] + - [ -24.0000, -1.14892, 0.15001, 0.01853 ] + - [ -20.0000, -1.09451, 0.10600, 0.00441 ] + - [ -18.0000, -1.05801, 0.08732, -0.00061 ] + - [ -16.0000, -1.02281, 0.07051, -0.00342 ] + - [ -14.0000, -0.99810, 0.05474, -0.00401 ] + - [ -12.0000, -0.98515, 0.04052, -0.00272 ] + - [ -10.0000, -0.89583, 0.02929, -0.01198 ] + - [ -8.0000, -0.67539, 0.02207, -0.03458 ] + - [ -6.0000, -0.43247, 0.01735, -0.05466 ] + - [ -4.0000, -0.15881, 0.01473, -0.07425 ] + - [ -2.0000, 0.13456, 0.01362, -0.09270 ] + - [ -1.0000, 0.28014, 0.01339, -0.10074 ] + - [ 0.0000, 0.42386, 0.01330, -0.10802 ] + - [ 1.0000, 0.56519, 0.01333, -0.11450 ] + - [ 2.0000, 0.70410, 0.01345, -0.12028 ] + - [ 3.0000, 0.84071, 0.01366, -0.12546 ] + - [ 4.0000, 0.97500, 0.01397, -0.13011 ] + - [ 5.0000, 1.10680, 0.01437, -0.13425 ] + - [ 6.0000, 1.23603, 0.01486, -0.13793 ] + - [ 7.0000, 1.36223, 0.01547, -0.14108 ] + - [ 8.0000, 1.48424, 0.01623, -0.14363 ] + - [ 9.0000, 1.60097, 0.01718, -0.14545 ] + - [ 10.0000, 1.71010, 0.01841, -0.14636 ] + - [ 11.0000, 1.80957, 0.02010, -0.14635 ] + - [ 12.0000, 1.89473, 0.02258, -0.14544 ] + - [ 13.0000, 1.95698, 0.02671, -0.14378 ] + - [ 14.0000, 1.98576, 0.03380, -0.14185 ] + - [ 15.0000, 1.99260, 0.04333, -0.14004 ] + - [ 16.0000, 1.99617, 0.05354, -0.13823 ] + - [ 18.0000, 1.96398, 0.07706, -0.13351 ] + - [ 20.0000, 1.81179, 0.11169, -0.13135 ] + - [ 24.0000, 1.56073, 0.19103, -0.14660 ] + - [ 28.0000, 1.46798, 0.27199, -0.17242 ] + - [ 32.0000, 1.39203, 0.35131, -0.19417 ] + - [ 36.1429, 1.27731, 0.43913, -0.21792 ] + - [ 40.2857, 1.17689, 0.53115, -0.24115 ] + - [ 44.4286, 1.08350, 0.62546, -0.25734 ] + - [ 48.5714, 0.99253, 0.72010, -0.27354 ] + - [ 52.7143, 0.90112, 0.81310, -0.28862 ] + - [ 56.8571, 0.80760, 0.90255, -0.30311 ] + - [ 61.0000, 0.71119, 0.98659, -0.31757 ] + - [ 65.1429, 0.61175, 1.06348, -0.33194 ] + - [ 69.2857, 0.50971, 1.13162, -0.34631 ] + - [ 73.4286, 0.40589, 1.18963, -0.36014 ] + - [ 77.5714, 0.30146, 1.23629, -0.37385 ] + - [ 81.7143, 0.19788, 1.27067, -0.38681 ] + - [ 85.8571, 0.09679, 1.29204, -0.39872 ] + - [ 90.0000, 0.00000, 1.30000, -0.41063 ] + - [ 94.1429, -0.06775, 1.29204, -0.41683 ] + - [ 98.2857, -0.13851, 1.27067, -0.42303 ] + - [ 102.4286, -0.21103, 1.23629, -0.42628 ] + - [ 106.5714, -0.28412, 1.18963, -0.42745 ] + - [ 110.7143, -0.35680, 1.13162, -0.42817 ] + - [ 114.8571, -0.42823, 1.06348, -0.42673 ] + - [ 119.0000, -0.49783, 0.98659, -0.42528 ] + - [ 123.1429, -0.56532, 0.90255, -0.42535 ] + - [ 127.2857, -0.63079, 0.81310, -0.42589 ] + - [ 131.4286, -0.69477, 0.72010, -0.42974 ] + - [ 135.5714, -0.75845, 0.62546, -0.43985 ] + - [ 139.7143, -0.82382, 0.53115, -0.44996 ] + - [ 143.8571, -0.89412, 0.43913, -0.46839 ] + - [ 148.0000, -0.97442, 0.35131, -0.48743 ] + - [ 150.2857, -0.90482, 0.30532, -0.49447 ] + - [ 152.5714, -0.83522, 0.26146, -0.47732 ] + - [ 154.8571, -0.76562, 0.22000, -0.46017 ] + - [ 157.1429, -0.69601, 0.18121, -0.44302 ] + - [ 159.4286, -0.62641, 0.14533, -0.42586 ] + - [ 161.7143, -0.55681, 0.11258, -0.43502 ] + - [ 164.0000, -0.48721, 0.08319, -0.45294 ] + - [ 166.2857, -0.41761, 0.05732, -0.47087 ] + - [ 168.5714, -0.34801, 0.04638, -0.48880 ] + - [ 170.8571, -0.27841, 0.04114, -0.45714 ] + - [ 173.1429, -0.20880, 0.03702, -0.34286 ] + - [ 175.4286, -0.13920, 0.03406, -0.22857 ] + - [ 177.7143, -0.06960, 0.03228, -0.11429 ] + - [ 179.9087, 0.00000, 0.03169, 0.00000 ] + - name : FFA-W3-360 # + relative_thickness : 0.36 # + data: # alpha c_l c_d c_m + - [ -179.9087, 0.00000, 0.03715, 0.00000 ] + - [ -177.7143, 0.07178, 0.03774, 0.09143 ] + - [ -175.4286, 0.14356, 0.03951, 0.18286 ] + - [ -173.1429, 0.21534, 0.04245, 0.27429 ] + - [ -170.8572, 0.28713, 0.04653, 0.36571 ] + - [ -168.5716, 0.35891, 0.05174, 0.40313 ] + - [ -166.2857, 0.43069, 0.06068, 0.40814 ] + - [ -164.0000, 0.50247, 0.08651, 0.41315 ] + - [ -161.7145, 0.57425, 0.11586, 0.41816 ] + - [ -159.4284, 0.64603, 0.14856, 0.42627 ] + - [ -157.1428, 0.71781, 0.18439, 0.44370 ] + - [ -154.8573, 0.78960, 0.22313, 0.46114 ] + - [ -152.5714, 0.86138, 0.26453, 0.47857 ] + - [ -150.2857, 0.93316, 0.30832, 0.49600 ] + - [ -148.0000, 1.00494, 0.35424, 0.48830 ] + - [ -143.8571, 0.91898, 0.44192, 0.46784 ] + - [ -139.7143, 0.84406, 0.53379, 0.44803 ] + - [ -135.5714, 0.77483, 0.62793, 0.43697 ] + - [ -131.4286, 0.70790, 0.72238, 0.42591 ] + - [ -127.2857, 0.64116, 0.81520, 0.42150 ] + - [ -123.1429, 0.57335, 0.90444, 0.42058 ] + - [ -119.0000, 0.50388, 0.98826, 0.42024 ] + - [ -114.8571, 0.43261, 1.06493, 0.42168 ] + - [ -110.7143, 0.35981, 1.13285, 0.42312 ] + - [ -106.5714, 0.28603, 1.19061, 0.42258 ] + - [ -102.4286, 0.21209, 1.23704, 0.42163 ] + - [ -98.2857, 0.13899, 1.27116, 0.41864 ] + - [ -94.1429, 0.06787, 1.29229, 0.41277 ] + - [ -90.0000, 0.00000, 1.30000, 0.40690 ] + - [ -85.8571, -0.06787, 1.29229, 0.39426 ] + - [ -81.7143, -0.13899, 1.27116, 0.38162 ] + - [ -77.5714, -0.21209, 1.23704, 0.36676 ] + - [ -73.4286, -0.28603, 1.19061, 0.35033 ] + - [ -69.2857, -0.35981, 1.13285, 0.33362 ] + - [ -65.1429, -0.43261, 1.06493, 0.31561 ] + - [ -61.0000, -0.50388, 0.98826, 0.29759 ] + - [ -56.8571, -0.57335, 0.90444, 0.27989 ] + - [ -52.7143, -0.64116, 0.81520, 0.26230 ] + - [ -48.5714, -0.70790, 0.72238, 0.24491 ] + - [ -44.4286, -0.77483, 0.62793, 0.22794 ] + - [ -40.2857, -0.84406, 0.53379, 0.21097 ] + - [ -36.1429, -0.91898, 0.44192, 0.13525 ] + - [ -32.0000, -1.00494, 0.35424, 0.05517 ] + - [ -28.0000, -1.11306, 0.20494, 0.03211 ] + - [ -24.0000, -1.05425, 0.15434, 0.01268 ] + - [ -20.0000, -0.98247, 0.10967, -0.00282 ] + - [ -18.0000, -0.94173, 0.09249, -0.00741 ] + - [ -16.0000, -0.89333, 0.07597, -0.01107 ] + - [ -14.0000, -0.85472, 0.06054, -0.01250 ] + - [ -12.0000, -0.82348, 0.04641, -0.01177 ] + - [ -10.0000, -0.79541, 0.03441, -0.01082 ] + - [ -8.0000, -0.63650, 0.02548, -0.02769 ] + - [ -6.0000, -0.39095, 0.01994, -0.05107 ] + - [ -4.0000, -0.13071, 0.01653, -0.07148 ] + - [ -2.0000, 0.16173, 0.01507, -0.09179 ] + - [ -1.0000, 0.31121, 0.01477, -0.10119 ] + - [ 0.0000, 0.45956, 0.01465, -0.10988 ] + - [ 1.0000, 0.60566, 0.01466, -0.11776 ] + - [ 2.0000, 0.74868, 0.01481, -0.12477 ] + - [ 3.0000, 0.88862, 0.01507, -0.13098 ] + - [ 4.0000, 1.02544, 0.01544, -0.13648 ] + - [ 5.0000, 1.15878, 0.01593, -0.14130 ] + - [ 6.0000, 1.28822, 0.01654, -0.14540 ] + - [ 7.0000, 1.41282, 0.01731, -0.14875 ] + - [ 8.0000, 1.53090, 0.01831, -0.15118 ] + - [ 9.0000, 1.64065, 0.01963, -0.15262 ] + - [ 10.0000, 1.73926, 0.02150, -0.15310 ] + - [ 11.0000, 1.81971, 0.02445, -0.15254 ] + - [ 12.0000, 1.87065, 0.02966, -0.15121 ] + - [ 13.0000, 1.89221, 0.03770, -0.14969 ] + - [ 14.0000, 1.87910, 0.04824, -0.14562 ] + - [ 15.0000, 1.88111, 0.05838, -0.14358 ] + - [ 16.0000, 1.86359, 0.06992, -0.14095 ] + - [ 18.0000, 1.73324, 0.10166, -0.13711 ] + - [ 20.0000, 1.59357, 0.13916, -0.14082 ] + - [ 24.0000, 1.46708, 0.21002, -0.15693 ] + - [ 28.0000, 1.44834, 0.28200, -0.17979 ] + - [ 32.0000, 1.43563, 0.35424, -0.20147 ] + - [ 36.1429, 1.31283, 0.44192, -0.22409 ] + - [ 40.2857, 1.20580, 0.53379, -0.24619 ] + - [ 44.4286, 1.10690, 0.62793, -0.26133 ] + - [ 48.5714, 1.01129, 0.72238, -0.27648 ] + - [ 52.7143, 0.91594, 0.81520, -0.29062 ] + - [ 56.8571, 0.81907, 0.90444, -0.30424 ] + - [ 61.0000, 0.71982, 0.98826, -0.31787 ] + - [ 65.1429, 0.61801, 1.06493, -0.33154 ] + - [ 69.2857, 0.51401, 1.13285, -0.34522 ] + - [ 73.4286, 0.40862, 1.19061, -0.35846 ] + - [ 77.5714, 0.30299, 1.23704, -0.37161 ] + - [ 81.7143, 0.19855, 1.27116, -0.38405 ] + - [ 85.8571, 0.09695, 1.29229, -0.39547 ] + - [ 90.0000, 0.00000, 1.30000, -0.40690 ] + - [ 94.1429, -0.06787, 1.29229, -0.41277 ] + - [ 98.2857, -0.13899, 1.27116, -0.41864 ] + - [ 102.4286, -0.21209, 1.23704, -0.42163 ] + - [ 106.5714, -0.28603, 1.19061, -0.42258 ] + - [ 110.7143, -0.35981, 1.13285, -0.42312 ] + - [ 114.8571, -0.43261, 1.06493, -0.42168 ] + - [ 119.0000, -0.50388, 0.98826, -0.42024 ] + - [ 123.1429, -0.57335, 0.90444, -0.42058 ] + - [ 127.2857, -0.64116, 0.81520, -0.42150 ] + - [ 131.4286, -0.70790, 0.72238, -0.42591 ] + - [ 135.5714, -0.77483, 0.62793, -0.43697 ] + - [ 139.7143, -0.84406, 0.53379, -0.44803 ] + - [ 143.8571, -0.91898, 0.44192, -0.46784 ] + - [ 148.0000, -1.00494, 0.35424, -0.48830 ] + - [ 150.2857, -0.93316, 0.30832, -0.49600 ] + - [ 152.5714, -0.86138, 0.26453, -0.47857 ] + - [ 154.8571, -0.78960, 0.22313, -0.46114 ] + - [ 157.1429, -0.71781, 0.18439, -0.44370 ] + - [ 159.4286, -0.64603, 0.14856, -0.42627 ] + - [ 161.7143, -0.57425, 0.11586, -0.43530 ] + - [ 164.0000, -0.50247, 0.08651, -0.45315 ] + - [ 166.2857, -0.43069, 0.06068, -0.47100 ] + - [ 168.5714, -0.35891, 0.05174, -0.48884 ] + - [ 170.8571, -0.28713, 0.04653, -0.45714 ] + - [ 173.1429, -0.21534, 0.04245, -0.34286 ] + - [ 175.4286, -0.14356, 0.03951, -0.22857 ] + - [ 177.7143, -0.07178, 0.03774, -0.11429 ] + - [ 179.9087, 0.00000, 0.03715, 0.00000 ] + + pitch_control: + GS_Angles: [0.06019804, 0.08713416, 0.10844806, 0.12685912, 0.14339822, 0.1586021 , 0.17279614, 0.18618935, 0.19892772, 0.21111989, 0.22285021, 0.23417256, 0.2451469 , 0.25580691, 0.26619545, 0.27632495, 0.28623134, 0.29593266, 0.30544521, 0.314779 , 0.32395154, 0.33297489, 0.3418577 , 0.35060844, 0.35923641, 0.36774807, 0.37614942, 0.38444655, 0.39264363, 0.40074407] + GS_Kp: [-0.9394215 , -0.80602855, -0.69555026, -0.60254912, -0.52318192, -0.45465531, -0.39489024, -0.34230736, -0.29568537, -0.25406506, -0.2166825 , -0.18292183, -0.15228099, -0.12434663, -0.09877533, -0.0752794 , -0.05361604, -0.0335789 , -0.01499149, 0.00229803, 0.01842102, 0.03349169, 0.0476098 , 0.0608629 , 0.07332812, 0.0850737 , 0.0961602 , 0.10664158, 0.11656607, 0.12597691] + GS_Ki: [-0.07416547, -0.06719673, -0.0614251 , -0.05656651, -0.0524202 , -0.04884022, -0.04571796, -0.04297091, -0.04053528, -0.03836094, -0.03640799, -0.03464426, -0.03304352, -0.03158417, -0.03024826, -0.02902079, -0.02788904, -0.02684226, -0.02587121, -0.02496797, -0.02412567, -0.02333834, -0.02260078, -0.02190841, -0.0212572 , -0.02064359, -0.0200644 , -0.01951683, -0.01899836, -0.01850671] + Fl_Kp: -9.35 + wt_ops: # operating points: wind speed [m/s], blade pitch [deg], rotor speed [rpm] + v: [3.0, 3.266896551724138, 3.533793103448276, 3.800689655172414, 4.067586206896552, 4.334482758620689, 4.601379310344828, 4.868275862068966, 5.135172413793104, 5.402068965517241, 5.6689655172413795, 5.935862068965518, 6.2027586206896554, 6.469655172413793, 6.736551724137931, 7.00344827586207, 7.270344827586207, 7.537241379310345, 7.804137931034483, 8.071034482758622, 8.337931034482759, 8.604827586206897, 8.871724137931036, 9.138620689655173, 9.405517241379311, 9.672413793103448, 9.939310344827586, 10.206206896551725, 10.473103448275863, 10.74, 11.231724137931035, 11.723448275862069, 12.215172413793104, 12.706896551724139, 13.198620689655172, 13.690344827586207, 14.182068965517242, 14.673793103448276, 15.16551724137931, 15.657241379310346, 16.14896551724138, 16.640689655172416, 17.13241379310345, 17.624137931034483, 18.11586206896552, 18.607586206896553, 19.099310344827586, 19.591034482758623, 20.082758620689653, 20.57448275862069, 21.066206896551726, 21.557931034482756, 22.049655172413793, 22.54137931034483, 23.03310344827586, 23.524827586206897, 24.016551724137933, 24.508275862068963, 25.0] + #pitch_op: [-0.25, -0.25, -0.25, -0.25, -0.25, -0.25, -0.25, -0.25, -0.25, -0.25, -0.25, -0.25, -0.25, -0.25, -0.25, -0.25, -0.25, -0.25, -0.25, -0.25, -0.25, -0.25, -0.25, -0.25, -0.25, -0.25, -0.25, -0.25, -0.25, -0.25, 3.57152, 5.12896, 6.36736, 7.43866, 8.40197, 9.28843, 10.1161, 10.8974, 11.641, 12.3529, 13.038, 13.6997, 14.3409, 14.9642, 15.5713, 16.1639, 16.7435, 17.3109, 17.8673, 18.4136, 18.9506, 19.4788, 19.9989, 20.5112, 21.0164, 21.5147, 22.0067, 22.4925, 22.9724] # original + pitch_op: [3.44, 3.44, 3.44, 3.44, 3.44, 3.44, 3.19, 2.94, 2.65, 2.32, 1.97, 1.59, 1.19, 0.79, 0.38, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.46, 1.27, 1.99, 2.61, 3.05, 3.69, 5.13, 6.37, 7.44, 8.40, 9.29, 10.12, 10.90, 11.64, 12.35, 13.04, 13.70, 14.34, 14.96, 15.57, 16.16, 16.74, 17.31, 17.87, 18.41, 18.95, 19.48, 20.00, 20.51, 21.02, 21.51, 22.01, 22.49, 22.97] # updated with min pitch to achieve peak thrust shaving + omega_op: [2.1486, 2.3397, 2.5309, 2.722, 2.9132, 3.1043, 3.2955, 3.4866, 3.6778, 3.8689, 4.0601, 4.2512, 4.4424, 4.6335, 4.8247, 5.0159, 5.207, 5.3982, 5.5893, 5.7805, 5.9716, 6.1628, 6.3539, 6.5451, 6.7362, 6.9274, 7.1185, 7.3097, 7.5008, 7.56, 7.56, 7.56, 7.56, 7.56, 7.56, 7.56, 7.56, 7.56, 7.56, 7.56, 7.56, 7.56, 7.56, 7.56, 7.56, 7.56, 7.56, 7.56, 7.56, 7.56, 7.56, 7.56, 7.56, 7.56, 7.56, 7.56, 7.56, 7.56, 7.56] + gear_ratio: 1 + torque_control: + VS_KP: -38609162.66552 + VS_KI: -4588245.18720 + + + tower: # (could remove some entries that don't apply for the tower) + dlsMax : 5.0 # maximum node splitting section amount; can't be 0 + + name : tower # [-] an identifier (no longer has to be number) + type : 1 # [-] + rA : [ 0, 0, 15] # [m] end A coordinates + rB : [ 0, 0, 144.582] # [m] and B coordinates + shape : circ # [-] circular or rectangular + gamma : 0.0 # [deg] twist angle about the member's z-axis + + # --- outer shell including hydro--- + stations : [ 15, 28, 28.001, 41, 41.001, 54, 54.001, 67, 67.001, 80, 80.001, 93, 93.001, 106, 106.001, 119, 119.001, 132, 132.001, 144.582 ] # [-] location of stations along axis. Will be normalized such that start value maps to rA and end value to rB + d : [ 10, 9.964, 9.964, 9.967, 9.967, 9.927, 9.927, 9.528, 9.528, 9.149, 9.149, 8.945, 8.945, 8.735, 8.735, 8.405, 8.405, 7.321, 7.321, 6.5 ] # [m] diameters if circular or side lengths if rectangular (can be pairs) + t : [ 0.082954, 0.082954, 0.083073, 0.083073, 0.082799, 0.082799, 0.0299, 0.0299, 0.027842, 0.027842, 0.025567, 0.025567, 0.022854, 0.022854, 0.02025, 0.02025, 0.018339, 0.018339, 0.021211, 0.021211 ] # [m] wall thicknesses (scalar or list of same length as stations) + Cd : 0.0 # [-] transverse drag coefficient (optional, scalar or list of same length as stations) + Ca : 0.0 # [-] transverse added mass coefficient (optional, scalar or list of same length as stations) + # (neglecting axial coefficients for now) + CdEnd : 0.0 # [-] end axial drag coefficient (optional, scalar or list of same length as stations) + CaEnd : 0.0 # [-] end axial added mass coefficient (optional, scalar or list of same length as stations) + rho_shell : 7850 # [kg/m3] material density + +platforms: + + - potModMaster : 1 # [int] master switch for potMod variables; 0=keeps all member potMod vars the same, 1=turns all potMod vars to False (no HAMS), 2=turns all potMod vars to True (no strip) + dlsMax : 5.0 # maximum node splitting section amount for platform members; can't be 0 + rFair : 40.5 # platform fairlead radius + zFair : -20 # platform fairlead z-location + type : FOWT + fairleads : # list of fairlead coordinates for the platform relative to platform coordinate and 0-degree heading + - name: fairlead1 + r_rel: [40.5,0,-20] + headings: [270, 30, 150] # headings in degrees for the fairlead (if multiple headings, the fairlead will be repeated for each heading) + Jtubes : # list of Jtube coordinates for the platform relative to platform coordinate and 0-degree heading + - name: Jtube1 + r_rel: [5,0,-20] + headings: [90, 210, 330] # headings in degrees for the Jtube (if multiple headings, the Jtube will be repeated for each heading) + + members: # list all members here + + - name : center_column # [-] an identifier (no longer has to be number) + type : 2 # [-] + rA : [ 0, 0, -20] # [m] end A coordinates + rB : [ 0, 0, 15] # [m] and B coordinates + shape : circ # [-] circular or rectangular + gamma : 0.0 # [deg] twist angle about the member's z-axis + potMod : True # [bool] Whether to model the member with potential flow (BEM model) plus viscous drag or purely strip theory + # --- outer shell including hydro--- + stations : [0, 1] # [-] location of stations along axis. Will be normalized such that start value maps to rA and end value to rB + d : 10.0 # [m] diameters if circular or side lengths if rectangular (can be pairs) + t : 0.05 # [m] wall thicknesses (scalar or list of same length as stations) + Cd : 0.6 # [-] transverse drag coefficient (optional, scalar or list of same length as stations) + Ca : 0.93 # [-] transverse added mass coefficient (optional, scalar or list of same length as stations) + CdEnd : 0.6 # [-] end axial drag coefficient (optional, scalar or list of same length as stations) + CaEnd : 1.0 # [-] end axial added mass coefficient (optional, scalar or list of same length as stations) + rho_shell : 7850 # [kg/m3] + # --- handling of end caps or any internal structures if we need them --- + cap_stations : [ 0 ] # [m] location along member of any inner structures (in same scaling as set by 'stations') + cap_t : [ 0.001 ] # [m] thickness of any internal structures + cap_d_in : [ 0 ] # [m] inner diameter of internal structures (0 for full cap/bulkhead, >0 for a ring shape) + + + - name : outer_column # [-] an identifier (no longer has to be number) + type : 2 # [-] + rA : [51.75, 0, -20] # [m] end A coordinates + rB : [51.75, 0, 15] # [m] and B coordinates + heading : [ 60, 180, 300] # [deg] heading rotation of column about z axis (for repeated members) + shape : circ # [-] circular or rectangular + gamma : 0.0 # [deg] twist angle about the member's z-axis + potMod : True # [bool] Whether to model the member with potential flow (BEM model) plus viscous drag or purely strip theory + # --- outer shell including hydro--- + stations : [0, 35] # [-] location of stations along axis. Will be normalized such that start value maps to rA and end value to rB + d : 12.5 # [m] diameters if circular or side lengths if rectangular (can be pairs) + t : 0.05 # [m] wall thicknesses (scalar or list of same length as stations) + Cd : 0.6 # [-] transverse drag coefficient (optional, scalar or list of same length as stations) + Ca : 0.93 # [-] transverse added mass coefficient (optional, scalar or list of same length as stations) + CdEnd : 1.0 # [-] end axial drag coefficient (optional, scalar or list of same length as stations) + CaEnd : 0.7 # value of 3.0 gives more heave response # [-] end axial added mass coefficient (optional, scalar or list of same length as stations) + rho_shell : 7850 # [kg/m3] + # --- ballast --- + l_fill : 1.4 # [m] + rho_fill : 5000 # [kg/m3] + # --- handling of end caps or any internal structures if we need them --- + cap_stations : [ 0 ] # [m] location along member of any inner structures (in same scaling as set by 'stations') + cap_t : [ 0.001 ] # [m] thickness of any internal structures + cap_d_in : [ 0 ] # [m] inner diameter of internal structures (0 for full cap/bulkhead, >0 for a ring shape) + + + - name : pontoon # [-] an identifier (no longer has to be number) + type : 2 # [-] + rA : [ 5 , 0, -16.5] # [m] end A coordinates + rB : [ 45.5, 0, -16.5] # [m] and B coordinates + heading : [ 60, 180, 300] # [deg] heading rotation of column about z axis (for repeated members) + shape : rect # [-] circular or rectangular + gamma : 0.0 # [deg] twist angle about the member's z-axis + potMod : False # [bool] Whether to model the member with potential flow (BEM model) plus viscous drag or purely strip theory + # --- outer shell including hydro--- + stations : [0, 40.5] # [-] location of stations along axis. Will be normalized such that start value maps to rA and end value to rB + d : [12.4, 7.0] # [m] diameters if circular or side lengths if rectangular (can be pairs) + t : 0.05 # [m] wall thicknesses (scalar or list of same length as stations) + Cd : [1.5, 2.2 ] # [-] transverse drag coefficient (optional, scalar or list of same length as stations) + Ca : [2.2, 0.2 ] # [-] transverse added mass coefficient (optional, scalar or list of same length as stations) + CdEnd : 0.0 # [-] end axial drag coefficient (optional, scalar or list of same length as stations) + CaEnd : 0.0 # [-] end axial added mass coefficient (optional, scalar or list of same length as stations) + rho_shell : 7850 # [kg/m3] + l_fill : 40.5 # [m] + rho_fill : 1025.0 # [kg/m3] + + + - name : upper_support # [-] an identifier (no longer has to be number) + type : 2 # [-] + rA : [ 5 , 0, 14.545] # [m] end A coordinates + rB : [ 45.5, 0, 14.545] # [m] and B coordinates + heading : [ 60, 180, 300] # [deg] heading rotation of column about z axis (for repeated members) + shape : circ # [-] circular or rectangular + gamma : 0.0 # [deg] twist angle about the member's z-axis + potMod : False # [bool] Whether to model the member with potential flow (BEM model) plus viscous drag or purely strip theory + # --- outer shell including hydro--- + stations : [0, 1] # [-] location of stations along axis. Will be normalized such that start value maps to rA and end value to rB + d : 0.91 # [m] diameters if circular or side lengths if rectangular (can be pairs) + t : 0.01 # [m] wall thicknesses (scalar or list of same length as stations) + Cd : 0.0 # [-] transverse drag coefficient (optional, scalar or list of same length as stations) + Ca : 0.0 # [-] transverse added mass coefficient (optional, scalar or list of same length as stations) + CdEnd : 0.0 # [-] end axial drag coefficient (optional, scalar or list of same length as stations) + CaEnd : 0.0 # [-] end axial added mass coefficient (optional, scalar or list of same length as stations) + rho_shell : 7850 # [kg/m3] + + +# ----- Mooring system ----- + +# Mooring system descriptions (each for an individual FOWT with no sharing) +mooring_systems: + + ms1: + name: 3 line taut poly mooring system + + keys: [MooringConfigID, heading, anchorType, fairlead] + data: + - [ rope_1, 45 , suction_pile1, 2 ] + - [ rope_1, 135 , suction_pile1, 3 ] + - [ rope_1, 270 , suction_pile1, 1 ] + + +# Mooring line configurations +mooring_line_configs: + + rope_1: # mooring line configuration identifier + + name: rope configuration 1 # descriptive name + + span: 1131.37 + + + sections: #in order from anchor to fairlead + - type: chain_155mm + length: 20 + - type: rope # ID of a mooring line section type + length: 1170 # [m] usntretched length of line section + adjustable: True # flags that this section could be adjusted to accommodate different spacings... + + +# Mooring line cross-sectional properties +mooring_line_types: + + rope: + d_nom: 0.2246 + d_vol: 0.1797 + m: 34.85 + EA: 4.761e7 + MBL: 11.75e6 + material: rope + + chain_155mm: + d_nom: 0.155 # [m] nominal diameter + d_vol: 0.279 # [m] volume-equivalent diameter + m: 480.9 # [kg/m] mass per unit length (linear density) + EA: 2058e6 # [N] quasi-static stiffness + MBL: 25.2e6 # [N] minimum breaking load + cost: 1486 # [$/m] cost per unit length + material: chain # [-] material composition descriptor + material details: R3 studless + +# Anchor type properties +anchor_types: + suction_pile1: + type : suction_pile + L : 16.4 # length of pile [m] + D : 5.45 # diameter of pile [m] + zlug : 9.32 # embedded depth of padeye [m] \ No newline at end of file diff --git a/tests/testOntology.yaml b/tests/testOntology.yaml index b554d5f0..dca64548 100644 --- a/tests/testOntology.yaml +++ b/tests/testOntology.yaml @@ -125,20 +125,20 @@ array_mooring: # - [ 5, suction1, -1900 , 0 , 2 ] line_keys : - [MooringConfigID , endA, endB, headingA, headingB, lengthAdjust] + [MooringConfigID , endA, endB, fairleadA, fairleadB, lengthAdjust] line_data : - - [ rope_shared , FOWT1, FOWT2, 270, 270, 0] - - [ rope_1 , Anch1, FOWT1, NONE, 135, 0] - - [ rope_1 , Anch1, FOWT3, NONE, 45, 0] + - [ rope_shared , FOWT1, FOWT2, 1, 1, 0] + - [ rope_1 , Anch1, FOWT1, NONE, 3, 0] + - [ rope_1 , Anch1, FOWT3, NONE, 2, 0] # - [ shared-2-clump , FOWT 2, FOWT 3, 0, 0, 0] # Array cables (compact table format, without routing info) array_cables: - keys: [ AttachA, AttachB, DynCableA, DynCableB, headingA, headingB, cableType, lengthAdjust] + keys: [ AttachA, AttachB, DynCableA, DynCableB, JtubeA, JtubeB, headingA, headingB, cableType, lengthAdjust] data: - - [ FOWT1, FOWT3, suspended_1, NONE, 180, 0, NONE, 0] - - [ FOWT3, FOWT4, lazy_wave1, lazy_wave1, 90, 90, static_cable_36, 0] + - [ FOWT1, FOWT3, suspended_1, NONE, 2, 3, 180, 0, NONE, 0] + - [ FOWT3, FOWT4, lazy_wave1, lazy_wave1, 3, 1, 285, 80, static_cable_36, 0] # ----- turbines and platforms ----- @@ -1203,6 +1203,14 @@ platforms: rFair : 40.5 # platform fairlead radius zFair : -20 # platform fairlead z-location type : FOWT + fairleads : # list of fairlead coordinates for the platform relative to platform coordinate and 0-degree heading + - name: fairlead1 + r_rel: [40.5,0,-20] + headings: [270, 30, 150] # headings in degrees for the fairlead (if multiple headings, the fairlead will be repeated for each heading) + Jtubes : # list of Jtube coordinates for the platform relative to platform coordinate and 0-degree heading + - name: Jtube1 + r_rel: [5, 0, -20] + headings: [90, 210, 330] # headings in degrees for the Jtube (if multiple headings, the Jtube will be repeated for each heading) members: # list all members here @@ -1302,34 +1310,34 @@ mooring_systems: ms1: name: 3-line semi-taut polyester mooring system with one line shared anchor - keys: [MooringConfigID, heading, anchorType, lengthAdjust] + keys: [MooringConfigID, heading, anchorType, fairlead] data: - - [ rope_1, 270 , suction_pile1, 0 ] - - [ rope_1, 135 , suction_pile1, 0 ] + - [ rope_1, 270 , suction_pile1, 1 ] + - [ rope_1, 135 , suction_pile1, 3 ] ms2: name: 2-line semitaut with a third shared line - keys: [MooringConfigID, heading, anchorType, lengthAdjust] + keys: [MooringConfigID, heading, anchorType, fairlead] data: - - [ rope_1, 45 , suction_pile1, 0 ] - - [ rope_1, 135 , suction_pile1, 0 ] + - [ rope_1, 45 , suction_pile1, 2 ] + - [ rope_1, 135 , suction_pile1, 3 ] ms3: name: 3-line semi-taut polyester mooring system with one line shared anchor and one shared line - keys: [MooringConfigID, heading, anchorType, lengthAdjust] + keys: [MooringConfigID, heading, anchorType, fairlead] data: - - [ rope_1, 45 , suction_pile1, 0 ] + - [ rope_1, 45 , suction_pile1, 2 ] ms4: name: 3 line taut poly mooring system - keys: [MooringConfigID, heading, anchorType, lengthAdjust] + keys: [MooringConfigID, heading, anchorType, fairlead] data: - - [ rope_1, 45 , suction_pile1, 0 ] - - [ rope_1, 135 , suction_pile1, 0 ] - - [ rope_1, 270 , suction_pile1, 0 ] + - [ rope_1, 45 , suction_pile1, 2 ] + - [ rope_1, 135 , suction_pile1, 3 ] + - [ rope_1, 270 , suction_pile1, 1 ] # Mooring line configurations @@ -1524,10 +1532,12 @@ cables: attachID: FOWT1 # FOWT/substation/junction ID heading: 270 # [deg] heading of attachment at end A dynamicID: lazy_wave1 # ID of dynamic cable configuration at this end + Jtube: 2 endB: attachID: FOWT2 heading: 270 dynamicID: lazy_wave1 + Jtube: 2 routing_x_y_r: - [700,150,20] # [x (m), y (m), radius (m)] - [900,150,20] diff --git a/tests/test_anchors.py b/tests/test_anchors.py index 0292c66a..ae177770 100644 --- a/tests/test_anchors.py +++ b/tests/test_anchors.py @@ -2,12 +2,16 @@ from famodel.project import Project import numpy as np import os +import matplotlib.pyplot as plt +import pytest +@pytest.fixture +def project(): + dir = os.path.dirname(os.path.realpath(__file__)) + return(Project(file=os.path.join(dir,'testOntology.yaml'), raft=False)) -def test_anchor_loads(): +def test_anchor_loads(project): # load in famodel project - dir = os.path.dirname(os.path.realpath(__file__)) - project = Project(file=os.path.join(dir,'testOntology.yaml'), raft=False) project.getMoorPyArray(cables=1) anch = project.anchorList['FOWT1a'] @@ -18,10 +22,8 @@ def test_anchor_loads(): assert('Hm' in anch.loads) assert(anch.loads['Ha'] != anch.loads['Hm']) -def test_anchor_capacities(): +def test_anchor_capacities(project): # load in famodel project (suction pile anchor) - dir = os.path.dirname(os.path.realpath(__file__)) - project = Project(file=os.path.join(dir,'testOntology.yaml'), raft=False) project.getMoorPyArray(cables=1) anch = project.anchorList['FOWT1a'] diff --git a/tests/test_integrations.py b/tests/test_integrations.py index d028035e..ced4120b 100644 --- a/tests/test_integrations.py +++ b/tests/test_integrations.py @@ -15,10 +15,12 @@ import os - -def test_MoorPy_integration(): +@pytest.fixture +def project(): dir = os.path.dirname(os.path.realpath(__file__)) - project = Project(file=os.path.join(dir,'testOntology.yaml'), raft=False) + return(Project(file=os.path.join(dir,'testOntology.yaml'), raft=False)) + +def test_MoorPy_integration(project): project.getMoorPyArray(cables=1,plt=1) # check a random mooring line for ss assert project.mooringList['FOWT1a'].ss is not None @@ -30,12 +32,11 @@ def test_RAFT_integration(): '''def test_FLORIS_integration():''' -def test_lineDesign_integration(): +def test_lineDesign_integration(project): # make a dummy design dictionary for Mooring to make a Subsystem with - dd = dict(sections={}, connectors={}) - dd['sections'] = [{} for i in range(1)] - dd['connectors'] = [{} for i in range(2)] + dd = dict(subcomponents=[]) + dd['subcomponents'] = [{} for i in range(3)] import moorpy as mp ms = mp.System(depth=200) # the sizing function coefficients to use in the design @@ -44,8 +45,8 @@ def test_lineDesign_integration(): # Assign section properties for use in Mooring's Subsystem.makeGeneric call for i in range(1): - dd['sections'][i]['type'] = lineProps[i] - dd['sections'][i]['L'] = lengths[i] + dd['subcomponents'][i+1]['type'] = lineProps[i] + dd['subcomponents'][i+1]['L'] = lengths[i] # # Assign props for intermediate points/connectors # for i in range(self.nLines-1): diff --git a/tests/test_moorings.py b/tests/test_moorings.py new file mode 100644 index 00000000..5dcc855d --- /dev/null +++ b/tests/test_moorings.py @@ -0,0 +1,169 @@ + +""" +Test mooring loading, configurations, methods +""" +import pytest +from pytest import approx + +import numpy as np +from numpy.testing import assert_allclose + +from famodel.project import Project + +from famodel.platform.fairlead import Fairlead +from famodel.platform.platform import Platform +from famodel.mooring.connector import Section, Connector +from famodel.famodel_base import Node, Edge + +import os + + +@pytest.fixture +def setup_project(): + dir = os.path.dirname(os.path.realpath(__file__)) + return(Project(file=os.path.join(dir,'mooring_ontology.yaml'), raft=False)) + + +def test_num_moorings(setup_project): + + assert(len(setup_project.mooringList)==11) + +def test_moor_heading(setup_project): + + moor = setup_project.mooringList['FOWT1a'] + dists = moor.rA[:2]-moor.rB[:2] + heading = np.pi/2 - np.arctan2(dists[1], dists[0]) + pf = setup_project.platformList['FOWT1'] + assert(heading == approx(np.radians(45+180),abs=1e-5)) + assert(heading == approx(pf.mooring_headings[0]+pf.phi,abs=1e-5)) + assert(heading == approx(np.radians(moor.heading),abs=1e-5)) + +def test_platform_connection(setup_project): + + moor = setup_project.mooringList['FOWT1a'] + assert(moor.attached_to[0]==setup_project.anchorList['FOWT1a']) + assert(moor.attached_to[1]==setup_project.platformList['FOWT1']) + +def test_fairlead_connection(setup_project): + + end_sub = setup_project.mooringList['FOWT1a'].subcomponents[-1] + assert(len(end_sub.attachments)==2) + assert(np.any([isinstance(att['obj'], Fairlead) for att in end_sub.attachments.values()])) + +def test_fairlead_position(setup_project): + moor = setup_project.mooringList['FOWT1a'] + fl = moor.subcomponents[-1].attachments['FOWT1_F1']['obj'] + # check fairlead is at same position as moor.rB + assert_allclose(fl.r,setup_project.mooringList['FOWT1a'].rB) + pf = setup_project.platformList['FOWT1'] + new_head = 15 + pf.setPosition(r = pf.r, + heading=new_head, + degrees=True, + project=setup_project) + head_fl = np.radians(90-30) # relative heading of fairlead to platform + head_pf = pf.phi + # calculate moor.rB manually and compare to fairlead position + # heading is 90-compass fairlead heading - platform heading + # ( compass platform heading = - unit circle platform heading ) + # this is because platform 0 is the same for both since it's user-defined + # but the direction they rotate is opposite + # but mooring headings are based on conventional 0s for compass and unit circle + # AND their rotation direction is opposite + assert_allclose(fl.r,[58*np.cos(head_fl-head_pf), + 58*np.sin(head_fl-head_pf),-14]) + +def test_rA_depth(setup_project): + moor = setup_project.mooringList['FOWT1a'] + loc = moor.rA + true_depth = setup_project.getDepthAtLocation(loc[0],loc[1]) + assert(moor.rA[2] == -true_depth) + assert(moor.dd['zAnchor'] == -true_depth) + assert(moor.z_anch == -true_depth) + setup_project.getMoorPyArray() + assert(moor.ss.rA[2] == -true_depth) + +''' + +def test_end_locs(self): + moor = self.project.mooringList['fowt1a'] + assert(moor.rB == ) + assert(moor.rA == ) +''' + +def test_num_sections(setup_project): + moor = setup_project.mooringList['FOWT1a'] + setup_project.getMoorPyArray() + assert(len(moor.i_sec)==len(moor.ss.lineList)) + assert(len(moor.i_sec)==2) + +def test_num_connectors(setup_project): + moor = setup_project.mooringList['FOWT1a'] + assert(len(moor.i_con)==3) + +def test_shared_connections(setup_project): + + moor = setup_project.mooringList['FOWT1-FOWT2'] + assert(len(moor.subcomponents[0].attachments)==2) + assert(np.any([isinstance(att['obj'], Fairlead) for att in moor.subcomponents[0].attachments.values()])) + assert(isinstance(moor.attached_to[0], Platform)) + assert(isinstance(moor.attached_to[1], Platform)) + +def test_shared_flag(setup_project): + + moor = setup_project.mooringList['FOWT1-FOWT2'] + assert(moor.shared == 1) + +# - - - -tests in progress- - - - + +@pytest.fixture +def bridle_project(): + dir = os.path.dirname(os.path.realpath(__file__)) + return(Project(file=os.path.join(dir,'mooring_ontology_parallels.yaml'), raft=False)) + +def test_bridle_setup(bridle_project): + moor = bridle_project.mooringList['FOWT2a'] + # check subcons_B is a list of length 2 + assert(len(moor.subcons_B)==2) + # check each item in subcons_B is attached to 2 things (fairlead and another subcomponent) + for sub in moor.subcons_B: + assert(isinstance(sub,Connector)) + assert(len(sub.attachments)==2) + for att in sub.attachments.values(): + assert(isinstance(att['obj'],(Fairlead,Section))) + pf = moor.attached_to[1] + fl_attachment = [False, False] + for i,sub in enumerate(moor.subcons_B): + for att in pf.attachments.values(): + if isinstance(att['obj'],Node) and sub.id in att['obj'].attachments: + fl_attachment[i] = True + + assert(all(fl_attachment)) + +def test_bridle_end_locs(bridle_project): + moor = bridle_project.mooringList['FOWT1a'] + # check rB is at midpoint of fairlead locs + fl_locs = [] + for sub in moor.subcons_B: + att = [att['obj'] for att in sub.attachments.values() if isinstance(att['obj'],Fairlead)] + fl_locs.append(att[0].r) + from famodel.helpers import calc_midpoint + midpoint = calc_midpoint(fl_locs) + assert_allclose(midpoint, moor.rB) + # check + # check location of anchor is correct + u = np.array([np.cos(np.radians(90-moor.heading)),np.sin(np.radians(90-moor.heading))]) + anch_loc = np.hstack((np.array(midpoint[:2])+moor.span*u,-bridle_project.depth)) + assert_allclose(anch_loc, moor.rA) + + + + +''' +def test_shared_depth(self): + + moor = self.project.mooringList['fowt1b'] + self.project.getMoorPyArray() + assert(moor.ss) +''' + diff --git a/tests/test_platform.py b/tests/test_platform.py new file mode 100644 index 00000000..d16365c7 --- /dev/null +++ b/tests/test_platform.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +""" +Created on Mon Jul 14 13:33:12 2025 + +@author: lsirkis +""" +import pytest +import os +from famodel import Project +import numpy as np +from copy import deepcopy +from numpy.testing import assert_allclose + +@pytest.fixture +def project(): + dir = os.path.dirname(os.path.realpath(__file__)) + return(Project(file=os.path.join(dir,'platform_ontology.yaml'), raft=False)) + +def test_platform_location(project): + assert_allclose(project.platformList['FOWT2'].r, [1600, 0, 0]) + +def test_basic_pf_relocation(project): + project.platformList['FOWT1'].setPosition(r=[20, 20, -10], heading=30, degrees=True) + assert_allclose(project.platformList['FOWT1'].r,[20, 20, -10]) + assert pytest.approx(project.platformList['FOWT1'].phi) == np.radians(30) + +def test_fl_relloc(project): + fl = project.platformList['FOWT2'].attachments['FOWT2_F2'] + fl_loc = [np.cos(np.radians(60))*40.5, + np.sin(np.radians(60))*40.5, + -20] + assert_allclose(fl['r_rel'],fl_loc) + +def test_pf_relocation(project): + new_r = [1500, 1500] + new_head = 15 + moor = project.mooringList['FOWT2a'] + moor_head_start = deepcopy(project.mooringList['FOWT2a'].heading) + fl = project.platformList['FOWT2'].attachments['FOWT2_F2'] + project.platformList['FOWT2'].setPosition(r = new_r, + heading=new_head, + degrees=True, + project=project) + assert pytest.approx(moor.heading) == new_head+moor_head_start + fl_loc_new = [1500 + np.cos(np.radians(60-new_head))*40.5, + 1500 + np.sin(np.radians(60-new_head))*40.5, + -20] + fl = project.platformList['FOWT2'].attachments['FOWT2_F2'] + assert_allclose(fl['obj'].r, fl_loc_new) + moor_head_new = np.radians(90-(new_head+moor_head_start)) + new_x = fl['obj'].r[0] + np.cos(moor_head_new)*moor.span + new_y = fl['obj'].r[1] + np.sin(moor_head_new)*moor.span + new_anch_r = [new_x, + new_y, + -project.getDepthAtLocation(new_x,new_y)] + project.plot2d() + assert_allclose(project.anchorList['FOWT2a'].r, new_anch_r) + + + + diff --git a/tests/test_project.py b/tests/test_project.py index 31c92133..61aac3f9 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -108,18 +108,20 @@ def test_check_connections(): # check number of things connected to platform if i == 1 or i == 3: - assert len(pf.attachments) == 5 # 3 lines, 1 turbine, 1 cable + assert len(pf.attachments) == 11 # 3 lines, 1 turbine, 1 cable , 3 fairleads, 3 j-tubes else: - assert len(pf.attachments) == 6 # 3 lines, 1 turbine, 2 cables + assert len(pf.attachments) == 12 # 3 lines, 1 turbine, 2 cables, 3 fairleads, 3 j-tubes def test_headings_repositioning(): dir = os.path.dirname(os.path.realpath(__file__)) project = Project(file=os.path.join(dir,'testOntology.yaml'), raft=False) # check angles and repositioning for regular mooring line and shared mooring line, reg cable and suspended cable assert_allclose(np.hstack((project.mooringList['FOWT1a'].rA,project.mooringList['FOWT1-FOWT2'].rA)), - np.hstack(([-828.637,-828.637,-600],[40.5,0,-20])),rtol=0,atol=0.5) + np.hstack(([-820.25,-835.07,-600],[40.5,0,-20])),rtol=0,atol=0.5) + x_off = 5*np.cos(np.radians(-60)) + y_off = 5*np.sin(np.radians(-60)) assert_allclose(np.hstack((project.cableList['array_cable12'].subcomponents[0].rB,project.cableList['cable0'].subcomponents[0].rB)), - np.hstack(([605,0,-600],[0,1615.5,-20])),rtol=0,atol=0.5) + np.hstack(([600+x_off,0+y_off,-600],[0+x_off,1656+y_off,-20])),rtol=0,atol=0.5) def test_marine_growth(): dir = os.path.dirname(os.path.realpath(__file__))