Skip to content

Dataset generation

Build each structural realization in ordinary Python. You supply one parameter mapping per realization; DynaResp runs the complete simulation and records only the result fields and case values you select.

Define cases and builders

System and load builders receive an ordinary mapping and decide what to construct. A separate case builder can create random values explicitly with the deterministic NumPy generator supplied for each sample.

import numpy as np

import dynaresp as dr

time = np.arange(1000) / 100.0
positions = np.arange(3)


def build_case(rng):
    return {
        "masses": rng.uniform(800.0, 1200.0, size=3),
        "stiffnesses": np.exp(rng.uniform(np.log(5.0e5), np.log(2.0e6), size=3)),
        "damping_ratio": rng.uniform(0.005, 0.03),
        "load_rms": np.exp(rng.uniform(np.log(5.0), np.log(20.0))),
    }


def build_system(case):
    return dr.ShearBuilding(
        masses=np.asarray(case["masses"], dtype=float),
        stiffnesses=np.asarray(case["stiffnesses"], dtype=float),
        damping_ratio=float(case["damping_ratio"]),
    )


def build_load(case):
    return dr.loads.WhiteNoise(
        position=0,
        rms=float(case["load_rms"]),
    )

The same case is passed to build_system and then build_load. This makes conditional and coupled definitions straightforward without hiding how values were chosen.

Generate the data

layout = [dr.sensors.Accelerometer(position=i) for i in range(3)]

dataset = dr.generate_dataset(
    n_samples=10_000,
    case_builder=build_case,
    system_builder=build_system,
    load_builder=build_load,
    sensors=layout,
    time=time,
    positions=positions,
    record=[
        dr.Record.signal,
        dr.Record.modal_parameter.frequencies_hz,
        dr.Record.case_parameter,
        dr.Record.time,
        dr.Record.positions,
        dr.Record.metadata.seed,
        dr.Record.metadata.generation,
    ],
    storage_dtype=np.float32,
    seed=42,
    workers=4,
    progress=True,
)

system_builder must return a StructuralSystem; load_builder must return a Load. Sensors are supplied directly; case generation, stochastic loads, and effects use a reproducible per-sample generator derived from the dataset seed.

Set progress=True to display completed samples with tqdm; it works with both serial and parallel generation.

Select recorded fields

record is required. It accepts typed leaf fields from Record, grouped into response, system_parameter, modal_parameter, load, and metadata families. The family objects are namespaces, not selectors: use Record.system_parameter.mass_matrix, for example, rather than Record.system_parameter. See the schema reference for the full selector tree and shapes.

Record.case_parameter selects every numeric value in the final case mapping. Those values are stored once under parameters/ and exposed through both the parameter and target views. Without that selector, sample.target and the parameter mappings are empty, and parameters is absent from sample.metadata.

The selection happens after run_simulation has produced its normal float64 result. It reduces the retained in-memory or Zarr dataset, but it does not skip solver work or change simulation precision. storage_dtype defaults to np.float64; another NumPy floating dtype such as np.float32 casts only selected floating arrays. Integer case values and the per-sample seed keep exact integer values in int64 and uint64 storage, respectively.

For example, a bridge-monitoring campaign can retain measured signals, modal frequencies, cases, axes, and provenance without writing response histories, load rasters, matrices, or mode shapes:

bridge = dr.generate_dataset(
    cases=bridge_cases,
    system_builder=build_beam,
    load_builder=build_bridge_load,
    sensors=accelerometers,
    time=time_axis,
    positions=reporting_positions,
    record=[
        dr.Record.signal,
        dr.Record.modal_parameter.frequencies_hz,
        dr.Record.case_parameter,
        dr.Record.time,
        dr.Record.positions,
        dr.Record.metadata.seed,
        dr.Record.metadata.generation,
    ],
    storage_dtype=np.float32,
    output="data/bridge.zarr",
    seed=42,
    workers=4,
)

Builders receive the same plain dictionary in pipeline order: system first, then load. A builder can add a numeric derived value, and every later stage sees it. When Record.case_parameter is selected, the final augmented dictionary is stored as ground truth:

def build_system(case):
    case["total_mass"] = float(np.sum(case["masses"]))
    return dr.ShearBuilding(case["masses"], case["stiffnesses"])


def build_load(case):
    # Added by build_system and later available as sample.target["total_mass"].
    return dr.loads.WhiteNoise(position=0, rms=0.01 * case["total_mass"])

DynaResp validates the dictionary's numeric contents when the case enters the pipeline and again before storage, but does not inspect which builder uses each value.

case_builder is called once per sample and must return a numeric mapping. It is mutually exclusive with cases and requires n_samples. Use it for random or correlated studies that should stream without first allocating a case list.

Controlled variability without a distribution

Use cases when you want chosen values rather than samples from a probability distribution. Each mapping describes exactly one realization.

cases = [
    {"mass_scale": 0.90, "stiffness_scale": 0.80, "load_rms": 5.0},
    {"mass_scale": 1.00, "stiffness_scale": 1.00, "load_rms": 10.0},
    {"mass_scale": 1.10, "stiffness_scale": 1.20, "load_rms": 15.0},
]


def controlled_system(case):
    mass_scale = float(case["mass_scale"])
    stiffness_scale = float(case["stiffness_scale"])
    return dr.ShearBuilding(
        masses=np.array([1000.0, 900.0, 800.0]) * mass_scale,
        stiffnesses=np.array([1.0e6, 8.0e5, 6.0e5]) * stiffness_scale,
    )


def controlled_load(case):
    return dr.loads.WhiteNoise(
        position=0,
        rms=float(case["load_rms"]),
    )


dataset = dr.generate_dataset(
    cases=cases,
    system_builder=controlled_system,
    load_builder=controlled_load,
    sensors=layout,
    time=time,
    positions=positions,
    record=[
        dr.Record.signal,
        dr.Record.case_parameter,
    ],
    seed=42,
)

n_samples is inferred from cases. Every case must contain the same names and value shapes so the stored arrays remain rectangular.

For a full factorial design, construct the cases with parameter_grid:

cases = dr.parameter_grid(
    mass_scale=[0.9, 1.0, 1.1],
    damping_ratio=[0.01, 0.02],
    load_rms=[5.0, 10.0],
)

This produces all 12 combinations in stable input order. Use a normal list of mappings when factors are paired, correlated, conditional, or otherwise not a Cartesian product.

Full control with NumPy

Use the supplied NumPy generator for correlations, conditional models, or other random designs. Put every value that should be available to the builders in the returned mapping; select Record.case_parameter when those values should also be retained as ground truth.

covariance = np.array([[1.0, 0.7], [0.7, 1.0]])


def build_correlated_case(rng):
    return {
        "latent": rng.multivariate_normal([0.0, 0.0], covariance),
        "load_rms": 10.0,
    }


def build_correlated_system(case):
    latent = np.asarray(case["latent"], dtype=float)
    masses = 1000.0 + 80.0 * latent
    return dr.ShearBuilding(
        masses=masses,
        stiffnesses=[1.0e6, 8.0e5],
    )

Builders may contain arbitrary validation and branching. All cases in one dataset must still contain the same parameter names and shapes, and every selected array must keep a stable shape because the stored result is rectangular.

In-memory and Zarr output

Without output, records are stacked in memory. For large studies, provide a new Zarr path:

dataset = dr.generate_dataset(
    n_samples=100_000,
    case_builder=build_case,
    system_builder=build_system,
    load_builder=build_load,
    sensors=layout,
    time=time,
    positions=positions,
    record=[
        dr.Record.signal,
        dr.Record.modal_parameter.frequencies_hz,
        dr.Record.case_parameter,
        dr.Record.time,
        dr.Record.positions,
        dr.Record.metadata.seed,
        dr.Record.metadata.generation,
    ],
    storage_dtype=np.float32,
    output="data/building.zarr",
    seed=42,
    workers=8,
)

Samples are committed in index order with a bounded worker buffer. Reopen the lazy dataset without rerunning the builders:

dataset = dr.StructuralDataset.open("data/building.zarr")
sample = dataset[500]

Inspect and export

sample = dataset[0]

sample.signal
sample.target
sample.modal["frequencies_hz"]
sample.time
sample.positions
sample.metadata["seed"]
sample.metadata["generation"]

Unrecorded scalar fields on StructuralSample are None; unrecorded grouped fields are absent from the system, modal, and load dictionaries. Accessing an unrecorded dataset property or requesting it through an adapter raises a clear KeyError.

Machine-learning and analysis views remain available:

torch_dataset = dataset.to_torch(
    inputs="signal",
    targets="damping_ratio",
    extras=["frequencies"],
)
labeled = dataset.to_xarray()
properties = dataset.properties()

Versioned beam-damage benchmark

from dynaresp.datasets import BeamDamage

train = BeamDamage(
    root="data",
    split="train",
    version="1.0",
    generate=True,
)

This predefined dataset uses the same public builder API and fixed seeds. Its beam targets include damaged_element and stiffness_reduction; inspect sample.target for its recorded parameter set.