Base Extensions#
Note
The Base-extensions module is a commercially available extension for DAVE.
This module provides a framework for easy making of larger custom nodes. This is useful when modelling many very similar assets such as barges, cranes spreaderbeams, etc.
This module provides means to create the structure of these things in a node and then read the actual properties from a .csv datafile. The different assets can then be loaded from different .csv files. Nodes like this are called super-elements.
Contents
Super elements (frame / rigidbody based) base classes
Standard nodes in super elements (super-element properties) and their tables in .csv file
Attachment types and points
Exposed properties of attachments
Data tables in .csv files
Writing the python backends
Super-Elements#
Super elements are nodes containing a number of other nodes, possibly combined with some custom defined logic.
The properties of some of these nodes are read from a .csv data-file. Other nodes can be set by the user, these are called ‘attachments’.
Assume you have a large number of islands. And you want to have a database of these islands in DAVE. In that case we can make a super-element that can use to model islands. Each island will get its own .csv file containing the particulars of that island.
Islands can have different sizes, shapes and may even be floating.
Besides that many things can optionally be present on the island. These do not really belong to the island, but are often there. So we want to give the user a option to easily add/remove them by (un)ticking a box:
In DAVE this super-element would look as follows:
Frame (island)
visuals
zero or more buoyancy shapes (the first island floats)
zero or more weight components
zero or more attachment points for swings
This first category are the nodes that are always there. These are called the “super-element properties”. The number of these nodes and their properties are defined in a .csv file. The user only has to select which .csv file to use.
On top of that there are attachments:
Trees
Lighthouse
Swings
Monkeys
The attachments are things that are optionally there. They are also defined in the .csv file but the user can choose to enable or disable them. We can also give the user the ability to change some of the properties of these attached nodes (exposed properties).
Super-element properties#
The super-elements properties are just ordinary nodes.
The types of nodes that can be present in a super-element are defined in the class. The number of nodes and their properties are obtained from the .csv file of the island.
Available super-element properties#
The following classes are included in this module:
Class |
Fields |
---|---|
SEP_Bollard |
|
SEP_Buoyancy |
|
SEP_ContactMesh |
|
SEP_Point |
|
SEP_RoundBarPoint |
|
SEP_Tank |
|
SEP_Visual |
|
SEP_Weight |
|
SEP_WindAreaPoint |
|
Attachments#
Attachments are nodes that can be attached to a super-element.
Attachments are a single node (but that node can be an Component)
To attach an attachment you need two things
An attachment point
An attachment-type
Say we want to plant some trees on the island, and all trees are equal.
We can then make a single attachement-type
for a Tree
. In this case the attachment will be a visual.
To specify where the trees can be planted, we create multiple attachment points
. Each of these points gets a name and at each of these points a tree can be spawned by the user.
Attachment points can accept more than a single attachment type. In the GUI the user gets a drop-down box for each attachment point in which one of the possible attachment types can be activated.
Attachment-points#
These define what can be attached where. They are defined in the .csv file as follows (Using the Table (*) definition):
*Attachments
#
# Name, Target, posx, posy, posz, rotx, roty, rotz, attachments
Monkey 1, self , 3 , 3 , 3, , 0 , 0 , 0 , Small monkey, Big monkey
Monkey 2, self , 2 , -3.5 , 5, , 0 ,10 , 0 , Small monkey, Big monkey
Palm tree center, self , 0 , 0 , 2 , 0 , 0 , 0 , Palmtree
Another point, self , 2 , 0 , 2 , 0 , 0 , 0 , Palmtree, Small monkey, Big monkey
name: name of the point [unique].
The name shall be unique and shall also not be the name of an already existing node.target: the node that the attachment needs to be attached to.
Use “self” to attach to the main node
Use “none” to attach to nothing
Use the relative name of the node for SE-properties created in the super-element. For example crane/boom.
posx, posy, posx, rotx, roty, rotz: define the location and rotation under which the attachment should be attached. For target=none these are global coordinate, otherwise they are local.
attachments: This is a comma-separated list of all possible attachment types (names) that can be attached to this point. These names need to match one of the defined attachment type names (next).
Attachment types#
These define WHAT can be attached. These are also defined in the .csv file
The definition of an attachment type is as follows (using the Section (@) definition):
@ATT: Small monkey # This is the NAME of an attachment type,
# @ATT: signals that this section is an attachment
class, Visual # This is the node-class. In this case the attachment is a "Visual"
path, res: monkey.obj # These are properties of the node. All properties are supported.
scale, 0.5,0.5,0.5 # this evaluates to node.scale = (0.5, 0.5, 0.5)
The first line is @ATT: <name>
where <name>
is the name of the attachment type. This name is unique.
Below that are a number of “settings”. They are in the following form: <setting>
, <value>
The only mandatory setting is class
which defines the type of node that this attachment is made of.
The other settings are the properties that need to be applied to that node. In this example
path, res: monkey.obj
sets the .path property of the created Visual to res: monkey.obj
.
scale, 0.5,0.5,0.5
sets the .scale property to (0.5, 0.5, 0.5)
All together this means that the following is done if this attachment-type is attached:
create a Visual
Attach it to the attachment point by setting its parent property
set its path property to res: monkey.obj
set its scale property to (0.5, 0.5, 0.5)
API - Attaching an attachment#
Now that we have attachment-types and attachment-points the hard work is done and we can attach something. Attaching something is easy:
node.attach(WHAT, WHERE)
For example:
s['Monkey Island'].attach(what = 'Palmtree', where = 'Palm tree center')
This creates
a dedicated
Frame
at the position of the attachment-point.the prescribed
Visual
node on that frame
and then sets the properties of the created visual as defined in the attachment-type.
Exposing properties of attached items#
The use may need to adjust one or more of the properties of an attached node. Because the attached nodes are managed by the super-element the user can not do that directly.
So we need to expose those properties through the super-element.
Exposing properties is done by including a line “exposed” in the Attachment type:
@ATT: Palmtree # This is the NAME of an attachement type,
class, Visual # This is the node-class. In this case
path, res: palmtree.obj # These are properties of the node
expose, scale, offset
expose, scale, offset # Expose scale and offset properties to user
API#
Once exposed, the exposed properties of the attachment can be get/set using:
a = b.attachment_points['Stability pontoon on ps']
x = a.get_exposed('x')
a.set_exposed('x',150)
assert a.get_exposed('x') == 150
The exposed properties are managed by the super-element and thus written to python when generating the model code.
To get a list of exposed properties do:
a.active_type.exposed_properties
Attachment classes#
The line
class, Visual
defines that the attachment is a “Visual”-node.
All standard DAVE nodes are supported as well as any node registered in the DAVE_Additional_Runtime_Modules
dictionary.
The fact that Components
are supported makes its possible to use other DAVE models as attachment.
Example of a component:
@ATT: Stability pontoon 15x3x4 # This is the NAME of an attachement type, @ATT: signals that this section is an attachment
class, Component # This is the node-class. In this case the attachement is a "Component"
path, res: SP/pontoon_15x3x4.dave , # These are properties of the node. All properties are supported.
z, -2.5 , # place the component at z=-2 below the connection point
Passing additional arguments to the Node constructor#
Some node classes may need additional arguments to be passed to the constructor. For example a Suspended4PLoad
requires information about the prongs (points) before it can be created.
When attachments are activated, DAVE scans for properties that can be passed directly to the constructor.
In this case the constructor (__init__
) is as follow:
def __init__(self, scene: Scene, name: str, prong1, prong2, prong3, prong4):
this means that the properties prong1
, prong2
, prong3
and prong4
are passed directly to init, meaning that the node can be made.
@ATT: Swing
class, Suspended4PLoad # Suspended load takes four prongs are required input
prong1, point1, #
prong2, point2, # these four attributes
prong3, point3, # are mandatory
prong4, point4, #
rigging_length, 2 # these are optional
length, 3.5
width, 4
height, 0.2
mass, 0.1
expose, mass, rigging_length, EA, length, width, height, cogx, cogy, cogz
The attachment activation function also checks if point1, point2, point3 and point4 happen to be names of super-element properties. If that is the case (which it is here) then they are replaced with references to the corresponding nodes:
*Points
# Name, x, y, z
point1, 4, -4 , 4
point2, 8, -4 , 4
point3, 8, -8 , 4
point4, 4, -8 , 4
the result is a configurable swing.
Other data#
Other data can be read from the .csv file as well.
Tabular data can be read as “data-tables” while single settings can be read as “settings”.
Data-tables#
Data-tables can be defined in the .csv file like:
*Bananas
Banana1,sweet, 1, in the tree
Banana2,baked, 1, hidden
And, now, for something, completely, different
The table is identified by the *
in front of the name. The table ends when an empty line is encountered or when something else starts.
It needs to be defined which data-tables are to be expected. This is done in the class by setting the class-variable data_table_names
:
class DEMO_Island(SuperElementFrameBased):
data_table_names = ('Bananas',) # note the notation for a tuple with one entry
All the data in the table is read into nested list which is stored in a dictionary data-tables.
print(monkey_island.data_tables['Bananas'])
gives:
[['Banana1', 'sweet', 1.0, 'in the tree'],
['Banana2', 'baked', 1.0, 'hidden'],
['And', 'now', 'for something', 'completely', 'different']]
Settings#
Any value defined in the .csv outside a table, attachment or attachment point is a setting
. Settings are defined as key, value:
# General settings
# key, value (s)
population, 0
location, imaginary space
best_visited_between, April, November
They are read into a dictionary with name data_settings:
monkey_island.data_settings :
{'population': 0.0,
'location': 'imaginary space',
'best_visited_between': ['April', 'November']}
hence all keys need to be unique.
It needs to be defined which settings are to be expected. This is done in the class by setting the class-variable expected_settings
.
The keys of this dictionary are the expected settings. The values can be strings or tuples. If a tuple then the first entry is the documentation and the second value is the default for the setting.
class DEMO_Island(SuperElementFrameBased):
expected_settings = {'population' : 'the number of monkies living on the island',
'location' : ('description of the location of the island',0),
'best_visited_between' : ('Suggestion of best months to visit',['April','November'])}
ValueErrors are thrown if settings are missing or unexpected settings are provided.
Optional settings#
Besides expected settings, there is also to provide optional settings. This is done by defining a optional_settings
dictionary next to the expected settings. The only difference is that optional settings that are not defined will simply not be added to the dictionary. In that case no error will be raised.
Doing something with the loaded data#
Override the method
def onCSVloaded(self):
"""Function is called after csv is loaded (setting .path triggers that)"""
pass
to do something with the data obtained from the .csv if needed.
Future work and ideas#
Managed or not-managed:#
In the current master branch the attachments are managed by the super-element. This means that the attachments themselves are read-only.
It is possible to make them not-managed, there is a PR for that:
The Frame
that is created by the attachment point is managed. The attached node itself is
not managed meaning that is it freely editable and can even be deleted.
The .node property of AttachmentPoint checks if the created node still exists in the scene before returning it. So it returns None if the node no longer exists.
This means that the attachment-point can not create the node. That would override its possible user changes. So AttachmentPoint.create() returns False for the node, but True for the Frame.
This means that exporting the AttachmentPoint needs to create the Frame but not the node.
But in the end the attachment point does need a reference to the node (if it exists). This is set at AFTER creating all nodes because otherwise we would run into circular references.
Dissolving attachments#
It should be straight-forward to dissolve an attachment. Just unmanage it
Attachments are nodes that can be attached to a super-element.
Attachments are a single node (but that node can be an Component)
To attach an attachment you need two things
An attachment point
An attachment-type
Creating a super-element in Python#
A super-element can be based on a frame or a rigid-body. The basics are similar.
For the island we will use a Frame
as basis. This means we derive our class from SuperElementFrameBased
We want the super-element to contain visuals, weights and buoyancy nodes. This is easily done using SEProperty objects. The properties and number of those nodes can then be defined in the .csv file.
The only thing that needs to be done for this is to set the class-variable su_properties_tables
dictionary. The keys of the dictionary correspond to the tables names in the .csv file while the value is the SEProperty class. So in the following example the .csv file is expected to have a table named “Visuals” where each row supplies the input for a single SEP_Visual object.
from DAVE_BaseExtensions.super_elements_common import SuperElementFrameBased
from DAVE_BaseExtensions.super_elements_properties import SEP_Visual, SEP_Weight,SEP_Buoyancy
class DEMO_Island(SuperElementFrameBased):
su_property_tables = dict()
su_property_tables['Visuals'] = SEP_Visual
su_property_tables['Weights'] = SEP_Weight
su_property_tables['Buoyancy'] = SEP_Buoyancy
su_property_tables['Points'] = SEP_Point
csv_extension = 'island.csv'
A dictionary su_property['key']
is created containing lists of SU_properties using the same keys. In the example above this means that any DEMO_Island node will contain a list of its visual nodes as node.su_property['Visuals']
Note: Reserved names (Tags, Attachments) should not be used here
and register is in the usual way:
# Register the super-elements for runtime
from DAVE.settings import DAVE_ADDITIONAL_RUNTIME_MODULES
DAVE_ADDITIONAL_RUNTIME_MODULES["DEMO_Island"] = DEMO_Island
We now have a super-element that can read a number of visuals, weight, buoyancy shapes and points from a comma-separated-values file ending with island.csv
An example of such a file is:
# This is example data for an island in .csv format (comma separated values)
#
# General settings
# key, value (s)
population, 0
location, imaginary space
best_visited_between, April, November
#
*Visuals
#
# Name , resource, off-x, off-y, off-z, rot-x, rot-y, rot-z, scale-x, scale-y, scale-z,,,
Visual1, res: island.obj,0,0,0,0,0,0,1,1,1,
#
*Buoyancy,,,,,,,,,,,,,
# Name, resource, off-x, off-y, off-z, rot-x, rot-y, rot-z, scale-x, scale-y, scale-z,invert_normals,,
Hull,res: island.obj,0,0,0,0,0,0,1,1,1,false
#
*Weights
# 'Name', 'mass [mT]', 'Cog-x', 'Cog-y', 'Cog-z', 'footprint: elevation', 'footprint: aft', 'footprint: front', 'footprint: sb', 'footprint: ps', 'inertia: rxx', 'inertia: ryy', 'inertia: rzz'
Main chunk, 230, 0 , 0 , 0 , 0 , -5 , 5 , -5 , 5 , 1 , 1 , 1
Second chunk, 3, 2 , 0 , 0 , 0 , -5 , 5 , -5 , 5 , 1 , 1 , 1
which looks awful. Luckily it can be edited in a spreadsheet program like excel or libreoffice which makes it look much better:
It follows a few simple rules:
anything after a # is ignored
A star (*) signals the start of a “table”.
A table ends when an other table starts or an empty row is encountered or the file ends. A row starting with a # does not count as an empty row.
In the class we instruct the table ‘Visuals’ (the key of the dict) to be read and transformed into SEP_Visual-objects.
The header of the table is only a comment (it starts with a #). The required columns are defined in SEP_Visual.tuple_fields() but are also listed in the overview in this document.
Warning:
Exporting .csv files from a spreadsheet program like excel when using a language where a comma (,) is used in numbers will very likely result in files that DAVE can not read
Using the above class and .csv file in the GUI results in the following:
the single visual, single buoyancy shape and two weight are read from the .csv file and created as managed nodes of the super-element.
The node-editor automatically gets a “File” entry where we can select a .csv file.
What not yet works is saving. For saving to work we need to override the give_python_code
of the class to yield the correct python code. In this case:
def give_python_code(self) -> str:
"""Returns the python code needed to re-create this object"""
code = list()
code.append(f"# Demo island {self.name}")
code.append(f'island = DEMO_Island(s, name = "{self.name}")')
code.extend(super().give_python_code_common("island"))
return "\n".join(code)
The call to super() adds the properties of the frame and the .csv file.
Adding attachments#
Now the island needs some trees where “some” is flexible. For that we need
Attachment-type: tree
Attachment-points: various points on the island where we can plant a tree
Both of these can be defined in the .csv file. No python code required.
Defining the tree attachment type#
The tree attachment is just a visual with a fixed visual file.
@ATT: Palmtree , # This is the NAME of an attachement type,
class, Visual , # This is the node-class. In this case a Visual
path, res: palmtree.obj , # Set the path of the visual to res: palmtree.obj
If we want the user to be able to manually edit some of the properties of the attachment, then use the EXPOSE functionality:
expose, scale, rotation # expose these
Defining the attachment locations#
Next (and final) step is to define the locations where an attachment can be added.
*Attachments
#
# Name, Target, posx, posy, posz, rotx, roty, rotz, attachments
Palm tree center, self , 0 , 0 , 2 , 0 , 0 , 0 , Palmtree
Another point, self , 2 , 0 , 2 , 0 , 0 , 0 , Palmtree
More fun with attachments#
#
#
*Attachments
#
# Name, Target, posx, posy, posz, rotx, roty, rotz, attachments
Monkey 1, self , 3 , 3 , 3, , 0 , 0 , 0 , Small monkey, Big monkey
Monkey 2, self , 2 , -3.5 , 5, , 0 ,10 , 0 , Small monkey, Big monkey
Palm tree center, self , 0 , 0 , 2 , 0 , 0 , 0 , Palmtree
Another point, self , 2 , 0 , 2 , 0 , 0 , 0 , Palmtree, Small monkey, Big monkey
#
# Attachments-types
#
@ATT: Small monkey # This is the NAME of an attachment type, @ATT: signals that this section is an attachment
class, Visual # This is the node-class. In this case the attachment is a "Visual"
path, res: monkey.obj # These are properties of the node. All properties are supported.
scale, 0.5,0.5,0.5, # this evaluates to node.scale = (0.5, 0.5, 0.5)
#
@ATT: Big monkey # This is the NAME of an attachment type, @ATT: signals that this section is an attachment
class, Visual # This is the node-class. In this case the attachment is a "Visual"
path, res: monkey.obj # These are properties of the node. All properties are supported.
scale, 1,1,4
#
#
@ATT: Palmtree # This is the NAME of an attachement type,
class, Visual # This is the node-class. In this case
path, res: palmtree.obj , # These are properties of the node
expose, scale, rotation # expose these
#
# DATA tables
#
*Bananas
Banana1,sweet, 1, in the tree
Banana2,baked, 1, hidden
And, now, for something, completely, different
Creating super-element properties in Python#
Super element properties are just normal nodes wrapped by a SEProperty class.
This class takes care of creating and maintaining a node from a row obtained from a table in the .csv file.
Specifically:
creating the node from a tuple and throwing sensible errors for incorrect input (the tuple is read from a user-defined .csv file)
maintaining the name, keeping it in sync with the super-element that it is on
creating new properties where needed, for example:
.scalez for trimesh-based nodes
adding a “capacity” property which get/sets on of the nodes limits.
Super-Element property implementation details:#
The SEProperty is an abstract class (ABC).
The SEProperty contains a Node which is managed by the super-element. This is self.node
.
The properties of this node are controlled through this SEProperty object as follows:
creating an instance of this class will create the corresponding node in the scene of the super-element (parent)
the name of the node will be prefixed by the name of the Super-Element as well as an identifier of the node type. For example
Barge/Bollard/PS5
attributes with their name defined in ‘linked_properties’ are synced from this element to the node: if ‘x’ is in linked_properties then setting this.x will trigger setting this.node.x as well. This is done by overriding setattr
To easily work with tables the class adds
to_tuple
andfrom_tuple
methodsThe
delete_node
is added to remove the managed node from the scene. By default this just deletes the node.
SEP_Bollard is a good example of how to derive from this class:
class SEP_Bollard(SEProperty):
"""A bollard is a Point on with a location and a capacity.
the capacity is the total force capacity of the bollard.
capacity_x and capacity_y are the capacities in local x and y directions.
The parent manager needs to derive from Frame"""
def _init_properties(self):
self.linked_props = ("x", "y", "z")
# Linked properties are automatically initialized to their default values
def _create_node(self):
# self.parent._scene is the scene where this node needs to be created
self.node = self.parent._scene.new_point(
name=self.node_name, parent=self.parent
)
self.node.manager = self.parent
@staticmethod
def tuple_fields() -> tuple:
"""These are human names describing the fields, they are used in (error) messages"""
return ("Name", "x", "y", "z", "capacity","capacity x","capacity y")
@staticmethod
def tuple_field_types() -> tuple:
return (str, float,float,float, float)
def _from_tuple(self, values: tuple):
self.name = values[0]
self.x = values[1]
self.y = values[2]
self.z = values[3]
self.capacity = values[4]
self.capacity_x = values[5]
self.capacity_y = values[6]
def to_tuple(self):
return (self.name, self.x, self.y, self.z, self.capacity, self.capacity_x, self.capacity_y)
@property
def node_name(self):
"""Returns the name of the node"""
return self.parent.name + "/Bol/" + self.name
@property
def capacity(self):
"""Capacity [kN]"""
return self.node.limits['force']
@capacity.setter
def capacity(self, value):
self.node.limits['force'] = value
@property
def capacity_x(self):
"""Capacity in local x direction [kN]"""
return self.node.limits['fx']
@capacity_x.setter
def capacity_x(self, value):
self.node.limits['fx'] = value
@property
def capacity_y(self):
"""Capacity in local y direction [kN]"""
return self.node.limits['fy']
@capacity_y.setter
def capacity_y(self, value):
self.node.limits['fy'] = value
It is very well possible to wrap and control more than a single node, this however requires overriding more of the base-class methods.
Appendix B - Example Super Element#
from DAVE_BaseExtensions import SuperElementFrameBased
from DAVE_BaseExtensions.super_elements_properties import SEP_Visual, SEP_Weight, SEP_Frame
class MyNode(SuperElementFrameBased):
# su_property_tables = dict() # class variable with
# key : name (str) of the property-table in the .csv file
# value : class of the SU_properties that have to be crated based on this
su_property_tables = dict()
su_property_tables['Visuals'] = SEP_Visual
su_property_tables['Weights'] = SEP_Weight
su_property_tables['Frames'] = SEP_Frame
# data_table_names = tuple() # data-tables to be read from the .csv file
data_table_names = ('Bar','Foo','Candy',)
expected_settings = dict() # key: name of setting , value: human documentation
expected_settings['Mandatory_setting'] = "Something needs to be provided here in the .csv file"
optional_settings = dict() # key: name of setting , value: human documentation
optional_settings['Optional_setting'] = "Define this if you need it"
# csv_extension = 'csv' # expected extension for the datafiles
csv_extension = 'mynode.csv'
def __init__(self, scene, name):
super().__init__(scene, name)
# other things that needs to be done.
# good idea to init the data_tables to empty
self.data_tables = dict()
for key in self.data_table_names:
self.data_tables[key] = []
self.block = None
def onCSVloaded(self):
# this executes when a .csv has loaded
body = self._scene.new_rigidbody('Demo')
self.nodes = [body]
# --- from manager ---
def _created_nodes(self):
return tuple(self.nodes) # iterable of nodes that are created by this node
# --- from node ---
def give_python_code(self):
code = list()
code.append(f'my_node = MyNode(s, name = "{self.name}")')
code.extend(super().give_python_code_common("my_node"))
return "\n".join(code)
# Register the super-elements for runtime
from DAVE.settings import DAVE_ADDITIONAL_RUNTIME_MODULES
DAVE_ADDITIONAL_RUNTIME_MODULES["MyNode"] = MyNode
# just a quick test
if __name__ == '__main__':
from DAVE import *
s = Scene()
my_node = MyNode(s,'my node')
print(s.give_python_code())