# Minimal executable example
library(gmsp)
library(data.table)
t_vec <- seq(0, 20, by = 0.01)
dt_acc <- data.table(
t = t_vec,
H1 = 500 * sin(2 * pi * 1 * t_vec) * exp(-0.1 * t_vec),
H2 = 300 * cos(2 * pi * 1.5 * t_vec) * exp(-0.1 * t_vec),
UP = 100 * sin(2 * pi * 0.8 * t_vec) * exp(-0.1 * t_vec)
)
tsl <- AT2TS(dt_acc, units.source = "mm", isRaw = FALSE,
output = "TSL", audit = FALSE)
ps <- TSL2PS(tsl[OCID == "H1"], xi = 0.05,
Tn = c(0.1, 0.2, 0.5, 1.0, 2.0),
output = "PSL")
head(ps)
#> OCID Tn ID S
#> <char> <num> <char> <num>
#> 1: H1 0.0 PSA 487.7138
#> 2: H1 0.1 PSA 492.8057
#> 3: H1 0.2 PSA 571.3886
#> 4: H1 0.5 PSA 1021.4305
#> 5: H1 1.0 PSA 3026.4257
#> 6: H1 2.0 PSA 316.6346TSL2PS() computes elastic single-degree-of-freedom
(SDOF) response spectra for canonical TSL input. Source
time-series ID values map to spectral ID
values: AT -> PSA, VT -> PSV, and
DT -> SD.
The reported spectral IDs are:
PSA — domain-normalised acceleration response from
AT,PSV — domain-normalised velocity response from
VT,SD — domain-normalised displacement response from
DT.These are three independent domain operators. They are not three classical pseudo-ordinates derived from one oscillator driven only by acceleration.
.x: canonical TSL data.table
with t, s, ID, OCID,
plus optional metadata.xi: damping ratio (\(0 \le
\xi \le 1\)), default 0.05.Tn: period vector in seconds. If NULL, a
default grid is used. Do not include 0; the function
prepends the Tn = 0 peak-value anchor internally.Grouping metadata is derived from the TSL schema: all
columns except t, s, ID, and
OCID are metadata keys. OCID remains the
component/channel key.
output = "PSL" returns the canonical long table.
output = "PSW" returns the wide projection:
PSW: wide projection with columns
PSA.<OCID>, PSV.<OCID>,
SD.<OCID>.PSL: canonical long table with columns Tn,
ID ∈ {PSA, PSV, SD}, OCID, S,
plus grouping keys.TSL2PS() is the public spectra interface. It consumes
canonical TSL; it does not expose BY,
COL.s, COL.t, or COL.ID.
PSL2PSW() and PSW2PSL() expose the same
long/wide projection for existing spectra tables:
psw <- PSL2PSW(ps)
psl_again <- PSW2PSL(psw)
head(psl_again)
#> OCID Tn ID S
#> <char> <num> <char> <num>
#> 1: H1 0.0 PSA 487.7138
#> 2: H1 0.1 PSA 492.8057
#> 3: H1 0.2 PSA 571.3886
#> 4: H1 0.5 PSA 1021.4305
#> 5: H1 1.0 PSA 3026.4257
#> 6: H1 2.0 PSA 316.6346When xi is a vector, TSL2PS() runs the
scalar spectra path once per damping ratio and adds xi as
metadata. Scalar xi keeps the historical schema without an
xi column.
ps_xi <- TSL2PS(tsl[OCID == "H1"], xi = c(0.02, 0.05),
Tn = c(0.1, 0.2), output = "PSW")
head(ps_xi)
#> xi Tn PSA.H1 PSV.H1 SD.H1
#> <num> <num> <num> <num> <num>
#> 1: 0.02 0.0 487.7138 56.42533 6.531097
#> 2: 0.02 0.1 493.6305 56.98416 6.596640
#> 3: 0.02 0.2 579.3027 58.97774 6.878137
#> 4: 0.05 0.0 487.7138 56.42533 6.531097
#> 5: 0.05 0.1 492.8057 56.98811 6.597085
#> 6: 0.05 0.2 571.3886 58.91904 6.839390For each period \(T_n\) the code computes
\[\omega_n = \frac{2\pi}{T_n}, \qquad C = 2\,\xi\,\omega_n, \qquad K = \omega_n^2.\]
A 2D linear state-space system is integrated:
\[\dot{\mathbf{y}} = \mathbf{A}\,\mathbf{y} + \mathbf{B}\,u(t), \qquad \mathbf{A} = \begin{bmatrix} 0 & 1 \\ -K & -C \end{bmatrix}, \qquad \mathbf{B} = \begin{bmatrix} 0 \\ 1 \end{bmatrix}.\]
The input \(u(t)\) is the series
value s[k]. The sign convention (e.g., \(u(t) = \pm a_g(t)\)) is not enforced by the
code; since the final outputs use absolute maxima, the sign does not
affect PSA / PSV / SD.
For a time step \(\Delta t\), assuming piecewise-constant input within the step, the exact update is
\[\mathbf{y}_k = e^{\mathbf{A}\Delta t}\,\mathbf{y}_{k-1} + \left(e^{\mathbf{A}\Delta t} - \mathbf{I}\right) \mathbf{A}^{-1}\,\mathbf{B}\,u_k.\]
Implementation:
Ae = expm::expm(A * dt)AeB = (Ae - I) %*% pracma::pinv(A) %*% BThe state is then updated by looping over \(k = 2 \ldots N\).
Let \(d \in \{A,V,D\}\) identify the
acceleration, velocity, or displacement domain and let \(x_d(t)\) be the matching AT,
VT, or DT input. The worker solves an
independent state equation in each domain,
\[ \ddot z_d + 2\xi\omega_n\dot z_d + \omega_n^2 z_d = x_d(t), \]
whose transfer function is
\[ H_d(p) = \frac{1}{p^2 + 2\xi\omega_n p + \omega_n^2}. \]
gmsp uses the domain-normalised operator
\[ G_d(p) = \omega_n^2 H_d(p), \qquad S_d(T_n) = \omega_n^2 \max_t |z_d(t)|. \]
The multiplier is therefore \(\omega_n^2\) in all three branches. If \(x_d\) has units \(U_d\), the first state has units \(U_d\,\mathrm{s}^2\) and \(S_d\) retains \(U_d\). Consequently PSA has acceleration units, PSV has velocity units, and SD has displacement units. Because \(z_A\), \(z_V\), and \(z_D\) are responses to different source histories, identities such as \(PSV = \omega_n SD\) do not apply across these columns.
The function prepends \(T_n = 0\)
with the corresponding unfiltered peak input value: PGA from
AT, PGV from VT, and PGD from
DT.
For canonical TSL input, TSL2PS(D50 = TRUE)
can add the median rotated horizontal component as an ordinary derived
OCID named D50.
TSL2PS(D100 = TRUE) can add the maximum rotated horizontal
component as OCID = "D100". The input must contain
OCID = "H1" and OCID = "H2" for each source
ID (AT, VT, and DT).
The vertical component UP remains an ordinary component and
is not used to compute D50 or D100.
D50 and D100 follow the same spectral-ID contract as the component spectra:
PSA.D50 is computed from rotated AT;PSV.D50 is computed from rotated VT;SD.D50 is computed from rotated DT.PSA.D100, PSV.D100, and
SD.D100 use the same source-ID mapping.D100 is computed independently for each period and spectral ID. The
angle that maximizes PSA can differ from the angle that
maximizes PSV or SD. The \(\omega_n^2\) domain normalisation is
applied to every rotated response before the D50 quantile or D100
maximum is evaluated.
t_d50 <- seq(0, 1, by = 0.01)
at_wide <- data.table(
t = t_d50,
H1 = 10 * sin(2 * pi * 3 * t_d50),
H2 = 7 * cos(2 * pi * 4 * t_d50),
UP = 3 * sin(2 * pi * 2 * t_d50)
)
tsl <- AT2TS(at_wide, units.source = "mm", isRaw = FALSE,
output = "TSL", audit = FALSE)
rot_psl <- TSL2PS(tsl, Tn = c(0.1, 0.2),
output = "PSL", D50 = TRUE, D100 = TRUE, nTheta = 12L)
rot_psl[OCID %chin% c("D50", "D100")]
#> OCID Tn ID S
#> <char> <num> <char> <num>
#> 1: D100 0.0 PSA 12.15984138
#> 2: D100 0.1 PSA 16.32736580
#> 3: D100 0.2 PSA 31.43589259
#> 4: D100 0.0 PSV 0.64248700
#> 5: D100 0.1 PSV 0.71579447
#> 6: D100 0.2 PSV 1.36423235
#> 7: D100 0.0 SD 0.03509437
#> 8: D100 0.1 SD 0.03863945
#> 9: D100 0.2 SD 0.07001303
#> 10: D50 0.0 PSA 11.26617109
#> 11: D50 0.1 PSA 14.12505433
#> 12: D50 0.2 PSA 27.59419196
#> 13: D50 0.0 PSV 0.59498360
#> 14: D50 0.1 PSV 0.66637326
#> 15: D50 0.2 PSV 1.24324408
#> 16: D50 0.0 SD 0.03133454
#> 17: D50 0.1 SD 0.03478889
#> 18: D50 0.2 SD 0.05544259Tn that includes 0, the call
errors because TSL2PS() adds the Tn = 0
peak-value anchor internally.dt.TSL2PS() derives grouping metadata from the
TSL schema. It does not expose BY,
COL.s, COL.t, or COL.ID.