Skip to content

Systems API

Shared interface

StructuralSystem

Bases: ABC

Strategy interface for a linear time-invariant structural model.

Implementations provide matrices for M u'' + C u' + K u = f and map physical positions to the generalized coordinates u. All matrices use one caller-selected, consistent unit system; DynaResp performs no unit conversion.

ndof abstractmethod property

ndof

Number of generalized coordinates used by the solver.

mass_matrix property

mass_matrix

Return an independent copy of the generalized mass matrix.

damping_matrix property

damping_matrix

Return an independent copy of the generalized damping matrix.

stiffness_matrix property

stiffness_matrix

Return an independent copy of the generalized stiffness matrix.

M property

M

Mass-matrix shorthand.

C property

C

Damping-matrix shorthand.

K property

K

Stiffness-matrix shorthand.

matrices abstractmethod

matrices()

Return independent (M, C, K) arrays of shape (ndof, ndof).

Source code in src/dynaresp/systems/base.py
@abstractmethod
def matrices(self) -> tuple[FloatArray, FloatArray, FloatArray]:
    """Return independent ``(M, C, K)`` arrays of shape ``(ndof, ndof)``."""

modal_analysis abstractmethod

modal_analysis(positions)

Return modes evaluated on the supplied physical or discrete axis.

Source code in src/dynaresp/systems/base.py
@abstractmethod
def modal_analysis(self, positions: ArrayLike) -> ModalProperties:
    """Return modes evaluated on the supplied physical or discrete axis."""

observation_vector abstractmethod

observation_vector(position)

Return h(position) such that physical response is h @ u.

Source code in src/dynaresp/systems/base.py
@abstractmethod
def observation_vector(self, position: float) -> FloatArray:
    """Return ``h(position)`` such that physical response is ``h @ u``."""

force_vector

force_vector(position)

Return the generalized force vector for a point load.

By virtual work this equals :meth:observation_vector for a point on the structure. Continuous systems override it when a moving force may temporarily lie outside their bounded domain.

Source code in src/dynaresp/systems/base.py
def force_vector(self, position: float) -> FloatArray:
    """Return the generalized force vector for a point load.

    By virtual work this equals :meth:`observation_vector` for a point on
    the structure. Continuous systems override it when a moving force may
    temporarily lie outside their bounded domain.
    """
    return self.observation_vector(position)

to_config abstractmethod

to_config()

Return a JSON-compatible physical-system definition.

Source code in src/dynaresp/systems/base.py
@abstractmethod
def to_config(self) -> dict[str, object]:
    """Return a JSON-compatible physical-system definition."""

ModalProperties dataclass

ModalProperties(frequencies_hz, mode_shapes, positions)

Natural frequencies and mode shapes evaluated at requested positions.

Attributes:

Name Type Description
frequencies_hz FloatArray

Vector with shape (mode,).

mode_shapes FloatArray

Physical displacement shapes with shape (mode, position).

positions FloatArray

Coordinates corresponding to the last mode-shape axis.

MDOF systems

MDOFSystem

MDOFSystem(M=None, C=None, K=None, *, labels=None)

Bases: StructuralSystem

Discrete system built from matrices or an attached mass-spring topology.

Parameters:

Name Type Description Default
M ArrayLike | None

Mass matrix with shape (ndof, ndof).

None
C ArrayLike | None

Damping matrix matching M.

None
K ArrayLike | None

Stiffness matrix matching M.

None
labels Sequence[str] | None

Optional non-empty label for every DOF.

None
Notes

Supply all three matrices together, or omit them and call :meth:attach_mass to assemble a topology. Positions for loads and sensors are zero-based integer DOF indices; no spatial interpolation is implied between them.

Source code in src/dynaresp/systems/mdof.py
def __init__(
    self,
    M: ArrayLike | None = None,
    C: ArrayLike | None = None,
    K: ArrayLike | None = None,
    *,
    labels: Sequence[str] | None = None,
) -> None:
    supplied = (M is not None, C is not None, K is not None)
    if any(supplied) and not all(supplied):
        raise ValueError("M, C, and K must be provided together")

    self._attachments: list[dict[str, object]] = []
    if all(supplied):
        assert M is not None and C is not None and K is not None
        mass = _matrix(M, "M")
        damping = _matrix(C, "C")
        stiffness = _matrix(K, "K")
        self._set_matrices(mass, damping, stiffness, labels)
    else:
        if labels is not None:
            raise ValueError("labels require M, C, and K")
        self._M = np.empty((0, 0), dtype=np.float64)
        self._C = np.empty((0, 0), dtype=np.float64)
        self._K = np.empty((0, 0), dtype=np.float64)
        self.labels: tuple[str, ...] = ()

ndof property

ndof

Number of discrete generalized coordinates currently assembled.

attach_mass

attach_mass(
    *, name, mass, to="ground", stiffness, damping=0.0
)

Attach one mass to ground or a previously attached mass.

Parameters:

Name Type Description Default
name str

Unique DOF label.

required
mass float

Positive lumped mass.

required
to str

"ground" or the label of an existing mass.

'ground'
stiffness float

Positive connection stiffness.

required
damping float

Non-negative connection damping.

0.0

Returns:

Type Description
Self

This system, enabling chained calls during configuration.

Source code in src/dynaresp/systems/mdof.py
def attach_mass(
    self,
    *,
    name: str,
    mass: float,
    to: str = "ground",
    stiffness: float,
    damping: float = 0.0,
) -> Self:
    """Attach one mass to ground or a previously attached mass.

    Args:
        name: Unique DOF label.
        mass: Positive lumped mass.
        to: ``"ground"`` or the label of an existing mass.
        stiffness: Positive connection stiffness.
        damping: Non-negative connection damping.

    Returns:
        This system, enabling chained calls during configuration.
    """
    if self.ndof and not self._attachments:
        raise RuntimeError("cannot attach masses to a matrix-defined system")
    if not name or name == "ground" or name in self.labels:
        raise ValueError("name must be unique and cannot be 'ground'")
    if to != "ground" and to not in self.labels:
        raise ValueError("to must be 'ground' or a previously attached mass")
    values = (float(mass), float(stiffness), float(damping))
    if not all(np.isfinite(value) for value in values):
        raise ValueError("mass, stiffness, and damping must be finite")
    if values[0] <= 0.0 or values[1] <= 0.0 or values[2] < 0.0:
        raise ValueError(
            "mass and stiffness must be positive; damping non-negative"
        )

    self._attachments.append(
        {
            "name": name,
            "mass": values[0],
            "to": to,
            "stiffness": values[1],
            "damping": values[2],
        }
    )
    self._assemble_attachments()
    return self

matrices

matrices()

Return independent mass, damping, and stiffness matrix copies.

Source code in src/dynaresp/systems/mdof.py
def matrices(self) -> tuple[FloatArray, FloatArray, FloatArray]:
    """Return independent mass, damping, and stiffness matrix copies."""
    if self.ndof == 0:
        raise RuntimeError("attach at least one mass before using the system")
    return self._M.copy(), self._C.copy(), self._K.copy()

modal_analysis

modal_analysis(positions)

Solve all modes, requiring positions [0, ..., ndof - 1].

Source code in src/dynaresp/systems/mdof.py
def modal_analysis(self, positions: ArrayLike) -> ModalProperties:
    """Solve all modes, requiring positions ``[0, ..., ndof - 1]``."""
    requested = np.asarray(positions, dtype=np.float64)
    expected = np.arange(self.ndof, dtype=np.float64)
    if requested.shape != expected.shape or not np.array_equal(requested, expected):
        raise ValueError("MDOF positions must be all DOF indices")
    eigenvalues, vectors = linalg.eigh(self._K, self._M)
    frequencies = np.sqrt(np.maximum(eigenvalues, 0.0)) / (2.0 * np.pi)
    # Eigenvectors are arbitrary up to sign; fix it for stable datasets.
    for mode in range(vectors.shape[1]):
        pivot = int(np.argmax(np.abs(vectors[:, mode])))
        if vectors[pivot, mode] < 0.0:
            vectors[:, mode] *= -1.0
    return ModalProperties(
        frequencies_hz=np.asarray(frequencies, dtype=np.float64),
        mode_shapes=np.asarray(vectors.T, dtype=np.float64),
        positions=requested.copy(),
    )

observation_vector

observation_vector(position)

Return a one-hot selector for an integer DOF index.

Source code in src/dynaresp/systems/mdof.py
def observation_vector(self, position: float) -> FloatArray:
    """Return a one-hot selector for an integer DOF index."""
    converted = float(position)
    if not converted.is_integer():
        raise ValueError("MDOF positions must be integer DOF indices")
    index = int(converted)
    if index < 0 or index >= self.ndof:
        raise ValueError("MDOF index is outside the system")
    vector = np.zeros(self.ndof, dtype=np.float64)
    vector[index] = 1.0
    return vector

to_config

to_config()

Return matrices, labels, and any attachment topology as plain data.

Source code in src/dynaresp/systems/mdof.py
def to_config(self) -> dict[str, object]:
    """Return matrices, labels, and any attachment topology as plain data."""
    return {
        "type": type(self).__name__,
        "M": self._M.tolist(),
        "C": self._C.tolist(),
        "K": self._K.tolist(),
        "labels": list(self.labels),
        "attachments": deepcopy(self._attachments),
    }

ShearBuilding

ShearBuilding(
    masses, stiffnesses, damping_ratio=0.02, *, labels=None
)

Bases: MDOFSystem

Shear-building chain assembled from story masses and stiffnesses.

Parameters:

Name Type Description Default
masses ArrayLike

Positive story masses ordered from the grounded story upward.

required
stiffnesses ArrayLike

Positive story stiffnesses in the same order. The first connects story one to ground; each later value connects adjacent stories.

required
damping_ratio float

Non-negative modal damping ratio used for every mode.

0.02
labels Sequence[str] | None

Optional label for each story DOF.

None
Source code in src/dynaresp/systems/mdof.py
def __init__(
    self,
    masses: ArrayLike,
    stiffnesses: ArrayLike,
    damping_ratio: float = 0.02,
    *,
    labels: Sequence[str] | None = None,
) -> None:
    self._masses = _vector(masses, "masses")
    self._stiffnesses = _vector(stiffnesses, "stiffnesses")
    if self._stiffnesses.shape != self._masses.shape:
        raise ValueError("masses and stiffnesses must have the same length")
    self._damping_ratio = float(damping_ratio)
    count = self._masses.size
    mass = np.diag(self._masses)
    stiffness = np.zeros((count, count), dtype=np.float64)
    for story in range(count):
        stiffness[story, story] += self._stiffnesses[story]
        if story > 0:
            stiffness[story - 1, story - 1] += self._stiffnesses[story]
            stiffness[story, story - 1] -= self._stiffnesses[story]
            stiffness[story - 1, story] -= self._stiffnesses[story]
    damping = _modal_damping(mass, stiffness, self._damping_ratio)
    super().__init__(M=mass, C=damping, K=stiffness, labels=labels)

masses property

masses

Return a protected copy of the story masses.

stiffnesses property

stiffnesses

Return a protected copy of the story stiffnesses.

damping_ratio property

damping_ratio

Uniform modal damping ratio used during assembly.

to_config

to_config()

Return the story-level definition as JSON-compatible data.

Source code in src/dynaresp/systems/mdof.py
def to_config(self) -> dict[str, object]:
    """Return the story-level definition as JSON-compatible data."""
    return {
        "type": type(self).__name__,
        "masses": self._masses.tolist(),
        "stiffnesses": self._stiffnesses.tolist(),
        "damping_ratio": self._damping_ratio,
        "labels": list(self.labels),
    }

Finite-element beam

Beam

Beam(
    *,
    length,
    young_modulus=None,
    density=None,
    area=None,
    second_moment=None,
    material=None,
    section=None,
    damping_ratio=0.02,
    n_elements=20,
    n_modes=8,
    stiffness_reductions=None,
)

Bases: StructuralSystem

Uniform simply supported Euler-Bernoulli beam in modal coordinates.

Parameters:

Name Type Description Default
length float

Beam span in the caller's length unit.

required
young_modulus float | None

Elastic modulus. May override a material preset.

None
density float | None

Mass density. May override a material preset.

None
area float | None

Cross-sectional area. May override a section preset.

None
second_moment float | None

Cross-sectional second moment of area. May override a section preset.

None
material str | None

Optional "steel", "aluminium"/"aluminum", or "concrete" preset for modulus and density.

None
section tuple[object, ...] | None

Optional ("rectangular", width, height) or ("circular", diameter) preset for geometric properties.

None
damping_ratio float

Modal viscous damping ratio applied to every retained mode.

0.02
n_elements int

Number of equal-length bending elements in the internal finite-element mesh.

20
n_modes int

Number of mass-normalized modes retained by the solver.

8
stiffness_reductions Mapping[int, float] | None

Fractional bending-stiffness loss by zero-based element index; values must satisfy 0 <= reduction < 1.

None
Notes

The internal mesh is independent of the public positions axis used for loads, sensors, and reported mode shapes. Configuration is locked after construction so cached matrices cannot become stale.

Source code in src/dynaresp/systems/beam.py
def __init__(
    self,
    *,
    length: float,
    young_modulus: float | None = None,
    density: float | None = None,
    area: float | None = None,
    second_moment: float | None = None,
    material: str | None = None,
    section: tuple[object, ...] | None = None,
    damping_ratio: float = 0.02,
    n_elements: int = 20,
    n_modes: int = 8,
    stiffness_reductions: Mapping[int, float] | None = None,
) -> None:
    self.length = positive_float("length", length)

    if material is not None:
        try:
            material_e, material_density = _MATERIALS[material.lower()]
        except KeyError as error:
            raise ValueError(
                f"unknown material {material!r}; use explicit properties"
            ) from error
        young_modulus = material_e if young_modulus is None else young_modulus
        density = material_density if density is None else density
    if section is not None:
        section_area, section_moment = _section_properties(section)
        area = section_area if area is None else area
        second_moment = section_moment if second_moment is None else second_moment
    if any(
        value is None for value in (young_modulus, density, area, second_moment)
    ):
        raise ValueError(
            "provide young_modulus, density, area, and second_moment, or "
            "use material and section"
        )
    assert young_modulus is not None
    assert density is not None
    assert area is not None
    assert second_moment is not None
    self.young_modulus = positive_float("young_modulus", young_modulus)
    self.density = positive_float("density", density)
    self.area = positive_float("area", area)
    self.second_moment = positive_float("second_moment", second_moment)
    self.material = material
    self.section = section

    self.damping_ratio = float(damping_ratio)
    if not np.isfinite(self.damping_ratio) or self.damping_ratio < 0.0:
        raise ValueError("damping_ratio must be finite and non-negative")
    if isinstance(n_elements, bool) or not isinstance(n_elements, int):
        raise TypeError("n_elements must be an integer")
    if n_elements < 2:
        raise ValueError("n_elements must be at least 2")
    if isinstance(n_modes, bool) or not isinstance(n_modes, int):
        raise TypeError("n_modes must be an integer")
    if n_modes < 1:
        raise ValueError("n_modes must be positive")
    self.n_elements = n_elements
    self.n_modes = n_modes
    self._stiffness_reductions = self._validate_reductions(stiffness_reductions)

    if self.n_modes > self._free_indices().size:
        raise ValueError("n_modes exceeds the unconstrained beam DOFs")
    self._configuration_locked = True

ndof property

ndof

Number of retained modal coordinates.

element_length property

element_length

Length of each uniform element.

element_edges property

element_edges

Physical coordinates of the element boundaries.

stiffness_reductions property

stiffness_reductions

Fractional stiffness reduction keyed by zero-based element index.

natural_frequencies property

natural_frequencies

Retained natural frequencies in hertz, ordered from low to high.

matrices

matrices()

Return diagonal modal (M, C, K) matrices.

Mass-normalized modes give an identity mass matrix; damping and stiffness are diagonal in the retained modal basis.

Source code in src/dynaresp/systems/beam.py
def matrices(self) -> tuple[FloatArray, FloatArray, FloatArray]:
    """Return diagonal modal ``(M, C, K)`` matrices.

    Mass-normalized modes give an identity mass matrix; damping and
    stiffness are diagonal in the retained modal basis.
    """
    angular = 2.0 * np.pi * self._reduced.frequencies_hz
    mass = np.eye(self.n_modes, dtype=np.float64)
    damping = np.diag(2.0 * self.damping_ratio * angular)
    stiffness = np.diag(angular**2)
    return mass, damping, stiffness

observation_vector

observation_vector(position)

Map a coordinate on [0, length] to retained modal coordinates.

Source code in src/dynaresp/systems/beam.py
def observation_vector(self, position: float) -> FloatArray:
    """Map a coordinate on ``[0, length]`` to retained modal coordinates."""
    full = self._full_interpolation_vector(position)
    free = self._reduced.free_dofs.astype(np.int64)
    return np.asarray(full[free] @ self._reduced.modes, dtype=np.float64)

force_vector

force_vector(position)

Map a point load, returning zero while a trajectory is off-span.

Fixed point loads use the stricter :meth:observation_vector; the simulation engine uses this method only for moving components that may legitimately enter and leave the beam.

Source code in src/dynaresp/systems/beam.py
def force_vector(self, position: float) -> FloatArray:
    """Map a point load, returning zero while a trajectory is off-span.

    Fixed point loads use the stricter :meth:`observation_vector`; the
    simulation engine uses this method only for moving components that may
    legitimately enter and leave the beam.
    """
    converted = float(position)
    if not np.isfinite(converted):
        raise ValueError("position must be finite")
    if converted < 0.0 or converted > self.length:
        return np.zeros(self.ndof, dtype=np.float64)
    return self.observation_vector(converted)

modal_analysis

modal_analysis(positions)

Evaluate retained physical displacement modes at beam coordinates.

Source code in src/dynaresp/systems/beam.py
def modal_analysis(self, positions: ArrayLike) -> ModalProperties:
    """Evaluate retained physical displacement modes at beam coordinates."""
    locations = np.asarray(positions, dtype=np.float64)
    if locations.ndim != 1 or locations.size == 0:
        raise ValueError("positions must be a non-empty vector")
    shapes = np.stack(
        [self.observation_vector(float(position)) for position in locations]
    ).T
    return ModalProperties(
        frequencies_hz=self.natural_frequencies,
        mode_shapes=np.asarray(shapes, dtype=np.float64),
        positions=locations.copy(),
    )

to_config

to_config()

Return the resolved physical and discretization configuration.

Source code in src/dynaresp/systems/beam.py
def to_config(self) -> dict[str, object]:
    """Return the resolved physical and discretization configuration."""
    reductions = [
        {
            "element": element,
            "start": element * self.element_length,
            "end": (element + 1) * self.element_length,
            "reduction": reduction,
        }
        for element, reduction in self._stiffness_reductions.items()
    ]
    return {
        "type": "Beam",
        "length": self.length,
        "young_modulus": self.young_modulus,
        "density": self.density,
        "area": self.area,
        "second_moment": self.second_moment,
        "damping_ratio": self.damping_ratio,
        "n_elements": self.n_elements,
        "n_modes": self.n_modes,
        "stiffness_reductions": reductions,
    }