Skip to content

visualdynamics.core.geometry

geometry

Test/model geometry: nodes, coordinate systems, tracelines, elements.

Node coordinates are stored in SI meters once their units are known, resolved into the global cartesian frame. A geometry imported from a source that does not declare units (sdynpy .npz, exodus, a UNV without a 164) keeps the file's raw coordinates and reports length_unit is None until define_units() is called. Element types use the UFF dataset 2412 FE descriptor codes as the interchange vocabulary.

Classes:

Name Description
Geometry

Nodes, coordinate systems, tracelines, elements and blocks.

Classes

Geometry

Geometry(
    node_id: Ids,
    node_xyz: ArrayLike,
    node_def_cs: ArrayLike | None = None,
    node_disp_cs: ArrayLike | None = None,
    node_color: ArrayLike | None = None,
    cs_id: Ids | None = None,
    cs_name: Sequence[str] | None = None,
    cs_type: ArrayLike | None = None,
    cs_matrix: ArrayLike | None = None,
    traceline_id: Ids | None = None,
    traceline_color: ArrayLike | None = None,
    traceline_desc: Sequence[str] | None = None,
    traceline_conn: Sequence[ArrayLike] | None = None,
    elem_id: Ids | None = None,
    elem_type: ArrayLike | None = None,
    elem_color: ArrayLike | None = None,
    elem_conn: Sequence[ArrayLike] | None = None,
    elem_block: Ids | None = None,
    block_id: Ids | None = None,
    block_name: Sequence[str] | None = None,
    length_unit: str | None = None,
)

Nodes, coordinate systems, tracelines, elements and blocks.

Parameters are array-likes; connectivity lists contain one integer array of node ids per traceline/element. All coordinates in SI meters.

The arrays below are the storage; nodes, coordinate_systems, tracelines, elements and blocks are views onto them, which is how the tree lists a geometry and how a script should usually reach one. A view is not a copy — writing through a row writes here.

Attributes: node_id: Every node's id. Unique, because connectivity and placement refer to nodes by id. node_xyz: (nodes, 3) coordinates, in metres once length_unit is declared and the file's raw numbers before that. node_def_cs: The coordinate system each node is placed in. node_disp_cs: The system each node is measured in — the frame a shape's values at that node are expressed in. node_color: Palette index per node. cs_id: Coordinate system ids. Unique, for the same reason as nodes. cs_name: A name per system, often empty. cs_type: 0 cartesian, 1 cylindrical, 2 spherical (CS_TYPES). cs_matrix: (systems, 4, 3) — three direction rows then the origin, so cs_matrix[i, 3] is where system i sits. traceline_id: Ids of the display polylines. Not unique: a UNV trace line that lifts the pen arrives as several runs under one id, and deleting that id removes all of them. traceline_color: Palette index per line. traceline_desc: Free text per line. traceline_conn: One array of node ids per line, in drawing order. elem_id: Element ids. Labels — nothing refers to them. elem_type: UFF dataset 2412 descriptor code per element (ELEMENT_TYPES names them and says how each is drawn). elem_color: Palette index per element. elem_conn: One array of node ids per element. elem_block: Which block each element belongs to, by block id. block_id: The declared blocks. Unique, since elements name them. block_name: A name per block — 'wing', 'arm front left'. This is where a mesh records that a region is a different part from its neighbour, and fem.Model.from_geometry reads a member's section from it. length_unit: What the coordinates are in, or None while that has not been declared — in which case they are the file's own numbers and nothing has been scaled.

Methods:

Name Description
validate

Check the geometry hangs together — every element's nodes

node_index

Positions of the given node ids in the node arrays.

contains_nodes

Boolean array: which of node_ids this geometry defines.

missing_dofs

The DOF strings whose node this geometry does not define.

suggest_mass_properties

The centroid of the nodes as the reference point, and no

define_units

Declare what the coordinates are in, converting them to SI.

undefine_units

Take the declaration back, restoring the file's raw coordinates.

add_node

Append a node at xyz (SI). Returns its id.

add_coordinate_system

Append a coordinate system. Returns its id.

add_traceline

Append a traceline through the given nodes. Returns its index.

block_of

The name of the block an element belongs to, or ''.

elements_in

The ids of the elements in a block, named or numbered.

add_block

Declare an element block. Returns its id.

add_element

Append an element. Type defaults to whatever fits the node count:

renumber_node

Give a node a new id, carrying its tracelines and elements over.

renumber_block

Give a block a new id, carrying its elements over.

renumber_coordinate_system

Give a coordinate system a new id, repointing the nodes using it.

delete_nodes

Remove nodes, and anything that referenced them.

delete_coordinate_systems

Remove coordinate systems, reassigning any node that used them.

delete_tracelines

Remove tracelines by id. Ids that are not there are ignored.

delete_blocks

Remove blocks, moving anything in them into the first one left.

delete_elements

Remove elements by id. Ids that are not there are ignored.

save

Write the geometry to a file of its own.

plot

Draw the geometry: nodes, elements and tracelines.

plot_dofs

This geometry with labelled arrows at every DOF source

Attributes:

Name Type Description
num_nodes int

How many nodes the geometry defines.

nodes EntityView

Every node: ids, xyz, colors, and the two systems.

coordinate_systems EntityView

Every coordinate system: ids, names, types, matrices.

tracelines EntityView

Every traceline: ids, descriptions, colors, nodes.

elements EntityView

Every element: ids, types, colors, nodes.

blocks EntityView

Every element block: ids and names.

extent tuple[ndarray, ndarray]

(min_xyz, max_xyz), in meters once units are defined.

units_defined bool

Whether the geometry knows what its coordinates mean.

Source code in src/visualdynamics/core/geometry.py
def __init__(self, node_id: Ids, node_xyz: ArrayLike,
             node_def_cs: ArrayLike | None = None,
             node_disp_cs: ArrayLike | None = None,
             node_color: ArrayLike | None = None,
             cs_id: Ids | None = None,
             cs_name: Sequence[str] | None = None,
             cs_type: ArrayLike | None = None,
             cs_matrix: ArrayLike | None = None,
             traceline_id: Ids | None = None,
             traceline_color: ArrayLike | None = None,
             traceline_desc: Sequence[str] | None = None,
             traceline_conn: Sequence[ArrayLike] | None = None,
             elem_id: Ids | None = None,
             elem_type: ArrayLike | None = None,
             elem_color: ArrayLike | None = None,
             elem_conn: Sequence[ArrayLike] | None = None,
             elem_block: Ids | None = None,
             block_id: Ids | None = None,
             block_name: Sequence[str] | None = None,
             length_unit: str | None = None) -> None:
    # coordinates are taken as given; length_unit records what they are
    # in (None = undefined, values are the file's raw numbers)
    self.length_unit: str | None = length_unit
    n = len(node_id)
    self.node_id: IdArray = _ids(node_id, 'node ids', unique=True)
    self.node_xyz: NDArray[np.float64] = np.asarray(
        node_xyz, dtype=np.float64).reshape(n, 3)
    self.node_def_cs: IdArray = self._default(node_def_cs, n, 1)
    self.node_disp_cs: IdArray = self._default(node_disp_cs, n, 1)
    self.node_color: NDArray[np.int64] = self._default(node_color, n, 1)

    if cs_id is None:
        cs_id, cs_name, cs_type = [1], [''], [0]
        cs_matrix = np.vstack([np.eye(3), np.zeros(3)])[np.newaxis]
    c = len(cs_id)
    self.cs_id: IdArray = _ids(cs_id, 'coordinate system ids', unique=True)
    self.cs_name: list[str] = (list(cs_name) if cs_name is not None
                               else [''] * c)
    self.cs_type: NDArray[np.int64] = self._default(cs_type, c, 0)
    self.cs_matrix: NDArray[np.float64] = (
        np.asarray(cs_matrix, dtype=np.float64).reshape(c, 4, 3)
        if cs_matrix is not None
        else np.tile(np.vstack([np.eye(3), np.zeros(3)]), (c, 1, 1)))

    t = len(traceline_conn) if traceline_conn is not None else 0
    self.traceline_id: IdArray = (
        _ids(traceline_id, 'traceline ids') if traceline_id is not None
        else self._default(None, t, None, arange=True))
    self.traceline_color: NDArray[np.int64] = self._default(
        traceline_color, t, 1)
    self.traceline_desc: list[str] = (
        list(traceline_desc) if traceline_desc is not None else [''] * t)
    self.traceline_conn: list[IdArray] = [
        np.asarray(c, dtype=np.int64) for c in (traceline_conn or [])]

    e = len(elem_conn) if elem_conn is not None else 0
    self.elem_id: IdArray = (
        _ids(elem_id, 'element ids') if elem_id is not None
        else self._default(None, e, None, arange=True))
    self.elem_type: NDArray[np.int64] = self._default(elem_type, e, 0)
    self.elem_color: NDArray[np.int64] = self._default(elem_color, e, 1)
    # Which block each element belongs to, and what the blocks are
    # called. This is how a mesh says "these elements are the wing and
    # those are the tail": exodus calls them element blocks, and it is
    # the only place a file records that a region is made of something
    # different from its neighbour. Read without keeping it, a
    # two-block file comes back as one anonymous block on the way out.
    self.elem_block: IdArray = (
        _ids(elem_block, 'element block ids') if elem_block is not None
        else self._default(None, e, 1))
    found = (np.unique(self.elem_block) if e
             else np.array([], dtype=np.int64))
    self.block_id: IdArray = (
        _ids(block_id, 'block ids', unique=True)
        if block_id is not None else found)
    self.block_name: list[str] = (list(block_name) if block_name is not None
                                  else [''] * len(self.block_id))
    self.elem_conn: list[IdArray] = [
        np.asarray(c, dtype=np.int64) for c in (elem_conn or [])]
    #: the reference point, mass and inertia the rigid-body view
    #: sets, riding the geometry the way averaging rides a time
    #: history (`core.rigid`); None until set or adopted
    self.mass_properties: MassProperties | None = None

    self.validate()
Attributes
num_nodes property
num_nodes: int

How many nodes the geometry defines.

nodes property
nodes: EntityView

Every node: ids, xyz, colors, and the two systems.

coordinate_systems property
coordinate_systems: EntityView

Every coordinate system: ids, names, types, matrices.

tracelines property
tracelines: EntityView

Every traceline: ids, descriptions, colors, nodes.

elements property
elements: EntityView

Every element: ids, types, colors, nodes.

blocks property
blocks: EntityView

Every element block: ids and names.

A block groups elements rather than holding them — which elements are in one is read off elem_block (elements_in), so moving an element between blocks is an edit to the element.

extent property
extent: tuple[ndarray, ndarray]

(min_xyz, max_xyz), in meters once units are defined.

units_defined property
units_defined: bool

Whether the geometry knows what its coordinates mean. False until define_units names the length unit.

Methods:
validate
validate() -> None

Check the geometry hangs together — every element's nodes present, every identifier unique — and report what does not.

Source code in src/visualdynamics/core/geometry.py
def validate(self) -> None:
    """Check the geometry hangs together — every element's nodes
    present, every identifier unique — and report what does not."""
    # An id that other data points at has to mean one thing:
    # connectivity names nodes by id, and a node names the systems it
    # is placed and measured in. Traceline and element ids are
    # labels — nothing refers to them — and a UNV trace line that
    # lifts the pen legitimately arrives as several polylines under
    # one id, so they are not held to this.
    # the same helper the constructor uses, so a duplicate introduced
    # by an edit is refused in the same words as one handed in
    for label, values in (('node ids', self.node_id),
                          ('coordinate system ids', self.cs_id)):
        _ids(values, label, unique=True)
    known = set(self.node_id.tolist())
    for kind, conns in (('traceline', self.traceline_conn),
                        ('element', self.elem_conn)):
        for i, conn in enumerate(conns):
            missing = set(conn.tolist()) - known
            if missing:
                raise ValueError(
                    f"{kind} {i} references unknown node ids {sorted(missing)}")
    if len(self.block_name) != len(self.block_id):
        raise ValueError(
            f'{len(self.block_name)} block names for '
            f'{len(self.block_id)} blocks')
    _ids(self.block_id, 'block ids', unique=True)
    stray = set(np.unique(self.elem_block).tolist()) - set(
        self.block_id.tolist())
    if stray:
        raise ValueError(
            'elements name blocks the geometry does not have: '
            + ', '.join(str(b) for b in sorted(stray)))
    for code in np.unique(self.elem_type) if len(self.elem_type) else []:
        if int(code) not in ELEMENT_TYPES:
            raise ValueError(f"Unknown element type code {int(code)}")
node_index
node_index(node_ids: Ids) -> ndarray

Positions of the given node ids in the node arrays.

Parameters:

Name Type Description Default
node_ids int or sequence of int

The identifiers, one or many.

required

Returns:

Type Description
ndarray

Each identifier's row in the node arrays.

Source code in src/visualdynamics/core/geometry.py
def node_index(self, node_ids: Ids) -> np.ndarray:
    """Positions of the given node ids in the node arrays.

    Parameters
    ----------
    node_ids : int or sequence of int
        The identifiers, one or many.

    Returns
    -------
    numpy.ndarray
        Each identifier's row in the node arrays.
    """
    order = np.argsort(self.node_id)
    pos = np.searchsorted(self.node_id, node_ids, sorter=order)
    return order[pos]
contains_nodes
contains_nodes(node_ids: Ids) -> ndarray

Boolean array: which of node_ids this geometry defines.

Parameters:

Name Type Description Default
node_ids int or sequence of int

The identifiers, one or many.

required

Returns:

Type Description
ndarray

A boolean per identifier: whether the geometry has it.

Source code in src/visualdynamics/core/geometry.py
def contains_nodes(self, node_ids: Ids) -> np.ndarray:
    """Boolean array: which of `node_ids` this geometry defines.

    Parameters
    ----------
    node_ids : int or sequence of int
        The identifiers, one or many.

    Returns
    -------
    numpy.ndarray
        A boolean per identifier: whether the geometry has it.
    """
    return np.isin(np.asarray(node_ids), self.node_id)
missing_dofs
missing_dofs(dofs: Sequence[str]) -> list[str]

The DOF strings whose node this geometry does not define.

Parameters:

Name Type Description Default
dofs sequence of str

The degrees of freedom to check, such as '101Z+'.

required

Returns:

Type Description
list of str

Those the geometry has no node for — what makes a data object incompatible with it.

Source code in src/visualdynamics/core/geometry.py
def missing_dofs(self, dofs: Sequence[str]) -> list[str]:
    """The DOF strings whose node this geometry does not define.

    Parameters
    ----------
    dofs : sequence of str
        The degrees of freedom to check, such as '101Z+'.

    Returns
    -------
    list of str
        Those the geometry has no node for — what makes a
        data object incompatible with it.
    """
    from .data import parse_dof

    nodes = [parse_dof(dof)[0] for dof in dofs]
    known = self.contains_nodes([-1 if n is None else n for n in nodes])
    return [dof for dof, present in zip(dofs, known) if not present]
suggest_mass_properties
suggest_mass_properties() -> MassProperties

The centroid of the nodes as the reference point, and no mass — unit rigid-body shapes about the middle of the model.

The seed the rigid-body pane opens with and what generate_rigid_body_modes adopts when nothing was set: unlike a whole-record truncation, this is a real answer, and the one the virtual-point transformation wants most often.

Returns:

Type Description
MassProperties

The centroid, unscaled.

Source code in src/visualdynamics/core/geometry.py
def suggest_mass_properties(self) -> MassProperties:
    """The centroid of the nodes as the reference point, and no
    mass — unit rigid-body shapes about the middle of the model.

    The seed the rigid-body pane opens with and what
    `generate_rigid_body_modes` adopts when nothing was set: unlike
    a whole-record truncation, this is a real answer, and the one
    the virtual-point transformation wants most often.

    Returns
    -------
    MassProperties
        The centroid, unscaled.
    """
    from .rigid import MassProperties

    if self.num_nodes == 0:
        raise ValueError('the geometry has no nodes to take the '
                         'centroid of')
    return MassProperties(tuple(self.node_xyz.mean(axis=0)))
define_units
define_units(length_unit: str) -> Geometry

Declare what the coordinates are in, converting them to SI.

Re-declaring reinterprets the original file values rather than scaling twice, so a wrong guess can simply be corrected.

Parameters:

Name Type Description Default
length_unit str

The unit the coordinates are in, such as 'm' or 'in'.

required

Returns:

Type Description
Geometry

Self, converted to SI in place.

Source code in src/visualdynamics/core/geometry.py
def define_units(self, length_unit: str) -> Geometry:
    """Declare what the coordinates are in, converting them to SI.

    Re-declaring reinterprets the original file values rather than
    scaling twice, so a wrong guess can simply be corrected.

    Parameters
    ----------
    length_unit : str
        The unit the coordinates are in, such as 'm' or 'in'.

    Returns
    -------
    Geometry
        Self, converted to SI in place.
    """
    scale, _ = si_transform(length_unit, 'length')
    raw = (self.node_xyz if self.length_unit is None
           else from_si(self.node_xyz, self.length_unit, 'length'))
    self.node_xyz = raw * scale
    origins = self.cs_matrix[:, 3, :]
    raw_origins = (origins if self.length_unit is None
                   else from_si(origins, self.length_unit, 'length'))
    self.cs_matrix[:, 3, :] = raw_origins * scale
    self.length_unit = length_unit
    return self
undefine_units
undefine_units() -> Geometry

Take the declaration back, restoring the file's raw coordinates.

Source code in src/visualdynamics/core/geometry.py
def undefine_units(self) -> Geometry:
    """Take the declaration back, restoring the file's raw coordinates."""
    if self.length_unit is None:
        return self
    self.node_xyz = from_si(self.node_xyz, self.length_unit, 'length')
    self.cs_matrix[:, 3, :] = from_si(self.cs_matrix[:, 3, :],
                                      self.length_unit, 'length')
    self.length_unit = None
    return self
add_node
add_node(
    xyz: ArrayLike,
    node_id: int | None = None,
    color: int = 1,
    def_cs: int | None = None,
    disp_cs: int | None = None,
) -> int

Append a node at xyz (SI). Returns its id.

Parameters:

Name Type Description Default
xyz array_like

The node's coordinates.

required
node_id int

Its identifier. The next free one when omitted.

None
color int

Its display colour index.

1
def_cs int

The coordinate system the position is given in.

None
disp_cs int

The coordinate system displacements are measured in.

None

Returns:

Type Description
int

The node's identifier.

Source code in src/visualdynamics/core/geometry.py
def add_node(self, xyz: ArrayLike, node_id: int | None = None,
             color: int = 1, def_cs: int | None = None,
             disp_cs: int | None = None) -> int:
    """Append a node at `xyz` (SI). Returns its id.

    Parameters
    ----------
    xyz : array_like
        The node's coordinates.
    node_id : int, optional
        Its identifier. The next free one when omitted.
    color : int, default 1
        Its display colour index.
    def_cs : int, optional
        The coordinate system the position is given in.
    disp_cs : int, optional
        The coordinate system displacements are measured in.

    Returns
    -------
    int
        The node's identifier.
    """
    node_id = self._next_id(self.node_id) if node_id is None else int(node_id)
    if node_id in self.node_id:
        raise ValueError(f'node {node_id} already exists')
    default_cs = int(self.cs_id[0]) if len(self.cs_id) else 1
    self.node_id = np.append(self.node_id, node_id)
    self.node_xyz = np.vstack([self.node_xyz,
                               np.asarray(xyz, dtype=np.float64)])
    self.node_color = np.append(self.node_color, int(color))
    self.node_def_cs = np.append(
        self.node_def_cs, default_cs if def_cs is None else int(def_cs))
    self.node_disp_cs = np.append(
        self.node_disp_cs, default_cs if disp_cs is None else int(disp_cs))
    return node_id
add_coordinate_system
add_coordinate_system(
    origin: ArrayLike = (0.0, 0.0, 0.0),
    rotation: ArrayLike | None = None,
    cs_id: int | None = None,
    name: str = "",
    cs_type: int = 0,
) -> int

Append a coordinate system. Returns its id.

Parameters:

Name Type Description Default
origin array_like

The system's origin.

(0, 0, 0)
rotation array_like

A 3x3 rotation matrix. Identity when omitted.

None
cs_id int

Its identifier. The next free one when omitted.

None
name str

What to call it.

''
cs_type int

0 cartesian, 1 cylindrical, 2 spherical.

0

Returns:

Type Description
int

The coordinate system's identifier.

Source code in src/visualdynamics/core/geometry.py
def add_coordinate_system(self, origin: ArrayLike = (0.0, 0.0, 0.0),
                          rotation: ArrayLike | None = None,
                          cs_id: int | None = None, name: str = '',
                          cs_type: int = 0) -> int:
    """Append a coordinate system. Returns its id.

    Parameters
    ----------
    origin : array_like, default (0, 0, 0)
        The system's origin.
    rotation : array_like, optional
        A 3x3 rotation matrix. Identity when omitted.
    cs_id : int, optional
        Its identifier. The next free one when omitted.
    name : str, optional
        What to call it.
    cs_type : int, default 0
        0 cartesian, 1 cylindrical, 2 spherical.

    Returns
    -------
    int
        The coordinate system's identifier.
    """
    cs_id = self._next_id(self.cs_id) if cs_id is None else int(cs_id)
    if cs_id in self.cs_id:
        raise ValueError(f'coordinate system {cs_id} already exists')
    rotation = np.eye(3) if rotation is None else np.asarray(rotation)
    matrix = np.vstack([rotation, np.asarray(origin, dtype=np.float64)])
    self.cs_id = np.append(self.cs_id, cs_id)
    self.cs_type = np.append(self.cs_type, int(cs_type))
    self.cs_name = [*self.cs_name, str(name)]
    self.cs_matrix = np.concatenate([self.cs_matrix, matrix[np.newaxis]])
    return cs_id
add_traceline
add_traceline(
    node_ids: Ids, color: int = 1, description: str = ""
) -> int

Append a traceline through the given nodes. Returns its index.

Parameters:

Name Type Description Default
node_ids int or sequence of int

The nodes the line passes through, in order.

required
color int

Its display colour index.

1
description str

A label for it.

''

Returns:

Type Description
int

The traceline's identifier.

Source code in src/visualdynamics/core/geometry.py
def add_traceline(self, node_ids: Ids, color: int = 1,
                  description: str = '') -> int:
    """Append a traceline through the given nodes. Returns its index.

    Parameters
    ----------
    node_ids : int or sequence of int
        The nodes the line passes through, in order.
    color : int, default 1
        Its display colour index.
    description : str, optional
        A label for it.

    Returns
    -------
    int
        The traceline's identifier.
    """
    nodes = np.asarray([int(n) for n in node_ids], dtype=np.int64)
    unknown = set(nodes.tolist()) - set(self.node_id.tolist())
    if unknown:
        raise ValueError(f'unknown nodes {sorted(unknown)}')
    if len(nodes) < 2:
        raise ValueError('a traceline needs at least two nodes')
    self.traceline_id = np.append(self.traceline_id,
                                  self._next_id(self.traceline_id))
    self.traceline_color = np.append(self.traceline_color, int(color))
    self.traceline_desc.append(str(description))
    self.traceline_conn.append(nodes)
    return len(self.traceline_conn) - 1
block_of
block_of(elem_id: int) -> str

The name of the block an element belongs to, or ''.

Parameters:

Name Type Description Default
elem_id int

Which element.

required

Returns:

Type Description
str

The name of the block it belongs to.

Source code in src/visualdynamics/core/geometry.py
def block_of(self, elem_id: int) -> str:
    """The name of the block an element belongs to, or ''.

    Parameters
    ----------
    elem_id : int
        Which element.

    Returns
    -------
    str
        The name of the block it belongs to.
    """
    row = int(np.flatnonzero(self.elem_id == int(elem_id))[0])
    block = int(self.elem_block[row])
    where = np.flatnonzero(self.block_id == block)
    return self.block_name[int(where[0])] if len(where) else ''
elements_in
elements_in(block: int | str) -> list[int]

The ids of the elements in a block, named or numbered.

Parameters:

Name Type Description Default
block int or str

A block, by identifier or by name.

required

Returns:

Type Description
list of int

The identifiers of the elements it holds.

Source code in src/visualdynamics/core/geometry.py
def elements_in(self, block: int | str) -> list[int]:
    """The ids of the elements in a block, named or numbered.

    Parameters
    ----------
    block : int or str
        A block, by identifier or by name.

    Returns
    -------
    list of int
        The identifiers of the elements it holds.
    """
    if isinstance(block, str):
        where = [i for i, name in enumerate(self.block_name)
                 if name == block]
        if not where:
            return []
        block = int(self.block_id[where[0]])
    return [int(self.elem_id[i])
            for i in np.flatnonzero(self.elem_block == int(block))]
add_block
add_block(
    name: str = "", block_id: int | None = None
) -> int

Declare an element block. Returns its id.

A block with nothing in it is legitimate — exodus files carry empty ones, and a block has to exist before an element can be put in it.

Parameters:

Name Type Description Default
name str

What to call the block.

''
block_id int

Its identifier. The next free one when omitted.

None

Returns:

Type Description
int

The block's identifier.

Source code in src/visualdynamics/core/geometry.py
def add_block(self, name: str = '', block_id: int | None = None) -> int:
    """Declare an element block. Returns its id.

    A block with nothing in it is legitimate — exodus files carry
    empty ones, and a block has to exist before an element can be put
    in it.

    Parameters
    ----------
    name : str, optional
        What to call the block.
    block_id : int, optional
        Its identifier. The next free one when omitted.

    Returns
    -------
    int
        The block's identifier.
    """
    block_id = (self._next_id(self.block_id) if block_id is None
                else int(block_id))
    if block_id in self.block_id:
        raise ValueError(f'block {block_id} already exists')
    self.block_id = np.append(self.block_id, block_id)
    self.block_name.append(str(name))
    return block_id
add_element
add_element(
    node_ids: Ids,
    elem_type: int | None = None,
    color: int = 1,
    block: int | None = None,
) -> int

Append an element. Type defaults to whatever fits the node count: 2 nodes a beam, 3 a triangle, 4 a quadrilateral. Returns its index.

Parameters:

Name Type Description Default
node_ids int or sequence of int

The nodes the element connects, in order.

required
elem_type int

The element type code. Inferred from the node count when omitted.

None
color int

Its display colour index.

1
block int

Which block it belongs to.

None

Returns:

Type Description
int

The element's identifier.

Source code in src/visualdynamics/core/geometry.py
def add_element(self, node_ids: Ids, elem_type: int | None = None,
                color: int = 1, block: int | None = None) -> int:
    """Append an element. Type defaults to whatever fits the node count:
    2 nodes a beam, 3 a triangle, 4 a quadrilateral. Returns its index.

    Parameters
    ----------
    node_ids : int or sequence of int
        The nodes the element connects, in order.
    elem_type : int, optional
        The element type code. Inferred from the node
        count when omitted.
    color : int, default 1
        Its display colour index.
    block : int, optional
        Which block it belongs to.

    Returns
    -------
    int
        The element's identifier.
    """
    nodes = np.asarray([int(n) for n in node_ids], dtype=np.int64)
    unknown = set(nodes.tolist()) - set(self.node_id.tolist())
    if unknown:
        raise ValueError(f'unknown nodes {sorted(unknown)}')
    if elem_type is None:
        elem_type = {2: 21, 3: 41, 4: 44}.get(len(nodes))
        if elem_type is None:
            raise ValueError(
                f'no default element type for {len(nodes)} nodes; '
                'give elem_type')
    if int(elem_type) not in ELEMENT_TYPES:
        raise ValueError(f'unknown element type {elem_type}')
    self.elem_id = np.append(self.elem_id, self._next_id(self.elem_id))
    self.elem_type = np.append(self.elem_type, int(elem_type))
    self.elem_color = np.append(self.elem_color, int(color))
    if block is None:
        block = int(self.block_id[0]) if len(self.block_id) else 1
    block = int(block)
    if block not in self.block_id.tolist():
        self.block_id = np.append(self.block_id, block)
        self.block_name.append('')
    self.elem_block = np.append(self.elem_block, block)
    self.elem_conn.append(nodes)
    return len(self.elem_conn) - 1
renumber_node
renumber_node(row: int, node_id: int) -> None

Give a node a new id, carrying its tracelines and elements over.

Connectivity names nodes by id, so a rename that left it alone would orphan every line and face touching the node.

Parameters:

Name Type Description Default
row int

Which node, by row.

required
node_id int

Its new identifier.

required

Returns:

Type Description
None
Source code in src/visualdynamics/core/geometry.py
def renumber_node(self, row: int, node_id: int) -> None:
    """Give a node a new id, carrying its tracelines and elements over.

    Connectivity names nodes by id, so a rename that left it alone
    would orphan every line and face touching the node.

    Parameters
    ----------
    row : int
        Which node, by row.
    node_id : int
        Its new identifier.

    Returns
    -------
    None
    """
    node_id = int(node_id)
    clash = np.flatnonzero(self.node_id == node_id)
    if len(clash) and clash[0] != row:
        raise ValueError(f'node {node_id} already exists')
    old = int(self.node_id[row])
    self.node_id[row] = node_id
    for conn in (*self.traceline_conn, *self.elem_conn):
        conn[conn == old] = node_id
renumber_block
renumber_block(row: int, block_id: int) -> None

Give a block a new id, carrying its elements over.

An element names its block by id, so a renumber that left them alone would put every element of the block in a block that is no longer there — which validate refuses, after the damage.

Parameters:

Name Type Description Default
row int

Which block, by row.

required
block_id int

Its new identifier.

required

Returns:

Type Description
None
Source code in src/visualdynamics/core/geometry.py
def renumber_block(self, row: int, block_id: int) -> None:
    """Give a block a new id, carrying its elements over.

    An element names its block by id, so a renumber that left them
    alone would put every element of the block in a block that is no
    longer there — which `validate` refuses, after the damage.

    Parameters
    ----------
    row : int
        Which block, by row.
    block_id : int
        Its new identifier.

    Returns
    -------
    None
    """
    block_id = int(block_id)
    clash = np.flatnonzero(self.block_id == block_id)
    if len(clash) and clash[0] != row:
        raise ValueError(f'block {block_id} already exists')
    old = int(self.block_id[row])
    self.block_id[row] = block_id
    self.elem_block[self.elem_block == old] = block_id
renumber_coordinate_system
renumber_coordinate_system(row: int, cs_id: int) -> None

Give a coordinate system a new id, repointing the nodes using it.

Parameters:

Name Type Description Default
row int

Which system, by row.

required
cs_id int

Its new identifier.

required

Returns:

Type Description
None
Source code in src/visualdynamics/core/geometry.py
def renumber_coordinate_system(self, row: int, cs_id: int) -> None:
    """Give a coordinate system a new id, repointing the nodes using it.

    Parameters
    ----------
    row : int
        Which system, by row.
    cs_id : int
        Its new identifier.

    Returns
    -------
    None
    """
    cs_id = int(cs_id)
    clash = np.flatnonzero(self.cs_id == cs_id)
    if len(clash) and clash[0] != row:
        raise ValueError(f'coordinate system {cs_id} already exists')
    old = int(self.cs_id[row])
    self.cs_id[row] = cs_id
    for name in ('node_def_cs', 'node_disp_cs'):
        references = getattr(self, name)
        references[references == old] = cs_id
delete_nodes
delete_nodes(node_ids: Ids) -> dict[str, int]

Remove nodes, and anything that referenced them.

A traceline or element naming a deleted node cannot survive, so it goes too. Returns what was removed, for reporting.

Parameters:

Name Type Description Default
node_ids int or sequence of int

The identifiers, one or many.

required

Returns:

Type Description
dict of str to int

How many of each kind were removed, including the dependants that went with them.

Source code in src/visualdynamics/core/geometry.py
def delete_nodes(self, node_ids: Ids) -> dict[str, int]:
    """Remove nodes, and anything that referenced them.

    A traceline or element naming a deleted node cannot survive, so it
    goes too. Returns what was removed, for reporting.

    Parameters
    ----------
    node_ids : int or sequence of int
        The identifiers, one or many.

    Returns
    -------
    dict of str to int
        How many of each kind were removed, including the
        dependants that went with them.
    """
    wanted = {int(node) for node in node_ids}
    keep = ~np.isin(self.node_id, list(wanted))
    removed_nodes = int((~keep).sum())
    if not removed_nodes:
        return {'nodes': 0, 'tracelines': 0, 'elements': 0}

    orphan_lines = [int(self.traceline_id[i])
                    for i, conn in enumerate(self.traceline_conn)
                    if wanted & {int(n) for n in conn}]
    orphan_elements = [int(self.elem_id[i])
                       for i, conn in enumerate(self.elem_conn)
                       if wanted & {int(n) for n in conn}]
    self.delete_tracelines(orphan_lines)
    self.delete_elements(orphan_elements)

    for name in ('node_id', 'node_def_cs', 'node_disp_cs', 'node_color'):
        setattr(self, name, getattr(self, name)[keep])
    self.node_xyz = self.node_xyz[keep]
    self.validate()
    return {'nodes': removed_nodes, 'tracelines': len(orphan_lines),
            'elements': len(orphan_elements)}
delete_coordinate_systems
delete_coordinate_systems(cs_ids: Ids) -> dict[str, int]

Remove coordinate systems, reassigning any node that used them.

The last coordinate system is never removed — nodes must reference something.

Parameters:

Name Type Description Default
cs_ids int or sequence of int

The identifiers, one or many.

required

Returns:

Type Description
dict of str to int

How many of each kind were removed, including the dependants that went with them.

Source code in src/visualdynamics/core/geometry.py
def delete_coordinate_systems(self, cs_ids: Ids) -> dict[str, int]:
    """Remove coordinate systems, reassigning any node that used them.

    The last coordinate system is never removed — nodes must reference
    something.

    Parameters
    ----------
    cs_ids : int or sequence of int
        The identifiers, one or many.

    Returns
    -------
    dict of str to int
        How many of each kind were removed, including the
        dependants that went with them.
    """
    wanted = {int(cs) for cs in cs_ids}
    keep = ~np.isin(self.cs_id, list(wanted))
    if keep.sum() == 0:
        raise ValueError('a geometry needs at least one coordinate system')
    removed = int((~keep).sum())
    if not removed:
        return {'coordinate_systems': 0, 'nodes_reassigned': 0}

    fallback = int(self.cs_id[keep][0])
    reassigned = 0
    for name in ('node_def_cs', 'node_disp_cs'):
        array = getattr(self, name)
        stale = np.isin(array, list(wanted))
        reassigned += int(stale.sum())
        array[stale] = fallback
    self.cs_id = self.cs_id[keep]
    self.cs_type = self.cs_type[keep]
    self.cs_matrix = self.cs_matrix[keep]
    self.cs_name = [name for name, k in zip(self.cs_name, keep) if k]
    return {'coordinate_systems': removed, 'nodes_reassigned': reassigned}
delete_tracelines
delete_tracelines(traceline_ids: Ids) -> dict[str, int]

Remove tracelines by id. Ids that are not there are ignored.

One id can name several polylines — a UNV trace line that lifts the pen arrives split into its drawn runs, all still that one trace line — and deleting it removes all of them, which is what deleting that trace line means.

Parameters:

Name Type Description Default
traceline_ids int or sequence of int

The identifiers, one or many.

required

Returns:

Type Description
dict of str to int

How many of each kind were removed, including the dependants that went with them.

Source code in src/visualdynamics/core/geometry.py
def delete_tracelines(self, traceline_ids: Ids) -> dict[str, int]:
    """Remove tracelines by id. Ids that are not there are ignored.

    One id can name several polylines — a UNV trace line that lifts
    the pen arrives split into its drawn runs, all still that one
    trace line — and deleting it removes all of them, which is what
    deleting that trace line means.

    Parameters
    ----------
    traceline_ids : int or sequence of int
        The identifiers, one or many.

    Returns
    -------
    dict of str to int
        How many of each kind were removed, including the
        dependants that went with them.
    """
    rows = self._rows_for(traceline_ids, self.traceline_id)
    for row in rows:
        del self.traceline_conn[row]
        del self.traceline_desc[row]
    keep = np.ones(len(self.traceline_id), dtype=bool)
    keep[rows] = False
    self.traceline_id = self.traceline_id[keep]
    self.traceline_color = self.traceline_color[keep]
    return {'tracelines': len(rows)}
delete_blocks
delete_blocks(block_ids: Ids) -> dict[str, int]

Remove blocks, moving anything in them into the first one left.

Deleting the grouping must not delete what was grouped — an element is a piece of the mesh and a block is a label on it — so the elements move rather than go, the way a node whose coordinate system is deleted is reassigned. The last block cannot go while any element names one; with no elements at all there is nothing to hold and the geometry may have none.

Parameters:

Name Type Description Default
block_ids int or sequence of int

The identifiers, one or many.

required

Returns:

Type Description
dict of str to int

How many of each kind were removed, including the dependants that went with them.

Source code in src/visualdynamics/core/geometry.py
def delete_blocks(self, block_ids: Ids) -> dict[str, int]:
    """Remove blocks, moving anything in them into the first one left.

    Deleting the grouping must not delete what was grouped — an
    element is a piece of the mesh and a block is a label on it — so
    the elements move rather than go, the way a node whose coordinate
    system is deleted is reassigned. The last block cannot go while
    any element names one; with no elements at all there is nothing
    to hold and the geometry may have none.

    Parameters
    ----------
    block_ids : int or sequence of int
        The identifiers, one or many.

    Returns
    -------
    dict of str to int
        How many of each kind were removed, including the
        dependants that went with them.
    """
    wanted = {int(block) for block in block_ids}
    keep = ~np.isin(self.block_id, list(wanted))
    removed = int((~keep).sum())
    if not removed:
        return {'blocks': 0, 'elements_reassigned': 0}
    if keep.sum() == 0 and len(self.elem_block):
        raise ValueError(
            'a geometry with elements needs a block to put them in')
    reassigned = 0
    if keep.sum():
        fallback = int(self.block_id[keep][0])
        stale = np.isin(self.elem_block, list(wanted))
        reassigned = int(stale.sum())
        self.elem_block[stale] = fallback
    self.block_name = [name for name, k in zip(self.block_name, keep) if k]
    self.block_id = self.block_id[keep]
    return {'blocks': removed, 'elements_reassigned': reassigned}
delete_elements
delete_elements(elem_ids: Ids) -> dict[str, int]

Remove elements by id. Ids that are not there are ignored.

Parameters:

Name Type Description Default
elem_ids int or sequence of int

The identifiers, one or many.

required

Returns:

Type Description
dict of str to int

How many of each kind were removed, including the dependants that went with them.

Source code in src/visualdynamics/core/geometry.py
def delete_elements(self, elem_ids: Ids) -> dict[str, int]:
    """Remove elements by id. Ids that are not there are ignored.

    Parameters
    ----------
    elem_ids : int or sequence of int
        The identifiers, one or many.

    Returns
    -------
    dict of str to int
        How many of each kind were removed, including the
        dependants that went with them.
    """
    rows = self._rows_for(elem_ids, self.elem_id)
    for row in rows:
        del self.elem_conn[row]
    keep = np.ones(len(self.elem_id), dtype=bool)
    keep[rows] = False
    self.elem_id = self.elem_id[keep]
    self.elem_type = self.elem_type[keep]
    self.elem_color = self.elem_color[keep]
    self.elem_block = self.elem_block[keep]
    # A block whose last element has gone is still a block: exodus
    # files carry empty ones, and forgetting the name would lose on a
    # round trip exactly what blocks were added to keep.
    return {'elements': len(rows)}
save
save(path: str | PathLike) -> None

Write the geometry to a file of its own.

Parameters:

Name Type Description Default
path str or PathLike

Where to write it.

required

Returns:

Type Description
None
Source code in src/visualdynamics/core/geometry.py
def save(self, path: str | os.PathLike) -> None:
    """Write the geometry to a file of its own.

    Parameters
    ----------
    path : str or os.PathLike
        Where to write it.

    Returns
    -------
    None
    """
    from ..io import native
    native.save(self, path)
plot
plot(
    unit_system: UnitSystem | None = None, **kwargs: Any
) -> Any

Draw the geometry: nodes, elements and tracelines.

Parameters:

Name Type Description Default
unit_system UnitSystem

Units to draw in.

None
**kwargs Any

Passed through to the scene.

{}

Returns:

Type Description
object

The plot widget or plotter.

Source code in src/visualdynamics/core/geometry.py
def plot(self, unit_system: UnitSystem | None = None,
         **kwargs: Any) -> Any:
    """Draw the geometry: nodes, elements and tracelines.

    Parameters
    ----------
    unit_system : UnitSystem, optional
        Units to draw in.
    **kwargs
        Passed through to the scene.

    Returns
    -------
    object
        The plot widget or plotter.
    """
    from ..viz.geometry import plot_geometry
    return plot_geometry(self, unit_system=unit_system, **kwargs)
plot_dofs
plot_dofs(
    source: Any,
    quantity: str,
    unit_system: UnitSystem | None = None,
    **kwargs: Any,
) -> Any

This geometry with labelled arrows at every DOF source measures as quantity — the GUI's DOF arrows.

Parameters:

Name Type Description Default
source DataArray

The object whose degrees of freedom are drawn.

required
quantity str

Which quantity's DOFs to show, such as 'acceleration'.

required
unit_system UnitSystem

Units to draw in.

None
**kwargs Any

Passed through to the scene.

{}

Returns:

Type Description
object

The plotter the scene is in.

Source code in src/visualdynamics/core/geometry.py
def plot_dofs(self, source: Any, quantity: str,
              unit_system: UnitSystem | None = None,
              **kwargs: Any) -> Any:
    """This geometry with labelled arrows at every DOF `source`
    measures as `quantity` — the GUI's DOF arrows.

    Parameters
    ----------
    source : DataArray
        The object whose degrees of freedom are drawn.
    quantity : str
        Which quantity's DOFs to show, such as 'acceleration'.
    unit_system : UnitSystem, optional
        Units to draw in.
    **kwargs
        Passed through to the scene.

    Returns
    -------
    object
        The plotter the scene is in.
    """
    from ..viz.geometry import plot_dofs
    return plot_dofs(self, source, quantity, unit_system=unit_system,
                     **kwargs)

Functions: