Module DAVE.scene

This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.

Ruben de Bruin - 2019

Expand source code
"""
  This Source Code Form is subject to the terms of the Mozilla Public
  License, v. 2.0. If a copy of the MPL was not distributed with this
  file, You can obtain one at http://mozilla.org/MPL/2.0/.

  Ruben de Bruin - 2019
"""


from abc import ABC, abstractmethod

import pyo3d
import numpy as np
import DAVE.settings as vfc
from DAVE.tools import *
from os.path import isfile, split, dirname, exists
from os import listdir
from pathlib import Path
import datetime


# we are wrapping all methods of pyo3d such that:
# - it is more user-friendly
# - code-completion is more robust
# - we can do some additional checks. pyo3d is written for speed, not robustness.
# - pyo3d is not a hard dependency
#
# notes and choices:
# - properties are returned as tuple to make sure they are not editable.
#    --> node.position[2] = 5 is not allowed


import functools


# Wrapper (decorator) for managed nodes
def node_setter_manageable(func):
    @functools.wraps(func)
    def wrapper_decorator(self, *args, **kwargs):
        self._verify_change_allowed()
        value = func(self, *args, **kwargs)
        return value

    return wrapper_decorator


# Wrapper (decorator) observed nodes
def node_setter_observable(func):
    @functools.wraps(func)
    def wrapper_decorator(self, *args, **kwargs):
        value = func(self, *args, **kwargs)
        # Do something after
        self._notify_observers()

        return value

    return wrapper_decorator


class ClaimManagement():
    """Helper class for doing:

    with ClaimManagement(scene, manager):
        change nodes that belong to manager

    """
    def __init__(self, scene, manager):
        assert isinstance(scene, Scene)
        assert isinstance(manager, Manager)
        self.scene = scene
        self.manager= manager


    def __enter__(self):
        self._old_manager = self.scene.current_manager
        self.scene.current_manager = self.manager

    def __exit__(self, *args, **kwargs):
        self.scene.current_manager = self._old_manager

class Node(ABC):
    """ABSTRACT CLASS - Properties defined here are applicable to all derived classes
    Master class for all nodes"""

    def __init__(self, scene):
        self._scene: Scene = scene
        """reference to the scene that the node lives is"""

        self._name: str = "A manager without a name"
        """Unique name of the node"""

        self._manager: Node or None = None
        """Reference to a node that controls this node"""

        self.observers = list()
        """List of nodes observing this node."""

        self._visible: bool = True
        """Determines if the visual for of this node (if any) should be visible"""

    def __repr__(self):
        return f"{self.name} <{self.__class__.__name__}>"

    def __str__(self):
        return self.name

    @property
    def class_name(self):
        return self.__class__.__name__

    @abstractmethod
    def depends_on(self) -> list:
        """Returns a list of nodes that need to be available present for this node to exist"""
        raise ValueError(
            f"Derived class should implement this method, but {type(self)} does not"
        )

    def give_python_code(self):
        """Returns the python code that can be executed to re-create this node"""
        return "# No python code generated for element {}".format(self.name)

    @property
    def visible(self):
        if self.manager:
            return self.manager.visible
        return self._visible

    @visible.setter
    @node_setter_manageable
    @node_setter_observable
    def visible(self, value):
        self._visible = value

    @property
    def manager(self):
        return self._manager

    @manager.setter
    @node_setter_manageable
    @node_setter_observable
    def manager(self, value):

        self._manager = value
        pass

    def _verify_change_allowed(self):
        """Changing the state of a node is only allowed if either:
        1. the node is not manages (node._manager is None)
        2. the manager of the node is identical to scene.current_manager
        """
        if self._scene._godmode:
            return True

        if self._manager is not None:
            if self._manager != self._scene.current_manager:
                if self._scene.current_manager is None:
                    name = None
                else:
                    name = self._scene.current_manager.name
                raise Exception(
                    f"Node {self.name} may not be changed because it is managed by {self._manager.name} and the current manager of the scene is {name}"
                )

    @property
    def name(self):
        """Name of the node (str), must be unique"""
        return self._name

    @name.setter
    @node_setter_manageable
    @node_setter_observable
    def name(self, name):

        self._name = name

    def _delete_vfc(self):
        """Removes any internally created core objects"""
        pass

    def update(self):
        """Performs internal updates relevant for physics. Called before solving statics or getting results such as
        forces or inertia"""
        pass

    def _notify_observers(self):
        for obs in self.observers:
            obs.on_observed_node_changed(self)

    def on_observed_node_changed(self, changed_node):
        """ """
        pass


class CoreConnectedNode(Node):
    """ABSTRACT CLASS - Properties defined here are applicable to all derived classes
    Master class for all nodes with a connected eqCore element"""

    def __init__(self, scene, vfNode):
        super().__init__(scene)
        self._vfNode = vfNode

    @property
    def name(self):
        """Name of the node (str), must be unique"""
        return self._vfNode.name

    @name.setter
    @node_setter_manageable
    @node_setter_observable
    def name(self, name):

        if not name == self._vfNode.name:
            self._scene._verify_name_available(name)
            self._vfNode.name = name

    def _delete_vfc(self):
        self._scene._vfc.delete(self._vfNode.name)


class NodeWithParent(CoreConnectedNode):
    """
    NodeWithParent

    Do not use this class directly.
    This is a base-class for all nodes that have a "parent" property.
    """

    def __init__(self, scene, vfNode):
        super().__init__(scene, vfNode)
        self._parent = None
        self._None_parent_acceptable = False
        self._parent_for_code_export = True
        """True : use parent, 
        None : use None, 
        Node : use that Node
        Used to prevent circular references, see groups section in documentation"""

    def depends_on(self):
        if self.parent_for_export is not None:
            return [self.parent_for_export]
        else:
            return []

    @property
    def parent_for_export(self):
        if self._parent_for_code_export == True:
            return self._parent
        else:
            return self._parent_for_code_export

    @property
    def parent(self):
        """Determines the parent of the node. Should be an axis or None"""
        if self._vfNode.parent is None:
            return None
        else:
            return self._parent
            # return Axis(self._scene, self._vfNode.parent)

    @parent.setter
    @node_setter_manageable
    @node_setter_observable
    def parent(self, var):
        """Assigns a new parent. Keeps the local position and rotations the same

        See also: change_parent_to
        """

        if var is None:

            if not self._None_parent_acceptable:
                raise ValueError(
                    "None is not an acceptable parent for {} of {}".format(
                        self.name, type(self)
                    )
                )

            self._parent = None
            self._vfNode.parent = None
        else:

            var = self._scene._node_from_node_or_str(var)

            if isinstance(var, Axis) or isinstance(var, GeometricContact):
                self._parent = var
                self._vfNode.parent = var._vfNode
            elif isinstance(var, Point):
                self._parent = var
                self._vfNode.parent = var._vfNode
            else:
                raise Exception(
                    "Parent can only be set to an instance of Axis or Poi, not to a {}".format(
                        type(var)
                    )
                )

    def change_parent_to(self, new_parent):
        """Assigns a new parent to the node but keeps the global position and rotation the same.

        See also: .parent (property)

        Args:
            new_parent: new parent node

        """

        if isinstance(self, Point) and isinstance(new_parent, Point):
            raise TypeError("Points can not be placed on points")

        try:
            self.rotation
            has_rotation = True
        except:
            has_rotation = False

        try:
            self.position
            has_position = True
        except:
            has_position = False

        # it is possible that this function is called on an object without position/rotation
        # in that case just fall-back to a change of parent
        if not has_position and not has_rotation:
            self.parent = new_parent
            return

        # check new_parent
        if new_parent is not None:

            if not isinstance(new_parent, Axis):
                if not has_rotation:
                    if not isinstance(new_parent, Point):
                        raise TypeError(
                            "Only Poi-type nodes (or derived types) can be used as parent. You tried to use a {} as parent".format(
                                type(new_parent)
                            )
                        )
                else:
                    raise TypeError(
                        "Only None or Axis-type nodes (or derived types)  can be used as parent. You tried to use a {} as parent".format(
                            type(new_parent)
                        )
                    )

        glob_pos = self.global_position

        if has_rotation:
            glob_rot = self.global_rotation

        self.parent = new_parent

        if new_parent is None:
            self.position = glob_pos
            if has_rotation:
                self.rotation = glob_rot

        else:
            self.position = new_parent.to_loc_position(glob_pos)
            if has_rotation:
                self.rotation = new_parent.to_loc_direction(glob_rot)


class NodeWithParentAndFootprint(NodeWithParent):
    """
    NodeWithParentAndFootprint

    Do not use this class directly.
    This is a base-class for all nodes that have a "footprint" property as well as a parent
    """

    def __init__(self, scene, vfNode):
        super().__init__(scene, vfNode)

    @property
    def footprint(self):
        """tuple of tuples ((x1,y1,z1), (x2,y2,z2), .... (xn,yn,zn)"""
        r = []
        for i in range(self._vfNode.nFootprintVertices):
            r.append(self._vfNode.footprintVertexGet(i))
        return tuple(r)

    @footprint.setter
    def footprint(self, value):
        """Sets the footprint vertices. Supply as an iterable with each element containing three floats"""
        for t in value:
            assert3f(t, "Each entry of value assigned to footprints ")

        self._vfNode.footprintVertexClearAll()
        for t in value:
            self._vfNode.footprintVertexAdd(*t)

    def add_footprint_python_code(self):
        if self.footprint:
            return f"\ns['{self.name}'].footprint = {str(self.footprint)}"
        else:
            return ""


# ==============================================================


class Visual(Node):
    """
    Visual

    .. image:: ./images/visual.png

    A Visual node contains a 3d visual, typically obtained from a .obj file.
    A visual node can be placed on an axis-type node.

    It is used for visualization. It does not affect the forces, dynamics or statics.

    The visual can be given an offset, rotation and scale. These are applied in the following order

    1. rotate
    2. scale
    3. offset

    Hint: To scale before rotation place the visual on a dedicated axis and rotate that axis.

    """

    def __init__(self, scene):

        super().__init__(scene)

        self.offset = [0, 0, 0]
        """Offset (x,y,z) of the visual. Offset is applied after scaling"""
        self.rotation = [0, 0, 0]
        """Rotation (rx,ry,rz) of the visual"""

        self.scale = [1, 1, 1]
        """Scaling of the visual. Scaling is applied before offset."""

        self.path = ""
        """Filename of the visual"""

        self.parent = None
        """Parent : Axis-type"""

    @property
    def file_path(self):
        return self._scene.get_resource_path(self.path)

    def depends_on(self):
        return [self.parent]

    def give_python_code(self):
        code = "# code for {}".format(self.name)

        code += "\ns.new_visual(name='{}',".format(self.name)
        code += "\n            parent='{}',".format(self.parent.name)
        code += "\n            path=r'{}',".format(self.path)
        code += "\n            offset=({}, {}, {}), ".format(*self.offset)
        code += "\n            rotation=({}, {}, {}), ".format(*self.rotation)
        code += "\n            scale=({}, {}, {}) )".format(*self.scale)

        return code

    def change_parent_to(self, new_parent):

        if not (isinstance(new_parent, Axis) or new_parent is None):
            raise ValueError(
                "Visuals can only be attached to an axis (or derived) or None"
            )

        # get current position and orientation
        if self.parent is not None:
            cur_position = self.parent.to_glob_position(self.offset)
            cur_rotation = self.parent.to_glob_direction(self.rotation)
        else:
            cur_position = self.offset
            cur_rotation = self.rotation

        self.parent = new_parent

        if new_parent is None:
            self.offset = cur_position
            self.rotation = cur_rotation
        else:
            self.offset = new_parent.to_loc_position(cur_position)
            self.rotation = new_parent.to_loc_direction(cur_rotation)


class Axis(NodeWithParentAndFootprint):
    """
    Axis

    Axes are the main building blocks of the geometry. They have a position and an rotation in space. Other nodes can be placed on them.
    Axes can be nested by parent/child relationships meaning that an axis can be placed on an other axis.
    The possible movements of an axis can be controlled in each degree of freedom using the "fixed" property.

    Axes are also the main building block of inertia.
    Dynamics are controlled using the inertia properties of an axis: inertia [mT], inertia_position[m,m,m] and inertia_radii [m,m,m]


    Notes:
         - circular references are not allowed: It is not allowed to place a on b and b on a

    """

    def __init__(self, scene, vfAxis):
        super().__init__(scene, vfAxis)
        self._None_parent_acceptable = True

        self._inertia = 0
        self._inertia_position = (0, 0, 0)
        self._inertia_radii = (0, 0, 0)

        self._pointmasses = list()
        for i in range(6):
            p = scene._vfc.new_pointmass(
                self.name + vfc.VF_NAME_SPLIT + "pointmass_{}".format(i)
            )
            p.parent = vfAxis
            self._pointmasses.append(p)
        self._update_inertia()

    def depends_on(self):
        if self.parent is None:
            return []
        else:
            return [self.parent]

    def _delete_vfc(self):
        for p in self._pointmasses:
            self._scene._vfc.delete(p.name)

        super()._delete_vfc()

    @property
    def inertia(self):
        """The linear inertia of the axis in [mT] Aka: "Mass"
        - used only for dynamics"""
        return self._inertia

    @inertia.setter
    @node_setter_manageable
    @node_setter_observable
    def inertia(self, val):

        assert1f(val, "Inertia")
        self._inertia = val
        self._update_inertia()

    @property
    def inertia_position(self):
        """The position of the center of inertia. Aka: "cog" [m,m,m] (local axis)
        - used only for dynamics
        - defined in local axis system"""
        return tuple(self._inertia_position)

    @inertia_position.setter
    @node_setter_manageable
    @node_setter_observable
    def inertia_position(self, val):

        assert3f(val, "Inertia position")
        self._inertia_position = tuple(val)
        self._update_inertia()

    @property
    def inertia_radii(self):
        """The radii of gyration of the inertia [m,m,m] (local axis)

        Used to calculate the mass moments of inertia via

        Ixx = rxx^2 * inertia
        Iyy = rxx^2 * inertia
        Izz = rxx^2 * inertia

        Note that DAVE does not directly support cross terms in the interia matrix of an axis system. If you want to
        use cross terms then combine multiple axis system to reach the same result. This is because inertia matrices with
        diagonal terms can not be translated.
        """
        return np.array(self._inertia_radii, dtype=float)

    @inertia_radii.setter
    @node_setter_manageable
    @node_setter_observable
    def inertia_radii(self, val):

        assert3f_positive(val, "Inertia radii of gyration")
        self._inertia_radii = val
        self._update_inertia()

    def _update_inertia(self):
        # update mass
        for i in range(6):
            self._pointmasses[i].inertia = self._inertia / 6

        if self._inertia <= 0:
            return

        # update radii and position
        pos = radii_to_positions(*self._inertia_radii)
        for i in range(6):
            p = (
                pos[i][0] + self._inertia_position[0],
                pos[i][1] + self._inertia_position[1],
                pos[i][2] + self._inertia_position[2],
            )
            self._pointmasses[i].position = p
            # print('{} at {} {} {}'.format(self._inertia/6, *p))

    @property
    def fixed(self):
        """Determines which of the six degrees of freedom are fixed, if any. (x,y,z,rx,ry,rz).
        True means that that degree of freedom will not change when solving statics.
        False means a that is may be changed in order to find equilibrium.

        These are the expressed on the coordinate system of the parent (if any) or the global axis system (if no parent)

        See Also: set_free, set_fixed
        """
        return self._vfNode.fixed

    @fixed.setter
    @node_setter_manageable
    @node_setter_observable
    def fixed(self, var):

        if var == True:
            var = (True, True, True, True, True, True)
        if var == False:
            var = (False, False, False, False, False, False)

        self._vfNode.fixed = var

    def set_free(self):
        """Sets .fixed to (False,False,False,False,False,False)"""
        self._vfNode.set_free()

    def set_fixed(self):
        """Sets .fixed to (True,True,True,True,True,True)"""

        self._vfNode.set_fixed()

    @property
    def x(self):
        """The x-component of the position vector (parent axis) [m]"""
        return self.position[0]

    @property
    def y(self):
        """The y-component of the position vector (parent axis) [m]"""
        return self.position[1]

    @property
    def z(self):
        """The z-component of the position vector (parent axis) [m]"""
        return self.position[2]

    @x.setter
    @node_setter_manageable
    @node_setter_observable
    def x(self, var):

        a = self.position
        self.position = (var, a[1], a[2])

    @y.setter
    @node_setter_manageable
    @node_setter_observable
    def y(self, var):

        a = self.position
        self.position = (a[0], var, a[2])

    @z.setter
    @node_setter_manageable
    @node_setter_observable
    def z(self, var):

        a = self.position
        self.position = (a[0], a[1], var)

    @property
    def position(self):
        """Position of the axis (parent axis) [m,m,m]

        These are the expressed on the coordinate system of the parent (if any) or the global axis system (if no parent)"""
        return self._vfNode.position

    @position.setter
    @node_setter_manageable
    @node_setter_observable
    def position(self, var):

        assert3f(var, "Position ")
        self._vfNode.position = var
        self._scene._geometry_changed()

    @property
    def rx(self):
        """The x-component of the rotation vector [degrees] (parent axis)"""
        return self.rotation[0]

    @property
    def ry(self):
        """The y-component of the rotation vector [degrees] (parent axis)"""
        return self.rotation[1]

    @property
    def rz(self):
        """The z-component of the rotation vector [degrees], (parent axis)"""
        return self.rotation[2]

    @rx.setter
    @node_setter_manageable
    @node_setter_observable
    def rx(self, var):

        a = self.rotation
        self.rotation = (var, a[1], a[2])

    @ry.setter
    @node_setter_manageable
    @node_setter_observable
    def ry(self, var):

        a = self.rotation
        self.rotation = (a[0], var, a[2])

    @rz.setter
    @node_setter_manageable
    @node_setter_observable
    def rz(self, var):

        a = self.rotation
        self.rotation = (a[0], a[1], var)

    @property
    def rotation(self):
        """Rotation of the axis about its origin (rx,ry,rz).
        Defined as a rotation about an axis where the direction of the axis is (rx,ry,rz) and the angle of rotation is |(rx,ry,rz| degrees.
        These are the expressed on the coordinate system of the parent (if any) or the global axis system (if no parent)"""
        return np.rad2deg(self._vfNode.rotation)

    @rotation.setter
    @node_setter_manageable
    @node_setter_observable
    def rotation(self, var):

        # convert to degrees
        assert3f(var, "Rotation ")
        self._vfNode.rotation = np.deg2rad(var)
        self._scene._geometry_changed()

    # we need to over-ride the parent property to be able to call _geometry_changed afterwards
    @property
    def parent(self):
        """Determines the parent of the axis. Should either be another axis or 'None'

        Other axis may be refered to by reference or by name (str). So the following are identical

            p = s.new_axis('parent_axis')
            c = s.new_axis('child axis')

            c.parent = p
            c.parent = 'parent_axis'

        To define that an axis does not have a parent use

            c.parent = None

        """
        return super().parent

    @parent.setter
    @node_setter_manageable
    @node_setter_observable
    def parent(self, val):

        if val is not None:
            # Circular reference check: are we trying to make self depend on val while val depends on self?
            if self._scene.node_A_core_depends_on_B_core(val, self):
                if isinstance(val, Axis):  # it better be
                    val.change_parent_to(
                        None
                    )  # change the parent of other to None, this breaks the previous dependancy

        NodeWithParent.parent.fset(self, val)
        self._scene._geometry_changed()

    @property
    def gx(self):
        """The x-component of the global position vector [m] (global axis )"""
        return self.global_position[0]

    @property
    def gy(self):
        """The y-component of the global position vector [m] (global axis )"""
        return self.global_position[1]

    @property
    def gz(self):
        """The z-component of the global position vector [m] (global axis )"""
        return self.global_position[2]

    @gx.setter
    @node_setter_manageable
    @node_setter_observable
    def gx(self, var):

        a = self.global_position
        self.global_position = (var, a[1], a[2])

    @gy.setter
    @node_setter_manageable
    @node_setter_observable
    def gy(self, var):

        a = self.global_position
        self.global_position = (a[0], var, a[2])

    @gz.setter
    @node_setter_manageable
    @node_setter_observable
    def gz(self, var):

        a = self.global_position
        self.global_position = (a[0], a[1], var)

    @property
    def global_position(self):
        """The global position of the origin of the axis system  [m,m,m] (global axis)"""
        return self._vfNode.global_position

    @global_position.setter
    @node_setter_manageable
    @node_setter_observable
    def global_position(self, val):

        assert3f(val, "Global Position")
        if self.parent:
            self.position = self.parent.to_loc_position(val)
        else:
            self.position = val

    @property
    def grx(self):
        """The x-component of the global rotation vector [degrees] (global axis)"""
        return self.global_rotation[0]

    @property
    def gry(self):
        """The y-component of the global rotation vector [degrees] (global axis)"""
        return self.global_rotation[1]

    @property
    def grz(self):
        """The z-component of the global rotation vector [degrees] (global axis)"""
        return self.global_rotation[2]

    @grx.setter
    @node_setter_manageable
    @node_setter_observable
    def grx(self, var):

        a = self.global_rotation
        self.global_rotation = (var, a[1], a[2])

    @gry.setter
    @node_setter_manageable
    @node_setter_observable
    def gry(self, var):

        a = self.global_rotation
        self.global_rotation = (a[0], var, a[2])

    @grz.setter
    @node_setter_manageable
    @node_setter_observable
    def grz(self, var):

        a = self.global_rotation
        self.global_rotation = (a[0], a[1], var)

    @property
    def tilt_x(self):
        """Tilt percentage. This is the z-component of the unit y vector [%].

        See Also: heel
        """
        y = (0, 1, 0)
        uy = self.to_glob_direction(y)
        return float(100 * uy[2])

    @property
    def heel(self):
        """Heel in degrees. SB down is positive [deg].
        This is the inverse sin of the unit y vector(This is the arcsin of the tiltx)

        See also: tilt_x
        """
        return np.rad2deg(np.arcsin(self.tilt_x / 100))

    @property
    def tilt_y(self):
        """Tilt percentage. This is the z-component of the unit -x vector [%].
        So a positive rotation about the y axis results in a positive tilt_y.

        See Also: trim
        """
        x = (-1, 0, 0)
        ux = self.to_glob_direction(x)
        return float(100 * ux[2])

    @property
    def trim(self):
        """Trim in degrees. Bow-down is positive [deg].

        This is the inverse sin of the unit -x vector(This is the arcsin of the tilt_y)

        See also: tilt_y
        """
        return np.rad2deg(np.arcsin(self.tilt_y / 100))

    @property
    def heading(self):
        """Direction (0..360) [deg] of the local x-axis relative to the global x axis. Measured about the global z axis

        heading = atan(u_y,u_x)

        typically:
            heading 0  --> local axis align with global axis
            heading 90 --> local x-axis in direction of global y axis


        See also: heading_compass
        """
        x = (1, 0, 0)
        ux = self.to_glob_direction(x)
        heading = np.rad2deg(np.arctan2(ux[1], ux[0]))
        return np.mod(heading, 360)

    @property
    def heading_compass(self):
        """The heading (0..360)[deg] assuming that the global y-axis is North and global x-axis is East and rotation accoring compass definition"""
        return np.mod(90 - self.heading, 360)

    @property
    def global_rotation(self):
        """Rotation [deg,deg,deg] (global axis)"""
        return tuple(np.rad2deg(self._vfNode.global_rotation))

    @global_rotation.setter
    @node_setter_manageable
    @node_setter_observable
    def global_rotation(self, val):

        assert3f(val, "Global Rotation")
        if self.parent:
            self.rotation = self.parent.to_loc_rotation(val)
        else:
            self.rotation = val

    @property
    def global_transform(self):
        """Read-only: The global transform of the axis system [matrix]"""
        return self._vfNode.global_transform

    @property
    def connection_force(self):
        """The forces and moments that this axis applies on its parent at the origin of this axis system. [kN, kN, kN, kNm, kNm, kNm] (Parent axis)

        If this axis would be connected to a point on its parent, and that point would be located at the location of the origin of this axis system
        then the connection force equals the force and moment applied on that point.

        Example:
            parent axis with name A
            this axis with name B
            this axis is located on A at position (10,0,0)
            there is a Point at the center of this axis system.
            A force with Fz = -10 acts on the Point.

            The connection_force is (-10,0,0,0,0,0)

            This is the force and moment as applied on A at point (10,0,0)


        """
        return self._vfNode.connection_force

    @property
    def connection_force_x(self):
        """The x-component of the connection-force vector [kN] (Parent axis)"""
        return self.connection_force[0]

    @property
    def connection_force_y(self):
        """The y-component of the connection-force vector [kN] (Parent axis)"""
        return self.connection_force[1]

    @property
    def connection_force_z(self):
        """The z-component of the connection-force vector [kN] (Parent axis)"""
        return self.connection_force[2]

    @property
    def connection_moment_x(self):
        """The mx-component of the connection-force vector [kNm] (Parent axis)"""
        return self.connection_force[3]

    @property
    def connection_moment_y(self):
        """The my-component of the connection-force vector [kNm] (Parent axis)"""
        return self.connection_force[4]

    @property
    def connection_moment_z(self):
        """The mx-component of the connection-force vector [kNm] (Parent axis)"""
        return self.connection_force[5]

    @property
    def applied_force(self):
        """The force and moment that is applied on origin of this axis [kN, kN, kN, kNm, kNm, kNm] (Global axis)"""
        return self._vfNode.applied_force

    @property
    def ux(self):
        """The unit x axis [m,m,m] (Global axis)"""
        return self.to_glob_direction((1, 0, 0))

    @property
    def uy(self):
        """The unit y axis [m,m,m] (Global axis)"""
        return self.to_glob_direction((0, 1, 0))

    @property
    def uz(self):
        """The unit z axis [m,m,m] (Global axis)"""
        return self.to_glob_direction((0, 0, 1))

    @property
    def equilibrium_error(self):
        """The unresolved force and moment that on this axis. Should be zero when in equilibrium  (applied-force minus connection force, Parent axis)"""
        return self._vfNode.equilibrium_error

    def to_loc_position(self, value):
        """Returns the local position of a point in the global axis system.
        This considers the position and the rotation of the axis system.
        See Also: to_loc_direction
        """
        return self._vfNode.global_to_local_point(value)

    def to_glob_position(self, value):
        """Returns the global position of a point in the local axis system.
        This considers the position and the rotation of the axis system.
        See Also: to_glob_direction
        """
        return self._vfNode.local_to_global_point(value)

    def to_loc_direction(self, value):
        """Returns the local direction of a point in the global axis system.
        This considers only the rotation of the axis system.
        See Also: to_loc_position
        """
        return self._vfNode.global_to_local_vector(value)

    def to_glob_direction(self, value):
        """Returns the global direction of a point in the local axis system.
        This considers only the rotation of the axis system.
        See Also: to_glob_position"""
        return self._vfNode.local_to_global_vector(value)

    def to_loc_rotation(self, value):
        """Returns the local rotation. Used for rotating rotations.
        See Also: to_loc_position, to_loc_direction
        """
        return np.rad2deg(self._vfNode.global_to_local_rotation(np.deg2rad(value)))

    def to_glob_rotation(self, value):
        """Returns the global rotation. Used for rotating rotations.
        See Also: to_loc_position, to_loc_direction
        """
        return np.rad2deg(self._vfNode.local_to_global_rotation(np.deg2rad(value)))

    def give_load_shear_moment_diagram(
        self, axis_system=None
    ) -> "LoadShearMomentDiagram":
        """Returns a LoadShearMoment diagram

        Args:
            axis_system : optional : coordinate system [axis node] to be used for calculation of the diagram.
            Defaults to the local axis system
        """

        if axis_system is None:
            axis_system = self

        assert isinstance(axis_system, Axis), ValueError(
            f"axis_system shall be an instance of Axis, but it is of type {type(axis_system)}"
        )

        # calculate in the right global direction
        glob_dir = axis_system.to_glob_direction((1, 0, 0))
        self._scene._vfc.calculateBendingMoments(*glob_dir)

        lsm = self._vfNode.getBendingMomentDiagram(axis_system._vfNode)

        return LoadShearMomentDiagram(lsm)

    def change_parent_to(self, new_parent):
        """Assigns a new parent to the node but keeps the global position and rotation the same.

        See also: .parent (property)

        Args:
            new_parent: new parent node

        """

        # check new_parent
        if new_parent is not None:
            if not (
                isinstance(new_parent, Axis) or isinstance(new_parent, GeometricContact)
            ):
                raise TypeError(
                    "Only None or Axis-type nodes (or derived types) can be used as parent. You tried to use a {} as parent".format(
                        type(new_parent)
                    )
                )

        glob_pos = self.global_position
        glob_rot = self.global_rotation
        self.parent = new_parent
        self.global_position = glob_pos
        self.global_rotation = glob_rot

    def give_python_code(self):
        code = "# code for {}".format(self.name)
        code += "\ns.new_axis(name='{}',".format(self.name)
        if self.parent_for_export:
            code += "\n           parent='{}',".format(self.parent_for_export.name)

        # position

        if self.fixed[0]:
            code += "\n           position=({},".format(self.position[0])
        else:
            code += "\n           position=(solved({}),".format(self.position[0])
        if self.fixed[1]:
            code += "\n                     {},".format(self.position[1])
        else:
            code += "\n                     solved({}),".format(self.position[1])
        if self.fixed[2]:
            code += "\n                     {}),".format(self.position[2])
        else:
            code += "\n                     solved({})),".format(self.position[2])

        # rotation

        if self.fixed[3]:
            code += "\n           rotation=({},".format(self.rotation[0])
        else:
            code += "\n           rotation=(solved({}),".format(self.rotation[0])
        if self.fixed[4]:
            code += "\n                     {},".format(self.rotation[1])
        else:
            code += "\n                     solved({}),".format(self.rotation[1])
        if self.fixed[5]:
            code += "\n                     {}),".format(self.rotation[2])
        else:
            code += "\n                     solved({})),".format(self.rotation[2])

        # inertia and radii of gyration
        if self.inertia > 0:
            code += "\n                     inertia = {},".format(self.inertia)

        if np.any(self.inertia_radii > 0):
            code += "\n                     inertia_radii = ({}, {}, {}),".format(
                *self.inertia_radii
            )

        # fixeties
        code += "\n           fixed =({}, {}, {}, {}, {}, {}) )".format(*self.fixed)

        code += self.add_footprint_python_code()

        return code


class Point(NodeWithParentAndFootprint):
    """A location on an axis"""

    # init parent and name are fully derived from NodeWithParent
    # _vfNode is a poi
    def __init__(self, scene, vfPoi):
        super().__init__(scene, vfPoi)
        self._None_parent_acceptable = True

    def on_observed_node_changed(self, changed_node):
        print(changed_node.name + " has changed")

    @property
    def x(self):
        """x component of local position [m] (parent axis)"""
        return self.position[0]

    @property
    def y(self):
        """y component of local position [m] (parent axis)"""
        return self.position[1]

    @property
    def z(self):
        """z component of local position [m] (parent axis)"""
        return self.position[2]

    @x.setter
    @node_setter_manageable
    @node_setter_observable
    def x(self, var):

        a = self.position
        self.position = (var, a[1], a[2])

    @y.setter
    @node_setter_manageable
    @node_setter_observable
    def y(self, var):

        a = self.position
        self.position = (a[0], var, a[2])

    @z.setter
    @node_setter_manageable
    @node_setter_observable
    @node_setter_manageable
    def z(self, var):

        """z component of local position"""
        a = self.position
        self.position = (a[0], a[1], var)

    @property
    def position(self):
        """Local position [m,m,m] (parent axis)"""
        return self._vfNode.position

    @position.setter
    @node_setter_manageable
    @node_setter_observable
    def position(self, new_position):

        assert3f(new_position)
        self._vfNode.position = new_position

    @property
    def applied_force_and_moment_global(self):
        """Applied force and moment on this point [kN, kN, kN, kNm, kNm, kNm] (Global axis)"""
        return self._vfNode.applied_force

    @property
    def gx(self):
        """x component of position [m] (global axis)"""
        return self.global_position[0]

    @property
    def gy(self):
        """y component of position [m] (global axis)"""
        return self.global_position[1]

    @property
    def gz(self):
        """z component of position [m] (global axis)"""
        return self.global_position[2]

    @gx.setter
    @node_setter_manageable
    @node_setter_observable
    def gx(self, var):

        a = self.global_position
        self.global_position = (var, a[1], a[2])

    @gy.setter
    @node_setter_manageable
    @node_setter_observable
    def gy(self, var):

        a = self.global_position
        self.global_position = (a[0], var, a[2])

    @gz.setter
    @node_setter_manageable
    @node_setter_observable
    def gz(self, var):

        a = self.global_position
        self.global_position = (a[0], a[1], var)

    @property
    def global_position(self):
        """Global position [m,m,m] (global axis)"""
        return self._vfNode.global_position

    @global_position.setter
    @node_setter_manageable
    @node_setter_observable
    def global_position(self, val):

        assert3f(val, "Global Position")
        if self.parent:
            self.position = self.parent.to_loc_position(val)
        else:
            self.position = val

    def give_python_code(self):
        code = "# code for {}".format(self.name)
        code += "\ns.new_point(name='{}',".format(self.name)
        if self.parent_for_export:
            code += "\n          parent='{}',".format(self.parent_for_export.name)

        # position

        code += "\n          position=({},".format(self.position[0])
        code += "\n                    {},".format(self.position[1])
        code += "\n                    {}))".format(self.position[2])

        code += self.add_footprint_python_code()

        return code


class RigidBody(Axis):
    """A Rigid body, internally composed of an axis, a point (cog) and a force (gravity)"""

    def __init__(self, scene, axis, poi, force):
        super().__init__(scene, axis)

        # The axis is the Node
        # poi and force are added separately

        self._vfPoi = poi
        self._vfForce = force

    # override the following properties
    # - name : sets the names of poi and force as well

    def _delete_vfc(self):
        super()._delete_vfc()
        self._scene._vfc.delete(self._vfPoi.name)
        self._scene._vfc.delete(self._vfForce.name)

    @property  # can not define a setter without a getter..?
    def name(self):
        return super().name

    @name.setter
    @node_setter_manageable
    @node_setter_observable
    def name(self, newname):
        """Name of the node (str), must be unique"""

        # super().name = newname
        super(RigidBody, self.__class__).name.fset(self, newname)
        self._vfPoi.name = newname + vfc.VF_NAME_SPLIT + "cog"
        self._vfForce.name = newname + vfc.VF_NAME_SPLIT + "gravity"

    @property
    def footprint(self):
        return super().footprint

    @footprint.setter
    def footprint(self, value):
        """Sets the footprint vertices. Supply as an iterable with each element containing three floats"""
        super(RigidBody, type(self)).footprint.fset(
            self, value
        )  # https://bugs.python.org/issue14965

        # assign the footprint to the CoG as well,
        # but subtract the cog position as
        self._sync_selfweight_footprint()

    def _sync_selfweight_footprint(self):
        """The footprint of the CoG is defined relative to the CoG, so its needs to be updated
        whenever the CoG or the footprint changes"""

        fp = self.footprint

        self._vfPoi.footprintVertexClearAll()
        for t in fp:
            pos = np.array(t, dtype=float)
            relpos = pos - self.cog
            self._vfPoi.footprintVertexAdd(*relpos)

    @property
    def cogx(self):
        """x-component of cog position [m] (local axis)"""
        return self.cog[0]

    @property
    def cogy(self):
        """y-component of cog position [m] (local axis)"""
        return self.cog[1]

    @property
    def cogz(self):
        """z-component of cog position [m] (local axis)"""
        return self.cog[2]

    @property
    def cog(self):
        """Center of Gravity position [m,m,m] (local axis)"""
        return self._vfPoi.position

    @cogx.setter
    @node_setter_manageable
    @node_setter_observable
    def cogx(self, var):

        a = self.cog
        self.cog = (var, a[1], a[2])

    @cogy.setter
    @node_setter_manageable
    @node_setter_observable
    def cogy(self, var):

        a = self.cog
        self.cog = (a[0], var, a[2])

    @cogz.setter
    @node_setter_manageable
    @node_setter_observable
    def cogz(self, var):

        a = self.cog
        self.cog = (a[0], a[1], var)

    @cog.setter
    @node_setter_manageable
    @node_setter_observable
    def cog(self, newcog):

        assert3f(newcog)
        self._vfPoi.position = newcog
        self.inertia_position = self.cog
        self._sync_selfweight_footprint()

    @property
    def mass(self):
        """Static mass of the body [mT]

        See Also: inertia
        """
        return self._vfForce.force[2] / -vfc.G

    @mass.setter
    @node_setter_manageable
    @node_setter_observable
    def mass(self, newmass):

        assert1f(newmass)
        self.inertia = newmass
        self._vfForce.force = (0, 0, -vfc.G * newmass)

    def give_python_code(self):
        code = "# code for {}".format(self.name)
        code += "\ns.new_rigidbody(name='{}',".format(self.name)
        code += "\n                mass={},".format(self.mass)
        code += "\n                cog=({},".format(self.cog[0])
        code += "\n                     {},".format(self.cog[1])
        code += "\n                     {}),".format(self.cog[2])

        if self.parent_for_export:
            code += "\n                parent='{}',".format(self.parent_for_export.name)

        # position

        if self.fixed[0]:
            code += "\n                position=({},".format(self.position[0])
        else:
            code += "\n                position=(solved({}),".format(self.position[0])
        if self.fixed[1]:
            code += "\n                          {},".format(self.position[1])
        else:
            code += "\n                          solved({}),".format(self.position[1])
        if self.fixed[2]:
            code += "\n                          {}),".format(self.position[2])
        else:
            code += "\n                          solved({})),".format(self.position[2])

        # rotation

        if self.fixed[3]:
            code += "\n                rotation=({},".format(self.rotation[0])
        else:
            code += "\n                rotation=(solved({}),".format(self.rotation[0])
        if self.fixed[4]:
            code += "\n                          {},".format(self.rotation[1])
        else:
            code += "\n                          solved({}),".format(self.rotation[1])
        if self.fixed[5]:
            code += "\n                          {}),".format(self.rotation[2])
        else:
            code += "\n                          solved({})),".format(self.rotation[2])

        if np.any(self.inertia_radii > 0):
            code += "\n                     inertia_radii = ({}, {}, {}),".format(
                *self.inertia_radii
            )

        code += "\n                fixed =({}, {}, {}, {}, {}, {}) )".format(
            *self.fixed
        )

        code += self.add_footprint_python_code()

        return code


class Cable(CoreConnectedNode):
    """A Cable represents a linear elastic wire running from a Poi or sheave to another Poi of sheave.

    A cable has a un-stretched length [length] and a stiffness [EA] and may have a diameter [m]. The tension in the cable is calculated.

    Intermediate pois or sheaves may be added.

    - Pois are considered as sheaves with a zero diameter.
    - Sheaves are considered sheaves with the given geometry. If defined then the diameter of the cable is considered when calculating the geometry. The cable runs over the sheave in the positive direction (right hand rule) as defined by the axis of the sheave.

    For cables running over a sheave the friction in sideways direction is considered to be infinite. The geometry is calculated such that the
    cable section between sheaves is perpendicular to the vector from the axis of the sheave to the point where the cable leaves the sheave.

    This assumption results in undefined behaviour when the axis of the sheave is parallel to the cable direction.

    Notes:
        If pois or sheaves on a cable come too close together (<1mm) then they will be pushed away from eachother.
        This prevents the unwanted situation where multiple pois end up at the same location. In that case it can not be determined which amount of force should be applied to each of the pois.


    """

    def __init__(self, scene, node):
        super().__init__(scene, node)
        self._pois = list()

    def depends_on(self):
        return [*self._pois]

    @property
    def tension(self):
        """Tension in the cable [kN]"""
        return self._vfNode.tension

    @property
    def stretch(self):
        """Stretch of the cable [m]

        Tension [kN] = EA [kN] * stretch [m] / length [m]
        """
        return self._vfNode.stretch

    @property
    def length(self):
        """Length of the cable when in rest [m]

        Tension [kN] = EA [kN] * stretch [m] / length [m]
        """
        return self._vfNode.Length

    @length.setter
    @node_setter_manageable
    @node_setter_observable
    def length(self, val):

        if val < 1e-9:
            raise Exception(
                "Length shall be more than 0 (otherwise stiffness EA/L becomes infinite)"
            )
        self._vfNode.Length = val

    @property
    def EA(self):
        """Stiffness of the cable [kN]

        Tension [kN] = EA [kN] * stretch [m] / length [m]
        """
        return self._vfNode.EA

    @EA.setter
    @node_setter_manageable
    @node_setter_observable
    def EA(self, ea):

        self._vfNode.EA = ea

    @property
    def diameter(self):
        """Diameter of the cable. Used when a cable runs over a circle. [m]"""
        return self._vfNode.diameter

    @diameter.setter
    @node_setter_manageable
    @node_setter_observable
    def diameter(self, diameter):

        self._vfNode.diameter = diameter

    @property
    def connections(self):
        """List or Tuple of nodes that this cable is connected to. Nodes may be passed by name (string) or by reference.

        Example:
            p1 = s.new_point('point 1')
            p2 = s.new_point('point 2', position = (0,0,10)
            p3 = s.new_point('point 3', position = (10,0,10)
            c1 = s.new_circle('circle 1',parent = p3, axis = (0,1,0), radius = 1)
            c = s.new_cable("cable_1", endA="Point", endB = "Circle", length = 1.2, EA = 10000)

            c.connections = ('point 1', 'point 2', 'point 3')
            # or
            c.connections = (p1, p2,p3)
            # or
            c.connections = [p1, 'point 2', p3]  # all the same

        Notes:
            1. Circles can not be used as endpoins. If one of the endpoints is a Circle then the Point that that circle
            is located on is used instead.
            2. Points should not be repeated directly.

        The following will fail:
        c.connections = ('point 1', 'point 3', 'circle 1')

        because the last point is a circle. So circle 1 will be replaced with the point that the circle is on: point 3.

        so this becomes
        ('point 1','point 3','point 3')

        this is invalid because point 3 is repeated.

        """
        return tuple(self._pois)

    @connections.setter
    @node_setter_manageable
    @node_setter_observable
    def connections(self, value):

        if len(value) < 2:
            raise ValueError("At least two connections required")

        nodes = []
        for p in value:
            n = self._scene._node_from_node_or_str(p)

            if not (isinstance(n, Point) or isinstance(n, Circle)):
                raise ValueError(
                    f"Only Sheaves and Pois can be used as connection, but {n.name} is a {type(n)}"
                )
            nodes.append(n)

        # check for repeated nodes
        n = len(nodes)
        for i in range(n - 1):
            node1 = nodes[i]
            node2 = nodes[i + 1]

            # # if first or last node is a sheave, the this will be replaced by the poi of the sheave
            # if i == 0 and isinstance(node1, Circle):
            #     node1 = node1.parent
            # if i == n - 2 and isinstance(node2, Circle):
            #     node2 = node2.parent

            if node1 == node2:
                raise ValueError(
                    f"It is not allowed to have the same node repeated - you have {node1.name} and {node2.name}"
                )

        self._pois.clear()
        self._pois.extend(nodes)
        self._update_pois()

    def get_points_for_visual(self):
        """A list of 3D locations which can be used for visualization"""
        return self._vfNode.global_points

    def _add_connection_to_core(self, connection):
        if isinstance(connection, Point):
            self._vfNode.add_connection_poi(connection._vfNode)
        if isinstance(connection, Circle):
            self._vfNode.add_connection_sheave(connection._vfNode)

    def _update_pois(self):
        self._vfNode.clear_connections()
        for point in self._pois:
            self._add_connection_to_core(point)

    def _give_poi_names(self):
        """Returns a list with the names of all the pois"""
        r = list()
        for p in self._pois:
            r.append(p.name)
        return r

    def give_python_code(self):
        code = "# code for {}".format(self.name)

        poi_names = self._give_poi_names()
        n_sheaves = len(poi_names) - 2

        code += "\ns.new_cable(name='{}',".format(self.name)
        code += "\n            endA='{}',".format(poi_names[0])
        code += "\n            endB='{}',".format(poi_names[-1])
        code += "\n            length={},".format(self.length)

        if self.diameter != 0:
            code += "\n            diameter={},".format(self.diameter)

        if len(poi_names) <= 2:
            code += "\n            EA={})".format(self.EA)
        else:
            code += "\n            EA={},".format(self.EA)

            if n_sheaves == 1:
                code += "\n            sheaves = ['{}'])".format(poi_names[1])
            else:
                code += "\n            sheaves = ['{}',".format(poi_names[1])
                for i in range(n_sheaves - 2):
                    code += "\n                       '{}',".format(poi_names[2 + i])
                code += "\n                       '{}']),".format(poi_names[-2])

        return code


class Force(NodeWithParent):
    """A Force models a force and moment on a poi.

    Both are expressed in the global axis system.

    """

    @property
    def force(self):
        """The x,y and z components of the force [kN,kN,kN] (global axis)

        Example s['wind'].force = (12,34,56)
        """
        return self._vfNode.force

    @force.setter
    @node_setter_manageable
    @node_setter_observable
    def force(self, val):

        assert3f(val)
        self._vfNode.force = val

    @property
    def fx(self):
        """The global x-component of the force [kN] (global axis)"""
        return self.force[0]

    @fx.setter
    @node_setter_manageable
    @node_setter_observable
    def fx(self, var):

        a = self.force
        self.force = (var, a[1], a[2])

    @property
    def fy(self):
        """The global y-component of the force [kN]  (global axis)"""
        return self.force[1]

    @fy.setter
    @node_setter_manageable
    @node_setter_observable
    def fy(self, var):

        a = self.force
        self.force = (a[0], var, a[2])

    @property
    def fz(self):
        """The global z-component of the force [kN]  (global axis)"""

        return self.force[2]

    @fz.setter
    @node_setter_manageable
    @node_setter_observable
    def fz(self, var):

        a = self.force
        self.force = (a[0], a[1], var)

    @property
    def moment(self):
        """The x,y and z components of the moment (kNm,kNm,kNm) in the global axis system.

        Example s['wind'].moment = (12,34,56)
        """
        return self._vfNode.moment

    @moment.setter
    @node_setter_manageable
    @node_setter_observable
    def moment(self, val):

        assert3f(val)
        self._vfNode.moment = val

    @property
    def mx(self):
        """The global x-component of the moment [kNm]  (global axis)"""
        return self.moment[0]

    @mx.setter
    @node_setter_manageable
    @node_setter_observable
    def mx(self, var):

        a = self.moment
        self.moment = (var, a[1], a[2])

    @property
    def my(self):
        """The global y-component of the moment [kNm]  (global axis)"""
        return self.moment[1]

    @my.setter
    @node_setter_manageable
    @node_setter_observable
    def my(self, var):

        a = self.moment
        self.moment = (a[0], var, a[2])

    @property
    def mz(self):
        """The global z-component of the moment [kNm]  (global axis)"""
        return self.moment[2]

    @mz.setter
    @node_setter_manageable
    @node_setter_observable
    def mz(self, var):

        a = self.moment
        self.moment = (a[0], a[1], var)

    def give_python_code(self):
        code = "# code for {}".format(self.name)

        # new_force(self, name, parent=None, force=None, moment=None):

        code += "\ns.new_force(name='{}',".format(self.name)
        code += "\n            parent='{}',".format(self.parent_for_export.name)
        code += "\n            force=({}, {}, {}),".format(*self.force)
        code += "\n            moment=({}, {}, {}) )".format(*self.moment)
        return code


class ContactMesh(NodeWithParent):
    """A ContactMesh is a tri-mesh with an axis parent"""

    def __init__(self, scene, vfContactMesh):
        super().__init__(scene, vfContactMesh)
        self._None_parent_acceptable = True
        self._trimesh = TriMeshSource(
            self._scene, self._vfNode.trimesh
        )  # the tri-mesh is wrapped in a custom object

    @property
    def trimesh(self):
        """The TriMeshSource object which can be used to change the mesh

        Example:
            s['Contactmesh'].trimesh.load_file('cube.obj', scale = (1.0,1.0,1.0), rotation = (0.0,0.0,0.0), offset = (0.0,0.0,0.0))
        """
        return self._trimesh

    def give_python_code(self):
        code = "# code for {}".format(self.name)
        code += "\nmesh = s.new_contactmesh(name='{}'".format(self.name)
        if self.parent_for_export:
            code += ", parent='{}')".format(self.parent_for_export.name)
        else:
            code += ")"
        code += "\nmesh.trimesh.load_file(r'{}', scale = ({},{},{}), rotation = ({},{},{}), offset = ({},{},{}))".format(
            self.trimesh._path,
            *self.trimesh._scale,
            *self.trimesh._rotation,
            *self.trimesh._offset,
        )

        return code


class ContactBall(NodeWithParent):
    """A ContactBall is a linear elastic ball which can contact with ContactMeshes.

    It is modelled as a sphere around a Poi. Radius and stiffness can be controlled using radius and k.

    The force is applied on the Poi and it not registered separately.
    """

    def __init__(self, scene, node):
        super().__init__(scene, node)
        self._meshes = list()

    @property
    def can_contact(self) -> bool:
        """True if the ball ball is perpendicular to at least one of the faces of one of the meshes. So when contact is possible. To check if there is contact use "force"
        See Also: Force
        """
        return self._vfNode.has_contact

    @property
    def contact_force(self) -> tuple:
        """Returns the force on the ball [kN, kN, kN] (global axis)

        The force is applied at the center of the ball

        See Also: contact_force_magnitude
        """
        return self._vfNode.force

    @property
    def contact_force_magnitude(self) -> float:
        """Returns the absolute force on the ball, if any [kN]

        The force is applied on the center of the ball

        See Also: contact_force
        """
        return np.linalg.norm(self._vfNode.force)

    @property
    def compression(self) -> float:
        """Returns the absolute compression of the ball, if any [m]"""
        return self._vfNode.force

    @property
    def contactpoint(self):
        """The nearest point on the nearest mesh. Only defined"""
        return self._vfNode.contact_point

    def update(self):
        """Updates the contact-points and applies forces on mesh and point"""
        self._vfNode.update()

    @property
    def meshes(self) -> tuple:
        """List of contact-mesh nodes.
        When getting this will yield a list of node references.
        When setting node references and node-names may be used.

        eg: ball.meshes = [mesh1, 'mesh2']
        """
        return tuple(self._meshes)

    @meshes.setter
    @node_setter_manageable
    @node_setter_observable
    def meshes(self, value):

        meshes = []

        for m in value:
            cm = self._scene._node_from_node_or_str(m)

            if not isinstance(cm, ContactMesh):
                raise ValueError(
                    f"Only ContactMesh nodes can be used as mesh, but {cm.name} is a {type(cm)}"
                )
            if cm in meshes:
                raise ValueError(f"Can not add {cm.name} twice")

            meshes.append(cm)

        # copy to meshes
        self._meshes.clear()
        self._vfNode.clear_contactmeshes()
        for mesh in meshes:
            self._meshes.append(mesh)
            self._vfNode.add_contactmesh(mesh._vfNode)

    @property
    def meshes_names(self) -> list:
        """List with the names of the meshes"""
        return [m.name for m in self._meshes]

    @property
    def radius(self):
        """Radius of the contact-ball [m]"""
        return self._vfNode.radius

    @radius.setter
    @node_setter_manageable
    @node_setter_observable
    def radius(self, value):

        assert1f_positive_or_zero(value, "radius")
        self._vfNode.radius = value
        pass

    @property
    def k(self):
        """Compression stiffness of the ball in force per meter of compression [kN/m]"""
        return self._vfNode.k

    @k.setter
    @node_setter_manageable
    @node_setter_observable
    def k(self, value):

        assert1f_positive_or_zero(value, "k")
        self._vfNode.k = value
        pass

    def give_python_code(self):
        code = "# code for {}".format(self.name)

        code += "\ns.new_contactball(name='{}',".format(self.name)
        code += "\n                  parent='{}',".format(self.parent_for_export.name)
        code += "\n                  radius={},".format(self.radius)
        code += "\n                  k={},".format(self.k)
        code += "\n                  meshes = [ "

        for m in self._meshes:
            code += '"' + m.name + '",'
        code = code[:-1] + "])"

        return code

    # =======================================================================


class SPMT(NodeWithParent):
    """An SPMT is a Self-propelled modular transporter

    These are platform vehicles

    ============  =======
    0 0 0 0 0 0   0 0 0 0

    A number of axles share a common suspension system.

    The SPMT node models such a system of axles.

    The SPMT is attached to an axis system.
    The upper locations of the axles are given as an array of 3d vectors.

    Rays are extended from these points in local -Z direction (down) until they hit a contact-shape.

    If no contact shape is found (or not within the maximum distance per axles) then the maximum defined extension for that axle is used.

    A shared pressure is obtained from the combination of all individual extensions.

    Finally an equal force is applied on all the axle connection points. This force acts in local Z direction.

    """

    def __init__(self, scene, node):
        super().__init__(scene, node)
        self._meshes = list()

    # read-only

    @property
    def axle_force(self) -> tuple:
        """Returns the force on each of the axles [kN, kN, kN] (global axis)"""
        return self._vfNode.force

    @property
    def compression(self) -> float:
        """Returns the total compression of all the axles together [m]"""
        return self._vfNode.compression

    def get_actual_global_points(self):
        """Returns a list of points: axle1, bottom wheels 1, axle2, bottom wheels 2, etc"""
        gp = self._vfNode.actual_global_points

        pts = []
        n2 = int(len(gp) / 2)
        for i in range(n2):
            pts.append(gp[2 * i + 1])
            pts.append(gp[2 * i])

            if i < n2 - 1:
                pts.append(gp[2 * i + 2])

        return pts

    # controllable

    # name is derived
    # parent is derived

    @property
    def k(self):
        """Compression stiffness of the ball in force per meter of compression [kN/m]"""
        return self._vfNode.k

    @k.setter
    @node_setter_manageable
    @node_setter_observable
    def k(self, value):

        assert1f_positive_or_zero(value, "k")
        self._vfNode.k = value
        pass

    @property
    def nominal_length(self):
        """Average Axle extension (defined point to bottom of wheel) for zero force [m]"""
        return self._vfNode.nominal_length

    @nominal_length.setter
    @node_setter_manageable
    @node_setter_observable
    def nominal_length(self, value):

        assert1f_positive_or_zero(value, "nominal_length")
        self._vfNode.nominal_length = value
        pass

    @property
    def max_length(self):
        """Maximum axle extension per axle (defined point to bottom of wheel) [m]"""
        return self._vfNode.max_length

    @max_length.setter
    @node_setter_manageable
    @node_setter_observable
    def max_length(self, value):

        assert1f_positive_or_zero(value, "max_length")
        self._vfNode.max_length = value
        pass

    # === control meshes ====

    @property
    def meshes(self) -> tuple:
        """List of contact-mesh nodes.
        When getting this will yield a list of node references.
        When setting node references and node-names may be used.

        eg: ball.meshes = [mesh1, 'mesh2']
        """
        return tuple(self._meshes)

    @meshes.setter
    @node_setter_manageable
    @node_setter_observable
    def meshes(self, value):

        meshes = []

        for m in value:
            cm = self._scene._node_from_node_or_str(m)

            if not isinstance(cm, ContactMesh):
                raise ValueError(
                    f"Only ContactMesh nodes can be used as mesh, but {cm.name} is a {type(cm)}"
                )
            if cm in meshes:
                raise ValueError(f"Can not add {cm.name} twice")

            meshes.append(cm)

        # copy to meshes
        self._meshes.clear()
        self._vfNode.clear_contact_meshes()
        for mesh in meshes:
            self._meshes.append(mesh)
            self._vfNode.add_contact_mesh(mesh._vfNode)

    @property
    def meshes_names(self) -> list:
        """List with the names of the meshes"""
        return [m.name for m in self._meshes]

    # === control axles ====

    def make_grid(self, nx=3, ny=1, dx=1.4, dy=1.45):
        offx = nx * dx / 2
        offy = ny * dy / 2
        self._vfNode.clear_axles()

        for ix in range(nx):
            for iy in range(ny):
                self._vfNode.add_axle(ix * dx - offx, iy * dy - offy, 0)

    @property
    def axles(self):
        """Axles is a list axle positions. Each entry is a (x,y,z) entry which determines the location of the axle on
        SPMT. This is relative to the parent of the SPMT.

        Example:
            [(-10,0,0),(-5,0,0),(0,0,0)] for three axles
        """
        return self._vfNode.get_axles()

    @axles.setter
    @node_setter_manageable
    @node_setter_observable
    def axles(self, value):
        self._vfNode.clear_axles()
        for v in value:
            assert3f(v, "Each entry should contain three floating point numbers")
            self._vfNode.add_axle(*v)

    # actions

    def update(self):
        """Updates the contact-points and applies forces on mesh and point"""
        self._vfNode.update()

    def give_python_code(self):
        code = "# code for {}".format(self.name)

        code += "\ns.new_spmt(name='{}',".format(self.name)
        code += "\n                  parent='{}',".format(self.parent_for_export.name)
        code += "\n                  maximal_length={},".format(self.max_length)
        code += "\n                  nominal_length={},".format(self.nominal_length)
        code += "\n                  k={},".format(self.k)
        code += "\n                  meshes = [ "

        for m in self._meshes:
            code += '"' + m.name + '",'
        code = code[:-1] + "],"

        code += "\n                  axles = [ "

        for p in self.axles:
            code += f"({p[0]}, {p[1]}, {p[2]}),"

        code = code[:-1] + "])"

        return code


class Circle(NodeWithParent):
    """A Circle models a circle shape based on a diameter and an axis direction"""

    @property
    def axis(self) -> tuple:
        """Direction of the sheave axis (x,y,z) in parent axis system.

        Note:
            The direction of the axis is also used to determine the positive direction over the circumference of the
            circle. This is then used when cables run over the circle or the circle is used for geometric contacts. So
            if a cable runs over the circle in the wrong direction then a solution is to change the axis direction to
            its opposite:  circle.axis =- circle.axis. (another solution in that case is to define the connections of
            the cable in the reverse order)
        """
        return self._vfNode.axis_direction

    @axis.setter
    @node_setter_manageable
    @node_setter_observable
    def axis(self, val):

        assert3f(val)
        if np.linalg.norm(val) == 0:
            raise ValueError("Axis can not be 0,0,0")
        self._vfNode.axis_direction = val

    @property
    def radius(self):
        """Radius of the circle [m]"""
        return self._vfNode.radius

    @radius.setter
    @node_setter_manageable
    @node_setter_observable
    def radius(self, val):

        assert1f(val)
        self._vfNode.radius = val

    def give_python_code(self):
        code = "# code for {}".format(self.name)
        code += "\ns.new_circle(name='{}',".format(self.name)
        code += "\n            parent='{}',".format(self.parent_for_export.name)
        code += "\n            axis=({}, {}, {}),".format(*self.axis)
        code += "\n            radius={} )".format(self.radius)
        return code

    @property
    def global_position(self):
        """Returns the global position of the center of the sheave.

        Note: this is the same as the global position of the parent point.
        """
        return self.parent.global_position


class HydSpring(NodeWithParent):
    """A HydSpring models a linearized hydrostatic spring.

    The cob (center of buoyancy) is defined in the parent axis system.
    All other properties are defined relative to the cob.

    """

    @property
    def cob(self):
        """Center of buoyancy in parent axis system (m,m,m)"""
        return self._vfNode.position

    @cob.setter
    @node_setter_manageable
    @node_setter_observable
    def cob(self, val):

        assert3f(val)
        self._vfNode.position = val

    @property
    def BMT(self):
        """Vertical distance between cob and metacenter for roll [m]"""
        return self._vfNode.BMT

    @BMT.setter
    @node_setter_manageable
    @node_setter_observable
    def BMT(self, val):

        self._vfNode.BMT = val

    @property
    def BML(self):
        """Vertical distance between cob and metacenter for pitch [m]"""
        return self._vfNode.BML

    @BML.setter
    @node_setter_manageable
    @node_setter_observable
    def BML(self, val):

        self._vfNode.BML = val

    @property
    def COFX(self):
        """Horizontal x-position Center of Floatation (center of waterplane area), relative to cob [m]"""
        return self._vfNode.COFX

    @COFX.setter
    @node_setter_manageable
    @node_setter_observable
    def COFX(self, val):

        self._vfNode.COFX = val

    @property
    def COFY(self):
        """Horizontal y-position Center of Floatation (center of waterplane area), relative to cob [m]"""
        return self._vfNode.COFY

    @COFY.setter
    @node_setter_manageable
    @node_setter_observable
    def COFY(self, val):

        self._vfNode.COFY = val

    @property
    def kHeave(self):
        """Heave stiffness [kN/m]"""
        return self._vfNode.kHeave

    @kHeave.setter
    @node_setter_manageable
    @node_setter_observable
    def kHeave(self, val):

        self._vfNode.kHeave = val

    @property
    def waterline(self):
        """Waterline-elevation relative to cob for un-stretched heave-spring. Positive if cob is below the waterline (which is where is normally is) [m]"""
        return self._vfNode.waterline

    @waterline.setter
    @node_setter_manageable
    @node_setter_observable
    def waterline(self, val):

        self._vfNode.waterline = val

    @property
    def displacement_kN(self):
        """Displacement when waterline is at waterline-elevation [kN]"""
        return self._vfNode.displacement_kN

    @displacement_kN.setter
    @node_setter_manageable
    @node_setter_observable
    def displacement_kN(self, val):

        self._vfNode.displacement_kN = val

    def give_python_code(self):
        code = "# code for {}".format(self.name)

        # new_force(self, name, parent=None, force=None, moment=None):

        code += "\ns.new_hydspring(name='{}',".format(self.name)
        code += "\n            parent='{}',".format(self.parent_for_export.name)
        code += "\n            cob=({}, {}, {}),".format(*self.cob)
        code += "\n            BMT={},".format(self.BMT)
        code += "\n            BML={},".format(self.BML)
        code += "\n            COFX={},".format(self.COFX)
        code += "\n            COFY={},".format(self.COFY)
        code += "\n            kHeave={},".format(self.kHeave)
        code += "\n            waterline={},".format(self.waterline)
        code += "\n            displacement_kN={} )".format(self.displacement_kN)

        return code


class LC6d(CoreConnectedNode):
    """A LC6d models a Linear Connector with 6 dofs.

    It connects two Axis elements with six linear springs.

    The first axis system is called "main", the second is called "secondary". The difference is that
    the "main" axis system is used for the definition of the stiffness values.

    The translational-springs are easy. The rotational springs may not be as intuitive. They are defined as:

      - rotation_x = arc-tan ( uy[0] / uy[1] )
      - rotation_y = arc-tan ( -ux[0] / ux[2] )
      - rotation_z = arc-tan ( ux[0] / ux [1] )

    which works fine for small rotations and rotations about only a single axis.

    Tip:
    It is better to use use the "fixed" property of axis systems to create joints.

    """

    def __init__(self, scene, node):
        super().__init__(scene, node)
        self._main = None
        self._secondary = None

    def depends_on(self):
        return [self._main, self._secondary]

    @property
    def stiffness(self):
        """Stiffness of the connector: kx, ky, kz, krx, kry, krz in [kN/m and kNm/rad] (axis system of the main axis)"""
        return self._vfNode.stiffness

    @stiffness.setter
    @node_setter_manageable
    @node_setter_observable
    def stiffness(self, val):

        self._vfNode.stiffness = val

    @property
    def main(self):
        """Main axis system. This axis system dictates the axis system that the stiffness is expressed in"""
        return self._main

    @main.setter
    @node_setter_manageable
    @node_setter_observable
    def main(self, val):

        val = self._scene._node_from_node_or_str(val)
        if not isinstance(val, Axis):
            raise TypeError("Provided nodeA should be a Axis")

        self._main = val
        self._vfNode.master = val._vfNode

    @property
    def secondary(self):
        """Secondary (connected) axis system"""
        return self._secondary

    @secondary.setter
    @node_setter_manageable
    @node_setter_observable
    def secondary(self, val):

        val = self._scene._node_from_node_or_str(val)
        if not isinstance(val, Axis):
            raise TypeError("Provided nodeA should be a Axis")

        self._secondary = val
        self._vfNode.slave = val._vfNode

    @property
    def fgx(self):
        """Force on main in global coordinate frame [kN]"""
        return self._vfNode.global_force[0]

    @property
    def fgy(self):
        """Force on main in global coordinate frame [kN]"""
        return self._vfNode.global_force[1]

    @property
    def fgz(self):
        """Force on main in global coordinate frame [kN]"""
        return self._vfNode.global_force[2]

    @property
    def force_global(self):
        """Force on main in global coordinate frame [kN]"""
        return self._vfNode.global_force

    @property
    def mgx(self):
        """Moment on main in global coordinate frame [kNm]"""
        return self._vfNode.global_moment[0]

    @property
    def mgy(self):
        """Moment on main in global coordinate frame [kNm]"""
        return self._vfNode.global_moment[1]

    @property
    def mgz(self):
        """Moment on main in global coordinate frame [kNm]"""
        return self._vfNode.global_moment[2]

    @property
    def moment_global(self):
        """Moment on main in global coordinate frame [kNm]"""
        return self._vfNode.global_moment

    def give_python_code(self):
        code = "# code for {}".format(self.name)

        code += "\ns.new_linear_connector_6d(name='{}',".format(self.name)
        code += "\n            main='{}',".format(self.main.name)
        code += "\n            secondary='{}',".format(self.secondary.name)
        code += "\n            stiffness=({}, {}, {}, ".format(*self.stiffness[:3])
        code += "\n                       {}, {}, {}) )".format(*self.stiffness[3:])

        return code


class Connector2d(CoreConnectedNode):
    """A Connector2d linear connector with acts both on linear displacement and angular displacement.

    * the linear stiffness is defined by k_linear and is defined over the actual shortest direction between nodeA and nodeB.
    * the angular stiffness is defined by k_angular and is defined over the actual smallest angle between the two systems.
    """

    def __init__(self, scene, node):
        super().__init__(scene, node)
        self._nodeA = None
        self._nodeB = None

    def depends_on(self):
        return [self._nodeA, self._nodeB]

    @property
    def angle(self):
        """Actual angle between nodeA and nodeB [deg] (read-only)"""
        return np.rad2deg(self._vfNode.angle)

    @property
    def force(self):
        """Actual force between nodeA and nodeB [kN] (read-only)"""
        return self._vfNode.force

    @property
    def moment(self):
        """Actual moment between nodeA and nodeB [kNm] (read-only)"""
        return self._vfNode.moment

    @property
    def axis(self):
        """Actual rotation axis between nodeA and nodeB (read-only)"""
        return self._vfNode.axis

    @property
    def ax(self):
        """X component of actual rotation axis between nodeA and nodeB (read-only)"""
        return self._vfNode.axis[0]

    @property
    def ay(self):
        """Y component of actual rotation axis between nodeA and nodeB (read-only)"""
        return self._vfNode.axis[1]

    @property
    def az(self):
        """Z component of actual rotation axis between nodeA and nodeB (read-only)"""
        return self._vfNode.axis[2]

    @property
    def k_linear(self):
        """Linear stiffness [kN/m]"""
        return self._vfNode.k_linear

    @k_linear.setter
    @node_setter_manageable
    @node_setter_observable
    def k_linear(self, value):

        self._vfNode.k_linear = value

    @property
    def k_angular(self):
        """Angular stiffness [kNm/rad]"""
        return self._vfNode.k_angular

    @k_angular.setter
    @node_setter_manageable
    @node_setter_observable
    def k_angular(self, value):

        self._vfNode.k_angular = value

    @property
    def nodeA(self) -> Axis:
        """Connected axis system A"""
        return self._nodeA

    @nodeA.setter
    @node_setter_manageable
    @node_setter_observable
    def nodeA(self, val):

        val = self._scene._node_from_node_or_str(val)
        if not isinstance(val, Axis):
            raise TypeError("Provided nodeA should be a Axis")

        self._nodeA = val
        self._vfNode.master = val._vfNode

    @property
    def nodeB(self) -> Axis:
        """Connected axis system B"""
        return self._nodeB

    @nodeB.setter
    @node_setter_manageable
    @node_setter_observable
    def nodeB(self, val):

        val = self._scene._node_from_node_or_str(val)
        if not isinstance(val, Axis):
            raise TypeError("Provided nodeA should be a Axis")

        self._nodeB = val
        self._vfNode.slave = val._vfNode

    def give_python_code(self):
        code = "# code for {}".format(self.name)

        code += "\ns.new_connector2d(name='{}',".format(self.name)
        code += "\n            nodeA='{}',".format(self.nodeA.name)
        code += "\n            nodeB='{}',".format(self.nodeB.name)
        code += "\n            k_linear ={},".format(self.k_linear)
        code += "\n            k_angular ={})".format(self.k_angular)

        return code


class Beam(CoreConnectedNode):
    """A LinearBeam models a FEM-like linear beam element.

    A LinearBeam node connects two Axis elements

    By definition the beam runs in the X-direction of the nodeA axis system. So it may be needed to create a
    dedicated Axis element for the beam to control the orientation.

    The beam is defined using the following properties:

    *  EIy  - bending stiffness about y-axis
    *  EIz  - bending stiffness about z-axis
    *  GIp  - torsional stiffness about x-axis
    *  EA   - axis stiffness in x-direction
    *  L    - the un-stretched length of the beam
    *  mass - mass of the beam in [mT]

    The beam element is in rest if the nodeB axis system

    1. has the same global orientation as the nodeA system
    2. is at global position equal to the global position of local point (L,0,0) of the nodeA axis. (aka: the end of the beam)

    The scene.new_linearbeam automatically creates a dedicated axis system for each end of the beam. The orientation of this axis-system
    is determined as follows:

    First the direction from nodeA to nodeB is determined: D
    The axis of rotation is the cross-product of the unit x-axis and D    AXIS = ux x D
    The angle of rotation is the angle between the nodeA x-axis and D
    The rotation about the rotated X-axis is undefined.

    """

    def __init__(self, scene, node):
        super().__init__(scene, node)
        self._nodeA = None
        self._nodeB = None

    def depends_on(self):
        return [self._nodeA, self._nodeB]

    @property
    def n_segments(self):
        return self._vfNode.nSegments

    @n_segments.setter
    @node_setter_manageable
    @node_setter_observable
    def n_segments(self, value):
        if value < 1:
            raise ValueError("Number of segments in beam should be 1 or more")
        self._vfNode.nSegments = int(value)

    @property
    def EIy(self):
        """E * Iyy : bending stiffness in the XZ plane [kN m2]

        E is the modulus of elasticity; for steel 190-210 GPa (10^6 kN/m2)
        Iyy is the cross section moment of inertia [m4]
        """
        return self._vfNode.EIy

    @EIy.setter
    @node_setter_manageable
    @node_setter_observable
    def EIy(self, value):

        self._vfNode.EIy = value

    @property
    def EIz(self):
        """E * Izz : bending stiffness in the XY plane [kN m2]

        E is the modulus of elasticity; for steel 190-210 GPa (10^6 kN/m2)
        Iyy is the cross section moment of inertia [m4]
        """
        return self._vfNode.EIz

    @EIz.setter
    @node_setter_manageable
    @node_setter_observable
    def EIz(self, value):

        self._vfNode.EIz = value

    @property
    def GIp(self):
        """G * Ipp : torsional stiffness about the X (length) axis [kN m2]

        G is the shear-modulus of elasticity; for steel 75-80 GPa (10^6 kN/m2)
        Ip is the cross section polar moment of inertia [m4]
        """
        return self._vfNode.GIp

    @GIp.setter
    @node_setter_manageable
    @node_setter_observable
    def GIp(self, value):

        self._vfNode.GIp = value

    @property
    def EA(self):
        """E * A : stiffness in the length direction [kN]

        E is the modulus of elasticity; for steel 190-210 GPa (10^6 kN/m2)
        A is the cross-section area in [m2]
        """
        return self._vfNode.EA

    @EA.setter
    @node_setter_manageable
    @node_setter_observable
    def EA(self, value):

        self._vfNode.EA = value

    @property
    def tension_only(self):
        """axial stiffness (EA) only applicable to tension [True/False]"""
        return self._vfNode.tensionOnly

    @tension_only.setter
    @node_setter_manageable
    @node_setter_observable
    def tension_only(self, value):
        assert isinstance(value, bool), ValueError(
            "Value for tension_only shall be True or False"
        )
        self._vfNode.tensionOnly = value

    @property
    def mass(self):
        """Mass of the beam in [mT]"""
        return self._vfNode.Mass

    @mass.setter
    @node_setter_manageable
    @node_setter_observable
    def mass(self, value):

        assert1f(value, "Mass shall be a number")
        self._vfNode.Mass = value
        pass

    @property
    def L(self):
        """Length of the beam in unloaded condition [m]"""
        return self._vfNode.L

    @L.setter
    @node_setter_manageable
    @node_setter_observable
    def L(self, value):

        self._vfNode.L = value

    @property
    def nodeA(self):
        """The axis system that the A-end of the beam is connected to. The beam leaves this axis system along the X-axis"""
        return self._nodeA

    @nodeA.setter
    @node_setter_manageable
    @node_setter_observable
    def nodeA(self, val):

        val = self._scene._node_from_node_or_str(val)

        if not isinstance(val, Axis):
            raise TypeError("Provided nodeA should be a Axis")

        self._nodeA = val
        self._vfNode.master = val._vfNode

    @property
    def nodeB(self):
        """The axis system that the B-end of the beam is connected to. The beam arrives at this axis system along the X-axis"""
        return self._nodeB

    @nodeB.setter
    @node_setter_manageable
    @node_setter_observable
    def nodeB(self, val):

        val = self._scene._node_from_node_or_str(val)
        if not isinstance(val, Axis):
            raise TypeError("Provided nodeA should be a Axis")

        self._nodeB = val
        self._vfNode.slave = val._vfNode

    # read-only
    @property
    def moment_A(self):
        """Moment on beam at node A (kNm, kNm, kNm) , axis system of node A"""
        return self._vfNode.moment_on_master

    @property
    def moment_B(self):
        """Moment on beam at node B (kNm, kNm, kNm) , axis system of node B"""
        return self._vfNode.moment_on_slave

    @property
    def tension(self):
        """Tension in the beam [kN], negative for compression

        tension is calculated at the midpoints of the beam segments.
        """
        return self._vfNode.tension

    @property
    def torsion(self):
        """Torsion moment [kNm]. Positive if end B has a positive rotation about the x-axis of end A

        torsion is calculated at the midpoints of the beam segments.
        """
        return self._vfNode.torsion

    @property
    def X_nodes(self):
        """Returns the x-positions of the end nodes and internal nodes along the length of the beam [m]"""
        return self._vfNode.x

    @property
    def X_midpoints(self):
        """X-positions of the beam centers measured along the length of the beam [m]"""
        return tuple(
            0.5 * (np.array(self._vfNode.x[:-1]) + np.array(self._vfNode.x[1:]))
        )

    @property
    def global_positions(self):
        """Global-positions of the end nodes and internal nodes [m,m,m]"""
        return np.array(self._vfNode.global_position, dtype=float)

    @property
    def global_orientations(self):
        """Global-orientations of the end nodes and internal nodes [deg,deg,deg]"""
        return np.rad2deg(self._vfNode.global_orientation)

    @property
    def bending(self):
        """Bending forces of the end nodes and internal nodes [0, kNm, kNm]"""
        return np.array(self._vfNode.bending)

    def give_python_code(self):
        code = "# code for beam {}".format(self.name)
        code += "\ns.new_beam(name='{}',".format(self.name)
        code += "\n            nodeA='{}',".format(self.nodeA.name)
        code += "\n            nodeB='{}',".format(self.nodeB.name)
        code += "\n            n_segments={},".format(self.n_segments)
        code += "\n            tension_only={},".format(self.tension_only)
        code += "\n            EIy ={},".format(self.EIy)
        code += "\n            EIz ={},".format(self.EIz)
        code += "\n            GIp ={},".format(self.GIp)
        code += "\n            EA ={},".format(self.EA)
        code += "\n            mass ={},".format(self.mass)
        code += "\n            L ={}) # L can possibly be omitted".format(self.L)

        return code


class TriMeshSource(Node):
    """
    TriMesh

    A TriMesh node contains triangular mesh which can be used for buoyancy or contact

    """

    def __init__(self, scene, source):

        super().__init__(scene)

        # Note: Visual does not have a corresponding vfCore Node in the scene but does have a vfCore
        self._TriMesh = source
        self._new_mesh = True  # cheat for visuals

        self._path = ""  # stores the data that was used to load the obj
        self._offset = (0, 0, 0)
        self._scale = (1, 1, 1)
        self._rotation = (0, 0, 0)

        self._invert_normals = False

    def depends_on(self) -> list:
        return []

    def AddVertex(self, x, y, z):
        """Adds a vertex (point)"""
        self._TriMesh.AddVertex(x, y, z)

    def AddFace(self, i, j, k):
        """Adds a triangular face between vertex numbers i,j and k"""
        self._TriMesh.AddFace(i, j, k)

    def get_extends(self):
        """Returns the extends of the mesh in global coordinates

        Returns: (minimum_x, maximum_x, minimum_y, maximum_y, minimum_z, maximum_z)

        """

        t = self._TriMesh

        if t.nFaces == 0:
            return (0, 0, 0, 0, 0, 0)

        v = t.GetVertex(0)
        xn = v[0]
        xp = v[0]
        yn = v[1]
        yp = v[1]
        zn = v[2]
        zp = v[2]

        for i in range(t.nVertices):
            v = t.GetVertex(i)
            x = v[0]
            y = v[1]
            z = v[2]

            if x < xn:
                xn = x
            if x > xp:
                xp = x
            if y < yn:
                yn = y
            if y > yp:
                yp = y
            if z < zn:
                zn = z
            if z > zp:
                zp = z

        return (xn, xp, yn, yp, zn, zp)

    def _fromVTKpolydata(
        self, polydata, offset=None, rotation=None, scale=None, invert_normals=False
    ):

        import vtk

        tri = vtk.vtkTriangleFilter()

        tri.SetInputConnection(polydata)

        scaleFilter = vtk.vtkTransformPolyDataFilter()
        rotationFilter = vtk.vtkTransformPolyDataFilter()

        s = vtk.vtkTransform()
        s.Identity()
        r = vtk.vtkTransform()
        r.Identity()

        rotationFilter.SetInputConnection(tri.GetOutputPort())
        scaleFilter.SetInputConnection(rotationFilter.GetOutputPort())

        if scale is not None:
            s.Scale(*scale)

        if rotation is not None:
            q = rotation
            angle = (q[0] ** 2 + q[1] ** 2 + q[2] ** 2) ** (0.5)
            if angle > 0:
                r.RotateWXYZ(angle, q[0] / angle, q[1] / angle, q[2] / angle)

        if offset is None:
            offset = [0, 0, 0]

        scaleFilter.SetTransform(s)
        rotationFilter.SetTransform(r)

        scaleFilter.Update()
        data = scaleFilter.GetOutput()
        self._TriMesh.Clear()

        for i in range(data.GetNumberOfPoints()):
            point = data.GetPoint(i)
            self._TriMesh.AddVertex(
                point[0] + offset[0], point[1] + offset[1], point[2] + offset[2]
            )

        for i in range(data.GetNumberOfCells()):
            cell = data.GetCell(i)

            if isinstance(cell, vtk.vtkLine):
                print("Cell nr {} is a line, not adding to mesh".format(i))
                continue

            if isinstance(cell, vtk.vtkVertex):
                print("Cell nr {} is a vertex, not adding to mesh".format(i))
                continue

            id0 = cell.GetPointId(0)
            id1 = cell.GetPointId(1)
            id2 = cell.GetPointId(2)

            if invert_normals:
                self._TriMesh.AddFace(id2, id1, id0)
            else:
                self._TriMesh.AddFace(id0, id1, id2)

        # check if anything was loaded
        if self._TriMesh.nFaces == 0:
            raise Exception(
                "No faces in poly-data - no geometry added (hint: empty obj file?)"
            )
        self._new_mesh = True
        self._scene.update()

    def check_shape(self):
        """Performs some checks on the shape in the trimesh
        - Boundary edges (edge with only one face attached)
        - Non-manifold edges (edit with more than two faces attached)
        - Volume should be positive
        """

        tm = self._TriMesh

        if tm.nFaces == 0:
            return ["No mesh"]

        # Make a list of all boundaries using their vertex IDs
        boundaries = np.zeros((3 * tm.nFaces, 2))
        for i in range(tm.nFaces):
            face = tm.GetFace(i)
            boundaries[3 * i] = [face[0], face[1]]
            boundaries[3 * i + 1] = [face[1], face[2]]
            boundaries[3 * i + 2] = [face[2], face[0]]

        # For an edge is doesn't matter in which direction it runs
        boundaries.sort(axis=1)

        rows_occurance_count = np.unique(boundaries, axis=0, return_counts=True)[
            1
        ]  # count of rows

        n_boundary = np.count_nonzero(rows_occurance_count == 1)
        n_nonmanifold = np.count_nonzero(rows_occurance_count > 2)

        messages = []

        if n_boundary > 0:
            messages.append(f"Mesh contains {n_boundary} boundary edges")
        if n_nonmanifold > 0:
            messages.append(f"Mesh contains {n_nonmanifold} non-manifold edges")

        # Do not check for volume if we have nonmanifold geometry or boundary edges
        try:
            volume = tm.Volume()
        except:
            volume = 1  # no available in every pyo3d yet

        if volume < 0:
            messages.append(
                f"Total mesh volume is negative ({volume:.2f} m3 of enclosed volume)."
            )
            messages.append("Hint: Use invert-normals")

        return messages

    def load_vtk_polydataSource(self, polydata):
        """Fills the triangle data from a vtk polydata such as a cubeSource.

        The vtk TriangleFilter is used to triangulate the source

        Examples:
            cube = vtk.vtkCubeSource()
            cube.SetXLength(122)
            cube.SetYLength(38)
            cube.SetZLength(10)
            trimesh.load_vtk_polydataSource(cube)
        """

        self._fromVTKpolydata(polydata.GetOutputPort())

    def load_obj(
        self, filename, offset=None, rotation=None, scale=None, invert_normals=False
    ):
        self.load_file(filename, offset, rotation, scale, invert_normals)

    def load_file(
        self, url, offset=None, rotation=None, scale=None, invert_normals=False
    ):
        """Loads an .obj or .stl file and and triangulates it.

        Order of modifications:

        1. rotate
        2. scale
        3. offset

        Args:
            url: (str or path or resource): file to load
            offset: : offset
            rotation:  : rotation
            scale:  scale

        """

        self._path = str(url)

        filename = str(self._scene.get_resource_path(url))

        import vtk

        ext = filename.lower()[-3:]
        if ext == "obj":
            obj = vtk.vtkOBJReader()
            obj.SetFileName(filename)
        elif ext == "stl":
            obj = vtk.vtkSTLReader()
            obj.SetFileName(filename)
        else:
            raise ValueError(
                f"File should be an .obj or .stl file but has extension {ext}"
            )

        # Add cleaning
        cln = vtk.vtkCleanPolyData()
        cln.SetInputConnection(obj.GetOutputPort())

        self._fromVTKpolydata(
            cln.GetOutputPort(),
            offset=offset,
            rotation=rotation,
            scale=scale,
            invert_normals=invert_normals,
        )

        self._scale = scale
        self._offset = offset
        self._rotation = rotation

        if self._scale is None:
            self._scale = (1.0, 1.0, 1.0)
        if self._offset is None:
            self._offset = (0.0, 0.0, 0.0)
        if self._rotation is None:
            self._rotation = (0.0, 0.0, 0.0)
        self._invert_normals = invert_normals

    def _load_from_privates(self):
        """(Re)Loads the mesh using the values currently stored in _scale, _offset, _rotation and _invert_normals"""
        self.load_file(url = self._path,
                       scale=self._scale,
                       offset=self._offset,
                       rotation=self._rotation,
                       invert_normals=self._invert_normals)


    def give_python_code(self):
        code = "# No code generated for TriMeshSource"
        return code

    # def change_parent_to(self, new_parent):
    #
    #     if not (isinstance(new_parent, Axis) or new_parent is None):
    #         raise ValueError('Visuals can only be attached to an axis (or derived) or None')
    #
    #     # get current position and orientation
    #     if self.parent is not None:
    #         cur_position = self.parent.to_glob_position(self.offset)
    #         cur_rotation = self.parent.to_glob_direction(self.rotation)
    #     else:
    #         cur_position = self.offset
    #         cur_rotation = self.rotation
    #
    #     self.parent = new_parent
    #
    #     if new_parent is None:
    #         self.offset = cur_position
    #         self.rotation = cur_rotation
    #     else:
    #         self.offset = new_parent.to_loc_position(cur_position)
    #         self.rotation = new_parent.to_loc_direction(cur_rotation)


class Buoyancy(NodeWithParent):
    """Buoyancy provides a buoyancy force based on a buoyancy mesh. The mesh is triangulated and chopped at the instantaneous flat water surface. Buoyancy is applied as an upwards force that the center of buoyancy.
    The calculation of buoyancy is as accurate as the provided geometry.

    There as no restrictions to the size or aspect ratio of the panels. It is excellent to model as box using 6 faces. Using smaller panels has a negative effect on performance.

    The normals of the panels should point towards to water.
    """

    # init parent and name are fully derived from NodeWithParent
    # _vfNode is a buoyancy
    def __init__(self, scene, vfBuoyancy):
        super().__init__(scene, vfBuoyancy)
        self._None_parent_acceptable = False
        self._trimesh = TriMeshSource(
            self._scene, self._vfNode.trimesh
        )  # the tri-mesh is wrapped in a custom object

    def update(self):
        self._vfNode.reloadTrimesh()

    @property
    def trimesh(self) -> TriMeshSource:
        return self._trimesh

    @property
    def cob(self):
        """GLOBAL position of the center of buoyancy [m,m,m] (global axis)"""
        return self._vfNode.cob

    @property
    def cob_local(self):
        """Position of the center of buoyancy [m,m,m] (local axis)"""

        return self.parent.to_loc_position(self.cob)

    @property
    def displacement(self):
        """Displaced volume of fluid [m^3]"""
        return self._vfNode.displacement

    @property
    def density(self):
        """Density of surrounding fluid [mT/m3].
        Typical values: Seawater = 1.025, fresh water = 1.00
        """
        return self._vfNode.density

    @density.setter
    @node_setter_manageable
    @node_setter_observable
    def density(self, value):
        assert1f_positive_or_zero(value, "density")
        self._vfNode.density = value

    def give_python_code(self):
        code = "# code for {}".format(self.name)
        code += "\nmesh = s.new_buoyancy(name='{}',".format(self.name)

        if self.density != 1.025:
            code += f"\n          density={self.density},"

        code += "\n          parent='{}')".format(self.parent_for_export.name)

        if self.trimesh._invert_normals:
            code += "\nmesh.trimesh.load_file(r'{}', scale = ({},{},{}), rotation = ({},{},{}), offset = ({},{},{}), invert_normals=True)".format(
                self.trimesh._path,
                *self.trimesh._scale,
                *self.trimesh._rotation,
                *self.trimesh._offset,
            )
        else:
            code += "\nmesh.trimesh.load_file(r'{}', scale = ({},{},{}), rotation = ({},{},{}), offset = ({},{},{}))".format(
                self.trimesh._path,
                *self.trimesh._scale,
                *self.trimesh._rotation,
                *self.trimesh._offset,
            )

        return code


class Tank(NodeWithParent):
    """Tank provides a fillable tank based on a mesh. The mesh is triangulated and chopped at the instantaneous flat fluid surface. Gravity is applied as an downwards force that the center of fluid.
    The calculation of fluid volume and center is as accurate as the provided geometry.

    There as no restrictions to the size or aspect ratio of the panels. It is excellent to model as box using 6 faces. Using smaller panels has a negative effect on performance.

    The normals of the panels should point *away* from the fluid. This means that the same basic shapes can be used for both buoyancy and tanks.
    """

    # init parent and name are fully derived from NodeWithParent
    # _vfNode is a tank
    def __init__(self, scene, vfTank):
        super().__init__(scene, vfTank)
        self._None_parent_acceptable = False
        self._trimesh = TriMeshSource(
            self._scene, self._vfNode.trimesh
        )  # the tri-mesh is wrapped in a custom object

        self._inertia = scene._vfc.new_pointmass(
            self.name + vfc.VF_NAME_SPLIT + "inertia"
        )

    def update(self):
        self._vfNode.reloadTrimesh()

        # update inertia
        self._inertia.parent = self.parent._vfNode
        self._inertia.position = self.cog_local
        self._inertia.inertia = self.volume * self.density


    def _delete_vfc(self):
        self._scene._vfc.delete(self._inertia.name)
        super()._delete_vfc()

    @property
    def trimesh(self) -> TriMeshSource:
        return self._trimesh

    @property
    def free_flooding(self):
        return self._vfNode.free_flooding

    @free_flooding.setter
    def free_flooding(self, value):
        assert isinstance(value, bool), ValueError(
            f"free_flooding shall be a bool, you passed a {type(value)}"
        )
        self._vfNode.free_flooding = value

    @property
    def permeability(self):
        """Permeability is the fraction of the enclosed volume that can be filled with fluid [-]"""
        return self._vfNode.permeability

    @permeability.setter
    def permeability(self, value):
        assert1f_positive_or_zero(value)
        self._vfNode.permeability = value

    @property
    def cog(self):
        """Returns the GLOBAL position of the center of volume / gravity"""
        return self._vfNode.cog

    @property
    def cog_local(self):
        """Returns the local position of the center of gravity"""
        return self.parent.to_loc_position(self.cog)

    @property
    def cog_when_full(self):
        """Returns the LOCAL position of the center of volume / gravity of the tank when it is filled"""
        return self._vfNode.cog_when_full

    @property
    def fill_pct(self):
        """Amount of volume in tank as percentage of capacity [%]"""
        if self.capacity == 0:
            return 0
        return 100 * self.volume / self.capacity

    @fill_pct.setter
    @node_setter_manageable
    @node_setter_observable
    def fill_pct(self, value):

        if value < 0 and value > -0.01:
            value = 0

        assert1f_positive_or_zero(value)

        if value > 100.1:
            raise ValueError(
                f"Fill percentage should be between 0 and 100 [%], {value} is not valid"
            )
        if value > 100:
            value = 100
        self.volume = value * self.capacity / 100

    @property
    def level_global(self):
        """The fluid plane elevation in the global axis system. Setting this adjusts the volume"""
        return self._vfNode.fluid_level_global

    @level_global.setter
    @node_setter_manageable
    @node_setter_observable
    def level_global(self, value):
        assert1f(value)
        self._vfNode.fluid_level_global = value

    @property
    def volume(self):
        """The volume of fluid in the tank in m3. Setting this adjusts the fluid level"""
        return self._vfNode.volume

    @volume.setter
    @node_setter_manageable
    @node_setter_observable
    def volume(self, value):
        assert1f_positive_or_zero(value, "Volume")
        self._vfNode.volume = value

    @property
    def density(self):
        """Density of the fluid in the tank in mT/m3"""
        return self._vfNode.density

    @density.setter
    @node_setter_manageable
    @node_setter_observable
    def density(self, value):
        assert1f(value)
        self._vfNode.density = value

    @property
    def capacity(self):
        """Returns the capacity of the tank in m3. This is calculated from the defined geometry."""
        return self._vfNode.capacity

    def give_python_code(self):
        code = "# code for {}".format(self.name)
        code += "\nmesh = s.new_tank(name='{}',".format(self.name)

        if self.density != 1.025:
            code += f"\n          density={self.density},"

        if self.free_flooding:
            code += f"\n          free_flooding=True,"

        code += "\n          parent='{}')".format(self.parent_for_export.name)

        if self.trimesh._invert_normals:
            code += "\nmesh.trimesh.load_file(r'{}', scale = ({},{},{}), rotation = ({},{},{}), offset = ({},{},{}), invert_normals=True)".format(
                self.trimesh._path,
                *self.trimesh._scale,
                *self.trimesh._rotation,
                *self.trimesh._offset,
            )
        else:
            code += "\nmesh.trimesh.load_file(r'{}', scale = ({},{},{}), rotation = ({},{},{}), offset = ({},{},{}))".format(
                self.trimesh._path,
                *self.trimesh._scale,
                *self.trimesh._rotation,
                *self.trimesh._offset,
            )
        code += f"\ns['{self.name}'].volume = {self.volume}   # first load mesh, then set volume"

        return code


class BallastSystem(Node):
    """A BallastSystem is a group of Tank objects.

    The tank objects are created separately and only their references are assigned to this ballast-system object.

    """

    def __init__(self, scene, parent):
        super().__init__(scene)

        self.tanks = []
        """List of Tank objects"""

        self.frozen = []
        """List of names of frozen tanks - The contents of a frozen tank should not be changed"""

        self.parent = parent

    def new_tank(
        self, name, position, capacity_kN, rho=1.025, frozen=False, actual_fill=0
    ):
        """Adds a new cubic shaped tank with the given volume as derived from capacity and rho

        Warning: provided for backwards compatibility only.
        """

        from warnings import warn

        warn(
            "BallastSystem.new_tank is outdated and may be removed in a future version."
        )

        tnk = self._scene.new_tank(name, parent=self.parent, density=rho)
        volume = capacity_kN / (9.81 * rho)
        side = volume ** (1 / 3)
        tnk.trimesh.load_file(
            "res: cube.obj",
            scale=(side, side, side),
            rotation=(0.0, 0.0, 0.0),
            offset=position,
        )
        if actual_fill > 0:
            tnk.fill_pct = actual_fill

        if frozen:
            tnk.frozen = frozen

        self.tanks.append(tnk)

        return tnk

    # for gui
    def change_parent_to(self, new_parent):
        if not (isinstance(new_parent, Axis) or new_parent is None):
            raise ValueError(
                "Visuals can only be attached to an axis (or derived) or None"
            )
        self.parent = new_parent

    # for node
    def depends_on(self):
        return [self.parent, *self.tanks]

    def is_frozen(self, name):
        """Returns True if the tank with this name if frozen"""
        return name in self.frozen

    def reorder_tanks(self, names):
        """Places tanks with given names at the top of the list. Other tanks are appended afterwards in original order.

        For a complete re-order give all tank names.

        Example:
            let tanks be 'a','b','c','d','e'

            then re_order_tanks(['e','b']) will result in ['e','b','a','c','d']
        """
        for name in names:
            if name not in self.tank_names():
                raise ValueError("No tank with name {}".format(name))

        old_tanks = self.tanks.copy()
        self.tanks.clear()
        to_be_deleted = list()

        for name in names:
            for tank in old_tanks:
                if tank.name == name:
                    self.tanks.append(tank)
                    to_be_deleted.append(tank)

        for tank in to_be_deleted:
            old_tanks.remove(tank)

        for tank in old_tanks:
            self.tanks.append(tank)

    def order_tanks_by_elevation(self):
        """Re-orders the existing tanks such that the lowest tanks are higher in the list"""

        zs = [tank.cog_when_full[2] for tank in self.tanks]
        inds = np.argsort(zs)
        self.tanks = [self.tanks[i] for i in inds]

    def order_tanks_by_distance_from_point(self, point, reverse=False):
        """Re-orders the existing tanks such that the tanks *furthest* from the point are first on the list

        Args:
            point : (x,y,z)  - reference point to determine the distance to
            reverse: (False) - order in reverse order: tanks nearest to the points first on list


        """
        pos = [tank.cog_when_full for tank in self.tanks]
        pos = np.array(pos, dtype=float)
        pos -= np.array(point)

        dist = np.apply_along_axis(np.linalg.norm, 1, pos)

        if reverse:
            inds = np.argsort(dist)
        else:
            inds = np.argsort(-dist)

        self.tanks = [self.tanks[i] for i in inds]

    def order_tanks_to_maximize_inertia_moment(self):
        """Re-order tanks such that tanks furthest from center of system are first on the list"""
        self._order_tanks_to_inertia_moment()

    def order_tanks_to_minimize_inertia_moment(self):
        """Re-order tanks such that tanks nearest to center of system are first on the list"""
        self._order_tanks_to_inertia_moment(maximize=False)

    def _order_tanks_to_inertia_moment(self, maximize=True):
        """Re-order tanks such that tanks furthest away from center of system are first on the list"""
        pos = [tank.cog_when_full for tank in self.tanks]
        m = [tank.capacity for tank in self.tanks]
        pos = np.array(pos, dtype=float)
        mxmymz = np.vstack((m, m, m)).transpose() * pos
        total = np.sum(m)
        point = sum(mxmymz) / total

        if maximize:
            self.order_tanks_by_distance_from_point(point)
        else:
            self.order_tanks_by_distance_from_point(point, reverse=True)

    def tank_names(self):
        return [tank.name for tank in self.tanks]

    def fill_tank(self, name, fill):

        assert1f(fill, "tank fill")

        for tank in self.tanks:
            if tank.name == name:
                tank.pct = fill
                return
        raise ValueError("No tank with name {}".format(name))

    def xyzw(self):
        """Gets the current ballast cog in GLOBAL axis system weight from the tanks

        Returns:
            (x,y,z), weight [mT]
        """
        """Calculates the weight and inertia properties of the tanks"""

        mxmymz = np.array((0.0, 0.0, 0.0))
        wt = 0

        for tank in self.tanks:
            w = tank.volume * tank.density
            p = np.array(tank.cog, dtype=float)
            mxmymz += p * w

            wt += w

        if wt == 0:
            xyz = np.array((0.0, 0.0, 0.0))
        else:
            xyz = mxmymz / wt

        return xyz, wt

    def empty_all_usable_tanks(self):
        """Empties all non-frozen tanks.
        Returns a list with tank number and fill percentage of all affected tanks. This can be used to restore the
        ballast situation as it was before emptying.

        See also: restore tank fillings
        """
        restore = []

        for i, t in enumerate(self.tanks):
            if not self.is_frozen(t.name):
                restore.append((i, t.fill_pct))
                t.fill_pct = 0

        return restore

    def restore_tank_fillings(self, restore):
        """Restores the tank fillings as per restore.

        Restore is typically obtained from the "empty_all_usable_tanks" function.

        See Also: empty_all_usable_tanks
        """

        for r in restore:
            i, pct = r
            self.tanks[i].fill_pct = pct

    def tank(self, name):

        for t in self.tanks:
            if t.name == name:
                return t
        raise ValueError("No tank with name {}".format(name))

    def __getitem__(self, item):
        return self.tank(item)

    @property
    def cogx(self):
        """X position of combined CoG of all tank contents in the ballast-system. (global coordinate) [m]"""
        return self.cog[0]

    @property
    def cogy(self):
        """Y position of combined CoG of all tank contents in the ballast-system. (global coordinate) [m]"""
        return self.cog[1]

    @property
    def cogz(self):
        """Z position of combined CoG of all tank contents in the ballast-system. (global coordinate) [m]"""
        return self.cog[2]

    @property
    def cog(self):
        """Combined CoG of all tank contents in the ballast-system. (global coordinate) [m,m,m]"""
        cog, wt = self.xyzw()
        return (cog[0], cog[1], cog[2])

    @property
    def weight(self):
        """Total weight of all tank fillings in the ballast system [kN]"""
        cog, wt = self.xyzw()
        return wt * 9.81

    def give_python_code(self):
        code = "\n# code for {} and its tanks".format(self.name)

        code += "\nbs = s.new_ballastsystem('{}', parent = '{}')".format(
            self.name, self.parent.name
        )

        for tank in self.tanks:
            code += "\nbs.tanks.append(s['{}'])".format(tank.name)

        return code


class WaveInteraction1(Node):
    """
    WaveInteraction

    Wave-interaction-1 couples a first-order hydrodynamic database to an axis.

    This adds:
    - wave-forces
    - damping
    - added mass

    The data is provided by a Hyddb1 object which is defined in the MaFreDo package. The contents are not embedded
    but are to be provided separately in a file. This node contains only the file-name.

    """

    def __init__(self, scene):

        super().__init__(scene)

        self.offset = [0, 0, 0]
        """Position (x,y,z) of the hydrodynamic origin in its parents axis system"""

        self.parent = None
        """Parent : Axis-type"""

        self.path = None
        """Filename of a file that can be read by a Hyddb1 object"""

    @property
    def file_path(self):
        return self._scene.get_resource_path(self.path)

    def depends_on(self):
        return [self.parent]

    def give_python_code(self):
        code = "# code for {}".format(self.name)

        code += "\ns.new_waveinteraction(name='{}',".format(self.name)
        code += "\n            parent='{}',".format(self.parent.name)
        code += "\n            path=r'{}',".format(self.path)
        code += "\n            offset=({}, {}, {}) )".format(*self.offset)

        return code

    def change_parent_to(self, new_parent):

        if not (isinstance(new_parent, Axis)):
            raise ValueError(
                "Hydrodynamic databases can only be attached to an axis (or derived)"
            )

        # get current position and orientation
        if self.parent is not None:
            cur_position = self.parent.to_glob_position(self.offset)
        else:
            cur_position = self.offset

        self.parent = new_parent
        self.offset = new_parent.to_loc_position(cur_position)


# ============== Managed nodes


class Manager(Node, ABC):


    # @abstractmethod                   not used anywhere outside the manager classes, so no requirement
    # def managed_nodes(self):
    #     """Returns a list of managed nodes"""
    #     raise Exception("derived class shall override this method")

    @abstractmethod
    def delete(self):
        """Carefully remove the manager, reinstate situation as before. Do not delete the manager itself but do
        delete all the nodes it created."""
        raise Exception("derived class shall override this method")

    @abstractmethod
    def creates(self, node: Node):
        """Returns True if node is created by this manager"""

        raise Exception("derived class shall override this method")
        # hint: return node in self.managed_nodes() # would be a good option, just not good enough as default



class GeometricContact(Manager):
    """
    GeometricContact

    A GeometricContact can be used to construct geometric connections between circular members:
        -       steel bars and holes, such as a shackle pin in a padeye (pin-hole)
        -       steel bars and steel bars, such as a shackle-shackle connection


    Situation before creation of geometric contact:

    Axis1
        Point1
            Circle1
    Axis2
        Point2
            Circle2

    Create a geometric contact with Circle1 and parent and Circle2 as child

    Axis1
        Point1              : observed, referenced as parent_circle_parent
            Circle1         : observed, referenced as parent_circle

        _axis_on_parent                 : managed
            _pin_hole_connection        : managed
                _connection_axial_rotation : managed
                    _axis_on_child      : managed
                        Axis2           : managed    , referenced as child_circle_parent_parent
                            Point2      : observed   , referenced as child_circle_parent
                                Circle2 : observed   , referenced as child_circle







    """

    def __init__(self, scene, child_circle, parent_circle, name, inside=True):
        """
        circle1 becomes the nodeB
        circle2 becomes the nodeA

        (attach circle 1 to circle 2)
        Args:
            scene:
            vfAxis:
            parent_circle:
            child_circle:
        """

        if parent_circle.parent.parent is None:
            raise ValueError(
                "The slaved pin is not located on an axis. Can not create the connection because there is no axis to nodeB"
            )

        super().__init__(scene)
        self.name = name

        name_prefix = self.name + vfc.MANAGED_NODE_IDENTIFIER

        self._parent_circle = parent_circle
        self._parent_circle_parent = parent_circle.parent  # point

        self._child_circle = child_circle
        self._child_circle_parent = child_circle.parent  # point
        self._child_circle_parent_parent = child_circle.parent.parent  # axis

        self._flipped = False
        self._inside_connection = inside

        self._axis_on_parent = self._scene.new_axis(
            scene.available_name_like(name_prefix + "_axis_on_parent")
        )
        """Axis on the nodeA axis at the location of the center of hole or pin"""

        self._pin_hole_connection = self._scene.new_axis(
            scene.available_name_like(name_prefix + "_pin_hole_connection")
        )
        """axis between the center of the hole and the center of the pin. Free to rotate about the center of the hole as well as the pin"""

        self._axis_on_child = self._scene.new_axis(
            scene.available_name_like(name_prefix + "_axis_on_child")
        )
        """axis to which the slaved body is connected. Either the center of the hole or the center of the pin """

        self._connection_axial_rotation = self._scene.new_axis(
            scene.available_name_like(name_prefix + "_connection_axial_rotation")
        )

        # prohibit changes to nodes that were used in the creation of this connection
        for node in self.managed_nodes():
            node.manager = self

        # observe circles and their points
        self._parent_circle.observers.append(self)
        self._parent_circle_parent.observers.append(self)

        self._child_circle.observers.append(self)
        self._child_circle_parent.observers.append(self)

        self._child_circle_parent_parent._parent_for_code_export = None

        self._update_connection()

    def on_observed_node_changed(self, changed_node):
        self._update_connection()

    @staticmethod
    def _assert_parent_child_possible(parent, child):
        if parent.parent.parent == child.parent.parent:
            raise ValueError(
                f"A GeometricContact can not be created between two circles on the same axis or body. Both circles are located on {parent.parent.parent}"
            )

    @property
    def child(self):
        """The Circle that is connected to the GeometricContact [Node]

        See Also: parent
        """
        return self._child_circle

    @child.setter
    def child(self, value):
        new_child = self._scene._node_from_node_or_str(value)
        if not isinstance(new_child, Circle):
            raise ValueError(
                f"Child of a geometric contact should be a Circle, but {new_child.name} is a {type(new_child)}"
            )

        if new_child.parent.parent is None:
            raise ValueError(
                f"Child circle {new_child.name} is not located on an axis or body and can thus not be used as child"
            )

        self._assert_parent_child_possible(self.parent, new_child)

        store = self._scene.current_manager
        self._scene.current_manager = self

        # release old child
        self._child_circle.observers.remove(self)
        self._child_circle_parent.observers.remove(self)

        # release the slaved axis system
        self._child_circle_parent_parent._parent_for_code_export = True
        self._child_circle_parent_parent.manager = None

        # set new parent
        self._child_circle = new_child
        self._child_circle_parent = new_child.parent
        self._child_circle_parent_parent = new_child.parent.parent

        # and observe
        self._child_circle.observers.append(self)
        self._child_circle_parent.observers.append(self)

        # and manage
        self._child_circle_parent_parent._parent_for_code_export = None
        self._child_circle_parent_parent.manager = self

        self._scene.current_manager = store

        self._update_connection()

    @property
    def parent(self):
        """The Circle that the GeometricConnection is connected to [Node]

        See Also: child
        """
        return self._parent_circle

    @parent.setter
    @node_setter_manageable
    @node_setter_observable
    def parent(self, var):
        if var is None:
            raise ValueError(
                "Parent of a geometric contact should be a Circle, not None"
            )

        new_parent = self._scene._node_from_node_or_str(var)
        if not isinstance(new_parent, Circle):
            raise ValueError(
                f"Parent of a geometric contact should be a Circle, but {new_parent.name} is a {type(new_parent)}"
            )

        self._assert_parent_child_possible(new_parent, self.child)

        # release old parent
        self._parent_circle.observers.remove(self)
        self._parent_circle_parent.observers.remove(self)

        # set new parent
        self._parent_circle = new_parent
        self._parent_circle_parent = new_parent.parent

        # and observe
        self._parent_circle.observers.append(self)
        self._parent_circle_parent.observers.append(self)

        self._update_connection()

    def change_parent_to(self, new_parent):
        self.parent = new_parent

    def delete(self):

        # release management
        for node in self.managed_nodes():
            node._manager = None

        self._child_circle_parent_parent.change_parent_to(None)

        self._scene.delete(self._axis_on_child)
        self._scene.delete(self._pin_hole_connection)
        self._scene.delete(self._axis_on_parent)

        # release observers
        self._parent_circle.observers.remove(self)
        self._parent_circle_parent.observers.remove(self)

        self._child_circle.observers.remove(self)
        self._child_circle_parent.observers.remove(self)

    def _update_connection(self):

        remember = self._scene.current_manager
        self._scene.current_manager = self  # claim management

        # get current properties

        c_swivel = self.swivel
        c_swivel_fixed = self.swivel_fixed
        c_rotation_on_parent = self.rotation_on_parent
        c_fixed_to_parent = self.fixed_to_parent
        c_child_rotation = self.child_rotation
        c_child_fixed = self.child_fixed

        pin1 = self._child_circle  # nodeB
        pin2 = self._parent_circle  # nodeA

        if pin1.parent.parent is None:
            raise ValueError(
                "The slaved pin is not located on an axis. Can not create the connection because there is no axis to nodeB"
            )

        # --------- prepare hole

        if pin2.parent.parent is not None:
            self._axis_on_parent.parent = pin2.parent.parent
        self._axis_on_parent.position = pin2.parent.position
        self._axis_on_parent.fixed = (True, True, True, True, True, True)

        self._axis_on_parent.rotation = rotation_from_y_axis_direction(pin2.axis)

        # Position connection axis at the center of the nodeA axis (pin2)
        # and allow it to rotate about the pin
        self._pin_hole_connection.position = (0, 0, 0)
        self._pin_hole_connection.parent = self._axis_on_parent
        self._pin_hole_connection.fixed = (True, True, True, True, False, True)

        self._connection_axial_rotation.parent = self._pin_hole_connection
        self._connection_axial_rotation.position = (0, 0, 0)

        # Position the connection pin (self) on the target pin and
        # place the parent of the parent of the pin (the axis) on the connection axis
        # and fix it
        slaved_axis = pin1.parent.parent

        slaved_axis.parent = self._axis_on_child
        slaved_axis.position = -np.array(pin1.parent.position)
        slaved_axis.rotation = rotation_from_y_axis_direction(-1 * np.array(pin1.axis))

        slaved_axis.fixed = True

        self._axis_on_child.parent = self._connection_axial_rotation
        self._axis_on_child.rotation = (0, 0, 0)
        self._axis_on_child.fixed = (True, True, True, True, False, True)

        if self._inside_connection:

            # Place the pin in the hole
            self._connection_axial_rotation.rotation = (0, 0, 0)
            self._axis_on_child.position = (pin2.radius - pin1.radius, 0, 0)

        else:

            # pin-pin connection
            self._axis_on_child.position = (pin1.radius + pin2.radius, 0, 0)
            self._connection_axial_rotation.rotation = (90, 0, 0)

        # restore settings
        self.swivel = c_swivel
        self.swivel_fixed = c_swivel_fixed
        self.rotation_on_parent = c_rotation_on_parent
        self.fixed_to_parent = c_fixed_to_parent
        self.child_rotation = c_child_rotation
        self.child_fixed = c_child_fixed

        self._scene.current_manager = remember

    def set_pin_pin_connection(self):
        """Sets the connection to be of type pin-pin"""

        self._inside_connection = False
        if self.swivel == 0:
            self.swivel = 90
        elif self.swivel == 180:
            self.swivel = 270

        self._update_connection()

    def set_pin_in_hole_connection(self):
        """Sets the connection to be of type pin-in-hole

        The axes of the two sheaves are aligned by rotating the slaved body
        The axes of the two sheaves are placed at a distance hole_dia - pin_dia apart, perpendicular to the axis direction
        An axes is created at the centers of the two sheaves
        These axes are connected with a shore axis which is allowed to rotate relative to the nodeA axis
        the nodeB axis is fixed to this rotating axis
        """
        self._inside_connection = True

        if self.swivel == 90:
            self.swivel = 0
        elif self.swivel == 270:
            self.swivel = 180

        self._update_connection()

    def managed_nodes(self):
        """Returns a list of managed nodes"""

        return [
            self._child_circle_parent_parent,
            self._axis_on_parent,
            self._axis_on_child,
            self._pin_hole_connection,
            self._connection_axial_rotation,
        ]

    def depends_on(self):
        return [self._parent_circle, self._child_circle]

    def creates(self, node: Node):
        return node in [
            self._axis_on_parent,
            self._axis_on_child,
            self._pin_hole_connection,
            self._connection_axial_rotation,
        ]

    def flip(self):
        """Changes the swivel angle by 180 degrees"""
        self.swivel = np.mod(self.swivel + 180, 360)

    def change_side(self):
        self.rotation_on_parent = np.mod(self.rotation_on_parent + 180, 360)
        self.child_rotation = np.mod(self.child_rotation + 180, 360)

    @property
    def swivel(self):
        """Swivel angle between parent and child objects [degrees]"""
        return self._connection_axial_rotation.rotation[0]

    @swivel.setter
    @node_setter_manageable
    @node_setter_observable
    def swivel(self, value):
        remember = self._scene.current_manager  # claim management
        self._scene.current_manager = self
        self._connection_axial_rotation.rx = value
        self._scene.current_manager = remember  # restore old manager

    @property
    def swivel_fixed(self):
        """Allow parent and child to swivel relative to eachother [boolean]"""
        return self._connection_axial_rotation.fixed[3]

    @swivel_fixed.setter
    @node_setter_manageable
    @node_setter_observable
    def swivel_fixed(self, value):
        remember = self._scene.current_manager  # claim management
        self._scene.current_manager = self
        self._connection_axial_rotation.fixed = [True, True, True, value, True, True]
        self._scene.current_manager = remember  # restore old manager

    @property
    def rotation_on_parent(self):
        """Angle between the line connecting the centers of the circles and the axis system of the parent node [degrees]"""
        return self._pin_hole_connection.ry

    @rotation_on_parent.setter
    @node_setter_manageable
    @node_setter_observable
    def rotation_on_parent(self, value):
        remember = self._scene.current_manager  # claim management
        self._scene.current_manager = self
        self._pin_hole_connection.ry = value
        self._scene.current_manager = remember  # restore old manager

    @property
    def fixed_to_parent(self):
        """Allow rotation around parent [boolean]

        see also: rotation_on_parent"""
        return self._pin_hole_connection.fixed[4]

    @fixed_to_parent.setter
    @node_setter_manageable
    @node_setter_observable
    def fixed_to_parent(self, value):
        remember = self._scene.current_manager  # claim management
        self._scene.current_manager = self
        self._pin_hole_connection.fixed = [True, True, True, True, value, True]
        self._scene.current_manager = remember  # restore old manager

    @property
    def child_rotation(self):
        """Angle between the line connecting the centers of the circles and the axis system of the child node [degrees]"""
        return self._axis_on_child.ry

    @child_rotation.setter
    @node_setter_manageable
    @node_setter_observable
    def child_rotation(self, value):
        remember = self._scene.current_manager  # claim management
        self._scene.current_manager = self
        self._axis_on_child.ry = value
        self._scene.current_manager = remember  # restore old manager

    @property
    def child_fixed(self):
        """Allow rotation of child relative to connection, see also: child_rotation [boolean]"""
        return self._axis_on_child.fixed[4]

    @child_fixed.setter
    @node_setter_manageable
    @node_setter_observable
    def child_fixed(self, value):
        remember = self._scene.current_manager  # claim management
        self._scene.current_manager = self
        self._axis_on_child.fixed = [True, True, True, True, value, True]
        self._scene.current_manager = remember  # restore old manager

    @property
    def inside(self):
        """Type of connection: True means child circle is inside parent circle, False means the child circle is outside but the circumferences contact [boolean]"""
        return self._inside_connection

    @inside.setter
    @node_setter_manageable
    @node_setter_observable
    def inside(self, value):
        if value == self._inside_connection:
            return

        if value:
            self.set_pin_in_hole_connection()
        else:
            self.set_pin_pin_connection()

    def give_python_code(self):

        old_manger = self._scene.current_manager
        self._scene.current_manager = self

        code = []

        # code.append('#  create the connection')
        code.append(f"s.new_geometriccontact(name = '{self.name}',")
        code.append(f"                       child = '{self._child_circle.name}',")
        code.append(f"                       parent = '{self._parent_circle.name}',")
        code.append(f"                       inside={self.inside},")

        if self.inside and self.swivel == 0:
            pass  # default for inside
        else:
            if not self.inside and self.swivel == 90:
                pass  # default for outside
            else:
                code.append(f"                       swivel={self.swivel},")

        if not self.swivel_fixed:
            code.append(f"                       swivel_fixed={self.swivel_fixed},")
        if self.fixed_to_parent:
            code.append(
                f"                       parent_rotation={self.rotation_on_parent},"
            )
            code.append(
                f"                       fixed_to_parent={self.fixed_to_parent},"
            )
        else:
            code.append(
                f"                       fixed_to_parent=solved({self.fixed_to_parent}),"
            )
        if self.child_fixed:
            code.append(f"                       child_fixed={self.child_fixed},")
            code.append(f"                       child_rotation={self.child_rotation},")
        else:
            code.append(
                f"                       child_rotation=solved({self.child_rotation}),"
            )

        code = [
            *code[:-1],
            code[-1][:-1] + " )",
        ]  # remove the , from the last entry [should be a quicker way to do this]

        self._scene.current_manager = old_manger

        return "\n".join(code)


class Sling(Manager):
    """A Sling is a single wire with an eye on each end. The eyes are created by splicing the end of the sling back
    into the itself.

    The geometry of a sling is defined as follows:

    diameter : diameter of the wire
    LeyeA, LeyeB : inside lengths of the eyes
    LsplicaA, LspliceB : the length of the splices
    Total : the distance between the insides of ends of the eyes A and B when pulled straight.

    Stiffness:
    The stiffness of the sling is specified by a single value: EA
    This determines the stiffnesses of the individual parts as follows:
    Wire in the eyes: EA
    Splices: Infinity (rigid)
    Main part: determined such that total stiffness (k) of the sling is EA/L


      Eye A           Splice A             nodeA part                   Splice B          Eye B

    /---------------\                                                                /---------------\
    |                =============-------------------------------------===============                |
    \---------------/                                                                \---------------/

    See Also: Grommet

    """

    def __init__(
        self,
        scene,
        name,
        length,
        LeyeA,
        LeyeB,
        LspliceA,
        LspliceB,
        diameter,
        EA,
        mass,
        endA=None,
        endB=None,
        sheaves=None,
    ):
        """
        Creates a new sling with the following structure

            endA
            eyeA (cable)
            splice (body , mass/2)
            nodeA (cable)     [optional: runs over sheave]
            splice (body, mass/2)
            eyeB (cable)
            endB

        Args:
            scene:     The scene in which the sling should be created
            name:  Name prefix
            length: Total length measured between the inside of the eyes of the sling is pulled straight.
            LeyeA: Total inside length in eye A if stretched flat
            LeyeB: Total inside length in eye B if stretched flat
            LspliceA: Length of the splice at end A
            LspliceB: Length of the splice at end B
            diameter: Diameter of the sling
            EA: Effective mean EA of the sling
            mass: total mass
            endA : Sheave or poi to fix end A of the sling to [optional]
            endB : Sheave or poi to fix end A of the sling to [optional]
            sheave : Sheave or poi for the nodeA part of the sling

        Returns:

        """

        super().__init__(scene)
        self.name = name

        name_prefix = self.name + vfc.MANAGED_NODE_IDENTIFIER

        # store the properties
        self._length = length
        self._LeyeA = LeyeA
        self._LeyeB = LeyeB
        self._LspliceA = LspliceA
        self._LspliceB = LspliceB
        self._diameter = diameter
        self._EA = EA
        self._mass = mass
        self._endA = scene._poi_or_sheave_from_node(endA)
        self._endB = scene._poi_or_sheave_from_node(endB)

        # create the two splices

        self.sa = scene.new_rigidbody(
            scene.available_name_like(name_prefix + "_spliceA"), fixed=False
        )
        self.a1 = scene.new_point(
            scene.available_name_like(name_prefix + "_spliceA"), parent=self.sa
        )
        self.a2 = scene.new_point(
            scene.available_name_like(name_prefix + "_spliceA2"), parent=self.sa
        )
        self.am = scene.new_point(
            scene.available_name_like(name_prefix + "_spliceAM"), parent=self.sa
        )

        self.avis = scene.new_visual(
            name + "_spliceA_visual",
            parent=self.sa,
            path=r"cylinder 1x1x1 lowres.obj",
            offset=(-LspliceA / 2, 0.0, 0.0),
            rotation=(0.0, 90.0, 0.0),
            scale=(LspliceA, 2 * diameter, diameter),
        )

        self.sb = scene.new_rigidbody(
            scene.available_name_like(name_prefix + "_spliceB"),
            rotation=(0, 0, 180),
            fixed=False,
        )
        self.b1 = scene.new_point(
            scene.available_name_like(name_prefix + "_spliceB1"), parent=self.sb
        )
        self.b2 = scene.new_point(
            scene.available_name_like(name_prefix + "_spliceB2"), parent=self.sb
        )
        self.bm = scene.new_point(
            scene.available_name_like(name_prefix + "_spliceBM"), parent=self.sb
        )

        self.bvis = scene.new_visual(
            scene.available_name_like(name_prefix + "_spliceB_visual"),
            parent=self.sb,
            path=r"cylinder 1x1x1 lowres.obj",
            offset=(-LspliceB / 2, 0.0, 0.0),
            rotation=(0.0, 90.0, 0.0),
            scale=(LspliceB, 2 * diameter, diameter),
        )

        self.main = scene.new_cable(
            scene.available_name_like(name_prefix + "_main_part"),
            endA=self.am,
            endB=self.bm,
            length=1,
            EA=1,
            diameter=diameter,
        )

        self.eyeA = scene.new_cable(
            scene.available_name_like(name_prefix + "_eyeA"),
            endA=self.a1,
            endB=self.a2,
            length=1,
            EA=1,
        )
        self.eyeB = scene.new_cable(
            scene.available_name_like(name_prefix + "_eyeB"),
            endA=self.b1,
            endB=self.b2,
            length=1,
            EA=1,
        )

        # set initial positions of splices if we can
        if self._endA is not None and self._endB is not None:
            a = np.array(self._endA.global_position)
            b = np.array(self._endB.global_position)

            dir = b - a
            dir /= np.linalg.norm(dir)

            self.sa.rotation = rotation_from_x_axis_direction(-dir)
            self.sb.rotation = rotation_from_x_axis_direction(dir)
            self.sa.position = a + (LeyeA + 0.5 * LspliceA) * dir
            self.sb.position = b - (LeyeB + 0.5 * LspliceB) * dir

        # Update properties
        self.sheaves = sheaves
        self._update_properties()

        for n in self.managed_nodes():
            n.manager = self

    def _update_properties(self):

        # The stiffness of the nodeA part is corrected to account for the stiffness of the splices.
        # It is considered that the stiffness of the splices is two times that of the wire.
        #
        # Springs in series: 1/Ktotal = 1/k1 + 1/k2 + 1/k3

        backup = self._scene.current_manager  # store
        self._scene.current_manager = self

        Lmain = (
            self._length - self._LspliceA - self._LspliceB - self._LeyeA - self._LeyeB
        )

        if self._EA == 0:
            EAmain = 0
        else:
            ka = 2 * self._EA / self._LspliceA
            kb = 2 * self._EA / self._LspliceB
            kmain = self._EA / Lmain
            k_total = 1 / ((1 / ka) + (1 / kmain) + (1 / kb))

            EAmain = k_total * Lmain

        self.sa.mass = self._mass / 2
        self.sa.inertia_radii = (
            self._LspliceA / 2,
            self._LspliceA / 2,
            self._diameter / 2,
        )

        self.a1.position = (self._LspliceA / 2, self._diameter / 2, 0)
        self.a2.position = (self._LspliceA / 2, -self._diameter / 2, 0)
        self.am.position = (-self._LspliceA / 2, 0, 0)

        self.avis.offset = (-self._LspliceA / 2, 0.0, 0.0)
        self.avis.scale = (self._LspliceA, 2 * self._diameter, self._diameter)

        self.sb.mass = self._mass / 2
        self.sb.inertia_radii = (
            self._LspliceB / 2,
            self._LspliceB / 2,
            self._diameter / 2,
        )

        self.b1.position = (self._LspliceB / 2, self._diameter / 2, 0)
        self.b2.position = (self._LspliceB / 2, -self._diameter / 2, 0)
        self.bm.position = (-self._LspliceB / 2, 0, 0)

        self.bvis.offset = (-self._LspliceB / 2, 0.0, 0.0)
        self.bvis.scale = (self._LspliceB, 2 * self._diameter, self._diameter)

        self.main.length = Lmain
        self.main.EA = EAmain
        self.main.diameter = self._diameter
        self.main.connections = tuple([self.am, *self._sheaves, self.bm])

        self.eyeA.length = self._LeyeA * 2 - self._diameter
        self.eyeA.EA = self._EA
        self.eyeA.diameter = self._diameter

        if self._endA is not None:
            self.eyeA.connections = (self.a1, self._endA, self.a2)
        else:
            self.eyeA.connections = (self.a1, self.a2)

        self.eyeB.length = self._LeyeB * 2 - self._diameter
        self.eyeB.EA = self._EA
        self.eyeB.diameter = self._diameter

        if self._endB is not None:
            self.eyeB.connections = (self.b1, self._endB, self.b2)
        else:
            self.eyeB.connections = (self.b1, self.b2)

        self._scene.current_manager = backup  # restore

    def depends_on(self):
        """The sling depends on the endpoints and sheaves (if any)"""

        a = list()

        if self._endA is not None:
            a.append(self._endA)
        if self._endB is not None:
            a.append(self._endB)

        a.extend(self.sheaves)

        return a

    def managed_nodes(self):
        a = [
            self.sa,
            self.a1,
            self.a2,
            self.am,
            self.avis,
            self.sb,
            self.b1,
            self.b2,
            self.bm,
            self.bvis,
            self.main,
            self.eyeA,
            self.eyeB,
        ]

        return a

    def creates(self, node: Node):
        return node in self.managed_nodes()  # all these are created

    def delete(self):

        # delete created nodes
        a = self.managed_nodes()

        for n in a:
            n._manager = None

        for n in a:
            if n in self._scene._nodes:
                self._scene.delete(n)  # delete if it is still available

    def give_python_code(self):
        code = f"# Exporting {self.name}"

        # if self.endA is not None:
        #     code += self.endA.give_python_code()
        # if self.endB is not None:
        #     code += self.endB.give_python_code()
        # for s in self.sheaves:
        #     code += s.give_python_code()

        code += "\n# Create sling"

        # (self, scene, name, Ltotal, LeyeA, LeyeB, LspliceA, LspliceB, diameter, EA, mass, endA = None, endB=None, sheaves=None):

        code += f'\ns.new_sling("{self.name}", length = {self.length},'
        code += f"\n            LeyeA = {self.LeyeA},"
        code += f"\n            LeyeB = {self.LeyeB},"
        code += f"\n            LspliceA = {self.LspliceA},"
        code += f"\n            LspliceB = {self.LspliceB},"
        code += f"\n            diameter = {self.diameter},"
        code += f"\n            EA = {self.EA},"
        code += f"\n            mass = {self.mass},"
        code += f'\n            endA = "{self.endA.name}",'
        code += f'\n            endB = "{self.endB.name}",'

        if self.sheaves:
            sheaves = "["
            for s in self.sheaves:
                sheaves += f'"{s.name}", '
            sheaves = sheaves[:-2] + "]"
        else:
            sheaves = "None"

        code += f"\n            sheaves = {sheaves})"

        return code

    # properties
    @property
    def length(self):
        """Total length measured between the INSIDE of the eyes of the sling is pulled straight. [m]"""
        return self._length

    @length.setter
    @node_setter_manageable
    @node_setter_observable
    def length(self, value):

        min_length = self.LeyeA + self.LeyeB + self.LspliceA + self.LspliceB
        if value <= min_length:
            raise ValueError(
                "Total length of the sling should be at least the length of the eyes plus the length of the splices"
            )

        self._length = value
        self._update_properties()

    @property
    def LeyeA(self):
        """Total length inside eye A if stretched flat [m]"""
        return self._LeyeA

    @LeyeA.setter
    @node_setter_manageable
    @node_setter_observable
    def LeyeA(self, value):

        max_length = self.length - (self.LeyeB + self.LspliceA + self.LspliceB)
        if value >= max_length:
            raise ValueError(
                "Total length of the sling should be at least the length of the eyes plus the length of the splices"
            )

        self._LeyeA = value
        self._update_properties()

    @property
    def LeyeB(self):
        """Total length inside eye B if stretched flat [m]"""
        return self._LeyeB

    @LeyeB.setter
    @node_setter_manageable
    @node_setter_observable
    def LeyeB(self, value):

        max_length = self.length - (self.LeyeA + self.LspliceA + self.LspliceB)
        if value >= max_length:
            raise ValueError(
                "Total length of the sling should be at least the length of the eyes plus the length of the splices"
            )

        self._LeyeB = value
        self._update_properties()

    @property
    def LspliceA(self):
        """Length of the splice at end A [m]"""
        return self._LspliceA

    @LspliceA.setter
    @node_setter_manageable
    @node_setter_observable
    def LspliceA(self, value):

        max_length = self.length - (self.LeyeA + self.LeyeB + self.LspliceB)
        if value >= max_length:
            raise ValueError(
                "Total length of the sling should be at least the length of the eyes plus the length of the splices"
            )

        self._LspliceA = value
        self._update_properties()

    @property
    def LspliceB(self):
        """Length of the splice at end B [m]"""
        return self._LspliceB

    @LspliceB.setter
    @node_setter_manageable
    @node_setter_observable
    def LspliceB(self, value):

        max_length = self.length - (self.LeyeA + self.LeyeB + self.LspliceA)
        if value >= max_length:
            raise ValueError(
                "Total length of the sling should be at least the length of the eyes plus the length of the splices"
            )

        self._LspliceB = value
        self._update_properties()

    @property
    def diameter(self):
        """Diameter of the sling (except the splices) [m]"""
        return self._diameter

    @diameter.setter
    @node_setter_manageable
    @node_setter_observable
    def diameter(self, value):
        self._diameter = value
        self._update_properties()

    @property
    def EA(self):
        """Effective mean EA of the sling when eyes are flat [kN].
        This is the EA that would be obtained when measuring the stiffness of the sling by putting zero-diameter pins in the eyes and stretching the sling and then using the length between the insides of the eyes."""
        return self._EA

    @EA.setter
    @node_setter_manageable
    @node_setter_observable
    def EA(self, value):
        self._EA = value
        self._update_properties()

    @property
    def mass(self):
        """Mass and weight of the sling. This mass is discretized  distributed over the two splices [mT]"""
        return self._mass

    @mass.setter
    @node_setter_manageable
    @node_setter_observable
    def mass(self, value):
        self._mass = value
        self._update_properties()

    @property
    def endA(self):
        """End A [circle or point node]"""
        return self._endA

    @endA.setter
    @node_setter_manageable
    @node_setter_observable
    def endA(self, value):
        node = self._scene._node_from_node_or_str(value)
        self._endA = self._scene._poi_or_sheave_from_node(node)
        self._update_properties()

    @property
    def endB(self):
        """End B [circle or point node]"""
        return self._endB

    @endB.setter
    @node_setter_manageable
    @node_setter_observable
    def endB(self, value):
        node = self._scene._node_from_node_or_str(value)
        self._endB = self._scene._poi_or_sheave_from_node(node)
        self._update_properties()

    @property
    def sheaves(self):
        """List of sheaves (circles, points) that the sling runs over between the two ends.

        May be provided as list of nodes or node-names.
        """
        return self._sheaves

    @sheaves.setter
    @node_setter_manageable
    @node_setter_observable
    def sheaves(self, value):
        s = []
        for v in value:
            node = self._scene._node_from_node_or_str(v)
            s.append(self._scene._poi_or_sheave_from_node(node))
        self._sheaves = s
        self._update_properties()


class Shackle(Manager, RigidBody):
    """
    Green-Pin Heavy Duty Bow Shackle BN

    visual from: https://www.traceparts.com/en/product/green-pinr-p-6036-green-pinr-heavy-duty-bow-shackle-bn-hdgphm0800-mm?CatalogPath=TRACEPARTS%3ATP04001002006&Product=10-04072013-086517&PartNumber=HDGPHM0800
    details from: https://www.greenpin.com/sites/default/files/2019-04/brochure-april-2019.pdf

    wll a b c d e f g h i j k weight
    [t] [mm]  [kg]
    120 95 95 208 95 147 400 238 647 453 428 50 110
    150 105 108 238 105 169 410 275 688 496 485 50 160
    200 120 130 279 120 179 513 290 838 564 530 70 235
    250 130 140 299 130 205 554 305 904 614 565 70 295
    300 140 150 325 140 205 618 305 996 644 585 80 368
    400 170 175 376 164 231 668 325 1114 690 665 70 560
    500 180 185 398 164 256 718 350 1190 720 710 70 685
    600 200 205 444 189 282 718 375 1243 810 775 70 880
    700 210 215 454 204 308 718 400 1263 870 820 70 980
    800 210 220 464 204 308 718 400 1270 870 820 70 1100
    900 220 230 485 215 328 718 420 1296 920 860 70 1280
    1000 240 240 515 215 349 718 420 1336 940 900 70 1460
    1250 260 270 585 230 369 768 450 1456 1025 970 70 1990
    1500 280 290 625 230 369 818 450 1556 1025 1010 70 2400

    Returns:

    """

    data = dict()
    # key = wll in t
    # dimensions a..k in [mm]
    #             a     b    c   d     e    f    g    h     i     j    k   weight[kg]
    # index       0     1    2    3    4    5    6    7     8     9    10   11
    data["GP120"] = (95, 95, 208, 95, 147, 400, 238, 647, 453, 428, 50, 110)
    data["GP150"] = (105, 108, 238, 105, 169, 410, 275, 688, 496, 485, 50, 160)
    data["GP200"] = (120, 130, 279, 120, 179, 513, 290, 838, 564, 530, 70, 235)
    data["GP250"] = (130, 140, 299, 130, 205, 554, 305, 904, 614, 565, 70, 295)
    data["GP300"] = (140, 150, 325, 140, 205, 618, 305, 996, 644, 585, 80, 368)
    data["GP400"] = (170, 175, 376, 164, 231, 668, 325, 1114, 690, 665, 70, 560)
    data["GP500"] = (180, 185, 398, 164, 256, 718, 350, 1190, 720, 710, 70, 685)
    data["GP600"] = (200, 205, 444, 189, 282, 718, 375, 1243, 810, 775, 70, 880)
    data["GP700"] = (210, 215, 454, 204, 308, 718, 400, 1263, 870, 820, 70, 980)
    data["GP800"] = (210, 220, 464, 204, 308, 718, 400, 1270, 870, 820, 70, 1100)
    data["GP900"] = (220, 230, 485, 215, 328, 718, 420, 1296, 920, 860, 70, 1280)
    data["GP1000"] = (240, 240, 515, 215, 349, 718, 420, 1336, 940, 900, 70, 1460)
    data["GP1250"] = (260, 270, 585, 230, 369, 768, 450, 1456, 1025, 970, 70, 1990)
    data["GP1500"] = (280, 290, 625, 230, 369, 818, 450, 1556, 1025, 1010, 70, 2400)

    def defined_kinds(self):
        """Defined shackle kinds"""
        list = [a for a in Shackle.data.keys()]
        return list

    def _give_values(self, kind):
        if kind not in Shackle.data:
            for key in Shackle.data.keys():
                print(key)
            raise ValueError(
                f"No data available for a Shackle of kind {kind}. Available values printed above"
            )

        return Shackle.data[kind]

    def __init__(self, scene, name, kind, a, p, g):

        Manager.__init__(self, scene)
        RigidBody.__init__(self, scene, axis=a, poi=p, force=g)

        self.name = name

        _ = self._give_values(kind)  # to make sure it exists

        # origin is at center of pin
        # z-axis up
        # y-axis in direction of pin

        # self.body = scene.new_rigidbody(name=name + '_body')

        # pin
        self.pin_point = scene.new_point(
            name=name + "_pin_point", parent=self, position=(0.0, 0.0, 0.0)
        )
        self.pin = scene.new_circle(
            name=name + "_pin", parent=self.pin_point, axis=(0.0, 1.0, 0.0)
        )

        # bow
        self.bow_point = scene.new_point(name=name + "_bow_point", parent=self)

        self.bow = scene.new_circle(
            name=name + "_bow", parent=self.bow_point, axis=(0.0, 1.0, 0.0)
        )

        # inside circle
        self.inside_point = scene.new_point(
            name=name + "_inside_circle_center", parent=self
        )
        self.inside = scene.new_circle(
            name=name + "_inside", parent=self.inside_point, axis=(1.0, 0, 0)
        )

        # code for GP800_visual
        self.visual_node = scene.new_visual(
            name=name + "_visual",
            parent=self,
            path=r"shackle_gp800.obj",
            offset=(0, 0, 0),
            rotation=(0, 0, 0),
        )

        self.kind = kind

        for n in self.managed_nodes():
            n.manager = self

    def depends_on(self):
        return []

    @property
    def kind(self):
        """Type of shackle, for example GP800 [text]"""
        return self._kind

    @kind.setter
    # @node_setter_manageable   : allow changing of shackle kind
    @node_setter_observable
    def kind(self, kind):

        values = self._give_values(kind)
        weight = values[11] / 1000  # convert to tonne
        pin_dia = values[1] / 1000
        bow_dia = values[0] / 1000
        bow_length_inside = values[5] / 1000
        bow_circle_inside = values[6] / 1000

        cogz = 0.5 * pin_dia + bow_length_inside / 3  # estimated

        remember = self._scene.current_manager

        self._scene.current_manager = (
            self.manager
        )  # WORK-AROUND : in case the shackle itself is managed, fake management

        self.mass = weight
        self.cog = (0, 0, cogz)

        self._scene.current_manager = self  # register self a manager (as it should)

        self.pin.radius = pin_dia / 2

        self.bow_point.position = (
            0.0,
            0.0,
            0.5 * pin_dia + bow_length_inside + 0.5 * bow_dia,
        )
        self.bow.radius = bow_dia / 2

        self.inside_point.position = (
            0,
            0,
            0.5 * pin_dia + bow_length_inside - 0.5 * bow_circle_inside,
        )
        self.inside.radius = bow_circle_inside / 2

        # determine the scale for the shackle
        # based on a GP800
        #
        actual_size = 0.5 * pin_dia + 0.5 * bow_dia + bow_length_inside
        gp800_size = 0.5 * 0.210 + 0.5 * 0.220 + 0.718

        scale = actual_size / gp800_size

        self.visual_node.scale = [scale, scale, scale]

        self._scene.current_manager = remember

        self._kind = kind

    def managed_nodes(self):
        return [
            self.pin_point,
            self.pin,
            self.bow_point,
            self.bow,
            self.inside_point,
            self.inside,
            self.visual_node,
        ]

    def creates(self, node: Node):
        return node in self.managed_nodes()  # all these are created

    def delete(self):

        # delete created nodes
        a = self.managed_nodes()

        for n in a:
            n._manager = None

        for n in a:
            if n in self._scene._nodes:
                self._scene.delete(n)  # delete if it is still available

    def give_python_code(self):
        code = f"# Exporting {self.name}"

        code += "\n# Create Shackle"
        code += f'\ns.new_shackle("{self.name}", kind = "{self.kind}")'  # , elastic={self.elastic})'

        if self.parent_for_export:
            code += f"\ns['{self.name}'].parent = s['{self.parent_for_export.name}']"

        code += "\ns['{}'].position = ({},{},{})".format(self.name, *self.position)
        code += "\ns['{}'].rotation = ({},{},{})".format(self.name, *self.rotation)

        return code


# =============== Scene


class Scene:
    """
    A Scene is the main component of DAVE.

    It provides a world to place nodes (elements) in.
    It interfaces with the equilibrium core for all calculations.

    By convention a Scene element is created with the name s, but create as many scenes as you want.

    Examples:

        s = Scene()
        s.new_axis('my_axis', position = (0,0,1))

        a = Scene() # another world
        a.new_point('a point')


    """

    def __init__(self, filename=None, copy_from=None, code=None):
        """Creates a new Scene

        Args:
            filename: (str or Path) Insert contents from this file into the newly created scene
            copy_from:  (Scene) Copy nodes from this other scene into the newly created scene
        """

        count = 0
        if filename:
            count += 1
        if copy_from:
            count += 1
        if code:
            count += 1
        if count > 1:
            raise ValueError(
                "Only one of the named arguments (filename OR copy_from OR code) can be used"
            )

        self.verbose = True
        """Report actions using print()"""

        self._vfc = pyo3d.Scene()
        """_vfc : DAVE Core, where the actual magic happens"""

        self._nodes = []
        """Contains a list of all nodes in the scene"""

        self.static_tolerance = 0.01
        """Desired tolerance when solving statics"""

        self.resources_paths = []
        """A list of paths where to look for resources such as .obj files. Priority is given to paths earlier in the list."""
        self.resources_paths.extend(vfc.RESOURCE_PATH)

        self._savepoint = None
        """Python code to re-create the scene, see savepoint_make()"""

        self._name_prefix = ""
        """An optional prefix to be applied to node names. Used when importing scenes."""

        self.current_manager = None
        """Setting this to an instance of a Manager allows nodes with that manager to be changed"""

        self._godmode = False
        """Icarus warning, wear proper PPE"""

        if filename is not None:
            self.load_scene(filename)

        if copy_from is not None:
            self.import_scene(copy_from, containerize=False)

        if code is not None:
            self.run_code(code)

    def clear(self):
        """Deletes all nodes"""

        self._nodes = []
        del self._vfc
        self._vfc = pyo3d.Scene()

    # =========== private functions =============

    def _print_cpp(self):
        print(self._vfc.to_string())

    def _print(self, what):
        if self.verbose:
            print(what)

    def _prefix_name(self, name):
        return self._name_prefix + name

    def _verify_name_available(self, name):
        """Throws an error if a node with name 'name' already exists"""
        names = [n.name for n in self._nodes]
        names.extend(self._vfc.names)
        if name in names:
            raise Exception(
                "The name '{}' is already in use. Pick a unique name".format(name)
            )

    def _node_from_node_or_str(self, node):
        """If node is a string, then returns the node with that name,
        if node is a node, then returns that node

        Raises:
            ValueError if a string is passed with an non-existing node
        """

        if isinstance(node, Node):
            return node
        if isinstance(node, str):
            return self[node]
        raise ValueError(
            "Node should be a Node or a string, not a {}".format(type(node))
        )

    def _node_from_node(self, node, reqtype):
        """Gets a node from the specified type

        Returns None if node is None
        Returns node if node is already a reqtype type node
        Else returns the axis with the given name

        Raises Exception if a node with name is not found"""

        if node is None:
            return None

        # node is a string then get the node with this name
        if type(node) == str:
            node = self[self._name_prefix + node]

        reqtype = make_iterable(reqtype)

        for r in reqtype:
            if isinstance(node, r):
                return node

        if issubclass(type(node), Node):
            raise Exception(
                "Element with name {} can not be used , it should be a {} or derived type but is a {}.".format(
                    node.name, reqtype, type(node)
                )
            )

        raise Exception("This is not an acceptable input argument {}".format(node))

    def _parent_from_node(self, node):
        """Returns None if node is None
        Returns node if node is an axis type node
        Else returns the axis with the given name

        Raises Exception if a node with name is not found"""

        return self._node_from_node(node, Axis)

    def _poi_from_node(self, node):
        """Returns None if node is None
        Returns node if node is an poi type node
        Else returns the poi with the given name

        Raises Exception if anything is not ok"""

        return self._node_from_node(node, Point)

    def _poi_or_sheave_from_node(self, node):
        """Returns None if node is None
        Returns node if node is an poi type node
        Else returns the poi with the given name

        Raises Exception if anything is not ok"""

        return self._node_from_node(node, [Point, Circle])

    def _sheave_from_node(self, node):
        """Returns None if node is None
        Returns node if node is an poi type node
        Else returns the poi with the given name

        Raises Exception if anything is not ok"""

        return self._node_from_node(node, Circle)

    def _geometry_changed(self):
        """Notify the scene that the geometry has changed and that the global transforms are invalid"""
        self._vfc.geometry_changed()

    def _fix_vessel_heel_trim(self):
        """Fixes the heel and trim of each node that has a buoyancy or linear hydrostatics node attached.

        Returns:
            Dictionary with original fixed properties as dict({'node name',fixed[6]}) which can be passed to _restore_original_fixes
        """

        vessel_indicators = [
            *self.nodes_of_type(Buoyancy),
            *self.nodes_of_type(HydSpring),
        ]
        r = dict()

        for node in vessel_indicators:
            parent = node.parent  # axis

            if parent.fixed[3] and parent.fixed[4]:
                continue  # already fixed

            r[parent.name] = parent.fixed  # store original fixes
            fixed = [*parent.fixed]
            fixed[3] = True
            fixed[4] = True

            # if fixed[3] and fixed[4] are non-zero, then yaw has to be fixed as well.
            # The solver does not support it when an angular dof is free, but one of the fixed
            # angular dofs is non-zero

            fixed[5] = True

            parent.fixed = fixed

        return r

    def _restore_original_fixes(self, original_fixes):
        """Restores the fixes as in original_fixes

        See also: _fix_vessel_heel_trim

        Args:
            original_fixes: dict with {'node name',fixes[6] }

        Returns:
            None

        """
        if original_fixes is None:
            return

        for name in original_fixes.keys():
            self.node_by_name(name).fixed = original_fixes[name]

    def _check_and_fix_geometric_contact_orientations(self) -> (bool, str):
        """A Geometric pin on pin contact may end up with tension in the contact. Fix that by moving the child pin to the other side of the parent pin

        Returns:
            True if anything was changed; False otherwise
        """

        changed = False
        message = ""
        for n in self.nodes_of_type(GeometricContact):
            if not n.inside:

                # connection force of the child is the
                # force applied on the connecting rod
                # in the axis system of the rod
                if n._axis_on_child.connection_force_x > 0:
                    message += f"Changing side of pin-pin connection {n.name} due to tension in connection\n"
                    n.change_side()
                    changed = True

        return (changed, message)

    # ======== resources =========

    def get_resource_path(self, url) -> Path:
        """Resolves the path on disk for resource url. Urls statring with res: result in a file from the resources system.

        Looks for a file with "name" in the specified resource-paths and returns the full path to the the first one
        that is found.
        If name is a full path to an existing file, then that is returned.

        See Also:
            resource_paths


        Returns:
            Full path to resource

        Raises:
            FileExistsError if resource is not found

        """

        # warning and work-around for backwards compatibility
        # filenames without a path get res: in front of it
        try:
            if isinstance(url, Path):
                test = str(url)
            else:
                test = url

            if not test.startswith("res:"):
                test = Path(test)
                if str(test.parent) == ".":
                    # from warnings import warn
                    #
                    # warn(
                    #     f'Resources should start with res: --> fixing "{url}" to "res: {url}"'
                    # )
                    url = "res: " + str(test)
        except:
            pass

        if isinstance(url, Path):
            file = url
        elif isinstance(url, str):
            if not url.startswith("res:"):
                file = Path(url)
            else:
                # we have a string starting with 'res:'
                filename = url[4:].strip()

                for res in self.resources_paths:
                    p = Path(res)

                    file = p / filename
                    if isfile(file):
                        return file

                # prepare feedback for error
                ext = str(url).split(".")[-1]  # everything after the last .

                print("Resource folders:")
                for res in self.resources_paths:
                    print(str(res))


                print(
                    "The following resources with extension {} are available with ".format(
                        ext
                    )
                )
                available = self.get_resource_list(ext)
                for a in available:
                    print(a)
                raise FileExistsError(
                    'Resource "{}" not found in resource paths. A list with available resources with this extension is printed above this error'.format(
                        url
                    )
                )
        else:
            raise ValueError(
                f"Provided url shall be a Path or a string, not a {type(url)}"
            )

        if file.exists():
            return file

        raise FileExistsError(
            'File "{}" not found.\nHint: To obtain a resource put res: in front of the name.'.format(
                url
            )
        )

    def get_resource_list(self, extension):
        """Returns a list of all file-paths (strings) given extension in any of the resource-paths"""

        r = []

        for dir in self.resources_paths:
            try:
                files = listdir(dir)
                for file in files:
                    if file.lower().endswith(extension):
                        if file not in r:
                            r.append("res: " + file)
            except FileNotFoundError:
                pass

        return r

    # ======== element functions =========

    def node_by_name(self, node_name, silent=False):
        for N in self._nodes:
            if N.name == node_name:
                return N

        if not silent:
            self.print_node_tree()
        raise ValueError(
            'No node with name "{}". Available names printed above.'.format(node_name)
        )

    def __getitem__(self, node_name):
        """Returns a node with name"""
        return self.node_by_name(node_name)

    def nodes_of_type(self, node_class):
        """Returns all nodes of the specified or derived type

        Examples:
            pois = scene.nodes_of_type(DAVE.Poi)
            axis_and_bodies = scene.nodes_of_type(DAVE.Axis)
        """
        r = list()
        for n in self._nodes:
            if isinstance(n, node_class):
                r.append(n)
        return r

    def assert_unique_names(self):
        """Asserts that all names are unique"""
        names = [n.name for n in self._nodes]
        unique_names = set(names)

        if len(unique_names) != len(names):
            previous_name = ""
            names.sort()
            duplicates = ""
            for name in names:
                if name == previous_name:
                    print(f"Duplicate: {name}")
                    duplicates += name + " "

                    for n in self._nodes:
                        if n.name == name:
                            print(n)

                previous_name = name
            raise ValueError(f"Duplicate names exist: " + duplicates)

    def sort_nodes_by_parent(self):
        """Sorts the nodes such that the parent of this node (if any) occurs earlier in the list.

        See Also:
            sort_nodes_by_dependency
        """

        self.assert_unique_names()

        exported = []
        to_be_exported = self._nodes.copy()
        counter = 0

        while to_be_exported:

            counter += 1
            if counter > len(self._nodes):
                raise Exception(
                    "Could not sort nodes by dependency, circular references exist?"
                )

            can_be_exported = []

            for node in to_be_exported:

                if hasattr(node, "parent"):
                    parent = node.parent
                    if parent is not None and parent not in exported:
                        continue

                if node.manager is not None and node.manager not in exported:
                    continue

                # otherwise the node can be exported
                can_be_exported.append(node)

            # remove exported nodes from
            for n in can_be_exported:
                to_be_exported.remove(n)

            exported.extend(can_be_exported)

        self._nodes = exported

    def sort_nodes_by_dependency(self):
        """Sorts the nodes such that a nodes creation only depends on nodes earlier in the list.

        This sorting is used for node creation order

        See Also:
            sort_nodes_by_parent
        """

        self.assert_unique_names()

        exported = []
        to_be_exported = self._nodes.copy()
        counter = 0

        while to_be_exported:

            counter += 1
            if counter > len(self._nodes):

                for node in to_be_exported:
                    print(f"Node : {node.name}")
                    for d in node.depends_on():
                        print(f"  depends on: {d.name}")
                    if node._manager:
                        print(f"   managed by: {node._manager.name}")

                raise Exception(
                    "Could not sort nodes by dependency, circular references exist?"
                )

            can_be_exported = []

            for node in to_be_exported:
                # if node._manager:
                #     if node._manager in exported:
                #         can_be_exported.append(node)
                # el
                if all(el in exported for el in node.depends_on()):
                    can_be_exported.append(node)

            # remove exported nodes from
            for n in can_be_exported:
                to_be_exported.remove(n)

            exported.extend(can_be_exported)

        self._nodes = exported

        # scene_names = [n.name for n in self._nodes]
        #
        # self._vfc.state_update()  # use the function from the core.
        # new_list = []
        # for name in self._vfc.names:  # and then build a new list using the names
        #     if vfc.VF_NAME_SPLIT in name:
        #         continue
        #
        #     if name not in scene_names:
        #         raise Exception('Something went wrong with sorting the the nodes by dependency. '
        #                         'Node naming between core and scene is inconsistent for node {}'.format(name))
        #
        #     new_list.append(self[name])
        #
        # # and add the nodes without a vfc-core connection
        # for node in self._nodes:
        #     if not node in new_list:
        #         new_list.append(node)
        #
        # self._nodes = new_list

    def name_available(self, name):
        """Returns True if the name is still available"""
        names = [n.name for n in self._nodes]
        names.extend(self._vfc.names)
        return not (name in names)

    def available_name_like(self, like):
        """Returns an available name like the one given, for example Axis23"""
        if self.name_available(like):
            return like
        counter = 1
        while True:
            name = like + "_" + str(counter)
            if self.name_available(name):
                return name
            counter += 1

    def node_A_core_depends_on_B_core(self, A, B):
        """Returns True if the node core of node A depends on the core node of node B"""

        A = self._node_from_node_or_str(A)
        B = self._node_from_node_or_str(B)

        if not isinstance(A, CoreConnectedNode):
            raise ValueError(
                f"{A.name} is not connected to a core node. Dependancies can not be traced using this function"
            )
        if not isinstance(B, CoreConnectedNode):
            raise ValueError(
                f"{B.name} is not connected to a core node. Dependancies can not be traced using this function"
            )

        return self._vfc.element_A_depends_on_B(A._vfNode.name, B._vfNode.name)

    def nodes_managed_by(self, manager : Manager):
        """Returns a list of nodes managed by manager"""

        return [node for node in self._nodes if node.manager == manager]

    def nodes_depending_on(self, node):
        """Returns a list of nodes that physically depend on node. Only direct dependants are obtained with a connection to the core.
        This function should be used to determine if a node can be created, deleted, exported.

        For making node-trees please use nodes_with_parent instead.

        Args:
            node : Node or node-name

        Returns:
            list of names

        See Also: nodes_with_parent
        """

        if isinstance(node, Node):
            node = node.name

        # check the node type
        _node = self[node]
        if not isinstance(_node, CoreConnectedNode):
            return []
        else:
            names = self._vfc.elements_depending_directly_on(node)

        r = []
        for name in names:
            try:
                node = self.node_by_name(name, silent=True)
                r.append(node.name)
            except:
                pass

        # check all other nodes in the scene

        for n in self._nodes:
            if _node in n.depends_on():
                if n.name not in r:
                    r.append(n.name)

        # for v in [*self.nodes_of_type(Visual), *self.nodes_of_type(WaveInteraction1)]:
        #     if v.parent is _node:
        #         r.append(v.name)

        return r

    def nodes_with_parent(self, node):
        """Returns a list of nodes that have given node as a parent. Good for making trees.
        For checking physical connections use nodes_depending_on instead.

        Args:
            node : Node or node-name

        Returns:
            list of names

        See Also: nodes_depending_on
        """

        if isinstance(node, str):
            node = self[node]

        r = []

        for n in self._nodes:

            try:
                parent = n.parent
            except AttributeError:
                continue

            if parent == node:
                r.append(n.name)

        return r

    def delete(self, node):
        """Deletes the given node from the scene as well as all nodes depending on it.

        See Also:
            dissolve
        """

        if isinstance(node, str):
            node = self[node]

        if node not in self._nodes:
            raise ValueError(
                "Can not delete node because it is not a node of this scene"
            )

        if isinstance(node, Manager):
            node.delete()
            # self._nodes.remove(node)
            # return <-- do not return

        depending_nodes = self.nodes_depending_on(node)
        depending_nodes.extend([n.name for n in node.observers])

        if node._manager:  # node, delete its manager
            # print('Deleting manager')
            self.delete(node._manager)
            if node in self._nodes:
                self.delete(node)  # node may have been deleted by the manager

        else:
            self._print(
                "Deleting {} [{}]".format(
                    node.name, str(type(node)).split(".")[-1][:-2]
                )
            )

            # First delete the dependencies
            for d in depending_nodes:
                if not self.name_available(d):  # element is still here
                    self.delete(d)

            # then remove the vtk node itself
            # self._print('removing vfc node')
            node._delete_vfc()
            self._nodes.remove(node)

    def dissolve(self, node):
        """Attempts to delete the given node without affecting the rest of the model.

        1. Look for nodes that have this node as parent
        2. Attach those nodes to the parent of this node.
        3. Delete this node.

        There are many situations in which this will fail because an it is impossible to dissolve
        the element. For example a poi can only be dissolved when nothing is attached to it.

        For now this function only works on AXIS

        #TODO: Add managers - just release management

        """

        if isinstance(node, str):
            node = self[node]

        ok = False
        if isinstance(node, Manager):

            if isinstance(node, Axis):
                p = self.new_axis(node.name + '_dissolved')
            else:
                p = None

            for d in self.nodes_managed_by(node):
                with ClaimManagement(self,node):
                    if node in d.observers:
                        d.observers.remove(node)
                    d.manager = None

                    if isinstance(d, NodeWithParent):
                        if d.parent == node:
                            d.parent = p

            ok = True

        if isinstance(node, Axis):
            for d in self.nodes_depending_on(node):
                self[d].change_parent_to(node.parent)
            ok = True

        if not ok:
            raise TypeError("Only nodes of type Axis and Manager can be dissolved at this moment")

        self._nodes.remove(node)  # do not call delete as that will fail on managers

    def savepoint_make(self):
        self._savepoint = self.give_python_code()

    def savepoint_restore(self):
        if self._savepoint is not None:
            self.clear()
            exec(self._savepoint, {}, {"s": self})
            self._savepoint = None
            return True
        else:
            return False

    # ========= The most important functions ========

    def update(self):
        """Updates the interface between the nodes and the core. This includes the re-calculation of all forces,
        buoyancy positions, ballast-system cogs etc.
        """
        for n in self._nodes:
            n.update()
        self._vfc.state_update()

    def solve_statics(self, silent=False, timeout=None):
        """Solves statics

        Args:
            silent: Do not print if successfully solved

        Returns:
            bool: True if successful, False otherwise.

        """
        self.update()

        if timeout is None:
            solve_func = self._vfc.state_solve_statics
        else:
            #       bool doStabilityCheck,
            #       double timeout,
            #           bool do_prepare_state,
            #           bool solve_linear_dofs_first,
            #           double stability_check_delta
            solve_func = lambda: self._vfc.state_solve_statics_with_timeout(
                True, timeout, True, True, 0
            )  # default stability value

        # pass 1
        orignal_fixes = self._fix_vessel_heel_trim()
        succes = solve_func()
        if not succes:
            self._restore_original_fixes(orignal_fixes)
            return False

        if orignal_fixes:
            # pass 2
            self._restore_original_fixes(orignal_fixes)
            succes = solve_func()

        if self.verify_equilibrium():

            changed, message = self._check_and_fix_geometric_contact_orientations()
            if changed:
                print(message)
                solve_func()
                if not self.verify_equilibrium():
                    return False

            if not silent:
                self._print("Solved to {}.".format(self._vfc.Emaxabs))
            return True

        d = np.array(self._vfc.get_dofs())
        if np.any(np.abs(d) > 2000):
            print(
                "Error: One of the degrees of freedom exceeded the boundary of 2000 [m]/[rad]."
            )
            return False

        return False

    def verify_equilibrium(self, tol=1e-2):
        """Checks if the current state is an equilibrium

        Returns:
            bool: True if successful, False if not an equilibrium.

        """
        self.update()
        return self._vfc.Emaxabs < tol

    # ====== goal seek ========

    def goal_seek(
        self, evaluate, target, change_node, change_property, bracket=None, tol=1e-3
    ):
        """goal_seek

        Goal seek is the classic goal-seek. It changes a single property of a single node in order to get
        some property of some node to a specified value. Just like excel.

        Args:
            evaluate : code to be evaluated to yield the value that is solved for. Eg: s['poi'].fx Scene is abbiviated as "s"
            target (number):       target value for that property
            change_node(Node or str):  node to be adjusted
            change_property (str): property of that node to be adjusted
            range(optional)  : specify the possible search-interval

        Returns:
            bool: True if successful, False otherwise.

        Examples:
            Change the y-position of the cog of a rigid body ('Barge')  in order to obtain zero roll (rx)
            >>> s.goal_seek("s['Barge'].fx",0,'Barge','cogy')

        """
        s = self

        change_node = self._node_from_node_or_str(change_node)

        # check that the attributes exist and are single numbers
        test = eval(evaluate)

        try:
            float(test)
        except:
            raise ValueError("Evaluation of {} does not result in a float")

        self._print(
            "Attempting to evaluate {} to {} (now {})".format(evaluate, target, test)
        )

        initial = getattr(change_node, change_property)
        self._print(
            "By changing the value of {}.{} (now {})".format(
                change_node.name, change_property, initial
            )
        )

        def set_and_get(x):
            setattr(change_node, change_property, x)
            self.solve_statics(silent=True)
            s = self
            result = eval(evaluate)
            self._print("setting {} results in {}".format(x, result))
            return result - target

        from scipy.optimize import root_scalar

        x0 = initial
        x1 = initial + 0.0001

        if bracket is not None:
            res = root_scalar(set_and_get, x0=x0, x1=x1, bracket=bracket, xtol=tol)
        else:
            res = root_scalar(set_and_get, x0=x0, x1=x1, xtol=tol)

        self._print(res)

        # evaluate result
        final_value = eval(evaluate)
        if abs(final_value - target) > 1e-3:
            raise ValueError(
                "Target not reached. Target was {}, reached value is {}".format(
                    target, final_value
                )
            )

        return True

    def plot_effect(self, evaluate, change_node, change_property, start, to, steps):
        """Produces a 2D plot with the relation between two properties of the scene. For example the length of a cable
        versus the force in another cable.

        The evaluate argument is processed using "eval" and may contain python code. This may be used to combine multiple
        properties to one value. For example calculate the diagonal load distribution from four independent loads.

        The plot is produced using matplotlob. The plot is produced in the current figure (if any) and plt.show is not executed.

        Args:
            evaluate (str): code to be evaluated to yield the value on the y-axis. Eg: s['poi'].fx Scene is abbiviated as "s"
            change_node(Node or str):  node to be adjusted
            change_property (str): property of that node to be adjusted
            start : left side of the interval
            to : right side of the interval
            steps : number of steps in the interval

        Returns:
            Tuple (x,y) with x and y coordinates

        Examples:
            >>> s.plot_effect("s['cable'].tension", "cable", "length", 11, 14, 10)
            >>> import matplotlib.pyplot as plt
            >>> plt.show()

        """
        s = self
        change_node = self._node_from_node_or_str(change_node)

        # check that the attributes exist and are single numbers
        test = eval(evaluate)

        try:
            float(test)
        except:
            raise ValueError("Evaluation of {} does not result in a float")

        def set_and_get(x):
            setattr(change_node, change_property, x)
            self.solve_statics(silent=True)
            s = self
            result = eval(evaluate)
            self._print("setting {} results in {}".format(x, result))
            return result

        xs = np.linspace(start, to, steps)
        y = []
        for x in xs:
            y.append(set_and_get(x))

        y = np.array(y)
        import matplotlib.pyplot as plt

        plt.plot(xs, y)
        plt.xlabel("{} of {}".format(change_property, change_node.name))
        plt.ylabel(evaluate)

        return (xs, y)

    # ======== create functions =========

    def new_axis(
        self,
        name,
        parent=None,
        position=None,
        rotation=None,
        inertia=None,
        inertia_radii=None,
        fixed=True,
    ) -> Axis:
        """Creates a new *axis* node and adds it to the scene.

        Args:
            name: Name for the node, should be unique
            parent: optional, name of the parent of the node
            position: optional, position for the node (x,y,z)
            rotation: optional, rotation for the node (rx,ry,rz)
            fixed [True]: optional, determines whether the axis is fixed [True] or free [False]. May also be a sequence of 6 booleans.

        Returns:
            Reference to newly created axis

        """

        # apply prefixes
        name = self._prefix_name(name)

        # first check
        assertValidName(name)
        self._verify_name_available(name)
        b = self._parent_from_node(parent)

        if position is not None:
            assert3f(position, "Position ")
        if rotation is not None:
            assert3f(rotation, "Rotation ")

        if inertia is not None:
            assert1f_positive_or_zero(inertia, "inertia ")

        if inertia_radii is not None:
            assert3f_positive(inertia_radii, "Radii of inertia")
            assert inertia is not None, ValueError(
                "Can not set radii of gyration without specifying inertia"
            )

        if not isinstance(fixed, bool):
            if len(fixed) != 6:
                raise Exception(
                    '"fixed" parameter should either be True/False or a 6x bool sequence such as (True,True,False,False,True,False)'
                )

        # then create
        a = self._vfc.new_axis(name)

        new_node = Axis(self, a)

        # and set properties
        if b is not None:
            new_node.parent = b
        if position is not None:
            new_node.position = position
        if rotation is not None:
            new_node.rotation = rotation
        if inertia is not None:
            new_node.inertia = inertia
        if inertia_radii is not None:
            new_node.inertia_radii = inertia_radii

        if isinstance(fixed, bool):
            if fixed:
                new_node.set_fixed()
            else:
                new_node.set_free()
        else:
            new_node.fixed = fixed

        self._nodes.append(new_node)
        return new_node

    def new_geometriccontact(
        self,
        name,
        child,
        parent,
        inside=False,
        swivel=None,
        rotation_on_parent=None,
        child_rotation=None,
        swivel_fixed=True,
        fixed_to_parent=False,
        child_fixed=False,
    ) -> GeometricContact:
        """Creates a new *new_geometriccontact* node and adds it to the scene.

        Geometric contact connects two circular elements and can be used to model bar-bar connections or pin-in-hole connections.

        By default a bar-bar connection is created between item1 and item2.

        Args:
            name: Name for the node, should be unique
            child : [Sheave] will be the nodeA of the connection
            parent : [Sheave] will be the nodeB of the connection
            inside: [False] False creates a pinpin connection. True creates a pin-hole type of connection
            swivel: Rotation angle between the two items. Defaults to 90 for pinpin and 0 for pin-hole
            rotation_on_parent: Angle of the connecting hinge relative to nodeA or None for default
            child_rotation: Angle of the nodeB relative to the connecting hinge or None for default
            swivel_fixed: Fix swivel [True]
            fixed_to_parent: Fix connecting hinge to nodeA [False]
            child_fixed: Fix nodeB to connecting hinge [False]

        Note:
            For pin-hole connections there is no geometrical difference between the pin and the hole. Therefore it is not needed to specify
            which is the pin and which is the hole

        Returns:
            Reference to newly created new_geometriccontact

        """

        # apply prefixes
        name = self._prefix_name(name)

        # first check
        assertValidName(name)
        self._verify_name_available(name)

        name_prefix = name + vfc.MANAGED_NODE_IDENTIFIER
        postfixes = [
            "_axis_on_parent",
            "_pin_hole_connection",
            "_axis_on_child",
            "_connection_axial_rotation",
        ]

        for pf in postfixes:
            self._verify_name_available(name_prefix + pf)

        child = self._sheave_from_node(child)
        parent = self._sheave_from_node(parent)

        assertBool(inside, "inside")
        assertBool(swivel_fixed, "swivel_fixed")
        assertBool(fixed_to_parent, "fixed_to_parent")
        assertBool(child_fixed, "child_fixed")

        GeometricContact._assert_parent_child_possible(parent, child)

        if swivel is None:
            if inside:
                swivel = 0
            else:
                swivel = 90

        assert1f(swivel, "swivel_angle")

        if rotation_on_parent is not None:
            assert1f(rotation_on_parent, "rotation_on_parent should be either None or ")
        if child_rotation is not None:
            assert1f(child_rotation, "child_rotation should be either None or ")

        if child is None:
            raise ValueError("child needs to be a sheave-type node")
        if parent is None:
            raise ValueError("parent needs to be a sheave-type node")

        if child.parent.parent is None:
            raise ValueError(
                f"The parent {child.parent.name} of the child item {child.name} is not located on an axis. Can not create the connection because there is no axis to nodeB"
            )

        if child.parent.parent.manager is not None:
            self.print_node_tree()
            raise ValueError(
                f"The axis or body that {child.name} is on is already managed by {child.parent.parent.manager.name} and can therefore not be changed - unable to create geometric contact"
            )

        new_node = GeometricContact(self, child, parent, name)
        if inside:
            new_node.set_pin_in_hole_connection()
        else:
            new_node.set_pin_pin_connection()

        new_node.swivel = swivel
        if rotation_on_parent is not None:
            new_node.rotation_on_parent = rotation_on_parent
        if child_rotation is not None:
            new_node.child_rotation = child_rotation

        new_node.fixed_to_parent = fixed_to_parent
        new_node.child_fixed = child_fixed
        new_node.swivel_fixed = swivel_fixed

        self._nodes.append(new_node)
        return new_node

    def new_waveinteraction(
        self,
        name,
        path,
        parent=None,
        offset=None,
    ) -> WaveInteraction1:
        """Creates a new *wave interaction* node and adds it to the scene.

        Args:
            name: Name for the node, should be unique
            path: Path to the hydrodynamic database
            parent: optional, name of the parent of the node
            offset: optional, position for the node (x,y,z)

        Returns:
            Reference to newly created wave-interaction object

        """

        if not parent:
            raise ValueError("Wave-interaction has to be located on an Axis")

        # apply prefixes
        name = self._prefix_name(name)

        # first check
        assertValidName(name)
        self._verify_name_available(name)
        b = self._parent_from_node(parent)

        if b is None:
            raise ValueError("Wave-interaction has to be located on an Axis")

        if offset is not None:
            assert3f(offset, "Offset ")

        self.get_resource_path(path)  # raises error when resource is not found

        # then create

        new_node = WaveInteraction1(self)

        new_node.name = name
        new_node.path = path
        new_node.parent = parent

        # and set properties
        new_node.parent = b
        if offset is not None:
            new_node.offset = offset

        self._nodes.append(new_node)
        return new_node

    def new_visual(
        self, name, path, parent=None, offset=None, rotation=None, scale=None
    ) -> Visual:
        """Creates a new *Visual* node and adds it to the scene.

        Args:
            name: Name for the node, should be unique
            path: Path to the resource
            parent: optional, name of the parent of the node
            offset: optional, position for the node (x,y,z)
            rotation: optional, rotation for the node (rx,ry,rz)
            scale : optional, scale of the visual (x,y,z).

        Returns:
            Reference to newly created visual

        """

        # apply prefixes
        name = self._prefix_name(name)

        # first check
        assertValidName(name)
        self._verify_name_available(name)
        b = self._parent_from_node(parent)

        if offset is not None:
            assert3f(offset, "Offset ")
        if rotation is not None:
            assert3f(rotation, "Rotation ")

        self.get_resource_path(path)  # raises error when resource is not found

        # then create

        new_node = Visual(self)

        new_node.name = name
        new_node.path = path
        new_node.parent = parent

        # and set properties
        if b is not None:
            new_node.parent = b
        if offset is not None:
            new_node.offset = offset
        if rotation is not None:
            new_node.rotation = rotation
        if scale is not None:
            new_node.scale = scale

        self._nodes.append(new_node)
        return new_node

    def new_point(self, name, parent=None, position=None) -> Point:
        """Creates a new *poi* node and adds it to the scene.

        Args:
            name: Name for the node, should be unique
            parent: optional, name of the parent of the node
            position: optional, position for the node (x,y,z)


        Returns:
            Reference to newly created poi

        """

        # apply prefixes
        name = self._prefix_name(name)

        # first check
        assertValidName(name)
        self._verify_name_available(name)
        b = self._parent_from_node(parent)

        if position is not None:
            assert3f(position, "Position ")

        # then create
        a = self._vfc.new_poi(name)

        new_node = Point(self, a)

        # and set properties
        if b is not None:
            new_node.parent = b
        if position is not None:
            new_node.position = position

        self._nodes.append(new_node)
        return new_node

    def new_rigidbody(
        self,
        name,
        mass=0,
        cog=(0, 0, 0),
        parent=None,
        position=None,
        rotation=None,
        inertia_radii=None,
        fixed=True,
    ) -> RigidBody:
        """Creates a new *rigidbody* node and adds it to the scene.

        Args:
            name: Name for the node, should be unique
            mass: optional, [0] mass in mT
            cog: optional, (0,0,0) cog-position in (m,m,m)
            parent: optional, name of the parent of the node
            position: optional, position for the node (x,y,z)
            rotation: optional, rotation for the node (rx,ry,rz)
            inertia_radii : optional, radii of gyration (rxx,ryy,rzz); only used for dynamics
            fixed [True]: optional, determines whether the axis is fixed [True] or free [False]. May also be a sequence of 6 booleans.

        Examples:
            scene.new_rigidbody("heavy_thing", mass = 10000, cog = (1.45, 0, -0.7))

        Returns:
            Reference to newly created RigidBody

        """

        # apply prefixes
        name = self._prefix_name(name)

        # check input
        assertValidName(name)
        self._verify_name_available(name)
        b = self._parent_from_node(parent)

        if position is not None:
            assert3f(position, "Position ")
        if rotation is not None:
            assert3f(rotation, "Rotation ")

        if inertia_radii is not None:
            assert3f_positive(inertia_radii, "Radii of inertia")
            assert mass > 0, ValueError(
                "Can not set radii of gyration without specifying mass"
            )

        if not isinstance(fixed, bool):
            if len(fixed) != 6:
                raise Exception(
                    '"fixed" parameter should either be True/False or a 6x bool sequence such as (True,True,False,False,True,False)'
                )

        # make elements

        a = self._vfc.new_axis(name)

        p = self._vfc.new_poi(name + vfc.VF_NAME_SPLIT + "cog")
        p.parent = a
        p.position = cog

        g = self._vfc.new_force(name + vfc.VF_NAME_SPLIT + "gravity")
        g.parent = p
        g.force = (0, 0, -vfc.G * mass)

        r = RigidBody(self, a, p, g)

        r.cog = cog  # set inertia
        r.mass = mass

        # and set properties
        if b is not None:
            r.parent = b
        if position is not None:
            r.position = position
        if rotation is not None:
            r.rotation = rotation

        if inertia_radii is not None:
            r.inertia_radii = inertia_radii

        if isinstance(fixed, bool):
            if fixed:
                r.set_fixed()
            else:
                r.set_free()
        else:
            r.fixed = fixed

        self._nodes.append(r)
        return r

    def new_cable(
        self, name, endA, endB, length=-1, EA=0, diameter=0, sheaves=None
    ) -> Cable:
        """Creates a new *cable* node and adds it to the scene.

        Args:
            name: Name for the node, should be unique
            endA : A Poi element to connect the first end of the cable to
            endB : A Poi element to connect the other end of the cable to
            length [-1] : un-stretched length of the cable in m; default [-1] create a cable with the current distance between the endpoints A and B
            EA [0] : stiffness of the cable in kN/m; default

            sheaves : [optional] A list of pois, these are sheaves that the cable runs over. Defined from endA to endB

        Examples:

            scene.new_cable('cable_name' endA='poi_start', endB = 'poi_end')  # minimal use

            scene.new_cable('cable_name', length=50, EA=1000, endA=poi_start, endB = poi_end, sheaves=[sheave1, sheave2])

            scene.new_cable('cable_name', length=50, EA=1000, endA='poi_start', endB = 'poi_end', sheaves=['single_sheave']) # also a single sheave needs to be provided as a list

        Notes:
            The default options for length and EA can be used to measure distances between points

        Returns:
            Reference to newly created Cable

        """

        # apply prefixes
        name = self._prefix_name(name)

        # first check
        assertValidName(name)
        self._verify_name_available(name)
        assert1f(length, "length")
        assert1f(EA, "EA")

        endA = self._poi_or_sheave_from_node(endA)
        endB = self._poi_or_sheave_from_node(endB)

        pois = [endA]
        if sheaves is not None:

            if isinstance(sheaves, Point):  # single sheave as poi or string
                sheaves = [sheaves]

            if isinstance(sheaves, Circle):  # single sheave as poi or string
                sheaves = [sheaves]

            if isinstance(sheaves, str):
                sheaves = [sheaves]

            for s in sheaves:
                # s may be a poi or a sheave
                pois.append(self._poi_or_sheave_from_node(s))

        pois.append(endB)

        # default options
        if length > -1:
            if length < 1e-9:
                raise Exception("Length should be more than 0")

        if EA < 0:
            raise Exception("EA should be more than 0")

        assert1f(diameter, "Diameter should be a number >= 0")

        if diameter < 0:
            raise Exception("Diameter should be >= 0")

        # then create
        a = self._vfc.new_cable(name)
        new_node = Cable(self, a)
        if length > 0:
            new_node.length = length
        new_node.EA = EA
        new_node.diameter = diameter

        new_node.connections = pois

        # and add to the scene
        self._nodes.append(new_node)

        if length < 0:
            new_node.length = 1e-8
            self._vfc.state_update()

            new_length = new_node.stretch + 1e-8

            if new_length > 0:
                new_node.length = new_length
            else:
                # is is possible that all nodes are at the same location which means the total length becomes 0
                self.delete(new_node.name)
                raise ValueError(
                    "No lengh has been supplied and all connection points are at the same location - unable to determine a non-zero default length. Please supply a length"
                )

        return new_node

    def new_force(self, name, parent=None, force=None, moment=None) -> Force:
        """Creates a new *force* node and adds it to the scene.

        Args:
            name: Name for the node, should be unique
            parent: name of the parent of the node [Poi]
            force: optional, global force on the node (x,y,z)
            moment: optional, global force on the node (x,y,z)


        Returns:
            Reference to newly created force

        """

        # apply prefixes
        name = self._prefix_name(name)

        # first check
        assertValidName(name)
        self._verify_name_available(name)
        b = self._poi_from_node(parent)

        if force is not None:
            assert3f(force, "Force ")

        if moment is not None:
            assert3f(moment, "Moment ")

        # then create
        a = self._vfc.new_force(name)

        new_node = Force(self, a)

        # and set properties
        if b is not None:
            new_node.parent = b
        if force is not None:
            new_node.force = force
        if moment is not None:
            new_node.moment = moment

        self._nodes.append(new_node)
        return new_node

    def new_circle(self, name, parent, axis, radius=0.0) -> Circle:
        """Creates a new *sheave* node and adds it to the scene.

        Args:
            name: Name for the node, should be unique
            parent: name of the parent of the node [Poi]
            axis: direction of the axis of rotation (x,y,z)
            radius: optional, radius of the sheave


        Returns:
            Reference to newly created sheave

        """

        # apply prefixes
        name = self._prefix_name(name)

        # first check
        assertValidName(name)
        self._verify_name_available(name)
        b = self._poi_from_node(parent)

        assert3f(axis, "Axis of rotation ")

        assert1f(radius, "Radius of sheave")

        # then create
        a = self._vfc.new_sheave(name)

        new_node = Circle(self, a)

        # and set properties
        new_node.parent = b
        new_node.axis = axis
        new_node.radius = radius

        self._nodes.append(new_node)
        return new_node

    def new_hydspring(
        self,
        name,
        parent,
        cob,
        BMT,
        BML,
        COFX,
        COFY,
        kHeave,
        waterline,
        displacement_kN,
    ) -> HydSpring:
        """Creates a new *hydspring* node and adds it to the scene.

        Args:
            name: Name for the node, should be unique
            parent: name of the parent of the node [Axis]
            cob: position of the CoB (x,y,z) in the parent axis system
            BMT: Vertical distance between CoB and meta-center for roll
            BML: Vertical distance between CoB and meta-center for pitch
            COFX: X-location of center of flotation (center of waterplane) relative to CoB
            COFY: Y-location of center of flotation (center of waterplane) relative to CoB
            kHeave : heave stiffness (typically Awl * rho * g)
            waterline : Z-position (elevation) of the waterline relative to CoB
            displacement_kN : displacement (typically volume * rho * g)


        Returns:
            Reference to newly created hydrostatic spring

        """

        # apply prefixes
        name = self._prefix_name(name)

        # first check
        assertValidName(name)
        self._verify_name_available(name)
        b = self._parent_from_node(parent)
        assert3f(cob, "CoB ")
        assert1f(BMT, "BMT ")
        assert1f(BML, "BML ")
        assert1f(COFX, "COFX ")
        assert1f(COFY, "COFY ")
        assert1f(kHeave, "kHeave ")
        assert1f(waterline, "waterline ")
        assert1f(displacement_kN, "displacement_kN ")

        # then create
        a = self._vfc.new_hydspring(name)
        new_node = HydSpring(self, a)

        new_node.cob = cob
        new_node.parent = b
        new_node.BMT = BMT
        new_node.BML = BML
        new_node.COFX = COFX
        new_node.COFY = COFY
        new_node.kHeave = kHeave
        new_node.waterline = waterline
        new_node.displacement_kN = displacement_kN

        self._nodes.append(new_node)

        return new_node

    def new_linear_connector_6d(self, name, main, secondary, stiffness=None) -> LC6d:
        """Creates a new *linear connector 6d* node and adds it to the scene.

        Args:
            name: Name for the node, should be unique
            main: Main axis system [Axis]
            secondary: Secondary axis system [Axis]
            stiffness: optional, connection stiffness (x,y,z, rx,ry,rz)

        See :py:class:`LC6d` for details

        Returns:
            Reference to newly created connector

        """

        # apply prefixes
        name = self._prefix_name(name)

        # first check
        assertValidName(name)
        self._verify_name_available(name)
        m = self._parent_from_node(secondary)
        s = self._parent_from_node(main)

        if stiffness is not None:
            assert6f(stiffness, "Stiffness ")
        else:
            stiffness = (0, 0, 0, 0, 0, 0)

        # then create
        a = self._vfc.new_linearconnector6d(name)

        new_node = LC6d(self, a)

        # and set properties
        new_node.main = m
        new_node.secondary = s
        new_node.stiffness = stiffness

        self._nodes.append(new_node)
        return new_node

    def new_connector2d(
        self, name, nodeA, nodeB, k_linear=0, k_angular=0
    ) -> Connector2d:
        """Creates a new *new_connector2d* node and adds it to the scene.

        Args:
            name: Name for the node, should be unique
            nodeB: First axis system [Axis]
            nodeA: Second axis system [Axis]

            k_linear : linear stiffness in kN/m
            k_angular : angular stiffness in kN*m / rad

        Returns:
            Reference to newly created connector2d

        """

        # apply prefixes
        name = self._prefix_name(name)

        # first check
        assertValidName(name)
        self._verify_name_available(name)
        m = self._parent_from_node(nodeA)
        s = self._parent_from_node(nodeB)

        assert1f(k_linear, "Linear stiffness")
        assert1f(k_angular, "Angular stiffness")

        # then create
        a = self._vfc.new_connector2d(name)

        new_node = Connector2d(self, a)

        # and set properties
        new_node.nodeA = m
        new_node.nodeB = s
        new_node.k_linear = k_linear
        new_node.k_angular = k_angular

        self._nodes.append(new_node)
        return new_node

    def new_beam(
        self,
        name,
        nodeA,
        nodeB,
        EIy=0,
        EIz=0,
        GIp=0,
        EA=0,
        L=None,
        mass=0,
        n_segments=1,
        tension_only=False,
    ) -> Beam:
        """Creates a new *beam* node and adds it to the scene.

        Args:
            name: Name for the node, should be unique
            nodeA: First axis system [Axis]
            nodeB: Second axis system [Axis]

            All stiffness terms default to 0
            The length defaults to the distance between nodeA and nodeB


        See :py:class:`LinearBeam` for details

        Returns:
            Reference to newly created beam

        """

        # apply prefixes
        name = self._prefix_name(name)

        # first check
        assertValidName(name)
        self._verify_name_available(name)
        m = self._parent_from_node(nodeA)
        s = self._parent_from_node(nodeB)

        if L is None:
            L = np.linalg.norm(
                np.array(m.global_position) - np.array(s.global_position)
            )
        else:
            if L <= 0:
                raise ValueError("L should be > 0 as stiffness is defined per length.")

        assert1f_positive_or_zero(EIy, "EIy should be >= 0")
        assert1f_positive_or_zero(EIz, "EIz should be >= 0")
        assert1f_positive_or_zero(GIp, "GIp should be >= 0")
        assert1f_positive_or_zero(EA, "EA should be >= 0")
        assertBool(tension_only, "tension_only should be bool")
        assert1f(mass, "Mass shall be a number")
        n_segments = int(round(n_segments))

        # then create
        a = self._vfc.new_linearbeam(name)

        new_node = Beam(self, a)

        # and set properties
        new_node.nodeA = m
        new_node.nodeB = s
        new_node.EIy = EIy
        new_node.EIz = EIz
        new_node.GIp = GIp
        new_node.EA = EA
        new_node.L = L
        new_node.mass = mass
        new_node.n_segments = n_segments
        new_node.tension_only = tension_only

        self._nodes.append(new_node)
        return new_node

    def new_buoyancy(self, name, parent=None, density=1.025) -> Buoyancy:
        """Creates a new *buoyancy* node and adds it to the scene.

        Args:
            name: Name for the node, should be unique
            parent: optional, name of the parent of the node


        Returns:
            Reference to newly created buoyancy

        """

        # apply prefixes
        name = self._prefix_name(name)

        # first check
        assertValidName(name)
        self._verify_name_available(name)
        b = self._parent_from_node(parent)

        if b is None:
            raise ValueError("A valid parent must be defined for a Buoyancy node")

        assert1f_positive_or_zero(density, "density")

        # then create
        a = self._vfc.new_buoyancy(name)
        new_node = Buoyancy(self, a)

        # and set properties
        if b is not None:
            new_node.parent = b

        new_node.density = density

        self._nodes.append(new_node)
        return new_node

    def new_tank(self, name, parent=None, density=1.025, free_flooding=False) -> Tank:
        """Creates a new *tank* node and adds it to the scene.

        Args:
            name: Name for the node, should be unique
            parent: optional, name of the parent of the node

        Returns:
            Reference to newly created Tank

        """

        # apply prefixes
        name = self._prefix_name(name)

        # first check
        assertValidName(name)
        self._verify_name_available(name)
        b = self._parent_from_node(parent)

        if b is None:
            raise ValueError("A valid parent must be defined for a Tank")

        assert isinstance(free_flooding, bool), ValueError(
            "free_flooding shall be True or False"
        )

        assert1f(density, "density")

        # then create
        a = self._vfc.new_tank(name)
        new_node = Tank(self, a)
        new_node.density = density

        # and set properties
        if b is not None:
            new_node.parent = b

        new_node.free_flooding = free_flooding

        self._nodes.append(new_node)
        return new_node

    def new_contactmesh(self, name, parent=None) -> ContactMesh:
        """Creates a new *contactmesh* node and adds it to the scene.

        Args:
            name: Name for the node, should be unique
            parent: optional, name of the parent of the node

        Returns:
            Reference to newly created contact mesh

        """

        # apply prefixes
        name = self._prefix_name(name)

        # first check
        assertValidName(name)
        self._verify_name_available(name)
        b = self._parent_from_node(parent)

        # then create
        a = self._vfc.new_contactmesh(name)
        new_node = ContactMesh(self, a)

        # and set properties
        if b is not None:
            new_node.parent = b

        self._nodes.append(new_node)
        return new_node

    def new_spmt(
        self,
        name,
        parent,
        maximal_length=1.8,
        nominal_length=1.5,
        k=1e6,
        meshes=None,
        axles=None,
    ) -> SPMT:
        """Creates a new *SPMT* node and adds it to the scene.

        Args:
            name: Name for the node, should be unique
            parent: name of the parent of the node [Axis]
            maximal_length: optional, maximum distance between top and bottom of wheel (1.5m + 300mm)
            nominal_length: optional, nominal distance between top and bottom of wheel [1.5m]
            k : stiffness per axle [kN/m]
            meshes : list of contact meshes
            axles  : list of axle locations [(x,y,z),(x,y,z), ... ]

        Returns:
            Reference to newly created SPMT

        """

        # apply prefixes
        name = self._prefix_name(name)

        # first check
        assertValidName(name)
        self._verify_name_available(name)
        parent = self._node_from_node_or_str(parent)
        assert isinstance(parent, Axis), ValueError(
            f"Parent should be an axis system or derived, not a {type(parent)}"
        )

        assert1f_positive_or_zero(maximal_length, "maximal_length ")
        assert1f_positive_or_zero(nominal_length, "nominal_length ")

        if meshes is not None:
            meshes = make_iterable(meshes)
            for mesh in meshes:
                test = self._node_from_node(
                    mesh, ContactMesh
                )  # throws error if not found

        if axles is not None:
            for p in axles:
                assert3f(p, "axle locations should be (x,y,z)")

        # then create
        a = self._vfc.new_spmt(name)

        new_node = SPMT(self, a)

        # and set properties
        new_node.parent = parent
        new_node.k = k
        new_node.max_length = maximal_length
        new_node.nominal_length = nominal_length

        if meshes is not None:
            new_node.meshes = meshes

        if axles is not None:
            new_node.axles = axles

        self._nodes.append(new_node)
        return new_node

    def new_contactball(
        self, name, parent=None, radius=1, k=9999, meshes=None
    ) -> ContactBall:
        """Creates a new *force* node and adds it to the scene.

        Args:
            name: Name for the node, should be unique
            parent: name of the parent of the node [Poi]
            force: optional, global force on the node (x,y,z)
            moment: optional, global force on the node (x,y,z)


        Returns:
            Reference to newly created force

        """

        # apply prefixes
        name = self._prefix_name(name)

        # first check
        assertValidName(name)
        self._verify_name_available(name)
        b = self._poi_from_node(parent)

        assert1f_positive_or_zero(radius, "Radius ")
        assert1f_positive_or_zero(k, "k ")

        if meshes is not None:
            meshes = make_iterable(meshes)
            for mesh in meshes:
                test = self._node_from_node(mesh, ContactMesh)

        # then create
        a = self._vfc.new_contactball(name)

        new_node = ContactBall(self, a)

        # and set properties
        if b is not None:
            new_node.parent = b
        if k is not None:
            new_node.k = k
        if radius is not None:
            new_node.radius = radius

        if meshes is not None:
            new_node.meshes = meshes

        self._nodes.append(new_node)
        return new_node

    def new_ballastsystem(self, name, parent: Axis) -> BallastSystem:
        """Creates a new *rigidbody* node and adds it to the scene.

        Args:
            name: Name for the node, should be unique
            parent: name of the parent of the ballast system (ie: the vessel axis system)

        Examples:
            scene.new_ballastsystem("cheetah_ballast", parent="Cheetah")

        Returns:
            Reference to newly created BallastSystem

        """

        # apply prefixes
        name = self._prefix_name(name)

        # check input
        assertValidName(name)
        self._verify_name_available(name)
        b = self._parent_from_node(parent)

        parent = self._parent_from_node(parent)  # handles verification of type as well

        # make elements
        r = BallastSystem(self, parent)
        r.name = name

        self._nodes.append(r)
        return r

    def new_sling(
        self,
        name,
        length=-1,
        EA=1.0,
        mass=0.1,
        endA=None,
        endB=None,
        LeyeA=None,
        LeyeB=None,
        LspliceA=None,
        LspliceB=None,
        diameter=0.1,
        sheaves=None,
    ) -> Sling:
        """
        Creates a new sling, adds it to the scene and returns a reference to the newly created object.

        See Also:
            Sling

        Args:
            name:    name
            length:  length of the sling [m], defaults to distance between endpoints
            EA:      stiffness in kN, default: 1.0 (note: equilibrium will fail if mass >0 and EA=0)
            mass:    mass in mT, default  0.1
            endA:    element to connect end A to [poi, circle]
            endB:    element to connect end B to [poi, circle]
            LeyeA:   inside eye on side A length [m], defaults to 1/6th of length
            LeyeB:   inside eye on side B length [m], defaults to 1/6th of length
            LspliceA: splice length on side A [m] (the part where the cable is connected to itself)
            LspliceB: splice length on side B [m] (the part where the cable is connected to itself)
            diameter: cable diameter in m, defaul to 0.1
            sheaves:  optional: list of sheaves/pois that the sling runs over

        Returns:
            a reference to the newly created Sling object.

        """

        # apply prefixes
        name = self._prefix_name(name)

        # first check
        assertValidName(name)
        self._verify_name_available(name)

        name_prefix = name + vfc.MANAGED_NODE_IDENTIFIER
        postfixes = [
            "_spliceA",
            "_spliceA",
            "_spliceA2",
            "_spliceAM",
            "_spliceA_visual",
            "spliceB",
            "_spliceB1",
            "_spliceB2",
            "_spliceBM",
            "_spliceB_visual",
            "_main_part",
            "_eyeA",
            "_eyeB",
        ]

        for pf in postfixes:
            self._verify_name_available(name_prefix + pf)

        endA = self._poi_or_sheave_from_node(endA)
        endB = self._poi_or_sheave_from_node(endB)

        if length == -1:  # default
            if endA is None or endB is None:
                raise ValueError(
                    "Length for cable is not provided, so defaults to distance between endpoints; but at least one of the endpoints is None."
                )

            length = np.linalg.norm(
                np.array(endA.global_position) - np.array(endB.global_position)
            )

        if LeyeA is None:  # default
            LeyeA = length / 6
        if LeyeB is None:  # default
            LeyeB = length / 6
        if LspliceA is None:  # default
            LspliceA = length / 6
        if LspliceB is None:  # default
            LspliceB = length / 6

        if sheaves is None:
            sheaves = []

        assert1f_positive_or_zero(diameter, "Diameter")
        assert1f_positive_or_zero(mass, "mass")

        assert1f_positive(length, "Length")
        assert1f_positive(LeyeA, "length of eye A")
        assert1f_positive(LeyeB, "length of eye B")
        assert1f_positive(LspliceA, "length of splice A")
        assert1f_positive(LspliceB, "length of splice B")

        for s in sheaves:
            _ = self._poi_or_sheave_from_node(s)

        # then make element
        # __init__(self, scene, name, Ltotal, LeyeA, LeyeB, LspliceA, LspliceB, diameter, EA, mass, endA = None, endB=None, sheaves=None):

        node = Sling(
            scene=self,
            name=name,
            length=length,
            LeyeA=LeyeA,
            LeyeB=LeyeB,
            LspliceA=LspliceA,
            LspliceB=LspliceB,
            diameter=diameter,
            EA=EA,
            mass=mass,
            endA=endA,
            endB=endB,
            sheaves=sheaves,
        )
        self._nodes.append(node)

        return node

    def new_shackle(self, name, kind="GP500") -> Shackle:
        """
        Creates a new shackle, adds it to the scene and returns a reference to the newly created object.

        See Also:
            Shackle

        Args:
            name:   name
            kind:  type of shackle; eg 'GP500'


        Returns:
            a reference to the newly created Shackle object.

        """

        # apply prefixes
        name = self._prefix_name(name)

        # first check
        assertValidName(name)
        self._verify_name_available(name)

        name_prefix = name + vfc.MANAGED_NODE_IDENTIFIER
        postfixes = [
            "_body",
            "_pin_point",
            "_bow_point",
            "_inside_circle_center",
            "_inside",
            "_visual",
        ]
        for pf in postfixes:
            self._verify_name_available(name_prefix + pf)

        # then make element

        # make elements

        a = self._vfc.new_axis(name)

        p = self._vfc.new_poi(name + vfc.VF_NAME_SPLIT + "cog")
        p.parent = a

        g = self._vfc.new_force(name + vfc.VF_NAME_SPLIT + "gravity")
        g.parent = p

        node = Shackle(scene=self, name=name, kind=kind, a=a, p=p, g=g)

        self._nodes.append(node)

        return node

    def print_python_code(self):
        """Prints the python code that generates the current scene

        See also: give_python_code
        """
        for line in self.give_python_code().split("\n"):
            print(line)

    def give_python_code(self):
        """Generates the python code that rebuilds the scene and elements in its current state."""

        import datetime
        import getpass

        self.sort_nodes_by_dependency()

        code = "# auto generated pyhton code"
        try:
            code += "\n# By {}".format(getpass.getuser())
        except:
            code += "\n# By an unknown"

        code += "\n# Time: {} UTC".format(str(datetime.datetime.now()).split(".")[0])

        code += "\n\n# To be able to distinguish the important number (eg: fixed positions) from"
        code += "\n# non-important numbers (eg: a position that is solved by the static solver) we use a dummy-function called 'solved'."
        code += "\n# For anything written as solved(number) that actual number does not influence the static solution"
        code += "\ndef solved(number):\n    return number\n"

        for n in self._nodes:

            if n._manager is None:
                # print(f'code for {n.name}')
                code += "\n" + n.give_python_code()
            else:
                if n._manager.creates(n):
                    pass
                else:
                    code += "\n" + n.give_python_code()

                # print(f'skipping {n.name} ')

        # store the visibility code separately

        for n in self._nodes:
            if not n.visible:
                code += f"\ns['{n.name}'].visible = False"  # only report is not the default value

        return code

    def save_scene(self, filename):
        """Saves the scene to a file

        This saves the scene in its current state to a file.
        Opening the saved file will reproduce exactly this scene.

        This sounds nice, but beware that it only saves the resulting model, not the process of creating the model.
        This means that if you created the model in a parametric fashion or assembled the model from other models then these are not re-evaluated when the model is openened again.
        So lets say this model uses a sub-model of a lifting hook which is imported from another file. If that other file is updated then
        the results of that update will not be reflected in the saved model.

        If no path is present in the file-name then the model will be saved in the last (lowest) resource-path (if any)

        Args:
            filename : filename or file-path to save the file. Default extension is .dave

        Returns:
            the full path to the saved file

        """

        code = self.give_python_code()

        filename = Path(filename)

        # add .dave extension if needed
        if filename.suffix != ".dave":
            filename = Path(str(filename) + ".dave")

        # add path if not provided
        if not filename.is_absolute():
            try:
                filename = Path(self.resources_paths[-1]) / filename
            except:
                pass  # save in current folder

        # make sure directory exists
        directory = filename.parent
        if not directory.exists():
            directory.mkdir()

        f = open(filename, "w+")
        f.write(code)
        f.close()

        self._print("Saved as {}".format(filename))

        return filename

    def print_node_tree(self):

        self.sort_nodes_by_dependency()

        to_be_printed = []
        for n in self._nodes:
            to_be_printed.append(n.name)

        # to_be_printed.reverse()

        def print_deps(name, spaces):

            node = self[name]
            deps = self.nodes_with_parent(node)
            print(spaces + name + " [" + str(type(node)).split(".")[-1][:-2] + "]")

            if deps is not None:
                for dep in deps:
                    if spaces == "":
                        spaces_plus = " |-> "
                    else:
                        spaces_plus = " |   " + spaces
                    print_deps(dep, spaces_plus)

            to_be_printed.remove(name)

        while to_be_printed:
            name = to_be_printed[0]
            print_deps(name, "")

    def run_code(self, code):
        """Runs the provided code with 's' as self"""

        import DAVE

        locals = DAVE.__dict__
        locals['s'] = self

        try:
            exec(code, {}, locals)
        except Exception as M:
            for i, line in enumerate(code.split("\n")):
                print(f"{i} {line}")
            raise M

    def load_scene(self, filename=None):
        """Loads the contents of filename into the current scene.

        This function is typically used on an empty scene.

        Filename is appended with .dave if needed.
        File is searched for in the resource-paths.

        See also: import scene"""

        if filename is None:
            raise Exception("Please provide a file-name")

        try:
            filename = self.get_resource_path(filename)
        except:
            if not str(filename).endswith(".dave"):
                filename = Path(str(filename) + ".dave")

        print("Loading {}".format(filename))

        f = open(file=filename, mode="r")
        code = ""
        for line in f:
            code += line + "\n"

        self.run_code(code)

    def import_scene(self, other, prefix="", containerize=True):
        """Copy-paste all nodes of scene "other" into current scene.

        To avoid double names it is recommended to use a prefix. This prefix will be added to all element names.

        Returns:
            Contained (Axis-type Node) : if the imported scene is containerized then a reference to the created container is returned.
        """

        if isinstance(other, Path):
            other = str(other)

        if isinstance(other, str):
            other = Scene(other)

        if not isinstance(other, Scene):
            raise TypeError("Other should be a Scene but is a " + str(type(other)))

        old_prefix = self._name_prefix
        imported_element_names = []

        for n in other._nodes:
            imported_element_names.append(prefix + n.name)

        # check for double names

        for new_node_name in imported_element_names:
            if not self.name_available(new_node_name):
                raise NameError(
                    'An element with name "{}" is already present. Please use a prefix to avoid double names'.format(
                        new_node_name
                    )
                )

        self._name_prefix = prefix

        code = other.give_python_code()

        self.run_code(code)

        self._name_prefix = old_prefix  # restore

        # Move all imported elements without a parent into a newly created axis system
        if containerize:

            container_name = self.available_name_like("import_container")

            c = self.new_axis(prefix + container_name)

            for name in imported_element_names:

                node = self[name]

                if not node.manager:
                    if not isinstance(node, NodeWithParent):
                        continue

                    if node.parent is None:
                        node.change_parent_to(c)

            return c

        return None

    def copy(self):
        """Creates a full and independent copy of the scene and returns it.

        Example:
            s = Scene()
            c = s.copy()
            c.new_axis('only in c')

        """

        c = Scene()
        c.import_scene(self, containerize=False)
        return c

    # =================== DYNAMICS ==================

    def dynamics_M(self, delta=1e-6):
        """Returns the mass matrix of the scene"""
        self.update()

        return self._vfc.M(delta)

    def dynamics_K(self, delta=1e-6):
        """Returns the stiffness matrix of the scene for a perturbation of delta

        A component is positive if a displacement introduces an reaction force in the opposite direction.
        or:
        A component is positive if a positive force is needed to introduce a positive displacement.
        """
        self.update()

        return -self._vfc.K(delta)

    def dynamics_nodes(self):
        """Returns a list of nodes associated with the rows/columns of M and K"""
        self.update()
        nodes = self._vfc.get_dof_elements()

        node_names = [n.name for n in self._nodes]

        r = []
        for n in nodes:
            if n.name in node_names:
                r.append(self[n.name])
            else:
                r.append(None)

        return r

    def dynamics_modes(self):
        """Returns a list of modes (0=x ... 5=rotation z) associated with the rows/columns of M and K"""
        self.update()
        return self._vfc.get_dof_modes()


# =================== None-Node Classes

"""This is a container for a pyo3d.MomentDiagram object providing plot methods"""


class LoadShearMomentDiagram:
    def __init__(self, datasource):
        """

        Args:
            datasource: pyo3d.MomentDiagram object
        """

        self.datasource = datasource

    def give_shear_and_moment(self, grid_n=100):
        """Returns (position, shear, moment)"""
        x = self.datasource.grid(grid_n)
        return x, self.datasource.Vz, self.datasource.My

    def plot_simple(self, **kwargs):
        """Plots the bending moment and shear in a single yy-plot.
        Creates a new figure

        any keyword arguments are passed to plt.figure(), so for example dpi=150 will increase the dpi

        Returns: figure
        """
        x, Vz, My = self.give_shear_and_moment()
        import matplotlib.pyplot as plt
        plt.rcParams.update({"font.family": "sans-serif"})
        plt.rcParams.update({"font.sans-serif": "consolas"})
        plt.rcParams.update({"font.size": 10})

        fig, ax1 = plt.subplots(1,1,**kwargs)
        ax2 = ax1.twinx()

        ax1.plot(x, My, "g", lw=1, label="Bending Moment")
        ax2.plot(x, Vz, "b", lw=1, label="Shear Force")

        from DAVE.gui.helpers.align_zeros_of_yyplots import align_y0_axis

        align_y0_axis(ax1, ax2)

        ax1.set_xlabel("Position [m]")
        ax1.set_ylabel("Bending Moment [kNm]")
        ax2.set_ylabel("Shear Force [kN]")

        ax1.tick_params(axis="y", colors="g")
        ax2.tick_params(axis="y", colors="b")

        # fig.legend()  - obvious from the axis

        ext = 0.1 * (np.max(x) - np.min(x))
        xx = [np.min(x) - ext, np.max(x) + ext]
        ax1.plot(xx, [0, 0], c=[0.5, 0.5, 0.5], lw=1, linestyle=":")
        ax1.set_xlim(xx)

        return fig

    def plot(self, grid_n=100, merge_adjacent_loads=True, filename=None):
        m = self.datasource  # alias

        x = m.grid(grid_n)
        linewidth = 1

        n = m.nLoads

        import matplotlib.pyplot as plt

        #
        plt.rcParams.update({"font.family": "sans-serif"})
        plt.rcParams.update({"font.sans-serif": "consolas"})
        plt.rcParams.update({"font.size": 6})

        fig, (ax0, ax1, ax2) = plt.subplots(3, 1, figsize=(8.27, 11.69), dpi=100)
        textsize = 6

        # get loads

        loads = [m.load(i) for i in range(n)]

        texts = []  # for label placement
        texts_second = []  # for label placement

        # merge loads with same source and matching endpoints

        if merge_adjacent_loads:

            to_be_plotted = [loads[0]]

            for load in loads[1:]:
                name = load[2]

                # if the previous load is a continuous load from the same source
                # and the current load is also a continuous load
                # then merge the two.
                prev_load = to_be_plotted[-1]

                if len(prev_load[0]) != 2:  # not a point-load
                    if len(load[0]) != 2:  # not a point-load
                        if prev_load[2] == load[2]:  # same name

                            # merge the two
                            # remove the last (zero) entry of the previous lds
                            # as well as the first entry of these

                            # smoothed
                            xx = [*prev_load[0][:-1], *load[0][2:]]
                            yy = [
                                *prev_load[1][:-2],
                                0.5 * (prev_load[1][-2] + load[1][1]),
                                *load[1][2:],
                            ]

                            to_be_plotted[-1] = (xx, yy, load[2])

                            continue
                # else
                if np.max(np.abs(load[1])) > 1e-6:
                    to_be_plotted.append(load)

        else:
            to_be_plotted = loads

        #
        from matplotlib import cm

        colors = cm.get_cmap("hsv", lut=len(to_be_plotted))

        from matplotlib.patches import Polygon

        ax0_second = ax0.twinx()

        for icol, ld in enumerate(to_be_plotted):

            xx = ld[0]
            yy = ld[1]
            name = ld[2]

            if np.max(np.abs(yy)) < 1e-6:
                continue

            is_concentrated = len(xx) == 2

            # determine the name, default to Force / q-load if no name is present
            if name == "":
                if is_concentrated:
                    name = "Force "
                else:
                    name = "q-load "

            col = [0.8 * c for c in colors(icol)]
            col[3] = 1.0  # alpha

            if is_concentrated:  # concentrated loads on left axis
                lbl = f" {name} {ld[1][1]:.2f}"
                texts.append(
                    ax0.text(
                        xx[0], yy[1], lbl, fontsize=textsize, horizontalalignment="left"
                    )
                )
                ax0.plot(xx, yy, label=lbl, color=col, linewidth=linewidth)
                if yy[1] > 0:
                    ax0.plot(xx[1], yy[1], marker="^", color=col, linewidth=linewidth)
                else:
                    ax0.plot(xx[1], yy[1], marker="v", color=col, linewidth=linewidth)

            else:  # distributed loads on right axis
                lbl = f"{name}"  # {yy[1]:.2f} kN/m at {xx[0]:.3f}m .. {yy[-2]:.2f} kN/m at {xx[-1]:.3f}m"

                vertices = [(xx[i], yy[i]) for i in range(len(xx))]

                ax0_second.add_patch(
                    Polygon(vertices, facecolor=[col[0], col[1], col[2], 0.2])
                )
                ax0_second.plot(xx, yy, label=lbl, color=col, linewidth=linewidth)

                lx = np.mean(xx)
                ly = np.interp(lx, xx, yy)

                texts_second.append(
                    ax0_second.text(
                        lx,
                        ly,
                        lbl,
                        color=[0, 0, 0],
                        horizontalalignment="center",
                        fontsize=textsize,
                    )
                )

        ax0.grid()
        ax0.set_title("Loads")
        ax0.set_ylabel("Load [kN]")
        ax0_second.set_ylabel("Load [kN/m]")

        # plot moments
        # each concentrated load may have a moment as well
        for i in range(m.nLoads):
            mom = m.moment(i)
            if np.linalg.norm(mom) > 1e-6:
                load = m.load(i)
                xx = load[0][0]
                lbl = f"{load[2]}, m = {mom[1]:.2f} kNm"
                ax0.plot(xx, 0, marker="x", label=lbl, color=(0, 0, 0, 1))
                texts.append(
                    ax0.text(
                        xx, 0, lbl, horizontalalignment="center", fontsize=textsize
                    )
                )

        fig.legend(loc="upper right")

        # add a zero-line
        xx = [np.min(x), np.max(x)]
        ax0.plot(xx, (0, 0), "k-")

        from DAVE.gui.helpers.align_zeros_of_yyplots import align_y0_axis

        align_y0_axis(ax0, ax0_second)

        from DAVE.reporting.utils.TextAvoidOverlap import minimizeTextOverlap

        minimizeTextOverlap(
            texts_second,
            fig=fig,
            ax=ax0_second,
            vertical_only=True,
            optimize_initial_positions=False,
            annotate=False,
        )
        minimizeTextOverlap(
            texts,
            fig=fig,
            ax=ax0,
            vertical_only=True,
            optimize_initial_positions=False,
            annotate=False,
        )

        ax0.spines["top"].set_visible(False)
        ax0.spines["bottom"].set_visible(False)

        ax0_second.spines["top"].set_visible(False)
        ax0_second.spines["bottom"].set_visible(False)

        ax1.plot(x, m.Vz, "k-", linewidth=linewidth)

        i = np.argmax(np.abs(m.Vz))
        ax1.plot(x[i], m.Vz[i], "b*")
        ax1.text(x[i], m.Vz[i], f"{m.Vz[i]:.2f}")

        ax1.grid()
        ax1.set_title("Shear")
        ax1.set_ylabel("[kN]")

        ax2.plot(x, m.My, "k-", linewidth=linewidth)
        i = np.argmax(np.abs(m.My))
        ax2.plot(x[i], m.My[i], "b*")
        ax2.text(x[i], m.My[i], f"{m.My[i]:.2f}")

        ax2.grid()
        ax2.set_title("Moment")
        ax2.set_ylabel("[kN*m]")

        if filename is None:
            plt.show()
        else:
            fig.savefig(filename)

Functions

def node_setter_manageable(func)
Expand source code
def node_setter_manageable(func):
    @functools.wraps(func)
    def wrapper_decorator(self, *args, **kwargs):
        self._verify_change_allowed()
        value = func(self, *args, **kwargs)
        return value

    return wrapper_decorator
def node_setter_observable(func)
Expand source code
def node_setter_observable(func):
    @functools.wraps(func)
    def wrapper_decorator(self, *args, **kwargs):
        value = func(self, *args, **kwargs)
        # Do something after
        self._notify_observers()

        return value

    return wrapper_decorator

Classes

class Axis (scene, vfAxis)

Axis

Axes are the main building blocks of the geometry. They have a position and an rotation in space. Other nodes can be placed on them. Axes can be nested by parent/child relationships meaning that an axis can be placed on an other axis. The possible movements of an axis can be controlled in each degree of freedom using the "fixed" property.

Axes are also the main building block of inertia. Dynamics are controlled using the inertia properties of an axis: inertia [mT], inertia_position[m,m,m] and inertia_radii [m,m,m]

Notes

  • circular references are not allowed: It is not allowed to place a on b and b on a
Expand source code
class Axis(NodeWithParentAndFootprint):
    """
    Axis

    Axes are the main building blocks of the geometry. They have a position and an rotation in space. Other nodes can be placed on them.
    Axes can be nested by parent/child relationships meaning that an axis can be placed on an other axis.
    The possible movements of an axis can be controlled in each degree of freedom using the "fixed" property.

    Axes are also the main building block of inertia.
    Dynamics are controlled using the inertia properties of an axis: inertia [mT], inertia_position[m,m,m] and inertia_radii [m,m,m]


    Notes:
         - circular references are not allowed: It is not allowed to place a on b and b on a

    """

    def __init__(self, scene, vfAxis):
        super().__init__(scene, vfAxis)
        self._None_parent_acceptable = True

        self._inertia = 0
        self._inertia_position = (0, 0, 0)
        self._inertia_radii = (0, 0, 0)

        self._pointmasses = list()
        for i in range(6):
            p = scene._vfc.new_pointmass(
                self.name + vfc.VF_NAME_SPLIT + "pointmass_{}".format(i)
            )
            p.parent = vfAxis
            self._pointmasses.append(p)
        self._update_inertia()

    def depends_on(self):
        if self.parent is None:
            return []
        else:
            return [self.parent]

    def _delete_vfc(self):
        for p in self._pointmasses:
            self._scene._vfc.delete(p.name)

        super()._delete_vfc()

    @property
    def inertia(self):
        """The linear inertia of the axis in [mT] Aka: "Mass"
        - used only for dynamics"""
        return self._inertia

    @inertia.setter
    @node_setter_manageable
    @node_setter_observable
    def inertia(self, val):

        assert1f(val, "Inertia")
        self._inertia = val
        self._update_inertia()

    @property
    def inertia_position(self):
        """The position of the center of inertia. Aka: "cog" [m,m,m] (local axis)
        - used only for dynamics
        - defined in local axis system"""
        return tuple(self._inertia_position)

    @inertia_position.setter
    @node_setter_manageable
    @node_setter_observable
    def inertia_position(self, val):

        assert3f(val, "Inertia position")
        self._inertia_position = tuple(val)
        self._update_inertia()

    @property
    def inertia_radii(self):
        """The radii of gyration of the inertia [m,m,m] (local axis)

        Used to calculate the mass moments of inertia via

        Ixx = rxx^2 * inertia
        Iyy = rxx^2 * inertia
        Izz = rxx^2 * inertia

        Note that DAVE does not directly support cross terms in the interia matrix of an axis system. If you want to
        use cross terms then combine multiple axis system to reach the same result. This is because inertia matrices with
        diagonal terms can not be translated.
        """
        return np.array(self._inertia_radii, dtype=float)

    @inertia_radii.setter
    @node_setter_manageable
    @node_setter_observable
    def inertia_radii(self, val):

        assert3f_positive(val, "Inertia radii of gyration")
        self._inertia_radii = val
        self._update_inertia()

    def _update_inertia(self):
        # update mass
        for i in range(6):
            self._pointmasses[i].inertia = self._inertia / 6

        if self._inertia <= 0:
            return

        # update radii and position
        pos = radii_to_positions(*self._inertia_radii)
        for i in range(6):
            p = (
                pos[i][0] + self._inertia_position[0],
                pos[i][1] + self._inertia_position[1],
                pos[i][2] + self._inertia_position[2],
            )
            self._pointmasses[i].position = p
            # print('{} at {} {} {}'.format(self._inertia/6, *p))

    @property
    def fixed(self):
        """Determines which of the six degrees of freedom are fixed, if any. (x,y,z,rx,ry,rz).
        True means that that degree of freedom will not change when solving statics.
        False means a that is may be changed in order to find equilibrium.

        These are the expressed on the coordinate system of the parent (if any) or the global axis system (if no parent)

        See Also: set_free, set_fixed
        """
        return self._vfNode.fixed

    @fixed.setter
    @node_setter_manageable
    @node_setter_observable
    def fixed(self, var):

        if var == True:
            var = (True, True, True, True, True, True)
        if var == False:
            var = (False, False, False, False, False, False)

        self._vfNode.fixed = var

    def set_free(self):
        """Sets .fixed to (False,False,False,False,False,False)"""
        self._vfNode.set_free()

    def set_fixed(self):
        """Sets .fixed to (True,True,True,True,True,True)"""

        self._vfNode.set_fixed()

    @property
    def x(self):
        """The x-component of the position vector (parent axis) [m]"""
        return self.position[0]

    @property
    def y(self):
        """The y-component of the position vector (parent axis) [m]"""
        return self.position[1]

    @property
    def z(self):
        """The z-component of the position vector (parent axis) [m]"""
        return self.position[2]

    @x.setter
    @node_setter_manageable
    @node_setter_observable
    def x(self, var):

        a = self.position
        self.position = (var, a[1], a[2])

    @y.setter
    @node_setter_manageable
    @node_setter_observable
    def y(self, var):

        a = self.position
        self.position = (a[0], var, a[2])

    @z.setter
    @node_setter_manageable
    @node_setter_observable
    def z(self, var):

        a = self.position
        self.position = (a[0], a[1], var)

    @property
    def position(self):
        """Position of the axis (parent axis) [m,m,m]

        These are the expressed on the coordinate system of the parent (if any) or the global axis system (if no parent)"""
        return self._vfNode.position

    @position.setter
    @node_setter_manageable
    @node_setter_observable
    def position(self, var):

        assert3f(var, "Position ")
        self._vfNode.position = var
        self._scene._geometry_changed()

    @property
    def rx(self):
        """The x-component of the rotation vector [degrees] (parent axis)"""
        return self.rotation[0]

    @property
    def ry(self):
        """The y-component of the rotation vector [degrees] (parent axis)"""
        return self.rotation[1]

    @property
    def rz(self):
        """The z-component of the rotation vector [degrees], (parent axis)"""
        return self.rotation[2]

    @rx.setter
    @node_setter_manageable
    @node_setter_observable
    def rx(self, var):

        a = self.rotation
        self.rotation = (var, a[1], a[2])

    @ry.setter
    @node_setter_manageable
    @node_setter_observable
    def ry(self, var):

        a = self.rotation
        self.rotation = (a[0], var, a[2])

    @rz.setter
    @node_setter_manageable
    @node_setter_observable
    def rz(self, var):

        a = self.rotation
        self.rotation = (a[0], a[1], var)

    @property
    def rotation(self):
        """Rotation of the axis about its origin (rx,ry,rz).
        Defined as a rotation about an axis where the direction of the axis is (rx,ry,rz) and the angle of rotation is |(rx,ry,rz| degrees.
        These are the expressed on the coordinate system of the parent (if any) or the global axis system (if no parent)"""
        return np.rad2deg(self._vfNode.rotation)

    @rotation.setter
    @node_setter_manageable
    @node_setter_observable
    def rotation(self, var):

        # convert to degrees
        assert3f(var, "Rotation ")
        self._vfNode.rotation = np.deg2rad(var)
        self._scene._geometry_changed()

    # we need to over-ride the parent property to be able to call _geometry_changed afterwards
    @property
    def parent(self):
        """Determines the parent of the axis. Should either be another axis or 'None'

        Other axis may be refered to by reference or by name (str). So the following are identical

            p = s.new_axis('parent_axis')
            c = s.new_axis('child axis')

            c.parent = p
            c.parent = 'parent_axis'

        To define that an axis does not have a parent use

            c.parent = None

        """
        return super().parent

    @parent.setter
    @node_setter_manageable
    @node_setter_observable
    def parent(self, val):

        if val is not None:
            # Circular reference check: are we trying to make self depend on val while val depends on self?
            if self._scene.node_A_core_depends_on_B_core(val, self):
                if isinstance(val, Axis):  # it better be
                    val.change_parent_to(
                        None
                    )  # change the parent of other to None, this breaks the previous dependancy

        NodeWithParent.parent.fset(self, val)
        self._scene._geometry_changed()

    @property
    def gx(self):
        """The x-component of the global position vector [m] (global axis )"""
        return self.global_position[0]

    @property
    def gy(self):
        """The y-component of the global position vector [m] (global axis )"""
        return self.global_position[1]

    @property
    def gz(self):
        """The z-component of the global position vector [m] (global axis )"""
        return self.global_position[2]

    @gx.setter
    @node_setter_manageable
    @node_setter_observable
    def gx(self, var):

        a = self.global_position
        self.global_position = (var, a[1], a[2])

    @gy.setter
    @node_setter_manageable
    @node_setter_observable
    def gy(self, var):

        a = self.global_position
        self.global_position = (a[0], var, a[2])

    @gz.setter
    @node_setter_manageable
    @node_setter_observable
    def gz(self, var):

        a = self.global_position
        self.global_position = (a[0], a[1], var)

    @property
    def global_position(self):
        """The global position of the origin of the axis system  [m,m,m] (global axis)"""
        return self._vfNode.global_position

    @global_position.setter
    @node_setter_manageable
    @node_setter_observable
    def global_position(self, val):

        assert3f(val, "Global Position")
        if self.parent:
            self.position = self.parent.to_loc_position(val)
        else:
            self.position = val

    @property
    def grx(self):
        """The x-component of the global rotation vector [degrees] (global axis)"""
        return self.global_rotation[0]

    @property
    def gry(self):
        """The y-component of the global rotation vector [degrees] (global axis)"""
        return self.global_rotation[1]

    @property
    def grz(self):
        """The z-component of the global rotation vector [degrees] (global axis)"""
        return self.global_rotation[2]

    @grx.setter
    @node_setter_manageable
    @node_setter_observable
    def grx(self, var):

        a = self.global_rotation
        self.global_rotation = (var, a[1], a[2])

    @gry.setter
    @node_setter_manageable
    @node_setter_observable
    def gry(self, var):

        a = self.global_rotation
        self.global_rotation = (a[0], var, a[2])

    @grz.setter
    @node_setter_manageable
    @node_setter_observable
    def grz(self, var):

        a = self.global_rotation
        self.global_rotation = (a[0], a[1], var)

    @property
    def tilt_x(self):
        """Tilt percentage. This is the z-component of the unit y vector [%].

        See Also: heel
        """
        y = (0, 1, 0)
        uy = self.to_glob_direction(y)
        return float(100 * uy[2])

    @property
    def heel(self):
        """Heel in degrees. SB down is positive [deg].
        This is the inverse sin of the unit y vector(This is the arcsin of the tiltx)

        See also: tilt_x
        """
        return np.rad2deg(np.arcsin(self.tilt_x / 100))

    @property
    def tilt_y(self):
        """Tilt percentage. This is the z-component of the unit -x vector [%].
        So a positive rotation about the y axis results in a positive tilt_y.

        See Also: trim
        """
        x = (-1, 0, 0)
        ux = self.to_glob_direction(x)
        return float(100 * ux[2])

    @property
    def trim(self):
        """Trim in degrees. Bow-down is positive [deg].

        This is the inverse sin of the unit -x vector(This is the arcsin of the tilt_y)

        See also: tilt_y
        """
        return np.rad2deg(np.arcsin(self.tilt_y / 100))

    @property
    def heading(self):
        """Direction (0..360) [deg] of the local x-axis relative to the global x axis. Measured about the global z axis

        heading = atan(u_y,u_x)

        typically:
            heading 0  --> local axis align with global axis
            heading 90 --> local x-axis in direction of global y axis


        See also: heading_compass
        """
        x = (1, 0, 0)
        ux = self.to_glob_direction(x)
        heading = np.rad2deg(np.arctan2(ux[1], ux[0]))
        return np.mod(heading, 360)

    @property
    def heading_compass(self):
        """The heading (0..360)[deg] assuming that the global y-axis is North and global x-axis is East and rotation accoring compass definition"""
        return np.mod(90 - self.heading, 360)

    @property
    def global_rotation(self):
        """Rotation [deg,deg,deg] (global axis)"""
        return tuple(np.rad2deg(self._vfNode.global_rotation))

    @global_rotation.setter
    @node_setter_manageable
    @node_setter_observable
    def global_rotation(self, val):

        assert3f(val, "Global Rotation")
        if self.parent:
            self.rotation = self.parent.to_loc_rotation(val)
        else:
            self.rotation = val

    @property
    def global_transform(self):
        """Read-only: The global transform of the axis system [matrix]"""
        return self._vfNode.global_transform

    @property
    def connection_force(self):
        """The forces and moments that this axis applies on its parent at the origin of this axis system. [kN, kN, kN, kNm, kNm, kNm] (Parent axis)

        If this axis would be connected to a point on its parent, and that point would be located at the location of the origin of this axis system
        then the connection force equals the force and moment applied on that point.

        Example:
            parent axis with name A
            this axis with name B
            this axis is located on A at position (10,0,0)
            there is a Point at the center of this axis system.
            A force with Fz = -10 acts on the Point.

            The connection_force is (-10,0,0,0,0,0)

            This is the force and moment as applied on A at point (10,0,0)


        """
        return self._vfNode.connection_force

    @property
    def connection_force_x(self):
        """The x-component of the connection-force vector [kN] (Parent axis)"""
        return self.connection_force[0]

    @property
    def connection_force_y(self):
        """The y-component of the connection-force vector [kN] (Parent axis)"""
        return self.connection_force[1]

    @property
    def connection_force_z(self):
        """The z-component of the connection-force vector [kN] (Parent axis)"""
        return self.connection_force[2]

    @property
    def connection_moment_x(self):
        """The mx-component of the connection-force vector [kNm] (Parent axis)"""
        return self.connection_force[3]

    @property
    def connection_moment_y(self):
        """The my-component of the connection-force vector [kNm] (Parent axis)"""
        return self.connection_force[4]

    @property
    def connection_moment_z(self):
        """The mx-component of the connection-force vector [kNm] (Parent axis)"""
        return self.connection_force[5]

    @property
    def applied_force(self):
        """The force and moment that is applied on origin of this axis [kN, kN, kN, kNm, kNm, kNm] (Global axis)"""
        return self._vfNode.applied_force

    @property
    def ux(self):
        """The unit x axis [m,m,m] (Global axis)"""
        return self.to_glob_direction((1, 0, 0))

    @property
    def uy(self):
        """The unit y axis [m,m,m] (Global axis)"""
        return self.to_glob_direction((0, 1, 0))

    @property
    def uz(self):
        """The unit z axis [m,m,m] (Global axis)"""
        return self.to_glob_direction((0, 0, 1))

    @property
    def equilibrium_error(self):
        """The unresolved force and moment that on this axis. Should be zero when in equilibrium  (applied-force minus connection force, Parent axis)"""
        return self._vfNode.equilibrium_error

    def to_loc_position(self, value):
        """Returns the local position of a point in the global axis system.
        This considers the position and the rotation of the axis system.
        See Also: to_loc_direction
        """
        return self._vfNode.global_to_local_point(value)

    def to_glob_position(self, value):
        """Returns the global position of a point in the local axis system.
        This considers the position and the rotation of the axis system.
        See Also: to_glob_direction
        """
        return self._vfNode.local_to_global_point(value)

    def to_loc_direction(self, value):
        """Returns the local direction of a point in the global axis system.
        This considers only the rotation of the axis system.
        See Also: to_loc_position
        """
        return self._vfNode.global_to_local_vector(value)

    def to_glob_direction(self, value):
        """Returns the global direction of a point in the local axis system.
        This considers only the rotation of the axis system.
        See Also: to_glob_position"""
        return self._vfNode.local_to_global_vector(value)

    def to_loc_rotation(self, value):
        """Returns the local rotation. Used for rotating rotations.
        See Also: to_loc_position, to_loc_direction
        """
        return np.rad2deg(self._vfNode.global_to_local_rotation(np.deg2rad(value)))

    def to_glob_rotation(self, value):
        """Returns the global rotation. Used for rotating rotations.
        See Also: to_loc_position, to_loc_direction
        """
        return np.rad2deg(self._vfNode.local_to_global_rotation(np.deg2rad(value)))

    def give_load_shear_moment_diagram(
        self, axis_system=None
    ) -> "LoadShearMomentDiagram":
        """Returns a LoadShearMoment diagram

        Args:
            axis_system : optional : coordinate system [axis node] to be used for calculation of the diagram.
            Defaults to the local axis system
        """

        if axis_system is None:
            axis_system = self

        assert isinstance(axis_system, Axis), ValueError(
            f"axis_system shall be an instance of Axis, but it is of type {type(axis_system)}"
        )

        # calculate in the right global direction
        glob_dir = axis_system.to_glob_direction((1, 0, 0))
        self._scene._vfc.calculateBendingMoments(*glob_dir)

        lsm = self._vfNode.getBendingMomentDiagram(axis_system._vfNode)

        return LoadShearMomentDiagram(lsm)

    def change_parent_to(self, new_parent):
        """Assigns a new parent to the node but keeps the global position and rotation the same.

        See also: .parent (property)

        Args:
            new_parent: new parent node

        """

        # check new_parent
        if new_parent is not None:
            if not (
                isinstance(new_parent, Axis) or isinstance(new_parent, GeometricContact)
            ):
                raise TypeError(
                    "Only None or Axis-type nodes (or derived types) can be used as parent. You tried to use a {} as parent".format(
                        type(new_parent)
                    )
                )

        glob_pos = self.global_position
        glob_rot = self.global_rotation
        self.parent = new_parent
        self.global_position = glob_pos
        self.global_rotation = glob_rot

    def give_python_code(self):
        code = "# code for {}".format(self.name)
        code += "\ns.new_axis(name='{}',".format(self.name)
        if self.parent_for_export:
            code += "\n           parent='{}',".format(self.parent_for_export.name)

        # position

        if self.fixed[0]:
            code += "\n           position=({},".format(self.position[0])
        else:
            code += "\n           position=(solved({}),".format(self.position[0])
        if self.fixed[1]:
            code += "\n                     {},".format(self.position[1])
        else:
            code += "\n                     solved({}),".format(self.position[1])
        if self.fixed[2]:
            code += "\n                     {}),".format(self.position[2])
        else:
            code += "\n                     solved({})),".format(self.position[2])

        # rotation

        if self.fixed[3]:
            code += "\n           rotation=({},".format(self.rotation[0])
        else:
            code += "\n           rotation=(solved({}),".format(self.rotation[0])
        if self.fixed[4]:
            code += "\n                     {},".format(self.rotation[1])
        else:
            code += "\n                     solved({}),".format(self.rotation[1])
        if self.fixed[5]:
            code += "\n                     {}),".format(self.rotation[2])
        else:
            code += "\n                     solved({})),".format(self.rotation[2])

        # inertia and radii of gyration
        if self.inertia > 0:
            code += "\n                     inertia = {},".format(self.inertia)

        if np.any(self.inertia_radii > 0):
            code += "\n                     inertia_radii = ({}, {}, {}),".format(
                *self.inertia_radii
            )

        # fixeties
        code += "\n           fixed =({}, {}, {}, {}, {}, {}) )".format(*self.fixed)

        code += self.add_footprint_python_code()

        return code

Ancestors

Subclasses

Instance variables

var applied_force

The force and moment that is applied on origin of this axis [kN, kN, kN, kNm, kNm, kNm] (Global axis)

Expand source code
@property
def applied_force(self):
    """The force and moment that is applied on origin of this axis [kN, kN, kN, kNm, kNm, kNm] (Global axis)"""
    return self._vfNode.applied_force
var connection_force

The forces and moments that this axis applies on its parent at the origin of this axis system. [kN, kN, kN, kNm, kNm, kNm] (Parent axis)

If this axis would be connected to a point on its parent, and that point would be located at the location of the origin of this axis system then the connection force equals the force and moment applied on that point.

Example

parent axis with name A this axis with name B this axis is located on A at position (10,0,0) there is a Point at the center of this axis system. A force with Fz = -10 acts on the Point.

The connection_force is (-10,0,0,0,0,0)

This is the force and moment as applied on A at point (10,0,0)

Expand source code
@property
def connection_force(self):
    """The forces and moments that this axis applies on its parent at the origin of this axis system. [kN, kN, kN, kNm, kNm, kNm] (Parent axis)

    If this axis would be connected to a point on its parent, and that point would be located at the location of the origin of this axis system
    then the connection force equals the force and moment applied on that point.

    Example:
        parent axis with name A
        this axis with name B
        this axis is located on A at position (10,0,0)
        there is a Point at the center of this axis system.
        A force with Fz = -10 acts on the Point.

        The connection_force is (-10,0,0,0,0,0)

        This is the force and moment as applied on A at point (10,0,0)


    """
    return self._vfNode.connection_force
var connection_force_x

The x-component of the connection-force vector [kN] (Parent axis)

Expand source code
@property
def connection_force_x(self):
    """The x-component of the connection-force vector [kN] (Parent axis)"""
    return self.connection_force[0]
var connection_force_y

The y-component of the connection-force vector [kN] (Parent axis)

Expand source code
@property
def connection_force_y(self):
    """The y-component of the connection-force vector [kN] (Parent axis)"""
    return self.connection_force[1]
var connection_force_z

The z-component of the connection-force vector [kN] (Parent axis)

Expand source code
@property
def connection_force_z(self):
    """The z-component of the connection-force vector [kN] (Parent axis)"""
    return self.connection_force[2]
var connection_moment_x

The mx-component of the connection-force vector [kNm] (Parent axis)

Expand source code
@property
def connection_moment_x(self):
    """The mx-component of the connection-force vector [kNm] (Parent axis)"""
    return self.connection_force[3]
var connection_moment_y

The my-component of the connection-force vector [kNm] (Parent axis)

Expand source code
@property
def connection_moment_y(self):
    """The my-component of the connection-force vector [kNm] (Parent axis)"""
    return self.connection_force[4]
var connection_moment_z

The mx-component of the connection-force vector [kNm] (Parent axis)

Expand source code
@property
def connection_moment_z(self):
    """The mx-component of the connection-force vector [kNm] (Parent axis)"""
    return self.connection_force[5]
var equilibrium_error

The unresolved force and moment that on this axis. Should be zero when in equilibrium (applied-force minus connection force, Parent axis)

Expand source code
@property
def equilibrium_error(self):
    """The unresolved force and moment that on this axis. Should be zero when in equilibrium  (applied-force minus connection force, Parent axis)"""
    return self._vfNode.equilibrium_error
var fixed

Determines which of the six degrees of freedom are fixed, if any. (x,y,z,rx,ry,rz). True means that that degree of freedom will not change when solving statics. False means a that is may be changed in order to find equilibrium.

These are the expressed on the coordinate system of the parent (if any) or the global axis system (if no parent)

See Also: set_free, set_fixed

Expand source code
@property
def fixed(self):
    """Determines which of the six degrees of freedom are fixed, if any. (x,y,z,rx,ry,rz).
    True means that that degree of freedom will not change when solving statics.
    False means a that is may be changed in order to find equilibrium.

    These are the expressed on the coordinate system of the parent (if any) or the global axis system (if no parent)

    See Also: set_free, set_fixed
    """
    return self._vfNode.fixed
var global_position

The global position of the origin of the axis system [m,m,m] (global axis)

Expand source code
@property
def global_position(self):
    """The global position of the origin of the axis system  [m,m,m] (global axis)"""
    return self._vfNode.global_position
var global_rotation

Rotation [deg,deg,deg] (global axis)

Expand source code
@property
def global_rotation(self):
    """Rotation [deg,deg,deg] (global axis)"""
    return tuple(np.rad2deg(self._vfNode.global_rotation))
var global_transform

Read-only: The global transform of the axis system [matrix]

Expand source code
@property
def global_transform(self):
    """Read-only: The global transform of the axis system [matrix]"""
    return self._vfNode.global_transform
var grx

The x-component of the global rotation vector [degrees] (global axis)

Expand source code
@property
def grx(self):
    """The x-component of the global rotation vector [degrees] (global axis)"""
    return self.global_rotation[0]
var gry

The y-component of the global rotation vector [degrees] (global axis)

Expand source code
@property
def gry(self):
    """The y-component of the global rotation vector [degrees] (global axis)"""
    return self.global_rotation[1]
var grz

The z-component of the global rotation vector [degrees] (global axis)

Expand source code
@property
def grz(self):
    """The z-component of the global rotation vector [degrees] (global axis)"""
    return self.global_rotation[2]
var gx

The x-component of the global position vector [m] (global axis )

Expand source code
@property
def gx(self):
    """The x-component of the global position vector [m] (global axis )"""
    return self.global_position[0]
var gy

The y-component of the global position vector [m] (global axis )

Expand source code
@property
def gy(self):
    """The y-component of the global position vector [m] (global axis )"""
    return self.global_position[1]
var gz

The z-component of the global position vector [m] (global axis )

Expand source code
@property
def gz(self):
    """The z-component of the global position vector [m] (global axis )"""
    return self.global_position[2]
var heading

Direction (0..360) [deg] of the local x-axis relative to the global x axis. Measured about the global z axis

heading = atan(u_y,u_x)

typically: heading 0 –> local axis align with global axis heading 90 –> local x-axis in direction of global y axis

See also: heading_compass

Expand source code
@property
def heading(self):
    """Direction (0..360) [deg] of the local x-axis relative to the global x axis. Measured about the global z axis

    heading = atan(u_y,u_x)

    typically:
        heading 0  --> local axis align with global axis
        heading 90 --> local x-axis in direction of global y axis


    See also: heading_compass
    """
    x = (1, 0, 0)
    ux = self.to_glob_direction(x)
    heading = np.rad2deg(np.arctan2(ux[1], ux[0]))
    return np.mod(heading, 360)
var heading_compass

The heading (0..360)[deg] assuming that the global y-axis is North and global x-axis is East and rotation accoring compass definition

Expand source code
@property
def heading_compass(self):
    """The heading (0..360)[deg] assuming that the global y-axis is North and global x-axis is East and rotation accoring compass definition"""
    return np.mod(90 - self.heading, 360)
var heel

Heel in degrees. SB down is positive [deg]. This is the inverse sin of the unit y vector(This is the arcsin of the tiltx)

See also: tilt_x

Expand source code
@property
def heel(self):
    """Heel in degrees. SB down is positive [deg].
    This is the inverse sin of the unit y vector(This is the arcsin of the tiltx)

    See also: tilt_x
    """
    return np.rad2deg(np.arcsin(self.tilt_x / 100))
var inertia

The linear inertia of the axis in [mT] Aka: "Mass" - used only for dynamics

Expand source code
@property
def inertia(self):
    """The linear inertia of the axis in [mT] Aka: "Mass"
    - used only for dynamics"""
    return self._inertia
var inertia_position

The position of the center of inertia. Aka: "cog" [m,m,m] (local axis) - used only for dynamics - defined in local axis system

Expand source code
@property
def inertia_position(self):
    """The position of the center of inertia. Aka: "cog" [m,m,m] (local axis)
    - used only for dynamics
    - defined in local axis system"""
    return tuple(self._inertia_position)
var inertia_radii

The radii of gyration of the inertia [m,m,m] (local axis)

Used to calculate the mass moments of inertia via

Ixx = rxx^2 * inertia Iyy = rxx^2 * inertia Izz = rxx^2 * inertia

Note that DAVE does not directly support cross terms in the interia matrix of an axis system. If you want to use cross terms then combine multiple axis system to reach the same result. This is because inertia matrices with diagonal terms can not be translated.

Expand source code
@property
def inertia_radii(self):
    """The radii of gyration of the inertia [m,m,m] (local axis)

    Used to calculate the mass moments of inertia via

    Ixx = rxx^2 * inertia
    Iyy = rxx^2 * inertia
    Izz = rxx^2 * inertia

    Note that DAVE does not directly support cross terms in the interia matrix of an axis system. If you want to
    use cross terms then combine multiple axis system to reach the same result. This is because inertia matrices with
    diagonal terms can not be translated.
    """
    return np.array(self._inertia_radii, dtype=float)
var parent

Determines the parent of the axis. Should either be another axis or 'None'

Other axis may be refered to by reference or by name (str). So the following are identical

p = s.new_axis('parent_axis')
c = s.new_axis('child axis')

c.parent = p
c.parent = 'parent_axis'

To define that an axis does not have a parent use

c.parent = None
Expand source code
@property
def parent(self):
    """Determines the parent of the axis. Should either be another axis or 'None'

    Other axis may be refered to by reference or by name (str). So the following are identical

        p = s.new_axis('parent_axis')
        c = s.new_axis('child axis')

        c.parent = p
        c.parent = 'parent_axis'

    To define that an axis does not have a parent use

        c.parent = None

    """
    return super().parent
var position

Position of the axis (parent axis) [m,m,m]

These are the expressed on the coordinate system of the parent (if any) or the global axis system (if no parent)

Expand source code
@property
def position(self):
    """Position of the axis (parent axis) [m,m,m]

    These are the expressed on the coordinate system of the parent (if any) or the global axis system (if no parent)"""
    return self._vfNode.position
var rotation

Rotation of the axis about its origin (rx,ry,rz). Defined as a rotation about an axis where the direction of the axis is (rx,ry,rz) and the angle of rotation is |(rx,ry,rz| degrees. These are the expressed on the coordinate system of the parent (if any) or the global axis system (if no parent)

Expand source code
@property
def rotation(self):
    """Rotation of the axis about its origin (rx,ry,rz).
    Defined as a rotation about an axis where the direction of the axis is (rx,ry,rz) and the angle of rotation is |(rx,ry,rz| degrees.
    These are the expressed on the coordinate system of the parent (if any) or the global axis system (if no parent)"""
    return np.rad2deg(self._vfNode.rotation)
var rx

The x-component of the rotation vector [degrees] (parent axis)

Expand source code
@property
def rx(self):
    """The x-component of the rotation vector [degrees] (parent axis)"""
    return self.rotation[0]
var ry

The y-component of the rotation vector [degrees] (parent axis)

Expand source code
@property
def ry(self):
    """The y-component of the rotation vector [degrees] (parent axis)"""
    return self.rotation[1]
var rz

The z-component of the rotation vector [degrees], (parent axis)

Expand source code
@property
def rz(self):
    """The z-component of the rotation vector [degrees], (parent axis)"""
    return self.rotation[2]
var tilt_x

Tilt percentage. This is the z-component of the unit y vector [%].

See Also: heel

Expand source code
@property
def tilt_x(self):
    """Tilt percentage. This is the z-component of the unit y vector [%].

    See Also: heel
    """
    y = (0, 1, 0)
    uy = self.to_glob_direction(y)
    return float(100 * uy[2])
var tilt_y

Tilt percentage. This is the z-component of the unit -x vector [%]. So a positive rotation about the y axis results in a positive tilt_y.

See Also: trim

Expand source code
@property
def tilt_y(self):
    """Tilt percentage. This is the z-component of the unit -x vector [%].
    So a positive rotation about the y axis results in a positive tilt_y.

    See Also: trim
    """
    x = (-1, 0, 0)
    ux = self.to_glob_direction(x)
    return float(100 * ux[2])
var trim

Trim in degrees. Bow-down is positive [deg].

This is the inverse sin of the unit -x vector(This is the arcsin of the tilt_y)

See also: tilt_y

Expand source code
@property
def trim(self):
    """Trim in degrees. Bow-down is positive [deg].

    This is the inverse sin of the unit -x vector(This is the arcsin of the tilt_y)

    See also: tilt_y
    """
    return np.rad2deg(np.arcsin(self.tilt_y / 100))
var ux

The unit x axis [m,m,m] (Global axis)

Expand source code
@property
def ux(self):
    """The unit x axis [m,m,m] (Global axis)"""
    return self.to_glob_direction((1, 0, 0))
var uy

The unit y axis [m,m,m] (Global axis)

Expand source code
@property
def uy(self):
    """The unit y axis [m,m,m] (Global axis)"""
    return self.to_glob_direction((0, 1, 0))
var uz

The unit z axis [m,m,m] (Global axis)

Expand source code
@property
def uz(self):
    """The unit z axis [m,m,m] (Global axis)"""
    return self.to_glob_direction((0, 0, 1))
var x

The x-component of the position vector (parent axis) [m]

Expand source code
@property
def x(self):
    """The x-component of the position vector (parent axis) [m]"""
    return self.position[0]
var y

The y-component of the position vector (parent axis) [m]

Expand source code
@property
def y(self):
    """The y-component of the position vector (parent axis) [m]"""
    return self.position[1]
var z

The z-component of the position vector (parent axis) [m]

Expand source code
@property
def z(self):
    """The z-component of the position vector (parent axis) [m]"""
    return self.position[2]

Methods

def give_load_shear_moment_diagram(self, axis_system=None) -> LoadShearMomentDiagram

Returns a LoadShearMoment diagram

Args

axis_system : optional : coordinate system [axis node] to be used for calculation of the diagram. Defaults to the local axis system

Expand source code
def give_load_shear_moment_diagram(
    self, axis_system=None
) -> "LoadShearMomentDiagram":
    """Returns a LoadShearMoment diagram

    Args:
        axis_system : optional : coordinate system [axis node] to be used for calculation of the diagram.
        Defaults to the local axis system
    """

    if axis_system is None:
        axis_system = self

    assert isinstance(axis_system, Axis), ValueError(
        f"axis_system shall be an instance of Axis, but it is of type {type(axis_system)}"
    )

    # calculate in the right global direction
    glob_dir = axis_system.to_glob_direction((1, 0, 0))
    self._scene._vfc.calculateBendingMoments(*glob_dir)

    lsm = self._vfNode.getBendingMomentDiagram(axis_system._vfNode)

    return LoadShearMomentDiagram(lsm)
def set_fixed(self)

Sets .fixed to (True,True,True,True,True,True)

Expand source code
def set_fixed(self):
    """Sets .fixed to (True,True,True,True,True,True)"""

    self._vfNode.set_fixed()
def set_free(self)

Sets .fixed to (False,False,False,False,False,False)

Expand source code
def set_free(self):
    """Sets .fixed to (False,False,False,False,False,False)"""
    self._vfNode.set_free()
def to_glob_direction(self, value)

Returns the global direction of a point in the local axis system. This considers only the rotation of the axis system. See Also: to_glob_position

Expand source code
def to_glob_direction(self, value):
    """Returns the global direction of a point in the local axis system.
    This considers only the rotation of the axis system.
    See Also: to_glob_position"""
    return self._vfNode.local_to_global_vector(value)
def to_glob_position(self, value)

Returns the global position of a point in the local axis system. This considers the position and the rotation of the axis system. See Also: to_glob_direction

Expand source code
def to_glob_position(self, value):
    """Returns the global position of a point in the local axis system.
    This considers the position and the rotation of the axis system.
    See Also: to_glob_direction
    """
    return self._vfNode.local_to_global_point(value)
def to_glob_rotation(self, value)

Returns the global rotation. Used for rotating rotations. See Also: to_loc_position, to_loc_direction

Expand source code
def to_glob_rotation(self, value):
    """Returns the global rotation. Used for rotating rotations.
    See Also: to_loc_position, to_loc_direction
    """
    return np.rad2deg(self._vfNode.local_to_global_rotation(np.deg2rad(value)))
def to_loc_direction(self, value)

Returns the local direction of a point in the global axis system. This considers only the rotation of the axis system. See Also: to_loc_position

Expand source code
def to_loc_direction(self, value):
    """Returns the local direction of a point in the global axis system.
    This considers only the rotation of the axis system.
    See Also: to_loc_position
    """
    return self._vfNode.global_to_local_vector(value)
def to_loc_position(self, value)

Returns the local position of a point in the global axis system. This considers the position and the rotation of the axis system. See Also: to_loc_direction

Expand source code
def to_loc_position(self, value):
    """Returns the local position of a point in the global axis system.
    This considers the position and the rotation of the axis system.
    See Also: to_loc_direction
    """
    return self._vfNode.global_to_local_point(value)
def to_loc_rotation(self, value)

Returns the local rotation. Used for rotating rotations. See Also: to_loc_position, to_loc_direction

Expand source code
def to_loc_rotation(self, value):
    """Returns the local rotation. Used for rotating rotations.
    See Also: to_loc_position, to_loc_direction
    """
    return np.rad2deg(self._vfNode.global_to_local_rotation(np.deg2rad(value)))

Inherited members

class BallastSystem (scene, parent)

A BallastSystem is a group of Tank objects.

The tank objects are created separately and only their references are assigned to this ballast-system object.

Expand source code
class BallastSystem(Node):
    """A BallastSystem is a group of Tank objects.

    The tank objects are created separately and only their references are assigned to this ballast-system object.

    """

    def __init__(self, scene, parent):
        super().__init__(scene)

        self.tanks = []
        """List of Tank objects"""

        self.frozen = []
        """List of names of frozen tanks - The contents of a frozen tank should not be changed"""

        self.parent = parent

    def new_tank(
        self, name, position, capacity_kN, rho=1.025, frozen=False, actual_fill=0
    ):
        """Adds a new cubic shaped tank with the given volume as derived from capacity and rho

        Warning: provided for backwards compatibility only.
        """

        from warnings import warn

        warn(
            "BallastSystem.new_tank is outdated and may be removed in a future version."
        )

        tnk = self._scene.new_tank(name, parent=self.parent, density=rho)
        volume = capacity_kN / (9.81 * rho)
        side = volume ** (1 / 3)
        tnk.trimesh.load_file(
            "res: cube.obj",
            scale=(side, side, side),
            rotation=(0.0, 0.0, 0.0),
            offset=position,
        )
        if actual_fill > 0:
            tnk.fill_pct = actual_fill

        if frozen:
            tnk.frozen = frozen

        self.tanks.append(tnk)

        return tnk

    # for gui
    def change_parent_to(self, new_parent):
        if not (isinstance(new_parent, Axis) or new_parent is None):
            raise ValueError(
                "Visuals can only be attached to an axis (or derived) or None"
            )
        self.parent = new_parent

    # for node
    def depends_on(self):
        return [self.parent, *self.tanks]

    def is_frozen(self, name):
        """Returns True if the tank with this name if frozen"""
        return name in self.frozen

    def reorder_tanks(self, names):
        """Places tanks with given names at the top of the list. Other tanks are appended afterwards in original order.

        For a complete re-order give all tank names.

        Example:
            let tanks be 'a','b','c','d','e'

            then re_order_tanks(['e','b']) will result in ['e','b','a','c','d']
        """
        for name in names:
            if name not in self.tank_names():
                raise ValueError("No tank with name {}".format(name))

        old_tanks = self.tanks.copy()
        self.tanks.clear()
        to_be_deleted = list()

        for name in names:
            for tank in old_tanks:
                if tank.name == name:
                    self.tanks.append(tank)
                    to_be_deleted.append(tank)

        for tank in to_be_deleted:
            old_tanks.remove(tank)

        for tank in old_tanks:
            self.tanks.append(tank)

    def order_tanks_by_elevation(self):
        """Re-orders the existing tanks such that the lowest tanks are higher in the list"""

        zs = [tank.cog_when_full[2] for tank in self.tanks]
        inds = np.argsort(zs)
        self.tanks = [self.tanks[i] for i in inds]

    def order_tanks_by_distance_from_point(self, point, reverse=False):
        """Re-orders the existing tanks such that the tanks *furthest* from the point are first on the list

        Args:
            point : (x,y,z)  - reference point to determine the distance to
            reverse: (False) - order in reverse order: tanks nearest to the points first on list


        """
        pos = [tank.cog_when_full for tank in self.tanks]
        pos = np.array(pos, dtype=float)
        pos -= np.array(point)

        dist = np.apply_along_axis(np.linalg.norm, 1, pos)

        if reverse:
            inds = np.argsort(dist)
        else:
            inds = np.argsort(-dist)

        self.tanks = [self.tanks[i] for i in inds]

    def order_tanks_to_maximize_inertia_moment(self):
        """Re-order tanks such that tanks furthest from center of system are first on the list"""
        self._order_tanks_to_inertia_moment()

    def order_tanks_to_minimize_inertia_moment(self):
        """Re-order tanks such that tanks nearest to center of system are first on the list"""
        self._order_tanks_to_inertia_moment(maximize=False)

    def _order_tanks_to_inertia_moment(self, maximize=True):
        """Re-order tanks such that tanks furthest away from center of system are first on the list"""
        pos = [tank.cog_when_full for tank in self.tanks]
        m = [tank.capacity for tank in self.tanks]
        pos = np.array(pos, dtype=float)
        mxmymz = np.vstack((m, m, m)).transpose() * pos
        total = np.sum(m)
        point = sum(mxmymz) / total

        if maximize:
            self.order_tanks_by_distance_from_point(point)
        else:
            self.order_tanks_by_distance_from_point(point, reverse=True)

    def tank_names(self):
        return [tank.name for tank in self.tanks]

    def fill_tank(self, name, fill):

        assert1f(fill, "tank fill")

        for tank in self.tanks:
            if tank.name == name:
                tank.pct = fill
                return
        raise ValueError("No tank with name {}".format(name))

    def xyzw(self):
        """Gets the current ballast cog in GLOBAL axis system weight from the tanks

        Returns:
            (x,y,z), weight [mT]
        """
        """Calculates the weight and inertia properties of the tanks"""

        mxmymz = np.array((0.0, 0.0, 0.0))
        wt = 0

        for tank in self.tanks:
            w = tank.volume * tank.density
            p = np.array(tank.cog, dtype=float)
            mxmymz += p * w

            wt += w

        if wt == 0:
            xyz = np.array((0.0, 0.0, 0.0))
        else:
            xyz = mxmymz / wt

        return xyz, wt

    def empty_all_usable_tanks(self):
        """Empties all non-frozen tanks.
        Returns a list with tank number and fill percentage of all affected tanks. This can be used to restore the
        ballast situation as it was before emptying.

        See also: restore tank fillings
        """
        restore = []

        for i, t in enumerate(self.tanks):
            if not self.is_frozen(t.name):
                restore.append((i, t.fill_pct))
                t.fill_pct = 0

        return restore

    def restore_tank_fillings(self, restore):
        """Restores the tank fillings as per restore.

        Restore is typically obtained from the "empty_all_usable_tanks" function.

        See Also: empty_all_usable_tanks
        """

        for r in restore:
            i, pct = r
            self.tanks[i].fill_pct = pct

    def tank(self, name):

        for t in self.tanks:
            if t.name == name:
                return t
        raise ValueError("No tank with name {}".format(name))

    def __getitem__(self, item):
        return self.tank(item)

    @property
    def cogx(self):
        """X position of combined CoG of all tank contents in the ballast-system. (global coordinate) [m]"""
        return self.cog[0]

    @property
    def cogy(self):
        """Y position of combined CoG of all tank contents in the ballast-system. (global coordinate) [m]"""
        return self.cog[1]

    @property
    def cogz(self):
        """Z position of combined CoG of all tank contents in the ballast-system. (global coordinate) [m]"""
        return self.cog[2]

    @property
    def cog(self):
        """Combined CoG of all tank contents in the ballast-system. (global coordinate) [m,m,m]"""
        cog, wt = self.xyzw()
        return (cog[0], cog[1], cog[2])

    @property
    def weight(self):
        """Total weight of all tank fillings in the ballast system [kN]"""
        cog, wt = self.xyzw()
        return wt * 9.81

    def give_python_code(self):
        code = "\n# code for {} and its tanks".format(self.name)

        code += "\nbs = s.new_ballastsystem('{}', parent = '{}')".format(
            self.name, self.parent.name
        )

        for tank in self.tanks:
            code += "\nbs.tanks.append(s['{}'])".format(tank.name)

        return code

Ancestors

Instance variables

var cog

Combined CoG of all tank contents in the ballast-system. (global coordinate) [m,m,m]

Expand source code
@property
def cog(self):
    """Combined CoG of all tank contents in the ballast-system. (global coordinate) [m,m,m]"""
    cog, wt = self.xyzw()
    return (cog[0], cog[1], cog[2])
var cogx

X position of combined CoG of all tank contents in the ballast-system. (global coordinate) [m]

Expand source code
@property
def cogx(self):
    """X position of combined CoG of all tank contents in the ballast-system. (global coordinate) [m]"""
    return self.cog[0]
var cogy

Y position of combined CoG of all tank contents in the ballast-system. (global coordinate) [m]

Expand source code
@property
def cogy(self):
    """Y position of combined CoG of all tank contents in the ballast-system. (global coordinate) [m]"""
    return self.cog[1]
var cogz

Z position of combined CoG of all tank contents in the ballast-system. (global coordinate) [m]

Expand source code
@property
def cogz(self):
    """Z position of combined CoG of all tank contents in the ballast-system. (global coordinate) [m]"""
    return self.cog[2]
var frozen

List of names of frozen tanks - The contents of a frozen tank should not be changed

var tanks

List of Tank objects

var weight

Total weight of all tank fillings in the ballast system [kN]

Expand source code
@property
def weight(self):
    """Total weight of all tank fillings in the ballast system [kN]"""
    cog, wt = self.xyzw()
    return wt * 9.81

Methods

def change_parent_to(self, new_parent)
Expand source code
def change_parent_to(self, new_parent):
    if not (isinstance(new_parent, Axis) or new_parent is None):
        raise ValueError(
            "Visuals can only be attached to an axis (or derived) or None"
        )
    self.parent = new_parent
def empty_all_usable_tanks(self)

Empties all non-frozen tanks. Returns a list with tank number and fill percentage of all affected tanks. This can be used to restore the ballast situation as it was before emptying.

See also: restore tank fillings

Expand source code
def empty_all_usable_tanks(self):
    """Empties all non-frozen tanks.
    Returns a list with tank number and fill percentage of all affected tanks. This can be used to restore the
    ballast situation as it was before emptying.

    See also: restore tank fillings
    """
    restore = []

    for i, t in enumerate(self.tanks):
        if not self.is_frozen(t.name):
            restore.append((i, t.fill_pct))
            t.fill_pct = 0

    return restore
def fill_tank(self, name, fill)
Expand source code
def fill_tank(self, name, fill):

    assert1f(fill, "tank fill")

    for tank in self.tanks:
        if tank.name == name:
            tank.pct = fill
            return
    raise ValueError("No tank with name {}".format(name))
def is_frozen(self, name)

Returns True if the tank with this name if frozen

Expand source code
def is_frozen(self, name):
    """Returns True if the tank with this name if frozen"""
    return name in self.frozen
def new_tank(self, name, position, capacity_kN, rho=1.025, frozen=False, actual_fill=0)

Adds a new cubic shaped tank with the given volume as derived from capacity and rho

Warning: provided for backwards compatibility only.

Expand source code
def new_tank(
    self, name, position, capacity_kN, rho=1.025, frozen=False, actual_fill=0
):
    """Adds a new cubic shaped tank with the given volume as derived from capacity and rho

    Warning: provided for backwards compatibility only.
    """

    from warnings import warn

    warn(
        "BallastSystem.new_tank is outdated and may be removed in a future version."
    )

    tnk = self._scene.new_tank(name, parent=self.parent, density=rho)
    volume = capacity_kN / (9.81 * rho)
    side = volume ** (1 / 3)
    tnk.trimesh.load_file(
        "res: cube.obj",
        scale=(side, side, side),
        rotation=(0.0, 0.0, 0.0),
        offset=position,
    )
    if actual_fill > 0:
        tnk.fill_pct = actual_fill

    if frozen:
        tnk.frozen = frozen

    self.tanks.append(tnk)

    return tnk
def order_tanks_by_distance_from_point(self, point, reverse=False)

Re-orders the existing tanks such that the tanks furthest from the point are first on the list

Args

point : (x,y,z) - reference point to determine the distance to
 
reverse
(False) - order in reverse order: tanks nearest to the points first on list
Expand source code
def order_tanks_by_distance_from_point(self, point, reverse=False):
    """Re-orders the existing tanks such that the tanks *furthest* from the point are first on the list

    Args:
        point : (x,y,z)  - reference point to determine the distance to
        reverse: (False) - order in reverse order: tanks nearest to the points first on list


    """
    pos = [tank.cog_when_full for tank in self.tanks]
    pos = np.array(pos, dtype=float)
    pos -= np.array(point)

    dist = np.apply_along_axis(np.linalg.norm, 1, pos)

    if reverse:
        inds = np.argsort(dist)
    else:
        inds = np.argsort(-dist)

    self.tanks = [self.tanks[i] for i in inds]
def order_tanks_by_elevation(self)

Re-orders the existing tanks such that the lowest tanks are higher in the list

Expand source code
def order_tanks_by_elevation(self):
    """Re-orders the existing tanks such that the lowest tanks are higher in the list"""

    zs = [tank.cog_when_full[2] for tank in self.tanks]
    inds = np.argsort(zs)
    self.tanks = [self.tanks[i] for i in inds]
def order_tanks_to_maximize_inertia_moment(self)

Re-order tanks such that tanks furthest from center of system are first on the list

Expand source code
def order_tanks_to_maximize_inertia_moment(self):
    """Re-order tanks such that tanks furthest from center of system are first on the list"""
    self._order_tanks_to_inertia_moment()
def order_tanks_to_minimize_inertia_moment(self)

Re-order tanks such that tanks nearest to center of system are first on the list

Expand source code
def order_tanks_to_minimize_inertia_moment(self):
    """Re-order tanks such that tanks nearest to center of system are first on the list"""
    self._order_tanks_to_inertia_moment(maximize=False)
def reorder_tanks(self, names)

Places tanks with given names at the top of the list. Other tanks are appended afterwards in original order.

For a complete re-order give all tank names.

Example

let tanks be 'a','b','c','d','e'

then re_order_tanks(['e','b']) will result in ['e','b','a','c','d']

Expand source code
def reorder_tanks(self, names):
    """Places tanks with given names at the top of the list. Other tanks are appended afterwards in original order.

    For a complete re-order give all tank names.

    Example:
        let tanks be 'a','b','c','d','e'

        then re_order_tanks(['e','b']) will result in ['e','b','a','c','d']
    """
    for name in names:
        if name not in self.tank_names():
            raise ValueError("No tank with name {}".format(name))

    old_tanks = self.tanks.copy()
    self.tanks.clear()
    to_be_deleted = list()

    for name in names:
        for tank in old_tanks:
            if tank.name == name:
                self.tanks.append(tank)
                to_be_deleted.append(tank)

    for tank in to_be_deleted:
        old_tanks.remove(tank)

    for tank in old_tanks:
        self.tanks.append(tank)
def restore_tank_fillings(self, restore)

Restores the tank fillings as per restore.

Restore is typically obtained from the "empty_all_usable_tanks" function.

See Also: empty_all_usable_tanks

Expand source code
def restore_tank_fillings(self, restore):
    """Restores the tank fillings as per restore.

    Restore is typically obtained from the "empty_all_usable_tanks" function.

    See Also: empty_all_usable_tanks
    """

    for r in restore:
        i, pct = r
        self.tanks[i].fill_pct = pct
def tank(self, name)
Expand source code
def tank(self, name):

    for t in self.tanks:
        if t.name == name:
            return t
    raise ValueError("No tank with name {}".format(name))
def tank_names(self)
Expand source code
def tank_names(self):
    return [tank.name for tank in self.tanks]
def xyzw(self)

Gets the current ballast cog in GLOBAL axis system weight from the tanks

Returns

(x,y,z), weight [mT]

Expand source code
def xyzw(self):
    """Gets the current ballast cog in GLOBAL axis system weight from the tanks

    Returns:
        (x,y,z), weight [mT]
    """
    """Calculates the weight and inertia properties of the tanks"""

    mxmymz = np.array((0.0, 0.0, 0.0))
    wt = 0

    for tank in self.tanks:
        w = tank.volume * tank.density
        p = np.array(tank.cog, dtype=float)
        mxmymz += p * w

        wt += w

    if wt == 0:
        xyz = np.array((0.0, 0.0, 0.0))
    else:
        xyz = mxmymz / wt

    return xyz, wt

Inherited members

class Beam (scene, node)

A LinearBeam models a FEM-like linear beam element.

A LinearBeam node connects two Axis elements

By definition the beam runs in the X-direction of the nodeA axis system. So it may be needed to create a dedicated Axis element for the beam to control the orientation.

The beam is defined using the following properties:

  • EIy - bending stiffness about y-axis
  • EIz - bending stiffness about z-axis
  • GIp - torsional stiffness about x-axis
  • EA - axis stiffness in x-direction
  • L - the un-stretched length of the beam
  • mass - mass of the beam in [mT]

The beam element is in rest if the nodeB axis system

  1. has the same global orientation as the nodeA system
  2. is at global position equal to the global position of local point (L,0,0) of the nodeA axis. (aka: the end of the beam)

The scene.new_linearbeam automatically creates a dedicated axis system for each end of the beam. The orientation of this axis-system is determined as follows:

First the direction from nodeA to nodeB is determined: D The axis of rotation is the cross-product of the unit x-axis and D AXIS = ux x D The angle of rotation is the angle between the nodeA x-axis and D The rotation about the rotated X-axis is undefined.

Expand source code
class Beam(CoreConnectedNode):
    """A LinearBeam models a FEM-like linear beam element.

    A LinearBeam node connects two Axis elements

    By definition the beam runs in the X-direction of the nodeA axis system. So it may be needed to create a
    dedicated Axis element for the beam to control the orientation.

    The beam is defined using the following properties:

    *  EIy  - bending stiffness about y-axis
    *  EIz  - bending stiffness about z-axis
    *  GIp  - torsional stiffness about x-axis
    *  EA   - axis stiffness in x-direction
    *  L    - the un-stretched length of the beam
    *  mass - mass of the beam in [mT]

    The beam element is in rest if the nodeB axis system

    1. has the same global orientation as the nodeA system
    2. is at global position equal to the global position of local point (L,0,0) of the nodeA axis. (aka: the end of the beam)

    The scene.new_linearbeam automatically creates a dedicated axis system for each end of the beam. The orientation of this axis-system
    is determined as follows:

    First the direction from nodeA to nodeB is determined: D
    The axis of rotation is the cross-product of the unit x-axis and D    AXIS = ux x D
    The angle of rotation is the angle between the nodeA x-axis and D
    The rotation about the rotated X-axis is undefined.

    """

    def __init__(self, scene, node):
        super().__init__(scene, node)
        self._nodeA = None
        self._nodeB = None

    def depends_on(self):
        return [self._nodeA, self._nodeB]

    @property
    def n_segments(self):
        return self._vfNode.nSegments

    @n_segments.setter
    @node_setter_manageable
    @node_setter_observable
    def n_segments(self, value):
        if value < 1:
            raise ValueError("Number of segments in beam should be 1 or more")
        self._vfNode.nSegments = int(value)

    @property
    def EIy(self):
        """E * Iyy : bending stiffness in the XZ plane [kN m2]

        E is the modulus of elasticity; for steel 190-210 GPa (10^6 kN/m2)
        Iyy is the cross section moment of inertia [m4]
        """
        return self._vfNode.EIy

    @EIy.setter
    @node_setter_manageable
    @node_setter_observable
    def EIy(self, value):

        self._vfNode.EIy = value

    @property
    def EIz(self):
        """E * Izz : bending stiffness in the XY plane [kN m2]

        E is the modulus of elasticity; for steel 190-210 GPa (10^6 kN/m2)
        Iyy is the cross section moment of inertia [m4]
        """
        return self._vfNode.EIz

    @EIz.setter
    @node_setter_manageable
    @node_setter_observable
    def EIz(self, value):

        self._vfNode.EIz = value

    @property
    def GIp(self):
        """G * Ipp : torsional stiffness about the X (length) axis [kN m2]

        G is the shear-modulus of elasticity; for steel 75-80 GPa (10^6 kN/m2)
        Ip is the cross section polar moment of inertia [m4]
        """
        return self._vfNode.GIp

    @GIp.setter
    @node_setter_manageable
    @node_setter_observable
    def GIp(self, value):

        self._vfNode.GIp = value

    @property
    def EA(self):
        """E * A : stiffness in the length direction [kN]

        E is the modulus of elasticity; for steel 190-210 GPa (10^6 kN/m2)
        A is the cross-section area in [m2]
        """
        return self._vfNode.EA

    @EA.setter
    @node_setter_manageable
    @node_setter_observable
    def EA(self, value):

        self._vfNode.EA = value

    @property
    def tension_only(self):
        """axial stiffness (EA) only applicable to tension [True/False]"""
        return self._vfNode.tensionOnly

    @tension_only.setter
    @node_setter_manageable
    @node_setter_observable
    def tension_only(self, value):
        assert isinstance(value, bool), ValueError(
            "Value for tension_only shall be True or False"
        )
        self._vfNode.tensionOnly = value

    @property
    def mass(self):
        """Mass of the beam in [mT]"""
        return self._vfNode.Mass

    @mass.setter
    @node_setter_manageable
    @node_setter_observable
    def mass(self, value):

        assert1f(value, "Mass shall be a number")
        self._vfNode.Mass = value
        pass

    @property
    def L(self):
        """Length of the beam in unloaded condition [m]"""
        return self._vfNode.L

    @L.setter
    @node_setter_manageable
    @node_setter_observable
    def L(self, value):

        self._vfNode.L = value

    @property
    def nodeA(self):
        """The axis system that the A-end of the beam is connected to. The beam leaves this axis system along the X-axis"""
        return self._nodeA

    @nodeA.setter
    @node_setter_manageable
    @node_setter_observable
    def nodeA(self, val):

        val = self._scene._node_from_node_or_str(val)

        if not isinstance(val, Axis):
            raise TypeError("Provided nodeA should be a Axis")

        self._nodeA = val
        self._vfNode.master = val._vfNode

    @property
    def nodeB(self):
        """The axis system that the B-end of the beam is connected to. The beam arrives at this axis system along the X-axis"""
        return self._nodeB

    @nodeB.setter
    @node_setter_manageable
    @node_setter_observable
    def nodeB(self, val):

        val = self._scene._node_from_node_or_str(val)
        if not isinstance(val, Axis):
            raise TypeError("Provided nodeA should be a Axis")

        self._nodeB = val
        self._vfNode.slave = val._vfNode

    # read-only
    @property
    def moment_A(self):
        """Moment on beam at node A (kNm, kNm, kNm) , axis system of node A"""
        return self._vfNode.moment_on_master

    @property
    def moment_B(self):
        """Moment on beam at node B (kNm, kNm, kNm) , axis system of node B"""
        return self._vfNode.moment_on_slave

    @property
    def tension(self):
        """Tension in the beam [kN], negative for compression

        tension is calculated at the midpoints of the beam segments.
        """
        return self._vfNode.tension

    @property
    def torsion(self):
        """Torsion moment [kNm]. Positive if end B has a positive rotation about the x-axis of end A

        torsion is calculated at the midpoints of the beam segments.
        """
        return self._vfNode.torsion

    @property
    def X_nodes(self):
        """Returns the x-positions of the end nodes and internal nodes along the length of the beam [m]"""
        return self._vfNode.x

    @property
    def X_midpoints(self):
        """X-positions of the beam centers measured along the length of the beam [m]"""
        return tuple(
            0.5 * (np.array(self._vfNode.x[:-1]) + np.array(self._vfNode.x[1:]))
        )

    @property
    def global_positions(self):
        """Global-positions of the end nodes and internal nodes [m,m,m]"""
        return np.array(self._vfNode.global_position, dtype=float)

    @property
    def global_orientations(self):
        """Global-orientations of the end nodes and internal nodes [deg,deg,deg]"""
        return np.rad2deg(self._vfNode.global_orientation)

    @property
    def bending(self):
        """Bending forces of the end nodes and internal nodes [0, kNm, kNm]"""
        return np.array(self._vfNode.bending)

    def give_python_code(self):
        code = "# code for beam {}".format(self.name)
        code += "\ns.new_beam(name='{}',".format(self.name)
        code += "\n            nodeA='{}',".format(self.nodeA.name)
        code += "\n            nodeB='{}',".format(self.nodeB.name)
        code += "\n            n_segments={},".format(self.n_segments)
        code += "\n            tension_only={},".format(self.tension_only)
        code += "\n            EIy ={},".format(self.EIy)
        code += "\n            EIz ={},".format(self.EIz)
        code += "\n            GIp ={},".format(self.GIp)
        code += "\n            EA ={},".format(self.EA)
        code += "\n            mass ={},".format(self.mass)
        code += "\n            L ={}) # L can possibly be omitted".format(self.L)

        return code

Ancestors

Instance variables

var EA

E * A : stiffness in the length direction [kN]

E is the modulus of elasticity; for steel 190-210 GPa (10^6 kN/m2) A is the cross-section area in [m2]

Expand source code
@property
def EA(self):
    """E * A : stiffness in the length direction [kN]

    E is the modulus of elasticity; for steel 190-210 GPa (10^6 kN/m2)
    A is the cross-section area in [m2]
    """
    return self._vfNode.EA
var EIy

E * Iyy : bending stiffness in the XZ plane [kN m2]

E is the modulus of elasticity; for steel 190-210 GPa (10^6 kN/m2) Iyy is the cross section moment of inertia [m4]

Expand source code
@property
def EIy(self):
    """E * Iyy : bending stiffness in the XZ plane [kN m2]

    E is the modulus of elasticity; for steel 190-210 GPa (10^6 kN/m2)
    Iyy is the cross section moment of inertia [m4]
    """
    return self._vfNode.EIy
var EIz

E * Izz : bending stiffness in the XY plane [kN m2]

E is the modulus of elasticity; for steel 190-210 GPa (10^6 kN/m2) Iyy is the cross section moment of inertia [m4]

Expand source code
@property
def EIz(self):
    """E * Izz : bending stiffness in the XY plane [kN m2]

    E is the modulus of elasticity; for steel 190-210 GPa (10^6 kN/m2)
    Iyy is the cross section moment of inertia [m4]
    """
    return self._vfNode.EIz
var GIp

G * Ipp : torsional stiffness about the X (length) axis [kN m2]

G is the shear-modulus of elasticity; for steel 75-80 GPa (10^6 kN/m2) Ip is the cross section polar moment of inertia [m4]

Expand source code
@property
def GIp(self):
    """G * Ipp : torsional stiffness about the X (length) axis [kN m2]

    G is the shear-modulus of elasticity; for steel 75-80 GPa (10^6 kN/m2)
    Ip is the cross section polar moment of inertia [m4]
    """
    return self._vfNode.GIp
var L

Length of the beam in unloaded condition [m]

Expand source code
@property
def L(self):
    """Length of the beam in unloaded condition [m]"""
    return self._vfNode.L
var X_midpoints

X-positions of the beam centers measured along the length of the beam [m]

Expand source code
@property
def X_midpoints(self):
    """X-positions of the beam centers measured along the length of the beam [m]"""
    return tuple(
        0.5 * (np.array(self._vfNode.x[:-1]) + np.array(self._vfNode.x[1:]))
    )
var X_nodes

Returns the x-positions of the end nodes and internal nodes along the length of the beam [m]

Expand source code
@property
def X_nodes(self):
    """Returns the x-positions of the end nodes and internal nodes along the length of the beam [m]"""
    return self._vfNode.x
var bending

Bending forces of the end nodes and internal nodes [0, kNm, kNm]

Expand source code
@property
def bending(self):
    """Bending forces of the end nodes and internal nodes [0, kNm, kNm]"""
    return np.array(self._vfNode.bending)
var global_orientations

Global-orientations of the end nodes and internal nodes [deg,deg,deg]

Expand source code
@property
def global_orientations(self):
    """Global-orientations of the end nodes and internal nodes [deg,deg,deg]"""
    return np.rad2deg(self._vfNode.global_orientation)
var global_positions

Global-positions of the end nodes and internal nodes [m,m,m]

Expand source code
@property
def global_positions(self):
    """Global-positions of the end nodes and internal nodes [m,m,m]"""
    return np.array(self._vfNode.global_position, dtype=float)
var mass

Mass of the beam in [mT]

Expand source code
@property
def mass(self):
    """Mass of the beam in [mT]"""
    return self._vfNode.Mass
var moment_A

Moment on beam at node A (kNm, kNm, kNm) , axis system of node A

Expand source code
@property
def moment_A(self):
    """Moment on beam at node A (kNm, kNm, kNm) , axis system of node A"""
    return self._vfNode.moment_on_master
var moment_B

Moment on beam at node B (kNm, kNm, kNm) , axis system of node B

Expand source code
@property
def moment_B(self):
    """Moment on beam at node B (kNm, kNm, kNm) , axis system of node B"""
    return self._vfNode.moment_on_slave
var n_segments
Expand source code
@property
def n_segments(self):
    return self._vfNode.nSegments
var nodeA

The axis system that the A-end of the beam is connected to. The beam leaves this axis system along the X-axis

Expand source code
@property
def nodeA(self):
    """The axis system that the A-end of the beam is connected to. The beam leaves this axis system along the X-axis"""
    return self._nodeA
var nodeB

The axis system that the B-end of the beam is connected to. The beam arrives at this axis system along the X-axis

Expand source code
@property
def nodeB(self):
    """The axis system that the B-end of the beam is connected to. The beam arrives at this axis system along the X-axis"""
    return self._nodeB
var tension

Tension in the beam [kN], negative for compression

tension is calculated at the midpoints of the beam segments.

Expand source code
@property
def tension(self):
    """Tension in the beam [kN], negative for compression

    tension is calculated at the midpoints of the beam segments.
    """
    return self._vfNode.tension
var tension_only

axial stiffness (EA) only applicable to tension [True/False]

Expand source code
@property
def tension_only(self):
    """axial stiffness (EA) only applicable to tension [True/False]"""
    return self._vfNode.tensionOnly
var torsion

Torsion moment [kNm]. Positive if end B has a positive rotation about the x-axis of end A

torsion is calculated at the midpoints of the beam segments.

Expand source code
@property
def torsion(self):
    """Torsion moment [kNm]. Positive if end B has a positive rotation about the x-axis of end A

    torsion is calculated at the midpoints of the beam segments.
    """
    return self._vfNode.torsion

Inherited members

class Buoyancy (scene, vfBuoyancy)

Buoyancy provides a buoyancy force based on a buoyancy mesh. The mesh is triangulated and chopped at the instantaneous flat water surface. Buoyancy is applied as an upwards force that the center of buoyancy. The calculation of buoyancy is as accurate as the provided geometry.

There as no restrictions to the size or aspect ratio of the panels. It is excellent to model as box using 6 faces. Using smaller panels has a negative effect on performance.

The normals of the panels should point towards to water.

Expand source code
class Buoyancy(NodeWithParent):
    """Buoyancy provides a buoyancy force based on a buoyancy mesh. The mesh is triangulated and chopped at the instantaneous flat water surface. Buoyancy is applied as an upwards force that the center of buoyancy.
    The calculation of buoyancy is as accurate as the provided geometry.

    There as no restrictions to the size or aspect ratio of the panels. It is excellent to model as box using 6 faces. Using smaller panels has a negative effect on performance.

    The normals of the panels should point towards to water.
    """

    # init parent and name are fully derived from NodeWithParent
    # _vfNode is a buoyancy
    def __init__(self, scene, vfBuoyancy):
        super().__init__(scene, vfBuoyancy)
        self._None_parent_acceptable = False
        self._trimesh = TriMeshSource(
            self._scene, self._vfNode.trimesh
        )  # the tri-mesh is wrapped in a custom object

    def update(self):
        self._vfNode.reloadTrimesh()

    @property
    def trimesh(self) -> TriMeshSource:
        return self._trimesh

    @property
    def cob(self):
        """GLOBAL position of the center of buoyancy [m,m,m] (global axis)"""
        return self._vfNode.cob

    @property
    def cob_local(self):
        """Position of the center of buoyancy [m,m,m] (local axis)"""

        return self.parent.to_loc_position(self.cob)

    @property
    def displacement(self):
        """Displaced volume of fluid [m^3]"""
        return self._vfNode.displacement

    @property
    def density(self):
        """Density of surrounding fluid [mT/m3].
        Typical values: Seawater = 1.025, fresh water = 1.00
        """
        return self._vfNode.density

    @density.setter
    @node_setter_manageable
    @node_setter_observable
    def density(self, value):
        assert1f_positive_or_zero(value, "density")
        self._vfNode.density = value

    def give_python_code(self):
        code = "# code for {}".format(self.name)
        code += "\nmesh = s.new_buoyancy(name='{}',".format(self.name)

        if self.density != 1.025:
            code += f"\n          density={self.density},"

        code += "\n          parent='{}')".format(self.parent_for_export.name)

        if self.trimesh._invert_normals:
            code += "\nmesh.trimesh.load_file(r'{}', scale = ({},{},{}), rotation = ({},{},{}), offset = ({},{},{}), invert_normals=True)".format(
                self.trimesh._path,
                *self.trimesh._scale,
                *self.trimesh._rotation,
                *self.trimesh._offset,
            )
        else:
            code += "\nmesh.trimesh.load_file(r'{}', scale = ({},{},{}), rotation = ({},{},{}), offset = ({},{},{}))".format(
                self.trimesh._path,
                *self.trimesh._scale,
                *self.trimesh._rotation,
                *self.trimesh._offset,
            )

        return code

Ancestors

Instance variables

var cob

GLOBAL position of the center of buoyancy [m,m,m] (global axis)

Expand source code
@property
def cob(self):
    """GLOBAL position of the center of buoyancy [m,m,m] (global axis)"""
    return self._vfNode.cob
var cob_local

Position of the center of buoyancy [m,m,m] (local axis)

Expand source code
@property
def cob_local(self):
    """Position of the center of buoyancy [m,m,m] (local axis)"""

    return self.parent.to_loc_position(self.cob)
var density

Density of surrounding fluid [mT/m3]. Typical values: Seawater = 1.025, fresh water = 1.00

Expand source code
@property
def density(self):
    """Density of surrounding fluid [mT/m3].
    Typical values: Seawater = 1.025, fresh water = 1.00
    """
    return self._vfNode.density
var displacement

Displaced volume of fluid [m^3]

Expand source code
@property
def displacement(self):
    """Displaced volume of fluid [m^3]"""
    return self._vfNode.displacement
var trimeshTriMeshSource
Expand source code
@property
def trimesh(self) -> TriMeshSource:
    return self._trimesh

Inherited members

class Cable (scene, node)

A Cable represents a linear elastic wire running from a Poi or sheave to another Poi of sheave.

A cable has a un-stretched length [length] and a stiffness [EA] and may have a diameter [m]. The tension in the cable is calculated.

Intermediate pois or sheaves may be added.

  • Pois are considered as sheaves with a zero diameter.
  • Sheaves are considered sheaves with the given geometry. If defined then the diameter of the cable is considered when calculating the geometry. The cable runs over the sheave in the positive direction (right hand rule) as defined by the axis of the sheave.

For cables running over a sheave the friction in sideways direction is considered to be infinite. The geometry is calculated such that the cable section between sheaves is perpendicular to the vector from the axis of the sheave to the point where the cable leaves the sheave.

This assumption results in undefined behaviour when the axis of the sheave is parallel to the cable direction.

Notes

If pois or sheaves on a cable come too close together (<1mm) then they will be pushed away from eachother. This prevents the unwanted situation where multiple pois end up at the same location. In that case it can not be determined which amount of force should be applied to each of the pois.

Expand source code
class Cable(CoreConnectedNode):
    """A Cable represents a linear elastic wire running from a Poi or sheave to another Poi of sheave.

    A cable has a un-stretched length [length] and a stiffness [EA] and may have a diameter [m]. The tension in the cable is calculated.

    Intermediate pois or sheaves may be added.

    - Pois are considered as sheaves with a zero diameter.
    - Sheaves are considered sheaves with the given geometry. If defined then the diameter of the cable is considered when calculating the geometry. The cable runs over the sheave in the positive direction (right hand rule) as defined by the axis of the sheave.

    For cables running over a sheave the friction in sideways direction is considered to be infinite. The geometry is calculated such that the
    cable section between sheaves is perpendicular to the vector from the axis of the sheave to the point where the cable leaves the sheave.

    This assumption results in undefined behaviour when the axis of the sheave is parallel to the cable direction.

    Notes:
        If pois or sheaves on a cable come too close together (<1mm) then they will be pushed away from eachother.
        This prevents the unwanted situation where multiple pois end up at the same location. In that case it can not be determined which amount of force should be applied to each of the pois.


    """

    def __init__(self, scene, node):
        super().__init__(scene, node)
        self._pois = list()

    def depends_on(self):
        return [*self._pois]

    @property
    def tension(self):
        """Tension in the cable [kN]"""
        return self._vfNode.tension

    @property
    def stretch(self):
        """Stretch of the cable [m]

        Tension [kN] = EA [kN] * stretch [m] / length [m]
        """
        return self._vfNode.stretch

    @property
    def length(self):
        """Length of the cable when in rest [m]

        Tension [kN] = EA [kN] * stretch [m] / length [m]
        """
        return self._vfNode.Length

    @length.setter
    @node_setter_manageable
    @node_setter_observable
    def length(self, val):

        if val < 1e-9:
            raise Exception(
                "Length shall be more than 0 (otherwise stiffness EA/L becomes infinite)"
            )
        self._vfNode.Length = val

    @property
    def EA(self):
        """Stiffness of the cable [kN]

        Tension [kN] = EA [kN] * stretch [m] / length [m]
        """
        return self._vfNode.EA

    @EA.setter
    @node_setter_manageable
    @node_setter_observable
    def EA(self, ea):

        self._vfNode.EA = ea

    @property
    def diameter(self):
        """Diameter of the cable. Used when a cable runs over a circle. [m]"""
        return self._vfNode.diameter

    @diameter.setter
    @node_setter_manageable
    @node_setter_observable
    def diameter(self, diameter):

        self._vfNode.diameter = diameter

    @property
    def connections(self):
        """List or Tuple of nodes that this cable is connected to. Nodes may be passed by name (string) or by reference.

        Example:
            p1 = s.new_point('point 1')
            p2 = s.new_point('point 2', position = (0,0,10)
            p3 = s.new_point('point 3', position = (10,0,10)
            c1 = s.new_circle('circle 1',parent = p3, axis = (0,1,0), radius = 1)
            c = s.new_cable("cable_1", endA="Point", endB = "Circle", length = 1.2, EA = 10000)

            c.connections = ('point 1', 'point 2', 'point 3')
            # or
            c.connections = (p1, p2,p3)
            # or
            c.connections = [p1, 'point 2', p3]  # all the same

        Notes:
            1. Circles can not be used as endpoins. If one of the endpoints is a Circle then the Point that that circle
            is located on is used instead.
            2. Points should not be repeated directly.

        The following will fail:
        c.connections = ('point 1', 'point 3', 'circle 1')

        because the last point is a circle. So circle 1 will be replaced with the point that the circle is on: point 3.

        so this becomes
        ('point 1','point 3','point 3')

        this is invalid because point 3 is repeated.

        """
        return tuple(self._pois)

    @connections.setter
    @node_setter_manageable
    @node_setter_observable
    def connections(self, value):

        if len(value) < 2:
            raise ValueError("At least two connections required")

        nodes = []
        for p in value:
            n = self._scene._node_from_node_or_str(p)

            if not (isinstance(n, Point) or isinstance(n, Circle)):
                raise ValueError(
                    f"Only Sheaves and Pois can be used as connection, but {n.name} is a {type(n)}"
                )
            nodes.append(n)

        # check for repeated nodes
        n = len(nodes)
        for i in range(n - 1):
            node1 = nodes[i]
            node2 = nodes[i + 1]

            # # if first or last node is a sheave, the this will be replaced by the poi of the sheave
            # if i == 0 and isinstance(node1, Circle):
            #     node1 = node1.parent
            # if i == n - 2 and isinstance(node2, Circle):
            #     node2 = node2.parent

            if node1 == node2:
                raise ValueError(
                    f"It is not allowed to have the same node repeated - you have {node1.name} and {node2.name}"
                )

        self._pois.clear()
        self._pois.extend(nodes)
        self._update_pois()

    def get_points_for_visual(self):
        """A list of 3D locations which can be used for visualization"""
        return self._vfNode.global_points

    def _add_connection_to_core(self, connection):
        if isinstance(connection, Point):
            self._vfNode.add_connection_poi(connection._vfNode)
        if isinstance(connection, Circle):
            self._vfNode.add_connection_sheave(connection._vfNode)

    def _update_pois(self):
        self._vfNode.clear_connections()
        for point in self._pois:
            self._add_connection_to_core(point)

    def _give_poi_names(self):
        """Returns a list with the names of all the pois"""
        r = list()
        for p in self._pois:
            r.append(p.name)
        return r

    def give_python_code(self):
        code = "# code for {}".format(self.name)

        poi_names = self._give_poi_names()
        n_sheaves = len(poi_names) - 2

        code += "\ns.new_cable(name='{}',".format(self.name)
        code += "\n            endA='{}',".format(poi_names[0])
        code += "\n            endB='{}',".format(poi_names[-1])
        code += "\n            length={},".format(self.length)

        if self.diameter != 0:
            code += "\n            diameter={},".format(self.diameter)

        if len(poi_names) <= 2:
            code += "\n            EA={})".format(self.EA)
        else:
            code += "\n            EA={},".format(self.EA)

            if n_sheaves == 1:
                code += "\n            sheaves = ['{}'])".format(poi_names[1])
            else:
                code += "\n            sheaves = ['{}',".format(poi_names[1])
                for i in range(n_sheaves - 2):
                    code += "\n                       '{}',".format(poi_names[2 + i])
                code += "\n                       '{}']),".format(poi_names[-2])

        return code

Ancestors

Instance variables

var EA

Stiffness of the cable [kN]

Tension [kN] = EA [kN] * stretch [m] / length [m]

Expand source code
@property
def EA(self):
    """Stiffness of the cable [kN]

    Tension [kN] = EA [kN] * stretch [m] / length [m]
    """
    return self._vfNode.EA
var connections

List or Tuple of nodes that this cable is connected to. Nodes may be passed by name (string) or by reference.

Example

p1 = s.new_point('point 1') p2 = s.new_point('point 2', position = (0,0,10) p3 = s.new_point('point 3', position = (10,0,10) c1 = s.new_circle('circle 1',parent = p3, axis = (0,1,0), radius = 1) c = s.new_cable("cable_1", endA="Point", endB = "Circle", length = 1.2, EA = 10000)

c.connections = ('point 1', 'point 2', 'point 3')

or

c.connections = (p1, p2,p3)

or

c.connections = [p1, 'point 2', p3] # all the same

Notes

  1. Circles can not be used as endpoins. If one of the endpoints is a Circle then the Point that that circle is located on is used instead.
  2. Points should not be repeated directly.

The following will fail: c.connections = ('point 1', 'point 3', 'circle 1')

because the last point is a circle. So circle 1 will be replaced with the point that the circle is on: point 3.

so this becomes ('point 1','point 3','point 3')

this is invalid because point 3 is repeated.

Expand source code
@property
def connections(self):
    """List or Tuple of nodes that this cable is connected to. Nodes may be passed by name (string) or by reference.

    Example:
        p1 = s.new_point('point 1')
        p2 = s.new_point('point 2', position = (0,0,10)
        p3 = s.new_point('point 3', position = (10,0,10)
        c1 = s.new_circle('circle 1',parent = p3, axis = (0,1,0), radius = 1)
        c = s.new_cable("cable_1", endA="Point", endB = "Circle", length = 1.2, EA = 10000)

        c.connections = ('point 1', 'point 2', 'point 3')
        # or
        c.connections = (p1, p2,p3)
        # or
        c.connections = [p1, 'point 2', p3]  # all the same

    Notes:
        1. Circles can not be used as endpoins. If one of the endpoints is a Circle then the Point that that circle
        is located on is used instead.
        2. Points should not be repeated directly.

    The following will fail:
    c.connections = ('point 1', 'point 3', 'circle 1')

    because the last point is a circle. So circle 1 will be replaced with the point that the circle is on: point 3.

    so this becomes
    ('point 1','point 3','point 3')

    this is invalid because point 3 is repeated.

    """
    return tuple(self._pois)
var diameter

Diameter of the cable. Used when a cable runs over a circle. [m]

Expand source code
@property
def diameter(self):
    """Diameter of the cable. Used when a cable runs over a circle. [m]"""
    return self._vfNode.diameter
var length

Length of the cable when in rest [m]

Tension [kN] = EA [kN] * stretch [m] / length [m]

Expand source code
@property
def length(self):
    """Length of the cable when in rest [m]

    Tension [kN] = EA [kN] * stretch [m] / length [m]
    """
    return self._vfNode.Length
var stretch

Stretch of the cable [m]

Tension [kN] = EA [kN] * stretch [m] / length [m]

Expand source code
@property
def stretch(self):
    """Stretch of the cable [m]

    Tension [kN] = EA [kN] * stretch [m] / length [m]
    """
    return self._vfNode.stretch
var tension

Tension in the cable [kN]

Expand source code
@property
def tension(self):
    """Tension in the cable [kN]"""
    return self._vfNode.tension

Methods

def get_points_for_visual(self)

A list of 3D locations which can be used for visualization

Expand source code
def get_points_for_visual(self):
    """A list of 3D locations which can be used for visualization"""
    return self._vfNode.global_points

Inherited members

class Circle (scene, vfNode)

A Circle models a circle shape based on a diameter and an axis direction

Expand source code
class Circle(NodeWithParent):
    """A Circle models a circle shape based on a diameter and an axis direction"""

    @property
    def axis(self) -> tuple:
        """Direction of the sheave axis (x,y,z) in parent axis system.

        Note:
            The direction of the axis is also used to determine the positive direction over the circumference of the
            circle. This is then used when cables run over the circle or the circle is used for geometric contacts. So
            if a cable runs over the circle in the wrong direction then a solution is to change the axis direction to
            its opposite:  circle.axis =- circle.axis. (another solution in that case is to define the connections of
            the cable in the reverse order)
        """
        return self._vfNode.axis_direction

    @axis.setter
    @node_setter_manageable
    @node_setter_observable
    def axis(self, val):

        assert3f(val)
        if np.linalg.norm(val) == 0:
            raise ValueError("Axis can not be 0,0,0")
        self._vfNode.axis_direction = val

    @property
    def radius(self):
        """Radius of the circle [m]"""
        return self._vfNode.radius

    @radius.setter
    @node_setter_manageable
    @node_setter_observable
    def radius(self, val):

        assert1f(val)
        self._vfNode.radius = val

    def give_python_code(self):
        code = "# code for {}".format(self.name)
        code += "\ns.new_circle(name='{}',".format(self.name)
        code += "\n            parent='{}',".format(self.parent_for_export.name)
        code += "\n            axis=({}, {}, {}),".format(*self.axis)
        code += "\n            radius={} )".format(self.radius)
        return code

    @property
    def global_position(self):
        """Returns the global position of the center of the sheave.

        Note: this is the same as the global position of the parent point.
        """
        return self.parent.global_position

Ancestors

Instance variables

var axis : tuple

Direction of the sheave axis (x,y,z) in parent axis system.

Note

The direction of the axis is also used to determine the positive direction over the circumference of the circle. This is then used when cables run over the circle or the circle is used for geometric contacts. So if a cable runs over the circle in the wrong direction then a solution is to change the axis direction to its opposite: circle.axis =- circle.axis. (another solution in that case is to define the connections of the cable in the reverse order)

Expand source code
@property
def axis(self) -> tuple:
    """Direction of the sheave axis (x,y,z) in parent axis system.

    Note:
        The direction of the axis is also used to determine the positive direction over the circumference of the
        circle. This is then used when cables run over the circle or the circle is used for geometric contacts. So
        if a cable runs over the circle in the wrong direction then a solution is to change the axis direction to
        its opposite:  circle.axis =- circle.axis. (another solution in that case is to define the connections of
        the cable in the reverse order)
    """
    return self._vfNode.axis_direction
var global_position

Returns the global position of the center of the sheave.

Note: this is the same as the global position of the parent point.

Expand source code
@property
def global_position(self):
    """Returns the global position of the center of the sheave.

    Note: this is the same as the global position of the parent point.
    """
    return self.parent.global_position
var radius

Radius of the circle [m]

Expand source code
@property
def radius(self):
    """Radius of the circle [m]"""
    return self._vfNode.radius

Inherited members

class ClaimManagement (scene, manager)

Helper class for doing:

with ClaimManagement(scene, manager): change nodes that belong to manager

Expand source code
class ClaimManagement():
    """Helper class for doing:

    with ClaimManagement(scene, manager):
        change nodes that belong to manager

    """
    def __init__(self, scene, manager):
        assert isinstance(scene, Scene)
        assert isinstance(manager, Manager)
        self.scene = scene
        self.manager= manager


    def __enter__(self):
        self._old_manager = self.scene.current_manager
        self.scene.current_manager = self.manager

    def __exit__(self, *args, **kwargs):
        self.scene.current_manager = self._old_manager
class Connector2d (scene, node)

A Connector2d linear connector with acts both on linear displacement and angular displacement.

  • the linear stiffness is defined by k_linear and is defined over the actual shortest direction between nodeA and nodeB.
  • the angular stiffness is defined by k_angular and is defined over the actual smallest angle between the two systems.
Expand source code
class Connector2d(CoreConnectedNode):
    """A Connector2d linear connector with acts both on linear displacement and angular displacement.

    * the linear stiffness is defined by k_linear and is defined over the actual shortest direction between nodeA and nodeB.
    * the angular stiffness is defined by k_angular and is defined over the actual smallest angle between the two systems.
    """

    def __init__(self, scene, node):
        super().__init__(scene, node)
        self._nodeA = None
        self._nodeB = None

    def depends_on(self):
        return [self._nodeA, self._nodeB]

    @property
    def angle(self):
        """Actual angle between nodeA and nodeB [deg] (read-only)"""
        return np.rad2deg(self._vfNode.angle)

    @property
    def force(self):
        """Actual force between nodeA and nodeB [kN] (read-only)"""
        return self._vfNode.force

    @property
    def moment(self):
        """Actual moment between nodeA and nodeB [kNm] (read-only)"""
        return self._vfNode.moment

    @property
    def axis(self):
        """Actual rotation axis between nodeA and nodeB (read-only)"""
        return self._vfNode.axis

    @property
    def ax(self):
        """X component of actual rotation axis between nodeA and nodeB (read-only)"""
        return self._vfNode.axis[0]

    @property
    def ay(self):
        """Y component of actual rotation axis between nodeA and nodeB (read-only)"""
        return self._vfNode.axis[1]

    @property
    def az(self):
        """Z component of actual rotation axis between nodeA and nodeB (read-only)"""
        return self._vfNode.axis[2]

    @property
    def k_linear(self):
        """Linear stiffness [kN/m]"""
        return self._vfNode.k_linear

    @k_linear.setter
    @node_setter_manageable
    @node_setter_observable
    def k_linear(self, value):

        self._vfNode.k_linear = value

    @property
    def k_angular(self):
        """Angular stiffness [kNm/rad]"""
        return self._vfNode.k_angular

    @k_angular.setter
    @node_setter_manageable
    @node_setter_observable
    def k_angular(self, value):

        self._vfNode.k_angular = value

    @property
    def nodeA(self) -> Axis:
        """Connected axis system A"""
        return self._nodeA

    @nodeA.setter
    @node_setter_manageable
    @node_setter_observable
    def nodeA(self, val):

        val = self._scene._node_from_node_or_str(val)
        if not isinstance(val, Axis):
            raise TypeError("Provided nodeA should be a Axis")

        self._nodeA = val
        self._vfNode.master = val._vfNode

    @property
    def nodeB(self) -> Axis:
        """Connected axis system B"""
        return self._nodeB

    @nodeB.setter
    @node_setter_manageable
    @node_setter_observable
    def nodeB(self, val):

        val = self._scene._node_from_node_or_str(val)
        if not isinstance(val, Axis):
            raise TypeError("Provided nodeA should be a Axis")

        self._nodeB = val
        self._vfNode.slave = val._vfNode

    def give_python_code(self):
        code = "# code for {}".format(self.name)

        code += "\ns.new_connector2d(name='{}',".format(self.name)
        code += "\n            nodeA='{}',".format(self.nodeA.name)
        code += "\n            nodeB='{}',".format(self.nodeB.name)
        code += "\n            k_linear ={},".format(self.k_linear)
        code += "\n            k_angular ={})".format(self.k_angular)

        return code

Ancestors

Instance variables

var angle

Actual angle between nodeA and nodeB [deg] (read-only)

Expand source code
@property
def angle(self):
    """Actual angle between nodeA and nodeB [deg] (read-only)"""
    return np.rad2deg(self._vfNode.angle)
var ax

X component of actual rotation axis between nodeA and nodeB (read-only)

Expand source code
@property
def ax(self):
    """X component of actual rotation axis between nodeA and nodeB (read-only)"""
    return self._vfNode.axis[0]
var axis

Actual rotation axis between nodeA and nodeB (read-only)

Expand source code
@property
def axis(self):
    """Actual rotation axis between nodeA and nodeB (read-only)"""
    return self._vfNode.axis
var ay

Y component of actual rotation axis between nodeA and nodeB (read-only)

Expand source code
@property
def ay(self):
    """Y component of actual rotation axis between nodeA and nodeB (read-only)"""
    return self._vfNode.axis[1]
var az

Z component of actual rotation axis between nodeA and nodeB (read-only)

Expand source code
@property
def az(self):
    """Z component of actual rotation axis between nodeA and nodeB (read-only)"""
    return self._vfNode.axis[2]
var force

Actual force between nodeA and nodeB [kN] (read-only)

Expand source code
@property
def force(self):
    """Actual force between nodeA and nodeB [kN] (read-only)"""
    return self._vfNode.force
var k_angular

Angular stiffness [kNm/rad]

Expand source code
@property
def k_angular(self):
    """Angular stiffness [kNm/rad]"""
    return self._vfNode.k_angular
var k_linear

Linear stiffness [kN/m]

Expand source code
@property
def k_linear(self):
    """Linear stiffness [kN/m]"""
    return self._vfNode.k_linear
var moment

Actual moment between nodeA and nodeB [kNm] (read-only)

Expand source code
@property
def moment(self):
    """Actual moment between nodeA and nodeB [kNm] (read-only)"""
    return self._vfNode.moment
var nodeAAxis

Connected axis system A

Expand source code
@property
def nodeA(self) -> Axis:
    """Connected axis system A"""
    return self._nodeA
var nodeBAxis

Connected axis system B

Expand source code
@property
def nodeB(self) -> Axis:
    """Connected axis system B"""
    return self._nodeB

Inherited members

class ContactBall (scene, node)

A ContactBall is a linear elastic ball which can contact with ContactMeshes.

It is modelled as a sphere around a Poi. Radius and stiffness can be controlled using radius and k.

The force is applied on the Poi and it not registered separately.

Expand source code
class ContactBall(NodeWithParent):
    """A ContactBall is a linear elastic ball which can contact with ContactMeshes.

    It is modelled as a sphere around a Poi. Radius and stiffness can be controlled using radius and k.

    The force is applied on the Poi and it not registered separately.
    """

    def __init__(self, scene, node):
        super().__init__(scene, node)
        self._meshes = list()

    @property
    def can_contact(self) -> bool:
        """True if the ball ball is perpendicular to at least one of the faces of one of the meshes. So when contact is possible. To check if there is contact use "force"
        See Also: Force
        """
        return self._vfNode.has_contact

    @property
    def contact_force(self) -> tuple:
        """Returns the force on the ball [kN, kN, kN] (global axis)

        The force is applied at the center of the ball

        See Also: contact_force_magnitude
        """
        return self._vfNode.force

    @property
    def contact_force_magnitude(self) -> float:
        """Returns the absolute force on the ball, if any [kN]

        The force is applied on the center of the ball

        See Also: contact_force
        """
        return np.linalg.norm(self._vfNode.force)

    @property
    def compression(self) -> float:
        """Returns the absolute compression of the ball, if any [m]"""
        return self._vfNode.force

    @property
    def contactpoint(self):
        """The nearest point on the nearest mesh. Only defined"""
        return self._vfNode.contact_point

    def update(self):
        """Updates the contact-points and applies forces on mesh and point"""
        self._vfNode.update()

    @property
    def meshes(self) -> tuple:
        """List of contact-mesh nodes.
        When getting this will yield a list of node references.
        When setting node references and node-names may be used.

        eg: ball.meshes = [mesh1, 'mesh2']
        """
        return tuple(self._meshes)

    @meshes.setter
    @node_setter_manageable
    @node_setter_observable
    def meshes(self, value):

        meshes = []

        for m in value:
            cm = self._scene._node_from_node_or_str(m)

            if not isinstance(cm, ContactMesh):
                raise ValueError(
                    f"Only ContactMesh nodes can be used as mesh, but {cm.name} is a {type(cm)}"
                )
            if cm in meshes:
                raise ValueError(f"Can not add {cm.name} twice")

            meshes.append(cm)

        # copy to meshes
        self._meshes.clear()
        self._vfNode.clear_contactmeshes()
        for mesh in meshes:
            self._meshes.append(mesh)
            self._vfNode.add_contactmesh(mesh._vfNode)

    @property
    def meshes_names(self) -> list:
        """List with the names of the meshes"""
        return [m.name for m in self._meshes]

    @property
    def radius(self):
        """Radius of the contact-ball [m]"""
        return self._vfNode.radius

    @radius.setter
    @node_setter_manageable
    @node_setter_observable
    def radius(self, value):

        assert1f_positive_or_zero(value, "radius")
        self._vfNode.radius = value
        pass

    @property
    def k(self):
        """Compression stiffness of the ball in force per meter of compression [kN/m]"""
        return self._vfNode.k

    @k.setter
    @node_setter_manageable
    @node_setter_observable
    def k(self, value):

        assert1f_positive_or_zero(value, "k")
        self._vfNode.k = value
        pass

    def give_python_code(self):
        code = "# code for {}".format(self.name)

        code += "\ns.new_contactball(name='{}',".format(self.name)
        code += "\n                  parent='{}',".format(self.parent_for_export.name)
        code += "\n                  radius={},".format(self.radius)
        code += "\n                  k={},".format(self.k)
        code += "\n                  meshes = [ "

        for m in self._meshes:
            code += '"' + m.name + '",'
        code = code[:-1] + "])"

        return code

    # =======================================================================

Ancestors

Instance variables

var can_contact : bool

True if the ball ball is perpendicular to at least one of the faces of one of the meshes. So when contact is possible. To check if there is contact use "force" See Also: Force

Expand source code
@property
def can_contact(self) -> bool:
    """True if the ball ball is perpendicular to at least one of the faces of one of the meshes. So when contact is possible. To check if there is contact use "force"
    See Also: Force
    """
    return self._vfNode.has_contact
var compression : float

Returns the absolute compression of the ball, if any [m]

Expand source code
@property
def compression(self) -> float:
    """Returns the absolute compression of the ball, if any [m]"""
    return self._vfNode.force
var contact_force : tuple

Returns the force on the ball [kN, kN, kN] (global axis)

The force is applied at the center of the ball

See Also: contact_force_magnitude

Expand source code
@property
def contact_force(self) -> tuple:
    """Returns the force on the ball [kN, kN, kN] (global axis)

    The force is applied at the center of the ball

    See Also: contact_force_magnitude
    """
    return self._vfNode.force
var contact_force_magnitude : float

Returns the absolute force on the ball, if any [kN]

The force is applied on the center of the ball

See Also: contact_force

Expand source code
@property
def contact_force_magnitude(self) -> float:
    """Returns the absolute force on the ball, if any [kN]

    The force is applied on the center of the ball

    See Also: contact_force
    """
    return np.linalg.norm(self._vfNode.force)
var contactpoint

The nearest point on the nearest mesh. Only defined

Expand source code
@property
def contactpoint(self):
    """The nearest point on the nearest mesh. Only defined"""
    return self._vfNode.contact_point
var k

Compression stiffness of the ball in force per meter of compression [kN/m]

Expand source code
@property
def k(self):
    """Compression stiffness of the ball in force per meter of compression [kN/m]"""
    return self._vfNode.k
var meshes : tuple

List of contact-mesh nodes. When getting this will yield a list of node references. When setting node references and node-names may be used.

eg: ball.meshes = [mesh1, 'mesh2']

Expand source code
@property
def meshes(self) -> tuple:
    """List of contact-mesh nodes.
    When getting this will yield a list of node references.
    When setting node references and node-names may be used.

    eg: ball.meshes = [mesh1, 'mesh2']
    """
    return tuple(self._meshes)
var meshes_names : list

List with the names of the meshes

Expand source code
@property
def meshes_names(self) -> list:
    """List with the names of the meshes"""
    return [m.name for m in self._meshes]
var radius

Radius of the contact-ball [m]

Expand source code
@property
def radius(self):
    """Radius of the contact-ball [m]"""
    return self._vfNode.radius

Methods

def update(self)

Updates the contact-points and applies forces on mesh and point

Expand source code
def update(self):
    """Updates the contact-points and applies forces on mesh and point"""
    self._vfNode.update()

Inherited members

class ContactMesh (scene, vfContactMesh)

A ContactMesh is a tri-mesh with an axis parent

Expand source code
class ContactMesh(NodeWithParent):
    """A ContactMesh is a tri-mesh with an axis parent"""

    def __init__(self, scene, vfContactMesh):
        super().__init__(scene, vfContactMesh)
        self._None_parent_acceptable = True
        self._trimesh = TriMeshSource(
            self._scene, self._vfNode.trimesh
        )  # the tri-mesh is wrapped in a custom object

    @property
    def trimesh(self):
        """The TriMeshSource object which can be used to change the mesh

        Example:
            s['Contactmesh'].trimesh.load_file('cube.obj', scale = (1.0,1.0,1.0), rotation = (0.0,0.0,0.0), offset = (0.0,0.0,0.0))
        """
        return self._trimesh

    def give_python_code(self):
        code = "# code for {}".format(self.name)
        code += "\nmesh = s.new_contactmesh(name='{}'".format(self.name)
        if self.parent_for_export:
            code += ", parent='{}')".format(self.parent_for_export.name)
        else:
            code += ")"
        code += "\nmesh.trimesh.load_file(r'{}', scale = ({},{},{}), rotation = ({},{},{}), offset = ({},{},{}))".format(
            self.trimesh._path,
            *self.trimesh._scale,
            *self.trimesh._rotation,
            *self.trimesh._offset,
        )

        return code

Ancestors

Instance variables

var trimesh

The TriMeshSource object which can be used to change the mesh

Example

s['Contactmesh'].trimesh.load_file('cube.obj', scale = (1.0,1.0,1.0), rotation = (0.0,0.0,0.0), offset = (0.0,0.0,0.0))

Expand source code
@property
def trimesh(self):
    """The TriMeshSource object which can be used to change the mesh

    Example:
        s['Contactmesh'].trimesh.load_file('cube.obj', scale = (1.0,1.0,1.0), rotation = (0.0,0.0,0.0), offset = (0.0,0.0,0.0))
    """
    return self._trimesh

Inherited members

class CoreConnectedNode (scene, vfNode)

ABSTRACT CLASS - Properties defined here are applicable to all derived classes Master class for all nodes with a connected eqCore element

Expand source code
class CoreConnectedNode(Node):
    """ABSTRACT CLASS - Properties defined here are applicable to all derived classes
    Master class for all nodes with a connected eqCore element"""

    def __init__(self, scene, vfNode):
        super().__init__(scene)
        self._vfNode = vfNode

    @property
    def name(self):
        """Name of the node (str), must be unique"""
        return self._vfNode.name

    @name.setter
    @node_setter_manageable
    @node_setter_observable
    def name(self, name):

        if not name == self._vfNode.name:
            self._scene._verify_name_available(name)
            self._vfNode.name = name

    def _delete_vfc(self):
        self._scene._vfc.delete(self._vfNode.name)

Ancestors

Subclasses

Inherited members

class Force (scene, vfNode)

A Force models a force and moment on a poi.

Both are expressed in the global axis system.

Expand source code
class Force(NodeWithParent):
    """A Force models a force and moment on a poi.

    Both are expressed in the global axis system.

    """

    @property
    def force(self):
        """The x,y and z components of the force [kN,kN,kN] (global axis)

        Example s['wind'].force = (12,34,56)
        """
        return self._vfNode.force

    @force.setter
    @node_setter_manageable
    @node_setter_observable
    def force(self, val):

        assert3f(val)
        self._vfNode.force = val

    @property
    def fx(self):
        """The global x-component of the force [kN] (global axis)"""
        return self.force[0]

    @fx.setter
    @node_setter_manageable
    @node_setter_observable
    def fx(self, var):

        a = self.force
        self.force = (var, a[1], a[2])

    @property
    def fy(self):
        """The global y-component of the force [kN]  (global axis)"""
        return self.force[1]

    @fy.setter
    @node_setter_manageable
    @node_setter_observable
    def fy(self, var):

        a = self.force
        self.force = (a[0], var, a[2])

    @property
    def fz(self):
        """The global z-component of the force [kN]  (global axis)"""

        return self.force[2]

    @fz.setter
    @node_setter_manageable
    @node_setter_observable
    def fz(self, var):

        a = self.force
        self.force = (a[0], a[1], var)

    @property
    def moment(self):
        """The x,y and z components of the moment (kNm,kNm,kNm) in the global axis system.

        Example s['wind'].moment = (12,34,56)
        """
        return self._vfNode.moment

    @moment.setter
    @node_setter_manageable
    @node_setter_observable
    def moment(self, val):

        assert3f(val)
        self._vfNode.moment = val

    @property
    def mx(self):
        """The global x-component of the moment [kNm]  (global axis)"""
        return self.moment[0]

    @mx.setter
    @node_setter_manageable
    @node_setter_observable
    def mx(self, var):

        a = self.moment
        self.moment = (var, a[1], a[2])

    @property
    def my(self):
        """The global y-component of the moment [kNm]  (global axis)"""
        return self.moment[1]

    @my.setter
    @node_setter_manageable
    @node_setter_observable
    def my(self, var):

        a = self.moment
        self.moment = (a[0], var, a[2])

    @property
    def mz(self):
        """The global z-component of the moment [kNm]  (global axis)"""
        return self.moment[2]

    @mz.setter
    @node_setter_manageable
    @node_setter_observable
    def mz(self, var):

        a = self.moment
        self.moment = (a[0], a[1], var)

    def give_python_code(self):
        code = "# code for {}".format(self.name)

        # new_force(self, name, parent=None, force=None, moment=None):

        code += "\ns.new_force(name='{}',".format(self.name)
        code += "\n            parent='{}',".format(self.parent_for_export.name)
        code += "\n            force=({}, {}, {}),".format(*self.force)
        code += "\n            moment=({}, {}, {}) )".format(*self.moment)
        return code

Ancestors

Instance variables

var force

The x,y and z components of the force [kN,kN,kN] (global axis)

Example s['wind'].force = (12,34,56)

Expand source code
@property
def force(self):
    """The x,y and z components of the force [kN,kN,kN] (global axis)

    Example s['wind'].force = (12,34,56)
    """
    return self._vfNode.force
var fx

The global x-component of the force [kN] (global axis)

Expand source code
@property
def fx(self):
    """The global x-component of the force [kN] (global axis)"""
    return self.force[0]
var fy

The global y-component of the force [kN] (global axis)

Expand source code
@property
def fy(self):
    """The global y-component of the force [kN]  (global axis)"""
    return self.force[1]
var fz

The global z-component of the force [kN] (global axis)

Expand source code
@property
def fz(self):
    """The global z-component of the force [kN]  (global axis)"""

    return self.force[2]
var moment

The x,y and z components of the moment (kNm,kNm,kNm) in the global axis system.

Example s['wind'].moment = (12,34,56)

Expand source code
@property
def moment(self):
    """The x,y and z components of the moment (kNm,kNm,kNm) in the global axis system.

    Example s['wind'].moment = (12,34,56)
    """
    return self._vfNode.moment
var mx

The global x-component of the moment [kNm] (global axis)

Expand source code
@property
def mx(self):
    """The global x-component of the moment [kNm]  (global axis)"""
    return self.moment[0]
var my

The global y-component of the moment [kNm] (global axis)

Expand source code
@property
def my(self):
    """The global y-component of the moment [kNm]  (global axis)"""
    return self.moment[1]
var mz

The global z-component of the moment [kNm] (global axis)

Expand source code
@property
def mz(self):
    """The global z-component of the moment [kNm]  (global axis)"""
    return self.moment[2]

Inherited members

class GeometricContact (scene, child_circle, parent_circle, name, inside=True)

GeometricContact

A GeometricContact can be used to construct geometric connections between circular members: - steel bars and holes, such as a shackle pin in a padeye (pin-hole) - steel bars and steel bars, such as a shackle-shackle connection

Situation before creation of geometric contact:

Axis1 Point1 Circle1 Axis2 Point2 Circle2

Create a geometric contact with Circle1 and parent and Circle2 as child

Axis1 Point1 : observed, referenced as parent_circle_parent Circle1 : observed, referenced as parent_circle

_axis_on_parent                 : managed
    _pin_hole_connection        : managed
        _connection_axial_rotation : managed
            _axis_on_child      : managed
                Axis2           : managed    , referenced as child_circle_parent_parent
                    Point2      : observed   , referenced as child_circle_parent
                        Circle2 : observed   , referenced as child_circle

circle1 becomes the nodeB circle2 becomes the nodeA

(attach circle 1 to circle 2)

Args

scene: vfAxis: parent_circle: child_circle:

Expand source code
class GeometricContact(Manager):
    """
    GeometricContact

    A GeometricContact can be used to construct geometric connections between circular members:
        -       steel bars and holes, such as a shackle pin in a padeye (pin-hole)
        -       steel bars and steel bars, such as a shackle-shackle connection


    Situation before creation of geometric contact:

    Axis1
        Point1
            Circle1
    Axis2
        Point2
            Circle2

    Create a geometric contact with Circle1 and parent and Circle2 as child

    Axis1
        Point1              : observed, referenced as parent_circle_parent
            Circle1         : observed, referenced as parent_circle

        _axis_on_parent                 : managed
            _pin_hole_connection        : managed
                _connection_axial_rotation : managed
                    _axis_on_child      : managed
                        Axis2           : managed    , referenced as child_circle_parent_parent
                            Point2      : observed   , referenced as child_circle_parent
                                Circle2 : observed   , referenced as child_circle







    """

    def __init__(self, scene, child_circle, parent_circle, name, inside=True):
        """
        circle1 becomes the nodeB
        circle2 becomes the nodeA

        (attach circle 1 to circle 2)
        Args:
            scene:
            vfAxis:
            parent_circle:
            child_circle:
        """

        if parent_circle.parent.parent is None:
            raise ValueError(
                "The slaved pin is not located on an axis. Can not create the connection because there is no axis to nodeB"
            )

        super().__init__(scene)
        self.name = name

        name_prefix = self.name + vfc.MANAGED_NODE_IDENTIFIER

        self._parent_circle = parent_circle
        self._parent_circle_parent = parent_circle.parent  # point

        self._child_circle = child_circle
        self._child_circle_parent = child_circle.parent  # point
        self._child_circle_parent_parent = child_circle.parent.parent  # axis

        self._flipped = False
        self._inside_connection = inside

        self._axis_on_parent = self._scene.new_axis(
            scene.available_name_like(name_prefix + "_axis_on_parent")
        )
        """Axis on the nodeA axis at the location of the center of hole or pin"""

        self._pin_hole_connection = self._scene.new_axis(
            scene.available_name_like(name_prefix + "_pin_hole_connection")
        )
        """axis between the center of the hole and the center of the pin. Free to rotate about the center of the hole as well as the pin"""

        self._axis_on_child = self._scene.new_axis(
            scene.available_name_like(name_prefix + "_axis_on_child")
        )
        """axis to which the slaved body is connected. Either the center of the hole or the center of the pin """

        self._connection_axial_rotation = self._scene.new_axis(
            scene.available_name_like(name_prefix + "_connection_axial_rotation")
        )

        # prohibit changes to nodes that were used in the creation of this connection
        for node in self.managed_nodes():
            node.manager = self

        # observe circles and their points
        self._parent_circle.observers.append(self)
        self._parent_circle_parent.observers.append(self)

        self._child_circle.observers.append(self)
        self._child_circle_parent.observers.append(self)

        self._child_circle_parent_parent._parent_for_code_export = None

        self._update_connection()

    def on_observed_node_changed(self, changed_node):
        self._update_connection()

    @staticmethod
    def _assert_parent_child_possible(parent, child):
        if parent.parent.parent == child.parent.parent:
            raise ValueError(
                f"A GeometricContact can not be created between two circles on the same axis or body. Both circles are located on {parent.parent.parent}"
            )

    @property
    def child(self):
        """The Circle that is connected to the GeometricContact [Node]

        See Also: parent
        """
        return self._child_circle

    @child.setter
    def child(self, value):
        new_child = self._scene._node_from_node_or_str(value)
        if not isinstance(new_child, Circle):
            raise ValueError(
                f"Child of a geometric contact should be a Circle, but {new_child.name} is a {type(new_child)}"
            )

        if new_child.parent.parent is None:
            raise ValueError(
                f"Child circle {new_child.name} is not located on an axis or body and can thus not be used as child"
            )

        self._assert_parent_child_possible(self.parent, new_child)

        store = self._scene.current_manager
        self._scene.current_manager = self

        # release old child
        self._child_circle.observers.remove(self)
        self._child_circle_parent.observers.remove(self)

        # release the slaved axis system
        self._child_circle_parent_parent._parent_for_code_export = True
        self._child_circle_parent_parent.manager = None

        # set new parent
        self._child_circle = new_child
        self._child_circle_parent = new_child.parent
        self._child_circle_parent_parent = new_child.parent.parent

        # and observe
        self._child_circle.observers.append(self)
        self._child_circle_parent.observers.append(self)

        # and manage
        self._child_circle_parent_parent._parent_for_code_export = None
        self._child_circle_parent_parent.manager = self

        self._scene.current_manager = store

        self._update_connection()

    @property
    def parent(self):
        """The Circle that the GeometricConnection is connected to [Node]

        See Also: child
        """
        return self._parent_circle

    @parent.setter
    @node_setter_manageable
    @node_setter_observable
    def parent(self, var):
        if var is None:
            raise ValueError(
                "Parent of a geometric contact should be a Circle, not None"
            )

        new_parent = self._scene._node_from_node_or_str(var)
        if not isinstance(new_parent, Circle):
            raise ValueError(
                f"Parent of a geometric contact should be a Circle, but {new_parent.name} is a {type(new_parent)}"
            )

        self._assert_parent_child_possible(new_parent, self.child)

        # release old parent
        self._parent_circle.observers.remove(self)
        self._parent_circle_parent.observers.remove(self)

        # set new parent
        self._parent_circle = new_parent
        self._parent_circle_parent = new_parent.parent

        # and observe
        self._parent_circle.observers.append(self)
        self._parent_circle_parent.observers.append(self)

        self._update_connection()

    def change_parent_to(self, new_parent):
        self.parent = new_parent

    def delete(self):

        # release management
        for node in self.managed_nodes():
            node._manager = None

        self._child_circle_parent_parent.change_parent_to(None)

        self._scene.delete(self._axis_on_child)
        self._scene.delete(self._pin_hole_connection)
        self._scene.delete(self._axis_on_parent)

        # release observers
        self._parent_circle.observers.remove(self)
        self._parent_circle_parent.observers.remove(self)

        self._child_circle.observers.remove(self)
        self._child_circle_parent.observers.remove(self)

    def _update_connection(self):

        remember = self._scene.current_manager
        self._scene.current_manager = self  # claim management

        # get current properties

        c_swivel = self.swivel
        c_swivel_fixed = self.swivel_fixed
        c_rotation_on_parent = self.rotation_on_parent
        c_fixed_to_parent = self.fixed_to_parent
        c_child_rotation = self.child_rotation
        c_child_fixed = self.child_fixed

        pin1 = self._child_circle  # nodeB
        pin2 = self._parent_circle  # nodeA

        if pin1.parent.parent is None:
            raise ValueError(
                "The slaved pin is not located on an axis. Can not create the connection because there is no axis to nodeB"
            )

        # --------- prepare hole

        if pin2.parent.parent is not None:
            self._axis_on_parent.parent = pin2.parent.parent
        self._axis_on_parent.position = pin2.parent.position
        self._axis_on_parent.fixed = (True, True, True, True, True, True)

        self._axis_on_parent.rotation = rotation_from_y_axis_direction(pin2.axis)

        # Position connection axis at the center of the nodeA axis (pin2)
        # and allow it to rotate about the pin
        self._pin_hole_connection.position = (0, 0, 0)
        self._pin_hole_connection.parent = self._axis_on_parent
        self._pin_hole_connection.fixed = (True, True, True, True, False, True)

        self._connection_axial_rotation.parent = self._pin_hole_connection
        self._connection_axial_rotation.position = (0, 0, 0)

        # Position the connection pin (self) on the target pin and
        # place the parent of the parent of the pin (the axis) on the connection axis
        # and fix it
        slaved_axis = pin1.parent.parent

        slaved_axis.parent = self._axis_on_child
        slaved_axis.position = -np.array(pin1.parent.position)
        slaved_axis.rotation = rotation_from_y_axis_direction(-1 * np.array(pin1.axis))

        slaved_axis.fixed = True

        self._axis_on_child.parent = self._connection_axial_rotation
        self._axis_on_child.rotation = (0, 0, 0)
        self._axis_on_child.fixed = (True, True, True, True, False, True)

        if self._inside_connection:

            # Place the pin in the hole
            self._connection_axial_rotation.rotation = (0, 0, 0)
            self._axis_on_child.position = (pin2.radius - pin1.radius, 0, 0)

        else:

            # pin-pin connection
            self._axis_on_child.position = (pin1.radius + pin2.radius, 0, 0)
            self._connection_axial_rotation.rotation = (90, 0, 0)

        # restore settings
        self.swivel = c_swivel
        self.swivel_fixed = c_swivel_fixed
        self.rotation_on_parent = c_rotation_on_parent
        self.fixed_to_parent = c_fixed_to_parent
        self.child_rotation = c_child_rotation
        self.child_fixed = c_child_fixed

        self._scene.current_manager = remember

    def set_pin_pin_connection(self):
        """Sets the connection to be of type pin-pin"""

        self._inside_connection = False
        if self.swivel == 0:
            self.swivel = 90
        elif self.swivel == 180:
            self.swivel = 270

        self._update_connection()

    def set_pin_in_hole_connection(self):
        """Sets the connection to be of type pin-in-hole

        The axes of the two sheaves are aligned by rotating the slaved body
        The axes of the two sheaves are placed at a distance hole_dia - pin_dia apart, perpendicular to the axis direction
        An axes is created at the centers of the two sheaves
        These axes are connected with a shore axis which is allowed to rotate relative to the nodeA axis
        the nodeB axis is fixed to this rotating axis
        """
        self._inside_connection = True

        if self.swivel == 90:
            self.swivel = 0
        elif self.swivel == 270:
            self.swivel = 180

        self._update_connection()

    def managed_nodes(self):
        """Returns a list of managed nodes"""

        return [
            self._child_circle_parent_parent,
            self._axis_on_parent,
            self._axis_on_child,
            self._pin_hole_connection,
            self._connection_axial_rotation,
        ]

    def depends_on(self):
        return [self._parent_circle, self._child_circle]

    def creates(self, node: Node):
        return node in [
            self._axis_on_parent,
            self._axis_on_child,
            self._pin_hole_connection,
            self._connection_axial_rotation,
        ]

    def flip(self):
        """Changes the swivel angle by 180 degrees"""
        self.swivel = np.mod(self.swivel + 180, 360)

    def change_side(self):
        self.rotation_on_parent = np.mod(self.rotation_on_parent + 180, 360)
        self.child_rotation = np.mod(self.child_rotation + 180, 360)

    @property
    def swivel(self):
        """Swivel angle between parent and child objects [degrees]"""
        return self._connection_axial_rotation.rotation[0]

    @swivel.setter
    @node_setter_manageable
    @node_setter_observable
    def swivel(self, value):
        remember = self._scene.current_manager  # claim management
        self._scene.current_manager = self
        self._connection_axial_rotation.rx = value
        self._scene.current_manager = remember  # restore old manager

    @property
    def swivel_fixed(self):
        """Allow parent and child to swivel relative to eachother [boolean]"""
        return self._connection_axial_rotation.fixed[3]

    @swivel_fixed.setter
    @node_setter_manageable
    @node_setter_observable
    def swivel_fixed(self, value):
        remember = self._scene.current_manager  # claim management
        self._scene.current_manager = self
        self._connection_axial_rotation.fixed = [True, True, True, value, True, True]
        self._scene.current_manager = remember  # restore old manager

    @property
    def rotation_on_parent(self):
        """Angle between the line connecting the centers of the circles and the axis system of the parent node [degrees]"""
        return self._pin_hole_connection.ry

    @rotation_on_parent.setter
    @node_setter_manageable
    @node_setter_observable
    def rotation_on_parent(self, value):
        remember = self._scene.current_manager  # claim management
        self._scene.current_manager = self
        self._pin_hole_connection.ry = value
        self._scene.current_manager = remember  # restore old manager

    @property
    def fixed_to_parent(self):
        """Allow rotation around parent [boolean]

        see also: rotation_on_parent"""
        return self._pin_hole_connection.fixed[4]

    @fixed_to_parent.setter
    @node_setter_manageable
    @node_setter_observable
    def fixed_to_parent(self, value):
        remember = self._scene.current_manager  # claim management
        self._scene.current_manager = self
        self._pin_hole_connection.fixed = [True, True, True, True, value, True]
        self._scene.current_manager = remember  # restore old manager

    @property
    def child_rotation(self):
        """Angle between the line connecting the centers of the circles and the axis system of the child node [degrees]"""
        return self._axis_on_child.ry

    @child_rotation.setter
    @node_setter_manageable
    @node_setter_observable
    def child_rotation(self, value):
        remember = self._scene.current_manager  # claim management
        self._scene.current_manager = self
        self._axis_on_child.ry = value
        self._scene.current_manager = remember  # restore old manager

    @property
    def child_fixed(self):
        """Allow rotation of child relative to connection, see also: child_rotation [boolean]"""
        return self._axis_on_child.fixed[4]

    @child_fixed.setter
    @node_setter_manageable
    @node_setter_observable
    def child_fixed(self, value):
        remember = self._scene.current_manager  # claim management
        self._scene.current_manager = self
        self._axis_on_child.fixed = [True, True, True, True, value, True]
        self._scene.current_manager = remember  # restore old manager

    @property
    def inside(self):
        """Type of connection: True means child circle is inside parent circle, False means the child circle is outside but the circumferences contact [boolean]"""
        return self._inside_connection

    @inside.setter
    @node_setter_manageable
    @node_setter_observable
    def inside(self, value):
        if value == self._inside_connection:
            return

        if value:
            self.set_pin_in_hole_connection()
        else:
            self.set_pin_pin_connection()

    def give_python_code(self):

        old_manger = self._scene.current_manager
        self._scene.current_manager = self

        code = []

        # code.append('#  create the connection')
        code.append(f"s.new_geometriccontact(name = '{self.name}',")
        code.append(f"                       child = '{self._child_circle.name}',")
        code.append(f"                       parent = '{self._parent_circle.name}',")
        code.append(f"                       inside={self.inside},")

        if self.inside and self.swivel == 0:
            pass  # default for inside
        else:
            if not self.inside and self.swivel == 90:
                pass  # default for outside
            else:
                code.append(f"                       swivel={self.swivel},")

        if not self.swivel_fixed:
            code.append(f"                       swivel_fixed={self.swivel_fixed},")
        if self.fixed_to_parent:
            code.append(
                f"                       parent_rotation={self.rotation_on_parent},"
            )
            code.append(
                f"                       fixed_to_parent={self.fixed_to_parent},"
            )
        else:
            code.append(
                f"                       fixed_to_parent=solved({self.fixed_to_parent}),"
            )
        if self.child_fixed:
            code.append(f"                       child_fixed={self.child_fixed},")
            code.append(f"                       child_rotation={self.child_rotation},")
        else:
            code.append(
                f"                       child_rotation=solved({self.child_rotation}),"
            )

        code = [
            *code[:-1],
            code[-1][:-1] + " )",
        ]  # remove the , from the last entry [should be a quicker way to do this]

        self._scene.current_manager = old_manger

        return "\n".join(code)

Ancestors

Instance variables

var child

The Circle that is connected to the GeometricContact [Node]

See Also: parent

Expand source code
@property
def child(self):
    """The Circle that is connected to the GeometricContact [Node]

    See Also: parent
    """
    return self._child_circle
var child_fixed

Allow rotation of child relative to connection, see also: child_rotation [boolean]

Expand source code
@property
def child_fixed(self):
    """Allow rotation of child relative to connection, see also: child_rotation [boolean]"""
    return self._axis_on_child.fixed[4]
var child_rotation

Angle between the line connecting the centers of the circles and the axis system of the child node [degrees]

Expand source code
@property
def child_rotation(self):
    """Angle between the line connecting the centers of the circles and the axis system of the child node [degrees]"""
    return self._axis_on_child.ry
var fixed_to_parent

Allow rotation around parent [boolean]

see also: rotation_on_parent

Expand source code
@property
def fixed_to_parent(self):
    """Allow rotation around parent [boolean]

    see also: rotation_on_parent"""
    return self._pin_hole_connection.fixed[4]
var inside

Type of connection: True means child circle is inside parent circle, False means the child circle is outside but the circumferences contact [boolean]

Expand source code
@property
def inside(self):
    """Type of connection: True means child circle is inside parent circle, False means the child circle is outside but the circumferences contact [boolean]"""
    return self._inside_connection
var parent

The Circle that the GeometricConnection is connected to [Node]

See Also: child

Expand source code
@property
def parent(self):
    """The Circle that the GeometricConnection is connected to [Node]

    See Also: child
    """
    return self._parent_circle
var rotation_on_parent

Angle between the line connecting the centers of the circles and the axis system of the parent node [degrees]

Expand source code
@property
def rotation_on_parent(self):
    """Angle between the line connecting the centers of the circles and the axis system of the parent node [degrees]"""
    return self._pin_hole_connection.ry
var swivel

Swivel angle between parent and child objects [degrees]

Expand source code
@property
def swivel(self):
    """Swivel angle between parent and child objects [degrees]"""
    return self._connection_axial_rotation.rotation[0]
var swivel_fixed

Allow parent and child to swivel relative to eachother [boolean]

Expand source code
@property
def swivel_fixed(self):
    """Allow parent and child to swivel relative to eachother [boolean]"""
    return self._connection_axial_rotation.fixed[3]

Methods

def change_parent_to(self, new_parent)
Expand source code
def change_parent_to(self, new_parent):
    self.parent = new_parent
def change_side(self)
Expand source code
def change_side(self):
    self.rotation_on_parent = np.mod(self.rotation_on_parent + 180, 360)
    self.child_rotation = np.mod(self.child_rotation + 180, 360)
def flip(self)

Changes the swivel angle by 180 degrees

Expand source code
def flip(self):
    """Changes the swivel angle by 180 degrees"""
    self.swivel = np.mod(self.swivel + 180, 360)
def managed_nodes(self)

Returns a list of managed nodes

Expand source code
def managed_nodes(self):
    """Returns a list of managed nodes"""

    return [
        self._child_circle_parent_parent,
        self._axis_on_parent,
        self._axis_on_child,
        self._pin_hole_connection,
        self._connection_axial_rotation,
    ]
def on_observed_node_changed(self, changed_node)
Expand source code
def on_observed_node_changed(self, changed_node):
    self._update_connection()
def set_pin_in_hole_connection(self)

Sets the connection to be of type pin-in-hole

The axes of the two sheaves are aligned by rotating the slaved body The axes of the two sheaves are placed at a distance hole_dia - pin_dia apart, perpendicular to the axis direction An axes is created at the centers of the two sheaves These axes are connected with a shore axis which is allowed to rotate relative to the nodeA axis the nodeB axis is fixed to this rotating axis

Expand source code
def set_pin_in_hole_connection(self):
    """Sets the connection to be of type pin-in-hole

    The axes of the two sheaves are aligned by rotating the slaved body
    The axes of the two sheaves are placed at a distance hole_dia - pin_dia apart, perpendicular to the axis direction
    An axes is created at the centers of the two sheaves
    These axes are connected with a shore axis which is allowed to rotate relative to the nodeA axis
    the nodeB axis is fixed to this rotating axis
    """
    self._inside_connection = True

    if self.swivel == 90:
        self.swivel = 0
    elif self.swivel == 270:
        self.swivel = 180

    self._update_connection()
def set_pin_pin_connection(self)

Sets the connection to be of type pin-pin

Expand source code
def set_pin_pin_connection(self):
    """Sets the connection to be of type pin-pin"""

    self._inside_connection = False
    if self.swivel == 0:
        self.swivel = 90
    elif self.swivel == 180:
        self.swivel = 270

    self._update_connection()

Inherited members

class HydSpring (scene, vfNode)

A HydSpring models a linearized hydrostatic spring.

The cob (center of buoyancy) is defined in the parent axis system. All other properties are defined relative to the cob.

Expand source code
class HydSpring(NodeWithParent):
    """A HydSpring models a linearized hydrostatic spring.

    The cob (center of buoyancy) is defined in the parent axis system.
    All other properties are defined relative to the cob.

    """

    @property
    def cob(self):
        """Center of buoyancy in parent axis system (m,m,m)"""
        return self._vfNode.position

    @cob.setter
    @node_setter_manageable
    @node_setter_observable
    def cob(self, val):

        assert3f(val)
        self._vfNode.position = val

    @property
    def BMT(self):
        """Vertical distance between cob and metacenter for roll [m]"""
        return self._vfNode.BMT

    @BMT.setter
    @node_setter_manageable
    @node_setter_observable
    def BMT(self, val):

        self._vfNode.BMT = val

    @property
    def BML(self):
        """Vertical distance between cob and metacenter for pitch [m]"""
        return self._vfNode.BML

    @BML.setter
    @node_setter_manageable
    @node_setter_observable
    def BML(self, val):

        self._vfNode.BML = val

    @property
    def COFX(self):
        """Horizontal x-position Center of Floatation (center of waterplane area), relative to cob [m]"""
        return self._vfNode.COFX

    @COFX.setter
    @node_setter_manageable
    @node_setter_observable
    def COFX(self, val):

        self._vfNode.COFX = val

    @property
    def COFY(self):
        """Horizontal y-position Center of Floatation (center of waterplane area), relative to cob [m]"""
        return self._vfNode.COFY

    @COFY.setter
    @node_setter_manageable
    @node_setter_observable
    def COFY(self, val):

        self._vfNode.COFY = val

    @property
    def kHeave(self):
        """Heave stiffness [kN/m]"""
        return self._vfNode.kHeave

    @kHeave.setter
    @node_setter_manageable
    @node_setter_observable
    def kHeave(self, val):

        self._vfNode.kHeave = val

    @property
    def waterline(self):
        """Waterline-elevation relative to cob for un-stretched heave-spring. Positive if cob is below the waterline (which is where is normally is) [m]"""
        return self._vfNode.waterline

    @waterline.setter
    @node_setter_manageable
    @node_setter_observable
    def waterline(self, val):

        self._vfNode.waterline = val

    @property
    def displacement_kN(self):
        """Displacement when waterline is at waterline-elevation [kN]"""
        return self._vfNode.displacement_kN

    @displacement_kN.setter
    @node_setter_manageable
    @node_setter_observable
    def displacement_kN(self, val):

        self._vfNode.displacement_kN = val

    def give_python_code(self):
        code = "# code for {}".format(self.name)

        # new_force(self, name, parent=None, force=None, moment=None):

        code += "\ns.new_hydspring(name='{}',".format(self.name)
        code += "\n            parent='{}',".format(self.parent_for_export.name)
        code += "\n            cob=({}, {}, {}),".format(*self.cob)
        code += "\n            BMT={},".format(self.BMT)
        code += "\n            BML={},".format(self.BML)
        code += "\n            COFX={},".format(self.COFX)
        code += "\n            COFY={},".format(self.COFY)
        code += "\n            kHeave={},".format(self.kHeave)
        code += "\n            waterline={},".format(self.waterline)
        code += "\n            displacement_kN={} )".format(self.displacement_kN)

        return code

Ancestors

Instance variables

var BML

Vertical distance between cob and metacenter for pitch [m]

Expand source code
@property
def BML(self):
    """Vertical distance between cob and metacenter for pitch [m]"""
    return self._vfNode.BML
var BMT

Vertical distance between cob and metacenter for roll [m]

Expand source code
@property
def BMT(self):
    """Vertical distance between cob and metacenter for roll [m]"""
    return self._vfNode.BMT
var COFX

Horizontal x-position Center of Floatation (center of waterplane area), relative to cob [m]

Expand source code
@property
def COFX(self):
    """Horizontal x-position Center of Floatation (center of waterplane area), relative to cob [m]"""
    return self._vfNode.COFX
var COFY

Horizontal y-position Center of Floatation (center of waterplane area), relative to cob [m]

Expand source code
@property
def COFY(self):
    """Horizontal y-position Center of Floatation (center of waterplane area), relative to cob [m]"""
    return self._vfNode.COFY
var cob

Center of buoyancy in parent axis system (m,m,m)

Expand source code
@property
def cob(self):
    """Center of buoyancy in parent axis system (m,m,m)"""
    return self._vfNode.position
var displacement_kN

Displacement when waterline is at waterline-elevation [kN]

Expand source code
@property
def displacement_kN(self):
    """Displacement when waterline is at waterline-elevation [kN]"""
    return self._vfNode.displacement_kN
var kHeave

Heave stiffness [kN/m]

Expand source code
@property
def kHeave(self):
    """Heave stiffness [kN/m]"""
    return self._vfNode.kHeave
var waterline

Waterline-elevation relative to cob for un-stretched heave-spring. Positive if cob is below the waterline (which is where is normally is) [m]

Expand source code
@property
def waterline(self):
    """Waterline-elevation relative to cob for un-stretched heave-spring. Positive if cob is below the waterline (which is where is normally is) [m]"""
    return self._vfNode.waterline

Inherited members

class LC6d (scene, node)

A LC6d models a Linear Connector with 6 dofs.

It connects two Axis elements with six linear springs.

The first axis system is called "main", the second is called "secondary". The difference is that the "main" axis system is used for the definition of the stiffness values.

The translational-springs are easy. The rotational springs may not be as intuitive. They are defined as:

  • rotation_x = arc-tan ( uy[0] / uy[1] )
  • rotation_y = arc-tan ( -ux[0] / ux[2] )
  • rotation_z = arc-tan ( ux[0] / ux [1] )

which works fine for small rotations and rotations about only a single axis.

Tip: It is better to use use the "fixed" property of axis systems to create joints.

Expand source code
class LC6d(CoreConnectedNode):
    """A LC6d models a Linear Connector with 6 dofs.

    It connects two Axis elements with six linear springs.

    The first axis system is called "main", the second is called "secondary". The difference is that
    the "main" axis system is used for the definition of the stiffness values.

    The translational-springs are easy. The rotational springs may not be as intuitive. They are defined as:

      - rotation_x = arc-tan ( uy[0] / uy[1] )
      - rotation_y = arc-tan ( -ux[0] / ux[2] )
      - rotation_z = arc-tan ( ux[0] / ux [1] )

    which works fine for small rotations and rotations about only a single axis.

    Tip:
    It is better to use use the "fixed" property of axis systems to create joints.

    """

    def __init__(self, scene, node):
        super().__init__(scene, node)
        self._main = None
        self._secondary = None

    def depends_on(self):
        return [self._main, self._secondary]

    @property
    def stiffness(self):
        """Stiffness of the connector: kx, ky, kz, krx, kry, krz in [kN/m and kNm/rad] (axis system of the main axis)"""
        return self._vfNode.stiffness

    @stiffness.setter
    @node_setter_manageable
    @node_setter_observable
    def stiffness(self, val):

        self._vfNode.stiffness = val

    @property
    def main(self):
        """Main axis system. This axis system dictates the axis system that the stiffness is expressed in"""
        return self._main

    @main.setter
    @node_setter_manageable
    @node_setter_observable
    def main(self, val):

        val = self._scene._node_from_node_or_str(val)
        if not isinstance(val, Axis):
            raise TypeError("Provided nodeA should be a Axis")

        self._main = val
        self._vfNode.master = val._vfNode

    @property
    def secondary(self):
        """Secondary (connected) axis system"""
        return self._secondary

    @secondary.setter
    @node_setter_manageable
    @node_setter_observable
    def secondary(self, val):

        val = self._scene._node_from_node_or_str(val)
        if not isinstance(val, Axis):
            raise TypeError("Provided nodeA should be a Axis")

        self._secondary = val
        self._vfNode.slave = val._vfNode

    @property
    def fgx(self):
        """Force on main in global coordinate frame [kN]"""
        return self._vfNode.global_force[0]

    @property
    def fgy(self):
        """Force on main in global coordinate frame [kN]"""
        return self._vfNode.global_force[1]

    @property
    def fgz(self):
        """Force on main in global coordinate frame [kN]"""
        return self._vfNode.global_force[2]

    @property
    def force_global(self):
        """Force on main in global coordinate frame [kN]"""
        return self._vfNode.global_force

    @property
    def mgx(self):
        """Moment on main in global coordinate frame [kNm]"""
        return self._vfNode.global_moment[0]

    @property
    def mgy(self):
        """Moment on main in global coordinate frame [kNm]"""
        return self._vfNode.global_moment[1]

    @property
    def mgz(self):
        """Moment on main in global coordinate frame [kNm]"""
        return self._vfNode.global_moment[2]

    @property
    def moment_global(self):
        """Moment on main in global coordinate frame [kNm]"""
        return self._vfNode.global_moment

    def give_python_code(self):
        code = "# code for {}".format(self.name)

        code += "\ns.new_linear_connector_6d(name='{}',".format(self.name)
        code += "\n            main='{}',".format(self.main.name)
        code += "\n            secondary='{}',".format(self.secondary.name)
        code += "\n            stiffness=({}, {}, {}, ".format(*self.stiffness[:3])
        code += "\n                       {}, {}, {}) )".format(*self.stiffness[3:])

        return code

Ancestors

Instance variables

var fgx

Force on main in global coordinate frame [kN]

Expand source code
@property
def fgx(self):
    """Force on main in global coordinate frame [kN]"""
    return self._vfNode.global_force[0]
var fgy

Force on main in global coordinate frame [kN]

Expand source code
@property
def fgy(self):
    """Force on main in global coordinate frame [kN]"""
    return self._vfNode.global_force[1]
var fgz

Force on main in global coordinate frame [kN]

Expand source code
@property
def fgz(self):
    """Force on main in global coordinate frame [kN]"""
    return self._vfNode.global_force[2]
var force_global

Force on main in global coordinate frame [kN]

Expand source code
@property
def force_global(self):
    """Force on main in global coordinate frame [kN]"""
    return self._vfNode.global_force
var main

Main axis system. This axis system dictates the axis system that the stiffness is expressed in

Expand source code
@property
def main(self):
    """Main axis system. This axis system dictates the axis system that the stiffness is expressed in"""
    return self._main
var mgx

Moment on main in global coordinate frame [kNm]

Expand source code
@property
def mgx(self):
    """Moment on main in global coordinate frame [kNm]"""
    return self._vfNode.global_moment[0]
var mgy

Moment on main in global coordinate frame [kNm]

Expand source code
@property
def mgy(self):
    """Moment on main in global coordinate frame [kNm]"""
    return self._vfNode.global_moment[1]
var mgz

Moment on main in global coordinate frame [kNm]

Expand source code
@property
def mgz(self):
    """Moment on main in global coordinate frame [kNm]"""
    return self._vfNode.global_moment[2]
var moment_global

Moment on main in global coordinate frame [kNm]

Expand source code
@property
def moment_global(self):
    """Moment on main in global coordinate frame [kNm]"""
    return self._vfNode.global_moment
var secondary

Secondary (connected) axis system

Expand source code
@property
def secondary(self):
    """Secondary (connected) axis system"""
    return self._secondary
var stiffness

Stiffness of the connector: kx, ky, kz, krx, kry, krz in [kN/m and kNm/rad] (axis system of the main axis)

Expand source code
@property
def stiffness(self):
    """Stiffness of the connector: kx, ky, kz, krx, kry, krz in [kN/m and kNm/rad] (axis system of the main axis)"""
    return self._vfNode.stiffness

Inherited members

class LoadShearMomentDiagram (datasource)

Args

datasource
pyo3d.MomentDiagram object
Expand source code
class LoadShearMomentDiagram:
    def __init__(self, datasource):
        """

        Args:
            datasource: pyo3d.MomentDiagram object
        """

        self.datasource = datasource

    def give_shear_and_moment(self, grid_n=100):
        """Returns (position, shear, moment)"""
        x = self.datasource.grid(grid_n)
        return x, self.datasource.Vz, self.datasource.My

    def plot_simple(self, **kwargs):
        """Plots the bending moment and shear in a single yy-plot.
        Creates a new figure

        any keyword arguments are passed to plt.figure(), so for example dpi=150 will increase the dpi

        Returns: figure
        """
        x, Vz, My = self.give_shear_and_moment()
        import matplotlib.pyplot as plt
        plt.rcParams.update({"font.family": "sans-serif"})
        plt.rcParams.update({"font.sans-serif": "consolas"})
        plt.rcParams.update({"font.size": 10})

        fig, ax1 = plt.subplots(1,1,**kwargs)
        ax2 = ax1.twinx()

        ax1.plot(x, My, "g", lw=1, label="Bending Moment")
        ax2.plot(x, Vz, "b", lw=1, label="Shear Force")

        from DAVE.gui.helpers.align_zeros_of_yyplots import align_y0_axis

        align_y0_axis(ax1, ax2)

        ax1.set_xlabel("Position [m]")
        ax1.set_ylabel("Bending Moment [kNm]")
        ax2.set_ylabel("Shear Force [kN]")

        ax1.tick_params(axis="y", colors="g")
        ax2.tick_params(axis="y", colors="b")

        # fig.legend()  - obvious from the axis

        ext = 0.1 * (np.max(x) - np.min(x))
        xx = [np.min(x) - ext, np.max(x) + ext]
        ax1.plot(xx, [0, 0], c=[0.5, 0.5, 0.5], lw=1, linestyle=":")
        ax1.set_xlim(xx)

        return fig

    def plot(self, grid_n=100, merge_adjacent_loads=True, filename=None):
        m = self.datasource  # alias

        x = m.grid(grid_n)
        linewidth = 1

        n = m.nLoads

        import matplotlib.pyplot as plt

        #
        plt.rcParams.update({"font.family": "sans-serif"})
        plt.rcParams.update({"font.sans-serif": "consolas"})
        plt.rcParams.update({"font.size": 6})

        fig, (ax0, ax1, ax2) = plt.subplots(3, 1, figsize=(8.27, 11.69), dpi=100)
        textsize = 6

        # get loads

        loads = [m.load(i) for i in range(n)]

        texts = []  # for label placement
        texts_second = []  # for label placement

        # merge loads with same source and matching endpoints

        if merge_adjacent_loads:

            to_be_plotted = [loads[0]]

            for load in loads[1:]:
                name = load[2]

                # if the previous load is a continuous load from the same source
                # and the current load is also a continuous load
                # then merge the two.
                prev_load = to_be_plotted[-1]

                if len(prev_load[0]) != 2:  # not a point-load
                    if len(load[0]) != 2:  # not a point-load
                        if prev_load[2] == load[2]:  # same name

                            # merge the two
                            # remove the last (zero) entry of the previous lds
                            # as well as the first entry of these

                            # smoothed
                            xx = [*prev_load[0][:-1], *load[0][2:]]
                            yy = [
                                *prev_load[1][:-2],
                                0.5 * (prev_load[1][-2] + load[1][1]),
                                *load[1][2:],
                            ]

                            to_be_plotted[-1] = (xx, yy, load[2])

                            continue
                # else
                if np.max(np.abs(load[1])) > 1e-6:
                    to_be_plotted.append(load)

        else:
            to_be_plotted = loads

        #
        from matplotlib import cm

        colors = cm.get_cmap("hsv", lut=len(to_be_plotted))

        from matplotlib.patches import Polygon

        ax0_second = ax0.twinx()

        for icol, ld in enumerate(to_be_plotted):

            xx = ld[0]
            yy = ld[1]
            name = ld[2]

            if np.max(np.abs(yy)) < 1e-6:
                continue

            is_concentrated = len(xx) == 2

            # determine the name, default to Force / q-load if no name is present
            if name == "":
                if is_concentrated:
                    name = "Force "
                else:
                    name = "q-load "

            col = [0.8 * c for c in colors(icol)]
            col[3] = 1.0  # alpha

            if is_concentrated:  # concentrated loads on left axis
                lbl = f" {name} {ld[1][1]:.2f}"
                texts.append(
                    ax0.text(
                        xx[0], yy[1], lbl, fontsize=textsize, horizontalalignment="left"
                    )
                )
                ax0.plot(xx, yy, label=lbl, color=col, linewidth=linewidth)
                if yy[1] > 0:
                    ax0.plot(xx[1], yy[1], marker="^", color=col, linewidth=linewidth)
                else:
                    ax0.plot(xx[1], yy[1], marker="v", color=col, linewidth=linewidth)

            else:  # distributed loads on right axis
                lbl = f"{name}"  # {yy[1]:.2f} kN/m at {xx[0]:.3f}m .. {yy[-2]:.2f} kN/m at {xx[-1]:.3f}m"

                vertices = [(xx[i], yy[i]) for i in range(len(xx))]

                ax0_second.add_patch(
                    Polygon(vertices, facecolor=[col[0], col[1], col[2], 0.2])
                )
                ax0_second.plot(xx, yy, label=lbl, color=col, linewidth=linewidth)

                lx = np.mean(xx)
                ly = np.interp(lx, xx, yy)

                texts_second.append(
                    ax0_second.text(
                        lx,
                        ly,
                        lbl,
                        color=[0, 0, 0],
                        horizontalalignment="center",
                        fontsize=textsize,
                    )
                )

        ax0.grid()
        ax0.set_title("Loads")
        ax0.set_ylabel("Load [kN]")
        ax0_second.set_ylabel("Load [kN/m]")

        # plot moments
        # each concentrated load may have a moment as well
        for i in range(m.nLoads):
            mom = m.moment(i)
            if np.linalg.norm(mom) > 1e-6:
                load = m.load(i)
                xx = load[0][0]
                lbl = f"{load[2]}, m = {mom[1]:.2f} kNm"
                ax0.plot(xx, 0, marker="x", label=lbl, color=(0, 0, 0, 1))
                texts.append(
                    ax0.text(
                        xx, 0, lbl, horizontalalignment="center", fontsize=textsize
                    )
                )

        fig.legend(loc="upper right")

        # add a zero-line
        xx = [np.min(x), np.max(x)]
        ax0.plot(xx, (0, 0), "k-")

        from DAVE.gui.helpers.align_zeros_of_yyplots import align_y0_axis

        align_y0_axis(ax0, ax0_second)

        from DAVE.reporting.utils.TextAvoidOverlap import minimizeTextOverlap

        minimizeTextOverlap(
            texts_second,
            fig=fig,
            ax=ax0_second,
            vertical_only=True,
            optimize_initial_positions=False,
            annotate=False,
        )
        minimizeTextOverlap(
            texts,
            fig=fig,
            ax=ax0,
            vertical_only=True,
            optimize_initial_positions=False,
            annotate=False,
        )

        ax0.spines["top"].set_visible(False)
        ax0.spines["bottom"].set_visible(False)

        ax0_second.spines["top"].set_visible(False)
        ax0_second.spines["bottom"].set_visible(False)

        ax1.plot(x, m.Vz, "k-", linewidth=linewidth)

        i = np.argmax(np.abs(m.Vz))
        ax1.plot(x[i], m.Vz[i], "b*")
        ax1.text(x[i], m.Vz[i], f"{m.Vz[i]:.2f}")

        ax1.grid()
        ax1.set_title("Shear")
        ax1.set_ylabel("[kN]")

        ax2.plot(x, m.My, "k-", linewidth=linewidth)
        i = np.argmax(np.abs(m.My))
        ax2.plot(x[i], m.My[i], "b*")
        ax2.text(x[i], m.My[i], f"{m.My[i]:.2f}")

        ax2.grid()
        ax2.set_title("Moment")
        ax2.set_ylabel("[kN*m]")

        if filename is None:
            plt.show()
        else:
            fig.savefig(filename)

Methods

def give_shear_and_moment(self, grid_n=100)

Returns (position, shear, moment)

Expand source code
def give_shear_and_moment(self, grid_n=100):
    """Returns (position, shear, moment)"""
    x = self.datasource.grid(grid_n)
    return x, self.datasource.Vz, self.datasource.My
def plot(self, grid_n=100, merge_adjacent_loads=True, filename=None)
Expand source code
def plot(self, grid_n=100, merge_adjacent_loads=True, filename=None):
    m = self.datasource  # alias

    x = m.grid(grid_n)
    linewidth = 1

    n = m.nLoads

    import matplotlib.pyplot as plt

    #
    plt.rcParams.update({"font.family": "sans-serif"})
    plt.rcParams.update({"font.sans-serif": "consolas"})
    plt.rcParams.update({"font.size": 6})

    fig, (ax0, ax1, ax2) = plt.subplots(3, 1, figsize=(8.27, 11.69), dpi=100)
    textsize = 6

    # get loads

    loads = [m.load(i) for i in range(n)]

    texts = []  # for label placement
    texts_second = []  # for label placement

    # merge loads with same source and matching endpoints

    if merge_adjacent_loads:

        to_be_plotted = [loads[0]]

        for load in loads[1:]:
            name = load[2]

            # if the previous load is a continuous load from the same source
            # and the current load is also a continuous load
            # then merge the two.
            prev_load = to_be_plotted[-1]

            if len(prev_load[0]) != 2:  # not a point-load
                if len(load[0]) != 2:  # not a point-load
                    if prev_load[2] == load[2]:  # same name

                        # merge the two
                        # remove the last (zero) entry of the previous lds
                        # as well as the first entry of these

                        # smoothed
                        xx = [*prev_load[0][:-1], *load[0][2:]]
                        yy = [
                            *prev_load[1][:-2],
                            0.5 * (prev_load[1][-2] + load[1][1]),
                            *load[1][2:],
                        ]

                        to_be_plotted[-1] = (xx, yy, load[2])

                        continue
            # else
            if np.max(np.abs(load[1])) > 1e-6:
                to_be_plotted.append(load)

    else:
        to_be_plotted = loads

    #
    from matplotlib import cm

    colors = cm.get_cmap("hsv", lut=len(to_be_plotted))

    from matplotlib.patches import Polygon

    ax0_second = ax0.twinx()

    for icol, ld in enumerate(to_be_plotted):

        xx = ld[0]
        yy = ld[1]
        name = ld[2]

        if np.max(np.abs(yy)) < 1e-6:
            continue

        is_concentrated = len(xx) == 2

        # determine the name, default to Force / q-load if no name is present
        if name == "":
            if is_concentrated:
                name = "Force "
            else:
                name = "q-load "

        col = [0.8 * c for c in colors(icol)]
        col[3] = 1.0  # alpha

        if is_concentrated:  # concentrated loads on left axis
            lbl = f" {name} {ld[1][1]:.2f}"
            texts.append(
                ax0.text(
                    xx[0], yy[1], lbl, fontsize=textsize, horizontalalignment="left"
                )
            )
            ax0.plot(xx, yy, label=lbl, color=col, linewidth=linewidth)
            if yy[1] > 0:
                ax0.plot(xx[1], yy[1], marker="^", color=col, linewidth=linewidth)
            else:
                ax0.plot(xx[1], yy[1], marker="v", color=col, linewidth=linewidth)

        else:  # distributed loads on right axis
            lbl = f"{name}"  # {yy[1]:.2f} kN/m at {xx[0]:.3f}m .. {yy[-2]:.2f} kN/m at {xx[-1]:.3f}m"

            vertices = [(xx[i], yy[i]) for i in range(len(xx))]

            ax0_second.add_patch(
                Polygon(vertices, facecolor=[col[0], col[1], col[2], 0.2])
            )
            ax0_second.plot(xx, yy, label=lbl, color=col, linewidth=linewidth)

            lx = np.mean(xx)
            ly = np.interp(lx, xx, yy)

            texts_second.append(
                ax0_second.text(
                    lx,
                    ly,
                    lbl,
                    color=[0, 0, 0],
                    horizontalalignment="center",
                    fontsize=textsize,
                )
            )

    ax0.grid()
    ax0.set_title("Loads")
    ax0.set_ylabel("Load [kN]")
    ax0_second.set_ylabel("Load [kN/m]")

    # plot moments
    # each concentrated load may have a moment as well
    for i in range(m.nLoads):
        mom = m.moment(i)
        if np.linalg.norm(mom) > 1e-6:
            load = m.load(i)
            xx = load[0][0]
            lbl = f"{load[2]}, m = {mom[1]:.2f} kNm"
            ax0.plot(xx, 0, marker="x", label=lbl, color=(0, 0, 0, 1))
            texts.append(
                ax0.text(
                    xx, 0, lbl, horizontalalignment="center", fontsize=textsize
                )
            )

    fig.legend(loc="upper right")

    # add a zero-line
    xx = [np.min(x), np.max(x)]
    ax0.plot(xx, (0, 0), "k-")

    from DAVE.gui.helpers.align_zeros_of_yyplots import align_y0_axis

    align_y0_axis(ax0, ax0_second)

    from DAVE.reporting.utils.TextAvoidOverlap import minimizeTextOverlap

    minimizeTextOverlap(
        texts_second,
        fig=fig,
        ax=ax0_second,
        vertical_only=True,
        optimize_initial_positions=False,
        annotate=False,
    )
    minimizeTextOverlap(
        texts,
        fig=fig,
        ax=ax0,
        vertical_only=True,
        optimize_initial_positions=False,
        annotate=False,
    )

    ax0.spines["top"].set_visible(False)
    ax0.spines["bottom"].set_visible(False)

    ax0_second.spines["top"].set_visible(False)
    ax0_second.spines["bottom"].set_visible(False)

    ax1.plot(x, m.Vz, "k-", linewidth=linewidth)

    i = np.argmax(np.abs(m.Vz))
    ax1.plot(x[i], m.Vz[i], "b*")
    ax1.text(x[i], m.Vz[i], f"{m.Vz[i]:.2f}")

    ax1.grid()
    ax1.set_title("Shear")
    ax1.set_ylabel("[kN]")

    ax2.plot(x, m.My, "k-", linewidth=linewidth)
    i = np.argmax(np.abs(m.My))
    ax2.plot(x[i], m.My[i], "b*")
    ax2.text(x[i], m.My[i], f"{m.My[i]:.2f}")

    ax2.grid()
    ax2.set_title("Moment")
    ax2.set_ylabel("[kN*m]")

    if filename is None:
        plt.show()
    else:
        fig.savefig(filename)
def plot_simple(self, **kwargs)

Plots the bending moment and shear in a single yy-plot. Creates a new figure

any keyword arguments are passed to plt.figure(), so for example dpi=150 will increase the dpi

Returns: figure

Expand source code
def plot_simple(self, **kwargs):
    """Plots the bending moment and shear in a single yy-plot.
    Creates a new figure

    any keyword arguments are passed to plt.figure(), so for example dpi=150 will increase the dpi

    Returns: figure
    """
    x, Vz, My = self.give_shear_and_moment()
    import matplotlib.pyplot as plt
    plt.rcParams.update({"font.family": "sans-serif"})
    plt.rcParams.update({"font.sans-serif": "consolas"})
    plt.rcParams.update({"font.size": 10})

    fig, ax1 = plt.subplots(1,1,**kwargs)
    ax2 = ax1.twinx()

    ax1.plot(x, My, "g", lw=1, label="Bending Moment")
    ax2.plot(x, Vz, "b", lw=1, label="Shear Force")

    from DAVE.gui.helpers.align_zeros_of_yyplots import align_y0_axis

    align_y0_axis(ax1, ax2)

    ax1.set_xlabel("Position [m]")
    ax1.set_ylabel("Bending Moment [kNm]")
    ax2.set_ylabel("Shear Force [kN]")

    ax1.tick_params(axis="y", colors="g")
    ax2.tick_params(axis="y", colors="b")

    # fig.legend()  - obvious from the axis

    ext = 0.1 * (np.max(x) - np.min(x))
    xx = [np.min(x) - ext, np.max(x) + ext]
    ax1.plot(xx, [0, 0], c=[0.5, 0.5, 0.5], lw=1, linestyle=":")
    ax1.set_xlim(xx)

    return fig
class Manager (scene)

ABSTRACT CLASS - Properties defined here are applicable to all derived classes Master class for all nodes

Expand source code
class Manager(Node, ABC):


    # @abstractmethod                   not used anywhere outside the manager classes, so no requirement
    # def managed_nodes(self):
    #     """Returns a list of managed nodes"""
    #     raise Exception("derived class shall override this method")

    @abstractmethod
    def delete(self):
        """Carefully remove the manager, reinstate situation as before. Do not delete the manager itself but do
        delete all the nodes it created."""
        raise Exception("derived class shall override this method")

    @abstractmethod
    def creates(self, node: Node):
        """Returns True if node is created by this manager"""

        raise Exception("derived class shall override this method")
        # hint: return node in self.managed_nodes() # would be a good option, just not good enough as default

Ancestors

Subclasses

Methods

def creates(self, node: Node)

Returns True if node is created by this manager

Expand source code
@abstractmethod
def creates(self, node: Node):
    """Returns True if node is created by this manager"""

    raise Exception("derived class shall override this method")
    # hint: return node in self.managed_nodes() # would be a good option, just not good enough as default
def delete(self)

Carefully remove the manager, reinstate situation as before. Do not delete the manager itself but do delete all the nodes it created.

Expand source code
@abstractmethod
def delete(self):
    """Carefully remove the manager, reinstate situation as before. Do not delete the manager itself but do
    delete all the nodes it created."""
    raise Exception("derived class shall override this method")

Inherited members

class Node (scene)

ABSTRACT CLASS - Properties defined here are applicable to all derived classes Master class for all nodes

Expand source code
class Node(ABC):
    """ABSTRACT CLASS - Properties defined here are applicable to all derived classes
    Master class for all nodes"""

    def __init__(self, scene):
        self._scene: Scene = scene
        """reference to the scene that the node lives is"""

        self._name: str = "A manager without a name"
        """Unique name of the node"""

        self._manager: Node or None = None
        """Reference to a node that controls this node"""

        self.observers = list()
        """List of nodes observing this node."""

        self._visible: bool = True
        """Determines if the visual for of this node (if any) should be visible"""

    def __repr__(self):
        return f"{self.name} <{self.__class__.__name__}>"

    def __str__(self):
        return self.name

    @property
    def class_name(self):
        return self.__class__.__name__

    @abstractmethod
    def depends_on(self) -> list:
        """Returns a list of nodes that need to be available present for this node to exist"""
        raise ValueError(
            f"Derived class should implement this method, but {type(self)} does not"
        )

    def give_python_code(self):
        """Returns the python code that can be executed to re-create this node"""
        return "# No python code generated for element {}".format(self.name)

    @property
    def visible(self):
        if self.manager:
            return self.manager.visible
        return self._visible

    @visible.setter
    @node_setter_manageable
    @node_setter_observable
    def visible(self, value):
        self._visible = value

    @property
    def manager(self):
        return self._manager

    @manager.setter
    @node_setter_manageable
    @node_setter_observable
    def manager(self, value):

        self._manager = value
        pass

    def _verify_change_allowed(self):
        """Changing the state of a node is only allowed if either:
        1. the node is not manages (node._manager is None)
        2. the manager of the node is identical to scene.current_manager
        """
        if self._scene._godmode:
            return True

        if self._manager is not None:
            if self._manager != self._scene.current_manager:
                if self._scene.current_manager is None:
                    name = None
                else:
                    name = self._scene.current_manager.name
                raise Exception(
                    f"Node {self.name} may not be changed because it is managed by {self._manager.name} and the current manager of the scene is {name}"
                )

    @property
    def name(self):
        """Name of the node (str), must be unique"""
        return self._name

    @name.setter
    @node_setter_manageable
    @node_setter_observable
    def name(self, name):

        self._name = name

    def _delete_vfc(self):
        """Removes any internally created core objects"""
        pass

    def update(self):
        """Performs internal updates relevant for physics. Called before solving statics or getting results such as
        forces or inertia"""
        pass

    def _notify_observers(self):
        for obs in self.observers:
            obs.on_observed_node_changed(self)

    def on_observed_node_changed(self, changed_node):
        """ """
        pass

Ancestors

  • abc.ABC

Subclasses

Instance variables

var class_name
Expand source code
@property
def class_name(self):
    return self.__class__.__name__
var manager
Expand source code
@property
def manager(self):
    return self._manager
var name

Name of the node (str), must be unique

Expand source code
@property
def name(self):
    """Name of the node (str), must be unique"""
    return self._name
var observers

List of nodes observing this node.

var visible
Expand source code
@property
def visible(self):
    if self.manager:
        return self.manager.visible
    return self._visible

Methods

def depends_on(self) -> list

Returns a list of nodes that need to be available present for this node to exist

Expand source code
@abstractmethod
def depends_on(self) -> list:
    """Returns a list of nodes that need to be available present for this node to exist"""
    raise ValueError(
        f"Derived class should implement this method, but {type(self)} does not"
    )
def give_python_code(self)

Returns the python code that can be executed to re-create this node

Expand source code
def give_python_code(self):
    """Returns the python code that can be executed to re-create this node"""
    return "# No python code generated for element {}".format(self.name)
def on_observed_node_changed(self, changed_node)
Expand source code
def on_observed_node_changed(self, changed_node):
    """ """
    pass
def update(self)

Performs internal updates relevant for physics. Called before solving statics or getting results such as forces or inertia

Expand source code
def update(self):
    """Performs internal updates relevant for physics. Called before solving statics or getting results such as
    forces or inertia"""
    pass
class NodeWithParent (scene, vfNode)

NodeWithParent

Do not use this class directly. This is a base-class for all nodes that have a "parent" property.

Expand source code
class NodeWithParent(CoreConnectedNode):
    """
    NodeWithParent

    Do not use this class directly.
    This is a base-class for all nodes that have a "parent" property.
    """

    def __init__(self, scene, vfNode):
        super().__init__(scene, vfNode)
        self._parent = None
        self._None_parent_acceptable = False
        self._parent_for_code_export = True
        """True : use parent, 
        None : use None, 
        Node : use that Node
        Used to prevent circular references, see groups section in documentation"""

    def depends_on(self):
        if self.parent_for_export is not None:
            return [self.parent_for_export]
        else:
            return []

    @property
    def parent_for_export(self):
        if self._parent_for_code_export == True:
            return self._parent
        else:
            return self._parent_for_code_export

    @property
    def parent(self):
        """Determines the parent of the node. Should be an axis or None"""
        if self._vfNode.parent is None:
            return None
        else:
            return self._parent
            # return Axis(self._scene, self._vfNode.parent)

    @parent.setter
    @node_setter_manageable
    @node_setter_observable
    def parent(self, var):
        """Assigns a new parent. Keeps the local position and rotations the same

        See also: change_parent_to
        """

        if var is None:

            if not self._None_parent_acceptable:
                raise ValueError(
                    "None is not an acceptable parent for {} of {}".format(
                        self.name, type(self)
                    )
                )

            self._parent = None
            self._vfNode.parent = None
        else:

            var = self._scene._node_from_node_or_str(var)

            if isinstance(var, Axis) or isinstance(var, GeometricContact):
                self._parent = var
                self._vfNode.parent = var._vfNode
            elif isinstance(var, Point):
                self._parent = var
                self._vfNode.parent = var._vfNode
            else:
                raise Exception(
                    "Parent can only be set to an instance of Axis or Poi, not to a {}".format(
                        type(var)
                    )
                )

    def change_parent_to(self, new_parent):
        """Assigns a new parent to the node but keeps the global position and rotation the same.

        See also: .parent (property)

        Args:
            new_parent: new parent node

        """

        if isinstance(self, Point) and isinstance(new_parent, Point):
            raise TypeError("Points can not be placed on points")

        try:
            self.rotation
            has_rotation = True
        except:
            has_rotation = False

        try:
            self.position
            has_position = True
        except:
            has_position = False

        # it is possible that this function is called on an object without position/rotation
        # in that case just fall-back to a change of parent
        if not has_position and not has_rotation:
            self.parent = new_parent
            return

        # check new_parent
        if new_parent is not None:

            if not isinstance(new_parent, Axis):
                if not has_rotation:
                    if not isinstance(new_parent, Point):
                        raise TypeError(
                            "Only Poi-type nodes (or derived types) can be used as parent. You tried to use a {} as parent".format(
                                type(new_parent)
                            )
                        )
                else:
                    raise TypeError(
                        "Only None or Axis-type nodes (or derived types)  can be used as parent. You tried to use a {} as parent".format(
                            type(new_parent)
                        )
                    )

        glob_pos = self.global_position

        if has_rotation:
            glob_rot = self.global_rotation

        self.parent = new_parent

        if new_parent is None:
            self.position = glob_pos
            if has_rotation:
                self.rotation = glob_rot

        else:
            self.position = new_parent.to_loc_position(glob_pos)
            if has_rotation:
                self.rotation = new_parent.to_loc_direction(glob_rot)

Ancestors

Subclasses

Instance variables

var parent

Determines the parent of the node. Should be an axis or None

Expand source code
@property
def parent(self):
    """Determines the parent of the node. Should be an axis or None"""
    if self._vfNode.parent is None:
        return None
    else:
        return self._parent
        # return Axis(self._scene, self._vfNode.parent)
var parent_for_export
Expand source code
@property
def parent_for_export(self):
    if self._parent_for_code_export == True:
        return self._parent
    else:
        return self._parent_for_code_export

Methods

def change_parent_to(self, new_parent)

Assigns a new parent to the node but keeps the global position and rotation the same.

See also: .parent (property)

Args

new_parent
new parent node
Expand source code
def change_parent_to(self, new_parent):
    """Assigns a new parent to the node but keeps the global position and rotation the same.

    See also: .parent (property)

    Args:
        new_parent: new parent node

    """

    if isinstance(self, Point) and isinstance(new_parent, Point):
        raise TypeError("Points can not be placed on points")

    try:
        self.rotation
        has_rotation = True
    except:
        has_rotation = False

    try:
        self.position
        has_position = True
    except:
        has_position = False

    # it is possible that this function is called on an object without position/rotation
    # in that case just fall-back to a change of parent
    if not has_position and not has_rotation:
        self.parent = new_parent
        return

    # check new_parent
    if new_parent is not None:

        if not isinstance(new_parent, Axis):
            if not has_rotation:
                if not isinstance(new_parent, Point):
                    raise TypeError(
                        "Only Poi-type nodes (or derived types) can be used as parent. You tried to use a {} as parent".format(
                            type(new_parent)
                        )
                    )
            else:
                raise TypeError(
                    "Only None or Axis-type nodes (or derived types)  can be used as parent. You tried to use a {} as parent".format(
                        type(new_parent)
                    )
                )

    glob_pos = self.global_position

    if has_rotation:
        glob_rot = self.global_rotation

    self.parent = new_parent

    if new_parent is None:
        self.position = glob_pos
        if has_rotation:
            self.rotation = glob_rot

    else:
        self.position = new_parent.to_loc_position(glob_pos)
        if has_rotation:
            self.rotation = new_parent.to_loc_direction(glob_rot)

Inherited members

class NodeWithParentAndFootprint (scene, vfNode)

NodeWithParentAndFootprint

Do not use this class directly. This is a base-class for all nodes that have a "footprint" property as well as a parent

Expand source code
class NodeWithParentAndFootprint(NodeWithParent):
    """
    NodeWithParentAndFootprint

    Do not use this class directly.
    This is a base-class for all nodes that have a "footprint" property as well as a parent
    """

    def __init__(self, scene, vfNode):
        super().__init__(scene, vfNode)

    @property
    def footprint(self):
        """tuple of tuples ((x1,y1,z1), (x2,y2,z2), .... (xn,yn,zn)"""
        r = []
        for i in range(self._vfNode.nFootprintVertices):
            r.append(self._vfNode.footprintVertexGet(i))
        return tuple(r)

    @footprint.setter
    def footprint(self, value):
        """Sets the footprint vertices. Supply as an iterable with each element containing three floats"""
        for t in value:
            assert3f(t, "Each entry of value assigned to footprints ")

        self._vfNode.footprintVertexClearAll()
        for t in value:
            self._vfNode.footprintVertexAdd(*t)

    def add_footprint_python_code(self):
        if self.footprint:
            return f"\ns['{self.name}'].footprint = {str(self.footprint)}"
        else:
            return ""

Ancestors

Subclasses

Instance variables

var footprint

tuple of tuples ((x1,y1,z1), (x2,y2,z2), .... (xn,yn,zn)

Expand source code
@property
def footprint(self):
    """tuple of tuples ((x1,y1,z1), (x2,y2,z2), .... (xn,yn,zn)"""
    r = []
    for i in range(self._vfNode.nFootprintVertices):
        r.append(self._vfNode.footprintVertexGet(i))
    return tuple(r)

Methods

def add_footprint_python_code(self)
Expand source code
def add_footprint_python_code(self):
    if self.footprint:
        return f"\ns['{self.name}'].footprint = {str(self.footprint)}"
    else:
        return ""

Inherited members

class Point (scene, vfPoi)

A location on an axis

Expand source code
class Point(NodeWithParentAndFootprint):
    """A location on an axis"""

    # init parent and name are fully derived from NodeWithParent
    # _vfNode is a poi
    def __init__(self, scene, vfPoi):
        super().__init__(scene, vfPoi)
        self._None_parent_acceptable = True

    def on_observed_node_changed(self, changed_node):
        print(changed_node.name + " has changed")

    @property
    def x(self):
        """x component of local position [m] (parent axis)"""
        return self.position[0]

    @property
    def y(self):
        """y component of local position [m] (parent axis)"""
        return self.position[1]

    @property
    def z(self):
        """z component of local position [m] (parent axis)"""
        return self.position[2]

    @x.setter
    @node_setter_manageable
    @node_setter_observable
    def x(self, var):

        a = self.position
        self.position = (var, a[1], a[2])

    @y.setter
    @node_setter_manageable
    @node_setter_observable
    def y(self, var):

        a = self.position
        self.position = (a[0], var, a[2])

    @z.setter
    @node_setter_manageable
    @node_setter_observable
    @node_setter_manageable
    def z(self, var):

        """z component of local position"""
        a = self.position
        self.position = (a[0], a[1], var)

    @property
    def position(self):
        """Local position [m,m,m] (parent axis)"""
        return self._vfNode.position

    @position.setter
    @node_setter_manageable
    @node_setter_observable
    def position(self, new_position):

        assert3f(new_position)
        self._vfNode.position = new_position

    @property
    def applied_force_and_moment_global(self):
        """Applied force and moment on this point [kN, kN, kN, kNm, kNm, kNm] (Global axis)"""
        return self._vfNode.applied_force

    @property
    def gx(self):
        """x component of position [m] (global axis)"""
        return self.global_position[0]

    @property
    def gy(self):
        """y component of position [m] (global axis)"""
        return self.global_position[1]

    @property
    def gz(self):
        """z component of position [m] (global axis)"""
        return self.global_position[2]

    @gx.setter
    @node_setter_manageable
    @node_setter_observable
    def gx(self, var):

        a = self.global_position
        self.global_position = (var, a[1], a[2])

    @gy.setter
    @node_setter_manageable
    @node_setter_observable
    def gy(self, var):

        a = self.global_position
        self.global_position = (a[0], var, a[2])

    @gz.setter
    @node_setter_manageable
    @node_setter_observable
    def gz(self, var):

        a = self.global_position
        self.global_position = (a[0], a[1], var)

    @property
    def global_position(self):
        """Global position [m,m,m] (global axis)"""
        return self._vfNode.global_position

    @global_position.setter
    @node_setter_manageable
    @node_setter_observable
    def global_position(self, val):

        assert3f(val, "Global Position")
        if self.parent:
            self.position = self.parent.to_loc_position(val)
        else:
            self.position = val

    def give_python_code(self):
        code = "# code for {}".format(self.name)
        code += "\ns.new_point(name='{}',".format(self.name)
        if self.parent_for_export:
            code += "\n          parent='{}',".format(self.parent_for_export.name)

        # position

        code += "\n          position=({},".format(self.position[0])
        code += "\n                    {},".format(self.position[1])
        code += "\n                    {}))".format(self.position[2])

        code += self.add_footprint_python_code()

        return code

Ancestors

Instance variables

var applied_force_and_moment_global

Applied force and moment on this point [kN, kN, kN, kNm, kNm, kNm] (Global axis)

Expand source code
@property
def applied_force_and_moment_global(self):
    """Applied force and moment on this point [kN, kN, kN, kNm, kNm, kNm] (Global axis)"""
    return self._vfNode.applied_force
var global_position

Global position [m,m,m] (global axis)

Expand source code
@property
def global_position(self):
    """Global position [m,m,m] (global axis)"""
    return self._vfNode.global_position
var gx

x component of position [m] (global axis)

Expand source code
@property
def gx(self):
    """x component of position [m] (global axis)"""
    return self.global_position[0]
var gy

y component of position [m] (global axis)

Expand source code
@property
def gy(self):
    """y component of position [m] (global axis)"""
    return self.global_position[1]
var gz

z component of position [m] (global axis)

Expand source code
@property
def gz(self):
    """z component of position [m] (global axis)"""
    return self.global_position[2]
var position

Local position [m,m,m] (parent axis)

Expand source code
@property
def position(self):
    """Local position [m,m,m] (parent axis)"""
    return self._vfNode.position
var x

x component of local position [m] (parent axis)

Expand source code
@property
def x(self):
    """x component of local position [m] (parent axis)"""
    return self.position[0]
var y

y component of local position [m] (parent axis)

Expand source code
@property
def y(self):
    """y component of local position [m] (parent axis)"""
    return self.position[1]
var z

z component of local position [m] (parent axis)

Expand source code
@property
def z(self):
    """z component of local position [m] (parent axis)"""
    return self.position[2]

Methods

def on_observed_node_changed(self, changed_node)
Expand source code
def on_observed_node_changed(self, changed_node):
    print(changed_node.name + " has changed")

Inherited members

class RigidBody (scene, axis, poi, force)

A Rigid body, internally composed of an axis, a point (cog) and a force (gravity)

Expand source code
class RigidBody(Axis):
    """A Rigid body, internally composed of an axis, a point (cog) and a force (gravity)"""

    def __init__(self, scene, axis, poi, force):
        super().__init__(scene, axis)

        # The axis is the Node
        # poi and force are added separately

        self._vfPoi = poi
        self._vfForce = force

    # override the following properties
    # - name : sets the names of poi and force as well

    def _delete_vfc(self):
        super()._delete_vfc()
        self._scene._vfc.delete(self._vfPoi.name)
        self._scene._vfc.delete(self._vfForce.name)

    @property  # can not define a setter without a getter..?
    def name(self):
        return super().name

    @name.setter
    @node_setter_manageable
    @node_setter_observable
    def name(self, newname):
        """Name of the node (str), must be unique"""

        # super().name = newname
        super(RigidBody, self.__class__).name.fset(self, newname)
        self._vfPoi.name = newname + vfc.VF_NAME_SPLIT + "cog"
        self._vfForce.name = newname + vfc.VF_NAME_SPLIT + "gravity"

    @property
    def footprint(self):
        return super().footprint

    @footprint.setter
    def footprint(self, value):
        """Sets the footprint vertices. Supply as an iterable with each element containing three floats"""
        super(RigidBody, type(self)).footprint.fset(
            self, value
        )  # https://bugs.python.org/issue14965

        # assign the footprint to the CoG as well,
        # but subtract the cog position as
        self._sync_selfweight_footprint()

    def _sync_selfweight_footprint(self):
        """The footprint of the CoG is defined relative to the CoG, so its needs to be updated
        whenever the CoG or the footprint changes"""

        fp = self.footprint

        self._vfPoi.footprintVertexClearAll()
        for t in fp:
            pos = np.array(t, dtype=float)
            relpos = pos - self.cog
            self._vfPoi.footprintVertexAdd(*relpos)

    @property
    def cogx(self):
        """x-component of cog position [m] (local axis)"""
        return self.cog[0]

    @property
    def cogy(self):
        """y-component of cog position [m] (local axis)"""
        return self.cog[1]

    @property
    def cogz(self):
        """z-component of cog position [m] (local axis)"""
        return self.cog[2]

    @property
    def cog(self):
        """Center of Gravity position [m,m,m] (local axis)"""
        return self._vfPoi.position

    @cogx.setter
    @node_setter_manageable
    @node_setter_observable
    def cogx(self, var):

        a = self.cog
        self.cog = (var, a[1], a[2])

    @cogy.setter
    @node_setter_manageable
    @node_setter_observable
    def cogy(self, var):

        a = self.cog
        self.cog = (a[0], var, a[2])

    @cogz.setter
    @node_setter_manageable
    @node_setter_observable
    def cogz(self, var):

        a = self.cog
        self.cog = (a[0], a[1], var)

    @cog.setter
    @node_setter_manageable
    @node_setter_observable
    def cog(self, newcog):

        assert3f(newcog)
        self._vfPoi.position = newcog
        self.inertia_position = self.cog
        self._sync_selfweight_footprint()

    @property
    def mass(self):
        """Static mass of the body [mT]

        See Also: inertia
        """
        return self._vfForce.force[2] / -vfc.G

    @mass.setter
    @node_setter_manageable
    @node_setter_observable
    def mass(self, newmass):

        assert1f(newmass)
        self.inertia = newmass
        self._vfForce.force = (0, 0, -vfc.G * newmass)

    def give_python_code(self):
        code = "# code for {}".format(self.name)
        code += "\ns.new_rigidbody(name='{}',".format(self.name)
        code += "\n                mass={},".format(self.mass)
        code += "\n                cog=({},".format(self.cog[0])
        code += "\n                     {},".format(self.cog[1])
        code += "\n                     {}),".format(self.cog[2])

        if self.parent_for_export:
            code += "\n                parent='{}',".format(self.parent_for_export.name)

        # position

        if self.fixed[0]:
            code += "\n                position=({},".format(self.position[0])
        else:
            code += "\n                position=(solved({}),".format(self.position[0])
        if self.fixed[1]:
            code += "\n                          {},".format(self.position[1])
        else:
            code += "\n                          solved({}),".format(self.position[1])
        if self.fixed[2]:
            code += "\n                          {}),".format(self.position[2])
        else:
            code += "\n                          solved({})),".format(self.position[2])

        # rotation

        if self.fixed[3]:
            code += "\n                rotation=({},".format(self.rotation[0])
        else:
            code += "\n                rotation=(solved({}),".format(self.rotation[0])
        if self.fixed[4]:
            code += "\n                          {},".format(self.rotation[1])
        else:
            code += "\n                          solved({}),".format(self.rotation[1])
        if self.fixed[5]:
            code += "\n                          {}),".format(self.rotation[2])
        else:
            code += "\n                          solved({})),".format(self.rotation[2])

        if np.any(self.inertia_radii > 0):
            code += "\n                     inertia_radii = ({}, {}, {}),".format(
                *self.inertia_radii
            )

        code += "\n                fixed =({}, {}, {}, {}, {}, {}) )".format(
            *self.fixed
        )

        code += self.add_footprint_python_code()

        return code

Ancestors

Subclasses

Instance variables

var cog

Center of Gravity position [m,m,m] (local axis)

Expand source code
@property
def cog(self):
    """Center of Gravity position [m,m,m] (local axis)"""
    return self._vfPoi.position
var cogx

x-component of cog position [m] (local axis)

Expand source code
@property
def cogx(self):
    """x-component of cog position [m] (local axis)"""
    return self.cog[0]
var cogy

y-component of cog position [m] (local axis)

Expand source code
@property
def cogy(self):
    """y-component of cog position [m] (local axis)"""
    return self.cog[1]
var cogz

z-component of cog position [m] (local axis)

Expand source code
@property
def cogz(self):
    """z-component of cog position [m] (local axis)"""
    return self.cog[2]
var mass

Static mass of the body [mT]

See Also: inertia

Expand source code
@property
def mass(self):
    """Static mass of the body [mT]

    See Also: inertia
    """
    return self._vfForce.force[2] / -vfc.G

Inherited members

class SPMT (scene, node)

An SPMT is a Self-propelled modular transporter

These are platform vehicles

============ ======= 0 0 0 0 0 0 0 0 0 0

A number of axles share a common suspension system.

The SPMT node models such a system of axles.

The SPMT is attached to an axis system. The upper locations of the axles are given as an array of 3d vectors.

Rays are extended from these points in local -Z direction (down) until they hit a contact-shape.

If no contact shape is found (or not within the maximum distance per axles) then the maximum defined extension for that axle is used.

A shared pressure is obtained from the combination of all individual extensions.

Finally an equal force is applied on all the axle connection points. This force acts in local Z direction.

Expand source code
class SPMT(NodeWithParent):
    """An SPMT is a Self-propelled modular transporter

    These are platform vehicles

    ============  =======
    0 0 0 0 0 0   0 0 0 0

    A number of axles share a common suspension system.

    The SPMT node models such a system of axles.

    The SPMT is attached to an axis system.
    The upper locations of the axles are given as an array of 3d vectors.

    Rays are extended from these points in local -Z direction (down) until they hit a contact-shape.

    If no contact shape is found (or not within the maximum distance per axles) then the maximum defined extension for that axle is used.

    A shared pressure is obtained from the combination of all individual extensions.

    Finally an equal force is applied on all the axle connection points. This force acts in local Z direction.

    """

    def __init__(self, scene, node):
        super().__init__(scene, node)
        self._meshes = list()

    # read-only

    @property
    def axle_force(self) -> tuple:
        """Returns the force on each of the axles [kN, kN, kN] (global axis)"""
        return self._vfNode.force

    @property
    def compression(self) -> float:
        """Returns the total compression of all the axles together [m]"""
        return self._vfNode.compression

    def get_actual_global_points(self):
        """Returns a list of points: axle1, bottom wheels 1, axle2, bottom wheels 2, etc"""
        gp = self._vfNode.actual_global_points

        pts = []
        n2 = int(len(gp) / 2)
        for i in range(n2):
            pts.append(gp[2 * i + 1])
            pts.append(gp[2 * i])

            if i < n2 - 1:
                pts.append(gp[2 * i + 2])

        return pts

    # controllable

    # name is derived
    # parent is derived

    @property
    def k(self):
        """Compression stiffness of the ball in force per meter of compression [kN/m]"""
        return self._vfNode.k

    @k.setter
    @node_setter_manageable
    @node_setter_observable
    def k(self, value):

        assert1f_positive_or_zero(value, "k")
        self._vfNode.k = value
        pass

    @property
    def nominal_length(self):
        """Average Axle extension (defined point to bottom of wheel) for zero force [m]"""
        return self._vfNode.nominal_length

    @nominal_length.setter
    @node_setter_manageable
    @node_setter_observable
    def nominal_length(self, value):

        assert1f_positive_or_zero(value, "nominal_length")
        self._vfNode.nominal_length = value
        pass

    @property
    def max_length(self):
        """Maximum axle extension per axle (defined point to bottom of wheel) [m]"""
        return self._vfNode.max_length

    @max_length.setter
    @node_setter_manageable
    @node_setter_observable
    def max_length(self, value):

        assert1f_positive_or_zero(value, "max_length")
        self._vfNode.max_length = value
        pass

    # === control meshes ====

    @property
    def meshes(self) -> tuple:
        """List of contact-mesh nodes.
        When getting this will yield a list of node references.
        When setting node references and node-names may be used.

        eg: ball.meshes = [mesh1, 'mesh2']
        """
        return tuple(self._meshes)

    @meshes.setter
    @node_setter_manageable
    @node_setter_observable
    def meshes(self, value):

        meshes = []

        for m in value:
            cm = self._scene._node_from_node_or_str(m)

            if not isinstance(cm, ContactMesh):
                raise ValueError(
                    f"Only ContactMesh nodes can be used as mesh, but {cm.name} is a {type(cm)}"
                )
            if cm in meshes:
                raise ValueError(f"Can not add {cm.name} twice")

            meshes.append(cm)

        # copy to meshes
        self._meshes.clear()
        self._vfNode.clear_contact_meshes()
        for mesh in meshes:
            self._meshes.append(mesh)
            self._vfNode.add_contact_mesh(mesh._vfNode)

    @property
    def meshes_names(self) -> list:
        """List with the names of the meshes"""
        return [m.name for m in self._meshes]

    # === control axles ====

    def make_grid(self, nx=3, ny=1, dx=1.4, dy=1.45):
        offx = nx * dx / 2
        offy = ny * dy / 2
        self._vfNode.clear_axles()

        for ix in range(nx):
            for iy in range(ny):
                self._vfNode.add_axle(ix * dx - offx, iy * dy - offy, 0)

    @property
    def axles(self):
        """Axles is a list axle positions. Each entry is a (x,y,z) entry which determines the location of the axle on
        SPMT. This is relative to the parent of the SPMT.

        Example:
            [(-10,0,0),(-5,0,0),(0,0,0)] for three axles
        """
        return self._vfNode.get_axles()

    @axles.setter
    @node_setter_manageable
    @node_setter_observable
    def axles(self, value):
        self._vfNode.clear_axles()
        for v in value:
            assert3f(v, "Each entry should contain three floating point numbers")
            self._vfNode.add_axle(*v)

    # actions

    def update(self):
        """Updates the contact-points and applies forces on mesh and point"""
        self._vfNode.update()

    def give_python_code(self):
        code = "# code for {}".format(self.name)

        code += "\ns.new_spmt(name='{}',".format(self.name)
        code += "\n                  parent='{}',".format(self.parent_for_export.name)
        code += "\n                  maximal_length={},".format(self.max_length)
        code += "\n                  nominal_length={},".format(self.nominal_length)
        code += "\n                  k={},".format(self.k)
        code += "\n                  meshes = [ "

        for m in self._meshes:
            code += '"' + m.name + '",'
        code = code[:-1] + "],"

        code += "\n                  axles = [ "

        for p in self.axles:
            code += f"({p[0]}, {p[1]}, {p[2]}),"

        code = code[:-1] + "])"

        return code

Ancestors

Instance variables

var axle_force : tuple

Returns the force on each of the axles [kN, kN, kN] (global axis)

Expand source code
@property
def axle_force(self) -> tuple:
    """Returns the force on each of the axles [kN, kN, kN] (global axis)"""
    return self._vfNode.force
var axles

Axles is a list axle positions. Each entry is a (x,y,z) entry which determines the location of the axle on SPMT. This is relative to the parent of the SPMT.

Example

[(-10,0,0),(-5,0,0),(0,0,0)] for three axles

Expand source code
@property
def axles(self):
    """Axles is a list axle positions. Each entry is a (x,y,z) entry which determines the location of the axle on
    SPMT. This is relative to the parent of the SPMT.

    Example:
        [(-10,0,0),(-5,0,0),(0,0,0)] for three axles
    """
    return self._vfNode.get_axles()
var compression : float

Returns the total compression of all the axles together [m]

Expand source code
@property
def compression(self) -> float:
    """Returns the total compression of all the axles together [m]"""
    return self._vfNode.compression
var k

Compression stiffness of the ball in force per meter of compression [kN/m]

Expand source code
@property
def k(self):
    """Compression stiffness of the ball in force per meter of compression [kN/m]"""
    return self._vfNode.k
var max_length

Maximum axle extension per axle (defined point to bottom of wheel) [m]

Expand source code
@property
def max_length(self):
    """Maximum axle extension per axle (defined point to bottom of wheel) [m]"""
    return self._vfNode.max_length
var meshes : tuple

List of contact-mesh nodes. When getting this will yield a list of node references. When setting node references and node-names may be used.

eg: ball.meshes = [mesh1, 'mesh2']

Expand source code
@property
def meshes(self) -> tuple:
    """List of contact-mesh nodes.
    When getting this will yield a list of node references.
    When setting node references and node-names may be used.

    eg: ball.meshes = [mesh1, 'mesh2']
    """
    return tuple(self._meshes)
var meshes_names : list

List with the names of the meshes

Expand source code
@property
def meshes_names(self) -> list:
    """List with the names of the meshes"""
    return [m.name for m in self._meshes]
var nominal_length

Average Axle extension (defined point to bottom of wheel) for zero force [m]

Expand source code
@property
def nominal_length(self):
    """Average Axle extension (defined point to bottom of wheel) for zero force [m]"""
    return self._vfNode.nominal_length

Methods

def get_actual_global_points(self)

Returns a list of points: axle1, bottom wheels 1, axle2, bottom wheels 2, etc

Expand source code
def get_actual_global_points(self):
    """Returns a list of points: axle1, bottom wheels 1, axle2, bottom wheels 2, etc"""
    gp = self._vfNode.actual_global_points

    pts = []
    n2 = int(len(gp) / 2)
    for i in range(n2):
        pts.append(gp[2 * i + 1])
        pts.append(gp[2 * i])

        if i < n2 - 1:
            pts.append(gp[2 * i + 2])

    return pts
def make_grid(self, nx=3, ny=1, dx=1.4, dy=1.45)
Expand source code
def make_grid(self, nx=3, ny=1, dx=1.4, dy=1.45):
    offx = nx * dx / 2
    offy = ny * dy / 2
    self._vfNode.clear_axles()

    for ix in range(nx):
        for iy in range(ny):
            self._vfNode.add_axle(ix * dx - offx, iy * dy - offy, 0)
def update(self)

Updates the contact-points and applies forces on mesh and point

Expand source code
def update(self):
    """Updates the contact-points and applies forces on mesh and point"""
    self._vfNode.update()

Inherited members

class Scene (filename=None, copy_from=None, code=None)

A Scene is the main component of DAVE.

It provides a world to place nodes (elements) in. It interfaces with the equilibrium core for all calculations.

By convention a Scene element is created with the name s, but create as many scenes as you want.

Examples

s = Scene() s.new_axis('my_axis', position = (0,0,1))

a = Scene() # another world a.new_point('a point')

Creates a new Scene

Args

filename
(str or Path) Insert contents from this file into the newly created scene
copy_from
(Scene) Copy nodes from this other scene into the newly created scene
Expand source code
class Scene:
    """
    A Scene is the main component of DAVE.

    It provides a world to place nodes (elements) in.
    It interfaces with the equilibrium core for all calculations.

    By convention a Scene element is created with the name s, but create as many scenes as you want.

    Examples:

        s = Scene()
        s.new_axis('my_axis', position = (0,0,1))

        a = Scene() # another world
        a.new_point('a point')


    """

    def __init__(self, filename=None, copy_from=None, code=None):
        """Creates a new Scene

        Args:
            filename: (str or Path) Insert contents from this file into the newly created scene
            copy_from:  (Scene) Copy nodes from this other scene into the newly created scene
        """

        count = 0
        if filename:
            count += 1
        if copy_from:
            count += 1
        if code:
            count += 1
        if count > 1:
            raise ValueError(
                "Only one of the named arguments (filename OR copy_from OR code) can be used"
            )

        self.verbose = True
        """Report actions using print()"""

        self._vfc = pyo3d.Scene()
        """_vfc : DAVE Core, where the actual magic happens"""

        self._nodes = []
        """Contains a list of all nodes in the scene"""

        self.static_tolerance = 0.01
        """Desired tolerance when solving statics"""

        self.resources_paths = []
        """A list of paths where to look for resources such as .obj files. Priority is given to paths earlier in the list."""
        self.resources_paths.extend(vfc.RESOURCE_PATH)

        self._savepoint = None
        """Python code to re-create the scene, see savepoint_make()"""

        self._name_prefix = ""
        """An optional prefix to be applied to node names. Used when importing scenes."""

        self.current_manager = None
        """Setting this to an instance of a Manager allows nodes with that manager to be changed"""

        self._godmode = False
        """Icarus warning, wear proper PPE"""

        if filename is not None:
            self.load_scene(filename)

        if copy_from is not None:
            self.import_scene(copy_from, containerize=False)

        if code is not None:
            self.run_code(code)

    def clear(self):
        """Deletes all nodes"""

        self._nodes = []
        del self._vfc
        self._vfc = pyo3d.Scene()

    # =========== private functions =============

    def _print_cpp(self):
        print(self._vfc.to_string())

    def _print(self, what):
        if self.verbose:
            print(what)

    def _prefix_name(self, name):
        return self._name_prefix + name

    def _verify_name_available(self, name):
        """Throws an error if a node with name 'name' already exists"""
        names = [n.name for n in self._nodes]
        names.extend(self._vfc.names)
        if name in names:
            raise Exception(
                "The name '{}' is already in use. Pick a unique name".format(name)
            )

    def _node_from_node_or_str(self, node):
        """If node is a string, then returns the node with that name,
        if node is a node, then returns that node

        Raises:
            ValueError if a string is passed with an non-existing node
        """

        if isinstance(node, Node):
            return node
        if isinstance(node, str):
            return self[node]
        raise ValueError(
            "Node should be a Node or a string, not a {}".format(type(node))
        )

    def _node_from_node(self, node, reqtype):
        """Gets a node from the specified type

        Returns None if node is None
        Returns node if node is already a reqtype type node
        Else returns the axis with the given name

        Raises Exception if a node with name is not found"""

        if node is None:
            return None

        # node is a string then get the node with this name
        if type(node) == str:
            node = self[self._name_prefix + node]

        reqtype = make_iterable(reqtype)

        for r in reqtype:
            if isinstance(node, r):
                return node

        if issubclass(type(node), Node):
            raise Exception(
                "Element with name {} can not be used , it should be a {} or derived type but is a {}.".format(
                    node.name, reqtype, type(node)
                )
            )

        raise Exception("This is not an acceptable input argument {}".format(node))

    def _parent_from_node(self, node):
        """Returns None if node is None
        Returns node if node is an axis type node
        Else returns the axis with the given name

        Raises Exception if a node with name is not found"""

        return self._node_from_node(node, Axis)

    def _poi_from_node(self, node):
        """Returns None if node is None
        Returns node if node is an poi type node
        Else returns the poi with the given name

        Raises Exception if anything is not ok"""

        return self._node_from_node(node, Point)

    def _poi_or_sheave_from_node(self, node):
        """Returns None if node is None
        Returns node if node is an poi type node
        Else returns the poi with the given name

        Raises Exception if anything is not ok"""

        return self._node_from_node(node, [Point, Circle])

    def _sheave_from_node(self, node):
        """Returns None if node is None
        Returns node if node is an poi type node
        Else returns the poi with the given name

        Raises Exception if anything is not ok"""

        return self._node_from_node(node, Circle)

    def _geometry_changed(self):
        """Notify the scene that the geometry has changed and that the global transforms are invalid"""
        self._vfc.geometry_changed()

    def _fix_vessel_heel_trim(self):
        """Fixes the heel and trim of each node that has a buoyancy or linear hydrostatics node attached.

        Returns:
            Dictionary with original fixed properties as dict({'node name',fixed[6]}) which can be passed to _restore_original_fixes
        """

        vessel_indicators = [
            *self.nodes_of_type(Buoyancy),
            *self.nodes_of_type(HydSpring),
        ]
        r = dict()

        for node in vessel_indicators:
            parent = node.parent  # axis

            if parent.fixed[3] and parent.fixed[4]:
                continue  # already fixed

            r[parent.name] = parent.fixed  # store original fixes
            fixed = [*parent.fixed]
            fixed[3] = True
            fixed[4] = True

            # if fixed[3] and fixed[4] are non-zero, then yaw has to be fixed as well.
            # The solver does not support it when an angular dof is free, but one of the fixed
            # angular dofs is non-zero

            fixed[5] = True

            parent.fixed = fixed

        return r

    def _restore_original_fixes(self, original_fixes):
        """Restores the fixes as in original_fixes

        See also: _fix_vessel_heel_trim

        Args:
            original_fixes: dict with {'node name',fixes[6] }

        Returns:
            None

        """
        if original_fixes is None:
            return

        for name in original_fixes.keys():
            self.node_by_name(name).fixed = original_fixes[name]

    def _check_and_fix_geometric_contact_orientations(self) -> (bool, str):
        """A Geometric pin on pin contact may end up with tension in the contact. Fix that by moving the child pin to the other side of the parent pin

        Returns:
            True if anything was changed; False otherwise
        """

        changed = False
        message = ""
        for n in self.nodes_of_type(GeometricContact):
            if not n.inside:

                # connection force of the child is the
                # force applied on the connecting rod
                # in the axis system of the rod
                if n._axis_on_child.connection_force_x > 0:
                    message += f"Changing side of pin-pin connection {n.name} due to tension in connection\n"
                    n.change_side()
                    changed = True

        return (changed, message)

    # ======== resources =========

    def get_resource_path(self, url) -> Path:
        """Resolves the path on disk for resource url. Urls statring with res: result in a file from the resources system.

        Looks for a file with "name" in the specified resource-paths and returns the full path to the the first one
        that is found.
        If name is a full path to an existing file, then that is returned.

        See Also:
            resource_paths


        Returns:
            Full path to resource

        Raises:
            FileExistsError if resource is not found

        """

        # warning and work-around for backwards compatibility
        # filenames without a path get res: in front of it
        try:
            if isinstance(url, Path):
                test = str(url)
            else:
                test = url

            if not test.startswith("res:"):
                test = Path(test)
                if str(test.parent) == ".":
                    # from warnings import warn
                    #
                    # warn(
                    #     f'Resources should start with res: --> fixing "{url}" to "res: {url}"'
                    # )
                    url = "res: " + str(test)
        except:
            pass

        if isinstance(url, Path):
            file = url
        elif isinstance(url, str):
            if not url.startswith("res:"):
                file = Path(url)
            else:
                # we have a string starting with 'res:'
                filename = url[4:].strip()

                for res in self.resources_paths:
                    p = Path(res)

                    file = p / filename
                    if isfile(file):
                        return file

                # prepare feedback for error
                ext = str(url).split(".")[-1]  # everything after the last .

                print("Resource folders:")
                for res in self.resources_paths:
                    print(str(res))


                print(
                    "The following resources with extension {} are available with ".format(
                        ext
                    )
                )
                available = self.get_resource_list(ext)
                for a in available:
                    print(a)
                raise FileExistsError(
                    'Resource "{}" not found in resource paths. A list with available resources with this extension is printed above this error'.format(
                        url
                    )
                )
        else:
            raise ValueError(
                f"Provided url shall be a Path or a string, not a {type(url)}"
            )

        if file.exists():
            return file

        raise FileExistsError(
            'File "{}" not found.\nHint: To obtain a resource put res: in front of the name.'.format(
                url
            )
        )

    def get_resource_list(self, extension):
        """Returns a list of all file-paths (strings) given extension in any of the resource-paths"""

        r = []

        for dir in self.resources_paths:
            try:
                files = listdir(dir)
                for file in files:
                    if file.lower().endswith(extension):
                        if file not in r:
                            r.append("res: " + file)
            except FileNotFoundError:
                pass

        return r

    # ======== element functions =========

    def node_by_name(self, node_name, silent=False):
        for N in self._nodes:
            if N.name == node_name:
                return N

        if not silent:
            self.print_node_tree()
        raise ValueError(
            'No node with name "{}". Available names printed above.'.format(node_name)
        )

    def __getitem__(self, node_name):
        """Returns a node with name"""
        return self.node_by_name(node_name)

    def nodes_of_type(self, node_class):
        """Returns all nodes of the specified or derived type

        Examples:
            pois = scene.nodes_of_type(DAVE.Poi)
            axis_and_bodies = scene.nodes_of_type(DAVE.Axis)
        """
        r = list()
        for n in self._nodes:
            if isinstance(n, node_class):
                r.append(n)
        return r

    def assert_unique_names(self):
        """Asserts that all names are unique"""
        names = [n.name for n in self._nodes]
        unique_names = set(names)

        if len(unique_names) != len(names):
            previous_name = ""
            names.sort()
            duplicates = ""
            for name in names:
                if name == previous_name:
                    print(f"Duplicate: {name}")
                    duplicates += name + " "

                    for n in self._nodes:
                        if n.name == name:
                            print(n)

                previous_name = name
            raise ValueError(f"Duplicate names exist: " + duplicates)

    def sort_nodes_by_parent(self):
        """Sorts the nodes such that the parent of this node (if any) occurs earlier in the list.

        See Also:
            sort_nodes_by_dependency
        """

        self.assert_unique_names()

        exported = []
        to_be_exported = self._nodes.copy()
        counter = 0

        while to_be_exported:

            counter += 1
            if counter > len(self._nodes):
                raise Exception(
                    "Could not sort nodes by dependency, circular references exist?"
                )

            can_be_exported = []

            for node in to_be_exported:

                if hasattr(node, "parent"):
                    parent = node.parent
                    if parent is not None and parent not in exported:
                        continue

                if node.manager is not None and node.manager not in exported:
                    continue

                # otherwise the node can be exported
                can_be_exported.append(node)

            # remove exported nodes from
            for n in can_be_exported:
                to_be_exported.remove(n)

            exported.extend(can_be_exported)

        self._nodes = exported

    def sort_nodes_by_dependency(self):
        """Sorts the nodes such that a nodes creation only depends on nodes earlier in the list.

        This sorting is used for node creation order

        See Also:
            sort_nodes_by_parent
        """

        self.assert_unique_names()

        exported = []
        to_be_exported = self._nodes.copy()
        counter = 0

        while to_be_exported:

            counter += 1
            if counter > len(self._nodes):

                for node in to_be_exported:
                    print(f"Node : {node.name}")
                    for d in node.depends_on():
                        print(f"  depends on: {d.name}")
                    if node._manager:
                        print(f"   managed by: {node._manager.name}")

                raise Exception(
                    "Could not sort nodes by dependency, circular references exist?"
                )

            can_be_exported = []

            for node in to_be_exported:
                # if node._manager:
                #     if node._manager in exported:
                #         can_be_exported.append(node)
                # el
                if all(el in exported for el in node.depends_on()):
                    can_be_exported.append(node)

            # remove exported nodes from
            for n in can_be_exported:
                to_be_exported.remove(n)

            exported.extend(can_be_exported)

        self._nodes = exported

        # scene_names = [n.name for n in self._nodes]
        #
        # self._vfc.state_update()  # use the function from the core.
        # new_list = []
        # for name in self._vfc.names:  # and then build a new list using the names
        #     if vfc.VF_NAME_SPLIT in name:
        #         continue
        #
        #     if name not in scene_names:
        #         raise Exception('Something went wrong with sorting the the nodes by dependency. '
        #                         'Node naming between core and scene is inconsistent for node {}'.format(name))
        #
        #     new_list.append(self[name])
        #
        # # and add the nodes without a vfc-core connection
        # for node in self._nodes:
        #     if not node in new_list:
        #         new_list.append(node)
        #
        # self._nodes = new_list

    def name_available(self, name):
        """Returns True if the name is still available"""
        names = [n.name for n in self._nodes]
        names.extend(self._vfc.names)
        return not (name in names)

    def available_name_like(self, like):
        """Returns an available name like the one given, for example Axis23"""
        if self.name_available(like):
            return like
        counter = 1
        while True:
            name = like + "_" + str(counter)
            if self.name_available(name):
                return name
            counter += 1

    def node_A_core_depends_on_B_core(self, A, B):
        """Returns True if the node core of node A depends on the core node of node B"""

        A = self._node_from_node_or_str(A)
        B = self._node_from_node_or_str(B)

        if not isinstance(A, CoreConnectedNode):
            raise ValueError(
                f"{A.name} is not connected to a core node. Dependancies can not be traced using this function"
            )
        if not isinstance(B, CoreConnectedNode):
            raise ValueError(
                f"{B.name} is not connected to a core node. Dependancies can not be traced using this function"
            )

        return self._vfc.element_A_depends_on_B(A._vfNode.name, B._vfNode.name)

    def nodes_managed_by(self, manager : Manager):
        """Returns a list of nodes managed by manager"""

        return [node for node in self._nodes if node.manager == manager]

    def nodes_depending_on(self, node):
        """Returns a list of nodes that physically depend on node. Only direct dependants are obtained with a connection to the core.
        This function should be used to determine if a node can be created, deleted, exported.

        For making node-trees please use nodes_with_parent instead.

        Args:
            node : Node or node-name

        Returns:
            list of names

        See Also: nodes_with_parent
        """

        if isinstance(node, Node):
            node = node.name

        # check the node type
        _node = self[node]
        if not isinstance(_node, CoreConnectedNode):
            return []
        else:
            names = self._vfc.elements_depending_directly_on(node)

        r = []
        for name in names:
            try:
                node = self.node_by_name(name, silent=True)
                r.append(node.name)
            except:
                pass

        # check all other nodes in the scene

        for n in self._nodes:
            if _node in n.depends_on():
                if n.name not in r:
                    r.append(n.name)

        # for v in [*self.nodes_of_type(Visual), *self.nodes_of_type(WaveInteraction1)]:
        #     if v.parent is _node:
        #         r.append(v.name)

        return r

    def nodes_with_parent(self, node):
        """Returns a list of nodes that have given node as a parent. Good for making trees.
        For checking physical connections use nodes_depending_on instead.

        Args:
            node : Node or node-name

        Returns:
            list of names

        See Also: nodes_depending_on
        """

        if isinstance(node, str):
            node = self[node]

        r = []

        for n in self._nodes:

            try:
                parent = n.parent
            except AttributeError:
                continue

            if parent == node:
                r.append(n.name)

        return r

    def delete(self, node):
        """Deletes the given node from the scene as well as all nodes depending on it.

        See Also:
            dissolve
        """

        if isinstance(node, str):
            node = self[node]

        if node not in self._nodes:
            raise ValueError(
                "Can not delete node because it is not a node of this scene"
            )

        if isinstance(node, Manager):
            node.delete()
            # self._nodes.remove(node)
            # return <-- do not return

        depending_nodes = self.nodes_depending_on(node)
        depending_nodes.extend([n.name for n in node.observers])

        if node._manager:  # node, delete its manager
            # print('Deleting manager')
            self.delete(node._manager)
            if node in self._nodes:
                self.delete(node)  # node may have been deleted by the manager

        else:
            self._print(
                "Deleting {} [{}]".format(
                    node.name, str(type(node)).split(".")[-1][:-2]
                )
            )

            # First delete the dependencies
            for d in depending_nodes:
                if not self.name_available(d):  # element is still here
                    self.delete(d)

            # then remove the vtk node itself
            # self._print('removing vfc node')
            node._delete_vfc()
            self._nodes.remove(node)

    def dissolve(self, node):
        """Attempts to delete the given node without affecting the rest of the model.

        1. Look for nodes that have this node as parent
        2. Attach those nodes to the parent of this node.
        3. Delete this node.

        There are many situations in which this will fail because an it is impossible to dissolve
        the element. For example a poi can only be dissolved when nothing is attached to it.

        For now this function only works on AXIS

        #TODO: Add managers - just release management

        """

        if isinstance(node, str):
            node = self[node]

        ok = False
        if isinstance(node, Manager):

            if isinstance(node, Axis):
                p = self.new_axis(node.name + '_dissolved')
            else:
                p = None

            for d in self.nodes_managed_by(node):
                with ClaimManagement(self,node):
                    if node in d.observers:
                        d.observers.remove(node)
                    d.manager = None

                    if isinstance(d, NodeWithParent):
                        if d.parent == node:
                            d.parent = p

            ok = True

        if isinstance(node, Axis):
            for d in self.nodes_depending_on(node):
                self[d].change_parent_to(node.parent)
            ok = True

        if not ok:
            raise TypeError("Only nodes of type Axis and Manager can be dissolved at this moment")

        self._nodes.remove(node)  # do not call delete as that will fail on managers

    def savepoint_make(self):
        self._savepoint = self.give_python_code()

    def savepoint_restore(self):
        if self._savepoint is not None:
            self.clear()
            exec(self._savepoint, {}, {"s": self})
            self._savepoint = None
            return True
        else:
            return False

    # ========= The most important functions ========

    def update(self):
        """Updates the interface between the nodes and the core. This includes the re-calculation of all forces,
        buoyancy positions, ballast-system cogs etc.
        """
        for n in self._nodes:
            n.update()
        self._vfc.state_update()

    def solve_statics(self, silent=False, timeout=None):
        """Solves statics

        Args:
            silent: Do not print if successfully solved

        Returns:
            bool: True if successful, False otherwise.

        """
        self.update()

        if timeout is None:
            solve_func = self._vfc.state_solve_statics
        else:
            #       bool doStabilityCheck,
            #       double timeout,
            #           bool do_prepare_state,
            #           bool solve_linear_dofs_first,
            #           double stability_check_delta
            solve_func = lambda: self._vfc.state_solve_statics_with_timeout(
                True, timeout, True, True, 0
            )  # default stability value

        # pass 1
        orignal_fixes = self._fix_vessel_heel_trim()
        succes = solve_func()
        if not succes:
            self._restore_original_fixes(orignal_fixes)
            return False

        if orignal_fixes:
            # pass 2
            self._restore_original_fixes(orignal_fixes)
            succes = solve_func()

        if self.verify_equilibrium():

            changed, message = self._check_and_fix_geometric_contact_orientations()
            if changed:
                print(message)
                solve_func()
                if not self.verify_equilibrium():
                    return False

            if not silent:
                self._print("Solved to {}.".format(self._vfc.Emaxabs))
            return True

        d = np.array(self._vfc.get_dofs())
        if np.any(np.abs(d) > 2000):
            print(
                "Error: One of the degrees of freedom exceeded the boundary of 2000 [m]/[rad]."
            )
            return False

        return False

    def verify_equilibrium(self, tol=1e-2):
        """Checks if the current state is an equilibrium

        Returns:
            bool: True if successful, False if not an equilibrium.

        """
        self.update()
        return self._vfc.Emaxabs < tol

    # ====== goal seek ========

    def goal_seek(
        self, evaluate, target, change_node, change_property, bracket=None, tol=1e-3
    ):
        """goal_seek

        Goal seek is the classic goal-seek. It changes a single property of a single node in order to get
        some property of some node to a specified value. Just like excel.

        Args:
            evaluate : code to be evaluated to yield the value that is solved for. Eg: s['poi'].fx Scene is abbiviated as "s"
            target (number):       target value for that property
            change_node(Node or str):  node to be adjusted
            change_property (str): property of that node to be adjusted
            range(optional)  : specify the possible search-interval

        Returns:
            bool: True if successful, False otherwise.

        Examples:
            Change the y-position of the cog of a rigid body ('Barge')  in order to obtain zero roll (rx)
            >>> s.goal_seek("s['Barge'].fx",0,'Barge','cogy')

        """
        s = self

        change_node = self._node_from_node_or_str(change_node)

        # check that the attributes exist and are single numbers
        test = eval(evaluate)

        try:
            float(test)
        except:
            raise ValueError("Evaluation of {} does not result in a float")

        self._print(
            "Attempting to evaluate {} to {} (now {})".format(evaluate, target, test)
        )

        initial = getattr(change_node, change_property)
        self._print(
            "By changing the value of {}.{} (now {})".format(
                change_node.name, change_property, initial
            )
        )

        def set_and_get(x):
            setattr(change_node, change_property, x)
            self.solve_statics(silent=True)
            s = self
            result = eval(evaluate)
            self._print("setting {} results in {}".format(x, result))
            return result - target

        from scipy.optimize import root_scalar

        x0 = initial
        x1 = initial + 0.0001

        if bracket is not None:
            res = root_scalar(set_and_get, x0=x0, x1=x1, bracket=bracket, xtol=tol)
        else:
            res = root_scalar(set_and_get, x0=x0, x1=x1, xtol=tol)

        self._print(res)

        # evaluate result
        final_value = eval(evaluate)
        if abs(final_value - target) > 1e-3:
            raise ValueError(
                "Target not reached. Target was {}, reached value is {}".format(
                    target, final_value
                )
            )

        return True

    def plot_effect(self, evaluate, change_node, change_property, start, to, steps):
        """Produces a 2D plot with the relation between two properties of the scene. For example the length of a cable
        versus the force in another cable.

        The evaluate argument is processed using "eval" and may contain python code. This may be used to combine multiple
        properties to one value. For example calculate the diagonal load distribution from four independent loads.

        The plot is produced using matplotlob. The plot is produced in the current figure (if any) and plt.show is not executed.

        Args:
            evaluate (str): code to be evaluated to yield the value on the y-axis. Eg: s['poi'].fx Scene is abbiviated as "s"
            change_node(Node or str):  node to be adjusted
            change_property (str): property of that node to be adjusted
            start : left side of the interval
            to : right side of the interval
            steps : number of steps in the interval

        Returns:
            Tuple (x,y) with x and y coordinates

        Examples:
            >>> s.plot_effect("s['cable'].tension", "cable", "length", 11, 14, 10)
            >>> import matplotlib.pyplot as plt
            >>> plt.show()

        """
        s = self
        change_node = self._node_from_node_or_str(change_node)

        # check that the attributes exist and are single numbers
        test = eval(evaluate)

        try:
            float(test)
        except:
            raise ValueError("Evaluation of {} does not result in a float")

        def set_and_get(x):
            setattr(change_node, change_property, x)
            self.solve_statics(silent=True)
            s = self
            result = eval(evaluate)
            self._print("setting {} results in {}".format(x, result))
            return result

        xs = np.linspace(start, to, steps)
        y = []
        for x in xs:
            y.append(set_and_get(x))

        y = np.array(y)
        import matplotlib.pyplot as plt

        plt.plot(xs, y)
        plt.xlabel("{} of {}".format(change_property, change_node.name))
        plt.ylabel(evaluate)

        return (xs, y)

    # ======== create functions =========

    def new_axis(
        self,
        name,
        parent=None,
        position=None,
        rotation=None,
        inertia=None,
        inertia_radii=None,
        fixed=True,
    ) -> Axis:
        """Creates a new *axis* node and adds it to the scene.

        Args:
            name: Name for the node, should be unique
            parent: optional, name of the parent of the node
            position: optional, position for the node (x,y,z)
            rotation: optional, rotation for the node (rx,ry,rz)
            fixed [True]: optional, determines whether the axis is fixed [True] or free [False]. May also be a sequence of 6 booleans.

        Returns:
            Reference to newly created axis

        """

        # apply prefixes
        name = self._prefix_name(name)

        # first check
        assertValidName(name)
        self._verify_name_available(name)
        b = self._parent_from_node(parent)

        if position is not None:
            assert3f(position, "Position ")
        if rotation is not None:
            assert3f(rotation, "Rotation ")

        if inertia is not None:
            assert1f_positive_or_zero(inertia, "inertia ")

        if inertia_radii is not None:
            assert3f_positive(inertia_radii, "Radii of inertia")
            assert inertia is not None, ValueError(
                "Can not set radii of gyration without specifying inertia"
            )

        if not isinstance(fixed, bool):
            if len(fixed) != 6:
                raise Exception(
                    '"fixed" parameter should either be True/False or a 6x bool sequence such as (True,True,False,False,True,False)'
                )

        # then create
        a = self._vfc.new_axis(name)

        new_node = Axis(self, a)

        # and set properties
        if b is not None:
            new_node.parent = b
        if position is not None:
            new_node.position = position
        if rotation is not None:
            new_node.rotation = rotation
        if inertia is not None:
            new_node.inertia = inertia
        if inertia_radii is not None:
            new_node.inertia_radii = inertia_radii

        if isinstance(fixed, bool):
            if fixed:
                new_node.set_fixed()
            else:
                new_node.set_free()
        else:
            new_node.fixed = fixed

        self._nodes.append(new_node)
        return new_node

    def new_geometriccontact(
        self,
        name,
        child,
        parent,
        inside=False,
        swivel=None,
        rotation_on_parent=None,
        child_rotation=None,
        swivel_fixed=True,
        fixed_to_parent=False,
        child_fixed=False,
    ) -> GeometricContact:
        """Creates a new *new_geometriccontact* node and adds it to the scene.

        Geometric contact connects two circular elements and can be used to model bar-bar connections or pin-in-hole connections.

        By default a bar-bar connection is created between item1 and item2.

        Args:
            name: Name for the node, should be unique
            child : [Sheave] will be the nodeA of the connection
            parent : [Sheave] will be the nodeB of the connection
            inside: [False] False creates a pinpin connection. True creates a pin-hole type of connection
            swivel: Rotation angle between the two items. Defaults to 90 for pinpin and 0 for pin-hole
            rotation_on_parent: Angle of the connecting hinge relative to nodeA or None for default
            child_rotation: Angle of the nodeB relative to the connecting hinge or None for default
            swivel_fixed: Fix swivel [True]
            fixed_to_parent: Fix connecting hinge to nodeA [False]
            child_fixed: Fix nodeB to connecting hinge [False]

        Note:
            For pin-hole connections there is no geometrical difference between the pin and the hole. Therefore it is not needed to specify
            which is the pin and which is the hole

        Returns:
            Reference to newly created new_geometriccontact

        """

        # apply prefixes
        name = self._prefix_name(name)

        # first check
        assertValidName(name)
        self._verify_name_available(name)

        name_prefix = name + vfc.MANAGED_NODE_IDENTIFIER
        postfixes = [
            "_axis_on_parent",
            "_pin_hole_connection",
            "_axis_on_child",
            "_connection_axial_rotation",
        ]

        for pf in postfixes:
            self._verify_name_available(name_prefix + pf)

        child = self._sheave_from_node(child)
        parent = self._sheave_from_node(parent)

        assertBool(inside, "inside")
        assertBool(swivel_fixed, "swivel_fixed")
        assertBool(fixed_to_parent, "fixed_to_parent")
        assertBool(child_fixed, "child_fixed")

        GeometricContact._assert_parent_child_possible(parent, child)

        if swivel is None:
            if inside:
                swivel = 0
            else:
                swivel = 90

        assert1f(swivel, "swivel_angle")

        if rotation_on_parent is not None:
            assert1f(rotation_on_parent, "rotation_on_parent should be either None or ")
        if child_rotation is not None:
            assert1f(child_rotation, "child_rotation should be either None or ")

        if child is None:
            raise ValueError("child needs to be a sheave-type node")
        if parent is None:
            raise ValueError("parent needs to be a sheave-type node")

        if child.parent.parent is None:
            raise ValueError(
                f"The parent {child.parent.name} of the child item {child.name} is not located on an axis. Can not create the connection because there is no axis to nodeB"
            )

        if child.parent.parent.manager is not None:
            self.print_node_tree()
            raise ValueError(
                f"The axis or body that {child.name} is on is already managed by {child.parent.parent.manager.name} and can therefore not be changed - unable to create geometric contact"
            )

        new_node = GeometricContact(self, child, parent, name)
        if inside:
            new_node.set_pin_in_hole_connection()
        else:
            new_node.set_pin_pin_connection()

        new_node.swivel = swivel
        if rotation_on_parent is not None:
            new_node.rotation_on_parent = rotation_on_parent
        if child_rotation is not None:
            new_node.child_rotation = child_rotation

        new_node.fixed_to_parent = fixed_to_parent
        new_node.child_fixed = child_fixed
        new_node.swivel_fixed = swivel_fixed

        self._nodes.append(new_node)
        return new_node

    def new_waveinteraction(
        self,
        name,
        path,
        parent=None,
        offset=None,
    ) -> WaveInteraction1:
        """Creates a new *wave interaction* node and adds it to the scene.

        Args:
            name: Name for the node, should be unique
            path: Path to the hydrodynamic database
            parent: optional, name of the parent of the node
            offset: optional, position for the node (x,y,z)

        Returns:
            Reference to newly created wave-interaction object

        """

        if not parent:
            raise ValueError("Wave-interaction has to be located on an Axis")

        # apply prefixes
        name = self._prefix_name(name)

        # first check
        assertValidName(name)
        self._verify_name_available(name)
        b = self._parent_from_node(parent)

        if b is None:
            raise ValueError("Wave-interaction has to be located on an Axis")

        if offset is not None:
            assert3f(offset, "Offset ")

        self.get_resource_path(path)  # raises error when resource is not found

        # then create

        new_node = WaveInteraction1(self)

        new_node.name = name
        new_node.path = path
        new_node.parent = parent

        # and set properties
        new_node.parent = b
        if offset is not None:
            new_node.offset = offset

        self._nodes.append(new_node)
        return new_node

    def new_visual(
        self, name, path, parent=None, offset=None, rotation=None, scale=None
    ) -> Visual:
        """Creates a new *Visual* node and adds it to the scene.

        Args:
            name: Name for the node, should be unique
            path: Path to the resource
            parent: optional, name of the parent of the node
            offset: optional, position for the node (x,y,z)
            rotation: optional, rotation for the node (rx,ry,rz)
            scale : optional, scale of the visual (x,y,z).

        Returns:
            Reference to newly created visual

        """

        # apply prefixes
        name = self._prefix_name(name)

        # first check
        assertValidName(name)
        self._verify_name_available(name)
        b = self._parent_from_node(parent)

        if offset is not None:
            assert3f(offset, "Offset ")
        if rotation is not None:
            assert3f(rotation, "Rotation ")

        self.get_resource_path(path)  # raises error when resource is not found

        # then create

        new_node = Visual(self)

        new_node.name = name
        new_node.path = path
        new_node.parent = parent

        # and set properties
        if b is not None:
            new_node.parent = b
        if offset is not None:
            new_node.offset = offset
        if rotation is not None:
            new_node.rotation = rotation
        if scale is not None:
            new_node.scale = scale

        self._nodes.append(new_node)
        return new_node

    def new_point(self, name, parent=None, position=None) -> Point:
        """Creates a new *poi* node and adds it to the scene.

        Args:
            name: Name for the node, should be unique
            parent: optional, name of the parent of the node
            position: optional, position for the node (x,y,z)


        Returns:
            Reference to newly created poi

        """

        # apply prefixes
        name = self._prefix_name(name)

        # first check
        assertValidName(name)
        self._verify_name_available(name)
        b = self._parent_from_node(parent)

        if position is not None:
            assert3f(position, "Position ")

        # then create
        a = self._vfc.new_poi(name)

        new_node = Point(self, a)

        # and set properties
        if b is not None:
            new_node.parent = b
        if position is not None:
            new_node.position = position

        self._nodes.append(new_node)
        return new_node

    def new_rigidbody(
        self,
        name,
        mass=0,
        cog=(0, 0, 0),
        parent=None,
        position=None,
        rotation=None,
        inertia_radii=None,
        fixed=True,
    ) -> RigidBody:
        """Creates a new *rigidbody* node and adds it to the scene.

        Args:
            name: Name for the node, should be unique
            mass: optional, [0] mass in mT
            cog: optional, (0,0,0) cog-position in (m,m,m)
            parent: optional, name of the parent of the node
            position: optional, position for the node (x,y,z)
            rotation: optional, rotation for the node (rx,ry,rz)
            inertia_radii : optional, radii of gyration (rxx,ryy,rzz); only used for dynamics
            fixed [True]: optional, determines whether the axis is fixed [True] or free [False]. May also be a sequence of 6 booleans.

        Examples:
            scene.new_rigidbody("heavy_thing", mass = 10000, cog = (1.45, 0, -0.7))

        Returns:
            Reference to newly created RigidBody

        """

        # apply prefixes
        name = self._prefix_name(name)

        # check input
        assertValidName(name)
        self._verify_name_available(name)
        b = self._parent_from_node(parent)

        if position is not None:
            assert3f(position, "Position ")
        if rotation is not None:
            assert3f(rotation, "Rotation ")

        if inertia_radii is not None:
            assert3f_positive(inertia_radii, "Radii of inertia")
            assert mass > 0, ValueError(
                "Can not set radii of gyration without specifying mass"
            )

        if not isinstance(fixed, bool):
            if len(fixed) != 6:
                raise Exception(
                    '"fixed" parameter should either be True/False or a 6x bool sequence such as (True,True,False,False,True,False)'
                )

        # make elements

        a = self._vfc.new_axis(name)

        p = self._vfc.new_poi(name + vfc.VF_NAME_SPLIT + "cog")
        p.parent = a
        p.position = cog

        g = self._vfc.new_force(name + vfc.VF_NAME_SPLIT + "gravity")
        g.parent = p
        g.force = (0, 0, -vfc.G * mass)

        r = RigidBody(self, a, p, g)

        r.cog = cog  # set inertia
        r.mass = mass

        # and set properties
        if b is not None:
            r.parent = b
        if position is not None:
            r.position = position
        if rotation is not None:
            r.rotation = rotation

        if inertia_radii is not None:
            r.inertia_radii = inertia_radii

        if isinstance(fixed, bool):
            if fixed:
                r.set_fixed()
            else:
                r.set_free()
        else:
            r.fixed = fixed

        self._nodes.append(r)
        return r

    def new_cable(
        self, name, endA, endB, length=-1, EA=0, diameter=0, sheaves=None
    ) -> Cable:
        """Creates a new *cable* node and adds it to the scene.

        Args:
            name: Name for the node, should be unique
            endA : A Poi element to connect the first end of the cable to
            endB : A Poi element to connect the other end of the cable to
            length [-1] : un-stretched length of the cable in m; default [-1] create a cable with the current distance between the endpoints A and B
            EA [0] : stiffness of the cable in kN/m; default

            sheaves : [optional] A list of pois, these are sheaves that the cable runs over. Defined from endA to endB

        Examples:

            scene.new_cable('cable_name' endA='poi_start', endB = 'poi_end')  # minimal use

            scene.new_cable('cable_name', length=50, EA=1000, endA=poi_start, endB = poi_end, sheaves=[sheave1, sheave2])

            scene.new_cable('cable_name', length=50, EA=1000, endA='poi_start', endB = 'poi_end', sheaves=['single_sheave']) # also a single sheave needs to be provided as a list

        Notes:
            The default options for length and EA can be used to measure distances between points

        Returns:
            Reference to newly created Cable

        """

        # apply prefixes
        name = self._prefix_name(name)

        # first check
        assertValidName(name)
        self._verify_name_available(name)
        assert1f(length, "length")
        assert1f(EA, "EA")

        endA = self._poi_or_sheave_from_node(endA)
        endB = self._poi_or_sheave_from_node(endB)

        pois = [endA]
        if sheaves is not None:

            if isinstance(sheaves, Point):  # single sheave as poi or string
                sheaves = [sheaves]

            if isinstance(sheaves, Circle):  # single sheave as poi or string
                sheaves = [sheaves]

            if isinstance(sheaves, str):
                sheaves = [sheaves]

            for s in sheaves:
                # s may be a poi or a sheave
                pois.append(self._poi_or_sheave_from_node(s))

        pois.append(endB)

        # default options
        if length > -1:
            if length < 1e-9:
                raise Exception("Length should be more than 0")

        if EA < 0:
            raise Exception("EA should be more than 0")

        assert1f(diameter, "Diameter should be a number >= 0")

        if diameter < 0:
            raise Exception("Diameter should be >= 0")

        # then create
        a = self._vfc.new_cable(name)
        new_node = Cable(self, a)
        if length > 0:
            new_node.length = length
        new_node.EA = EA
        new_node.diameter = diameter

        new_node.connections = pois

        # and add to the scene
        self._nodes.append(new_node)

        if length < 0:
            new_node.length = 1e-8
            self._vfc.state_update()

            new_length = new_node.stretch + 1e-8

            if new_length > 0:
                new_node.length = new_length
            else:
                # is is possible that all nodes are at the same location which means the total length becomes 0
                self.delete(new_node.name)
                raise ValueError(
                    "No lengh has been supplied and all connection points are at the same location - unable to determine a non-zero default length. Please supply a length"
                )

        return new_node

    def new_force(self, name, parent=None, force=None, moment=None) -> Force:
        """Creates a new *force* node and adds it to the scene.

        Args:
            name: Name for the node, should be unique
            parent: name of the parent of the node [Poi]
            force: optional, global force on the node (x,y,z)
            moment: optional, global force on the node (x,y,z)


        Returns:
            Reference to newly created force

        """

        # apply prefixes
        name = self._prefix_name(name)

        # first check
        assertValidName(name)
        self._verify_name_available(name)
        b = self._poi_from_node(parent)

        if force is not None:
            assert3f(force, "Force ")

        if moment is not None:
            assert3f(moment, "Moment ")

        # then create
        a = self._vfc.new_force(name)

        new_node = Force(self, a)

        # and set properties
        if b is not None:
            new_node.parent = b
        if force is not None:
            new_node.force = force
        if moment is not None:
            new_node.moment = moment

        self._nodes.append(new_node)
        return new_node

    def new_circle(self, name, parent, axis, radius=0.0) -> Circle:
        """Creates a new *sheave* node and adds it to the scene.

        Args:
            name: Name for the node, should be unique
            parent: name of the parent of the node [Poi]
            axis: direction of the axis of rotation (x,y,z)
            radius: optional, radius of the sheave


        Returns:
            Reference to newly created sheave

        """

        # apply prefixes
        name = self._prefix_name(name)

        # first check
        assertValidName(name)
        self._verify_name_available(name)
        b = self._poi_from_node(parent)

        assert3f(axis, "Axis of rotation ")

        assert1f(radius, "Radius of sheave")

        # then create
        a = self._vfc.new_sheave(name)

        new_node = Circle(self, a)

        # and set properties
        new_node.parent = b
        new_node.axis = axis
        new_node.radius = radius

        self._nodes.append(new_node)
        return new_node

    def new_hydspring(
        self,
        name,
        parent,
        cob,
        BMT,
        BML,
        COFX,
        COFY,
        kHeave,
        waterline,
        displacement_kN,
    ) -> HydSpring:
        """Creates a new *hydspring* node and adds it to the scene.

        Args:
            name: Name for the node, should be unique
            parent: name of the parent of the node [Axis]
            cob: position of the CoB (x,y,z) in the parent axis system
            BMT: Vertical distance between CoB and meta-center for roll
            BML: Vertical distance between CoB and meta-center for pitch
            COFX: X-location of center of flotation (center of waterplane) relative to CoB
            COFY: Y-location of center of flotation (center of waterplane) relative to CoB
            kHeave : heave stiffness (typically Awl * rho * g)
            waterline : Z-position (elevation) of the waterline relative to CoB
            displacement_kN : displacement (typically volume * rho * g)


        Returns:
            Reference to newly created hydrostatic spring

        """

        # apply prefixes
        name = self._prefix_name(name)

        # first check
        assertValidName(name)
        self._verify_name_available(name)
        b = self._parent_from_node(parent)
        assert3f(cob, "CoB ")
        assert1f(BMT, "BMT ")
        assert1f(BML, "BML ")
        assert1f(COFX, "COFX ")
        assert1f(COFY, "COFY ")
        assert1f(kHeave, "kHeave ")
        assert1f(waterline, "waterline ")
        assert1f(displacement_kN, "displacement_kN ")

        # then create
        a = self._vfc.new_hydspring(name)
        new_node = HydSpring(self, a)

        new_node.cob = cob
        new_node.parent = b
        new_node.BMT = BMT
        new_node.BML = BML
        new_node.COFX = COFX
        new_node.COFY = COFY
        new_node.kHeave = kHeave
        new_node.waterline = waterline
        new_node.displacement_kN = displacement_kN

        self._nodes.append(new_node)

        return new_node

    def new_linear_connector_6d(self, name, main, secondary, stiffness=None) -> LC6d:
        """Creates a new *linear connector 6d* node and adds it to the scene.

        Args:
            name: Name for the node, should be unique
            main: Main axis system [Axis]
            secondary: Secondary axis system [Axis]
            stiffness: optional, connection stiffness (x,y,z, rx,ry,rz)

        See :py:class:`LC6d` for details

        Returns:
            Reference to newly created connector

        """

        # apply prefixes
        name = self._prefix_name(name)

        # first check
        assertValidName(name)
        self._verify_name_available(name)
        m = self._parent_from_node(secondary)
        s = self._parent_from_node(main)

        if stiffness is not None:
            assert6f(stiffness, "Stiffness ")
        else:
            stiffness = (0, 0, 0, 0, 0, 0)

        # then create
        a = self._vfc.new_linearconnector6d(name)

        new_node = LC6d(self, a)

        # and set properties
        new_node.main = m
        new_node.secondary = s
        new_node.stiffness = stiffness

        self._nodes.append(new_node)
        return new_node

    def new_connector2d(
        self, name, nodeA, nodeB, k_linear=0, k_angular=0
    ) -> Connector2d:
        """Creates a new *new_connector2d* node and adds it to the scene.

        Args:
            name: Name for the node, should be unique
            nodeB: First axis system [Axis]
            nodeA: Second axis system [Axis]

            k_linear : linear stiffness in kN/m
            k_angular : angular stiffness in kN*m / rad

        Returns:
            Reference to newly created connector2d

        """

        # apply prefixes
        name = self._prefix_name(name)

        # first check
        assertValidName(name)
        self._verify_name_available(name)
        m = self._parent_from_node(nodeA)
        s = self._parent_from_node(nodeB)

        assert1f(k_linear, "Linear stiffness")
        assert1f(k_angular, "Angular stiffness")

        # then create
        a = self._vfc.new_connector2d(name)

        new_node = Connector2d(self, a)

        # and set properties
        new_node.nodeA = m
        new_node.nodeB = s
        new_node.k_linear = k_linear
        new_node.k_angular = k_angular

        self._nodes.append(new_node)
        return new_node

    def new_beam(
        self,
        name,
        nodeA,
        nodeB,
        EIy=0,
        EIz=0,
        GIp=0,
        EA=0,
        L=None,
        mass=0,
        n_segments=1,
        tension_only=False,
    ) -> Beam:
        """Creates a new *beam* node and adds it to the scene.

        Args:
            name: Name for the node, should be unique
            nodeA: First axis system [Axis]
            nodeB: Second axis system [Axis]

            All stiffness terms default to 0
            The length defaults to the distance between nodeA and nodeB


        See :py:class:`LinearBeam` for details

        Returns:
            Reference to newly created beam

        """

        # apply prefixes
        name = self._prefix_name(name)

        # first check
        assertValidName(name)
        self._verify_name_available(name)
        m = self._parent_from_node(nodeA)
        s = self._parent_from_node(nodeB)

        if L is None:
            L = np.linalg.norm(
                np.array(m.global_position) - np.array(s.global_position)
            )
        else:
            if L <= 0:
                raise ValueError("L should be > 0 as stiffness is defined per length.")

        assert1f_positive_or_zero(EIy, "EIy should be >= 0")
        assert1f_positive_or_zero(EIz, "EIz should be >= 0")
        assert1f_positive_or_zero(GIp, "GIp should be >= 0")
        assert1f_positive_or_zero(EA, "EA should be >= 0")
        assertBool(tension_only, "tension_only should be bool")
        assert1f(mass, "Mass shall be a number")
        n_segments = int(round(n_segments))

        # then create
        a = self._vfc.new_linearbeam(name)

        new_node = Beam(self, a)

        # and set properties
        new_node.nodeA = m
        new_node.nodeB = s
        new_node.EIy = EIy
        new_node.EIz = EIz
        new_node.GIp = GIp
        new_node.EA = EA
        new_node.L = L
        new_node.mass = mass
        new_node.n_segments = n_segments
        new_node.tension_only = tension_only

        self._nodes.append(new_node)
        return new_node

    def new_buoyancy(self, name, parent=None, density=1.025) -> Buoyancy:
        """Creates a new *buoyancy* node and adds it to the scene.

        Args:
            name: Name for the node, should be unique
            parent: optional, name of the parent of the node


        Returns:
            Reference to newly created buoyancy

        """

        # apply prefixes
        name = self._prefix_name(name)

        # first check
        assertValidName(name)
        self._verify_name_available(name)
        b = self._parent_from_node(parent)

        if b is None:
            raise ValueError("A valid parent must be defined for a Buoyancy node")

        assert1f_positive_or_zero(density, "density")

        # then create
        a = self._vfc.new_buoyancy(name)
        new_node = Buoyancy(self, a)

        # and set properties
        if b is not None:
            new_node.parent = b

        new_node.density = density

        self._nodes.append(new_node)
        return new_node

    def new_tank(self, name, parent=None, density=1.025, free_flooding=False) -> Tank:
        """Creates a new *tank* node and adds it to the scene.

        Args:
            name: Name for the node, should be unique
            parent: optional, name of the parent of the node

        Returns:
            Reference to newly created Tank

        """

        # apply prefixes
        name = self._prefix_name(name)

        # first check
        assertValidName(name)
        self._verify_name_available(name)
        b = self._parent_from_node(parent)

        if b is None:
            raise ValueError("A valid parent must be defined for a Tank")

        assert isinstance(free_flooding, bool), ValueError(
            "free_flooding shall be True or False"
        )

        assert1f(density, "density")

        # then create
        a = self._vfc.new_tank(name)
        new_node = Tank(self, a)
        new_node.density = density

        # and set properties
        if b is not None:
            new_node.parent = b

        new_node.free_flooding = free_flooding

        self._nodes.append(new_node)
        return new_node

    def new_contactmesh(self, name, parent=None) -> ContactMesh:
        """Creates a new *contactmesh* node and adds it to the scene.

        Args:
            name: Name for the node, should be unique
            parent: optional, name of the parent of the node

        Returns:
            Reference to newly created contact mesh

        """

        # apply prefixes
        name = self._prefix_name(name)

        # first check
        assertValidName(name)
        self._verify_name_available(name)
        b = self._parent_from_node(parent)

        # then create
        a = self._vfc.new_contactmesh(name)
        new_node = ContactMesh(self, a)

        # and set properties
        if b is not None:
            new_node.parent = b

        self._nodes.append(new_node)
        return new_node

    def new_spmt(
        self,
        name,
        parent,
        maximal_length=1.8,
        nominal_length=1.5,
        k=1e6,
        meshes=None,
        axles=None,
    ) -> SPMT:
        """Creates a new *SPMT* node and adds it to the scene.

        Args:
            name: Name for the node, should be unique
            parent: name of the parent of the node [Axis]
            maximal_length: optional, maximum distance between top and bottom of wheel (1.5m + 300mm)
            nominal_length: optional, nominal distance between top and bottom of wheel [1.5m]
            k : stiffness per axle [kN/m]
            meshes : list of contact meshes
            axles  : list of axle locations [(x,y,z),(x,y,z), ... ]

        Returns:
            Reference to newly created SPMT

        """

        # apply prefixes
        name = self._prefix_name(name)

        # first check
        assertValidName(name)
        self._verify_name_available(name)
        parent = self._node_from_node_or_str(parent)
        assert isinstance(parent, Axis), ValueError(
            f"Parent should be an axis system or derived, not a {type(parent)}"
        )

        assert1f_positive_or_zero(maximal_length, "maximal_length ")
        assert1f_positive_or_zero(nominal_length, "nominal_length ")

        if meshes is not None:
            meshes = make_iterable(meshes)
            for mesh in meshes:
                test = self._node_from_node(
                    mesh, ContactMesh
                )  # throws error if not found

        if axles is not None:
            for p in axles:
                assert3f(p, "axle locations should be (x,y,z)")

        # then create
        a = self._vfc.new_spmt(name)

        new_node = SPMT(self, a)

        # and set properties
        new_node.parent = parent
        new_node.k = k
        new_node.max_length = maximal_length
        new_node.nominal_length = nominal_length

        if meshes is not None:
            new_node.meshes = meshes

        if axles is not None:
            new_node.axles = axles

        self._nodes.append(new_node)
        return new_node

    def new_contactball(
        self, name, parent=None, radius=1, k=9999, meshes=None
    ) -> ContactBall:
        """Creates a new *force* node and adds it to the scene.

        Args:
            name: Name for the node, should be unique
            parent: name of the parent of the node [Poi]
            force: optional, global force on the node (x,y,z)
            moment: optional, global force on the node (x,y,z)


        Returns:
            Reference to newly created force

        """

        # apply prefixes
        name = self._prefix_name(name)

        # first check
        assertValidName(name)
        self._verify_name_available(name)
        b = self._poi_from_node(parent)

        assert1f_positive_or_zero(radius, "Radius ")
        assert1f_positive_or_zero(k, "k ")

        if meshes is not None:
            meshes = make_iterable(meshes)
            for mesh in meshes:
                test = self._node_from_node(mesh, ContactMesh)

        # then create
        a = self._vfc.new_contactball(name)

        new_node = ContactBall(self, a)

        # and set properties
        if b is not None:
            new_node.parent = b
        if k is not None:
            new_node.k = k
        if radius is not None:
            new_node.radius = radius

        if meshes is not None:
            new_node.meshes = meshes

        self._nodes.append(new_node)
        return new_node

    def new_ballastsystem(self, name, parent: Axis) -> BallastSystem:
        """Creates a new *rigidbody* node and adds it to the scene.

        Args:
            name: Name for the node, should be unique
            parent: name of the parent of the ballast system (ie: the vessel axis system)

        Examples:
            scene.new_ballastsystem("cheetah_ballast", parent="Cheetah")

        Returns:
            Reference to newly created BallastSystem

        """

        # apply prefixes
        name = self._prefix_name(name)

        # check input
        assertValidName(name)
        self._verify_name_available(name)
        b = self._parent_from_node(parent)

        parent = self._parent_from_node(parent)  # handles verification of type as well

        # make elements
        r = BallastSystem(self, parent)
        r.name = name

        self._nodes.append(r)
        return r

    def new_sling(
        self,
        name,
        length=-1,
        EA=1.0,
        mass=0.1,
        endA=None,
        endB=None,
        LeyeA=None,
        LeyeB=None,
        LspliceA=None,
        LspliceB=None,
        diameter=0.1,
        sheaves=None,
    ) -> Sling:
        """
        Creates a new sling, adds it to the scene and returns a reference to the newly created object.

        See Also:
            Sling

        Args:
            name:    name
            length:  length of the sling [m], defaults to distance between endpoints
            EA:      stiffness in kN, default: 1.0 (note: equilibrium will fail if mass >0 and EA=0)
            mass:    mass in mT, default  0.1
            endA:    element to connect end A to [poi, circle]
            endB:    element to connect end B to [poi, circle]
            LeyeA:   inside eye on side A length [m], defaults to 1/6th of length
            LeyeB:   inside eye on side B length [m], defaults to 1/6th of length
            LspliceA: splice length on side A [m] (the part where the cable is connected to itself)
            LspliceB: splice length on side B [m] (the part where the cable is connected to itself)
            diameter: cable diameter in m, defaul to 0.1
            sheaves:  optional: list of sheaves/pois that the sling runs over

        Returns:
            a reference to the newly created Sling object.

        """

        # apply prefixes
        name = self._prefix_name(name)

        # first check
        assertValidName(name)
        self._verify_name_available(name)

        name_prefix = name + vfc.MANAGED_NODE_IDENTIFIER
        postfixes = [
            "_spliceA",
            "_spliceA",
            "_spliceA2",
            "_spliceAM",
            "_spliceA_visual",
            "spliceB",
            "_spliceB1",
            "_spliceB2",
            "_spliceBM",
            "_spliceB_visual",
            "_main_part",
            "_eyeA",
            "_eyeB",
        ]

        for pf in postfixes:
            self._verify_name_available(name_prefix + pf)

        endA = self._poi_or_sheave_from_node(endA)
        endB = self._poi_or_sheave_from_node(endB)

        if length == -1:  # default
            if endA is None or endB is None:
                raise ValueError(
                    "Length for cable is not provided, so defaults to distance between endpoints; but at least one of the endpoints is None."
                )

            length = np.linalg.norm(
                np.array(endA.global_position) - np.array(endB.global_position)
            )

        if LeyeA is None:  # default
            LeyeA = length / 6
        if LeyeB is None:  # default
            LeyeB = length / 6
        if LspliceA is None:  # default
            LspliceA = length / 6
        if LspliceB is None:  # default
            LspliceB = length / 6

        if sheaves is None:
            sheaves = []

        assert1f_positive_or_zero(diameter, "Diameter")
        assert1f_positive_or_zero(mass, "mass")

        assert1f_positive(length, "Length")
        assert1f_positive(LeyeA, "length of eye A")
        assert1f_positive(LeyeB, "length of eye B")
        assert1f_positive(LspliceA, "length of splice A")
        assert1f_positive(LspliceB, "length of splice B")

        for s in sheaves:
            _ = self._poi_or_sheave_from_node(s)

        # then make element
        # __init__(self, scene, name, Ltotal, LeyeA, LeyeB, LspliceA, LspliceB, diameter, EA, mass, endA = None, endB=None, sheaves=None):

        node = Sling(
            scene=self,
            name=name,
            length=length,
            LeyeA=LeyeA,
            LeyeB=LeyeB,
            LspliceA=LspliceA,
            LspliceB=LspliceB,
            diameter=diameter,
            EA=EA,
            mass=mass,
            endA=endA,
            endB=endB,
            sheaves=sheaves,
        )
        self._nodes.append(node)

        return node

    def new_shackle(self, name, kind="GP500") -> Shackle:
        """
        Creates a new shackle, adds it to the scene and returns a reference to the newly created object.

        See Also:
            Shackle

        Args:
            name:   name
            kind:  type of shackle; eg 'GP500'


        Returns:
            a reference to the newly created Shackle object.

        """

        # apply prefixes
        name = self._prefix_name(name)

        # first check
        assertValidName(name)
        self._verify_name_available(name)

        name_prefix = name + vfc.MANAGED_NODE_IDENTIFIER
        postfixes = [
            "_body",
            "_pin_point",
            "_bow_point",
            "_inside_circle_center",
            "_inside",
            "_visual",
        ]
        for pf in postfixes:
            self._verify_name_available(name_prefix + pf)

        # then make element

        # make elements

        a = self._vfc.new_axis(name)

        p = self._vfc.new_poi(name + vfc.VF_NAME_SPLIT + "cog")
        p.parent = a

        g = self._vfc.new_force(name + vfc.VF_NAME_SPLIT + "gravity")
        g.parent = p

        node = Shackle(scene=self, name=name, kind=kind, a=a, p=p, g=g)

        self._nodes.append(node)

        return node

    def print_python_code(self):
        """Prints the python code that generates the current scene

        See also: give_python_code
        """
        for line in self.give_python_code().split("\n"):
            print(line)

    def give_python_code(self):
        """Generates the python code that rebuilds the scene and elements in its current state."""

        import datetime
        import getpass

        self.sort_nodes_by_dependency()

        code = "# auto generated pyhton code"
        try:
            code += "\n# By {}".format(getpass.getuser())
        except:
            code += "\n# By an unknown"

        code += "\n# Time: {} UTC".format(str(datetime.datetime.now()).split(".")[0])

        code += "\n\n# To be able to distinguish the important number (eg: fixed positions) from"
        code += "\n# non-important numbers (eg: a position that is solved by the static solver) we use a dummy-function called 'solved'."
        code += "\n# For anything written as solved(number) that actual number does not influence the static solution"
        code += "\ndef solved(number):\n    return number\n"

        for n in self._nodes:

            if n._manager is None:
                # print(f'code for {n.name}')
                code += "\n" + n.give_python_code()
            else:
                if n._manager.creates(n):
                    pass
                else:
                    code += "\n" + n.give_python_code()

                # print(f'skipping {n.name} ')

        # store the visibility code separately

        for n in self._nodes:
            if not n.visible:
                code += f"\ns['{n.name}'].visible = False"  # only report is not the default value

        return code

    def save_scene(self, filename):
        """Saves the scene to a file

        This saves the scene in its current state to a file.
        Opening the saved file will reproduce exactly this scene.

        This sounds nice, but beware that it only saves the resulting model, not the process of creating the model.
        This means that if you created the model in a parametric fashion or assembled the model from other models then these are not re-evaluated when the model is openened again.
        So lets say this model uses a sub-model of a lifting hook which is imported from another file. If that other file is updated then
        the results of that update will not be reflected in the saved model.

        If no path is present in the file-name then the model will be saved in the last (lowest) resource-path (if any)

        Args:
            filename : filename or file-path to save the file. Default extension is .dave

        Returns:
            the full path to the saved file

        """

        code = self.give_python_code()

        filename = Path(filename)

        # add .dave extension if needed
        if filename.suffix != ".dave":
            filename = Path(str(filename) + ".dave")

        # add path if not provided
        if not filename.is_absolute():
            try:
                filename = Path(self.resources_paths[-1]) / filename
            except:
                pass  # save in current folder

        # make sure directory exists
        directory = filename.parent
        if not directory.exists():
            directory.mkdir()

        f = open(filename, "w+")
        f.write(code)
        f.close()

        self._print("Saved as {}".format(filename))

        return filename

    def print_node_tree(self):

        self.sort_nodes_by_dependency()

        to_be_printed = []
        for n in self._nodes:
            to_be_printed.append(n.name)

        # to_be_printed.reverse()

        def print_deps(name, spaces):

            node = self[name]
            deps = self.nodes_with_parent(node)
            print(spaces + name + " [" + str(type(node)).split(".")[-1][:-2] + "]")

            if deps is not None:
                for dep in deps:
                    if spaces == "":
                        spaces_plus = " |-> "
                    else:
                        spaces_plus = " |   " + spaces
                    print_deps(dep, spaces_plus)

            to_be_printed.remove(name)

        while to_be_printed:
            name = to_be_printed[0]
            print_deps(name, "")

    def run_code(self, code):
        """Runs the provided code with 's' as self"""

        import DAVE

        locals = DAVE.__dict__
        locals['s'] = self

        try:
            exec(code, {}, locals)
        except Exception as M:
            for i, line in enumerate(code.split("\n")):
                print(f"{i} {line}")
            raise M

    def load_scene(self, filename=None):
        """Loads the contents of filename into the current scene.

        This function is typically used on an empty scene.

        Filename is appended with .dave if needed.
        File is searched for in the resource-paths.

        See also: import scene"""

        if filename is None:
            raise Exception("Please provide a file-name")

        try:
            filename = self.get_resource_path(filename)
        except:
            if not str(filename).endswith(".dave"):
                filename = Path(str(filename) + ".dave")

        print("Loading {}".format(filename))

        f = open(file=filename, mode="r")
        code = ""
        for line in f:
            code += line + "\n"

        self.run_code(code)

    def import_scene(self, other, prefix="", containerize=True):
        """Copy-paste all nodes of scene "other" into current scene.

        To avoid double names it is recommended to use a prefix. This prefix will be added to all element names.

        Returns:
            Contained (Axis-type Node) : if the imported scene is containerized then a reference to the created container is returned.
        """

        if isinstance(other, Path):
            other = str(other)

        if isinstance(other, str):
            other = Scene(other)

        if not isinstance(other, Scene):
            raise TypeError("Other should be a Scene but is a " + str(type(other)))

        old_prefix = self._name_prefix
        imported_element_names = []

        for n in other._nodes:
            imported_element_names.append(prefix + n.name)

        # check for double names

        for new_node_name in imported_element_names:
            if not self.name_available(new_node_name):
                raise NameError(
                    'An element with name "{}" is already present. Please use a prefix to avoid double names'.format(
                        new_node_name
                    )
                )

        self._name_prefix = prefix

        code = other.give_python_code()

        self.run_code(code)

        self._name_prefix = old_prefix  # restore

        # Move all imported elements without a parent into a newly created axis system
        if containerize:

            container_name = self.available_name_like("import_container")

            c = self.new_axis(prefix + container_name)

            for name in imported_element_names:

                node = self[name]

                if not node.manager:
                    if not isinstance(node, NodeWithParent):
                        continue

                    if node.parent is None:
                        node.change_parent_to(c)

            return c

        return None

    def copy(self):
        """Creates a full and independent copy of the scene and returns it.

        Example:
            s = Scene()
            c = s.copy()
            c.new_axis('only in c')

        """

        c = Scene()
        c.import_scene(self, containerize=False)
        return c

    # =================== DYNAMICS ==================

    def dynamics_M(self, delta=1e-6):
        """Returns the mass matrix of the scene"""
        self.update()

        return self._vfc.M(delta)

    def dynamics_K(self, delta=1e-6):
        """Returns the stiffness matrix of the scene for a perturbation of delta

        A component is positive if a displacement introduces an reaction force in the opposite direction.
        or:
        A component is positive if a positive force is needed to introduce a positive displacement.
        """
        self.update()

        return -self._vfc.K(delta)

    def dynamics_nodes(self):
        """Returns a list of nodes associated with the rows/columns of M and K"""
        self.update()
        nodes = self._vfc.get_dof_elements()

        node_names = [n.name for n in self._nodes]

        r = []
        for n in nodes:
            if n.name in node_names:
                r.append(self[n.name])
            else:
                r.append(None)

        return r

    def dynamics_modes(self):
        """Returns a list of modes (0=x ... 5=rotation z) associated with the rows/columns of M and K"""
        self.update()
        return self._vfc.get_dof_modes()

Instance variables

var current_manager

Setting this to an instance of a Manager allows nodes with that manager to be changed

var resources_paths

A list of paths where to look for resources such as .obj files. Priority is given to paths earlier in the list.

var static_tolerance

Desired tolerance when solving statics

var verbose

Report actions using print()

Methods

def assert_unique_names(self)

Asserts that all names are unique

Expand source code
def assert_unique_names(self):
    """Asserts that all names are unique"""
    names = [n.name for n in self._nodes]
    unique_names = set(names)

    if len(unique_names) != len(names):
        previous_name = ""
        names.sort()
        duplicates = ""
        for name in names:
            if name == previous_name:
                print(f"Duplicate: {name}")
                duplicates += name + " "

                for n in self._nodes:
                    if n.name == name:
                        print(n)

            previous_name = name
        raise ValueError(f"Duplicate names exist: " + duplicates)
def available_name_like(self, like)

Returns an available name like the one given, for example Axis23

Expand source code
def available_name_like(self, like):
    """Returns an available name like the one given, for example Axis23"""
    if self.name_available(like):
        return like
    counter = 1
    while True:
        name = like + "_" + str(counter)
        if self.name_available(name):
            return name
        counter += 1
def clear(self)

Deletes all nodes

Expand source code
def clear(self):
    """Deletes all nodes"""

    self._nodes = []
    del self._vfc
    self._vfc = pyo3d.Scene()
def copy(self)

Creates a full and independent copy of the scene and returns it.

Example

s = Scene() c = s.copy() c.new_axis('only in c')

Expand source code
def copy(self):
    """Creates a full and independent copy of the scene and returns it.

    Example:
        s = Scene()
        c = s.copy()
        c.new_axis('only in c')

    """

    c = Scene()
    c.import_scene(self, containerize=False)
    return c
def delete(self, node)

Deletes the given node from the scene as well as all nodes depending on it.

See Also: dissolve

Expand source code
def delete(self, node):
    """Deletes the given node from the scene as well as all nodes depending on it.

    See Also:
        dissolve
    """

    if isinstance(node, str):
        node = self[node]

    if node not in self._nodes:
        raise ValueError(
            "Can not delete node because it is not a node of this scene"
        )

    if isinstance(node, Manager):
        node.delete()
        # self._nodes.remove(node)
        # return <-- do not return

    depending_nodes = self.nodes_depending_on(node)
    depending_nodes.extend([n.name for n in node.observers])

    if node._manager:  # node, delete its manager
        # print('Deleting manager')
        self.delete(node._manager)
        if node in self._nodes:
            self.delete(node)  # node may have been deleted by the manager

    else:
        self._print(
            "Deleting {} [{}]".format(
                node.name, str(type(node)).split(".")[-1][:-2]
            )
        )

        # First delete the dependencies
        for d in depending_nodes:
            if not self.name_available(d):  # element is still here
                self.delete(d)

        # then remove the vtk node itself
        # self._print('removing vfc node')
        node._delete_vfc()
        self._nodes.remove(node)
def dissolve(self, node)

Attempts to delete the given node without affecting the rest of the model.

  1. Look for nodes that have this node as parent
  2. Attach those nodes to the parent of this node.
  3. Delete this node.

There are many situations in which this will fail because an it is impossible to dissolve the element. For example a poi can only be dissolved when nothing is attached to it.

For now this function only works on AXIS

TODO: Add managers - just release management

Expand source code
def dissolve(self, node):
    """Attempts to delete the given node without affecting the rest of the model.

    1. Look for nodes that have this node as parent
    2. Attach those nodes to the parent of this node.
    3. Delete this node.

    There are many situations in which this will fail because an it is impossible to dissolve
    the element. For example a poi can only be dissolved when nothing is attached to it.

    For now this function only works on AXIS

    #TODO: Add managers - just release management

    """

    if isinstance(node, str):
        node = self[node]

    ok = False
    if isinstance(node, Manager):

        if isinstance(node, Axis):
            p = self.new_axis(node.name + '_dissolved')
        else:
            p = None

        for d in self.nodes_managed_by(node):
            with ClaimManagement(self,node):
                if node in d.observers:
                    d.observers.remove(node)
                d.manager = None

                if isinstance(d, NodeWithParent):
                    if d.parent == node:
                        d.parent = p

        ok = True

    if isinstance(node, Axis):
        for d in self.nodes_depending_on(node):
            self[d].change_parent_to(node.parent)
        ok = True

    if not ok:
        raise TypeError("Only nodes of type Axis and Manager can be dissolved at this moment")

    self._nodes.remove(node)  # do not call delete as that will fail on managers
def dynamics_K(self, delta=1e-06)

Returns the stiffness matrix of the scene for a perturbation of delta

A component is positive if a displacement introduces an reaction force in the opposite direction. or: A component is positive if a positive force is needed to introduce a positive displacement.

Expand source code
def dynamics_K(self, delta=1e-6):
    """Returns the stiffness matrix of the scene for a perturbation of delta

    A component is positive if a displacement introduces an reaction force in the opposite direction.
    or:
    A component is positive if a positive force is needed to introduce a positive displacement.
    """
    self.update()

    return -self._vfc.K(delta)
def dynamics_M(self, delta=1e-06)

Returns the mass matrix of the scene

Expand source code
def dynamics_M(self, delta=1e-6):
    """Returns the mass matrix of the scene"""
    self.update()

    return self._vfc.M(delta)
def dynamics_modes(self)

Returns a list of modes (0=x … 5=rotation z) associated with the rows/columns of M and K

Expand source code
def dynamics_modes(self):
    """Returns a list of modes (0=x ... 5=rotation z) associated with the rows/columns of M and K"""
    self.update()
    return self._vfc.get_dof_modes()
def dynamics_nodes(self)

Returns a list of nodes associated with the rows/columns of M and K

Expand source code
def dynamics_nodes(self):
    """Returns a list of nodes associated with the rows/columns of M and K"""
    self.update()
    nodes = self._vfc.get_dof_elements()

    node_names = [n.name for n in self._nodes]

    r = []
    for n in nodes:
        if n.name in node_names:
            r.append(self[n.name])
        else:
            r.append(None)

    return r
def get_resource_list(self, extension)

Returns a list of all file-paths (strings) given extension in any of the resource-paths

Expand source code
def get_resource_list(self, extension):
    """Returns a list of all file-paths (strings) given extension in any of the resource-paths"""

    r = []

    for dir in self.resources_paths:
        try:
            files = listdir(dir)
            for file in files:
                if file.lower().endswith(extension):
                    if file not in r:
                        r.append("res: " + file)
        except FileNotFoundError:
            pass

    return r
def get_resource_path(self, url) -> pathlib.Path

Resolves the path on disk for resource url. Urls statring with res: result in a file from the resources system.

Looks for a file with "name" in the specified resource-paths and returns the full path to the the first one that is found. If name is a full path to an existing file, then that is returned.

See Also: resource_paths

Returns

Full path to resource
 

Raises

FileExistsError if resource is not found
 
Expand source code
def get_resource_path(self, url) -> Path:
    """Resolves the path on disk for resource url. Urls statring with res: result in a file from the resources system.

    Looks for a file with "name" in the specified resource-paths and returns the full path to the the first one
    that is found.
    If name is a full path to an existing file, then that is returned.

    See Also:
        resource_paths


    Returns:
        Full path to resource

    Raises:
        FileExistsError if resource is not found

    """

    # warning and work-around for backwards compatibility
    # filenames without a path get res: in front of it
    try:
        if isinstance(url, Path):
            test = str(url)
        else:
            test = url

        if not test.startswith("res:"):
            test = Path(test)
            if str(test.parent) == ".":
                # from warnings import warn
                #
                # warn(
                #     f'Resources should start with res: --> fixing "{url}" to "res: {url}"'
                # )
                url = "res: " + str(test)
    except:
        pass

    if isinstance(url, Path):
        file = url
    elif isinstance(url, str):
        if not url.startswith("res:"):
            file = Path(url)
        else:
            # we have a string starting with 'res:'
            filename = url[4:].strip()

            for res in self.resources_paths:
                p = Path(res)

                file = p / filename
                if isfile(file):
                    return file

            # prepare feedback for error
            ext = str(url).split(".")[-1]  # everything after the last .

            print("Resource folders:")
            for res in self.resources_paths:
                print(str(res))


            print(
                "The following resources with extension {} are available with ".format(
                    ext
                )
            )
            available = self.get_resource_list(ext)
            for a in available:
                print(a)
            raise FileExistsError(
                'Resource "{}" not found in resource paths. A list with available resources with this extension is printed above this error'.format(
                    url
                )
            )
    else:
        raise ValueError(
            f"Provided url shall be a Path or a string, not a {type(url)}"
        )

    if file.exists():
        return file

    raise FileExistsError(
        'File "{}" not found.\nHint: To obtain a resource put res: in front of the name.'.format(
            url
        )
    )
def give_python_code(self)

Generates the python code that rebuilds the scene and elements in its current state.

Expand source code
def give_python_code(self):
    """Generates the python code that rebuilds the scene and elements in its current state."""

    import datetime
    import getpass

    self.sort_nodes_by_dependency()

    code = "# auto generated pyhton code"
    try:
        code += "\n# By {}".format(getpass.getuser())
    except:
        code += "\n# By an unknown"

    code += "\n# Time: {} UTC".format(str(datetime.datetime.now()).split(".")[0])

    code += "\n\n# To be able to distinguish the important number (eg: fixed positions) from"
    code += "\n# non-important numbers (eg: a position that is solved by the static solver) we use a dummy-function called 'solved'."
    code += "\n# For anything written as solved(number) that actual number does not influence the static solution"
    code += "\ndef solved(number):\n    return number\n"

    for n in self._nodes:

        if n._manager is None:
            # print(f'code for {n.name}')
            code += "\n" + n.give_python_code()
        else:
            if n._manager.creates(n):
                pass
            else:
                code += "\n" + n.give_python_code()

            # print(f'skipping {n.name} ')

    # store the visibility code separately

    for n in self._nodes:
        if not n.visible:
            code += f"\ns['{n.name}'].visible = False"  # only report is not the default value

    return code
def goal_seek(self, evaluate, target, change_node, change_property, bracket=None, tol=0.001)

goal_seek

Goal seek is the classic goal-seek. It changes a single property of a single node in order to get some property of some node to a specified value. Just like excel.

Args

evaluate : code to be evaluated to yield the value that is solved for. Eg: s['poi'].fx Scene is abbiviated as "s"
 
target : number

target value for that property

change_node(Node or str): node to be adjusted
change_property : str
property of that node to be adjusted

range(optional) : specify the possible search-interval

Returns

bool
True if successful, False otherwise.

Examples

Change the y-position of the cog of a rigid body ('Barge') in order to obtain zero roll (rx)

>>> s.goal_seek("s['Barge'].fx",0,'Barge','cogy')
Expand source code
def goal_seek(
    self, evaluate, target, change_node, change_property, bracket=None, tol=1e-3
):
    """goal_seek

    Goal seek is the classic goal-seek. It changes a single property of a single node in order to get
    some property of some node to a specified value. Just like excel.

    Args:
        evaluate : code to be evaluated to yield the value that is solved for. Eg: s['poi'].fx Scene is abbiviated as "s"
        target (number):       target value for that property
        change_node(Node or str):  node to be adjusted
        change_property (str): property of that node to be adjusted
        range(optional)  : specify the possible search-interval

    Returns:
        bool: True if successful, False otherwise.

    Examples:
        Change the y-position of the cog of a rigid body ('Barge')  in order to obtain zero roll (rx)
        >>> s.goal_seek("s['Barge'].fx",0,'Barge','cogy')

    """
    s = self

    change_node = self._node_from_node_or_str(change_node)

    # check that the attributes exist and are single numbers
    test = eval(evaluate)

    try:
        float(test)
    except:
        raise ValueError("Evaluation of {} does not result in a float")

    self._print(
        "Attempting to evaluate {} to {} (now {})".format(evaluate, target, test)
    )

    initial = getattr(change_node, change_property)
    self._print(
        "By changing the value of {}.{} (now {})".format(
            change_node.name, change_property, initial
        )
    )

    def set_and_get(x):
        setattr(change_node, change_property, x)
        self.solve_statics(silent=True)
        s = self
        result = eval(evaluate)
        self._print("setting {} results in {}".format(x, result))
        return result - target

    from scipy.optimize import root_scalar

    x0 = initial
    x1 = initial + 0.0001

    if bracket is not None:
        res = root_scalar(set_and_get, x0=x0, x1=x1, bracket=bracket, xtol=tol)
    else:
        res = root_scalar(set_and_get, x0=x0, x1=x1, xtol=tol)

    self._print(res)

    # evaluate result
    final_value = eval(evaluate)
    if abs(final_value - target) > 1e-3:
        raise ValueError(
            "Target not reached. Target was {}, reached value is {}".format(
                target, final_value
            )
        )

    return True
def import_scene(self, other, prefix='', containerize=True)

Copy-paste all nodes of scene "other" into current scene.

To avoid double names it is recommended to use a prefix. This prefix will be added to all element names.

Returns

Contained (Axis-type Node) : if the imported scene is containerized then a reference to the created container is returned.

Expand source code
def import_scene(self, other, prefix="", containerize=True):
    """Copy-paste all nodes of scene "other" into current scene.

    To avoid double names it is recommended to use a prefix. This prefix will be added to all element names.

    Returns:
        Contained (Axis-type Node) : if the imported scene is containerized then a reference to the created container is returned.
    """

    if isinstance(other, Path):
        other = str(other)

    if isinstance(other, str):
        other = Scene(other)

    if not isinstance(other, Scene):
        raise TypeError("Other should be a Scene but is a " + str(type(other)))

    old_prefix = self._name_prefix
    imported_element_names = []

    for n in other._nodes:
        imported_element_names.append(prefix + n.name)

    # check for double names

    for new_node_name in imported_element_names:
        if not self.name_available(new_node_name):
            raise NameError(
                'An element with name "{}" is already present. Please use a prefix to avoid double names'.format(
                    new_node_name
                )
            )

    self._name_prefix = prefix

    code = other.give_python_code()

    self.run_code(code)

    self._name_prefix = old_prefix  # restore

    # Move all imported elements without a parent into a newly created axis system
    if containerize:

        container_name = self.available_name_like("import_container")

        c = self.new_axis(prefix + container_name)

        for name in imported_element_names:

            node = self[name]

            if not node.manager:
                if not isinstance(node, NodeWithParent):
                    continue

                if node.parent is None:
                    node.change_parent_to(c)

        return c

    return None
def load_scene(self, filename=None)

Loads the contents of filename into the current scene.

This function is typically used on an empty scene.

Filename is appended with .dave if needed. File is searched for in the resource-paths.

See also: import scene

Expand source code
def load_scene(self, filename=None):
    """Loads the contents of filename into the current scene.

    This function is typically used on an empty scene.

    Filename is appended with .dave if needed.
    File is searched for in the resource-paths.

    See also: import scene"""

    if filename is None:
        raise Exception("Please provide a file-name")

    try:
        filename = self.get_resource_path(filename)
    except:
        if not str(filename).endswith(".dave"):
            filename = Path(str(filename) + ".dave")

    print("Loading {}".format(filename))

    f = open(file=filename, mode="r")
    code = ""
    for line in f:
        code += line + "\n"

    self.run_code(code)
def name_available(self, name)

Returns True if the name is still available

Expand source code
def name_available(self, name):
    """Returns True if the name is still available"""
    names = [n.name for n in self._nodes]
    names.extend(self._vfc.names)
    return not (name in names)
def new_axis(self, name, parent=None, position=None, rotation=None, inertia=None, inertia_radii=None, fixed=True) -> Axis

Creates a new axis node and adds it to the scene.

Args

name
Name for the node, should be unique
parent
optional, name of the parent of the node
position
optional, position for the node (x,y,z)
rotation
optional, rotation for the node (rx,ry,rz)

fixed [True]: optional, determines whether the axis is fixed [True] or free [False]. May also be a sequence of 6 booleans.

Returns

Reference to newly created axis
 
Expand source code
def new_axis(
    self,
    name,
    parent=None,
    position=None,
    rotation=None,
    inertia=None,
    inertia_radii=None,
    fixed=True,
) -> Axis:
    """Creates a new *axis* node and adds it to the scene.

    Args:
        name: Name for the node, should be unique
        parent: optional, name of the parent of the node
        position: optional, position for the node (x,y,z)
        rotation: optional, rotation for the node (rx,ry,rz)
        fixed [True]: optional, determines whether the axis is fixed [True] or free [False]. May also be a sequence of 6 booleans.

    Returns:
        Reference to newly created axis

    """

    # apply prefixes
    name = self._prefix_name(name)

    # first check
    assertValidName(name)
    self._verify_name_available(name)
    b = self._parent_from_node(parent)

    if position is not None:
        assert3f(position, "Position ")
    if rotation is not None:
        assert3f(rotation, "Rotation ")

    if inertia is not None:
        assert1f_positive_or_zero(inertia, "inertia ")

    if inertia_radii is not None:
        assert3f_positive(inertia_radii, "Radii of inertia")
        assert inertia is not None, ValueError(
            "Can not set radii of gyration without specifying inertia"
        )

    if not isinstance(fixed, bool):
        if len(fixed) != 6:
            raise Exception(
                '"fixed" parameter should either be True/False or a 6x bool sequence such as (True,True,False,False,True,False)'
            )

    # then create
    a = self._vfc.new_axis(name)

    new_node = Axis(self, a)

    # and set properties
    if b is not None:
        new_node.parent = b
    if position is not None:
        new_node.position = position
    if rotation is not None:
        new_node.rotation = rotation
    if inertia is not None:
        new_node.inertia = inertia
    if inertia_radii is not None:
        new_node.inertia_radii = inertia_radii

    if isinstance(fixed, bool):
        if fixed:
            new_node.set_fixed()
        else:
            new_node.set_free()
    else:
        new_node.fixed = fixed

    self._nodes.append(new_node)
    return new_node
def new_ballastsystem(self, name, parent: Axis) -> BallastSystem

Creates a new rigidbody node and adds it to the scene.

Args

name
Name for the node, should be unique
parent
name of the parent of the ballast system (ie: the vessel axis system)

Examples

scene.new_ballastsystem("cheetah_ballast", parent="Cheetah")

Returns

Reference to newly created BallastSystem
 
Expand source code
def new_ballastsystem(self, name, parent: Axis) -> BallastSystem:
    """Creates a new *rigidbody* node and adds it to the scene.

    Args:
        name: Name for the node, should be unique
        parent: name of the parent of the ballast system (ie: the vessel axis system)

    Examples:
        scene.new_ballastsystem("cheetah_ballast", parent="Cheetah")

    Returns:
        Reference to newly created BallastSystem

    """

    # apply prefixes
    name = self._prefix_name(name)

    # check input
    assertValidName(name)
    self._verify_name_available(name)
    b = self._parent_from_node(parent)

    parent = self._parent_from_node(parent)  # handles verification of type as well

    # make elements
    r = BallastSystem(self, parent)
    r.name = name

    self._nodes.append(r)
    return r
def new_beam(self, name, nodeA, nodeB, EIy=0, EIz=0, GIp=0, EA=0, L=None, mass=0, n_segments=1, tension_only=False) -> Beam

Creates a new beam node and adds it to the scene.

Args

name
Name for the node, should be unique
nodeA
First axis system [Axis]
nodeB
Second axis system [Axis]

All stiffness terms default to 0 The length defaults to the distance between nodeA and nodeB See :py:class:LinearBeam for details

Returns

Reference to newly created beam
 
Expand source code
def new_beam(
    self,
    name,
    nodeA,
    nodeB,
    EIy=0,
    EIz=0,
    GIp=0,
    EA=0,
    L=None,
    mass=0,
    n_segments=1,
    tension_only=False,
) -> Beam:
    """Creates a new *beam* node and adds it to the scene.

    Args:
        name: Name for the node, should be unique
        nodeA: First axis system [Axis]
        nodeB: Second axis system [Axis]

        All stiffness terms default to 0
        The length defaults to the distance between nodeA and nodeB


    See :py:class:`LinearBeam` for details

    Returns:
        Reference to newly created beam

    """

    # apply prefixes
    name = self._prefix_name(name)

    # first check
    assertValidName(name)
    self._verify_name_available(name)
    m = self._parent_from_node(nodeA)
    s = self._parent_from_node(nodeB)

    if L is None:
        L = np.linalg.norm(
            np.array(m.global_position) - np.array(s.global_position)
        )
    else:
        if L <= 0:
            raise ValueError("L should be > 0 as stiffness is defined per length.")

    assert1f_positive_or_zero(EIy, "EIy should be >= 0")
    assert1f_positive_or_zero(EIz, "EIz should be >= 0")
    assert1f_positive_or_zero(GIp, "GIp should be >= 0")
    assert1f_positive_or_zero(EA, "EA should be >= 0")
    assertBool(tension_only, "tension_only should be bool")
    assert1f(mass, "Mass shall be a number")
    n_segments = int(round(n_segments))

    # then create
    a = self._vfc.new_linearbeam(name)

    new_node = Beam(self, a)

    # and set properties
    new_node.nodeA = m
    new_node.nodeB = s
    new_node.EIy = EIy
    new_node.EIz = EIz
    new_node.GIp = GIp
    new_node.EA = EA
    new_node.L = L
    new_node.mass = mass
    new_node.n_segments = n_segments
    new_node.tension_only = tension_only

    self._nodes.append(new_node)
    return new_node
def new_buoyancy(self, name, parent=None, density=1.025) -> Buoyancy

Creates a new buoyancy node and adds it to the scene.

Args

name
Name for the node, should be unique
parent
optional, name of the parent of the node

Returns

Reference to newly created buoyancy
 
Expand source code
def new_buoyancy(self, name, parent=None, density=1.025) -> Buoyancy:
    """Creates a new *buoyancy* node and adds it to the scene.

    Args:
        name: Name for the node, should be unique
        parent: optional, name of the parent of the node


    Returns:
        Reference to newly created buoyancy

    """

    # apply prefixes
    name = self._prefix_name(name)

    # first check
    assertValidName(name)
    self._verify_name_available(name)
    b = self._parent_from_node(parent)

    if b is None:
        raise ValueError("A valid parent must be defined for a Buoyancy node")

    assert1f_positive_or_zero(density, "density")

    # then create
    a = self._vfc.new_buoyancy(name)
    new_node = Buoyancy(self, a)

    # and set properties
    if b is not None:
        new_node.parent = b

    new_node.density = density

    self._nodes.append(new_node)
    return new_node
def new_cable(self, name, endA, endB, length=-1, EA=0, diameter=0, sheaves=None) -> Cable

Creates a new cable node and adds it to the scene.

Args

name
Name for the node, should be unique
endA : A Poi element to connect the first end of the cable to
 
endB : A Poi element to connect the other end of the cable to
 

length [-1] : un-stretched length of the cable in m; default [-1] create a cable with the current distance between the endpoints A and B EA [0] : stiffness of the cable in kN/m; default

sheaves : [optional] A list of pois, these are sheaves that the cable runs over. Defined from endA to endB
 

Examples

scene.new_cable('cable_name' endA='poi_start', endB = 'poi_end') # minimal use

scene.new_cable('cable_name', length=50, EA=1000, endA=poi_start, endB = poi_end, sheaves=[sheave1, sheave2])

scene.new_cable('cable_name', length=50, EA=1000, endA='poi_start', endB = 'poi_end', sheaves=['single_sheave']) # also a single sheave needs to be provided as a list

Notes

The default options for length and EA can be used to measure distances between points

Returns

Reference to newly created Cable
 
Expand source code
def new_cable(
    self, name, endA, endB, length=-1, EA=0, diameter=0, sheaves=None
) -> Cable:
    """Creates a new *cable* node and adds it to the scene.

    Args:
        name: Name for the node, should be unique
        endA : A Poi element to connect the first end of the cable to
        endB : A Poi element to connect the other end of the cable to
        length [-1] : un-stretched length of the cable in m; default [-1] create a cable with the current distance between the endpoints A and B
        EA [0] : stiffness of the cable in kN/m; default

        sheaves : [optional] A list of pois, these are sheaves that the cable runs over. Defined from endA to endB

    Examples:

        scene.new_cable('cable_name' endA='poi_start', endB = 'poi_end')  # minimal use

        scene.new_cable('cable_name', length=50, EA=1000, endA=poi_start, endB = poi_end, sheaves=[sheave1, sheave2])

        scene.new_cable('cable_name', length=50, EA=1000, endA='poi_start', endB = 'poi_end', sheaves=['single_sheave']) # also a single sheave needs to be provided as a list

    Notes:
        The default options for length and EA can be used to measure distances between points

    Returns:
        Reference to newly created Cable

    """

    # apply prefixes
    name = self._prefix_name(name)

    # first check
    assertValidName(name)
    self._verify_name_available(name)
    assert1f(length, "length")
    assert1f(EA, "EA")

    endA = self._poi_or_sheave_from_node(endA)
    endB = self._poi_or_sheave_from_node(endB)

    pois = [endA]
    if sheaves is not None:

        if isinstance(sheaves, Point):  # single sheave as poi or string
            sheaves = [sheaves]

        if isinstance(sheaves, Circle):  # single sheave as poi or string
            sheaves = [sheaves]

        if isinstance(sheaves, str):
            sheaves = [sheaves]

        for s in sheaves:
            # s may be a poi or a sheave
            pois.append(self._poi_or_sheave_from_node(s))

    pois.append(endB)

    # default options
    if length > -1:
        if length < 1e-9:
            raise Exception("Length should be more than 0")

    if EA < 0:
        raise Exception("EA should be more than 0")

    assert1f(diameter, "Diameter should be a number >= 0")

    if diameter < 0:
        raise Exception("Diameter should be >= 0")

    # then create
    a = self._vfc.new_cable(name)
    new_node = Cable(self, a)
    if length > 0:
        new_node.length = length
    new_node.EA = EA
    new_node.diameter = diameter

    new_node.connections = pois

    # and add to the scene
    self._nodes.append(new_node)

    if length < 0:
        new_node.length = 1e-8
        self._vfc.state_update()

        new_length = new_node.stretch + 1e-8

        if new_length > 0:
            new_node.length = new_length
        else:
            # is is possible that all nodes are at the same location which means the total length becomes 0
            self.delete(new_node.name)
            raise ValueError(
                "No lengh has been supplied and all connection points are at the same location - unable to determine a non-zero default length. Please supply a length"
            )

    return new_node
def new_circle(self, name, parent, axis, radius=0.0) -> Circle

Creates a new sheave node and adds it to the scene.

Args

name
Name for the node, should be unique
parent
name of the parent of the node [Poi]
axis
direction of the axis of rotation (x,y,z)
radius
optional, radius of the sheave

Returns

Reference to newly created sheave
 
Expand source code
def new_circle(self, name, parent, axis, radius=0.0) -> Circle:
    """Creates a new *sheave* node and adds it to the scene.

    Args:
        name: Name for the node, should be unique
        parent: name of the parent of the node [Poi]
        axis: direction of the axis of rotation (x,y,z)
        radius: optional, radius of the sheave


    Returns:
        Reference to newly created sheave

    """

    # apply prefixes
    name = self._prefix_name(name)

    # first check
    assertValidName(name)
    self._verify_name_available(name)
    b = self._poi_from_node(parent)

    assert3f(axis, "Axis of rotation ")

    assert1f(radius, "Radius of sheave")

    # then create
    a = self._vfc.new_sheave(name)

    new_node = Circle(self, a)

    # and set properties
    new_node.parent = b
    new_node.axis = axis
    new_node.radius = radius

    self._nodes.append(new_node)
    return new_node
def new_connector2d(self, name, nodeA, nodeB, k_linear=0, k_angular=0) -> Connector2d

Creates a new new_connector2d node and adds it to the scene.

Args

name
Name for the node, should be unique
nodeB
First axis system [Axis]
nodeA
Second axis system [Axis]
k_linear : linear stiffness in kN/m
 
k_angular : angular stiffness in kN*m / rad
 

Returns

Reference to newly created connector2d
 
Expand source code
def new_connector2d(
    self, name, nodeA, nodeB, k_linear=0, k_angular=0
) -> Connector2d:
    """Creates a new *new_connector2d* node and adds it to the scene.

    Args:
        name: Name for the node, should be unique
        nodeB: First axis system [Axis]
        nodeA: Second axis system [Axis]

        k_linear : linear stiffness in kN/m
        k_angular : angular stiffness in kN*m / rad

    Returns:
        Reference to newly created connector2d

    """

    # apply prefixes
    name = self._prefix_name(name)

    # first check
    assertValidName(name)
    self._verify_name_available(name)
    m = self._parent_from_node(nodeA)
    s = self._parent_from_node(nodeB)

    assert1f(k_linear, "Linear stiffness")
    assert1f(k_angular, "Angular stiffness")

    # then create
    a = self._vfc.new_connector2d(name)

    new_node = Connector2d(self, a)

    # and set properties
    new_node.nodeA = m
    new_node.nodeB = s
    new_node.k_linear = k_linear
    new_node.k_angular = k_angular

    self._nodes.append(new_node)
    return new_node
def new_contactball(self, name, parent=None, radius=1, k=9999, meshes=None) -> ContactBall

Creates a new force node and adds it to the scene.

Args

name
Name for the node, should be unique
parent
name of the parent of the node [Poi]
force
optional, global force on the node (x,y,z)
moment
optional, global force on the node (x,y,z)

Returns

Reference to newly created force
 
Expand source code
def new_contactball(
    self, name, parent=None, radius=1, k=9999, meshes=None
) -> ContactBall:
    """Creates a new *force* node and adds it to the scene.

    Args:
        name: Name for the node, should be unique
        parent: name of the parent of the node [Poi]
        force: optional, global force on the node (x,y,z)
        moment: optional, global force on the node (x,y,z)


    Returns:
        Reference to newly created force

    """

    # apply prefixes
    name = self._prefix_name(name)

    # first check
    assertValidName(name)
    self._verify_name_available(name)
    b = self._poi_from_node(parent)

    assert1f_positive_or_zero(radius, "Radius ")
    assert1f_positive_or_zero(k, "k ")

    if meshes is not None:
        meshes = make_iterable(meshes)
        for mesh in meshes:
            test = self._node_from_node(mesh, ContactMesh)

    # then create
    a = self._vfc.new_contactball(name)

    new_node = ContactBall(self, a)

    # and set properties
    if b is not None:
        new_node.parent = b
    if k is not None:
        new_node.k = k
    if radius is not None:
        new_node.radius = radius

    if meshes is not None:
        new_node.meshes = meshes

    self._nodes.append(new_node)
    return new_node
def new_contactmesh(self, name, parent=None) -> ContactMesh

Creates a new contactmesh node and adds it to the scene.

Args

name
Name for the node, should be unique
parent
optional, name of the parent of the node

Returns

Reference to newly created contact mesh
 
Expand source code
def new_contactmesh(self, name, parent=None) -> ContactMesh:
    """Creates a new *contactmesh* node and adds it to the scene.

    Args:
        name: Name for the node, should be unique
        parent: optional, name of the parent of the node

    Returns:
        Reference to newly created contact mesh

    """

    # apply prefixes
    name = self._prefix_name(name)

    # first check
    assertValidName(name)
    self._verify_name_available(name)
    b = self._parent_from_node(parent)

    # then create
    a = self._vfc.new_contactmesh(name)
    new_node = ContactMesh(self, a)

    # and set properties
    if b is not None:
        new_node.parent = b

    self._nodes.append(new_node)
    return new_node
def new_force(self, name, parent=None, force=None, moment=None) -> Force

Creates a new force node and adds it to the scene.

Args

name
Name for the node, should be unique
parent
name of the parent of the node [Poi]
force
optional, global force on the node (x,y,z)
moment
optional, global force on the node (x,y,z)

Returns

Reference to newly created force
 
Expand source code
def new_force(self, name, parent=None, force=None, moment=None) -> Force:
    """Creates a new *force* node and adds it to the scene.

    Args:
        name: Name for the node, should be unique
        parent: name of the parent of the node [Poi]
        force: optional, global force on the node (x,y,z)
        moment: optional, global force on the node (x,y,z)


    Returns:
        Reference to newly created force

    """

    # apply prefixes
    name = self._prefix_name(name)

    # first check
    assertValidName(name)
    self._verify_name_available(name)
    b = self._poi_from_node(parent)

    if force is not None:
        assert3f(force, "Force ")

    if moment is not None:
        assert3f(moment, "Moment ")

    # then create
    a = self._vfc.new_force(name)

    new_node = Force(self, a)

    # and set properties
    if b is not None:
        new_node.parent = b
    if force is not None:
        new_node.force = force
    if moment is not None:
        new_node.moment = moment

    self._nodes.append(new_node)
    return new_node
def new_geometriccontact(self, name, child, parent, inside=False, swivel=None, rotation_on_parent=None, child_rotation=None, swivel_fixed=True, fixed_to_parent=False, child_fixed=False) -> GeometricContact

Creates a new new_geometriccontact node and adds it to the scene.

Geometric contact connects two circular elements and can be used to model bar-bar connections or pin-in-hole connections.

By default a bar-bar connection is created between item1 and item2.

Args

name
Name for the node, should be unique
child : [Sheave] will be the nodeA of the connection
 
parent : [Sheave] will be the nodeB of the connection
 
inside
[False] False creates a pinpin connection. True creates a pin-hole type of connection
swivel
Rotation angle between the two items. Defaults to 90 for pinpin and 0 for pin-hole
rotation_on_parent
Angle of the connecting hinge relative to nodeA or None for default
child_rotation
Angle of the nodeB relative to the connecting hinge or None for default
swivel_fixed
Fix swivel [True]
fixed_to_parent
Fix connecting hinge to nodeA [False]
child_fixed
Fix nodeB to connecting hinge [False]

Note

For pin-hole connections there is no geometrical difference between the pin and the hole. Therefore it is not needed to specify which is the pin and which is the hole

Returns

Reference to newly created new_geometriccontact
 
Expand source code
def new_geometriccontact(
    self,
    name,
    child,
    parent,
    inside=False,
    swivel=None,
    rotation_on_parent=None,
    child_rotation=None,
    swivel_fixed=True,
    fixed_to_parent=False,
    child_fixed=False,
) -> GeometricContact:
    """Creates a new *new_geometriccontact* node and adds it to the scene.

    Geometric contact connects two circular elements and can be used to model bar-bar connections or pin-in-hole connections.

    By default a bar-bar connection is created between item1 and item2.

    Args:
        name: Name for the node, should be unique
        child : [Sheave] will be the nodeA of the connection
        parent : [Sheave] will be the nodeB of the connection
        inside: [False] False creates a pinpin connection. True creates a pin-hole type of connection
        swivel: Rotation angle between the two items. Defaults to 90 for pinpin and 0 for pin-hole
        rotation_on_parent: Angle of the connecting hinge relative to nodeA or None for default
        child_rotation: Angle of the nodeB relative to the connecting hinge or None for default
        swivel_fixed: Fix swivel [True]
        fixed_to_parent: Fix connecting hinge to nodeA [False]
        child_fixed: Fix nodeB to connecting hinge [False]

    Note:
        For pin-hole connections there is no geometrical difference between the pin and the hole. Therefore it is not needed to specify
        which is the pin and which is the hole

    Returns:
        Reference to newly created new_geometriccontact

    """

    # apply prefixes
    name = self._prefix_name(name)

    # first check
    assertValidName(name)
    self._verify_name_available(name)

    name_prefix = name + vfc.MANAGED_NODE_IDENTIFIER
    postfixes = [
        "_axis_on_parent",
        "_pin_hole_connection",
        "_axis_on_child",
        "_connection_axial_rotation",
    ]

    for pf in postfixes:
        self._verify_name_available(name_prefix + pf)

    child = self._sheave_from_node(child)
    parent = self._sheave_from_node(parent)

    assertBool(inside, "inside")
    assertBool(swivel_fixed, "swivel_fixed")
    assertBool(fixed_to_parent, "fixed_to_parent")
    assertBool(child_fixed, "child_fixed")

    GeometricContact._assert_parent_child_possible(parent, child)

    if swivel is None:
        if inside:
            swivel = 0
        else:
            swivel = 90

    assert1f(swivel, "swivel_angle")

    if rotation_on_parent is not None:
        assert1f(rotation_on_parent, "rotation_on_parent should be either None or ")
    if child_rotation is not None:
        assert1f(child_rotation, "child_rotation should be either None or ")

    if child is None:
        raise ValueError("child needs to be a sheave-type node")
    if parent is None:
        raise ValueError("parent needs to be a sheave-type node")

    if child.parent.parent is None:
        raise ValueError(
            f"The parent {child.parent.name} of the child item {child.name} is not located on an axis. Can not create the connection because there is no axis to nodeB"
        )

    if child.parent.parent.manager is not None:
        self.print_node_tree()
        raise ValueError(
            f"The axis or body that {child.name} is on is already managed by {child.parent.parent.manager.name} and can therefore not be changed - unable to create geometric contact"
        )

    new_node = GeometricContact(self, child, parent, name)
    if inside:
        new_node.set_pin_in_hole_connection()
    else:
        new_node.set_pin_pin_connection()

    new_node.swivel = swivel
    if rotation_on_parent is not None:
        new_node.rotation_on_parent = rotation_on_parent
    if child_rotation is not None:
        new_node.child_rotation = child_rotation

    new_node.fixed_to_parent = fixed_to_parent
    new_node.child_fixed = child_fixed
    new_node.swivel_fixed = swivel_fixed

    self._nodes.append(new_node)
    return new_node
def new_hydspring(self, name, parent, cob, BMT, BML, COFX, COFY, kHeave, waterline, displacement_kN) -> HydSpring

Creates a new hydspring node and adds it to the scene.

Args

name
Name for the node, should be unique
parent
name of the parent of the node [Axis]
cob
position of the CoB (x,y,z) in the parent axis system
BMT
Vertical distance between CoB and meta-center for roll
BML
Vertical distance between CoB and meta-center for pitch
COFX
X-location of center of flotation (center of waterplane) relative to CoB
COFY
Y-location of center of flotation (center of waterplane) relative to CoB
kHeave : heave stiffness (typically Awl * rho * g)
 
waterline : Z-position (elevation) of the waterline relative to CoB
 
displacement_kN : displacement (typically volume * rho * g)
 

Returns

Reference to newly created hydrostatic spring
 
Expand source code
def new_hydspring(
    self,
    name,
    parent,
    cob,
    BMT,
    BML,
    COFX,
    COFY,
    kHeave,
    waterline,
    displacement_kN,
) -> HydSpring:
    """Creates a new *hydspring* node and adds it to the scene.

    Args:
        name: Name for the node, should be unique
        parent: name of the parent of the node [Axis]
        cob: position of the CoB (x,y,z) in the parent axis system
        BMT: Vertical distance between CoB and meta-center for roll
        BML: Vertical distance between CoB and meta-center for pitch
        COFX: X-location of center of flotation (center of waterplane) relative to CoB
        COFY: Y-location of center of flotation (center of waterplane) relative to CoB
        kHeave : heave stiffness (typically Awl * rho * g)
        waterline : Z-position (elevation) of the waterline relative to CoB
        displacement_kN : displacement (typically volume * rho * g)


    Returns:
        Reference to newly created hydrostatic spring

    """

    # apply prefixes
    name = self._prefix_name(name)

    # first check
    assertValidName(name)
    self._verify_name_available(name)
    b = self._parent_from_node(parent)
    assert3f(cob, "CoB ")
    assert1f(BMT, "BMT ")
    assert1f(BML, "BML ")
    assert1f(COFX, "COFX ")
    assert1f(COFY, "COFY ")
    assert1f(kHeave, "kHeave ")
    assert1f(waterline, "waterline ")
    assert1f(displacement_kN, "displacement_kN ")

    # then create
    a = self._vfc.new_hydspring(name)
    new_node = HydSpring(self, a)

    new_node.cob = cob
    new_node.parent = b
    new_node.BMT = BMT
    new_node.BML = BML
    new_node.COFX = COFX
    new_node.COFY = COFY
    new_node.kHeave = kHeave
    new_node.waterline = waterline
    new_node.displacement_kN = displacement_kN

    self._nodes.append(new_node)

    return new_node
def new_linear_connector_6d(self, name, main, secondary, stiffness=None) -> LC6d

Creates a new linear connector 6d node and adds it to the scene.

Args

name
Name for the node, should be unique
main
Main axis system [Axis]
secondary
Secondary axis system [Axis]
stiffness
optional, connection stiffness (x,y,z, rx,ry,rz)

See :py:class:LC6d for details

Returns

Reference to newly created connector
 
Expand source code
def new_linear_connector_6d(self, name, main, secondary, stiffness=None) -> LC6d:
    """Creates a new *linear connector 6d* node and adds it to the scene.

    Args:
        name: Name for the node, should be unique
        main: Main axis system [Axis]
        secondary: Secondary axis system [Axis]
        stiffness: optional, connection stiffness (x,y,z, rx,ry,rz)

    See :py:class:`LC6d` for details

    Returns:
        Reference to newly created connector

    """

    # apply prefixes
    name = self._prefix_name(name)

    # first check
    assertValidName(name)
    self._verify_name_available(name)
    m = self._parent_from_node(secondary)
    s = self._parent_from_node(main)

    if stiffness is not None:
        assert6f(stiffness, "Stiffness ")
    else:
        stiffness = (0, 0, 0, 0, 0, 0)

    # then create
    a = self._vfc.new_linearconnector6d(name)

    new_node = LC6d(self, a)

    # and set properties
    new_node.main = m
    new_node.secondary = s
    new_node.stiffness = stiffness

    self._nodes.append(new_node)
    return new_node
def new_point(self, name, parent=None, position=None) -> Point

Creates a new poi node and adds it to the scene.

Args

name
Name for the node, should be unique
parent
optional, name of the parent of the node
position
optional, position for the node (x,y,z)

Returns

Reference to newly created poi
 
Expand source code
def new_point(self, name, parent=None, position=None) -> Point:
    """Creates a new *poi* node and adds it to the scene.

    Args:
        name: Name for the node, should be unique
        parent: optional, name of the parent of the node
        position: optional, position for the node (x,y,z)


    Returns:
        Reference to newly created poi

    """

    # apply prefixes
    name = self._prefix_name(name)

    # first check
    assertValidName(name)
    self._verify_name_available(name)
    b = self._parent_from_node(parent)

    if position is not None:
        assert3f(position, "Position ")

    # then create
    a = self._vfc.new_poi(name)

    new_node = Point(self, a)

    # and set properties
    if b is not None:
        new_node.parent = b
    if position is not None:
        new_node.position = position

    self._nodes.append(new_node)
    return new_node
def new_rigidbody(self, name, mass=0, cog=(0, 0, 0), parent=None, position=None, rotation=None, inertia_radii=None, fixed=True) -> RigidBody

Creates a new rigidbody node and adds it to the scene.

Args

name
Name for the node, should be unique
mass
optional, [0] mass in mT
cog
optional, (0,0,0) cog-position in (m,m,m)
parent
optional, name of the parent of the node
position
optional, position for the node (x,y,z)
rotation
optional, rotation for the node (rx,ry,rz)
inertia_radii : optional, radii of gyration (rxx,ryy,rzz); only used for dynamics
 

fixed [True]: optional, determines whether the axis is fixed [True] or free [False]. May also be a sequence of 6 booleans.

Examples

scene.new_rigidbody("heavy_thing", mass = 10000, cog = (1.45, 0, -0.7))

Returns

Reference to newly created RigidBody
 
Expand source code
def new_rigidbody(
    self,
    name,
    mass=0,
    cog=(0, 0, 0),
    parent=None,
    position=None,
    rotation=None,
    inertia_radii=None,
    fixed=True,
) -> RigidBody:
    """Creates a new *rigidbody* node and adds it to the scene.

    Args:
        name: Name for the node, should be unique
        mass: optional, [0] mass in mT
        cog: optional, (0,0,0) cog-position in (m,m,m)
        parent: optional, name of the parent of the node
        position: optional, position for the node (x,y,z)
        rotation: optional, rotation for the node (rx,ry,rz)
        inertia_radii : optional, radii of gyration (rxx,ryy,rzz); only used for dynamics
        fixed [True]: optional, determines whether the axis is fixed [True] or free [False]. May also be a sequence of 6 booleans.

    Examples:
        scene.new_rigidbody("heavy_thing", mass = 10000, cog = (1.45, 0, -0.7))

    Returns:
        Reference to newly created RigidBody

    """

    # apply prefixes
    name = self._prefix_name(name)

    # check input
    assertValidName(name)
    self._verify_name_available(name)
    b = self._parent_from_node(parent)

    if position is not None:
        assert3f(position, "Position ")
    if rotation is not None:
        assert3f(rotation, "Rotation ")

    if inertia_radii is not None:
        assert3f_positive(inertia_radii, "Radii of inertia")
        assert mass > 0, ValueError(
            "Can not set radii of gyration without specifying mass"
        )

    if not isinstance(fixed, bool):
        if len(fixed) != 6:
            raise Exception(
                '"fixed" parameter should either be True/False or a 6x bool sequence such as (True,True,False,False,True,False)'
            )

    # make elements

    a = self._vfc.new_axis(name)

    p = self._vfc.new_poi(name + vfc.VF_NAME_SPLIT + "cog")
    p.parent = a
    p.position = cog

    g = self._vfc.new_force(name + vfc.VF_NAME_SPLIT + "gravity")
    g.parent = p
    g.force = (0, 0, -vfc.G * mass)

    r = RigidBody(self, a, p, g)

    r.cog = cog  # set inertia
    r.mass = mass

    # and set properties
    if b is not None:
        r.parent = b
    if position is not None:
        r.position = position
    if rotation is not None:
        r.rotation = rotation

    if inertia_radii is not None:
        r.inertia_radii = inertia_radii

    if isinstance(fixed, bool):
        if fixed:
            r.set_fixed()
        else:
            r.set_free()
    else:
        r.fixed = fixed

    self._nodes.append(r)
    return r
def new_shackle(self, name, kind='GP500') -> Shackle

Creates a new shackle, adds it to the scene and returns a reference to the newly created object.

See Also: Shackle

Args

name
name
kind
type of shackle; eg 'GP500'

Returns

a reference to the newly created Shackle object.

Expand source code
def new_shackle(self, name, kind="GP500") -> Shackle:
    """
    Creates a new shackle, adds it to the scene and returns a reference to the newly created object.

    See Also:
        Shackle

    Args:
        name:   name
        kind:  type of shackle; eg 'GP500'


    Returns:
        a reference to the newly created Shackle object.

    """

    # apply prefixes
    name = self._prefix_name(name)

    # first check
    assertValidName(name)
    self._verify_name_available(name)

    name_prefix = name + vfc.MANAGED_NODE_IDENTIFIER
    postfixes = [
        "_body",
        "_pin_point",
        "_bow_point",
        "_inside_circle_center",
        "_inside",
        "_visual",
    ]
    for pf in postfixes:
        self._verify_name_available(name_prefix + pf)

    # then make element

    # make elements

    a = self._vfc.new_axis(name)

    p = self._vfc.new_poi(name + vfc.VF_NAME_SPLIT + "cog")
    p.parent = a

    g = self._vfc.new_force(name + vfc.VF_NAME_SPLIT + "gravity")
    g.parent = p

    node = Shackle(scene=self, name=name, kind=kind, a=a, p=p, g=g)

    self._nodes.append(node)

    return node
def new_sling(self, name, length=-1, EA=1.0, mass=0.1, endA=None, endB=None, LeyeA=None, LeyeB=None, LspliceA=None, LspliceB=None, diameter=0.1, sheaves=None) -> Sling

Creates a new sling, adds it to the scene and returns a reference to the newly created object.

See Also: Sling

Args

name
name
length
length of the sling [m], defaults to distance between endpoints
EA

stiffness in kN, default: 1.0 (note: equilibrium will fail if mass >0 and EA=0)

mass
mass in mT, default 0.1
endA
element to connect end A to [poi, circle]
endB
element to connect end B to [poi, circle]
LeyeA
inside eye on side A length [m], defaults to 1/6th of length
LeyeB
inside eye on side B length [m], defaults to 1/6th of length
LspliceA
splice length on side A [m] (the part where the cable is connected to itself)
LspliceB
splice length on side B [m] (the part where the cable is connected to itself)
diameter
cable diameter in m, defaul to 0.1
sheaves
optional: list of sheaves/pois that the sling runs over

Returns

a reference to the newly created Sling object.

Expand source code
def new_sling(
    self,
    name,
    length=-1,
    EA=1.0,
    mass=0.1,
    endA=None,
    endB=None,
    LeyeA=None,
    LeyeB=None,
    LspliceA=None,
    LspliceB=None,
    diameter=0.1,
    sheaves=None,
) -> Sling:
    """
    Creates a new sling, adds it to the scene and returns a reference to the newly created object.

    See Also:
        Sling

    Args:
        name:    name
        length:  length of the sling [m], defaults to distance between endpoints
        EA:      stiffness in kN, default: 1.0 (note: equilibrium will fail if mass >0 and EA=0)
        mass:    mass in mT, default  0.1
        endA:    element to connect end A to [poi, circle]
        endB:    element to connect end B to [poi, circle]
        LeyeA:   inside eye on side A length [m], defaults to 1/6th of length
        LeyeB:   inside eye on side B length [m], defaults to 1/6th of length
        LspliceA: splice length on side A [m] (the part where the cable is connected to itself)
        LspliceB: splice length on side B [m] (the part where the cable is connected to itself)
        diameter: cable diameter in m, defaul to 0.1
        sheaves:  optional: list of sheaves/pois that the sling runs over

    Returns:
        a reference to the newly created Sling object.

    """

    # apply prefixes
    name = self._prefix_name(name)

    # first check
    assertValidName(name)
    self._verify_name_available(name)

    name_prefix = name + vfc.MANAGED_NODE_IDENTIFIER
    postfixes = [
        "_spliceA",
        "_spliceA",
        "_spliceA2",
        "_spliceAM",
        "_spliceA_visual",
        "spliceB",
        "_spliceB1",
        "_spliceB2",
        "_spliceBM",
        "_spliceB_visual",
        "_main_part",
        "_eyeA",
        "_eyeB",
    ]

    for pf in postfixes:
        self._verify_name_available(name_prefix + pf)

    endA = self._poi_or_sheave_from_node(endA)
    endB = self._poi_or_sheave_from_node(endB)

    if length == -1:  # default
        if endA is None or endB is None:
            raise ValueError(
                "Length for cable is not provided, so defaults to distance between endpoints; but at least one of the endpoints is None."
            )

        length = np.linalg.norm(
            np.array(endA.global_position) - np.array(endB.global_position)
        )

    if LeyeA is None:  # default
        LeyeA = length / 6
    if LeyeB is None:  # default
        LeyeB = length / 6
    if LspliceA is None:  # default
        LspliceA = length / 6
    if LspliceB is None:  # default
        LspliceB = length / 6

    if sheaves is None:
        sheaves = []

    assert1f_positive_or_zero(diameter, "Diameter")
    assert1f_positive_or_zero(mass, "mass")

    assert1f_positive(length, "Length")
    assert1f_positive(LeyeA, "length of eye A")
    assert1f_positive(LeyeB, "length of eye B")
    assert1f_positive(LspliceA, "length of splice A")
    assert1f_positive(LspliceB, "length of splice B")

    for s in sheaves:
        _ = self._poi_or_sheave_from_node(s)

    # then make element
    # __init__(self, scene, name, Ltotal, LeyeA, LeyeB, LspliceA, LspliceB, diameter, EA, mass, endA = None, endB=None, sheaves=None):

    node = Sling(
        scene=self,
        name=name,
        length=length,
        LeyeA=LeyeA,
        LeyeB=LeyeB,
        LspliceA=LspliceA,
        LspliceB=LspliceB,
        diameter=diameter,
        EA=EA,
        mass=mass,
        endA=endA,
        endB=endB,
        sheaves=sheaves,
    )
    self._nodes.append(node)

    return node
def new_spmt(self, name, parent, maximal_length=1.8, nominal_length=1.5, k=1000000.0, meshes=None, axles=None) -> SPMT

Creates a new SPMT node and adds it to the scene.

Args

name
Name for the node, should be unique
parent
name of the parent of the node [Axis]
maximal_length
optional, maximum distance between top and bottom of wheel (1.5m + 300mm)
nominal_length
optional, nominal distance between top and bottom of wheel [1.5m]
k : stiffness per axle [kN/m]
 
meshes : list of contact meshes
 

axles : list of axle locations [(x,y,z),(x,y,z), … ]

Returns

Reference to newly created SPMT
 
Expand source code
def new_spmt(
    self,
    name,
    parent,
    maximal_length=1.8,
    nominal_length=1.5,
    k=1e6,
    meshes=None,
    axles=None,
) -> SPMT:
    """Creates a new *SPMT* node and adds it to the scene.

    Args:
        name: Name for the node, should be unique
        parent: name of the parent of the node [Axis]
        maximal_length: optional, maximum distance between top and bottom of wheel (1.5m + 300mm)
        nominal_length: optional, nominal distance between top and bottom of wheel [1.5m]
        k : stiffness per axle [kN/m]
        meshes : list of contact meshes
        axles  : list of axle locations [(x,y,z),(x,y,z), ... ]

    Returns:
        Reference to newly created SPMT

    """

    # apply prefixes
    name = self._prefix_name(name)

    # first check
    assertValidName(name)
    self._verify_name_available(name)
    parent = self._node_from_node_or_str(parent)
    assert isinstance(parent, Axis), ValueError(
        f"Parent should be an axis system or derived, not a {type(parent)}"
    )

    assert1f_positive_or_zero(maximal_length, "maximal_length ")
    assert1f_positive_or_zero(nominal_length, "nominal_length ")

    if meshes is not None:
        meshes = make_iterable(meshes)
        for mesh in meshes:
            test = self._node_from_node(
                mesh, ContactMesh
            )  # throws error if not found

    if axles is not None:
        for p in axles:
            assert3f(p, "axle locations should be (x,y,z)")

    # then create
    a = self._vfc.new_spmt(name)

    new_node = SPMT(self, a)

    # and set properties
    new_node.parent = parent
    new_node.k = k
    new_node.max_length = maximal_length
    new_node.nominal_length = nominal_length

    if meshes is not None:
        new_node.meshes = meshes

    if axles is not None:
        new_node.axles = axles

    self._nodes.append(new_node)
    return new_node
def new_tank(self, name, parent=None, density=1.025, free_flooding=False) -> Tank

Creates a new tank node and adds it to the scene.

Args

name
Name for the node, should be unique
parent
optional, name of the parent of the node

Returns

Reference to newly created Tank
 
Expand source code
def new_tank(self, name, parent=None, density=1.025, free_flooding=False) -> Tank:
    """Creates a new *tank* node and adds it to the scene.

    Args:
        name: Name for the node, should be unique
        parent: optional, name of the parent of the node

    Returns:
        Reference to newly created Tank

    """

    # apply prefixes
    name = self._prefix_name(name)

    # first check
    assertValidName(name)
    self._verify_name_available(name)
    b = self._parent_from_node(parent)

    if b is None:
        raise ValueError("A valid parent must be defined for a Tank")

    assert isinstance(free_flooding, bool), ValueError(
        "free_flooding shall be True or False"
    )

    assert1f(density, "density")

    # then create
    a = self._vfc.new_tank(name)
    new_node = Tank(self, a)
    new_node.density = density

    # and set properties
    if b is not None:
        new_node.parent = b

    new_node.free_flooding = free_flooding

    self._nodes.append(new_node)
    return new_node
def new_visual(self, name, path, parent=None, offset=None, rotation=None, scale=None) -> Visual

Creates a new Visual node and adds it to the scene.

Args

name
Name for the node, should be unique
path
Path to the resource
parent
optional, name of the parent of the node
offset
optional, position for the node (x,y,z)
rotation
optional, rotation for the node (rx,ry,rz)

scale : optional, scale of the visual (x,y,z).

Returns

Reference to newly created visual
 
Expand source code
def new_visual(
    self, name, path, parent=None, offset=None, rotation=None, scale=None
) -> Visual:
    """Creates a new *Visual* node and adds it to the scene.

    Args:
        name: Name for the node, should be unique
        path: Path to the resource
        parent: optional, name of the parent of the node
        offset: optional, position for the node (x,y,z)
        rotation: optional, rotation for the node (rx,ry,rz)
        scale : optional, scale of the visual (x,y,z).

    Returns:
        Reference to newly created visual

    """

    # apply prefixes
    name = self._prefix_name(name)

    # first check
    assertValidName(name)
    self._verify_name_available(name)
    b = self._parent_from_node(parent)

    if offset is not None:
        assert3f(offset, "Offset ")
    if rotation is not None:
        assert3f(rotation, "Rotation ")

    self.get_resource_path(path)  # raises error when resource is not found

    # then create

    new_node = Visual(self)

    new_node.name = name
    new_node.path = path
    new_node.parent = parent

    # and set properties
    if b is not None:
        new_node.parent = b
    if offset is not None:
        new_node.offset = offset
    if rotation is not None:
        new_node.rotation = rotation
    if scale is not None:
        new_node.scale = scale

    self._nodes.append(new_node)
    return new_node
def new_waveinteraction(self, name, path, parent=None, offset=None) -> WaveInteraction1

Creates a new wave interaction node and adds it to the scene.

Args

name
Name for the node, should be unique
path
Path to the hydrodynamic database
parent
optional, name of the parent of the node
offset
optional, position for the node (x,y,z)

Returns

Reference to newly created wave-interaction object
 
Expand source code
def new_waveinteraction(
    self,
    name,
    path,
    parent=None,
    offset=None,
) -> WaveInteraction1:
    """Creates a new *wave interaction* node and adds it to the scene.

    Args:
        name: Name for the node, should be unique
        path: Path to the hydrodynamic database
        parent: optional, name of the parent of the node
        offset: optional, position for the node (x,y,z)

    Returns:
        Reference to newly created wave-interaction object

    """

    if not parent:
        raise ValueError("Wave-interaction has to be located on an Axis")

    # apply prefixes
    name = self._prefix_name(name)

    # first check
    assertValidName(name)
    self._verify_name_available(name)
    b = self._parent_from_node(parent)

    if b is None:
        raise ValueError("Wave-interaction has to be located on an Axis")

    if offset is not None:
        assert3f(offset, "Offset ")

    self.get_resource_path(path)  # raises error when resource is not found

    # then create

    new_node = WaveInteraction1(self)

    new_node.name = name
    new_node.path = path
    new_node.parent = parent

    # and set properties
    new_node.parent = b
    if offset is not None:
        new_node.offset = offset

    self._nodes.append(new_node)
    return new_node
def node_A_core_depends_on_B_core(self, A, B)

Returns True if the node core of node A depends on the core node of node B

Expand source code
def node_A_core_depends_on_B_core(self, A, B):
    """Returns True if the node core of node A depends on the core node of node B"""

    A = self._node_from_node_or_str(A)
    B = self._node_from_node_or_str(B)

    if not isinstance(A, CoreConnectedNode):
        raise ValueError(
            f"{A.name} is not connected to a core node. Dependancies can not be traced using this function"
        )
    if not isinstance(B, CoreConnectedNode):
        raise ValueError(
            f"{B.name} is not connected to a core node. Dependancies can not be traced using this function"
        )

    return self._vfc.element_A_depends_on_B(A._vfNode.name, B._vfNode.name)
def node_by_name(self, node_name, silent=False)
Expand source code
def node_by_name(self, node_name, silent=False):
    for N in self._nodes:
        if N.name == node_name:
            return N

    if not silent:
        self.print_node_tree()
    raise ValueError(
        'No node with name "{}". Available names printed above.'.format(node_name)
    )
def nodes_depending_on(self, node)

Returns a list of nodes that physically depend on node. Only direct dependants are obtained with a connection to the core. This function should be used to determine if a node can be created, deleted, exported.

For making node-trees please use nodes_with_parent instead.

Args

node : Node or node-name
 

Returns

list of names
 
See Also: nodes_with_parent
 
Expand source code
def nodes_depending_on(self, node):
    """Returns a list of nodes that physically depend on node. Only direct dependants are obtained with a connection to the core.
    This function should be used to determine if a node can be created, deleted, exported.

    For making node-trees please use nodes_with_parent instead.

    Args:
        node : Node or node-name

    Returns:
        list of names

    See Also: nodes_with_parent
    """

    if isinstance(node, Node):
        node = node.name

    # check the node type
    _node = self[node]
    if not isinstance(_node, CoreConnectedNode):
        return []
    else:
        names = self._vfc.elements_depending_directly_on(node)

    r = []
    for name in names:
        try:
            node = self.node_by_name(name, silent=True)
            r.append(node.name)
        except:
            pass

    # check all other nodes in the scene

    for n in self._nodes:
        if _node in n.depends_on():
            if n.name not in r:
                r.append(n.name)

    # for v in [*self.nodes_of_type(Visual), *self.nodes_of_type(WaveInteraction1)]:
    #     if v.parent is _node:
    #         r.append(v.name)

    return r
def nodes_managed_by(self, manager: Manager)

Returns a list of nodes managed by manager

Expand source code
def nodes_managed_by(self, manager : Manager):
    """Returns a list of nodes managed by manager"""

    return [node for node in self._nodes if node.manager == manager]
def nodes_of_type(self, node_class)

Returns all nodes of the specified or derived type

Examples

pois = scene.nodes_of_type(DAVE.Poi) axis_and_bodies = scene.nodes_of_type(DAVE.Axis)

Expand source code
def nodes_of_type(self, node_class):
    """Returns all nodes of the specified or derived type

    Examples:
        pois = scene.nodes_of_type(DAVE.Poi)
        axis_and_bodies = scene.nodes_of_type(DAVE.Axis)
    """
    r = list()
    for n in self._nodes:
        if isinstance(n, node_class):
            r.append(n)
    return r
def nodes_with_parent(self, node)

Returns a list of nodes that have given node as a parent. Good for making trees. For checking physical connections use nodes_depending_on instead.

Args

node : Node or node-name
 

Returns

list of names
 
See Also: nodes_depending_on
 
Expand source code
def nodes_with_parent(self, node):
    """Returns a list of nodes that have given node as a parent. Good for making trees.
    For checking physical connections use nodes_depending_on instead.

    Args:
        node : Node or node-name

    Returns:
        list of names

    See Also: nodes_depending_on
    """

    if isinstance(node, str):
        node = self[node]

    r = []

    for n in self._nodes:

        try:
            parent = n.parent
        except AttributeError:
            continue

        if parent == node:
            r.append(n.name)

    return r
def plot_effect(self, evaluate, change_node, change_property, start, to, steps)

Produces a 2D plot with the relation between two properties of the scene. For example the length of a cable versus the force in another cable.

The evaluate argument is processed using "eval" and may contain python code. This may be used to combine multiple properties to one value. For example calculate the diagonal load distribution from four independent loads.

The plot is produced using matplotlob. The plot is produced in the current figure (if any) and plt.show is not executed.

Args

evaluate : str
code to be evaluated to yield the value on the y-axis. Eg: s['poi'].fx Scene is abbiviated as "s"
change_node(Node or str): node to be adjusted
change_property : str
property of that node to be adjusted
start : left side of the interval
 
to : right side of the interval
 
steps : number of steps in the interval
 

Returns

Tuple (x,y) with x and y coordinates
 

Examples

>>> s.plot_effect("s['cable'].tension", "cable", "length", 11, 14, 10)
>>> import matplotlib.pyplot as plt
>>> plt.show()
Expand source code
def plot_effect(self, evaluate, change_node, change_property, start, to, steps):
    """Produces a 2D plot with the relation between two properties of the scene. For example the length of a cable
    versus the force in another cable.

    The evaluate argument is processed using "eval" and may contain python code. This may be used to combine multiple
    properties to one value. For example calculate the diagonal load distribution from four independent loads.

    The plot is produced using matplotlob. The plot is produced in the current figure (if any) and plt.show is not executed.

    Args:
        evaluate (str): code to be evaluated to yield the value on the y-axis. Eg: s['poi'].fx Scene is abbiviated as "s"
        change_node(Node or str):  node to be adjusted
        change_property (str): property of that node to be adjusted
        start : left side of the interval
        to : right side of the interval
        steps : number of steps in the interval

    Returns:
        Tuple (x,y) with x and y coordinates

    Examples:
        >>> s.plot_effect("s['cable'].tension", "cable", "length", 11, 14, 10)
        >>> import matplotlib.pyplot as plt
        >>> plt.show()

    """
    s = self
    change_node = self._node_from_node_or_str(change_node)

    # check that the attributes exist and are single numbers
    test = eval(evaluate)

    try:
        float(test)
    except:
        raise ValueError("Evaluation of {} does not result in a float")

    def set_and_get(x):
        setattr(change_node, change_property, x)
        self.solve_statics(silent=True)
        s = self
        result = eval(evaluate)
        self._print("setting {} results in {}".format(x, result))
        return result

    xs = np.linspace(start, to, steps)
    y = []
    for x in xs:
        y.append(set_and_get(x))

    y = np.array(y)
    import matplotlib.pyplot as plt

    plt.plot(xs, y)
    plt.xlabel("{} of {}".format(change_property, change_node.name))
    plt.ylabel(evaluate)

    return (xs, y)
def print_node_tree(self)
Expand source code
def print_node_tree(self):

    self.sort_nodes_by_dependency()

    to_be_printed = []
    for n in self._nodes:
        to_be_printed.append(n.name)

    # to_be_printed.reverse()

    def print_deps(name, spaces):

        node = self[name]
        deps = self.nodes_with_parent(node)
        print(spaces + name + " [" + str(type(node)).split(".")[-1][:-2] + "]")

        if deps is not None:
            for dep in deps:
                if spaces == "":
                    spaces_plus = " |-> "
                else:
                    spaces_plus = " |   " + spaces
                print_deps(dep, spaces_plus)

        to_be_printed.remove(name)

    while to_be_printed:
        name = to_be_printed[0]
        print_deps(name, "")
def print_python_code(self)

Prints the python code that generates the current scene

See also: give_python_code

Expand source code
def print_python_code(self):
    """Prints the python code that generates the current scene

    See also: give_python_code
    """
    for line in self.give_python_code().split("\n"):
        print(line)
def run_code(self, code)

Runs the provided code with 's' as self

Expand source code
def run_code(self, code):
    """Runs the provided code with 's' as self"""

    import DAVE

    locals = DAVE.__dict__
    locals['s'] = self

    try:
        exec(code, {}, locals)
    except Exception as M:
        for i, line in enumerate(code.split("\n")):
            print(f"{i} {line}")
        raise M
def save_scene(self, filename)

Saves the scene to a file

This saves the scene in its current state to a file. Opening the saved file will reproduce exactly this scene.

This sounds nice, but beware that it only saves the resulting model, not the process of creating the model. This means that if you created the model in a parametric fashion or assembled the model from other models then these are not re-evaluated when the model is openened again. So lets say this model uses a sub-model of a lifting hook which is imported from another file. If that other file is updated then the results of that update will not be reflected in the saved model.

If no path is present in the file-name then the model will be saved in the last (lowest) resource-path (if any)

Args

filename : filename or file-path to save the file. Default extension is .dave
 

Returns

the full path to the saved file
 
Expand source code
def save_scene(self, filename):
    """Saves the scene to a file

    This saves the scene in its current state to a file.
    Opening the saved file will reproduce exactly this scene.

    This sounds nice, but beware that it only saves the resulting model, not the process of creating the model.
    This means that if you created the model in a parametric fashion or assembled the model from other models then these are not re-evaluated when the model is openened again.
    So lets say this model uses a sub-model of a lifting hook which is imported from another file. If that other file is updated then
    the results of that update will not be reflected in the saved model.

    If no path is present in the file-name then the model will be saved in the last (lowest) resource-path (if any)

    Args:
        filename : filename or file-path to save the file. Default extension is .dave

    Returns:
        the full path to the saved file

    """

    code = self.give_python_code()

    filename = Path(filename)

    # add .dave extension if needed
    if filename.suffix != ".dave":
        filename = Path(str(filename) + ".dave")

    # add path if not provided
    if not filename.is_absolute():
        try:
            filename = Path(self.resources_paths[-1]) / filename
        except:
            pass  # save in current folder

    # make sure directory exists
    directory = filename.parent
    if not directory.exists():
        directory.mkdir()

    f = open(filename, "w+")
    f.write(code)
    f.close()

    self._print("Saved as {}".format(filename))

    return filename
def savepoint_make(self)
Expand source code
def savepoint_make(self):
    self._savepoint = self.give_python_code()
def savepoint_restore(self)
Expand source code
def savepoint_restore(self):
    if self._savepoint is not None:
        self.clear()
        exec(self._savepoint, {}, {"s": self})
        self._savepoint = None
        return True
    else:
        return False
def solve_statics(self, silent=False, timeout=None)

Solves statics

Args

silent
Do not print if successfully solved

Returns

bool
True if successful, False otherwise.
Expand source code
def solve_statics(self, silent=False, timeout=None):
    """Solves statics

    Args:
        silent: Do not print if successfully solved

    Returns:
        bool: True if successful, False otherwise.

    """
    self.update()

    if timeout is None:
        solve_func = self._vfc.state_solve_statics
    else:
        #       bool doStabilityCheck,
        #       double timeout,
        #           bool do_prepare_state,
        #           bool solve_linear_dofs_first,
        #           double stability_check_delta
        solve_func = lambda: self._vfc.state_solve_statics_with_timeout(
            True, timeout, True, True, 0
        )  # default stability value

    # pass 1
    orignal_fixes = self._fix_vessel_heel_trim()
    succes = solve_func()
    if not succes:
        self._restore_original_fixes(orignal_fixes)
        return False

    if orignal_fixes:
        # pass 2
        self._restore_original_fixes(orignal_fixes)
        succes = solve_func()

    if self.verify_equilibrium():

        changed, message = self._check_and_fix_geometric_contact_orientations()
        if changed:
            print(message)
            solve_func()
            if not self.verify_equilibrium():
                return False

        if not silent:
            self._print("Solved to {}.".format(self._vfc.Emaxabs))
        return True

    d = np.array(self._vfc.get_dofs())
    if np.any(np.abs(d) > 2000):
        print(
            "Error: One of the degrees of freedom exceeded the boundary of 2000 [m]/[rad]."
        )
        return False

    return False
def sort_nodes_by_dependency(self)

Sorts the nodes such that a nodes creation only depends on nodes earlier in the list.

This sorting is used for node creation order

See Also: sort_nodes_by_parent

Expand source code
def sort_nodes_by_dependency(self):
    """Sorts the nodes such that a nodes creation only depends on nodes earlier in the list.

    This sorting is used for node creation order

    See Also:
        sort_nodes_by_parent
    """

    self.assert_unique_names()

    exported = []
    to_be_exported = self._nodes.copy()
    counter = 0

    while to_be_exported:

        counter += 1
        if counter > len(self._nodes):

            for node in to_be_exported:
                print(f"Node : {node.name}")
                for d in node.depends_on():
                    print(f"  depends on: {d.name}")
                if node._manager:
                    print(f"   managed by: {node._manager.name}")

            raise Exception(
                "Could not sort nodes by dependency, circular references exist?"
            )

        can_be_exported = []

        for node in to_be_exported:
            # if node._manager:
            #     if node._manager in exported:
            #         can_be_exported.append(node)
            # el
            if all(el in exported for el in node.depends_on()):
                can_be_exported.append(node)

        # remove exported nodes from
        for n in can_be_exported:
            to_be_exported.remove(n)

        exported.extend(can_be_exported)

    self._nodes = exported

    # scene_names = [n.name for n in self._nodes]
    #
    # self._vfc.state_update()  # use the function from the core.
    # new_list = []
    # for name in self._vfc.names:  # and then build a new list using the names
    #     if vfc.VF_NAME_SPLIT in name:
    #         continue
    #
    #     if name not in scene_names:
    #         raise Exception('Something went wrong with sorting the the nodes by dependency. '
    #                         'Node naming between core and scene is inconsistent for node {}'.format(name))
    #
    #     new_list.append(self[name])
    #
    # # and add the nodes without a vfc-core connection
    # for node in self._nodes:
    #     if not node in new_list:
    #         new_list.append(node)
    #
    # self._nodes = new_list
def sort_nodes_by_parent(self)

Sorts the nodes such that the parent of this node (if any) occurs earlier in the list.

See Also: sort_nodes_by_dependency

Expand source code
def sort_nodes_by_parent(self):
    """Sorts the nodes such that the parent of this node (if any) occurs earlier in the list.

    See Also:
        sort_nodes_by_dependency
    """

    self.assert_unique_names()

    exported = []
    to_be_exported = self._nodes.copy()
    counter = 0

    while to_be_exported:

        counter += 1
        if counter > len(self._nodes):
            raise Exception(
                "Could not sort nodes by dependency, circular references exist?"
            )

        can_be_exported = []

        for node in to_be_exported:

            if hasattr(node, "parent"):
                parent = node.parent
                if parent is not None and parent not in exported:
                    continue

            if node.manager is not None and node.manager not in exported:
                continue

            # otherwise the node can be exported
            can_be_exported.append(node)

        # remove exported nodes from
        for n in can_be_exported:
            to_be_exported.remove(n)

        exported.extend(can_be_exported)

    self._nodes = exported
def update(self)

Updates the interface between the nodes and the core. This includes the re-calculation of all forces, buoyancy positions, ballast-system cogs etc.

Expand source code
def update(self):
    """Updates the interface between the nodes and the core. This includes the re-calculation of all forces,
    buoyancy positions, ballast-system cogs etc.
    """
    for n in self._nodes:
        n.update()
    self._vfc.state_update()
def verify_equilibrium(self, tol=0.01)

Checks if the current state is an equilibrium

Returns

bool
True if successful, False if not an equilibrium.
Expand source code
def verify_equilibrium(self, tol=1e-2):
    """Checks if the current state is an equilibrium

    Returns:
        bool: True if successful, False if not an equilibrium.

    """
    self.update()
    return self._vfc.Emaxabs < tol
class Shackle (scene, name, kind, a, p, g)

Green-Pin Heavy Duty Bow Shackle BN

visual from: https://www.traceparts.com/en/product/green-pinr-p-6036-green-pinr-heavy-duty-bow-shackle-bn-hdgphm0800-mm?CatalogPath=TRACEPARTS%3ATP04001002006&Product=10-04072013-086517&PartNumber=HDGPHM0800 details from: https://www.greenpin.com/sites/default/files/2019-04/brochure-april-2019.pdf

wll a b c d e f g h i j k weight [t] [mm] [kg] 120 95 95 208 95 147 400 238 647 453 428 50 110 150 105 108 238 105 169 410 275 688 496 485 50 160 200 120 130 279 120 179 513 290 838 564 530 70 235 250 130 140 299 130 205 554 305 904 614 565 70 295 300 140 150 325 140 205 618 305 996 644 585 80 368 400 170 175 376 164 231 668 325 1114 690 665 70 560 500 180 185 398 164 256 718 350 1190 720 710 70 685 600 200 205 444 189 282 718 375 1243 810 775 70 880 700 210 215 454 204 308 718 400 1263 870 820 70 980 800 210 220 464 204 308 718 400 1270 870 820 70 1100 900 220 230 485 215 328 718 420 1296 920 860 70 1280 1000 240 240 515 215 349 718 420 1336 940 900 70 1460 1250 260 270 585 230 369 768 450 1456 1025 970 70 1990 1500 280 290 625 230 369 818 450 1556 1025 1010 70 2400

Returns:

Expand source code
class Shackle(Manager, RigidBody):
    """
    Green-Pin Heavy Duty Bow Shackle BN

    visual from: https://www.traceparts.com/en/product/green-pinr-p-6036-green-pinr-heavy-duty-bow-shackle-bn-hdgphm0800-mm?CatalogPath=TRACEPARTS%3ATP04001002006&Product=10-04072013-086517&PartNumber=HDGPHM0800
    details from: https://www.greenpin.com/sites/default/files/2019-04/brochure-april-2019.pdf

    wll a b c d e f g h i j k weight
    [t] [mm]  [kg]
    120 95 95 208 95 147 400 238 647 453 428 50 110
    150 105 108 238 105 169 410 275 688 496 485 50 160
    200 120 130 279 120 179 513 290 838 564 530 70 235
    250 130 140 299 130 205 554 305 904 614 565 70 295
    300 140 150 325 140 205 618 305 996 644 585 80 368
    400 170 175 376 164 231 668 325 1114 690 665 70 560
    500 180 185 398 164 256 718 350 1190 720 710 70 685
    600 200 205 444 189 282 718 375 1243 810 775 70 880
    700 210 215 454 204 308 718 400 1263 870 820 70 980
    800 210 220 464 204 308 718 400 1270 870 820 70 1100
    900 220 230 485 215 328 718 420 1296 920 860 70 1280
    1000 240 240 515 215 349 718 420 1336 940 900 70 1460
    1250 260 270 585 230 369 768 450 1456 1025 970 70 1990
    1500 280 290 625 230 369 818 450 1556 1025 1010 70 2400

    Returns:

    """

    data = dict()
    # key = wll in t
    # dimensions a..k in [mm]
    #             a     b    c   d     e    f    g    h     i     j    k   weight[kg]
    # index       0     1    2    3    4    5    6    7     8     9    10   11
    data["GP120"] = (95, 95, 208, 95, 147, 400, 238, 647, 453, 428, 50, 110)
    data["GP150"] = (105, 108, 238, 105, 169, 410, 275, 688, 496, 485, 50, 160)
    data["GP200"] = (120, 130, 279, 120, 179, 513, 290, 838, 564, 530, 70, 235)
    data["GP250"] = (130, 140, 299, 130, 205, 554, 305, 904, 614, 565, 70, 295)
    data["GP300"] = (140, 150, 325, 140, 205, 618, 305, 996, 644, 585, 80, 368)
    data["GP400"] = (170, 175, 376, 164, 231, 668, 325, 1114, 690, 665, 70, 560)
    data["GP500"] = (180, 185, 398, 164, 256, 718, 350, 1190, 720, 710, 70, 685)
    data["GP600"] = (200, 205, 444, 189, 282, 718, 375, 1243, 810, 775, 70, 880)
    data["GP700"] = (210, 215, 454, 204, 308, 718, 400, 1263, 870, 820, 70, 980)
    data["GP800"] = (210, 220, 464, 204, 308, 718, 400, 1270, 870, 820, 70, 1100)
    data["GP900"] = (220, 230, 485, 215, 328, 718, 420, 1296, 920, 860, 70, 1280)
    data["GP1000"] = (240, 240, 515, 215, 349, 718, 420, 1336, 940, 900, 70, 1460)
    data["GP1250"] = (260, 270, 585, 230, 369, 768, 450, 1456, 1025, 970, 70, 1990)
    data["GP1500"] = (280, 290, 625, 230, 369, 818, 450, 1556, 1025, 1010, 70, 2400)

    def defined_kinds(self):
        """Defined shackle kinds"""
        list = [a for a in Shackle.data.keys()]
        return list

    def _give_values(self, kind):
        if kind not in Shackle.data:
            for key in Shackle.data.keys():
                print(key)
            raise ValueError(
                f"No data available for a Shackle of kind {kind}. Available values printed above"
            )

        return Shackle.data[kind]

    def __init__(self, scene, name, kind, a, p, g):

        Manager.__init__(self, scene)
        RigidBody.__init__(self, scene, axis=a, poi=p, force=g)

        self.name = name

        _ = self._give_values(kind)  # to make sure it exists

        # origin is at center of pin
        # z-axis up
        # y-axis in direction of pin

        # self.body = scene.new_rigidbody(name=name + '_body')

        # pin
        self.pin_point = scene.new_point(
            name=name + "_pin_point", parent=self, position=(0.0, 0.0, 0.0)
        )
        self.pin = scene.new_circle(
            name=name + "_pin", parent=self.pin_point, axis=(0.0, 1.0, 0.0)
        )

        # bow
        self.bow_point = scene.new_point(name=name + "_bow_point", parent=self)

        self.bow = scene.new_circle(
            name=name + "_bow", parent=self.bow_point, axis=(0.0, 1.0, 0.0)
        )

        # inside circle
        self.inside_point = scene.new_point(
            name=name + "_inside_circle_center", parent=self
        )
        self.inside = scene.new_circle(
            name=name + "_inside", parent=self.inside_point, axis=(1.0, 0, 0)
        )

        # code for GP800_visual
        self.visual_node = scene.new_visual(
            name=name + "_visual",
            parent=self,
            path=r"shackle_gp800.obj",
            offset=(0, 0, 0),
            rotation=(0, 0, 0),
        )

        self.kind = kind

        for n in self.managed_nodes():
            n.manager = self

    def depends_on(self):
        return []

    @property
    def kind(self):
        """Type of shackle, for example GP800 [text]"""
        return self._kind

    @kind.setter
    # @node_setter_manageable   : allow changing of shackle kind
    @node_setter_observable
    def kind(self, kind):

        values = self._give_values(kind)
        weight = values[11] / 1000  # convert to tonne
        pin_dia = values[1] / 1000
        bow_dia = values[0] / 1000
        bow_length_inside = values[5] / 1000
        bow_circle_inside = values[6] / 1000

        cogz = 0.5 * pin_dia + bow_length_inside / 3  # estimated

        remember = self._scene.current_manager

        self._scene.current_manager = (
            self.manager
        )  # WORK-AROUND : in case the shackle itself is managed, fake management

        self.mass = weight
        self.cog = (0, 0, cogz)

        self._scene.current_manager = self  # register self a manager (as it should)

        self.pin.radius = pin_dia / 2

        self.bow_point.position = (
            0.0,
            0.0,
            0.5 * pin_dia + bow_length_inside + 0.5 * bow_dia,
        )
        self.bow.radius = bow_dia / 2

        self.inside_point.position = (
            0,
            0,
            0.5 * pin_dia + bow_length_inside - 0.5 * bow_circle_inside,
        )
        self.inside.radius = bow_circle_inside / 2

        # determine the scale for the shackle
        # based on a GP800
        #
        actual_size = 0.5 * pin_dia + 0.5 * bow_dia + bow_length_inside
        gp800_size = 0.5 * 0.210 + 0.5 * 0.220 + 0.718

        scale = actual_size / gp800_size

        self.visual_node.scale = [scale, scale, scale]

        self._scene.current_manager = remember

        self._kind = kind

    def managed_nodes(self):
        return [
            self.pin_point,
            self.pin,
            self.bow_point,
            self.bow,
            self.inside_point,
            self.inside,
            self.visual_node,
        ]

    def creates(self, node: Node):
        return node in self.managed_nodes()  # all these are created

    def delete(self):

        # delete created nodes
        a = self.managed_nodes()

        for n in a:
            n._manager = None

        for n in a:
            if n in self._scene._nodes:
                self._scene.delete(n)  # delete if it is still available

    def give_python_code(self):
        code = f"# Exporting {self.name}"

        code += "\n# Create Shackle"
        code += f'\ns.new_shackle("{self.name}", kind = "{self.kind}")'  # , elastic={self.elastic})'

        if self.parent_for_export:
            code += f"\ns['{self.name}'].parent = s['{self.parent_for_export.name}']"

        code += "\ns['{}'].position = ({},{},{})".format(self.name, *self.position)
        code += "\ns['{}'].rotation = ({},{},{})".format(self.name, *self.rotation)

        return code

Ancestors

Class variables

var data

Instance variables

var kind

Type of shackle, for example GP800 [text]

Expand source code
@property
def kind(self):
    """Type of shackle, for example GP800 [text]"""
    return self._kind

Methods

def defined_kinds(self)

Defined shackle kinds

Expand source code
def defined_kinds(self):
    """Defined shackle kinds"""
    list = [a for a in Shackle.data.keys()]
    return list
def managed_nodes(self)
Expand source code
def managed_nodes(self):
    return [
        self.pin_point,
        self.pin,
        self.bow_point,
        self.bow,
        self.inside_point,
        self.inside,
        self.visual_node,
    ]

Inherited members

class Sling (scene, name, length, LeyeA, LeyeB, LspliceA, LspliceB, diameter, EA, mass, endA=None, endB=None, sheaves=None)

A Sling is a single wire with an eye on each end. The eyes are created by splicing the end of the sling back into the itself.

The geometry of a sling is defined as follows:

diameter : diameter of the wire LeyeA, LeyeB : inside lengths of the eyes LsplicaA, LspliceB : the length of the splices Total : the distance between the insides of ends of the eyes A and B when pulled straight.

Stiffness: The stiffness of the sling is specified by a single value: EA This determines the stiffnesses of the individual parts as follows: Wire in the eyes: EA Splices: Infinity (rigid) Main part: determined such that total stiffness (k) of the sling is EA/L

Eye A Splice A nodeA part Splice B Eye B

/---------------\ /--------------- | =============-------------------------------------=============== | ---------------/ ---------------/

See Also: Grommet

Creates a new sling with the following structure

endA
eyeA (cable)
splice (body , mass/2)
nodeA (cable)     [optional: runs over sheave]
splice (body, mass/2)
eyeB (cable)
endB

Args

scene

The scene in which the sling should be created

name
Name prefix
length
Total length measured between the inside of the eyes of the sling is pulled straight.
LeyeA
Total inside length in eye A if stretched flat
LeyeB
Total inside length in eye B if stretched flat
LspliceA
Length of the splice at end A
LspliceB
Length of the splice at end B
diameter
Diameter of the sling
EA
Effective mean EA of the sling
mass
total mass
endA : Sheave or poi to fix end A of the sling to [optional]
 
endB : Sheave or poi to fix end A of the sling to [optional]
 
sheave : Sheave or poi for the nodeA part of the sling
 

Returns:

Expand source code
class Sling(Manager):
    """A Sling is a single wire with an eye on each end. The eyes are created by splicing the end of the sling back
    into the itself.

    The geometry of a sling is defined as follows:

    diameter : diameter of the wire
    LeyeA, LeyeB : inside lengths of the eyes
    LsplicaA, LspliceB : the length of the splices
    Total : the distance between the insides of ends of the eyes A and B when pulled straight.

    Stiffness:
    The stiffness of the sling is specified by a single value: EA
    This determines the stiffnesses of the individual parts as follows:
    Wire in the eyes: EA
    Splices: Infinity (rigid)
    Main part: determined such that total stiffness (k) of the sling is EA/L


      Eye A           Splice A             nodeA part                   Splice B          Eye B

    /---------------\                                                                /---------------\
    |                =============-------------------------------------===============                |
    \---------------/                                                                \---------------/

    See Also: Grommet

    """

    def __init__(
        self,
        scene,
        name,
        length,
        LeyeA,
        LeyeB,
        LspliceA,
        LspliceB,
        diameter,
        EA,
        mass,
        endA=None,
        endB=None,
        sheaves=None,
    ):
        """
        Creates a new sling with the following structure

            endA
            eyeA (cable)
            splice (body , mass/2)
            nodeA (cable)     [optional: runs over sheave]
            splice (body, mass/2)
            eyeB (cable)
            endB

        Args:
            scene:     The scene in which the sling should be created
            name:  Name prefix
            length: Total length measured between the inside of the eyes of the sling is pulled straight.
            LeyeA: Total inside length in eye A if stretched flat
            LeyeB: Total inside length in eye B if stretched flat
            LspliceA: Length of the splice at end A
            LspliceB: Length of the splice at end B
            diameter: Diameter of the sling
            EA: Effective mean EA of the sling
            mass: total mass
            endA : Sheave or poi to fix end A of the sling to [optional]
            endB : Sheave or poi to fix end A of the sling to [optional]
            sheave : Sheave or poi for the nodeA part of the sling

        Returns:

        """

        super().__init__(scene)
        self.name = name

        name_prefix = self.name + vfc.MANAGED_NODE_IDENTIFIER

        # store the properties
        self._length = length
        self._LeyeA = LeyeA
        self._LeyeB = LeyeB
        self._LspliceA = LspliceA
        self._LspliceB = LspliceB
        self._diameter = diameter
        self._EA = EA
        self._mass = mass
        self._endA = scene._poi_or_sheave_from_node(endA)
        self._endB = scene._poi_or_sheave_from_node(endB)

        # create the two splices

        self.sa = scene.new_rigidbody(
            scene.available_name_like(name_prefix + "_spliceA"), fixed=False
        )
        self.a1 = scene.new_point(
            scene.available_name_like(name_prefix + "_spliceA"), parent=self.sa
        )
        self.a2 = scene.new_point(
            scene.available_name_like(name_prefix + "_spliceA2"), parent=self.sa
        )
        self.am = scene.new_point(
            scene.available_name_like(name_prefix + "_spliceAM"), parent=self.sa
        )

        self.avis = scene.new_visual(
            name + "_spliceA_visual",
            parent=self.sa,
            path=r"cylinder 1x1x1 lowres.obj",
            offset=(-LspliceA / 2, 0.0, 0.0),
            rotation=(0.0, 90.0, 0.0),
            scale=(LspliceA, 2 * diameter, diameter),
        )

        self.sb = scene.new_rigidbody(
            scene.available_name_like(name_prefix + "_spliceB"),
            rotation=(0, 0, 180),
            fixed=False,
        )
        self.b1 = scene.new_point(
            scene.available_name_like(name_prefix + "_spliceB1"), parent=self.sb
        )
        self.b2 = scene.new_point(
            scene.available_name_like(name_prefix + "_spliceB2"), parent=self.sb
        )
        self.bm = scene.new_point(
            scene.available_name_like(name_prefix + "_spliceBM"), parent=self.sb
        )

        self.bvis = scene.new_visual(
            scene.available_name_like(name_prefix + "_spliceB_visual"),
            parent=self.sb,
            path=r"cylinder 1x1x1 lowres.obj",
            offset=(-LspliceB / 2, 0.0, 0.0),
            rotation=(0.0, 90.0, 0.0),
            scale=(LspliceB, 2 * diameter, diameter),
        )

        self.main = scene.new_cable(
            scene.available_name_like(name_prefix + "_main_part"),
            endA=self.am,
            endB=self.bm,
            length=1,
            EA=1,
            diameter=diameter,
        )

        self.eyeA = scene.new_cable(
            scene.available_name_like(name_prefix + "_eyeA"),
            endA=self.a1,
            endB=self.a2,
            length=1,
            EA=1,
        )
        self.eyeB = scene.new_cable(
            scene.available_name_like(name_prefix + "_eyeB"),
            endA=self.b1,
            endB=self.b2,
            length=1,
            EA=1,
        )

        # set initial positions of splices if we can
        if self._endA is not None and self._endB is not None:
            a = np.array(self._endA.global_position)
            b = np.array(self._endB.global_position)

            dir = b - a
            dir /= np.linalg.norm(dir)

            self.sa.rotation = rotation_from_x_axis_direction(-dir)
            self.sb.rotation = rotation_from_x_axis_direction(dir)
            self.sa.position = a + (LeyeA + 0.5 * LspliceA) * dir
            self.sb.position = b - (LeyeB + 0.5 * LspliceB) * dir

        # Update properties
        self.sheaves = sheaves
        self._update_properties()

        for n in self.managed_nodes():
            n.manager = self

    def _update_properties(self):

        # The stiffness of the nodeA part is corrected to account for the stiffness of the splices.
        # It is considered that the stiffness of the splices is two times that of the wire.
        #
        # Springs in series: 1/Ktotal = 1/k1 + 1/k2 + 1/k3

        backup = self._scene.current_manager  # store
        self._scene.current_manager = self

        Lmain = (
            self._length - self._LspliceA - self._LspliceB - self._LeyeA - self._LeyeB
        )

        if self._EA == 0:
            EAmain = 0
        else:
            ka = 2 * self._EA / self._LspliceA
            kb = 2 * self._EA / self._LspliceB
            kmain = self._EA / Lmain
            k_total = 1 / ((1 / ka) + (1 / kmain) + (1 / kb))

            EAmain = k_total * Lmain

        self.sa.mass = self._mass / 2
        self.sa.inertia_radii = (
            self._LspliceA / 2,
            self._LspliceA / 2,
            self._diameter / 2,
        )

        self.a1.position = (self._LspliceA / 2, self._diameter / 2, 0)
        self.a2.position = (self._LspliceA / 2, -self._diameter / 2, 0)
        self.am.position = (-self._LspliceA / 2, 0, 0)

        self.avis.offset = (-self._LspliceA / 2, 0.0, 0.0)
        self.avis.scale = (self._LspliceA, 2 * self._diameter, self._diameter)

        self.sb.mass = self._mass / 2
        self.sb.inertia_radii = (
            self._LspliceB / 2,
            self._LspliceB / 2,
            self._diameter / 2,
        )

        self.b1.position = (self._LspliceB / 2, self._diameter / 2, 0)
        self.b2.position = (self._LspliceB / 2, -self._diameter / 2, 0)
        self.bm.position = (-self._LspliceB / 2, 0, 0)

        self.bvis.offset = (-self._LspliceB / 2, 0.0, 0.0)
        self.bvis.scale = (self._LspliceB, 2 * self._diameter, self._diameter)

        self.main.length = Lmain
        self.main.EA = EAmain
        self.main.diameter = self._diameter
        self.main.connections = tuple([self.am, *self._sheaves, self.bm])

        self.eyeA.length = self._LeyeA * 2 - self._diameter
        self.eyeA.EA = self._EA
        self.eyeA.diameter = self._diameter

        if self._endA is not None:
            self.eyeA.connections = (self.a1, self._endA, self.a2)
        else:
            self.eyeA.connections = (self.a1, self.a2)

        self.eyeB.length = self._LeyeB * 2 - self._diameter
        self.eyeB.EA = self._EA
        self.eyeB.diameter = self._diameter

        if self._endB is not None:
            self.eyeB.connections = (self.b1, self._endB, self.b2)
        else:
            self.eyeB.connections = (self.b1, self.b2)

        self._scene.current_manager = backup  # restore

    def depends_on(self):
        """The sling depends on the endpoints and sheaves (if any)"""

        a = list()

        if self._endA is not None:
            a.append(self._endA)
        if self._endB is not None:
            a.append(self._endB)

        a.extend(self.sheaves)

        return a

    def managed_nodes(self):
        a = [
            self.sa,
            self.a1,
            self.a2,
            self.am,
            self.avis,
            self.sb,
            self.b1,
            self.b2,
            self.bm,
            self.bvis,
            self.main,
            self.eyeA,
            self.eyeB,
        ]

        return a

    def creates(self, node: Node):
        return node in self.managed_nodes()  # all these are created

    def delete(self):

        # delete created nodes
        a = self.managed_nodes()

        for n in a:
            n._manager = None

        for n in a:
            if n in self._scene._nodes:
                self._scene.delete(n)  # delete if it is still available

    def give_python_code(self):
        code = f"# Exporting {self.name}"

        # if self.endA is not None:
        #     code += self.endA.give_python_code()
        # if self.endB is not None:
        #     code += self.endB.give_python_code()
        # for s in self.sheaves:
        #     code += s.give_python_code()

        code += "\n# Create sling"

        # (self, scene, name, Ltotal, LeyeA, LeyeB, LspliceA, LspliceB, diameter, EA, mass, endA = None, endB=None, sheaves=None):

        code += f'\ns.new_sling("{self.name}", length = {self.length},'
        code += f"\n            LeyeA = {self.LeyeA},"
        code += f"\n            LeyeB = {self.LeyeB},"
        code += f"\n            LspliceA = {self.LspliceA},"
        code += f"\n            LspliceB = {self.LspliceB},"
        code += f"\n            diameter = {self.diameter},"
        code += f"\n            EA = {self.EA},"
        code += f"\n            mass = {self.mass},"
        code += f'\n            endA = "{self.endA.name}",'
        code += f'\n            endB = "{self.endB.name}",'

        if self.sheaves:
            sheaves = "["
            for s in self.sheaves:
                sheaves += f'"{s.name}", '
            sheaves = sheaves[:-2] + "]"
        else:
            sheaves = "None"

        code += f"\n            sheaves = {sheaves})"

        return code

    # properties
    @property
    def length(self):
        """Total length measured between the INSIDE of the eyes of the sling is pulled straight. [m]"""
        return self._length

    @length.setter
    @node_setter_manageable
    @node_setter_observable
    def length(self, value):

        min_length = self.LeyeA + self.LeyeB + self.LspliceA + self.LspliceB
        if value <= min_length:
            raise ValueError(
                "Total length of the sling should be at least the length of the eyes plus the length of the splices"
            )

        self._length = value
        self._update_properties()

    @property
    def LeyeA(self):
        """Total length inside eye A if stretched flat [m]"""
        return self._LeyeA

    @LeyeA.setter
    @node_setter_manageable
    @node_setter_observable
    def LeyeA(self, value):

        max_length = self.length - (self.LeyeB + self.LspliceA + self.LspliceB)
        if value >= max_length:
            raise ValueError(
                "Total length of the sling should be at least the length of the eyes plus the length of the splices"
            )

        self._LeyeA = value
        self._update_properties()

    @property
    def LeyeB(self):
        """Total length inside eye B if stretched flat [m]"""
        return self._LeyeB

    @LeyeB.setter
    @node_setter_manageable
    @node_setter_observable
    def LeyeB(self, value):

        max_length = self.length - (self.LeyeA + self.LspliceA + self.LspliceB)
        if value >= max_length:
            raise ValueError(
                "Total length of the sling should be at least the length of the eyes plus the length of the splices"
            )

        self._LeyeB = value
        self._update_properties()

    @property
    def LspliceA(self):
        """Length of the splice at end A [m]"""
        return self._LspliceA

    @LspliceA.setter
    @node_setter_manageable
    @node_setter_observable
    def LspliceA(self, value):

        max_length = self.length - (self.LeyeA + self.LeyeB + self.LspliceB)
        if value >= max_length:
            raise ValueError(
                "Total length of the sling should be at least the length of the eyes plus the length of the splices"
            )

        self._LspliceA = value
        self._update_properties()

    @property
    def LspliceB(self):
        """Length of the splice at end B [m]"""
        return self._LspliceB

    @LspliceB.setter
    @node_setter_manageable
    @node_setter_observable
    def LspliceB(self, value):

        max_length = self.length - (self.LeyeA + self.LeyeB + self.LspliceA)
        if value >= max_length:
            raise ValueError(
                "Total length of the sling should be at least the length of the eyes plus the length of the splices"
            )

        self._LspliceB = value
        self._update_properties()

    @property
    def diameter(self):
        """Diameter of the sling (except the splices) [m]"""
        return self._diameter

    @diameter.setter
    @node_setter_manageable
    @node_setter_observable
    def diameter(self, value):
        self._diameter = value
        self._update_properties()

    @property
    def EA(self):
        """Effective mean EA of the sling when eyes are flat [kN].
        This is the EA that would be obtained when measuring the stiffness of the sling by putting zero-diameter pins in the eyes and stretching the sling and then using the length between the insides of the eyes."""
        return self._EA

    @EA.setter
    @node_setter_manageable
    @node_setter_observable
    def EA(self, value):
        self._EA = value
        self._update_properties()

    @property
    def mass(self):
        """Mass and weight of the sling. This mass is discretized  distributed over the two splices [mT]"""
        return self._mass

    @mass.setter
    @node_setter_manageable
    @node_setter_observable
    def mass(self, value):
        self._mass = value
        self._update_properties()

    @property
    def endA(self):
        """End A [circle or point node]"""
        return self._endA

    @endA.setter
    @node_setter_manageable
    @node_setter_observable
    def endA(self, value):
        node = self._scene._node_from_node_or_str(value)
        self._endA = self._scene._poi_or_sheave_from_node(node)
        self._update_properties()

    @property
    def endB(self):
        """End B [circle or point node]"""
        return self._endB

    @endB.setter
    @node_setter_manageable
    @node_setter_observable
    def endB(self, value):
        node = self._scene._node_from_node_or_str(value)
        self._endB = self._scene._poi_or_sheave_from_node(node)
        self._update_properties()

    @property
    def sheaves(self):
        """List of sheaves (circles, points) that the sling runs over between the two ends.

        May be provided as list of nodes or node-names.
        """
        return self._sheaves

    @sheaves.setter
    @node_setter_manageable
    @node_setter_observable
    def sheaves(self, value):
        s = []
        for v in value:
            node = self._scene._node_from_node_or_str(v)
            s.append(self._scene._poi_or_sheave_from_node(node))
        self._sheaves = s
        self._update_properties()

Ancestors

Instance variables

var EA

Effective mean EA of the sling when eyes are flat [kN]. This is the EA that would be obtained when measuring the stiffness of the sling by putting zero-diameter pins in the eyes and stretching the sling and then using the length between the insides of the eyes.

Expand source code
@property
def EA(self):
    """Effective mean EA of the sling when eyes are flat [kN].
    This is the EA that would be obtained when measuring the stiffness of the sling by putting zero-diameter pins in the eyes and stretching the sling and then using the length between the insides of the eyes."""
    return self._EA
var LeyeA

Total length inside eye A if stretched flat [m]

Expand source code
@property
def LeyeA(self):
    """Total length inside eye A if stretched flat [m]"""
    return self._LeyeA
var LeyeB

Total length inside eye B if stretched flat [m]

Expand source code
@property
def LeyeB(self):
    """Total length inside eye B if stretched flat [m]"""
    return self._LeyeB
var LspliceA

Length of the splice at end A [m]

Expand source code
@property
def LspliceA(self):
    """Length of the splice at end A [m]"""
    return self._LspliceA
var LspliceB

Length of the splice at end B [m]

Expand source code
@property
def LspliceB(self):
    """Length of the splice at end B [m]"""
    return self._LspliceB
var diameter

Diameter of the sling (except the splices) [m]

Expand source code
@property
def diameter(self):
    """Diameter of the sling (except the splices) [m]"""
    return self._diameter
var endA

End A [circle or point node]

Expand source code
@property
def endA(self):
    """End A [circle or point node]"""
    return self._endA
var endB

End B [circle or point node]

Expand source code
@property
def endB(self):
    """End B [circle or point node]"""
    return self._endB
var length

Total length measured between the INSIDE of the eyes of the sling is pulled straight. [m]

Expand source code
@property
def length(self):
    """Total length measured between the INSIDE of the eyes of the sling is pulled straight. [m]"""
    return self._length
var mass

Mass and weight of the sling. This mass is discretized distributed over the two splices [mT]

Expand source code
@property
def mass(self):
    """Mass and weight of the sling. This mass is discretized  distributed over the two splices [mT]"""
    return self._mass
var sheaves

List of sheaves (circles, points) that the sling runs over between the two ends.

May be provided as list of nodes or node-names.

Expand source code
@property
def sheaves(self):
    """List of sheaves (circles, points) that the sling runs over between the two ends.

    May be provided as list of nodes or node-names.
    """
    return self._sheaves

Methods

def depends_on(self)

The sling depends on the endpoints and sheaves (if any)

Expand source code
def depends_on(self):
    """The sling depends on the endpoints and sheaves (if any)"""

    a = list()

    if self._endA is not None:
        a.append(self._endA)
    if self._endB is not None:
        a.append(self._endB)

    a.extend(self.sheaves)

    return a
def managed_nodes(self)
Expand source code
def managed_nodes(self):
    a = [
        self.sa,
        self.a1,
        self.a2,
        self.am,
        self.avis,
        self.sb,
        self.b1,
        self.b2,
        self.bm,
        self.bvis,
        self.main,
        self.eyeA,
        self.eyeB,
    ]

    return a

Inherited members

class Tank (scene, vfTank)

Tank provides a fillable tank based on a mesh. The mesh is triangulated and chopped at the instantaneous flat fluid surface. Gravity is applied as an downwards force that the center of fluid. The calculation of fluid volume and center is as accurate as the provided geometry.

There as no restrictions to the size or aspect ratio of the panels. It is excellent to model as box using 6 faces. Using smaller panels has a negative effect on performance.

The normals of the panels should point away from the fluid. This means that the same basic shapes can be used for both buoyancy and tanks.

Expand source code
class Tank(NodeWithParent):
    """Tank provides a fillable tank based on a mesh. The mesh is triangulated and chopped at the instantaneous flat fluid surface. Gravity is applied as an downwards force that the center of fluid.
    The calculation of fluid volume and center is as accurate as the provided geometry.

    There as no restrictions to the size or aspect ratio of the panels. It is excellent to model as box using 6 faces. Using smaller panels has a negative effect on performance.

    The normals of the panels should point *away* from the fluid. This means that the same basic shapes can be used for both buoyancy and tanks.
    """

    # init parent and name are fully derived from NodeWithParent
    # _vfNode is a tank
    def __init__(self, scene, vfTank):
        super().__init__(scene, vfTank)
        self._None_parent_acceptable = False
        self._trimesh = TriMeshSource(
            self._scene, self._vfNode.trimesh
        )  # the tri-mesh is wrapped in a custom object

        self._inertia = scene._vfc.new_pointmass(
            self.name + vfc.VF_NAME_SPLIT + "inertia"
        )

    def update(self):
        self._vfNode.reloadTrimesh()

        # update inertia
        self._inertia.parent = self.parent._vfNode
        self._inertia.position = self.cog_local
        self._inertia.inertia = self.volume * self.density


    def _delete_vfc(self):
        self._scene._vfc.delete(self._inertia.name)
        super()._delete_vfc()

    @property
    def trimesh(self) -> TriMeshSource:
        return self._trimesh

    @property
    def free_flooding(self):
        return self._vfNode.free_flooding

    @free_flooding.setter
    def free_flooding(self, value):
        assert isinstance(value, bool), ValueError(
            f"free_flooding shall be a bool, you passed a {type(value)}"
        )
        self._vfNode.free_flooding = value

    @property
    def permeability(self):
        """Permeability is the fraction of the enclosed volume that can be filled with fluid [-]"""
        return self._vfNode.permeability

    @permeability.setter
    def permeability(self, value):
        assert1f_positive_or_zero(value)
        self._vfNode.permeability = value

    @property
    def cog(self):
        """Returns the GLOBAL position of the center of volume / gravity"""
        return self._vfNode.cog

    @property
    def cog_local(self):
        """Returns the local position of the center of gravity"""
        return self.parent.to_loc_position(self.cog)

    @property
    def cog_when_full(self):
        """Returns the LOCAL position of the center of volume / gravity of the tank when it is filled"""
        return self._vfNode.cog_when_full

    @property
    def fill_pct(self):
        """Amount of volume in tank as percentage of capacity [%]"""
        if self.capacity == 0:
            return 0
        return 100 * self.volume / self.capacity

    @fill_pct.setter
    @node_setter_manageable
    @node_setter_observable
    def fill_pct(self, value):

        if value < 0 and value > -0.01:
            value = 0

        assert1f_positive_or_zero(value)

        if value > 100.1:
            raise ValueError(
                f"Fill percentage should be between 0 and 100 [%], {value} is not valid"
            )
        if value > 100:
            value = 100
        self.volume = value * self.capacity / 100

    @property
    def level_global(self):
        """The fluid plane elevation in the global axis system. Setting this adjusts the volume"""
        return self._vfNode.fluid_level_global

    @level_global.setter
    @node_setter_manageable
    @node_setter_observable
    def level_global(self, value):
        assert1f(value)
        self._vfNode.fluid_level_global = value

    @property
    def volume(self):
        """The volume of fluid in the tank in m3. Setting this adjusts the fluid level"""
        return self._vfNode.volume

    @volume.setter
    @node_setter_manageable
    @node_setter_observable
    def volume(self, value):
        assert1f_positive_or_zero(value, "Volume")
        self._vfNode.volume = value

    @property
    def density(self):
        """Density of the fluid in the tank in mT/m3"""
        return self._vfNode.density

    @density.setter
    @node_setter_manageable
    @node_setter_observable
    def density(self, value):
        assert1f(value)
        self._vfNode.density = value

    @property
    def capacity(self):
        """Returns the capacity of the tank in m3. This is calculated from the defined geometry."""
        return self._vfNode.capacity

    def give_python_code(self):
        code = "# code for {}".format(self.name)
        code += "\nmesh = s.new_tank(name='{}',".format(self.name)

        if self.density != 1.025:
            code += f"\n          density={self.density},"

        if self.free_flooding:
            code += f"\n          free_flooding=True,"

        code += "\n          parent='{}')".format(self.parent_for_export.name)

        if self.trimesh._invert_normals:
            code += "\nmesh.trimesh.load_file(r'{}', scale = ({},{},{}), rotation = ({},{},{}), offset = ({},{},{}), invert_normals=True)".format(
                self.trimesh._path,
                *self.trimesh._scale,
                *self.trimesh._rotation,
                *self.trimesh._offset,
            )
        else:
            code += "\nmesh.trimesh.load_file(r'{}', scale = ({},{},{}), rotation = ({},{},{}), offset = ({},{},{}))".format(
                self.trimesh._path,
                *self.trimesh._scale,
                *self.trimesh._rotation,
                *self.trimesh._offset,
            )
        code += f"\ns['{self.name}'].volume = {self.volume}   # first load mesh, then set volume"

        return code

Ancestors

Instance variables

var capacity

Returns the capacity of the tank in m3. This is calculated from the defined geometry.

Expand source code
@property
def capacity(self):
    """Returns the capacity of the tank in m3. This is calculated from the defined geometry."""
    return self._vfNode.capacity
var cog

Returns the GLOBAL position of the center of volume / gravity

Expand source code
@property
def cog(self):
    """Returns the GLOBAL position of the center of volume / gravity"""
    return self._vfNode.cog
var cog_local

Returns the local position of the center of gravity

Expand source code
@property
def cog_local(self):
    """Returns the local position of the center of gravity"""
    return self.parent.to_loc_position(self.cog)
var cog_when_full

Returns the LOCAL position of the center of volume / gravity of the tank when it is filled

Expand source code
@property
def cog_when_full(self):
    """Returns the LOCAL position of the center of volume / gravity of the tank when it is filled"""
    return self._vfNode.cog_when_full
var density

Density of the fluid in the tank in mT/m3

Expand source code
@property
def density(self):
    """Density of the fluid in the tank in mT/m3"""
    return self._vfNode.density
var fill_pct

Amount of volume in tank as percentage of capacity [%]

Expand source code
@property
def fill_pct(self):
    """Amount of volume in tank as percentage of capacity [%]"""
    if self.capacity == 0:
        return 0
    return 100 * self.volume / self.capacity
var free_flooding
Expand source code
@property
def free_flooding(self):
    return self._vfNode.free_flooding
var level_global

The fluid plane elevation in the global axis system. Setting this adjusts the volume

Expand source code
@property
def level_global(self):
    """The fluid plane elevation in the global axis system. Setting this adjusts the volume"""
    return self._vfNode.fluid_level_global
var permeability

Permeability is the fraction of the enclosed volume that can be filled with fluid [-]

Expand source code
@property
def permeability(self):
    """Permeability is the fraction of the enclosed volume that can be filled with fluid [-]"""
    return self._vfNode.permeability
var trimeshTriMeshSource
Expand source code
@property
def trimesh(self) -> TriMeshSource:
    return self._trimesh
var volume

The volume of fluid in the tank in m3. Setting this adjusts the fluid level

Expand source code
@property
def volume(self):
    """The volume of fluid in the tank in m3. Setting this adjusts the fluid level"""
    return self._vfNode.volume

Inherited members

class TriMeshSource (scene, source)

TriMesh

A TriMesh node contains triangular mesh which can be used for buoyancy or contact

Expand source code
class TriMeshSource(Node):
    """
    TriMesh

    A TriMesh node contains triangular mesh which can be used for buoyancy or contact

    """

    def __init__(self, scene, source):

        super().__init__(scene)

        # Note: Visual does not have a corresponding vfCore Node in the scene but does have a vfCore
        self._TriMesh = source
        self._new_mesh = True  # cheat for visuals

        self._path = ""  # stores the data that was used to load the obj
        self._offset = (0, 0, 0)
        self._scale = (1, 1, 1)
        self._rotation = (0, 0, 0)

        self._invert_normals = False

    def depends_on(self) -> list:
        return []

    def AddVertex(self, x, y, z):
        """Adds a vertex (point)"""
        self._TriMesh.AddVertex(x, y, z)

    def AddFace(self, i, j, k):
        """Adds a triangular face between vertex numbers i,j and k"""
        self._TriMesh.AddFace(i, j, k)

    def get_extends(self):
        """Returns the extends of the mesh in global coordinates

        Returns: (minimum_x, maximum_x, minimum_y, maximum_y, minimum_z, maximum_z)

        """

        t = self._TriMesh

        if t.nFaces == 0:
            return (0, 0, 0, 0, 0, 0)

        v = t.GetVertex(0)
        xn = v[0]
        xp = v[0]
        yn = v[1]
        yp = v[1]
        zn = v[2]
        zp = v[2]

        for i in range(t.nVertices):
            v = t.GetVertex(i)
            x = v[0]
            y = v[1]
            z = v[2]

            if x < xn:
                xn = x
            if x > xp:
                xp = x
            if y < yn:
                yn = y
            if y > yp:
                yp = y
            if z < zn:
                zn = z
            if z > zp:
                zp = z

        return (xn, xp, yn, yp, zn, zp)

    def _fromVTKpolydata(
        self, polydata, offset=None, rotation=None, scale=None, invert_normals=False
    ):

        import vtk

        tri = vtk.vtkTriangleFilter()

        tri.SetInputConnection(polydata)

        scaleFilter = vtk.vtkTransformPolyDataFilter()
        rotationFilter = vtk.vtkTransformPolyDataFilter()

        s = vtk.vtkTransform()
        s.Identity()
        r = vtk.vtkTransform()
        r.Identity()

        rotationFilter.SetInputConnection(tri.GetOutputPort())
        scaleFilter.SetInputConnection(rotationFilter.GetOutputPort())

        if scale is not None:
            s.Scale(*scale)

        if rotation is not None:
            q = rotation
            angle = (q[0] ** 2 + q[1] ** 2 + q[2] ** 2) ** (0.5)
            if angle > 0:
                r.RotateWXYZ(angle, q[0] / angle, q[1] / angle, q[2] / angle)

        if offset is None:
            offset = [0, 0, 0]

        scaleFilter.SetTransform(s)
        rotationFilter.SetTransform(r)

        scaleFilter.Update()
        data = scaleFilter.GetOutput()
        self._TriMesh.Clear()

        for i in range(data.GetNumberOfPoints()):
            point = data.GetPoint(i)
            self._TriMesh.AddVertex(
                point[0] + offset[0], point[1] + offset[1], point[2] + offset[2]
            )

        for i in range(data.GetNumberOfCells()):
            cell = data.GetCell(i)

            if isinstance(cell, vtk.vtkLine):
                print("Cell nr {} is a line, not adding to mesh".format(i))
                continue

            if isinstance(cell, vtk.vtkVertex):
                print("Cell nr {} is a vertex, not adding to mesh".format(i))
                continue

            id0 = cell.GetPointId(0)
            id1 = cell.GetPointId(1)
            id2 = cell.GetPointId(2)

            if invert_normals:
                self._TriMesh.AddFace(id2, id1, id0)
            else:
                self._TriMesh.AddFace(id0, id1, id2)

        # check if anything was loaded
        if self._TriMesh.nFaces == 0:
            raise Exception(
                "No faces in poly-data - no geometry added (hint: empty obj file?)"
            )
        self._new_mesh = True
        self._scene.update()

    def check_shape(self):
        """Performs some checks on the shape in the trimesh
        - Boundary edges (edge with only one face attached)
        - Non-manifold edges (edit with more than two faces attached)
        - Volume should be positive
        """

        tm = self._TriMesh

        if tm.nFaces == 0:
            return ["No mesh"]

        # Make a list of all boundaries using their vertex IDs
        boundaries = np.zeros((3 * tm.nFaces, 2))
        for i in range(tm.nFaces):
            face = tm.GetFace(i)
            boundaries[3 * i] = [face[0], face[1]]
            boundaries[3 * i + 1] = [face[1], face[2]]
            boundaries[3 * i + 2] = [face[2], face[0]]

        # For an edge is doesn't matter in which direction it runs
        boundaries.sort(axis=1)

        rows_occurance_count = np.unique(boundaries, axis=0, return_counts=True)[
            1
        ]  # count of rows

        n_boundary = np.count_nonzero(rows_occurance_count == 1)
        n_nonmanifold = np.count_nonzero(rows_occurance_count > 2)

        messages = []

        if n_boundary > 0:
            messages.append(f"Mesh contains {n_boundary} boundary edges")
        if n_nonmanifold > 0:
            messages.append(f"Mesh contains {n_nonmanifold} non-manifold edges")

        # Do not check for volume if we have nonmanifold geometry or boundary edges
        try:
            volume = tm.Volume()
        except:
            volume = 1  # no available in every pyo3d yet

        if volume < 0:
            messages.append(
                f"Total mesh volume is negative ({volume:.2f} m3 of enclosed volume)."
            )
            messages.append("Hint: Use invert-normals")

        return messages

    def load_vtk_polydataSource(self, polydata):
        """Fills the triangle data from a vtk polydata such as a cubeSource.

        The vtk TriangleFilter is used to triangulate the source

        Examples:
            cube = vtk.vtkCubeSource()
            cube.SetXLength(122)
            cube.SetYLength(38)
            cube.SetZLength(10)
            trimesh.load_vtk_polydataSource(cube)
        """

        self._fromVTKpolydata(polydata.GetOutputPort())

    def load_obj(
        self, filename, offset=None, rotation=None, scale=None, invert_normals=False
    ):
        self.load_file(filename, offset, rotation, scale, invert_normals)

    def load_file(
        self, url, offset=None, rotation=None, scale=None, invert_normals=False
    ):
        """Loads an .obj or .stl file and and triangulates it.

        Order of modifications:

        1. rotate
        2. scale
        3. offset

        Args:
            url: (str or path or resource): file to load
            offset: : offset
            rotation:  : rotation
            scale:  scale

        """

        self._path = str(url)

        filename = str(self._scene.get_resource_path(url))

        import vtk

        ext = filename.lower()[-3:]
        if ext == "obj":
            obj = vtk.vtkOBJReader()
            obj.SetFileName(filename)
        elif ext == "stl":
            obj = vtk.vtkSTLReader()
            obj.SetFileName(filename)
        else:
            raise ValueError(
                f"File should be an .obj or .stl file but has extension {ext}"
            )

        # Add cleaning
        cln = vtk.vtkCleanPolyData()
        cln.SetInputConnection(obj.GetOutputPort())

        self._fromVTKpolydata(
            cln.GetOutputPort(),
            offset=offset,
            rotation=rotation,
            scale=scale,
            invert_normals=invert_normals,
        )

        self._scale = scale
        self._offset = offset
        self._rotation = rotation

        if self._scale is None:
            self._scale = (1.0, 1.0, 1.0)
        if self._offset is None:
            self._offset = (0.0, 0.0, 0.0)
        if self._rotation is None:
            self._rotation = (0.0, 0.0, 0.0)
        self._invert_normals = invert_normals

    def _load_from_privates(self):
        """(Re)Loads the mesh using the values currently stored in _scale, _offset, _rotation and _invert_normals"""
        self.load_file(url = self._path,
                       scale=self._scale,
                       offset=self._offset,
                       rotation=self._rotation,
                       invert_normals=self._invert_normals)


    def give_python_code(self):
        code = "# No code generated for TriMeshSource"
        return code

    # def change_parent_to(self, new_parent):
    #
    #     if not (isinstance(new_parent, Axis) or new_parent is None):
    #         raise ValueError('Visuals can only be attached to an axis (or derived) or None')
    #
    #     # get current position and orientation
    #     if self.parent is not None:
    #         cur_position = self.parent.to_glob_position(self.offset)
    #         cur_rotation = self.parent.to_glob_direction(self.rotation)
    #     else:
    #         cur_position = self.offset
    #         cur_rotation = self.rotation
    #
    #     self.parent = new_parent
    #
    #     if new_parent is None:
    #         self.offset = cur_position
    #         self.rotation = cur_rotation
    #     else:
    #         self.offset = new_parent.to_loc_position(cur_position)
    #         self.rotation = new_parent.to_loc_direction(cur_rotation)

Ancestors

Methods

def AddFace(self, i, j, k)

Adds a triangular face between vertex numbers i,j and k

Expand source code
def AddFace(self, i, j, k):
    """Adds a triangular face between vertex numbers i,j and k"""
    self._TriMesh.AddFace(i, j, k)
def AddVertex(self, x, y, z)

Adds a vertex (point)

Expand source code
def AddVertex(self, x, y, z):
    """Adds a vertex (point)"""
    self._TriMesh.AddVertex(x, y, z)
def check_shape(self)

Performs some checks on the shape in the trimesh - Boundary edges (edge with only one face attached) - Non-manifold edges (edit with more than two faces attached) - Volume should be positive

Expand source code
def check_shape(self):
    """Performs some checks on the shape in the trimesh
    - Boundary edges (edge with only one face attached)
    - Non-manifold edges (edit with more than two faces attached)
    - Volume should be positive
    """

    tm = self._TriMesh

    if tm.nFaces == 0:
        return ["No mesh"]

    # Make a list of all boundaries using their vertex IDs
    boundaries = np.zeros((3 * tm.nFaces, 2))
    for i in range(tm.nFaces):
        face = tm.GetFace(i)
        boundaries[3 * i] = [face[0], face[1]]
        boundaries[3 * i + 1] = [face[1], face[2]]
        boundaries[3 * i + 2] = [face[2], face[0]]

    # For an edge is doesn't matter in which direction it runs
    boundaries.sort(axis=1)

    rows_occurance_count = np.unique(boundaries, axis=0, return_counts=True)[
        1
    ]  # count of rows

    n_boundary = np.count_nonzero(rows_occurance_count == 1)
    n_nonmanifold = np.count_nonzero(rows_occurance_count > 2)

    messages = []

    if n_boundary > 0:
        messages.append(f"Mesh contains {n_boundary} boundary edges")
    if n_nonmanifold > 0:
        messages.append(f"Mesh contains {n_nonmanifold} non-manifold edges")

    # Do not check for volume if we have nonmanifold geometry or boundary edges
    try:
        volume = tm.Volume()
    except:
        volume = 1  # no available in every pyo3d yet

    if volume < 0:
        messages.append(
            f"Total mesh volume is negative ({volume:.2f} m3 of enclosed volume)."
        )
        messages.append("Hint: Use invert-normals")

    return messages
def get_extends(self)

Returns the extends of the mesh in global coordinates

Returns: (minimum_x, maximum_x, minimum_y, maximum_y, minimum_z, maximum_z)

Expand source code
def get_extends(self):
    """Returns the extends of the mesh in global coordinates

    Returns: (minimum_x, maximum_x, minimum_y, maximum_y, minimum_z, maximum_z)

    """

    t = self._TriMesh

    if t.nFaces == 0:
        return (0, 0, 0, 0, 0, 0)

    v = t.GetVertex(0)
    xn = v[0]
    xp = v[0]
    yn = v[1]
    yp = v[1]
    zn = v[2]
    zp = v[2]

    for i in range(t.nVertices):
        v = t.GetVertex(i)
        x = v[0]
        y = v[1]
        z = v[2]

        if x < xn:
            xn = x
        if x > xp:
            xp = x
        if y < yn:
            yn = y
        if y > yp:
            yp = y
        if z < zn:
            zn = z
        if z > zp:
            zp = z

    return (xn, xp, yn, yp, zn, zp)
def load_file(self, url, offset=None, rotation=None, scale=None, invert_normals=False)

Loads an .obj or .stl file and and triangulates it.

Order of modifications:

  1. rotate
  2. scale
  3. offset

Args

url
(str or path or resource): file to load
offset
: offset
rotation
: rotation
scale
scale
Expand source code
def load_file(
    self, url, offset=None, rotation=None, scale=None, invert_normals=False
):
    """Loads an .obj or .stl file and and triangulates it.

    Order of modifications:

    1. rotate
    2. scale
    3. offset

    Args:
        url: (str or path or resource): file to load
        offset: : offset
        rotation:  : rotation
        scale:  scale

    """

    self._path = str(url)

    filename = str(self._scene.get_resource_path(url))

    import vtk

    ext = filename.lower()[-3:]
    if ext == "obj":
        obj = vtk.vtkOBJReader()
        obj.SetFileName(filename)
    elif ext == "stl":
        obj = vtk.vtkSTLReader()
        obj.SetFileName(filename)
    else:
        raise ValueError(
            f"File should be an .obj or .stl file but has extension {ext}"
        )

    # Add cleaning
    cln = vtk.vtkCleanPolyData()
    cln.SetInputConnection(obj.GetOutputPort())

    self._fromVTKpolydata(
        cln.GetOutputPort(),
        offset=offset,
        rotation=rotation,
        scale=scale,
        invert_normals=invert_normals,
    )

    self._scale = scale
    self._offset = offset
    self._rotation = rotation

    if self._scale is None:
        self._scale = (1.0, 1.0, 1.0)
    if self._offset is None:
        self._offset = (0.0, 0.0, 0.0)
    if self._rotation is None:
        self._rotation = (0.0, 0.0, 0.0)
    self._invert_normals = invert_normals
def load_obj(self, filename, offset=None, rotation=None, scale=None, invert_normals=False)
Expand source code
def load_obj(
    self, filename, offset=None, rotation=None, scale=None, invert_normals=False
):
    self.load_file(filename, offset, rotation, scale, invert_normals)
def load_vtk_polydataSource(self, polydata)

Fills the triangle data from a vtk polydata such as a cubeSource.

The vtk TriangleFilter is used to triangulate the source

Examples

cube = vtk.vtkCubeSource() cube.SetXLength(122) cube.SetYLength(38) cube.SetZLength(10) trimesh.load_vtk_polydataSource(cube)

Expand source code
def load_vtk_polydataSource(self, polydata):
    """Fills the triangle data from a vtk polydata such as a cubeSource.

    The vtk TriangleFilter is used to triangulate the source

    Examples:
        cube = vtk.vtkCubeSource()
        cube.SetXLength(122)
        cube.SetYLength(38)
        cube.SetZLength(10)
        trimesh.load_vtk_polydataSource(cube)
    """

    self._fromVTKpolydata(polydata.GetOutputPort())

Inherited members

class Visual (scene)

Visual

A Visual node contains a 3d visual, typically obtained from a .obj file. A visual node can be placed on an axis-type node.

It is used for visualization. It does not affect the forces, dynamics or statics.

The visual can be given an offset, rotation and scale. These are applied in the following order

  1. rotate
  2. scale
  3. offset

Hint: To scale before rotation place the visual on a dedicated axis and rotate that axis.

Expand source code
class Visual(Node):
    """
    Visual

    .. image:: ./images/visual.png

    A Visual node contains a 3d visual, typically obtained from a .obj file.
    A visual node can be placed on an axis-type node.

    It is used for visualization. It does not affect the forces, dynamics or statics.

    The visual can be given an offset, rotation and scale. These are applied in the following order

    1. rotate
    2. scale
    3. offset

    Hint: To scale before rotation place the visual on a dedicated axis and rotate that axis.

    """

    def __init__(self, scene):

        super().__init__(scene)

        self.offset = [0, 0, 0]
        """Offset (x,y,z) of the visual. Offset is applied after scaling"""
        self.rotation = [0, 0, 0]
        """Rotation (rx,ry,rz) of the visual"""

        self.scale = [1, 1, 1]
        """Scaling of the visual. Scaling is applied before offset."""

        self.path = ""
        """Filename of the visual"""

        self.parent = None
        """Parent : Axis-type"""

    @property
    def file_path(self):
        return self._scene.get_resource_path(self.path)

    def depends_on(self):
        return [self.parent]

    def give_python_code(self):
        code = "# code for {}".format(self.name)

        code += "\ns.new_visual(name='{}',".format(self.name)
        code += "\n            parent='{}',".format(self.parent.name)
        code += "\n            path=r'{}',".format(self.path)
        code += "\n            offset=({}, {}, {}), ".format(*self.offset)
        code += "\n            rotation=({}, {}, {}), ".format(*self.rotation)
        code += "\n            scale=({}, {}, {}) )".format(*self.scale)

        return code

    def change_parent_to(self, new_parent):

        if not (isinstance(new_parent, Axis) or new_parent is None):
            raise ValueError(
                "Visuals can only be attached to an axis (or derived) or None"
            )

        # get current position and orientation
        if self.parent is not None:
            cur_position = self.parent.to_glob_position(self.offset)
            cur_rotation = self.parent.to_glob_direction(self.rotation)
        else:
            cur_position = self.offset
            cur_rotation = self.rotation

        self.parent = new_parent

        if new_parent is None:
            self.offset = cur_position
            self.rotation = cur_rotation
        else:
            self.offset = new_parent.to_loc_position(cur_position)
            self.rotation = new_parent.to_loc_direction(cur_rotation)

Ancestors

Instance variables

var file_path
Expand source code
@property
def file_path(self):
    return self._scene.get_resource_path(self.path)
var offset

Offset (x,y,z) of the visual. Offset is applied after scaling

var parent

Parent : Axis-type

var path

Filename of the visual

var rotation

Rotation (rx,ry,rz) of the visual

var scale

Scaling of the visual. Scaling is applied before offset.

Methods

def change_parent_to(self, new_parent)
Expand source code
def change_parent_to(self, new_parent):

    if not (isinstance(new_parent, Axis) or new_parent is None):
        raise ValueError(
            "Visuals can only be attached to an axis (or derived) or None"
        )

    # get current position and orientation
    if self.parent is not None:
        cur_position = self.parent.to_glob_position(self.offset)
        cur_rotation = self.parent.to_glob_direction(self.rotation)
    else:
        cur_position = self.offset
        cur_rotation = self.rotation

    self.parent = new_parent

    if new_parent is None:
        self.offset = cur_position
        self.rotation = cur_rotation
    else:
        self.offset = new_parent.to_loc_position(cur_position)
        self.rotation = new_parent.to_loc_direction(cur_rotation)

Inherited members

class WaveInteraction1 (scene)

WaveInteraction

Wave-interaction-1 couples a first-order hydrodynamic database to an axis.

This adds: - wave-forces - damping - added mass

The data is provided by a Hyddb1 object which is defined in the MaFreDo package. The contents are not embedded but are to be provided separately in a file. This node contains only the file-name.

Expand source code
class WaveInteraction1(Node):
    """
    WaveInteraction

    Wave-interaction-1 couples a first-order hydrodynamic database to an axis.

    This adds:
    - wave-forces
    - damping
    - added mass

    The data is provided by a Hyddb1 object which is defined in the MaFreDo package. The contents are not embedded
    but are to be provided separately in a file. This node contains only the file-name.

    """

    def __init__(self, scene):

        super().__init__(scene)

        self.offset = [0, 0, 0]
        """Position (x,y,z) of the hydrodynamic origin in its parents axis system"""

        self.parent = None
        """Parent : Axis-type"""

        self.path = None
        """Filename of a file that can be read by a Hyddb1 object"""

    @property
    def file_path(self):
        return self._scene.get_resource_path(self.path)

    def depends_on(self):
        return [self.parent]

    def give_python_code(self):
        code = "# code for {}".format(self.name)

        code += "\ns.new_waveinteraction(name='{}',".format(self.name)
        code += "\n            parent='{}',".format(self.parent.name)
        code += "\n            path=r'{}',".format(self.path)
        code += "\n            offset=({}, {}, {}) )".format(*self.offset)

        return code

    def change_parent_to(self, new_parent):

        if not (isinstance(new_parent, Axis)):
            raise ValueError(
                "Hydrodynamic databases can only be attached to an axis (or derived)"
            )

        # get current position and orientation
        if self.parent is not None:
            cur_position = self.parent.to_glob_position(self.offset)
        else:
            cur_position = self.offset

        self.parent = new_parent
        self.offset = new_parent.to_loc_position(cur_position)

Ancestors

Instance variables

var file_path
Expand source code
@property
def file_path(self):
    return self._scene.get_resource_path(self.path)
var offset

Position (x,y,z) of the hydrodynamic origin in its parents axis system

var parent

Parent : Axis-type

var path

Filename of a file that can be read by a Hyddb1 object

Methods

def change_parent_to(self, new_parent)
Expand source code
def change_parent_to(self, new_parent):

    if not (isinstance(new_parent, Axis)):
        raise ValueError(
            "Hydrodynamic databases can only be attached to an axis (or derived)"
        )

    # get current position and orientation
    if self.parent is not None:
        cur_position = self.parent.to_glob_position(self.offset)
    else:
        cur_position = self.offset

    self.parent = new_parent
    self.offset = new_parent.to_loc_position(cur_position)

Inherited members