Elastic SDOF response spectra: TSL2PS

# 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.6346

TSL2PS() 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:

These are three independent domain operators. They are not three classical pseudo-ordinates derived from one oscillator driven only by acceleration.

Input and output

Input

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

output = "PSL" returns the canonical long table. output = "PSW" returns the wide projection:

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.6346

When 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.839390

SDOF model

For 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.

Discretisation via matrix exponential

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:

The state is then updated by looping over \(k = 2 \ldots N\).

How PSA / PSV / SD are built

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.

D50 and D100 horizontal spectra

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:

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.05544259

Notes (limitations and corner cases)