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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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