Skip to content

Simulation API

The explicit API puts all inputs for one run in a validated specification:

from dynaresp import SimulationSpec, run_simulation

spec = SimulationSpec(
    system=system,
    load=load,
    sensors=tuple(sensor_layout),
    time=time,
    positions=positions,
)
result = run_simulation(spec, seed=42)

SimulationSpec dataclass

SimulationSpec(system, load, sensors, time, positions)

Validated configuration for one simulation.

Parameters:

Name Type Description Default
system StructuralSystem

Structural strategy providing M, C, K and physical observation/force mappings.

required
load Load

Excitation strategy evaluated on this specification's axes.

required
sensors tuple[Sensor, ...]

Sensors in result-row order. An empty collection is valid.

required
time FloatArray

Strictly increasing, uniformly sampled seconds, shape (n_time,) with at least two samples.

required
positions FloatArray

Strictly increasing reporting and modal-output coordinates, shape (n_position,). Beam coordinates normally use metres; MDOF systems require all integer DOF indices.

required

The dataclass is frozen. Sensor input is converted to a tuple, and both axes are defensively copied and made read-only so a specification can be reused.

run_simulation

run_simulation(spec, *, seed=None, rng=None)

Run an explicit specification without consulting process-wide axes.

Parameters:

Name Type Description Default
spec SimulationSpec

Validated system, load, sensors, and numerical axes.

required
seed int | None

Optional root seed for reproducible stochastic load and sensor effects.

None
rng Generator | None

Optional caller-owned NumPy generator. Its state is consumed. Pass either seed or rng, never both.

None

Returns:

Type Description
SimulationResult

Structural states, clean and affected measurements, exact generalized

SimulationResult

force, reporting rasters, modal properties, and serialized metadata.

Randomness is hierarchical: the root stream creates one independent load stream and one stream per sensor; each sensor creates one stream per effect. Thus a fixed spec and seed reproduce results without touching NumPy's global random state.

Execution order is load generation, exact/distributed force projection, zero-initial-condition response solution, clean sensor sampling, then each sensor's effects in declaration order.

Source code in src/dynaresp/simulation/engine.py
def run_simulation(
    spec: SimulationSpec,
    *,
    seed: int | None = None,
    rng: np.random.Generator | None = None,
) -> SimulationResult:
    """Run an explicit specification without consulting process-wide axes.

    Args:
        spec: Validated system, load, sensors, and numerical axes.
        seed: Optional root seed for reproducible stochastic load and sensor
            effects.
        rng: Optional caller-owned NumPy generator. Its state is consumed. Pass
            either ``seed`` or ``rng``, never both.

    Returns:
        Structural states, clean and affected measurements, exact generalized
        force, reporting rasters, modal properties, and serialized metadata.

    Randomness is hierarchical: the root stream creates one independent load
    stream and one stream per sensor; each sensor creates one stream per effect.
    Thus a fixed ``spec`` and ``seed`` reproduce results without touching
    NumPy's global random state.

    Execution order is load generation, exact/distributed force projection,
    zero-initial-condition response solution, clean sensor sampling, then each
    sensor's effects in declaration order.
    """
    if not isinstance(spec, SimulationSpec):
        raise TypeError("spec must be a SimulationSpec")
    if rng is not None and seed is not None:
        raise ValueError("provide seed or rng, not both")
    if rng is not None and not isinstance(rng, np.random.Generator):
        raise TypeError("rng must be a numpy random Generator")
    resolved_rng = np.random.default_rng(seed) if rng is None else rng
    load_seed = int(resolved_rng.integers(0, np.iinfo(np.uint64).max, dtype=np.uint64))
    history = spec.load.generate(
        spec.time,
        spec.positions,
        seed=load_seed,
    )
    if not isinstance(history, LoadHistory):
        raise TypeError("load.generate must return a LoadHistory")
    if not np.array_equal(history.time, spec.time) or not np.array_equal(
        history.positions,
        spec.positions,
    ):
        raise ValueError("generated load history must preserve the specification axes")
    force = _project_load(spec.system, history)
    displacement, velocity, acceleration = _solve(spec.system, force, spec.time)
    responses = {
        "displacement": displacement,
        "velocity": velocity,
        "acceleration": acceleration,
    }

    clean_rows: list[FloatArray] = []
    measured_rows: list[FloatArray] = []
    sensor_seeds = resolved_rng.integers(
        0,
        np.iinfo(np.uint64).max,
        len(spec.sensors),
        dtype=np.uint64,
    )
    for sensor_item, sensor_seed in zip(spec.sensors, sensor_seeds, strict=True):
        clean = sensor_item.measure_clean(
            spec.system,
            responses[sensor_item.quantity],
        )
        measured = sensor_item.apply_effects(
            clean,
            np.random.default_rng(int(sensor_seed)),
        )
        clean_rows.append(clean)
        measured_rows.append(measured)

    empty = np.empty((0, spec.time.size), dtype=np.float64)
    clean_signal = np.stack(clean_rows) if clean_rows else empty.copy()
    measured_signal = np.stack(measured_rows) if measured_rows else empty.copy()
    mass, damping, stiffness = spec.system.matrices()
    modal = spec.system.modal_analysis(spec.positions)
    return SimulationResult(
        time=spec.time.copy(),
        positions=spec.positions.copy(),
        signal=np.asarray(measured_signal, dtype=np.float64),
        clean_signal=np.asarray(clean_signal, dtype=np.float64),
        displacement=displacement,
        velocity=velocity,
        acceleration=acceleration,
        force=force,
        load_values=history.values.copy(),
        mass_matrix=mass,
        damping_matrix=damping,
        stiffness_matrix=stiffness,
        frequencies=modal.frequencies_hz,
        mode_shapes=modal.mode_shapes,
        system_properties=spec.system.to_config(),
        load_properties=history.description,
        sensor_properties=tuple(item.to_config() for item in spec.sensors),
        point_forces=tuple(history.point_forces),
    )

SimulationResult dataclass

SimulationResult(
    time,
    positions,
    signal,
    clean_signal,
    displacement,
    velocity,
    acceleration,
    force,
    load_values,
    mass_matrix,
    damping_matrix,
    stiffness_matrix,
    frequencies,
    mode_shapes,
    system_properties,
    load_properties,
    sensor_properties,
    point_forces=(),
)

Simulation outputs and the physics needed to interpret them.

Parameters:

Name Type Description Default
time FloatArray

Simulation time in seconds, shape (n_time,).

required
positions FloatArray

Reporting/modal positions, shape (n_position,).

required
signal FloatArray

Sensor measurements after effects, shape (n_sensor, n_time) in sensor declaration order.

required
clean_signal FloatArray

The same sensor rows before effects, shape (n_sensor, n_time).

required
displacement FloatArray

Generalized displacement, shape (ndof, n_time).

required
velocity FloatArray

Generalized velocity, shape (ndof, n_time).

required
acceleration FloatArray

Generalized acceleration, shape (ndof, n_time).

required
force FloatArray

Generalized force actually passed to the solver, shape (ndof, n_time).

required
load_values FloatArray

Complete load reporting raster, shape (n_position, n_time). Point forces are snapped here for display only; use force or point_forces for exact excitation.

required
mass_matrix FloatArray

Generalized mass matrix, shape (ndof, ndof).

required
damping_matrix FloatArray

Generalized damping matrix, shape (ndof, ndof).

required
stiffness_matrix FloatArray

Generalized stiffness matrix, shape (ndof, ndof).

required
frequencies FloatArray

Natural frequencies in hertz, shape (ndof,).

required
mode_shapes FloatArray

Physical mode shapes at positions, shape (ndof, n_position).

required
system_properties dict[str, object]

JSON-compatible structural configuration.

required
load_properties dict[str, object]

JSON-compatible generated-load configuration.

required
sensor_properties tuple[dict[str, object], ...]

Sensor configurations in measurement-row order.

required
point_forces tuple[PointForceComponent, ...]

Exact point-force trajectories. These, rather than their nearest-grid representation in load_values, produce force.

()

With no sensors, signal and clean_signal have shape (0, n_time); the structural response and ground truth are still present.