Overview
Input-output data of a damped, softening Duffing oscillator, it also includes the internal state. The system is regionally but not globally stable: the origin is asymptotically stable, but the saddle points (±1, 0) bound its basin of attraction, and inputs that push the state across that boundary make it diverge. The dataset therefore has two parts: converging trajectories that stay bounded over the whole horizon, and diverging trajectories that start at rest and are driven out of the basin.
Use the training sets to fit model parameters, the validation sets for hyperparameter selection, and the test sets only for the final evaluation.
System
q'' = -delta_d q' - q + q^3 + u, delta_d = 0.3, x = (q, q')
For u = 0 the origin is a stable focus (eigenvalues -0.15 ± 0.989i) and (±1, 0) are saddles (eigenvalues 1.27 and -1.57).
The system is sampled at T_s = 0.05 s with a zero-order hold on u: each sample is one call to scipy.integrate.solve_ivp (RK45, rtol = 1e-5, atol = 1e-7) over one sampling period.
Input generation
Each input trajectory is white Gaussian noise at 1/T_s = 20 Hz, filtered with a zero-phase 4th-order Butterworth low-pass (cutoff 2 Hz, scipy.signal.filtfilt, the first 16 samples discarded). It is then multiplied by the envelope exp(-0.9 t), with t running from 0 to 1 over the trajectory, and scaled so that max|u| = a, with a ~ U(0, 3.5) drawn per trajectory.
Initial states and labelling
Trajectories are rejection-sampled. The label is decided on the noise-free state with the divergence threshold 5:
rand_conv_NNN.csv: x_0 ~ U(-0.6, 0.6) per component, kept if |q| ≤ 5 and |q'| ≤ 5 for all 4000 samples (200 s). 100 kept out of 296 draws.
zero_div_NNN.csv: x_0 = (0, 0), kept if a state component exceeds 5 within 4000 samples. The trajectory ends with the last sample before the threshold is crossed, so these files are ragged. 50 kept out of 81 draws.
Noise
White Gaussian noise is added to q and q' (not to u), independently per trajectory and channel, with standard deviation std(x_i) / 10^(30/20), i.e. an SNR of 30 dB. It is added to every stored sample, k = 0 included, so the zero_div files do not start at exactly zero.
Folders and statistics
Folder Content Trajectories Length (min / median / max) Samples
--------------- ---------------- ------------ --------------------------- -------
raw/ all trajectories 150 49 / 4000 / 4000 416,605
train/ converging 60 4000 / 4000 / 4000 240,000
validation/ converging 10 4000 / 4000 / 4000 40,000
test/ converging 30 4000 / 4000 / 4000 120,000
train_div/ diverging 30 49 / 237 / 1162 9,681
validation_div/ diverging 5 96 / 117 / 1086 1,885
test_div/ diverging 15 52 / 181 / 1149 5,039
The split is a random permutation (seed 42), 60/10/30 per group. The split folders contain copies of the files in raw/.
Value ranges over raw/:
Group Column min max std
--------- ------ ------- ------ ------
rand_conv u -1.849 1.963 0.1787
rand_conv q -0.8528 0.9345 0.1335
rand_conv q_dot -0.8069 0.9354 0.1227
zero_div u -3.299 3.462 0.6968
zero_div q -2.93 2.894 0.6131
zero_div q_dot -4.952 4.983 0.6106
File format
One CSV per trajectory, comma separated, header u,q,q_dot, values rounded to 8 decimals. Row k holds the input u_k and the measured state x_k at time t_k = k T_s; x_(k+1) is the state after holding u_k for one sampling period. id/params.json holds the generation parameters in machine-readable form.
In the accompanying code the input is u, the output q, and (q, q_dot) the state used for initial conditions.
Reproduction
code/ contains the generator. With Python 3.10.18, NumPy 1.26.4, SciPy 1.15.3 it reproduces every file in id/ byte for byte:
pip install -r code/requirements.txt
python code/generate_duffing_dataset.py --out-dir regenerated/id
The generator is also part of the genSecSysId repository (https://github.com/Dany-L/genSecSysId), commit aa168f6.
License
CC BY 4.0