From 4196016e9ff97647aeef34f8b8d90bb23bd8149f Mon Sep 17 00:00:00 2001 From: Matt Hall <5151457+mattEhall@users.noreply.github.com> Date: Fri, 24 Jan 2025 20:44:53 -0700 Subject: [PATCH 01/34] famodel_base node-node attachments are now hierarchical: - Changed node to node attachmenst so that one node is attached_to the other, rather than both nodes being equal. This will make it easier for more than 2 nodes to be lumped together by being part of one higher-level node. - Removed a couple subordinate flags that are then unnecessary. - Removed some older commented out code. - Compatibility with rest of FAModel not yet confirmed... --- famodel/famodel_base.py | 145 ++++++++-------------------------------- 1 file changed, 29 insertions(+), 116 deletions(-) diff --git a/famodel/famodel_base.py b/famodel/famodel_base.py index 0e66471e..42c31574 100644 --- a/famodel/famodel_base.py +++ b/famodel/famodel_base.py @@ -22,7 +22,7 @@ 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. @@ -82,7 +82,6 @@ def isAttached(self, object, end=None): raise Exception('Provided object is not an Edge or Node.') # 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 @@ -120,36 +119,23 @@ def attach(self, object, r_rel=[0,0], end=None): 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): + + # Attach the object + if isinstance(object2, Node): # (object is a node) self.attachments[object2.id] = dict(obj=object2, id=object2.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') + object2._attach_to(self) # tell it it's attached to this Node - elif isinstance(object2, Edge): + elif isinstance(object2, Edge): # (object is an 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 end in ['b', 'B', 1]: - new_entry['end'] = 'b' - object._attach_to(self, 1) - - else: - raise Exception('End A or B must be specified when attaching an edge.') - ''' + else: raise Exception('Unrecognized object type') @@ -185,41 +171,28 @@ def detach(self, object, end=None): i_end = endToIndex(end, estr='when detaching an edge from a node.') 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.') - - + - 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 @@ -334,27 +307,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. @@ -383,29 +336,6 @@ 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): @@ -429,27 +359,16 @@ def _attach_to(self, object, end): self.attached_to[i_end] = object # Recursively attach any subcomponent at the end - if len(self.subcomponents) > 0: + if len(self.subcomponents) > 0: # this edge has subcomponents subcon = self.subcomponents[-i_end] # index 0 for A, -1 for B - 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 isinstance(subcon, Node): # if the end subcomponent is a node + subcon._attach_to(object) - elif isinstance(subcon, Edge): - #print(f'attaching {object.id} to {subcon.id}') + elif isinstance(subcon, Edge): # if it's an edge subcon._attach_to(object, i_end) - # object._attach_to(subcon) - - ''' - if end: - self.sub_edges[-1]._attach_to(object, end) - else: - self.sub_edges[0]._attach_to(object, end) - ''' - + + def detachFrom(self, end): '''Detach the specified end of the edge from whatever it's attached to. @@ -506,8 +425,10 @@ 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.''' - + # Attach the sequency of nodes and edges to each other assemble(items) + + # Store them as subcomponents of this edge self.subcomponents = items for item in items: item.part_of = self @@ -532,16 +453,7 @@ def getTopLevelEdge(self, end): 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 - - ''' - supe = self.part_of # shorthand for the super edge - if end: - if supe.sub_edges[-1] == self: # if we're at super end B - return supe.getTopLevelEdge(end) - else: # end A - if supe.sub_edges[0] == self: # if we're at super end A - return supe.getTopLevelEdge(end) - ''' + else: return self, i_end # if not part of something bigger, just return self @@ -578,9 +490,10 @@ def delete(self): # 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. @@ -597,7 +510,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. ''' From 22bf547e9dccdc09dff8e449338d82b478953505 Mon Sep 17 00:00:00 2001 From: Matt Hall <5151457+mattEhall@users.noreply.github.com> Date: Fri, 31 Jan 2025 16:58:34 -0700 Subject: [PATCH 02/34] famodel_base class work in progress for more versatile edges/poly --- famodel/famodel_base.py | 290 +++++++++++++++++++++++++++++++++++-- famodel/turbine/turbine.py | 1 - 2 files changed, 279 insertions(+), 12 deletions(-) diff --git a/famodel/famodel_base.py b/famodel/famodel_base.py index 42c31574..4ca00457 100644 --- a/famodel/famodel_base.py +++ b/famodel/famodel_base.py @@ -26,6 +26,23 @@ 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??? + + + ''' class Node(): @@ -94,7 +111,7 @@ def isAttached(self, object, end=None): return False else: return False - + def attach(self, object, r_rel=[0,0], end=None): '''Attach something to this node. @@ -216,7 +233,7 @@ 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 @@ -283,7 +300,7 @@ def __init__(self, id): # 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 + 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 @@ -360,7 +377,7 @@ def _attach_to(self, object, end): # Recursively attach any subcomponent at the end if len(self.subcomponents) > 0: # this edge has subcomponents - subcon = self.subcomponents[-i_end] # index 0 for A, -1 for B + >> subcon = self.subcomponents[-i_end] # index 0 for A, -1 for B if isinstance(subcon, Node): # if the end subcomponent is a node subcon._attach_to(object) @@ -388,7 +405,7 @@ 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. + highest-level one first, then it will recursively call any lower ones. Parameters ---------- @@ -401,7 +418,7 @@ def _detach_from(self, end): # Recursively detach the ends of any sub-edges if len(self.subcomponents) > 0: - subcon = self.subcomponents[-i_end] # index 0 for A, -1 for B + >> subcon = self.subcomponents[-i_end] # index 0 for A, -1 for B if isinstance(subcon, Node): subcon._detach_from() @@ -422,14 +439,18 @@ 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.''' + '''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 times is a dict: Adds them, assuming they are already assembled. + ''' # Attach the sequency of nodes and edges to each other assemble(items) - # Store them as subcomponents of this edge - self.subcomponents = items + # Store them as subcomponents of this edge, as a dict 0:N + self.subcomponents = dict(enumerate(items)) for item in items: item.part_of = self @@ -451,7 +472,7 @@ def getTopLevelEdge(self, 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 + >> 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: @@ -491,6 +512,253 @@ def delete(self): +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 + + 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 poly 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 a poly.') + + # 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 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 + ---------- + 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 + + # 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 + + if isinstance(subcon, Node): + subcon._detach_from() + + elif isinstance(subcon, Edge): + subcon._detach_from(i_end) + + ''' + if self.sub_edges: + if end: + self.sub_edges[-1]._detach_from(end) + else: + self.sub_edges[0]._detach_from(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): + '''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? <<< + + # 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) + + + 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 + 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 + + ''' + supe = self.part_of # shorthand for the super edge + if end: + if supe.sub_edges[-1] == self: # if we're at super end B + return supe.getTopLevelEdge(end) + else: # end A + if supe.sub_edges[0] == self: # if we're at super end A + return supe.getTopLevelEdge(end) + ''' + else: + return self, i_end # if not part of something bigger, just return self + + + 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 in ['a', 'A', 0]: + self.rA = np.array(r) + elif end in ['b', 'B', 1]: + self.rB = np.array(r) + else: + 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... + + + # general functions def areAttached(object1, object2): 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 From b073d510a99e0c33a0660b1e08dc79e1114a7b53 Mon Sep 17 00:00:00 2001 From: Matt Hall <5151457+mattEhall@users.noreply.github.com> Date: Mon, 16 Jun 2025 16:05:26 -0600 Subject: [PATCH 03/34] famodel_base refactor work in progress... - Added support for "join" method to connect two nodes in a mutual way. - Updated hierarchy of how nodes and edges can be grouped and connected. - Lots still to finish and get working. --- famodel/famodel_base.py | 277 ++++++++++++++++++++++++++++++---------- 1 file changed, 209 insertions(+), 68 deletions(-) diff --git a/famodel/famodel_base.py b/famodel/famodel_base.py index 4ca00457..9aeb30a4 100644 --- a/famodel/famodel_base.py +++ b/famodel/famodel_base.py @@ -43,6 +43,28 @@ + +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(): @@ -64,10 +86,10 @@ 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 @@ -78,41 +100,97 @@ 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 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, Node): + self.part_of.attach_to( object.part_of ) ...? + elif isinstance(self.part_of, Node) and isinstance(object.part_of, Edge): + object.part_of.attach_to( self.part_of ) ...? + + + 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.part_of, Node): + self.part_of.detach...( object.part_of ) ...? + elif isinstance(self.part_of, Node) and isinstance(object.part_of, Edge): + object.part_of.detach...( self.part_of ) ...? + + def attach(self, object, r_rel=[0,0], end=None): '''Attach something to this node. Attachments are noted with an entry in self.attachments @@ -126,6 +204,14 @@ def attach(self, object, r_rel=[0,0], end=None): x and y coordinates of the attachment point [m]. 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): @@ -135,26 +221,40 @@ 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: + if object.id in self.attachments: raise Exception(f"Object {object.id} is already attached to {self.id}") # Attach the object - if isinstance(object2, Node): # (object is a node) - self.attachments[object2.id] = dict(obj=object2, id=object2.id, + if isinstance(object, Node): # (object is a node) + 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 + object._attach_to(self) # tell it it's attached to this Node - elif isinstance(object2, Edge): # (object is an edge) - self.attachments[object2.id] = dict(obj=object2, id=object2.id, + elif isinstance(object, Edge): # (object is an edge) + self.attachments[object.id] = dict(obj=object, id=object.id, r_rel=np.array(r_rel), type='edge', - end=['a', 'b'][i_end]) + end=i_end) - object2._attach_to(self, i_end) # tell it it's attached to this Node + 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) + if isinstance(object.part_of, Edge): # attached object is part of an edge + if self.part_of == None: # this node isn't part of an edge + >>> figure out which edge of parent object <<<< + 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, r_rel=r_rel, end=<<<) + else: + # attach higher edge to this node + self.attach(object.part_of, r_rel=r_rel, end=<<<) + + else: # if self is part of a higher level edge, then what?? + raise Exception("Can't currently attach two edges - even if higher level...") def detach(self, object, end=None): @@ -223,9 +323,9 @@ def _detach_from(self): - 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.''' @@ -291,7 +391,7 @@ 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 = [[], []] # whether either end [A, B] of this object is bound to other object(s) # End A and B locations self.rA = [0,0] @@ -300,10 +400,13 @@ def __init__(self, id): # 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 + 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.sub_ind_A = [] # subcomponent index for end A (can be multiple) + self.sub_ind_B = [] # subcomponent index for end B (can be multiple) + + whole = True # false if there is a disconnect among the sub edges/nodes def isAttachedTo(self, query, end=None): @@ -357,8 +460,7 @@ def attachTo(self, object, r_rel=[0,0], end=None): 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. Parameters ---------- @@ -376,14 +478,18 @@ def _attach_to(self, object, end): self.attached_to[i_end] = object # Recursively attach any subcomponent at the end + >>> do we really want to do this still? <<< if len(self.subcomponents) > 0: # this edge has subcomponents - >> subcon = self.subcomponents[-i_end] # index 0 for A, -1 for B + 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) + 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): @@ -438,33 +544,51 @@ def _detach_from(self, end): # or go up a level... - def addSubcomponents(self, items): + 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 times is a dict: Adds them, assuming they are already assembled. + iA, iB : index of the end subcomponent(s) - provides as lists ''' # Attach the sequency of nodes and edges to each other assemble(items) # Store them as subcomponents of this edge, as a dict 0:N - self.subcomponents = dict(enumerate(items)) + self.subcomponents = items # dict(enumerate(items)) for item in items: item.part_of = self + # Assign ends (this feels pretty crude so far) + self.end_inds = [list(iA), list(iB)] + # 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]: - self._attach_to(self.attached_to[0], 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 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, + 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.''' @@ -472,7 +596,7 @@ def getTopLevelEdge(self, 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 + 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: @@ -544,6 +668,9 @@ def __init__(self, id): 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 @@ -692,13 +819,16 @@ def addSubcomponents(self, 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 @@ -798,16 +928,28 @@ 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): @@ -870,4 +1012,3 @@ def assemble(items): E.addSubcomponents([e0,n0,e1,n1,e2]) - From 023d627f468db5ebe513847e7c4e4e8ba0f81173 Mon Sep 17 00:00:00 2001 From: Matt Hall <5151457+mattEhall@users.noreply.github.com> Date: Wed, 18 Jun 2025 15:18:13 -0600 Subject: [PATCH 04/34] famodel_base updates for hierarchical connections finished (hopefully) --- famodel/famodel_base.py | 382 ++++++++++++++++++++++++++++++++-------- 1 file changed, 310 insertions(+), 72 deletions(-) diff --git a/famodel/famodel_base.py b/famodel/famodel_base.py index 9aeb30a4..10551473 100644 --- a/famodel/famodel_base.py +++ b/famodel/famodel_base.py @@ -42,6 +42,10 @@ 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... @@ -162,12 +166,22 @@ def join(self, object): 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, Node): - self.part_of.attach_to( object.part_of ) ...? - elif isinstance(self.part_of, Node) and isinstance(object.part_of, Edge): - object.part_of.attach_to( self.part_of ) ...? - + 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) + #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) + + 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 separate(self, object): '''Opposite of join''' @@ -185,10 +199,13 @@ def separate(self, object): del object.attachments[self.id] # Register the separation in higher level objects if applicable - if isinstance(self.part_of, Edge) and isinstance(object.part_of, Node): - self.part_of.detach...( object.part_of ) ...? - elif isinstance(self.part_of, Node) and isinstance(object.part_of, Edge): - object.part_of.detach...( self.part_of ) ...? + 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): @@ -206,11 +223,8 @@ def attach(self, object, r_rel=[0,0], end=None): If an Edge is being attached, which end of it, 'a' or 'b'. ''' - - object_parent - self_parent - - + #object_parent + #self_parent ''' # find top-level edge end if applicable @@ -226,13 +240,23 @@ def attach(self, object, r_rel=[0,0], end=None): if object.id in self.attachments: 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') object._attach_to(self) # tell it it's attached to this Node elif isinstance(object, Edge): # (object is an edge) + i_end = endToIndex(end, estr='when attaching an edge to a node.') + + 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.") + self.attachments[object.id] = dict(obj=object, id=object.id, r_rel=np.array(r_rel), type='edge', end=i_end) @@ -242,25 +266,61 @@ def attach(self, object, r_rel=[0,0], end=None): else: raise Exception('Unrecognized object type') - # See about attaching higher-level objects (new) + # 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 - if self.part_of == None: # this node isn't part of an edge - >>> figure out which edge of parent object <<<< + + # figure out which end of the edge object corresponds to + if 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, r_rel=r_rel, end=<<<) + 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=<<<) + 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) - else: # if self is part of a higher level edge, then what?? - raise Exception("Can't currently attach two edges - even if higher level...") + 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 ---------- @@ -275,23 +335,27 @@ 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) 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): '''Internal method to update the Node's attached_to registry when @@ -322,7 +386,7 @@ def _detach_from(self): self.attached_to = None - + """ 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, @@ -337,9 +401,9 @@ def getTopLevelObject(self): 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 @@ -348,24 +412,50 @@ 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.part_of and not force: + raise Exception("Can't setPosition of an object that's part of a higher object unless force=True.") + # Store updated position and orientation self.r = np.array(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(r) == 2: + self.R = np.array([[np.cos(theta), -np.sin(theta)],[np.sin(theta), np.cos(theta)]]) + elif len(r) == 3: + if np.isscalar(theta): + angles = np.array([theta, 0, 0]) + elif len(theta)==1: + angles = np.array([theta[0], 0, 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 or r must be 2 or 3.") # Update the position of any attach objects for att in self.attachments.values(): + if len(r) == len(att['r_rel']): + pass # all good + elif len(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']) if isinstance(att['obj'], Node): - att['obj'].setPosition(r_att) + att['obj'].setPosition(r_att, theta=theta, force=True) elif isinstance(att['obj'], Edge): att['obj'].setEndPosition(r_att, att['end']) @@ -391,7 +481,7 @@ def __init__(self, id): self.id = id # id number or string, used as the key when attached to things - self.attached_to = [[], []] # whether either end [A, B] of this object is bound to other object(s) + self.attached_to = [None, None] # object end [A, B] of this edge is attached to # End A and B locations self.rA = [0,0] @@ -403,8 +493,8 @@ def __init__(self, id): self.subcomponents = [] # chain of edges and nodes that make up this edge # (e.g. sections of a mooring line, and connetors between them) - >> self.sub_ind_A = [] # subcomponent index for end A (can be multiple) - self.sub_ind_B = [] # subcomponent index for end B (can be multiple) + 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 @@ -460,7 +550,7 @@ def attachTo(self, object, r_rel=[0,0], end=None): def _attach_to(self, object, end): '''Internal method to update the edge's attached_to registry when - requested by a node. + requested by a node. This doesn't do higher level attachments. Parameters ---------- @@ -474,9 +564,12 @@ 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 >>> do we really want to do this still? <<< if len(self.subcomponents) > 0: # this edge has subcomponents @@ -488,7 +581,7 @@ def _attach_to(self, object, end): 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 - + ''' @@ -510,8 +603,8 @@ 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 one first, then it will recursively call any lower ones. + requested by a node. In nested edges, it will recursively call any + lower objects. Parameters ---------- @@ -522,23 +615,23 @@ def _detach_from(self, end): # Delete the attachment(s) of the edge end (there should only be one) self.attached_to[i_end] = None - # Recursively detach the ends of any sub-edges - if len(self.subcomponents) > 0: - >> subcon = self.subcomponents[-i_end] # index 0 for A, -1 for B - + # 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): - subcon._detach_from() + if subcon.attached_to: + subcon.attached_to.detach(subcon) + # the above will then call subcon._detach_from() elif isinstance(subcon, Edge): - subcon._detach_from(i_end) - - ''' - if self.sub_edges: - if end: - self.sub_edges[-1]._detach_from(end) - else: - self.sub_edges[0]._detach_from(end) - ''' + 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... @@ -547,23 +640,24 @@ def _detach_from(self, end): 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 times is a dict: Adds them, assuming they are already assembled. - iA, iB : index of the end subcomponent(s) - provides as lists + 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, as a dict 0:N + # Store them as subcomponents of this edge self.subcomponents = items # dict(enumerate(items)) for item in items: item.part_of = self - # Assign ends (this feels pretty crude so far) - self.end_inds = [list(iA), list(iB)] + # Assign ends + self.subcons_A = list([items[0]]) # subcomponent for end A (can be multiple) + 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! <<< @@ -585,7 +679,7 @@ def addSubcomponents(self, items, iA=[0], iB=[-1]): 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, @@ -601,6 +695,22 @@ def getTopLevelObject(self, 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: + raise Exception("This object is not a subcomponent of this edge!") + + if object in self.subcons_A: + end = 0 + elif object 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 setEndPosition(self, r, end): @@ -709,7 +819,7 @@ def attachTo(self, object, r_rel=[0,0], end=None): # Determine which end to attach if end == None: - raise Exception("Poly end must be given...) + raise Exception("Poly end must be given...") else: i_end = int(end) # <<>> endToIndex(end, estr='when attaching an edge to something.') @@ -785,7 +895,7 @@ def _detach_from(self, end): # Recursively detach the ends of any sub-edges - >>> need to figure out which subcomponent would correspond to the requested end of the poly <<< + #>>> 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 @@ -824,12 +934,12 @@ def addSubcomponents(self, items): # to whatever this Edge might be attached to for i in range(len(end_inds)): if self.attached_to[end_inds[i]]: - if isinstance(self.attached_to[end_inds[i], Node): + 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, @@ -886,7 +996,7 @@ def delete(self): # next step would just be to separately remove any other references # to this object... - + """ # general functions @@ -936,7 +1046,7 @@ def endToIndex(end, estr='', n=2): 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.") + 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.') @@ -955,6 +1065,8 @@ def endToIndex(end, estr='', n=2): def assemble(items): '''Strings together a sequence of nodes and edges''' + # >>> TODO: adjust this so it can connect parallel elements .eg. for bridles <<< + n = len(items) for i in range(n-1): @@ -967,8 +1079,38 @@ def assemble(items): else: raise Exception('sequences is not alternating between nodes and edges') + +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__': @@ -993,7 +1135,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') @@ -1012,3 +1154,99 @@ def assemble(items): E.addSubcomponents([e0,n0,e1,n1,e2]) + # ----- 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) + + From e22521595e6c0897003691e4e668a1434403e358 Mon Sep 17 00:00:00 2001 From: lsirkis Date: Tue, 15 Jul 2025 13:01:04 -0600 Subject: [PATCH 05/34] Significant update -- fairlead objects now attach subordinately to platforms -- connector objects join to fairlead objects -- moorings automatically then connect to platform at the position of attached fairlead -- similar concept for Jtubes with cables - Jtubes attach subordinately to platforms - dynamic cables attach to Jtubes - overall cable object then automatically connects to platform at the position of the Jtubes -- Jtubes and fairleads are optional; if not provided in the platform definition it will not be used -- updated examples to work with this new methodology, added more examples showing this -- added more tests -- platform.setPosition now calls node.setPosition first, and updates the theta property - for platforms, theta = -phi (because phi follows +CW convention and theta follows +CCW convention - so if phi = pi/4, platform will rotate pi/4 rad in CW direction from the 0 degree orientation, and theta=-pi/4 - this is to maintain compass direction setup for users while mapping to cartesian coords/rotation matrices for nodes and in background processes -- updates to documentation, tests, and examples are a work in progress - will be provided in future pushes --- .../01_Visualization/02_visual_moorings.yaml | 8 +- examples/01_Visualization/03_visual_cables.py | 4 +- .../01_Visualization/03_visual_cables.yaml | 4 +- .../08_Design_Adjustment/01_Fairleads.yaml | 69 + examples/08_Design_Adjustment/02_Jtubes.py | 27 + examples/08_Design_Adjustment/02_Jtubes.yaml | 71 + examples/08_Design_Adjustment/02_fairleads.py | 31 + examples/08_Design_Adjustment/02_rotations.py | 39 + examples/Inputs/OntologySample200m.yaml | 53 +- .../Inputs/OntologySample600m_shared.yaml | 35 +- famodel/cables/cable.py | 42 +- famodel/cables/components.py | 5 + famodel/famodel_base.py | 43 +- famodel/mooring/mooring.py | 50 +- famodel/platform/fairlead.py | 12 + famodel/platform/platform.py | 32 +- famodel/project.py | 315 +++-- tests/mooring_ontology.yaml | 159 +++ tests/platform_ontology.yaml | 1247 +++++++++++++++++ tests/testOntology.yaml | 50 +- tests/test_anchors.py | 14 +- tests/test_integrations.py | 10 +- tests/test_moorings.py | 97 ++ tests/test_platform.py | 61 + tests/test_project.py | 10 +- 25 files changed, 2255 insertions(+), 233 deletions(-) create mode 100644 examples/08_Design_Adjustment/01_Fairleads.yaml create mode 100644 examples/08_Design_Adjustment/02_Jtubes.py create mode 100644 examples/08_Design_Adjustment/02_Jtubes.yaml create mode 100644 examples/08_Design_Adjustment/02_fairleads.py create mode 100644 examples/08_Design_Adjustment/02_rotations.py create mode 100644 famodel/platform/fairlead.py create mode 100644 tests/mooring_ontology.yaml create mode 100644 tests/platform_ontology.yaml create mode 100644 tests/test_moorings.py create mode 100644 tests/test_platform.py 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..733068e4 --- /dev/null +++ b/examples/08_Design_Adjustment/01_Fairleads.yaml @@ -0,0 +1,69 @@ + +# ----- 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 : 58 + z : -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/OntologySample200m.yaml b/examples/Inputs/OntologySample200m.yaml index a59e0a1c..fce81d8d 100644 --- a/examples/Inputs/OntologySample200m.yaml +++ b/examples/Inputs/OntologySample200m.yaml @@ -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] # fairlead listing is 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 ] + - [ semitaut-poly_1, 150 , drag-embedment1, 2 ] + - [ semitaut-poly_1, 270, drag-embedment1, 3 ] # Mooring line configurations @@ -349,7 +344,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 +358,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 +384,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 +1524,16 @@ 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: 58 + z: -14 + headings: [30, 150, 270] # 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: 5 + z: -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 +1631,16 @@ 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: 58 + z: -14 + headings: [30, 150, 270] # headings in degrees for the fairlead (if multiple headings, the fairlead will be repeated for each heading) + 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) type : Substation members: # list all members here diff --git a/examples/Inputs/OntologySample600m_shared.yaml b/examples/Inputs/OntologySample600m_shared.yaml index 675d2f94..f30a0bb7 100644 --- a/examples/Inputs/OntologySample600m_shared.yaml +++ b/examples/Inputs/OntologySample600m_shared.yaml @@ -125,11 +125,11 @@ 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, 3, 3, 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,15 @@ 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: 58 + z: -14 + headings: [30, 150, 270] # headings in degrees for the fairlead (if multiple headings, the fairlead will be repeated for each heading) + rFair : 40.5 # platform fairlead radius + zFair : -20 # platform fairlead z-location + type : FOWT members: # list all members here @@ -1302,10 +1311,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, fairleads] data: - - [ rope_1, 270 , suction_pile1, 0 ] - - [ rope_1, 135 , suction_pile1, 0 ] + - [ rope_1, 270 , suction_pile1, 3 ] + - [ rope_1, 135 , suction_pile1, 2 ] ms2: name: 2-line semitaut with a third shared line @@ -1318,18 +1327,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, fairleads] 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, fairleads] 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 diff --git a/famodel/cables/cable.py b/famodel/cables/cable.py index 21ca3ada..52ded174 100644 --- a/famodel/cables/cable.py +++ b/famodel/cables/cable.py @@ -8,7 +8,7 @@ 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 @@ -145,18 +145,34 @@ def reposition(self,headings=None,project=None,rad_fair=[]): headingA = headings[0] 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 (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 + # 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.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] + + 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.subcomponents[-1].rB = Bloc; self.rB = Bloc if project: # set end points of subcomponents @@ -236,7 +252,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): 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/famodel_base.py b/famodel/famodel_base.py index 0f983556..0b0945a3 100644 --- a/famodel/famodel_base.py +++ b/famodel/famodel_base.py @@ -99,7 +99,7 @@ def __init__(self, id): # 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] @@ -175,12 +175,14 @@ def join(self, object): 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) + 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) + 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.") @@ -275,7 +277,10 @@ def attach(self, object, r_rel=[0,0], end=None): 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: + 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 @@ -425,17 +430,21 @@ def setPosition(self, r, theta=0, force=False): raise Exception("Can't setPosition of an object that's part of a higher object unless force=True.") # Store updated position and orientation - self.r = np.array(r) + if len(r) > len(self.r): + self.r = np.array(r) + else: + self.r[:len(r)] = r + self.theta = theta # Get rotation matrix... - if len(r) == 2: + if len(self.r) == 2: self.R = np.array([[np.cos(theta), -np.sin(theta)],[np.sin(theta), np.cos(theta)]]) - elif len(r) == 3: + elif len(self.r) == 3: if np.isscalar(theta): - angles = np.array([theta, 0, 0]) + angles = np.array([ 0, 0, theta]) elif len(theta)==1: - angles = np.array([theta[0], 0, 0]) + angles = np.array([0, 0, theta[0]]) elif len(theta)==3: angles = np.array(theta) else: @@ -443,14 +452,14 @@ def setPosition(self, r, theta=0, force=False): self.R = rotationMatrix(*angles) else: - raise Exception("Length or r must be 2 or 3.") + 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(r) == len(att['r_rel']): + if len(self.r) == len(att['r_rel']): pass # all good - elif len(r) == 3 and len(att['r_rel']) == 2: # pad r_rel with z=0 if needed + 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.") @@ -458,11 +467,15 @@ def setPosition(self, r, theta=0, force=False): # 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, theta=theta, force=True) + 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']) + @@ -712,9 +725,9 @@ def findEnd(self, object): if not object in self.subcomponents: raise Exception("This object is not a subcomponent of this edge!") - if object in self.subcons_A: + if any([object is con for con in self.subcons_A]): end = 0 - elif object in self.subcons_B: + 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 diff --git a/famodel/mooring/mooring.py b/famodel/mooring/mooring.py index 6802d799..701706e3 100644 --- a/famodel/mooring/mooring.py +++ b/famodel/mooring/mooring.py @@ -239,18 +239,20 @@ 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)] + # 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 len(self.subcons_B[0].attachments) > 1: + # 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: #if i_line is not defined, assumed segment 0 will be adjusted @@ -264,27 +266,29 @@ 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 + 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([xy_loc, self.z_anch]), 'a', sink=True) # Update the mooring profile given the repositioned ends if self.ss: @@ -1032,14 +1036,14 @@ def connectSubcomponents(self): 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 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..16d9499d 100644 --- a/famodel/platform/platform.py +++ b/famodel/platform/platform.py @@ -74,31 +74,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,8 +106,9 @@ 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) diff --git a/famodel/project.py b/famodel/project.py index 31340c2d..6c1ad0b5 100644 --- a/famodel/project.py +++ b/famodel/project.py @@ -27,7 +27,8 @@ 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 @@ -105,8 +106,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') @@ -377,11 +378,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 +409,57 @@ 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' in fl: + for head in fl['headings']: + r_rel = [fl['r']*np.cos(np.radians(90-head)), + fl['r']*np.sin(np.radians(90-head)), + fl['z']] + pf_fairs.append(self.addFairlead(id=platform.id+'_F'+str(fct), + platform=platform, + r_rel=r_rel)) + fct += 1 + 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']: + r_rel = [jt['r']*np.cos(np.radians(90-head)), + jt['r']*np.sin(np.radians(90-head)), + jt['z']] + 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 +490,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']] @@ -483,16 +528,26 @@ def loadDesign(self, d, raft=True): # attach ends moor.attachTo(anch, end='A') - moor.attachTo(platform, end='B') + if 'fairlead' in mySys[j]: + flID = platform.id+'_F'+str(mySys[j]['fairlead']) + moor.subcomponents[-1].join(platform.attachments[flID]['obj']) + elif pf_fairs: + flID = pf_fairs[j].id + moor.subcomponents[-1].join(platform.attachments[flID]['obj']) + 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) + # # reposition mooring + # moor.reposition(r_center=platform.r, heading=headings[j]+platform.phi, project=self) - # update anchor depth and soils - self.updateAnchor(anch=anch) + # # update anchor depth and soils + # self.updateAnchor(anch=anch) # 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,7 +557,7 @@ 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): @@ -516,49 +571,54 @@ def loadDesign(self, d, raft=True): # 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) + m_config = 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=m_config, shared=1) # attach ends - moor.attachTo(self.platformList[PFNum[1]],end='A') - moor.attachTo(self.platformList[PFNum[0]],end='B') + flIDB = PF[0].id+'_F'+str(arrayMooring[j]['fairleadB']) + moor.subcomponents[-1].join(PF[0].attachments[flIDB]['obj']) + flIDA = PF[1].id+'_F'+str(arrayMooring[j]['fairleadA']) + moor.subcomponents[0].join(PF[1].attachments[flIDA]['obj']) + - # reposition - moor.reposition(r_center=[self.platformList[PFNum[1]].r, - self.platformList[PFNum[0]].r], + # determine heading + dists = PF[1].attachments[flIDA]['obj'].r[:2] - PF[0].attachments[flIDB]['obj'].r[:2] + headingB = np.pi/2 - np.arctan2(dists[1], dists[0]) + moor.reposition(r_center=[PF[1].r, + PF[0].r], heading=headingB, project=self) 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) + m_config = getMoorings(lineconfig, lineConfigs, connectorTypes, PF[0].id, self) # get letter number for mooring line - ind = len(self.platformList[PFNum[0]].getMoorings()) + ind = len(PF[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) + moor = self.addMooring(id=str(PF[0].id)+alph[ind], + dd=m_config) # 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 +642,46 @@ def loadDesign(self, d, raft=True): # attach anchor moor.attachTo(anchor,end='A') # attach platform - moor.attachTo(self.platformList[PFNum[0]],end='B') + flID = PF[0].id+'_F'+str(arrayMooring[j]['fairleadB']) + moor.subcomponents[-1].join(PF[0].attachments[flID]['obj']) + # determine heading + dists = anchor.r[:2] - PF[0].attachments[flID]['obj'].r[:2] + headingB = np.pi/2 - np.arctan2(dists[1],dists[0]) # 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] - # 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 +689,25 @@ 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 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(90-cab['headingA']) # heading only if not suspended else: # this is a suspended cable - add headingB Acondd['headingB'] = np.radians(90-cab['headingB']) - rJTubeA = dyn_cable_configs[dyn_cabA]['rJTube'] - Acondd['rJTube'] = rJTubeA + # 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'] @@ -654,8 +726,8 @@ def loadDesign(self, d, raft=True): self.depth, rho_water=self.rho_water, g=self.g) - rJTubeB = dyn_cable_configs[dyn_cabB]['rJTube'] - Bcondd['rJTube'] = rJTubeB + # 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']) dd['cables'].append(Bcondd) @@ -669,29 +741,38 @@ 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: raise Exception(f'AttachB {arrayCableInfo[i]["AttachB"]} for array cable {i} does not match any platforms or appendages.') # reposition the cable - self.cableList[cableID].reposition(project=self, rad_fair=[rJTubeA,rJTubeB]) + self.cableList[cableID].reposition(project=self) #, rad_fair=[rJTubeA,rJTubeB]) # create any cables from cables section (this is a more descriptive cable format that may have routing etc) if cableInfo: for cab in cableInfo: - rJTubeA = None; rJTubeB = None + # 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':[]} @@ -716,8 +797,8 @@ def loadDesign(self, d, raft=True): # add headingA Acondd['headingA'] = np.radians(90-cab['endA']['heading']) - rJTubeA = dyn_cable_configs[dyn_cabA]['rJTube'] - Acondd['rJTube'] = rJTubeA + # rJTubeA = dyn_cable_configs[dyn_cabA]['rJTube'] + # Acondd['rJTube'] = rJTubeA # append to cables list dd['cables'].append(Acondd) @@ -747,8 +828,8 @@ def loadDesign(self, d, raft=True): # add headingB Bcondd['headingB'] = np.radians(90-cab['endB']['heading']) - rJTubeB = dyn_cable_configs[dyn_cabB]['rJTube'] - Bcondd['rJTube'] = rJTubeB + # rJTubeB = dyn_cable_configs[dyn_cabB]['rJTube'] + # Bcondd['rJTube'] = rJTubeB # append to cables list dd['cables'].append(Bcondd) # append to joints list @@ -762,25 +843,32 @@ 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: raise Exception(f"AttachB {cab['endB']['attachID']} for cable {cab['name']} does not match any platforms or appendages.") # reposition the cable - self.cableList[cableID].reposition(project=self, rad_fair=[rJTubeA,rJTubeB]) - + 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: @@ -1493,6 +1581,35 @@ 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 + 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=[], @@ -1694,6 +1811,32 @@ def addSubstation(self, id=None, platform=None, dd={}): self.substationList[id] = Substation(dd, id) if platform != None: platform.attach(self.substationList[id]) + + def addJtube(self, id=None, platform=None, r_rel=[0,0,0], + cable=None, end='b'): + ''' + 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) + + # 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) def cableDesignInterpolation(self,depth,cables): @@ -2514,14 +2657,14 @@ def getMoorPyArray(self, plt=0, pristineLines=True, cables=True): 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 + # add fairlead point A 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 # add in cables if desired diff --git a/tests/mooring_ontology.yaml b/tests/mooring_ontology.yaml new file mode 100644 index 00000000..b75cba6f --- /dev/null +++ b/tests/mooring_ontology.yaml @@ -0,0 +1,159 @@ +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: 58 + z: -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/platform_ontology.yaml b/tests/platform_ontology.yaml new file mode 100644 index 00000000..980f9aaa --- /dev/null +++ b/tests/platform_ontology.yaml @@ -0,0 +1,1247 @@ +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: 40.5 + z: -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: 5 + z: -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..bdeaafab 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,16 @@ 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: 40.5 + z: -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: 5 + z: -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 +1312,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 +1534,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..a5453c5e 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,7 +32,7 @@ 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={}) diff --git a/tests/test_moorings.py b/tests/test_moorings.py new file mode 100644 index 00000000..a57748fe --- /dev/null +++ b/tests/test_moorings.py @@ -0,0 +1,97 @@ + +""" +Test mooring loading, configurations, methods +""" +import pytest + +import numpy as np + +from famodel.project import Project + +from famodel.platform.fairlead import Fairlead +from famodel.platform.platform import Platform + +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 == np.radians(45+180)) + assert(heading == pf.mooring_headings[0]+pf.phi) + assert(heading == np.radians(moor.heading)) + +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_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.dd['sections'])==len(moor.ss.lineList)) + assert(len(moor.dd['sections'])==2) + +def test_num_connectors(setup_project): + moor = setup_project.mooringList['FOWT1a'] + assert(len(moor.dd['connectors'])==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) + +''' +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__)) From 597a4b64e8850e2a269c887a07ad40a4d2ecb435 Mon Sep 17 00:00:00 2001 From: Matt Hall <5151457+mattEhall@users.noreply.github.com> Date: Fri, 18 Jul 2025 10:04:38 -0600 Subject: [PATCH 06/34] famodel_base updates to support parallel things in edges: Subcomponents can now include parallel strings of subcomponents among the subcomponents of an edge, including at the ends. This gives support for things like bridles or double chain sections. The assemble function supports this by accepting a nested list of items, where nesting indicates multiple parallel edges or sequences of edges and nodes. --- famodel/famodel_base.py | 63 +++++++++++++++++++++++++++++++++--- famodel/mooring/mooring.py | 6 ++-- famodel/platform/platform.py | 1 + 3 files changed, 61 insertions(+), 9 deletions(-) diff --git a/famodel/famodel_base.py b/famodel/famodel_base.py index 0b0945a3..5f80674b 100644 --- a/famodel/famodel_base.py +++ b/famodel/famodel_base.py @@ -673,7 +673,11 @@ def addSubcomponents(self, items, iA=[0], iB=[-1]): # Store them as subcomponents of this edge self.subcomponents = items # dict(enumerate(items)) for item in items: - item.part_of = self + if isinstance(item, list): + for subitem in item: + subitem.part_of = self + else: + item.part_of = self # Assign ends self.subcons_A = list([items[0]]) # subcomponent for end A (can be multiple) @@ -1089,10 +1093,42 @@ def assemble(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 + 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): @@ -1174,7 +1210,24 @@ def rotationMatrix(x3,x2,x1): 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') + e2a = Edge(id='e2a') + e2b = Edge(id='e2b') + + + thelist = [e0, n0, e1, n1, [e2a, e2b]] + + E = Edge(id='big edge') + + E.addSubcomponents(thelist) # ----- try joining two nodes ----- @@ -1184,7 +1237,7 @@ def rotationMatrix(x3,x2,x1): # ----- tests connecting multi-level node and edge objects ---- - ''' + # --- Test 1 --- # platform and fairlead n1 = Node(id='n1') @@ -1254,7 +1307,7 @@ def rotationMatrix(x3,x2,x1): 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') diff --git a/famodel/mooring/mooring.py b/famodel/mooring/mooring.py index 701706e3..566fc3f8 100644 --- a/famodel/mooring/mooring.py +++ b/famodel/mooring/mooring.py @@ -483,7 +483,8 @@ def createSubsystem(self, case=0,pristine=True,dd=None, mooringSys=None): # save to modified ss (may have marine growth, corrosion, etc) self.ss_mod = ss return(self.ss_mod) - + + 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 @@ -1057,6 +1058,3 @@ def connectSubcomponents(self): self.i_con = list(range(0, 2*self.n_sec+1, 2)) self.i_sec = list(range(1, 2*self.n_sec+1, 2)) - - - \ No newline at end of file diff --git a/famodel/platform/platform.py b/famodel/platform/platform.py index 16d9499d..d3d8fde4 100644 --- a/famodel/platform/platform.py +++ b/famodel/platform/platform.py @@ -61,6 +61,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 From ea00174ebcab7439eed832ad9d25e103fca07e29 Mon Sep 17 00:00:00 2001 From: lsirkis Date: Fri, 18 Jul 2025 16:23:56 -0600 Subject: [PATCH 07/34] In progress commit -- started changing loadDesign helper functiosn to work with parallel mooring structures within a mooring line i.e. bridles etc -- read in subsections poirtions of mooring line configs and separate out to proper connectors and sections list of dd -- upload example of use of bridles/double chain section in ontologySample200m.yaml -- some hair-brained ideas in here that probably should be removed such as: - passing in a subcons list to mooring object on initialization. Original idea was to create the connectors and sections list, and a list of the subcons in the proper order at the same time on load in, since that information is easily available, to help with building out the subcomponent list and avoid extra logic requried to re-form that ordre from the connectors and sections list. - the issue with this is now when we make the contents of connector and sections list of dd into actual Connector and Section objects, we have no way to map to the subcons list so purpose is likely defeated here.. -- fixed a few random bugs in other parts of the code unrelated --- examples/Inputs/OntologySample200m.yaml | 44 +++++++++++----- famodel/cables/static_cable.py | 3 +- famodel/helpers.py | 69 ++++++++++++++++++++++--- famodel/mooring/mooring.py | 51 ++++++++++++------ famodel/project.py | 18 ++++--- 5 files changed, 139 insertions(+), 46 deletions(-) diff --git a/examples/Inputs/OntologySample200m.yaml b/examples/Inputs/OntologySample200m.yaml index fce81d8d..c5cd8ba2 100644 --- a/examples/Inputs/OntologySample200m.yaml +++ b/examples/Inputs/OntologySample200m.yaml @@ -142,11 +142,11 @@ mooring_systems: ms3: name: 3-line semi-taut polyester mooring system - keys: [MooringConfigID, heading, anchorType, fairlead] # fairlead listing is optional; if not specified, fairlead list follows order of fairleads in the platform definition + 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, 30 , drag-embedment1, 1 ] - - [ semitaut-poly_1, 150 , drag-embedment1, 2 ] - - [ semitaut-poly_1, 270, drag-embedment1, 3 ] + - [ semitaut-poly_1, 30 , drag-embedment1, [1,4], 1 ] + - [ semitaut-poly_1, 150 , drag-embedment1, 2, 1 ] + - [ semitaut-poly_1, 270, drag-embedment1, 3, [2,1] ] # Mooring line configurations @@ -159,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 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/helpers.py b/famodel/helpers.py index bdc9a941..28714d0a 100644 --- a/famodel/helpers.py +++ b/famodel/helpers.py @@ -701,6 +701,7 @@ def getMoorings(lcID, lineConfigs, connectorTypes, pfID, proj): m_config = {'sections':[],'anchor':{},'span':{},'zAnchor':{}}#,'EndPositions':{}} # set up connector dictionary c_config = [] + config = [] 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 @@ -712,6 +713,7 @@ 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) @@ -723,6 +725,7 @@ def getMoorings(lcID, lineConfigs, connectorTypes, pfID, proj): m_config['sections'][ct]['type']['EA'] = float(lt['EA']) # set line length m_config['sections'][ct]['L'] = lc['length'] + config.append(m_config['sections'][ct]) # update counter for line types ct = ct + 1 # update line last boolean @@ -740,22 +743,59 @@ def getMoorings(lcID, lineConfigs, connectorTypes, pfID, proj): if cID in connectorTypes: c_config.append(connectorTypes[cID]) # add connector to list c_config[-1]['type'] = cID + config.append(c_config[-1]) else: # try pointProps try: props = loadPointProps(None) design = {f"num_c_{cID}":1} c_config.append(getPointProps(design, Props=props)) + config.append(c_config[-1]) 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!! + c_config.append([]) + m_config.append([]) + sublineLast = [lineLast]*len(lc['subsections']) # to check if there was a connector provided before this + for ii,sub in enumerate(lc['subsections']): + c_config[-1].append([]) + m_config[-1].append([]) + config.append([]) + for subsub in sub: + if 'connectorType' in subsub: + if cID in connectorTypes: + cID = subsub['connectorType'] + c_config[-1][-1].append(connectorTypes[cID]) + config[-1][-1].append(connectorTypes[cID]) + else: + # try pointProps + try: + props = loadPointProps(None) + design = {f"num_c_{cID}":1} + c_config[-1][-1].append(getPointProps(design, Props=props)) + config[-1][-1].append(c_config[-1][-1]) + 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 'type' or 'mooringFamily' in subsub: + lt = MooringProps(subsub) + m_config[-1][-1].append({'type':lt, + 'L': subsub['length']}) + config[-1][-1].append(m_config[-1][-1]) + sublineLast[ii] = 1 + else: + raise Exception(f"keys in subsection line definitions must either be 'type', 'mooringFamily', or 'connectorType'") + 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 + # TODO: LHS figure out subsections with symmetrical lines... or just ban symmetrical for situations with subsections... 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): @@ -787,7 +827,8 @@ def getMoorings(lcID, lineConfigs, connectorTypes, pfID, proj): m_config['connectors'] = c_config # add connectors section to the mooring dict - return(m_config) #, c_config) + return(m_config, config) #, c_config) + def getConnectors(c_config, mName, proj): @@ -871,15 +912,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]) @@ -891,8 +933,21 @@ 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 = [] + breakpoint() + 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) diff --git a/famodel/mooring/mooring.py b/famodel/mooring/mooring.py index 566fc3f8..58e25721 100644 --- a/famodel/mooring/mooring.py +++ b/famodel/mooring/mooring.py @@ -16,7 +16,7 @@ class Mooring(Edge): ''' def __init__(self, dd=None, subsystem=None, anchor=None, - rho=1025, g=9.81,id=None,shared=0): + rho=1025, g=9.81,id=None,shared=0,subcons=None): ''' Parameters ---------- @@ -91,18 +91,38 @@ def __init__(self, dd=None, subsystem=None, anchor=None, self.i_con = [] self.i_sec = [] # Turn what's in dd and turn it into Sections and Connectors + con_i = 0 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) - + 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']): - self.addSection(sec['L'],sec['type'],i, insert=False) + 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 + # connect subcomponents and update i_sec and i_conn lists - self.connectSubcomponents() + self.connectSubcomponents(subcons) @@ -1030,7 +1050,7 @@ def addConnector(self, conn_dd, index, id=None, insert=True): return(newconn) - def connectSubcomponents(self): + def connectSubcomponents(self, subcons=None): # first disconnect any current subcomponents for ii in self.i_sec: @@ -1047,11 +1067,12 @@ def connectSubcomponents(self): # 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]) + 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 # Indices of connectors and sections in self.subcomponents list diff --git a/famodel/project.py b/famodel/project.py index 6c1ad0b5..2fe5fb9c 100644 --- a/famodel/project.py +++ b/famodel/project.py @@ -517,12 +517,13 @@ 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) + m_config, config = 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=m_config, subcons=config, + reposition=False) anch = self.addAnchor(id=name, dd=ad, mass=mass) @@ -585,11 +586,12 @@ def loadDesign(self, d, raft=True): lineconfig = arrayMooring[j]['MooringConfigID'] # create mooring and connector dictionary for that line - m_config = getMoorings(lineconfig, lineConfigs, connectorTypes, PF[0].id, self) + m_config, config = getMoorings(lineconfig, lineConfigs, connectorTypes, PF[0].id, self) # create mooring class instance moor = self.addMooring(id=str(PF[1].id)+'-'+str(PF[0].id), - dd=m_config, shared=1) + dd=m_config, subcons=config, + shared=1) # attach ends flIDB = PF[0].id+'_F'+str(arrayMooring[j]['fairleadB']) @@ -612,13 +614,13 @@ def loadDesign(self, d, raft=True): # get configuration for that line lineconfig = arrayMooring[j]['MooringConfigID'] # create mooring and connector dictionary for that line - m_config = getMoorings(lineconfig, lineConfigs, connectorTypes, PF[0].id, self) + m_config, config = getMoorings(lineconfig, lineConfigs, connectorTypes, PF[0].id, self) # get letter number for mooring line ind = len(PF[0].getMoorings()) # create mooring class instance, attach to end A and end B objects, reposition moor = self.addMooring(id=str(PF[0].id)+alph[ind], - dd=m_config) + dd=m_config, subcons=config) # check if anchor instance already exists if any(tt == arrayMooring[j]['endA'] for tt in self.anchorList): # anchor name exists already in list @@ -1614,7 +1616,7 @@ def addFairlead(self, id=None, platform=None, r_rel=[0,0,0], 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, ''' @@ -1693,7 +1695,7 @@ def addMooring(self, id=None, endA=None, endB=None, heading=0, dd={}, 'rad_fair':self.platformList[id_part[0]].rFair if id_part else 0, 'z_fair':self.platformList[id_part[0]].zFair if id_part else 0} - mooring = Mooring(dd=dd, id=id, subsystem=subsystem) # create mooring object + mooring = Mooring(dd=dd, id=id, subsystem=subsystem, subcons=subcons) # create mooring object # update shared prop if needed if len(id_part)==2 and shared<1: From 8b86cc784ec35c9d9959eda55980d48e7fbf9da5 Mon Sep 17 00:00:00 2001 From: Matt Hall <5151457+mattEhall@users.noreply.github.com> Date: Mon, 21 Jul 2025 16:08:43 -0600 Subject: [PATCH 08/34] famodel_base subcomponent fixes and Edge.getSubcomponent method: - Several updates in famodel_base to now ensure parallel strings of subcomponents are always doubly nested [serial position along Edge, parallel strand number, then serial position along string] - Edge.getSubcomponent created to return the subcomponent based on this indexing. --- famodel/famodel_base.py | 72 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 65 insertions(+), 7 deletions(-) diff --git a/famodel/famodel_base.py b/famodel/famodel_base.py index 5f80674b..b1edf289 100644 --- a/famodel/famodel_base.py +++ b/famodel/famodel_base.py @@ -674,8 +674,9 @@ def addSubcomponents(self, items, iA=[0], iB=[-1]): self.subcomponents = items # dict(enumerate(items)) for item in items: if isinstance(item, list): - for subitem in item: - subitem.part_of = self + for branch in item: + for subitem in branch: + subitem.part_of = self else: item.part_of = self @@ -739,6 +740,53 @@ def findEnd(self, object): 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 @@ -1117,6 +1165,8 @@ def assemble(items): # 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 @@ -1219,18 +1269,22 @@ def rotationMatrix(x3,x2,x1): n0 = Node(id='n0') e1 = Edge(id='e1') n1 = Node(id='n1') - e2a = Edge(id='e2a') - e2b = Edge(id='e2b') + e2a1 = Edge(id='e2a') + e2a2 = Node(id='e2a') + e2a3 = Edge(id='e2a') + e2b = Edge(id='e2b') - thelist = [e0, n0, e1, n1, [e2a, e2b]] + thelist = [e0, n0, e1, n1, [[e2a1, e2a2, e2a3], [e2b]]] E = Edge(id='big edge') E.addSubcomponents(thelist) - # ----- try joining two nodes ----- + s = E.getSubcomponent([4,0,2]) + # ----- try joining two nodes ----- + """ A = Node(id='Node A') B = Node(id='Node B') A.join(B) @@ -1323,5 +1377,9 @@ def rotationMatrix(x3,x2,x1): n1.setPosition(r=[0,0,0], theta=0) #print(n1.attachments) #print(e1.attached_to) - + """ + + + + \ No newline at end of file From 2b193ab9c7c102275abe05097491b2d0a9c16144 Mon Sep 17 00:00:00 2001 From: lsirkis Date: Tue, 22 Jul 2025 17:52:54 -0600 Subject: [PATCH 09/34] In progress update -- updated helpers function for loading and project.loadDesign to load in subcomponents in series and parallel under the key subcomponents instead of connectors and sections lists -- updated mooring.py to collect subcomponents list from design dictionary and convert to sections and connectors objects -- updated mooring.py methods (mostly) to work with self.dd['subcomponents'] and use the edge.getSubcomponent method with i_sec and i_con to get lists of sections and connectors This is only loosely tested with basic test, likely not to work for more complex situations. --- famodel/helpers.py | 76 ++++----- famodel/mooring/mooring.py | 317 +++++++++++++++++++++++++------------ famodel/ontology/README.md | 27 ++-- famodel/project.py | 16 +- 4 files changed, 276 insertions(+), 160 deletions(-) diff --git a/famodel/helpers.py b/famodel/helpers.py index 28714d0a..b56936a6 100644 --- a/famodel/helpers.py +++ b/famodel/helpers.py @@ -691,17 +691,15 @@ 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 = {'sections':[],'span':{},'zAnchor':{}}#,'EndPositions':{}} # set up connector dictionary c_config = [] - 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 @@ -719,15 +717,13 @@ def getMoorings(lcID, lineConfigs, connectorTypes, pfID, proj): 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']) # set line length - m_config['sections'][ct]['L'] = lc['length'] - config.append(m_config['sections'][ct]) - # update counter for line types - ct = ct + 1 + config[-1]['L'] = lc['length'] + # update line last boolean lineLast = 1 @@ -741,16 +737,15 @@ 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(c_config[-1]) + 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(c_config[-1]) + 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) @@ -758,34 +753,28 @@ def getMoorings(lcID, lineConfigs, connectorTypes, pfID, proj): lineLast = 0 elif 'subsections' in lc: # TODO: LHS: ERROR CHECKING FOR ORDER OF COMPONENTS PROVIDED WITHIN SUBSECTIONS, ADD IN NEEDED CONNECTORS!! - c_config.append([]) - m_config.append([]) + config.append([]) sublineLast = [lineLast]*len(lc['subsections']) # to check if there was a connector provided before this for ii,sub in enumerate(lc['subsections']): - c_config[-1].append([]) - m_config[-1].append([]) - config.append([]) + config[-1].append([]) for subsub in sub: if 'connectorType' in subsub: if cID in connectorTypes: cID = subsub['connectorType'] - c_config[-1][-1].append(connectorTypes[cID]) config[-1][-1].append(connectorTypes[cID]) else: # try pointProps try: props = loadPointProps(None) design = {f"num_c_{cID}":1} - c_config[-1][-1].append(getPointProps(design, Props=props)) - config[-1][-1].append(c_config[-1][-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 'type' or 'mooringFamily' in subsub: lt = MooringProps(subsub) - m_config[-1][-1].append({'type':lt, + config[-1][-1].append({'type':lt, 'L': subsub['length']}) - config[-1][-1].append(m_config[-1][-1]) sublineLast[ii] = 1 else: raise Exception(f"keys in subsection line definitions must either be 'type', 'mooringFamily', or 'connectorType'") @@ -795,39 +784,38 @@ def getMoorings(lcID, lineConfigs, connectorTypes, pfID, proj): 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 - # TODO: LHS figure out subsections with symmetrical lines... or just ban symmetrical for situations with subsections... 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, config) #, c_config) + return(dd) #, c_config) diff --git a/famodel/mooring/mooring.py b/famodel/mooring/mooring.py index 58e25721..56b72f3a 100644 --- a/famodel/mooring/mooring.py +++ b/famodel/mooring/mooring.py @@ -16,7 +16,7 @@ class Mooring(Edge): ''' def __init__(self, dd=None, subsystem=None, anchor=None, - rho=1025, g=9.81,id=None,shared=0,subcons=None): + rho=1025, g=9.81,id=None,shared=0): ''' Parameters ---------- @@ -74,55 +74,74 @@ 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'] = [] + 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 + self.dd['subcomponents'].append({'CdA':lp.CdA, 'm':lp.m, 'v':lp.v}) + break + for s in range(len(self.ss.lineList)): + endA = [lp.attachedEndB[i] for i in lp.attachedEndB if i==0][0] + line_num = lp.attached[endA] + ls = self.ss.lineList[line_num-1] + self.dd['subcomponents'].append({'type':ls.type, 'L':ls.L}) + for lb in self.ss.pointList: + if line_num in lb.attached and lb != lp: + lp = lb + self.dd['subcomponents'].append({'CdA':lp.CdA, 'm':lp.m, 'v':lp.v}) + + + # 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 - 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 + + # # 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']) + + # connect subcomponents + self.addSubcomponents(self.dd['subcomponents']) - # connect subcomponents and update i_sec and i_conn lists - self.connectSubcomponents(subcons) + # point dd['subcomponents'] list to self.subcomponents + self.dd['subcomponents'] = self.subcomponents @@ -168,9 +187,12 @@ 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'] @@ -181,8 +203,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.getSubcomponents(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) @@ -199,9 +221,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.getSubcomponents(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 @@ -446,6 +468,7 @@ def createSubsystem(self, case=0,pristine=True,dd=None, mooringSys=None): mooringSys : MoorPy System, optional MoorPy system this subsystem is a part of ''' + # TODO: Figure out how to handle subsystems for lines with subsections (esp when double chain in middle...) # set design dictionary as self.dd if none given, same with connectorList if not dd: dd = self.dd @@ -462,17 +485,22 @@ def createSubsystem(self, case=0,pristine=True,dd=None, mooringSys=None): lengths = [] types = [] # run through each line section and collect the length and type - for i, sec in enumerate(dd['sections']): + for i in self.i_sec: + sec = self.getSubcomponent(i) 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'] + conns = [] + for i in self.i_con: + conns.append(self.getSubcomponent(i)) + # make the lines and set the points ss.makeGeneric(lengths, types, - connectors=[dd['connectors'][ic+1] for ic in range(len(dd['connectors'])-2)], + 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) @@ -485,10 +513,11 @@ def createSubsystem(self, case=0,pristine=True,dd=None, mooringSys=None): else: # no anchor - need to include all connections startNum = 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 in range(startNum,len(ss.pointList)): + conn = self.getSubcomponent(self.i_con[i]) + conn.mpConn = ss.pointList[i] + conn.mpConn.CdA = conns[i]['CdA'] + conn.getProps() # solve the system ss.initialize() @@ -516,9 +545,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 @@ -919,7 +950,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['material']=='chain': MBL_cor = sec['MBL']*( (sec['d_nom']-(corrosion_mm/1000))/sec['d_nom'] )**2 # corroded MBL else: @@ -992,27 +1024,47 @@ 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 connectors after this in series + if len(i)==len(index) and i[-1]>index[-1]: + 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]]) + id='_'.join(['S',*[str(j) for j in index]]) newsection_dd = {'type':section_type,'L':section_length} - newsection = Section(id,**newsection_dd) + 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): ''' @@ -1023,7 +1075,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 @@ -1037,45 +1089,116 @@ 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 connectors after this in series + if len(i)==len(index) and i[-1]>index[-1]: + if np.all([i[j]>=index[j] for j in range(len(i))]) and i[-1]>index[-1]: + conn = self.getSubcomponent(i) + conn.id = '_'.join(['C',*[str(j) for j in i]]) + id = '_'.join(['C',*[str(j) for j in index]]) newconn = Connector(id, **conn_dd) 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]].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, subcons=None): + # 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! - 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 + # # 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 + - # 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)) + def convertSubcomponents(self, subs_list): + for i,sub in enumerate(subs_list): + if isinstance(sub,list): + for j,subsub in enumerate(sub): + if isinstance(subsub, list): + for k, subsubsub in enumerate(subsub): + + if 'L' in sub: + 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: + 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: + id = 'S'+str(i) + subs_list[i] = self.addSection(sub['L'], + sub['type'], + [i], + id=id, + insert=False) + self.i_sec.append([i]) + else: + id = 'C'+str(i) + subs_list[i] = self.addConnector(sub, [i], id=id, insert=False) + self.i_con.append([i]) + + # 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 diff --git a/famodel/ontology/README.md b/famodel/ontology/README.md index 33de9515..6a3e1d1e 100644 --- a/famodel/ontology/README.md +++ b/famodel/ontology/README.md @@ -307,7 +307,8 @@ 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 fairlead connection point for end A and end B is represented with an integer value in fairleadA and fairlead respectively. This integer value maps to an index 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. 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. ```yaml array_mooring: @@ -318,11 +319,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_2 , fowt1, f2, 2, 3] ``` ### Array Cables @@ -330,7 +331,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 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 the center 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 +340,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 +390,12 @@ 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 relative positions can be listed for each fairlead, or the fairlead radius, depth, 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. +rFair is the fairlead radius, zFair is the fairlead depth with respect to the platform depth. 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. 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 diff --git a/famodel/project.py b/famodel/project.py index 2fe5fb9c..f73d1361 100644 --- a/famodel/project.py +++ b/famodel/project.py @@ -517,12 +517,12 @@ def loadDesign(self, d, raft=True): lineconfig = mySys[j]['MooringConfigID'] # create mooring and connector dictionary - m_config, 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, subcons=config, + dd=mdd, reposition=False) anch = self.addAnchor(id=name, dd=ad, mass=mass) @@ -586,11 +586,11 @@ def loadDesign(self, d, raft=True): lineconfig = arrayMooring[j]['MooringConfigID'] # create mooring and connector dictionary for that line - m_config, config = getMoorings(lineconfig, lineConfigs, connectorTypes, PF[0].id, self) + mdd = getMoorings(lineconfig, lineConfigs, connectorTypes, PF[0].id, self) # create mooring class instance moor = self.addMooring(id=str(PF[1].id)+'-'+str(PF[0].id), - dd=m_config, subcons=config, + dd=mdd, shared=1) # attach ends @@ -614,13 +614,13 @@ def loadDesign(self, d, raft=True): # get configuration for that line lineconfig = arrayMooring[j]['MooringConfigID'] # create mooring and connector dictionary for that line - m_config, config = getMoorings(lineconfig, lineConfigs, connectorTypes, PF[0].id, self) + mdd = getMoorings(lineconfig, lineConfigs, connectorTypes, PF[0].id, self) # get letter number for mooring line ind = len(PF[0].getMoorings()) - # create mooring class instance, attach to end A and end B objects, reposition + # create mooring class instance moor = self.addMooring(id=str(PF[0].id)+alph[ind], - dd=m_config, subcons=config) + 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 @@ -1695,7 +1695,7 @@ def addMooring(self, id=None, endA=None, endB=None, heading=0, dd={}, 'rad_fair':self.platformList[id_part[0]].rFair if id_part else 0, 'z_fair':self.platformList[id_part[0]].zFair if id_part else 0} - mooring = Mooring(dd=dd, id=id, subsystem=subsystem, subcons=subcons) # create mooring object + mooring = Mooring(dd=dd, id=id, subsystem=subsystem) # create mooring object # update shared prop if needed if len(id_part)==2 and shared<1: From 94d2fe3c4975ae7fffc9f1d25a844b0ca5be9ec4 Mon Sep 17 00:00:00 2001 From: lsirkis Date: Wed, 23 Jul 2025 13:45:15 -0600 Subject: [PATCH 10/34] WIP update -- added some more needed things for loading -- still a few errors to work out, but getting closer --- famodel/helpers.py | 79 ++++++++++++++++++++++++++++++++++++++++++++++ famodel/project.py | 54 +++++++++++++++++++------------ 2 files changed, 113 insertions(+), 20 deletions(-) diff --git a/famodel/helpers.py b/famodel/helpers.py index b56936a6..e916c8a7 100644 --- a/famodel/helpers.py +++ b/famodel/helpers.py @@ -877,6 +877,85 @@ def getAnchors(lineAnch, arrayAnchor, proj): return(ad, mass) +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 heading from points, if pointA or pointB is a list of points, + the average of those points will be used for that end''' + if isinstance(pointA[0],list) or isinstance(pointA[0],np.ndarray): + pointAx = sum([x[0] for x in pointA])/len(pointA) + pointAy = sum([x[1] for x in pointA])/len(pointA) + else: + pointAx = pointA[0] + pointAy = pointA[1] + if isinstance(pointB[0],list) or isinstance(pointB[0],np.ndarray): + pointBx = sum([x[0] for x in pointB])/len(pointB) + pointBy = sum([x[1] for x in pointB])/len(pointB) + else: + pointBx = pointB[0] + pointBy = pointB[1] + + dists = np.array([pointAx,pointAy]) - np.array([pointBx,pointBy]) + headingB = np.pi/2 - np.arctan2(dists[1], dists[0]) + + return(headingB) + + def route_around_anchors(proj, anchor=True, cable=True, padding=50): # make anchor buffers with 50m radius diff --git a/famodel/project.py b/famodel/project.py index f73d1361..df8cc92c 100644 --- a/famodel/project.py +++ b/famodel/project.py @@ -36,7 +36,8 @@ 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) class Project(): @@ -530,19 +531,21 @@ def loadDesign(self, d, raft=True): # attach ends moor.attachTo(anch, end='A') if 'fairlead' in mySys[j]: - flID = platform.id+'_F'+str(mySys[j]['fairlead']) - moor.subcomponents[-1].join(platform.attachments[flID]['obj']) + attachFairleads(moor, + 1, + platform, + fair_ID_start=platform.id+'_F', + fair_inds=mySys[j]['fairlead']) + elif pf_fairs: - flID = pf_fairs[j].id - moor.subcomponents[-1].join(platform.attachments[flID]['obj']) + 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) # update counter mct += 1 @@ -594,15 +597,22 @@ def loadDesign(self, d, raft=True): shared=1) # attach ends - flIDB = PF[0].id+'_F'+str(arrayMooring[j]['fairleadB']) - moor.subcomponents[-1].join(PF[0].attachments[flIDB]['obj']) - flIDA = PF[1].id+'_F'+str(arrayMooring[j]['fairleadA']) - moor.subcomponents[0].join(PF[1].attachments[flIDA]['obj']) + 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']) # determine heading - dists = PF[1].attachments[flIDA]['obj'].r[:2] - PF[0].attachments[flIDB]['obj'].r[:2] - headingB = np.pi/2 - np.arctan2(dists[1], dists[0]) + 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) @@ -644,11 +654,15 @@ def loadDesign(self, d, raft=True): # attach anchor moor.attachTo(anchor,end='A') # attach platform - flID = PF[0].id+'_F'+str(arrayMooring[j]['fairleadB']) - moor.subcomponents[-1].join(PF[0].attachments[flID]['obj']) + fairsB = attachFairleads(moor, + 1, + PF[0], + fair_ID_start=PF[0].id+'_F', + fair_inds=arrayMooring[j]['fairleadB']) + # determine heading - dists = anchor.r[:2] - PF[0].attachments[flID]['obj'].r[:2] - headingB = np.pi/2 - np.arctan2(dists[1],dists[0]) + headingB = calc_heading(anchor.r[:2],[f.r[:2] for f in fairsB]) + # reposition mooring moor.reposition(r_center=PF[0].r, heading=headingB, project=self) # update depths From 1b93d0d4d6296f87db6f95d6d540c5a96b2bbaaf Mon Sep 17 00:00:00 2001 From: lsirkis Date: Thu, 24 Jul 2025 11:05:06 -0600 Subject: [PATCH 11/34] WIP update calc_midpoint, bug fixes, test with shared example -- added new helper method for calculating midpoint of 2 points - calc_midpoint calculates midpoint of 2 points, useful for calculating where span starts from bridles -- fixed some bugs - updated to determine anchor end point from midpoint of end point(s) - if not a bridle, midpoint is just the actual point - fixed small bugs in famodel_base - fixed small bugs in helpers.getMoorings to add connectors around subsections as needed -- added bridle to shared example and tested - plotting does not currently work, need to fix base class or somewhere to update rA and rB of each section - this does not happen with node.setPosition because that only sets node position and one end of the attached section --- .../Inputs/OntologySample600m_shared.yaml | 33 ++++++++++++-- examples/example_sharedmoorings.py | 4 +- famodel/famodel_base.py | 26 ++++++++--- famodel/helpers.py | 45 ++++++++++++------- famodel/mooring/mooring.py | 11 +++-- famodel/project.py | 19 +++++--- 6 files changed, 101 insertions(+), 37 deletions(-) diff --git a/examples/Inputs/OntologySample600m_shared.yaml b/examples/Inputs/OntologySample600m_shared.yaml index f30a0bb7..8809fff6 100644 --- a/examples/Inputs/OntologySample600m_shared.yaml +++ b/examples/Inputs/OntologySample600m_shared.yaml @@ -1209,6 +1209,10 @@ platforms: r: 58 z: -14 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 @@ -1311,9 +1315,9 @@ mooring_systems: ms1: name: 3-line semi-taut polyester mooring system with one line shared anchor - keys: [MooringConfigID, heading, anchorType, fairleads] + keys: [MooringConfigID, heading, anchorType, fairlead] data: - - [ rope_1, 270 , suction_pile1, 3 ] + - [ rope_1_bridle, 270 , suction_pile1, [4,5] ] - [ rope_1, 135 , suction_pile1, 2 ] ms2: @@ -1327,14 +1331,14 @@ mooring_systems: ms3: name: 3-line semi-taut polyester mooring system with one line shared anchor and one shared line - keys: [MooringConfigID, heading, anchorType, fairleads] + keys: [MooringConfigID, heading, anchorType, fairlead] data: - [ rope_1, 45 , suction_pile1, 1 ] ms4: name: 3 line taut poly mooring system - keys: [MooringConfigID, heading, anchorType, fairleads] + keys: [MooringConfigID, heading, anchorType, fairlead] data: - [ rope_1, 45 , suction_pile1, 1 ] - [ rope_1, 135 , suction_pile1, 2 ] @@ -1358,6 +1362,27 @@ 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: 1120 # [m] usntretched length of line section + adjustable: True # flags that this section could be adjusted to accommodate different spacings... + - subsections: # bridle sections for end B + - - type: rope + length: 50 + - connectorType: shackle + - - type: rope + length: 50 + - connectorType: shackle + rope_shared: name: shared rope diff --git a/examples/example_sharedmoorings.py b/examples/example_sharedmoorings.py index 3e4d156b..1acf0c13 100644 --- a/examples/example_sharedmoorings.py +++ b/examples/example_sharedmoorings.py @@ -18,10 +18,10 @@ os.chdir(input_directory) # load in yaml -project = Project(file=filename,raft=True) +project = Project(file=filename,raft=False) # plot in 2d and 3d project.plot2d() -project.plot3d(fowt=True) +#project.plot3d(fowt=True) plt.show() \ No newline at end of file diff --git a/famodel/famodel_base.py b/famodel/famodel_base.py index b1edf289..b5f279c6 100644 --- a/famodel/famodel_base.py +++ b/famodel/famodel_base.py @@ -244,7 +244,9 @@ def attach(self, object, r_rel=[0,0], end=None): ''' # Make sure it's not already attached (note this doesn't distinguish end A/B) if object.id in self.attachments: - raise Exception(f"Object {object.id} is already attached to {self.id}") + return # for bridles, the mooring will already be attached to platform + # for second bridle section + # raise Exception(f"Object {object.id} is already attached to {self.id}") # Attach the object @@ -679,10 +681,16 @@ def addSubcomponents(self, items, iA=[0], iB=[-1]): subitem.part_of = self else: item.part_of = self - # Assign ends - self.subcons_A = list([items[0]]) # subcomponent for end A (can be multiple) - self.subcons_B = list([items[-1]]) # subcomponent for end B (can be multiple) + 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 @@ -728,7 +736,15 @@ def findEnd(self, object): '''Checks if object is a subcomponent of self and which end it's at.''' if not object in self.subcomponents: - raise Exception("This object is not a subcomponent of this edge!") + 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 diff --git a/famodel/helpers.py b/famodel/helpers.py index e916c8a7..87e5f01e 100644 --- a/famodel/helpers.py +++ b/famodel/helpers.py @@ -753,12 +753,18 @@ def getMoorings(lcID, lineConfigs, connectorTypes, pfID, proj): 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 subsub in sub: - if 'connectorType' in subsub: + if 'connectorType' in subsub and sublineLast[ii]: + cID = subsub['connectorType'] if cID in connectorTypes: cID = subsub['connectorType'] config[-1][-1].append(connectorTypes[cID]) @@ -771,8 +777,13 @@ def getMoorings(lcID, lineConfigs, connectorTypes, pfID, proj): 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: - lt = MooringProps(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']}) sublineLast[ii] = 1 @@ -937,23 +948,24 @@ def attachFairleads(moor, end, platform, fair_ID_start=None, fair_ID=None, fair_ def calc_heading(pointA, pointB): '''calculate a heading from points, if pointA or pointB is a list of points, the average of those points will be used for that end''' - if isinstance(pointA[0],list) or isinstance(pointA[0],np.ndarray): - pointAx = sum([x[0] for x in pointA])/len(pointA) - pointAy = sum([x[1] for x in pointA])/len(pointA) - else: - pointAx = pointA[0] - pointAy = pointA[1] - if isinstance(pointB[0],list) or isinstance(pointB[0],np.ndarray): - pointBx = sum([x[0] for x in pointB])/len(pointB) - pointBy = sum([x[1] for x in pointB])/len(pointB) - else: - pointBx = pointB[0] - pointBy = pointB[1] - - dists = np.array([pointAx,pointAy]) - np.array([pointBx,pointBy]) + # 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) + else: + pointx = point[0] + pointy = point[1] + + return([pointx,pointy]) def route_around_anchors(proj, anchor=True, cable=True, padding=50): @@ -1003,7 +1015,6 @@ def angle(pt): # determine relative positions of new routing points among other routing points rel_dist = [] orig_coords = [] - breakpoint() 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]))) diff --git a/famodel/mooring/mooring.py b/famodel/mooring/mooring.py index 56b72f3a..5e42b432 100644 --- a/famodel/mooring/mooring.py +++ b/famodel/mooring/mooring.py @@ -6,6 +6,7 @@ from moorpy import helpers from famodel.mooring.connector import Connector, Section from famodel.famodel_base import Edge +from famodel.helpers import calc_midpoint class Mooring(Edge): ''' @@ -281,8 +282,9 @@ def reposition(self, r_center=None, heading=None, project=None, r_centerA = self.attached_to[0].r r_centerB = self.attached_to[1].r + 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 len(self.subcons_B[0].attachments) > 1: + 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)] @@ -324,7 +326,10 @@ def reposition(self, r_center=None, heading=None, project=None, 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 = self.rB[:2] + self.span*u #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'] @@ -1154,7 +1159,7 @@ def convertSubcomponents(self, subs_list): if isinstance(subsub, list): for k, subsubsub in enumerate(subsub): - if 'L' in sub: + 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'], diff --git a/famodel/project.py b/famodel/project.py index df8cc92c..4e9ae455 100644 --- a/famodel/project.py +++ b/famodel/project.py @@ -655,10 +655,10 @@ def loadDesign(self, d, raft=True): moor.attachTo(anchor,end='A') # attach platform fairsB = attachFairleads(moor, - 1, - PF[0], - fair_ID_start=PF[0].id+'_F', - fair_inds=arrayMooring[j]['fairleadB']) + 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]) @@ -2187,8 +2187,15 @@ def plot2d(self, ax=None, plot_seabed=True,plot_bathymetry=True, plot_boundary=T mooring.ss.drawLine2d(0, ax, color="k", endpoints=False, Xuvec=[1,0,0], Yuvec=[0,1,0],label='Mooring Line') 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') + if len(mooring.subcons_B[0].attachments)>1: + # there are fairleads, plot from their locations + for i in mooring.i_sec: + sec = mooring.getSubcomponent(i) + ax.plot([sec.rA[0],sec.rB[0]], + [sec.rA[1],sec.rB[1]]) + else: + ax.plot([mooring.rA[0], mooring.rB[0]], + [mooring.rA[1], mooring.rB[1]], 'k', lw=0.5, label='Mooring Line') if plot_anchors: for anchor in self.anchorList.values(): From 3b0e1f05ec8a0eacd984264245b36ea736326d54 Mon Sep 17 00:00:00 2001 From: lsirkis Date: Tue, 29 Jul 2025 14:38:20 -0600 Subject: [PATCH 12/34] Bug fix famodel_base, fairlead ontology adjusted, add comments -- fixed bug in famodel_base to ensure bridles at end B are connected in assemble function -- add Node.calculate_r_rel method to determine r_rel of node to edge or other node that may be connected via joined subcomponent/subordinate node --> takes into account r_rel of the subs - helpful for determining end point of bridles - attach method now calls this to determine r_rel of each point on an end when there's multiple points, then calls helpers.calculate_midpoint function to determine the r_rel by averaging the r_rels. In moorings, this updates the rB/rA to be the midpoint of the fairlead locations. -- changed fairlead ontology definition to always have a r_rel, and then optionally have headings apply to that r_rel -- small bug fix helpers function adjustMooring --- .../Inputs/OntologySample600m_shared.yaml | 58 ++++++++-- famodel/cables/dynamic_cable.py | 3 +- famodel/famodel_base.py | 107 ++++++++++++++++-- famodel/helpers.py | 5 +- famodel/mooring/mooring.py | 41 +++++-- famodel/project.py | 34 +++--- tests/test_moorings.py | 35 ++++++ 7 files changed, 242 insertions(+), 41 deletions(-) diff --git a/examples/Inputs/OntologySample600m_shared.yaml b/examples/Inputs/OntologySample600m_shared.yaml index 8809fff6..8e59f43a 100644 --- a/examples/Inputs/OntologySample600m_shared.yaml +++ b/examples/Inputs/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,7 +118,7 @@ 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 ] @@ -127,7 +127,7 @@ array_mooring: line_keys : [MooringConfigID , endA, endB, fairleadA, fairleadB, lengthAdjust] line_data : - - [ rope_shared , FOWT1, FOWT2, 3, 3, 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] @@ -1206,8 +1206,7 @@ platforms: fairleads : # list of fairlead coordinates for the platform relative to platform coordinate and 0-degree heading - name: fairlead1 - r: 58 - z: -14 + 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] @@ -1373,8 +1372,46 @@ mooring_line_configs: - type: chain_155mm length: 20 - type: rope # ID of a mooring line section type - length: 1120 # [m] usntretched length of line section - adjustable: True # flags that this section could be adjusted to accommodate different spacings... + length: 500 # [m] usntretched length of line section + - connectorType: triplate + - subsections: # double chain section + - - type: chain_155mm + length: 120 + - - type: chain_155mm + 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 @@ -1514,6 +1551,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/famodel/cables/dynamic_cable.py b/famodel/cables/dynamic_cable.py index 022a836f..c649094b 100644 --- a/famodel/cables/dynamic_cable.py +++ b/famodel/cables/dynamic_cable.py @@ -79,7 +79,8 @@ 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'] # <<< ?? - self.headingA = 0 + # if there's no headingA, likely a suspended cable - headingA = headingB+180 degrees + self.headingA = self.headingB + np.pi else: self.headingA = 0 diff --git a/famodel/famodel_base.py b/famodel/famodel_base.py index b5f279c6..75d47e8f 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 @@ -244,9 +244,19 @@ def attach(self, object, r_rel=[0,0], end=None): ''' # Make sure it's not already attached (note this doesn't distinguish end A/B) if object.id in self.attachments: - return # for bridles, the mooring will already be attached to platform + # for bridles, the mooring will already be attached to platform # for second bridle section - # raise Exception(f"Object {object.id} is already attached to {self.id}") + # 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 @@ -432,9 +442,9 @@ def setPosition(self, r, theta=0, force=False): raise Exception("Can't setPosition of an object that's part of a higher object unless force=True.") # Store updated position and orientation - if len(r) > len(self.r): + if len(r) > len(self.r): # default r is 2D, but can be adjusted to 3D self.r = np.array(r) - else: + 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 @@ -478,6 +488,63 @@ def setPosition(self, r, theta=0, force=False): 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') + + + @@ -1164,13 +1231,11 @@ def assemble(items): return ''' n = len(items) - for i in range(n-1): - 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 @@ -1202,7 +1267,31 @@ 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 diff --git a/famodel/helpers.py b/famodel/helpers.py index 87e5f01e..ea277bee 100644 --- a/famodel/helpers.py +++ b/famodel/helpers.py @@ -696,7 +696,7 @@ def getMoorings(lcID, lineConfigs, connectorTypes, pfID, proj): ''' # set up dictionary of information on the mooring configurations - dd = {'sections':[],'span':{},'zAnchor':{}}#,'EndPositions':{}} + dd = {'span':{},'zAnchor':{}}#,'EndPositions':{}} # set up connector dictionary c_config = [] config = [] # mooring and connector combined configuation list @@ -1171,7 +1171,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 diff --git a/famodel/mooring/mooring.py b/famodel/mooring/mooring.py index 5e42b432..6db660e6 100644 --- a/famodel/mooring/mooring.py +++ b/famodel/mooring/mooring.py @@ -76,20 +76,26 @@ def __init__(self, dd=None, subsystem=None, anchor=None, self.dd['z_fair'] = self.ss.z_fair self.dd['subcomponents'] = [] - + # find the starting point index for lp in self.ss.pointList: if not any(lp.attachedEndB): - # this is the starting point at end A + # 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}) @@ -282,6 +288,7 @@ def reposition(self, r_center=None, heading=None, project=None, r_centerA = self.attached_to[0].r r_centerB = self.attached_to[1].r + # 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: @@ -1038,13 +1045,19 @@ def addSection(self, section_length, section_type, index, id=None, insert=True): if not id: if insert: for i in self.i_sec: - # update ids of connectors after this in series + # 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} + # create section object newsec = Section(id,**newsection_dd) if insert: if len(index)==1: @@ -1095,20 +1108,27 @@ def addConnector(self, conn_dd, index, id=None, insert=True): if not id: if insert: for i in self.i_con: - # update ids of connectors after this in series + # 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]: - if np.all([i[j]>=index[j] for j in range(len(i))]) 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: if len(index)==1: self.dd['subcomponents'].insert(index[0], newconn) elif len(index)==2: - self.dd['subcomponents'][index[0]][index[1]].insert(0, - newconn) + 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) @@ -1153,12 +1173,14 @@ def addConnector(self, conn_dd, index, id=None, insert=True): def convertSubcomponents(self, subs_list): + # 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): 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 @@ -1169,6 +1191,7 @@ def convertSubcomponents(self, subs_list): 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], @@ -1178,6 +1201,7 @@ def convertSubcomponents(self, subs_list): 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'], @@ -1186,6 +1210,7 @@ def convertSubcomponents(self, subs_list): 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]) diff --git a/famodel/project.py b/famodel/project.py index 4e9ae455..6fb8b531 100644 --- a/famodel/project.py +++ b/famodel/project.py @@ -30,7 +30,7 @@ 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, rotationMatrix # Import select required helper functions from famodel.helpers import (check_headings, head_adjust, getCableDD, getDynamicCables, @@ -420,15 +420,23 @@ def loadDesign(self, d, raft=True): 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']: - r_rel = [fl['r']*np.cos(np.radians(90-head)), - fl['r']*np.sin(np.radians(90-head)), - fl['z']] + # 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, @@ -2187,15 +2195,15 @@ def plot2d(self, ax=None, plot_seabed=True,plot_bathymetry=True, plot_boundary=T mooring.ss.drawLine2d(0, ax, color="k", endpoints=False, Xuvec=[1,0,0], Yuvec=[0,1,0],label='Mooring Line') else: # simple line plot - if len(mooring.subcons_B[0].attachments)>1: - # there are fairleads, plot from their locations - for i in mooring.i_sec: - sec = mooring.getSubcomponent(i) - ax.plot([sec.rA[0],sec.rB[0]], - [sec.rA[1],sec.rB[1]]) - else: - ax.plot([mooring.rA[0], mooring.rB[0]], - [mooring.rA[1], mooring.rB[1]], 'k', lw=0.5, label='Mooring Line') + # if len(mooring.subcons_B[0].attachments)>1: + # # there are fairleads, plot from their locations + # for i in mooring.i_sec: + # sec = mooring.getSubcomponent(i) + # ax.plot([sec.rA[0],sec.rB[0]], + # [sec.rA[1],sec.rB[1]]) + # else: + ax.plot([mooring.rA[0], mooring.rB[0]], + [mooring.rA[1], mooring.rB[1]], 'k', lw=0.5, label='Mooring Line') if plot_anchors: for anchor in self.anchorList.values(): diff --git a/tests/test_moorings.py b/tests/test_moorings.py index a57748fe..ee0f3f21 100644 --- a/tests/test_moorings.py +++ b/tests/test_moorings.py @@ -10,6 +10,8 @@ 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 @@ -86,6 +88,39 @@ def test_shared_flag(setup_project): moor = setup_project.mooringList['FOWT1-FOWT2'] assert(moor.shared == 1) + +# - - - -tests in progress- - - - + +''' +def bridle_project(): + dir = os.path.dirname(os.path.realpath(__file__)) + return(Project(file=os.path.join(dir,'bridle_mooring_ontology.yaml'), raft=False)) + +def test_bridle_setup(bridle_project): + moor = bridle_project.mooringList['FOWT1a'] + # 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(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 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 correct (midpoint of bridle fairlead locs) + rB_loc = + # check location of anchor is correct + anch_loc = +''' ''' def test_shared_depth(self): From 83e46868c9817d4999decbac4d0cac120190bd48 Mon Sep 17 00:00:00 2001 From: Matt Hall <5151457+mattEhall@users.noreply.github.com> Date: Tue, 29 Jul 2025 14:57:33 -0600 Subject: [PATCH 13/34] WIP creating MoorPy objects for Moorings with bridle/parallel parts - Copied some of example_sharedmoorings.py into project.py for quick testing. - Moved the Subsystem addition to a MoorPy System's lineList into the Mooring.createSubystem method to consolidate. - Section class: added mpLine attribute and makeMoorPyLine method. - Mooring class - - createSubsystem: adapted to also create the MoorPy objects for bridle/parallel situations, with no subsystem. Method can be renamed at some point. - - Mooring.parallels flags whether there are parallel subcomponents in which case there is no subsystem. - Project.createMoorPyArray: worked on adapting this to work with bridle/parallel cases. Also streamlined a few for loops to not index things as often. This part is very much in progress! --- examples/example_sharedmoorings.py | 15 +- famodel/mooring/connector.py | 40 ++++- famodel/mooring/mooring.py | 272 +++++++++++++++++++++++------ famodel/platform/platform.py | 2 + famodel/project.py | 169 +++++++++++++----- 5 files changed, 392 insertions(+), 106 deletions(-) diff --git a/examples/example_sharedmoorings.py b/examples/example_sharedmoorings.py index 1acf0c13..91bf757c 100644 --- a/examples/example_sharedmoorings.py +++ b/examples/example_sharedmoorings.py @@ -4,24 +4,21 @@ 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 # 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 - - -# switch to directory of input files -os.chdir(input_directory) +filename = 'Inputs/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.plot2d() #project.plot3d(fowt=True) -plt.show() \ No newline at end of file +#plt.show() \ No newline at end of file diff --git a/famodel/mooring/connector.py b/famodel/mooring/connector.py index 72620e98..da4e8dd1 100644 --- a/famodel/mooring/connector.py +++ b/famodel/mooring/connector.py @@ -45,7 +45,8 @@ def __init__(self,id, r=[0,0,0], **kwargs): self.cost = {} self.getProps() - + + def makeMoorPyConnector(self, ms): '''Create a MoorPy connector object in a MoorPy system Parameters @@ -57,7 +58,6 @@ def makeMoorPyConnector(self, ms): ------- ms : class instance MoorPy system - ''' # create connector as a point in MoorPy system ms.addPoint(0,self.r) @@ -71,11 +71,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 @@ -104,6 +103,7 @@ 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''' @@ -139,4 +139,34 @@ 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 + + + 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 + ms.addLine(self['L'], self['type'], pointA=pointA, pointB=pointB) + diff --git a/famodel/mooring/mooring.py b/famodel/mooring/mooring.py index 6db660e6..8e24d59c 100644 --- a/famodel/mooring/mooring.py +++ b/famodel/mooring/mooring.py @@ -99,7 +99,7 @@ def __init__(self, dd=None, subsystem=None, anchor=None, 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 @@ -196,6 +196,7 @@ def update(self, dd=None): 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 in range(len(self.i_sec)): sec = self.getSubcomponent(self.i_sec[i]) @@ -463,7 +464,7 @@ def updateTensions(self): return(self.loads['TAmax'],self.loads['TBmax']) - 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 @@ -477,74 +478,236 @@ 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 + ms : MoorPy System, optional MoorPy system this subsystem is a part of ''' # TODO: Figure out how to handle subsystems for lines with subsections (esp when double chain in middle...) # 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, - 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) + if self.parallels: # make parts of a MoorPy system + + # Make Points + for i in self.i_con: + # >>> leah had some checks here that I didn't understand <<< + con = self.getSubcomponent(i) + con.makeMoorPyConnector(ms) + + # Make Lines + for i in self.i_sec: + sec = self.getSubcomponent(i) + sec.makeMoorPyLine(ms) + + """ + n = len(self.subcomponents) # number of serial subcomponent items + + for i in range(n): + + 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): + items[i].attach(items[i+1], end='a') + + elif isinstance(items[i], Edge) and isinstance(items[i+1], Node): + items[i+1].attach(items[i], end='b') + + else: + raise Exception('sequences is not alternating between nodes and edges') + + + + # some initialization steps. + self.nLines = len(lengths) + if len(connectors) == 0: + connectors = [{}]*(self.nLines - 1) + elif not len(connectors) == self.nLines - 1: + raise Exception('Length of connectors must be nLines - 1') + + if not len(types)==self.nLines: + raise Exception("The specified number of lengths and types is inconsistent.") + + # get cumulative sum of line lengths, starting from anchor segment + Lcsum = np.cumsum(np.array(lengths)) + + # set end A location depending on whether configuration is suspended/symmetrical + if suspended==2: # symmetrical suspended case + rA = np.array([-0.5*self.span-self.rad_fair, 0, -1]) # shared line midpoint coordinates + self.shared = True # flag that it's being modeled as symmetric + elif suspended==1: # general suspended case + rA = np.array([-self.span-self.rad_fair, 0, self.z_fair]) # other suspended end + else: # normal anchored line case + rA = np.array([-self.span-self.rad_fair, 0, -self.depth]) # anchor coordinates + rB = np.array([-self.rad_fair, 0, self.z_fair]) # fairlead coordinates + self.rA = rA + self.rB = rB + + # Go through each line segment and add its upper point, add the line, and connect the line to the points + for i in range(self.nLines): + # find the specified lineType dict and save a reference to it + if type(types[i]) == dict: # if it's a dictionary, just point to it + self.lineTypes[i] = types[i] + # otherwise we're assuming it's a string of the lineType name + elif types[i] in self.lineTypes: # first look for the name in the subsystem + self.lineTypes[i] = self.lineTypes[types[i]] + elif self.sys: # otherwise look in the parent system, if there is one + if types[i] in self.sys.lineTypes: # first look for the name in the subsystem + self.lineTypes[i] = self.sys.lineTypes[types[i]] + else: + raise Exception(f"Can't find lineType '{types[i]}' in the SubSystem or parent System.") + else: + raise Exception(f"Can't find lineType '{types[i]}' in the SubSystem.") + + # add the line segment using the reference to its lineType dict + if nSegs is None: + self.addLine(lengths[i], self.lineTypes[i]) + elif isinstance(nSegs, (int, float)): + self.addLine(lengths[i], self.lineTypes[i], nSegs=nSegs) + elif isinstance(nSegs, list): + self.addLine(lengths[i], self.lineTypes[i], nSegs=nSegs[i]) + else: + raise ValueError("Invalid type for nSegs. Expected None, a number, or a list.") - lengths = [] - types = [] - # run through each line section and collect the length and type - for i in self.i_sec: - sec = self.getSubcomponent(i) - 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'] - conns = [] - for i in self.i_con: - conns.append(self.getSubcomponent(i)) - + # add the upper end point of the segment + if i==self.nLines-1: # if this is the upper-most line + self.addPoint(-1, rB, DOFs=[0,2]) # add the fairlead point (make it coupled) + #self.bodyList[0].attachPoint(i+2, rB) # attach the fairlead point to the body (two points already created) + else: # if this is an intermediate line + m = connectors[i].get('m', 0) + v = connectors[i].get('v', 0) + # add the point, initializing linearly between anchor and fairlead/midpoint + self.addPoint(0, rA + (rB-rA)*Lcsum[i]/Lcsum[-1], m=m, v=v, DOFs=[0,2]) + # attach the line to the points + self.pointList[-2].attachLine(i+1, 0) # attach end A of the line + self.pointList[-1].attachLine(i+1, 1) # attach end B of the line + + """ + - # 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) - - # note: next bit has similar code/function as Connector.makeMoorPyConnector <<< + 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 - # 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 + #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 in self.i_sec: + sec = self.getSubcomponent(i) + lengths.append(sec['L']) + types.append(sec['type']) # list of type names + + conns = [] + for i in self.i_con: + conns.append(self.getSubcomponent(i)) + + + # 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) + + # 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 = self.getSubcomponent(self.i_con[i]) - conn.mpConn = ss.pointList[i] - conn.mpConn.CdA = conns[i]['CdA'] - conn.getProps() + for i in range(startNum,len(ss.pointList)): + conn = self.getSubcomponent(self.i_con[i]) + conn.mpConn = ss.pointList[i] + conn.mpConn.CdA = conns[i]['CdA'] + conn.getProps() + + # solve the system + ss.initialize() + ss.staticSolve() - # 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) + + # 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.''' + + # Tabulate the section lengths + L = [] + + n = len(items) + for i in range(n): + + if isinstance(items[i], list): + subL = [] + for subitem in items[i]: # go through each parallel subitem + + if isinstance(subitem, list): # if it's a concatenation of multiple things + + else: + raise Exception("Unsupported situation ... parallel subitems must be lists") + + elif isinstance(items[i], Node): + pass + + elif isinstance(items[i], Edge): + L.append(items[i]['L']) + + + Lcsum = np.cumsum(np.array(lengths)) + + # add the point, initializing linearly between anchor and fairlead/midpoint + self.addPoint(0, rA + (rB-rA)*Lcsum[i]/Lcsum[-1], m=m, v=v, DOFs=[0,2]) + + # Calculate and set approximate node positions + """ def mirror(self,create_subsystem=True): ''' Mirrors a half design dictionary. Useful for symmetrical shared mooring lines where @@ -1173,10 +1336,13 @@ def addConnector(self, conn_dd, index, id=None, insert=True): 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): diff --git a/famodel/platform/platform.py b/famodel/platform/platform.py index d3d8fde4..4a23a7fe 100644 --- a/famodel/platform/platform.py +++ b/famodel/platform/platform.py @@ -188,9 +188,11 @@ def mooringSystem(self,rotateBool=0,mList=None,bodyInfo=None, project=None): ssloc = mooring.createSubsystem() 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 diff --git a/famodel/project.py b/famodel/project.py index 6fb8b531..0a203ed0 100644 --- a/famodel/project.py +++ b/famodel/project.py @@ -21,7 +21,7 @@ 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 @@ -30,7 +30,7 @@ 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, rotationMatrix +from famodel.famodel_base import Node, Edge, rotationMatrix # Import select required helper functions from famodel.helpers import (check_headings, head_adjust, getCableDD, getDynamicCables, @@ -2614,53 +2614,120 @@ 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=1, 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(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, Edge): + anchor.mpAnchor.attachLine(subcom.mpLine.number, att['end']) + elif isinstance(subcom, Node): + # The end is a node, eventually could attach it to the anchor if there's an r_rel + pass + # (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 + anchor.mpAnchor.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) + if mooring.parallels: + + # 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(len(self.ms.pointList), [r[0]-PF.r[0], r[1]-PF.r[1], r[2]-PF.r[2]]) + 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[0]-PF.r[0], r[1]-PF.r[1], r[2]-PF.r[2]]) + + elif isinstance(subcom, Edge): + r = subcom.attached_to[1].r # approximate end point...? + point = self.ms.addPoint(1, r) + PF.body.attachPoint(len(self.ms.pointList), [r[0]-PF.r[0], r[1]-PF.r[1], r[2]-PF.r[2]]) + 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[0]-PF.r[0], r[1]-PF.r[1], r[2]-PF.r[2]]) + # (the section line object(s) should already be attached to this point) + else: + # 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(len(self.ms.pointList),[ssloc.rB[0]-PF.r[0],ssloc.rB[1]-PF.r[1],ssloc.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 + + # 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 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 @@ -2669,18 +2736,21 @@ def getMoorPyArray(self, plt=0, pristineLines=True, cables=True): # 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) + self.mooringList[i].createSubsystem(case=self.mooringList[i].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 + self.mooringList[i].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 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) - + ''' # find associated platforms/ buoys att = self.mooringList[i].attached_to @@ -5084,7 +5154,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 @@ -5100,4 +5170,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/Inputs/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 From 8a25a288523d7e2051be4e58f6f706fead57f76d Mon Sep 17 00:00:00 2001 From: Matt Hall <5151457+mattEhall@users.noreply.github.com> Date: Wed, 30 Jul 2025 13:02:55 -0600 Subject: [PATCH 14/34] Mooring.positionSubcomponents and Project.getMoorPyArray progress Mooring.positionSubcomponents now works, it seems, except for parallel portions still to do. Project.getMoorPyArray now gets through creating the mooring system, but getting error related to lineType entries being strings not numeric. --- famodel/famodel_base.py | 8 +-- famodel/mooring/mooring.py | 74 +++++++++++++++------- famodel/project.py | 123 ++++++++++++++++++++++++++----------- 3 files changed, 144 insertions(+), 61 deletions(-) diff --git a/famodel/famodel_base.py b/famodel/famodel_base.py index 75d47e8f..f648aa06 100644 --- a/famodel/famodel_base.py +++ b/famodel/famodel_base.py @@ -438,8 +438,8 @@ def setPosition(self, r, theta=0, force=False): ''' # Don't allow this if this is part of another object - if self.part_of and not force: - raise Exception("Can't setPosition of an object that's part of a higher object unless force=True.") + 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 if len(r) > len(self.r): # default r is 2D, but can be adjusted to 3D @@ -570,8 +570,8 @@ def __init__(self, id): 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 diff --git a/famodel/mooring/mooring.py b/famodel/mooring/mooring.py index 8e24d59c..5157d27a 100644 --- a/famodel/mooring/mooring.py +++ b/famodel/mooring/mooring.py @@ -5,7 +5,7 @@ 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): @@ -497,7 +497,7 @@ def createSubsystem(self, case=0, pristine=True, dd=None, ms=None): # Make Lines for i in self.i_sec: sec = self.getSubcomponent(i) - sec.makeMoorPyLine(ms) + sec.makeMoorPyLine(ms) # this also will connect the Lines to Points """ n = len(self.subcomponents) # number of serial subcomponent items @@ -672,42 +672,72 @@ def createSubsystem(self, case=0, pristine=True, dd=None, ms=None): 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.''' + print('positionSubcomponents!!!') + # Tabulate the section lengths L = [] - n = len(items) - - for i in range(n): - - if isinstance(items[i], list): - subL = [] - for subitem in items[i]: # go through each parallel subitem + n_serial_nodes = 0 # number of serial nodes, including first and last - if isinstance(subitem, list): # if it's a concatenation of multiple things - + # First pass, going through each section in series to 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") - - elif isinstance(items[i], Node): - pass - elif isinstance(items[i], Edge): - L.append(items[i]['L']) + 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 + print(f'There are {n_serial_nodes} serial nodes') + + # Position nodes along main serial string between rA and rB + Lsum = np.cumsum(np.array(L)) + i = 0 # index of node along serial string (at A is 0) + + for item in self.subcomponents: + if isinstance(item, list) or isinstance(item, Edge): + i = i+1 # note that we're moving a certain length along the string + print(i) + + # if it's a node, but no the first or last one + elif isinstance(item, Node) and i > 0 and i < n_serial_nodes-1: + r = self.rA + (self.rB-self.rA)*Lsum[i]/Lsum[-1] + item.setPosition(r) + print(f'Set position of Node {i} to {r[0]:5.0f}, {r[1]:5.0f}, {r[2]:5.0f}') - Lcsum = np.cumsum(np.array(lengths)) - # add the point, initializing linearly between anchor and fairlead/midpoint - self.addPoint(0, rA + (rB-rA)*Lcsum[i]/Lcsum[-1], m=m, v=v, DOFs=[0,2]) - # Calculate and set approximate node positions - """ + # Second pass, to position any nodes that are along parallel sections + ''' + for i in range(n): + TODO + if isinstance(items[i], list): # indicates there are parallel sections here + subL = [] + for subitem in items[i]: # go through each parallel subitem + ''' + + + def mirror(self,create_subsystem=True): ''' Mirrors a half design dictionary. Useful for symmetrical shared mooring lines where diff --git a/famodel/project.py b/famodel/project.py index 0a203ed0..e6bfe466 100644 --- a/famodel/project.py +++ b/famodel/project.py @@ -554,7 +554,10 @@ def loadDesign(self, d, raft=True): else: moor.attachTo(platform, r_rel=[platform.rFair,0,platform.zFair], end='b') - + + # Position the subcomponents along the Mooring + moor.positionSubcomponents() + # update counter mct += 1 @@ -576,10 +579,12 @@ def loadDesign(self, d, raft=True): 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 @@ -624,6 +629,9 @@ def loadDesign(self, d, raft=True): 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 @@ -677,7 +685,10 @@ def loadDesign(self, d, raft=True): 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) @@ -2680,7 +2691,7 @@ def getMoorPyArray(self, plt=0, pristineLines=True, cables=True): break # attach rB point to platform - if mooring.parallels: + if mooring.parallels: # case with paralles/bridles # Look at end B object(s) subcom = mooring.subcomponents[-1] @@ -2693,26 +2704,28 @@ def getMoorPyArray(self, plt=0, pristineLines=True, cables=True): if isinstance(subcom2, Edge): r = subcom2.attached_to[1].r # approximate end point...? point = self.ms.addPoint(1, r) - PF.body.attachPoint(len(self.ms.pointList), [r[0]-PF.r[0], r[1]-PF.r[1], r[2]-PF.r[2]]) + 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[0]-PF.r[0], r[1]-PF.r[1], r[2]-PF.r[2]]) + 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(len(self.ms.pointList), [r[0]-PF.r[0], r[1]-PF.r[1], r[2]-PF.r[2]]) + 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[0]-PF.r[0], r[1]-PF.r[1], r[2]-PF.r[2]]) + PF.body.attachPoint(pnum, r-PF.r) # (the section line object(s) should already be attached to this point) - else: + + + else: # normal serial/subsystem case # add fairlead point point = self.ms.addPoint(1,ssloc.rB) # add connector info for fairlead point @@ -2722,52 +2735,92 @@ def getMoorPyArray(self, plt=0, pristineLines=True, cables=True): point.CdA = self.ms.lineList[-1].pointList[-1].CdA # attach the line to point point.attachLine(ssloc.number,1) - PF.body.attachPoint(len(self.ms.pointList),[ssloc.rB[0]-PF.r[0],ssloc.rB[1]-PF.r[1],ssloc.rB[2]-PF.r[2]]) # attach to fairlead (need to subtract out location of platform from point for subsystem integration to work correctly) - + 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) - check = np.ones((len(self.mooringList),1)) # 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 + 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 + 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, + if hasattr(mooring, 'shared'): # <<< + mooring.createSubsystem(case=mooring.shared, pristine=pristineLines, ms=self.ms) else: - self.mooringList[i].createSubsystem(case=1,pristine=pristineLines, + 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): - # add fairlead point A 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]]) + 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 = self.rA + else: + rEnd = self.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: From 88a91fa3fb6ea0ddce18de82e7a22c33b4593d3e Mon Sep 17 00:00:00 2001 From: lsirkis Date: Wed, 30 Jul 2025 13:39:58 -0600 Subject: [PATCH 15/34] WIP parallels tests, fix example input folder, bug fixes -- added tests for parallel sections (bridles etc) -- removed examples input folders, fixed directory -- bug fix loading EA for parallel sections, now forces to be floats -- working on bug fix for relocation of shared anchors when loading if span is slightly off (working to recalc span so anchor loc doesn't change since it's explicitly defined in ontology) -- cable bug fix make sure connection location is at depth relative to platform depth --- .../08_Design_Adjustment/01_Fairleads.yaml | 3 +- examples/Inputs/output_MD.dat | 55 ------ examples/Inputs/soil_sample.txt | 15 -- .../{Inputs => }/Moordyn_semitaut200m.dat | 0 examples/{Inputs => }/OntologySample200m.yaml | 0 .../OntologySample200m_1turb.yaml | 0 .../OntologySample200m_uniformArray.yaml | 0 .../OntologySample600m_shared.yaml | 0 .../{Inputs => }/bathymetry200m_Array.txt | 0 .../{Inputs => }/bathymetry200m_sample.txt | 0 examples/{Inputs => }/boundary_sample.csv | 0 examples/create_platform_from_ms.py | 7 +- examples/duplicate_platform.py | 6 +- examples/example_anchors.py | 4 +- examples/example_driver.py | 6 +- examples/example_manual.py | 12 +- examples/example_sharedmoorings.py | 9 +- examples/{Inputs => }/gch.yaml | 0 examples/{Inputs => }/iea_15MW.yaml | 0 examples/{Inputs => }/maine_rose.csv | 0 examples/platform_ms.py | 8 +- examples/uniformArray.py | 8 +- famodel/cables/cable.py | 4 +- famodel/helpers.py | 2 + famodel/project.py | 14 +- tests/mooring_ontology.yaml | 3 +- tests/mooring_ontology_parallels.yaml | 159 ++++++++++++++++++ tests/platform_ontology.yaml | 3 +- tests/testOntology.yaml | 3 +- tests/test_moorings.py | 34 +++- 30 files changed, 230 insertions(+), 125 deletions(-) delete mode 100644 examples/Inputs/output_MD.dat delete mode 100644 examples/Inputs/soil_sample.txt rename examples/{Inputs => }/Moordyn_semitaut200m.dat (100%) rename examples/{Inputs => }/OntologySample200m.yaml (100%) rename examples/{Inputs => }/OntologySample200m_1turb.yaml (100%) rename examples/{Inputs => }/OntologySample200m_uniformArray.yaml (100%) rename examples/{Inputs => }/OntologySample600m_shared.yaml (100%) rename examples/{Inputs => }/bathymetry200m_Array.txt (100%) rename examples/{Inputs => }/bathymetry200m_sample.txt (100%) rename examples/{Inputs => }/boundary_sample.csv (100%) rename examples/{Inputs => }/gch.yaml (100%) rename examples/{Inputs => }/iea_15MW.yaml (100%) rename examples/{Inputs => }/maine_rose.csv (100%) create mode 100644 tests/mooring_ontology_parallels.yaml diff --git a/examples/08_Design_Adjustment/01_Fairleads.yaml b/examples/08_Design_Adjustment/01_Fairleads.yaml index 733068e4..4fa235bd 100644 --- a/examples/08_Design_Adjustment/01_Fairleads.yaml +++ b/examples/08_Design_Adjustment/01_Fairleads.yaml @@ -19,8 +19,7 @@ platform: type : FOWT fairleads : # list of fairlead coordinates for the platform relative to platform coordinate and 0-degree heading - name : fairlead1 - r : 58 - z : -14 + r_rel : [58, 0, -14] headings : [30, 150, 270] 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/soil_sample.txt b/examples/Inputs/soil_sample.txt deleted file mode 100644 index 70d8fa14..00000000 --- a/examples/Inputs/soil_sample.txt +++ /dev/null @@ -1,15 +0,0 @@ ---- MoorPy Soil Input File --- -nGridX 3 -nGridY 3 - -1901 0 1900 --1900 mud mud mud - 2 mud rock mud - 1900 mud mud mud ---- SOIL TYPES --- -Class Gamma Su0 k alpha phi UCS Em -(name) (kN/m^3) (kPa) (kPa/m) (-) (deg) (MPa) (MPa) -mud 4.7 2.39 1.41 0.7 - - - -mud_firm 4.7 23.94 2.67 0.7 - - - -rock - - - - - 7 50 ------------------- - 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 100% rename from examples/Inputs/OntologySample200m.yaml rename to examples/OntologySample200m.yaml 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 100% rename from examples/Inputs/OntologySample600m_shared.yaml rename to examples/OntologySample600m_shared.yaml 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..60b22c38 100644 --- a/examples/example_driver.py +++ b/examples/example_driver.py @@ -22,16 +22,16 @@ import os import matplotlib.pyplot as plt -os.chdir('./Inputs/') +dir = os.path.dirname(os.path.realpath(__file__)) # 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=dir+ontology_file,raft=False) # create moorpy system of the array, include cables in the system project.getMoorPyArray(cables=1) # plot in 3d, using moorpy system for the mooring and cable plots 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 91bf757c..6b7ed2dc 100644 --- a/examples/example_sharedmoorings.py +++ b/examples/example_sharedmoorings.py @@ -7,18 +7,21 @@ import famodel from famodel import Project import matplotlib.pyplot as plt +import os # point to location of yaml file with uniform array info -filename = 'Inputs/OntologySample600m_shared.yaml' # yaml file for project +dir = os.path.dirname(os.path.realpath(__file__)) +filename = '\OntologySample600m_shared.yaml' # yaml file for project + # load in yaml -project = Project(file=filename,raft=False) +project = Project(file=dir+filename,raft=False) project.getMoorPyArray() # plot in 2d and 3d -#project.plot2d() +project.plot2d() #project.plot3d(fowt=True) #plt.show() \ No newline at end of file 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/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/cables/cable.py b/famodel/cables/cable.py index 52ded174..da320b9d 100644 --- a/famodel/cables/cable.py +++ b/famodel/cables/cable.py @@ -158,7 +158,7 @@ def reposition(self,headings=None,project=None,rad_fair=[]): # 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].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: @@ -171,7 +171,7 @@ def reposition(self,headings=None,project=None,rad_fair=[]): 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].zFair+self.attached_to[1].r[2]] self.subcomponents[-1].rB = Bloc; self.rB = Bloc if project: diff --git a/famodel/helpers.py b/famodel/helpers.py index ea277bee..dff5fae2 100644 --- a/famodel/helpers.py +++ b/famodel/helpers.py @@ -786,6 +786,8 @@ def getMoorings(lcID, lineConfigs, connectorTypes, pfID, proj): 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']) sublineLast[ii] = 1 else: raise Exception(f"keys in subsection line definitions must either be 'type', 'mooringFamily', or 'connectorType'") diff --git a/famodel/project.py b/famodel/project.py index e6bfe466..7dd0909a 100644 --- a/famodel/project.py +++ b/famodel/project.py @@ -37,7 +37,7 @@ getMoorings, getAnchors, getFromDict, cleanDataTypes, getStaticCables, getCableDesign, m2nm, loadYAML, configureAdjuster, route_around_anchors, attachFairleads, - calc_heading) + calc_heading, calc_midpoint) class Project(): @@ -452,9 +452,10 @@ def loadDesign(self, d, raft=True): for jt in platforms[pfID]['Jtubes']: if 'headings' in jt: for head in jt['headings']: - r_rel = [jt['r']*np.cos(np.radians(90-head)), - jt['r']*np.sin(np.radians(90-head)), - jt['z']] + # 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)) @@ -678,6 +679,11 @@ def loadDesign(self, d, raft=True): # 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=PF[0].r, heading=headingB, project=self) diff --git a/tests/mooring_ontology.yaml b/tests/mooring_ontology.yaml index b75cba6f..aa06dd1d 100644 --- a/tests/mooring_ontology.yaml +++ b/tests/mooring_ontology.yaml @@ -43,8 +43,7 @@ platforms: - fairleads : # list of fairlead coordinates for the platform relative to platform coordinate and 0-degree heading - name: fairlead1 - r: 58 - z: -14 + 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 diff --git a/tests/mooring_ontology_parallels.yaml b/tests/mooring_ontology_parallels.yaml new file mode 100644 index 00000000..d8e5eefc --- /dev/null +++ b/tests/mooring_ontology_parallels.yaml @@ -0,0 +1,159 @@ +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, 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: # 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 index 980f9aaa..55d95270 100644 --- a/tests/platform_ontology.yaml +++ b/tests/platform_ontology.yaml @@ -1085,8 +1085,7 @@ platforms: type : FOWT fairleads : # list of fairlead coordinates for the platform relative to platform coordinate and 0-degree heading - name: fairlead1 - r: 40.5 - z: -20 + 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 diff --git a/tests/testOntology.yaml b/tests/testOntology.yaml index bdeaafab..7da8a431 100644 --- a/tests/testOntology.yaml +++ b/tests/testOntology.yaml @@ -1205,8 +1205,7 @@ platforms: type : FOWT fairleads : # list of fairlead coordinates for the platform relative to platform coordinate and 0-degree heading - name: fairlead1 - r: 40.5 - z: -20 + 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 diff --git a/tests/test_moorings.py b/tests/test_moorings.py index ee0f3f21..2afce8b0 100644 --- a/tests/test_moorings.py +++ b/tests/test_moorings.py @@ -48,6 +48,15 @@ def test_fairlead_connection(setup_project): 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'] + assert(fl.r==setup_project.mooringList['FOWT1a'].rB) + pf = setup_project.platformList['FOWT1'] + head_fl = np.radians(90-30) + head_pf = np.radians(90)-pf.phi + assert(fl.r==[58*cos(head_fl+head_pf),58*sin(head_fl+head_pf),-14]) + def test_rA_depth(setup_project): moor = setup_project.mooringList['FOWT1a'] loc = moor.rA @@ -91,13 +100,13 @@ def test_shared_flag(setup_project): # - - - -tests in progress- - - - -''' + def bridle_project(): dir = os.path.dirname(os.path.realpath(__file__)) - return(Project(file=os.path.join(dir,'bridle_mooring_ontology.yaml'), raft=False)) + return(Project(file=os.path.join(dir,'mooring_ontology_parallels.yaml'), raft=False)) def test_bridle_setup(bridle_project): - moor = bridle_project.mooringList['FOWT1a'] + 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) @@ -116,11 +125,22 @@ def test_bridle_setup(bridle_project): def test_bridle_end_locs(bridle_project): moor = bridle_project.mooringList['FOWT1a'] - # check rB is correct (midpoint of bridle fairlead locs) - rB_loc = + # 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 calculate_midpoint + midpoint = calculate_midpoint(fl_locs) + assert(midpoint==moor.rB) + # check # check location of anchor is correct - anch_loc = -''' + u = np.array([np.cos(np.radians(moor.heading)),np.sin(np.radians(moor.heading))]) + anch_loc = np.hstack((np.array(midpoint[:2])+moor.span*u,-bridle_project.depth)) + assert(anch_loc==moor.rA) + + + ''' def test_shared_depth(self): From 240f1ff8cf54508ad802cc879c89502223746480 Mon Sep 17 00:00:00 2001 From: Matt Hall <5151457+mattEhall@users.noreply.github.com> Date: Wed, 30 Jul 2025 15:12:47 -0600 Subject: [PATCH 16/34] Mooring.positionSubcomponents done, Project.getMoorPyArray progress - Fixed some bugs in both the above. - Added missing parallel node positioning in Mooring.positionSubcomponents. - Seeing errors because bridle connector nodes aren't connected by the time Project.getMoorPyArray is called. --- famodel/mooring/mooring.py | 87 +++++++++++++++++++++++++++++--------- famodel/project.py | 6 ++- 2 files changed, 72 insertions(+), 21 deletions(-) diff --git a/famodel/mooring/mooring.py b/famodel/mooring/mooring.py index 5157d27a..9c2530f3 100644 --- a/famodel/mooring/mooring.py +++ b/famodel/mooring/mooring.py @@ -682,10 +682,11 @@ def positionSubcomponents(self): # 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 to figure out lengths + # ----- 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 @@ -712,32 +713,78 @@ def positionSubcomponents(self): # Position nodes along main serial string between rA and rB Lsum = np.cumsum(np.array(L)) - i = 0 # index of node along serial string (at A is 0) + j = 0 # index of node along serial string (at A is 0) - for item in self.subcomponents: + for i, item in enumerate(self.subcomponents): if isinstance(item, list) or isinstance(item, Edge): - i = i+1 # note that we're moving a certain length along the string - print(i) + j = j+1 # note that we're moving a certain length along the string - # if it's a node, but no the first or last one - elif isinstance(item, Node) and i > 0 and i < n_serial_nodes-1: - r = self.rA + (self.rB-self.rA)*Lsum[i]/Lsum[-1] + # 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]/Lsum[-1] item.setPosition(r) - print(f'Set position of Node {i} to {r[0]:5.0f}, {r[1]:5.0f}, {r[2]:5.0f}') + print(f'Set position of subcom {i} to {r[0]:5.0f}, {r[1]:5.0f}, {r[2]:5.0f}') - # Second pass, to position any nodes that are along parallel sections - ''' - for i in range(n): - TODO - if isinstance(items[i], list): # indicates there are parallel sections here - subL = [] - for subitem in items[i]: # go through each parallel subitem - ''' - - + # ----- 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)) + print(f'parallel string ends A and B are at') + print(f'{rA[0]:5.0f}, {rA[1]:5.0f}, {rA[2]:5.0f}') + print(f'{rB[0]:5.0f}, {rB[1]:5.0f}, {rB[2]:5.0f}') + + 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) + print(f'Set position of Node {j} to {r[0]:5.0f}, {r[1]:5.0f}, {r[2]:5.0f}') + else: + print('end of parallel') + breakpoint() + print('yep') def mirror(self,create_subsystem=True): ''' Mirrors a half design dictionary. Useful for symmetrical shared mooring lines where diff --git a/famodel/project.py b/famodel/project.py index 7dd0909a..241ba8fe 100644 --- a/famodel/project.py +++ b/famodel/project.py @@ -2795,6 +2795,10 @@ def getMoorPyArray(self, plt=0, pristineLines=True, cables=True): elif isinstance(subcom2, Node): r = subcom2.r # approximate end point...? pnum = subcom2.mpConn.number + + print("Stopping here because bridles don't seem to be attached to platform") + print("can try 'p subcom2.attached_to' in the debugger. it gives None" + breakpoint() platform.body.attachPoint(pnum, r-platform.r) elif isinstance(subcom, Edge): @@ -5233,7 +5237,7 @@ def style_it(sheet, row, col_start, col_end, fill_color="FFFF00"): # point to location of yaml file with uniform array info - filename = '../Examples/Inputs/OntologySample600m_shared.yaml' # yaml file for project + filename = '../Examples/OntologySample600m_shared.yaml' # yaml file for project # load in yaml project = Project(file=filename,raft=False) From 9eee5b6ba0e9775118be84a16d9b40f1c09fe999 Mon Sep 17 00:00:00 2001 From: Matt Hall <5151457+mattEhall@users.noreply.github.com> Date: Wed, 30 Jul 2025 16:26:44 -0600 Subject: [PATCH 17/34] Bit of cleanup in Project/Mooring Just cleaning up after myself. A MoorPy commit will fix the last bridle issue. --- famodel/mooring/mooring.py | 121 ++----------------------------------- famodel/project.py | 4 -- 2 files changed, 5 insertions(+), 120 deletions(-) diff --git a/famodel/mooring/mooring.py b/famodel/mooring/mooring.py index 9c2530f3..0812be9a 100644 --- a/famodel/mooring/mooring.py +++ b/famodel/mooring/mooring.py @@ -479,15 +479,18 @@ def createSubsystem(self, case=0, pristine=True, dd=None, ms=None): dd : dict, optional Dictionary describing the design ms : MoorPy System, optional - MoorPy system this subsystem is a part of + MoorPy system this subsystem is a part of. Necessary if ''' - # TODO: Figure out how to handle subsystems for lines with subsections (esp when double chain in middle...) + # set design dictionary as self.dd if none given, same with connectorList if not dd: dd = self.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 i in self.i_con: # >>> leah had some checks here that I didn't understand <<< @@ -498,120 +501,6 @@ def createSubsystem(self, case=0, pristine=True, dd=None, ms=None): for i in self.i_sec: sec = self.getSubcomponent(i) sec.makeMoorPyLine(ms) # this also will connect the Lines to Points - - """ - n = len(self.subcomponents) # number of serial subcomponent items - - for i in range(n): - - 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): - items[i].attach(items[i+1], end='a') - - elif isinstance(items[i], Edge) and isinstance(items[i+1], Node): - items[i+1].attach(items[i], end='b') - - else: - raise Exception('sequences is not alternating between nodes and edges') - - - - # some initialization steps. - self.nLines = len(lengths) - if len(connectors) == 0: - connectors = [{}]*(self.nLines - 1) - elif not len(connectors) == self.nLines - 1: - raise Exception('Length of connectors must be nLines - 1') - - if not len(types)==self.nLines: - raise Exception("The specified number of lengths and types is inconsistent.") - - # get cumulative sum of line lengths, starting from anchor segment - Lcsum = np.cumsum(np.array(lengths)) - - # set end A location depending on whether configuration is suspended/symmetrical - if suspended==2: # symmetrical suspended case - rA = np.array([-0.5*self.span-self.rad_fair, 0, -1]) # shared line midpoint coordinates - self.shared = True # flag that it's being modeled as symmetric - elif suspended==1: # general suspended case - rA = np.array([-self.span-self.rad_fair, 0, self.z_fair]) # other suspended end - else: # normal anchored line case - rA = np.array([-self.span-self.rad_fair, 0, -self.depth]) # anchor coordinates - rB = np.array([-self.rad_fair, 0, self.z_fair]) # fairlead coordinates - - self.rA = rA - self.rB = rB - - - # Go through each line segment and add its upper point, add the line, and connect the line to the points - for i in range(self.nLines): - - # find the specified lineType dict and save a reference to it - if type(types[i]) == dict: # if it's a dictionary, just point to it - self.lineTypes[i] = types[i] - # otherwise we're assuming it's a string of the lineType name - elif types[i] in self.lineTypes: # first look for the name in the subsystem - self.lineTypes[i] = self.lineTypes[types[i]] - elif self.sys: # otherwise look in the parent system, if there is one - if types[i] in self.sys.lineTypes: # first look for the name in the subsystem - self.lineTypes[i] = self.sys.lineTypes[types[i]] - else: - raise Exception(f"Can't find lineType '{types[i]}' in the SubSystem or parent System.") - else: - raise Exception(f"Can't find lineType '{types[i]}' in the SubSystem.") - - # add the line segment using the reference to its lineType dict - if nSegs is None: - self.addLine(lengths[i], self.lineTypes[i]) - elif isinstance(nSegs, (int, float)): - self.addLine(lengths[i], self.lineTypes[i], nSegs=nSegs) - elif isinstance(nSegs, list): - self.addLine(lengths[i], self.lineTypes[i], nSegs=nSegs[i]) - else: - raise ValueError("Invalid type for nSegs. Expected None, a number, or a list.") - - # add the upper end point of the segment - if i==self.nLines-1: # if this is the upper-most line - self.addPoint(-1, rB, DOFs=[0,2]) # add the fairlead point (make it coupled) - #self.bodyList[0].attachPoint(i+2, rB) # attach the fairlead point to the body (two points already created) - else: # if this is an intermediate line - m = connectors[i].get('m', 0) - v = connectors[i].get('v', 0) - # add the point, initializing linearly between anchor and fairlead/midpoint - self.addPoint(0, rA + (rB-rA)*Lcsum[i]/Lcsum[-1], m=m, v=v, DOFs=[0,2]) - - # attach the line to the points - self.pointList[-2].attachLine(i+1, 0) # attach end A of the line - self.pointList[-1].attachLine(i+1, 1) # attach end B of the line - - """ - else: ss=Subsystem(mooringSys=ms, depth=-dd['zAnchor'], rho=self.rho, g=self.g, diff --git a/famodel/project.py b/famodel/project.py index 241ba8fe..8337ea26 100644 --- a/famodel/project.py +++ b/famodel/project.py @@ -2795,10 +2795,6 @@ def getMoorPyArray(self, plt=0, pristineLines=True, cables=True): elif isinstance(subcom2, Node): r = subcom2.r # approximate end point...? pnum = subcom2.mpConn.number - - print("Stopping here because bridles don't seem to be attached to platform") - print("can try 'p subcom2.attached_to' in the debugger. it gives None" - breakpoint() platform.body.attachPoint(pnum, r-platform.r) elif isinstance(subcom, Edge): From 9639f75a280b9b27af429a5c624c63c41b88f99b Mon Sep 17 00:00:00 2001 From: Matt Hall <5151457+mattEhall@users.noreply.github.com> Date: Fri, 1 Aug 2025 12:09:30 -0600 Subject: [PATCH 18/34] Connector.makeMoorPyConnector now makes fixed point when joined: - Created Node.isJoined method to check if a node is joined to anything. - Changed Connector.makeMoorPyConnector to check if a node is joined, in which case the MP point will be fixed rather than free. --- famodel/famodel_base.py | 13 +++++++++++++ famodel/mooring/connector.py | 8 +++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/famodel/famodel_base.py b/famodel/famodel_base.py index f648aa06..cd6b79b5 100644 --- a/famodel/famodel_base.py +++ b/famodel/famodel_base.py @@ -188,6 +188,19 @@ def join(self, object): 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''' diff --git a/famodel/mooring/connector.py b/famodel/mooring/connector.py index da4e8dd1..c6506322 100644 --- a/famodel/mooring/connector.py +++ b/famodel/mooring/connector.py @@ -59,8 +59,14 @@ 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] From 47cdce409ba17ad5cb49ee35870d2f92bd79423c Mon Sep 17 00:00:00 2001 From: lsirkis Date: Tue, 5 Aug 2025 16:20:38 -0600 Subject: [PATCH 19/34] Tiny bug fix famodel_base, bug fix helpers, add tests -- helpers bug fix: catch when a connector is not provided at the end of bridles - now we check for this and add empty connector object if needed -- tiny bug fix in famodel_base to prevent weird rounding - apparently, when assigning values into an array if you use array[:len(array)] = vals, it rounds the vals to whole numbers. Fixed this by adding >= to reassign value of whole array rather than just > -- add tests for parallel sections (bridles, double chains, etc) -- mooring.addMarineGrowth now works with updated dd (subcomponents instead of sections/connectors lists) - does not currently work for parallel sections -- arrayWatchCircle added warning that this does not currently work for parallel sections, WIP to add this capability -- tiny change to anchor.py to use mooring.sections method instead of sections in mooring design dictionary as this part of the design dictionary was removed -- updated ontologies for examples -- udpated tests --- examples/OntologySample200m.yaml | 43 +++++-- examples/OntologySample600m_shared.yaml | 6 +- examples/example_driver.py | 4 +- examples/example_sharedmoorings.py | 8 +- examples/soil_sample.txt | 15 +++ famodel/anchors/anchor.py | 13 +- famodel/famodel_base.py | 3 +- famodel/helpers.py | 20 ++- famodel/mooring/connector.py | 2 +- famodel/mooring/mooring.py | 79 ++++++++---- famodel/platform/platform.py | 162 ++++++++++++++++-------- famodel/project.py | 106 ++++++++++++---- tests/mooring_ontology_parallels.yaml | 19 ++- tests/platform_ontology.yaml | 3 +- tests/testOntology.yaml | 3 +- tests/test_integrations.py | 9 +- tests/test_moorings.py | 55 +++++--- 17 files changed, 388 insertions(+), 162 deletions(-) create mode 100644 examples/soil_sample.txt diff --git a/examples/OntologySample200m.yaml b/examples/OntologySample200m.yaml index c5cd8ba2..8ba24618 100644 --- a/examples/OntologySample200m.yaml +++ b/examples/OntologySample200m.yaml @@ -144,9 +144,9 @@ mooring_systems: 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, 30 , drag-embedment1, [1,4], 1 ] + - [ semitaut-poly_1, 30 , drag-embedment1, 1, 1 ] - [ semitaut-poly_1, 150 , drag-embedment1, 2, 1 ] - - [ semitaut-poly_1, 270, drag-embedment1, 3, [2,1] ] + - [ semitaut-poly_1, 270, drag-embedment1, 3, 1 ] # Mooring line configurations @@ -210,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 @@ -1542,13 +1565,11 @@ platforms: qtfPath : 'IEA-15-240-RWT-UMaineSemi.12d' # path to the qtf file for the platform fairleads : # list of fairlead coordinates for the platform relative to platform coordinate and 0-degree heading - name: fairlead1 - r: 58 - z: -14 - headings: [30, 150, 270] # headings in degrees for the fairlead (if multiple headings, the fairlead will be repeated for each heading) + 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: 5 - z: -20 + 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 @@ -1649,13 +1670,11 @@ platforms: zFair : -15 fairleads : # list of fairlead coordinates for the platform relative to platform coordinate and 0-degree heading - name: fairlead1 - r: 58 - z: -14 - headings: [30, 150, 270] # headings in degrees for the fairlead (if multiple headings, the fairlead will be repeated for each heading) + 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: 5 - z: -20 + 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 diff --git a/examples/OntologySample600m_shared.yaml b/examples/OntologySample600m_shared.yaml index 8e59f43a..33def23b 100644 --- a/examples/OntologySample600m_shared.yaml +++ b/examples/OntologySample600m_shared.yaml @@ -1375,9 +1375,11 @@ mooring_line_configs: length: 500 # [m] usntretched length of line section - connectorType: triplate - subsections: # double chain section - - - type: chain_155mm + - - mooringFamily: chain + d_nom: 0.1 length: 120 - - - type: chain_155mm + - - mooringFamily: chain + d_nom: 0.1 length: 120 - connectorType: triplate - type: rope # ID of a mooring line section type diff --git a/examples/example_driver.py b/examples/example_driver.py index 60b22c38..f9faada3 100644 --- a/examples/example_driver.py +++ b/examples/example_driver.py @@ -33,7 +33,7 @@ # create project object project = Project(file=dir+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() @@ -41,7 +41,7 @@ #%% Section 2: Project with RAFT print('\nCreating project with RAFT \n') #create project object, automatically create RAFT object (and automatically create moorpy system in the process!) -project = Project(file=ontology_file,raft=True) +project = Project(file=dir+ontology_file,raft=True) # plot in 3d, use moorpy system for mooring and cables, use RAFT for platform, tower, and turbine visuals project.plot3d(fowt=True,draw_boundary=False,boundary_on_bath=False,save=True) diff --git a/examples/example_sharedmoorings.py b/examples/example_sharedmoorings.py index 6b7ed2dc..b2f40083 100644 --- a/examples/example_sharedmoorings.py +++ b/examples/example_sharedmoorings.py @@ -15,13 +15,11 @@ # load in yaml -project = Project(file=dir+filename,raft=False) +project = Project(file=dir+filename, raft=True) -project.getMoorPyArray() - # plot in 2d and 3d project.plot2d() -#project.plot3d(fowt=True) +project.plot3d(fowt=True) -#plt.show() \ No newline at end of file +plt.show() \ No newline at end of file diff --git a/examples/soil_sample.txt b/examples/soil_sample.txt new file mode 100644 index 00000000..70d8fa14 --- /dev/null +++ b/examples/soil_sample.txt @@ -0,0 +1,15 @@ +--- MoorPy Soil Input File --- +nGridX 3 +nGridY 3 + -1901 0 1900 +-1900 mud mud mud + 2 mud rock mud + 1900 mud mud mud +--- SOIL TYPES --- +Class Gamma Su0 k alpha phi UCS Em +(name) (kN/m^3) (kPa) (kPa/m) (-) (deg) (MPa) (MPa) +mud 4.7 2.39 1.41 0.7 - - - +mud_firm 4.7 23.94 2.67 0.7 - - - +rock - - - - - 7 50 +------------------ + diff --git a/famodel/anchors/anchor.py b/famodel/anchors/anchor.py index 2ec40b3e..7acdc6cb 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/famodel_base.py b/famodel/famodel_base.py index cd6b79b5..c21d014c 100644 --- a/famodel/famodel_base.py +++ b/famodel/famodel_base.py @@ -455,7 +455,7 @@ def setPosition(self, r, theta=0, force=False): raise Exception("Can't setPosition of an object that's attached to a higher object unless force=True.") # Store updated position and orientation - if len(r) > len(self.r): # default r is 2D, but can be adjusted to 3D + 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 @@ -491,7 +491,6 @@ def setPosition(self, r, theta=0, force=False): # 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): diff --git a/famodel/helpers.py b/famodel/helpers.py index dff5fae2..cbb41f82 100644 --- a/famodel/helpers.py +++ b/famodel/helpers.py @@ -703,7 +703,8 @@ def getMoorings(lcID, lineConfigs, connectorTypes, pfID, proj): 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 @@ -762,7 +763,7 @@ def getMoorings(lcID, lineConfigs, connectorTypes, pfID, proj): 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 subsub in sub: + for jj,subsub in enumerate(sub): if 'connectorType' in subsub and sublineLast[ii]: cID = subsub['connectorType'] if cID in connectorTypes: @@ -791,6 +792,13 @@ def getMoorings(lcID, lineConfigs, connectorTypes, pfID, proj): 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 @@ -963,9 +971,17 @@ def calc_midpoint(point): 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]) diff --git a/famodel/mooring/connector.py b/famodel/mooring/connector.py index c6506322..ff84922f 100644 --- a/famodel/mooring/connector.py +++ b/famodel/mooring/connector.py @@ -174,5 +174,5 @@ def makeMoorPyLine(self, ms): pointB = self.attached_to[1].mpConn.number # Create a Line for the section in MoorPy system - ms.addLine(self['L'], self['type'], pointA=pointA, pointB=pointB) + 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 0812be9a..498cb5e5 100644 --- a/famodel/mooring/mooring.py +++ b/famodel/mooring/mooring.py @@ -274,7 +274,7 @@ def reposition(self, r_center=None, heading=None, project=None, self.heading = heading else: self.heading = np.degrees(heading) - + #breakpoint() phi = np.radians(90-self.heading) # heading in x-y radian convention [rad] (CONVERTS FROM COMPASS HEADINGS!) # heading 2D unit vector u = np.array([np.cos(phi), np.sin(phi)]) @@ -336,7 +336,7 @@ def reposition(self, r_center=None, heading=None, project=None, else: # otherwise just set the anchor position based on a set spacing (NEED TO UPDATE THE ANCHOR DEPTH AFTER!) if not fairs: xy_loc = self.rB[:2] + self.span*u #r_centerB[:2] + (self.span + rad_fair[1])*u - else: + 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]) @@ -356,6 +356,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() @@ -486,20 +489,21 @@ def createSubsystem(self, case=0, pristine=True, dd=None, ms=None): if not dd: dd = self.dd + # 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 i in self.i_con: + for con in conns: # >>> leah had some checks here that I didn't understand <<< - con = self.getSubcomponent(i) con.makeMoorPyConnector(ms) # Make Lines - for i in self.i_sec: - sec = self.getSubcomponent(i) + for sec in secs: sec.makeMoorPyLine(ms) # this also will connect the Lines to Points else: @@ -512,15 +516,10 @@ def createSubsystem(self, case=0, pristine=True, dd=None, ms=None): lengths = [] types = [] # run through each line section and collect the length and type - for i in self.i_sec: - sec = self.getSubcomponent(i) + for sec in secs: lengths.append(sec['L']) types.append(sec['type']) # list of type names - conns = [] - for i in self.i_con: - conns.append(self.getSubcomponent(i)) - # make the lines and set the points ss.makeGeneric(lengths, types, @@ -536,7 +535,7 @@ def createSubsystem(self, case=0, pristine=True, dd=None, ms=None): startNum = 0 for i in range(startNum,len(ss.pointList)): - conn = self.getSubcomponent(self.i_con[i]) + conn = conns[i] conn.mpConn = ss.pointList[i] conn.mpConn.CdA = conns[i]['CdA'] conn.getProps() @@ -567,7 +566,6 @@ def positionSubcomponents(self): approximate positions relative to the endpoints based on the section lengths.''' - print('positionSubcomponents!!!') # Tabulate the section lengths L = [] @@ -598,19 +596,18 @@ def positionSubcomponents(self): elif isinstance(item, Edge): L.append(item['L']) # save length of section - print(f'There are {n_serial_nodes} serial nodes') # 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, 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 # 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]/Lsum[-1] + r = self.rA + (self.rB-self.rA)*Lsum[j-1]/Lsum[-1] item.setPosition(r) print(f'Set position of subcom {i} to {r[0]:5.0f}, {r[1]:5.0f}, {r[2]:5.0f}') @@ -820,7 +817,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() @@ -837,7 +835,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) @@ -943,7 +941,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 = [] @@ -1065,8 +1063,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] = connList[i] + nd1['subcomponents'][2*i+1] = nd[i] + nd1['subcomponents'][2*i+2] = connList[i+1] # call createSubsystem() to make moorpy subsystem with marine growth if self.shared: @@ -1346,6 +1347,38 @@ def convertSubcomponents(self, subs_list): 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) + 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: + conns.append(sub) + 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 diff --git a/famodel/platform/platform.py b/famodel/platform/platform.py index 4a23a7fe..f9ab1875 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): ''' @@ -85,7 +86,7 @@ def setPosition(self, r, heading=None, degrees=False,project=None): self.phi = np.radians(heading) else: self.phi = heading - # send in cartesian heading to node.setPosition (+ rotations CCW here) + # send in cartesian heading to node.setPosition (+ rotations CCW here) Node.setPosition(self, r, theta=-self.phi) # then also adjust the anchor points @@ -138,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)) @@ -167,49 +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() diff --git a/famodel/project.py b/famodel/project.py index 8337ea26..be34cc98 100644 --- a/famodel/project.py +++ b/famodel/project.py @@ -536,7 +536,7 @@ def loadDesign(self, d, raft=True): reposition=False) anch = self.addAnchor(id=name, dd=ad, mass=mass) - + # attach ends moor.attachTo(anch, end='A') if 'fairlead' in mySys[j]: @@ -2210,15 +2210,16 @@ def plot2d(self, ax=None, plot_seabed=True,plot_bathymetry=True, plot_boundary=T if mooring.ss: # plot with Subsystem if available mooring.ss.drawLine2d(0, ax, color="k", endpoints=False, - Xuvec=[1,0,0], Yuvec=[0,1,0],label='Mooring Line') + Xuvec=[1,0,0], Yuvec=[0,1,0],label='Mooring Line') + elif mooring.parallels: + for i in mooring.i_sec: + sec = mooring.getSubcomponent(i) + if hasattr(sec,'mpLine'): + line = sec.mpLine + line.drawLine2d(0,ax,color="k", + Xuvec=[1,0,0], Yuvec=[0,1,0], + label='Mooring Line') else: # simple line plot - # if len(mooring.subcons_B[0].attachments)>1: - # # there are fairleads, plot from their locations - # for i in mooring.i_sec: - # sec = mooring.getSubcomponent(i) - # ax.plot([sec.rA[0],sec.rB[0]], - # [sec.rA[1],sec.rB[1]]) - # else: ax.plot([mooring.rA[0], mooring.rB[0]], [mooring.rA[1], mooring.rB[1]], 'k', lw=0.5, label='Mooring Line') @@ -2541,6 +2542,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: @@ -2651,12 +2665,12 @@ def getMoorPyArray(self, plt=0, pristineLines=True, cables=True): # create subsystem if pristineLines: - mooring.createSubsystem(pristine=1, ms=self.ms) + mooring.createSubsystem(pristine=True, ms=self.ms) # set location of subsystem for simpler coding ssloc = mooring.ss else: - mooring.createSubsystem(ms=self.ms) + mooring.createSubsystem(pristine=False, ms=self.ms) # set location of subsystem for simpler coding ssloc = mooring.ss_mod @@ -2670,20 +2684,27 @@ def getMoorPyArray(self, plt=0, pristineLines=True, cables=True): # 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, Edge): - anchor.mpAnchor.attachLine(subcom.mpLine.number, att['end']) elif isinstance(subcom, Node): - # The end is a node, eventually could attach it to the anchor if there's an r_rel - pass + # 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 - anchor.mpAnchor.attachLine(ssloc.number, att['end']) + # 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()]): @@ -2749,7 +2770,8 @@ def getMoorPyArray(self, plt=0, pristineLines=True, cables=True): check = 1 # temporary approach to identify shared lines <<< for j in self.anchorList: # j is key (name) of anchor object if mkey in self.anchorList[j].attachments: # check if line has already been put in ms - check = 0 + check = 0 + break if check == 1: # mooring object not in any anchor lists # new shared line # create subsystem for shared line @@ -2813,9 +2835,9 @@ def getMoorPyArray(self, plt=0, pristineLines=True, cables=True): else: # normal serial/subsystem case if ki==0: - rEnd = self.rA + rEnd = mooring.rA else: - rEnd = self.rB + rEnd = mooring.rB # add fairlead point A and attach the line to it point = self.ms.addPoint(1, rEnd) @@ -3875,7 +3897,7 @@ 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) @@ -3914,11 +3936,27 @@ def arrayWatchCircle(self,plot=False, ang_spacing=45, RNAheight=150, atts = [att['obj'] for att in anch.attachments.values()] F1 = [None]*len(atts) for jj,moor in enumerate(atts): + if moor.parallels: + raise Exception('arrayWatchCircle not set up yet to work with parallels') if isinstance(moor.attached_to[0],Anchor): # anchor attached to end A - F1[jj] = moor.ss.fA*DAF + if moor.ss: + F1[jj] = moor.ss.fA*DAF + else: + secs = [] + for c in range(len(moor.subcons_A)): + secs.append(moor.getSubcomponent(moor.i_sec[c])) + F1[jj] = np.max([sec.mpLine.fA*DAF for sec in secs]) else: - F1[jj] = moor.ss.fB*DAF + if moor.ss: + F1[jj] = moor.ss.fB*DAF + else: + secs = [] + for c in range(len(moor.subcons_B)): + secs.append(moor.getSubcomponent(moor.i_sec[-c])) + largest = np.max([np.linalg.norm(sec.fB) for sec in secs]) + F1[jj] = np.max([sec.mpLine.fB for sec in secs])*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])] H = np.hypot(F2[0],F2[1]) # horizontal force @@ -3934,15 +3972,29 @@ def arrayWatchCircle(self,plot=False, ang_spacing=45, RNAheight=150, # 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']) + if moor.ss: + MBLA = float(moor.ss.lineList[0].type['MBL']) + MBLB = float(moor.ss.lineList[-1].type['MBL']) + else: + secsA = []; secsB = [] + for c in range(len(moor.subcons_A)): + secsA.append(moor.getSubcomponent(moor.i_sec[c])) + for c in range(len(moor.subcons_B)): + secsB.append(moor.getSubcomponent(moor.i_sec[-c])) + MBLA = np.min([float(sec['MBL']) for sec in secsA]) + MBLB = np.min([float(sec['MBL']) for sec in secsB]) + # 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 + if moor.ss: + moor.loads['TAmax'] = moor.ss.TA*DAF + moor.loads['TBmax'] = moor.ss.TB*DAF + else: + moor.loads['TAmax'] = np.max([sec.TA for sec in secsA])*DAF + moor.loads['TBmax'] = np.max([sec.TB for sec in secsB])*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)' diff --git a/tests/mooring_ontology_parallels.yaml b/tests/mooring_ontology_parallels.yaml index d8e5eefc..e98647ae 100644 --- a/tests/mooring_ontology_parallels.yaml +++ b/tests/mooring_ontology_parallels.yaml @@ -67,7 +67,7 @@ mooring_systems: keys: [MooringConfigID, heading, anchorType, fairlead] data: - - [ rope_1, 45 , suction_pile1, [4,5] ] + - [ rope_1_bridle, 45 , suction_pile1, [4,5] ] - [ rope_1, 135 , suction_pile1, 2 ] ms3: @@ -89,6 +89,23 @@ mooring_systems: # 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 diff --git a/tests/platform_ontology.yaml b/tests/platform_ontology.yaml index 55d95270..0dae6a53 100644 --- a/tests/platform_ontology.yaml +++ b/tests/platform_ontology.yaml @@ -1089,8 +1089,7 @@ platforms: 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: 5 - z: -20 + 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 diff --git a/tests/testOntology.yaml b/tests/testOntology.yaml index 7da8a431..dca64548 100644 --- a/tests/testOntology.yaml +++ b/tests/testOntology.yaml @@ -1209,8 +1209,7 @@ platforms: 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: 5 - z: -20 + 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 diff --git a/tests/test_integrations.py b/tests/test_integrations.py index a5453c5e..ced4120b 100644 --- a/tests/test_integrations.py +++ b/tests/test_integrations.py @@ -35,9 +35,8 @@ def test_RAFT_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 @@ -46,8 +45,8 @@ def test_lineDesign_integration(project): # 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 index 2afce8b0..5dcc855d 100644 --- a/tests/test_moorings.py +++ b/tests/test_moorings.py @@ -3,8 +3,10 @@ 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 @@ -32,9 +34,9 @@ def test_moor_heading(setup_project): dists = moor.rA[:2]-moor.rB[:2] heading = np.pi/2 - np.arctan2(dists[1], dists[0]) pf = setup_project.platformList['FOWT1'] - assert(heading == np.radians(45+180)) - assert(heading == pf.mooring_headings[0]+pf.phi) - assert(heading == np.radians(moor.heading)) + 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): @@ -50,12 +52,26 @@ def test_fairlead_connection(setup_project): def test_fairlead_position(setup_project): moor = setup_project.mooringList['FOWT1a'] - fl = moor.subcomponents[-1].attachments['FOWT1_F1'] - assert(fl.r==setup_project.mooringList['FOWT1a'].rB) + 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'] - head_fl = np.radians(90-30) - head_pf = np.radians(90)-pf.phi - assert(fl.r==[58*cos(head_fl+head_pf),58*sin(head_fl+head_pf),-14]) + 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'] @@ -78,12 +94,12 @@ def test_end_locs(self): def test_num_sections(setup_project): moor = setup_project.mooringList['FOWT1a'] setup_project.getMoorPyArray() - assert(len(moor.dd['sections'])==len(moor.ss.lineList)) - assert(len(moor.dd['sections'])==2) + 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.dd['connectors'])==3) + assert(len(moor.i_con)==3) def test_shared_connections(setup_project): @@ -100,7 +116,7 @@ def test_shared_flag(setup_project): # - - - -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)) @@ -111,14 +127,15 @@ def test_bridle_setup(bridle_project): 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])) + 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 sub.id in att['obj'].attachments: + if isinstance(att['obj'],Node) and sub.id in att['obj'].attachments: fl_attachment[i] = True assert(all(fl_attachment)) @@ -130,14 +147,14 @@ def test_bridle_end_locs(bridle_project): 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 calculate_midpoint - midpoint = calculate_midpoint(fl_locs) - assert(midpoint==moor.rB) + 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(moor.heading)),np.sin(np.radians(moor.heading))]) + 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(anch_loc==moor.rA) + assert_allclose(anch_loc, moor.rA) From 8d0f950b3b7e5f4ba5ed6dc0f47d47ecee0f205f Mon Sep 17 00:00:00 2001 From: lsirkis Date: Tue, 5 Aug 2025 17:32:46 -0600 Subject: [PATCH 20/34] bug fix examples join path for windows and other systems properly --- examples/example_driver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/example_driver.py b/examples/example_driver.py index f9faada3..a88c1e79 100644 --- a/examples/example_driver.py +++ b/examples/example_driver.py @@ -25,13 +25,13 @@ dir = os.path.dirname(os.path.realpath(__file__)) # 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=dir+ontology_file,raft=False) +project = Project(file=os.path.join(dir,ontology_file),raft=False) # create moorpy system of the array, include cables in the system project.getMoorPyArray(cables=True) # plot in 3d, using moorpy system for the mooring and cable plots From ae40a54f8b54228976a3b610fed789134a8df143 Mon Sep 17 00:00:00 2001 From: lsirkis Date: Tue, 5 Aug 2025 17:54:26 -0600 Subject: [PATCH 21/34] Still trying to fix joining dir & filename for conda CI tests.... --- examples/example_driver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/example_driver.py b/examples/example_driver.py index a88c1e79..29b7b2d3 100644 --- a/examples/example_driver.py +++ b/examples/example_driver.py @@ -25,13 +25,13 @@ dir = os.path.dirname(os.path.realpath(__file__)) # 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=os.path.join(dir,ontology_file),raft=False) +project = Project(file=os.path.join(dir, ontology_file), raft=False) # create moorpy system of the array, include cables in the system project.getMoorPyArray(cables=True) # plot in 3d, using moorpy system for the mooring and cable plots From 20f9dd557958cf950c0eb4f8748e3f1cea95fcf9 Mon Sep 17 00:00:00 2001 From: lsirkis Date: Tue, 5 Aug 2025 18:08:31 -0600 Subject: [PATCH 22/34] hopefully final adjustment to dir for examples... --- examples/example_driver.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/example_driver.py b/examples/example_driver.py index 29b7b2d3..a957caca 100644 --- a/examples/example_driver.py +++ b/examples/example_driver.py @@ -23,15 +23,16 @@ import matplotlib.pyplot as plt dir = os.path.dirname(os.path.realpath(__file__)) +os.chdir(dir) # set yaml file location and name ontology_file = "OntologySample200m.yaml" #%% Section 1: Project without RAFT print('Creating project without RAFT\n') -print(os.getcwd()) + # create project object -project = Project(file=os.path.join(dir, 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=True) # plot in 3d, using moorpy system for the mooring and cable plots From d782f3ca4520366810070e62aef83fb3f5243e01 Mon Sep 17 00:00:00 2001 From: lsirkis Date: Tue, 5 Aug 2025 18:25:07 -0600 Subject: [PATCH 23/34] still working on github CI issues --- examples/example_driver.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/examples/example_driver.py b/examples/example_driver.py index a957caca..ded6150f 100644 --- a/examples/example_driver.py +++ b/examples/example_driver.py @@ -22,9 +22,6 @@ import os import matplotlib.pyplot as plt -dir = os.path.dirname(os.path.realpath(__file__)) -os.chdir(dir) - # set yaml file location and name ontology_file = "OntologySample200m.yaml" From 469598f79e0344dd6aeebc466521ee3a0fbd137b Mon Sep 17 00:00:00 2001 From: lsirkis Date: Tue, 5 Aug 2025 18:28:31 -0600 Subject: [PATCH 24/34] final adjust github CI --- examples/example_driver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/example_driver.py b/examples/example_driver.py index ded6150f..c8d043cc 100644 --- a/examples/example_driver.py +++ b/examples/example_driver.py @@ -39,7 +39,7 @@ #%% Section 2: Project with RAFT print('\nCreating project with RAFT \n') #create project object, automatically create RAFT object (and automatically create moorpy system in the process!) -project = Project(file=dir+ontology_file,raft=True) +project = Project(file=ontology_file,raft=True) # plot in 3d, use moorpy system for mooring and cables, use RAFT for platform, tower, and turbine visuals project.plot3d(fowt=True,draw_boundary=False,boundary_on_bath=False,save=True) From 2c6c796951893982f53cc7c5dfb84a3741a9c81f Mon Sep 17 00:00:00 2001 From: Matt Hall <5151457+mattEhall@users.noreply.github.com> Date: Fri, 8 Aug 2025 12:36:37 -0600 Subject: [PATCH 25/34] Small edit in Project.laodSite to work with different directories --- examples/OntologySample200m.yaml | 2 +- famodel/project.py | 22 ++++++++++++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/examples/OntologySample200m.yaml b/examples/OntologySample200m.yaml index 8ba24618..ee348243 100644 --- a/examples/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] diff --git a/famodel/project.py b/famodel/project.py index be34cc98..79a420b5 100644 --- a/famodel/project.py +++ b/famodel/project.py @@ -154,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 @@ -954,10 +959,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 @@ -969,7 +977,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']) From d6f0515f8cecd14f9a49a292af4e48b5a7b90ad0 Mon Sep 17 00:00:00 2001 From: lsirkis Date: Tue, 12 Aug 2025 15:11:20 -0600 Subject: [PATCH 26/34] misc. updates -- adjust how cable headings are set - now matches moorings, where cable subcomponent headings are absolute in compass headings -- add loads and safety_factors dictionary to Section objects - mooring loads are now stored in Section object dicts insetead of in mooring objects -- mooring.updateTensions() updated to reflect new location of loads -- updated mooring.addMarineGrowth() to properly work with new dd setup -- in Mooring.sections() allow option to input dictionary of subcomponents instead of using i_sec values, useful for cases like marine growth where new sections are added but the design dicitonary property of mooring needs to be the pristine version -- remove cable heading adjustment when updating platform position -- update platform.getWatchCircle to use & store Section loads/safety_factors -- project.loadDesign update how cable headings are loaded in to match moorings -- project.duplicate update to work with new mooring design dictionaries, fairleads, etc -- project.addPlatformMS update to work with new mooring design dicts, fairleads, etc -- project.arrayWatchCircle update to use & store Section loads/safety_factors -- project.unload update to unload fairleads and jtubes -- allow rJtube to be provided in cable description (this is how it was before, then was removed) --- famodel/cables/cable.py | 11 +- famodel/cables/dynamic_cable.py | 2 +- famodel/helpers.py | 4 + famodel/mooring/README.md | 41 ++- famodel/mooring/connector.py | 7 + famodel/mooring/mooring.py | 60 ++-- famodel/ontology/README.md | 89 ++++- famodel/platform/platform.py | 58 ++-- famodel/project.py | 577 ++++++++++++++++++-------------- 9 files changed, 514 insertions(+), 335 deletions(-) diff --git a/famodel/cables/cable.py b/famodel/cables/cable.py index da320b9d..4d0c51a9 100644 --- a/famodel/cables/cable.py +++ b/famodel/cables/cable.py @@ -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,11 +139,11 @@ 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 + headingA = np.pi/2 - self.subcomponents[0].headingA + headingB = np.pi/2 - self.subcomponents[-1].headingB else: - headingA = headings[0] - headingB = headings[1] + headingA = np.pi/2 - headings[0] + headingB = np.pi/2 - headings[1] if not isinstance(self.subcomponents[0].attached_to[0], Jtube): if not rad_fair: @@ -154,7 +154,6 @@ def reposition(self,headings=None,project=None,rad_fair=[]): 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, diff --git a/famodel/cables/dynamic_cable.py b/famodel/cables/dynamic_cable.py index c649094b..84f74c47 100644 --- a/famodel/cables/dynamic_cable.py +++ b/famodel/cables/dynamic_cable.py @@ -80,7 +80,7 @@ def __init__(self, id, dd=None, subsystem=None, rA=[0,0,0], rB=[0,0,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 = self.headingB + np.pi + self.headingA = 0 else: self.headingA = 0 diff --git a/famodel/helpers.py b/famodel/helpers.py index cbb41f82..b20ea24a 100644 --- a/famodel/helpers.py +++ b/famodel/helpers.py @@ -657,6 +657,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: @@ -668,6 +669,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.') @@ -722,6 +724,7 @@ def getMoorings(lcID, lineConfigs, connectorTypes, pfID, proj): config[-1]['type']['name'] = str(ct)+'_'+str(lt['name']) # make EA a float not a string config[-1]['type']['EA'] = float(lt['EA']) + config[-1]['type']['MBL'] = float(lt['MBL']) # set line length config[-1]['L'] = lc['length'] @@ -789,6 +792,7 @@ def getMoorings(lcID, lineConfigs, connectorTypes, pfID, proj): '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'") 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 ff84922f..a646f1e3 100644 --- a/famodel/mooring/connector.py +++ b/famodel/mooring/connector.py @@ -44,6 +44,7 @@ def __init__(self,id, r=[0,0,0], **kwargs): # cost dictionary self.cost = {} + self.getProps() @@ -149,6 +150,12 @@ def __init__(self,id, **kwargs): # 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): diff --git a/famodel/mooring/mooring.py b/famodel/mooring/mooring.py index 498cb5e5..ed77084a 100644 --- a/famodel/mooring/mooring.py +++ b/famodel/mooring/mooring.py @@ -274,7 +274,7 @@ def reposition(self, r_center=None, heading=None, project=None, self.heading = heading else: self.heading = np.degrees(heading) - #breakpoint() + phi = np.radians(90-self.heading) # heading in x-y radian convention [rad] (CONVERTS FROM COMPASS HEADINGS!) # heading 2D unit vector u = np.array([np.cos(phi), np.sin(phi)]) @@ -450,21 +450,20 @@ 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 = [] + # 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']) - return(self.loads['TAmax'],self.loads['TBmax']) + return max(Ts) def createSubsystem(self, case=0, pristine=True, dd=None, ms=None): @@ -528,6 +527,11 @@ def createSubsystem(self, case=0, pristine=True, dd=None, ms=None): 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 @@ -609,7 +613,6 @@ def positionSubcomponents(self): 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) - print(f'Set position of subcom {i} to {r[0]:5.0f}, {r[1]:5.0f}, {r[2]:5.0f}') @@ -653,9 +656,6 @@ def positionSubcomponents(self): # --- Do the positioning --- Lsum = np.cumsum(np.array(L)) - print(f'parallel string ends A and B are at') - print(f'{rA[0]:5.0f}, {rA[1]:5.0f}, {rA[2]:5.0f}') - print(f'{rB[0]:5.0f}, {rB[1]:5.0f}, {rB[2]:5.0f}') for subitem in parallel: if isinstance(subitem, Edge): @@ -666,11 +666,9 @@ def positionSubcomponents(self): if j > 0 and j < n_serial_nodes-1: r = rA + (rB-rA)*Lsum[j]/Lsum[-1] item.setPosition(r) - print(f'Set position of Node {j} to {r[0]:5.0f}, {r[1]:5.0f}, {r[2]:5.0f}') else: print('end of parallel') breakpoint() - print('yep') def mirror(self,create_subsystem=True): ''' Mirrors a half design dictionary. Useful for symmetrical shared mooring lines where @@ -1065,9 +1063,9 @@ def addMarineGrowth(self, mgDict, project=None, idx=None): nd1 = deepcopy(self.dd) nd1['subcomponents'] = [None]*(len(nd)*2+1) for i in range(len(nd)): - nd1['subcomponents'][2*i] = connList[i] - nd1['subcomponents'][2*i+1] = nd[i] - nd1['subcomponents'][2*i+2] = connList[i+1] + 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: @@ -1358,6 +1356,14 @@ def sections(self, dd=None): 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)) @@ -1372,8 +1378,16 @@ def connectors(self, dd=None): # allow option to input dict of subcomponents and pull sections from that if dd: for sub in dd['subcomponents']: - if not 'L' in sub: + 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)) diff --git a/famodel/ontology/README.md b/famodel/ontology/README.md index 6a3e1d1e..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 @@ -308,7 +307,9 @@ 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. -The fairlead connection point for end A and end B is represented with an integer value in fairleadA and fairlead respectively. This integer value maps to an index 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. 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. +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: @@ -323,7 +324,7 @@ array_mooring: line_data : - [ semitaut-poly_1 , anch1, fowt1, None, 1] - [ semitaut-poly_1 , anch2, fowt3, None, 2] - - [ semitaut-poly_2 , fowt1, f2, 2, 3] + - [ semitaut-poly_bridle , fowt1, f2, 2, [3,4]] ``` ### Array Cables @@ -331,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), 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 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 the center of the platform. +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. @@ -394,8 +395,11 @@ By default, the format here follows that used by 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 relative positions can be listed for each fairlead, or the fairlead radius, depth, 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. -rFair is the fairlead radius, zFair is the fairlead depth with respect to the platform depth. 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. + - *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 @@ -406,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 @@ -446,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 @@ -487,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 @@ -517,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 @@ -668,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 @@ -679,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] @@ -705,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 @@ -718,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/platform.py b/famodel/platform/platform.py index f9ab1875..c37acd23 100644 --- a/famodel/platform/platform.py +++ b/famodel/platform/platform.py @@ -109,11 +109,11 @@ def setPosition(self, r, heading=None, degrees=False,project=None): cab = self.attachments[att]['obj'] # 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]] + # 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): @@ -281,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. @@ -324,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() @@ -375,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) + for sec in moor.sections(): + sec.safety_factors['tension'] = sec['type']['MBL']/sec.loads['Tmax'] + sec.safety_factors['analysisType'] = 'quasi-static (MoorPy)' + sec.loads['info'] = f'determined from platform.getWatchCircle() with DAF of {DAF}' + if moor_seabed_disturbance: + 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() @@ -435,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 79a420b5..43237870 100644 --- a/famodel/project.py +++ b/famodel/project.py @@ -737,6 +737,11 @@ def loadDesign(self, d, raft=True): 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'] @@ -747,13 +752,13 @@ def loadDesign(self, d, raft=True): # 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(90-cab['headingA']) # heading only if not suspended + 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 + 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'] @@ -772,10 +777,10 @@ def loadDesign(self, d, raft=True): self.depth, rho_water=self.rho_water, g=self.g) - # rJTubeB = dyn_cable_configs[dyn_cabB]['rJTube'] - # Bcondd['rJTube'] = rJTubeB + 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) @@ -809,14 +814,14 @@ def loadDesign(self, d, raft=True): raise Exception(f'AttachB {arrayCableInfo[i]["AttachB"]} for array cable {i} does not match any platforms or appendages.') # reposition the cable - self.cableList[cableID].reposition(project=self) #, rad_fair=[rJTubeA,rJTubeB]) + self.cableList[cableID].reposition(project=self, rad_fair=[rJTubeA,rJTubeB]) # create any cables from cables section (this is a more descriptive cable format that may have routing etc) if cableInfo: for cab in cableInfo: - # rJTubeA = None; rJTubeB = None + 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 @@ -826,7 +831,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: @@ -834,17 +842,19 @@ 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 + rJTubeA = dyn_cable_configs[dyn_cabA]['rJTube'] + Acondd['rJTube'] = rJTubeA # append to cables list dd['cables'].append(Acondd) @@ -872,10 +882,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']) + Bcondd['headingB'] = np.radians(cab['endB']['heading']) + B_phi - # rJTubeB = dyn_cable_configs[dyn_cabB]['rJTube'] - # Bcondd['rJTube'] = rJTubeB + rJTubeB = dyn_cable_configs[dyn_cabB]['rJTube'] + Bcondd['rJTube'] = rJTubeB # append to cables list dd['cables'].append(Bcondd) # append to joints list @@ -911,7 +921,7 @@ def loadDesign(self, d, raft=True): raise Exception(f"AttachB {cab['endB']['attachID']} for cable {cab['name']} does not match any platforms or appendages.") # reposition the cable - self.cableList[cableID].reposition(project=self) #, rad_fair=[rJTubeA,rJTubeB]) + self.cableList[cableID].reposition(project=self, rad_fair=[rJTubeA,rJTubeB]) for pf in self.platformList.values(): pf.setPosition(pf.r, project=self) @@ -1645,7 +1655,7 @@ def addFairlead(self, id=None, platform=None, r_rel=[0,0,0], # create an id if needed if id == None: if platform != None: - id = platform.id + len(platform.attachments) + id = platform.id + str(len(platform.attachments)) # create fairlead object fl = Fairlead(id=id) @@ -3604,47 +3614,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) @@ -3695,7 +3793,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 @@ -3718,53 +3816,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] @@ -3871,7 +3934,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, @@ -3947,35 +4010,10 @@ 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 moor.parallels: - raise Exception('arrayWatchCircle not set up yet to work with parallels') - if isinstance(moor.attached_to[0],Anchor): - # anchor attached to end A - if moor.ss: - F1[jj] = moor.ss.fA*DAF - else: - secs = [] - for c in range(len(moor.subcons_A)): - secs.append(moor.getSubcomponent(moor.i_sec[c])) - F1[jj] = np.max([sec.mpLine.fA*DAF for sec in secs]) - else: - if moor.ss: - F1[jj] = moor.ss.fB*DAF - else: - secs = [] - for c in range(len(moor.subcons_B)): - secs.append(moor.getSubcomponent(moor.i_sec[-c])) - largest = np.max([np.linalg.norm(sec.fB) for sec in secs]) - F1[jj] = np.max([sec.mpLine.fB for sec in secs])*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 @@ -3986,39 +4024,16 @@ def arrayWatchCircle(self,plot=False, ang_spacing=45, RNAheight=150, # get tensions on mooring line for j, moor in enumerate(self.mooringList.values()): - if moor.ss: - MBLA = float(moor.ss.lineList[0].type['MBL']) - MBLB = float(moor.ss.lineList[-1].type['MBL']) - else: - secsA = []; secsB = [] - for c in range(len(moor.subcons_A)): - secsA.append(moor.getSubcomponent(moor.i_sec[c])) - for c in range(len(moor.subcons_B)): - secsB.append(moor.getSubcomponent(moor.i_sec[-c])) - MBLA = np.min([float(sec['MBL']) for sec in secsA]) - MBLB = np.min([float(sec['MBL']) for sec in secsB]) - - # 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) - if moor.ss: - moor.loads['TAmax'] = moor.ss.TA*DAF - moor.loads['TBmax'] = moor.ss.TB*DAF - else: - moor.loads['TAmax'] = np.max([sec.TA for sec in secsA])*DAF - moor.loads['TBmax'] = np.max([sec.TB for sec in secsB])*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 - if moor_seabed_disturbance: - lBot = 0 - for line in moor.ss.lineList: - lBot += line.LBot - lBots[j] = max(lBots[j], lBot) + lBot = 0 + moor.updateTensions(DAF=DAF) + for sec in moor.sections(): + sec.safety_factors['tension'] = sec['type']['MBL']/sec.loads['Tmax'] + sec.safety_factors['analysisType'] = 'quasi-static (MoorPy)' + sec.loads['info'] = f'determined from arrayWatchCircle() with DAF of {DAF}' + if moor_seabed_disturbance: + lBot += sec.mpLine.LBot + lBots[j] = max(lBots[j], lBot) + # get tensions and curvature on cables for j,cab in enumerate(self.cableList.values()): @@ -4032,7 +4047,7 @@ def arrayWatchCircle(self,plot=False, ang_spacing=45, RNAheight=150, 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: + if CminTenSF[j][jj] is None or CminTenSF[j][jj]>CMTSF: CminTenSF[j][jj] = deepcopy(CMTSF) dc.loads['TAmax'] = dc.ss.TA*DAF dc.loads['TBmax'] = dc.ss.TB*DAF @@ -4044,15 +4059,6 @@ def arrayWatchCircle(self,plot=False, ang_spacing=45, RNAheight=150, 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,moor.attached_to[0].id, moor.attached_to[1].id, headA,headB,int(0)]) + if fairleads: + # append mooring line to array_moor section + arrayMoor.append([current_config,moor.attached_to[0].id, moor.attached_to[1].id, 'None',flB]) + else: + # append mooring line to array_moor section + arrayMoor.append([current_config,moor.attached_to[0].id, moor.attached_to[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: @@ -4310,19 +4360,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] + @@ -4368,12 +4406,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 @@ -4384,48 +4422,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} @@ -4450,6 +4512,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 = {} @@ -4493,6 +4559,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'] @@ -4558,9 +4633,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 @@ -4581,9 +4658,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}) @@ -4599,11 +4680,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, From 6ffec518d6a9069d6c941f8dc66358e5d855bc7d Mon Sep 17 00:00:00 2001 From: lsirkis Date: Tue, 12 Aug 2025 15:34:42 -0600 Subject: [PATCH 27/34] small loadDesign bug fix --- famodel/project.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/famodel/project.py b/famodel/project.py index 43237870..cf610569 100644 --- a/famodel/project.py +++ b/famodel/project.py @@ -757,8 +757,9 @@ def loadDesign(self, d, raft=True): # this is a suspended cable - add 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'] @@ -776,9 +777,9 @@ 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(arrayCableInfo[i]['headingB']) + B_phi dd['cables'].append(Bcondd) @@ -853,8 +854,9 @@ def loadDesign(self, d, raft=True): # add headingA Acondd['headingA'] = np.radians(cab['endA']['heading']) + A_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 # append to cables list dd['cables'].append(Acondd) @@ -883,9 +885,9 @@ def loadDesign(self, d, raft=True): g=self.g) # add headingB Bcondd['headingB'] = np.radians(cab['endB']['heading']) + B_phi - - 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 # append to cables list dd['cables'].append(Bcondd) # append to joints list From fe178a78ad9abe1c7c1b7152d1e16f4b0727ef0a Mon Sep 17 00:00:00 2001 From: lsirkis Date: Tue, 19 Aug 2025 18:03:50 -0600 Subject: [PATCH 28/34] addCablesConnections bug fix, helpers bug fix, connectors pointProps adjust -- bug fix project.addCablesConnections for calculating cable headings to adjust to updated methods -- helpers head_adjust adjust to keep headings in compass headings -- connectors adjust cost function use of pointProps (bug fix) -- add images for readmes -- add some comments in cable.py --- famodel/cables/cable.py | 4 ++++ famodel/helpers.py | 29 ++++++++++++++++++++---- famodel/images/fairleads_and_jtubes.png | Bin 0 -> 24627 bytes famodel/images/parallel_sections.png | Bin 0 -> 59579 bytes famodel/mooring/connector.py | 25 ++++++++++++++------ famodel/project.py | 16 ++++++------- 6 files changed, 55 insertions(+), 19 deletions(-) create mode 100644 famodel/images/fairleads_and_jtubes.png create mode 100644 famodel/images/parallel_sections.png diff --git a/famodel/cables/cable.py b/famodel/cables/cable.py index 4d0c51a9..5c07ef9a 100644 --- a/famodel/cables/cable.py +++ b/famodel/cables/cable.py @@ -139,11 +139,15 @@ 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: + # convert headings to unit circle headingA = np.pi/2 - self.subcomponents[0].headingA headingB = np.pi/2 - self.subcomponents[-1].headingB else: + # 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 isinstance(self.subcomponents[0].attached_to[0], Jtube): if not rad_fair: diff --git a/famodel/helpers.py b/famodel/helpers.py index b20ea24a..5b4ab8e0 100644 --- a/famodel/helpers.py +++ b/famodel/helpers.py @@ -10,6 +10,7 @@ import moorpy as mp from moorpy.helpers import loadPointProps, getPointProps import shapely as sh +from famodel.turbine.turbine import Turbine def cart2pol(x, y): @@ -232,7 +233,7 @@ def head_adjust(att,heading,rad_buff=np.radians(30),endA_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 @@ -249,7 +250,7 @@ def head_adjust(att,heading,rad_buff=np.radians(30),endA_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,10 +261,11 @@ def head_adjust(att,heading,rad_buff=np.radians(30),endA_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: @@ -1307,6 +1309,25 @@ def gothroughlist(dat): # return cleaned dictionary return(info) +''' +def createRAFTDict(project): + # Create a RAFT dictionary from a project class to create RAFT model + rd = {'keys':['ID', 'turbineID', 'platformID', 'mooringID', 'x_location', 'y_location'], + 'data':[]} + for pf in project.platformList.values(): + for att in pf.attachments.values(): + if isinstance(att['obj'],Turbine): + turb = att['obj'] + break + rd.append(pf.id, turb.dd['type'], pf.dd['type'], 0, pf.r[0], pf.r[1]) + 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 + + for tt in project.turbineType + + 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 0000000000000000000000000000000000000000..a9289fbfafda29c43e4ed6f59c891a5022f3b223 GIT binary patch literal 24627 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 literal 0 HcmV?d00001 diff --git a/famodel/images/parallel_sections.png b/famodel/images/parallel_sections.png new file mode 100644 index 0000000000000000000000000000000000000000..332ddaa102d40508eb68dfd9b92afb1abbd0a70b GIT binary patch literal 59579 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 literal 0 HcmV?d00001 diff --git a/famodel/mooring/connector.py b/famodel/mooring/connector.py index a646f1e3..558bfdd8 100644 --- a/famodel/mooring/connector.py +++ b/famodel/mooring/connector.py @@ -100,8 +100,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'] @@ -115,13 +118,21 @@ 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()) diff --git a/famodel/project.py b/famodel/project.py index de04f22f..fda4dfc3 100644 --- a/famodel/project.py +++ b/famodel/project.py @@ -2026,12 +2026,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 = [] @@ -2127,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) @@ -3202,6 +3201,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) From f50ec161039e892269aef5bcce4a164c02e5f825 Mon Sep 17 00:00:00 2001 From: lsirkis Date: Tue, 19 Aug 2025 18:13:32 -0600 Subject: [PATCH 29/34] Fix circular import --- famodel/helpers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/famodel/helpers.py b/famodel/helpers.py index 5b4ab8e0..d5046df4 100644 --- a/famodel/helpers.py +++ b/famodel/helpers.py @@ -10,7 +10,6 @@ import moorpy as mp from moorpy.helpers import loadPointProps, getPointProps import shapely as sh -from famodel.turbine.turbine import Turbine def cart2pol(x, y): From e0213b6365ea98365c46db048309f9bad80bfd98 Mon Sep 17 00:00:00 2001 From: lsirkis Date: Tue, 26 Aug 2025 11:18:42 -0600 Subject: [PATCH 30/34] Add option to switch sides in cable routing --- famodel/helpers.py | 29 ++++++++++++++++++----------- famodel/project.py | 23 ++++++++++++++++++----- 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/famodel/helpers.py b/famodel/helpers.py index d5046df4..bdeefa6c 100644 --- a/famodel/helpers.py +++ b/famodel/helpers.py @@ -223,7 +223,7 @@ def check_headings(m_headings,c_heading,rad_buff): return([]) -def head_adjust(att,heading,rad_buff=np.radians(30),endA_dir=1): +def head_adjust(att,heading,rad_buff=np.radians(30),endA_dir=1, adj_dir=1): ''' function to adjust heading of cable based on angle buffer from mooring lines @@ -238,6 +238,10 @@ def head_adjust(att,heading,rad_buff=np.radians(30),endA_dir=1): 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 ------- @@ -269,7 +273,7 @@ def head_adjust(att,heading,rad_buff=np.radians(30),endA_dir=1): # if the headings interfere, adjust them by angle buffer for mhead in interfere_h: ang_diff_dir = np.sign(headnew - mhead) if headnew != mhead else 1 - headnew = mhead - rad_buff*endA_dir*ang_diff_dir #headnew + np.sign(ang_diff)*(rad_buff - abs(ang_diff))*endA_dir + headnew = mhead - adj_dir*rad_buff*endA_dir*ang_diff_dir #headnew + np.sign(ang_diff)*(rad_buff - abs(ang_diff))*endA_dir interfere_hi = check_headings(attheadings,headnew,rad_buff) for i in interfere_hi: # try rotating other way @@ -961,7 +965,7 @@ def attachFairleads(moor, end, platform, fair_ID_start=None, fair_ID=None, fair_ return(fairs) def calc_heading(pointA, pointB): - '''calculate a heading from points, if pointA or pointB is a list of points, + '''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) @@ -992,7 +996,7 @@ def calc_midpoint(point): 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 = [] @@ -1308,24 +1312,27 @@ 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 = {'keys':['ID', 'turbineID', 'platformID', 'mooringID', 'x_location', 'y_location'], - 'data':[]} + 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'] + turb = att['obj'].dd['type'] break - rd.append(pf.id, turb.dd['type'], pf.dd['type'], 0, pf.r[0], pf.r[1]) + 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 - for tt in project.turbineType + rd['turbines'] = project.turbineTypes + rd['platforms'] = project.platformTypes return rd -''' + def getFromDict(dict, key, shape=0, dtype=float, default=None, index=None): ''' diff --git a/famodel/project.py b/famodel/project.py index fba77ca6..3056ac41 100644 --- a/famodel/project.py +++ b/famodel/project.py @@ -1942,7 +1942,7 @@ def addCablesConnections(self,connDict,cableType_def='dynamic_cable_66',oss=Fals substation_r=[None],ss_id=200,id_method='location', keep_old_cables=False, connect_ss=True, cableConfig=None, configType=0,heading_buffer=30, - route_anchors=True): + route_anchors=True, adj_dir=1): '''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 @@ -2098,11 +2098,24 @@ def addCablesConnections(self,connDict,cableType_def='dynamic_cable_66',oss=Fals # consider mooring headings from both ends if close enough pfsp = np.linalg.norm(attA.r-attB.r) if pfsp-2*attA.rFair < msp+dc0s: - headingA = head_adjust([attA,attB],headingA,rad_buff=rad_buff) - headingB = head_adjust([attB,attA],headingB,rad_buff=rad_buff,endA_dir=-1) + headingA = head_adjust([attA,attB], + headingA, + rad_buff=rad_buff, + adj_dir=adj_dir) + headingB = head_adjust([attB,attA], + headingB, + rad_buff=rad_buff, + endA_dir=-1, + adj_dir=adj_dir) else: - headingA = head_adjust([attA],headingA,rad_buff=rad_buff) - headingB = head_adjust([attB],headingB,rad_buff=rad_buff) + headingA = head_adjust([attA], + headingA, + rad_buff=rad_buff, + adj_dir=adj_dir) + headingB = head_adjust([attB], + headingB, + rad_buff=rad_buff, + adj_dir=adj_dir) heads = [headingA,headingB] # reposition cable From 7ab71cc639dcc22b84f2e6702d879bd6a6dcd684 Mon Sep 17 00:00:00 2001 From: lsirkis Date: Mon, 29 Sep 2025 11:08:43 -0600 Subject: [PATCH 31/34] Add interpolation between cable designs to adjust for depth; - add multiple cable designs for range of depths and FAModel will now determine suitable cable design options and interpolate for new depths more efficient load/sf storage for connectors§ions --- famodel/cables/cable.py | 83 ++++++++++- famodel/cables/dynamic_cable.py | 3 + famodel/helpers.py | 238 +++++++++++++++++++++++++------- famodel/mooring/connector.py | 3 + famodel/mooring/mooring.py | 74 +++++++--- famodel/platform/platform.py | 12 +- famodel/project.py | 89 ++++-------- 7 files changed, 360 insertions(+), 142 deletions(-) diff --git a/famodel/cables/cable.py b/famodel/cables/cable.py index 5c07ef9a..ef504409 100644 --- a/famodel/cables/cable.py +++ b/famodel/cables/cable.py @@ -10,6 +10,7 @@ from famodel.cables.static_cable import StaticCable 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 = {} @@ -185,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 @@ -201,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 @@ -272,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['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/dynamic_cable.py b/famodel/cables/dynamic_cable.py index 84f74c47..59f6f9e3 100644 --- a/famodel/cables/dynamic_cable.py +++ b/famodel/cables/dynamic_cable.py @@ -110,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/helpers.py b/famodel/helpers.py index 9da9d176..55f9b243 100644 --- a/famodel/helpers.py +++ b/famodel/helpers.py @@ -290,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() @@ -321,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:] @@ -358,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): @@ -1243,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') diff --git a/famodel/mooring/connector.py b/famodel/mooring/connector.py index 558bfdd8..5bf5eec6 100644 --- a/famodel/mooring/connector.py +++ b/famodel/mooring/connector.py @@ -38,6 +38,9 @@ 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 = {} diff --git a/famodel/mooring/mooring.py b/famodel/mooring/mooring.py index 25a53de0..e90860f6 100644 --- a/famodel/mooring/mooring.py +++ b/famodel/mooring/mooring.py @@ -24,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: @@ -43,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 @@ -211,7 +205,7 @@ def update(self, dd=None): def setSectionLength(self, L, i): '''Sets length of section, including in the subdsystem if there is one.''' - sec = self.getSubcomponents(self.i_sec[i]) + 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 @@ -229,7 +223,7 @@ def setSectionDiameter(self, d, i): def setSectionType(self, lineType, i): '''Sets lineType of section, including in the subdsystem if there is one.''' - sec = self.getSubcomponents(self.i_sec[i]) + sec = self.getSubcomponent(self.i_sec[i]) # set type dict in dd (which is also Section/subcomponent) sec['type'] = lineType @@ -325,8 +319,7 @@ def reposition(self, r_center=None, heading=None, project=None, else: print('Warning: depth of mooring line, anchor, and subsystem must be updated manually.') - self.setEndPosition(np.hstack([self.rB[:2] + self.span*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 if no fairlead objects @@ -457,6 +450,7 @@ def updateTensions(self, DAF=1): ''' Gets tensions from subsystem and updates the max tensions dictionary if it is larger than a previous tension ''' Ts = [] + Tc = [] # get tensions for each section for sec in self.sections(): if not 'Tmax' in sec.loads: @@ -465,9 +459,51 @@ def updateTensions(self, DAF=1): 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 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, ms=None): ''' Create a subsystem for a line configuration from the design dictionary @@ -512,8 +548,6 @@ def createSubsystem(self, case=0, pristine=True, dd=None, ms=None): 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 = [] diff --git a/famodel/platform/platform.py b/famodel/platform/platform.py index c37acd23..c4ae0739 100644 --- a/famodel/platform/platform.py +++ b/famodel/platform/platform.py @@ -394,13 +394,13 @@ def getWatchCircle(self, plot=0, ang_spacing=45, RNAheight=150, for j,moor in enumerate(moorings): lBot = 0 moor.updateTensions(DAF=DAF) - for sec in moor.sections(): - sec.safety_factors['tension'] = sec['type']['MBL']/sec.loads['Tmax'] - sec.safety_factors['analysisType'] = 'quasi-static (MoorPy)' - sec.loads['info'] = f'determined from platform.getWatchCircle() with DAF of {DAF}' - if moor_seabed_disturbance: + 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) + lBots[j] = max(lBots[j], lBot) # get tensions, sag, and curvature on cable for j,cab in enumerate(dcs): diff --git a/famodel/project.py b/famodel/project.py index a624153f..c94425af 100644 --- a/famodel/project.py +++ b/famodel/project.py @@ -1906,36 +1906,6 @@ def addJtube(self, id=None, platform=None, r_rel=[0,0,0], # return fairlead object return(jt) - - - def cableDesignInterpolation(self,depth,cables): - '''Interpolates between dynamic cable designs for different depths to produce - a design for the given depth - ''' - # 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]) - - - return(newdd) @@ -2054,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'] = [] @@ -2098,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']) @@ -2288,7 +2262,7 @@ def plot2d(self, ax=None, plot_seabed=True,plot_bathymetry=True, plot_boundary=T 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", endpoints=False, + line.drawLine2d(0, ax, color="self", Xuvec=[1,0,0], Yuvec=[0,1,0],label=labs[i]) else: # simple line plot @@ -2736,7 +2710,6 @@ def getMoorPyArray(self, plt=0, pristineLines=True, cables=True): # create subsystem if pristineLines: - mooring.createSubsystem(pristine=True, ms=self.ms) # set location of subsystem for simpler coding @@ -4028,15 +4001,17 @@ def arrayWatchCircle(self,plot=False, ang_spacing=45, RNAheight=150, 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() @@ -4069,40 +4044,30 @@ def arrayWatchCircle(self,plot=False, ang_spacing=45, RNAheight=150, 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()): lBot = 0 moor.updateTensions(DAF=DAF) - for sec in moor.sections(): - sec.safety_factors['tension'] = sec['type']['MBL']/sec.loads['Tmax'] - sec.safety_factors['analysisType'] = 'quasi-static (MoorPy)' - sec.loads['info'] = f'determined from arrayWatchCircle() with DAF of {DAF}' - if moor_seabed_disturbance: + moor.updateSafetyFactors(info=info) + if moor_seabed_disturbance: + for sec in moor.sections(): lBot += sec.mpLine.LBot - lBots[j] = max(lBots[j], 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 CminTenSF[j][jj] is None 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: From 52908f8bc84cd1ea83b3078ddcf8b44254b4f687 Mon Sep 17 00:00:00 2001 From: lsirkis Date: Mon, 29 Sep 2025 11:24:21 -0600 Subject: [PATCH 32/34] small bug fix cable safety factor update --- famodel/cables/cable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/famodel/cables/cable.py b/famodel/cables/cable.py index ef504409..bbbe540c 100644 --- a/famodel/cables/cable.py +++ b/famodel/cables/cable.py @@ -347,7 +347,7 @@ def updateSafetyFactors(self, key='tension', load='Tmax', prop='MBL', """ for dc in self.dynamicCables(): - dc.safety_factors[key] = dc.dd['type'][prop]/dc.loads[load] + dc.safety_factors[key] = dc.dd['cable_type'][prop]/dc.loads[load] dc.safety_factors['info'] = info From 6d6587123a3c96345754d2fe026b1bd831456cf9 Mon Sep 17 00:00:00 2001 From: Matt Hall <5151457+mattEhall@users.noreply.github.com> Date: Tue, 30 Sep 2025 21:41:56 -0600 Subject: [PATCH 33/34] FAD readme update --- README.md | 92 ++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 60 insertions(+), 32 deletions(-) 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 From 63c9b67b5661b5c59c8f71fbf5885a9f067550d9 Mon Sep 17 00:00:00 2001 From: Matt Hall <5151457+mattEhall@users.noreply.github.com> Date: Tue, 30 Sep 2025 21:42:29 -0600 Subject: [PATCH 34/34] bringing in the core design tools --- famodel/design/CableDesign.py | 1312 ++++++++++++ famodel/design/CableLayout_functions.py | 1034 ++++++++++ famodel/design/LineDesign.py | 2181 ++++++++++++++++++++ famodel/design/LinearSystem.py | 2083 +++++++++++++++++++ famodel/design/fadsolvers.py | 1857 +++++++++++++++++ famodel/design/layout.py | 2485 +++++++++++++++++++++++ famodel/design/layout_helpers.py | 1177 +++++++++++ 7 files changed, 12129 insertions(+) create mode 100644 famodel/design/CableDesign.py create mode 100644 famodel/design/CableLayout_functions.py create mode 100644 famodel/design/LineDesign.py create mode 100644 famodel/design/LinearSystem.py create mode 100644 famodel/design/fadsolvers.py create mode 100644 famodel/design/layout.py create mode 100644 famodel/design/layout_helpers.py 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 + + + + + + + + + + +