diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 707d2477e..46e61d05d 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -114,7 +114,7 @@ boundaries above remain the target modular MSA architecture. | `membership_target` | language, episode, template, department, and opportunity-pool targets cannot collapse into entity or project | | `topic_measurement` | logistic-normal ALR and sequential Egozcue ILR topic coordinates | | `analysis_engine` | bounded cutoff-safe temporal evidence readiness execution and digest-bound terminal artifacts | -| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; first-occasion `T0TIPREDEFFECT` shift is `t0_b z` and Eq. 3 first-summand carry is `e^{A Δt} t0_b z` (`T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_b z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), first-occasion `T0TDPREDEFFECT` shift is `t0_m x0` and Eq. 3 first-summand carry is `e^{A Δt} t0_m x0` (`T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean; §7.2 level-change `CINT` is `κ = −a m x` with `a < 0` so `−κ / a = m x` (`−a m x` is not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`, and not the extra near-zero-drift latent process also named in §7.2; Eq. 3 of that setting is `(1 − e^{a Δt}) m x`, which is not `m x`, not `κ`, and not `TIPREDEFFECT`; §7.2 extra-process contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (`ε = a` is `a_{ηξ} x Δt e^{a Δt}`; identification `TDPREDEFFECT` on the extra process is 1; printed extra `DRIFT` is `−0.000001`; not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`; `ε ≥ 0` fails closed; Eq. 5 of that contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; the extra process has `LAMBDA` 0 and is not an observed indicator; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`; after-t0 extra-process `TDPREDEFFECT` is `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` while `μ_t` uses `Δt`; Eq. 5 of that after-t0 contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)`; the first-occasion extra-process observed mean is not that observed mean when `u ≠ t0`; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive; §7.2 `asymTIPREDEFFECT` is `-B z / a` for `a < 0` (`-B z / a` is not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; §7.2 `addedTIPREDVAR` is `(B / a)² v`, not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`; Table 2 `asymCINT` is `-κ / a` for `a < 0` and is not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`; p. 16 stationary `T0MEANS` is `-κ / a + −B z / a` and is not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean; Eq. 5 of that constrained mean is `τ + λ(−κ / a + −B z / a)`; `τ + λ μ_0` is not that observed mean; `τ + λ(−κ / a)` is not that observed mean when `B z ≠ 0`; `τ + λ μ_t` is not that observed mean; `MANIFESTMEANS` is not `E(y_0)`; the constrained latent mean is not `E(y_0)`; stationary `T0VAR` is `trait + −q / (2 a) + (B / a)² v` (not free `T0VAR`, not `asymDIFFUSION` alone, not `TRAITVAR` alone, not `addedTIPREDVAR` alone, and not the finite-interval discrete latent variance. Eq. 5 of that constrained variance is `λ²(trait + −q / (2 a) + (B / a)² v) + θ + ψ` (JSS PDF re-opened 2026-08-22T03:20Z; form the stationary latent variance first, then `λ² p + θ + ψ`; `λ² p_0` is not that observed variance; `λ²(−q / (2 a)) + θ` is not that observed variance when `TRAITVAR` or `addedTIPREDVAR` is nonzero; `MANIFESTVAR` is not `Var(y_0)`; the constrained latent variance is not `Var(y_0)`); lagged stationary `T0VAR` is `trait + e^{a Δt}(−q / (2 a)) + (B / a)² v` (trait and `addedTIPREDVAR` do not decay; contemporaneous `T0VAR` is not that lagged map; decaying the constrained total as if it were all state is not that lagged map; Eq. 5 of that lagged covariance is `λ²(trait + e^{a Δt}(−q / (2 a)) + (B / a)² v) + ψ`; `Θ` does not enter; contemporaneous `Var(y_0)` is not that lagged observed covariance; the lagged latent covariance is not that observed covariance); later-occasion stationary `T0VAR` is `trait + e^{2 a Δt}(−q / (2 a)) + Q_Δt + (B / a)² v` (trait and `addedTIPREDVAR` do not enter `Q_Δt`; under stationarity that composition equals contemporaneous `T0VAR`; evolving the constrained total as if it were all state is not that later map; the lagged covariance omits `Q_Δt`; `Q_Δt` is not that later map; Eq. 5 of that later-occasion variance is `λ²(trait + e^{2 a Δt}(−q / (2 a)) + Q_Δt + (B / a)² v) + θ + ψ`; lagged observed covariance omits `Q_Δt` and `θ`; `MANIFESTVAR` is not `Var(y_t)`; the later-occasion latent variance is not `Var(y_t)`))), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016) | +| `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; first-occasion `T0TIPREDEFFECT` shift is `t0_b z` and Eq. 3 first-summand carry is `e^{A Δt} t0_b z` (`T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_b z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), first-occasion `T0TDPREDEFFECT` shift is `t0_m x0` and Eq. 3 first-summand carry is `e^{A Δt} t0_m x0` (`T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean; §7.2 level-change `CINT` is `κ = −a m x` with `a < 0` so `−κ / a = m x` (`−a m x` is not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`, and not the extra near-zero-drift latent process also named in §7.2; Eq. 3 of that setting is `(1 − e^{a Δt}) m x`, which is not `m x`, not `κ`, and not `TIPREDEFFECT`; §7.2 extra-process contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (`ε = a` is `a_{ηξ} x Δt e^{a Δt}`; identification `TDPREDEFFECT` on the extra process is 1; printed extra `DRIFT` is `−0.000001`; not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`; `ε ≥ 0` fails closed; Eq. 5 of that contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; the extra process has `LAMBDA` 0 and is not an observed indicator; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`; after-t0 extra-process `TDPREDEFFECT` is `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` while `μ_t` uses `Δt`; Eq. 5 of that after-t0 contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)`; the first-occasion extra-process observed mean is not that observed mean when `u ≠ t0`; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive; §7.2 `asymTIPREDEFFECT` is `-B z / a` for `a < 0` (`-B z / a` is not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; §7.2 `addedTIPREDVAR` is `(B / a)² v`, not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`; Table 2 `asymCINT` is `-κ / a` for `a < 0` and is not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`; p. 16 stationary `T0MEANS` is `-κ / a + −B z / a` and is not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean; Eq. 5 of that constrained mean is `τ + λ(−κ / a + −B z / a)`; `τ + λ μ_0` is not that observed mean; `τ + λ(−κ / a)` is not that observed mean when `B z ≠ 0`; `τ + λ μ_t` is not that observed mean; `MANIFESTMEANS` is not `E(y_0)`; the constrained latent mean is not `E(y_0)`; stationary `T0VAR` is `trait + −q / (2 a) + (B / a)² v` (not free `T0VAR`, not `asymDIFFUSION` alone, not `TRAITVAR` alone, not `addedTIPREDVAR` alone, and not the finite-interval discrete latent variance. Eq. 5 of that constrained variance is `λ²(trait + −q / (2 a) + (B / a)² v) + θ + ψ` (JSS PDF re-opened 2026-08-22T03:20Z; form the stationary latent variance first, then `λ² p + θ + ψ`; `λ² p_0` is not that observed variance; `λ²(−q / (2 a)) + θ` is not that observed variance when `TRAITVAR` or `addedTIPREDVAR` is nonzero; `MANIFESTVAR` is not `Var(y_0)`; the constrained latent variance is not `Var(y_0)`); lagged stationary `T0VAR` is `trait + e^{a Δt}(−q / (2 a)) + (B / a)² v` (trait and `addedTIPREDVAR` do not decay; contemporaneous `T0VAR` is not that lagged map; decaying the constrained total as if it were all state is not that lagged map; Eq. 5 of that lagged covariance is `λ²(trait + e^{a Δt}(−q / (2 a)) + (B / a)² v) + ψ`; `Θ` does not enter; contemporaneous `Var(y_0)` is not that lagged observed covariance; the lagged latent covariance is not that observed covariance); later-occasion stationary `T0VAR` is `trait + e^{2 a Δt}(−q / (2 a)) + Q_Δt + (B / a)² v` (trait and `addedTIPREDVAR` do not enter `Q_Δt`; under stationarity that composition equals contemporaneous `T0VAR`; evolving the constrained total as if it were all state is not that later map; the lagged covariance omits `Q_Δt`; `Q_Δt` is not that later map; Eq. 5 of that later-occasion variance is `λ²(trait + e^{2 a Δt}(−q / (2 a)) + Q_Δt + (B / a)² v) + θ + ψ`; lagged observed covariance omits `Q_Δt` and `θ`; `MANIFESTVAR` is not `Var(y_t)`; the later-occasion latent variance is not `Var(y_t)`); predetermined later-occasion `T0VAR` is `trait + e^{2 a Δt} p_0 + Q_Δt + (B / a)² v` (free `T0VAR` `p_0` is not that later map; setting `p_0 = −q / (2 a)` recovers the stationary later-occasion map; stationary later variance uses `−q / (2 a)` in place of `p_0` and is not that later map when `p_0` is free; evolving `trait + p_0 + (B / a)² v` as if it were all state is not that later map; Eq. 5 of that predetermined later-occasion variance is `λ²(trait + e^{2 a Δt} p_0 + Q_Δt + (B / a)² v) + θ + ψ`; `MANIFESTVAR` is not `Var(y_t)`; the predetermined later-occasion latent variance is not `Var(y_t)`; stationary later observed variance is not that observed variance when `p_0` is free); predetermined lagged `T0VAR` is `trait + e^{a Δt} p_0 + (B / a)² v` (free `T0VAR` `p_0` is not that lagged map; setting `p_0 = −q / (2 a)` recovers the stationary lagged map; stationary lagged covariance uses `−q / (2 a)` in place of `p_0` and is not that lagged map when `p_0` is free; evolving `trait + p_0 + (B / a)² v` as if it were all state is not that lagged map; later-occasion variance includes `Q_Δt` and is not that lagged map; Eq. 5 of that predetermined lagged covariance is `λ²(trait + e^{a Δt} p_0 + (B / a)² v) + ψ`; `MANIFESTVAR` does not enter; the predetermined lagged latent covariance is not that observed covariance; predetermined later observed variance includes `Q_Δt` and `θ` and is not that lagged observed covariance; stationary lagged observed covariance is not that observed covariance when `p_0` is free; the predetermined first-occasion variance of §4.3 predetermined `T0VAR` is `trait + p_0 + (B / a)² v`; free `p_0` is not that map; stationary first-occasion variance uses `−q / (2 a)` in place of `p_0` and is not that map when `p_0` is free; lagged covariance decays the state and is not that map; later-occasion variance includes `Q_Δt` and is not that map; Eq. 5 of that predetermined first-occasion variance is `λ²(trait + p_0 + (B / a)² v) + θ + ψ`; `MANIFESTVAR` is not that first-occasion observed variance; the predetermined first-occasion latent variance is not that observed variance; stationary first-occasion observed variance is not that observed variance when `p_0` is free; predetermined later observed variance includes `Q_Δt` and is not that first-occasion observed variance; later-start lagged covariance of predetermined `T0VAR` is `trait + e^{a s}(e^{2 a u} p_0 + Q_u) + (B / a)² v` (Driver et al., 2017, §4.3 `startoffset`; Eq. 4; JSS PDF re-opened 2026-08-23T10:27Z; first-occasion lagged omits `e^{a s} Q_u`; later-occasion variance does not lag; stationary lagged uses `−q / (2 a)`; decaying the later total is not that map; Eq. 5 of that later-start lagged covariance is `λ²` of it plus `ψ`; `Θ` does not enter; first-occasion lagged observed omits `e^{a s} Q_u`; later observed variance includes `Q_u` and `θ`; later-start later-occasion variance of predetermined `T0VAR` is `trait + e^{2 a s}(e^{2 a u} p_0 + Q_u) + Q_s + (B / a)² v` (Driver et al., 2017, §4.3 `startoffset`; Eq. 3–4 Chapman–Kolmogorov `Q_{u+s} = e^{2 a s} Q_u + Q_s`; JSS PDF re-opened 2026-08-23T11:05Z; later-occasion variance at `u` omits `Q_s`; later-start lagged covariance omits `Q_s`; stationary later uses `−q / (2 a)`; evolving the later total as if it were all state is not that map; ignoring `startoffset` omits `e^{2 a s} Q_u`; Eq. 5 of that later-start later-occasion variance is `λ²` of it plus `θ + ψ`; `MANIFESTVAR` is not that observed variance; p. 16 `discreteDRIFTstd` is `e^{a Δt}` after strictly positive `asymDIFFUSION` `-q / (2 a)` (footnote 4; unstandardised `e^{a Δt}` is defined for growing `a ≥ 0` and for zero diffusion and is not `discreteDRIFTstd`; the §7.1 trait-plus-state autocorrelation uses `TRAITVAR` and is not `discreteDRIFTstd`; `TRAITVAR` is not the standardisation variance; p. 16 `discreteDIFFUSIONstd` is `Q_Δt / (−q / (2 a))` after strictly positive `asymDIFFUSION` `-q / (2 a)` (footnote 4; unstandardised `Q_Δt` is defined for growing `a ≥ 0` and for zero diffusion and is not `discreteDIFFUSIONstd`; the continuous standardisation `−2 a` is not `discreteDIFFUSIONstd`; `Q_Δt / (trait + p + added)` uses `TRAITVAR` and is not `discreteDIFFUSIONstd`; `TRAITVAR` is not the standardisation variance; p. 16 `DIFFUSIONstd` is `q / (−q / (2 a)) = −2 a` after strictly positive `asymDIFFUSION` `-q / (2 a)` (Driver et al., 2017, p. 16; Eq. 4; footnote 4; JSS PDF re-opened 2026-08-23T13:20Z; unstandardised `q` is defined for growing `a ≥ 0` and for zero diffusion and is not `DIFFUSIONstd`; the discrete standardisation `Q_Δt / (−q / (2 a))` depends on `Δt` and is not `DIFFUSIONstd`; `q / (trait + p + added)` uses `TRAITVAR` and is not `DIFFUSIONstd`; `TRAITVAR` is not the standardisation variance; p. 16 `DRIFTstd` is the continuous auto-effect after strictly positive `asymDIFFUSION` `-q / (2 a)` (Driver et al., 2017, p. 16; Eq. 1; footnote 4; JSS PDF re-opened 2026-08-23T13:28Z); unstandardised `a` is defined for growing `a ≥ 0` and for zero diffusion and is not `DRIFTstd`; the discrete standardisation `e^{a Δt}` depends on the event interval and is not `DRIFTstd`; `a p / (trait + p + added)` uses `TRAITVAR` and is not `DRIFTstd`; `TRAITVAR` is not the standardisation variance); p. 16 `asymTIPREDEFFECTstd` is `(-B / a) · √v / √(-q / (2 a))` after strictly positive `asymDIFFUSION` `-q / (2 a)` and strictly positive predictor variance `v` (Driver et al., 2017, p. 16; §7.2; footnote 4; JSS PDF re-opened 2026-08-23T14:25Z; unstandardised `-B / a` is defined for a zero coefficient and for zero predictor variance and is not `asymTIPREDEFFECTstd`; the finite-interval standardisation `A^{-1}[e^{A Δt} − I] B · √v / √p` depends on the event interval and is not `asymTIPREDEFFECTstd`; `(-B / a) · √v / √(trait + p + added)` uses `TRAITVAR` and is not `asymTIPREDEFFECTstd`; `TRAITVAR` is not the standardisation variance); p. 16 `TIPREDEFFECTstd` is `B · √v / √(-q / (2 a))` after strictly positive `asymDIFFUSION` `-q / (2 a)` and strictly positive predictor variance `v` (Driver et al., 2017, p. 16; §7.2; footnote 4; JSS PDF re-opened 2026-08-23T16:21Z; unstandardised `B` is defined for a zero coefficient and for zero predictor variance and is not `TIPREDEFFECTstd`; the asymptotic standardisation `(-B / a) · √v / √p` is the total change and is not `TIPREDEFFECTstd`; the finite-interval standardisation `A^{-1}[e^{A Δt} − I] B · √v / √p` depends on the event interval and is not `TIPREDEFFECTstd`; `B · √v / √(trait + p + added)` uses `TRAITVAR` and is not `TIPREDEFFECTstd`; `TRAITVAR` is not the standardisation variance); Table 3 `T0TIPREDEFFECTstd` is `t0_b · √v / √p_0` after strictly positive free `T0VAR` `p_0` and strictly positive predictor variance `v` (Driver et al., 2017, Table 3, p. 13; p. 16; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T17:20Z; the affected variance is free `T0VAR`, not `asymDIFFUSION`; unstandardised `t0_b` is defined for a zero coefficient and for zero predictor variance and is not `T0TIPREDEFFECTstd`; `TIPREDEFFECTstd` `B · √v / √(-q / (2 a))` is the continuous coefficient and is not `T0TIPREDEFFECTstd`; `asymTIPREDEFFECTstd` `(-B / a) · √v / √p` is the total change and is not `T0TIPREDEFFECTstd`; `t0_b · √v / √(trait + p_0 + added)` uses `TRAITVAR` and is not `T0TIPREDEFFECTstd`; `TRAITVAR` is not the standardisation variance); 2017-era `addedT0TIPREDVAR` is `t0_b² v` (Driver et al., 2017, Table 3, p. 13; p. 16; §7.2; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T18:20Z; `T0TIPREDEFFECT %*% TIPREDVAR %*% t(T0TIPREDEFFECT)` immediately after `T0TIPREDEFFECTstd`; form `t0_b` first, then square, then multiply by `v`; a zero coefficient or zero predictor variance is exactly zero; free `T0TIPREDEFFECT` does not require `a < 0`; `(B / a)² v` is `addedTIPREDVAR` and is not this first-occasion map; `t0_b · √v / √p_0` is `T0TIPREDEFFECTstd` and is not this variance; free `T0VAR` is not this extra TI variance; `TRAITVAR` is not this extra TI variance; Equation 5 of 2017-era `addedT0TIPREDVAR` is `λ² t0_b² v` (Driver et al., 2017, Eq. 5, p. 5; Table 3, p. 13; Table 2, p. 12; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T19:10Z; form `t0_b² v` first, then `(λ extra) λ` with `θ = 0`; a zero loading or zero extra is exactly zero; `t0_b² v` is the latent extra, not the observed extra; `λ² p_0 + θ` is first-occasion observed variance, not this extra; `λ² (B / a)² v` is Eq. 5 of `addedTIPREDVAR`, not this first-occasion observed extra; `MANIFESTVAR` `θ` is not this extra; Equation 5 of §7.2 `addedTIPREDVAR` is `λ² (B / a)² v`; form `(B / a)² v` first, then `(λ extra) λ` with `θ = 0`; a zero loading or zero extra is exactly zero; lasting asymptotic extra requires `a < 0`; `(B / a)² v` is the latent extra, not the observed extra; `λ² t0_b² v` is first-occasion extra observed TI variance, not this extra; `λ² p + θ` is stationary observed variance, not this extra; `MANIFESTVAR` `θ` is not this extra; p. 16 `TDPREDEFFECTstd` is `m · √v / √(-q / (2 a))` after strictly positive `asymDIFFUSION` and strictly positive time-dependent predictor variance; unstandardised `M` is not `TDPREDEFFECTstd`; `TIPREDEFFECTstd` is not `TDPREDEFFECTstd` even when `M = B`; intercept-style `A^{-1}[e^{A Δt} − I] M · √v / √p` is not `TDPREDEFFECTstd`; `m · √v / √(trait + p + added)` uses `TRAITVAR` and is not `TDPREDEFFECTstd`; Table 3 / p. 16 `T0TDPREDEFFECTstd` is `t0_m · √v / √p_0` after strictly positive free `T0VAR` and strictly positive TD predictor variance; unstandardised `t0_m` is not `T0TDPREDEFFECTstd`; `TDPREDEFFECTstd` uses `asymDIFFUSION` and is not `T0TDPREDEFFECTstd`; `T0TIPREDEFFECTstd` is not `T0TDPREDEFFECTstd` even when `t0_m = t0_b`; `t0_m · √v / √(trait + p_0 + added)` uses `TRAITVAR` and is not `T0TDPREDEFFECTstd`; free `T0VAR` does not require `a < 0`; p. 16 `T0VARstd` is `p_0 / p_0 = 1` after strictly positive free `T0VAR` (`solve(sqrt(diag(T0VAR))) %&% T0VAR`; OpenMx `%&%` is `t(A) %*% B %*% A`; default ridge is 0); unstandardised `T0VAR` is not `T0VARstd`; `T0TDPREDEFFECTstd` is not `T0VARstd`; `addedT0TIPREDVAR` is not `T0VARstd`; p. 16 `TRAITVARstd` is `trait / trait = 1` after strictly positive `TRAITVAR` (`solve(sqrt(diag(TRAITVAR))) %&% TRAITVAR`; OpenMx `%&%` is `t(A) %*% B %*% A`; no ridge addend); unstandardised `TRAITVAR` is not `TRAITVARstd`; `T0VARstd` is not `TRAITVARstd` even when both equal 1; `addedT0TIPREDVAR` is not `TRAITVARstd`; p. 16 `MANIFESTTRAITVARstd` is `ψ / ψ = 1` after strictly positive `MANIFESTTRAITVAR` (`solve(sqrt(diag(MANIFESTTRAITVAR))) %&% MANIFESTTRAITVAR`; OpenMx `%&%` is `t(A) %*% B %*% A`; 2017-era source adds ridging; default ridge is 0); unstandardised `MANIFESTTRAITVAR` is not `MANIFESTTRAITVARstd`; `TRAITVARstd` is not `MANIFESTTRAITVARstd` even when both equal 1; `MANIFESTVAR` is not `MANIFESTTRAITVARstd`; p. 16 `MANIFESTVARstd` is `θ / θ = 1` after strictly positive `MANIFESTVAR` (`solve(sqrt(diag(MANIFESTVAR))) %&% MANIFESTVAR`; OpenMx `%&%` is `t(A) %*% B %*% A`; 2017-era source adds ridging; default ridge is 0; 2017-era `dimnames` assignment to `latentNames` is a source bug); unstandardised `MANIFESTVAR` is not `MANIFESTVARstd`; `MANIFESTTRAITVARstd` is not `MANIFESTVARstd` even when both equal 1; Equation 5 `Var(y)` is not `MANIFESTVARstd`; p. 16 `TIPREDVARstd` is `v / v = 1` after strictly positive `TIPREDVAR` (`solve(sqrt(diag(TIPREDVAR))) %&% TIPREDVAR`; OpenMx `%&%` is `t(A) %*% B %*% A`; 2017-era source adds ridging; default ridge is 0; `dimnames` are `TIpredNames`); unstandardised `TIPREDVAR` is not `TIPREDVARstd`; `MANIFESTVARstd` is not `TIPREDVARstd` even when both equal 1; §7.2 `addedTIPREDVAR` is not `TIPREDVARstd`; p. 16 `asymDIFFUSIONstd` is `p / p = 1` after strictly positive `asymDIFFUSION` (`solve(sqrt(diag(asymDIFFUSION))) %&% asymDIFFUSION`; OpenMx `%&%` is `t(A) %*% B %*% A`; 2017-era source adds ridging; default ridge is 0; `dimnames` are `latentNames`); unstandardised `asymDIFFUSION` is not `asymDIFFUSIONstd`; `TIPREDVARstd` is not `asymDIFFUSIONstd` even when both equal 1; `DIFFUSIONstd` `−2 a` is not `asymDIFFUSIONstd`; p. 16 `discreteCINTstd` is `A^{-1}[e^{A Δt} − I] κ / √p` after strictly positive `asymDIFFUSION`; unstandardised `discreteCINT` is not `discreteCINTstd`; `κ / √p` is not `discreteCINTstd`; `(-κ / a) / √p` is not `discreteCINTstd`; `asymCINTstd` is `(-κ / a) / √p` after strictly positive `asymDIFFUSION`; unstandardised `asymCINT` is not `asymCINTstd`; `κ / √p` is not `asymCINTstd`; `discreteCINTstd` is not `asymCINTstd`; `T0MEANSstd` is `μ_0 / √p_0` after strictly positive free `T0VAR`; unstandardised `T0MEANS` is not `T0MEANSstd`; `T0VARstd` is not `T0MEANSstd`; `μ_0 / √asymDIFFUSION` is not `T0MEANSstd`;))))), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016) | Foundation crates expose only tested contracts. Empty façades are not public diff --git a/CHANGELOG.md b/CHANGELOG.md index 9117220dd..b828de6de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,40 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang - Restored protected-main gate integrity after the consolidation merges: hourly-scheduler prompt-contract tests now assert the gap-baseline-derived task contract (Gap ID naming, no invented weights) instead of stale increment-specific tokens, the operator-gap register inventory matches the live 33-PR queue, `evidence_core::image_unit` non-image/empty-subtype refusals and `load_union_branch_totals` valid-record accumulation have exact coverage, and the README crate fence plus duplicate registry entries stay deduped. - Branch-coverage diagnostics on the post-consolidation head exposed two uncovered outcomes in `evidence_core::image_unit` (`is_image_media_type_token` non-image prefix and empty-subtype refusals), one uncovered authored line (the strip-prefix refusal), and lost valid-record coverage for `load_union_branch_totals`; exact red-to-green cases now cover the non-image/empty-subtype data URIs and per-coordinate True/False accumulation. - Repaired post-consolidation merge fallout that left protected `main` red: restored the lost `return True` in the `check_coverage.py` match-guard branch, removed the shadowed duplicate `load_union_branch_totals` and `_is_multiline_match_guard` definitions plus duplicate workspace-crate entries (`episode_membership`, `analysis_engine`) from the contract tuple and Cargo member arrays, split two union-fused four-tuples back into `(variant, message)` pairs in the `event_core` error table, repaired the fused `identity_recovery_rate` body in `episode_membership::window`, deduplicated the checked-arithmetic eligible-count block in `analysis_engine`, fixed four-argument `unit()` test call sites, rebalanced the README crate-list fence around all 54 unique crates, and deduplicated the `location_membership`/`validation_core`/`tepp_api` architecture-table rows. Also documents private `PLAUSIBLE_IMAGE_MEDIA_TYPES` so `cargo doc -D warnings` passes. +- `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, p. 16 `T0MEANSstd`; Table 2, p. 12; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-24T22:30Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar standardised initial latent mean. Page 16 prints standardised matrices with the suffix `std` when appropriate. Footnote 4 standardises using only the relevant variance, not the total. Table 2 names `T0MEANS` the latent process means at the first time point `T0`. The first-occasion relevant variance is free `T0VAR` `p_0`, not process-dynamics `asymDIFFUSION` `-q / (2 a)`, matching Table 3 `T0TIPREDEFFECTstd`. The 2017-era `summary.ctsemFit.R` forms unstandardised `T0MEANS` as `OpenMx::mxEval(T0MEANS, mxobj, compute=TRUE)`. That source does not form a `T0MEANSstd` matrix; the scalar map here is the footnote 4 standardisation of that named first-occasion mean: `μ_0 / √p_0`. Form strictly positive `p_0` first, then divide `μ_0` by `√p_0`. A zero mean is exactly zero. Unstandardised `T0MEANS` is defined for a zero first-occasion variance; standardised `T0MEANS` is not. Zero `p_0` has no positive SD and fails closed. `T0` is an event-time occasion, so a non-event clock fails closed. Free `T0MEANS` does not require stable `a < 0`. `T0VARstd` `p_0 / p_0 = 1` recovers the same number when `μ_0 = √p_0` and remains a distinct named quantity. `μ_0 / √asymDIFFUSION` uses process-dynamics variance and is not this first-occasion map. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) remains unread (Unpaywall 2026-08-24T08:02Z: `is_oa: false`, 0 locations; title *Measurement Invariance, Factor Analysis and Factorial Invariance*). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread (Unpaywall 2026-08-24T08:02Z: `is_oa: false`, 0 locations; title *Randomization-Based Inference about Latent Variables from Complex Samples*). +- `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, p. 16 `asymCINTstd`; Eq. 3, p. 4; footnote 4; Table 2, p. 12; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-24T09:05Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar standardised asymptotic continuous intercept. Page 16 prints standardised matrices with the suffix `std` when appropriate. Footnote 4 standardises using only the relevant variance, not the total. `CINT` is the process intercept of individual, or average individual, dynamics, so that relevant variance is within-subject `asymDIFFUSION` `p = −q / (2 a)`. The 2017-era `summary.ctsemFit.R` forms `asymCINT` whenever `verbose = TRUE`, as `-solve(DRIFT) %*% CINT`. That source does not form an `asymCINTstd` matrix; the scalar map here is the footnote 4 standardisation of that named asymptotic intercept: `(-κ / a) / √p`. Form strictly positive `p` first, then the asymptotic intercept, then divide by `√p`. A zero intercept is exactly zero. Unstandardised `asymCINT` is defined for a zero process; standardised `asymCINT` is not. Zero `q` has no positive process SD and fails closed. Lasting `asymDIFFUSION` requires stable `a < 0`. A non-event clock fails closed. `κ / √p` is the continuous intercept standardisation and is not this total-change map. `A^{-1}[e^{A Δt} − I] κ / √p` depends on the event interval and is not this `Δt → ∞` map. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, not `CINTstd`, not `discreteCINTstd`, and not ctsem estimation. Meredith (1993) remains unread (Unpaywall 2026-08-24T08:02Z: `is_oa: false`, 0 locations; title *Measurement Invariance, Factor Analysis and Factorial Invariance*). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread (Unpaywall 2026-08-24T08:02Z: `is_oa: false`, 0 locations; title *Randomization-Based Inference about Latent Variables from Complex Samples*). +- `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, p. 16 `discreteCINTstd`; Eq. 3, p. 4; footnote 4; Table 2, p. 12; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-24T05:20Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar standardised discrete continuous intercept. Page 16 prints discrete-time transformations for a chosen event interval and, when appropriate, standardised matrices with the suffix `std`. Footnote 4 standardises using only the relevant variance, not the total. `CINT` is the process intercept of individual, or average individual, dynamics, so that relevant variance is within-subject `asymDIFFUSION` `p = −q / (2 a)`. The 2017-era `summary.ctsemFit.R` forms `discreteCINT` whenever `verbose = TRUE`, as `solve(DRIFT) %*% (discreteDRIFT − I) %*% CINT`. That source does not form a `discreteCINTstd` matrix; the scalar map here is the footnote 4 standardisation of that named discrete intercept: `A^{-1}[e^{A Δt} − I] κ / √p`. Form strictly positive `p` first, then the discrete intercept, then divide by `√p`. A zero intercept is exactly zero. Unstandardised `discreteCINT` is defined for growing `a ≥ 0` and for zero diffusion; standardised `discreteCINT` is not. Zero `q` has no positive process SD and fails closed. Lasting `asymDIFFUSION` requires stable `a < 0`. A non-event clock fails closed. A non-positive event interval fails closed. `κ / √p` does not depend on `Δt` and is not this finite-interval map. `(-κ / a) / √p` is the standardised asymptotic intercept and is not this map. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, not `CINTstd`, and not ctsem estimation. Meredith (1993) remains unread (Unpaywall 2026-08-24T08:02Z: `is_oa: false`, 0 locations; title *Measurement Invariance, Factor Analysis and Factorial Invariance*). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread (Unpaywall 2026-08-24T08:02Z: `is_oa: false`, 0 locations; title *Randomization-Based Inference about Latent Variables from Complex Samples*). +- `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, p. 16 `asymDIFFUSIONstd`; footnote 4; Eq. 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T23:02Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar standardised asymptotic within-subject variance. Page 16 names `asymDIFFUSION` the total within-subject variance as `Δt → ∞` and prints standardised matrices with the suffix `std` when appropriate. Footnote 4 standardises using only the relevant variance, not the total. The 2017-era `summary.ctsemFit.R` forms `asymDIFFUSIONstd` whenever `verbose = TRUE`, as `solve(sqrt(diag(asymDIFFUSION) + ridging)) %&% asymDIFFUSION`. OpenMx `%&%` is the quadratic form `t(A) %*% B %*% A`. That formation adds `diag(c(ridging), n.latent)`. The default `ridging = FALSE` adds 0, not `0.0001`; that ridge is a numerical hack and is not this exact map. The 2017-era source assigns `dimnames(asymDIFFUSIONstd)` to `latentNames`; that assignment matches the `n.latent × n.latent` matrix and is this map. The scalar correlation is `p / p = 1` after strictly positive Lyapunov `p = −q / (2 a)`. Form strictly positive `p` first, then `1 / √p`, then `(1 / √p) p (1 / √p)`. Unstandardised `asymDIFFUSION` is defined for a zero process; standardised `asymDIFFUSION` is not. Zero `q` makes `solve(sqrt(0))` fail in the 2017-era source and fails closed here. That source does not skip forming `asymDIFFUSIONstd` when `p = 0`. Within-subject variance is an event-time structural quantity, so a non-event clock fails closed. Lasting `asymDIFFUSION` requires stable `a < 0`. Distinct positive `p` recover the same 1. `TIPREDVARstd` `v / v = 1` recovers the same number and remains a distinct named quantity. `DIFFUSIONstd` `q / p = −2 a` is the continuous-diffusion ratio, not this correlation. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) remains unread (Unpaywall 2026-08-24T08:02Z: `is_oa: false`, 0 locations; title *Measurement Invariance, Factor Analysis and Factorial Invariance*). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread (Unpaywall 2026-08-24T08:02Z: `is_oa: false`, 0 locations; title *Randomization-Based Inference about Latent Variables from Complex Samples*). +- `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Table 2, p. 12 `TIPREDVAR`; p. 16 `TIPREDVARstd`; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T22:53Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar standardised time-independent predictor variance. Table 2 names `TIPREDVAR` the variance/covariance of time-independent predictors. Page 16 prints standardised matrices with the suffix `std` when appropriate. The 2017-era `summary.ctsemFit.R` forms `TIPREDVARstd` whenever `verbose = TRUE` and `n.TIpred > 0`, as `solve(sqrt(diag(TIPREDVAR) + ridging)) %&% TIPREDVAR`. OpenMx `%&%` is the quadratic form `t(A) %*% B %*% A`. Unlike `TRAITVARstd`, that formation adds `diag(c(ridging), n.TIpred)`. The default `ridging = FALSE` adds 0, not `0.0001`; that ridge is a numerical hack and is not this exact map. The 2017-era source assigns `dimnames(TIPREDVARstd)` to `TIpredNames`; that assignment matches the `n.TIpred × n.TIpred` matrix and is this map. The scalar correlation is `v / v = 1` after strictly positive `TIPREDVAR`. Form strictly positive `v` first, then `1 / √v`, then `(1 / √v) v (1 / √v)`. Unstandardised `TIPREDVAR` is defined for a zero predictor; standardised `TIPREDVAR` is not. Zero `v` makes `solve(sqrt(0))` fail in the 2017-era source and fails closed here. Unlike `TRAITVAR` / `MANIFESTTRAITVAR`, that source does not skip forming `TIPREDVARstd` when `v = 0`. Predictor variance is an event-time structural quantity, so a non-event clock fails closed. `TIPREDVAR` does not require stable `a < 0`. Distinct positive `v` recover the same 1. `MANIFESTVARstd` `θ / θ = 1` recovers the same number and remains a distinct named quantity. Section 7.2 `addedTIPREDVAR` `(B / a)² v` is extra process variance, not this correlation. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) remains unread (Unpaywall 2026-08-23T22:21Z: `is_oa: false`; title *Measurement Invariance, Factor Analysis and Factorial Invariance*). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread (Unpaywall 2026-08-23T22:21Z: `is_oa: false`; title *Randomization-Based Inference about Latent Variables from Complex Samples*). +- `psychometric_core` recovers Equation 5 of the scalar analog of Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Table 2, p. 12 `TDPREDVAR` / `T0TDPREDCOV`; Table 3, p. 13 `T0TIPREDEFFECT`; p. 16; §7.2, pp. 20–21; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T22:26Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) extra first-occasion time-dependent predictor variance. Equation 5 writes `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and `Γ ~ N(τ, Ψ)`. The 2017-era `summary.ctsemFit.R` forms the latent extra `addedT0TIPREDVAR` as `T0TIPREDEFFECT %*% TIPREDVAR %*% t(T0TIPREDEFFECT)` immediately after `T0TIPREDEFFECTstd`. That file comments out `TDPREDVAR` and does not form `addedT0TDPREDVAR`. The scalar analog of that quadratic form using the stack's first-occasion TD coefficient `t0_m` and Table 2 `TDPREDVAR` `v` is `t0_m² v`. Equation 5 of that analog extra, with `θ = 0` and `ψ = 0`, is `λ² t0_m² v`. Form the analog extra first, then `(λ extra) λ`. Do not form `λ²` first. A zero loading or zero extra is exactly zero. `v < 0` fails closed. `T0` is an event-time occasion, so a non-event clock fails closed. Free `t0_m` does not require stable `a < 0`. `t0_m² v` is the latent extra, not this observed extra. `λ² p_0 + θ` is first-occasion observed variance, not this extra. `λ² t0_b² v` is Eq. 5 of `addedT0TIPREDVAR` and is not this extra even when `t0_m = t0_b`. `MANIFESTVAR` `θ` is measurement error, not this extra. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) remains unread (Unpaywall 2026-08-23T22:26Z: `is_oa: false`; title *Measurement Invariance, Factor Analysis and Factorial Invariance*). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread (Unpaywall 2026-08-23T22:26Z: `is_oa: false`; title *Randomization-Based Inference about Latent Variables from Complex Samples*). +- `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Table 2, p. 12 `MANIFESTVAR`; Eq. 5, p. 5; p. 16 `MANIFESTVARstd`; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T22:40Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar standardised measurement-error variance. Table 2 names `MANIFESTVAR` `Θ` the residual covariance of the indicators. Equation 5 writes `ζ ~ N(0, Θ)`. Page 16 prints standardised matrices with the suffix `std` when appropriate. The 2017-era `summary.ctsemFit.R` forms `MANIFESTVARstd` whenever `verbose = TRUE`, as `solve(sqrt(diag(MANIFESTVAR) + ridging)) %&% MANIFESTVAR`. OpenMx `%&%` is the quadratic form `t(A) %*% B %*% A`. Unlike `TRAITVARstd`, that formation adds `diag(c(ridging), n.manifest)`. The default `ridging = FALSE` adds 0, not `0.0001`; that ridge is a numerical hack and is not this exact map. The 2017-era source assigns `dimnames(MANIFESTVARstd)` to `latentNames`; the matrix is `n.manifest × n.manifest`. That assignment is a source bug and is not this exact map. The scalar correlation is `θ / θ = 1` after strictly positive `MANIFESTVAR`. Form strictly positive `θ` first, then `1 / √θ`, then `(1 / √θ) θ (1 / √θ)`. Unstandardised `MANIFESTVAR` is defined for a zero residual; standardised `MANIFESTVAR` is not. Zero `θ` makes `solve(sqrt(0))` fail in the 2017-era source and fails closed here. Unlike `TRAITVAR` / `MANIFESTTRAITVAR`, that source does not skip forming `MANIFESTVARstd` when `θ = 0`. Measurement-error variance is an event-time structural quantity, so a non-event clock fails closed. `MANIFESTVAR` does not require stable `a < 0`. Distinct positive `θ` recover the same 1. `MANIFESTTRAITVARstd` `ψ / ψ = 1` recovers the same number and remains a distinct named quantity. Equation 5 `λ² Var(η) + θ` is `Var(y)`, not this correlation. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) remains unread (Unpaywall 2026-08-23T22:21Z: `is_oa: false`; title *Measurement Invariance, Factor Analysis and Factorial Invariance*). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread (Unpaywall 2026-08-23T22:21Z: `is_oa: false`; title *Randomization-Based Inference about Latent Variables from Complex Samples*). +- `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Table 2, p. 12 `MANIFESTTRAITVAR`; §7.1, p. 19; p. 16 `MANIFESTTRAITVARstd`; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T22:28Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar standardised manifest-trait variance. Table 2 names `MANIFESTTRAITVAR` `Ψ_τ` the additional time-invariant variance-covariance on the measurement level and sets it `NULL` when there is no manifest trait. Section 7.1 names manifest traits stable individual differences in indicator levels, distinct from process-level `TRAITVAR` `φ_ξ`. Page 16 prints standardised matrices with the suffix `std` when appropriate. The 2017-era `summary.ctsemFit.R` forms `MANIFESTTRAITVARstd` only when `MANIFESTTRAITVAR != 0`, as `solve(sqrt(diag(MANIFESTTRAITVAR) + ridging)) %&% MANIFESTTRAITVAR` when `verbose = TRUE`. OpenMx `%&%` is the quadratic form `t(A) %*% B %*% A`. Unlike `TRAITVARstd`, that formation adds `diag(c(ridging), n.manifest)`. The default `ridging = FALSE` adds 0, not `0.0001`; that ridge is a numerical hack and is not this exact map. The scalar correlation is `ψ / ψ = 1` after strictly positive `MANIFESTTRAITVAR`. Form strictly positive `ψ` first, then `1 / √ψ`, then `(1 / √ψ) ψ (1 / √ψ)`. Unstandardised `MANIFESTTRAITVAR` is defined for a zero trait; standardised `MANIFESTTRAITVAR` is not. Zero `MANIFESTTRAITVAR` skips forming `MANIFESTTRAITVARstd` in the 2017-era source and fails closed here. Indicator-level trait variance is an event-time structural quantity, so a non-event clock fails closed. `MANIFESTTRAITVAR` does not require stable `a < 0`. Distinct positive `ψ` recover the same 1. `TRAITVARstd` `trait / trait = 1` recovers the same number and remains a distinct named quantity. `MANIFESTVAR` `θ` is measurement error, not this correlation. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) remains unread (Unpaywall 2026-08-23T22:21Z: `is_oa: false`; title *Measurement Invariance, Factor Analysis and Factorial Invariance*). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread (Unpaywall 2026-08-23T22:21Z: `is_oa: false`; title *Randomization-Based Inference about Latent Variables from Complex Samples*). +- `psychometric_core` recovers the scalar analog of Driver, Oud, and Voelkle (2017, Table 2, p. 12 `TDPREDVAR` / `T0TDPREDCOV`; Table 3, p. 13 `T0TIPREDEFFECT`; p. 16; §7.2, pp. 20–21; Eq. 3, p. 5; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T22:13Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) extra first-occasion time-dependent predictor variance. The 2017-era `summary.ctsemFit.R` forms `addedT0TIPREDVAR` as `T0TIPREDEFFECT %*% TIPREDVAR %*% t(T0TIPREDEFFECT)` immediately after `T0TIPREDEFFECTstd`. That file comments out `TDPREDVAR` and does not form `addedT0TDPREDVAR`. Table 2 names `T0TDPREDCOV` the covariance between latents at `T0` and time-dependent predictors, not this extra variance. Table 3 names `T0TIPREDEFFECT`, not a TD first-occasion effect matrix. The scalar analog of that quadratic form using the stack's first-occasion TD coefficient `t0_m` and Table 2 `TDPREDVAR` `v` is `t0_m² v`. Form `t0_m` first, then square, then multiply by `v`. A zero coefficient or zero predictor variance is exactly zero. `v < 0` fails closed. `T0` is an event-time occasion, so a non-event clock fails closed. Free `t0_m` does not require stable `a < 0`. `t0_b² v` is `addedT0TIPREDVAR` and is not this extra even when `t0_m = t0_b`. `t0_m · √v / √p_0` is `T0TDPREDEFFECTstd` and is not this variance. `T0TDPREDCOV` is the covariance, not `t0_m² v`. Free `T0VAR` `p_0` is the first-occasion state, not the extra TD variance. `TRAITVAR` is a zero-drift latent process, not `t0_m² v`. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) remains unread (Unpaywall 2026-08-23T21:34Z: `is_oa: false`; title *Measurement Invariance, Factor Analysis and Factorial Invariance*). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread (Unpaywall 2026-08-23T21:34Z: `is_oa: false`; title *Randomization-Based Inference about Latent Variables from Complex Samples*). +- `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Table 2, p. 12 `TRAITVAR`; §7.1, pp. 18–19; p. 16 `TRAITVARstd`; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T22:21Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar standardised trait variance. Table 2 names `TRAITVAR` `φ_ξ` the latent trait variance/covariance and sets it `NULL` when there is no trait. Section 7.1 names traits the stable between-subject differences (unit-level unobserved heterogeneity). Page 16 prints standardised matrices with the suffix `std` when appropriate. The 2017-era `summary.ctsemFit.R` forms `TRAITVARstd` only when `TRAITVAR != 0`, as `solve(sqrt(diag(TRAITVAR))) %&% TRAITVAR` when `verbose = TRUE`. OpenMx `%&%` is the quadratic form `t(A) %*% B %*% A`. Unlike `T0VARstd`, that formation uses `diag(diag(TRAITVAR))` and does not add `diag(c(ridging))`. The ridge is a `T0VAR` numerical hack and is not this exact map. The scalar correlation is `trait / trait = 1` after strictly positive `TRAITVAR`. Form strictly positive `trait` first, then `1 / √trait`, then `(1 / √trait) trait (1 / √trait)`. Unstandardised `TRAITVAR` is defined for a zero trait; standardised `TRAITVAR` is not. Zero `TRAITVAR` skips forming `TRAITVARstd` in the 2017-era source and fails closed here. Between-subject variance is an event-time structural quantity, so a non-event clock fails closed. `TRAITVAR` does not require stable `a < 0`. Distinct positive `trait` recover the same 1. `T0VARstd` `p_0 / p_0 = 1` recovers the same number and remains a distinct named quantity. `addedT0TIPREDVAR` `t0_b² v` is extra first-occasion TI variance, not this correlation. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) remains unread (Unpaywall 2026-08-23T22:21Z: `is_oa: false`; title *Measurement Invariance, Factor Analysis and Factorial Invariance*). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread (Unpaywall 2026-08-23T22:21Z: `is_oa: false`; title *Randomization-Based Inference about Latent Variables from Complex Samples*). +- `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Table 3, p. 13 `T0TDPREDEFFECTstd`; Table 2, p. 12 `TDPREDVAR`; p. 16; footnote 4; Eq. 3, p. 5; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T18:17Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar standardised first-occasion time-dependent predictor effect. Table 3 names `T0TDPREDEFFECT` the effect of time-dependent predictors on latents at `T0`. Page 16 prints standardised matrices with the suffix `std` when appropriate. Footnote 4: standardisations use only the relevant variance, not the total. The affecting variance is time-dependent predictor variance `TDPREDVAR` `v_x`, not `TIPREDVAR`. The affected variance is free first-occasion `T0VAR` `p_0`, not within-subject `asymDIFFUSION` `-q / (2 a)`, because Table 3 is the first occasion, not the process dynamics. Form strictly positive `p_0` first, then strictly positive `v_x`, then `t0_m · √v_x / √p_0`. Unstandardised `t0_m` is defined for a zero coefficient and for zero predictor variance; standardised `T0TDPREDEFFECT` is not. Zero `p_0` or zero `v_x` has no positive SD and fails closed. `T0` is an event-time occasion, so a non-event clock fails closed. Free `T0VAR` does not require stable `a < 0`. Same numbers as `T0TIPREDEFFECTstd` yield the same product; Table 3 names a different matrix. The continuous standardisation `B · √v / √(-q / (2 a))` uses `asymDIFFUSION` and is not this first-occasion map. Section 7.1 warns that omitting trait variance confounds between- and within-person information. `t0_m · √v_x / √(trait + p_0 + added)` uses the total, not free `T0VAR`, and is not `T0TDPREDEFFECTstd` when `TRAITVAR` is nonzero. `TRAITVAR` is not the standardisation variance. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) remains unread (Unpaywall 2026-08-23T18:17Z: `is_oa: false`; title *Measurement Invariance, Factor Analysis and Factorial Invariance*). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread (Unpaywall 2026-08-23T18:17Z: `is_oa: false`; title *Randomization-Based Inference about Latent Variables from Complex Samples*). +- `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, p. 16; §7.2, pp. 20–21; Table 2, p. 12; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T21:22Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar standardised extra time-independent predictor variance `addedTIPREDVARstd`. Page 16 prints standardised matrices with the suffix `std` when appropriate. After `addedTIPREDVAR` as `asymTIPREDEFFECT %*% TIPREDVAR %*% t(asymTIPREDEFFECT)`, the 2017-era `summary.ctsemFit.R` forms `addedTIPREDVARstd = solve(sqrt(diag(addedTIPREDVAR))) %&% addedTIPREDVAR`. OpenMx `%&%` is the quadratic form `t(A) %*% B %*% A`. The default `ridging = FALSE` adds 0, not `0.0001`; that ridge is a numerical hack and is not this exact map. The scalar correlation is `extra / extra = 1` after strictly positive extra. Form `addedTIPREDVAR` first, then the ratio. A zero extra has no positive extra SD and fails closed. Unstandardised `(B / a)² v` is defined for a zero coefficient and for zero predictor variance; standardised `addedTIPREDVAR` is not. `λ² (B / a)² v` is Eq. 5 of the extra, not this correlation. `t0_b² v` is `addedT0TIPREDVAR`, not this asymptotic extra correlation. `TRAITVAR` is not the standardisation variance. The printed 2-latent `addedTIPREDVAR` 2.838 is not this scalar 1. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) remains unread (Unpaywall 2026-08-23T21:22Z: `is_oa: false`; title *Measurement Invariance, Factor Analysis and Factorial Invariance*). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread (Unpaywall 2026-08-23T21:22Z: `is_oa: false`; title *Randomization-Based Inference about Latent Variables from Complex Samples*). +- `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, p. 16 `TDPREDEFFECTstd`; Table 2, p. 12; Eq. 3, p. 5; footnote 4; §7.1, pp. 18–19; JSS PDF re-opened 2026-08-23T21:10Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar standardised continuous time-dependent predictor effect. Page 16 prints continuous-time parameters and, when appropriate, standardised matrices with the suffix `std`. Table 2 names `M` `TDPREDEFFECT`. Footnote 4: standardisations use only the relevant variance, not the total. The affecting variance is time-dependent predictor variance `v`. The affected variance is within-subject `asymDIFFUSION` `-q / (2 a)`, because the process dynamics are individual, or average individual, temporal dynamics. Form strictly positive `asymDIFFUSION` first, then strictly positive `v`, then `m · √v / √(-q / (2 a))`. Unstandardised `M` is defined for a zero coefficient and for zero predictor variance; standardised `TDPREDEFFECT` is not. Zero `asymDIFFUSION` or zero `v` has no positive SD and fails closed. `TIPREDEFFECTstd` `B · √v / √p` is a different named matrix even when `M = B` and the predictor variances match. The finite-interval intercept-style standardisation `A^{-1}[e^{A Δt} − I] M · √v / √p` depends on the event interval and is not this continuous Dirac coefficient. Section 7.1 warns that omitting trait variance confounds between- and within-person information. `m · √v / √(trait + p + added)` uses the total, not `asymDIFFUSION`, and is not `TDPREDEFFECTstd` when `TRAITVAR` is nonzero. `TRAITVAR` is not the standardisation variance. Predecessor `event_time.rs` asymptotic-std `process_sd == 0` / `predictor_sd == 0` gates after already-checked `within == 0` and `v == 0` were unreachable; this slice drops them so stacked line/branch coverage can close. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) remains unread (Unpaywall 2026-08-23T21:10Z: `is_oa: false`; title *Measurement Invariance, Factor Analysis and Factorial Invariance*). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread (Unpaywall 2026-08-23T21:10Z: `is_oa: false`; title *Randomization-Based Inference about Latent Variables from Complex Samples*). +- `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, p. 16 `CINTstd`; Eq. 1, p. 4; Table 2, p. 12; footnote 4; §7.1, pp. 18–19; JSS PDF re-opened 2026-08-23T17:10Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar standardised continuous intercept. Page 16 prints continuous-time parameters and, when appropriate, standardised matrices with the suffix `std`. Table 2 names `κ` `CINT`. Footnote 4: standardisations use only the relevant variance, not the total. `CINT` is the process intercept of individual, or average individual, dynamics, so that relevant variance is within-subject `asymDIFFUSION` `-q / (2 a)`. Form strictly positive `asymDIFFUSION` first, then `κ / √(-q / (2 a))`. Unstandardised `κ` is defined for growing `a ≥ 0` and for zero diffusion; standardised `CINT` is not. Zero `asymDIFFUSION` has no positive SD and fails closed. The asymptotic standardisation `(-κ / a) / √p` is the total change, not this continuous intercept. The finite-interval standardisation `A^{-1}[e^{A Δt} − I] κ / √p` depends on the event interval and is not this continuous map. Section 7.1 warns that omitting trait variance confounds between- and within-person information. `κ / √(trait + p + added)` uses the total, not `asymDIFFUSION`, and is not `CINTstd` when `TRAITVAR` is nonzero. `TRAITVAR` is not the standardisation variance. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) remains unread (Unpaywall 2026-08-23T13:19Z: `is_oa: false`; title *Measurement Invariance, Factor Analysis and Factorial Invariance*). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread (Unpaywall 2026-08-23T13:19Z: `is_oa: false`; title *Randomization-Based Inference about Latent Variables from Complex Samples*). +- `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, p. 16 `TIPREDEFFECTstd`; §7.2, pp. 20–21; Eq. 3, p. 5; Table 2, p. 12; footnote 4; JSS PDF re-opened 2026-08-23T16:21Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar standardised continuous time-independent predictor effect. Page 16 prints continuous-time parameters and, when appropriate, standardised matrices with the suffix `std`. Table 2 names `B` `TIPREDEFFECT`. Footnote 4: standardisations use only the relevant variance, not the total. The affecting variance is predictor variance `TIPREDVAR` `v`. The affected variance is within-subject `asymDIFFUSION` `-q / (2 a)`, because the process dynamics are individual, or average individual, temporal dynamics. Form strictly positive `asymDIFFUSION` first, then strictly positive `v`, then `B · √v / √(-q / (2 a))`. Unstandardised `B` is defined for a zero coefficient and for zero predictor variance; standardised `TIPREDEFFECT` is not. Zero `asymDIFFUSION` or zero `v` has no positive SD and fails closed. The asymptotic standardisation `(-B / a) · √v / √p` is the total change, not this continuous coefficient. The finite-interval standardisation `A^{-1}[e^{A Δt} − I] B · √v / √p` depends on the event interval and is not this continuous map. Section 7.1 warns that omitting trait variance confounds between- and within-person information. `B · √v / √(trait + p + added)` uses the total, not `asymDIFFUSION`, and is not `TIPREDEFFECTstd` when `TRAITVAR` is nonzero. `TRAITVAR` is not the standardisation variance. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) remains unread (Unpaywall 2026-08-23T13:19Z: `is_oa: false`; title *Measurement Invariance, Factor Analysis and Factorial Invariance*). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread (Unpaywall 2026-08-23T13:19Z: `is_oa: false`; title *Randomization-Based Inference about Latent Variables from Complex Samples*). +- `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, p. 16 `asymTIPREDEFFECTstd`; §7.2, pp. 20–21; Eq. 3, p. 5; Table 2, p. 12; footnote 4; JSS PDF re-opened 2026-08-23T14:25Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar standardised asymptotic time-independent predictor effect. Page 16 prints continuous-time parameters and, when appropriate, standardised matrices with the suffix `std`. Section 7.2 names `asymTIPREDEFFECT` the expected total change in process means given a unit increase on a time-independent predictor. The scalar map is `-B / a` for stable `a < 0`. Footnote 4: standardisations use only the relevant variance, not the total. The affecting variance is predictor variance `TIPREDVAR` `v`. The affected variance is within-subject `asymDIFFUSION` `-q / (2 a)`, because the process dynamics are individual, or average individual, temporal dynamics. Form strictly positive `asymDIFFUSION` first, then strictly positive `v`, then the unit asymptotic effect, then `(-B / a) · √v / √(-q / (2 a))`. Unstandardised `-B / a` is defined for a zero coefficient and for zero predictor variance; standardised `asymTIPREDEFFECT` is not. Zero `asymDIFFUSION` or zero `v` has no positive SD and fails closed. The finite-interval standardisation `A^{-1}[e^{A Δt} − I] B · √v / √p` depends on the event interval and is not this `Δt → ∞` map. Section 7.1 warns that omitting trait variance confounds between- and within-person information. `(-B / a) · √v / √(trait + p + added)` uses the total, not `asymDIFFUSION`, and is not `asymTIPREDEFFECTstd` when `TRAITVAR` is nonzero. `TRAITVAR` is not the standardisation variance. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) remains unread (Unpaywall 2026-08-23T13:19Z: `is_oa: false`; title *Measurement Invariance, Factor Analysis and Factorial Invariance*). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread (Unpaywall 2026-08-23T13:19Z: `is_oa: false`; title *Randomization-Based Inference about Latent Variables from Complex Samples*). +- `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, p. 16 `DRIFTstd`; Eq. 1, p. 4; footnote 4; §7.1, pp. 18–19; JSS PDF re-opened 2026-08-23T13:28Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar standardised continuous `DRIFT`. Page 16 prints continuous-time parameters (e.g., `DRIFT`) and, when appropriate, standardised matrices with the suffix `std`. Footnote 4: standardisations use only the relevant variance, not the total. For `DRIFT` that relevant variance is within-subject `asymDIFFUSION` `-q / (2 a)`, because `DRIFT` is intended to represent individual, or average individual, temporal dynamics. Form strictly positive `asymDIFFUSION` first. In the scalar stationary case the within-subject SD ratio is 1, so the standardised auto-effect equals the unstandardised log-rate numerically; those remain distinct named quantities. Unstandardised `a` is defined for growing `a ≥ 0` and for zero diffusion; standardised `DRIFT` is not. Zero `asymDIFFUSION` has no positive SD and fails closed. The discrete standardisation `e^{a Δt}` depends on the event interval and is not the continuous map. Section 7.1 warns that omitting trait variance confounds between- and within-person information. `a p / (trait + p + added)` uses the total, not `asymDIFFUSION`, and is not `DRIFTstd` when `TRAITVAR` is nonzero. `TRAITVAR` is not the standardisation variance. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) remains unread (Unpaywall 2026-08-23T13:19Z: `is_oa: false`; title *Measurement Invariance, Factor Analysis and Factorial Invariance*). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread (Unpaywall 2026-08-23T13:19Z: `is_oa: false`; title *Randomization-Based Inference about Latent Variables from Complex Samples*). +- `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, p. 16 `DIFFUSIONstd`; Eq. 4, p. 5; footnote 4; §7.1, pp. 18–19; JSS PDF re-opened 2026-08-23T13:20Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar standardised continuous `DIFFUSION`. Page 16 prints continuous-time parameters (e.g., `DRIFT`, `DIFFUSION`) and, when appropriate, standardised matrices with the suffix `std`. Footnote 4: standardisations use only the relevant variance, not the total. Process noise is within-subject stochastic input, so that relevant variance is within-subject `asymDIFFUSION` `-q / (2 a)`, the same footnote 4 variance used for `DRIFT`. Form strictly positive `asymDIFFUSION` first, then `q / (−q / (2 a))`. In the scalar stationary case that ratio equals `-2 a` and does not depend on `q` once `q > 0`. Unstandardised `q` is defined for growing `a ≥ 0` and for zero diffusion; standardised `DIFFUSION` is not. Zero `asymDIFFUSION` has no positive SD and fails closed. The discrete standardisation `Q_Δt / (−q / (2 a)) = 1 − exp(2 a Δt)` depends on the event interval and is not the continuous map. Section 7.1 warns that omitting trait variance confounds between- and within-person information. `q / (trait + p + added)` uses the total, not `asymDIFFUSION`, and is not `DIFFUSIONstd` when `TRAITVAR` is nonzero. `TRAITVAR` is not the standardisation variance. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) remains unread (Unpaywall 2026-08-23T13:19Z: `is_oa: false`; title *Measurement Invariance, Factor Analysis and Factorial Invariance*). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread (Unpaywall 2026-08-23T13:19Z: `is_oa: false`; title *Randomization-Based Inference about Latent Variables from Complex Samples*). +- `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, p. 16 `discreteDIFFUSIONstd`; Eq. 3–4, pp. 4–5; footnote 4; §7.1, pp. 18–19; JSS PDF re-opened 2026-08-23T13:06Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar standardised discrete `DIFFUSION`. Page 16 prints discrete-time transformations for a chosen event interval (`discreteDRIFT`, `discreteDIFFUSION`) and, when appropriate, standardised matrices with the suffix `std`. Footnote 4: standardisations use only the relevant variance, not the total. Process noise is within-subject stochastic input, so that relevant variance is within-subject `asymDIFFUSION` `-q / (2 a)`, the same footnote 4 variance used for `DRIFT`. Form strictly positive `asymDIFFUSION` first, then `Q_Δt` from Equation 4, then `Q_Δt / (−q / (2 a))`. In the scalar stationary case that ratio equals `1 − exp(2 a Δt)`. Unstandardised `Q_Δt` is defined for growing `a ≥ 0` and for zero diffusion; standardised `DIFFUSION` is not. Zero `asymDIFFUSION` has no positive SD and fails closed. The continuous standardisation `q / (−q / (2 a)) = −2 a` is not the discrete map. Section 7.1 warns that omitting trait variance confounds between- and within-person information. `Q_Δt / (trait + p + added)` uses the total, not `asymDIFFUSION`, and is not `discreteDIFFUSIONstd` when `TRAITVAR` is nonzero. `TRAITVAR` is not the standardisation variance. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) remains unread (Unpaywall 2026-08-23T13:06Z: `is_oa: false`; title *Measurement Invariance, Factor Analysis and Factorial Invariance*). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread (Unpaywall 2026-08-23T13:06Z: `is_oa: false`; title *Randomization-Based Inference about Latent Variables from Complex Samples*). +- `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, p. 16 `discreteDRIFTstd`; Eq. 3, p. 5; footnote 4; §7.1, pp. 18–19; JSS PDF re-opened 2026-08-23T11:40Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar standardised discrete `DRIFT`. Page 16 prints `discreteDRIFT` as `expm(DRIFT Δt)` and, when appropriate, `discreteDRIFTstd`. Footnote 4: standardisations use only the relevant variance, not the total. For `DRIFT` that relevant variance is within-subject `asymDIFFUSION` `-q / (2 a)`, because `DRIFT` is intended to represent individual, or average individual, temporal dynamics. Form strictly positive `asymDIFFUSION` first, then `φ = exp(a Δt)`. In the scalar stationary case the within-subject SD ratio is 1, so the standardised auto-effect equals the unstandardised discrete lag numerically; those remain distinct named quantities. Unstandardised `e^{a Δt}` is defined for growing `a ≥ 0` and for zero diffusion; standardised `DRIFT` is not. Zero `asymDIFFUSION` has no positive SD and fails closed. Section 7.1 warns that omitting trait variance confounds between- and within-person information. The trait-plus-state autocorrelation `(trait + e^{a Δt} p + added) / (trait + p + added)` uses the total, not `asymDIFFUSION`, and is not `discreteDRIFTstd` when `TRAITVAR` is nonzero. `TRAITVAR` is not the standardisation variance. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) remains unread (Unpaywall 2026-08-23T11:40Z: `is_oa: false`; title *Measurement Invariance, Factor Analysis and Factorial Invariance*). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread (Unpaywall 2026-08-23T11:40Z: `is_oa: false`; title *Randomization-Based Inference about Latent Variables from Complex Samples*). +- `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, §4.3, pp. 9–10; Eq. 5, p. 5; Table 2, p. 12; p. 16; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-23T10:03Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar first-occasion variance of §4.3 predetermined `T0VAR`. Section 4.3 treats the first time point as predetermined when no assumptions are made about the process prior to the initial time point. Free `T0VAR` `p_0` is then estimated. Between-subject `TRAITVAR` and `addedTIPREDVAR` are inherently stationary. The first-occasion composition is `trait + p_0 + (B / a)² v`. Form the free first-occasion state variance first, then include the trait, then include the TI extra variance, then add. Setting `p_0 = −q / (2 a)` recovers the stationary first-occasion map. Stationary first-occasion variance uses `−q / (2 a)` in place of `p_0` and is not this map when `p_0` is free. Free `T0VAR` `p_0` is not this map. The lagged map `trait + e^{a Δt} p_0 + (B / a)² v` decays the state and is not this map. The later-occasion map `trait + e^{2 a Δt} p_0 + Q_Δt + (B / a)² v` includes `Q_Δt` and is not this map. As `Δt → 0+` those maps approach this composition. A zero trait, a zero initial variance, and a zero TI contribution is exactly zero. A zero initial variance and a zero TI contribution is exactly the trait. Trait-only variance does not require a stable drift. `a ≥ 0` cannot hold a finite TI extra variance when that contribution is nonzero and fails closed. Equation 5 of that first-occasion variance is `λ²(trait + p_0 + (B / a)² v) + θ + ψ`. `MANIFESTVAR` is not that first-occasion observed variance. The predetermined first-occasion latent variance is not the predetermined first-occasion observed variance. Stationary first-occasion observed variance is not that observed variance when `p_0` is free. Predetermined later observed variance includes `Q_Δt` and is not that first-occasion observed variance. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) remains unread (Unpaywall 2026-08-23T10:03Z: `is_oa: false`; title *Measurement Invariance, Factor Analysis and Factorial Invariance*). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread (Unpaywall 2026-08-23T10:03Z: `is_oa: false`; title *Randomization-Based Inference about Latent Variables from Complex Samples*). +- `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, §4.3, pp. 9–10; Eq. 3–5, pp. 4–5; Table 2, p. 12; p. 16; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-23T20:20Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar lagged covariance of §4.3 predetermined `T0VAR`. Section 4.3 treats the first time point as predetermined when no assumptions are made about the process prior to the initial time point. Free `T0VAR` `p_0` is then estimated. Equation 3 writes `η(t) = exp(A Δt) η(t0) + …`. Equation 4 writes `cov(η_t, η_{t-1}) = A_Δt cov(η_{t-1})`. Trait variance and `addedTIPREDVAR` are time-invariant between-subject and do not decay with `e^{a Δt}`. The lagged composition is `trait + e^{a Δt} p_0 + (B / a)² v`. Form the lagged free first-occasion covariance first, then include the trait, then include the TI extra variance, then add. Setting `p_0 = −q / (2 a)` recovers the stationary lagged map. Stationary lagged covariance uses `−q / (2 a)` in place of `p_0` and is not this map when `p_0` is free. Evolving `trait + p_0 + (B / a)² v` as if it were all state (`e^{a Δt}` of that total) is not this map. Free `T0VAR` `p_0` is not this map. The later-occasion map `trait + e^{2 a Δt} p_0 + Q_Δt + (B / a)² v` includes `Q_Δt` and is not this map. As `Δt → ∞` with stable `a < 0` the state term vanishes. As `Δt → 0+` the composition approaches `trait + p_0 + (B / a)² v`. A zero-diffusion carry with `a ≥ 0` is `e^{a Δt} p_0` and is kept. Trait-only variance does not require a stable drift. The interval must be event time and strictly positive. Equation 5 of that lagged covariance is `λ²(trait + e^{a Δt} p_0 + (B / a)² v) + ψ`. Independent `ε_t` does not enter. `MANIFESTVAR` is not that lagged observed covariance. The predetermined lagged latent covariance is not the predetermined lagged observed covariance. Predetermined later observed variance includes `Q_Δt` and `θ` and is not that lagged observed covariance. Stationary lagged observed covariance is not that observed covariance when `p_0` is free. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) remains unread (Unpaywall 2026-08-23T20:20Z: `is_oa: false`; title *Measurement Invariance, Factor Analysis and Factorial Invariance*). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread (Unpaywall 2026-08-23T20:20Z: `is_oa: false`; title *Randomization-Based Inference about Latent Variables from Complex Samples*). +- `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, §4.3, pp. 9–10; Eq. 3–5, pp. 4–5; Table 2, p. 12; p. 16; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-23T20:20Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar later-occasion variance of §4.3 predetermined `T0VAR`. Section 4.3 treats the first time point as predetermined when no assumptions are made about the process prior to the initial time point. Free `T0VAR` `p_0` is then estimated. The process gradually transitions from the variances of the initial parameters toward those of the parameters when the model is stationary. Equation 3 writes `η(t) = exp(A Δt) η(t0) + … +` the stochastic integral. Equation 4 writes that the integral exhibits covariance `Q_Δt`. The law of total variance on the within-subject state is `e^{2 a Δt} p_0 + Q_Δt`. Trait variance and `addedTIPREDVAR` are time-invariant between-subject and do not enter that process-noise integral. The later-occasion composition is `trait + e^{2 a Δt} p_0 + Q_Δt + (B / a)² v`. Form the evolved free first-occasion variance first, then include the trait, then include the TI extra variance, then add. Setting `p_0 = −q / (2 a)` recovers the stationary later-occasion map. Stationary later-occasion variance uses `−q / (2 a)` in place of `p_0` and is not this map when `p_0` is free. Evolving `trait + p_0 + (B / a)² v` as if it were all state (`e^{2 a Δt}` of that total plus `Q_Δt`) is not this map. Free `T0VAR` `p_0` is not this map. As `Δt → ∞` with stable `a < 0` the carried `p_0` vanishes and `Q_Δt` approaches `−q / (2 a)`, so the composition approaches contemporaneous stationary `T0VAR`. As `Δt → 0+` the composition approaches `trait + p_0 + (B / a)² v`. Nonzero diffusion with `a ≥ 0` is a growing process and is kept. The interval must be event time and strictly positive. Equation 5 of that later-occasion variance is `λ²(trait + e^{2 a Δt} p_0 + Q_Δt + (B / a)² v) + θ + ψ`. `MANIFESTVAR` is not that later-occasion observed variance. The predetermined later-occasion latent variance is not the predetermined later-occasion observed variance. Stationary later-occasion observed variance is not that observed variance when `p_0` is free. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) remains unread (Unpaywall 2026-08-23T20:20Z: `is_oa: false`; title *Measurement Invariance, Factor Analysis and Factorial Invariance*). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread (Unpaywall 2026-08-23T20:20Z: `is_oa: false`; title *Randomization-Based Inference about Latent Variables from Complex Samples*). +- `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, p. 16 finite-interval standardised `TIPREDEFFECT`; §7.2, pp. 20–21; Eq. 3, p. 5; Table 2, p. 12; footnote 4; JSS PDF re-opened 2026-08-24T01:20Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar standardised finite-interval time-independent predictor effect. Page 16 prints discrete-time transformations for a chosen event interval and, when appropriate, standardised matrices with the suffix `std`. Equation 3 maps a finite event interval as `A^{-1}[e^{A Δt} − I] B`. Table 2 names `B` `TIPREDEFFECT`. Footnote 4: standardisations use only the relevant variance, not the total. The affecting variance is predictor variance `TIPREDVAR` `v`. The affected variance is within-subject `asymDIFFUSION` `-q / (2 a)`, because the process dynamics are individual, or average individual, temporal dynamics. Form strictly positive `asymDIFFUSION` first, then strictly positive `v`, then the unit discrete increment, then `A^{-1}[e^{A Δt} − I] B · √v / √(-q / (2 a))`. Unstandardised `A^{-1}[e^{A Δt} − I] B` is defined for a zero coefficient and for zero predictor variance; standardised finite-interval `TIPREDEFFECT` is not. Zero `asymDIFFUSION` or zero `v` has no positive SD and fails closed. Page 16 `asymTIPREDEFFECTstd` `(-B / a) · √v / √p` is the `Δt → ∞` map and is not this finite interval. A later event interval yields a different standardised increment. Section 7.1 warns that omitting trait variance confounds between- and within-person information. `A^{-1}[e^{A Δt} − I] B · √v / √(trait + p + added)` uses the total, not `asymDIFFUSION`, and is not the finite-interval map when `TRAITVAR` is nonzero. `TRAITVAR` is not the standardisation variance. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) remains unread (Unpaywall 2026-08-24T01:20Z: `is_oa: false`; Springer `content/pdf` is HTML 200; title *Measurement Invariance, Factor Analysis and Factorial Invariance*). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread (Unpaywall 2026-08-24T01:20Z: `is_oa: false`; Springer `content/pdf` is HTML 200; title *Randomization-Based Inference about Latent Variables from Complex Samples*). +- `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Table 2, p. 12 `T0VAR`; p. 16 `T0VARstd`; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T22:06Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar standardised first-occasion latent variance. Table 2 names `T0VAR` the latent process initial variance/covariance. Page 16 prints standardised matrices with the suffix `std` when appropriate. The 2017-era `summary.ctsemFit.R` forms `T0VARstd` as `solve(sqrt(diag(T0VAR))) %&% T0VAR` when `verbose = TRUE`. OpenMx `%&%` is the quadratic form `t(A) %*% B %*% A`. The default `ridging = FALSE` adds 0, not `0.0001`; that ridge is a numerical hack and is not this exact map. The scalar correlation is `p_0 / p_0 = 1` after strictly positive free `T0VAR`. Form strictly positive `p_0` first, then `1 / √p_0`, then `(1 / √p_0) p_0 (1 / √p_0)`. Unstandardised `T0VAR` is defined for a zero first-occasion variance; standardised `T0VAR` is not. Zero `p_0` has no positive SD and fails closed. `T0` is an event-time occasion, so a non-event clock fails closed. Free `T0VAR` does not require stable `a < 0`. Distinct positive `p_0` recover the same 1. `T0TDPREDEFFECTstd` `t0_m · √v / √p_0` depends on `p_0` and is not this correlation. `addedT0TIPREDVAR` `t0_b² v` is extra TI variance, not this correlation. `TRAITVAR` is not the standardisation variance. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) remains unread (Unpaywall 2026-08-23T22:06Z: `is_oa: false`; title *Measurement Invariance, Factor Analysis and Factorial Invariance*). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread (Unpaywall 2026-08-23T22:06Z: `is_oa: false`; title *Randomization-Based Inference about Latent Variables from Complex Samples*). +- `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Table 3, p. 13 `T0TDPREDEFFECTstd`; Table 2, p. 12; p. 16; footnote 4; Eq. 3, p. 5; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T21:34Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar standardised first-occasion time-dependent predictor effect. Table 3 names `T0TDPREDEFFECT` the effect of time-dependent predictors on latents at `T0`. Table 2 names `M` `TDPREDEFFECT` and names `T0TDPREDCOV` the first-occasion covariance, not this coefficient. Page 16 prints standardised matrices with the suffix `std` when appropriate. Footnote 4: standardisations use only the relevant variance, not the total. The affecting variance is time-dependent predictor variance `v`, not `TIPREDVAR`. The affected variance is free first-occasion `T0VAR` `p_0`, not within-subject `asymDIFFUSION` `-q / (2 a)`, because Table 3 is the first occasion, not the process dynamics. Form strictly positive `p_0` first, then strictly positive `v`, then `t0_m · √v / √p_0`. Unstandardised `t0_m` is defined for a zero coefficient and for zero predictor variance; standardised `T0TDPREDEFFECT` is not. Zero `p_0` or zero `v` has no positive SD and fails closed. `T0` is an event-time occasion, so a non-event clock fails closed. Free `T0VAR` does not require stable `a < 0`. The continuous standardisation `m · √v / √(-q / (2 a))` uses `asymDIFFUSION` and is not this first-occasion map. `T0TIPREDEFFECTstd` `t0_b · √v / √p_0` is a different named matrix even when `t0_m = t0_b` and the predictor variances match. Section 7.1 warns that omitting trait variance confounds between- and within-person information. `t0_m · √v / √(trait + p_0 + added)` uses the total, not free `T0VAR`, and is not `T0TDPREDEFFECTstd` when `TRAITVAR` is nonzero. `TRAITVAR` is not the standardisation variance. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) remains unread (Unpaywall 2026-08-23T21:34Z: `is_oa: false`; title *Measurement Invariance, Factor Analysis and Factorial Invariance*). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread (Unpaywall 2026-08-23T21:34Z: `is_oa: false`; title *Randomization-Based Inference about Latent Variables from Complex Samples*). +- `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Table 2, p. 12; §7.2, pp. 20–21; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T19:23Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar extra observed-indicator time-independent predictor variance of §7.2 `addedTIPREDVAR`. Equation 5 writes `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and `Γ ~ N(τ, Ψ)`. The 2017-era `summary.ctsemFit.R` forms the latent extra `addedTIPREDVAR` as `asymTIPREDEFFECT %*% TIPREDVAR %*% t(asymTIPREDEFFECT)`. The scalar latent extra is `(B / a)² v`. Equation 5 of that extra, with `θ = 0` and `ψ = 0`, is `λ² (B / a)² v`. Form `addedTIPREDVAR` first, then `(λ extra) λ`. Do not form `λ²` first. A zero loading or zero extra is exactly zero. `v < 0` fails closed. A non-event clock fails closed. `a ≥ 0` cannot hold a finite process-mean change when the extra is nonzero and fails closed. `(B / a)² v` is the latent extra, not this observed extra. `λ² t0_b² v` is Eq. 5 of `addedT0TIPREDVAR`, not this asymptotic observed extra. `λ² p + θ` is stationary observed variance, not this extra. `MANIFESTVAR` `θ` is measurement error, not this extra. `Ψ` is intercept variance and is not extra TI. The printed 2-latent `addedTIPREDVAR` 2.838 is not this scalar map. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) remains unread (Unpaywall 2026-08-23T19:10Z: `is_oa: false`; title *Measurement Invariance, Factor Analysis and Factorial Invariance*). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread (Unpaywall 2026-08-23T19:10Z: `is_oa: false`; title *Randomization-Based Inference about Latent Variables from Complex Samples*). +- `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Table 3, p. 13 `T0TIPREDEFFECT`; Table 2, p. 12; p. 16; §7.2, pp. 20–21; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T19:10Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar extra observed-indicator time-independent predictor variance of 2017-era `addedT0TIPREDVAR`. Equation 5 writes `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and `Γ ~ N(τ, Ψ)`. The 2017-era `summary.ctsemFit.R` forms the latent extra `addedT0TIPREDVAR` as `T0TIPREDEFFECT %*% TIPREDVAR %*% t(T0TIPREDEFFECT)` immediately after `T0TIPREDEFFECTstd`. The scalar latent extra is `t0_b² v`. Equation 5 of that extra, with `θ = 0` and `ψ = 0`, is `λ² t0_b² v`. Form `addedT0TIPREDVAR` first, then `(λ extra) λ`. Do not form `λ²` first. A zero loading or zero extra is exactly zero. `v < 0` fails closed. `T0` is an event-time occasion, so a non-event clock fails closed. Free `T0TIPREDEFFECT` does not require stable `a < 0`. `t0_b² v` is the latent extra, not this observed extra. `λ² p_0 + θ` is first-occasion observed variance, not this extra. `λ² (B / a)² v` is Eq. 5 of `addedTIPREDVAR`, not this first-occasion observed extra. `MANIFESTVAR` `θ` is measurement error, not this extra. `Ψ` is intercept variance and is not extra TI. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) remains unread (Unpaywall 2026-08-23T19:10Z: `is_oa: false`; title *Measurement Invariance, Factor Analysis and Factorial Invariance*). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread (Unpaywall 2026-08-23T19:10Z: `is_oa: false`; title *Randomization-Based Inference about Latent Variables from Complex Samples*). +- `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Table 3, p. 13 `T0TIPREDEFFECT`; p. 16; §7.2, pp. 20–21; Eq. 3, p. 5; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T18:20Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar first-occasion extra time-independent predictor variance `addedT0TIPREDVAR`. Table 3 names `T0TIPREDEFFECT` the effect of time-independent predictors on latents at `T0`. Page 16 prints extra summary matrices when `verbose = TRUE`. The 2017-era `summary.ctsemFit.R` forms `addedT0TIPREDVAR` as `T0TIPREDEFFECT %*% TIPREDVAR %*% t(T0TIPREDEFFECT)` immediately after `T0TIPREDEFFECTstd`. Section 7.2 names `addedTIPREDVAR` the stable between-subject variance accounted for by time-independent predictors at the process asymptote, `(B / a)² v`. The first-occasion analogue uses free `T0TIPREDEFFECT`, not `-B / a`. The scalar map is `t0_b² v`. Form `t0_b` first, then square, then multiply by `v`. A zero coefficient or zero predictor variance is exactly zero. `v < 0` fails closed. `T0` is an event-time occasion, so a non-event clock fails closed. Free `T0TIPREDEFFECT` does not require stable `a < 0`. `(B / a)² v` is `addedTIPREDVAR` and is not this first-occasion map. `t0_b · √v / √p_0` is `T0TIPREDEFFECTstd` and is not this variance. Free `T0VAR` `p_0` is the first-occasion state, not the extra TI variance. `TRAITVAR` is a zero-drift latent process, not `t0_b² v`. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) remains unread (Unpaywall 2026-08-23T18:20Z: `is_oa: false`; title *Measurement Invariance, Factor Analysis and Factorial Invariance*). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread (Unpaywall 2026-08-23T18:20Z: `is_oa: false`; title *Randomization-Based Inference about Latent Variables from Complex Samples*). +- `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Table 3, p. 13 `T0TIPREDEFFECTstd`; p. 16; footnote 4; Eq. 3, p. 5; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T17:20Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar standardised first-occasion time-independent predictor effect. Table 3 names `T0TIPREDEFFECT` the effect of time-independent predictors on latents at `T0`. Page 16 prints standardised matrices with the suffix `std` when appropriate. Footnote 4: standardisations use only the relevant variance, not the total. The affecting variance is predictor variance `TIPREDVAR` `v`. The affected variance is free first-occasion `T0VAR` `p_0`, not within-subject `asymDIFFUSION` `-q / (2 a)`, because Table 3 is the first occasion, not the process dynamics. Form strictly positive `p_0` first, then strictly positive `v`, then `t0_b · √v / √p_0`. Unstandardised `t0_b` is defined for a zero coefficient and for zero predictor variance; standardised `T0TIPREDEFFECT` is not. Zero `p_0` or zero `v` has no positive SD and fails closed. `T0` is an event-time occasion, so a non-event clock fails closed. Free `T0VAR` does not require stable `a < 0`. The continuous standardisation `B · √v / √(-q / (2 a))` uses `asymDIFFUSION` and is not this first-occasion map. The asymptotic standardisation `(-B / a) · √v / √p` is the total change, not this first-occasion coefficient. Section 7.1 warns that omitting trait variance confounds between- and within-person information. `t0_b · √v / √(trait + p_0 + added)` uses the total, not free `T0VAR`, and is not `T0TIPREDEFFECTstd` when `TRAITVAR` is nonzero. `TRAITVAR` is not the standardisation variance. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) remains unread (Unpaywall 2026-08-23T17:20Z: `is_oa: false`; title *Measurement Invariance, Factor Analysis and Factorial Invariance*). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread (Unpaywall 2026-08-23T17:20Z: `is_oa: false`; title *Randomization-Based Inference about Latent Variables from Complex Samples*). +- `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, §4.3, pp. 9–10; Eq. 3–5, pp. 4–5; Table 2, p. 12; p. 16; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-23T11:05Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar later-start later-occasion variance of §4.3 predetermined `T0VAR`. Section 4.3 treats the first time point as predetermined when no assumptions are made about the process prior to the initial time point. Free `T0VAR` `p_0` is then estimated. Section 4.3 notes that the process gradually transitions from the initial variances toward the stationary variances, and that the initial time point need not reflect the first measurement occasion (`startoffset`). Equation 3 writes `η(t) = exp(A Δt) η(t0) + … +` the stochastic integral. Equation 4 writes that the integral exhibits covariance `Q_Δt`. After a later start `u` the within-subject state variance is `e^{2 a u} p_0 + Q_u`. Evolving that later start over `s` is `e^{2 a s}(e^{2 a u} p_0 + Q_u) + Q_s`. Chapman–Kolmogorov writes `Q_{u+s} = e^{2 a s} Q_u + Q_s`, so the later-occasion map over `u + s` is the same composition. Trait variance and `addedTIPREDVAR` are time-invariant between-subject and do not enter `Q_s`. The later-start later-occasion composition is `trait + e^{2 a s}(e^{2 a u} p_0 + Q_u) + Q_s + (B / a)² v`. Form the later-start within-subject variance first, then evolve that state, then include the trait, then include the TI extra variance, then add. Setting `p_0 = −q / (2 a)` recovers the stationary later-occasion map. Stationary later-occasion variance uses `−q / (2 a)` in place of `p_0` and is not this map when `p_0` is free. Later-occasion variance at `u` omits `Q_s` and is not this map when `s > 0`. Later-start lagged covariance is `e^{a s}` of the later state and omits `Q_s`; it is not this map. Evolving the later total as if it were all state (`e^{2 a s}` of `trait + e^{2 a u} p_0 + Q_u + (B / a)² v` plus `Q_s`) is not this map. Later-occasion variance over the lag interval alone ignores `startoffset` and omits `e^{2 a s} Q_u`; it is not this map when `u > 0`. As `u → 0+` the composition approaches later-occasion variance over `s`. As `s → 0+` the composition approaches later-occasion variance at `u`. As `s → ∞` with stable `a < 0` the carried later state vanishes and `Q_s` approaches `−q / (2 a)`, so the composition approaches contemporaneous stationary `T0VAR`. A zero trait, a zero initial variance, a zero diffusion, and a zero TI contribution is exactly zero. A zero initial variance, a zero diffusion, and a zero TI contribution is exactly the trait. Trait-only variance does not require a stable drift. `a ≥ 0` cannot hold a finite TI extra variance when that contribution is nonzero and fails closed. Nonzero diffusion with `a ≥ 0` is a growing process and is kept. Both intervals must be event time and strictly positive. Equation 5 of that later-start later-occasion variance is `λ²(trait + e^{2 a s}(e^{2 a u} p_0 + Q_u) + Q_s + (B / a)² v) + θ + ψ`. `MANIFESTVAR` is not that later-start later-occasion observed variance. The later-start later-occasion latent variance is not the later-start later-occasion observed variance. Predetermined later observed variance at `u` omits `Q_s` and is not that observed variance when `s > 0`. Later-start lagged observed covariance omits `Q_s` and `θ` and is not that observed variance. Stationary later-occasion observed variance is not that observed variance when `p_0` is free. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) remains unread (Unpaywall 2026-08-23T11:05Z: `is_oa: false`; title *Measurement Invariance, Factor Analysis and Factorial Invariance*). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread (Unpaywall 2026-08-23T11:05Z: `is_oa: false`; title *Randomization-Based Inference about Latent Variables from Complex Samples*). +- `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, §4.3, pp. 9–10; Eq. 3–5, pp. 4–5; Table 2, p. 12; p. 16; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-23T10:27Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar later-start lagged covariance of §4.3 predetermined `T0VAR`. Section 4.3 treats the first time point as predetermined when no assumptions are made about the process prior to the initial time point. Free `T0VAR` `p_0` is then estimated. Section 4.3 notes that the process gradually transitions from the initial variances toward the stationary variances, and that the initial time point need not reflect the first measurement occasion (`startoffset`). Equation 3 writes `η(t) = exp(A Δt) η(t0) + … +` the stochastic integral. Equation 4 writes `cov(η_t, η_{t-1}) = A_Δt cov(η_{t-1})`. After a later start `u` the within-subject state variance is `e^{2 a u} p_0 + Q_u`. Lagging that later start over `s` is `e^{a s}(e^{2 a u} p_0 + Q_u)`. Trait variance and `addedTIPREDVAR` are time-invariant between-subject and do not decay with `e^{a s}`. The lagged composition is `trait + e^{a s}(e^{2 a u} p_0 + Q_u) + (B / a)² v`. Form the later-start within-subject variance first, then lag that state, then include the trait, then include the TI extra variance, then add. Setting `p_0 = −q / (2 a)` recovers the stationary lagged map. Stationary lagged covariance uses `−q / (2 a)` in place of `p_0` and is not this map when `p_0` is free. First-occasion lagged covariance `trait + e^{a s} p_0 + (B / a)² v` omits `e^{a s} Q_u` and is not this map when `u > 0`. Later-occasion variance includes `Q_u` without lagging that later state and is not this map. Evolving the later total as if it were all state (`e^{a s}` of `trait + e^{2 a u} p_0 + Q_u + (B / a)² v`) is not this map. As `u → 0+` the composition approaches first-occasion lagged covariance. As `s → 0+` the composition approaches later-occasion variance at `u`. As `s → ∞` with stable `a < 0` the state term vanishes. A zero trait, a zero initial variance, a zero diffusion, and a zero TI contribution is exactly zero. A zero initial variance, a zero diffusion, and a zero TI contribution is exactly the trait. Trait-only variance does not require a stable drift. `a ≥ 0` cannot hold a finite TI extra variance when that contribution is nonzero and fails closed. Nonzero diffusion with `a ≥ 0` is a growing process and is kept. Both intervals must be event time and strictly positive. Equation 5 of that later-start lagged covariance is `λ²(trait + e^{a s}(e^{2 a u} p_0 + Q_u) + (B / a)² v) + ψ`. Independent `ε_t` does not enter. `MANIFESTVAR` is not that later-start lagged observed covariance. The later-start lagged latent covariance is not the later-start lagged observed covariance. First-occasion lagged observed covariance omits `e^{a s} Q_u` and is not that observed covariance when `u > 0`. Predetermined later observed variance includes `Q_u` and `θ` and is not that later-start lagged observed covariance. Stationary lagged observed covariance is not that observed covariance when `p_0` is free. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) remains unread (Unpaywall 2026-08-23T10:27Z: `is_oa: false`; title *Measurement Invariance, Factor Analysis and Factorial Invariance*). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread (Unpaywall 2026-08-23T10:27Z: `is_oa: false`; title *Randomization-Based Inference about Latent Variables from Complex Samples*). +- `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, §4.3, pp. 9–10; Eq. 3–5, pp. 4–5; Table 2, p. 12; p. 16; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-23T09:04Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar lagged covariance of §4.3 predetermined `T0VAR`. Section 4.3 treats the first time point as predetermined when no assumptions are made about the process prior to the initial time point. Free `T0VAR` `p_0` is then estimated. Equation 3 writes `η(t) = exp(A Δt) η(t0) + …`. Equation 4 writes `cov(η_t, η_{t-1}) = A_Δt cov(η_{t-1})`. Trait variance and `addedTIPREDVAR` are time-invariant between-subject and do not decay with `e^{a Δt}`. The lagged composition is `trait + e^{a Δt} p_0 + (B / a)² v`. Form the lagged free first-occasion covariance first, then include the trait, then include the TI extra variance, then add. Setting `p_0 = −q / (2 a)` recovers the stationary lagged map. Stationary lagged covariance uses `−q / (2 a)` in place of `p_0` and is not this map when `p_0` is free. Evolving `trait + p_0 + (B / a)² v` as if it were all state (`e^{a Δt}` of that total) is not this map. Free `T0VAR` `p_0` is not this map. The later-occasion map `trait + e^{2 a Δt} p_0 + Q_Δt + (B / a)² v` includes `Q_Δt` and is not this map. As `Δt → ∞` with stable `a < 0` the state term vanishes. As `Δt → 0+` the composition approaches `trait + p_0 + (B / a)² v`. A zero-diffusion carry with `a ≥ 0` is `e^{a Δt} p_0` and is kept. Trait-only variance does not require a stable drift. The interval must be event time and strictly positive. Equation 5 of that lagged covariance is `λ²(trait + e^{a Δt} p_0 + (B / a)² v) + ψ`. Independent `ε_t` does not enter. `MANIFESTVAR` is not that lagged observed covariance. The predetermined lagged latent covariance is not the predetermined lagged observed covariance. Predetermined later observed variance includes `Q_Δt` and `θ` and is not that lagged observed covariance. Stationary lagged observed covariance is not that observed covariance when `p_0` is free. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) remains unread (Unpaywall 2026-08-23T09:04Z: `is_oa: false`; title *Measurement Invariance, Factor Analysis and Factorial Invariance*). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread (Unpaywall 2026-08-23T09:04Z: `is_oa: false`; title *Randomization-Based Inference about Latent Variables from Complex Samples*). +- `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, §4.3, pp. 9–10; Eq. 3–5, pp. 4–5; Table 2, p. 12; p. 16; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-23T05:12Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar later-occasion variance of §4.3 predetermined `T0VAR`. Section 4.3 treats the first time point as predetermined when no assumptions are made about the process prior to the initial time point. Free `T0VAR` `p_0` is then estimated. The process gradually transitions from the variances of the initial parameters toward those of the parameters when the model is stationary. Equation 3 writes `η(t) = exp(A Δt) η(t0) + … +` the stochastic integral. Equation 4 writes that the integral exhibits covariance `Q_Δt`. The law of total variance on the within-subject state is `e^{2 a Δt} p_0 + Q_Δt`. Trait variance and `addedTIPREDVAR` are time-invariant between-subject and do not enter that process-noise integral. The later-occasion composition is `trait + e^{2 a Δt} p_0 + Q_Δt + (B / a)² v`. Form the evolved free first-occasion variance first, then include the trait, then include the TI extra variance, then add. Setting `p_0 = −q / (2 a)` recovers the stationary later-occasion map. Stationary later-occasion variance uses `−q / (2 a)` in place of `p_0` and is not this map when `p_0` is free. Evolving `trait + p_0 + (B / a)² v` as if it were all state (`e^{2 a Δt}` of that total plus `Q_Δt`) is not this map. Free `T0VAR` `p_0` is not this map. As `Δt → ∞` with stable `a < 0` the carried `p_0` vanishes and `Q_Δt` approaches `−q / (2 a)`, so the composition approaches contemporaneous stationary `T0VAR`. As `Δt → 0+` the composition approaches `trait + p_0 + (B / a)² v`. Nonzero diffusion with `a ≥ 0` is a growing process and is kept. The interval must be event time and strictly positive. Equation 5 of that later-occasion variance is `λ²(trait + e^{2 a Δt} p_0 + Q_Δt + (B / a)² v) + θ + ψ`. `MANIFESTVAR` is not that later-occasion observed variance. The predetermined later-occasion latent variance is not the predetermined later-occasion observed variance. Stationary later-occasion observed variance is not that observed variance when `p_0` is free. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) remains unread (Unpaywall 2026-08-23T05:12Z: `is_oa: false`; title *Measurement Invariance, Factor Analysis and Factorial Invariance*). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread (Unpaywall 2026-08-23T05:12Z: `is_oa: false`; title *Randomization-Based Inference about Latent Variables from Complex Samples*). - Branch coverage JSON now unique-folds `files[].branches` True/False counts across instantiations. Nightly totals on #49 head `1e3e2eb` reported `event_time.rs` 505/506 while every unique site had both arms taken (253 sites × 2 instantiations). Summary-only reports without branch arrays still fail closed on totals. The 100% contract is unique production arms, matching the LCOV authored-line gate. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. - `psychometric_core` maps overflowing `expm1(a Δt)` / `expm1(2 a Δt)` in `recover_discrete_constant_predictor_effect` and `recover_discrete_process_noise` through the log-space rewrite without a redundant `if !argument.is_finite()` after overflow. Local crate llvm-cov on #49 head `559e7b399473ee90ba3234677dd9ef7f05f7fd2e` was 509/510: the same LLVM `exp`/`expm1` finite-argument proof as L768/L5040. Existing rewrite (`a = 800` / `a = 400`) and overflow (`a = 1e308`) tests remain the contract. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. - `psychometric_core` maps overflowing `e^{a Δt}` / `e^{a(t−u)}` through the log-space rewrite without redundant `if !argument.is_finite()` after `exp` overflow on lagged covariance, T0 TI/TD carry, and impulse carry. Nightly branch coverage on #49 head `7e669babcc54408dd8407bbac56be0f304fa99e5` was 1713/1714: LLVM counted `event_time.rs` L5040 True and treated the finite-argument overflow False as uncovered after proving `exp` of a finite argument is finite, which binary64 overflow falsifies. `fit_scalar_log_rate` now also skips a zero earlier residual and a negative lag while still recovering from a valid pair. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. diff --git a/CLAUDE.md b/CLAUDE.md index a7a04122d..18a067b68 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ Read and follow `AGENTS.md` before changing this repository. The repository-wide - Do not remove repeated report language with global stopword lists or use TF-IDF/BM25 as inferential weights. Model template, section, copied-text, style, modality, and corpus-background sources explicitly. - Do not treat raw topic proportions as ordinary Euclidean indicators. Use logistic-normal coordinates or valid log-ratio coordinates and propagate posterior uncertainty into ESEM/DSEM. - Do not treat metric/weak invariance as a latent-mean license. Strong (equal loading and intercept) or strict is required; `#84` `metric` licenses shared metric meaning only. Putnick and Bornstein (2016, PMC5145197 opened 2026-08-19T22:15Z) require scalar invariance before latent-mean comparison; residual invariance is not a prerequisite. Two-observation series have no residual degrees of freedom (`ordinary_least_squares_fit` returns residual variance `0`) and cap at strong/scalar; they still license means. This is two-group OLS, not MGCFA. Meredith (1993) names remain unread labels. -- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. The discrete latent mean is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12). `T0MEANS` is not `μ_t`. `CINT` is not that discrete increment. A zero drift is `κ Δt`. Underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`. The evolved observed mean is `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map). The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `μ_t` is not `E(y_t)`. The contemporaneous time-dependent predictor impulse is `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`). Form `μ_t` first, then add `m x`. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{A Δt} − I] B z` and is not Voelkle et al. (2012, Eq. 14). The §7.2 level-change form is not that impulse. The observed mean of that contemporaneous impulse is `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-impulse latent mean is not `E(y_t)`. The time-independent predictor increment is `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`). Form `B z` first, then the discrete intercept map. A zero drift is `B z Δt`. `TIPREDEFFECT` is `B`, not that discrete increment. `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle et al. (2012, Eq. 14). The observed mean of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The within-interval time-dependent impulse carry is `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation). Form `m x` first, then `e^{a(t−u)} m x`. A zero drift is `m x` with no dissipation. Underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept. `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). An impulse at `u = t` is the contemporaneous map. An impulse at `u ≤ t0` is already in `η(t0)`. The observed mean of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The carried latent mean is not `E(y_t)`. The first-occasion time-independent predictor shift is `t0_b z` (Driver et al., 2017, Table 3 `T0TIPREDEFFECT`; Eq. 3 first summand). Form `t0_b z` first, then `e^{a Δt} t0_b z`. Form `μ_t` first, then add that carry. A zero drift is `t0_b z`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. `e^{A Δt} t0_b z` is not `t0_b z`. `T0TIPREDEFFECT` is the coefficient, not the shift. The observed mean of that first-occasion carry is `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The evolved-plus-carry latent mean is not `E(y_t)`. The first-occasion time-dependent predictor shift is `t0_m x0` (Driver et al., 2017, Table 3 `T0TDPREDEFFECT`; Eq. 3 first summand; JSS PDF re-opened 2026-08-20T19:10Z). Form `t0_m x0` first, then `e^{a Δt} t0_m x0`. Form `μ_t` first, then add that carry. A zero drift is `t0_m x0`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`. `e^{A Δt} t0_m x0` is not `t0_m x0`. `T0TDPREDEFFECT` is the coefficient, not the shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The observed mean of that first-occasion TD carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean. The evolved-plus-carry latent mean is not `E(y_t)`. The lasting level-change `CINT` is `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z). Form `m x` first, then multiply by `−a`. Stable `a < 0` is required so `−κ / a = m x` is an equilibrium offset. `a ≥ 0` cannot hold a new process mean. `−a m x` is not the dissipating Dirac `m x`, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not this `CINT` setting. Equation 3 maps that intercept as `(1 − e^{a Δt}) m x` (JSS PDF re-opened 2026-08-20T19:50Z). Form the level-change `CINT` first, then the discrete intercept map. Underflow of `e^{a Δt}` to `+0` keeps `m x`. `(1 − e^{a Δt}) m x` is not `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`. The printed §7.2 lasting level change is an extra near-zero-drift latent process (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z). `T0MEANS`, `CINT`, `T0VAR`, `DIFFUSION`, and `TRAITVAR` of that process are fixed to 0; `TDPREDEFFECT` on it is fixed to 1; its `DRIFT` diagonal is very close to 0 (printed example `−0.000001`; precisely 0 causes computational problems); the original process is driven by the `DRIFT` coupling `a_{ηξ}`. After a unit identification impulse the scalar contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (`ε = a` is `a_{ηξ} x Δt e^{a Δt}`). Form `a_{ηξ} x` first. A zero coupling or zero predictor is exactly zero. `ε ≥ 0` fails closed. That contribution is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. The observed mean of that extra-process contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5 of that §7.2 contribution; JSS PDF re-opened 2026-08-21T06:12Z). The extra process has `LAMBDA` 0 and is not an observed indicator. Original indicators load on the original process after the `DRIFT` coupling. The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The contribution is not `E(y_t)`. The evolved-plus-contribution latent mean is not `E(y_t)`. `T0TDPREDEFFECT` on the extra process begins at `t = 0` and uses `Δt = t − t0` for both the original-process evolution and the extra drive. `TDPREDEFFECT` after `t0` uses `t − u` with `t0 < u < t` for the extra drive while `μ_t` still uses `Δt`. The observed mean of that after-t0 extra-process contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, Eq. 5 of that §7.2 after-t0 contribution; JSS PDF re-opened 2026-08-21T06:32Z). The first-occasion extra-process observed mean is not that observed mean when `u ≠ t0`. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is a Dirac on the original process and is not that `DRIFT` drive. An impulse at `u = t0` or `u = t` is not interior. The asymptotic time-independent predictor effect is `-B z / a` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z). Form `B z` first, then divide by `-a`. Stable `a < 0` is required. `a ≥ 0` cannot hold a finite process-mean change. `-B z / a` is not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. The asymptotic time-independent predictor variance is `(B / a)² v` (Driver et al., 2017, §7.2, pp. 20–21 `addedTIPREDVAR`). Form the unit asymptotic effect first, then square, then multiply by `v`. `(B / a)² v` is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`. The asymptotic continuous intercept is `-κ / a` (Driver et al., 2017, Table 2, p. 12 `asymCINT`; Eq. 3 as `Δt → ∞`; JSS PDF opened 2026-08-21T16:13Z). Form `κ` first, then divide by `-a`. Stable `a < 0` is required. `-κ / a` is not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`. The p. 16 stationary `T0MEANS` constraint is `-κ / a + −B z / a`. Form the intercept contribution first, then include the TI extra effect, then add. That constrained first-occasion mean is not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean. Equation 5 of that constrained mean is `τ + λ(−κ / a + −B z / a)` (Driver et al., 2017, §4.3, pp. 9–10; Eq. 5, p. 5; JSS PDF re-opened 2026-08-21T20:07Z). Form the stationary latent mean first, then `τ + λ` of that mean. `τ + λ μ_0` for free `T0MEANS` is not that composition. `τ + λ(−κ / a)` is not that composition when `B z ≠ 0`. `τ + λ μ_t` is not that composition. `MANIFESTMEANS` is not `E(y_0)`. The constrained latent mean is not `E(y_0)`. The p. 16 constrained first-occasion variance `trait + −q / (2 a) + (B / a)² v` is not free `T0VAR`, not `asymDIFFUSION` alone, not `TRAITVAR` alone, not `addedTIPREDVAR` alone, and not the finite-interval discrete latent variance. Eq. 5 of that constrained variance is `λ²(trait + −q / (2 a) + (B / a)² v) + θ + ψ` (JSS PDF re-opened 2026-08-22T03:20Z; form the stationary latent variance first, then `λ² p + θ + ψ`; `λ² p_0` is not that observed variance; `λ²(−q / (2 a)) + θ` is not that observed variance when `TRAITVAR` or `addedTIPREDVAR` is nonzero; `MANIFESTVAR` is not `Var(y_0)`; the constrained latent variance is not `Var(y_0)`). The lagged covariance of that constrained process is `trait + e^{a Δt}(−q / (2 a)) + (B / a)² v` (Driver et al., 2017, Eq. 3–4 of §4.3 / p. 16 `T0VAR`; JSS PDF re-opened 2026-08-22T19:13Z). Trait and `addedTIPREDVAR` do not decay with `e^{a Δt}`. Contemporaneous `T0VAR` is not that lagged map. Decaying the constrained total as if it were all state is not that lagged map. Equation 5 of that lagged covariance is `λ²(trait + e^{a Δt}(−q / (2 a)) + (B / a)² v) + ψ`. `Θ` does not enter. Contemporaneous `Var(y_0)` is not that lagged observed covariance. The lagged latent covariance is not that observed covariance. The later-occasion variance of that constrained process is `trait + e^{2 a Δt}(−q / (2 a)) + Q_Δt + (B / a)² v` (Driver et al., 2017, Eq. 3–4 of §4.3 / p. 16 `T0VAR`; JSS PDF re-opened 2026-08-22T23:12Z). Trait and `addedTIPREDVAR` do not enter `Q_Δt`. Under stationarity that composition equals contemporaneous `T0VAR`. Evolving the constrained total as if it were all state is not that later map. The lagged covariance omits `Q_Δt` and is not that later map. `Q_Δt` is not that later map. Equation 5 of that later-occasion variance is `λ²(trait + e^{2 a Δt}(−q / (2 a)) + Q_Δt + (B / a)² v) + θ + ψ`. The lagged observed covariance omits `Q_Δt` and `θ`. `MANIFESTVAR` is not `Var(y_t)`. The later-occasion latent variance is not `Var(y_t)`. Evolving from that stationary start with `CINT` and `TIPREDEFFECT` stays at the stationary mean. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. +- Do not use the difference quotient as a continuous-time rate. The scalar map is `a = ln(φ) / Δt` on event time. Discrete lags from unequal event intervals are not one coefficient; remap them through that log-rate. Binary64 `exp(a Δt) = 0` is not a discrete lag. A constant predictor's discrete effect is Voelkle et al. (2012, Eq. 12), evaluated as `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt` so a finite result is not lost when `z` overflows to `-∞` or when `a_yx Δt` overflows. When `expm1(z)` overflows at a finite `z`, rewrite in log space; a zero continuous effect is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed. The first-order product is the underflow limit of that equation, not the general constant-predictor discrete effect. A time-varying predictor whose sampling interval equals its constancy interval uses Voelkle et al. (2012, Eq. 14): `b* = a_yx Δt`. Unmatched intervals fail closed (Oud & Jansen, 2000, unread). Discrete process noise is Driver et al. (2017, Eq. 3): `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; a zero diffusion is exactly zero; an overflowing rewrite scale `0.5 q / a` fails closed; this is not a Kalman filter. `Q_Δt` is `cov(η_t | η_{t-1})`, not `Var(η_t)`. The lagged covariance is `exp(a Δt) p` and the unconditional variance is `exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS has no numbered §2.2). A zero diffusion whose `2 (a Δt)` overflows to `+∞` is not a finite `Var(η_t)`. The stationary within-subject variance is the `Δt → ∞` limit of Eq. 4: `-q / (2 a)` for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3). When `2 a` is finite, form `q / -(2 a)` so `q / a` overflow does not lose a finite result (`q = MAX`, `a = -0.75` → `MAX / 1.5`). When `2 a` overflows, form `(q / a) * -0.5`. Do not form `0.5 q` first (`q = from_bits(1)` underflows). `a ≥ 0` has no finite stationary variance. Finite-interval `Q_Δt` is not that limit. Trait-plus-state variance is `trait + state` and lagged covariance is `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9). Trait variance is not process noise and not `asymDIFFUSION`. Evolving the summed variance as if it were all state is not that map. This is not RI-CLPM. Observed-indicator variance is `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12). Lagged observed covariance is `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` does not enter. Observed-indicator mean is `τ + λ μ` (Driver et al., 2017, Eq. 5; Table 2, p. 12). `MANIFESTMEANS` is `τ`, not `E(y)`. `E(η)` is not `E(y)`. `CINT` is not `MANIFESTMEANS`. `T0MEANS` is not `E(y)`. The discrete latent mean is `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12). `T0MEANS` is not `μ_t`. `CINT` is not that discrete increment. A zero drift is `κ Δt`. Underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`. The evolved observed mean is `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map). The first-occasion map `τ + λ μ_0` is not `E(y_t)`. `μ_t` is not `E(y_t)`. The contemporaneous time-dependent predictor impulse is `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`). Form `μ_t` first, then add `m x`. `TDPREDEFFECT` is not `CINT`. `M x` is not `A^{-1}[e^{A Δt} − I] B z` and is not Voelkle et al. (2012, Eq. 14). The §7.2 level-change form is not that impulse. The observed mean of that contemporaneous impulse is `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-impulse latent mean is not `E(y_t)`. The time-independent predictor increment is `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`). Form `B z` first, then the discrete intercept map. A zero drift is `B z Δt`. `TIPREDEFFECT` is `B`, not that discrete increment. `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle et al. (2012, Eq. 14). The observed mean of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of the Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The within-interval time-dependent impulse carry is `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation). Form `m x` first, then `e^{a(t−u)} m x`. A zero drift is `m x` with no dissipation. Underflow of `e^{a(t−u)}` to `+0` is vanishing dissipation and is kept. `e^{A(t−u)} M x` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). An impulse at `u = t` is the contemporaneous map. An impulse at `u ≤ t0` is already in `η(t0)`. The observed mean of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean). The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The carried latent mean is not `E(y_t)`. The first-occasion time-independent predictor shift is `t0_b z` (Driver et al., 2017, Table 3 `T0TIPREDEFFECT`; Eq. 3 first summand). Form `t0_b z` first, then `e^{a Δt} t0_b z`. Form `μ_t` first, then add that carry. A zero drift is `t0_b z`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. `e^{A Δt} t0_b z` is not `t0_b z`. `T0TIPREDEFFECT` is the coefficient, not the shift. The observed mean of that first-occasion carry is `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The evolved-plus-carry latent mean is not `E(y_t)`. The first-occasion time-dependent predictor shift is `t0_m x0` (Driver et al., 2017, Table 3 `T0TDPREDEFFECT`; Eq. 3 first summand; JSS PDF re-opened 2026-08-20T19:10Z). Form `t0_m x0` first, then `e^{a Δt} t0_m x0`. Form `μ_t` first, then add that carry. A zero drift is `t0_m x0`. Underflow of `e^{a Δt}` to `+0` is a vanishing carry of the first-occasion shift and is kept. `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`. `e^{A Δt} t0_m x0` is not `t0_m x0`. `T0TDPREDEFFECT` is the coefficient, not the shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The observed mean of that first-occasion TD carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand TD composition; JSS PDF re-opened 2026-08-20T19:07Z). The evolved map `τ + λ μ_t` is not that observed mean. The process-increment map `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t0`. The first-occasion TI map `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean. The evolved-plus-carry latent mean is not `E(y_t)`. The lasting level-change `CINT` is `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T19:45Z). Form `m x` first, then multiply by `−a`. Stable `a < 0` is required so `−κ / a = m x` is an equilibrium offset. `a ≥ 0` cannot hold a new process mean. `−a m x` is not the dissipating Dirac `m x`, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not this `CINT` setting. Equation 3 maps that intercept as `(1 − e^{a Δt}) m x` (JSS PDF re-opened 2026-08-20T19:50Z). Form the level-change `CINT` first, then the discrete intercept map. Underflow of `e^{a Δt}` to `+0` keeps `m x`. `(1 − e^{a Δt}) m x` is not `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`. The printed §7.2 lasting level change is an extra near-zero-drift latent process (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z). `T0MEANS`, `CINT`, `T0VAR`, `DIFFUSION`, and `TRAITVAR` of that process are fixed to 0; `TDPREDEFFECT` on it is fixed to 1; its `DRIFT` diagonal is very close to 0 (printed example `−0.000001`; precisely 0 causes computational problems); the original process is driven by the `DRIFT` coupling `a_{ηξ}`. After a unit identification impulse the scalar contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (`ε = a` is `a_{ηξ} x Δt e^{a Δt}`). Form `a_{ηξ} x` first. A zero coupling or zero predictor is exactly zero. `ε ≥ 0` fails closed. That contribution is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. The observed mean of that extra-process contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5 of that §7.2 contribution; JSS PDF re-opened 2026-08-21T06:12Z). The extra process has `LAMBDA` 0 and is not an observed indicator. Original indicators load on the original process after the `DRIFT` coupling. The evolved map `τ + λ μ_t` is not that observed mean. The contemporaneous map `τ + λ(μ_t + m x)` is not that observed mean. The contribution is not `E(y_t)`. The evolved-plus-contribution latent mean is not `E(y_t)`. `T0TDPREDEFFECT` on the extra process begins at `t = 0` and uses `Δt = t − t0` for both the original-process evolution and the extra drive. `TDPREDEFFECT` after `t0` uses `t − u` with `t0 < u < t` for the extra drive while `μ_t` still uses `Δt`. The observed mean of that after-t0 extra-process contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, Eq. 5 of that §7.2 after-t0 contribution; JSS PDF re-opened 2026-08-21T06:32Z). The first-occasion extra-process observed mean is not that observed mean when `u ≠ t0`. The impulse-carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is a Dirac on the original process and is not that `DRIFT` drive. An impulse at `u = t0` or `u = t` is not interior. The asymptotic time-independent predictor effect is `-B z / a` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z). Form `B z` first, then divide by `-a`. Stable `a < 0` is required. `a ≥ 0` cannot hold a finite process-mean change. `-B z / a` is not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. The asymptotic time-independent predictor variance is `(B / a)² v` (Driver et al., 2017, §7.2, pp. 20–21 `addedTIPREDVAR`). Form the unit asymptotic effect first, then square, then multiply by `v`. `(B / a)² v` is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`. The asymptotic continuous intercept is `-κ / a` (Driver et al., 2017, Table 2, p. 12 `asymCINT`; Eq. 3 as `Δt → ∞`; JSS PDF opened 2026-08-21T16:13Z). Form `κ` first, then divide by `-a`. Stable `a < 0` is required. `-κ / a` is not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`. The p. 16 stationary `T0MEANS` constraint is `-κ / a + −B z / a`. Form the intercept contribution first, then include the TI extra effect, then add. That constrained first-occasion mean is not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean. Equation 5 of that constrained mean is `τ + λ(−κ / a + −B z / a)` (Driver et al., 2017, §4.3, pp. 9–10; Eq. 5, p. 5; JSS PDF re-opened 2026-08-21T20:07Z). Form the stationary latent mean first, then `τ + λ` of that mean. `τ + λ μ_0` for free `T0MEANS` is not that composition. `τ + λ(−κ / a)` is not that composition when `B z ≠ 0`. `τ + λ μ_t` is not that composition. `MANIFESTMEANS` is not `E(y_0)`. The constrained latent mean is not `E(y_0)`. The p. 16 constrained first-occasion variance `trait + −q / (2 a) + (B / a)² v` is not free `T0VAR`, not `asymDIFFUSION` alone, not `TRAITVAR` alone, not `addedTIPREDVAR` alone, and not the finite-interval discrete latent variance. Eq. 5 of that constrained variance is `λ²(trait + −q / (2 a) + (B / a)² v) + θ + ψ` (JSS PDF re-opened 2026-08-22T03:20Z; form the stationary latent variance first, then `λ² p + θ + ψ`; `λ² p_0` is not that observed variance; `λ²(−q / (2 a)) + θ` is not that observed variance when `TRAITVAR` or `addedTIPREDVAR` is nonzero; `MANIFESTVAR` is not `Var(y_0)`; the constrained latent variance is not `Var(y_0)`). The lagged covariance of that constrained process is `trait + e^{a Δt}(−q / (2 a)) + (B / a)² v` (Driver et al., 2017, Eq. 3–4 of §4.3 / p. 16 `T0VAR`; JSS PDF re-opened 2026-08-22T19:13Z). Trait and `addedTIPREDVAR` do not decay with `e^{a Δt}`. Contemporaneous `T0VAR` is not that lagged map. Decaying the constrained total as if it were all state is not that lagged map. Equation 5 of that lagged covariance is `λ²(trait + e^{a Δt}(−q / (2 a)) + (B / a)² v) + ψ`. `Θ` does not enter. Contemporaneous `Var(y_0)` is not that lagged observed covariance. The lagged latent covariance is not that observed covariance. The later-occasion variance of that constrained process is `trait + e^{2 a Δt}(−q / (2 a)) + Q_Δt + (B / a)² v` (Driver et al., 2017, Eq. 3–4 of §4.3 / p. 16 `T0VAR`; JSS PDF re-opened 2026-08-22T23:12Z). Trait and `addedTIPREDVAR` do not enter `Q_Δt`. Under stationarity that composition equals contemporaneous `T0VAR`. Evolving the constrained total as if it were all state is not that later map. The lagged covariance omits `Q_Δt` and is not that later map. `Q_Δt` is not that later map. Equation 5 of that later-occasion variance is `λ²(trait + e^{2 a Δt}(−q / (2 a)) + Q_Δt + (B / a)² v) + θ + ψ`. The lagged observed covariance omits `Q_Δt` and `θ`. `MANIFESTVAR` is not `Var(y_t)`. The later-occasion latent variance is not `Var(y_t)`. The later-occasion variance of §4.3 predetermined `T0VAR` is `trait + e^{2 a Δt} p_0 + Q_Δt + (B / a)² v` (Driver et al., 2017, Eq. 3–4 of §4.3 predetermined first occasion; JSS PDF re-opened 2026-08-23T05:12Z). Trait and `addedTIPREDVAR` do not enter `Q_Δt`. Free `T0VAR` `p_0` is not that later map. Setting `p_0 = −q / (2 a)` recovers the stationary later-occasion map. Stationary later variance uses `−q / (2 a)` in place of `p_0` and is not that later map when `p_0` is free. Evolving `trait + p_0 + (B / a)² v` as if it were all state is not that later map. As `Δt → ∞` with stable `a < 0` the composition approaches contemporaneous stationary `T0VAR`. As `Δt → 0+` the composition approaches `trait + p_0 + (B / a)² v`. Nonzero diffusion with `a ≥ 0` is a growing process and is kept. Equation 5 of that predetermined later-occasion variance is `λ²(trait + e^{2 a Δt} p_0 + Q_Δt + (B / a)² v) + θ + ψ`. `MANIFESTVAR` is not `Var(y_t)`. The predetermined later-occasion latent variance is not `Var(y_t)`. Stationary later observed variance is not that observed variance when `p_0` is free. The lagged covariance of §4.3 predetermined `T0VAR` is `trait + e^{a Δt} p_0 + (B / a)² v` (Driver et al., 2017, Eq. 3–4 of §4.3 predetermined first occasion; JSS PDF re-opened 2026-08-23T09:04Z). Trait and `addedTIPREDVAR` do not decay with `e^{a Δt}`. Free `T0VAR` `p_0` is not that lagged map. Setting `p_0 = −q / (2 a)` recovers the stationary lagged map. Stationary lagged covariance uses `−q / (2 a)` in place of `p_0` and is not that lagged map when `p_0` is free. Evolving `trait + p_0 + (B / a)² v` as if it were all state is not that lagged map. Later-occasion variance includes `Q_Δt` and is not that lagged map. As `Δt → ∞` with stable `a < 0` the state term vanishes. As `Δt → 0+` the composition approaches `trait + p_0 + (B / a)² v`. Equation 5 of that predetermined lagged covariance is `λ²(trait + e^{a Δt} p_0 + (B / a)² v) + ψ`. `MANIFESTVAR` does not enter. The predetermined lagged latent covariance is not that observed covariance. Predetermined later observed variance includes `Q_Δt` and `θ` and is not that lagged observed covariance. Stationary lagged observed covariance is not that observed covariance when `p_0` is free. The predetermined first-occasion variance of §4.3 predetermined `T0VAR` is `trait + p_0 + (B / a)² v`. Free `p_0` is not that map. Stationary first-occasion variance uses `−q / (2 a)` in place of `p_0` and is not that map when `p_0` is free. Lagged covariance decays the state and is not that map. Later-occasion variance includes `Q_Δt` and is not that map. Equation 5 of that predetermined first-occasion variance is `λ²(trait + p_0 + (B / a)² v) + θ + ψ`. `MANIFESTVAR` is not that first-occasion observed variance. The predetermined first-occasion latent variance is not that observed variance. Stationary first-occasion observed variance is not that observed variance when `p_0` is free. Predetermined later observed variance includes `Q_Δt` and is not that first-occasion observed variance. Later-start lagged covariance of predetermined `T0VAR` is `trait + e^{a s}(e^{2 a u} p_0 + Q_u) + (B / a)² v` (Driver et al., 2017, §4.3 `startoffset`; Eq. 4; JSS PDF re-opened 2026-08-23T10:27Z). First-occasion lagged omits `e^{a s} Q_u`. Later-occasion variance does not lag. Stationary lagged uses `−q / (2 a)`. Decaying the later total is not that map. Equation 5 of that later-start lagged covariance is `λ²` of it plus `ψ`. Independent `ε_t` does not enter. First-occasion lagged observed omits `e^{a s} Q_u`. Predetermined later observed variance includes `Q_u` and `θ` and is not that later-start lagged observed covariance. Later-start later-occasion variance of predetermined `T0VAR` is `trait + e^{2 a s}(e^{2 a u} p_0 + Q_u) + Q_s + (B / a)² v` (Driver et al., 2017, §4.3 `startoffset`; Eq. 3–4 Chapman–Kolmogorov `Q_{u+s} = e^{2 a s} Q_u + Q_s`; JSS PDF re-opened 2026-08-23T11:05Z). Later-occasion variance at `u` omits `Q_s`. Later-start lagged covariance omits `Q_s`. Stationary later uses `−q / (2 a)`. Evolving the later total as if it were all state is not that map. Ignoring `startoffset` omits `e^{2 a s} Q_u`. Equation 5 of that later-start later-occasion variance is `λ²` of it plus `θ + ψ`. `MANIFESTVAR` is not that observed variance. Page 16 `discreteDRIFTstd` is `e^{a Δt}` after strictly positive `asymDIFFUSION` `-q / (2 a)` (Driver et al., 2017, p. 16; footnote 4; §7.1; JSS PDF re-opened 2026-08-23T11:40Z). Unstandardised `e^{a Δt}` is defined for growing `a ≥ 0` and for zero diffusion and is not `discreteDRIFTstd`. The §7.1 trait-plus-state autocorrelation `(trait + e^{a Δt} p + added) / (trait + p + added)` uses `TRAITVAR` and is not `discreteDRIFTstd`. `TRAITVAR` is not the standardisation variance. Page 16 `discreteDIFFUSIONstd` is `Q_Δt / (−q / (2 a))` after strictly positive `asymDIFFUSION` `-q / (2 a)` (Driver et al., 2017, p. 16; Eq. 4; footnote 4; JSS PDF re-opened 2026-08-23T13:06Z). Unstandardised `Q_Δt` is defined for growing `a ≥ 0` and for zero diffusion and is not `discreteDIFFUSIONstd`. The continuous standardisation `−2 a` is not `discreteDIFFUSIONstd`. `Q_Δt / (trait + p + added)` uses `TRAITVAR` and is not `discreteDIFFUSIONstd`. `TRAITVAR` is not the standardisation variance. Page 16 `DIFFUSIONstd` is `q / (−q / (2 a)) = −2 a` after strictly positive `asymDIFFUSION` `-q / (2 a)` (Driver et al., 2017, p. 16; Eq. 4; footnote 4; JSS PDF re-opened 2026-08-23T13:20Z). Unstandardised `q` is defined for growing `a ≥ 0` and for zero diffusion and is not `DIFFUSIONstd`. The discrete standardisation `Q_Δt / (−q / (2 a))` depends on `Δt` and is not `DIFFUSIONstd`. `q / (trait + p + added)` uses `TRAITVAR` and is not `DIFFUSIONstd`. `TRAITVAR` is not the standardisation variance. Page 16 `DRIFTstd` is the continuous auto-effect after strictly positive `asymDIFFUSION` `-q / (2 a)` (Driver et al., 2017, p. 16; Eq. 1; footnote 4; JSS PDF re-opened 2026-08-23T13:28Z). Unstandardised `a` is defined for growing `a ≥ 0` and for zero diffusion and is not `DRIFTstd`. The discrete standardisation `e^{a Δt}` depends on the event interval and is not `DRIFTstd`. `a p / (trait + p + added)` uses `TRAITVAR` and is not `DRIFTstd`. `TRAITVAR` is not the standardisation variance. Page 16 `asymTIPREDEFFECTstd` is `(-B / a) · √v / √(-q / (2 a))` after strictly positive `asymDIFFUSION` `-q / (2 a)` and strictly positive predictor variance `v` (Driver et al., 2017, p. 16; §7.2; footnote 4; JSS PDF re-opened 2026-08-23T14:25Z). Unstandardised `-B / a` is defined for a zero coefficient and for zero predictor variance and is not `asymTIPREDEFFECTstd`. The finite-interval standardisation `A^{-1}[e^{A Δt} − I] B · √v / √p` depends on the event interval and is not `asymTIPREDEFFECTstd`. `(-B / a) · √v / √(trait + p + added)` uses `TRAITVAR` and is not `asymTIPREDEFFECTstd`. `TRAITVAR` is not the standardisation variance. Page 16 `TIPREDEFFECTstd` is `B · √v / √(-q / (2 a))` after strictly positive `asymDIFFUSION` `-q / (2 a)` and strictly positive predictor variance `v` (Driver et al., 2017, p. 16; §7.2; footnote 4; JSS PDF re-opened 2026-08-23T16:21Z). Unstandardised `B` is defined for a zero coefficient and for zero predictor variance and is not `TIPREDEFFECTstd`. The asymptotic standardisation `(-B / a) · √v / √p` is the total change and is not `TIPREDEFFECTstd`. The finite-interval standardisation `A^{-1}[e^{A Δt} − I] B · √v / √p` depends on the event interval and is not `TIPREDEFFECTstd`. `B · √v / √(trait + p + added)` uses `TRAITVAR` and is not `TIPREDEFFECTstd`. `TRAITVAR` is not the standardisation variance. Page 16 / Table 3 `T0TIPREDEFFECTstd` is `t0_b · √v / √p_0` after strictly positive free `T0VAR` `p_0` and strictly positive predictor variance `v` (Driver et al., 2017, Table 3, p. 13; p. 16; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T17:20Z). The affected variance is free first-occasion `T0VAR`, not `asymDIFFUSION`. Unstandardised `t0_b` is defined for a zero coefficient and for zero predictor variance and is not `T0TIPREDEFFECTstd`. `TIPREDEFFECTstd` `B · √v / √(-q / (2 a))` is the continuous coefficient and is not `T0TIPREDEFFECTstd`. `asymTIPREDEFFECTstd` `(-B / a) · √v / √p` is the total change and is not `T0TIPREDEFFECTstd`. `t0_b · √v / √(trait + p_0 + added)` uses `TRAITVAR` and is not `T0TIPREDEFFECTstd`. `TRAITVAR` is not the standardisation variance. 2017-era `addedT0TIPREDVAR` is `t0_b² v` after a first-occasion time-independent predictor (Driver et al., 2017, Table 3, p. 13; p. 16; §7.2; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T18:20Z). Form `t0_b` first, then square, then multiply by `v`. A zero coefficient or zero predictor variance is exactly zero. Free `T0TIPREDEFFECT` does not require `a < 0`. `(B / a)² v` is `addedTIPREDVAR` and is not this first-occasion map. `t0_b · √v / √p_0` is `T0TIPREDEFFECTstd` and is not this variance. Free `T0VAR` is not this extra TI variance. `TRAITVAR` is not this extra TI variance. Equation 5 of 2017-era `addedT0TIPREDVAR` is `λ² t0_b² v` (Driver et al., 2017, Eq. 5, p. 5; Table 3, p. 13; Table 2, p. 12; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T19:10Z). Form `t0_b² v` first, then `(λ extra) λ` with `θ = 0`. A zero loading or zero extra is exactly zero. `t0_b² v` is the latent extra, not the observed extra. `λ² p_0 + θ` is first-occasion observed variance, not this extra. `λ² (B / a)² v` is Eq. 5 of `addedTIPREDVAR`, not this first-occasion observed extra. `MANIFESTVAR` `θ` is not this extra. Equation 5 of §7.2 `addedTIPREDVAR` is `λ² (B / a)² v` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; §7.2, pp. 20–21; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T19:23Z). Form `(B / a)² v` first, then `(λ extra) λ` with `θ = 0`. A zero loading or zero extra is exactly zero. Lasting asymptotic extra requires `a < 0`. `(B / a)² v` is the latent extra, not the observed extra. `λ² t0_b² v` is first-occasion extra observed TI variance, not this extra. `λ² p + θ` is stationary observed variance, not this extra. `MANIFESTVAR` `θ` is not this extra. Page 16 `TDPREDEFFECTstd` is `m · √v / √(-q / (2 a))` after strictly positive `asymDIFFUSION` and strictly positive time-dependent predictor variance. Unstandardised `M` is not `TDPREDEFFECTstd`. `TIPREDEFFECTstd` is not `TDPREDEFFECTstd` even when `M = B`. intercept-style `A^{-1}[e^{A Δt} − I] M · √v / √p` is not `TDPREDEFFECTstd`. `m · √v / √(trait + p + added)` uses `TRAITVAR` and is not `TDPREDEFFECTstd`. Table 3 / p. 16 `T0TDPREDEFFECTstd` is `t0_m · √v / √p_0` after strictly positive free `T0VAR` and strictly positive TD predictor variance. Unstandardised `t0_m` is not `T0TDPREDEFFECTstd`. `TDPREDEFFECTstd` uses `asymDIFFUSION` and is not `T0TDPREDEFFECTstd`. `T0TIPREDEFFECTstd` is not `T0TDPREDEFFECTstd` even when `t0_m = t0_b`. `t0_m · √v / √(trait + p_0 + added)` uses `TRAITVAR` and is not `T0TDPREDEFFECTstd`. Free `T0VAR` does not require `a < 0`. Page 16 `T0VARstd` is `p_0 / p_0 = 1` after strictly positive free `T0VAR` (`solve(sqrt(diag(T0VAR))) %&% T0VAR`; OpenMx `%&%` is `t(A) %*% B %*% A`; the default ridge is 0). Unstandardised `T0VAR` is not `T0VARstd`. `T0TDPREDEFFECTstd` is not `T0VARstd`. `addedT0TIPREDVAR` is not `T0VARstd`. Page 16 `TRAITVARstd` is `trait / trait = 1` after strictly positive `TRAITVAR` (`solve(sqrt(diag(TRAITVAR))) %&% TRAITVAR`; OpenMx `%&%` is `t(A) %*% B %*% A`; no ridge addend). Unstandardised `TRAITVAR` is not `TRAITVARstd`. `T0VARstd` is not `TRAITVARstd` even when both equal 1. `addedT0TIPREDVAR` is not `TRAITVARstd`. Page 16 `MANIFESTTRAITVARstd` is `ψ / ψ = 1` after strictly positive `MANIFESTTRAITVAR` (`solve(sqrt(diag(MANIFESTTRAITVAR))) %&% MANIFESTTRAITVAR`; OpenMx `%&%` is `t(A) %*% B %*% A`; 2017-era source adds ridging; default ridge is 0). Unstandardised `MANIFESTTRAITVAR` is not `MANIFESTTRAITVARstd`. `TRAITVARstd` is not `MANIFESTTRAITVARstd` even when both equal 1. `MANIFESTVAR` is not `MANIFESTTRAITVARstd`. Page 16 `MANIFESTVARstd` is `θ / θ = 1` after strictly positive `MANIFESTVAR` (`solve(sqrt(diag(MANIFESTVAR))) %&% MANIFESTVAR`; OpenMx `%&%` is `t(A) %*% B %*% A`; 2017-era source adds ridging; default ridge is 0; 2017-era `dimnames` assignment to `latentNames` is a source bug). Unstandardised `MANIFESTVAR` is not `MANIFESTVARstd`. `MANIFESTTRAITVARstd` is not `MANIFESTVARstd` even when both equal 1. Equation 5 `Var(y)` is not `MANIFESTVARstd`. Page 16 `TIPREDVARstd` is `v / v = 1` after strictly positive `TIPREDVAR` (`solve(sqrt(diag(TIPREDVAR))) %&% TIPREDVAR`; OpenMx `%&%` is `t(A) %*% B %*% A`; 2017-era source adds ridging; default ridge is 0; `dimnames` are `TIpredNames`). Unstandardised `TIPREDVAR` is not `TIPREDVARstd`. `MANIFESTVARstd` is not `TIPREDVARstd` even when both equal 1. Section 7.2 `addedTIPREDVAR` is not `TIPREDVARstd`. Page 16 `asymDIFFUSIONstd` is `p / p = 1` after strictly positive `asymDIFFUSION` (`solve(sqrt(diag(asymDIFFUSION))) %&% asymDIFFUSION`; OpenMx `%&%` is `t(A) %*% B %*% A`; 2017-era source adds ridging; default ridge is 0; `dimnames` are `latentNames`). Unstandardised `asymDIFFUSION` is not `asymDIFFUSIONstd`. `TIPREDVARstd` is not `asymDIFFUSIONstd` even when both equal 1. `DIFFUSIONstd` `−2 a` is not `asymDIFFUSIONstd`. Page 16 `discreteCINTstd` is `A^{-1}[e^{A Δt} − I] κ / √p` after strictly positive `asymDIFFUSION`. Unstandardised `discreteCINT` is not `discreteCINTstd`. `κ / √p` is not `discreteCINTstd`. `(-κ / a) / √p` is not `discreteCINTstd`. `asymCINTstd` is `(-κ / a) / √p` after strictly positive `asymDIFFUSION`. Unstandardised `asymCINT` is not `asymCINTstd`. `κ / √p` is not `asymCINTstd`. `discreteCINTstd` is not `asymCINTstd`. `T0MEANSstd` is `μ_0 / √p_0` after strictly positive free `T0VAR`. Unstandardised `T0MEANS` is not `T0MEANSstd`. `T0VARstd` is not `T0MEANSstd`. `μ_0 / √asymDIFFUSION` is not `T0MEANSstd`. Evolving from that stationary start with `CINT` and `TIPREDEFFECT` stays at the stationary mean. Equation 1 is the latent SDE, not the measurement model. Form `(λ p) λ` then add `θ`, then add `ψ`. `MANIFESTVAR` is `Θ`, not `Var(y)`. `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`. `TRAITVAR` is latent and scaled by `λ²`. `Var(η)` is not `Var(y)`. - Separate cluster means before within-unit lag. CWC plus an event-time lag is not DSEM. Subtracting the person-specific mean from a raw autoregressive series does not isolate the lagged within-person effect (Curran & Bauer, 2011, pp. 607–608); already-centered residuals with irregular event intervals use the exact scalar map. - Do not treat the CWC cluster-mean coefficient as the between-cluster effect. It is the contextual effect `between − within` (Enders & Tofighi, 2007, Table 2, pp. 124–127). - Never use future-available evidence in historical model fits. diff --git a/crates/psychometric_core/src/error.rs b/crates/psychometric_core/src/error.rs index a1ddfe252..5f723b429 100644 --- a/crates/psychometric_core/src/error.rs +++ b/crates/psychometric_core/src/error.rs @@ -504,6 +504,601 @@ pub enum PsychometricError { /// later-occasion stationary observed variance. Lagged covariance /// omits `Q_Δt` and `θ`. StationaryLaggedObservedCovarianceIsNotStationaryLaterObservedVariance, + /// Driver §4.3 predetermined later-occasion variance was treated as + /// later-occasion stationary `T0VAR`. Free `T0VAR` is not + /// `−q / (2 a)`. + PredeterminedLaterLatentVarianceIsNotStationaryLaterLatentVariance, + /// Driver §4.3 predetermined later-occasion variance was treated as + /// the free discrete evolution of `trait + p_0 + (B / a)² v`. + /// Trait variance and `addedTIPREDVAR` do not enter `Q_Δt`. + PredeterminedLaterLatentVarianceIsNotDiscreteVariance, + /// Driver §4.3 predetermined later-occasion variance was treated as + /// free first-occasion `T0VAR`. `e^{2 a Δt} p_0 + Q_Δt` is not `p_0`. + PredeterminedLaterLatentVarianceIsNotInitialLatentVariance, + /// Driver §4.3 predetermined later-occasion variance was treated as + /// predetermined later-occasion observed variance. Equation 5 maps + /// `Var(y_t) = λ²` of that variance plus `θ + ψ`. + PredeterminedLaterLatentVarianceIsNotObservedVariance, + /// Driver Eq. 5 measurement error was treated as predetermined + /// later-occasion observed variance. `θ` is not + /// `λ²(trait + e^{2 a Δt} p_0 + Q_Δt + (B / a)² v) + θ + ψ`. + MeasurementErrorIsNotPredeterminedLaterObservedVariance, + /// Driver Eq. 5 of later-occasion §4.3 stationary `T0VAR` was treated + /// as predetermined later-occasion observed variance. Stationary + /// later variance uses `−q / (2 a)`, not free `p_0`. + StationaryLaterObservedVarianceIsNotPredeterminedLaterObservedVariance, + /// Driver §4.3 predetermined lagged covariance was treated as lagged + /// stationary `T0VAR`. Free `T0VAR` is not `−q / (2 a)`. + PredeterminedLaggedLatentCovarianceIsNotStationaryLaggedCovariance, + /// Driver §4.3 predetermined lagged covariance was treated as + /// predetermined later-occasion variance. Lagged covariance omits + /// `Q_Δt` and uses `e^{a Δt} p_0`, not `e^{2 a Δt} p_0`. + PredeterminedLaggedLatentCovarianceIsNotLaterLatentVariance, + /// Driver §4.3 predetermined lagged covariance was treated as the + /// decayed total `e^{a Δt}(trait + p_0 + (B / a)² v)`. Trait + /// variance and `addedTIPREDVAR` do not decay. + PredeterminedLaggedLatentCovarianceIsNotDecayedTotal, + /// Driver §4.3 predetermined lagged covariance was treated as free + /// first-occasion `T0VAR`. `e^{a Δt} p_0` is not `p_0`. + PredeterminedLaggedLatentCovarianceIsNotInitialLatentVariance, + /// Driver §4.3 predetermined lagged covariance was treated as + /// predetermined lagged observed covariance. Equation 5 maps + /// `cov(y_t, y_{t-1}) = λ²` of that covariance plus `ψ`. + PredeterminedLaggedLatentCovarianceIsNotObservedCovariance, + /// Driver Eq. 5 measurement error was treated as predetermined + /// lagged observed covariance. Independent `ε_t` does not enter + /// `cov(y_t, y_{t-1})`. + MeasurementErrorIsNotPredeterminedLaggedObservedCovariance, + /// Driver Eq. 5 of predetermined later-occasion `T0VAR` was treated + /// as predetermined lagged observed covariance. Later variance + /// includes `Q_Δt` and `θ`. + PredeterminedLaterObservedVarianceIsNotPredeterminedLaggedObservedCovariance, + /// Driver Eq. 5 of lagged §4.3 stationary `T0VAR` was treated as + /// predetermined lagged observed covariance. Stationary lagged + /// covariance uses `−q / (2 a)`, not free `p_0`. + StationaryLaggedObservedCovarianceIsNotPredeterminedLaggedObservedCovariance, + /// Driver §4.3 predetermined first-occasion variance was treated as + /// stationary first-occasion `T0VAR`. Free `T0VAR` is not + /// `−q / (2 a)`. + PredeterminedInitialLatentVarianceIsNotStationaryInitialLatentVariance, + /// Driver §4.3 predetermined first-occasion variance was treated as + /// free first-occasion `T0VAR`. `trait + p_0 + (B / a)² v` is not + /// `p_0`. + PredeterminedInitialLatentVarianceIsNotInitialLatentVariance, + /// Driver §4.3 predetermined first-occasion variance was treated as + /// predetermined lagged covariance. First-occasion variance does + /// not decay the state. + PredeterminedInitialLatentVarianceIsNotLaggedLatentCovariance, + /// Driver §4.3 predetermined first-occasion variance was treated as + /// predetermined later-occasion variance. First-occasion variance + /// omits `Q_Δt`. + PredeterminedInitialLatentVarianceIsNotLaterLatentVariance, + /// Driver §4.3 predetermined first-occasion variance was treated as + /// predetermined first-occasion observed variance. Equation 5 maps + /// `Var(y_0) = λ²` of that variance plus `θ + ψ`. + PredeterminedInitialLatentVarianceIsNotObservedVariance, + /// Driver Eq. 5 measurement error was treated as predetermined + /// first-occasion observed variance. `θ` is not + /// `λ²(trait + p_0 + (B / a)² v) + θ + ψ`. + MeasurementErrorIsNotPredeterminedInitialObservedVariance, + /// Driver Eq. 5 of §4.3 stationary `T0VAR` was treated as + /// predetermined first-occasion observed variance. Stationary + /// first-occasion variance uses `−q / (2 a)`, not free `p_0`. + StationaryInitialObservedVarianceIsNotPredeterminedInitialObservedVariance, + /// Driver Eq. 5 of predetermined later-occasion `T0VAR` was treated + /// as predetermined first-occasion observed variance. Later + /// variance includes `Q_Δt`. + PredeterminedLaterObservedVarianceIsNotPredeterminedInitialObservedVariance, + /// Driver §4.3 later-start lagged covariance of predetermined + /// `T0VAR` was treated as first-occasion lagged covariance. + /// Later-start lag includes `e^{a s} Q_u`. + PredeterminedLaterLaggedLatentCovarianceIsNotPredeterminedLaggedCovariance, + /// Driver §4.3 later-start lagged covariance of predetermined + /// `T0VAR` was treated as later-occasion variance. Lagged + /// covariance is `e^{a s}` of the later state, not that variance. + PredeterminedLaterLaggedLatentCovarianceIsNotLaterLatentVariance, + /// Driver §4.3 later-start lagged covariance of predetermined + /// `T0VAR` was treated as lagged stationary `T0VAR`. Free `T0VAR` + /// is not `−q / (2 a)`. + PredeterminedLaterLaggedLatentCovarianceIsNotStationaryLaggedCovariance, + /// Driver §4.3 later-start lagged covariance of predetermined + /// `T0VAR` was treated as the decayed later total. Trait variance + /// and `addedTIPREDVAR` do not decay. + PredeterminedLaterLaggedLatentCovarianceIsNotDecayedLaterTotal, + /// Driver §4.3 later-start lagged covariance of predetermined + /// `T0VAR` was treated as later-start lagged observed covariance. + /// Equation 5 maps `cov(y, y_{lag}) = λ²` of that covariance plus + /// `ψ`. + PredeterminedLaterLaggedLatentCovarianceIsNotObservedCovariance, + /// Driver Eq. 5 measurement error was treated as later-start lagged + /// observed covariance of predetermined `T0VAR`. Independent `ε_t` + /// does not enter. + MeasurementErrorIsNotPredeterminedLaterLaggedObservedCovariance, + /// Driver Eq. 5 of first-occasion lagged predetermined `T0VAR` was + /// treated as later-start lagged observed covariance. First-occasion + /// lag omits `e^{a s} Q_u`. + PredeterminedLaggedObservedCovarianceIsNotPredeterminedLaterLaggedObservedCovariance, + /// Driver Eq. 5 of lagged §4.3 stationary `T0VAR` was treated as + /// later-start lagged observed covariance of predetermined `T0VAR`. + /// Stationary lagged covariance uses `−q / (2 a)`, not free `p_0`. + StationaryLaggedObservedCovarianceIsNotPredeterminedLaterLaggedObservedCovariance, + /// Driver Eq. 5 of predetermined later-occasion `T0VAR` was treated + /// as later-start lagged observed covariance. Later variance + /// includes `Q_Δt` and `θ`. + PredeterminedLaterObservedVarianceIsNotPredeterminedLaterLaggedObservedCovariance, + /// Driver §4.3 later-start later-occasion variance of predetermined + /// `T0VAR` was treated as later-occasion variance at the later + /// start. Later-start later-occasion variance adds `Q_s`. + PredeterminedLaterStartLaterLatentVarianceIsNotLaterLatentVariance, + /// Driver §4.3 later-start later-occasion variance of predetermined + /// `T0VAR` was treated as later-start lagged covariance. Lagged + /// covariance is `e^{a s}` of the later state and omits `Q_s`. + PredeterminedLaterStartLaterLatentVarianceIsNotLaterLaggedCovariance, + /// Driver §4.3 later-start later-occasion variance of predetermined + /// `T0VAR` was treated as later-occasion stationary `T0VAR`. Free + /// `T0VAR` is not `−q / (2 a)`. + PredeterminedLaterStartLaterLatentVarianceIsNotStationaryLaterLatentVariance, + /// Driver §4.3 later-start later-occasion variance of predetermined + /// `T0VAR` was treated as the evolved later total. Trait variance + /// and `addedTIPREDVAR` do not enter `Q_s`. + PredeterminedLaterStartLaterLatentVarianceIsNotDecayedLaterTotal, + /// Driver §4.3 later-start later-occasion variance of predetermined + /// `T0VAR` was treated as later-occasion variance over the lag + /// interval alone. Ignoring `startoffset` omits `e^{2 a s} Q_u`. + PredeterminedLaterStartLaterLatentVarianceIsNotLagIntervalLaterLatentVariance, + /// Driver §4.3 later-start later-occasion variance of predetermined + /// `T0VAR` was treated as later-start later-occasion observed + /// variance. Equation 5 maps `Var(y) = λ²` of that variance plus + /// `θ + ψ`. + PredeterminedLaterStartLaterLatentVarianceIsNotObservedVariance, + /// Driver Eq. 5 measurement error was treated as later-start + /// later-occasion observed variance of predetermined `T0VAR`. `θ` + /// is not `λ²(trait + e^{2 a s}(e^{2 a u} p_0 + Q_u) + Q_s + (B / a)² v) + θ + ψ`. + MeasurementErrorIsNotPredeterminedLaterStartLaterObservedVariance, + /// Driver Eq. 5 of predetermined later-occasion `T0VAR` was treated + /// as later-start later-occasion observed variance. Later-occasion + /// variance at `u` omits `Q_s`. + PredeterminedLaterObservedVarianceIsNotPredeterminedLaterStartLaterObservedVariance, + /// Driver Eq. 5 of later-start lagged predetermined `T0VAR` was + /// treated as later-start later-occasion observed variance. Lagged + /// covariance omits `Q_s` and `θ`. + PredeterminedLaterLaggedObservedCovarianceIsNotPredeterminedLaterStartLaterObservedVariance, + /// Driver Eq. 5 of later-occasion §4.3 stationary `T0VAR` was + /// treated as later-start later-occasion observed variance of + /// predetermined `T0VAR`. Stationary later variance uses + /// `−q / (2 a)`, not free `p_0`. + StationaryLaterObservedVarianceIsNotPredeterminedLaterStartLaterObservedVariance, + /// Driver p. 16 `discreteDRIFTstd` was requested with a non-positive + /// within-subject variance. Footnote 4 standardises `DRIFT` using + /// only strictly positive `asymDIFFUSION`. + StandardisedDiscreteDriftRequiresPositiveWithinSubjectVariance, + /// Driver p. 16 unstandardised `discreteDRIFT` `e^{a Δt}` was treated + /// as `discreteDRIFTstd`. Unstandardised `e^{a Δt}` is defined for + /// growing or zero-diffusion processes; standardised `DRIFT` is not. + UnstandardisedDiscreteDriftIsNotStandardisedDiscreteDrift, + /// Driver §7.1 trait-plus-state autocorrelation was treated as + /// p. 16 `discreteDRIFTstd`. Footnote 4 uses only `asymDIFFUSION`, + /// not `TRAITVAR`. + TraitPlusStateAutocorrelationIsNotStandardisedDiscreteDrift, + /// Driver §4.3 / §7.1 trait variance was treated as the p. 16 + /// footnote 4 standardisation variance. `TRAITVAR` is not + /// `asymDIFFUSION`. + TraitVarianceIsNotStandardisationVariance, + /// Driver p. 16 `discreteDIFFUSIONstd` was requested with a + /// non-positive within-subject variance. Footnote 4 standardises + /// process noise using only strictly positive `asymDIFFUSION`. + StandardisedDiscreteDiffusionRequiresPositiveWithinSubjectVariance, + /// Driver p. 16 unstandardised `discreteDIFFUSION` `Q_Δt` was + /// treated as `discreteDIFFUSIONstd`. Unstandardised `Q_Δt` is + /// defined for growing or zero-diffusion processes; standardised + /// `DIFFUSION` is not. + UnstandardisedDiscreteDiffusionIsNotStandardisedDiscreteDiffusion, + /// Driver continuous `DIFFUSION` standardisation `q / (−q / (2 a))` + /// was treated as p. 16 `discreteDIFFUSIONstd`. `−2 a` is not + /// `Q_Δt / (−q / (2 a))`. + StandardisedContinuousDiffusionIsNotStandardisedDiscreteDiffusion, + /// Driver §7.1 trait-contaminated process noise + /// `Q_Δt / (trait + p + added)` was treated as p. 16 + /// `discreteDIFFUSIONstd`. Footnote 4 uses only `asymDIFFUSION`, + /// not `TRAITVAR`. + TraitContaminatedProcessNoiseIsNotStandardisedDiscreteDiffusion, + /// Driver p. 16 `DIFFUSIONstd` was requested with a non-positive + /// within-subject variance. Footnote 4 standardises continuous + /// process noise using only strictly positive `asymDIFFUSION`. + StandardisedContinuousDiffusionRequiresPositiveWithinSubjectVariance, + /// Driver p. 16 unstandardised `DIFFUSION` `q` was treated as + /// `DIFFUSIONstd`. Unstandardised `q` is defined for growing or + /// zero-diffusion processes; standardised `DIFFUSION` is not. + UnstandardisedContinuousDiffusionIsNotStandardisedContinuousDiffusion, + /// Driver p. 16 `discreteDIFFUSIONstd` `Q_Δt / (−q / (2 a))` was + /// treated as `DIFFUSIONstd`. `1 − exp(2 a Δt)` is not `−2 a`. + StandardisedDiscreteDiffusionIsNotStandardisedContinuousDiffusion, + /// Driver §7.1 trait-contaminated continuous diffusion + /// `q / (trait + p + added)` was treated as p. 16 `DIFFUSIONstd`. + /// Footnote 4 uses only `asymDIFFUSION`, not `TRAITVAR`. + TraitContaminatedContinuousDiffusionIsNotStandardisedContinuousDiffusion, + /// Driver p. 16 `DRIFTstd` was requested with a non-positive + /// within-subject variance. Footnote 4 standardises `DRIFT` using + /// only strictly positive `asymDIFFUSION`. + StandardisedContinuousDriftRequiresPositiveWithinSubjectVariance, + /// Driver p. 16 unstandardised `DRIFT` `a` was treated as + /// `DRIFTstd`. Unstandardised `a` is defined for growing or + /// zero-diffusion processes; standardised `DRIFT` is not. + UnstandardisedContinuousDriftIsNotStandardisedContinuousDrift, + /// Driver p. 16 `discreteDRIFTstd` `e^{a Δt}` was treated as + /// `DRIFTstd`. The discrete auto-effect is not the continuous + /// log-rate. + StandardisedDiscreteDriftIsNotStandardisedContinuousDrift, + /// Driver §7.1 trait-contaminated continuous drift + /// `a p / (trait + p + added)` was treated as p. 16 `DRIFTstd`. + /// Footnote 4 uses only `asymDIFFUSION`, not `TRAITVAR`. + TraitContaminatedContinuousDriftIsNotStandardisedContinuousDrift, + /// Driver p. 16 `asymTIPREDEFFECTstd` was requested with a + /// non-positive within-subject variance. Footnote 4 standardises + /// the affected process using only strictly positive + /// `asymDIFFUSION`. + StandardisedAsymptoticTimeIndependentEffectRequiresPositiveWithinSubjectVariance, + /// Driver p. 16 `asymTIPREDEFFECTstd` was requested with a + /// non-positive predictor variance. Footnote 4 standardises the + /// affecting predictor using only strictly positive `TIPREDVAR`. + StandardisedAsymptoticTimeIndependentEffectRequiresPositivePredictorVariance, + /// Driver §7.2 unstandardised `asymTIPREDEFFECT` `-B / a` was + /// treated as p. 16 `asymTIPREDEFFECTstd`. Unstandardised + /// `-B / a` is defined for a zero coefficient or zero predictor + /// variance; standardised `asymTIPREDEFFECT` is not. + UnstandardisedAsymptoticTimeIndependentEffectIsNotStandardisedAsymptoticTimeIndependentEffect, + /// Driver finite-interval standardised `TIPREDEFFECT` + /// `A^{-1}[e^{A Δt} − I] B · √v / √p` was treated as p. 16 + /// `asymTIPREDEFFECTstd`. The discrete increment depends on the + /// event interval; the asymptotic map does not. + StandardisedDiscreteTimeIndependentEffectIsNotStandardisedAsymptoticTimeIndependentEffect, + /// Driver §7.1 trait-contaminated asymptotic TI effect + /// `(-B / a) · √v / √(trait + p + added)` was treated as p. 16 + /// `asymTIPREDEFFECTstd`. Footnote 4 uses only `asymDIFFUSION`, + /// not `TRAITVAR`. + TraitContaminatedAsymptoticTimeIndependentEffectIsNotStandardisedAsymptoticTimeIndependentEffect, + /// Driver p. 16 `TIPREDEFFECTstd` was requested with a + /// non-positive within-subject variance. Footnote 4 standardises + /// the affected process using only strictly positive + /// `asymDIFFUSION`. + StandardisedContinuousTimeIndependentEffectRequiresPositiveWithinSubjectVariance, + /// Driver p. 16 `TIPREDEFFECTstd` was requested with a + /// non-positive predictor variance. Footnote 4 standardises the + /// affecting predictor using only strictly positive `TIPREDVAR`. + StandardisedContinuousTimeIndependentEffectRequiresPositivePredictorVariance, + /// Driver Table 2 unstandardised `TIPREDEFFECT` `B` was treated + /// as p. 16 `TIPREDEFFECTstd`. Unstandardised `B` is defined for + /// a zero coefficient or zero predictor variance; standardised + /// `TIPREDEFFECT` is not. + UnstandardisedContinuousTimeIndependentEffectIsNotStandardisedContinuousTimeIndependentEffect, + /// Driver p. 16 `asymTIPREDEFFECTstd` + /// `(-B / a) · √v / √(-q / (2 a))` was treated as p. 16 + /// `TIPREDEFFECTstd`. The asymptotic map is the total change, not + /// the continuous coefficient. + StandardisedAsymptoticTimeIndependentEffectIsNotStandardisedContinuousTimeIndependentEffect, + /// Driver finite-interval standardised `TIPREDEFFECT` + /// `A^{-1}[e^{A Δt} − I] B · √v / √p` was treated as p. 16 + /// `TIPREDEFFECTstd`. The discrete increment depends on the + /// event interval; the continuous coefficient does not. + StandardisedDiscreteTimeIndependentEffectIsNotStandardisedContinuousTimeIndependentEffect, + /// Driver §7.1 trait-contaminated continuous TI effect + /// `B · √v / √(trait + p + added)` was treated as p. 16 + /// `TIPREDEFFECTstd`. Footnote 4 uses only `asymDIFFUSION`, + /// not `TRAITVAR`. + TraitContaminatedContinuousTimeIndependentEffectIsNotStandardisedContinuousTimeIndependentEffect, + /// Driver Table 3 / p. 16 `T0TIPREDEFFECTstd` was requested with + /// a non-positive free first-occasion variance. Footnote 4 + /// standardises the affected first-occasion latent using only + /// strictly positive free `T0VAR`. + StandardisedInitialTimeIndependentEffectRequiresPositiveInitialLatentVariance, + /// Driver Table 3 / p. 16 `T0TIPREDEFFECTstd` was requested with + /// a non-positive predictor variance. Footnote 4 standardises the + /// affecting predictor using only strictly positive `TIPREDVAR`. + StandardisedInitialTimeIndependentEffectRequiresPositivePredictorVariance, + /// Driver Table 3 unstandardised `T0TIPREDEFFECT` `t0_b` was + /// treated as p. 16 `T0TIPREDEFFECTstd`. Unstandardised `t0_b` + /// is defined for a zero coefficient or zero predictor variance; + /// standardised `T0TIPREDEFFECT` is not. + UnstandardisedInitialTimeIndependentEffectIsNotStandardisedInitialTimeIndependentEffect, + /// Driver p. 16 `TIPREDEFFECTstd` + /// `B · √v / √(-q / (2 a))` was treated as Table 3 / p. 16 + /// `T0TIPREDEFFECTstd`. The continuous map uses `asymDIFFUSION`; + /// the first-occasion map uses free `T0VAR`. + StandardisedContinuousTimeIndependentEffectIsNotStandardisedInitialTimeIndependentEffect, + /// Driver p. 16 `asymTIPREDEFFECTstd` + /// `(-B / a) · √v / √(-q / (2 a))` was treated as Table 3 / + /// p. 16 `T0TIPREDEFFECTstd`. The asymptotic map is the total + /// change, not the first-occasion coefficient. + StandardisedAsymptoticTimeIndependentEffectIsNotStandardisedInitialTimeIndependentEffect, + /// Driver §7.1 trait-contaminated first-occasion TI effect + /// `t0_b · √v / √(trait + p_0 + added)` was treated as Table 3 / + /// p. 16 `T0TIPREDEFFECTstd`. Footnote 4 uses only free `T0VAR`, + /// not `TRAITVAR`. + TraitContaminatedInitialTimeIndependentEffectIsNotStandardisedInitialTimeIndependentEffect, + /// Driver 2017-era `addedT0TIPREDVAR` `t0_b² v` was treated as + /// §7.2 `addedTIPREDVAR` `(B / a)² v`. The first-occasion extra + /// variance uses free `T0TIPREDEFFECT`, not `-B / a`. + InitialTimeIndependentVarianceIsNotAsymptoticTimeIndependentVariance, + /// Driver 2017-era `addedT0TIPREDVAR` `t0_b² v` was treated as + /// Table 3 / p. 16 `T0TIPREDEFFECTstd`. The extra first-occasion + /// variance is not the standardised coefficient. + InitialTimeIndependentVarianceIsNotStandardisedInitialTimeIndependentEffect, + /// Driver 2017-era `addedT0TIPREDVAR` `t0_b² v` was treated as + /// free first-occasion `T0VAR`. `p_0` is the first-occasion + /// state, not the extra TI variance. + InitialTimeIndependentVarianceIsNotInitialLatentVariance, + /// Driver 2017-era `addedT0TIPREDVAR` `t0_b² v` was treated as + /// `TRAITVAR`. Section 4.3 `TRAITVAR` is a zero-drift latent + /// process, not first-occasion TI extra variance. + InitialTimeIndependentVarianceIsNotTraitVariance, + /// Driver Eq. 5 of 2017-era `addedT0TIPREDVAR` `λ² t0_b² v` was + /// treated as the latent extra `t0_b² v`. The observed extra is + /// not the latent extra. + InitialTimeIndependentObservedVarianceIsNotInitialTimeIndependentVariance, + /// Driver Eq. 5 of 2017-era `addedT0TIPREDVAR` `λ² t0_b² v` was + /// treated as first-occasion observed variance `λ² p_0 + θ`. + /// The extra is not the full first-occasion `Var(y_0)`. + InitialTimeIndependentObservedVarianceIsNotInitialObservedVariance, + /// Driver Eq. 5 of 2017-era `addedT0TIPREDVAR` `λ² t0_b² v` was + /// treated as Eq. 5 of `addedTIPREDVAR` `λ² (B / a)² v`. The + /// first-occasion observed extra uses free `T0TIPREDEFFECT`, + /// not `-B / a`. + InitialTimeIndependentObservedVarianceIsNotAsymptoticTimeIndependentObservedVariance, + /// Driver Eq. 5 of 2017-era `addedT0TIPREDVAR` `λ² t0_b² v` was + /// treated as `MANIFESTVAR` `θ`. Measurement error is not extra + /// observed TI variance. + InitialTimeIndependentObservedVarianceIsNotMeasurementError, + /// Driver Eq. 5 of §7.2 `addedTIPREDVAR` `λ² (B / a)² v` was + /// treated as the latent extra `(B / a)² v`. The observed extra + /// is not the latent extra. + AsymptoticTimeIndependentObservedVarianceIsNotAsymptoticTimeIndependentVariance, + /// Driver Eq. 5 of §7.2 `addedTIPREDVAR` `λ² (B / a)² v` was + /// treated as Eq. 5 of `addedT0TIPREDVAR` `λ² t0_b² v`. The + /// asymptotic observed extra uses `-B / a`, not free + /// `T0TIPREDEFFECT`. + AsymptoticTimeIndependentObservedVarianceIsNotInitialTimeIndependentObservedVariance, + /// Driver Eq. 5 of §7.2 `addedTIPREDVAR` `λ² (B / a)² v` was + /// treated as stationary observed variance `λ² p + θ`. The extra + /// is not the full stationary `Var(y)`. + AsymptoticTimeIndependentObservedVarianceIsNotStationaryObservedVariance, + /// Driver Eq. 5 of §7.2 `addedTIPREDVAR` `λ² (B / a)² v` was + /// treated as `MANIFESTVAR` `θ`. Measurement error is not extra + /// observed TI variance. + AsymptoticTimeIndependentObservedVarianceIsNotMeasurementError, + /// Driver p. 16 `TDPREDEFFECTstd` was requested with a + /// non-positive within-subject variance. Footnote 4 standardises + /// the affected process using only strictly positive + /// `asymDIFFUSION`. + StandardisedContinuousTimeDependentEffectRequiresPositiveWithinSubjectVariance, + /// Driver p. 16 `TDPREDEFFECTstd` was requested with a + /// non-positive predictor variance. Footnote 4 standardises the + /// affecting predictor using only strictly positive TD predictor + /// variance. + StandardisedContinuousTimeDependentEffectRequiresPositivePredictorVariance, + /// Driver Table 2 unstandardised `TDPREDEFFECT` `M` was treated + /// as p. 16 `TDPREDEFFECTstd`. Unstandardised `M` is defined for + /// a zero coefficient or zero predictor variance; standardised + /// `TDPREDEFFECT` is not. + UnstandardisedContinuousTimeDependentEffectIsNotStandardisedContinuousTimeDependentEffect, + /// Driver p. 16 `TIPREDEFFECTstd` `B · √v / √(-q / (2 a))` was + /// treated as p. 16 `TDPREDEFFECTstd`. Table 2 names `M` + /// `TDPREDEFFECT` and `B` `TIPREDEFFECT`. Equal numbers when + /// `M = B` and the predictor variances match are still distinct + /// named quantities. + StandardisedContinuousTimeIndependentEffectIsNotStandardisedContinuousTimeDependentEffect, + /// Driver finite-interval standardised `TDPREDEFFECT` + /// `A^{-1}[e^{A Δt} − I] M · √v / √p` was treated as p. 16 + /// `TDPREDEFFECTstd`. That intercept-style discrete map depends + /// on the event interval; the continuous Dirac coefficient does + /// not. + StandardisedDiscreteTimeDependentEffectIsNotStandardisedContinuousTimeDependentEffect, + /// Driver §7.1 trait-contaminated continuous TD effect + /// `m · √v / √(trait + p + added)` was treated as p. 16 + /// `TDPREDEFFECTstd`. Footnote 4 uses only `asymDIFFUSION`, + /// not `TRAITVAR`. + TraitContaminatedContinuousTimeDependentEffectIsNotStandardisedContinuousTimeDependentEffect, + /// Driver Table 3 / p. 16 `T0TDPREDEFFECTstd` was requested with + /// a non-positive free first-occasion variance. Footnote 4 + /// standardises the affected first-occasion latent using only + /// strictly positive free `T0VAR`. + StandardisedInitialTimeDependentEffectRequiresPositiveInitialLatentVariance, + /// Driver Table 3 / p. 16 `T0TDPREDEFFECTstd` was requested with + /// a non-positive predictor variance. Footnote 4 standardises the + /// affecting predictor using only strictly positive TD predictor + /// variance. + StandardisedInitialTimeDependentEffectRequiresPositivePredictorVariance, + /// Driver Table 3 unstandardised `T0TDPREDEFFECT` `t0_m` was + /// treated as p. 16 `T0TDPREDEFFECTstd`. Unstandardised `t0_m` + /// is defined for a zero coefficient or zero predictor variance; + /// standardised `T0TDPREDEFFECT` is not. + UnstandardisedInitialTimeDependentEffectIsNotStandardisedInitialTimeDependentEffect, + /// Driver p. 16 `TDPREDEFFECTstd` + /// `m · √v / √(-q / (2 a))` was treated as Table 3 / p. 16 + /// `T0TDPREDEFFECTstd`. The continuous map uses `asymDIFFUSION`; + /// the first-occasion map uses free `T0VAR`. + StandardisedContinuousTimeDependentEffectIsNotStandardisedInitialTimeDependentEffect, + /// Driver Table 3 / p. 16 `T0TIPREDEFFECTstd` + /// `t0_b · √v / √p_0` was treated as Table 3 / p. 16 + /// `T0TDPREDEFFECTstd`. Table 3 names different matrices. Equal + /// numbers when `t0_m = t0_b` are still distinct named + /// quantities. + StandardisedInitialTimeIndependentEffectIsNotStandardisedInitialTimeDependentEffect, + /// Driver §7.1 trait-contaminated first-occasion TD effect + /// `t0_m · √v / √(trait + p_0 + added)` was treated as Table 3 + /// / p. 16 `T0TDPREDEFFECTstd`. Footnote 4 uses only free + /// `T0VAR`, not `TRAITVAR`. + TraitContaminatedInitialTimeDependentEffectIsNotStandardisedInitialTimeDependentEffect, + /// Driver p. 16 `T0VARstd` was requested with a non-positive + /// free first-occasion variance. The 2017-era correlation form + /// requires strictly positive free `T0VAR`. + StandardisedInitialLatentVarianceRequiresPositiveInitialLatentVariance, + /// Driver Table 2 unstandardised `T0VAR` `p_0` was treated as + /// p. 16 `T0VARstd`. Unstandardised `p_0` is defined for a zero + /// first-occasion variance; standardised `T0VAR` is not. + UnstandardisedInitialLatentVarianceIsNotStandardisedInitialLatentVariance, + /// Driver Table 3 / p. 16 `T0TDPREDEFFECTstd` + /// `t0_m · √v / √p_0` was treated as p. 16 `T0VARstd`. The + /// effect map depends on `p_0`; the correlation form of free + /// `T0VAR` does not. + StandardisedInitialTimeDependentEffectIsNotStandardisedInitialLatentVariance, + /// Driver 2017-era `addedT0TIPREDVAR` `t0_b² v` was treated as + /// p. 16 `T0VARstd`. Extra TI variance is not the correlation + /// form of free `T0VAR`. + InitialTimeIndependentVarianceIsNotStandardisedInitialLatentVariance, + /// Driver p. 16 `TRAITVARstd` was requested with a non-positive + /// trait variance. The 2017-era correlation form requires + /// strictly positive `TRAITVAR` and is not formed when + /// `TRAITVAR` is zero. + StandardisedTraitVarianceRequiresPositiveTraitVariance, + /// Driver Table 2 unstandardised `TRAITVAR` was treated as + /// p. 16 `TRAITVARstd`. Unstandardised trait variance is + /// defined for a zero trait; standardised `TRAITVAR` is not. + UnstandardisedTraitVarianceIsNotStandardisedTraitVariance, + /// Driver p. 16 `T0VARstd` was treated as p. 16 `TRAITVARstd`. + /// Equal numbers when both correlations equal 1 are still + /// distinct named quantities. + StandardisedInitialLatentVarianceIsNotStandardisedTraitVariance, + /// Driver 2017-era `addedT0TIPREDVAR` `t0_b² v` was treated as + /// p. 16 `TRAITVARstd`. Extra first-occasion TI variance is not + /// the correlation form of between-subject `TRAITVAR`. + InitialTimeIndependentVarianceIsNotStandardisedTraitVariance, + /// Driver p. 16 `MANIFESTTRAITVARstd` was requested with a + /// non-positive manifest-trait variance. The 2017-era + /// correlation form requires strictly positive + /// `MANIFESTTRAITVAR` and is not formed when + /// `MANIFESTTRAITVAR` is zero. + StandardisedManifestTraitVarianceRequiresPositiveManifestTraitVariance, + /// Driver Table 2 unstandardised `MANIFESTTRAITVAR` `Ψ_τ` was + /// treated as p. 16 `MANIFESTTRAITVARstd`. Unstandardised + /// manifest-trait variance is defined for a zero trait; + /// standardised `MANIFESTTRAITVAR` is not. + UnstandardisedManifestTraitVarianceIsNotStandardisedManifestTraitVariance, + /// Driver p. 16 `TRAITVARstd` was treated as p. 16 + /// `MANIFESTTRAITVARstd`. Equal numbers when both correlations + /// equal 1 are still distinct named quantities. `TRAITVAR` is + /// process-level; `MANIFESTTRAITVAR` is indicator-level. + StandardisedTraitVarianceIsNotStandardisedManifestTraitVariance, + /// Driver Table 2 `MANIFESTVAR` `Θ` was treated as p. 16 + /// `MANIFESTTRAITVARstd`. Measurement error is not the + /// correlation form of indicator-level trait variance. + MeasurementErrorIsNotStandardisedManifestTraitVariance, + /// Driver p. 16 `MANIFESTVARstd` was requested with a + /// non-positive measurement-error variance. The 2017-era + /// correlation form requires strictly positive `MANIFESTVAR`. + /// Zero `θ` makes `solve(sqrt(0))` fail in that source. + StandardisedManifestVarianceRequiresPositiveManifestVariance, + /// Driver Table 2 unstandardised `MANIFESTVAR` `Θ` was treated + /// as p. 16 `MANIFESTVARstd`. Unstandardised measurement error + /// is defined for a zero residual; standardised `MANIFESTVAR` + /// is not. + UnstandardisedManifestVarianceIsNotStandardisedManifestVariance, + /// Driver p. 16 `MANIFESTTRAITVARstd` was treated as p. 16 + /// `MANIFESTVARstd`. Equal numbers when both correlations equal + /// 1 are still distinct named quantities. `MANIFESTTRAITVAR` is + /// indicator-level trait variance; `MANIFESTVAR` is + /// contemporaneous measurement error. + StandardisedManifestTraitVarianceIsNotStandardisedManifestVariance, + /// Driver Eq. 5 observed-indicator variance was treated as p. 16 + /// `MANIFESTVARstd`. `λ² Var(η) + θ` is `Var(y)`, not the + /// correlation form of `Θ`. + ObservedVarianceIsNotStandardisedManifestVariance, + /// Driver p. 16 `TIPREDVARstd` was requested with a non-positive + /// time-independent predictor variance. The 2017-era correlation + /// form requires strictly positive `TIPREDVAR`. Zero `v` makes + /// `solve(sqrt(0))` fail in that source. + StandardisedTimeIndependentPredictorVarianceRequiresPositivePredictorVariance, + /// Driver Table 2 unstandardised `TIPREDVAR` was treated as p. 16 + /// `TIPREDVARstd`. Unstandardised predictor variance is defined + /// for a zero predictor; standardised `TIPREDVAR` is not. + UnstandardisedTimeIndependentPredictorVarianceIsNotStandardisedTimeIndependentPredictorVariance, + /// Driver p. 16 `MANIFESTVARstd` was treated as p. 16 + /// `TIPREDVARstd`. Equal numbers when both correlations equal 1 + /// are still distinct named quantities. `MANIFESTVAR` is + /// contemporaneous measurement error; `TIPREDVAR` is + /// time-independent predictor variance. + StandardisedManifestVarianceIsNotStandardisedTimeIndependentPredictorVariance, + /// Driver §7.2 `addedTIPREDVAR` was treated as p. 16 + /// `TIPREDVARstd`. `(B / a)² v` is extra process variance, not + /// the correlation form of the predictor covariance. + AsymptoticTimeIndependentPredictorVarianceIsNotStandardisedTimeIndependentPredictorVariance, + /// Driver p. 16 `asymDIFFUSIONstd` was requested with a + /// non-positive within-subject variance. The 2017-era + /// correlation form requires strictly positive `asymDIFFUSION`. + /// Zero `q` makes `solve(sqrt(0))` fail in that source. + StandardisedAsymptoticDiffusionRequiresPositiveWithinSubjectVariance, + /// Driver p. 16 unstandardised `asymDIFFUSION` `p` was treated + /// as `asymDIFFUSIONstd`. Unstandardised within-subject variance + /// is defined for a zero process; standardised `asymDIFFUSION` + /// is not. + UnstandardisedAsymptoticDiffusionIsNotStandardisedAsymptoticDiffusion, + /// Driver p. 16 `TIPREDVARstd` was treated as p. 16 + /// `asymDIFFUSIONstd`. Equal numbers when both correlations + /// equal 1 are still distinct named quantities. `TIPREDVAR` is + /// predictor covariance; `asymDIFFUSION` is within-subject + /// process variance. + StandardisedTimeIndependentPredictorVarianceIsNotStandardisedAsymptoticDiffusion, + /// Driver p. 16 `DIFFUSIONstd` `q / p = −2 a` was treated as + /// `asymDIFFUSIONstd`. Footnote 4 `DIFFUSIONstd` is the + /// continuous-diffusion ratio, not the correlation of + /// `asymDIFFUSION`. + StandardisedContinuousDiffusionIsNotStandardisedAsymptoticDiffusion, + /// Driver p. 16 `discreteCINTstd` was requested with a + /// non-positive within-subject variance. Footnote 4 + /// standardisation of the 2017-era `discreteCINT` vector + /// requires strictly positive `asymDIFFUSION`. + StandardisedDiscreteContinuousInterceptRequiresPositiveWithinSubjectVariance, + /// Driver Eq. 3 unstandardised `discreteCINT` + /// `A^{-1}[e^{A Δt} − I] κ` was treated as `discreteCINTstd`. + /// Unstandardised discrete intercept is defined for growing + /// `a ≥ 0` and for zero diffusion; standardised `discreteCINT` + /// is not. + UnstandardisedDiscreteContinuousInterceptIsNotStandardisedDiscreteContinuousIntercept, + /// Driver p. 16 `CINTstd` analog `κ / √p` was treated as + /// `discreteCINTstd`. The continuous intercept standardisation + /// does not depend on the event interval. + StandardisedContinuousInterceptIsNotStandardisedDiscreteContinuousIntercept, + /// Driver Table 2 `asymCINT` `/ √p` was treated as + /// `discreteCINTstd`. `(-κ / a) / √p` is the standardised + /// total intercept change, not the finite-interval map. + AsymptoticStandardisedContinuousInterceptIsNotStandardisedDiscreteContinuousIntercept, + /// Driver p. 16 `asymCINTstd` was requested with a + /// non-positive within-subject variance. Footnote 4 + /// standardisation of the 2017-era `asymCINT` vector + /// requires strictly positive `asymDIFFUSION`. + StandardisedAsymptoticContinuousInterceptRequiresPositiveWithinSubjectVariance, + /// Driver Table 2 unstandardised `asymCINT` `-κ / a` was + /// treated as `asymCINTstd`. Unstandardised asymptotic + /// intercept is defined for a zero process; standardised + /// `asymCINT` is not. + UnstandardisedAsymptoticContinuousInterceptIsNotStandardisedAsymptoticContinuousIntercept, + /// Driver p. 16 `CINTstd` analog `κ / √p` was treated as + /// `asymCINTstd`. The continuous intercept standardisation + /// is not the standardised total intercept change. + StandardisedContinuousInterceptIsNotStandardisedAsymptoticContinuousIntercept, + /// Driver p. 16 `discreteCINTstd` was treated as + /// `asymCINTstd`. A finite event interval is not the + /// `Δt → ∞` intercept change. + StandardisedDiscreteContinuousInterceptIsNotStandardisedAsymptoticContinuousIntercept, + /// Driver p. 16 `T0MEANSstd` was requested with a + /// non-positive first-occasion variance. Footnote 4 + /// standardisation of the 2017-era `T0MEANS` vector + /// requires strictly positive free `T0VAR`. + StandardisedInitialLatentMeanRequiresPositiveInitialLatentVariance, + /// Driver Table 2 unstandardised `T0MEANS` `μ_0` was treated + /// as `T0MEANSstd`. Unstandardised first-occasion mean is + /// defined for a zero first-occasion variance; standardised + /// `T0MEANS` is not. + UnstandardisedInitialLatentMeanIsNotStandardisedInitialLatentMean, + /// Driver p. 16 `T0VARstd` was treated as p. 16 `T0MEANSstd`. + /// Equal numbers when `μ_0 = √p_0` are still distinct named + /// quantities. `T0VARstd` is the correlation form of free + /// `T0VAR`; `T0MEANSstd` is the first-occasion mean. + StandardisedInitialLatentVarianceIsNotStandardisedInitialLatentMean, + /// Driver p. 16 `T0MEANS` `/ √asymDIFFUSION` was treated as + /// `T0MEANSstd`. Footnote 4 standardises the first-occasion + /// mean using free `T0VAR`, not process-dynamics + /// `asymDIFFUSION`. + WithinSubjectScaledInitialLatentMeanIsNotStandardisedInitialLatentMean, } impl fmt::Display for PsychometricError { @@ -899,6 +1494,408 @@ impl fmt::Display for PsychometricError { Self::StationaryLaggedObservedCovarianceIsNotStationaryLaterObservedVariance => { "stationary lagged observed covariance is not the stationary later-occasion observed variance" } + Self::PredeterminedLaterLatentVarianceIsNotStationaryLaterLatentVariance => { + "predetermined later-occasion latent variance is not the stationary later-occasion latent variance" + } + Self::PredeterminedLaterLatentVarianceIsNotDiscreteVariance => { + "predetermined later-occasion latent variance is not the free discrete latent variance" + } + Self::PredeterminedLaterLatentVarianceIsNotInitialLatentVariance => { + "predetermined later-occasion latent variance is not the free first-occasion latent variance" + } + Self::PredeterminedLaterLatentVarianceIsNotObservedVariance => { + "predetermined later-occasion latent variance is not the predetermined later-occasion observed variance" + } + Self::MeasurementErrorIsNotPredeterminedLaterObservedVariance => { + "measurement-error variance is not the predetermined later-occasion observed variance" + } + Self::StationaryLaterObservedVarianceIsNotPredeterminedLaterObservedVariance => { + "stationary later-occasion observed variance is not the predetermined later-occasion observed variance" + } + Self::PredeterminedLaggedLatentCovarianceIsNotStationaryLaggedCovariance => { + "predetermined lagged latent covariance is not the stationary lagged latent covariance" + } + Self::PredeterminedLaggedLatentCovarianceIsNotLaterLatentVariance => { + "predetermined lagged latent covariance is not the predetermined later-occasion latent variance" + } + Self::PredeterminedLaggedLatentCovarianceIsNotDecayedTotal => { + "predetermined lagged latent covariance is not the decayed predetermined total" + } + Self::PredeterminedLaggedLatentCovarianceIsNotInitialLatentVariance => { + "predetermined lagged latent covariance is not the free first-occasion latent variance" + } + Self::PredeterminedLaggedLatentCovarianceIsNotObservedCovariance => { + "predetermined lagged latent covariance is not the predetermined lagged observed covariance" + } + Self::MeasurementErrorIsNotPredeterminedLaggedObservedCovariance => { + "measurement-error variance is not the predetermined lagged observed covariance" + } + Self::PredeterminedLaterObservedVarianceIsNotPredeterminedLaggedObservedCovariance => { + "predetermined later-occasion observed variance is not the predetermined lagged observed covariance" + } + Self::StationaryLaggedObservedCovarianceIsNotPredeterminedLaggedObservedCovariance => { + "stationary lagged observed covariance is not the predetermined lagged observed covariance" + } + Self::PredeterminedInitialLatentVarianceIsNotStationaryInitialLatentVariance => { + "predetermined first-occasion latent variance is not the stationary first-occasion latent variance" + } + Self::PredeterminedInitialLatentVarianceIsNotInitialLatentVariance => { + "predetermined first-occasion latent variance is not the free first-occasion latent variance" + } + Self::PredeterminedInitialLatentVarianceIsNotLaggedLatentCovariance => { + "predetermined first-occasion latent variance is not the predetermined lagged latent covariance" + } + Self::PredeterminedInitialLatentVarianceIsNotLaterLatentVariance => { + "predetermined first-occasion latent variance is not the predetermined later-occasion latent variance" + } + Self::PredeterminedInitialLatentVarianceIsNotObservedVariance => { + "predetermined first-occasion latent variance is not the predetermined first-occasion observed variance" + } + Self::MeasurementErrorIsNotPredeterminedInitialObservedVariance => { + "measurement-error variance is not the predetermined first-occasion observed variance" + } + Self::StationaryInitialObservedVarianceIsNotPredeterminedInitialObservedVariance => { + "stationary first-occasion observed variance is not the predetermined first-occasion observed variance" + } + Self::PredeterminedLaterObservedVarianceIsNotPredeterminedInitialObservedVariance => { + "predetermined later-occasion observed variance is not the predetermined first-occasion observed variance" + } + Self::PredeterminedLaterLaggedLatentCovarianceIsNotPredeterminedLaggedCovariance => { + "predetermined later-start lagged latent covariance is not the predetermined first-occasion lagged latent covariance" + } + Self::PredeterminedLaterLaggedLatentCovarianceIsNotLaterLatentVariance => { + "predetermined later-start lagged latent covariance is not the predetermined later-occasion latent variance" + } + Self::PredeterminedLaterLaggedLatentCovarianceIsNotStationaryLaggedCovariance => { + "predetermined later-start lagged latent covariance is not the stationary lagged latent covariance" + } + Self::PredeterminedLaterLaggedLatentCovarianceIsNotDecayedLaterTotal => { + "predetermined later-start lagged latent covariance is not the decayed later-occasion total" + } + Self::PredeterminedLaterLaggedLatentCovarianceIsNotObservedCovariance => { + "predetermined later-start lagged latent covariance is not the predetermined later-start lagged observed covariance" + } + Self::MeasurementErrorIsNotPredeterminedLaterLaggedObservedCovariance => { + "measurement-error variance is not the predetermined later-start lagged observed covariance" + } + Self::PredeterminedLaggedObservedCovarianceIsNotPredeterminedLaterLaggedObservedCovariance => { + "predetermined first-occasion lagged observed covariance is not the predetermined later-start lagged observed covariance" + } + Self::StationaryLaggedObservedCovarianceIsNotPredeterminedLaterLaggedObservedCovariance => { + "stationary lagged observed covariance is not the predetermined later-start lagged observed covariance" + } + Self::PredeterminedLaterObservedVarianceIsNotPredeterminedLaterLaggedObservedCovariance => { + "predetermined later-occasion observed variance is not the predetermined later-start lagged observed covariance" + } + Self::PredeterminedLaterStartLaterLatentVarianceIsNotLaterLatentVariance => { + "predetermined later-start later-occasion latent variance is not the predetermined later-occasion latent variance" + } + Self::PredeterminedLaterStartLaterLatentVarianceIsNotLaterLaggedCovariance => { + "predetermined later-start later-occasion latent variance is not the predetermined later-start lagged latent covariance" + } + Self::PredeterminedLaterStartLaterLatentVarianceIsNotStationaryLaterLatentVariance => { + "predetermined later-start later-occasion latent variance is not the stationary later-occasion latent variance" + } + Self::PredeterminedLaterStartLaterLatentVarianceIsNotDecayedLaterTotal => { + "predetermined later-start later-occasion latent variance is not the evolved later-occasion total" + } + Self::PredeterminedLaterStartLaterLatentVarianceIsNotLagIntervalLaterLatentVariance => { + "predetermined later-start later-occasion latent variance is not the lag-interval later-occasion latent variance" + } + Self::PredeterminedLaterStartLaterLatentVarianceIsNotObservedVariance => { + "predetermined later-start later-occasion latent variance is not the predetermined later-start later-occasion observed variance" + } + Self::MeasurementErrorIsNotPredeterminedLaterStartLaterObservedVariance => { + "measurement-error variance is not the predetermined later-start later-occasion observed variance" + } + Self::PredeterminedLaterObservedVarianceIsNotPredeterminedLaterStartLaterObservedVariance => { + "predetermined later-occasion observed variance is not the predetermined later-start later-occasion observed variance" + } + Self::PredeterminedLaterLaggedObservedCovarianceIsNotPredeterminedLaterStartLaterObservedVariance => { + "predetermined later-start lagged observed covariance is not the predetermined later-start later-occasion observed variance" + } + Self::StationaryLaterObservedVarianceIsNotPredeterminedLaterStartLaterObservedVariance => { + "stationary later-occasion observed variance is not the predetermined later-start later-occasion observed variance" + } + Self::StandardisedDiscreteDriftRequiresPositiveWithinSubjectVariance => { + "standardised discrete DRIFT requires strictly positive within-subject variance" + } + Self::UnstandardisedDiscreteDriftIsNotStandardisedDiscreteDrift => { + "unstandardised discrete DRIFT is not standardised discrete DRIFT" + } + Self::TraitPlusStateAutocorrelationIsNotStandardisedDiscreteDrift => { + "trait-plus-state autocorrelation is not standardised discrete DRIFT" + } + Self::TraitVarianceIsNotStandardisationVariance => { + "trait variance is not the standardisation variance" + } + Self::StandardisedDiscreteDiffusionRequiresPositiveWithinSubjectVariance => { + "standardised discrete DIFFUSION requires strictly positive within-subject variance" + } + Self::UnstandardisedDiscreteDiffusionIsNotStandardisedDiscreteDiffusion => { + "unstandardised discrete DIFFUSION is not standardised discrete DIFFUSION" + } + Self::StandardisedContinuousDiffusionIsNotStandardisedDiscreteDiffusion => { + "standardised continuous DIFFUSION is not standardised discrete DIFFUSION" + } + Self::TraitContaminatedProcessNoiseIsNotStandardisedDiscreteDiffusion => { + "trait-contaminated process noise is not standardised discrete DIFFUSION" + } + Self::StandardisedContinuousDiffusionRequiresPositiveWithinSubjectVariance => { + "standardised continuous DIFFUSION requires strictly positive within-subject variance" + } + Self::UnstandardisedContinuousDiffusionIsNotStandardisedContinuousDiffusion => { + "unstandardised continuous DIFFUSION is not standardised continuous DIFFUSION" + } + Self::StandardisedDiscreteDiffusionIsNotStandardisedContinuousDiffusion => { + "standardised discrete DIFFUSION is not standardised continuous DIFFUSION" + } + Self::TraitContaminatedContinuousDiffusionIsNotStandardisedContinuousDiffusion => { + "trait-contaminated continuous DIFFUSION is not standardised continuous DIFFUSION" + } + Self::StandardisedContinuousDriftRequiresPositiveWithinSubjectVariance => { + "standardised continuous DRIFT requires strictly positive within-subject variance" + } + Self::UnstandardisedContinuousDriftIsNotStandardisedContinuousDrift => { + "unstandardised continuous DRIFT is not standardised continuous DRIFT" + } + Self::StandardisedDiscreteDriftIsNotStandardisedContinuousDrift => { + "standardised discrete DRIFT is not standardised continuous DRIFT" + } + Self::TraitContaminatedContinuousDriftIsNotStandardisedContinuousDrift => { + "trait-contaminated continuous DRIFT is not standardised continuous DRIFT" + } + Self::StandardisedAsymptoticTimeIndependentEffectRequiresPositiveWithinSubjectVariance => { + "standardised asymptotic time-independent predictor effect requires strictly positive within-subject variance" + } + Self::StandardisedAsymptoticTimeIndependentEffectRequiresPositivePredictorVariance => { + "standardised asymptotic time-independent predictor effect requires strictly positive predictor variance" + } + Self::UnstandardisedAsymptoticTimeIndependentEffectIsNotStandardisedAsymptoticTimeIndependentEffect => { + "unstandardised asymptotic time-independent predictor effect is not standardised asymptotic time-independent predictor effect" + } + Self::StandardisedDiscreteTimeIndependentEffectIsNotStandardisedAsymptoticTimeIndependentEffect => { + "standardised discrete time-independent predictor effect is not standardised asymptotic time-independent predictor effect" + } + Self::TraitContaminatedAsymptoticTimeIndependentEffectIsNotStandardisedAsymptoticTimeIndependentEffect => { + "trait-contaminated asymptotic time-independent predictor effect is not standardised asymptotic time-independent predictor effect" + } + Self::StandardisedContinuousTimeIndependentEffectRequiresPositiveWithinSubjectVariance => { + "standardised continuous time-independent predictor effect requires strictly positive within-subject variance" + } + Self::StandardisedContinuousTimeIndependentEffectRequiresPositivePredictorVariance => { + "standardised continuous time-independent predictor effect requires strictly positive predictor variance" + } + Self::UnstandardisedContinuousTimeIndependentEffectIsNotStandardisedContinuousTimeIndependentEffect => { + "unstandardised continuous time-independent predictor effect is not standardised continuous time-independent predictor effect" + } + Self::StandardisedAsymptoticTimeIndependentEffectIsNotStandardisedContinuousTimeIndependentEffect => { + "standardised asymptotic time-independent predictor effect is not standardised continuous time-independent predictor effect" + } + Self::StandardisedDiscreteTimeIndependentEffectIsNotStandardisedContinuousTimeIndependentEffect => { + "standardised discrete time-independent predictor effect is not standardised continuous time-independent predictor effect" + } + Self::TraitContaminatedContinuousTimeIndependentEffectIsNotStandardisedContinuousTimeIndependentEffect => { + "trait-contaminated continuous time-independent predictor effect is not standardised continuous time-independent predictor effect" + } + Self::StandardisedInitialTimeIndependentEffectRequiresPositiveInitialLatentVariance => { + "standardised initial time-independent predictor effect requires strictly positive initial latent variance" + } + Self::StandardisedInitialTimeIndependentEffectRequiresPositivePredictorVariance => { + "standardised initial time-independent predictor effect requires strictly positive predictor variance" + } + Self::UnstandardisedInitialTimeIndependentEffectIsNotStandardisedInitialTimeIndependentEffect => { + "unstandardised initial time-independent predictor effect is not standardised initial time-independent predictor effect" + } + Self::StandardisedContinuousTimeIndependentEffectIsNotStandardisedInitialTimeIndependentEffect => { + "standardised continuous time-independent predictor effect is not standardised initial time-independent predictor effect" + } + Self::StandardisedAsymptoticTimeIndependentEffectIsNotStandardisedInitialTimeIndependentEffect => { + "standardised asymptotic time-independent predictor effect is not standardised initial time-independent predictor effect" + } + Self::TraitContaminatedInitialTimeIndependentEffectIsNotStandardisedInitialTimeIndependentEffect => { + "trait-contaminated initial time-independent predictor effect is not standardised initial time-independent predictor effect" + } + Self::InitialTimeIndependentVarianceIsNotAsymptoticTimeIndependentVariance => { + "initial time-independent predictor variance is not asymptotic time-independent predictor variance" + } + Self::InitialTimeIndependentVarianceIsNotStandardisedInitialTimeIndependentEffect => { + "initial time-independent predictor variance is not standardised initial time-independent predictor effect" + } + Self::InitialTimeIndependentVarianceIsNotInitialLatentVariance => { + "initial time-independent predictor variance is not initial latent variance" + } + Self::InitialTimeIndependentVarianceIsNotTraitVariance => { + "initial time-independent predictor variance is not trait variance" + } + Self::InitialTimeIndependentObservedVarianceIsNotInitialTimeIndependentVariance => { + "initial time-independent observed variance is not initial time-independent predictor variance" + } + Self::InitialTimeIndependentObservedVarianceIsNotInitialObservedVariance => { + "initial time-independent observed variance is not initial observed variance" + } + Self::InitialTimeIndependentObservedVarianceIsNotAsymptoticTimeIndependentObservedVariance => { + "initial time-independent observed variance is not asymptotic time-independent observed variance" + } + Self::InitialTimeIndependentObservedVarianceIsNotMeasurementError => { + "initial time-independent observed variance is not measurement-error variance" + } + Self::AsymptoticTimeIndependentObservedVarianceIsNotAsymptoticTimeIndependentVariance => { + "asymptotic time-independent observed variance is not asymptotic time-independent predictor variance" + } + Self::AsymptoticTimeIndependentObservedVarianceIsNotInitialTimeIndependentObservedVariance => { + "asymptotic time-independent observed variance is not initial time-independent observed variance" + } + Self::AsymptoticTimeIndependentObservedVarianceIsNotStationaryObservedVariance => { + "asymptotic time-independent observed variance is not stationary observed variance" + } + Self::AsymptoticTimeIndependentObservedVarianceIsNotMeasurementError => { + "asymptotic time-independent observed variance is not measurement-error variance" + } + Self::StandardisedContinuousTimeDependentEffectRequiresPositiveWithinSubjectVariance => { + "standardised continuous time-dependent predictor effect requires strictly positive within-subject variance" + } + Self::StandardisedContinuousTimeDependentEffectRequiresPositivePredictorVariance => { + "standardised continuous time-dependent predictor effect requires strictly positive predictor variance" + } + Self::UnstandardisedContinuousTimeDependentEffectIsNotStandardisedContinuousTimeDependentEffect => { + "unstandardised continuous time-dependent predictor effect is not standardised continuous time-dependent predictor effect" + } + Self::StandardisedContinuousTimeIndependentEffectIsNotStandardisedContinuousTimeDependentEffect => { + "standardised continuous time-independent predictor effect is not standardised continuous time-dependent predictor effect" + } + Self::StandardisedDiscreteTimeDependentEffectIsNotStandardisedContinuousTimeDependentEffect => { + "standardised discrete time-dependent predictor effect is not standardised continuous time-dependent predictor effect" + } + Self::TraitContaminatedContinuousTimeDependentEffectIsNotStandardisedContinuousTimeDependentEffect => { + "trait-contaminated continuous time-dependent predictor effect is not standardised continuous time-dependent predictor effect" + } + Self::StandardisedInitialTimeDependentEffectRequiresPositiveInitialLatentVariance => { + "standardised initial time-dependent predictor effect requires strictly positive initial latent variance" + } + Self::StandardisedInitialTimeDependentEffectRequiresPositivePredictorVariance => { + "standardised initial time-dependent predictor effect requires strictly positive predictor variance" + } + Self::UnstandardisedInitialTimeDependentEffectIsNotStandardisedInitialTimeDependentEffect => { + "unstandardised initial time-dependent predictor effect is not standardised initial time-dependent predictor effect" + } + Self::StandardisedContinuousTimeDependentEffectIsNotStandardisedInitialTimeDependentEffect => { + "standardised continuous time-dependent predictor effect is not standardised initial time-dependent predictor effect" + } + Self::StandardisedInitialTimeIndependentEffectIsNotStandardisedInitialTimeDependentEffect => { + "standardised initial time-independent predictor effect is not standardised initial time-dependent predictor effect" + } + Self::TraitContaminatedInitialTimeDependentEffectIsNotStandardisedInitialTimeDependentEffect => { + "trait-contaminated initial time-dependent predictor effect is not standardised initial time-dependent predictor effect" + } + Self::StandardisedInitialLatentVarianceRequiresPositiveInitialLatentVariance => { + "standardised initial latent variance requires strictly positive initial latent variance" + } + Self::UnstandardisedInitialLatentVarianceIsNotStandardisedInitialLatentVariance => { + "unstandardised initial latent variance is not standardised initial latent variance" + } + Self::StandardisedInitialTimeDependentEffectIsNotStandardisedInitialLatentVariance => { + "standardised initial time-dependent predictor effect is not standardised initial latent variance" + } + Self::InitialTimeIndependentVarianceIsNotStandardisedInitialLatentVariance => { + "initial time-independent predictor variance is not standardised initial latent variance" + } + Self::StandardisedTraitVarianceRequiresPositiveTraitVariance => { + "standardised trait variance requires strictly positive trait variance" + } + Self::UnstandardisedTraitVarianceIsNotStandardisedTraitVariance => { + "unstandardised trait variance is not standardised trait variance" + } + Self::StandardisedInitialLatentVarianceIsNotStandardisedTraitVariance => { + "standardised initial latent variance is not standardised trait variance" + } + Self::InitialTimeIndependentVarianceIsNotStandardisedTraitVariance => { + "initial time-independent predictor variance is not standardised trait variance" + } + Self::StandardisedManifestTraitVarianceRequiresPositiveManifestTraitVariance => { + "standardised manifest-trait variance requires strictly positive manifest-trait variance" + } + Self::UnstandardisedManifestTraitVarianceIsNotStandardisedManifestTraitVariance => { + "unstandardised manifest-trait variance is not standardised manifest-trait variance" + } + Self::StandardisedTraitVarianceIsNotStandardisedManifestTraitVariance => { + "standardised trait variance is not standardised manifest-trait variance" + } + Self::MeasurementErrorIsNotStandardisedManifestTraitVariance => { + "measurement error is not standardised manifest-trait variance" + } + Self::StandardisedManifestVarianceRequiresPositiveManifestVariance => { + "standardised measurement-error variance requires strictly positive measurement-error variance" + } + Self::UnstandardisedManifestVarianceIsNotStandardisedManifestVariance => { + "unstandardised measurement-error variance is not standardised measurement-error variance" + } + Self::StandardisedManifestTraitVarianceIsNotStandardisedManifestVariance => { + "standardised manifest-trait variance is not standardised measurement-error variance" + } + Self::ObservedVarianceIsNotStandardisedManifestVariance => { + "observed-indicator variance is not standardised measurement-error variance" + } + Self::StandardisedTimeIndependentPredictorVarianceRequiresPositivePredictorVariance => { + "standardised time-independent predictor variance requires strictly positive time-independent predictor variance" + } + Self::UnstandardisedTimeIndependentPredictorVarianceIsNotStandardisedTimeIndependentPredictorVariance => { + "unstandardised time-independent predictor variance is not standardised time-independent predictor variance" + } + Self::StandardisedManifestVarianceIsNotStandardisedTimeIndependentPredictorVariance => { + "standardised measurement-error variance is not standardised time-independent predictor variance" + } + Self::AsymptoticTimeIndependentPredictorVarianceIsNotStandardisedTimeIndependentPredictorVariance => { + "asymptotic time-independent predictor variance is not standardised time-independent predictor variance" + } + Self::StandardisedAsymptoticDiffusionRequiresPositiveWithinSubjectVariance => { + "standardised asymptotic DIFFUSION requires strictly positive within-subject variance" + } + Self::UnstandardisedAsymptoticDiffusionIsNotStandardisedAsymptoticDiffusion => { + "unstandardised asymptotic DIFFUSION is not standardised asymptotic DIFFUSION" + } + Self::StandardisedTimeIndependentPredictorVarianceIsNotStandardisedAsymptoticDiffusion => { + "standardised time-independent predictor variance is not standardised asymptotic DIFFUSION" + } + Self::StandardisedContinuousDiffusionIsNotStandardisedAsymptoticDiffusion => { + "standardised continuous DIFFUSION is not standardised asymptotic DIFFUSION" + } + Self::StandardisedDiscreteContinuousInterceptRequiresPositiveWithinSubjectVariance => { + "standardised discrete continuous intercept requires strictly positive within-subject variance" + } + Self::UnstandardisedDiscreteContinuousInterceptIsNotStandardisedDiscreteContinuousIntercept => { + "unstandardised discrete continuous intercept is not standardised discrete continuous intercept" + } + Self::StandardisedContinuousInterceptIsNotStandardisedDiscreteContinuousIntercept => { + "standardised continuous intercept is not standardised discrete continuous intercept" + } + Self::AsymptoticStandardisedContinuousInterceptIsNotStandardisedDiscreteContinuousIntercept => { + "asymptotic standardised continuous intercept is not standardised discrete continuous intercept" + } + Self::StandardisedAsymptoticContinuousInterceptRequiresPositiveWithinSubjectVariance => { + "standardised asymptotic continuous intercept requires strictly positive within-subject variance" + } + Self::UnstandardisedAsymptoticContinuousInterceptIsNotStandardisedAsymptoticContinuousIntercept => { + "unstandardised asymptotic continuous intercept is not standardised asymptotic continuous intercept" + } + Self::StandardisedContinuousInterceptIsNotStandardisedAsymptoticContinuousIntercept => { + "standardised continuous intercept is not standardised asymptotic continuous intercept" + } + Self::StandardisedDiscreteContinuousInterceptIsNotStandardisedAsymptoticContinuousIntercept => { + "standardised discrete continuous intercept is not standardised asymptotic continuous intercept" + } + Self::StandardisedInitialLatentMeanRequiresPositiveInitialLatentVariance => { + "standardised initial latent mean requires strictly positive initial latent variance" + } + Self::UnstandardisedInitialLatentMeanIsNotStandardisedInitialLatentMean => { + "unstandardised initial latent mean is not standardised initial latent mean" + } + Self::StandardisedInitialLatentVarianceIsNotStandardisedInitialLatentMean => { + "standardised initial latent variance is not standardised initial latent mean" + } + Self::WithinSubjectScaledInitialLatentMeanIsNotStandardisedInitialLatentMean => { + "within-subject scaled initial latent mean is not standardised initial latent mean" + } }; formatter.write_str(message) } @@ -1515,4 +2512,767 @@ mod tests { "stationary lagged observed covariance is not the stationary later-occasion observed variance" ); } + + #[test] + fn predetermined_later_variance_boundary_messages_are_stable() { + assert_eq!( + PsychometricError::PredeterminedLaterLatentVarianceIsNotStationaryLaterLatentVariance + .to_string(), + "predetermined later-occasion latent variance is not the stationary later-occasion latent variance" + ); + assert_eq!( + PsychometricError::PredeterminedLaterLatentVarianceIsNotDiscreteVariance.to_string(), + "predetermined later-occasion latent variance is not the free discrete latent variance" + ); + assert_eq!( + PsychometricError::PredeterminedLaterLatentVarianceIsNotInitialLatentVariance + .to_string(), + "predetermined later-occasion latent variance is not the free first-occasion latent variance" + ); + assert_eq!( + PsychometricError::PredeterminedLaterLatentVarianceIsNotObservedVariance.to_string(), + "predetermined later-occasion latent variance is not the predetermined later-occasion observed variance" + ); + assert_eq!( + PsychometricError::MeasurementErrorIsNotPredeterminedLaterObservedVariance.to_string(), + "measurement-error variance is not the predetermined later-occasion observed variance" + ); + assert_eq!( + PsychometricError::StationaryLaterObservedVarianceIsNotPredeterminedLaterObservedVariance + .to_string(), + "stationary later-occasion observed variance is not the predetermined later-occasion observed variance" + ); + } + + #[test] + fn predetermined_lagged_covariance_boundary_messages_are_stable() { + assert_eq!( + PsychometricError::PredeterminedLaggedLatentCovarianceIsNotStationaryLaggedCovariance + .to_string(), + "predetermined lagged latent covariance is not the stationary lagged latent covariance" + ); + assert_eq!( + PsychometricError::PredeterminedLaggedLatentCovarianceIsNotLaterLatentVariance + .to_string(), + "predetermined lagged latent covariance is not the predetermined later-occasion latent variance" + ); + assert_eq!( + PsychometricError::PredeterminedLaggedLatentCovarianceIsNotDecayedTotal.to_string(), + "predetermined lagged latent covariance is not the decayed predetermined total" + ); + assert_eq!( + PsychometricError::PredeterminedLaggedLatentCovarianceIsNotInitialLatentVariance + .to_string(), + "predetermined lagged latent covariance is not the free first-occasion latent variance" + ); + assert_eq!( + PsychometricError::PredeterminedLaggedLatentCovarianceIsNotObservedCovariance + .to_string(), + "predetermined lagged latent covariance is not the predetermined lagged observed covariance" + ); + assert_eq!( + PsychometricError::MeasurementErrorIsNotPredeterminedLaggedObservedCovariance + .to_string(), + "measurement-error variance is not the predetermined lagged observed covariance" + ); + assert_eq!( + PsychometricError::PredeterminedLaterObservedVarianceIsNotPredeterminedLaggedObservedCovariance + .to_string(), + "predetermined later-occasion observed variance is not the predetermined lagged observed covariance" + ); + assert_eq!( + PsychometricError::StationaryLaggedObservedCovarianceIsNotPredeterminedLaggedObservedCovariance + .to_string(), + "stationary lagged observed covariance is not the predetermined lagged observed covariance" + ); + } + + #[test] + fn predetermined_initial_variance_boundary_messages_are_stable() { + assert_eq!( + PsychometricError::PredeterminedInitialLatentVarianceIsNotStationaryInitialLatentVariance + .to_string(), + "predetermined first-occasion latent variance is not the stationary first-occasion latent variance" + ); + assert_eq!( + PsychometricError::PredeterminedInitialLatentVarianceIsNotInitialLatentVariance + .to_string(), + "predetermined first-occasion latent variance is not the free first-occasion latent variance" + ); + assert_eq!( + PsychometricError::PredeterminedInitialLatentVarianceIsNotLaggedLatentCovariance + .to_string(), + "predetermined first-occasion latent variance is not the predetermined lagged latent covariance" + ); + assert_eq!( + PsychometricError::PredeterminedInitialLatentVarianceIsNotLaterLatentVariance + .to_string(), + "predetermined first-occasion latent variance is not the predetermined later-occasion latent variance" + ); + assert_eq!( + PsychometricError::PredeterminedInitialLatentVarianceIsNotObservedVariance.to_string(), + "predetermined first-occasion latent variance is not the predetermined first-occasion observed variance" + ); + assert_eq!( + PsychometricError::MeasurementErrorIsNotPredeterminedInitialObservedVariance + .to_string(), + "measurement-error variance is not the predetermined first-occasion observed variance" + ); + assert_eq!( + PsychometricError::StationaryInitialObservedVarianceIsNotPredeterminedInitialObservedVariance + .to_string(), + "stationary first-occasion observed variance is not the predetermined first-occasion observed variance" + ); + assert_eq!( + PsychometricError::PredeterminedLaterObservedVarianceIsNotPredeterminedInitialObservedVariance + .to_string(), + "predetermined later-occasion observed variance is not the predetermined first-occasion observed variance" + ); + } + + #[test] + fn predetermined_later_lagged_covariance_boundary_messages_are_stable() { + assert_eq!( + PsychometricError::PredeterminedLaterLaggedLatentCovarianceIsNotPredeterminedLaggedCovariance + .to_string(), + "predetermined later-start lagged latent covariance is not the predetermined first-occasion lagged latent covariance" + ); + assert_eq!( + PsychometricError::PredeterminedLaterLaggedLatentCovarianceIsNotLaterLatentVariance + .to_string(), + "predetermined later-start lagged latent covariance is not the predetermined later-occasion latent variance" + ); + assert_eq!( + PsychometricError::PredeterminedLaterLaggedLatentCovarianceIsNotStationaryLaggedCovariance + .to_string(), + "predetermined later-start lagged latent covariance is not the stationary lagged latent covariance" + ); + assert_eq!( + PsychometricError::PredeterminedLaterLaggedLatentCovarianceIsNotDecayedLaterTotal + .to_string(), + "predetermined later-start lagged latent covariance is not the decayed later-occasion total" + ); + assert_eq!( + PsychometricError::PredeterminedLaterLaggedLatentCovarianceIsNotObservedCovariance + .to_string(), + "predetermined later-start lagged latent covariance is not the predetermined later-start lagged observed covariance" + ); + assert_eq!( + PsychometricError::MeasurementErrorIsNotPredeterminedLaterLaggedObservedCovariance + .to_string(), + "measurement-error variance is not the predetermined later-start lagged observed covariance" + ); + assert_eq!( + PsychometricError::PredeterminedLaggedObservedCovarianceIsNotPredeterminedLaterLaggedObservedCovariance + .to_string(), + "predetermined first-occasion lagged observed covariance is not the predetermined later-start lagged observed covariance" + ); + assert_eq!( + PsychometricError::StationaryLaggedObservedCovarianceIsNotPredeterminedLaterLaggedObservedCovariance + .to_string(), + "stationary lagged observed covariance is not the predetermined later-start lagged observed covariance" + ); + assert_eq!( + PsychometricError::PredeterminedLaterObservedVarianceIsNotPredeterminedLaterLaggedObservedCovariance + .to_string(), + "predetermined later-occasion observed variance is not the predetermined later-start lagged observed covariance" + ); + } + + #[test] + fn predetermined_later_start_later_variance_boundary_messages_are_stable() { + assert_eq!( + PsychometricError::PredeterminedLaterStartLaterLatentVarianceIsNotLaterLatentVariance + .to_string(), + "predetermined later-start later-occasion latent variance is not the predetermined later-occasion latent variance" + ); + assert_eq!( + PsychometricError::PredeterminedLaterStartLaterLatentVarianceIsNotLaterLaggedCovariance + .to_string(), + "predetermined later-start later-occasion latent variance is not the predetermined later-start lagged latent covariance" + ); + assert_eq!( + PsychometricError::PredeterminedLaterStartLaterLatentVarianceIsNotStationaryLaterLatentVariance + .to_string(), + "predetermined later-start later-occasion latent variance is not the stationary later-occasion latent variance" + ); + assert_eq!( + PsychometricError::PredeterminedLaterStartLaterLatentVarianceIsNotDecayedLaterTotal + .to_string(), + "predetermined later-start later-occasion latent variance is not the evolved later-occasion total" + ); + assert_eq!( + PsychometricError::PredeterminedLaterStartLaterLatentVarianceIsNotLagIntervalLaterLatentVariance + .to_string(), + "predetermined later-start later-occasion latent variance is not the lag-interval later-occasion latent variance" + ); + assert_eq!( + PsychometricError::PredeterminedLaterStartLaterLatentVarianceIsNotObservedVariance + .to_string(), + "predetermined later-start later-occasion latent variance is not the predetermined later-start later-occasion observed variance" + ); + assert_eq!( + PsychometricError::MeasurementErrorIsNotPredeterminedLaterStartLaterObservedVariance + .to_string(), + "measurement-error variance is not the predetermined later-start later-occasion observed variance" + ); + assert_eq!( + PsychometricError::PredeterminedLaterObservedVarianceIsNotPredeterminedLaterStartLaterObservedVariance + .to_string(), + "predetermined later-occasion observed variance is not the predetermined later-start later-occasion observed variance" + ); + assert_eq!( + PsychometricError::PredeterminedLaterLaggedObservedCovarianceIsNotPredeterminedLaterStartLaterObservedVariance + .to_string(), + "predetermined later-start lagged observed covariance is not the predetermined later-start later-occasion observed variance" + ); + assert_eq!( + PsychometricError::StationaryLaterObservedVarianceIsNotPredeterminedLaterStartLaterObservedVariance + .to_string(), + "stationary later-occasion observed variance is not the predetermined later-start later-occasion observed variance" + ); + } + + #[test] + fn standardised_discrete_drift_boundary_messages_are_stable() { + assert_eq!( + PsychometricError::StandardisedDiscreteDriftRequiresPositiveWithinSubjectVariance + .to_string(), + "standardised discrete DRIFT requires strictly positive within-subject variance" + ); + assert_eq!( + PsychometricError::UnstandardisedDiscreteDriftIsNotStandardisedDiscreteDrift + .to_string(), + "unstandardised discrete DRIFT is not standardised discrete DRIFT" + ); + assert_eq!( + PsychometricError::TraitPlusStateAutocorrelationIsNotStandardisedDiscreteDrift + .to_string(), + "trait-plus-state autocorrelation is not standardised discrete DRIFT" + ); + assert_eq!( + PsychometricError::TraitVarianceIsNotStandardisationVariance.to_string(), + "trait variance is not the standardisation variance" + ); + } + + #[test] + fn standardised_discrete_diffusion_boundary_messages_are_stable() { + assert_eq!( + PsychometricError::StandardisedDiscreteDiffusionRequiresPositiveWithinSubjectVariance + .to_string(), + "standardised discrete DIFFUSION requires strictly positive within-subject variance" + ); + assert_eq!( + PsychometricError::UnstandardisedDiscreteDiffusionIsNotStandardisedDiscreteDiffusion + .to_string(), + "unstandardised discrete DIFFUSION is not standardised discrete DIFFUSION" + ); + assert_eq!( + PsychometricError::StandardisedContinuousDiffusionIsNotStandardisedDiscreteDiffusion + .to_string(), + "standardised continuous DIFFUSION is not standardised discrete DIFFUSION" + ); + assert_eq!( + PsychometricError::TraitContaminatedProcessNoiseIsNotStandardisedDiscreteDiffusion + .to_string(), + "trait-contaminated process noise is not standardised discrete DIFFUSION" + ); + } + + #[test] + fn standardised_continuous_diffusion_boundary_messages_are_stable() { + assert_eq!( + PsychometricError::StandardisedContinuousDiffusionRequiresPositiveWithinSubjectVariance + .to_string(), + "standardised continuous DIFFUSION requires strictly positive within-subject variance" + ); + assert_eq!( + PsychometricError::UnstandardisedContinuousDiffusionIsNotStandardisedContinuousDiffusion + .to_string(), + "unstandardised continuous DIFFUSION is not standardised continuous DIFFUSION" + ); + assert_eq!( + PsychometricError::StandardisedDiscreteDiffusionIsNotStandardisedContinuousDiffusion + .to_string(), + "standardised discrete DIFFUSION is not standardised continuous DIFFUSION" + ); + assert_eq!( + PsychometricError::TraitContaminatedContinuousDiffusionIsNotStandardisedContinuousDiffusion + .to_string(), + "trait-contaminated continuous DIFFUSION is not standardised continuous DIFFUSION" + ); + } + + #[test] + fn standardised_continuous_drift_boundary_messages_are_stable() { + assert_eq!( + PsychometricError::StandardisedContinuousDriftRequiresPositiveWithinSubjectVariance + .to_string(), + "standardised continuous DRIFT requires strictly positive within-subject variance" + ); + assert_eq!( + PsychometricError::UnstandardisedContinuousDriftIsNotStandardisedContinuousDrift + .to_string(), + "unstandardised continuous DRIFT is not standardised continuous DRIFT" + ); + assert_eq!( + PsychometricError::StandardisedDiscreteDriftIsNotStandardisedContinuousDrift + .to_string(), + "standardised discrete DRIFT is not standardised continuous DRIFT" + ); + assert_eq!( + PsychometricError::TraitContaminatedContinuousDriftIsNotStandardisedContinuousDrift + .to_string(), + "trait-contaminated continuous DRIFT is not standardised continuous DRIFT" + ); + } + + #[test] + fn standardised_asymptotic_time_independent_effect_boundary_messages_are_stable() { + assert_eq!( + PsychometricError::StandardisedAsymptoticTimeIndependentEffectRequiresPositiveWithinSubjectVariance + .to_string(), + "standardised asymptotic time-independent predictor effect requires strictly positive within-subject variance" + ); + assert_eq!( + PsychometricError::StandardisedAsymptoticTimeIndependentEffectRequiresPositivePredictorVariance + .to_string(), + "standardised asymptotic time-independent predictor effect requires strictly positive predictor variance" + ); + assert_eq!( + PsychometricError::UnstandardisedAsymptoticTimeIndependentEffectIsNotStandardisedAsymptoticTimeIndependentEffect + .to_string(), + "unstandardised asymptotic time-independent predictor effect is not standardised asymptotic time-independent predictor effect" + ); + assert_eq!( + PsychometricError::StandardisedDiscreteTimeIndependentEffectIsNotStandardisedAsymptoticTimeIndependentEffect + .to_string(), + "standardised discrete time-independent predictor effect is not standardised asymptotic time-independent predictor effect" + ); + assert_eq!( + PsychometricError::TraitContaminatedAsymptoticTimeIndependentEffectIsNotStandardisedAsymptoticTimeIndependentEffect + .to_string(), + "trait-contaminated asymptotic time-independent predictor effect is not standardised asymptotic time-independent predictor effect" + ); + } + + #[test] + fn standardised_continuous_time_independent_effect_boundary_messages_are_stable() { + assert_eq!( + PsychometricError::StandardisedContinuousTimeIndependentEffectRequiresPositiveWithinSubjectVariance + .to_string(), + "standardised continuous time-independent predictor effect requires strictly positive within-subject variance" + ); + assert_eq!( + PsychometricError::StandardisedContinuousTimeIndependentEffectRequiresPositivePredictorVariance + .to_string(), + "standardised continuous time-independent predictor effect requires strictly positive predictor variance" + ); + assert_eq!( + PsychometricError::UnstandardisedContinuousTimeIndependentEffectIsNotStandardisedContinuousTimeIndependentEffect + .to_string(), + "unstandardised continuous time-independent predictor effect is not standardised continuous time-independent predictor effect" + ); + assert_eq!( + PsychometricError::StandardisedAsymptoticTimeIndependentEffectIsNotStandardisedContinuousTimeIndependentEffect + .to_string(), + "standardised asymptotic time-independent predictor effect is not standardised continuous time-independent predictor effect" + ); + assert_eq!( + PsychometricError::StandardisedDiscreteTimeIndependentEffectIsNotStandardisedContinuousTimeIndependentEffect + .to_string(), + "standardised discrete time-independent predictor effect is not standardised continuous time-independent predictor effect" + ); + assert_eq!( + PsychometricError::TraitContaminatedContinuousTimeIndependentEffectIsNotStandardisedContinuousTimeIndependentEffect + .to_string(), + "trait-contaminated continuous time-independent predictor effect is not standardised continuous time-independent predictor effect" + ); + } + + #[test] + fn standardised_initial_time_independent_effect_boundary_messages_are_stable() { + assert_eq!( + PsychometricError::StandardisedInitialTimeIndependentEffectRequiresPositiveInitialLatentVariance + .to_string(), + "standardised initial time-independent predictor effect requires strictly positive initial latent variance" + ); + assert_eq!( + PsychometricError::StandardisedInitialTimeIndependentEffectRequiresPositivePredictorVariance + .to_string(), + "standardised initial time-independent predictor effect requires strictly positive predictor variance" + ); + assert_eq!( + PsychometricError::UnstandardisedInitialTimeIndependentEffectIsNotStandardisedInitialTimeIndependentEffect + .to_string(), + "unstandardised initial time-independent predictor effect is not standardised initial time-independent predictor effect" + ); + assert_eq!( + PsychometricError::StandardisedContinuousTimeIndependentEffectIsNotStandardisedInitialTimeIndependentEffect + .to_string(), + "standardised continuous time-independent predictor effect is not standardised initial time-independent predictor effect" + ); + assert_eq!( + PsychometricError::StandardisedAsymptoticTimeIndependentEffectIsNotStandardisedInitialTimeIndependentEffect + .to_string(), + "standardised asymptotic time-independent predictor effect is not standardised initial time-independent predictor effect" + ); + assert_eq!( + PsychometricError::TraitContaminatedInitialTimeIndependentEffectIsNotStandardisedInitialTimeIndependentEffect + .to_string(), + "trait-contaminated initial time-independent predictor effect is not standardised initial time-independent predictor effect" + ); + } + + #[test] + fn initial_time_independent_variance_boundary_messages_are_stable() { + assert_eq!( + PsychometricError::InitialTimeIndependentVarianceIsNotAsymptoticTimeIndependentVariance + .to_string(), + "initial time-independent predictor variance is not asymptotic time-independent predictor variance" + ); + assert_eq!( + PsychometricError::InitialTimeIndependentVarianceIsNotStandardisedInitialTimeIndependentEffect + .to_string(), + "initial time-independent predictor variance is not standardised initial time-independent predictor effect" + ); + assert_eq!( + PsychometricError::InitialTimeIndependentVarianceIsNotInitialLatentVariance.to_string(), + "initial time-independent predictor variance is not initial latent variance" + ); + assert_eq!( + PsychometricError::InitialTimeIndependentVarianceIsNotTraitVariance.to_string(), + "initial time-independent predictor variance is not trait variance" + ); + } + + #[test] + fn initial_time_independent_observed_variance_boundary_messages_are_stable() { + assert_eq!( + PsychometricError::InitialTimeIndependentObservedVarianceIsNotInitialTimeIndependentVariance + .to_string(), + "initial time-independent observed variance is not initial time-independent predictor variance" + ); + assert_eq!( + PsychometricError::InitialTimeIndependentObservedVarianceIsNotInitialObservedVariance + .to_string(), + "initial time-independent observed variance is not initial observed variance" + ); + assert_eq!( + PsychometricError::InitialTimeIndependentObservedVarianceIsNotAsymptoticTimeIndependentObservedVariance + .to_string(), + "initial time-independent observed variance is not asymptotic time-independent observed variance" + ); + assert_eq!( + PsychometricError::InitialTimeIndependentObservedVarianceIsNotMeasurementError + .to_string(), + "initial time-independent observed variance is not measurement-error variance" + ); + } + + #[test] + fn asymptotic_time_independent_observed_variance_boundary_messages_are_stable() { + assert_eq!( + PsychometricError::AsymptoticTimeIndependentObservedVarianceIsNotAsymptoticTimeIndependentVariance + .to_string(), + "asymptotic time-independent observed variance is not asymptotic time-independent predictor variance" + ); + assert_eq!( + PsychometricError::AsymptoticTimeIndependentObservedVarianceIsNotInitialTimeIndependentObservedVariance + .to_string(), + "asymptotic time-independent observed variance is not initial time-independent observed variance" + ); + assert_eq!( + PsychometricError::AsymptoticTimeIndependentObservedVarianceIsNotStationaryObservedVariance + .to_string(), + "asymptotic time-independent observed variance is not stationary observed variance" + ); + assert_eq!( + PsychometricError::AsymptoticTimeIndependentObservedVarianceIsNotMeasurementError + .to_string(), + "asymptotic time-independent observed variance is not measurement-error variance" + ); + } + + #[test] + fn standardised_continuous_time_dependent_effect_boundary_messages_are_stable() { + assert_eq!( + PsychometricError::StandardisedContinuousTimeDependentEffectRequiresPositiveWithinSubjectVariance + .to_string(), + "standardised continuous time-dependent predictor effect requires strictly positive within-subject variance" + ); + assert_eq!( + PsychometricError::StandardisedContinuousTimeDependentEffectRequiresPositivePredictorVariance + .to_string(), + "standardised continuous time-dependent predictor effect requires strictly positive predictor variance" + ); + assert_eq!( + PsychometricError::UnstandardisedContinuousTimeDependentEffectIsNotStandardisedContinuousTimeDependentEffect + .to_string(), + "unstandardised continuous time-dependent predictor effect is not standardised continuous time-dependent predictor effect" + ); + assert_eq!( + PsychometricError::StandardisedContinuousTimeIndependentEffectIsNotStandardisedContinuousTimeDependentEffect + .to_string(), + "standardised continuous time-independent predictor effect is not standardised continuous time-dependent predictor effect" + ); + assert_eq!( + PsychometricError::StandardisedDiscreteTimeDependentEffectIsNotStandardisedContinuousTimeDependentEffect + .to_string(), + "standardised discrete time-dependent predictor effect is not standardised continuous time-dependent predictor effect" + ); + assert_eq!( + PsychometricError::TraitContaminatedContinuousTimeDependentEffectIsNotStandardisedContinuousTimeDependentEffect + .to_string(), + "trait-contaminated continuous time-dependent predictor effect is not standardised continuous time-dependent predictor effect" + ); + } + + #[test] + fn standardised_initial_time_dependent_effect_boundary_messages_are_stable() { + assert_eq!( + PsychometricError::StandardisedInitialTimeDependentEffectRequiresPositiveInitialLatentVariance + .to_string(), + "standardised initial time-dependent predictor effect requires strictly positive initial latent variance" + ); + assert_eq!( + PsychometricError::StandardisedInitialTimeDependentEffectRequiresPositivePredictorVariance + .to_string(), + "standardised initial time-dependent predictor effect requires strictly positive predictor variance" + ); + assert_eq!( + PsychometricError::UnstandardisedInitialTimeDependentEffectIsNotStandardisedInitialTimeDependentEffect + .to_string(), + "unstandardised initial time-dependent predictor effect is not standardised initial time-dependent predictor effect" + ); + assert_eq!( + PsychometricError::StandardisedContinuousTimeDependentEffectIsNotStandardisedInitialTimeDependentEffect + .to_string(), + "standardised continuous time-dependent predictor effect is not standardised initial time-dependent predictor effect" + ); + assert_eq!( + PsychometricError::StandardisedInitialTimeIndependentEffectIsNotStandardisedInitialTimeDependentEffect + .to_string(), + "standardised initial time-independent predictor effect is not standardised initial time-dependent predictor effect" + ); + assert_eq!( + PsychometricError::TraitContaminatedInitialTimeDependentEffectIsNotStandardisedInitialTimeDependentEffect + .to_string(), + "trait-contaminated initial time-dependent predictor effect is not standardised initial time-dependent predictor effect" + ); + } + + #[test] + fn standardised_initial_latent_variance_boundary_messages_are_stable() { + assert_eq!( + PsychometricError::StandardisedInitialLatentVarianceRequiresPositiveInitialLatentVariance + .to_string(), + "standardised initial latent variance requires strictly positive initial latent variance" + ); + assert_eq!( + PsychometricError::UnstandardisedInitialLatentVarianceIsNotStandardisedInitialLatentVariance + .to_string(), + "unstandardised initial latent variance is not standardised initial latent variance" + ); + assert_eq!( + PsychometricError::StandardisedInitialTimeDependentEffectIsNotStandardisedInitialLatentVariance + .to_string(), + "standardised initial time-dependent predictor effect is not standardised initial latent variance" + ); + assert_eq!( + PsychometricError::InitialTimeIndependentVarianceIsNotStandardisedInitialLatentVariance + .to_string(), + "initial time-independent predictor variance is not standardised initial latent variance" + ); + } + + #[test] + fn standardised_trait_variance_boundary_messages_are_stable() { + assert_eq!( + PsychometricError::StandardisedTraitVarianceRequiresPositiveTraitVariance.to_string(), + "standardised trait variance requires strictly positive trait variance" + ); + assert_eq!( + PsychometricError::UnstandardisedTraitVarianceIsNotStandardisedTraitVariance + .to_string(), + "unstandardised trait variance is not standardised trait variance" + ); + assert_eq!( + PsychometricError::StandardisedInitialLatentVarianceIsNotStandardisedTraitVariance + .to_string(), + "standardised initial latent variance is not standardised trait variance" + ); + assert_eq!( + PsychometricError::InitialTimeIndependentVarianceIsNotStandardisedTraitVariance + .to_string(), + "initial time-independent predictor variance is not standardised trait variance" + ); + } + + #[test] + fn standardised_manifest_trait_variance_boundary_messages_are_stable() { + assert_eq!( + PsychometricError::StandardisedManifestTraitVarianceRequiresPositiveManifestTraitVariance + .to_string(), + "standardised manifest-trait variance requires strictly positive manifest-trait variance" + ); + assert_eq!( + PsychometricError::UnstandardisedManifestTraitVarianceIsNotStandardisedManifestTraitVariance + .to_string(), + "unstandardised manifest-trait variance is not standardised manifest-trait variance" + ); + assert_eq!( + PsychometricError::StandardisedTraitVarianceIsNotStandardisedManifestTraitVariance + .to_string(), + "standardised trait variance is not standardised manifest-trait variance" + ); + assert_eq!( + PsychometricError::MeasurementErrorIsNotStandardisedManifestTraitVariance.to_string(), + "measurement error is not standardised manifest-trait variance" + ); + } + + #[test] + fn standardised_manifest_variance_boundary_messages_are_stable() { + assert_eq!( + PsychometricError::StandardisedManifestVarianceRequiresPositiveManifestVariance + .to_string(), + "standardised measurement-error variance requires strictly positive measurement-error variance" + ); + assert_eq!( + PsychometricError::UnstandardisedManifestVarianceIsNotStandardisedManifestVariance + .to_string(), + "unstandardised measurement-error variance is not standardised measurement-error variance" + ); + assert_eq!( + PsychometricError::StandardisedManifestTraitVarianceIsNotStandardisedManifestVariance + .to_string(), + "standardised manifest-trait variance is not standardised measurement-error variance" + ); + assert_eq!( + PsychometricError::ObservedVarianceIsNotStandardisedManifestVariance.to_string(), + "observed-indicator variance is not standardised measurement-error variance" + ); + } + + #[test] + fn standardised_time_independent_predictor_variance_boundary_messages_are_stable() { + assert_eq!( + PsychometricError::StandardisedTimeIndependentPredictorVarianceRequiresPositivePredictorVariance + .to_string(), + "standardised time-independent predictor variance requires strictly positive time-independent predictor variance" + ); + assert_eq!( + PsychometricError::UnstandardisedTimeIndependentPredictorVarianceIsNotStandardisedTimeIndependentPredictorVariance + .to_string(), + "unstandardised time-independent predictor variance is not standardised time-independent predictor variance" + ); + assert_eq!( + PsychometricError::StandardisedManifestVarianceIsNotStandardisedTimeIndependentPredictorVariance + .to_string(), + "standardised measurement-error variance is not standardised time-independent predictor variance" + ); + assert_eq!( + PsychometricError::AsymptoticTimeIndependentPredictorVarianceIsNotStandardisedTimeIndependentPredictorVariance + .to_string(), + "asymptotic time-independent predictor variance is not standardised time-independent predictor variance" + ); + } + + #[test] + fn standardised_asymptotic_diffusion_boundary_messages_are_stable() { + assert_eq!( + PsychometricError::StandardisedAsymptoticDiffusionRequiresPositiveWithinSubjectVariance + .to_string(), + "standardised asymptotic DIFFUSION requires strictly positive within-subject variance" + ); + assert_eq!( + PsychometricError::UnstandardisedAsymptoticDiffusionIsNotStandardisedAsymptoticDiffusion + .to_string(), + "unstandardised asymptotic DIFFUSION is not standardised asymptotic DIFFUSION" + ); + assert_eq!( + PsychometricError::StandardisedTimeIndependentPredictorVarianceIsNotStandardisedAsymptoticDiffusion + .to_string(), + "standardised time-independent predictor variance is not standardised asymptotic DIFFUSION" + ); + assert_eq!( + PsychometricError::StandardisedContinuousDiffusionIsNotStandardisedAsymptoticDiffusion + .to_string(), + "standardised continuous DIFFUSION is not standardised asymptotic DIFFUSION" + ); + } + + #[test] + fn standardised_discrete_continuous_intercept_boundary_messages_are_stable() { + assert_eq!( + PsychometricError::StandardisedDiscreteContinuousInterceptRequiresPositiveWithinSubjectVariance + .to_string(), + "standardised discrete continuous intercept requires strictly positive within-subject variance" + ); + assert_eq!( + PsychometricError::UnstandardisedDiscreteContinuousInterceptIsNotStandardisedDiscreteContinuousIntercept + .to_string(), + "unstandardised discrete continuous intercept is not standardised discrete continuous intercept" + ); + assert_eq!( + PsychometricError::StandardisedContinuousInterceptIsNotStandardisedDiscreteContinuousIntercept + .to_string(), + "standardised continuous intercept is not standardised discrete continuous intercept" + ); + assert_eq!( + PsychometricError::AsymptoticStandardisedContinuousInterceptIsNotStandardisedDiscreteContinuousIntercept + .to_string(), + "asymptotic standardised continuous intercept is not standardised discrete continuous intercept" + ); + } + + #[test] + fn standardised_asymptotic_continuous_intercept_boundary_messages_are_stable() { + assert_eq!( + PsychometricError::StandardisedAsymptoticContinuousInterceptRequiresPositiveWithinSubjectVariance + .to_string(), + "standardised asymptotic continuous intercept requires strictly positive within-subject variance" + ); + assert_eq!( + PsychometricError::UnstandardisedAsymptoticContinuousInterceptIsNotStandardisedAsymptoticContinuousIntercept + .to_string(), + "unstandardised asymptotic continuous intercept is not standardised asymptotic continuous intercept" + ); + assert_eq!( + PsychometricError::StandardisedContinuousInterceptIsNotStandardisedAsymptoticContinuousIntercept + .to_string(), + "standardised continuous intercept is not standardised asymptotic continuous intercept" + ); + assert_eq!( + PsychometricError::StandardisedDiscreteContinuousInterceptIsNotStandardisedAsymptoticContinuousIntercept + .to_string(), + "standardised discrete continuous intercept is not standardised asymptotic continuous intercept" + ); + } + + #[test] + fn standardised_initial_latent_mean_boundary_messages_are_stable() { + assert_eq!( + PsychometricError::StandardisedInitialLatentMeanRequiresPositiveInitialLatentVariance + .to_string(), + "standardised initial latent mean requires strictly positive initial latent variance" + ); + assert_eq!( + PsychometricError::UnstandardisedInitialLatentMeanIsNotStandardisedInitialLatentMean + .to_string(), + "unstandardised initial latent mean is not standardised initial latent mean" + ); + assert_eq!( + PsychometricError::StandardisedInitialLatentVarianceIsNotStandardisedInitialLatentMean + .to_string(), + "standardised initial latent variance is not standardised initial latent mean" + ); + assert_eq!( + PsychometricError::WithinSubjectScaledInitialLatentMeanIsNotStandardisedInitialLatentMean + .to_string(), + "within-subject scaled initial latent mean is not standardised initial latent mean" + ); + } } diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index e5a889bcf..5e7c4cbf8 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -138,6 +138,77 @@ //! The lagged observed covariance omits `Q_Δt` and `θ`. `θ` is //! not that later-occasion observed variance. The later-occasion //! latent variance is not that observed variance. +//! The later-occasion variance of §4.3 predetermined `T0VAR` is +//! `trait + e^{2 a Δt} p_0 + Q_Δt + (B / a)² v` (Eq. 3–4 of §4.3 +//! predetermined first occasion; JSS PDF re-opened 2026-08-23T05:12Z). +//! Form the evolved free first-occasion variance first, then include +//! the trait, then include the TI extra variance, then add. Trait +//! variance and `addedTIPREDVAR` do not enter `Q_Δt`. Free `T0VAR` +//! `p_0` is not the later-occasion map. Setting `p_0 = −q / (2 a)` +//! recovers the stationary later-occasion map. Stationary later +//! variance uses `−q / (2 a)` in place of `p_0` and is not this map +//! when `p_0` is free. Evolving `trait + p_0 + (B / a)² v` as if it +//! were all state is not this map. As `Δt → ∞` with stable `a < 0` +//! the carried `p_0` vanishes and `Q_Δt` approaches `−q / (2 a)`, so +//! the composition approaches contemporaneous stationary `T0VAR`. +//! As `Δt → 0+` the composition approaches +//! `trait + p_0 + (B / a)² v`. Nonzero diffusion with `a ≥ 0` is a +//! growing process and is kept. Equation 5 of that predetermined +//! later-occasion variance is +//! `λ²(trait + e^{2 a Δt} p_0 + Q_Δt + (B / a)² v) + θ + ψ`. `θ` is +//! not that later-occasion observed variance. The predetermined +//! later-occasion latent variance is not that observed variance. +//! Stationary later observed variance is not that observed variance +//! when `p_0` is free. +//! The lagged covariance of §4.3 predetermined `T0VAR` is +//! `trait + e^{a Δt} p_0 + (B / a)² v` (Eq. 3–4 of §4.3 +//! predetermined first occasion; JSS PDF re-opened 2026-08-23T09:04Z). +//! Form the lagged free first-occasion covariance first, then include +//! the trait, then include the TI extra variance, then add. Trait +//! variance and `addedTIPREDVAR` do not decay with `e^{a Δt}`. Free +//! `T0VAR` `p_0` is not the lagged map. Setting `p_0 = −q / (2 a)` +//! recovers the stationary lagged map. Stationary lagged covariance +//! uses `−q / (2 a)` in place of `p_0` and is not this map when +//! `p_0` is free. Evolving `trait + p_0 + (B / a)² v` as if it were +//! all state is not this map. The later-occasion map includes +//! `Q_Δt` and `e^{2 a Δt} p_0` and is not this map. As `Δt → ∞` +//! with stable `a < 0` the state term vanishes. As `Δt → 0+` the +//! composition approaches `trait + p_0 + (B / a)² v`. Equation 5 of +//! that predetermined lagged covariance is +//! `λ²(trait + e^{a Δt} p_0 + (B / a)² v) + ψ`. Independent `ε_t` +//! does not enter. `θ` is not that lagged observed covariance. The +//! predetermined lagged latent covariance is not that observed +//! covariance. Predetermined later observed variance includes +//! `Q_Δt` and `θ` and is not that lagged observed covariance. +//! Stationary lagged observed covariance is not that observed +//! covariance when `p_0` is free. +//! The first-occasion variance of §4.3 predetermined `T0VAR` is +//! `trait + p_0 + (B / a)² v` (JSS PDF re-opened 2026-08-23T10:03Z). +//! Form the free first-occasion state variance first, then include +//! the trait, then include the TI extra variance, then add. Setting +//! `p_0 = −q / (2 a)` recovers the stationary first-occasion map. +//! Equation 5 of that first-occasion variance is +//! `λ²(trait + p_0 + (B / a)² v) + θ + ψ`. +//! The later-start lagged covariance of §4.3 predetermined `T0VAR` +//! is `trait + e^{a s}(e^{2 a u} p_0 + Q_u) + (B / a)² v` (Eq. 3–4 +//! of §4.3 later start / `startoffset`; JSS PDF re-opened +//! 2026-08-23T10:27Z). Form the later-start within-subject variance +//! first, then lag that state, then include the trait, then include +//! the TI extra variance, then add. Trait variance and +//! `addedTIPREDVAR` do not decay with `e^{a s}`. First-occasion +//! lagged covariance omits `e^{a s} Q_u`. Later-occasion variance +//! does not lag that later state. Setting `p_0 = −q / (2 a)` +//! recovers the stationary lagged map. As `u → 0+` the composition +//! approaches first-occasion lagged covariance. As `s → 0+` the +//! composition approaches later-occasion variance at `u`. Equation 5 +//! of that later-start lagged covariance is +//! `λ²(trait + e^{a s}(e^{2 a u} p_0 + Q_u) + (B / a)² v) + ψ`. +//! Independent `ε_t` does not enter. `θ` is not that lagged observed +//! covariance. The later-start lagged latent covariance is not that +//! observed covariance. First-occasion lagged observed covariance +//! omits `e^{a s} Q_u`. Predetermined later observed variance +//! includes `Q_u` and `θ`. Stationary lagged observed covariance is +//! not that observed covariance when `p_0` is free. //! Table 3 (p. 13) names a different matrix //! `T0TIPREDEFFECT` for time-independent predictors on latents at //! `T0`. The scalar first-occasion shift is `t0_b z`. Equation 3's @@ -188,7 +259,84 @@ //! `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (the //! first-occasion extra-process observed mean is not that observed //! mean when `u ≠ t0`; `e^{a(t−u)} m x` is a Dirac on the original -//! process, not this `DRIFT` drive). The JSS article +//! process, not this `DRIFT` drive). Equation 5 of 2017-era +//! `addedT0TIPREDVAR` is `λ² t0_b² v` (Table 3 / p. 16 / +//! 2017-era `summary.ctsemFit.R`; JSS PDF re-opened +//! 2026-08-23T19:10Z). Form `t0_b² v` first, then `(λ extra) λ` +//! with `θ = 0`. A zero loading or zero extra is exactly zero. +//! `t0_b² v` is the latent extra, not the observed extra. +//! `λ² p_0 + θ` is first-occasion observed variance, not this +//! extra. `λ² (B / a)² v` is Eq. 5 of `addedTIPREDVAR`, not this +//! first-occasion observed extra. `MANIFESTVAR` `θ` is not this +//! extra. Free `T0TIPREDEFFECT` does not require `a < 0`. Equation 5 +//! of §7.2 `addedTIPREDVAR` is `λ² (B / a)² v` (Table 2 / §7.2 / +//! 2017-era `summary.ctsemFit.R`; JSS PDF re-opened +//! 2026-08-23T19:23Z). Form `(B / a)² v` first, then `(λ extra) λ` +//! with `θ = 0`. A zero loading or zero extra is exactly zero. +//! Lasting asymptotic extra requires `a < 0`. `(B / a)² v` is the +//! latent extra, not the observed extra. `λ² t0_b² v` is +//! first-occasion extra observed TI variance, not this extra. +//! `λ² p + θ` is stationary observed variance, not this extra. +//! `MANIFESTVAR` `θ` is not this extra. Page 16 `TDPREDEFFECTstd` is +//! `m · √v / √(-q / (2 a))` after strictly positive `asymDIFFUSION` +//! and strictly positive TD predictor variance (JSS PDF re-opened +//! 2026-08-23T21:10Z). Unstandardised `M` is not `TDPREDEFFECTstd`. +//! `TIPREDEFFECTstd` is not `TDPREDEFFECTstd` even when `M = B`. +//! Table 3 / p. 16 `T0TDPREDEFFECTstd` is `t0_m · √v / √p_0` after +//! strictly positive free `T0VAR` and strictly positive TD predictor +//! variance (JSS PDF re-opened 2026-08-23T21:34Z). Unstandardised +//! `t0_m` is not `T0TDPREDEFFECTstd`. `TDPREDEFFECTstd` uses +//! `asymDIFFUSION` and is not this first-occasion map. +//! `T0TIPREDEFFECTstd` is not `T0TDPREDEFFECTstd` even when +//! `t0_m = t0_b`. Free `T0VAR` does not require `a < 0`. Page 16 +//! `T0VARstd` is the correlation form +//! `solve(sqrt(diag(T0VAR))) %&% T0VAR` after strictly positive +//! free `T0VAR` (2017-era `summary.ctsemFit.R`; JSS PDF re-opened +//! 2026-08-23T22:06Z). The scalar map is `p_0 / p_0 = 1`. +//! Unstandardised `T0VAR` is not `T0VARstd`. `T0TDPREDEFFECTstd` +//! is not `T0VARstd`. `addedT0TIPREDVAR` is not `T0VARstd`. Page 16 +//! `TRAITVARstd` is the correlation form +//! `solve(sqrt(diag(TRAITVAR))) %&% TRAITVAR` after strictly +//! positive `TRAITVAR` (2017-era `summary.ctsemFit.R` forms it only +//! when `TRAITVAR != 0`; JSS PDF re-opened 2026-08-23T22:21Z). The +//! scalar map is `trait / trait = 1`. Unstandardised `TRAITVAR` is +//! not `TRAITVARstd`. `T0VARstd` is not `TRAITVARstd` even when +//! both equal 1. `addedT0TIPREDVAR` is not `TRAITVARstd`. Page 16 +//! `MANIFESTTRAITVARstd` is the correlation form +//! `solve(sqrt(diag(MANIFESTTRAITVAR))) %&% MANIFESTTRAITVAR` after +//! strictly positive `MANIFESTTRAITVAR` (2017-era +//! `summary.ctsemFit.R` forms it only when `MANIFESTTRAITVAR != 0`; +//! unlike `TRAITVARstd` the 2017-era source adds ridging; JSS PDF +//! re-opened 2026-08-23T22:28Z). The scalar map is `ψ / ψ = 1`. +//! Unstandardised `MANIFESTTRAITVAR` is not `MANIFESTTRAITVARstd`. +//! `TRAITVARstd` is not `MANIFESTTRAITVARstd` even when both equal 1. +//! `MANIFESTVAR` is not `MANIFESTTRAITVARstd`. Page 16 +//! `MANIFESTVARstd` is the correlation form +//! `solve(sqrt(diag(MANIFESTVAR) + ridging)) %&% MANIFESTVAR` after +//! strictly positive `MANIFESTVAR` (2017-era `summary.ctsemFit.R` +//! forms it whenever `verbose = TRUE`; unlike `TRAITVARstd` the +//! 2017-era source adds ridging; default ridge is 0; JSS PDF +//! re-opened 2026-08-23T22:40Z). The scalar map is `θ / θ = 1`. +//! Unstandardised `MANIFESTVAR` is not `MANIFESTVARstd`. +//! `MANIFESTTRAITVARstd` is not `MANIFESTVARstd` even when both +//! equal 1. `Var(y)` is not `MANIFESTVARstd`. Page 16 +//! `TIPREDVARstd` is the correlation form +//! `solve(sqrt(diag(TIPREDVAR) + ridging)) %&% TIPREDVAR` after +//! strictly positive `TIPREDVAR` (2017-era `summary.ctsemFit.R` +//! forms it whenever `verbose = TRUE` and `n.TIpred > 0`; unlike +//! `TRAITVARstd` the 2017-era source adds ridging; default ridge +//! is 0; `dimnames` are `TIpredNames`; JSS PDF re-opened +//! 2026-08-23T22:53Z). The scalar map is `v / v = 1`. +//! Unstandardised `TIPREDVAR` is not `TIPREDVARstd`. +//! `MANIFESTVARstd` is not `TIPREDVARstd` even when both equal 1. +//! `addedTIPREDVAR` `(B / a)² v` is not `TIPREDVARstd`. Page 16 +//! `T0MEANSstd` is `μ_0 / √p_0` after strictly positive free +//! `T0VAR` (2017-era `summary.ctsemFit.R` forms unstandardised +//! `T0MEANS` and does not form `T0MEANSstd`; JSS PDF re-opened +//! 2026-08-24T22:30Z). Unstandardised `T0MEANS` is not +//! `T0MEANSstd`. `T0VARstd` is not `T0MEANSstd` even when both +//! equal 1. `μ_0 / √asymDIFFUSION` is not `T0MEANSstd`. Free +//! `T0MEANS` does not require `a < 0`. The JSS article //! has no numbered §2.2 (2.1 is Continuous time and SEM; §3 follows). //! The difference quotient `(x(t+Δt) − x(t)) / Δt` (their //! Eqs. 3–4) is refused. This is not DSEM and not a matrix `expm`. @@ -357,3855 +505,4310 @@ pub fn recover_discrete_lag_from_log_rate( Ok(discrete_lag) } -/// Recover the exact scalar pair `(φ, a)` on event time. +/// Exact scalar p. 16 `discreteDRIFTstd` after strictly positive +/// `asymDIFFUSION`. +/// +/// Driver, Oud, and Voelkle (2017, p. 16; Eq. 3, p. 5; footnote 4; +/// §7.1, pp. 18–19; JSS PDF re-opened 2026-08-23T11:40Z from +/// ) +/// print `discreteDRIFT` as the unstandardised discrete-time equivalent +/// of `DRIFT` for a chosen event interval, `expm(DRIFT Δt)`. Equation 3 +/// writes that map. Page 16 then prints `discreteDRIFTstd` for +/// `Δt = 1` when a standardised matrix is appropriate. Footnote 4: +/// standardisations use only the relevant variance, not the total. For +/// `DRIFT`, that relevant variance is the within-subject variance +/// `asymDIFFUSION`, because `DRIFT` is intended to represent +/// individual, or average individual, temporal dynamics. The scalar +/// Lyapunov solution is `−q / (2 a)` for stable `a < 0`. Form that +/// strictly positive within-subject variance first, then +/// `φ = exp(a Δt)`. In the scalar stationary case the within-subject +/// SD ratio is 1, so the standardised auto-effect equals the +/// unstandardised discrete lag numerically; those remain distinct +/// named quantities. Unstandardised `e^{a Δt}` is defined for growing +/// `a ≥ 0` and for zero diffusion; standardised `DRIFT` is not. +/// Zero `asymDIFFUSION` has no positive SD and fails closed. +/// Section 7.1 warns that omitting trait variance confounds between- +/// and within-person information. The trait-plus-state autocorrelation +/// `(trait + e^{a Δt} p + added) / (trait + p + added)` uses the +/// total, not `asymDIFFUSION`, and is not this map when `TRAITVAR` +/// is nonzero. `TRAITVAR` is not the standardisation variance. This +/// is not a Kalman filter, not a matrix `expm`, not DSEM, and not +/// ctsem estimation. /// /// # Errors /// -/// Propagates [`recover_discrete_lag_one`] and [`recover_local_log_rate`]. -pub fn recover_event_time_discrete_lag_and_log_rate( - earlier: f64, - later: f64, +/// Propagates [`recover_stationary_latent_variance`] and +/// [`recover_discrete_lag_from_log_rate`]. Returns +/// [`PsychometricError::EventTimeRequired`] for any non-event clock, +/// [`PsychometricError::NonPositiveInterval`] when `event_delta` is +/// not strictly positive, +/// [`PsychometricError::StationaryVarianceRequiresStableDrift`] when +/// the log-rate is not strictly negative, +/// [`PsychometricError::StandardisedDiscreteDriftRequiresPositiveWithinSubjectVariance`] +/// when `asymDIFFUSION` is zero, and +/// [`PsychometricError::InvalidNumericInput`] when an input is +/// non-finite or the exponential overflows or underflows to zero. +pub fn recover_standardised_discrete_drift( + continuous_diffusion: f64, + log_rate: f64, event_delta: f64, clock: LagClock, -) -> Result { - let discrete_lag = recover_discrete_lag_one(earlier, later)?; - let log_rate = recover_local_log_rate(discrete_lag, event_delta, clock)?; - Ok(DiscreteLagAndLogRate { - discrete_lag, - log_rate, - event_delta, - }) +) -> Result { + let within = recover_stationary_latent_variance(continuous_diffusion, log_rate, clock)?; + if within == 0.0 { + return Err( + PsychometricError::StandardisedDiscreteDriftRequiresPositiveWithinSubjectVariance, + ); + } + recover_discrete_lag_from_log_rate(log_rate, event_delta, clock) } -/// Map a discrete lag from one event interval onto another through `a`. +/// Refuse treating unstandardised `discreteDRIFT` as p. 16 +/// `discreteDRIFTstd`. /// -/// Voelkle et al. (2012, ZORA accepted manuscript pp. 2, 16, 33) show that -/// discrete-time autoregressive coefficients from different intervals are -/// not comparable. The licensed path is `a = ln(φ_src) / Δt_src` then -/// `φ_ref = exp(a Δt_ref)`. Equal source and reference intervals still go -/// through that map. This is not DSEM. +/// `e^{a Δt}` is defined for growing and zero-diffusion processes. +/// Footnote 4 `discreteDRIFTstd` requires strictly positive +/// `asymDIFFUSION`. Equal numbers in the scalar stationary case are +/// still distinct named quantities. /// /// # Errors /// -/// Propagates [`recover_local_log_rate`] and -/// [`recover_discrete_lag_from_log_rate`]. -pub fn map_discrete_lag_across_event_intervals( - discrete_lag: f64, - source_delta: f64, - reference_delta: f64, - clock: LagClock, +/// Always returns +/// [`PsychometricError::UnstandardisedDiscreteDriftIsNotStandardisedDiscreteDrift`]. +pub fn refuse_unstandardised_discrete_drift_as_standardised_discrete_drift( + unstandardised_discrete_drift: f64, + standardised_discrete_drift: f64, ) -> Result { - let log_rate = recover_local_log_rate(discrete_lag, source_delta, clock)?; - recover_discrete_lag_from_log_rate(log_rate, reference_delta, clock) + let _ = (unstandardised_discrete_drift, standardised_discrete_drift); + Err(PsychometricError::UnstandardisedDiscreteDriftIsNotStandardisedDiscreteDrift) } -/// Exact scalar discrete effect of a constant event-time predictor. +/// Refuse treating Driver §7.1 trait-plus-state autocorrelation as +/// p. 16 `discreteDRIFTstd`. /// -/// Voelkle et al. (2012, Eq. 12; ZORA accepted manuscript, Introducing -/// Intercepts, manuscript p. 20): adding a continuous-time intercept -/// `b` yields the discrete increment `A^{-1}(exp(A Δt) − I) b`. Driver, -/// Oud, and Voelkle (2017, Eq. 3) write the same term as -/// `A^{-1}[e^{A Δt} − I] ξ`. The scalar case is -/// `b*_y.x(Δt) = (a_yx / a_xx) (exp(a_xx Δt) − 1)` for `a_xx ≠ 0`. -/// The algebraically identical finite-`expm1` evaluation is -/// `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt`. Dividing the increment -/// by the finite auto-effect keeps a finite Eq. 12 result when `z` -/// overflows to `-∞` (`exp(z) → 0`, so Eq. 12 → `-a_yx / a_xx`) and -/// when `a_yx Δt` overflows. When binary64 `z` underflows to `+0`, the -/// mathematical limit of Eq. 12 is `a_yx Δt`. When `a_yx = 0`, Eq. 12 -/// is exactly `0` even if `expm1(z)` overflows (`0 * +∞` is `NaN`). -/// When `expm1(z)` overflows to `+∞` at a finite `z`, rewrite as -/// `sign(a_yx / a_xx) exp(ln|a_yx| + z − ln|a_xx|) − a_yx / a_xx` so a -/// finite Eq. 12 result is not lost. `z → +∞` is an unstable process -/// and fails closed unless `a_yx = 0`. The first-order product is not -/// the general discrete effect. This is not DSEM and not a matrix -/// `expm`. +/// `(trait + e^{a Δt} p + added) / (trait + p + added)` mixes +/// between-subject `TRAITVAR` into the auto-effect. Footnote 4 +/// standardises `DRIFT` using only `asymDIFFUSION`. /// /// # Errors /// -/// Returns [`PsychometricError::EventTimeRequired`] for any non-event clock, -/// [`PsychometricError::NonPositiveInterval`] when `event_delta` is not -/// strictly positive, and [`PsychometricError::InvalidNumericInput`] when -/// either rate is non-finite, the predictor auto-effect is zero, or the -/// mapped effect is non-finite. -pub fn recover_discrete_constant_predictor_effect( - outcome_on_predictor: f64, - predictor_log_rate: f64, - event_delta: f64, - clock: LagClock, +/// Always returns +/// [`PsychometricError::TraitPlusStateAutocorrelationIsNotStandardisedDiscreteDrift`]. +pub fn refuse_trait_plus_state_autocorrelation_as_standardised_discrete_drift( + trait_plus_state_autocorrelation: f64, + standardised_discrete_drift: f64, ) -> Result { - if !clock.admits_structural_lag() { - return Err(PsychometricError::EventTimeRequired); - } - if !event_delta.is_finite() || event_delta <= 0.0 { - return Err(PsychometricError::NonPositiveInterval); - } - if !outcome_on_predictor.is_finite() - || !predictor_log_rate.is_finite() - || predictor_log_rate == 0.0 - { - return Err(PsychometricError::InvalidNumericInput); - } - // Voelkle Eq. 12 / Driver Eq. 3: (0 / a)(exp(a Δt) − 1) = 0. - // Direct 0 * (expm1(z) / a) is NaN when expm1 overflows. - if outcome_on_predictor == 0.0 { - return Ok(0.0); - } - let increment_argument = predictor_log_rate * event_delta; - if increment_argument == 0.0 { - // Binary64 underflow of a_xx Δt. lim z→0 of Eq. 12 is a_yx Δt. - return require_finite(outcome_on_predictor * event_delta); - } - let increment = increment_argument.exp_m1(); - if increment.is_finite() { - // Divide expm1(z) by the finite a_xx, not by z. expm1(-∞)/-∞ - // is +0 and loses the equilibrium increment -a_yx/a_xx (Voelkle - // 2012, Introducing Intercepts: the exponential vanishes as Δt - // grows). - return require_finite(outcome_on_predictor * (increment / predictor_log_rate)); - } - // expm1 overflowed. Finite z uses the log-space rewrite; a - // non-finite argument also fails closed through `require_finite`. - // Finite z, overflowed expm1. (a_yx/a_xx)(exp(z) − 1) = - // sign(a_yx/a_xx) exp(ln|a_yx| + z − ln|a_xx|) − a_yx/a_xx. - // The subtracted scale must itself be finite: if a_yx/a_xx overflows, - // the rewrite term is not a binary64 number. That path is dead if - // dominant is required first (dominant is then also infinite), so - // refuse the scale before forming the exponential. - let scale = outcome_on_predictor / predictor_log_rate; - if !scale.is_finite() { - return Err(PsychometricError::InvalidNumericInput); - } - let log_abs_dominant = - outcome_on_predictor.abs().ln() + increment_argument - predictor_log_rate.abs().ln(); - let dominant = require_finite( - outcome_on_predictor.signum() * predictor_log_rate.signum() * log_abs_dominant.exp(), - )?; - require_finite(dominant - scale) + let _ = ( + trait_plus_state_autocorrelation, + standardised_discrete_drift, + ); + Err(PsychometricError::TraitPlusStateAutocorrelationIsNotStandardisedDiscreteDrift) } -/// Refuse treating discrete lags from unequal event intervals as one coefficient. +/// Refuse treating Driver §4.3 trait variance as the p. 16 footnote 4 +/// standardisation variance. /// -/// Always fails closed. Map each lag through -/// [`map_discrete_lag_across_event_intervals`] instead. +/// `TRAITVAR` is time-invariant between-subject variance. Footnote 4 +/// uses only within-subject `asymDIFFUSION`. /// /// # Errors /// -/// Always returns [`PsychometricError::UnequalIntervalPoolingForbidden`]. -pub fn refuse_pooled_discrete_lag_across_unequal_intervals( - first_delta: f64, - second_delta: f64, +/// Always returns +/// [`PsychometricError::TraitVarianceIsNotStandardisationVariance`]. +pub fn refuse_trait_variance_as_standardisation_variance( + trait_variance: f64, + within_subject_variance: f64, ) -> Result { - let _ = (first_delta, second_delta); - Err(PsychometricError::UnequalIntervalPoolingForbidden) + let _ = (trait_variance, within_subject_variance); + Err(PsychometricError::TraitVarianceIsNotStandardisationVariance) } -/// Exact scalar discrete effect of a time-varying event-time predictor. +/// Exact scalar p. 16 `discreteDIFFUSIONstd` after strictly positive +/// `asymDIFFUSION`. /// -/// Voelkle et al. (2012, Eq. 14; ZORA accepted manuscript, Introducing -/// Intercepts, manuscript p. 21): when the predictor can take a new value -/// at each occasion **and** the sampling interval equals the interval -/// during which that predictor is assumed constant, the discrete effect -/// is `b*_y.x(Δt) = a_yx Δt`. It does not depend on the predictor -/// auto-effect. The manuscript calls this a first-order approximation -/// that deteriorates as `Δt` grows. It is not Eq. 12. The general case -/// (sampling interval ≠ constancy interval) cites Oud and Jansen (2000), -/// which is unread, and fails closed. This is not DSEM. +/// Driver, Oud, and Voelkle (2017, p. 16; Eq. 3–4, pp. 4–5; footnote 4; +/// §7.1, pp. 18–19; JSS PDF re-opened 2026-08-23T13:06Z from +/// ) +/// print discrete-time transformations for a chosen event interval +/// (`discreteDRIFT`, `discreteDIFFUSION`) and, when appropriate, +/// standardised matrices with the suffix `std`. Footnote 4: +/// standardisations use only the relevant variance, not the total. +/// Process noise is within-subject stochastic input, so that relevant +/// variance is `asymDIFFUSION`, the same footnote 4 variance used +/// for `DRIFT`. The scalar Lyapunov solution is `−q / (2 a)` for +/// stable `a < 0`. Form that strictly positive within-subject +/// variance first, then `Q_Δt` from Equation 4, then +/// `Q_Δt / (−q / (2 a))`. In the scalar stationary case that ratio +/// equals `1 − exp(2 a Δt)`. Unstandardised `Q_Δt` is defined for +/// growing `a ≥ 0` and for zero diffusion; standardised `DIFFUSION` +/// is not. Zero `asymDIFFUSION` has no positive SD and fails closed. +/// The continuous standardisation `q / (−q / (2 a)) = −2 a` is not +/// the discrete map. Section 7.1 warns that omitting trait variance +/// confounds between- and within-person information. +/// `Q_Δt / (trait + p + added)` uses the total, not +/// `asymDIFFUSION`, and is not this map when `TRAITVAR` is nonzero. +/// `TRAITVAR` is not the standardisation variance. This is not a +/// Kalman filter, not a matrix `expm`, not DSEM, and not ctsem +/// estimation. /// /// # Errors /// -/// Returns [`PsychometricError::EventTimeRequired`] for any non-event clock, -/// [`PsychometricError::NonPositiveInterval`] when any interval is not -/// strictly positive, [`PsychometricError::UnmatchedTimeVaryingInterval`] -/// when the event, sampling, and constancy intervals are not the same -/// finite value, and [`PsychometricError::InvalidNumericInput`] when the -/// continuous effect is non-finite or the product overflows. -pub fn recover_discrete_time_varying_predictor_effect( - outcome_on_predictor: f64, +/// Propagates [`recover_stationary_latent_variance`] and +/// [`recover_discrete_process_noise`]. Returns +/// [`PsychometricError::EventTimeRequired`] for any non-event clock, +/// [`PsychometricError::NonPositiveInterval`] when `event_delta` is +/// not strictly positive, +/// [`PsychometricError::StationaryVarianceRequiresStableDrift`] when +/// the log-rate is not strictly negative, +/// [`PsychometricError::StandardisedDiscreteDiffusionRequiresPositiveWithinSubjectVariance`] +/// when `asymDIFFUSION` is zero, and +/// [`PsychometricError::InvalidNumericInput`] when an input is +/// non-finite or the mapped ratio is non-finite. +pub fn recover_standardised_discrete_diffusion( + continuous_diffusion: f64, + log_rate: f64, event_delta: f64, - sampling_interval: f64, - constancy_interval: f64, clock: LagClock, ) -> Result { - if !clock.admits_structural_lag() { - return Err(PsychometricError::EventTimeRequired); - } - if !event_delta.is_finite() - || event_delta <= 0.0 - || !sampling_interval.is_finite() - || sampling_interval <= 0.0 - || !constancy_interval.is_finite() - || constancy_interval <= 0.0 - { - return Err(PsychometricError::NonPositiveInterval); - } - if event_delta.to_bits() != sampling_interval.to_bits() - || sampling_interval.to_bits() != constancy_interval.to_bits() - { - return Err(PsychometricError::UnmatchedTimeVaryingInterval); - } - if !outcome_on_predictor.is_finite() { - return Err(PsychometricError::InvalidNumericInput); + let within = recover_stationary_latent_variance(continuous_diffusion, log_rate, clock)?; + if within == 0.0 { + return Err( + PsychometricError::StandardisedDiscreteDiffusionRequiresPositiveWithinSubjectVariance, + ); } - require_finite(outcome_on_predictor * event_delta) + let process_noise = + recover_discrete_process_noise(continuous_diffusion, log_rate, event_delta, clock)?; + require_finite(process_noise / within) } -/// Refuse mapping a time-varying predictor when sampling ≠ constancy. +/// Refuse treating unstandardised `discreteDIFFUSION` as p. 16 +/// `discreteDIFFUSIONstd`. /// -/// Always fails closed. Oud and Jansen (2000) is unread. Use -/// [`recover_discrete_time_varying_predictor_effect`] only when the -/// intervals already match, or [`recover_discrete_constant_predictor_effect`] -/// for a constant predictor (Eq. 12). +/// `Q_Δt` is defined for growing and zero-diffusion processes. +/// Footnote 4 `discreteDIFFUSIONstd` requires strictly positive +/// `asymDIFFUSION`. Equal numbers would still be distinct named +/// quantities. /// /// # Errors /// -/// Always returns [`PsychometricError::UnmatchedTimeVaryingInterval`]. -pub fn refuse_unmatched_time_varying_predictor_interval( - sampling_interval: f64, - constancy_interval: f64, +/// Always returns +/// [`PsychometricError::UnstandardisedDiscreteDiffusionIsNotStandardisedDiscreteDiffusion`]. +pub fn refuse_unstandardised_discrete_diffusion_as_standardised_discrete_diffusion( + unstandardised_discrete_diffusion: f64, + standardised_discrete_diffusion: f64, ) -> Result { - let _ = (sampling_interval, constancy_interval); - Err(PsychometricError::UnmatchedTimeVaryingInterval) + let _ = ( + unstandardised_discrete_diffusion, + standardised_discrete_diffusion, + ); + Err(PsychometricError::UnstandardisedDiscreteDiffusionIsNotStandardisedDiscreteDiffusion) } -/// Exact scalar discrete process noise on event time. +/// Refuse treating continuous `DIFFUSION` standardisation as p. 16 +/// `discreteDIFFUSIONstd`. /// -/// Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF re-opened 2026-08-17T21:03Z, -/// p. 4) write the discrete process-noise covariance -/// `Q_Δt = ∫_0^{Δt} expm(A(Δt−τ)) L G G⊤ L⊤ expm(A(Δt−τ))⊤ dτ`. -/// This slice takes scalar `L = 1` (every latent subject to system noise). -/// The noiseless scalar closed form with continuous diffusion -/// `q = G G⊤ ≥ 0` is `q (exp(2 a Δt) − 1) / (2 a)` for `a ≠ 0` and -/// `q Δt` for `a = 0`. The algebraically identical finite-`expm1` -/// evaluation is `0.5 q (expm1(z) / a)` with `z = 2 (a Δt)`. Form -/// `z` as twice the already-finite product `a Δt`. Forming `2 a` -/// first overflows when `|a|` is at the binary64 extreme even if -/// `a Δt` and `Q_Δt` are finite (`a = ±1e308`, `Δt = 1e-308`). -/// When binary64 `z` underflows to `+0`, the mathematical limit is -/// `q Δt`. When `z → −∞` the exponential vanishes and the result is -/// the equilibrium variance `−q / (2 a) = −0.5 q / a` for stable -/// `a < 0`. When `expm1(z)` overflows to `+∞` at a finite `z`, -/// rewrite as `sign(q / a) exp(ln|q| + z − ln|a| − ln 2) − 0.5 q / a`. -/// An overflowing rewrite scale `0.5 q / a` is not a finite `Q_Δt` -/// (`q = 1e308`, `a = 0.1`, `Δt = 4000` → `z = 800`, `0.5 q / a = +∞`). -/// `z → +∞` is an unstable process and fails closed unless `q = 0`. -/// A zero diffusion is exactly zero even if `expm1` overflows. This -/// is not a Kalman filter, not DSEM, and not a matrix `expm`. +/// `q / (−q / (2 a)) = −2 a` standardises the continuous diffusion +/// against `asymDIFFUSION`. Footnote 4 `discreteDIFFUSIONstd` is +/// `Q_Δt / (−q / (2 a))`. /// /// # Errors /// -/// Returns [`PsychometricError::EventTimeRequired`] for any non-event clock, -/// [`PsychometricError::NonPositiveInterval`] when `event_delta` is not -/// strictly positive, and [`PsychometricError::InvalidNumericInput`] when -/// the diffusion is negative or non-finite, the log-rate is non-finite, or -/// the mapped variance is non-finite. -pub fn recover_discrete_process_noise( +/// Always returns +/// [`PsychometricError::StandardisedContinuousDiffusionIsNotStandardisedDiscreteDiffusion`]. +pub fn refuse_standardised_continuous_diffusion_as_standardised_discrete_diffusion( + standardised_continuous_diffusion: f64, + standardised_discrete_diffusion: f64, +) -> Result { + let _ = ( + standardised_continuous_diffusion, + standardised_discrete_diffusion, + ); + Err(PsychometricError::StandardisedContinuousDiffusionIsNotStandardisedDiscreteDiffusion) +} + +/// Refuse treating Driver §7.1 trait-contaminated process noise as +/// p. 16 `discreteDIFFUSIONstd`. +/// +/// `Q_Δt / (trait + p + added)` mixes between-subject `TRAITVAR` +/// into the process-noise ratio. Footnote 4 standardises `DIFFUSION` +/// using only `asymDIFFUSION`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::TraitContaminatedProcessNoiseIsNotStandardisedDiscreteDiffusion`]. +pub fn refuse_trait_contaminated_process_noise_as_standardised_discrete_diffusion( + trait_contaminated_process_noise: f64, + standardised_discrete_diffusion: f64, +) -> Result { + let _ = ( + trait_contaminated_process_noise, + standardised_discrete_diffusion, + ); + Err(PsychometricError::TraitContaminatedProcessNoiseIsNotStandardisedDiscreteDiffusion) +} + +/// Exact scalar p. 16 `DIFFUSIONstd` after strictly positive +/// `asymDIFFUSION`. +/// +/// Driver, Oud, and Voelkle (2017, p. 16; Eq. 4, p. 5; footnote 4; +/// §7.1, pp. 18–19; JSS PDF re-opened 2026-08-23T13:20Z from +/// ) +/// print continuous-time parameters (e.g., `DRIFT`, `DIFFUSION`) and, +/// when appropriate, standardised matrices with the suffix `std`. +/// Footnote 4: standardisations use only the relevant variance, not +/// the total. Process noise is within-subject stochastic input, so +/// that relevant variance is `asymDIFFUSION`, the same footnote 4 +/// variance used for `DRIFT`. The scalar Lyapunov solution is +/// `−q / (2 a)` for stable `a < 0`. Form that strictly positive +/// within-subject variance first, then `q / (−q / (2 a))`. In the +/// scalar stationary case that ratio equals `−2 a` and does not +/// depend on `q` once `q > 0`. Unstandardised `q` is defined for +/// growing `a ≥ 0` and for zero diffusion; standardised `DIFFUSION` +/// is not. Zero `asymDIFFUSION` has no positive SD and fails closed. +/// The discrete standardisation `Q_Δt / (−q / (2 a)) = 1 − exp(2 a Δt)` +/// depends on the event interval and is not this continuous map. +/// Section 7.1 warns that omitting trait variance confounds between- +/// and within-person information. `q / (trait + p + added)` uses the +/// total, not `asymDIFFUSION`, and is not this map when `TRAITVAR` +/// is nonzero. `TRAITVAR` is not the standardisation variance. This +/// is not a Kalman filter, not a matrix `expm`, not DSEM, and not +/// ctsem estimation. +/// +/// # Errors +/// +/// Propagates [`recover_stationary_latent_variance`]. Returns +/// [`PsychometricError::EventTimeRequired`] for any non-event clock, +/// [`PsychometricError::StationaryVarianceRequiresStableDrift`] when +/// the log-rate is not strictly negative, +/// [`PsychometricError::StandardisedContinuousDiffusionRequiresPositiveWithinSubjectVariance`] +/// when `asymDIFFUSION` is zero, and +/// [`PsychometricError::InvalidNumericInput`] when an input is +/// non-finite or the mapped ratio is non-finite. +pub fn recover_standardised_continuous_diffusion( continuous_diffusion: f64, log_rate: f64, - event_delta: f64, clock: LagClock, ) -> Result { - if !clock.admits_structural_lag() { - return Err(PsychometricError::EventTimeRequired); + let within = recover_stationary_latent_variance(continuous_diffusion, log_rate, clock)?; + if within == 0.0 { + return Err( + PsychometricError::StandardisedContinuousDiffusionRequiresPositiveWithinSubjectVariance, + ); } - if !event_delta.is_finite() || event_delta <= 0.0 { - return Err(PsychometricError::NonPositiveInterval); - } - if !continuous_diffusion.is_finite() || continuous_diffusion < 0.0 || !log_rate.is_finite() { - return Err(PsychometricError::InvalidNumericInput); - } - // Driver Eq. 3: the integral of a zero diffusion is zero. - // Direct 0 * (expm1(z) / (2 a)) is NaN when expm1 overflows. - if continuous_diffusion == 0.0 { - return Ok(0.0); - } - if log_rate == 0.0 { - return require_finite(continuous_diffusion * event_delta); - } - // z = 2 (a Δt), not (2 a) Δt. 2 a overflows at |a| = 1e308 even - // when a Δt is finite (Driver Eq. 3 scalar closed form). - let drift_interval = log_rate * event_delta; - let increment_argument = 2.0 * drift_interval; - if increment_argument == 0.0 { - // Binary64 underflow of 2 a Δt. lim z→0 of Eq. 3 Q_Δt is q Δt. - return require_finite(continuous_diffusion * event_delta); - } - let increment = increment_argument.exp_m1(); - if increment.is_finite() { - // Q = q expm1(z) / (2 a) = 0.5 q (expm1(z) / a). Divide by - // the finite a, not by 2 a: 2 a overflows when |a| = 1e308. - // expm1(−∞) is −1, so this path also keeps −0.5 q / a. - return require_finite(0.5 * continuous_diffusion * (increment / log_rate)); - } - // expm1 overflowed. Finite z uses the log-space rewrite; a - // non-finite argument also fails closed through `require_finite`. - // Finite z, overflowed expm1. (q / (2 a))(exp(z) − 1) = - // sign(q / a) exp(ln|q| + z − ln|a| − ln 2) − 0.5 q / a. - // Driver Eq. 3 (JSS PDF re-opened 2026-08-18T03:07Z, p. 4): - // Q_Δt is that integral. If 0.5 q / a overflows, the rewrite - // scale is not finite and Q_Δt is not finite. - let half_scale = 0.5 * continuous_diffusion / log_rate; - if !half_scale.is_finite() { - return Err(PsychometricError::InvalidNumericInput); - } - let log_abs_dominant = continuous_diffusion.abs().ln() + increment_argument - - log_rate.abs().ln() - - std::f64::consts::LN_2; - let dominant = - require_finite(continuous_diffusion.signum() * log_rate.signum() * log_abs_dominant.exp())?; - require_finite(dominant - half_scale) + require_finite(continuous_diffusion / within) } -/// Exact scalar lagged latent covariance on event time. +/// Refuse treating unstandardised `DIFFUSION` as p. 16 +/// `DIFFUSIONstd`. /// -/// Driver, Oud, and Voelkle (2017, Eq. 3–4, pp. 4–5; JSS PDF -/// re-opened 2026-08-18T11:20Z) write `η(t) = exp(A Δt) η(t0) + … +` -/// the stochastic integral (Eq. 3) and that the integral exhibits -/// covariance `Q_Δt` (Eq. 4). The homogeneous-process consequence is -/// `cov(η_ti, η_{t-1,i}) = A_Δt cov(η_{t-1,i})`. The scalar map is -/// `exp(a Δt) p` with prior variance `p ≥ 0`. This is not `Q_Δt`. -/// Binary64 underflow of `exp(a Δt)` to `+0` is a vanishing -/// covariance and is kept. A zero prior variance is exactly zero even -/// if the exponential overflows. When `exp(a Δt)` overflows at a -/// finite `a Δt`, rewrite as `exp(ln p + a Δt)`. An overflowing -/// rewrite fails closed. A finite `exp(a Δt)` whose product with `p` -/// overflows also fails closed. The JSS article has no numbered §2.2. -/// This is not a Kalman filter, not DSEM, and not a matrix `expm`. +/// `q` is defined for growing and zero-diffusion processes. +/// Footnote 4 `DIFFUSIONstd` requires strictly positive +/// `asymDIFFUSION`. Equal numbers would still be distinct named +/// quantities. /// /// # Errors /// -/// Returns [`PsychometricError::EventTimeRequired`] for any non-event clock, -/// [`PsychometricError::NonPositiveInterval`] when `event_delta` is not -/// strictly positive, and [`PsychometricError::InvalidNumericInput`] when -/// the prior variance is negative or non-finite, the log-rate is -/// non-finite, or the mapped covariance is non-finite. -pub fn recover_discrete_lagged_latent_covariance( - prior_variance: f64, - log_rate: f64, - event_delta: f64, - clock: LagClock, +/// Always returns +/// [`PsychometricError::UnstandardisedContinuousDiffusionIsNotStandardisedContinuousDiffusion`]. +pub fn refuse_unstandardised_continuous_diffusion_as_standardised_continuous_diffusion( + unstandardised_continuous_diffusion: f64, + standardised_continuous_diffusion: f64, ) -> Result { - if !clock.admits_structural_lag() { - return Err(PsychometricError::EventTimeRequired); - } - if !event_delta.is_finite() || event_delta <= 0.0 { - return Err(PsychometricError::NonPositiveInterval); - } - if !prior_variance.is_finite() || prior_variance < 0.0 || !log_rate.is_finite() { - return Err(PsychometricError::InvalidNumericInput); - } - // 0 * +∞ is NaN. Driver Eq. 3: A_Δt * 0 = 0. - if prior_variance == 0.0 { - return Ok(0.0); - } - let drift_interval = log_rate * event_delta; - let auto_effect = drift_interval.exp(); - if auto_effect.is_finite() { - // +0 underflow is a vanishing lagged covariance. - return require_finite(auto_effect * prior_variance); - } - // Overflow of a finite `a Δt` is the log-space rewrite. - // A non-finite argument also fails closed through `require_finite`. - require_finite((prior_variance.ln() + drift_interval).exp()) + let _ = ( + unstandardised_continuous_diffusion, + standardised_continuous_diffusion, + ); + Err(PsychometricError::UnstandardisedContinuousDiffusionIsNotStandardisedContinuousDiffusion) } -/// Exact scalar discrete latent variance on event time. +/// Refuse treating p. 16 `discreteDIFFUSIONstd` as p. 16 +/// `DIFFUSIONstd`. /// -/// Driver, Oud, and Voelkle (2017, Eq. 3–4, pp. 4–5; JSS PDF -/// re-opened 2026-08-18T11:20Z) write `Q_Δt` as the covariance of the -/// stochastic integral (Eq. 4) after the homogeneous map -/// `η(t) = exp(A Δt) η(t0) + …` (Eq. 3). That pair is -/// `Q_Δt = cov(η_ti | η_{t-1,i})` and -/// `cov(η_ti, η_{t-1,i}) = A_Δt cov(η_{t-1,i})` when `ξ` and `z` are -/// given. The law of total variance on that pair is -/// `Var(η_ti) = A_Δt Var(η_{t-1,i}) A_Δt⊤ + Q_Δt`. The scalar map is -/// `exp(2 a Δt) p + Q_Δt`. This is not `Q_Δt` alone and not a Kalman -/// measurement update. A zero prior variance is exactly `Q_Δt`. -/// Binary64 underflow of `exp(2 a Δt)` keeps `Q_Δt`. When `exp(z)` -/// overflows at a finite `z = 2 (a Δt)`, rewrite as -/// `exp(ln p + z) + Q_Δt`. An overflowing rewrite fails closed. -/// A finite `exp(z) p` whose sum with `Q_Δt` overflows fails closed. -/// A zero diffusion skips the process-noise `z → +∞` refusal; the -/// carried term `exp(2 a Δt) p` is then still non-finite when -/// `2 (a Δt)` overflows to `+∞` (`p = 2`, `q = 0`, `a = 1e308`, -/// `Δt = 2`) and fails closed. The JSS article has no numbered §2.2. +/// `Q_Δt / (−q / (2 a)) = 1 − exp(2 a Δt)` depends on the event +/// interval. Footnote 4 `DIFFUSIONstd` is `q / (−q / (2 a)) = −2 a`. /// /// # Errors /// -/// Propagates [`recover_discrete_process_noise`]. Returns -/// [`PsychometricError::InvalidNumericInput`] when the prior variance is -/// negative or non-finite or the mapped variance is non-finite. -pub fn recover_discrete_latent_variance( - prior_variance: f64, - continuous_diffusion: f64, - log_rate: f64, - event_delta: f64, - clock: LagClock, +/// Always returns +/// [`PsychometricError::StandardisedDiscreteDiffusionIsNotStandardisedContinuousDiffusion`]. +pub fn refuse_standardised_discrete_diffusion_as_standardised_continuous_diffusion( + standardised_discrete_diffusion: f64, + standardised_continuous_diffusion: f64, ) -> Result { - let process_noise = - recover_discrete_process_noise(continuous_diffusion, log_rate, event_delta, clock)?; - if !prior_variance.is_finite() || prior_variance < 0.0 { - return Err(PsychometricError::InvalidNumericInput); - } - if prior_variance == 0.0 { - return Ok(process_noise); - } - let increment_argument = 2.0 * (log_rate * event_delta); - if increment_argument == 0.0 { - return require_finite(prior_variance + process_noise); - } - let auto_effect_square = increment_argument.exp(); - if auto_effect_square.is_finite() { - return require_finite(auto_effect_square * prior_variance + process_noise); - } - // `e^{2 a Δt}` overflow of a finite `2 a Δt` is the log-space rewrite. - // A non-finite argument also fails closed through `require_finite`. - let carried = require_finite((prior_variance.ln() + increment_argument).exp())?; - require_finite(carried + process_noise) + let _ = ( + standardised_discrete_diffusion, + standardised_continuous_diffusion, + ); + Err(PsychometricError::StandardisedDiscreteDiffusionIsNotStandardisedContinuousDiffusion) } -/// Exact scalar stationary within-subject variance on event time. +/// Refuse treating Driver §7.1 trait-contaminated continuous +/// diffusion as p. 16 `DIFFUSIONstd`. /// -/// Driver, Oud, and Voelkle (2017, Eq. 4, p. 5; JSS PDF re-opened -/// 2026-08-19T04:10Z) write `Q_Δt` as -/// `irow(A#^{-1}[e^{A# Δt} − I] row(Q))` with `A# = A ⊗ I + I ⊗ A`. -/// The scalar Kronecker sum is `2 a`. As `Δt → ∞` with stable -/// `a < 0`, `e^{2 a Δt} → 0` and Eq. 4 becomes `-q / (2 a)`. The -/// JSS summary names that limit `asymDIFFUSION` and takes it as the -/// total within-subject variance (p. 16). Section 4.3 (pp. 9–10) -/// constrains a stationary `T0VAR` to that same model-predicted -/// variance. When `2 a` is finite, form `q / -(2 a)` so `q / a` -/// overflow does not lose a finite Lyapunov solution (`q = MAX`, -/// `a = -0.75` → `MAX / 1.5`; `CodeRabbit` on `75ecdd3`). When `2 a` -/// overflows, form `(q / a) * -0.5`. Do not form `2 a` as the only -/// path: at `a = -1e308`, `q = 1e308`, `2 a` overflows and -/// `-q / (2 a)` collapses to `+0`, but `(q / a) * -0.5 = 0.5`. Do -/// not form `0.5 q` first: at `q = from_bits(1)`, `a = -from_bits(1)`, -/// `-0.5 * q` underflows to `-0` and the quotient is `+0`, but -/// the representable Lyapunov solution is `0.5`. A zero diffusion is -/// exactly zero. `a ≥ 0` has no finite stationary variance -/// (including Brownian `a = 0`, whose variance grows as `q Δt`). -/// An overflowing Lyapunov solution fails closed. This is not a Kalman -/// filter, not DSEM, not a matrix `expm`, and not ctsem estimation. +/// `q / (trait + p + added)` mixes between-subject `TRAITVAR` into +/// the continuous-diffusion ratio. Footnote 4 standardises +/// `DIFFUSION` using only `asymDIFFUSION`. /// /// # Errors /// -/// Returns [`PsychometricError::EventTimeRequired`] for any non-event -/// clock, [`PsychometricError::StationaryVarianceRequiresStableDrift`] -/// when the log-rate is not strictly negative, and -/// [`PsychometricError::InvalidNumericInput`] when the diffusion is -/// negative or non-finite, the log-rate is non-finite, or the mapped -/// variance is non-finite. -pub fn recover_stationary_latent_variance( +/// Always returns +/// [`PsychometricError::TraitContaminatedContinuousDiffusionIsNotStandardisedContinuousDiffusion`]. +pub fn refuse_trait_contaminated_continuous_diffusion_as_standardised_continuous_diffusion( + trait_contaminated_continuous_diffusion: f64, + standardised_continuous_diffusion: f64, +) -> Result { + let _ = ( + trait_contaminated_continuous_diffusion, + standardised_continuous_diffusion, + ); + Err(PsychometricError::TraitContaminatedContinuousDiffusionIsNotStandardisedContinuousDiffusion) +} + +/// Exact scalar p. 16 `DRIFTstd` after strictly positive +/// `asymDIFFUSION`. +/// +/// Driver, Oud, and Voelkle (2017, p. 16; Eq. 1, p. 4; footnote 4; +/// §7.1, pp. 18–19; JSS PDF re-opened 2026-08-23T13:28Z from +/// ) +/// print continuous-time parameters (e.g., `DRIFT`) and, when +/// appropriate, standardised matrices with the suffix `std`. +/// Footnote 4: standardisations use only the relevant variance, not +/// the total. For `DRIFT`, that relevant variance is the +/// within-subject variance `asymDIFFUSION`, because `DRIFT` is +/// intended to represent individual, or average individual, temporal +/// dynamics. The scalar Lyapunov solution is `−q / (2 a)` for +/// stable `a < 0`. Form that strictly positive within-subject +/// variance first. In the scalar stationary case the within-subject +/// SD ratio is 1, so the standardised auto-effect equals the +/// unstandardised log-rate numerically; those remain distinct named +/// quantities. Unstandardised `a` is defined for growing `a ≥ 0` and +/// for zero diffusion; standardised `DRIFT` is not. Zero +/// `asymDIFFUSION` has no positive SD and fails closed. The discrete +/// standardisation `e^{a Δt}` depends on the event interval and is +/// not this continuous map. Section 7.1 warns that omitting trait +/// variance confounds between- and within-person information. The +/// instantaneous mixed auto-effect `a p / (trait + p + added)` uses +/// the total, not `asymDIFFUSION`, and is not this map when +/// `TRAITVAR` is nonzero. `TRAITVAR` is not the standardisation +/// variance. This is not a Kalman filter, not a matrix `expm`, not +/// DSEM, and not ctsem estimation. +/// +/// # Errors +/// +/// Propagates [`recover_stationary_latent_variance`]. Returns +/// [`PsychometricError::EventTimeRequired`] for any non-event clock, +/// [`PsychometricError::StationaryVarianceRequiresStableDrift`] when +/// the log-rate is not strictly negative, +/// [`PsychometricError::StandardisedContinuousDriftRequiresPositiveWithinSubjectVariance`] +/// when `asymDIFFUSION` is zero, and +/// [`PsychometricError::InvalidNumericInput`] when an input is +/// non-finite. +pub fn recover_standardised_continuous_drift( continuous_diffusion: f64, log_rate: f64, clock: LagClock, ) -> Result { - if !clock.admits_structural_lag() { - return Err(PsychometricError::EventTimeRequired); - } - if !continuous_diffusion.is_finite() || continuous_diffusion < 0.0 || !log_rate.is_finite() { - return Err(PsychometricError::InvalidNumericInput); - } - if log_rate >= 0.0 { - return Err(PsychometricError::StationaryVarianceRequiresStableDrift); - } - // Driver Eq. 4 as Δt → ∞: (0 − 1) q / (2 a) = −q / (2 a). - // Direct 0 * (1 / (2 a)) is not needed; a zero diffusion is zero. - if continuous_diffusion == 0.0 { - return Ok(0.0); + let within = recover_stationary_latent_variance(continuous_diffusion, log_rate, clock)?; + if within == 0.0 { + return Err( + PsychometricError::StandardisedContinuousDriftRequiresPositiveWithinSubjectVariance, + ); } - // −q / (2 a). When 2 a is finite, divide by that Kronecker sum - // so q/a overflow does not lose a finite Lyapunov solution - // (q = MAX, a = −0.75 → MAX/1.5; CodeRabbit on 75ecdd3). - // When 2 a overflows (|a| = 1e308), form (q/a)*−0.5 instead. - // Do not form 0.5 q first (min-subnormal underflow). - let twice_rate = log_rate * 2.0; - let stationary = if twice_rate.is_finite() { - continuous_diffusion / -twice_rate - } else { - (continuous_diffusion / log_rate) * -0.5 - }; - require_finite(stationary) + require_finite(log_rate) } -/// Refuse treating finite-interval process noise as `asymDIFFUSION`. +/// Refuse treating unstandardised `DRIFT` as p. 16 `DRIFTstd`. /// -/// Driver, Oud, and Voelkle (2017, Eq. 4 and p. 16): `Q_Δt` at a -/// finite event interval is the covariance of the stochastic integral -/// over that interval. The asymptotic within-subject variance is the -/// `Δt → ∞` limit. Section 4.3 distinguishes that stationary -/// constraint from a predetermined `T0VAR`. +/// `a` is defined for growing and zero-diffusion processes. +/// Footnote 4 `DRIFTstd` requires strictly positive +/// `asymDIFFUSION`. Equal numbers in the scalar stationary case are +/// still distinct named quantities. /// /// # Errors /// /// Always returns -/// [`PsychometricError::FiniteIntervalProcessNoiseIsNotStationary`]. -pub fn refuse_finite_interval_process_noise_as_stationary_variance( - process_noise: f64, - event_delta: f64, +/// [`PsychometricError::UnstandardisedContinuousDriftIsNotStandardisedContinuousDrift`]. +pub fn refuse_unstandardised_continuous_drift_as_standardised_continuous_drift( + unstandardised_continuous_drift: f64, + standardised_continuous_drift: f64, ) -> Result { - let _ = (process_noise, event_delta); - Err(PsychometricError::FiniteIntervalProcessNoiseIsNotStationary) + let _ = ( + unstandardised_continuous_drift, + standardised_continuous_drift, + ); + Err(PsychometricError::UnstandardisedContinuousDriftIsNotStandardisedContinuousDrift) } -/// Exact scalar trait-plus-state latent variance. +/// Refuse treating p. 16 `discreteDRIFTstd` as p. 16 `DRIFTstd`. /// -/// Driver, Oud, and Voelkle (2017, §4.3, p. 9; JSS PDF re-opened -/// 2026-08-18T21:07Z) add a stable trait process with `DRIFT` and -/// `DIFFUSION` fixed to zero. The scalar sum is `trait + state`. The -/// ctsem `TRAITVAR` parameterization that adds the trait to the -/// `DIFFUSION` matrix is a software rewrite; it does not license -/// treating trait variance as process noise. This is not RI-CLPM, not -/// a Kalman filter, and not ctsem estimation. +/// `e^{a Δt}` depends on the event interval. Footnote 4 `DRIFTstd` +/// is the continuous auto-effect after strictly positive +/// `asymDIFFUSION`. /// /// # Errors /// -/// Returns [`PsychometricError::InvalidNumericInput`] when either -/// variance is negative or non-finite, or the sum overflows. -pub fn recover_trait_plus_state_latent_variance( - trait_variance: f64, - state_variance: f64, +/// Always returns +/// [`PsychometricError::StandardisedDiscreteDriftIsNotStandardisedContinuousDrift`]. +pub fn refuse_standardised_discrete_drift_as_standardised_continuous_drift( + standardised_discrete_drift: f64, + standardised_continuous_drift: f64, ) -> Result { - if !trait_variance.is_finite() || trait_variance < 0.0 { - return Err(PsychometricError::InvalidNumericInput); - } - if !state_variance.is_finite() || state_variance < 0.0 { - return Err(PsychometricError::InvalidNumericInput); - } - if trait_variance == 0.0 { - return Ok(state_variance); - } - if state_variance == 0.0 { - return Ok(trait_variance); - } - require_finite(trait_variance + state_variance) + let _ = (standardised_discrete_drift, standardised_continuous_drift); + Err(PsychometricError::StandardisedDiscreteDriftIsNotStandardisedContinuousDrift) } -/// Exact scalar trait-plus-state lagged latent covariance. +/// Refuse treating Driver §7.1 trait-contaminated continuous drift +/// as p. 16 `DRIFTstd`. /// -/// Driver, Oud, and Voelkle (2017, §4.3, p. 9): a stable trait has no -/// temporal dynamics, so `cov(trait_t, trait_{t-1}) = trait`. The -/// state lagged covariance remains `exp(a Δt) p` (Eq. 3–4). The -/// scalar sum is `trait + exp(a Δt) p`. Evolving the summed variance -/// as if it were all state is not this map. This is not RI-CLPM. +/// `a p / (trait + p + added)` mixes between-subject `TRAITVAR` +/// into the continuous auto-effect. Footnote 4 standardises `DRIFT` +/// using only `asymDIFFUSION`. /// /// # Errors /// -/// Propagates [`recover_discrete_lagged_latent_covariance`]. Returns -/// [`PsychometricError::InvalidNumericInput`] when the trait variance -/// is negative or non-finite or the sum overflows. -pub fn recover_trait_plus_state_lagged_covariance( - trait_variance: f64, - state_prior_variance: f64, +/// Always returns +/// [`PsychometricError::TraitContaminatedContinuousDriftIsNotStandardisedContinuousDrift`]. +pub fn refuse_trait_contaminated_continuous_drift_as_standardised_continuous_drift( + trait_contaminated_continuous_drift: f64, + standardised_continuous_drift: f64, +) -> Result { + let _ = ( + trait_contaminated_continuous_drift, + standardised_continuous_drift, + ); + Err(PsychometricError::TraitContaminatedContinuousDriftIsNotStandardisedContinuousDrift) +} + +/// Exact scalar p. 16 `asymTIPREDEFFECTstd` after strictly positive +/// `asymDIFFUSION` and strictly positive predictor variance. +/// +/// Driver, Oud, and Voelkle (2017, p. 16; §7.2, pp. 20–21; Eq. 3, +/// p. 5; Table 2, p. 12; footnote 4; JSS PDF re-opened +/// 2026-08-23T14:25Z from +/// ) +/// print continuous-time parameters and, when appropriate, +/// standardised matrices with the suffix `std`. Section 7.2 names +/// `asymTIPREDEFFECT` the expected total change in process means +/// given a unit increase on a time-independent predictor. The scalar +/// map is `-B / a` for stable `a < 0`. Footnote 4: standardisations +/// use only the relevant variance, not the total. The affecting +/// variance is predictor variance `TIPREDVAR` `v`. The affected +/// variance is within-subject `asymDIFFUSION` `-q / (2 a)`, because +/// the process dynamics are individual, or average individual, +/// temporal dynamics. Form strictly positive `asymDIFFUSION` first, +/// then strictly positive `v`, then the unit asymptotic effect, then +/// `(-B / a) · √v / √(-q / (2 a))`. Unstandardised `-B / a` is +/// defined for a zero coefficient and for zero predictor variance; +/// standardised `asymTIPREDEFFECT` is not. Zero `asymDIFFUSION` or +/// zero `v` has no positive SD and fails closed. The finite-interval +/// standardisation `A^{-1}[e^{A Δt} − I] B · √v / √p` depends on the +/// event interval and is not this `Δt → ∞` map. Section 7.1 warns +/// that omitting trait variance confounds between- and within-person +/// information. `(-B / a) · √v / √(trait + p + added)` uses the +/// total, not `asymDIFFUSION`, and is not this map when `TRAITVAR` +/// is nonzero. `TRAITVAR` is not the standardisation variance. This +/// is not a Kalman filter, not a matrix `expm`, not DSEM, and not +/// ctsem estimation. +/// +/// # Errors +/// +/// Propagates [`recover_stationary_latent_variance`] and +/// [`recover_asymptotic_time_independent_predictor_effect`]. Returns +/// [`PsychometricError::EventTimeRequired`] for any non-event clock, +/// [`PsychometricError::StationaryVarianceRequiresStableDrift`] when +/// the log-rate is not strictly negative, +/// [`PsychometricError::StandardisedAsymptoticTimeIndependentEffectRequiresPositiveWithinSubjectVariance`] +/// when `asymDIFFUSION` is zero, +/// [`PsychometricError::StandardisedAsymptoticTimeIndependentEffectRequiresPositivePredictorVariance`] +/// when predictor variance is zero, and +/// [`PsychometricError::InvalidNumericInput`] when an input is +/// non-finite, predictor variance is negative, or the product +/// overflows. +pub fn recover_standardised_asymptotic_time_independent_predictor_effect( + time_independent_effect: f64, + predictor_variance: f64, + continuous_diffusion: f64, log_rate: f64, - event_delta: f64, clock: LagClock, ) -> Result { - if !trait_variance.is_finite() || trait_variance < 0.0 { + let within = recover_stationary_latent_variance(continuous_diffusion, log_rate, clock)?; + if within == 0.0 { + return Err( + PsychometricError::StandardisedAsymptoticTimeIndependentEffectRequiresPositiveWithinSubjectVariance, + ); + } + if !predictor_variance.is_finite() || predictor_variance < 0.0 { return Err(PsychometricError::InvalidNumericInput); } - let state_lagged = recover_discrete_lagged_latent_covariance( - state_prior_variance, + if predictor_variance == 0.0 { + return Err( + PsychometricError::StandardisedAsymptoticTimeIndependentEffectRequiresPositivePredictorVariance, + ); + } + let unit_effect = recover_asymptotic_time_independent_predictor_effect( + time_independent_effect, + 1.0, log_rate, - event_delta, clock, )?; - if trait_variance == 0.0 { - return Ok(state_lagged); - } - require_finite(trait_variance + state_lagged) + let process_sd = within.sqrt(); + let predictor_sd = predictor_variance.sqrt(); + let ratio = require_finite(predictor_sd / process_sd)?; + require_finite(unit_effect * ratio) } -/// Refuse treating Driver §4.3 trait variance as process noise. +/// Refuse treating unstandardised `asymTIPREDEFFECT` as p. 16 +/// `asymTIPREDEFFECTstd`. /// -/// A stable trait has `DIFFUSION` fixed to zero. The ctsem -/// `TRAITVAR` rewrite that adds the trait to `DIFFUSION` is not a -/// license to treat trait variance as `Q_Δt`. +/// `-B / a` is defined for a zero coefficient and for zero +/// predictor variance. Footnote 4 `asymTIPREDEFFECTstd` requires +/// strictly positive `asymDIFFUSION` and strictly positive `v`. +/// Equal numbers when `v = p` are still distinct named quantities. /// /// # Errors /// -/// Always returns [`PsychometricError::TraitVarianceIsNotProcessNoise`]. -pub fn refuse_trait_variance_as_process_noise( - trait_variance: f64, - process_noise: f64, +/// Always returns +/// [`PsychometricError::UnstandardisedAsymptoticTimeIndependentEffectIsNotStandardisedAsymptoticTimeIndependentEffect`]. +pub fn refuse_unstandardised_asymptotic_time_independent_effect_as_standardised_asymptotic_time_independent_effect( + unstandardised_asymptotic_effect: f64, + standardised_asymptotic_effect: f64, ) -> Result { - let _ = (trait_variance, process_noise); - Err(PsychometricError::TraitVarianceIsNotProcessNoise) + let _ = ( + unstandardised_asymptotic_effect, + standardised_asymptotic_effect, + ); + Err(PsychometricError::UnstandardisedAsymptoticTimeIndependentEffectIsNotStandardisedAsymptoticTimeIndependentEffect) } -/// Refuse treating Driver §4.3 trait variance as `asymDIFFUSION`. +/// Refuse treating a finite-interval standardised `TIPREDEFFECT` +/// as p. 16 `asymTIPREDEFFECTstd`. /// -/// Trait variance is time-invariant between-subject variance. The -/// stationary within-subject variance is the `Δt → ∞` limit of Eq. 4. +/// `A^{-1}[e^{A Δt} − I] B · √v / √p` depends on the event +/// interval. Footnote 4 `asymTIPREDEFFECTstd` is +/// `(-B / a) · √v / √(-q / (2 a))`. /// /// # Errors /// /// Always returns -/// [`PsychometricError::TraitVarianceIsNotStationaryWithinSubject`]. -pub fn refuse_trait_variance_as_stationary_within_subject( - trait_variance: f64, - stationary_state_variance: f64, +/// [`PsychometricError::StandardisedDiscreteTimeIndependentEffectIsNotStandardisedAsymptoticTimeIndependentEffect`]. +pub fn refuse_standardised_discrete_time_independent_effect_as_standardised_asymptotic_time_independent_effect( + standardised_discrete_effect: f64, + standardised_asymptotic_effect: f64, ) -> Result { - let _ = (trait_variance, stationary_state_variance); - Err(PsychometricError::TraitVarianceIsNotStationaryWithinSubject) + let _ = (standardised_discrete_effect, standardised_asymptotic_effect); + Err(PsychometricError::StandardisedDiscreteTimeIndependentEffectIsNotStandardisedAsymptoticTimeIndependentEffect) } -/// Exact scalar observed-indicator variance from Driver Equation 5 -/// with `MANIFESTTRAITVAR = 0`. +/// Refuse treating Driver §7.1 trait-contaminated asymptotic +/// time-independent predictor effect as p. 16 +/// `asymTIPREDEFFECTstd`. /// -/// Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Table 2, p. 12; JSS -/// PDF re-opened 2026-08-19T04:18Z) write `y_i(t) = τ_i + Λ η_i(t) + -/// ε_i(t)` with `ε ~ N(0, Θ)` and `τ_i ~ N(μ_τ, Ψ_τ)`. Equation 1 -/// (p. 4) is the latent SDE, not the measurement model. Table 2 names -/// `Θ` `MANIFESTVAR` and `Ψ_τ` `MANIFESTTRAITVAR`. The p. 16 summary -/// restates those names; it is not the equation. With `Ψ_τ = 0` the -/// scalar map is `Var(y) = λ² Var(η) + θ`. Form `(λ p) λ` then add -/// `θ`. Do not form `λ²` first: at `λ = 1e308`, `p = 1e-308`, `λ²` -/// overflows and `λ² p` is non-finite, but `(λ p) λ = 1e308`. A zero -/// loading or zero latent variance is exactly `θ`. A zero -/// measurement error is exactly `λ² p`. Negative latent or -/// measurement-error variance fails closed. An overflowing product -/// or sum fails closed. This is not a Kalman filter, not ESEM -/// estimation, and not ctsem estimation. +/// `(-B / a) · √v / √(trait + p + added)` mixes between-subject +/// `TRAITVAR` into the affected SD. Footnote 4 standardises using +/// only `asymDIFFUSION`. /// /// # Errors /// -/// Returns [`PsychometricError::InvalidNumericInput`] when the loading -/// is non-finite, either variance is negative or non-finite, or the -/// mapped variance is non-finite. -pub fn recover_manifest_observed_variance( - loading: f64, - latent_variance: f64, - measurement_error_variance: f64, +/// Always returns +/// [`PsychometricError::TraitContaminatedAsymptoticTimeIndependentEffectIsNotStandardisedAsymptoticTimeIndependentEffect`]. +pub fn refuse_trait_contaminated_asymptotic_time_independent_effect_as_standardised_asymptotic_time_independent_effect( + trait_contaminated_asymptotic_effect: f64, + standardised_asymptotic_effect: f64, ) -> Result { - if !loading.is_finite() - || !latent_variance.is_finite() - || latent_variance < 0.0 - || !measurement_error_variance.is_finite() - || measurement_error_variance < 0.0 - { - return Err(PsychometricError::InvalidNumericInput); + let _ = ( + trait_contaminated_asymptotic_effect, + standardised_asymptotic_effect, + ); + Err(PsychometricError::TraitContaminatedAsymptoticTimeIndependentEffectIsNotStandardisedAsymptoticTimeIndependentEffect) +} + +/// Exact scalar p. 16 `TIPREDEFFECTstd` after strictly positive +/// `asymDIFFUSION` and strictly positive predictor variance. +/// +/// Driver, Oud, and Voelkle (2017, p. 16; §7.2, pp. 20–21; Eq. 3, +/// p. 5; Table 2, p. 12; footnote 4; JSS PDF re-opened +/// 2026-08-23T16:21Z from +/// ) +/// print continuous-time parameters and, when appropriate, +/// standardised matrices with the suffix `std`. Table 2 names `B` +/// `TIPREDEFFECT`. Footnote 4: standardisations use only the +/// relevant variance, not the total. The affecting variance is +/// predictor variance `TIPREDVAR` `v`. The affected variance is +/// within-subject `asymDIFFUSION` `-q / (2 a)`, because the process +/// dynamics are individual, or average individual, temporal +/// dynamics. Form strictly positive `asymDIFFUSION` first, then +/// strictly positive `v`, then `B · √v / √(-q / (2 a))`. +/// Unstandardised `B` is defined for a zero coefficient and for +/// zero predictor variance; standardised `TIPREDEFFECT` is not. +/// Zero `asymDIFFUSION` or zero `v` has no positive SD and fails +/// closed. The asymptotic standardisation +/// `(-B / a) · √v / √(-q / (2 a))` is the total change, not this +/// continuous coefficient. The finite-interval standardisation +/// `A^{-1}[e^{A Δt} − I] B · √v / √p` depends on the event interval +/// and is not this continuous map. Section 7.1 warns that omitting +/// trait variance confounds between- and within-person information. +/// `B · √v / √(trait + p + added)` uses the total, not +/// `asymDIFFUSION`, and is not this map when `TRAITVAR` is nonzero. +/// `TRAITVAR` is not the standardisation variance. This is not a +/// Kalman filter, not a matrix `expm`, not DSEM, and not ctsem +/// estimation. +/// +/// # Errors +/// +/// Propagates [`recover_stationary_latent_variance`]. Returns +/// [`PsychometricError::EventTimeRequired`] for any non-event clock, +/// [`PsychometricError::StationaryVarianceRequiresStableDrift`] when +/// the log-rate is not strictly negative, +/// [`PsychometricError::StandardisedContinuousTimeIndependentEffectRequiresPositiveWithinSubjectVariance`] +/// when `asymDIFFUSION` is zero, +/// [`PsychometricError::StandardisedContinuousTimeIndependentEffectRequiresPositivePredictorVariance`] +/// when predictor variance is zero, and +/// [`PsychometricError::InvalidNumericInput`] when an input is +/// non-finite, predictor variance is negative, or the product +/// overflows. +pub fn recover_standardised_continuous_time_independent_predictor_effect( + time_independent_effect: f64, + predictor_variance: f64, + continuous_diffusion: f64, + log_rate: f64, + clock: LagClock, +) -> Result { + let within = recover_stationary_latent_variance(continuous_diffusion, log_rate, clock)?; + if within == 0.0 { + return Err( + PsychometricError::StandardisedContinuousTimeIndependentEffectRequiresPositiveWithinSubjectVariance, + ); } - if loading == 0.0 || latent_variance == 0.0 { - return Ok(measurement_error_variance); + if !predictor_variance.is_finite() || predictor_variance < 0.0 { + return Err(PsychometricError::InvalidNumericInput); } - let explained = require_finite((loading * latent_variance) * loading)?; - if measurement_error_variance == 0.0 { - return Ok(explained); + if predictor_variance == 0.0 { + return Err( + PsychometricError::StandardisedContinuousTimeIndependentEffectRequiresPositivePredictorVariance, + ); } - require_finite(explained + measurement_error_variance) + let coefficient = require_finite(time_independent_effect)?; + let process_sd = within.sqrt(); + let predictor_sd = predictor_variance.sqrt(); + let ratio = require_finite(predictor_sd / process_sd)?; + require_finite(coefficient * ratio) } -/// Refuse treating Driver Eq. 5 measurement error as `Var(y)`. +/// Refuse treating unstandardised `TIPREDEFFECT` as p. 16 +/// `TIPREDEFFECTstd`. /// -/// Table 2 (p. 12) names `MANIFESTVAR` as `Θ`, the variance of `ε`. -/// Equation 5 maps `Var(y) = λ² Var(η) + θ` when `Ψ_τ = 0`. +/// `B` is defined for a zero coefficient and for zero predictor +/// variance. Footnote 4 `TIPREDEFFECTstd` requires strictly +/// positive `asymDIFFUSION` and strictly positive `v`. Equal +/// numbers when `v = p` are still distinct named quantities. /// /// # Errors /// /// Always returns -/// [`PsychometricError::MeasurementErrorIsNotObservedVariance`]. -pub fn refuse_measurement_error_as_observed_variance( - measurement_error_variance: f64, - observed_variance: f64, +/// [`PsychometricError::UnstandardisedContinuousTimeIndependentEffectIsNotStandardisedContinuousTimeIndependentEffect`]. +pub fn refuse_unstandardised_continuous_time_independent_effect_as_standardised_continuous_time_independent_effect( + unstandardised_continuous_effect: f64, + standardised_continuous_effect: f64, ) -> Result { - let _ = (measurement_error_variance, observed_variance); - Err(PsychometricError::MeasurementErrorIsNotObservedVariance) + let _ = ( + unstandardised_continuous_effect, + standardised_continuous_effect, + ); + Err(PsychometricError::UnstandardisedContinuousTimeIndependentEffectIsNotStandardisedContinuousTimeIndependentEffect) } -/// Refuse treating Driver Eq. 5 latent variance as `Var(y)`. +/// Refuse treating p. 16 `asymTIPREDEFFECTstd` as p. 16 +/// `TIPREDEFFECTstd`. /// -/// `Var(η)` is the latent process variance. Equation 5 maps -/// `Var(y) = λ² Var(η) + θ` when `Ψ_τ = 0`. +/// `(-B / a) · √v / √(-q / (2 a))` is the standardised expected +/// total change. Footnote 4 `TIPREDEFFECTstd` is the continuous +/// coefficient `B · √v / √(-q / (2 a))`. /// /// # Errors /// -/// Always returns [`PsychometricError::LatentVarianceIsNotObservedVariance`]. -pub fn refuse_latent_variance_as_observed_variance( - latent_variance: f64, - observed_variance: f64, +/// Always returns +/// [`PsychometricError::StandardisedAsymptoticTimeIndependentEffectIsNotStandardisedContinuousTimeIndependentEffect`]. +pub fn refuse_standardised_asymptotic_time_independent_effect_as_standardised_continuous_time_independent_effect( + standardised_asymptotic_effect: f64, + standardised_continuous_effect: f64, ) -> Result { - let _ = (latent_variance, observed_variance); - Err(PsychometricError::LatentVarianceIsNotObservedVariance) + let _ = ( + standardised_asymptotic_effect, + standardised_continuous_effect, + ); + Err(PsychometricError::StandardisedAsymptoticTimeIndependentEffectIsNotStandardisedContinuousTimeIndependentEffect) } -/// Exact scalar observed-indicator variance from Driver Equation 5 -/// with nonzero `MANIFESTTRAITVAR`. +/// Refuse treating a finite-interval standardised `TIPREDEFFECT` +/// as p. 16 `TIPREDEFFECTstd`. /// -/// Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Table 2, p. 12; JSS -/// PDF re-opened 2026-08-19T04:18Z) write `τ_i ~ N(μ_τ, Ψ_τ)` on the -/// indicator intercept. The scalar map is `Var(y) = λ² Var(η) + θ + -/// ψ`. Form the `Ψ_τ = 0` map first, then add `ψ`. Do not form -/// `λ²` first. A zero manifest trait is exactly `λ² p + θ`. A zero -/// loading or zero latent variance is exactly `θ + ψ`. `Ψ_τ` is not -/// `Θ`: Table 2 names `MANIFESTTRAITVAR` separately from -/// `MANIFESTVAR`. `TRAITVAR` is latent additional variance and is -/// scaled by `λ²`; `MANIFESTTRAITVAR` is not. Negative trait -/// variance fails closed. An overflowing sum fails closed. This is -/// not a Kalman filter, not ESEM estimation, and not ctsem -/// estimation. +/// `A^{-1}[e^{A Δt} − I] B · √v / √p` depends on the event +/// interval. Footnote 4 `TIPREDEFFECTstd` is +/// `B · √v / √(-q / (2 a))`. /// /// # Errors /// -/// Propagates [`recover_manifest_observed_variance`]. Returns -/// [`PsychometricError::InvalidNumericInput`] when the manifest-trait -/// variance is negative or non-finite or the sum overflows. -pub fn recover_manifest_trait_plus_state_observed_variance( - loading: f64, - latent_variance: f64, - measurement_error_variance: f64, - manifest_trait_variance: f64, +/// Always returns +/// [`PsychometricError::StandardisedDiscreteTimeIndependentEffectIsNotStandardisedContinuousTimeIndependentEffect`]. +pub fn refuse_standardised_discrete_time_independent_effect_as_standardised_continuous_time_independent_effect( + standardised_discrete_effect: f64, + standardised_continuous_effect: f64, ) -> Result { - if !manifest_trait_variance.is_finite() || manifest_trait_variance < 0.0 { - return Err(PsychometricError::InvalidNumericInput); - } - let within = - recover_manifest_observed_variance(loading, latent_variance, measurement_error_variance)?; - if manifest_trait_variance == 0.0 { - return Ok(within); - } - require_finite(within + manifest_trait_variance) + let _ = (standardised_discrete_effect, standardised_continuous_effect); + Err(PsychometricError::StandardisedDiscreteTimeIndependentEffectIsNotStandardisedContinuousTimeIndependentEffect) } -/// Refuse treating Driver Eq. 5 `MANIFESTTRAITVAR` as `MANIFESTVAR`. +/// Refuse treating Driver §7.1 trait-contaminated continuous +/// time-independent predictor effect as p. 16 `TIPREDEFFECTstd`. /// -/// Table 2 (p. 12) names `MANIFESTTRAITVAR` as `Ψ_τ`, additional -/// intercept variance on the indicators, and `MANIFESTVAR` as `Θ`, -/// the variance of `ε`. Equation 5 maps `Var(y) = λ² Var(η) + θ + -/// ψ`. `Ψ_τ` is not `Θ`. +/// `B · √v / √(trait + p + added)` mixes between-subject +/// `TRAITVAR` into the affected SD. Footnote 4 standardises using +/// only `asymDIFFUSION`. /// /// # Errors /// /// Always returns -/// [`PsychometricError::ManifestTraitVarianceIsNotMeasurementError`]. -pub fn refuse_manifest_trait_variance_as_measurement_error( - manifest_trait_variance: f64, - measurement_error_variance: f64, +/// [`PsychometricError::TraitContaminatedContinuousTimeIndependentEffectIsNotStandardisedContinuousTimeIndependentEffect`]. +pub fn refuse_trait_contaminated_continuous_time_independent_effect_as_standardised_continuous_time_independent_effect( + trait_contaminated_continuous_effect: f64, + standardised_continuous_effect: f64, ) -> Result { - let _ = (manifest_trait_variance, measurement_error_variance); - Err(PsychometricError::ManifestTraitVarianceIsNotMeasurementError) + let _ = ( + trait_contaminated_continuous_effect, + standardised_continuous_effect, + ); + Err(PsychometricError::TraitContaminatedContinuousTimeIndependentEffectIsNotStandardisedContinuousTimeIndependentEffect) } -/// Exact scalar lagged observed-indicator covariance from Driver -/// Equation 5. +/// Exact scalar p. 16 `TDPREDEFFECTstd` after strictly positive +/// `asymDIFFUSION` and strictly positive predictor variance. /// -/// Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Eq. 3–4, pp. 4–5; -/// Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z) write -/// `y_i(t) = τ_i + Λ η_i(t) + ε_i(t)` with independent measurement -/// error and a person-level intercept `τ_i ~ N(μ_τ, Ψ_τ)`. The -/// scalar lagged covariance is `cov(y_t, y_{t-1}) = λ² cov(η_t, -/// η_{t-1}) + ψ`. `Θ` does not enter: `ε_t` and `ε_{t-1}` are -/// independent. Form `(λ c) λ` then add `ψ`. Do not form `λ²` -/// first. A zero loading or zero latent lagged covariance is -/// exactly `ψ`. A zero manifest trait is exactly `λ² c`. Negative -/// latent lagged covariance or trait variance fails closed. An -/// overflowing product or sum fails closed. This is not a Kalman -/// filter and not ctsem estimation. +/// Driver, Oud, and Voelkle (2017, p. 16; §7.2, pp. 20–21; Eq. 3, +/// p. 5; Table 2, p. 12; footnote 4; JSS PDF re-opened +/// 2026-08-23T21:10Z from +/// ) +/// print continuous-time parameters and, when appropriate, +/// standardised matrices with the suffix `std`. Table 2 names `M` +/// `TDPREDEFFECT`. Footnote 4: standardisations use only the +/// relevant variance, not the total. The affecting variance is +/// time-dependent predictor variance `v`. The affected variance is +/// within-subject `asymDIFFUSION` `-q / (2 a)`, because the process +/// dynamics are individual, or average individual, temporal +/// dynamics. Form strictly positive `asymDIFFUSION` first, then +/// strictly positive `v`, then `m · √v / √(-q / (2 a))`. +/// Unstandardised `M` is defined for a zero coefficient and for +/// zero predictor variance; standardised `TDPREDEFFECT` is not. +/// Zero `asymDIFFUSION` or zero `v` has no positive SD and fails +/// closed. `TIPREDEFFECTstd` `B · √v / √(-q / (2 a))` is a +/// different named matrix even when `M = B` and the predictor +/// variances match. The finite-interval intercept-style +/// standardisation `A^{-1}[e^{A Δt} − I] M · √v / √p` depends on +/// the event interval and is not this continuous Dirac coefficient. +/// Section 7.1 warns that omitting trait variance confounds +/// between- and within-person information. +/// `m · √v / √(trait + p + added)` uses the total, not +/// `asymDIFFUSION`, and is not this map when `TRAITVAR` is nonzero. +/// `TRAITVAR` is not the standardisation variance. This is not a +/// Kalman filter, not a matrix `expm`, not DSEM, and not ctsem +/// estimation. /// /// # Errors /// -/// Returns [`PsychometricError::InvalidNumericInput`] when the loading -/// is non-finite, the latent lagged covariance is negative or -/// non-finite, the manifest-trait variance is negative or -/// non-finite, or the mapped covariance is non-finite. -pub fn recover_manifest_lagged_observed_covariance( - loading: f64, - lagged_latent_covariance: f64, - manifest_trait_variance: f64, +/// Propagates [`recover_stationary_latent_variance`]. Returns +/// [`PsychometricError::EventTimeRequired`] for any non-event clock, +/// [`PsychometricError::StationaryVarianceRequiresStableDrift`] when +/// the log-rate is not strictly negative, +/// [`PsychometricError::StandardisedContinuousTimeDependentEffectRequiresPositiveWithinSubjectVariance`] +/// when `asymDIFFUSION` is zero, +/// [`PsychometricError::StandardisedContinuousTimeDependentEffectRequiresPositivePredictorVariance`] +/// when predictor variance is zero, and +/// [`PsychometricError::InvalidNumericInput`] when an input is +/// non-finite, predictor variance is negative, or the product +/// overflows. +pub fn recover_standardised_continuous_time_dependent_predictor_effect( + time_dependent_effect: f64, + predictor_variance: f64, + continuous_diffusion: f64, + log_rate: f64, + clock: LagClock, ) -> Result { - if !loading.is_finite() - || !lagged_latent_covariance.is_finite() - || lagged_latent_covariance < 0.0 - || !manifest_trait_variance.is_finite() - || manifest_trait_variance < 0.0 - { - return Err(PsychometricError::InvalidNumericInput); + let within = recover_stationary_latent_variance(continuous_diffusion, log_rate, clock)?; + if within == 0.0 { + return Err( + PsychometricError::StandardisedContinuousTimeDependentEffectRequiresPositiveWithinSubjectVariance, + ); } - if loading == 0.0 || lagged_latent_covariance == 0.0 { - return Ok(manifest_trait_variance); + if !predictor_variance.is_finite() || predictor_variance < 0.0 { + return Err(PsychometricError::InvalidNumericInput); } - let explained = require_finite((loading * lagged_latent_covariance) * loading)?; - if manifest_trait_variance == 0.0 { - return Ok(explained); + if predictor_variance == 0.0 { + return Err( + PsychometricError::StandardisedContinuousTimeDependentEffectRequiresPositivePredictorVariance, + ); } - require_finite(explained + manifest_trait_variance) + let coefficient = require_finite(time_dependent_effect)?; + let process_sd = within.sqrt(); + let predictor_sd = predictor_variance.sqrt(); + let ratio = require_finite(predictor_sd / process_sd)?; + require_finite(coefficient * ratio) } -/// Refuse treating Driver Eq. 3–4 lagged latent covariance as -/// `cov(y_t, y_{t-1})`. +/// Refuse treating unstandardised `TDPREDEFFECT` as p. 16 +/// `TDPREDEFFECTstd`. /// -/// Equation 5 maps `cov(y_t, y_{t-1}) = λ² cov(η_t, η_{t-1}) + ψ`. -/// The latent lagged covariance is not the observed lagged -/// covariance. +/// `M` is defined for a zero coefficient and for zero predictor +/// variance. Footnote 4 `TDPREDEFFECTstd` requires strictly +/// positive `asymDIFFUSION` and strictly positive `v`. Equal +/// numbers when `v = p` are still distinct named quantities. /// /// # Errors /// /// Always returns -/// [`PsychometricError::LatentLaggedCovarianceIsNotObservedCovariance`]. -pub fn refuse_latent_lagged_covariance_as_observed_covariance( - lagged_latent_covariance: f64, - observed_lagged_covariance: f64, +/// [`PsychometricError::UnstandardisedContinuousTimeDependentEffectIsNotStandardisedContinuousTimeDependentEffect`]. +pub fn refuse_unstandardised_continuous_time_dependent_effect_as_standardised_continuous_time_dependent_effect( + unstandardised_continuous_effect: f64, + standardised_continuous_effect: f64, ) -> Result { - let _ = (lagged_latent_covariance, observed_lagged_covariance); - Err(PsychometricError::LatentLaggedCovarianceIsNotObservedCovariance) + let _ = ( + unstandardised_continuous_effect, + standardised_continuous_effect, + ); + Err(PsychometricError::UnstandardisedContinuousTimeDependentEffectIsNotStandardisedContinuousTimeDependentEffect) } -/// Refuse treating Driver Eq. 5 measurement error as lagged observed -/// covariance. +/// Refuse treating p. 16 `TIPREDEFFECTstd` as p. 16 +/// `TDPREDEFFECTstd`. /// -/// `MANIFESTVAR` is `Θ`. Independent `ε_t` does not enter -/// `cov(y_t, y_{t-1})`. +/// `B · √v / √(-q / (2 a))` standardises Table 2 `TIPREDEFFECT`. +/// Footnote 4 `TDPREDEFFECTstd` is `m · √v / √(-q / (2 a))`. Equal +/// numbers when `M = B` are still distinct named quantities. /// /// # Errors /// /// Always returns -/// [`PsychometricError::MeasurementErrorIsNotLaggedObservedCovariance`]. -pub fn refuse_measurement_error_as_lagged_observed_covariance( - measurement_error_variance: f64, - observed_lagged_covariance: f64, +/// [`PsychometricError::StandardisedContinuousTimeIndependentEffectIsNotStandardisedContinuousTimeDependentEffect`]. +pub fn refuse_standardised_continuous_time_independent_effect_as_standardised_continuous_time_dependent_effect( + standardised_continuous_time_independent_effect: f64, + standardised_continuous_time_dependent_effect: f64, ) -> Result { - let _ = (measurement_error_variance, observed_lagged_covariance); - Err(PsychometricError::MeasurementErrorIsNotLaggedObservedCovariance) + let _ = ( + standardised_continuous_time_independent_effect, + standardised_continuous_time_dependent_effect, + ); + Err(PsychometricError::StandardisedContinuousTimeIndependentEffectIsNotStandardisedContinuousTimeDependentEffect) } -/// Exact scalar observed-indicator mean from Driver Equation 5. +/// Refuse treating a finite-interval standardised `TDPREDEFFECT` +/// as p. 16 `TDPREDEFFECTstd`. /// -/// Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Table 2, p. 12; JSS -/// PDF re-opened 2026-08-19T14:08Z) write `y_i(t) = Γ + Λ η_i(t) + -/// ζ_i(t)` with `ζ ~ N(0, Θ)` and `Γ ~ N(τ, Ψ)`. The expected -/// intercept is `τ`. Table 2 names `τ` `MANIFESTMEANS`. The scalar -/// map is `E(y) = τ + λ μ`. Form `λ μ` then add `τ`. A zero loading -/// or zero latent mean is exactly `τ`. A zero intercept is exactly -/// `λ μ`. `MANIFESTMEANS` is not `E(y)`. `E(η)` is not `E(y)`. -/// `CINT` `κ` is the latent continuous intercept from Equation 1, -/// not `τ`. `T0MEANS` is the initial latent mean, not `E(y)`. An -/// overflowing product or sum fails closed. This is not a Kalman -/// filter and not ctsem estimation. +/// `A^{-1}[e^{A Δt} − I] M · √v / √p` treats the Dirac coefficient +/// as an intercept-style integrated effect and depends on the event +/// interval. Footnote 4 `TDPREDEFFECTstd` is +/// `m · √v / √(-q / (2 a))`. /// /// # Errors /// -/// Returns [`PsychometricError::InvalidNumericInput`] when the loading, -/// latent mean, or intercept is non-finite, or the mapped mean is -/// non-finite. -pub fn recover_manifest_observed_mean( - loading: f64, - latent_mean: f64, - manifest_mean: f64, +/// Always returns +/// [`PsychometricError::StandardisedDiscreteTimeDependentEffectIsNotStandardisedContinuousTimeDependentEffect`]. +pub fn refuse_standardised_discrete_time_dependent_effect_as_standardised_continuous_time_dependent_effect( + standardised_discrete_effect: f64, + standardised_continuous_effect: f64, ) -> Result { - if !loading.is_finite() || !latent_mean.is_finite() || !manifest_mean.is_finite() { + let _ = (standardised_discrete_effect, standardised_continuous_effect); + Err(PsychometricError::StandardisedDiscreteTimeDependentEffectIsNotStandardisedContinuousTimeDependentEffect) +} + +/// Refuse treating Driver §7.1 trait-contaminated continuous +/// time-dependent predictor effect as p. 16 `TDPREDEFFECTstd`. +/// +/// `m · √v / √(trait + p + added)` mixes between-subject +/// `TRAITVAR` into the affected SD. Footnote 4 standardises using +/// only `asymDIFFUSION`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::TraitContaminatedContinuousTimeDependentEffectIsNotStandardisedContinuousTimeDependentEffect`]. +pub fn refuse_trait_contaminated_continuous_time_dependent_effect_as_standardised_continuous_time_dependent_effect( + trait_contaminated_continuous_effect: f64, + standardised_continuous_effect: f64, +) -> Result { + let _ = ( + trait_contaminated_continuous_effect, + standardised_continuous_effect, + ); + Err(PsychometricError::TraitContaminatedContinuousTimeDependentEffectIsNotStandardisedContinuousTimeDependentEffect) +} + +/// Exact scalar Table 3 / p. 16 `T0TIPREDEFFECTstd` after strictly +/// positive free `T0VAR` and strictly positive predictor variance. +/// +/// Driver, Oud, and Voelkle (2017, Table 3, p. 13; p. 16; footnote +/// 4; Eq. 3, p. 5; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF +/// re-opened 2026-08-23T17:20Z from +/// ) +/// name `T0TIPREDEFFECT` the effect of time-independent predictors +/// on latents at `T0`. Page 16 prints standardised matrices with +/// the suffix `std` when appropriate. Footnote 4: standardisations +/// use only the relevant variance, not the total. The affecting +/// variance is predictor variance `TIPREDVAR` `v`. The affected +/// variance is free first-occasion `T0VAR` `p_0`, not within-subject +/// `asymDIFFUSION` `-q / (2 a)`, because Table 3 is the first +/// occasion, not the process dynamics. Form strictly positive +/// `p_0` first, then strictly positive `v`, then +/// `t0_b · √v / √p_0`. Unstandardised `t0_b` is defined for a zero +/// coefficient and for zero predictor variance; standardised +/// `T0TIPREDEFFECT` is not. Zero `p_0` or zero `v` has no positive +/// SD and fails closed. `T0` is an event-time occasion, so a +/// non-event clock fails closed. Free `T0VAR` does not require +/// stable `a < 0`. The continuous standardisation +/// `B · √v / √(-q / (2 a))` uses `asymDIFFUSION` and is not this +/// first-occasion map. The asymptotic standardisation +/// `(-B / a) · √v / √(-q / (2 a))` is the total change, not this +/// first-occasion coefficient. Section 7.1 warns that omitting +/// trait variance confounds between- and within-person information. +/// `t0_b · √v / √(trait + p_0 + added)` uses the total, not free +/// `T0VAR`, and is not this map when `TRAITVAR` is nonzero. +/// `TRAITVAR` is not the standardisation variance. This is not a +/// Kalman filter, not a matrix `expm`, not DSEM, and not ctsem +/// estimation. +/// +/// # Errors +/// +/// Returns [`PsychometricError::EventTimeRequired`] for any +/// non-event clock, +/// [`PsychometricError::StandardisedInitialTimeIndependentEffectRequiresPositiveInitialLatentVariance`] +/// when `T0VAR` is zero, +/// [`PsychometricError::StandardisedInitialTimeIndependentEffectRequiresPositivePredictorVariance`] +/// when predictor variance is zero, and +/// [`PsychometricError::InvalidNumericInput`] when an input is +/// non-finite, a variance is negative, or the product overflows. +pub fn recover_standardised_initial_time_independent_predictor_effect( + initial_time_independent_effect: f64, + predictor_variance: f64, + initial_latent_variance: f64, + clock: LagClock, +) -> Result { + if !clock.admits_structural_lag() { + return Err(PsychometricError::EventTimeRequired); + } + if !initial_latent_variance.is_finite() || initial_latent_variance < 0.0 { return Err(PsychometricError::InvalidNumericInput); } - if loading == 0.0 || latent_mean == 0.0 { - return Ok(manifest_mean); + if initial_latent_variance == 0.0 { + return Err( + PsychometricError::StandardisedInitialTimeIndependentEffectRequiresPositiveInitialLatentVariance, + ); } - let explained = require_finite(loading * latent_mean)?; - if manifest_mean == 0.0 { - return Ok(explained); + if !predictor_variance.is_finite() || predictor_variance < 0.0 { + return Err(PsychometricError::InvalidNumericInput); } - require_finite(explained + manifest_mean) + if predictor_variance == 0.0 { + return Err( + PsychometricError::StandardisedInitialTimeIndependentEffectRequiresPositivePredictorVariance, + ); + } + let coefficient = require_finite(initial_time_independent_effect)?; + let process_sd = initial_latent_variance.sqrt(); + let predictor_sd = predictor_variance.sqrt(); + let ratio = require_finite(predictor_sd / process_sd)?; + require_finite(coefficient * ratio) } -/// Refuse treating Driver Eq. 5 `MANIFESTMEANS` as `E(y)`. +/// Refuse treating unstandardised `T0TIPREDEFFECT` as Table 3 / +/// p. 16 `T0TIPREDEFFECTstd`. /// -/// Table 2 (p. 12) names `τ` the expected intercept `Γ`. Equation 5 -/// maps `E(y) = τ + λ μ`. +/// `t0_b` is defined for a zero coefficient and for zero predictor +/// variance. Footnote 4 `T0TIPREDEFFECTstd` requires strictly +/// positive free `T0VAR` and strictly positive `v`. Equal numbers +/// when `v = p_0` are still distinct named quantities. /// /// # Errors /// -/// Always returns [`PsychometricError::ManifestMeansIsNotObservedMean`]. -pub fn refuse_manifest_means_as_observed_mean( - manifest_mean: f64, - observed_mean: f64, +/// Always returns +/// [`PsychometricError::UnstandardisedInitialTimeIndependentEffectIsNotStandardisedInitialTimeIndependentEffect`]. +pub fn refuse_unstandardised_initial_time_independent_effect_as_standardised_initial_time_independent_effect( + unstandardised_initial_effect: f64, + standardised_initial_effect: f64, ) -> Result { - let _ = (manifest_mean, observed_mean); - Err(PsychometricError::ManifestMeansIsNotObservedMean) + let _ = (unstandardised_initial_effect, standardised_initial_effect); + Err(PsychometricError::UnstandardisedInitialTimeIndependentEffectIsNotStandardisedInitialTimeIndependentEffect) } -/// Refuse treating Driver Eq. 5 latent mean as `E(y)`. +/// Refuse treating p. 16 `TIPREDEFFECTstd` as Table 3 / p. 16 +/// `T0TIPREDEFFECTstd`. /// -/// `E(η)` is the latent process mean. Equation 5 maps `E(y) = τ + λ μ`. -/// `T0MEANS` is that latent mean at the first occasion, not `E(y)`. +/// `B · √v / √(-q / (2 a))` standardises the continuous coefficient +/// against `asymDIFFUSION`. Footnote 4 `T0TIPREDEFFECTstd` is +/// `t0_b · √v / √p_0` against free first-occasion `T0VAR`. /// /// # Errors /// -/// Always returns [`PsychometricError::LatentMeanIsNotObservedMean`]. -pub fn refuse_latent_mean_as_observed_mean( - latent_mean: f64, - observed_mean: f64, +/// Always returns +/// [`PsychometricError::StandardisedContinuousTimeIndependentEffectIsNotStandardisedInitialTimeIndependentEffect`]. +pub fn refuse_standardised_continuous_time_independent_effect_as_standardised_initial_time_independent_effect( + standardised_continuous_effect: f64, + standardised_initial_effect: f64, ) -> Result { - let _ = (latent_mean, observed_mean); - Err(PsychometricError::LatentMeanIsNotObservedMean) + let _ = (standardised_continuous_effect, standardised_initial_effect); + Err(PsychometricError::StandardisedContinuousTimeIndependentEffectIsNotStandardisedInitialTimeIndependentEffect) } -/// Refuse treating Driver Table 2 `CINT` as `MANIFESTMEANS`. +/// Refuse treating p. 16 `asymTIPREDEFFECTstd` as Table 3 / p. 16 +/// `T0TIPREDEFFECTstd`. /// -/// Table 2 (p. 12) names `κ` `CINT`, the latent continuous intercept -/// from Equation 1, and `τ` `MANIFESTMEANS`, the expected `Γ` from -/// Equation 5. `κ` is not `τ`. +/// `(-B / a) · √v / √(-q / (2 a))` is the standardised expected +/// total change. Footnote 4 `T0TIPREDEFFECTstd` is the +/// first-occasion coefficient `t0_b · √v / √p_0`. /// /// # Errors /// /// Always returns -/// [`PsychometricError::ContinuousInterceptIsNotManifestMeans`]. -pub fn refuse_continuous_intercept_as_manifest_means( - continuous_intercept: f64, - manifest_mean: f64, +/// [`PsychometricError::StandardisedAsymptoticTimeIndependentEffectIsNotStandardisedInitialTimeIndependentEffect`]. +pub fn refuse_standardised_asymptotic_time_independent_effect_as_standardised_initial_time_independent_effect( + standardised_asymptotic_effect: f64, + standardised_initial_effect: f64, ) -> Result { - let _ = (continuous_intercept, manifest_mean); - Err(PsychometricError::ContinuousInterceptIsNotManifestMeans) + let _ = (standardised_asymptotic_effect, standardised_initial_effect); + Err(PsychometricError::StandardisedAsymptoticTimeIndependentEffectIsNotStandardisedInitialTimeIndependentEffect) } -/// Exact scalar discrete intercept increment from Driver Equation 3. +/// Refuse treating Driver §7.1 trait-contaminated first-occasion +/// time-independent predictor effect as Table 3 / p. 16 +/// `T0TIPREDEFFECTstd`. /// -/// Driver, Oud, and Voelkle (2017, Eq. 3, p. 4; Table 2, p. 12; JSS -/// PDF re-opened 2026-08-19T18:10Z) write the expected-value term -/// `A^{-1}[e^{A Δt} − I] b` after the stochastic integral is taken -/// to have mean zero. Table 2 names `κ` `CINT`. The scalar map is -/// `κ (expm1(a Δt) / a)` for `a ≠ 0`. A zero drift is the Eq. 3 -/// integral with `A = 0`: `κ Δt`. That path has no matrix inverse. -/// A zero intercept is exactly zero. `CINT` is not this discrete -/// increment. The `a ≠ 0` evaluation is -/// [`recover_discrete_constant_predictor_effect`]. This is not a -/// Kalman filter and not ctsem estimation. +/// `t0_b · √v / √(trait + p_0 + added)` mixes between-subject +/// `TRAITVAR` into the affected SD. Footnote 4 standardises using +/// only free `T0VAR`. /// /// # Errors /// -/// Returns [`PsychometricError::EventTimeRequired`] for any non-event -/// clock, [`PsychometricError::NonPositiveInterval`] when -/// `event_delta` is not strictly positive, and -/// [`PsychometricError::InvalidNumericInput`] when the intercept or -/// drift is non-finite or the mapped increment is non-finite. -pub fn recover_discrete_continuous_intercept_effect( - continuous_intercept: f64, - log_rate: f64, - event_delta: f64, - clock: LagClock, +/// Always returns +/// [`PsychometricError::TraitContaminatedInitialTimeIndependentEffectIsNotStandardisedInitialTimeIndependentEffect`]. +pub fn refuse_trait_contaminated_initial_time_independent_effect_as_standardised_initial_time_independent_effect( + trait_contaminated_initial_effect: f64, + standardised_initial_effect: f64, ) -> Result { - if !clock.admits_structural_lag() { - return Err(PsychometricError::EventTimeRequired); - } - if !event_delta.is_finite() || event_delta <= 0.0 { - return Err(PsychometricError::NonPositiveInterval); - } - if !continuous_intercept.is_finite() || !log_rate.is_finite() { - return Err(PsychometricError::InvalidNumericInput); - } - if log_rate == 0.0 { - if continuous_intercept == 0.0 { - return Ok(0.0); - } - return require_finite(continuous_intercept * event_delta); - } - recover_discrete_constant_predictor_effect(continuous_intercept, log_rate, event_delta, clock) + let _ = ( + trait_contaminated_initial_effect, + standardised_initial_effect, + ); + Err(PsychometricError::TraitContaminatedInitialTimeIndependentEffectIsNotStandardisedInitialTimeIndependentEffect) } -/// Exact scalar discrete latent mean from Driver Equation 3. +/// Exact scalar Table 3 / p. 16 `T0TDPREDEFFECTstd` after strictly +/// positive free `T0VAR` and strictly positive predictor variance. /// -/// Driver, Oud, and Voelkle (2017, Eq. 3, p. 4; Table 2, p. 12; JSS -/// PDF re-opened 2026-08-19T18:10Z) write -/// `η(t) = exp(A Δt) η(t0) + ∫ exp(A(t−s)) (b + …) ds` plus a -/// stochastic integral of mean zero. With no time-varying covariates -/// the scalar expected-value map is -/// `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`. Table 2 names `μ_0` -/// at the first occasion `T0MEANS` and `κ` `CINT`. Form the CINT -/// increment first, then add the carried `T0MEANS` term. A zero -/// initial mean is exactly the increment. A zero intercept is exactly -/// `exp(a Δt) μ_0`. A zero drift carries `T0MEANS` unchanged and adds -/// `κ Δt`. As `Δt → ∞` with stable `a < 0`, `μ_t → −κ / a`. Binary64 -/// underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and -/// keeps the equilibrium increment. `T0MEANS` is not `μ_t`. `CINT` is -/// not the discrete increment. `CINT` is not `T0MEANS`. This is not a -/// Kalman filter and not ctsem estimation. +/// Driver, Oud, and Voelkle (2017, Table 3, p. 13; Table 2, p. 12; +/// p. 16; footnote 4; Eq. 3, p. 5; 2017-era ctsem +/// `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T21:34Z from +/// ) +/// name `T0TDPREDEFFECT` the effect of time-dependent predictors on +/// latents at `T0`. Table 2 names `M` `TDPREDEFFECT` and names +/// `T0TDPREDCOV` the first-occasion covariance, not this +/// coefficient. Page 16 prints standardised matrices with the +/// suffix `std` when appropriate. Footnote 4: standardisations use +/// only the relevant variance, not the total. The affecting +/// variance is time-dependent predictor variance `v`, not +/// `TIPREDVAR`. The affected variance is free first-occasion +/// `T0VAR` `p_0`, not within-subject `asymDIFFUSION` +/// `-q / (2 a)`, because Table 3 is the first occasion, not the +/// process dynamics. Form strictly positive `p_0` first, then +/// strictly positive `v`, then `t0_m · √v / √p_0`. Unstandardised +/// `t0_m` is defined for a zero coefficient and for zero predictor +/// variance; standardised `T0TDPREDEFFECT` is not. Zero `p_0` or +/// zero `v` has no positive SD and fails closed. `T0` is an +/// event-time occasion, so a non-event clock fails closed. Free +/// `T0VAR` does not require stable `a < 0`. The continuous +/// standardisation `m · √v / √(-q / (2 a))` uses `asymDIFFUSION` +/// and is not this first-occasion map. `T0TIPREDEFFECTstd` +/// `t0_b · √v / √p_0` is a different named matrix even when +/// `t0_m = t0_b` and the predictor variances match. Section 7.1 +/// warns that omitting trait variance confounds between- and +/// within-person information. `t0_m · √v / √(trait + p_0 + added)` +/// uses the total, not free `T0VAR`, and is not this map when +/// `TRAITVAR` is nonzero. `TRAITVAR` is not the standardisation +/// variance. This is not a Kalman filter, not a matrix `expm`, not +/// DSEM, and not ctsem estimation. /// /// # Errors /// -/// Propagates [`recover_discrete_continuous_intercept_effect`] and -/// returns [`PsychometricError::InvalidNumericInput`] when the initial -/// mean is non-finite, the carried exponential overflows, or the -/// mapped mean is non-finite. -pub fn recover_discrete_latent_mean( - initial_latent_mean: f64, - log_rate: f64, - continuous_intercept: f64, - event_delta: f64, +/// Returns [`PsychometricError::EventTimeRequired`] for any +/// non-event clock, +/// [`PsychometricError::StandardisedInitialTimeDependentEffectRequiresPositiveInitialLatentVariance`] +/// when `T0VAR` is zero, +/// [`PsychometricError::StandardisedInitialTimeDependentEffectRequiresPositivePredictorVariance`] +/// when predictor variance is zero, and +/// [`PsychometricError::InvalidNumericInput`] when an input is +/// non-finite, a variance is negative, or the product overflows. +pub fn recover_standardised_initial_time_dependent_predictor_effect( + initial_time_dependent_effect: f64, + predictor_variance: f64, + initial_latent_variance: f64, clock: LagClock, ) -> Result { - let intercept_effect = recover_discrete_continuous_intercept_effect( - continuous_intercept, - log_rate, - event_delta, - clock, - )?; - if !initial_latent_mean.is_finite() { + if !clock.admits_structural_lag() { + return Err(PsychometricError::EventTimeRequired); + } + if !initial_latent_variance.is_finite() || initial_latent_variance < 0.0 { return Err(PsychometricError::InvalidNumericInput); } - if initial_latent_mean == 0.0 { - return Ok(intercept_effect); + if initial_latent_variance == 0.0 { + return Err( + PsychometricError::StandardisedInitialTimeDependentEffectRequiresPositiveInitialLatentVariance, + ); } - let carried = if log_rate == 0.0 { - initial_latent_mean - } else { - let increment_argument = log_rate * event_delta; - if increment_argument == 0.0 { - initial_latent_mean - } else { - let discrete_lag = increment_argument.exp(); - if discrete_lag == 0.0 { - 0.0 - } else if !discrete_lag.is_finite() { - return Err(PsychometricError::InvalidNumericInput); - } else { - require_finite(discrete_lag * initial_latent_mean)? - } - } - }; - if intercept_effect == 0.0 { - return Ok(carried); + if !predictor_variance.is_finite() || predictor_variance < 0.0 { + return Err(PsychometricError::InvalidNumericInput); } - if carried == 0.0 { - return Ok(intercept_effect); + if predictor_variance == 0.0 { + return Err( + PsychometricError::StandardisedInitialTimeDependentEffectRequiresPositivePredictorVariance, + ); } - require_finite(carried + intercept_effect) + let coefficient = require_finite(initial_time_dependent_effect)?; + let process_sd = initial_latent_variance.sqrt(); + let predictor_sd = predictor_variance.sqrt(); + let ratio = require_finite(predictor_sd / process_sd)?; + require_finite(coefficient * ratio) } -/// Exact scalar discrete observed-indicator mean from Driver -/// Equations 3 and 5. +/// Refuse treating unstandardised `T0TDPREDEFFECT` as Table 3 / +/// p. 16 `T0TDPREDEFFECTstd`. /// -/// Driver, Oud, and Voelkle (2017, Eq. 3, p. 5; Eq. 5, p. 5; -/// Table 2, p. 12; JSS PDF re-opened 2026-08-19T22:10Z) write -/// `η_i(t) = exp(A Δt) η_i(t0) + A^{-1}[exp(A Δt) − I] ξ_i + …` -/// with `ξ_i ~ N(κ, φ_ξ)` (p. 4) and a stochastic integral of -/// mean zero, then `y_i(t) = Γ_i + Λ η_i(t) + ζ_i(t)` with -/// `Γ ~ N(τ, Ψ)` and `ζ ~ N(0, Θ)`. The scalar expected-value -/// composition is `E(y_t) = τ + λ μ_t` with -/// `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`. Form `μ_t` -/// first, then `τ + λ μ_t`. Table 2 names `μ_0` `T0MEANS`, `κ` -/// `CINT`, and `τ` `MANIFESTMEANS`. A zero loading is exactly -/// `τ`. A zero evolved latent mean is exactly `τ`. A zero -/// intercept is exactly `λ μ_t`. The first-occasion map -/// `τ + λ μ_0` is not `E(y_t)`. `MANIFESTMEANS` is not -/// `E(y_t)`. `T0MEANS` is not `E(y_t)`. `μ_t` is not `E(y_t)`. -/// `CINT` is not `E(y_t)`. This is not a Kalman filter and not -/// ctsem estimation. +/// `t0_m` is defined for a zero coefficient and for zero predictor +/// variance. Footnote 4 `T0TDPREDEFFECTstd` requires strictly +/// positive free `T0VAR` and strictly positive `v`. Equal numbers +/// when `v = p_0` are still distinct named quantities. /// /// # Errors /// -/// Propagates [`recover_discrete_latent_mean`] and -/// [`recover_manifest_observed_mean`]. -pub fn recover_discrete_observed_mean( - loading: f64, - initial_latent_mean: f64, - log_rate: f64, - continuous_intercept: f64, - manifest_mean: f64, - event_delta: f64, - clock: LagClock, +/// Always returns +/// [`PsychometricError::UnstandardisedInitialTimeDependentEffectIsNotStandardisedInitialTimeDependentEffect`]. +pub fn refuse_unstandardised_initial_time_dependent_effect_as_standardised_initial_time_dependent_effect( + unstandardised_initial_effect: f64, + standardised_initial_effect: f64, ) -> Result { - let evolved_latent_mean = recover_discrete_latent_mean( - initial_latent_mean, - log_rate, - continuous_intercept, - event_delta, - clock, - )?; - recover_manifest_observed_mean(loading, evolved_latent_mean, manifest_mean) + let _ = (unstandardised_initial_effect, standardised_initial_effect); + Err(PsychometricError::UnstandardisedInitialTimeDependentEffectIsNotStandardisedInitialTimeDependentEffect) } -/// Refuse treating the first-occasion observed mean as `E(y_t)`. +/// Refuse treating p. 16 `TDPREDEFFECTstd` as Table 3 / p. 16 +/// `T0TDPREDEFFECTstd`. /// -/// Equation 5 of `T0MEANS` is `τ + λ μ_0`. Equation 5 of the -/// Eq. 3 evolved mean is `τ + λ μ_t`. Those are not the same -/// map. +/// `m · √v / √(-q / (2 a))` standardises the continuous Dirac +/// coefficient against `asymDIFFUSION`. Footnote 4 +/// `T0TDPREDEFFECTstd` is `t0_m · √v / √p_0` against free +/// first-occasion `T0VAR`. /// /// # Errors /// /// Always returns -/// [`PsychometricError::InitialObservedMeanIsNotEvolvedObservedMean`]. -pub fn refuse_initial_observed_mean_as_evolved_observed_mean( - initial_observed_mean: f64, - evolved_observed_mean: f64, +/// [`PsychometricError::StandardisedContinuousTimeDependentEffectIsNotStandardisedInitialTimeDependentEffect`]. +pub fn refuse_standardised_continuous_time_dependent_effect_as_standardised_initial_time_dependent_effect( + standardised_continuous_effect: f64, + standardised_initial_effect: f64, ) -> Result { - let _ = (initial_observed_mean, evolved_observed_mean); - Err(PsychometricError::InitialObservedMeanIsNotEvolvedObservedMean) + let _ = (standardised_continuous_effect, standardised_initial_effect); + Err(PsychometricError::StandardisedContinuousTimeDependentEffectIsNotStandardisedInitialTimeDependentEffect) } -/// Exact scalar contemporaneous impulse from Driver Equation 3. +/// Refuse treating Table 3 / p. 16 `T0TIPREDEFFECTstd` as Table 3 +/// / p. 16 `T0TDPREDEFFECTstd`. /// -/// Driver, Oud, and Voelkle (2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; -/// §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z from -/// ) -/// write the time-dependent predictor as the Dirac impulse -/// `χ_i(t) = Σ_{u ∈ U_i} x_{i,u} δ(t − u)` (Eq. 2). Equation 3's -/// fourth summand is `M Σ x_{i,u} δ(t − u)`. Table 2 names `M` -/// `TDPREDEFFECT`. Section 7.2 calls this "a sudden impulse to the -/// system which then dissipates back to the process mean" and reports -/// `TDPREDEFFECT` as "the initial impact of the predictor on the -/// processes." The scalar contemporaneous jump is `m x`. It is not -/// the second-summand `CINT` map `A^{-1}[e^{A Δt} − I] κ`, not the -/// third-summand time-independent map `A^{-1}[e^{A Δt} − I] B z`, -/// and not Voelkle et al. (2012, Eq. 14) `a_{yx} Δt`. The §7.2 -/// lasting level change sets `CINT` to `TDPREDEFFECT * −DRIFT` -/// (`κ = −a m x`) and is not this jump. The extra near-zero-drift -/// latent process also named in §7.2 is a third specification. A -/// zero effect or zero predictor is exactly zero. This is not a -/// Kalman filter and not ctsem estimation. +/// `t0_b · √v / √p_0` standardises Table 3 `T0TIPREDEFFECT`. +/// Footnote 4 `T0TDPREDEFFECTstd` is `t0_m · √v / √p_0`. Equal +/// numbers when `t0_m = t0_b` are still distinct named quantities. /// /// # Errors /// -/// Returns [`PsychometricError::InvalidNumericInput`] when the effect -/// or predictor is non-finite or the product overflows. -pub fn recover_time_dependent_predictor_impulse( - time_dependent_effect: f64, - time_dependent_predictor: f64, +/// Always returns +/// [`PsychometricError::StandardisedInitialTimeIndependentEffectIsNotStandardisedInitialTimeDependentEffect`]. +pub fn refuse_standardised_initial_time_independent_effect_as_standardised_initial_time_dependent_effect( + standardised_initial_time_independent_effect: f64, + standardised_initial_time_dependent_effect: f64, ) -> Result { - if !time_dependent_effect.is_finite() || !time_dependent_predictor.is_finite() { - return Err(PsychometricError::InvalidNumericInput); - } - if time_dependent_effect == 0.0 || time_dependent_predictor == 0.0 { - return Ok(0.0); - } - require_finite(time_dependent_effect * time_dependent_predictor) + let _ = ( + standardised_initial_time_independent_effect, + standardised_initial_time_dependent_effect, + ); + Err(PsychometricError::StandardisedInitialTimeIndependentEffectIsNotStandardisedInitialTimeDependentEffect) } -/// Exact scalar level-change `CINT` from Driver Section 7.2. +/// Refuse treating Driver §7.1 trait-contaminated first-occasion +/// time-dependent predictor effect as Table 3 / p. 16 +/// `T0TDPREDEFFECTstd`. /// -/// Driver, Oud, and Voelkle (2017, §7.2, pp. 20–21; Eq. 1–3, pp. 4–5; -/// Table 2, p. 12; JSS PDF re-opened 2026-08-20T19:45Z from +/// `t0_m · √v / √(trait + p_0 + added)` mixes between-subject +/// `TRAITVAR` into the affected SD. Footnote 4 standardises using +/// only free `T0VAR`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::TraitContaminatedInitialTimeDependentEffectIsNotStandardisedInitialTimeDependentEffect`]. +pub fn refuse_trait_contaminated_initial_time_dependent_effect_as_standardised_initial_time_dependent_effect( + trait_contaminated_initial_effect: f64, + standardised_initial_effect: f64, +) -> Result { + let _ = ( + trait_contaminated_initial_effect, + standardised_initial_effect, + ); + Err(PsychometricError::TraitContaminatedInitialTimeDependentEffectIsNotStandardisedInitialTimeDependentEffect) +} + +/// Exact scalar p. 16 `T0VARstd` after strictly positive free `T0VAR`. +/// +/// Driver, Oud, and Voelkle (2017, Table 2, p. 12; p. 16; footnote 4; +/// 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened +/// 2026-08-23T22:06Z from /// ) -/// contrast a sudden Dirac that dissipates back to the process mean -/// with a lasting level change. To generate that lasting change, -/// `CINT` is set to `TDPREDEFFECT * −DRIFT`. The scalar setting is -/// `κ = −a m x`. Form `m x` first, then multiply by `−a`. A zero -/// effect or zero predictor is exactly zero. Stable `a < 0` is -/// required so `−κ / a = m x` is an equilibrium offset. `a ≥ 0` -/// cannot hold a new process mean. `−a m x` is not the -/// contemporaneous jump `m x`. `−a m x` is not a free `CINT`. -/// `−a m x` is not `A^{-1}[e^{A Δt} − I] B z`. The extra -/// near-zero-drift latent process also named in §7.2 is a different -/// specification and is not this `CINT` setting. This is not a -/// Kalman filter and not ctsem estimation. +/// name `T0VAR` the latent process initial variance/covariance. +/// Page 16 prints standardised matrices with the suffix `std` when +/// appropriate. The 2017-era `summary.ctsemFit.R` forms `T0VARstd` +/// as `solve(sqrt(diag(T0VAR))) %&% T0VAR` when `verbose = TRUE`. +/// `OpenMx` `%&%` is the quadratic form `t(A) %*% B %*% A`. The +/// default `ridging = FALSE` adds 0, not `0.0001`; that ridge is a +/// numerical hack and is not this exact map. The scalar correlation +/// is `p_0 / p_0 = 1` after strictly positive free `T0VAR`. Form +/// strictly positive `p_0` first, then `1 / √p_0`, then +/// `(1 / √p_0) p_0 (1 / √p_0)`. Unstandardised `T0VAR` is defined +/// for a zero first-occasion variance; standardised `T0VAR` is not. +/// Zero `p_0` has no positive SD and fails closed. `T0` is an +/// event-time occasion, so a non-event clock fails closed. Free +/// `T0VAR` does not require stable `a < 0`. Distinct positive +/// `p_0` recover the same 1. `T0TDPREDEFFECTstd` +/// `t0_m · √v / √p_0` depends on `p_0` and is not this +/// correlation. `addedT0TIPREDVAR` `t0_b² v` is extra TI variance, +/// not this correlation. `TRAITVAR` is not the standardisation +/// variance. This is not a Kalman filter, not a matrix `expm`, not +/// DSEM, and not ctsem estimation. /// /// # Errors /// -/// Returns [`PsychometricError::InvalidNumericInput`] when an input -/// is non-finite or a product overflows, and -/// [`PsychometricError::LevelChangeRequiresStableDrift`] when the -/// drift is not strictly negative and the impulse is nonzero. -pub fn recover_level_change_continuous_intercept( - time_dependent_effect: f64, - time_dependent_predictor: f64, - log_rate: f64, +/// Returns [`PsychometricError::EventTimeRequired`] for any +/// non-event clock, +/// [`PsychometricError::StandardisedInitialLatentVarianceRequiresPositiveInitialLatentVariance`] +/// when `T0VAR` is zero, and +/// [`PsychometricError::InvalidNumericInput`] when the variance is +/// non-finite or negative. +pub fn recover_standardised_initial_latent_variance( + initial_latent_variance: f64, + clock: LagClock, ) -> Result { - if !time_dependent_effect.is_finite() - || !time_dependent_predictor.is_finite() - || !log_rate.is_finite() - { - return Err(PsychometricError::InvalidNumericInput); + if !clock.admits_structural_lag() { + return Err(PsychometricError::EventTimeRequired); } - if time_dependent_effect == 0.0 || time_dependent_predictor == 0.0 { - return Ok(0.0); + if !initial_latent_variance.is_finite() || initial_latent_variance < 0.0 { + return Err(PsychometricError::InvalidNumericInput); } - if log_rate >= 0.0 { - return Err(PsychometricError::LevelChangeRequiresStableDrift); + if initial_latent_variance == 0.0 { + return Err( + PsychometricError::StandardisedInitialLatentVarianceRequiresPositiveInitialLatentVariance, + ); } - let impulse = require_finite(time_dependent_effect * time_dependent_predictor)?; - require_finite(-log_rate * impulse) + Ok(1.0) } -/// Refuse treating the §7.2 level-change `CINT` as the -/// contemporaneous Dirac. +/// Refuse treating unstandardised `T0VAR` as p. 16 `T0VARstd`. /// -/// `κ = −a m x` is not the jump `m x`. +/// Free `T0VAR` `p_0` is defined for a zero first-occasion +/// variance. Footnote 4 `T0VARstd` requires strictly positive +/// `p_0`. Equal numbers when `p_0 = 1` are still distinct named +/// quantities. /// /// # Errors /// -/// Always returns [`PsychometricError::LevelChangeInterceptIsNotImpulse`]. -pub fn refuse_level_change_intercept_as_impulse( - level_change_intercept: f64, - time_dependent_impulse: f64, +/// Always returns +/// [`PsychometricError::UnstandardisedInitialLatentVarianceIsNotStandardisedInitialLatentVariance`]. +pub fn refuse_unstandardised_initial_latent_variance_as_standardised_initial_latent_variance( + unstandardised_initial_variance: f64, + standardised_initial_variance: f64, ) -> Result { - let _ = (level_change_intercept, time_dependent_impulse); - Err(PsychometricError::LevelChangeInterceptIsNotImpulse) + let _ = ( + unstandardised_initial_variance, + standardised_initial_variance, + ); + Err(PsychometricError::UnstandardisedInitialLatentVarianceIsNotStandardisedInitialLatentVariance) } -/// Refuse treating the §7.2 level-change `CINT` as a free `CINT`. +/// Refuse treating Table 3 / p. 16 `T0TDPREDEFFECTstd` as p. 16 +/// `T0VARstd`. /// -/// `κ = −a m x` is not an arbitrary continuous intercept. +/// `t0_m · √v / √p_0` standardises a first-occasion TD effect. +/// `T0VARstd` is the correlation form of free `T0VAR`. Those are +/// not the same map. /// /// # Errors /// /// Always returns -/// [`PsychometricError::LevelChangeInterceptIsNotFreeContinuousIntercept`]. -pub fn refuse_level_change_intercept_as_free_continuous_intercept( - level_change_intercept: f64, - continuous_intercept: f64, +/// [`PsychometricError::StandardisedInitialTimeDependentEffectIsNotStandardisedInitialLatentVariance`]. +pub fn refuse_standardised_initial_time_dependent_effect_as_standardised_initial_latent_variance( + standardised_initial_effect: f64, + standardised_initial_variance: f64, ) -> Result { - let _ = (level_change_intercept, continuous_intercept); - Err(PsychometricError::LevelChangeInterceptIsNotFreeContinuousIntercept) + let _ = (standardised_initial_effect, standardised_initial_variance); + Err(PsychometricError::StandardisedInitialTimeDependentEffectIsNotStandardisedInitialLatentVariance) } -/// Refuse treating the §7.2 level-change `CINT` as the Eq. 3 -/// process increment. +/// Refuse treating 2017-era `addedT0TIPREDVAR` as p. 16 `T0VARstd`. /// -/// `κ = −a m x` is not `A^{-1}[e^{A Δt} − I] B z`. +/// `t0_b² v` is extra first-occasion TI variance. `T0VARstd` is +/// the correlation form of free `T0VAR`. Those are not the same +/// map. /// /// # Errors /// /// Always returns -/// [`PsychometricError::LevelChangeInterceptIsNotProcessIncrement`]. -pub fn refuse_level_change_intercept_as_process_increment( - level_change_intercept: f64, - time_independent_increment: f64, +/// [`PsychometricError::InitialTimeIndependentVarianceIsNotStandardisedInitialLatentVariance`]. +pub fn refuse_initial_time_independent_variance_as_standardised_initial_latent_variance( + initial_predictor_variance: f64, + standardised_initial_variance: f64, ) -> Result { - let _ = (level_change_intercept, time_independent_increment); - Err(PsychometricError::LevelChangeInterceptIsNotProcessIncrement) + let _ = (initial_predictor_variance, standardised_initial_variance); + Err(PsychometricError::InitialTimeIndependentVarianceIsNotStandardisedInitialLatentVariance) } -/// Exact scalar discrete increment of the §7.2 level-change `CINT`. +/// Exact scalar p. 16 `TRAITVARstd` after strictly positive `TRAITVAR`. /// -/// Driver, Oud, and Voelkle (2017, §7.2, pp. 20–21; Eq. 3, pp. 4–5; -/// Table 2, p. 12; JSS PDF re-opened 2026-08-20T19:50Z from +/// Driver, Oud, and Voelkle (2017, Table 2, p. 12; §7.1, pp. 18–19; +/// p. 16; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF +/// re-opened 2026-08-23T22:21Z from /// ) -/// set `CINT` to `TDPREDEFFECT * −DRIFT` so a sudden impulse holds a -/// new process mean. Equation 3 maps that intercept through -/// `A^{-1}[e^{A Δt} − I] κ`. With `κ = −a m x` the scalar increment -/// is `(e^{a Δt} − 1)/a · (−a m x) = (1 − e^{a Δt}) m x`. Form the -/// level-change `CINT` first, then the discrete intercept map. -/// Underflow of `e^{a Δt}` to `+0` keeps the equilibrium offset -/// `m x`. A zero effect or zero predictor is exactly zero. Stable -/// `a < 0` is required. `(1 − e^{a Δt}) m x` is not the -/// contemporaneous jump `m x`. `(1 − e^{a Δt}) m x` is not `κ`. -/// `(1 − e^{a Δt}) m x` is not `A^{-1}[e^{A Δt} − I] B z`. The extra -/// near-zero-drift latent process also named in §7.2 is a different -/// specification. This is not a Kalman filter and not ctsem +/// name `TRAITVAR` `φ_ξ` the latent trait variance/covariance. +/// Table 2 sets it `NULL` when there is no trait variance. +/// Section 7.1 names traits the stable between-subject differences +/// (unit-level unobserved heterogeneity) and estimates `φ_ξ` of the +/// intercepts `ξ` across individuals. Page 16 prints standardised +/// matrices with the suffix `std` when appropriate. The 2017-era +/// `summary.ctsemFit.R` forms `TRAITVARstd` only when +/// `TRAITVAR != 0`, as `solve(sqrt(diag(TRAITVAR))) %&% TRAITVAR` +/// when `verbose = TRUE`. `OpenMx` `%&%` is the quadratic form +/// `t(A) %*% B %*% A`. Unlike `T0VARstd`, that formation uses +/// `diag(diag(TRAITVAR))` and does not add `diag(c(ridging))`. The +/// ridge is a `T0VAR` numerical hack and is not this exact map. The +/// scalar correlation is `trait / trait = 1` after strictly +/// positive `TRAITVAR`. Form strictly positive `trait` first, then +/// `1 / √trait`, then `(1 / √trait) trait (1 / √trait)`. +/// Unstandardised `TRAITVAR` is defined for a zero trait; +/// standardised `TRAITVAR` is not. Zero `TRAITVAR` skips forming +/// `TRAITVARstd` in the 2017-era source and fails closed here. +/// Between-subject variance is an event-time structural quantity, +/// so a non-event clock fails closed. `TRAITVAR` does not require +/// stable `a < 0`. Distinct positive `trait` recover the same 1. +/// `T0VARstd` `p_0 / p_0 = 1` recovers the same number and remains +/// a distinct named quantity. `addedT0TIPREDVAR` `t0_b² v` is extra +/// first-occasion TI variance, not this correlation. This is not a +/// Kalman filter, not a matrix `expm`, not DSEM, and not ctsem /// estimation. /// /// # Errors /// -/// Propagates [`recover_level_change_continuous_intercept`] and -/// [`recover_discrete_continuous_intercept_effect`]. -pub fn recover_level_change_discrete_increment( - time_dependent_effect: f64, - time_dependent_predictor: f64, - log_rate: f64, - event_delta: f64, +/// Returns [`PsychometricError::EventTimeRequired`] for any +/// non-event clock, +/// [`PsychometricError::StandardisedTraitVarianceRequiresPositiveTraitVariance`] +/// when `TRAITVAR` is zero, and +/// [`PsychometricError::InvalidNumericInput`] when the variance is +/// non-finite or negative. +pub fn recover_standardised_trait_variance( + trait_variance: f64, clock: LagClock, ) -> Result { - let intercept = recover_level_change_continuous_intercept( - time_dependent_effect, - time_dependent_predictor, - log_rate, - )?; - recover_discrete_continuous_intercept_effect(intercept, log_rate, event_delta, clock) + if !clock.admits_structural_lag() { + return Err(PsychometricError::EventTimeRequired); + } + if !trait_variance.is_finite() || trait_variance < 0.0 { + return Err(PsychometricError::InvalidNumericInput); + } + if trait_variance == 0.0 { + return Err(PsychometricError::StandardisedTraitVarianceRequiresPositiveTraitVariance); + } + Ok(1.0) } -/// Refuse treating the §7.2 level-change CINT increment as the -/// contemporaneous Dirac. +/// Refuse treating unstandardised `TRAITVAR` as p. 16 `TRAITVARstd`. /// -/// `(1 − e^{a Δt}) m x` is not the jump `m x`. +/// Unstandardised `TRAITVAR` is defined for a zero trait. Footnote +/// 4 `TRAITVARstd` requires strictly positive `TRAITVAR`. Equal +/// numbers when `trait = 1` are still distinct named quantities. /// /// # Errors /// -/// Always returns [`PsychometricError::LevelChangeIncrementIsNotImpulse`]. -pub fn refuse_level_change_increment_as_impulse( - level_change_increment: f64, - time_dependent_impulse: f64, +/// Always returns +/// [`PsychometricError::UnstandardisedTraitVarianceIsNotStandardisedTraitVariance`]. +pub fn refuse_unstandardised_trait_variance_as_standardised_trait_variance( + unstandardised_trait_variance: f64, + standardised_trait_variance: f64, ) -> Result { - let _ = (level_change_increment, time_dependent_impulse); - Err(PsychometricError::LevelChangeIncrementIsNotImpulse) + let _ = (unstandardised_trait_variance, standardised_trait_variance); + Err(PsychometricError::UnstandardisedTraitVarianceIsNotStandardisedTraitVariance) } -/// Refuse treating the §7.2 level-change CINT increment as `CINT`. +/// Refuse treating p. 16 `T0VARstd` as p. 16 `TRAITVARstd`. /// -/// `(1 − e^{a Δt}) m x` is not `κ = −a m x`. +/// Both scalar correlations equal 1 after strictly positive +/// variances. `T0VARstd` standardises free first-occasion `T0VAR`. +/// `TRAITVARstd` standardises between-subject `TRAITVAR`. Equal +/// numbers remain distinct named quantities. /// /// # Errors /// -/// Always returns [`PsychometricError::LevelChangeIncrementIsNotIntercept`]. -pub fn refuse_level_change_increment_as_intercept( - level_change_increment: f64, - level_change_intercept: f64, +/// Always returns +/// [`PsychometricError::StandardisedInitialLatentVarianceIsNotStandardisedTraitVariance`]. +pub fn refuse_standardised_initial_latent_variance_as_standardised_trait_variance( + standardised_initial_variance: f64, + standardised_trait_variance: f64, ) -> Result { - let _ = (level_change_increment, level_change_intercept); - Err(PsychometricError::LevelChangeIncrementIsNotIntercept) + let _ = (standardised_initial_variance, standardised_trait_variance); + Err(PsychometricError::StandardisedInitialLatentVarianceIsNotStandardisedTraitVariance) } -/// Refuse treating the §7.2 level-change CINT increment as the Eq. 3 -/// process increment. +/// Refuse treating 2017-era `addedT0TIPREDVAR` as p. 16 `TRAITVARstd`. /// -/// `(1 − e^{a Δt}) m x` is not `A^{-1}[e^{A Δt} − I] B z`. +/// `t0_b² v` is extra first-occasion TI variance. `TRAITVARstd` is +/// the correlation form of between-subject `TRAITVAR`. Those are +/// not the same map. /// /// # Errors /// /// Always returns -/// [`PsychometricError::LevelChangeIncrementIsNotProcessIncrement`]. -pub fn refuse_level_change_increment_as_process_increment( - level_change_increment: f64, - time_independent_increment: f64, +/// [`PsychometricError::InitialTimeIndependentVarianceIsNotStandardisedTraitVariance`]. +pub fn refuse_initial_time_independent_variance_as_standardised_trait_variance( + initial_predictor_variance: f64, + standardised_trait_variance: f64, ) -> Result { - let _ = (level_change_increment, time_independent_increment); - Err(PsychometricError::LevelChangeIncrementIsNotProcessIncrement) + let _ = (initial_predictor_variance, standardised_trait_variance); + Err(PsychometricError::InitialTimeIndependentVarianceIsNotStandardisedTraitVariance) } -/// Exact scalar contribution of the §7.2 extra near-zero-drift process. +/// Exact scalar p. 16 `MANIFESTTRAITVARstd` after strictly positive +/// `MANIFESTTRAITVAR`. /// -/// Driver, Oud, and Voelkle (2017, §7.2, pp. 22–23; Eq. 1–3, pp. 4–5; -/// Table 2, p. 12; JSS PDF re-opened 2026-08-20T23:10Z from +/// Driver, Oud, and Voelkle (2017, Table 2, p. 12; §7.1, p. 19; +/// p. 16; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF +/// re-opened 2026-08-23T22:28Z from /// ) -/// specify a lasting level change by an extra latent process, not by -/// rewriting `CINT`. `T0MEANS`, `CINT`, `T0VAR`, `DIFFUSION`, and -/// `TRAITVAR` of that process are fixed to 0. `TDPREDEFFECT` on it is -/// fixed to 1 to identify the effect. Its `DRIFT` diagonal is very -/// close to 0 (printed example `−0.000001`; precisely 0 causes -/// computational problems). The original process is driven by the -/// `DRIFT` coupling `a_{ηξ}`. After a unit identification impulse the -/// extra state is `x e^{ε t}` and the scalar contribution to the -/// original process is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`. -/// Form `a_{ηξ} x` first. When `ε = a` the contribution is -/// `a_{ηξ} x Δt e^{a Δt}`. A zero coupling or zero predictor is -/// exactly zero. `ε ≥ 0` cannot hold a lasting extra state and fails -/// closed. That contribution is not `κ = −a m x`, not -/// `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. This -/// is not a Kalman filter, not a matrix `expm`, and not ctsem -/// estimation. +/// name `MANIFESTTRAITVAR` `Ψ_τ` the additional time-invariant +/// variance-covariance on the measurement level. Table 2 sets it +/// `NULL` when there is no manifest trait. Section 7.1 names +/// manifest traits stable individual differences in indicator +/// levels, distinct from process-level `TRAITVAR` `φ_ξ`. Page 16 +/// prints standardised matrices with the suffix `std` when +/// appropriate. The 2017-era `summary.ctsemFit.R` forms +/// `MANIFESTTRAITVARstd` only when `MANIFESTTRAITVAR != 0`, as +/// `solve(sqrt(diag(MANIFESTTRAITVAR) + ridging)) %&% +/// MANIFESTTRAITVAR` when `verbose = TRUE`. `OpenMx` `%&%` is the +/// quadratic form `t(A) %*% B %*% A`. Unlike `TRAITVARstd`, that +/// formation adds `diag(c(ridging), n.manifest)`. The default +/// `ridging = FALSE` adds 0, not `0.0001`; that ridge is a +/// numerical hack and is not this exact map. The scalar +/// correlation is `ψ / ψ = 1` after strictly positive +/// `MANIFESTTRAITVAR`. Form strictly positive `ψ` first, then +/// `1 / √ψ`, then `(1 / √ψ) ψ (1 / √ψ)`. Unstandardised +/// `MANIFESTTRAITVAR` is defined for a zero trait; standardised +/// `MANIFESTTRAITVAR` is not. Zero `MANIFESTTRAITVAR` skips +/// forming `MANIFESTTRAITVARstd` in the 2017-era source and fails +/// closed here. Indicator-level trait variance is an event-time +/// structural quantity, so a non-event clock fails closed. +/// `MANIFESTTRAITVAR` does not require stable `a < 0`. Distinct +/// positive `ψ` recover the same 1. `TRAITVARstd` +/// `trait / trait = 1` recovers the same number and remains a +/// distinct named quantity. `MANIFESTVAR` `θ` is measurement +/// error, not this correlation. This is not a Kalman filter, not a +/// matrix `expm`, not DSEM, and not ctsem estimation. /// /// # Errors /// -/// Returns [`PsychometricError::EventTimeRequired`] for any non-event -/// clock, [`PsychometricError::NonPositiveInterval`] when the interval -/// is not strictly positive, -/// [`PsychometricError::LevelChangeExtraProcessRequiresNegativeDrift`] -/// when the extra drift is not strictly negative and the contribution -/// is nonzero, and [`PsychometricError::InvalidNumericInput`] when an -/// input is non-finite or a product, exponential, or quotient -/// overflows. -pub fn recover_level_change_extra_process_contribution( - original_from_extra_drift: f64, - time_dependent_predictor: f64, - original_log_rate: f64, - extra_log_rate: f64, - event_delta: f64, +/// Returns [`PsychometricError::EventTimeRequired`] for any +/// non-event clock, +/// [`PsychometricError::StandardisedManifestTraitVarianceRequiresPositiveManifestTraitVariance`] +/// when `MANIFESTTRAITVAR` is zero, and +/// [`PsychometricError::InvalidNumericInput`] when the variance is +/// non-finite or negative. +pub fn recover_standardised_manifest_trait_variance( + manifest_trait_variance: f64, clock: LagClock, ) -> Result { if !clock.admits_structural_lag() { return Err(PsychometricError::EventTimeRequired); } - if !event_delta.is_finite() || event_delta <= 0.0 { - return Err(PsychometricError::NonPositiveInterval); - } - if !original_from_extra_drift.is_finite() - || !time_dependent_predictor.is_finite() - || !original_log_rate.is_finite() - || !extra_log_rate.is_finite() - { + if !manifest_trait_variance.is_finite() || manifest_trait_variance < 0.0 { return Err(PsychometricError::InvalidNumericInput); } - if original_from_extra_drift == 0.0 || time_dependent_predictor == 0.0 { - return Ok(0.0); - } - if extra_log_rate >= 0.0 { - return Err(PsychometricError::LevelChangeExtraProcessRequiresNegativeDrift); - } - let coupling = require_finite(original_from_extra_drift * time_dependent_predictor)?; - // `e^{ε Δt}` with `ε < 0`; `exp(0) = 1` after product underflow. - let extra_lag = (extra_log_rate * event_delta).exp(); - let original_argument = original_log_rate * event_delta; - let original_lag = if original_log_rate == 0.0 { - 1.0 - } else { - let lag = original_argument.exp(); - if !lag.is_finite() { - return Err(PsychometricError::InvalidNumericInput); - } - lag - }; - let rate_gap = extra_log_rate - original_log_rate; - let gap_argument = rate_gap * event_delta; - if gap_argument == 0.0 { - return require_finite(coupling * event_delta * original_lag); - } - let increment = gap_argument.exp_m1(); - if !increment.is_finite() { - return require_finite(coupling * (extra_lag - original_lag) / rate_gap); - } - if original_lag == 0.0 { - return require_finite(coupling * extra_lag / rate_gap); + if manifest_trait_variance == 0.0 { + return Err( + PsychometricError::StandardisedManifestTraitVarianceRequiresPositiveManifestTraitVariance, + ); } - require_finite(coupling * original_lag * (increment / rate_gap)) + Ok(1.0) } -/// Refuse treating the §7.2 extra-process contribution as the -/// contemporaneous Dirac. +/// Refuse treating unstandardised `MANIFESTTRAITVAR` as p. 16 +/// `MANIFESTTRAITVARstd`. /// -/// `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` is not the jump `m x`. +/// Unstandardised `Ψ_τ` is defined for a zero manifest trait. +/// Footnote 4 `MANIFESTTRAITVARstd` requires strictly positive +/// `MANIFESTTRAITVAR`. Equal numbers when `ψ = 1` are still +/// distinct named quantities. /// /// # Errors /// -/// Always returns [`PsychometricError::LevelChangeExtraProcessIsNotImpulse`]. -pub fn refuse_level_change_extra_process_as_impulse( - extra_process_contribution: f64, - time_dependent_impulse: f64, +/// Always returns +/// [`PsychometricError::UnstandardisedManifestTraitVarianceIsNotStandardisedManifestTraitVariance`]. +pub fn refuse_unstandardised_manifest_trait_variance_as_standardised_manifest_trait_variance( + unstandardised_manifest_trait_variance: f64, + standardised_manifest_trait_variance: f64, ) -> Result { - let _ = (extra_process_contribution, time_dependent_impulse); - Err(PsychometricError::LevelChangeExtraProcessIsNotImpulse) + let _ = ( + unstandardised_manifest_trait_variance, + standardised_manifest_trait_variance, + ); + Err( + PsychometricError::UnstandardisedManifestTraitVarianceIsNotStandardisedManifestTraitVariance, + ) } -/// Refuse treating the §7.2 extra-process contribution as the -/// level-change `CINT`. +/// Refuse treating p. 16 `TRAITVARstd` as p. 16 `MANIFESTTRAITVARstd`. /// -/// `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` is not `κ = −a m x`. +/// Both scalar correlations equal 1 after strictly positive +/// variances. `TRAITVARstd` standardises process-level `TRAITVAR`. +/// `MANIFESTTRAITVARstd` standardises indicator-level +/// `MANIFESTTRAITVAR`. Equal numbers remain distinct named +/// quantities. /// /// # Errors /// /// Always returns -/// [`PsychometricError::LevelChangeExtraProcessIsNotIntercept`]. -pub fn refuse_level_change_extra_process_as_intercept( - extra_process_contribution: f64, - level_change_intercept: f64, +/// [`PsychometricError::StandardisedTraitVarianceIsNotStandardisedManifestTraitVariance`]. +pub fn refuse_standardised_trait_variance_as_standardised_manifest_trait_variance( + standardised_trait_variance: f64, + standardised_manifest_trait_variance: f64, ) -> Result { - let _ = (extra_process_contribution, level_change_intercept); - Err(PsychometricError::LevelChangeExtraProcessIsNotIntercept) + let _ = ( + standardised_trait_variance, + standardised_manifest_trait_variance, + ); + Err(PsychometricError::StandardisedTraitVarianceIsNotStandardisedManifestTraitVariance) } -/// Refuse treating the §7.2 extra-process contribution as the Eq. 3 -/// level-change increment. +/// Refuse treating Table 2 `MANIFESTVAR` `Θ` as p. 16 +/// `MANIFESTTRAITVARstd`. /// -/// `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` is not -/// `(1 − e^{a Δt}) m x`. +/// `θ` is contemporaneous measurement error. `MANIFESTTRAITVARstd` +/// is the correlation form of indicator-level trait variance. +/// Those are not the same map. /// /// # Errors /// /// Always returns -/// [`PsychometricError::LevelChangeExtraProcessIsNotIncrement`]. -pub fn refuse_level_change_extra_process_as_increment( - extra_process_contribution: f64, - level_change_increment: f64, +/// [`PsychometricError::MeasurementErrorIsNotStandardisedManifestTraitVariance`]. +pub fn refuse_measurement_error_as_standardised_manifest_trait_variance( + measurement_error_variance: f64, + standardised_manifest_trait_variance: f64, ) -> Result { - let _ = (extra_process_contribution, level_change_increment); - Err(PsychometricError::LevelChangeExtraProcessIsNotIncrement) + let _ = ( + measurement_error_variance, + standardised_manifest_trait_variance, + ); + Err(PsychometricError::MeasurementErrorIsNotStandardisedManifestTraitVariance) } -/// Exact scalar evolved latent mean plus a §7.2 extra-process contribution. +/// Exact scalar p. 16 `MANIFESTVARstd` after strictly positive +/// `MANIFESTVAR`. /// -/// Driver, Oud, and Voelkle (2017, Eq. 3, p. 5; §7.2, pp. 22–23; JSS -/// PDF re-opened 2026-08-21T06:12Z from +/// Driver, Oud, and Voelkle (2017, Table 2, p. 12; Eq. 5, p. 5; +/// p. 16; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF +/// re-opened 2026-08-23T22:40Z from /// ) -/// write the first two summands as the carried `T0MEANS` and `CINT` -/// increment. Section 7.2 then drives the original process by the -/// extra near-zero-drift latent process through the `DRIFT` coupling -/// `a_{ηξ}`. Form `μ_t` first, then add the extra-process -/// contribution. A zero contribution is exactly `μ_t`. A zero evolved -/// mean is exactly the contribution. The first-occasion map -/// `μ_0 + contribution` is not this composition when the process has -/// already evolved. The contemporaneous Dirac `μ_t + m x` is not this -/// composition. The printed specification puts `TDPREDEFFECT` on the -/// extra process, not on the original process. This is not a Kalman -/// filter and not ctsem estimation. +/// name `MANIFESTVAR` `Θ` the residual covariance of the +/// indicators. Equation 5 writes `ζ ~ N(0, Θ)`. Page 16 prints +/// standardised matrices with the suffix `std` when appropriate. +/// The 2017-era `summary.ctsemFit.R` forms `MANIFESTVARstd` whenever +/// `verbose = TRUE`, as +/// `solve(sqrt(diag(MANIFESTVAR) + ridging)) %&% MANIFESTVAR`. +/// `OpenMx` `%&%` is the quadratic form `t(A) %*% B %*% A`. Unlike +/// `TRAITVARstd`, that formation adds +/// `diag(c(ridging), n.manifest)`. The default `ridging = FALSE` +/// adds 0, not `0.0001`; that ridge is a numerical hack and is not +/// this exact map. The 2017-era source assigns +/// `dimnames(MANIFESTVARstd)` to `latentNames`; the matrix is +/// `n.manifest × n.manifest`. That assignment is a source bug and +/// is not this exact map. The scalar correlation is `θ / θ = 1` +/// after strictly positive `MANIFESTVAR`. Form strictly positive +/// `θ` first, then `1 / √θ`, then `(1 / √θ) θ (1 / √θ)`. +/// Unstandardised `MANIFESTVAR` is defined for a zero residual; +/// standardised `MANIFESTVAR` is not. Zero `θ` makes +/// `solve(sqrt(0))` fail in the 2017-era source and fails closed +/// here. Unlike `TRAITVAR` / `MANIFESTTRAITVAR`, that source does +/// not skip forming `MANIFESTVARstd` when `θ = 0`; the quadratic +/// still fails. Measurement-error variance is an event-time +/// structural quantity, so a non-event clock fails closed. +/// `MANIFESTVAR` does not require stable `a < 0`. Distinct +/// positive `θ` recover the same 1. `MANIFESTTRAITVARstd` +/// `ψ / ψ = 1` recovers the same number and remains a distinct +/// named quantity. Equation 5 `λ² Var(η) + θ` is `Var(y)`, not +/// this correlation. This is not a Kalman filter, not a matrix +/// `expm`, not DSEM, and not ctsem estimation. /// /// # Errors /// -/// Propagates [`recover_discrete_latent_mean`] and -/// [`recover_level_change_extra_process_contribution`], and returns -/// [`PsychometricError::InvalidNumericInput`] when the sum overflows. -#[allow(clippy::too_many_arguments)] -pub fn recover_discrete_latent_mean_with_extra_process( - initial_latent_mean: f64, - original_log_rate: f64, - continuous_intercept: f64, - original_from_extra_drift: f64, - time_dependent_predictor: f64, - extra_log_rate: f64, - event_delta: f64, +/// Returns [`PsychometricError::EventTimeRequired`] for any +/// non-event clock, +/// [`PsychometricError::StandardisedManifestVarianceRequiresPositiveManifestVariance`] +/// when `MANIFESTVAR` is zero, and +/// [`PsychometricError::InvalidNumericInput`] when the variance is +/// non-finite or negative. +pub fn recover_standardised_manifest_variance( + measurement_error_variance: f64, clock: LagClock, ) -> Result { - let evolved_latent_mean = recover_discrete_latent_mean( - initial_latent_mean, - original_log_rate, - continuous_intercept, - event_delta, - clock, - )?; - let contribution = recover_level_change_extra_process_contribution( - original_from_extra_drift, - time_dependent_predictor, - original_log_rate, - extra_log_rate, - event_delta, - clock, - )?; - if contribution == 0.0 { - return Ok(evolved_latent_mean); + if !clock.admits_structural_lag() { + return Err(PsychometricError::EventTimeRequired); } - if evolved_latent_mean == 0.0 { - return Ok(contribution); + if !measurement_error_variance.is_finite() || measurement_error_variance < 0.0 { + return Err(PsychometricError::InvalidNumericInput); } - require_finite(evolved_latent_mean + contribution) + if measurement_error_variance == 0.0 { + return Err( + PsychometricError::StandardisedManifestVarianceRequiresPositiveManifestVariance, + ); + } + Ok(1.0) } -/// Exact scalar observed mean of a §7.2 extra-process contribution. +/// Refuse treating unstandardised `MANIFESTVAR` as p. 16 +/// `MANIFESTVARstd`. /// -/// Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Eq. 1–3, pp. 4–5; -/// §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:12Z from -/// ) -/// write `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and -/// `Γ ~ N(τ, Ψ)`. The expected intercept is `τ`. Section 7.2's -/// printed extra process has `LAMBDA` 0: it is not an observed -/// indicator. Original indicators load on the original process after -/// the `DRIFT` coupling. The latent process at `t` after that -/// contribution is `μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`. -/// The scalar composition is -/// `E(y_t) = τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))`. -/// Form the evolved-plus-contribution latent mean first, then -/// `τ + λ` of that mean. Table 2 names `τ` `MANIFESTMEANS`. A zero -/// loading is exactly `τ`. A zero evolved-plus-contribution latent -/// mean is exactly `τ`. A zero intercept is exactly `λ` of that -/// latent mean. The evolved observed mean `τ + λ μ_t` is not this -/// composition when the contribution is nonzero. The contemporaneous -/// map `τ + λ(μ_t + m x)` is not this composition. `MANIFESTMEANS` is -/// not `E(y_t)`. The extra-process contribution is not `E(y_t)`. The -/// evolved-plus-contribution latent mean is not `E(y_t)`. The extra -/// process itself is not an observed indicator. This is not a Kalman -/// filter and not ctsem estimation. +/// Unstandardised `Θ` is defined for a zero residual. Footnote 4 +/// `MANIFESTVARstd` requires strictly positive `MANIFESTVAR`. Equal +/// numbers when `θ = 1` are still distinct named quantities. /// /// # Errors /// -/// Propagates [`recover_discrete_latent_mean_with_extra_process`] and -/// [`recover_manifest_observed_mean`]. -#[allow(clippy::too_many_arguments)] -pub fn recover_discrete_observed_mean_with_extra_process( - loading: f64, - initial_latent_mean: f64, - original_log_rate: f64, - continuous_intercept: f64, - original_from_extra_drift: f64, - time_dependent_predictor: f64, - extra_log_rate: f64, - manifest_mean: f64, - event_delta: f64, - clock: LagClock, +/// Always returns +/// [`PsychometricError::UnstandardisedManifestVarianceIsNotStandardisedManifestVariance`]. +pub fn refuse_unstandardised_manifest_variance_as_standardised_manifest_variance( + unstandardised_manifest_variance: f64, + standardised_manifest_variance: f64, ) -> Result { - let extra_latent_mean = recover_discrete_latent_mean_with_extra_process( - initial_latent_mean, - original_log_rate, - continuous_intercept, - original_from_extra_drift, - time_dependent_predictor, - extra_log_rate, - event_delta, - clock, - )?; - recover_manifest_observed_mean(loading, extra_latent_mean, manifest_mean) + let _ = ( + unstandardised_manifest_variance, + standardised_manifest_variance, + ); + Err(PsychometricError::UnstandardisedManifestVarianceIsNotStandardisedManifestVariance) } -/// Refuse treating the evolved observed mean as the extra-process -/// observed mean. +/// Refuse treating p. 16 `MANIFESTTRAITVARstd` as p. 16 +/// `MANIFESTVARstd`. /// -/// Equation 5 of the Eq. 3 evolved mean is `τ + λ μ_t`. Equation 5 -/// of the §7.2 extra-process contribution is -/// `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))`. Those -/// are not the same map. +/// Both scalar correlations equal 1 after strictly positive +/// variances. `MANIFESTTRAITVARstd` standardises indicator-level +/// trait variance `Ψ_τ`. `MANIFESTVARstd` standardises +/// contemporaneous measurement error `Θ`. Equal numbers remain +/// distinct named quantities. /// /// # Errors /// /// Always returns -/// [`PsychometricError::EvolvedObservedMeanIsNotExtraProcessObservedMean`]. -pub fn refuse_evolved_observed_mean_as_extra_process_observed_mean( - evolved_observed_mean: f64, - extra_process_observed_mean: f64, +/// [`PsychometricError::StandardisedManifestTraitVarianceIsNotStandardisedManifestVariance`]. +pub fn refuse_standardised_manifest_trait_variance_as_standardised_manifest_variance( + standardised_manifest_trait_variance: f64, + standardised_manifest_variance: f64, ) -> Result { - let _ = (evolved_observed_mean, extra_process_observed_mean); - Err(PsychometricError::EvolvedObservedMeanIsNotExtraProcessObservedMean) + let _ = ( + standardised_manifest_trait_variance, + standardised_manifest_variance, + ); + Err(PsychometricError::StandardisedManifestTraitVarianceIsNotStandardisedManifestVariance) } -/// Refuse treating the contemporaneous-impulse observed mean as the -/// extra-process observed mean. +/// Refuse treating Driver Eq. 5 `Var(y)` as p. 16 `MANIFESTVARstd`. /// -/// Equation 5 of the contemporaneous Dirac is `τ + λ(μ_t + m x)`. -/// Equation 5 of the §7.2 extra-process contribution is -/// `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))`. Those -/// are not the same map. The printed specification puts -/// `TDPREDEFFECT` on the extra process, not on the original process. +/// `λ² Var(η) + θ` is the observed-indicator variance. Table 2 +/// names `MANIFESTVAR` `Θ`, not `Var(y)`. The correlation form of +/// `Θ` is not that observed variance. /// /// # Errors /// /// Always returns -/// [`PsychometricError::ImpulseObservedMeanIsNotExtraProcessObservedMean`]. -pub fn refuse_impulse_observed_mean_as_extra_process_observed_mean( - impulse_observed_mean: f64, - extra_process_observed_mean: f64, +/// [`PsychometricError::ObservedVarianceIsNotStandardisedManifestVariance`]. +pub fn refuse_observed_variance_as_standardised_manifest_variance( + observed_indicator_variance: f64, + standardised_manifest_variance: f64, ) -> Result { - let _ = (impulse_observed_mean, extra_process_observed_mean); - Err(PsychometricError::ImpulseObservedMeanIsNotExtraProcessObservedMean) + let _ = (observed_indicator_variance, standardised_manifest_variance); + Err(PsychometricError::ObservedVarianceIsNotStandardisedManifestVariance) } -/// Refuse treating the §7.2 extra-process contribution as `E(y_t)`. +/// Exact scalar p. 16 `TIPREDVARstd` after strictly positive +/// `TIPREDVAR`. /// -/// The contribution is not `τ + λ` of the evolved-plus-contribution -/// latent mean. The extra process has `LAMBDA` 0 in the printed -/// specification. +/// Driver, Oud, and Voelkle (2017, Table 2, p. 12; p. 16; +/// footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF +/// re-opened 2026-08-23T22:53Z from +/// ) +/// name `TIPREDVAR` the variance/covariance of time-independent +/// predictors. Page 16 prints standardised matrices with the suffix +/// `std` when appropriate. The 2017-era `summary.ctsemFit.R` forms +/// `TIPREDVARstd` whenever `verbose = TRUE` and `n.TIpred > 0`, as +/// `solve(sqrt(diag(TIPREDVAR) + ridging)) %&% TIPREDVAR`. +/// `OpenMx` `%&%` is the quadratic form `t(A) %*% B %*% A`. Unlike +/// `TRAITVARstd`, that formation adds +/// `diag(c(ridging), n.TIpred)`. The default `ridging = FALSE` +/// adds 0, not `0.0001`; that ridge is a numerical hack and is not +/// this exact map. The 2017-era source assigns +/// `dimnames(TIPREDVARstd)` to `TIpredNames`; that assignment +/// matches the `n.TIpred × n.TIpred` matrix and is this map. +/// The scalar correlation is `v / v = 1` after strictly positive +/// `TIPREDVAR`. Form strictly positive `v` first, then `1 / √v`, +/// then `(1 / √v) v (1 / √v)`. Unstandardised `TIPREDVAR` is +/// defined for a zero predictor; standardised `TIPREDVAR` is not. +/// Zero `v` makes `solve(sqrt(0))` fail in the 2017-era source and +/// fails closed here. Unlike `TRAITVAR` / `MANIFESTTRAITVAR`, that +/// source does not skip forming `TIPREDVARstd` when `v = 0`; the +/// quadratic still fails. Predictor variance is an event-time +/// structural quantity, so a non-event clock fails closed. +/// `TIPREDVAR` does not require stable `a < 0`. Distinct positive +/// `v` recover the same 1. `MANIFESTVARstd` `θ / θ = 1` recovers +/// the same number and remains a distinct named quantity. +/// Section 7.2 `addedTIPREDVAR` `(B / a)² v` is extra process +/// variance, not this correlation. This is not a Kalman filter, +/// not a matrix `expm`, not DSEM, and not ctsem estimation. +/// +/// # Errors +/// +/// Returns [`PsychometricError::EventTimeRequired`] for any +/// non-event clock, +/// [`PsychometricError::StandardisedTimeIndependentPredictorVarianceRequiresPositivePredictorVariance`] +/// when `TIPREDVAR` is zero, and +/// [`PsychometricError::InvalidNumericInput`] when the variance is +/// non-finite or negative. +pub fn recover_standardised_time_independent_predictor_variance( + predictor_variance: f64, + clock: LagClock, +) -> Result { + if !clock.admits_structural_lag() { + return Err(PsychometricError::EventTimeRequired); + } + if !predictor_variance.is_finite() || predictor_variance < 0.0 { + return Err(PsychometricError::InvalidNumericInput); + } + if predictor_variance == 0.0 { + return Err( + PsychometricError::StandardisedTimeIndependentPredictorVarianceRequiresPositivePredictorVariance, + ); + } + Ok(1.0) +} + +/// Refuse treating unstandardised `TIPREDVAR` as p. 16 +/// `TIPREDVARstd`. +/// +/// Unstandardised predictor variance is defined for a zero +/// predictor. Footnote 4 `TIPREDVARstd` requires strictly positive +/// `TIPREDVAR`. Equal numbers when `v = 1` are still distinct +/// named quantities. /// /// # Errors /// /// Always returns -/// [`PsychometricError::ExtraProcessContributionIsNotObservedMean`]. -pub fn refuse_extra_process_contribution_as_observed_mean( - extra_process_contribution: f64, - extra_process_observed_mean: f64, +/// [`PsychometricError::UnstandardisedTimeIndependentPredictorVarianceIsNotStandardisedTimeIndependentPredictorVariance`]. +pub fn refuse_unstandardised_time_independent_predictor_variance_as_standardised_time_independent_predictor_variance( + unstandardised_predictor_variance: f64, + standardised_predictor_variance: f64, ) -> Result { - let _ = (extra_process_contribution, extra_process_observed_mean); - Err(PsychometricError::ExtraProcessContributionIsNotObservedMean) + let _ = ( + unstandardised_predictor_variance, + standardised_predictor_variance, + ); + Err(PsychometricError::UnstandardisedTimeIndependentPredictorVarianceIsNotStandardisedTimeIndependentPredictorVariance) } -/// Refuse treating the evolved-plus-contribution latent mean as -/// `E(y_t)`. +/// Refuse treating p. 16 `MANIFESTVARstd` as p. 16 `TIPREDVARstd`. /// -/// Equation 5 maps `E(y_t) = τ + λ` of that mean. The latent mean is -/// not the observed mean. +/// Both scalar correlations equal 1 after strictly positive +/// variances. `MANIFESTVARstd` standardises contemporaneous +/// measurement error `Θ`. `TIPREDVARstd` standardises +/// time-independent predictor variance. Equal numbers remain +/// distinct named quantities. /// /// # Errors /// /// Always returns -/// [`PsychometricError::ExtraProcessLatentMeanIsNotObservedMean`]. -pub fn refuse_extra_process_latent_mean_as_observed_mean( - extra_process_latent_mean: f64, - extra_process_observed_mean: f64, +/// [`PsychometricError::StandardisedManifestVarianceIsNotStandardisedTimeIndependentPredictorVariance`]. +pub fn refuse_standardised_manifest_variance_as_standardised_time_independent_predictor_variance( + standardised_manifest_variance: f64, + standardised_predictor_variance: f64, ) -> Result { - let _ = (extra_process_latent_mean, extra_process_observed_mean); - Err(PsychometricError::ExtraProcessLatentMeanIsNotObservedMean) + let _ = ( + standardised_manifest_variance, + standardised_predictor_variance, + ); + Err(PsychometricError::StandardisedManifestVarianceIsNotStandardisedTimeIndependentPredictorVariance) } -/// Exact scalar §7.2 extra-process contribution of a `TDPREDEFFECT` -/// impulse strictly after `t0`. +/// Refuse treating §7.2 `addedTIPREDVAR` as p. 16 `TIPREDVARstd`. /// -/// Driver, Oud, and Voelkle (2017, §7.2, pp. 22–23; JSS PDF -/// re-opened 2026-08-21T06:32Z from -/// ) -/// name `T0TDPREDEFFECT` when the extra process begins at `t = 0` -/// and `TDPREDEFFECT` when it begins after `t = 0`. The printed -/// extra `TDPREDEFFECT` is 1. The original process is driven through -/// the `DRIFT` coupling, not through a Dirac on the original -/// process. After an identification impulse at `u` with -/// `t0 < u < t` the scalar contribution is -/// `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)`. Form the -/// interior interval `t − u` first, then the extra-process -/// contribution on that interval. An impulse at `u = t0` is the -/// first-occasion extra-process map. An impulse at `u = t` has not -/// yet driven the original process. This is not a Kalman filter, -/// not a matrix `expm`, and not ctsem estimation. +/// `(B / a)² v` is extra process variance accounted for by the +/// predictor. `TIPREDVARstd` is the correlation form of the +/// predictor covariance itself. /// /// # Errors /// -/// Returns [`PsychometricError::NonPositiveInterval`] when -/// `t − u` is not strictly interior to `(0, t − t0)`, and -/// otherwise propagates -/// [`recover_level_change_extra_process_contribution`]. -pub fn recover_level_change_extra_process_contribution_after( - original_from_extra_drift: f64, - time_dependent_predictor: f64, - original_log_rate: f64, - extra_log_rate: f64, - event_delta: f64, - elapsed_after_impulse: f64, - clock: LagClock, +/// Always returns +/// [`PsychometricError::AsymptoticTimeIndependentPredictorVarianceIsNotStandardisedTimeIndependentPredictorVariance`]. +pub fn refuse_asymptotic_time_independent_predictor_variance_as_standardised_time_independent_predictor_variance( + asymptotic_predictor_variance: f64, + standardised_predictor_variance: f64, ) -> Result { - if !clock.admits_structural_lag() { - return Err(PsychometricError::EventTimeRequired); - } - if !event_delta.is_finite() || event_delta <= 0.0 { - return Err(PsychometricError::NonPositiveInterval); - } - if !elapsed_after_impulse.is_finite() || elapsed_after_impulse <= 0.0 { - return Err(PsychometricError::NonPositiveInterval); - } - if elapsed_after_impulse >= event_delta { - return Err(PsychometricError::NonPositiveInterval); - } - recover_level_change_extra_process_contribution( - original_from_extra_drift, - time_dependent_predictor, - original_log_rate, - extra_log_rate, - elapsed_after_impulse, - clock, - ) + let _ = ( + asymptotic_predictor_variance, + standardised_predictor_variance, + ); + Err(PsychometricError::AsymptoticTimeIndependentPredictorVarianceIsNotStandardisedTimeIndependentPredictorVariance) } -/// Exact scalar evolved latent mean plus a §7.2 extra-process -/// contribution after `t0`. +/// Exact scalar p. 16 `asymDIFFUSIONstd` after strictly positive +/// `asymDIFFUSION`. /// -/// Driver, Oud, and Voelkle (2017, Eq. 3, p. 5; §7.2, pp. 22–23; -/// JSS PDF re-opened 2026-08-21T06:32Z) evolve `T0MEANS` and `CINT` -/// over `Δt = t − t0`. `TDPREDEFFECT` on the extra process after -/// `t0` drives the original process only over `t − u` with -/// `t0 < u < t`. Form `μ_t` first, then add the after-t0 -/// extra-process contribution. A zero contribution is exactly -/// `μ_t`. The first-occasion extra-process map uses `Δt` for both -/// the evolution and the extra drive and is not this composition -/// when `u ≠ t0`. The impulse-carry `μ_t + e^{a(t−u)} m x` is a -/// Dirac on the original process and is not this composition. +/// Driver, Oud, and Voelkle (2017, p. 16; footnote 4; Eq. 4; +/// 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened +/// 2026-08-23T23:02Z from +/// ) +/// name `asymDIFFUSION` the total within-subject variance as +/// `Δt → ∞`. Page 16 prints standardised matrices with the suffix +/// `std` when appropriate. Footnote 4 standardises using only the +/// relevant variance, not the total. The 2017-era +/// `summary.ctsemFit.R` forms `asymDIFFUSIONstd` whenever +/// `verbose = TRUE`, as +/// `solve(sqrt(diag(asymDIFFUSION) + ridging)) %&% asymDIFFUSION`. +/// `OpenMx` `%&%` is the quadratic form `t(A) %*% B %*% A`. That +/// formation adds `diag(c(ridging), n.latent)`. The default +/// `ridging = FALSE` adds 0, not `0.0001`; that ridge is a +/// numerical hack and is not this exact map. The 2017-era source +/// assigns `dimnames(asymDIFFUSIONstd)` to `latentNames`; that +/// assignment matches the `n.latent × n.latent` matrix and is +/// this map. The scalar correlation is `p / p = 1` after +/// strictly positive Lyapunov `p = −q / (2 a)`. Form strictly +/// positive `p` first, then `1 / √p`, then `(1 / √p) p (1 / √p)`. +/// Unstandardised `asymDIFFUSION` is defined for a zero process; +/// standardised `asymDIFFUSION` is not. Zero `q` makes +/// `solve(sqrt(0))` fail in the 2017-era source and fails closed +/// here. That source does not skip forming `asymDIFFUSIONstd` +/// when `p = 0`; the quadratic still fails. Within-subject +/// variance is an event-time structural quantity, so a non-event +/// clock fails closed. Lasting `asymDIFFUSION` requires stable +/// `a < 0`. Distinct positive `p` recover the same 1. +/// `TIPREDVARstd` `v / v = 1` recovers the same number and +/// remains a distinct named quantity. `DIFFUSIONstd` +/// `q / p = −2 a` is the continuous-diffusion ratio, not this +/// correlation. This is not a Kalman filter, not a matrix +/// `expm`, not DSEM, and not ctsem estimation. /// /// # Errors /// -/// Propagates [`recover_discrete_latent_mean`] and -/// [`recover_level_change_extra_process_contribution_after`], and -/// returns [`PsychometricError::InvalidNumericInput`] when the sum -/// overflows. -#[allow(clippy::too_many_arguments)] -pub fn recover_discrete_latent_mean_with_extra_process_after( - initial_latent_mean: f64, - original_log_rate: f64, - continuous_intercept: f64, - original_from_extra_drift: f64, - time_dependent_predictor: f64, - extra_log_rate: f64, - event_delta: f64, - elapsed_after_impulse: f64, +/// Propagates [`recover_stationary_latent_variance`]. Returns +/// [`PsychometricError::EventTimeRequired`] for any non-event +/// clock, +/// [`PsychometricError::StationaryVarianceRequiresStableDrift`] +/// when the log-rate is not strictly negative, +/// [`PsychometricError::StandardisedAsymptoticDiffusionRequiresPositiveWithinSubjectVariance`] +/// when `asymDIFFUSION` is zero, and +/// [`PsychometricError::InvalidNumericInput`] when an input is +/// non-finite, negative, or the stationary variance overflows. +pub fn recover_standardised_asymptotic_diffusion( + continuous_diffusion: f64, + log_rate: f64, clock: LagClock, ) -> Result { - let evolved_latent_mean = recover_discrete_latent_mean( - initial_latent_mean, - original_log_rate, - continuous_intercept, - event_delta, - clock, - )?; - let contribution = recover_level_change_extra_process_contribution_after( - original_from_extra_drift, - time_dependent_predictor, - original_log_rate, - extra_log_rate, - event_delta, - elapsed_after_impulse, - clock, - )?; - if contribution == 0.0 { - return Ok(evolved_latent_mean); - } - if evolved_latent_mean == 0.0 { - return Ok(contribution); + let within = recover_stationary_latent_variance(continuous_diffusion, log_rate, clock)?; + if within == 0.0 { + return Err( + PsychometricError::StandardisedAsymptoticDiffusionRequiresPositiveWithinSubjectVariance, + ); } - require_finite(evolved_latent_mean + contribution) + Ok(1.0) } -/// Exact scalar observed mean of a §7.2 extra-process contribution -/// after `t0`. +/// Refuse treating unstandardised `asymDIFFUSION` as p. 16 +/// `asymDIFFUSIONstd`. /// -/// Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; §7.2, pp. 22–23; -/// JSS PDF re-opened 2026-08-21T06:32Z) write -/// `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and -/// `Γ ~ N(τ, Ψ)`. The printed extra process has `LAMBDA` 0. -/// Original indicators load on the original process after the -/// `DRIFT` coupling over `t − u` with `t0 < u < t`. The scalar -/// composition is -/// `E(y_t) = τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))`. -/// Form the evolved-plus-after-contribution latent mean first, then -/// `τ + λ` of that mean. The first-occasion extra-process observed -/// mean uses `Δt` for both the evolution and the extra drive and is -/// not this composition when `u ≠ t0`. The evolved observed mean -/// `τ + λ μ_t` is not this composition. The impulse-carry map -/// `τ + λ(μ_t + e^{a(t−u)} m x)` is not this composition. The -/// extra process itself is not an observed indicator. This is not a -/// Kalman filter and not ctsem estimation. +/// Unstandardised within-subject variance is defined for a zero +/// process. Footnote 4 `asymDIFFUSIONstd` requires strictly +/// positive `asymDIFFUSION`. Equal numbers when `p = 1` are +/// still distinct named quantities. /// /// # Errors /// -/// Propagates [`recover_discrete_latent_mean_with_extra_process_after`] -/// and [`recover_manifest_observed_mean`]. -#[allow(clippy::too_many_arguments)] -pub fn recover_discrete_observed_mean_with_extra_process_after( - loading: f64, - initial_latent_mean: f64, - original_log_rate: f64, - continuous_intercept: f64, - original_from_extra_drift: f64, - time_dependent_predictor: f64, - extra_log_rate: f64, - manifest_mean: f64, - event_delta: f64, - elapsed_after_impulse: f64, - clock: LagClock, +/// Always returns +/// [`PsychometricError::UnstandardisedAsymptoticDiffusionIsNotStandardisedAsymptoticDiffusion`]. +pub fn refuse_unstandardised_asymptotic_diffusion_as_standardised_asymptotic_diffusion( + unstandardised_asymptotic_diffusion: f64, + standardised_asymptotic_diffusion: f64, ) -> Result { - let extra_latent_mean = recover_discrete_latent_mean_with_extra_process_after( - initial_latent_mean, - original_log_rate, - continuous_intercept, - original_from_extra_drift, - time_dependent_predictor, - extra_log_rate, - event_delta, - elapsed_after_impulse, - clock, - )?; - recover_manifest_observed_mean(loading, extra_latent_mean, manifest_mean) + let _ = ( + unstandardised_asymptotic_diffusion, + standardised_asymptotic_diffusion, + ); + Err(PsychometricError::UnstandardisedAsymptoticDiffusionIsNotStandardisedAsymptoticDiffusion) } -/// Refuse treating the first-occasion extra-process observed mean -/// as the after-t0 extra-process observed mean. +/// Refuse treating p. 16 `TIPREDVARstd` as p. 16 `asymDIFFUSIONstd`. /// -/// `T0TDPREDEFFECT` on the extra process uses `Δt = t − t0`. -/// `TDPREDEFFECT` after `t0` uses `t − u` with `t0 < u < t`. +/// Both scalar correlations equal 1 after strictly positive +/// variances. `TIPREDVARstd` standardises predictor covariance. +/// `asymDIFFUSIONstd` standardises within-subject process +/// variance. Equal numbers remain distinct named quantities. /// /// # Errors /// /// Always returns -/// [`PsychometricError::ExtraProcessObservedMeanIsNotAfterExtraProcessObservedMean`]. -pub fn refuse_extra_process_observed_mean_as_after_extra_process_observed_mean( - extra_process_observed_mean: f64, - after_extra_process_observed_mean: f64, +/// [`PsychometricError::StandardisedTimeIndependentPredictorVarianceIsNotStandardisedAsymptoticDiffusion`]. +pub fn refuse_standardised_time_independent_predictor_variance_as_standardised_asymptotic_diffusion( + standardised_predictor_variance: f64, + standardised_asymptotic_diffusion: f64, ) -> Result { let _ = ( - extra_process_observed_mean, - after_extra_process_observed_mean, + standardised_predictor_variance, + standardised_asymptotic_diffusion, ); - Err(PsychometricError::ExtraProcessObservedMeanIsNotAfterExtraProcessObservedMean) + Err(PsychometricError::StandardisedTimeIndependentPredictorVarianceIsNotStandardisedAsymptoticDiffusion) } -/// Refuse treating the evolved observed mean as the after-t0 -/// extra-process observed mean. +/// Refuse treating p. 16 `DIFFUSIONstd` as p. 16 `asymDIFFUSIONstd`. /// -/// Equation 5 of the Eq. 3 evolved mean is `τ + λ μ_t`. Equation 5 -/// of the after-t0 extra-process contribution is -/// `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))`. +/// `q / p = −2 a` is the continuous-diffusion ratio after +/// strictly positive `asymDIFFUSION`. `asymDIFFUSIONstd` is the +/// correlation form `p / p = 1` of that same within-subject +/// variance. Equal numbers when `a = −0.5` remain distinct +/// named quantities. /// /// # Errors /// /// Always returns -/// [`PsychometricError::EvolvedObservedMeanIsNotAfterExtraProcessObservedMean`]. -pub fn refuse_evolved_observed_mean_as_after_extra_process_observed_mean( - evolved_observed_mean: f64, - after_extra_process_observed_mean: f64, +/// [`PsychometricError::StandardisedContinuousDiffusionIsNotStandardisedAsymptoticDiffusion`]. +pub fn refuse_standardised_continuous_diffusion_as_standardised_asymptotic_diffusion( + standardised_continuous_diffusion: f64, + standardised_asymptotic_diffusion: f64, ) -> Result { - let _ = (evolved_observed_mean, after_extra_process_observed_mean); - Err(PsychometricError::EvolvedObservedMeanIsNotAfterExtraProcessObservedMean) + let _ = ( + standardised_continuous_diffusion, + standardised_asymptotic_diffusion, + ); + Err(PsychometricError::StandardisedContinuousDiffusionIsNotStandardisedAsymptoticDiffusion) } -/// Refuse treating the impulse-carry observed mean as the after-t0 -/// extra-process observed mean. +/// Exact scalar p. 16 `discreteCINTstd` after strictly positive +/// `asymDIFFUSION`. /// -/// `e^{a(t−u)} m x` is a Dirac on the original process. Extra-process -/// `TDPREDEFFECT` after `t0` drives the original process through -/// `DRIFT`. +/// Driver, Oud, and Voelkle (2017, p. 16; footnote 4; Eq. 3, p. 4; +/// Table 2, p. 12; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF +/// re-opened 2026-08-24T05:20Z from +/// ) +/// print standardised matrices with the suffix `std` when appropriate. +/// Footnote 4 standardises using only the relevant variance, not the +/// total. `CINT` is the process intercept of individual, or average +/// individual, dynamics, so that relevant variance is within-subject +/// `asymDIFFUSION` `p = −q / (2 a)`. The 2017-era +/// `summary.ctsemFit.R` forms `discreteCINT` whenever +/// `verbose = TRUE`, as +/// `solve(DRIFT) %*% (discreteDRIFT − I) %*% CINT`. That source does +/// not form a `discreteCINTstd` matrix; the scalar map here is the +/// footnote 4 standardisation of that named discrete intercept: +/// `A^{-1}[e^{A Δt} − I] κ / √p`. Form strictly positive `p` first, +/// then the discrete intercept, then divide by `√p`. A zero intercept +/// is exactly zero. Unstandardised `discreteCINT` is defined for +/// growing `a ≥ 0` and for zero diffusion; standardised +/// `discreteCINT` is not. Zero `q` has no positive process SD and +/// fails closed. Lasting `asymDIFFUSION` requires stable `a < 0`. +/// A non-event clock fails closed. A non-positive event interval +/// fails closed. `κ / √p` does not depend on `Δt` and is not this +/// finite-interval map. `(-κ / a) / √p` is the standardised +/// asymptotic intercept and is not this map. This is not a Kalman +/// filter, not a matrix `expm`, not DSEM, not `CINTstd`, and not +/// ctsem estimation. +/// +/// # Errors +/// +/// Propagates [`recover_stationary_latent_variance`] and +/// [`recover_discrete_continuous_intercept_effect`]. Returns +/// [`PsychometricError::EventTimeRequired`] for any non-event +/// clock, +/// [`PsychometricError::StationaryVarianceRequiresStableDrift`] +/// when the log-rate is not strictly negative, +/// [`PsychometricError::NonPositiveInterval`] when `event_delta` +/// is not strictly positive, +/// [`PsychometricError::StandardisedDiscreteContinuousInterceptRequiresPositiveWithinSubjectVariance`] +/// when `asymDIFFUSION` is zero, and +/// [`PsychometricError::InvalidNumericInput`] when the diffusion or +/// log-rate is non-finite/invalid, an input is non-finite, or the +/// mapped ratio overflows. Negative intercepts remain valid signed +/// effects. +pub fn recover_standardised_discrete_continuous_intercept( + continuous_intercept: f64, + continuous_diffusion: f64, + log_rate: f64, + event_delta: f64, + clock: LagClock, +) -> Result { + let within = recover_stationary_latent_variance(continuous_diffusion, log_rate, clock)?; + if within == 0.0 { + return Err( + PsychometricError::StandardisedDiscreteContinuousInterceptRequiresPositiveWithinSubjectVariance, + ); + } + let discrete = recover_discrete_continuous_intercept_effect( + continuous_intercept, + log_rate, + event_delta, + clock, + )?; + let process_sd = within.sqrt(); + require_finite(discrete / process_sd) +} + +/// Refuse treating unstandardised `discreteCINT` as p. 16 +/// `discreteCINTstd`. +/// +/// Unstandardised discrete intercept is defined for growing +/// `a ≥ 0` and for zero diffusion. Footnote 4 `discreteCINTstd` +/// requires strictly positive `asymDIFFUSION`. Equal numbers +/// when `p = 1` are still distinct named quantities. /// /// # Errors /// /// Always returns -/// [`PsychometricError::ImpulseCarryObservedMeanIsNotAfterExtraProcessObservedMean`]. -pub fn refuse_impulse_carry_observed_mean_as_after_extra_process_observed_mean( - impulse_carry_observed_mean: f64, - after_extra_process_observed_mean: f64, +/// [`PsychometricError::UnstandardisedDiscreteContinuousInterceptIsNotStandardisedDiscreteContinuousIntercept`]. +pub fn refuse_unstandardised_discrete_continuous_intercept_as_standardised_discrete_continuous_intercept( + unstandardised_discrete_intercept: f64, + standardised_discrete_intercept: f64, ) -> Result { let _ = ( - impulse_carry_observed_mean, - after_extra_process_observed_mean, + unstandardised_discrete_intercept, + standardised_discrete_intercept, ); - Err(PsychometricError::ImpulseCarryObservedMeanIsNotAfterExtraProcessObservedMean) + Err(PsychometricError::UnstandardisedDiscreteContinuousInterceptIsNotStandardisedDiscreteContinuousIntercept) } -/// Refuse treating the after-t0 extra-process contribution as -/// `E(y_t)`. +/// Refuse treating `κ / √p` as p. 16 `discreteCINTstd`. /// -/// The contribution is not `τ + λ` of the -/// evolved-plus-after-contribution latent mean. The extra process -/// has `LAMBDA` 0 in the printed specification. +/// Footnote 4 continuous intercept standardisation does not +/// depend on the event interval. `discreteCINTstd` is +/// `A^{-1}[e^{A Δt} − I] κ / √p`. Equal numbers when +/// `(e^{a Δt} − 1)/a = 1` remain distinct named quantities. /// /// # Errors /// /// Always returns -/// [`PsychometricError::AfterExtraProcessContributionIsNotObservedMean`]. -pub fn refuse_after_extra_process_contribution_as_observed_mean( - after_extra_process_contribution: f64, - after_extra_process_observed_mean: f64, +/// [`PsychometricError::StandardisedContinuousInterceptIsNotStandardisedDiscreteContinuousIntercept`]. +pub fn refuse_standardised_continuous_intercept_as_standardised_discrete_continuous_intercept( + standardised_continuous_intercept: f64, + standardised_discrete_intercept: f64, ) -> Result { let _ = ( - after_extra_process_contribution, - after_extra_process_observed_mean, + standardised_continuous_intercept, + standardised_discrete_intercept, ); - Err(PsychometricError::AfterExtraProcessContributionIsNotObservedMean) + Err(PsychometricError::StandardisedContinuousInterceptIsNotStandardisedDiscreteContinuousIntercept) } -/// Refuse treating the evolved-plus-after-contribution latent mean -/// as `E(y_t)`. +/// Refuse treating `(-κ / a) / √p` as p. 16 `discreteCINTstd`. /// -/// Equation 5 maps `E(y_t) = τ + λ` of that mean. The latent mean -/// is not the observed mean. +/// Table 2 `asymCINT` `/ √p` is the standardised total intercept +/// change as `Δt → ∞`. A finite event interval is not that limit. /// /// # Errors /// /// Always returns -/// [`PsychometricError::AfterExtraProcessLatentMeanIsNotObservedMean`]. -pub fn refuse_after_extra_process_latent_mean_as_observed_mean( - after_extra_process_latent_mean: f64, - after_extra_process_observed_mean: f64, +/// [`PsychometricError::AsymptoticStandardisedContinuousInterceptIsNotStandardisedDiscreteContinuousIntercept`]. +pub fn refuse_asymptotic_standardised_continuous_intercept_as_standardised_discrete_continuous_intercept( + asymptotic_standardised_intercept: f64, + standardised_discrete_intercept: f64, ) -> Result { let _ = ( - after_extra_process_latent_mean, - after_extra_process_observed_mean, + asymptotic_standardised_intercept, + standardised_discrete_intercept, ); - Err(PsychometricError::AfterExtraProcessLatentMeanIsNotObservedMean) + Err(PsychometricError::AsymptoticStandardisedContinuousInterceptIsNotStandardisedDiscreteContinuousIntercept) } -/// Exact scalar evolved latent mean plus a contemporaneous impulse. +/// Exact scalar p. 16 `asymCINTstd` after strictly positive +/// `asymDIFFUSION`. /// -/// Driver, Oud, and Voelkle (2017, Eq. 3, p. 5) write the first two -/// summands as the carried `T0MEANS` and `CINT` increment, then add -/// the fourth-summand impulse at the observation instant. Form `μ_t` -/// first, then add `m x`. A zero impulse is exactly `μ_t`. A zero -/// evolved mean is exactly the impulse. The first-occasion map -/// `μ_0 + m x` is not this composition when the process has already -/// evolved. The level-change form is not this map. +/// Driver, Oud, and Voelkle (2017, p. 16; footnote 4; Eq. 3, p. 4; +/// Table 2, p. 12; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF +/// re-opened 2026-08-24T09:05Z from +/// ) +/// print standardised matrices with the suffix `std` when appropriate. +/// Footnote 4 standardises using only the relevant variance, not the +/// total. `CINT` is the process intercept of individual, or average +/// individual, dynamics, so that relevant variance is within-subject +/// `asymDIFFUSION` `p = −q / (2 a)`. The 2017-era +/// `summary.ctsemFit.R` forms `asymCINT` whenever +/// `verbose = TRUE`, as `-solve(DRIFT) %*% CINT`. That source does +/// not form an `asymCINTstd` matrix; the scalar map here is the +/// footnote 4 standardisation of that named asymptotic intercept: +/// `(-κ / a) / √p`. Form strictly positive `p` first, then the +/// asymptotic intercept, then divide by `√p`. A zero intercept is +/// exactly zero. Unstandardised `asymCINT` is defined for a zero +/// process; standardised `asymCINT` is not. Zero `q` has no +/// positive process SD and fails closed. Lasting `asymDIFFUSION` +/// requires stable `a < 0`. A non-event clock fails closed. +/// `κ / √p` is the continuous intercept standardisation and is not +/// this total-change map. `A^{-1}[e^{A Δt} − I] κ / √p` depends on +/// the event interval and is not this `Δt → ∞` map. This is not a +/// Kalman filter, not a matrix `expm`, not DSEM, not `CINTstd`, +/// not `discreteCINTstd`, and not ctsem estimation. /// /// # Errors /// -/// Propagates [`recover_discrete_latent_mean`] and -/// [`recover_time_dependent_predictor_impulse`], and returns -/// [`PsychometricError::InvalidNumericInput`] when the sum overflows. -pub fn recover_discrete_latent_mean_with_impulse( - initial_latent_mean: f64, - log_rate: f64, +/// Propagates [`recover_stationary_latent_variance`] and +/// [`recover_asymptotic_continuous_intercept`]. Returns +/// [`PsychometricError::EventTimeRequired`] for any non-event +/// clock, +/// [`PsychometricError::StationaryVarianceRequiresStableDrift`] +/// when the log-rate is not strictly negative, +/// [`PsychometricError::StandardisedAsymptoticContinuousInterceptRequiresPositiveWithinSubjectVariance`] +/// when `asymDIFFUSION` is zero, and +/// [`PsychometricError::InvalidNumericInput`] when the diffusion or +/// log-rate is non-finite/invalid, an input is non-finite, or the +/// mapped ratio overflows. Negative intercepts remain valid signed +/// effects. +pub fn recover_standardised_asymptotic_continuous_intercept( continuous_intercept: f64, - time_dependent_effect: f64, - time_dependent_predictor: f64, - event_delta: f64, + continuous_diffusion: f64, + log_rate: f64, clock: LagClock, ) -> Result { - let evolved_latent_mean = recover_discrete_latent_mean( - initial_latent_mean, - log_rate, - continuous_intercept, - event_delta, - clock, - )?; - let impulse = - recover_time_dependent_predictor_impulse(time_dependent_effect, time_dependent_predictor)?; - if impulse == 0.0 { - return Ok(evolved_latent_mean); - } - if evolved_latent_mean == 0.0 { - return Ok(impulse); + let within = recover_stationary_latent_variance(continuous_diffusion, log_rate, clock)?; + if within == 0.0 { + return Err( + PsychometricError::StandardisedAsymptoticContinuousInterceptRequiresPositiveWithinSubjectVariance, + ); } - require_finite(evolved_latent_mean + impulse) + let asymptotic = + recover_asymptotic_continuous_intercept(continuous_intercept, log_rate, clock)?; + let process_sd = within.sqrt(); + require_finite(asymptotic / process_sd) } -/// Exact scalar observed mean of a contemporaneous impulse. +/// Refuse treating unstandardised `asymCINT` as p. 16 +/// `asymCINTstd`. /// -/// Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Eq. 1–3, pp. 4–5; -/// Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T09:01Z -/// from -/// ) -/// write `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and -/// `Γ ~ N(τ, Ψ)`. The expected intercept is `τ`. The latent process -/// at `t` after a contemporaneous Dirac (`u = t`) is `μ_t + m x`. -/// The scalar composition is `E(y_t) = τ + λ(μ_t + m x)`. Form the -/// evolved-plus-impulse latent mean first, then `τ + λ` of that -/// mean. Table 2 names `τ` `MANIFESTMEANS`. A zero loading is -/// exactly `τ`. A zero evolved-plus-impulse latent mean is exactly -/// `τ`. A zero intercept is exactly `λ(μ_t + m x)`. The evolved -/// observed mean `τ + λ μ_t` is not this composition when the -/// impulse is nonzero. The carry map -/// `τ + λ(μ_t + e^{a(t−u)} m x)` is not this composition when -/// `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The -/// evolved-plus-impulse latent mean is not `E(y_t)`. The §7.2 -/// level-change form is a different specification and is not this -/// map. This is not a Kalman filter and not ctsem estimation. +/// Unstandardised asymptotic intercept is defined for a zero +/// process. Footnote 4 `asymCINTstd` requires strictly +/// positive `asymDIFFUSION`. Equal numbers when `p = 1` are +/// still distinct named quantities. /// /// # Errors /// -/// Propagates [`recover_discrete_latent_mean_with_impulse`] and -/// [`recover_manifest_observed_mean`]. -#[allow(clippy::too_many_arguments)] -pub fn recover_discrete_observed_mean_with_impulse( - loading: f64, - initial_latent_mean: f64, - log_rate: f64, - continuous_intercept: f64, - time_dependent_effect: f64, - time_dependent_predictor: f64, - manifest_mean: f64, - event_delta: f64, - clock: LagClock, +/// Always returns +/// [`PsychometricError::UnstandardisedAsymptoticContinuousInterceptIsNotStandardisedAsymptoticContinuousIntercept`]. +pub fn refuse_unstandardised_asymptotic_continuous_intercept_as_standardised_asymptotic_continuous_intercept( + unstandardised_asymptotic_intercept: f64, + standardised_asymptotic_intercept: f64, ) -> Result { - let impulse_latent_mean = recover_discrete_latent_mean_with_impulse( - initial_latent_mean, - log_rate, - continuous_intercept, - time_dependent_effect, - time_dependent_predictor, - event_delta, - clock, - )?; - recover_manifest_observed_mean(loading, impulse_latent_mean, manifest_mean) + let _ = ( + unstandardised_asymptotic_intercept, + standardised_asymptotic_intercept, + ); + Err(PsychometricError::UnstandardisedAsymptoticContinuousInterceptIsNotStandardisedAsymptoticContinuousIntercept) } -/// Refuse treating the evolved observed mean as the contemporaneous- -/// impulse observed mean. +/// Refuse treating `κ / √p` as p. 16 `asymCINTstd`. /// -/// Equation 5 of the Eq. 3 evolved mean is `τ + λ μ_t`. Equation 5 -/// of the Eq. 3 contemporaneous impulse is `τ + λ(μ_t + m x)`. -/// Those are not the same map. +/// Footnote 4 continuous intercept standardisation is not the +/// standardised total intercept change `(-κ / a) / √p`. /// /// # Errors /// /// Always returns -/// [`PsychometricError::EvolvedObservedMeanIsNotImpulseObservedMean`]. -pub fn refuse_evolved_observed_mean_as_impulse_observed_mean( - evolved_observed_mean: f64, - impulse_observed_mean: f64, +/// [`PsychometricError::StandardisedContinuousInterceptIsNotStandardisedAsymptoticContinuousIntercept`]. +pub fn refuse_standardised_continuous_intercept_as_standardised_asymptotic_continuous_intercept( + standardised_continuous_intercept: f64, + standardised_asymptotic_intercept: f64, ) -> Result { - let _ = (evolved_observed_mean, impulse_observed_mean); - Err(PsychometricError::EvolvedObservedMeanIsNotImpulseObservedMean) + let _ = ( + standardised_continuous_intercept, + standardised_asymptotic_intercept, + ); + Err(PsychometricError::StandardisedContinuousInterceptIsNotStandardisedAsymptoticContinuousIntercept) } -/// Refuse treating the contemporaneous-impulse observed mean as the -/// impulse-carry observed mean. +/// Refuse treating p. 16 `discreteCINTstd` as p. 16 +/// `asymCINTstd`. /// -/// Equation 5 of the contemporaneous Dirac is `τ + λ(μ_t + m x)`. -/// Equation 5 of the Eq. 1–2 carried latent mean is -/// `τ + λ(μ_t + e^{a(t−u)} m x)`. Those are not the same map when -/// `u ≠ t`. +/// `A^{-1}[e^{A Δt} − I] κ / √p` is the standardised finite +/// interval. `(-κ / a) / √p` is the `Δt → ∞` limit. /// /// # Errors /// /// Always returns -/// [`PsychometricError::ImpulseObservedMeanIsNotImpulseCarryObservedMean`]. -pub fn refuse_impulse_observed_mean_as_impulse_carry_observed_mean( - impulse_observed_mean: f64, - impulse_carry_observed_mean: f64, +/// [`PsychometricError::StandardisedDiscreteContinuousInterceptIsNotStandardisedAsymptoticContinuousIntercept`]. +pub fn refuse_standardised_discrete_continuous_intercept_as_standardised_asymptotic_continuous_intercept( + standardised_discrete_intercept: f64, + standardised_asymptotic_intercept: f64, ) -> Result { - let _ = (impulse_observed_mean, impulse_carry_observed_mean); - Err(PsychometricError::ImpulseObservedMeanIsNotImpulseCarryObservedMean) + let _ = ( + standardised_discrete_intercept, + standardised_asymptotic_intercept, + ); + Err(PsychometricError::StandardisedDiscreteContinuousInterceptIsNotStandardisedAsymptoticContinuousIntercept) } -/// Refuse treating the Eq. 3 impulse as `CINT`. +/// Exact scalar p. 16 `T0MEANSstd` after strictly positive free +/// `T0VAR`. /// -/// Table 2 names `M` `TDPREDEFFECT` and `κ` `CINT`. The impulse is -/// `m x`. The continuous intercept is not that jump. +/// Driver, Oud, and Voelkle (2017, Table 2, p. 12; p. 16; footnote +/// 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened +/// 2026-08-24T22:30Z from +/// ) +/// name `T0MEANS` the latent process means at the first time point +/// `T0`. Page 16 prints standardised matrices with the suffix +/// `std` when appropriate. Footnote 4: standardisations use only +/// the relevant variance, not the total. The first-occasion +/// relevant variance is free `T0VAR` `p_0`, not within-subject +/// `asymDIFFUSION` `-q / (2 a)`, because Table 2 is the first +/// occasion, not the process dynamics. The 2017-era +/// `summary.ctsemFit.R` forms unstandardised `T0MEANS` as +/// `OpenMx::mxEval(T0MEANS, mxobj, compute=TRUE)`. That source +/// does not form a `T0MEANSstd` matrix; the scalar map here is +/// the footnote 4 standardisation of that named first-occasion +/// mean: `μ_0 / √p_0`. Form strictly positive `p_0` first, then +/// divide `μ_0` by `√p_0`. A zero mean is exactly zero. +/// Unstandardised `T0MEANS` is defined for a zero first-occasion +/// variance; standardised `T0MEANS` is not. Zero `p_0` has no +/// positive SD and fails closed. `T0` is an event-time occasion, +/// so a non-event clock fails closed. Free `T0MEANS` does not +/// require stable `a < 0`. `T0VARstd` `p_0 / p_0 = 1` recovers +/// the same number when `μ_0 = √p_0` and remains a distinct +/// named quantity. `μ_0 / √asymDIFFUSION` uses process-dynamics +/// variance and is not this first-occasion map. This is not a +/// Kalman filter, not a matrix `expm`, not DSEM, and not ctsem +/// estimation. +/// +/// # Errors +/// +/// Returns [`PsychometricError::EventTimeRequired`] for any +/// non-event clock, +/// [`PsychometricError::StandardisedInitialLatentMeanRequiresPositiveInitialLatentVariance`] +/// when `T0VAR` is zero, and +/// [`PsychometricError::InvalidNumericInput`] when the mean is +/// non-finite, the variance is non-finite or negative, or the +/// mapped ratio overflows. Negative means remain valid signed +/// locations. +pub fn recover_standardised_initial_latent_mean( + initial_latent_mean: f64, + initial_latent_variance: f64, + clock: LagClock, +) -> Result { + if !clock.admits_structural_lag() { + return Err(PsychometricError::EventTimeRequired); + } + if !initial_latent_variance.is_finite() || initial_latent_variance < 0.0 { + return Err(PsychometricError::InvalidNumericInput); + } + if initial_latent_variance == 0.0 { + return Err( + PsychometricError::StandardisedInitialLatentMeanRequiresPositiveInitialLatentVariance, + ); + } + let mean = require_finite(initial_latent_mean)?; + if mean == 0.0 { + return Ok(0.0); + } + let process_sd = initial_latent_variance.sqrt(); + require_finite(mean / process_sd) +} + +/// Refuse treating unstandardised `T0MEANS` as p. 16 +/// `T0MEANSstd`. +/// +/// Free `T0MEANS` `μ_0` is defined for a zero first-occasion +/// variance. Footnote 4 `T0MEANSstd` requires strictly positive +/// `p_0`. Equal numbers when `p_0 = 1` are still distinct named +/// quantities. /// /// # Errors /// /// Always returns -/// [`PsychometricError::TimeDependentImpulseIsNotContinuousIntercept`]. -pub fn refuse_time_dependent_impulse_as_continuous_intercept( - time_dependent_impulse: f64, - continuous_intercept: f64, +/// [`PsychometricError::UnstandardisedInitialLatentMeanIsNotStandardisedInitialLatentMean`]. +pub fn refuse_unstandardised_initial_latent_mean_as_standardised_initial_latent_mean( + unstandardised_initial_mean: f64, + standardised_initial_mean: f64, ) -> Result { - let _ = (time_dependent_impulse, continuous_intercept); - Err(PsychometricError::TimeDependentImpulseIsNotContinuousIntercept) + let _ = (unstandardised_initial_mean, standardised_initial_mean); + Err(PsychometricError::UnstandardisedInitialLatentMeanIsNotStandardisedInitialLatentMean) } -/// Refuse treating the Eq. 3 impulse as the time-independent effect. +/// Refuse treating p. 16 `T0VARstd` as p. 16 `T0MEANSstd`. /// -/// The third summand is `A^{-1}[e^{A Δt} − I] B z`. Table 2 names -/// `B` `TIPREDEFFECT`. The fourth-summand impulse is `M x`. +/// Both scalar maps equal 1 when `μ_0 = √p_0`. `T0VARstd` is +/// the correlation form of free `T0VAR`. `T0MEANSstd` is the +/// first-occasion mean. Equal numbers remain distinct named +/// quantities. /// /// # Errors /// /// Always returns -/// [`PsychometricError::TimeDependentImpulseIsNotTimeIndependentEffect`]. -pub fn refuse_time_dependent_impulse_as_time_independent_effect( - time_dependent_impulse: f64, - time_independent_effect: f64, +/// [`PsychometricError::StandardisedInitialLatentVarianceIsNotStandardisedInitialLatentMean`]. +pub fn refuse_standardised_initial_latent_variance_as_standardised_initial_latent_mean( + standardised_initial_variance: f64, + standardised_initial_mean: f64, ) -> Result { - let _ = (time_dependent_impulse, time_independent_effect); - Err(PsychometricError::TimeDependentImpulseIsNotTimeIndependentEffect) + let _ = (standardised_initial_variance, standardised_initial_mean); + Err(PsychometricError::StandardisedInitialLatentVarianceIsNotStandardisedInitialLatentMean) } -/// Refuse treating the Eq. 3 impulse as Voelkle et al. (2012, Eq. 14). +/// Refuse treating `μ_0 / √asymDIFFUSION` as p. 16 `T0MEANSstd`. /// -/// Equation 14 is `a_{yx} Δt` for a piecewise-constant time-varying -/// predictor whose sampling interval equals its constancy interval. -/// The Dirac impulse is `m x`. +/// Footnote 4 first-occasion standardisation uses free `T0VAR`, +/// not process-dynamics `asymDIFFUSION`. /// /// # Errors /// /// Always returns -/// [`PsychometricError::TimeDependentImpulseIsNotTimeVaryingDiscreteEffect`]. -pub fn refuse_time_dependent_impulse_as_time_varying_discrete_effect( - time_dependent_impulse: f64, - time_varying_discrete_effect: f64, +/// [`PsychometricError::WithinSubjectScaledInitialLatentMeanIsNotStandardisedInitialLatentMean`]. +pub fn refuse_within_subject_scaled_initial_latent_mean_as_standardised_initial_latent_mean( + within_subject_scaled_mean: f64, + standardised_initial_mean: f64, ) -> Result { - let _ = (time_dependent_impulse, time_varying_discrete_effect); - Err(PsychometricError::TimeDependentImpulseIsNotTimeVaryingDiscreteEffect) + let _ = (within_subject_scaled_mean, standardised_initial_mean); + Err(PsychometricError::WithinSubjectScaledInitialLatentMeanIsNotStandardisedInitialLatentMean) } -/// Exact scalar discrete time-independent predictor effect from -/// Driver Equation 3. +/// Exact scalar 2017-era `addedT0TIPREDVAR` after a first-occasion +/// time-independent predictor. /// -/// Driver, Oud, and Voelkle (2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; -/// JSS PDF re-opened 2026-08-20T10:13Z from +/// Driver, Oud, and Voelkle (2017, Table 3, p. 13; p. 16; §7.2, +/// pp. 20–21; Eq. 3, p. 5; 2017-era ctsem `summary.ctsemFit.R`; +/// JSS PDF re-opened 2026-08-23T18:20Z from /// ) -/// write the latent SDE -/// `dη = (A η + b + A_{ηξ} ξ + B z) dt + G dW + M dχ`. Equation 3's -/// second summand is `A^{-1}[e^{A Δt} − I](b + A_{ηξ} ξ + B z)`. -/// Table 2 names `B` `TIPREDEFFECT`, `κ`/`b` `CINT`, and `M` -/// `TDPREDEFFECT`. The scalar map of the time-independent predictor -/// is `(e^{a Δt} − 1)/a · B z` for `a ≠ 0`. Form `B z` first, then -/// the discrete intercept map. A zero drift is the Eq. 3 integral -/// `B z Δt`. A zero effect or zero predictor is exactly zero. -/// `TIPREDEFFECT` is `B`, not that discrete increment. `B z` is not -/// `CINT`. `A^{-1}[e^{A Δt} − I] B z` is not the contemporaneous -/// impulse `M x` and is not Voelkle et al. (2012, Eq. 14) `a_{yx} Δt`. -/// This is not a Kalman filter and not ctsem estimation. +/// name `T0TIPREDEFFECT` the effect of time-independent predictors +/// on latents at `T0`. Page 16 prints extra summary matrices when +/// `verbose = TRUE`. The 2017-era `summary.ctsemFit.R` forms +/// `addedT0TIPREDVAR` as +/// `T0TIPREDEFFECT %*% TIPREDVAR %*% t(T0TIPREDEFFECT)` immediately +/// after `T0TIPREDEFFECTstd`. Section 7.2 names `addedTIPREDVAR` the +/// stable between-subject variance accounted for by +/// time-independent predictors at the process asymptote, +/// `(B / a)² v`. The first-occasion analogue uses free +/// `T0TIPREDEFFECT`, not `-B / a`. The scalar map is `t0_b² v`. +/// Form `t0_b` first, then square, then multiply by `v`. A zero +/// coefficient or zero predictor variance is exactly zero. `v < 0` +/// fails closed. `T0` is an event-time occasion, so a non-event +/// clock fails closed. Free `T0TIPREDEFFECT` does not require +/// stable `a < 0`. `(B / a)² v` is `addedTIPREDVAR` and is not this +/// first-occasion map. `t0_b · √v / √p_0` is `T0TIPREDEFFECTstd` +/// and is not this variance. Free `T0VAR` `p_0` is the +/// first-occasion state, not the extra TI variance. `TRAITVAR` is a +/// zero-drift latent process, not `t0_b² v`. This is not a Kalman +/// filter, not a matrix `expm`, not DSEM, and not ctsem estimation. /// /// # Errors /// -/// Returns [`PsychometricError::EventTimeRequired`] for any non-event -/// clock, [`PsychometricError::NonPositiveInterval`] when -/// `event_delta` is not strictly positive, and +/// Returns [`PsychometricError::EventTimeRequired`] for any +/// non-event clock and /// [`PsychometricError::InvalidNumericInput`] when an input is -/// non-finite or `B z` or the mapped increment overflows. -pub fn recover_discrete_time_independent_predictor_effect( - time_independent_effect: f64, - time_independent_predictor: f64, - log_rate: f64, - event_delta: f64, +/// non-finite, the predictor variance is negative, or the product +/// overflows. +pub fn recover_initial_time_independent_predictor_variance( + initial_time_independent_effect: f64, + predictor_variance: f64, clock: LagClock, ) -> Result { if !clock.admits_structural_lag() { return Err(PsychometricError::EventTimeRequired); } - if !event_delta.is_finite() || event_delta <= 0.0 { - return Err(PsychometricError::NonPositiveInterval); - } - if !time_independent_effect.is_finite() - || !time_independent_predictor.is_finite() - || !log_rate.is_finite() + if !initial_time_independent_effect.is_finite() + || !predictor_variance.is_finite() + || predictor_variance < 0.0 { return Err(PsychometricError::InvalidNumericInput); } - if time_independent_effect == 0.0 || time_independent_predictor == 0.0 { + if initial_time_independent_effect == 0.0 || predictor_variance == 0.0 { return Ok(0.0); } - let continuous = require_finite(time_independent_effect * time_independent_predictor)?; - if log_rate == 0.0 { - return require_finite(continuous * event_delta); - } - recover_discrete_constant_predictor_effect(continuous, log_rate, event_delta, clock) + let squared = + require_finite(initial_time_independent_effect * initial_time_independent_effect)?; + require_finite(squared * predictor_variance) } -/// Exact scalar evolved latent mean plus a time-independent predictor. +/// Refuse treating 2017-era `addedT0TIPREDVAR` as §7.2 +/// `addedTIPREDVAR`. /// -/// Driver, Oud, and Voelkle (2017, Eq. 3, p. 5) write the first two -/// summands as the carried `T0MEANS`, the `CINT` increment, and the -/// `TIPREDEFFECT` increment `A^{-1}[e^{A Δt} − I] B z`. Form `μ_t` -/// first, then add that increment. A zero time-independent increment -/// is exactly `μ_t`. A zero evolved mean is exactly the increment. -/// Adding `B z` to `μ_t` is not this map. Adding `M x` is not this -/// map. +/// `t0_b² v` uses free first-occasion `T0TIPREDEFFECT`. +/// `(B / a)² v` uses the asymptotic unit effect `-B / a` and +/// requires stable `a < 0`. Those are not the same map. /// /// # Errors /// -/// Propagates [`recover_discrete_latent_mean`] and -/// [`recover_discrete_time_independent_predictor_effect`], and -/// returns [`PsychometricError::InvalidNumericInput`] when the sum -/// overflows. -pub fn recover_discrete_latent_mean_with_time_independent_predictor( - initial_latent_mean: f64, - log_rate: f64, - continuous_intercept: f64, - time_independent_effect: f64, - time_independent_predictor: f64, - event_delta: f64, - clock: LagClock, +/// Always returns +/// [`PsychometricError::InitialTimeIndependentVarianceIsNotAsymptoticTimeIndependentVariance`]. +pub fn refuse_initial_time_independent_variance_as_asymptotic_time_independent_variance( + initial_predictor_variance: f64, + asymptotic_predictor_variance: f64, ) -> Result { - let evolved_latent_mean = recover_discrete_latent_mean( - initial_latent_mean, - log_rate, - continuous_intercept, - event_delta, - clock, - )?; - let time_independent_increment = recover_discrete_time_independent_predictor_effect( - time_independent_effect, - time_independent_predictor, - log_rate, - event_delta, - clock, - )?; - if time_independent_increment == 0.0 { - return Ok(evolved_latent_mean); - } - if evolved_latent_mean == 0.0 { - return Ok(time_independent_increment); - } - require_finite(evolved_latent_mean + time_independent_increment) + let _ = (initial_predictor_variance, asymptotic_predictor_variance); + Err(PsychometricError::InitialTimeIndependentVarianceIsNotAsymptoticTimeIndependentVariance) } -/// Refuse treating the Eq. 3 time-independent increment as `CINT`. +/// Refuse treating 2017-era `addedT0TIPREDVAR` as Table 3 / p. 16 +/// `T0TIPREDEFFECTstd`. /// -/// Table 2 names `B` `TIPREDEFFECT` and `κ` `CINT`. The discrete -/// increment is `A^{-1}[e^{A Δt} − I] B z`. The continuous intercept -/// is not that increment. +/// `t0_b² v` is a variance. `t0_b · √v / √p_0` is a standardised +/// coefficient. Those are not the same map. /// /// # Errors /// /// Always returns -/// [`PsychometricError::TimeIndependentEffectIsNotContinuousIntercept`]. -pub fn refuse_time_independent_effect_as_continuous_intercept( - time_independent_increment: f64, - continuous_intercept: f64, +/// [`PsychometricError::InitialTimeIndependentVarianceIsNotStandardisedInitialTimeIndependentEffect`]. +pub fn refuse_initial_time_independent_variance_as_standardised_initial_time_independent_effect( + initial_predictor_variance: f64, + standardised_initial_effect: f64, ) -> Result { - let _ = (time_independent_increment, continuous_intercept); - Err(PsychometricError::TimeIndependentEffectIsNotContinuousIntercept) + let _ = (initial_predictor_variance, standardised_initial_effect); + Err(PsychometricError::InitialTimeIndependentVarianceIsNotStandardisedInitialTimeIndependentEffect) } -/// Refuse treating the Eq. 3 time-independent increment as `M x`. +/// Refuse treating 2017-era `addedT0TIPREDVAR` as free first-occasion +/// `T0VAR`. /// -/// The fourth-summand impulse is contemporaneous. The second-summand -/// `TIPREDEFFECT` map integrates `B z` over the event interval. +/// `t0_b² v` is extra first-occasion variance accounted for by a +/// time-independent predictor. Free `T0VAR` `p_0` is the +/// first-occasion state. /// /// # Errors /// /// Always returns -/// [`PsychometricError::TimeIndependentEffectIsNotTimeDependentImpulse`]. -pub fn refuse_time_independent_effect_as_time_dependent_impulse( - time_independent_increment: f64, - time_dependent_impulse: f64, +/// [`PsychometricError::InitialTimeIndependentVarianceIsNotInitialLatentVariance`]. +pub fn refuse_initial_time_independent_variance_as_initial_latent_variance( + initial_predictor_variance: f64, + initial_latent_variance: f64, ) -> Result { - let _ = (time_independent_increment, time_dependent_impulse); - Err(PsychometricError::TimeIndependentEffectIsNotTimeDependentImpulse) + let _ = (initial_predictor_variance, initial_latent_variance); + Err(PsychometricError::InitialTimeIndependentVarianceIsNotInitialLatentVariance) } -/// Refuse treating the Eq. 3 time-independent increment as Voelkle -/// et al. (2012, Eq. 14). +/// Refuse treating 2017-era `addedT0TIPREDVAR` as `TRAITVAR`. /// -/// Equation 14 is `a_{yx} Δt` for a piecewise-constant time-varying -/// predictor whose sampling interval equals its constancy interval. -/// `TIPREDEFFECT` integrates a constant `z` through the drift. +/// `t0_b² v` is extra first-occasion variance accounted for by a +/// time-independent predictor. Section 4.3 `TRAITVAR` is a +/// zero-drift latent process. /// /// # Errors /// /// Always returns -/// [`PsychometricError::TimeIndependentEffectIsNotTimeVaryingDiscreteEffect`]. -pub fn refuse_time_independent_effect_as_time_varying_discrete_effect( - time_independent_increment: f64, - time_varying_discrete_effect: f64, +/// [`PsychometricError::InitialTimeIndependentVarianceIsNotTraitVariance`]. +pub fn refuse_initial_time_independent_variance_as_trait_variance( + initial_predictor_variance: f64, + trait_variance: f64, ) -> Result { - let _ = (time_independent_increment, time_varying_discrete_effect); - Err(PsychometricError::TimeIndependentEffectIsNotTimeVaryingDiscreteEffect) + let _ = (initial_predictor_variance, trait_variance); + Err(PsychometricError::InitialTimeIndependentVarianceIsNotTraitVariance) } -/// Refuse treating Driver Table 2 `TIPREDEFFECT` as the discrete -/// increment. +/// Exact scalar Eq. 5 of 2017-era `addedT0TIPREDVAR`. /// -/// `B` is the continuous-time coefficient. Equation 3 maps -/// `A^{-1}[e^{A Δt} − I] B z`. +/// Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Table 3, p. 13; +/// Table 2, p. 12; p. 16; §7.2, pp. 20–21; 2017-era ctsem +/// `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T19:10Z from +/// ) +/// write `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and +/// `Γ ~ N(τ, Ψ)`. Equation 5 maps extra latent variance through +/// `Λ`. The 2017-era `summary.ctsemFit.R` forms the latent extra +/// `addedT0TIPREDVAR` as +/// `T0TIPREDEFFECT %*% TIPREDVAR %*% t(T0TIPREDEFFECT)` immediately +/// after `T0TIPREDEFFECTstd`. The scalar latent extra is `t0_b² v`. +/// Equation 5 of that extra, with `θ = 0` and `ψ = 0`, is +/// `λ² t0_b² v`. Form `addedT0TIPREDVAR` first, then +/// `(λ extra) λ`. Do not form `λ²` first: at `λ = 1e308`, +/// `extra = 1e-308`, `λ²` overflows and `λ² extra` is non-finite, +/// but `(λ extra) λ = 1e308`. A zero loading or zero extra is +/// exactly zero. `v < 0` fails closed. `T0` is an event-time +/// occasion, so a non-event clock fails closed. Free +/// `T0TIPREDEFFECT` does not require stable `a < 0`. `t0_b² v` is +/// the latent extra and is not this observed extra. +/// `λ² p_0 + θ` is first-occasion observed variance and is not this +/// extra. `λ² (B / a)² v` is Eq. 5 of `addedTIPREDVAR` and is not +/// this first-occasion observed extra. `MANIFESTVAR` `θ` is +/// measurement error and is not this extra. `Ψ` is intercept +/// variance and is not extra TI. This is not a Kalman filter, not +/// a matrix `expm`, not DSEM, and not ctsem estimation. /// /// # Errors /// -/// Always returns -/// [`PsychometricError::TimeIndependentCoefficientIsNotDiscreteEffect`]. -pub fn refuse_time_independent_coefficient_as_discrete_effect( - time_independent_coefficient: f64, - time_independent_increment: f64, +/// Propagates [`recover_initial_time_independent_predictor_variance`] +/// and [`recover_manifest_observed_variance`]. +pub fn recover_initial_time_independent_observed_variance( + loading: f64, + initial_time_independent_effect: f64, + predictor_variance: f64, + clock: LagClock, ) -> Result { - let _ = (time_independent_coefficient, time_independent_increment); - Err(PsychometricError::TimeIndependentCoefficientIsNotDiscreteEffect) + let extra = recover_initial_time_independent_predictor_variance( + initial_time_independent_effect, + predictor_variance, + clock, + )?; + recover_manifest_observed_variance(loading, extra, 0.0) } -/// Exact scalar §7.2 `asymTIPREDEFFECT`. +/// Refuse treating Eq. 5 of 2017-era `addedT0TIPREDVAR` as the +/// latent extra. /// -/// Driver, Oud, and Voelkle (2017, §7.2, pp. 20–21; Eq. 3, p. 5; -/// Table 2, p. 12; JSS PDF opened 2026-08-21T13:08Z from -/// ) -/// name `TIPREDEFFECT` the continuous-time coefficient `B`. Equation 3 -/// maps a finite event interval as `A^{-1}[e^{A Δt} − I] B z`. Section -/// 7.2 then names `asymTIPREDEFFECT` the expected total change in -/// process means given an increase of 1 on the time-independent -/// predictor. For stable `a < 0` that total change is `-A^{-1} B`. -/// The scalar map is `-B z / a`. Form `B z` first, then divide by -/// `-a`. A zero coefficient or zero predictor is exactly zero. -/// `a ≥ 0` cannot hold a finite process-mean change and fails closed. -/// `-B z / a` is not the coefficient `B`, not the finite-interval -/// increment `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. -/// This is not a Kalman filter, not a matrix `expm`, and not ctsem -/// estimation. +/// `λ² t0_b² v` is extra observed-indicator variance. +/// `t0_b² v` is extra latent variance. Those are not the same map. /// /// # Errors /// -/// Returns [`PsychometricError::EventTimeRequired`] for any non-event -/// clock, [`PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift`] -/// when the drift is not strictly negative and the effect is nonzero, -/// and [`PsychometricError::InvalidNumericInput`] when an input is -/// non-finite or `B z` or the quotient overflows. -pub fn recover_asymptotic_time_independent_predictor_effect( - time_independent_effect: f64, - time_independent_predictor: f64, - log_rate: f64, - clock: LagClock, +/// Always returns +/// [`PsychometricError::InitialTimeIndependentObservedVarianceIsNotInitialTimeIndependentVariance`]. +pub fn refuse_initial_time_independent_observed_variance_as_initial_time_independent_variance( + initial_observed_predictor_variance: f64, + initial_predictor_variance: f64, ) -> Result { - if !clock.admits_structural_lag() { - return Err(PsychometricError::EventTimeRequired); - } - if !time_independent_effect.is_finite() - || !time_independent_predictor.is_finite() - || !log_rate.is_finite() - { - return Err(PsychometricError::InvalidNumericInput); - } - if time_independent_effect == 0.0 || time_independent_predictor == 0.0 { - return Ok(0.0); - } - if log_rate >= 0.0 { - return Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift); - } - let continuous = require_finite(time_independent_effect * time_independent_predictor)?; - require_finite(continuous / -log_rate) + let _ = ( + initial_observed_predictor_variance, + initial_predictor_variance, + ); + Err( + PsychometricError::InitialTimeIndependentObservedVarianceIsNotInitialTimeIndependentVariance, + ) } -/// Refuse treating §7.2 `asymTIPREDEFFECT` as `TIPREDEFFECT`. +/// Refuse treating Eq. 5 of 2017-era `addedT0TIPREDVAR` as +/// first-occasion observed variance. /// -/// `-B z / a` is the expected total change in process means. Table 2 -/// names `B` `TIPREDEFFECT`. The coefficient is not that total change. +/// `λ² t0_b² v` is extra observed TI variance. `λ² p_0 + θ` is +/// first-occasion observed-indicator variance. Those are not the +/// same map. /// /// # Errors /// /// Always returns -/// [`PsychometricError::AsymptoticTimeIndependentEffectIsNotCoefficient`]. -pub fn refuse_asymptotic_time_independent_effect_as_coefficient( - asymptotic_effect: f64, - time_independent_coefficient: f64, +/// [`PsychometricError::InitialTimeIndependentObservedVarianceIsNotInitialObservedVariance`]. +pub fn refuse_initial_time_independent_observed_variance_as_initial_observed_variance( + initial_observed_predictor_variance: f64, + initial_observed_variance: f64, ) -> Result { - let _ = (asymptotic_effect, time_independent_coefficient); - Err(PsychometricError::AsymptoticTimeIndependentEffectIsNotCoefficient) + let _ = ( + initial_observed_predictor_variance, + initial_observed_variance, + ); + Err(PsychometricError::InitialTimeIndependentObservedVarianceIsNotInitialObservedVariance) } -/// Refuse treating §7.2 `asymTIPREDEFFECT` as the finite-interval -/// discrete increment. +/// Refuse treating Eq. 5 of 2017-era `addedT0TIPREDVAR` as Eq. 5 of +/// `addedTIPREDVAR`. /// -/// `-B z / a` is the `Δt → ∞` limit of `A^{-1}[e^{A Δt} − I] B z` -/// under stable `a < 0`. A finite event interval is not that limit. +/// `λ² t0_b² v` uses free first-occasion `T0TIPREDEFFECT`. +/// `λ² (B / a)² v` uses the asymptotic unit effect `-B / a` and +/// requires stable `a < 0`. Those are not the same map. /// /// # Errors /// /// Always returns -/// [`PsychometricError::AsymptoticTimeIndependentEffectIsNotDiscreteEffect`]. -pub fn refuse_asymptotic_time_independent_effect_as_discrete_effect( - asymptotic_effect: f64, - time_independent_increment: f64, +/// [`PsychometricError::InitialTimeIndependentObservedVarianceIsNotAsymptoticTimeIndependentObservedVariance`]. +pub fn refuse_initial_time_independent_observed_variance_as_asymptotic_time_independent_observed_variance( + initial_observed_predictor_variance: f64, + asymptotic_observed_predictor_variance: f64, ) -> Result { - let _ = (asymptotic_effect, time_independent_increment); - Err(PsychometricError::AsymptoticTimeIndependentEffectIsNotDiscreteEffect) + let _ = ( + initial_observed_predictor_variance, + asymptotic_observed_predictor_variance, + ); + Err( + PsychometricError::InitialTimeIndependentObservedVarianceIsNotAsymptoticTimeIndependentObservedVariance, + ) } -/// Refuse treating §7.2 `asymTIPREDEFFECT` as `CINT`. +/// Refuse treating Eq. 5 of 2017-era `addedT0TIPREDVAR` as +/// `MANIFESTVAR`. /// -/// `-B z / a` is the expected total change from a time-independent -/// predictor. Table 2 names `κ` `CINT`. Those are not the same map. +/// `λ² t0_b² v` is extra observed TI variance. Table 2 names +/// `MANIFESTVAR` as `Θ`, the variance of `ζ`. Those are not the +/// same map. /// /// # Errors /// /// Always returns -/// [`PsychometricError::AsymptoticTimeIndependentEffectIsNotContinuousIntercept`]. -pub fn refuse_asymptotic_time_independent_effect_as_continuous_intercept( - asymptotic_effect: f64, - continuous_intercept: f64, +/// [`PsychometricError::InitialTimeIndependentObservedVarianceIsNotMeasurementError`]. +pub fn refuse_initial_time_independent_observed_variance_as_measurement_error( + initial_observed_predictor_variance: f64, + measurement_error_variance: f64, ) -> Result { - let _ = (asymptotic_effect, continuous_intercept); - Err(PsychometricError::AsymptoticTimeIndependentEffectIsNotContinuousIntercept) + let _ = ( + initial_observed_predictor_variance, + measurement_error_variance, + ); + Err(PsychometricError::InitialTimeIndependentObservedVarianceIsNotMeasurementError) } -/// Refuse treating §7.2 `asymTIPREDEFFECT` as `M x`. +/// Recover the exact scalar pair `(φ, a)` on event time. /// -/// The fourth-summand impulse is contemporaneous. The asymptotic -/// time-independent effect is a new process mean, not a Dirac. +/// # Errors +/// +/// Propagates [`recover_discrete_lag_one`] and [`recover_local_log_rate`]. +pub fn recover_event_time_discrete_lag_and_log_rate( + earlier: f64, + later: f64, + event_delta: f64, + clock: LagClock, +) -> Result { + let discrete_lag = recover_discrete_lag_one(earlier, later)?; + let log_rate = recover_local_log_rate(discrete_lag, event_delta, clock)?; + Ok(DiscreteLagAndLogRate { + discrete_lag, + log_rate, + event_delta, + }) +} + +/// Map a discrete lag from one event interval onto another through `a`. +/// +/// Voelkle et al. (2012, ZORA accepted manuscript pp. 2, 16, 33) show that +/// discrete-time autoregressive coefficients from different intervals are +/// not comparable. The licensed path is `a = ln(φ_src) / Δt_src` then +/// `φ_ref = exp(a Δt_ref)`. Equal source and reference intervals still go +/// through that map. This is not DSEM. /// /// # Errors /// -/// Always returns -/// [`PsychometricError::AsymptoticTimeIndependentEffectIsNotTimeDependentImpulse`]. -pub fn refuse_asymptotic_time_independent_effect_as_time_dependent_impulse( - asymptotic_effect: f64, - time_dependent_impulse: f64, +/// Propagates [`recover_local_log_rate`] and +/// [`recover_discrete_lag_from_log_rate`]. +pub fn map_discrete_lag_across_event_intervals( + discrete_lag: f64, + source_delta: f64, + reference_delta: f64, + clock: LagClock, ) -> Result { - let _ = (asymptotic_effect, time_dependent_impulse); - Err(PsychometricError::AsymptoticTimeIndependentEffectIsNotTimeDependentImpulse) + let log_rate = recover_local_log_rate(discrete_lag, source_delta, clock)?; + recover_discrete_lag_from_log_rate(log_rate, reference_delta, clock) } -/// Exact scalar §7.2 `addedTIPREDVAR`. +/// Exact scalar discrete effect of a constant event-time predictor. /// -/// Driver, Oud, and Voelkle (2017, §7.2, pp. 20–21; Eq. 3, p. 5; -/// Table 2, p. 12; JSS PDF opened 2026-08-21T13:08Z from -/// ) -/// name `asymTIPREDEFFECT` the expected total change in process means -/// given a unit increase on a time-independent predictor. The scalar -/// map is `-B / a` for stable `a < 0`. Section 7.2 then names -/// `addedTIPREDVAR` the stable between-subject variance accounted for -/// by those predictors. For predictor variance `v ≥ 0` that variance -/// is `(-B / a)² v`. Form the unit asymptotic effect first, then -/// square, then multiply by `v`. A zero coefficient or zero predictor -/// variance is exactly zero. `v < 0` fails closed. `a ≥ 0` cannot hold -/// a finite process-mean change and fails closed. `(B / a)² v` is not -/// `TRAITVAR`, not `asymDIFFUSION`, and not the expected total change -/// `-B z / a`. This is not a Kalman filter, not a matrix `expm`, and -/// not ctsem estimation. +/// Voelkle et al. (2012, Eq. 12; ZORA accepted manuscript, Introducing +/// Intercepts, manuscript p. 20): adding a continuous-time intercept +/// `b` yields the discrete increment `A^{-1}(exp(A Δt) − I) b`. Driver, +/// Oud, and Voelkle (2017, Eq. 3) write the same term as +/// `A^{-1}[e^{A Δt} − I] ξ`. The scalar case is +/// `b*_y.x(Δt) = (a_yx / a_xx) (exp(a_xx Δt) − 1)` for `a_xx ≠ 0`. +/// The algebraically identical finite-`expm1` evaluation is +/// `a_yx (expm1(z) / a_xx)` with `z = a_xx Δt`. Dividing the increment +/// by the finite auto-effect keeps a finite Eq. 12 result when `z` +/// overflows to `-∞` (`exp(z) → 0`, so Eq. 12 → `-a_yx / a_xx`) and +/// when `a_yx Δt` overflows. When binary64 `z` underflows to `+0`, the +/// mathematical limit of Eq. 12 is `a_yx Δt`. When `a_yx = 0`, Eq. 12 +/// is exactly `0` even if `expm1(z)` overflows (`0 * +∞` is `NaN`). +/// When `expm1(z)` overflows to `+∞` at a finite `z`, rewrite as +/// `sign(a_yx / a_xx) exp(ln|a_yx| + z − ln|a_xx|) − a_yx / a_xx` so a +/// finite Eq. 12 result is not lost. `z → +∞` is an unstable process +/// and fails closed unless `a_yx = 0`. The first-order product is not +/// the general discrete effect. This is not DSEM and not a matrix +/// `expm`. /// /// # Errors /// -/// Returns [`PsychometricError::EventTimeRequired`] for any non-event -/// clock, [`PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift`] -/// when the drift is not strictly negative and the variance is nonzero, -/// and [`PsychometricError::InvalidNumericInput`] when an input is -/// non-finite, the predictor variance is negative, or the product -/// overflows. -pub fn recover_asymptotic_time_independent_predictor_variance( - time_independent_effect: f64, - predictor_variance: f64, - log_rate: f64, +/// Returns [`PsychometricError::EventTimeRequired`] for any non-event clock, +/// [`PsychometricError::NonPositiveInterval`] when `event_delta` is not +/// strictly positive, and [`PsychometricError::InvalidNumericInput`] when +/// either rate is non-finite, the predictor auto-effect is zero, or the +/// mapped effect is non-finite. +pub fn recover_discrete_constant_predictor_effect( + outcome_on_predictor: f64, + predictor_log_rate: f64, + event_delta: f64, clock: LagClock, ) -> Result { if !clock.admits_structural_lag() { return Err(PsychometricError::EventTimeRequired); } - if !time_independent_effect.is_finite() - || !predictor_variance.is_finite() - || !log_rate.is_finite() - || predictor_variance < 0.0 + if !event_delta.is_finite() || event_delta <= 0.0 { + return Err(PsychometricError::NonPositiveInterval); + } + if !outcome_on_predictor.is_finite() + || !predictor_log_rate.is_finite() + || predictor_log_rate == 0.0 { return Err(PsychometricError::InvalidNumericInput); } - if time_independent_effect == 0.0 || predictor_variance == 0.0 { + // Voelkle Eq. 12 / Driver Eq. 3: (0 / a)(exp(a Δt) − 1) = 0. + // Direct 0 * (expm1(z) / a) is NaN when expm1 overflows. + if outcome_on_predictor == 0.0 { return Ok(0.0); } - let unit_effect = recover_asymptotic_time_independent_predictor_effect( - time_independent_effect, - 1.0, - log_rate, - clock, + let increment_argument = predictor_log_rate * event_delta; + if increment_argument == 0.0 { + // Binary64 underflow of a_xx Δt. lim z→0 of Eq. 12 is a_yx Δt. + return require_finite(outcome_on_predictor * event_delta); + } + let increment = increment_argument.exp_m1(); + if increment.is_finite() { + // Divide expm1(z) by the finite a_xx, not by z. expm1(-∞)/-∞ + // is +0 and loses the equilibrium increment -a_yx/a_xx (Voelkle + // 2012, Introducing Intercepts: the exponential vanishes as Δt + // grows). + return require_finite(outcome_on_predictor * (increment / predictor_log_rate)); + } + // expm1 overflowed. z → +∞ diverges (unstable auto-effect). + if !increment_argument.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + // Finite z, overflowed expm1. (a_yx/a_xx)(exp(z) − 1) = + // sign(a_yx/a_xx) exp(ln|a_yx| + z − ln|a_xx|) − a_yx/a_xx. + // The subtracted scale must itself be finite: if a_yx/a_xx overflows, + // the rewrite term is not a binary64 number. That path is dead if + // dominant is required first (dominant is then also infinite), so + // refuse the scale before forming the exponential. + let scale = outcome_on_predictor / predictor_log_rate; + if !scale.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + let log_abs_dominant = + outcome_on_predictor.abs().ln() + increment_argument - predictor_log_rate.abs().ln(); + let dominant = require_finite( + outcome_on_predictor.signum() * predictor_log_rate.signum() * log_abs_dominant.exp(), )?; - let squared = require_finite(unit_effect * unit_effect)?; - require_finite(squared * predictor_variance) + require_finite(dominant - scale) } -/// Refuse treating §7.2 `addedTIPREDVAR` as `TRAITVAR`. +/// Refuse treating discrete lags from unequal event intervals as one coefficient. /// -/// `(B / a)² v` is between-subject variance accounted for by a -/// time-independent predictor. Section 4.3 `TRAITVAR` is a zero-drift -/// latent process. Those are not the same map. +/// Always fails closed. Map each lag through +/// [`map_discrete_lag_across_event_intervals`] instead. /// /// # Errors /// -/// Always returns -/// [`PsychometricError::AsymptoticTimeIndependentVarianceIsNotTraitVariance`]. -pub fn refuse_asymptotic_time_independent_variance_as_trait_variance( - added_predictor_variance: f64, - trait_variance: f64, +/// Always returns [`PsychometricError::UnequalIntervalPoolingForbidden`]. +pub fn refuse_pooled_discrete_lag_across_unequal_intervals( + first_delta: f64, + second_delta: f64, ) -> Result { - let _ = (added_predictor_variance, trait_variance); - Err(PsychometricError::AsymptoticTimeIndependentVarianceIsNotTraitVariance) + let _ = (first_delta, second_delta); + Err(PsychometricError::UnequalIntervalPoolingForbidden) } -/// Refuse treating §7.2 `addedTIPREDVAR` as `asymDIFFUSION`. -/// -/// `(B / a)² v` is between-subject variance from a time-independent -/// predictor. `asymDIFFUSION` is the stationary within-subject -/// variance `-q / (2 a)`. -/// -/// # Errors +/// Exact scalar discrete effect of a time-varying event-time predictor. /// -/// Always returns -/// [`PsychometricError::AsymptoticTimeIndependentVarianceIsNotStationaryWithinSubject`]. -pub fn refuse_asymptotic_time_independent_variance_as_stationary_within_subject( - added_predictor_variance: f64, - stationary_variance: f64, +/// Voelkle et al. (2012, Eq. 14; ZORA accepted manuscript, Introducing +/// Intercepts, manuscript p. 21): when the predictor can take a new value +/// at each occasion **and** the sampling interval equals the interval +/// during which that predictor is assumed constant, the discrete effect +/// is `b*_y.x(Δt) = a_yx Δt`. It does not depend on the predictor +/// auto-effect. The manuscript calls this a first-order approximation +/// that deteriorates as `Δt` grows. It is not Eq. 12. The general case +/// (sampling interval ≠ constancy interval) cites Oud and Jansen (2000), +/// which is unread, and fails closed. This is not DSEM. +/// +/// # Errors +/// +/// Returns [`PsychometricError::EventTimeRequired`] for any non-event clock, +/// [`PsychometricError::NonPositiveInterval`] when any interval is not +/// strictly positive, [`PsychometricError::UnmatchedTimeVaryingInterval`] +/// when the event, sampling, and constancy intervals are not the same +/// finite value, and [`PsychometricError::InvalidNumericInput`] when the +/// continuous effect is non-finite or the product overflows. +pub fn recover_discrete_time_varying_predictor_effect( + outcome_on_predictor: f64, + event_delta: f64, + sampling_interval: f64, + constancy_interval: f64, + clock: LagClock, ) -> Result { - let _ = (added_predictor_variance, stationary_variance); - Err(PsychometricError::AsymptoticTimeIndependentVarianceIsNotStationaryWithinSubject) + if !clock.admits_structural_lag() { + return Err(PsychometricError::EventTimeRequired); + } + if !event_delta.is_finite() + || event_delta <= 0.0 + || !sampling_interval.is_finite() + || sampling_interval <= 0.0 + || !constancy_interval.is_finite() + || constancy_interval <= 0.0 + { + return Err(PsychometricError::NonPositiveInterval); + } + if event_delta.to_bits() != sampling_interval.to_bits() + || sampling_interval.to_bits() != constancy_interval.to_bits() + { + return Err(PsychometricError::UnmatchedTimeVaryingInterval); + } + if !outcome_on_predictor.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + require_finite(outcome_on_predictor * event_delta) } -/// Refuse treating §7.2 `addedTIPREDVAR` as `asymTIPREDEFFECT`. +/// Refuse mapping a time-varying predictor when sampling ≠ constancy. /// -/// `(B / a)² v` is a variance. `-B z / a` is the expected total -/// change in process means. Those are not the same map. +/// Always fails closed. Oud and Jansen (2000) is unread. Use +/// [`recover_discrete_time_varying_predictor_effect`] only when the +/// intervals already match, or [`recover_discrete_constant_predictor_effect`] +/// for a constant predictor (Eq. 12). /// /// # Errors /// -/// Always returns -/// [`PsychometricError::AsymptoticTimeIndependentVarianceIsNotAsymptoticEffect`]. -pub fn refuse_asymptotic_time_independent_variance_as_asymptotic_effect( - added_predictor_variance: f64, - asymptotic_effect: f64, +/// Always returns [`PsychometricError::UnmatchedTimeVaryingInterval`]. +pub fn refuse_unmatched_time_varying_predictor_interval( + sampling_interval: f64, + constancy_interval: f64, ) -> Result { - let _ = (added_predictor_variance, asymptotic_effect); - Err(PsychometricError::AsymptoticTimeIndependentVarianceIsNotAsymptoticEffect) + let _ = (sampling_interval, constancy_interval); + Err(PsychometricError::UnmatchedTimeVaryingInterval) } -/// Exact scalar Table 2 `asymCINT`. +/// Exact scalar discrete process noise on event time. /// -/// Driver, Oud, and Voelkle (2017, Table 2, p. 12; Eq. 3, p. 5; -/// §4.3 / p. 16; JSS PDF opened 2026-08-21T16:13Z from -/// ) -/// name `asymCINT` the asymptotic (`Δt = ∞`) expected change in -/// processes for a 1 unit change in intercept (`CINT`). Table 2 names -/// `κ` `CINT`. Equation 3 maps a finite event interval as -/// `A^{-1}[e^{A Δt} − I] κ`. For stable `a < 0` that `Δt → ∞` limit -/// is `-A^{-1} κ`. The scalar map is `-κ / a`. A unit intercept is -/// `-1 / a`. Form `κ` first, then divide by `-a`. A zero intercept is -/// exactly zero. `a ≥ 0` cannot hold a finite process-mean change and -/// fails closed. `-κ / a` is not `κ`, not the finite-interval -/// increment `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not -/// `asymTIPREDEFFECT` `-B z / a`. Page 16 notes that a `T0MEANS` -/// stationarity constraint includes time-independent predictors; that -/// composition is not this intercept-only map. The printed 2-latent -/// `CINT` values are not this scalar map. This is not a Kalman filter, -/// not a matrix `expm`, and not ctsem estimation. +/// Driver, Oud, and Voelkle (2017, Eq. 3; JSS PDF re-opened 2026-08-17T21:03Z, +/// p. 4) write the discrete process-noise covariance +/// `Q_Δt = ∫_0^{Δt} expm(A(Δt−τ)) L G G⊤ L⊤ expm(A(Δt−τ))⊤ dτ`. +/// This slice takes scalar `L = 1` (every latent subject to system noise). +/// The noiseless scalar closed form with continuous diffusion +/// `q = G G⊤ ≥ 0` is `q (exp(2 a Δt) − 1) / (2 a)` for `a ≠ 0` and +/// `q Δt` for `a = 0`. The algebraically identical finite-`expm1` +/// evaluation is `0.5 q (expm1(z) / a)` with `z = 2 (a Δt)`. Form +/// `z` as twice the already-finite product `a Δt`. Forming `2 a` +/// first overflows when `|a|` is at the binary64 extreme even if +/// `a Δt` and `Q_Δt` are finite (`a = ±1e308`, `Δt = 1e-308`). +/// When binary64 `z` underflows to `+0`, the mathematical limit is +/// `q Δt`. When `z → −∞` the exponential vanishes and the result is +/// the equilibrium variance `−q / (2 a) = −0.5 q / a` for stable +/// `a < 0`. When `expm1(z)` overflows to `+∞` at a finite `z`, +/// rewrite as `sign(q / a) exp(ln|q| + z − ln|a| − ln 2) − 0.5 q / a`. +/// An overflowing rewrite scale `0.5 q / a` is not a finite `Q_Δt` +/// (`q = 1e308`, `a = 0.1`, `Δt = 4000` → `z = 800`, `0.5 q / a = +∞`). +/// `z → +∞` is an unstable process and fails closed unless `q = 0`. +/// A zero diffusion is exactly zero even if `expm1` overflows. This +/// is not a Kalman filter, not DSEM, and not a matrix `expm`. /// /// # Errors /// -/// Returns [`PsychometricError::EventTimeRequired`] for any non-event -/// clock, [`PsychometricError::AsymptoticContinuousInterceptRequiresStableDrift`] -/// when the drift is not strictly negative and the intercept is -/// nonzero, and [`PsychometricError::InvalidNumericInput`] when an -/// input is non-finite or the quotient overflows. -pub fn recover_asymptotic_continuous_intercept( - continuous_intercept: f64, +/// Returns [`PsychometricError::EventTimeRequired`] for any non-event clock, +/// [`PsychometricError::NonPositiveInterval`] when `event_delta` is not +/// strictly positive, and [`PsychometricError::InvalidNumericInput`] when +/// the diffusion is negative or non-finite, the log-rate is non-finite, or +/// the mapped variance is non-finite. +pub fn recover_discrete_process_noise( + continuous_diffusion: f64, log_rate: f64, + event_delta: f64, clock: LagClock, ) -> Result { if !clock.admits_structural_lag() { return Err(PsychometricError::EventTimeRequired); } - if !continuous_intercept.is_finite() || !log_rate.is_finite() { + if !event_delta.is_finite() || event_delta <= 0.0 { + return Err(PsychometricError::NonPositiveInterval); + } + if !continuous_diffusion.is_finite() || continuous_diffusion < 0.0 || !log_rate.is_finite() { return Err(PsychometricError::InvalidNumericInput); } - if continuous_intercept == 0.0 { + // Driver Eq. 3: the integral of a zero diffusion is zero. + // Direct 0 * (expm1(z) / (2 a)) is NaN when expm1 overflows. + if continuous_diffusion == 0.0 { return Ok(0.0); } - if log_rate >= 0.0 { - return Err(PsychometricError::AsymptoticContinuousInterceptRequiresStableDrift); + if log_rate == 0.0 { + return require_finite(continuous_diffusion * event_delta); } - require_finite(continuous_intercept / -log_rate) + // z = 2 (a Δt), not (2 a) Δt. 2 a overflows at |a| = 1e308 even + // when a Δt is finite (Driver Eq. 3 scalar closed form). + let drift_interval = log_rate * event_delta; + let increment_argument = 2.0 * drift_interval; + if increment_argument == 0.0 { + // Binary64 underflow of 2 a Δt. lim z→0 of Eq. 3 Q_Δt is q Δt. + return require_finite(continuous_diffusion * event_delta); + } + let increment = increment_argument.exp_m1(); + if increment.is_finite() { + // Q = q expm1(z) / (2 a) = 0.5 q (expm1(z) / a). Divide by + // the finite a, not by 2 a: 2 a overflows when |a| = 1e308. + // expm1(−∞) is −1, so this path also keeps −0.5 q / a. + return require_finite(0.5 * continuous_diffusion * (increment / log_rate)); + } + // expm1 overflowed. z → +∞ diverges (unstable auto-effect). + // z → −∞ is already handled above because expm1(−∞) is finite. + if !increment_argument.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + // Finite z, overflowed expm1. (q / (2 a))(exp(z) − 1) = + // sign(q / a) exp(ln|q| + z − ln|a| − ln 2) − 0.5 q / a. + // Driver Eq. 3 (JSS PDF re-opened 2026-08-18T03:07Z, p. 4): + // Q_Δt is that integral. If 0.5 q / a overflows, the rewrite + // scale is not finite and Q_Δt is not finite. + let half_scale = 0.5 * continuous_diffusion / log_rate; + if !half_scale.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + let log_abs_dominant = continuous_diffusion.abs().ln() + increment_argument + - log_rate.abs().ln() + - std::f64::consts::LN_2; + let dominant = + require_finite(continuous_diffusion.signum() * log_rate.signum() * log_abs_dominant.exp())?; + require_finite(dominant - half_scale) } -/// Refuse treating Table 2 `asymCINT` as `CINT`. +/// Exact scalar lagged latent covariance on event time. /// -/// `-κ / a` is the expected change in process means. Table 2 names -/// `κ` `CINT`. The intercept is not that total change. +/// Driver, Oud, and Voelkle (2017, Eq. 3–4, pp. 4–5; JSS PDF +/// re-opened 2026-08-18T11:20Z) write `η(t) = exp(A Δt) η(t0) + … +` +/// the stochastic integral (Eq. 3) and that the integral exhibits +/// covariance `Q_Δt` (Eq. 4). The homogeneous-process consequence is +/// `cov(η_ti, η_{t-1,i}) = A_Δt cov(η_{t-1,i})`. The scalar map is +/// `exp(a Δt) p` with prior variance `p ≥ 0`. This is not `Q_Δt`. +/// Binary64 underflow of `exp(a Δt)` to `+0` is a vanishing +/// covariance and is kept. A zero prior variance is exactly zero even +/// if the exponential overflows. When `exp(a Δt)` overflows at a +/// finite `a Δt`, rewrite as `exp(ln p + a Δt)`. An overflowing +/// rewrite fails closed. A finite `exp(a Δt)` whose product with `p` +/// overflows also fails closed. The JSS article has no numbered §2.2. +/// This is not a Kalman filter, not DSEM, and not a matrix `expm`. /// /// # Errors /// -/// Always returns -/// [`PsychometricError::AsymptoticContinuousInterceptIsNotContinuousIntercept`]. -pub fn refuse_asymptotic_continuous_intercept_as_continuous_intercept( - asymptotic_intercept: f64, - continuous_intercept: f64, +/// Returns [`PsychometricError::EventTimeRequired`] for any non-event clock, +/// [`PsychometricError::NonPositiveInterval`] when `event_delta` is not +/// strictly positive, and [`PsychometricError::InvalidNumericInput`] when +/// the prior variance is negative or non-finite, the log-rate is +/// non-finite, or the mapped covariance is non-finite. +pub fn recover_discrete_lagged_latent_covariance( + prior_variance: f64, + log_rate: f64, + event_delta: f64, + clock: LagClock, ) -> Result { - let _ = (asymptotic_intercept, continuous_intercept); - Err(PsychometricError::AsymptoticContinuousInterceptIsNotContinuousIntercept) + if !clock.admits_structural_lag() { + return Err(PsychometricError::EventTimeRequired); + } + if !event_delta.is_finite() || event_delta <= 0.0 { + return Err(PsychometricError::NonPositiveInterval); + } + if !prior_variance.is_finite() || prior_variance < 0.0 || !log_rate.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + // 0 * +∞ is NaN. Driver Eq. 3: A_Δt * 0 = 0. + if prior_variance == 0.0 { + return Ok(0.0); + } + let drift_interval = log_rate * event_delta; + let auto_effect = drift_interval.exp(); + if auto_effect.is_finite() { + // +0 underflow is a vanishing lagged covariance. + return require_finite(auto_effect * prior_variance); + } + if !drift_interval.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + // Finite a Δt, overflowed exp. exp(a Δt) p = exp(ln p + a Δt). + require_finite((prior_variance.ln() + drift_interval).exp()) } -/// Refuse treating Table 2 `asymCINT` as the finite-interval discrete -/// intercept increment. +/// Exact scalar discrete latent variance on event time. /// -/// `-κ / a` is the `Δt → ∞` limit of `A^{-1}[e^{A Δt} − I] κ` under -/// stable `a < 0`. A finite event interval is not that limit. +/// Driver, Oud, and Voelkle (2017, Eq. 3–4, pp. 4–5; JSS PDF +/// re-opened 2026-08-18T11:20Z) write `Q_Δt` as the covariance of the +/// stochastic integral (Eq. 4) after the homogeneous map +/// `η(t) = exp(A Δt) η(t0) + …` (Eq. 3). That pair is +/// `Q_Δt = cov(η_ti | η_{t-1,i})` and +/// `cov(η_ti, η_{t-1,i}) = A_Δt cov(η_{t-1,i})` when `ξ` and `z` are +/// given. The law of total variance on that pair is +/// `Var(η_ti) = A_Δt Var(η_{t-1,i}) A_Δt⊤ + Q_Δt`. The scalar map is +/// `exp(2 a Δt) p + Q_Δt`. This is not `Q_Δt` alone and not a Kalman +/// measurement update. A zero prior variance is exactly `Q_Δt`. +/// Binary64 underflow of `exp(2 a Δt)` keeps `Q_Δt`. When `exp(z)` +/// overflows at a finite `z = 2 (a Δt)`, rewrite as +/// `exp(ln p + z) + Q_Δt`. An overflowing rewrite fails closed. +/// A finite `exp(z) p` whose sum with `Q_Δt` overflows fails closed. +/// A zero diffusion skips the process-noise `z → +∞` refusal; the +/// carried term `exp(2 a Δt) p` is then still non-finite when +/// `2 (a Δt)` overflows to `+∞` (`p = 2`, `q = 0`, `a = 1e308`, +/// `Δt = 2`) and fails closed. The JSS article has no numbered §2.2. /// /// # Errors /// -/// Always returns -/// [`PsychometricError::AsymptoticContinuousInterceptIsNotDiscreteIncrement`]. -pub fn refuse_asymptotic_continuous_intercept_as_discrete_increment( - asymptotic_intercept: f64, - discrete_increment: f64, +/// Propagates [`recover_discrete_process_noise`]. Returns +/// [`PsychometricError::InvalidNumericInput`] when the prior variance is +/// negative or non-finite or the mapped variance is non-finite. +pub fn recover_discrete_latent_variance( + prior_variance: f64, + continuous_diffusion: f64, + log_rate: f64, + event_delta: f64, + clock: LagClock, ) -> Result { - let _ = (asymptotic_intercept, discrete_increment); - Err(PsychometricError::AsymptoticContinuousInterceptIsNotDiscreteIncrement) + let process_noise = + recover_discrete_process_noise(continuous_diffusion, log_rate, event_delta, clock)?; + if !prior_variance.is_finite() || prior_variance < 0.0 { + return Err(PsychometricError::InvalidNumericInput); + } + if prior_variance == 0.0 { + return Ok(process_noise); + } + let increment_argument = 2.0 * (log_rate * event_delta); + if increment_argument == 0.0 { + return require_finite(prior_variance + process_noise); + } + let auto_effect_square = increment_argument.exp(); + if auto_effect_square.is_finite() { + return require_finite(auto_effect_square * prior_variance + process_noise); + } + if !increment_argument.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + let carried = require_finite((prior_variance.ln() + increment_argument).exp())?; + require_finite(carried + process_noise) } -/// Refuse treating Table 2 `asymCINT` as `T0MEANS`. +/// Exact scalar stationary within-subject variance on event time. /// -/// `-κ / a` is the intercept contribution to the stationary process -/// mean. Table 2 names `μ_0` `T0MEANS`. Those are not the same map. +/// Driver, Oud, and Voelkle (2017, Eq. 4, p. 5; JSS PDF re-opened +/// 2026-08-19T04:10Z) write `Q_Δt` as +/// `irow(A#^{-1}[e^{A# Δt} − I] row(Q))` with `A# = A ⊗ I + I ⊗ A`. +/// The scalar Kronecker sum is `2 a`. As `Δt → ∞` with stable +/// `a < 0`, `e^{2 a Δt} → 0` and Eq. 4 becomes `-q / (2 a)`. The +/// JSS summary names that limit `asymDIFFUSION` and takes it as the +/// total within-subject variance (p. 16). Section 4.3 (pp. 9–10) +/// constrains a stationary `T0VAR` to that same model-predicted +/// variance. When `2 a` is finite, form `q / -(2 a)` so `q / a` +/// overflow does not lose a finite Lyapunov solution (`q = MAX`, +/// `a = -0.75` → `MAX / 1.5`; `CodeRabbit` on `75ecdd3`). When `2 a` +/// overflows, form `(q / a) * -0.5`. Do not form `2 a` as the only +/// path: at `a = -1e308`, `q = 1e308`, `2 a` overflows and +/// `-q / (2 a)` collapses to `+0`, but `(q / a) * -0.5 = 0.5`. Do +/// not form `0.5 q` first: at `q = from_bits(1)`, `a = -from_bits(1)`, +/// `-0.5 * q` underflows to `-0` and the quotient is `+0`, but +/// the representable Lyapunov solution is `0.5`. A zero diffusion is +/// exactly zero. `a ≥ 0` has no finite stationary variance +/// (including Brownian `a = 0`, whose variance grows as `q Δt`). +/// An overflowing Lyapunov solution fails closed. This is not a Kalman +/// filter, not DSEM, not a matrix `expm`, and not ctsem estimation. /// /// # Errors /// -/// Always returns -/// [`PsychometricError::AsymptoticContinuousInterceptIsNotInitialLatentMean`]. -pub fn refuse_asymptotic_continuous_intercept_as_initial_latent_mean( - asymptotic_intercept: f64, - initial_latent_mean: f64, +/// Returns [`PsychometricError::EventTimeRequired`] for any non-event +/// clock, [`PsychometricError::StationaryVarianceRequiresStableDrift`] +/// when the log-rate is not strictly negative, and +/// [`PsychometricError::InvalidNumericInput`] when the diffusion is +/// negative or non-finite, the log-rate is non-finite, or the mapped +/// variance is non-finite. +pub fn recover_stationary_latent_variance( + continuous_diffusion: f64, + log_rate: f64, + clock: LagClock, ) -> Result { - let _ = (asymptotic_intercept, initial_latent_mean); - Err(PsychometricError::AsymptoticContinuousInterceptIsNotInitialLatentMean) + if !clock.admits_structural_lag() { + return Err(PsychometricError::EventTimeRequired); + } + if !continuous_diffusion.is_finite() || continuous_diffusion < 0.0 || !log_rate.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + if log_rate >= 0.0 { + return Err(PsychometricError::StationaryVarianceRequiresStableDrift); + } + // Driver Eq. 4 as Δt → ∞: (0 − 1) q / (2 a) = −q / (2 a). + // Direct 0 * (1 / (2 a)) is not needed; a zero diffusion is zero. + if continuous_diffusion == 0.0 { + return Ok(0.0); + } + // −q / (2 a). When 2 a is finite, divide by that Kronecker sum + // so q/a overflow does not lose a finite Lyapunov solution + // (q = MAX, a = −0.75 → MAX/1.5; CodeRabbit on 75ecdd3). + // When 2 a overflows (|a| = 1e308), form (q/a)*−0.5 instead. + // Do not form 0.5 q first (min-subnormal underflow). + let twice_rate = log_rate * 2.0; + let stationary = if twice_rate.is_finite() { + continuous_diffusion / -twice_rate + } else { + (continuous_diffusion / log_rate) * -0.5 + }; + require_finite(stationary) } -/// Refuse treating Table 2 `asymCINT` as `asymTIPREDEFFECT`. +/// Refuse treating finite-interval process noise as `asymDIFFUSION`. /// -/// `-κ / a` is the intercept contribution. `-B z / a` is the -/// time-independent predictor contribution. Page 16 notes that a -/// `T0MEANS` stationarity constraint includes time-independent -/// predictors; that composition is not this intercept-only map. +/// Driver, Oud, and Voelkle (2017, Eq. 4 and p. 16): `Q_Δt` at a +/// finite event interval is the covariance of the stochastic integral +/// over that interval. The asymptotic within-subject variance is the +/// `Δt → ∞` limit. Section 4.3 distinguishes that stationary +/// constraint from a predetermined `T0VAR`. /// /// # Errors /// /// Always returns -/// [`PsychometricError::AsymptoticContinuousInterceptIsNotAsymptoticTimeIndependentEffect`]. -pub fn refuse_asymptotic_continuous_intercept_as_asymptotic_time_independent_effect( - asymptotic_intercept: f64, - asymptotic_time_independent_effect: f64, +/// [`PsychometricError::FiniteIntervalProcessNoiseIsNotStationary`]. +pub fn refuse_finite_interval_process_noise_as_stationary_variance( + process_noise: f64, + event_delta: f64, ) -> Result { - let _ = (asymptotic_intercept, asymptotic_time_independent_effect); - Err(PsychometricError::AsymptoticContinuousInterceptIsNotAsymptoticTimeIndependentEffect) + let _ = (process_noise, event_delta); + Err(PsychometricError::FiniteIntervalProcessNoiseIsNotStationary) } -/// Exact scalar p. 16 stationary `T0MEANS`. +/// Exact scalar trait-plus-state latent variance. /// -/// Driver, Oud, and Voelkle (2017, p. 16; Table 2, p. 12; Eq. 3, p. 5; -/// JSS PDF opened 2026-08-21T16:13Z from -/// ) -/// constrain `T0MEANS` to the model-implied values using -/// `T0MEANSbase` / `T0MEANSfree` when the first observation is -/// determined by the process in the same way as later observations. -/// Those constraints include extra effects due to time-independent -/// predictors (`asymTIPREDEFFECT`). Table 2 names `κ` `CINT` and -/// names `asymCINT` the `Δt → ∞` intercept contribution `-κ / a`. -/// For stable `a < 0` the scalar composition is -/// `-κ / a + −B z / a`. Form the intercept contribution first, then -/// include the TI extra effect, then add. A zero intercept and a zero -/// TI contribution is exactly zero. `a ≥ 0` cannot hold a finite -/// process-mean change when either contribution is nonzero and fails -/// closed. That constrained first-occasion mean is not free -/// `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and -/// not the finite-interval discrete latent mean -/// `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`. The printed 2-latent -/// `T0MEANS` 2.823 is not this scalar map. This is not a Kalman -/// filter, not a matrix `expm`, and not ctsem estimation. +/// Driver, Oud, and Voelkle (2017, §4.3, p. 9; JSS PDF re-opened +/// 2026-08-18T21:07Z) add a stable trait process with `DRIFT` and +/// `DIFFUSION` fixed to zero. The scalar sum is `trait + state`. The +/// ctsem `TRAITVAR` parameterization that adds the trait to the +/// `DIFFUSION` matrix is a software rewrite; it does not license +/// treating trait variance as process noise. This is not RI-CLPM, not +/// a Kalman filter, and not ctsem estimation. /// /// # Errors /// -/// Returns [`PsychometricError::EventTimeRequired`] for any non-event -/// clock, [`PsychometricError::AsymptoticContinuousInterceptRequiresStableDrift`] -/// or [`PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift`] -/// when the drift is not strictly negative and the corresponding -/// contribution is nonzero, and [`PsychometricError::InvalidNumericInput`] -/// when an input is non-finite or a quotient or sum overflows. -pub fn recover_stationary_initial_latent_mean( - continuous_intercept: f64, - time_independent_effect: f64, - time_independent_predictor: f64, +/// Returns [`PsychometricError::InvalidNumericInput`] when either +/// variance is negative or non-finite, or the sum overflows. +pub fn recover_trait_plus_state_latent_variance( + trait_variance: f64, + state_variance: f64, +) -> Result { + if !trait_variance.is_finite() || trait_variance < 0.0 { + return Err(PsychometricError::InvalidNumericInput); + } + if !state_variance.is_finite() || state_variance < 0.0 { + return Err(PsychometricError::InvalidNumericInput); + } + if trait_variance == 0.0 { + return Ok(state_variance); + } + if state_variance == 0.0 { + return Ok(trait_variance); + } + require_finite(trait_variance + state_variance) +} + +/// Exact scalar trait-plus-state lagged latent covariance. +/// +/// Driver, Oud, and Voelkle (2017, §4.3, p. 9): a stable trait has no +/// temporal dynamics, so `cov(trait_t, trait_{t-1}) = trait`. The +/// state lagged covariance remains `exp(a Δt) p` (Eq. 3–4). The +/// scalar sum is `trait + exp(a Δt) p`. Evolving the summed variance +/// as if it were all state is not this map. This is not RI-CLPM. +/// +/// # Errors +/// +/// Propagates [`recover_discrete_lagged_latent_covariance`]. Returns +/// [`PsychometricError::InvalidNumericInput`] when the trait variance +/// is negative or non-finite or the sum overflows. +pub fn recover_trait_plus_state_lagged_covariance( + trait_variance: f64, + state_prior_variance: f64, log_rate: f64, + event_delta: f64, clock: LagClock, ) -> Result { - let intercept = recover_asymptotic_continuous_intercept(continuous_intercept, log_rate, clock)?; - let tipred = recover_asymptotic_time_independent_predictor_effect( - time_independent_effect, - time_independent_predictor, + if !trait_variance.is_finite() || trait_variance < 0.0 { + return Err(PsychometricError::InvalidNumericInput); + } + let state_lagged = recover_discrete_lagged_latent_covariance( + state_prior_variance, log_rate, + event_delta, clock, )?; - require_finite(intercept + tipred) + if trait_variance == 0.0 { + return Ok(state_lagged); + } + require_finite(trait_variance + state_lagged) } -/// Refuse treating p. 16 stationary `T0MEANS` as free `T0MEANS`. +/// Refuse treating Driver §4.3 trait variance as process noise. /// -/// `-κ / a + −B z / a` is the constrained first-occasion mean. Table 2 -/// names the free first-occasion latent mean `T0MEANS`. Those are not -/// the same map. +/// A stable trait has `DIFFUSION` fixed to zero. The ctsem +/// `TRAITVAR` rewrite that adds the trait to `DIFFUSION` is not a +/// license to treat trait variance as `Q_Δt`. /// /// # Errors /// -/// Always returns -/// [`PsychometricError::StationaryInitialLatentMeanIsNotInitialLatentMean`]. -pub fn refuse_stationary_initial_latent_mean_as_initial_latent_mean( - stationary_mean: f64, - initial_latent_mean: f64, +/// Always returns [`PsychometricError::TraitVarianceIsNotProcessNoise`]. +pub fn refuse_trait_variance_as_process_noise( + trait_variance: f64, + process_noise: f64, ) -> Result { - let _ = (stationary_mean, initial_latent_mean); - Err(PsychometricError::StationaryInitialLatentMeanIsNotInitialLatentMean) + let _ = (trait_variance, process_noise); + Err(PsychometricError::TraitVarianceIsNotProcessNoise) } -/// Refuse treating p. 16 stationary `T0MEANS` as `asymCINT`. +/// Refuse treating Driver §4.3 trait variance as `asymDIFFUSION`. /// -/// The constraint includes time-independent predictors. `-κ / a` is -/// the intercept contribution and is not that composition when -/// `B z ≠ 0`. +/// Trait variance is time-invariant between-subject variance. The +/// stationary within-subject variance is the `Δt → ∞` limit of Eq. 4. /// /// # Errors /// /// Always returns -/// [`PsychometricError::StationaryInitialLatentMeanIsNotAsymptoticContinuousIntercept`]. -pub fn refuse_stationary_initial_latent_mean_as_asymptotic_continuous_intercept( - stationary_mean: f64, - asymptotic_intercept: f64, +/// [`PsychometricError::TraitVarianceIsNotStationaryWithinSubject`]. +pub fn refuse_trait_variance_as_stationary_within_subject( + trait_variance: f64, + stationary_state_variance: f64, ) -> Result { - let _ = (stationary_mean, asymptotic_intercept); - Err(PsychometricError::StationaryInitialLatentMeanIsNotAsymptoticContinuousIntercept) + let _ = (trait_variance, stationary_state_variance); + Err(PsychometricError::TraitVarianceIsNotStationaryWithinSubject) } -/// Refuse treating p. 16 stationary `T0MEANS` as `asymTIPREDEFFECT`. +/// Exact scalar observed-indicator variance from Driver Equation 5 +/// with `MANIFESTTRAITVAR = 0`. /// -/// The constraint includes the intercept contribution. `-B z / a` is -/// the TI extra effect and is not that composition when `κ ≠ 0`. +/// Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Table 2, p. 12; JSS +/// PDF re-opened 2026-08-19T04:18Z) write `y_i(t) = τ_i + Λ η_i(t) + +/// ε_i(t)` with `ε ~ N(0, Θ)` and `τ_i ~ N(μ_τ, Ψ_τ)`. Equation 1 +/// (p. 4) is the latent SDE, not the measurement model. Table 2 names +/// `Θ` `MANIFESTVAR` and `Ψ_τ` `MANIFESTTRAITVAR`. The p. 16 summary +/// restates those names; it is not the equation. With `Ψ_τ = 0` the +/// scalar map is `Var(y) = λ² Var(η) + θ`. Form `(λ p) λ` then add +/// `θ`. Do not form `λ²` first: at `λ = 1e308`, `p = 1e-308`, `λ²` +/// overflows and `λ² p` is non-finite, but `(λ p) λ = 1e308`. A zero +/// loading or zero latent variance is exactly `θ`. A zero +/// measurement error is exactly `λ² p`. Negative latent or +/// measurement-error variance fails closed. An overflowing product +/// or sum fails closed. This is not a Kalman filter, not ESEM +/// estimation, and not ctsem estimation. /// /// # Errors /// -/// Always returns -/// [`PsychometricError::StationaryInitialLatentMeanIsNotAsymptoticTimeIndependentEffect`]. -pub fn refuse_stationary_initial_latent_mean_as_asymptotic_time_independent_effect( - stationary_mean: f64, - asymptotic_time_independent_effect: f64, +/// Returns [`PsychometricError::InvalidNumericInput`] when the loading +/// is non-finite, either variance is negative or non-finite, or the +/// mapped variance is non-finite. +pub fn recover_manifest_observed_variance( + loading: f64, + latent_variance: f64, + measurement_error_variance: f64, ) -> Result { - let _ = (stationary_mean, asymptotic_time_independent_effect); - Err(PsychometricError::StationaryInitialLatentMeanIsNotAsymptoticTimeIndependentEffect) + if !loading.is_finite() + || !latent_variance.is_finite() + || latent_variance < 0.0 + || !measurement_error_variance.is_finite() + || measurement_error_variance < 0.0 + { + return Err(PsychometricError::InvalidNumericInput); + } + if loading == 0.0 || latent_variance == 0.0 { + return Ok(measurement_error_variance); + } + let explained = require_finite((loading * latent_variance) * loading)?; + if measurement_error_variance == 0.0 { + return Ok(explained); + } + require_finite(explained + measurement_error_variance) } -/// Refuse treating p. 16 stationary `T0MEANS` as a finite-interval -/// discrete latent mean. +/// Refuse treating Driver Eq. 5 measurement error as `Var(y)`. /// -/// `-κ / a + −B z / a` is the `Δt → ∞` constrained first-occasion -/// mean. `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` is a finite event -/// interval and is not that limit. +/// Table 2 (p. 12) names `MANIFESTVAR` as `Θ`, the variance of `ε`. +/// Equation 5 maps `Var(y) = λ² Var(η) + θ` when `Ψ_τ = 0`. /// /// # Errors /// /// Always returns -/// [`PsychometricError::StationaryInitialLatentMeanIsNotDiscreteMean`]. -pub fn refuse_stationary_initial_latent_mean_as_discrete_mean( - stationary_mean: f64, - discrete_mean: f64, +/// [`PsychometricError::MeasurementErrorIsNotObservedVariance`]. +pub fn refuse_measurement_error_as_observed_variance( + measurement_error_variance: f64, + observed_variance: f64, ) -> Result { - let _ = (stationary_mean, discrete_mean); - Err(PsychometricError::StationaryInitialLatentMeanIsNotDiscreteMean) + let _ = (measurement_error_variance, observed_variance); + Err(PsychometricError::MeasurementErrorIsNotObservedVariance) } -/// Exact scalar observed mean of §4.3 stationary `T0MEANS`. +/// Refuse treating Driver Eq. 5 latent variance as `Var(y)`. /// -/// Driver, Oud, and Voelkle (2017, §4.3, pp. 9–10; Eq. 5, p. 5; -/// Table 2, p. 12; Eq. 3, p. 5; JSS PDF re-opened 2026-08-21T20:07Z -/// from -/// ) -/// constrain the first-occasion mean to the model-predicted mean -/// when `stationary` includes `"T0MEANS"`. Equation 5 writes -/// `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and -/// `Γ ~ N(τ, Ψ)`. The constrained latent mean is -/// `-κ / a + −B z / a`. The scalar composition is -/// `E(y_0) = τ + λ(−κ / a + −B z / a)`. Form the stationary latent -/// mean first, then `τ + λ` of that mean. A zero loading is exactly -/// `τ`. A zero intercept and a zero TI contribution is exactly `τ`. -/// `τ + λ μ_0` for free `T0MEANS` is not this composition. -/// `τ + λ(−κ / a)` is not this composition when `B z ≠ 0`. -/// `τ + λ μ_t` is not this composition. `MANIFESTMEANS` is not -/// `E(y_0)`. The constrained latent mean is not `E(y_0)`. This is -/// not a Kalman filter, not a matrix `expm`, and not ctsem -/// estimation. +/// `Var(η)` is the latent process variance. Equation 5 maps +/// `Var(y) = λ² Var(η) + θ` when `Ψ_τ = 0`. /// /// # Errors /// -/// Propagates [`recover_stationary_initial_latent_mean`] and -/// [`recover_manifest_observed_mean`]. -#[allow(clippy::too_many_arguments)] -pub fn recover_stationary_initial_observed_mean( - loading: f64, - continuous_intercept: f64, - time_independent_effect: f64, - time_independent_predictor: f64, - log_rate: f64, - manifest_mean: f64, - clock: LagClock, +/// Always returns [`PsychometricError::LatentVarianceIsNotObservedVariance`]. +pub fn refuse_latent_variance_as_observed_variance( + latent_variance: f64, + observed_variance: f64, ) -> Result { - let stationary_latent_mean = recover_stationary_initial_latent_mean( - continuous_intercept, - time_independent_effect, - time_independent_predictor, - log_rate, - clock, - )?; - recover_manifest_observed_mean(loading, stationary_latent_mean, manifest_mean) + let _ = (latent_variance, observed_variance); + Err(PsychometricError::LatentVarianceIsNotObservedVariance) } -/// Refuse treating §4.3 stationary `T0MEANS` as `E(y_0)`. +/// Exact scalar observed-indicator variance from Driver Equation 5 +/// with nonzero `MANIFESTTRAITVAR`. /// -/// `−κ / a + −B z / a` is the constrained latent mean. Equation 5 -/// maps `E(y_0) = τ + λ` of that mean. +/// Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Table 2, p. 12; JSS +/// PDF re-opened 2026-08-19T04:18Z) write `τ_i ~ N(μ_τ, Ψ_τ)` on the +/// indicator intercept. The scalar map is `Var(y) = λ² Var(η) + θ + +/// ψ`. Form the `Ψ_τ = 0` map first, then add `ψ`. Do not form +/// `λ²` first. A zero manifest trait is exactly `λ² p + θ`. A zero +/// loading or zero latent variance is exactly `θ + ψ`. `Ψ_τ` is not +/// `Θ`: Table 2 names `MANIFESTTRAITVAR` separately from +/// `MANIFESTVAR`. `TRAITVAR` is latent additional variance and is +/// scaled by `λ²`; `MANIFESTTRAITVAR` is not. Negative trait +/// variance fails closed. An overflowing sum fails closed. This is +/// not a Kalman filter, not ESEM estimation, and not ctsem +/// estimation. /// /// # Errors /// -/// Always returns -/// [`PsychometricError::StationaryInitialLatentMeanIsNotObservedMean`]. -pub fn refuse_stationary_initial_latent_mean_as_observed_mean( - stationary_latent_mean: f64, - stationary_observed_mean: f64, +/// Propagates [`recover_manifest_observed_variance`]. Returns +/// [`PsychometricError::InvalidNumericInput`] when the manifest-trait +/// variance is negative or non-finite or the sum overflows. +pub fn recover_manifest_trait_plus_state_observed_variance( + loading: f64, + latent_variance: f64, + measurement_error_variance: f64, + manifest_trait_variance: f64, ) -> Result { - let _ = (stationary_latent_mean, stationary_observed_mean); - Err(PsychometricError::StationaryInitialLatentMeanIsNotObservedMean) + if !manifest_trait_variance.is_finite() || manifest_trait_variance < 0.0 { + return Err(PsychometricError::InvalidNumericInput); + } + let within = + recover_manifest_observed_variance(loading, latent_variance, measurement_error_variance)?; + if manifest_trait_variance == 0.0 { + return Ok(within); + } + require_finite(within + manifest_trait_variance) } -/// Refuse treating `MANIFESTMEANS` as Eq. 5 of §4.3 stationary -/// `T0MEANS`. +/// Refuse treating Driver Eq. 5 `MANIFESTTRAITVAR` as `MANIFESTVAR`. /// -/// Table 2 names `τ` `MANIFESTMEANS`. `τ + λ(−κ / a + −B z / a)` is -/// not `τ` when the loading and constrained mean are nonzero. +/// Table 2 (p. 12) names `MANIFESTTRAITVAR` as `Ψ_τ`, additional +/// intercept variance on the indicators, and `MANIFESTVAR` as `Θ`, +/// the variance of `ε`. Equation 5 maps `Var(y) = λ² Var(η) + θ + +/// ψ`. `Ψ_τ` is not `Θ`. /// /// # Errors /// /// Always returns -/// [`PsychometricError::StationaryInitialObservedMeanIsNotManifestMeans`]. -pub fn refuse_stationary_initial_observed_mean_as_manifest_means( - stationary_observed_mean: f64, - manifest_mean: f64, +/// [`PsychometricError::ManifestTraitVarianceIsNotMeasurementError`]. +pub fn refuse_manifest_trait_variance_as_measurement_error( + manifest_trait_variance: f64, + measurement_error_variance: f64, ) -> Result { - let _ = (stationary_observed_mean, manifest_mean); - Err(PsychometricError::StationaryInitialObservedMeanIsNotManifestMeans) + let _ = (manifest_trait_variance, measurement_error_variance); + Err(PsychometricError::ManifestTraitVarianceIsNotMeasurementError) } -/// Refuse treating evolved `τ + λ μ_t` as Eq. 5 of §4.3 stationary -/// `T0MEANS`. +/// Exact scalar lagged observed-indicator covariance from Driver +/// Equation 5. /// -/// A finite-interval evolved observed mean is not the constrained -/// first-occasion observed mean. +/// Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Eq. 3–4, pp. 4–5; +/// Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z) write +/// `y_i(t) = τ_i + Λ η_i(t) + ε_i(t)` with independent measurement +/// error and a person-level intercept `τ_i ~ N(μ_τ, Ψ_τ)`. The +/// scalar lagged covariance is `cov(y_t, y_{t-1}) = λ² cov(η_t, +/// η_{t-1}) + ψ`. `Θ` does not enter: `ε_t` and `ε_{t-1}` are +/// independent. Form `(λ c) λ` then add `ψ`. Do not form `λ²` +/// first. A zero loading or zero latent lagged covariance is +/// exactly `ψ`. A zero manifest trait is exactly `λ² c`. Negative +/// latent lagged covariance or trait variance fails closed. An +/// overflowing product or sum fails closed. This is not a Kalman +/// filter and not ctsem estimation. /// /// # Errors /// -/// Always returns -/// [`PsychometricError::EvolvedObservedMeanIsNotStationaryInitialObservedMean`]. -pub fn refuse_evolved_observed_mean_as_stationary_initial_observed_mean( - evolved_observed_mean: f64, - stationary_observed_mean: f64, +/// Returns [`PsychometricError::InvalidNumericInput`] when the loading +/// is non-finite, the latent lagged covariance is negative or +/// non-finite, the manifest-trait variance is negative or +/// non-finite, or the mapped covariance is non-finite. +pub fn recover_manifest_lagged_observed_covariance( + loading: f64, + lagged_latent_covariance: f64, + manifest_trait_variance: f64, ) -> Result { - let _ = (evolved_observed_mean, stationary_observed_mean); - Err(PsychometricError::EvolvedObservedMeanIsNotStationaryInitialObservedMean) + if !loading.is_finite() + || !lagged_latent_covariance.is_finite() + || lagged_latent_covariance < 0.0 + || !manifest_trait_variance.is_finite() + || manifest_trait_variance < 0.0 + { + return Err(PsychometricError::InvalidNumericInput); + } + if loading == 0.0 || lagged_latent_covariance == 0.0 { + return Ok(manifest_trait_variance); + } + let explained = require_finite((loading * lagged_latent_covariance) * loading)?; + if manifest_trait_variance == 0.0 { + return Ok(explained); + } + require_finite(explained + manifest_trait_variance) } -/// Refuse treating `τ + λ(−κ / a)` as Eq. 5 of §4.3 stationary -/// `T0MEANS`. +/// Refuse treating Driver Eq. 3–4 lagged latent covariance as +/// `cov(y_t, y_{t-1})`. /// -/// The constraint includes time-independent predictors. -/// `τ + λ(−κ / a)` is not that composition when `B z ≠ 0`. +/// Equation 5 maps `cov(y_t, y_{t-1}) = λ² cov(η_t, η_{t-1}) + ψ`. +/// The latent lagged covariance is not the observed lagged +/// covariance. /// /// # Errors /// /// Always returns -/// [`PsychometricError::AsymptoticContinuousInterceptObservedMeanIsNotStationaryInitialObservedMean`]. -pub fn refuse_asymptotic_continuous_intercept_observed_mean_as_stationary_initial_observed_mean( - asymptotic_intercept_observed_mean: f64, - stationary_observed_mean: f64, +/// [`PsychometricError::LatentLaggedCovarianceIsNotObservedCovariance`]. +pub fn refuse_latent_lagged_covariance_as_observed_covariance( + lagged_latent_covariance: f64, + observed_lagged_covariance: f64, ) -> Result { - let _ = (asymptotic_intercept_observed_mean, stationary_observed_mean); - Err( - PsychometricError::AsymptoticContinuousInterceptObservedMeanIsNotStationaryInitialObservedMean, - ) + let _ = (lagged_latent_covariance, observed_lagged_covariance); + Err(PsychometricError::LatentLaggedCovarianceIsNotObservedCovariance) } -/// Refuse treating `τ + λ μ_0` as Eq. 5 of §4.3 stationary -/// `T0MEANS`. +/// Refuse treating Driver Eq. 5 measurement error as lagged observed +/// covariance. /// -/// Free first-occasion `T0MEANS` is not the constrained -/// first-occasion mean. +/// `MANIFESTVAR` is `Θ`. Independent `ε_t` does not enter +/// `cov(y_t, y_{t-1})`. /// /// # Errors /// /// Always returns -/// [`PsychometricError::InitialObservedMeanIsNotStationaryInitialObservedMean`]. -pub fn refuse_initial_observed_mean_as_stationary_initial_observed_mean( - initial_observed_mean: f64, - stationary_observed_mean: f64, +/// [`PsychometricError::MeasurementErrorIsNotLaggedObservedCovariance`]. +pub fn refuse_measurement_error_as_lagged_observed_covariance( + measurement_error_variance: f64, + observed_lagged_covariance: f64, ) -> Result { - let _ = (initial_observed_mean, stationary_observed_mean); - Err(PsychometricError::InitialObservedMeanIsNotStationaryInitialObservedMean) + let _ = (measurement_error_variance, observed_lagged_covariance); + Err(PsychometricError::MeasurementErrorIsNotLaggedObservedCovariance) } -/// Exact scalar §4.3 / p. 16 stationary `T0VAR`. +/// Exact scalar observed-indicator mean from Driver Equation 5. /// -/// Driver, Oud, and Voelkle (2017, §4.3, pp. 9–10; p. 16; Table 2, -/// p. 12; §7.2, pp. 20–21; Eq. 4, p. 5; JSS PDF re-opened -/// 2026-08-22T03:07Z from -/// ) -/// constrain `T0VAR` to the model-predicted variance when -/// `stationary` includes `"T0VAR"`. Section 4.3 writes that the -/// first-occasion variances are constrained according to the model -/// predicted variances across all time points. Page 16 names -/// `asymDIFFUSION` the total within-subject variance as `Δt → ∞`. -/// For stable `a < 0` that scalar is `-q / (2 a)`. Section 4.3 -/// (p. 9) adds a stable trait process with `DRIFT` and `DIFFUSION` -/// fixed to zero (`TRAITVAR`). Section 7.2 names `addedTIPREDVAR` -/// the stable between-subject variance accounted for by -/// time-independent predictors; the scalar map is `(B / a)² v`. -/// The constrained first-occasion variance is -/// `trait + −q / (2 a) + (B / a)² v`. Form the within-subject -/// contribution first, then include the trait, then include the TI -/// extra variance, then add. A zero trait, a zero diffusion, and a -/// zero TI contribution is exactly zero. A zero diffusion and a -/// zero TI contribution is exactly the trait. `a ≥ 0` cannot hold a -/// finite process variance when the diffusion or the TI -/// contribution is nonzero and fails closed. Trait-only variance -/// does not require a stable drift. That constrained -/// first-occasion variance is not free `T0VAR`, not -/// `asymDIFFUSION` alone, not `TRAITVAR` alone, not -/// `addedTIPREDVAR` alone, and not the finite-interval discrete -/// latent variance `exp(2 a Δt) p + Q_Δt`. The printed 2-latent -/// `addedTIPREDVAR` 2.838 is not this scalar map. This is not a -/// Kalman filter, not a matrix `expm`, and not ctsem estimation. +/// Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Table 2, p. 12; JSS +/// PDF re-opened 2026-08-19T14:08Z) write `y_i(t) = Γ + Λ η_i(t) + +/// ζ_i(t)` with `ζ ~ N(0, Θ)` and `Γ ~ N(τ, Ψ)`. The expected +/// intercept is `τ`. Table 2 names `τ` `MANIFESTMEANS`. The scalar +/// map is `E(y) = τ + λ μ`. Form `λ μ` then add `τ`. A zero loading +/// or zero latent mean is exactly `τ`. A zero intercept is exactly +/// `λ μ`. `MANIFESTMEANS` is not `E(y)`. `E(η)` is not `E(y)`. +/// `CINT` `κ` is the latent continuous intercept from Equation 1, +/// not `τ`. `T0MEANS` is the initial latent mean, not `E(y)`. An +/// overflowing product or sum fails closed. This is not a Kalman +/// filter and not ctsem estimation. /// /// # Errors /// -/// Returns [`PsychometricError::EventTimeRequired`] for any -/// non-event clock, -/// [`PsychometricError::StationaryVarianceRequiresStableDrift`] -/// when the diffusion is nonzero and the drift is not strictly -/// negative, -/// [`PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift`] -/// when the TI contribution is nonzero and the drift is not -/// strictly negative, and -/// [`PsychometricError::InvalidNumericInput`] when an input is -/// non-finite, a variance is negative, or a product or sum -/// overflows. -pub fn recover_stationary_initial_latent_variance( - trait_variance: f64, - continuous_diffusion: f64, - time_independent_effect: f64, - predictor_variance: f64, - log_rate: f64, - clock: LagClock, +/// Returns [`PsychometricError::InvalidNumericInput`] when the loading, +/// latent mean, or intercept is non-finite, or the mapped mean is +/// non-finite. +pub fn recover_manifest_observed_mean( + loading: f64, + latent_mean: f64, + manifest_mean: f64, ) -> Result { - if !clock.admits_structural_lag() { - return Err(PsychometricError::EventTimeRequired); + if !loading.is_finite() || !latent_mean.is_finite() || !manifest_mean.is_finite() { + return Err(PsychometricError::InvalidNumericInput); } - let state = if continuous_diffusion == 0.0 { - 0.0 - } else { - recover_stationary_latent_variance(continuous_diffusion, log_rate, clock)? - }; - let trait_plus_state = recover_trait_plus_state_latent_variance(trait_variance, state)?; - let added = recover_asymptotic_time_independent_predictor_variance( - time_independent_effect, - predictor_variance, - log_rate, - clock, - )?; - require_finite(trait_plus_state + added) + if loading == 0.0 || latent_mean == 0.0 { + return Ok(manifest_mean); + } + let explained = require_finite(loading * latent_mean)?; + if manifest_mean == 0.0 { + return Ok(explained); + } + require_finite(explained + manifest_mean) } -/// Refuse treating §4.3 / p. 16 stationary `T0VAR` as free `T0VAR`. +/// Refuse treating Driver Eq. 5 `MANIFESTMEANS` as `E(y)`. /// -/// `trait + −q / (2 a) + (B / a)² v` is the constrained -/// first-occasion variance. Table 2 names the free first-occasion -/// latent variance `T0VAR`. Those are not the same map. +/// Table 2 (p. 12) names `τ` the expected intercept `Γ`. Equation 5 +/// maps `E(y) = τ + λ μ`. /// /// # Errors /// -/// Always returns -/// [`PsychometricError::StationaryInitialLatentVarianceIsNotInitialLatentVariance`]. -pub fn refuse_stationary_initial_latent_variance_as_initial_latent_variance( - stationary_variance: f64, - initial_latent_variance: f64, +/// Always returns [`PsychometricError::ManifestMeansIsNotObservedMean`]. +pub fn refuse_manifest_means_as_observed_mean( + manifest_mean: f64, + observed_mean: f64, ) -> Result { - let _ = (stationary_variance, initial_latent_variance); - Err(PsychometricError::StationaryInitialLatentVarianceIsNotInitialLatentVariance) + let _ = (manifest_mean, observed_mean); + Err(PsychometricError::ManifestMeansIsNotObservedMean) } -/// Refuse treating §4.3 / p. 16 stationary `T0VAR` as -/// `asymDIFFUSION`. +/// Refuse treating Driver Eq. 5 latent mean as `E(y)`. /// -/// The constraint includes trait variance and time-independent -/// predictor variance. `-q / (2 a)` is the within-subject -/// contribution and is not that composition when `TRAITVAR` or -/// `addedTIPREDVAR` is nonzero. +/// `E(η)` is the latent process mean. Equation 5 maps `E(y) = τ + λ μ`. +/// `T0MEANS` is that latent mean at the first occasion, not `E(y)`. /// /// # Errors /// -/// Always returns -/// [`PsychometricError::StationaryInitialLatentVarianceIsNotStationaryWithinSubject`]. -pub fn refuse_stationary_initial_latent_variance_as_stationary_within_subject( - stationary_t0_variance: f64, - asymptotic_within_subject: f64, +/// Always returns [`PsychometricError::LatentMeanIsNotObservedMean`]. +pub fn refuse_latent_mean_as_observed_mean( + latent_mean: f64, + observed_mean: f64, ) -> Result { - let _ = (stationary_t0_variance, asymptotic_within_subject); - Err(PsychometricError::StationaryInitialLatentVarianceIsNotStationaryWithinSubject) + let _ = (latent_mean, observed_mean); + Err(PsychometricError::LatentMeanIsNotObservedMean) } -/// Refuse treating §4.3 / p. 16 stationary `T0VAR` as `TRAITVAR`. +/// Refuse treating Driver Table 2 `CINT` as `MANIFESTMEANS`. /// -/// The constraint includes the within-subject process variance and -/// time-independent predictor variance. `TRAITVAR` is not that -/// composition when those contributions are nonzero. +/// Table 2 (p. 12) names `κ` `CINT`, the latent continuous intercept +/// from Equation 1, and `τ` `MANIFESTMEANS`, the expected `Γ` from +/// Equation 5. `κ` is not `τ`. /// /// # Errors /// /// Always returns -/// [`PsychometricError::StationaryInitialLatentVarianceIsNotTraitVariance`]. -pub fn refuse_stationary_initial_latent_variance_as_trait_variance( - stationary_t0_variance: f64, - trait_variance: f64, +/// [`PsychometricError::ContinuousInterceptIsNotManifestMeans`]. +pub fn refuse_continuous_intercept_as_manifest_means( + continuous_intercept: f64, + manifest_mean: f64, ) -> Result { - let _ = (stationary_t0_variance, trait_variance); - Err(PsychometricError::StationaryInitialLatentVarianceIsNotTraitVariance) + let _ = (continuous_intercept, manifest_mean); + Err(PsychometricError::ContinuousInterceptIsNotManifestMeans) } -/// Refuse treating §4.3 / p. 16 stationary `T0VAR` as -/// `addedTIPREDVAR`. +/// Exact scalar discrete intercept increment from Driver Equation 3. /// -/// The constraint includes trait variance and `asymDIFFUSION`. -/// `(B / a)² v` is the TI extra variance and is not that -/// composition when those contributions are nonzero. +/// Driver, Oud, and Voelkle (2017, Eq. 3, p. 4; Table 2, p. 12; JSS +/// PDF re-opened 2026-08-19T18:10Z) write the expected-value term +/// `A^{-1}[e^{A Δt} − I] b` after the stochastic integral is taken +/// to have mean zero. Table 2 names `κ` `CINT`. The scalar map is +/// `κ (expm1(a Δt) / a)` for `a ≠ 0`. A zero drift is the Eq. 3 +/// integral with `A = 0`: `κ Δt`. That path has no matrix inverse. +/// A zero intercept is exactly zero. `CINT` is not this discrete +/// increment. The `a ≠ 0` evaluation is +/// [`recover_discrete_constant_predictor_effect`]. This is not a +/// Kalman filter and not ctsem estimation. /// /// # Errors /// -/// Always returns -/// [`PsychometricError::StationaryInitialLatentVarianceIsNotAsymptoticTimeIndependentVariance`]. -pub fn refuse_stationary_initial_latent_variance_as_asymptotic_time_independent_variance( - stationary_t0_variance: f64, - added_predictor_variance: f64, +/// Returns [`PsychometricError::EventTimeRequired`] for any non-event +/// clock, [`PsychometricError::NonPositiveInterval`] when +/// `event_delta` is not strictly positive, and +/// [`PsychometricError::InvalidNumericInput`] when the intercept or +/// drift is non-finite or the mapped increment is non-finite. +pub fn recover_discrete_continuous_intercept_effect( + continuous_intercept: f64, + log_rate: f64, + event_delta: f64, + clock: LagClock, ) -> Result { - let _ = (stationary_t0_variance, added_predictor_variance); - Err(PsychometricError::StationaryInitialLatentVarianceIsNotAsymptoticTimeIndependentVariance) + if !clock.admits_structural_lag() { + return Err(PsychometricError::EventTimeRequired); + } + if !event_delta.is_finite() || event_delta <= 0.0 { + return Err(PsychometricError::NonPositiveInterval); + } + if !continuous_intercept.is_finite() || !log_rate.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + if log_rate == 0.0 { + if continuous_intercept == 0.0 { + return Ok(0.0); + } + return require_finite(continuous_intercept * event_delta); + } + recover_discrete_constant_predictor_effect(continuous_intercept, log_rate, event_delta, clock) } -/// Refuse treating §4.3 / p. 16 stationary `T0VAR` as a -/// finite-interval discrete latent variance. +/// Exact scalar discrete latent mean from Driver Equation 3. /// -/// `trait + −q / (2 a) + (B / a)² v` is the `Δt → ∞` constrained -/// first-occasion variance. `exp(2 a Δt) p + Q_Δt` is a finite -/// event interval and is not that limit. +/// Driver, Oud, and Voelkle (2017, Eq. 3, p. 4; Table 2, p. 12; JSS +/// PDF re-opened 2026-08-19T18:10Z) write +/// `η(t) = exp(A Δt) η(t0) + ∫ exp(A(t−s)) (b + …) ds` plus a +/// stochastic integral of mean zero. With no time-varying covariates +/// the scalar expected-value map is +/// `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`. Table 2 names `μ_0` +/// at the first occasion `T0MEANS` and `κ` `CINT`. Form the CINT +/// increment first, then add the carried `T0MEANS` term. A zero +/// initial mean is exactly the increment. A zero intercept is exactly +/// `exp(a Δt) μ_0`. A zero drift carries `T0MEANS` unchanged and adds +/// `κ Δt`. As `Δt → ∞` with stable `a < 0`, `μ_t → −κ / a`. Binary64 +/// underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and +/// keeps the equilibrium increment. `T0MEANS` is not `μ_t`. `CINT` is +/// not the discrete increment. `CINT` is not `T0MEANS`. This is not a +/// Kalman filter and not ctsem estimation. /// /// # Errors /// -/// Always returns -/// [`PsychometricError::StationaryInitialLatentVarianceIsNotDiscreteVariance`]. -pub fn refuse_stationary_initial_latent_variance_as_discrete_variance( - stationary_t0_variance: f64, - discrete_variance: f64, +/// Propagates [`recover_discrete_continuous_intercept_effect`] and +/// returns [`PsychometricError::InvalidNumericInput`] when the initial +/// mean is non-finite, the carried exponential overflows, or the +/// mapped mean is non-finite. +pub fn recover_discrete_latent_mean( + initial_latent_mean: f64, + log_rate: f64, + continuous_intercept: f64, + event_delta: f64, + clock: LagClock, ) -> Result { - let _ = (stationary_t0_variance, discrete_variance); - Err(PsychometricError::StationaryInitialLatentVarianceIsNotDiscreteVariance) -} + let intercept_effect = recover_discrete_continuous_intercept_effect( + continuous_intercept, + log_rate, + event_delta, + clock, + )?; + if !initial_latent_mean.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + if initial_latent_mean == 0.0 { + return Ok(intercept_effect); + } + let carried = if log_rate == 0.0 { + initial_latent_mean + } else { + let increment_argument = log_rate * event_delta; + if increment_argument == 0.0 { + initial_latent_mean + } else { + let discrete_lag = increment_argument.exp(); + if discrete_lag == 0.0 { + 0.0 + } else if !discrete_lag.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } else { + require_finite(discrete_lag * initial_latent_mean)? + } + } + }; + if intercept_effect == 0.0 { + return Ok(carried); + } + if carried == 0.0 { + return Ok(intercept_effect); + } + require_finite(carried + intercept_effect) +} -/// Exact scalar Eq. 5 of §4.3 / p. 16 stationary `T0VAR`. +/// Exact scalar discrete observed-indicator mean from Driver +/// Equations 3 and 5. /// -/// Driver, Oud, and Voelkle (2017, §4.3, pp. 9–10; Eq. 5, p. 5; -/// Table 2, p. 12; p. 16; §7.2, pp. 20–21; JSS PDF re-opened -/// 2026-08-22T03:20Z from -/// ) -/// constrain `T0VAR` to the model-predicted variance when -/// `stationary` includes `"T0VAR"`. Equation 5 writes -/// `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and -/// `Γ ~ N(τ, Ψ)`. The constrained latent variance is -/// `trait + −q / (2 a) + (B / a)² v`. The scalar composition is -/// `Var(y_0) = λ²(trait + −q / (2 a) + (B / a)² v) + θ + ψ`. Form -/// the stationary latent variance first, then `λ² p + θ + ψ`. A -/// zero loading is exactly `θ + ψ`. A zero trait, a zero diffusion, -/// and a zero TI contribution is exactly `θ + ψ`. `λ² p_0` for -/// free `T0VAR` is not this composition. `λ²(−q / (2 a)) + θ` is -/// not this composition when `TRAITVAR` or `addedTIPREDVAR` is -/// nonzero. Evolving the constrained variance as if it were all -/// state is not this composition when the trait or TI contribution -/// is nonzero. `MANIFESTVAR` is not `Var(y_0)`. The constrained -/// latent variance is not `Var(y_0)`. `TRAITVAR` is latent and is -/// scaled by `λ²`; `MANIFESTTRAITVAR` is not. This is not a Kalman -/// filter, not a matrix `expm`, and not ctsem estimation. +/// Driver, Oud, and Voelkle (2017, Eq. 3, p. 5; Eq. 5, p. 5; +/// Table 2, p. 12; JSS PDF re-opened 2026-08-19T22:10Z) write +/// `η_i(t) = exp(A Δt) η_i(t0) + A^{-1}[exp(A Δt) − I] ξ_i + …` +/// with `ξ_i ~ N(κ, φ_ξ)` (p. 4) and a stochastic integral of +/// mean zero, then `y_i(t) = Γ_i + Λ η_i(t) + ζ_i(t)` with +/// `Γ ~ N(τ, Ψ)` and `ζ ~ N(0, Θ)`. The scalar expected-value +/// composition is `E(y_t) = τ + λ μ_t` with +/// `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`. Form `μ_t` +/// first, then `τ + λ μ_t`. Table 2 names `μ_0` `T0MEANS`, `κ` +/// `CINT`, and `τ` `MANIFESTMEANS`. A zero loading is exactly +/// `τ`. A zero evolved latent mean is exactly `τ`. A zero +/// intercept is exactly `λ μ_t`. The first-occasion map +/// `τ + λ μ_0` is not `E(y_t)`. `MANIFESTMEANS` is not +/// `E(y_t)`. `T0MEANS` is not `E(y_t)`. `μ_t` is not `E(y_t)`. +/// `CINT` is not `E(y_t)`. This is not a Kalman filter and not +/// ctsem estimation. /// /// # Errors /// -/// Propagates [`recover_stationary_initial_latent_variance`] and -/// [`recover_manifest_trait_plus_state_observed_variance`]. -#[allow(clippy::too_many_arguments)] -pub fn recover_stationary_initial_observed_variance( +/// Propagates [`recover_discrete_latent_mean`] and +/// [`recover_manifest_observed_mean`]. +pub fn recover_discrete_observed_mean( loading: f64, - trait_variance: f64, - continuous_diffusion: f64, - time_independent_effect: f64, - predictor_variance: f64, + initial_latent_mean: f64, log_rate: f64, - measurement_error_variance: f64, - manifest_trait_variance: f64, + continuous_intercept: f64, + manifest_mean: f64, + event_delta: f64, clock: LagClock, ) -> Result { - let stationary_latent_variance = recover_stationary_initial_latent_variance( - trait_variance, - continuous_diffusion, - time_independent_effect, - predictor_variance, + let evolved_latent_mean = recover_discrete_latent_mean( + initial_latent_mean, log_rate, + continuous_intercept, + event_delta, clock, )?; - recover_manifest_trait_plus_state_observed_variance( - loading, - stationary_latent_variance, - measurement_error_variance, - manifest_trait_variance, - ) + recover_manifest_observed_mean(loading, evolved_latent_mean, manifest_mean) } -/// Refuse treating §4.3 stationary `T0VAR` as `Var(y_0)`. +/// Refuse treating the first-occasion observed mean as `E(y_t)`. /// -/// `trait + −q / (2 a) + (B / a)² v` is the constrained latent -/// variance. Equation 5 maps `Var(y_0) = λ²` of that variance plus -/// `θ + ψ`. +/// Equation 5 of `T0MEANS` is `τ + λ μ_0`. Equation 5 of the +/// Eq. 3 evolved mean is `τ + λ μ_t`. Those are not the same +/// map. /// /// # Errors /// /// Always returns -/// [`PsychometricError::StationaryInitialLatentVarianceIsNotObservedVariance`]. -pub fn refuse_stationary_initial_latent_variance_as_observed_variance( - stationary_latent_variance: f64, - stationary_observed_variance: f64, +/// [`PsychometricError::InitialObservedMeanIsNotEvolvedObservedMean`]. +pub fn refuse_initial_observed_mean_as_evolved_observed_mean( + initial_observed_mean: f64, + evolved_observed_mean: f64, ) -> Result { - let _ = (stationary_latent_variance, stationary_observed_variance); - Err(PsychometricError::StationaryInitialLatentVarianceIsNotObservedVariance) + let _ = (initial_observed_mean, evolved_observed_mean); + Err(PsychometricError::InitialObservedMeanIsNotEvolvedObservedMean) } -/// Refuse treating `MANIFESTVAR` as Eq. 5 of §4.3 stationary -/// `T0VAR`. +/// Exact scalar contemporaneous impulse from Driver Equation 3. /// -/// Table 2 names `θ` `MANIFESTVAR`. -/// `λ²(trait + −q / (2 a) + (B / a)² v) + θ + ψ` is not `θ` when -/// the loading and constrained variance are nonzero. +/// Driver, Oud, and Voelkle (2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; +/// §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z from +/// ) +/// write the time-dependent predictor as the Dirac impulse +/// `χ_i(t) = Σ_{u ∈ U_i} x_{i,u} δ(t − u)` (Eq. 2). Equation 3's +/// fourth summand is `M Σ x_{i,u} δ(t − u)`. Table 2 names `M` +/// `TDPREDEFFECT`. Section 7.2 calls this "a sudden impulse to the +/// system which then dissipates back to the process mean" and reports +/// `TDPREDEFFECT` as "the initial impact of the predictor on the +/// processes." The scalar contemporaneous jump is `m x`. It is not +/// the second-summand `CINT` map `A^{-1}[e^{A Δt} − I] κ`, not the +/// third-summand time-independent map `A^{-1}[e^{A Δt} − I] B z`, +/// and not Voelkle et al. (2012, Eq. 14) `a_{yx} Δt`. The §7.2 +/// lasting level change sets `CINT` to `TDPREDEFFECT * −DRIFT` +/// (`κ = −a m x`) and is not this jump. The extra near-zero-drift +/// latent process also named in §7.2 is a third specification. A +/// zero effect or zero predictor is exactly zero. This is not a +/// Kalman filter and not ctsem estimation. /// /// # Errors /// -/// Always returns -/// [`PsychometricError::StationaryInitialObservedVarianceIsNotMeasurementError`]. -pub fn refuse_stationary_initial_observed_variance_as_measurement_error( - stationary_observed_variance: f64, - measurement_error_variance: f64, +/// Returns [`PsychometricError::InvalidNumericInput`] when the effect +/// or predictor is non-finite or the product overflows. +pub fn recover_time_dependent_predictor_impulse( + time_dependent_effect: f64, + time_dependent_predictor: f64, ) -> Result { - let _ = (stationary_observed_variance, measurement_error_variance); - Err(PsychometricError::StationaryInitialObservedVarianceIsNotMeasurementError) + if !time_dependent_effect.is_finite() || !time_dependent_predictor.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + if time_dependent_effect == 0.0 || time_dependent_predictor == 0.0 { + return Ok(0.0); + } + require_finite(time_dependent_effect * time_dependent_predictor) } -/// Refuse treating evolved `λ² Var(η_t) + θ` as Eq. 5 of §4.3 -/// stationary `T0VAR`. +/// Exact scalar level-change `CINT` from Driver Section 7.2. /// -/// Evolving the constrained first-occasion variance as if it were -/// all state is not `λ²(trait + −q / (2 a) + (B / a)² v) + θ + ψ` -/// when the trait or TI contribution is nonzero. +/// Driver, Oud, and Voelkle (2017, §7.2, pp. 20–21; Eq. 1–3, pp. 4–5; +/// Table 2, p. 12; JSS PDF re-opened 2026-08-20T19:45Z from +/// ) +/// contrast a sudden Dirac that dissipates back to the process mean +/// with a lasting level change. To generate that lasting change, +/// `CINT` is set to `TDPREDEFFECT * −DRIFT`. The scalar setting is +/// `κ = −a m x`. Form `m x` first, then multiply by `−a`. A zero +/// effect or zero predictor is exactly zero. Stable `a < 0` is +/// required so `−κ / a = m x` is an equilibrium offset. `a ≥ 0` +/// cannot hold a new process mean. `−a m x` is not the +/// contemporaneous jump `m x`. `−a m x` is not a free `CINT`. +/// `−a m x` is not `A^{-1}[e^{A Δt} − I] B z`. The extra +/// near-zero-drift latent process also named in §7.2 is a different +/// specification and is not this `CINT` setting. This is not a +/// Kalman filter and not ctsem estimation. /// /// # Errors /// -/// Always returns -/// [`PsychometricError::EvolvedObservedVarianceIsNotStationaryInitialObservedVariance`]. -pub fn refuse_evolved_observed_variance_as_stationary_initial_observed_variance( - evolved_observed_variance: f64, - stationary_observed_variance: f64, +/// Returns [`PsychometricError::InvalidNumericInput`] when an input +/// is non-finite or a product overflows, and +/// [`PsychometricError::LevelChangeRequiresStableDrift`] when the +/// drift is not strictly negative and the impulse is nonzero. +pub fn recover_level_change_continuous_intercept( + time_dependent_effect: f64, + time_dependent_predictor: f64, + log_rate: f64, ) -> Result { - let _ = (evolved_observed_variance, stationary_observed_variance); - Err(PsychometricError::EvolvedObservedVarianceIsNotStationaryInitialObservedVariance) + if !time_dependent_effect.is_finite() + || !time_dependent_predictor.is_finite() + || !log_rate.is_finite() + { + return Err(PsychometricError::InvalidNumericInput); + } + if time_dependent_effect == 0.0 || time_dependent_predictor == 0.0 { + return Ok(0.0); + } + if log_rate >= 0.0 { + return Err(PsychometricError::LevelChangeRequiresStableDrift); + } + let impulse = require_finite(time_dependent_effect * time_dependent_predictor)?; + require_finite(-log_rate * impulse) } -/// Refuse treating Eq. 5 of `asymDIFFUSION` as Eq. 5 of §4.3 -/// stationary `T0VAR`. +/// Refuse treating the §7.2 level-change `CINT` as the +/// contemporaneous Dirac. /// -/// `λ²(−q / (2 a)) + θ` is the within-subject observed contribution -/// and is not that composition when `TRAITVAR` or `addedTIPREDVAR` -/// is nonzero. +/// `κ = −a m x` is not the jump `m x`. /// /// # Errors /// -/// Always returns -/// [`PsychometricError::StationaryWithinSubjectObservedVarianceIsNotStationaryInitialObservedVariance`]. -pub fn refuse_stationary_within_subject_observed_variance_as_stationary_initial_observed_variance( - within_subject_observed_variance: f64, - stationary_observed_variance: f64, +/// Always returns [`PsychometricError::LevelChangeInterceptIsNotImpulse`]. +pub fn refuse_level_change_intercept_as_impulse( + level_change_intercept: f64, + time_dependent_impulse: f64, ) -> Result { - let _ = ( - within_subject_observed_variance, - stationary_observed_variance, - ); - Err( - PsychometricError::StationaryWithinSubjectObservedVarianceIsNotStationaryInitialObservedVariance, - ) + let _ = (level_change_intercept, time_dependent_impulse); + Err(PsychometricError::LevelChangeInterceptIsNotImpulse) } -/// Refuse treating Eq. 5 of free `T0VAR` as Eq. 5 of §4.3 -/// stationary `T0VAR`. +/// Refuse treating the §7.2 level-change `CINT` as a free `CINT`. /// -/// `λ² p_0 + θ` is the free first-occasion observed variance. -/// `λ²(trait + −q / (2 a) + (B / a)² v) + θ + ψ` is not that map. +/// `κ = −a m x` is not an arbitrary continuous intercept. /// /// # Errors /// /// Always returns -/// [`PsychometricError::InitialObservedVarianceIsNotStationaryInitialObservedVariance`]. -pub fn refuse_initial_observed_variance_as_stationary_initial_observed_variance( - free_initial_observed_variance: f64, - stationary_observed_variance: f64, +/// [`PsychometricError::LevelChangeInterceptIsNotFreeContinuousIntercept`]. +pub fn refuse_level_change_intercept_as_free_continuous_intercept( + level_change_intercept: f64, + continuous_intercept: f64, ) -> Result { - let _ = (free_initial_observed_variance, stationary_observed_variance); - Err(PsychometricError::InitialObservedVarianceIsNotStationaryInitialObservedVariance) + let _ = (level_change_intercept, continuous_intercept); + Err(PsychometricError::LevelChangeInterceptIsNotFreeContinuousIntercept) } -/// Exact scalar lagged covariance of §4.3 / p. 16 stationary -/// `T0VAR`. +/// Refuse treating the §7.2 level-change `CINT` as the Eq. 3 +/// process increment. /// -/// Driver, Oud, and Voelkle (2017, §4.3, pp. 9–10; Eq. 3–4, pp. 4–5; -/// Table 2, p. 12; p. 16; §7.2, pp. 20–21; JSS PDF re-opened -/// 2026-08-22T19:13Z from +/// `κ = −a m x` is not `A^{-1}[e^{A Δt} − I] B z`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::LevelChangeInterceptIsNotProcessIncrement`]. +pub fn refuse_level_change_intercept_as_process_increment( + level_change_intercept: f64, + time_independent_increment: f64, +) -> Result { + let _ = (level_change_intercept, time_independent_increment); + Err(PsychometricError::LevelChangeInterceptIsNotProcessIncrement) +} + +/// Exact scalar discrete increment of the §7.2 level-change `CINT`. +/// +/// Driver, Oud, and Voelkle (2017, §7.2, pp. 20–21; Eq. 3, pp. 4–5; +/// Table 2, p. 12; JSS PDF re-opened 2026-08-20T19:50Z from /// ) -/// constrain `T0VAR` to the model-predicted variance when -/// `stationary` includes `"T0VAR"`. Equation 3 writes -/// `η(t) = exp(A Δt) η(t0) + …`. Equation 4 writes -/// `cov(η_t, η_{t-1}) = A_Δt cov(η_{t-1})`. Page 16 names -/// `asymDIFFUSION` the total within-subject variance `-q / (2 a)`. -/// Section 4.3 (p. 9) adds a stable trait process with `DRIFT` and -/// `DIFFUSION` fixed to zero (`TRAITVAR`). Section 7.2 names -/// `addedTIPREDVAR` the stable between-subject variance accounted -/// for by time-independent predictors; the scalar map is -/// `(B / a)² v`. Trait variance and that TI extra variance are -/// time-invariant between-subject; they do not decay with -/// `e^{a Δt}`. The lagged covariance of the constrained process is -/// `trait + e^{a Δt}(−q / (2 a)) + (B / a)² v`. Form the lagged -/// within-subject covariance first, then include the trait, then -/// include the TI extra variance, then add. A zero trait, a zero -/// diffusion, and a zero TI contribution is exactly zero. A zero -/// diffusion and a zero TI contribution is exactly the trait. -/// As `Δt → ∞` with stable `a < 0` the state term vanishes and the -/// lagged covariance is `trait + (B / a)² v`. As `Δt → 0+` the -/// lagged covariance approaches contemporaneous `T0VAR`. Those -/// limits are not this finite-lag map. Evolving the constrained -/// total as if it were all state is not this map. -/// `trait + e^{a Δt} p` is not this map when `addedTIPREDVAR` is -/// nonzero. Contemporaneous `T0VAR` is not this map. `a ≥ 0` cannot -/// hold a finite process variance when the diffusion or the TI -/// contribution is nonzero and fails closed. Trait-only covariance -/// does not require a stable drift. The interval must be event time -/// and strictly positive. This is not a Kalman filter, not a matrix -/// `expm`, and not ctsem estimation. +/// set `CINT` to `TDPREDEFFECT * −DRIFT` so a sudden impulse holds a +/// new process mean. Equation 3 maps that intercept through +/// `A^{-1}[e^{A Δt} − I] κ`. With `κ = −a m x` the scalar increment +/// is `(e^{a Δt} − 1)/a · (−a m x) = (1 − e^{a Δt}) m x`. Form the +/// level-change `CINT` first, then the discrete intercept map. +/// Underflow of `e^{a Δt}` to `+0` keeps the equilibrium offset +/// `m x`. A zero effect or zero predictor is exactly zero. Stable +/// `a < 0` is required. `(1 − e^{a Δt}) m x` is not the +/// contemporaneous jump `m x`. `(1 − e^{a Δt}) m x` is not `κ`. +/// `(1 − e^{a Δt}) m x` is not `A^{-1}[e^{A Δt} − I] B z`. The extra +/// near-zero-drift latent process also named in §7.2 is a different +/// specification. This is not a Kalman filter and not ctsem +/// estimation. /// /// # Errors /// -/// Propagates [`recover_stationary_initial_latent_variance`] path -/// refusals and [`recover_trait_plus_state_lagged_covariance`]. -/// Returns [`PsychometricError::EventTimeRequired`] for any -/// non-event clock, -/// [`PsychometricError::NonPositiveInterval`] when `event_delta` is -/// not strictly positive, -/// [`PsychometricError::StationaryVarianceRequiresStableDrift`] -/// when the diffusion is nonzero and the drift is not strictly -/// negative, -/// [`PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift`] -/// when the TI contribution is nonzero and the drift is not -/// strictly negative, and -/// [`PsychometricError::InvalidNumericInput`] when an input is -/// non-finite, a variance is negative, or a product or sum -/// overflows. -#[allow(clippy::too_many_arguments)] -pub fn recover_stationary_lagged_latent_covariance( - trait_variance: f64, - continuous_diffusion: f64, - time_independent_effect: f64, - predictor_variance: f64, +/// Propagates [`recover_level_change_continuous_intercept`] and +/// [`recover_discrete_continuous_intercept_effect`]. +pub fn recover_level_change_discrete_increment( + time_dependent_effect: f64, + time_dependent_predictor: f64, log_rate: f64, event_delta: f64, clock: LagClock, ) -> Result { - if !clock.admits_structural_lag() { - return Err(PsychometricError::EventTimeRequired); - } - let state = if continuous_diffusion == 0.0 { - 0.0 - } else { - recover_stationary_latent_variance(continuous_diffusion, log_rate, clock)? - }; - let trait_plus_state = recover_trait_plus_state_lagged_covariance( - trait_variance, - state, - log_rate, - event_delta, - clock, - )?; - let added = recover_asymptotic_time_independent_predictor_variance( - time_independent_effect, - predictor_variance, + let intercept = recover_level_change_continuous_intercept( + time_dependent_effect, + time_dependent_predictor, log_rate, - clock, )?; - require_finite(trait_plus_state + added) + recover_discrete_continuous_intercept_effect(intercept, log_rate, event_delta, clock) } -/// Refuse treating lagged §4.3 stationary `T0VAR` as contemporaneous -/// stationary `T0VAR`. +/// Refuse treating the §7.2 level-change CINT increment as the +/// contemporaneous Dirac. /// -/// `trait + e^{a Δt}(−q / (2 a)) + (B / a)² v` is the lagged -/// covariance at a strictly positive event interval. -/// `trait + −q / (2 a) + (B / a)² v` is the first-occasion -/// variance. Those are not the same map. +/// `(1 − e^{a Δt}) m x` is not the jump `m x`. /// /// # Errors /// -/// Always returns -/// [`PsychometricError::StationaryLaggedLatentCovarianceIsNotStationaryInitialLatentVariance`]. -pub fn refuse_stationary_lagged_latent_covariance_as_stationary_initial_latent_variance( - lagged_covariance: f64, - contemporaneous_variance: f64, +/// Always returns [`PsychometricError::LevelChangeIncrementIsNotImpulse`]. +pub fn refuse_level_change_increment_as_impulse( + level_change_increment: f64, + time_dependent_impulse: f64, ) -> Result { - let _ = (lagged_covariance, contemporaneous_variance); - Err(PsychometricError::StationaryLaggedLatentCovarianceIsNotStationaryInitialLatentVariance) + let _ = (level_change_increment, time_dependent_impulse); + Err(PsychometricError::LevelChangeIncrementIsNotImpulse) } -/// Refuse treating lagged §4.3 stationary `T0VAR` as decayed total -/// stationary variance. +/// Refuse treating the §7.2 level-change CINT increment as `CINT`. /// -/// Evolving `trait + −q / (2 a) + (B / a)² v` as if it were all -/// state yields `e^{a Δt}` of that total. Trait variance and -/// `addedTIPREDVAR` do not decay. +/// `(1 − e^{a Δt}) m x` is not `κ = −a m x`. /// /// # Errors /// -/// Always returns -/// [`PsychometricError::StationaryLaggedLatentCovarianceIsNotDecayedStationaryVariance`]. -pub fn refuse_stationary_lagged_latent_covariance_as_decayed_stationary_variance( - lagged_covariance: f64, - decayed_total: f64, +/// Always returns [`PsychometricError::LevelChangeIncrementIsNotIntercept`]. +pub fn refuse_level_change_increment_as_intercept( + level_change_increment: f64, + level_change_intercept: f64, ) -> Result { - let _ = (lagged_covariance, decayed_total); - Err(PsychometricError::StationaryLaggedLatentCovarianceIsNotDecayedStationaryVariance) + let _ = (level_change_increment, level_change_intercept); + Err(PsychometricError::LevelChangeIncrementIsNotIntercept) } -/// Refuse treating §4.3 trait-plus-state lagged covariance as lagged -/// stationary `T0VAR`. +/// Refuse treating the §7.2 level-change CINT increment as the Eq. 3 +/// process increment. /// -/// `trait + e^{a Δt} p` omits `addedTIPREDVAR`. The constrained -/// lagged covariance includes that TI extra variance. +/// `(1 − e^{a Δt}) m x` is not `A^{-1}[e^{A Δt} − I] B z`. /// /// # Errors /// /// Always returns -/// [`PsychometricError::TraitPlusStateLaggedCovarianceIsNotStationaryLaggedLatentCovariance`]. -pub fn refuse_trait_plus_state_lagged_covariance_as_stationary_lagged_latent_covariance( - trait_plus_state_lagged: f64, - stationary_lagged: f64, +/// [`PsychometricError::LevelChangeIncrementIsNotProcessIncrement`]. +pub fn refuse_level_change_increment_as_process_increment( + level_change_increment: f64, + time_independent_increment: f64, ) -> Result { - let _ = (trait_plus_state_lagged, stationary_lagged); - Err(PsychometricError::TraitPlusStateLaggedCovarianceIsNotStationaryLaggedLatentCovariance) + let _ = (level_change_increment, time_independent_increment); + Err(PsychometricError::LevelChangeIncrementIsNotProcessIncrement) } -/// Exact scalar Eq. 5 of lagged §4.3 / p. 16 stationary `T0VAR`. +/// Exact scalar contribution of the §7.2 extra near-zero-drift process. /// -/// Driver, Oud, and Voelkle (2017, §4.3, pp. 9–10; Eq. 5, p. 5; -/// Eq. 3–4, pp. 4–5; Table 2, p. 12; p. 16; §7.2, pp. 20–21; JSS PDF -/// re-opened 2026-08-22T19:13Z from +/// Driver, Oud, and Voelkle (2017, §7.2, pp. 22–23; Eq. 1–3, pp. 4–5; +/// Table 2, p. 12; JSS PDF re-opened 2026-08-20T23:10Z from /// ) -/// write `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and -/// `Γ ~ N(τ, Ψ)`. Independent measurement error does not enter -/// `cov(y_t, y_{t-1})`. The lagged latent covariance is -/// `trait + e^{a Δt}(−q / (2 a)) + (B / a)² v`. The scalar -/// composition is -/// `cov(y_t, y_{t-1}) = λ²(trait + e^{a Δt}(−q / (2 a)) + (B / a)² v) + ψ`. -/// Form the lagged latent covariance first, then `λ² c + ψ`. A zero -/// loading is exactly `ψ`. A zero trait, a zero diffusion, and a -/// zero TI contribution is exactly `ψ`. `MANIFESTVAR` `θ` is not -/// this composition. Contemporaneous `Var(y_0)` includes `θ` and is -/// not this composition. The lagged latent covariance is not this -/// observed covariance. Evolving the constrained total as if it -/// were all state is not this composition when the trait or TI -/// contribution is nonzero. `TRAITVAR` is latent and is scaled by -/// `λ²`; `MANIFESTTRAITVAR` is not. This is not a Kalman filter, -/// not a matrix `expm`, and not ctsem estimation. +/// specify a lasting level change by an extra latent process, not by +/// rewriting `CINT`. `T0MEANS`, `CINT`, `T0VAR`, `DIFFUSION`, and +/// `TRAITVAR` of that process are fixed to 0. `TDPREDEFFECT` on it is +/// fixed to 1 to identify the effect. Its `DRIFT` diagonal is very +/// close to 0 (printed example `−0.000001`; precisely 0 causes +/// computational problems). The original process is driven by the +/// `DRIFT` coupling `a_{ηξ}`. After a unit identification impulse the +/// extra state is `x e^{ε t}` and the scalar contribution to the +/// original process is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`. +/// Form `a_{ηξ} x` first. When `ε = a` the contribution is +/// `a_{ηξ} x Δt e^{a Δt}`. A zero coupling or zero predictor is +/// exactly zero. `ε ≥ 0` cannot hold a lasting extra state and fails +/// closed. That contribution is not `κ = −a m x`, not +/// `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. This +/// is not a Kalman filter, not a matrix `expm`, and not ctsem +/// estimation. /// /// # Errors /// -/// Propagates [`recover_stationary_lagged_latent_covariance`] and -/// [`recover_manifest_lagged_observed_covariance`]. -#[allow(clippy::too_many_arguments)] -pub fn recover_stationary_lagged_observed_covariance( - loading: f64, - trait_variance: f64, - continuous_diffusion: f64, - time_independent_effect: f64, - predictor_variance: f64, - log_rate: f64, +/// Returns [`PsychometricError::EventTimeRequired`] for any non-event +/// clock, [`PsychometricError::NonPositiveInterval`] when the interval +/// is not strictly positive, +/// [`PsychometricError::LevelChangeExtraProcessRequiresNegativeDrift`] +/// when the extra drift is not strictly negative and the contribution +/// is nonzero, and [`PsychometricError::InvalidNumericInput`] when an +/// input is non-finite or a product, exponential, or quotient +/// overflows. +pub fn recover_level_change_extra_process_contribution( + original_from_extra_drift: f64, + time_dependent_predictor: f64, + original_log_rate: f64, + extra_log_rate: f64, event_delta: f64, - manifest_trait_variance: f64, clock: LagClock, ) -> Result { - let lagged_latent = recover_stationary_lagged_latent_covariance( - trait_variance, - continuous_diffusion, - time_independent_effect, - predictor_variance, - log_rate, - event_delta, - clock, - )?; - recover_manifest_lagged_observed_covariance(loading, lagged_latent, manifest_trait_variance) + if !clock.admits_structural_lag() { + return Err(PsychometricError::EventTimeRequired); + } + if !event_delta.is_finite() || event_delta <= 0.0 { + return Err(PsychometricError::NonPositiveInterval); + } + if !original_from_extra_drift.is_finite() + || !time_dependent_predictor.is_finite() + || !original_log_rate.is_finite() + || !extra_log_rate.is_finite() + { + return Err(PsychometricError::InvalidNumericInput); + } + if original_from_extra_drift == 0.0 || time_dependent_predictor == 0.0 { + return Ok(0.0); + } + if extra_log_rate >= 0.0 { + return Err(PsychometricError::LevelChangeExtraProcessRequiresNegativeDrift); + } + let coupling = require_finite(original_from_extra_drift * time_dependent_predictor)?; + let extra_argument = extra_log_rate * event_delta; + let extra_lag = if extra_argument == 0.0 { + 1.0 + } else { + extra_argument.exp() + }; + let original_argument = original_log_rate * event_delta; + let original_lag = if original_log_rate == 0.0 || original_argument == 0.0 { + 1.0 + } else { + let lag = original_argument.exp(); + if !lag.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + lag + }; + let rate_gap = extra_log_rate - original_log_rate; + let gap_argument = rate_gap * event_delta; + if gap_argument == 0.0 { + return require_finite(coupling * event_delta * original_lag); + } + let increment = gap_argument.exp_m1(); + if increment.is_finite() { + if original_lag == 0.0 { + return require_finite(coupling * extra_lag / rate_gap); + } + return require_finite(coupling * original_lag * (increment / rate_gap)); + } + require_finite(coupling * (extra_lag - original_lag) / rate_gap) } -/// Refuse treating lagged §4.3 stationary `T0VAR` as lagged observed -/// covariance. +/// Refuse treating the §7.2 extra-process contribution as the +/// contemporaneous Dirac. /// -/// `trait + e^{a Δt}(−q / (2 a)) + (B / a)² v` is the lagged latent -/// covariance. Equation 5 maps `cov(y_t, y_{t-1}) = λ²` of that -/// covariance plus `ψ`. +/// `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` is not the jump `m x`. /// /// # Errors /// -/// Always returns -/// [`PsychometricError::StationaryLaggedLatentCovarianceIsNotObservedCovariance`]. -pub fn refuse_stationary_lagged_latent_covariance_as_observed_covariance( - lagged_latent_covariance: f64, - lagged_observed_covariance: f64, +/// Always returns [`PsychometricError::LevelChangeExtraProcessIsNotImpulse`]. +pub fn refuse_level_change_extra_process_as_impulse( + extra_process_contribution: f64, + time_dependent_impulse: f64, ) -> Result { - let _ = (lagged_latent_covariance, lagged_observed_covariance); - Err(PsychometricError::StationaryLaggedLatentCovarianceIsNotObservedCovariance) + let _ = (extra_process_contribution, time_dependent_impulse); + Err(PsychometricError::LevelChangeExtraProcessIsNotImpulse) } -/// Refuse treating `MANIFESTVAR` as Eq. 5 of lagged §4.3 stationary -/// `T0VAR`. +/// Refuse treating the §7.2 extra-process contribution as the +/// level-change `CINT`. /// -/// Table 2 names `θ` `MANIFESTVAR`. Independent `ε_t` does not -/// enter `cov(y_t, y_{t-1})`. +/// `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` is not `κ = −a m x`. /// /// # Errors /// /// Always returns -/// [`PsychometricError::MeasurementErrorIsNotStationaryLaggedObservedCovariance`]. -pub fn refuse_measurement_error_as_stationary_lagged_observed_covariance( - measurement_error_variance: f64, - lagged_observed_covariance: f64, +/// [`PsychometricError::LevelChangeExtraProcessIsNotIntercept`]. +pub fn refuse_level_change_extra_process_as_intercept( + extra_process_contribution: f64, + level_change_intercept: f64, ) -> Result { - let _ = (measurement_error_variance, lagged_observed_covariance); - Err(PsychometricError::MeasurementErrorIsNotStationaryLaggedObservedCovariance) + let _ = (extra_process_contribution, level_change_intercept); + Err(PsychometricError::LevelChangeExtraProcessIsNotIntercept) } -/// Refuse treating Eq. 5 of contemporaneous §4.3 stationary `T0VAR` -/// as lagged stationary observed covariance. +/// Refuse treating the §7.2 extra-process contribution as the Eq. 3 +/// level-change increment. /// -/// `λ²(trait + −q / (2 a) + (B / a)² v) + θ + ψ` is -/// contemporaneous and includes `θ`. -/// `λ²(trait + e^{a Δt}(−q / (2 a)) + (B / a)² v) + ψ` is not that -/// map. +/// `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` is not +/// `(1 − e^{a Δt}) m x`. /// /// # Errors /// /// Always returns -/// [`PsychometricError::StationaryInitialObservedVarianceIsNotStationaryLaggedObservedCovariance`]. -pub fn refuse_stationary_initial_observed_variance_as_stationary_lagged_observed_covariance( - contemporaneous_observed_variance: f64, - lagged_observed_covariance: f64, +/// [`PsychometricError::LevelChangeExtraProcessIsNotIncrement`]. +pub fn refuse_level_change_extra_process_as_increment( + extra_process_contribution: f64, + level_change_increment: f64, ) -> Result { - let _ = ( - contemporaneous_observed_variance, - lagged_observed_covariance, - ); - Err(PsychometricError::StationaryInitialObservedVarianceIsNotStationaryLaggedObservedCovariance) + let _ = (extra_process_contribution, level_change_increment); + Err(PsychometricError::LevelChangeExtraProcessIsNotIncrement) } -/// Exact scalar later-occasion variance of §4.3 / p. 16 stationary -/// `T0VAR`. +/// Exact scalar evolved latent mean plus a §7.2 extra-process contribution. /// -/// Driver, Oud, and Voelkle (2017, §4.3, pp. 9–10; Eq. 3–4, pp. 4–5; -/// Table 2, p. 12; p. 16; §7.2, pp. 20–21; JSS PDF re-opened -/// 2026-08-22T23:05Z from +/// Driver, Oud, and Voelkle (2017, Eq. 3, p. 5; §7.2, pp. 22–23; JSS +/// PDF re-opened 2026-08-21T06:12Z from /// ) -/// constrain the first-occasion variance according to the -/// model-predicted variances across all time points when `stationary` -/// includes `"T0VAR"`. Equation 3 writes `η(t) = exp(A Δt) η(t0) + … +` -/// the stochastic integral. Equation 4 writes that the integral -/// exhibits covariance `Q_Δt`. The law of total variance on the -/// within-subject state is `e^{2 a Δt}(−q / (2 a)) + Q_Δt`. Trait -/// variance and `addedTIPREDVAR` are time-invariant between-subject; -/// they do not enter that process-noise integral. The later-occasion -/// composition is -/// `trait + e^{2 a Δt}(−q / (2 a)) + Q_Δt + (B / a)² v`. Form the -/// evolved within-subject variance first, then include the trait, -/// then include the TI extra variance, then add. A zero trait, a -/// zero diffusion, and a zero TI contribution is exactly zero. A -/// zero diffusion and a zero TI contribution is exactly the trait. -/// Under stationarity that composition equals contemporaneous -/// `T0VAR`. Evolving the constrained total as if it were all state -/// (`e^{2 a Δt} p + Q_Δt`) is not this map. The lagged covariance -/// `trait + e^{a Δt}(−q / (2 a)) + (B / a)² v` omits `Q_Δt` and is -/// not this map. `Q_Δt` is not this map. `a ≥ 0` cannot hold a -/// finite process variance when the diffusion or the TI contribution -/// is nonzero and fails closed. Trait-only variance does not require -/// a stable drift. The interval must be event time and strictly -/// positive. This is not a Kalman filter, not a matrix `expm`, and -/// not ctsem estimation. +/// write the first two summands as the carried `T0MEANS` and `CINT` +/// increment. Section 7.2 then drives the original process by the +/// extra near-zero-drift latent process through the `DRIFT` coupling +/// `a_{ηξ}`. Form `μ_t` first, then add the extra-process +/// contribution. A zero contribution is exactly `μ_t`. A zero evolved +/// mean is exactly the contribution. The first-occasion map +/// `μ_0 + contribution` is not this composition when the process has +/// already evolved. The contemporaneous Dirac `μ_t + m x` is not this +/// composition. The printed specification puts `TDPREDEFFECT` on the +/// extra process, not on the original process. This is not a Kalman +/// filter and not ctsem estimation. /// /// # Errors /// -/// Propagates [`recover_stationary_initial_latent_variance`] path -/// refusals and [`recover_discrete_latent_variance`]. Returns -/// [`PsychometricError::EventTimeRequired`] for any non-event clock, -/// [`PsychometricError::NonPositiveInterval`] when `event_delta` is -/// not strictly positive, -/// [`PsychometricError::StationaryVarianceRequiresStableDrift`] -/// when the diffusion is nonzero and the drift is not strictly -/// negative, -/// [`PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift`] -/// when the TI contribution is nonzero and the drift is not -/// strictly negative, and -/// [`PsychometricError::InvalidNumericInput`] when an input is -/// non-finite, a variance is negative, or a product or sum -/// overflows. +/// Propagates [`recover_discrete_latent_mean`] and +/// [`recover_level_change_extra_process_contribution`], and returns +/// [`PsychometricError::InvalidNumericInput`] when the sum overflows. #[allow(clippy::too_many_arguments)] -pub fn recover_stationary_later_latent_variance( - trait_variance: f64, - continuous_diffusion: f64, - time_independent_effect: f64, - predictor_variance: f64, - log_rate: f64, +pub fn recover_discrete_latent_mean_with_extra_process( + initial_latent_mean: f64, + original_log_rate: f64, + continuous_intercept: f64, + original_from_extra_drift: f64, + time_dependent_predictor: f64, + extra_log_rate: f64, event_delta: f64, clock: LagClock, ) -> Result { - if !clock.admits_structural_lag() { - return Err(PsychometricError::EventTimeRequired); - } - let state = if continuous_diffusion == 0.0 { - 0.0 - } else { - recover_stationary_latent_variance(continuous_diffusion, log_rate, clock)? - }; - let evolved_state = recover_discrete_latent_variance( - state, - continuous_diffusion, - log_rate, + let evolved_latent_mean = recover_discrete_latent_mean( + initial_latent_mean, + original_log_rate, + continuous_intercept, event_delta, clock, )?; - let trait_plus_evolved = - recover_trait_plus_state_latent_variance(trait_variance, evolved_state)?; - let added = recover_asymptotic_time_independent_predictor_variance( - time_independent_effect, - predictor_variance, - log_rate, + let contribution = recover_level_change_extra_process_contribution( + original_from_extra_drift, + time_dependent_predictor, + original_log_rate, + extra_log_rate, + event_delta, clock, )?; - require_finite(trait_plus_evolved + added) + if contribution == 0.0 { + return Ok(evolved_latent_mean); + } + if evolved_latent_mean == 0.0 { + return Ok(contribution); + } + require_finite(evolved_latent_mean + contribution) } -/// Refuse treating later-occasion §4.3 stationary `T0VAR` as lagged -/// stationary covariance. +/// Exact scalar observed mean of a §7.2 extra-process contribution. /// -/// `trait + e^{2 a Δt}(−q / (2 a)) + Q_Δt + (B / a)² v` is the -/// unconditional variance at a later event occasion. The lagged -/// covariance omits `Q_Δt` and uses `e^{a Δt}` of the state. +/// Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Eq. 1–3, pp. 4–5; +/// §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:12Z from +/// ) +/// write `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and +/// `Γ ~ N(τ, Ψ)`. The expected intercept is `τ`. Section 7.2's +/// printed extra process has `LAMBDA` 0: it is not an observed +/// indicator. Original indicators load on the original process after +/// the `DRIFT` coupling. The latent process at `t` after that +/// contribution is `μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`. +/// The scalar composition is +/// `E(y_t) = τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))`. +/// Form the evolved-plus-contribution latent mean first, then +/// `τ + λ` of that mean. Table 2 names `τ` `MANIFESTMEANS`. A zero +/// loading is exactly `τ`. A zero evolved-plus-contribution latent +/// mean is exactly `τ`. A zero intercept is exactly `λ` of that +/// latent mean. The evolved observed mean `τ + λ μ_t` is not this +/// composition when the contribution is nonzero. The contemporaneous +/// map `τ + λ(μ_t + m x)` is not this composition. `MANIFESTMEANS` is +/// not `E(y_t)`. The extra-process contribution is not `E(y_t)`. The +/// evolved-plus-contribution latent mean is not `E(y_t)`. The extra +/// process itself is not an observed indicator. This is not a Kalman +/// filter and not ctsem estimation. /// /// # Errors /// -/// Always returns -/// [`PsychometricError::StationaryLaterLatentVarianceIsNotLaggedCovariance`]. -pub fn refuse_stationary_later_latent_variance_as_lagged_covariance( - later_variance: f64, - lagged_covariance: f64, -) -> Result { - let _ = (later_variance, lagged_covariance); - Err(PsychometricError::StationaryLaterLatentVarianceIsNotLaggedCovariance) -} - -/// Refuse treating later-occasion §4.3 stationary `T0VAR` as the free -/// discrete evolution of the constrained total. -/// -/// Evolving `trait + −q / (2 a) + (B / a)² v` as if it were all -/// state yields `e^{2 a Δt}` of that total plus `Q_Δt`. Trait -/// variance and `addedTIPREDVAR` do not enter `Q_Δt`. -/// -/// # Errors -/// -/// Always returns -/// [`PsychometricError::StationaryLaterLatentVarianceIsNotDiscreteVariance`]. -pub fn refuse_stationary_later_latent_variance_as_discrete_variance( - later_variance: f64, - free_discrete_variance: f64, -) -> Result { - let _ = (later_variance, free_discrete_variance); - Err(PsychometricError::StationaryLaterLatentVarianceIsNotDiscreteVariance) -} - -/// Refuse treating later-occasion §4.3 stationary `T0VAR` as -/// finite-interval process noise. -/// -/// `Q_Δt` is the covariance of the stochastic integral. The -/// later-occasion composition includes the trait, the evolved state, -/// and `addedTIPREDVAR`. -/// -/// # Errors -/// -/// Always returns -/// [`PsychometricError::StationaryLaterLatentVarianceIsNotProcessNoise`]. -pub fn refuse_stationary_later_latent_variance_as_process_noise( - later_variance: f64, - process_noise: f64, -) -> Result { - let _ = (later_variance, process_noise); - Err(PsychometricError::StationaryLaterLatentVarianceIsNotProcessNoise) -} - -/// Exact scalar Eq. 5 of later-occasion §4.3 / p. 16 stationary -/// `T0VAR`. -/// -/// Driver, Oud, and Voelkle (2017, §4.3, pp. 9–10; Eq. 5, p. 5; -/// Eq. 3–4, pp. 4–5; Table 2, p. 12; p. 16; §7.2, pp. 20–21; JSS PDF -/// re-opened 2026-08-22T23:05Z from -/// ) -/// write `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and -/// `Γ ~ N(τ, Ψ)`. The later-occasion latent variance is -/// `trait + e^{2 a Δt}(−q / (2 a)) + Q_Δt + (B / a)² v`. The scalar -/// composition is -/// `Var(y_t) = λ²(trait + e^{2 a Δt}(−q / (2 a)) + Q_Δt + (B / a)² v) + θ + ψ`. -/// Form the later-occasion latent variance first, then `λ² p + θ + ψ`. -/// A zero loading is exactly `θ + ψ`. A zero trait, a zero diffusion, -/// and a zero TI contribution is exactly `θ + ψ`. Under stationarity -/// that composition equals contemporaneous `Var(y_0)`. The lagged -/// observed covariance omits `Q_Δt` and `θ`. `MANIFESTVAR` `θ` is -/// not this composition. The later-occasion latent variance is not -/// this observed variance. `TRAITVAR` is latent and is scaled by -/// `λ²`; `MANIFESTTRAITVAR` is not. This is not a Kalman filter, not -/// a matrix `expm`, and not ctsem estimation. -/// -/// # Errors -/// -/// Propagates [`recover_stationary_later_latent_variance`] and -/// [`recover_manifest_trait_plus_state_observed_variance`]. +/// Propagates [`recover_discrete_latent_mean_with_extra_process`] and +/// [`recover_manifest_observed_mean`]. #[allow(clippy::too_many_arguments)] -pub fn recover_stationary_later_observed_variance( +pub fn recover_discrete_observed_mean_with_extra_process( loading: f64, - trait_variance: f64, - continuous_diffusion: f64, - time_independent_effect: f64, - predictor_variance: f64, - log_rate: f64, + initial_latent_mean: f64, + original_log_rate: f64, + continuous_intercept: f64, + original_from_extra_drift: f64, + time_dependent_predictor: f64, + extra_log_rate: f64, + manifest_mean: f64, event_delta: f64, - measurement_error_variance: f64, - manifest_trait_variance: f64, clock: LagClock, ) -> Result { - let later_latent = recover_stationary_later_latent_variance( - trait_variance, - continuous_diffusion, - time_independent_effect, - predictor_variance, - log_rate, + let extra_latent_mean = recover_discrete_latent_mean_with_extra_process( + initial_latent_mean, + original_log_rate, + continuous_intercept, + original_from_extra_drift, + time_dependent_predictor, + extra_log_rate, event_delta, clock, )?; - recover_manifest_trait_plus_state_observed_variance( - loading, - later_latent, - measurement_error_variance, - manifest_trait_variance, - ) + recover_manifest_observed_mean(loading, extra_latent_mean, manifest_mean) } -/// Refuse treating later-occasion §4.3 stationary `T0VAR` as -/// later-occasion observed variance. +/// Refuse treating the evolved observed mean as the extra-process +/// observed mean. /// -/// `trait + e^{2 a Δt}(−q / (2 a)) + Q_Δt + (B / a)² v` is the -/// later-occasion latent variance. Equation 5 maps `Var(y_t) = λ²` -/// of that variance plus `θ + ψ`. +/// Equation 5 of the Eq. 3 evolved mean is `τ + λ μ_t`. Equation 5 +/// of the §7.2 extra-process contribution is +/// `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))`. Those +/// are not the same map. /// /// # Errors /// /// Always returns -/// [`PsychometricError::StationaryLaterLatentVarianceIsNotObservedVariance`]. -pub fn refuse_stationary_later_latent_variance_as_observed_variance( - later_latent_variance: f64, - later_observed_variance: f64, +/// [`PsychometricError::EvolvedObservedMeanIsNotExtraProcessObservedMean`]. +pub fn refuse_evolved_observed_mean_as_extra_process_observed_mean( + evolved_observed_mean: f64, + extra_process_observed_mean: f64, ) -> Result { - let _ = (later_latent_variance, later_observed_variance); - Err(PsychometricError::StationaryLaterLatentVarianceIsNotObservedVariance) + let _ = (evolved_observed_mean, extra_process_observed_mean); + Err(PsychometricError::EvolvedObservedMeanIsNotExtraProcessObservedMean) } -/// Refuse treating `MANIFESTVAR` as Eq. 5 of later-occasion §4.3 -/// stationary `T0VAR`. +/// Refuse treating the contemporaneous-impulse observed mean as the +/// extra-process observed mean. /// -/// Table 2 names `θ` `MANIFESTVAR`. `θ` is not -/// `λ²(trait + e^{2 a Δt} p + Q_Δt + (B / a)² v) + θ + ψ`. +/// Equation 5 of the contemporaneous Dirac is `τ + λ(μ_t + m x)`. +/// Equation 5 of the §7.2 extra-process contribution is +/// `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))`. Those +/// are not the same map. The printed specification puts +/// `TDPREDEFFECT` on the extra process, not on the original process. /// /// # Errors /// /// Always returns -/// [`PsychometricError::MeasurementErrorIsNotStationaryLaterObservedVariance`]. -pub fn refuse_measurement_error_as_stationary_later_observed_variance( - measurement_error_variance: f64, - later_observed_variance: f64, +/// [`PsychometricError::ImpulseObservedMeanIsNotExtraProcessObservedMean`]. +pub fn refuse_impulse_observed_mean_as_extra_process_observed_mean( + impulse_observed_mean: f64, + extra_process_observed_mean: f64, ) -> Result { - let _ = (measurement_error_variance, later_observed_variance); - Err(PsychometricError::MeasurementErrorIsNotStationaryLaterObservedVariance) + let _ = (impulse_observed_mean, extra_process_observed_mean); + Err(PsychometricError::ImpulseObservedMeanIsNotExtraProcessObservedMean) } -/// Refuse treating Eq. 5 of lagged §4.3 stationary `T0VAR` as -/// later-occasion stationary observed variance. +/// Refuse treating the §7.2 extra-process contribution as `E(y_t)`. /// -/// `λ²(trait + e^{a Δt}(−q / (2 a)) + (B / a)² v) + ψ` omits `Q_Δt` -/// and `θ`. -/// `λ²(trait + e^{2 a Δt}(−q / (2 a)) + Q_Δt + (B / a)² v) + θ + ψ` -/// is not that map. +/// The contribution is not `τ + λ` of the evolved-plus-contribution +/// latent mean. The extra process has `LAMBDA` 0 in the printed +/// specification. /// /// # Errors /// /// Always returns -/// [`PsychometricError::StationaryLaggedObservedCovarianceIsNotStationaryLaterObservedVariance`]. -pub fn refuse_stationary_lagged_observed_covariance_as_stationary_later_observed_variance( - lagged_observed_covariance: f64, - later_observed_variance: f64, +/// [`PsychometricError::ExtraProcessContributionIsNotObservedMean`]. +pub fn refuse_extra_process_contribution_as_observed_mean( + extra_process_contribution: f64, + extra_process_observed_mean: f64, ) -> Result { - let _ = (lagged_observed_covariance, later_observed_variance); - Err(PsychometricError::StationaryLaggedObservedCovarianceIsNotStationaryLaterObservedVariance) + let _ = (extra_process_contribution, extra_process_observed_mean); + Err(PsychometricError::ExtraProcessContributionIsNotObservedMean) } -/// Exact scalar observed mean of a time-independent predictor. +/// Refuse treating the evolved-plus-contribution latent mean as +/// `E(y_t)`. /// -/// Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Eq. 3, p. 5; Table 2, -/// p. 12; JSS PDF re-opened 2026-08-20T12:12Z from -/// ) -/// write `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and -/// `Γ ~ N(τ, Ψ)`. Equation 3 (p. 5) writes the time-independent -/// predictor as the printed addend `A^{-1}[e^{A(t−t0)} − I] B z_i` -/// after the `T0MEANS` carry and the `CINT` increment. Table 2 names -/// `B` `TIPREDEFFECT`. The expected intercept is `τ`. The latent -/// process at `t` after that increment is -/// `μ_t + A^{-1}[e^{A Δt} − I] B z`. The scalar composition is -/// `E(y_t) = τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. Form the -/// evolved-plus-increment latent mean first, then `τ + λ` of that -/// mean. A zero loading is exactly `τ`. A zero evolved-plus-increment -/// latent mean is exactly `τ`. A zero intercept is exactly -/// `λ(μ_t + increment)`. The evolved observed mean `τ + λ μ_t` is -/// not this composition when the increment is nonzero. The -/// contemporaneous map `τ + λ(μ_t + m x)` is not this composition. -/// The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not this -/// composition when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The -/// evolved-plus-increment latent mean is not `E(y_t)`. `TIPREDEFFECT` -/// is `B`, not that observed mean. This is not a Kalman filter and -/// not ctsem estimation. +/// Equation 5 maps `E(y_t) = τ + λ` of that mean. The latent mean is +/// not the observed mean. /// /// # Errors /// -/// Propagates -/// [`recover_discrete_latent_mean_with_time_independent_predictor`] -/// and [`recover_manifest_observed_mean`]. -#[allow(clippy::too_many_arguments)] -pub fn recover_discrete_observed_mean_with_time_independent_predictor( - loading: f64, - initial_latent_mean: f64, - log_rate: f64, - continuous_intercept: f64, - time_independent_effect: f64, - time_independent_predictor: f64, - manifest_mean: f64, - event_delta: f64, - clock: LagClock, +/// Always returns +/// [`PsychometricError::ExtraProcessLatentMeanIsNotObservedMean`]. +pub fn refuse_extra_process_latent_mean_as_observed_mean( + extra_process_latent_mean: f64, + extra_process_observed_mean: f64, ) -> Result { - let composed_latent_mean = recover_discrete_latent_mean_with_time_independent_predictor( - initial_latent_mean, - log_rate, - continuous_intercept, - time_independent_effect, - time_independent_predictor, - event_delta, - clock, - )?; - recover_manifest_observed_mean(loading, composed_latent_mean, manifest_mean) + let _ = (extra_process_latent_mean, extra_process_observed_mean); + Err(PsychometricError::ExtraProcessLatentMeanIsNotObservedMean) } -/// Refuse treating the evolved observed mean as the time-independent- -/// predictor observed mean. +/// Exact scalar §7.2 extra-process contribution of a `TDPREDEFFECT` +/// impulse strictly after `t0`. /// -/// Equation 5 of the Eq. 3 evolved mean is `τ + λ μ_t`. Equation 5 -/// of the Eq. 3 time-independent predictor is -/// `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. Those are not the same -/// map. +/// Driver, Oud, and Voelkle (2017, §7.2, pp. 22–23; JSS PDF +/// re-opened 2026-08-21T06:32Z from +/// ) +/// name `T0TDPREDEFFECT` when the extra process begins at `t = 0` +/// and `TDPREDEFFECT` when it begins after `t = 0`. The printed +/// extra `TDPREDEFFECT` is 1. The original process is driven through +/// the `DRIFT` coupling, not through a Dirac on the original +/// process. After an identification impulse at `u` with +/// `t0 < u < t` the scalar contribution is +/// `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)`. Form the +/// interior interval `t − u` first, then the extra-process +/// contribution on that interval. An impulse at `u = t0` is the +/// first-occasion extra-process map. An impulse at `u = t` has not +/// yet driven the original process. This is not a Kalman filter, +/// not a matrix `expm`, and not ctsem estimation. /// /// # Errors /// -/// Always returns -/// [`PsychometricError::EvolvedObservedMeanIsNotTimeIndependentObservedMean`]. -pub fn refuse_evolved_observed_mean_as_time_independent_observed_mean( - evolved_observed_mean: f64, - time_independent_observed_mean: f64, -) -> Result { - let _ = (evolved_observed_mean, time_independent_observed_mean); - Err(PsychometricError::EvolvedObservedMeanIsNotTimeIndependentObservedMean) -} - -/// Refuse treating the contemporaneous-impulse observed mean as the -/// time-independent-predictor observed mean. -/// -/// Equation 5 of the contemporaneous Dirac is `τ + λ(μ_t + m x)`. -/// Equation 5 of the Eq. 3 time-independent predictor is -/// `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. Those are not the same -/// map. -/// -/// # Errors -/// -/// Always returns -/// [`PsychometricError::ImpulseObservedMeanIsNotTimeIndependentObservedMean`]. -pub fn refuse_impulse_observed_mean_as_time_independent_observed_mean( - impulse_observed_mean: f64, - time_independent_observed_mean: f64, -) -> Result { - let _ = (impulse_observed_mean, time_independent_observed_mean); - Err(PsychometricError::ImpulseObservedMeanIsNotTimeIndependentObservedMean) -} - -/// Refuse treating the impulse-carry observed mean as the -/// time-independent-predictor observed mean. -/// -/// Equation 5 of the Eq. 1–2 carried latent mean is -/// `τ + λ(μ_t + e^{a(t−u)} m x)`. Equation 5 of the Eq. 3 -/// time-independent predictor is -/// `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. Those are not the same -/// map. -/// -/// # Errors -/// -/// Always returns -/// [`PsychometricError::ImpulseCarryObservedMeanIsNotTimeIndependentObservedMean`]. -pub fn refuse_impulse_carry_observed_mean_as_time_independent_observed_mean( - impulse_carry_observed_mean: f64, - time_independent_observed_mean: f64, -) -> Result { - let _ = (impulse_carry_observed_mean, time_independent_observed_mean); - Err(PsychometricError::ImpulseCarryObservedMeanIsNotTimeIndependentObservedMean) -} - -/// Exact scalar first-occasion time-independent predictor shift. -/// -/// Driver, Oud, and Voelkle (2017, Table 3, p. 13; Eq. 3 first -/// summand, p. 5; JSS PDF opened 2026-08-20T15:14Z from -/// ) -/// name `T0TIPREDEFFECT` the effect of time-independent predictors on -/// latents at `T0`. Table 2 / Table 3 name `TIPREDEFFECT` `B`, which -/// enters Equation 3 as the printed addend -/// `A^{-1}[e^{A(t−t0)} − I] B z`. Those are not the same matrix. The -/// scalar first-occasion shift is `t0_b z`. It is not `B`, not -/// `A^{-1}[e^{A Δt} − I] B z`, not `κ`, and not `M x`. A zero effect -/// or zero predictor is exactly zero. This is not a Kalman filter and -/// not ctsem estimation. -/// -/// # Errors -/// -/// Returns [`PsychometricError::InvalidNumericInput`] when the effect -/// or predictor is non-finite or the product overflows. -pub fn recover_initial_time_independent_predictor_effect( - initial_time_independent_effect: f64, - time_independent_predictor: f64, -) -> Result { - if !initial_time_independent_effect.is_finite() || !time_independent_predictor.is_finite() { - return Err(PsychometricError::InvalidNumericInput); - } - if initial_time_independent_effect == 0.0 || time_independent_predictor == 0.0 { - return Ok(0.0); - } - require_finite(initial_time_independent_effect * time_independent_predictor) -} - -/// Exact scalar carried first-occasion time-independent predictor. -/// -/// Driver, Oud, and Voelkle (2017, Eq. 3, p. 5; Table 3, p. 13; JSS -/// PDF opened 2026-08-20T15:14Z) write the first summand as -/// `e^{A(t−t0)} η_i(t0)`. A Table 3 `T0TIPREDEFFECT` shift that is -/// already in `η(t0)` therefore appears at `t` as `e^{A Δt} t0_b z`. -/// Form `t0_b z` first, then `e^{a Δt} t0_b z`. A zero drift is -/// `t0_b z` with no dissipation of the first-occasion shift. Binary64 -/// underflow of `e^{a Δt}` to `+0` is a vanishing carry of that -/// shift and is kept. This carry is not the first-occasion shift, not -/// `A^{-1}[e^{A Δt} − I] B z` (`TIPREDEFFECT`), not `CINT`, and not -/// `M x`. When `exp` overflows at a finite `a Δt`, rewrite as -/// `sign(t0_b z) exp(ln|t0_b z| + a Δt)`. An overflowing rewrite -/// fails closed. This is not a Kalman filter and not ctsem estimation. -/// -/// # Errors -/// -/// Returns [`PsychometricError::EventTimeRequired`] for any non-event -/// clock, [`PsychometricError::NonPositiveInterval`] when -/// `event_delta` is not strictly positive, and -/// [`PsychometricError::InvalidNumericInput`] when an input is -/// non-finite or the mapped carry overflows. -pub fn recover_initial_time_independent_predictor_carry( - initial_time_independent_effect: f64, - time_independent_predictor: f64, - log_rate: f64, +/// Returns [`PsychometricError::NonPositiveInterval`] when +/// `t − u` is not strictly interior to `(0, t − t0)`, and +/// otherwise propagates +/// [`recover_level_change_extra_process_contribution`]. +pub fn recover_level_change_extra_process_contribution_after( + original_from_extra_drift: f64, + time_dependent_predictor: f64, + original_log_rate: f64, + extra_log_rate: f64, event_delta: f64, + elapsed_after_impulse: f64, clock: LagClock, ) -> Result { if !clock.admits_structural_lag() { @@ -4214,366 +4817,437 @@ pub fn recover_initial_time_independent_predictor_carry( if !event_delta.is_finite() || event_delta <= 0.0 { return Err(PsychometricError::NonPositiveInterval); } - if !log_rate.is_finite() { - return Err(PsychometricError::InvalidNumericInput); - } - let initial_shift = recover_initial_time_independent_predictor_effect( - initial_time_independent_effect, - time_independent_predictor, - )?; - if initial_shift == 0.0 { - return Ok(0.0); + if !elapsed_after_impulse.is_finite() || elapsed_after_impulse <= 0.0 { + return Err(PsychometricError::NonPositiveInterval); } - let drift_interval = log_rate * event_delta; - let auto_effect = drift_interval.exp(); - if auto_effect.is_finite() { - // +0 underflow is a vanishing carry of the T0 shift. - return require_finite(auto_effect * initial_shift); + if elapsed_after_impulse >= event_delta { + return Err(PsychometricError::NonPositiveInterval); } - // Overflow of a finite `a Δt` is the log-space rewrite. - // A non-finite argument also fails closed through `require_finite`. - // e^{a Δt} t0_b z = sign(t0_b z) exp(ln|t0_b z| + a Δt). - require_finite(initial_shift.signum() * (initial_shift.abs().ln() + drift_interval).exp()) + recover_level_change_extra_process_contribution( + original_from_extra_drift, + time_dependent_predictor, + original_log_rate, + extra_log_rate, + elapsed_after_impulse, + clock, + ) } -/// Exact scalar evolved latent mean plus a first-occasion TI predictor. +/// Exact scalar evolved latent mean plus a §7.2 extra-process +/// contribution after `t0`. /// -/// Driver, Oud, and Voelkle (2017, Eq. 3, p. 5; Table 3, p. 13) write -/// the first summand as the carried `T0MEANS`, which includes any -/// `T0TIPREDEFFECT` shift already in `η(t0)`. Form `μ_t` first, then -/// add `e^{a Δt} t0_b z`. A zero carry is exactly `μ_t`. A zero -/// evolved mean is exactly the carry. Adding `t0_b z` without the -/// exponential is not this composition when `a Δt ≠ 0`. Adding -/// `A^{-1}[e^{A Δt} − I] B z` is not this composition. +/// Driver, Oud, and Voelkle (2017, Eq. 3, p. 5; §7.2, pp. 22–23; +/// JSS PDF re-opened 2026-08-21T06:32Z) evolve `T0MEANS` and `CINT` +/// over `Δt = t − t0`. `TDPREDEFFECT` on the extra process after +/// `t0` drives the original process only over `t − u` with +/// `t0 < u < t`. Form `μ_t` first, then add the after-t0 +/// extra-process contribution. A zero contribution is exactly +/// `μ_t`. The first-occasion extra-process map uses `Δt` for both +/// the evolution and the extra drive and is not this composition +/// when `u ≠ t0`. The impulse-carry `μ_t + e^{a(t−u)} m x` is a +/// Dirac on the original process and is not this composition. /// /// # Errors /// /// Propagates [`recover_discrete_latent_mean`] and -/// [`recover_initial_time_independent_predictor_carry`], and returns -/// [`PsychometricError::InvalidNumericInput`] when the sum overflows. +/// [`recover_level_change_extra_process_contribution_after`], and +/// returns [`PsychometricError::InvalidNumericInput`] when the sum +/// overflows. #[allow(clippy::too_many_arguments)] -pub fn recover_discrete_latent_mean_with_initial_time_independent_predictor( +pub fn recover_discrete_latent_mean_with_extra_process_after( initial_latent_mean: f64, - log_rate: f64, + original_log_rate: f64, continuous_intercept: f64, - initial_time_independent_effect: f64, - time_independent_predictor: f64, + original_from_extra_drift: f64, + time_dependent_predictor: f64, + extra_log_rate: f64, event_delta: f64, + elapsed_after_impulse: f64, clock: LagClock, ) -> Result { let evolved_latent_mean = recover_discrete_latent_mean( initial_latent_mean, - log_rate, + original_log_rate, continuous_intercept, event_delta, clock, )?; - let initial_carry = recover_initial_time_independent_predictor_carry( - initial_time_independent_effect, - time_independent_predictor, - log_rate, + let contribution = recover_level_change_extra_process_contribution_after( + original_from_extra_drift, + time_dependent_predictor, + original_log_rate, + extra_log_rate, event_delta, + elapsed_after_impulse, clock, )?; - if initial_carry == 0.0 { + if contribution == 0.0 { return Ok(evolved_latent_mean); } if evolved_latent_mean == 0.0 { - return Ok(initial_carry); + return Ok(contribution); } - require_finite(evolved_latent_mean + initial_carry) + require_finite(evolved_latent_mean + contribution) } -/// Refuse treating the Table 3 first-occasion shift as the Eq. 3 -/// process increment. +/// Exact scalar observed mean of a §7.2 extra-process contribution +/// after `t0`. /// -/// `T0TIPREDEFFECT` shifts `η(t0)`. `TIPREDEFFECT` `B` enters the -/// SDE and maps as `A^{-1}[e^{A Δt} − I] B z`. +/// Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; §7.2, pp. 22–23; +/// JSS PDF re-opened 2026-08-21T06:32Z) write +/// `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and +/// `Γ ~ N(τ, Ψ)`. The printed extra process has `LAMBDA` 0. +/// Original indicators load on the original process after the +/// `DRIFT` coupling over `t − u` with `t0 < u < t`. The scalar +/// composition is +/// `E(y_t) = τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))`. +/// Form the evolved-plus-after-contribution latent mean first, then +/// `τ + λ` of that mean. The first-occasion extra-process observed +/// mean uses `Δt` for both the evolution and the extra drive and is +/// not this composition when `u ≠ t0`. The evolved observed mean +/// `τ + λ μ_t` is not this composition. The impulse-carry map +/// `τ + λ(μ_t + e^{a(t−u)} m x)` is not this composition. The +/// extra process itself is not an observed indicator. This is not a +/// Kalman filter and not ctsem estimation. /// /// # Errors /// -/// Always returns -/// [`PsychometricError::InitialTimeIndependentEffectIsNotProcessIncrement`]. -pub fn refuse_initial_time_independent_effect_as_process_increment( - initial_time_independent_effect: f64, - time_independent_increment: f64, +/// Propagates [`recover_discrete_latent_mean_with_extra_process_after`] +/// and [`recover_manifest_observed_mean`]. +#[allow(clippy::too_many_arguments)] +pub fn recover_discrete_observed_mean_with_extra_process_after( + loading: f64, + initial_latent_mean: f64, + original_log_rate: f64, + continuous_intercept: f64, + original_from_extra_drift: f64, + time_dependent_predictor: f64, + extra_log_rate: f64, + manifest_mean: f64, + event_delta: f64, + elapsed_after_impulse: f64, + clock: LagClock, ) -> Result { - let _ = (initial_time_independent_effect, time_independent_increment); - Err(PsychometricError::InitialTimeIndependentEffectIsNotProcessIncrement) + let extra_latent_mean = recover_discrete_latent_mean_with_extra_process_after( + initial_latent_mean, + original_log_rate, + continuous_intercept, + original_from_extra_drift, + time_dependent_predictor, + extra_log_rate, + event_delta, + elapsed_after_impulse, + clock, + )?; + recover_manifest_observed_mean(loading, extra_latent_mean, manifest_mean) } -/// Refuse treating the Eq. 3 carry of `T0TIPREDEFFECT` as the -/// first-occasion shift. -/// -/// `e^{A Δt} t0_b z` is the first summand's contribution at `t`. -/// `t0_b z` is the shift at `T0`. +/// Refuse treating the first-occasion extra-process observed mean +/// as the after-t0 extra-process observed mean. +/// +/// `T0TDPREDEFFECT` on the extra process uses `Δt = t − t0`. +/// `TDPREDEFFECT` after `t0` uses `t − u` with `t0 < u < t`. /// /// # Errors /// /// Always returns -/// [`PsychometricError::InitialTimeIndependentCarryIsNotInitialEffect`]. -pub fn refuse_initial_time_independent_carry_as_initial_effect( - initial_time_independent_carry: f64, - initial_time_independent_effect: f64, +/// [`PsychometricError::ExtraProcessObservedMeanIsNotAfterExtraProcessObservedMean`]. +pub fn refuse_extra_process_observed_mean_as_after_extra_process_observed_mean( + extra_process_observed_mean: f64, + after_extra_process_observed_mean: f64, ) -> Result { let _ = ( - initial_time_independent_carry, - initial_time_independent_effect, + extra_process_observed_mean, + after_extra_process_observed_mean, ); - Err(PsychometricError::InitialTimeIndependentCarryIsNotInitialEffect) + Err(PsychometricError::ExtraProcessObservedMeanIsNotAfterExtraProcessObservedMean) } -/// Refuse treating the Table 3 first-occasion shift as `CINT`. +/// Refuse treating the evolved observed mean as the after-t0 +/// extra-process observed mean. /// -/// `t0_b z` is an initial-mean shift. `κ` is the continuous intercept. +/// Equation 5 of the Eq. 3 evolved mean is `τ + λ μ_t`. Equation 5 +/// of the after-t0 extra-process contribution is +/// `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))`. /// /// # Errors /// /// Always returns -/// [`PsychometricError::InitialTimeIndependentEffectIsNotContinuousIntercept`]. -pub fn refuse_initial_time_independent_effect_as_continuous_intercept( - initial_time_independent_effect: f64, - continuous_intercept: f64, +/// [`PsychometricError::EvolvedObservedMeanIsNotAfterExtraProcessObservedMean`]. +pub fn refuse_evolved_observed_mean_as_after_extra_process_observed_mean( + evolved_observed_mean: f64, + after_extra_process_observed_mean: f64, ) -> Result { - let _ = (initial_time_independent_effect, continuous_intercept); - Err(PsychometricError::InitialTimeIndependentEffectIsNotContinuousIntercept) + let _ = (evolved_observed_mean, after_extra_process_observed_mean); + Err(PsychometricError::EvolvedObservedMeanIsNotAfterExtraProcessObservedMean) } -/// Refuse treating the Table 3 first-occasion shift as `M x`. +/// Refuse treating the impulse-carry observed mean as the after-t0 +/// extra-process observed mean. /// -/// The product `t0_b z` is algebraically a product, as is `M x`. -/// Table 3 names `T0TIPREDEFFECT` for `T0`. Table 2 names `M` -/// `TDPREDEFFECT` for the Dirac impulse. +/// `e^{a(t−u)} m x` is a Dirac on the original process. Extra-process +/// `TDPREDEFFECT` after `t0` drives the original process through +/// `DRIFT`. /// /// # Errors /// /// Always returns -/// [`PsychometricError::InitialTimeIndependentEffectIsNotTimeDependentImpulse`]. -pub fn refuse_initial_time_independent_effect_as_time_dependent_impulse( - initial_time_independent_effect: f64, - time_dependent_impulse: f64, +/// [`PsychometricError::ImpulseCarryObservedMeanIsNotAfterExtraProcessObservedMean`]. +pub fn refuse_impulse_carry_observed_mean_as_after_extra_process_observed_mean( + impulse_carry_observed_mean: f64, + after_extra_process_observed_mean: f64, ) -> Result { - let _ = (initial_time_independent_effect, time_dependent_impulse); - Err(PsychometricError::InitialTimeIndependentEffectIsNotTimeDependentImpulse) + let _ = ( + impulse_carry_observed_mean, + after_extra_process_observed_mean, + ); + Err(PsychometricError::ImpulseCarryObservedMeanIsNotAfterExtraProcessObservedMean) } -/// Refuse treating Driver Table 3 `T0TIPREDEFFECT` as the -/// first-occasion shift. +/// Refuse treating the after-t0 extra-process contribution as +/// `E(y_t)`. /// -/// `T0TIPREDEFFECT` is the coefficient. The shift is `t0_b z`. +/// The contribution is not `τ + λ` of the +/// evolved-plus-after-contribution latent mean. The extra process +/// has `LAMBDA` 0 in the printed specification. /// /// # Errors /// /// Always returns -/// [`PsychometricError::InitialTimeIndependentCoefficientIsNotInitialEffect`]. -pub fn refuse_initial_time_independent_coefficient_as_initial_effect( - initial_time_independent_coefficient: f64, - initial_time_independent_effect: f64, +/// [`PsychometricError::AfterExtraProcessContributionIsNotObservedMean`]. +pub fn refuse_after_extra_process_contribution_as_observed_mean( + after_extra_process_contribution: f64, + after_extra_process_observed_mean: f64, ) -> Result { let _ = ( - initial_time_independent_coefficient, - initial_time_independent_effect, + after_extra_process_contribution, + after_extra_process_observed_mean, ); - Err(PsychometricError::InitialTimeIndependentCoefficientIsNotInitialEffect) + Err(PsychometricError::AfterExtraProcessContributionIsNotObservedMean) } -/// Exact scalar observed mean of a first-occasion time-independent -/// predictor. +/// Refuse treating the evolved-plus-after-contribution latent mean +/// as `E(y_t)`. /// -/// Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Eq. 3 first summand, -/// p. 5; Table 3, p. 13; JSS PDF re-opened 2026-08-20T15:28Z from +/// Equation 5 maps `E(y_t) = τ + λ` of that mean. The latent mean +/// is not the observed mean. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::AfterExtraProcessLatentMeanIsNotObservedMean`]. +pub fn refuse_after_extra_process_latent_mean_as_observed_mean( + after_extra_process_latent_mean: f64, + after_extra_process_observed_mean: f64, +) -> Result { + let _ = ( + after_extra_process_latent_mean, + after_extra_process_observed_mean, + ); + Err(PsychometricError::AfterExtraProcessLatentMeanIsNotObservedMean) +} + +/// Exact scalar evolved latent mean plus a contemporaneous impulse. +/// +/// Driver, Oud, and Voelkle (2017, Eq. 3, p. 5) write the first two +/// summands as the carried `T0MEANS` and `CINT` increment, then add +/// the fourth-summand impulse at the observation instant. Form `μ_t` +/// first, then add `m x`. A zero impulse is exactly `μ_t`. A zero +/// evolved mean is exactly the impulse. The first-occasion map +/// `μ_0 + m x` is not this composition when the process has already +/// evolved. The level-change form is not this map. +/// +/// # Errors +/// +/// Propagates [`recover_discrete_latent_mean`] and +/// [`recover_time_dependent_predictor_impulse`], and returns +/// [`PsychometricError::InvalidNumericInput`] when the sum overflows. +pub fn recover_discrete_latent_mean_with_impulse( + initial_latent_mean: f64, + log_rate: f64, + continuous_intercept: f64, + time_dependent_effect: f64, + time_dependent_predictor: f64, + event_delta: f64, + clock: LagClock, +) -> Result { + let evolved_latent_mean = recover_discrete_latent_mean( + initial_latent_mean, + log_rate, + continuous_intercept, + event_delta, + clock, + )?; + let impulse = + recover_time_dependent_predictor_impulse(time_dependent_effect, time_dependent_predictor)?; + if impulse == 0.0 { + return Ok(evolved_latent_mean); + } + if evolved_latent_mean == 0.0 { + return Ok(impulse); + } + require_finite(evolved_latent_mean + impulse) +} + +/// Exact scalar observed mean of a contemporaneous impulse. +/// +/// Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Eq. 1–3, pp. 4–5; +/// Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T09:01Z +/// from /// ) /// write `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and -/// `Γ ~ N(τ, Ψ)`. Table 3 names `T0TIPREDEFFECT` the effect of -/// time-independent predictors on latents at `T0`. Equation 3's -/// first summand carries that shift as `e^{A Δt} t0_b z`. The -/// expected intercept is `τ`. The latent process at `t` after that -/// carry is `μ_t + e^{a Δt} t0_b z`. The scalar composition is -/// `E(y_t) = τ + λ(μ_t + e^{a Δt} t0_b z)`. Form the -/// evolved-plus-carry latent mean first, then `τ + λ` of that mean. -/// A zero loading is exactly `τ`. A zero evolved-plus-carry latent -/// mean is exactly `τ`. A zero intercept is exactly -/// `λ(μ_t + e^{a Δt} t0_b z)`. The evolved observed mean -/// `τ + λ μ_t` is not this composition when the carry is nonzero. -/// The process-increment map -/// `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not this composition. -/// The contemporaneous map `τ + λ(μ_t + m x)` is not this -/// composition. The impulse-carry map +/// `Γ ~ N(τ, Ψ)`. The expected intercept is `τ`. The latent process +/// at `t` after a contemporaneous Dirac (`u = t`) is `μ_t + m x`. +/// The scalar composition is `E(y_t) = τ + λ(μ_t + m x)`. Form the +/// evolved-plus-impulse latent mean first, then `τ + λ` of that +/// mean. Table 2 names `τ` `MANIFESTMEANS`. A zero loading is +/// exactly `τ`. A zero evolved-plus-impulse latent mean is exactly +/// `τ`. A zero intercept is exactly `λ(μ_t + m x)`. The evolved +/// observed mean `τ + λ μ_t` is not this composition when the +/// impulse is nonzero. The carry map /// `τ + λ(μ_t + e^{a(t−u)} m x)` is not this composition when -/// `u ≠ t0`. `MANIFESTMEANS` is not `E(y_t)`. The -/// evolved-plus-carry latent mean is not `E(y_t)`. -/// `T0TIPREDEFFECT` is the coefficient, not that observed mean. -/// This is not a Kalman filter and not ctsem estimation. +/// `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The +/// evolved-plus-impulse latent mean is not `E(y_t)`. The §7.2 +/// level-change form is a different specification and is not this +/// map. This is not a Kalman filter and not ctsem estimation. /// /// # Errors /// -/// Propagates -/// [`recover_discrete_latent_mean_with_initial_time_independent_predictor`] -/// and [`recover_manifest_observed_mean`]. +/// Propagates [`recover_discrete_latent_mean_with_impulse`] and +/// [`recover_manifest_observed_mean`]. #[allow(clippy::too_many_arguments)] -pub fn recover_discrete_observed_mean_with_initial_time_independent_predictor( +pub fn recover_discrete_observed_mean_with_impulse( loading: f64, initial_latent_mean: f64, log_rate: f64, continuous_intercept: f64, - initial_time_independent_effect: f64, - time_independent_predictor: f64, + time_dependent_effect: f64, + time_dependent_predictor: f64, manifest_mean: f64, event_delta: f64, clock: LagClock, ) -> Result { - let composed_latent_mean = - recover_discrete_latent_mean_with_initial_time_independent_predictor( - initial_latent_mean, - log_rate, - continuous_intercept, - initial_time_independent_effect, - time_independent_predictor, - event_delta, - clock, - )?; - recover_manifest_observed_mean(loading, composed_latent_mean, manifest_mean) + let impulse_latent_mean = recover_discrete_latent_mean_with_impulse( + initial_latent_mean, + log_rate, + continuous_intercept, + time_dependent_effect, + time_dependent_predictor, + event_delta, + clock, + )?; + recover_manifest_observed_mean(loading, impulse_latent_mean, manifest_mean) } -/// Refuse treating the evolved observed mean as the first-occasion -/// time-independent-predictor observed mean. +/// Refuse treating the evolved observed mean as the contemporaneous- +/// impulse observed mean. /// /// Equation 5 of the Eq. 3 evolved mean is `τ + λ μ_t`. Equation 5 -/// of the Table 3 first-occasion TI predictor is -/// `τ + λ(μ_t + e^{a Δt} t0_b z)`. Those are not the same map. +/// of the Eq. 3 contemporaneous impulse is `τ + λ(μ_t + m x)`. +/// Those are not the same map. /// /// # Errors /// /// Always returns -/// [`PsychometricError::EvolvedObservedMeanIsNotInitialTimeIndependentObservedMean`]. -pub fn refuse_evolved_observed_mean_as_initial_time_independent_observed_mean( +/// [`PsychometricError::EvolvedObservedMeanIsNotImpulseObservedMean`]. +pub fn refuse_evolved_observed_mean_as_impulse_observed_mean( evolved_observed_mean: f64, - initial_time_independent_observed_mean: f64, + impulse_observed_mean: f64, ) -> Result { - let _ = ( - evolved_observed_mean, - initial_time_independent_observed_mean, - ); - Err(PsychometricError::EvolvedObservedMeanIsNotInitialTimeIndependentObservedMean) + let _ = (evolved_observed_mean, impulse_observed_mean); + Err(PsychometricError::EvolvedObservedMeanIsNotImpulseObservedMean) } -/// Refuse treating the process-increment observed mean as the -/// first-occasion time-independent-predictor observed mean. +/// Refuse treating the contemporaneous-impulse observed mean as the +/// impulse-carry observed mean. /// -/// Equation 5 of the Eq. 3 `TIPREDEFFECT` increment is -/// `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. Equation 5 of the -/// Table 3 first-occasion TI predictor is -/// `τ + λ(μ_t + e^{a Δt} t0_b z)`. Those are not the same map. +/// Equation 5 of the contemporaneous Dirac is `τ + λ(μ_t + m x)`. +/// Equation 5 of the Eq. 1–2 carried latent mean is +/// `τ + λ(μ_t + e^{a(t−u)} m x)`. Those are not the same map when +/// `u ≠ t`. /// /// # Errors /// /// Always returns -/// [`PsychometricError::TimeIndependentObservedMeanIsNotInitialTimeIndependentObservedMean`]. -pub fn refuse_time_independent_observed_mean_as_initial_time_independent_observed_mean( - time_independent_observed_mean: f64, - initial_time_independent_observed_mean: f64, +/// [`PsychometricError::ImpulseObservedMeanIsNotImpulseCarryObservedMean`]. +pub fn refuse_impulse_observed_mean_as_impulse_carry_observed_mean( + impulse_observed_mean: f64, + impulse_carry_observed_mean: f64, ) -> Result { - let _ = ( - time_independent_observed_mean, - initial_time_independent_observed_mean, - ); - Err(PsychometricError::TimeIndependentObservedMeanIsNotInitialTimeIndependentObservedMean) + let _ = (impulse_observed_mean, impulse_carry_observed_mean); + Err(PsychometricError::ImpulseObservedMeanIsNotImpulseCarryObservedMean) } -/// Refuse treating the contemporaneous-impulse observed mean as the -/// first-occasion time-independent-predictor observed mean. +/// Refuse treating the Eq. 3 impulse as `CINT`. /// -/// Equation 5 of the contemporaneous Dirac is `τ + λ(μ_t + m x)`. -/// Equation 5 of the Table 3 first-occasion TI predictor is -/// `τ + λ(μ_t + e^{a Δt} t0_b z)`. Those are not the same map. +/// Table 2 names `M` `TDPREDEFFECT` and `κ` `CINT`. The impulse is +/// `m x`. The continuous intercept is not that jump. /// /// # Errors /// /// Always returns -/// [`PsychometricError::ImpulseObservedMeanIsNotInitialTimeIndependentObservedMean`]. -pub fn refuse_impulse_observed_mean_as_initial_time_independent_observed_mean( - impulse_observed_mean: f64, - initial_time_independent_observed_mean: f64, +/// [`PsychometricError::TimeDependentImpulseIsNotContinuousIntercept`]. +pub fn refuse_time_dependent_impulse_as_continuous_intercept( + time_dependent_impulse: f64, + continuous_intercept: f64, ) -> Result { - let _ = ( - impulse_observed_mean, - initial_time_independent_observed_mean, - ); - Err(PsychometricError::ImpulseObservedMeanIsNotInitialTimeIndependentObservedMean) + let _ = (time_dependent_impulse, continuous_intercept); + Err(PsychometricError::TimeDependentImpulseIsNotContinuousIntercept) } -/// Refuse treating the impulse-carry observed mean as the -/// first-occasion time-independent-predictor observed mean. +/// Refuse treating the Eq. 3 impulse as the time-independent effect. /// -/// Equation 5 of the Eq. 1–2 carried latent mean is -/// `τ + λ(μ_t + e^{a(t−u)} m x)`. Equation 5 of the Table 3 -/// first-occasion TI predictor is `τ + λ(μ_t + e^{a Δt} t0_b z)`. -/// Those are not the same map. +/// The third summand is `A^{-1}[e^{A Δt} − I] B z`. Table 2 names +/// `B` `TIPREDEFFECT`. The fourth-summand impulse is `M x`. /// /// # Errors /// /// Always returns -/// [`PsychometricError::ImpulseCarryObservedMeanIsNotInitialTimeIndependentObservedMean`]. -pub fn refuse_impulse_carry_observed_mean_as_initial_time_independent_observed_mean( - impulse_carry_observed_mean: f64, - initial_time_independent_observed_mean: f64, +/// [`PsychometricError::TimeDependentImpulseIsNotTimeIndependentEffect`]. +pub fn refuse_time_dependent_impulse_as_time_independent_effect( + time_dependent_impulse: f64, + time_independent_effect: f64, ) -> Result { - let _ = ( - impulse_carry_observed_mean, - initial_time_independent_observed_mean, - ); - Err(PsychometricError::ImpulseCarryObservedMeanIsNotInitialTimeIndependentObservedMean) + let _ = (time_dependent_impulse, time_independent_effect); + Err(PsychometricError::TimeDependentImpulseIsNotTimeIndependentEffect) } -/// Exact scalar first-occasion time-dependent predictor shift. +/// Refuse treating the Eq. 3 impulse as Voelkle et al. (2012, Eq. 14). /// -/// Driver, Oud, and Voelkle (2017, Table 3, p. 13; Eq. 3 first -/// summand, p. 5; JSS PDF re-opened 2026-08-20T19:10Z from -/// ) -/// name `T0TDPREDEFFECT` the effect of time-dependent predictors on -/// latents at `T0`. Table 2 / Table 3 name `TDPREDEFFECT` `M`, which -/// enters Equation 3 as the printed fourth-summand Dirac `M x` at -/// `u = t`. Those are not the same matrix. The scalar first-occasion -/// shift is `t0_m x0`. It is not `M`, not `M x`, not -/// `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not -/// `A^{-1}[e^{A Δt} − I] B z`, and not `κ`. An impulse at `u ≤ t0` -/// that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as -/// `T0TDPREDEFFECT`. A zero effect or zero predictor is exactly -/// zero. This is not a Kalman filter and not ctsem estimation. +/// Equation 14 is `a_{yx} Δt` for a piecewise-constant time-varying +/// predictor whose sampling interval equals its constancy interval. +/// The Dirac impulse is `m x`. /// /// # Errors /// -/// Returns [`PsychometricError::InvalidNumericInput`] when the effect -/// or predictor is non-finite or the product overflows. -pub fn recover_initial_time_dependent_predictor_effect( - initial_time_dependent_effect: f64, - time_dependent_predictor: f64, +/// Always returns +/// [`PsychometricError::TimeDependentImpulseIsNotTimeVaryingDiscreteEffect`]. +pub fn refuse_time_dependent_impulse_as_time_varying_discrete_effect( + time_dependent_impulse: f64, + time_varying_discrete_effect: f64, ) -> Result { - if !initial_time_dependent_effect.is_finite() || !time_dependent_predictor.is_finite() { - return Err(PsychometricError::InvalidNumericInput); - } - if initial_time_dependent_effect == 0.0 || time_dependent_predictor == 0.0 { - return Ok(0.0); - } - require_finite(initial_time_dependent_effect * time_dependent_predictor) + let _ = (time_dependent_impulse, time_varying_discrete_effect); + Err(PsychometricError::TimeDependentImpulseIsNotTimeVaryingDiscreteEffect) } -/// Exact scalar carried first-occasion time-dependent predictor. +/// Exact scalar discrete time-independent predictor effect from +/// Driver Equation 3. /// -/// Driver, Oud, and Voelkle (2017, Eq. 3, p. 5; Table 3, p. 13; JSS -/// PDF re-opened 2026-08-20T19:10Z) write the first summand as -/// `e^{A(t−t0)} η_i(t0)`. A Table 3 `T0TDPREDEFFECT` shift that is -/// already in `η(t0)` therefore appears at `t` as `e^{A Δt} t0_m x0`. -/// Form `t0_m x0` first, then `e^{a Δt} t0_m x0`. A zero drift is -/// `t0_m x0` with no dissipation of the first-occasion shift. -/// Binary64 underflow of `e^{a Δt}` to `+0` is a vanishing carry of -/// that shift and is kept. This carry is not the first-occasion -/// shift, not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not -/// `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`. When -/// `exp` overflows at a finite `a Δt`, rewrite as -/// `sign(t0_m x0) exp(ln|t0_m x0| + a Δt)`. An overflowing rewrite -/// fails closed. This is not a Kalman filter and not ctsem -/// estimation. +/// Driver, Oud, and Voelkle (2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; +/// JSS PDF re-opened 2026-08-20T10:13Z from +/// ) +/// write the latent SDE +/// `dη = (A η + b + A_{ηξ} ξ + B z) dt + G dW + M dχ`. Equation 3's +/// second summand is `A^{-1}[e^{A Δt} − I](b + A_{ηξ} ξ + B z)`. +/// Table 2 names `B` `TIPREDEFFECT`, `κ`/`b` `CINT`, and `M` +/// `TDPREDEFFECT`. The scalar map of the time-independent predictor +/// is `(e^{a Δt} − 1)/a · B z` for `a ≠ 0`. Form `B z` first, then +/// the discrete intercept map. A zero drift is the Eq. 3 integral +/// `B z Δt`. A zero effect or zero predictor is exactly zero. +/// `TIPREDEFFECT` is `B`, not that discrete increment. `B z` is not +/// `CINT`. `A^{-1}[e^{A Δt} − I] B z` is not the contemporaneous +/// impulse `M x` and is not Voelkle et al. (2012, Eq. 14) `a_{yx} Δt`. +/// This is not a Kalman filter and not ctsem estimation. /// /// # Errors /// @@ -4581,10 +5255,10 @@ pub fn recover_initial_time_dependent_predictor_effect( /// clock, [`PsychometricError::NonPositiveInterval`] when /// `event_delta` is not strictly positive, and /// [`PsychometricError::InvalidNumericInput`] when an input is -/// non-finite or the mapped carry overflows. -pub fn recover_initial_time_dependent_predictor_carry( - initial_time_dependent_effect: f64, - time_dependent_predictor: f64, +/// non-finite or `B z` or the mapped increment overflows. +pub fn recover_discrete_time_independent_predictor_effect( + time_independent_effect: f64, + time_independent_predictor: f64, log_rate: f64, event_delta: f64, clock: LagClock, @@ -4595,50 +5269,44 @@ pub fn recover_initial_time_dependent_predictor_carry( if !event_delta.is_finite() || event_delta <= 0.0 { return Err(PsychometricError::NonPositiveInterval); } - if !log_rate.is_finite() { + if !time_independent_effect.is_finite() + || !time_independent_predictor.is_finite() + || !log_rate.is_finite() + { return Err(PsychometricError::InvalidNumericInput); } - let initial_shift = recover_initial_time_dependent_predictor_effect( - initial_time_dependent_effect, - time_dependent_predictor, - )?; - if initial_shift == 0.0 { + if time_independent_effect == 0.0 || time_independent_predictor == 0.0 { return Ok(0.0); } - let drift_interval = log_rate * event_delta; - let auto_effect = drift_interval.exp(); - if auto_effect.is_finite() { - // +0 underflow is a vanishing carry of the T0 TD shift. - return require_finite(auto_effect * initial_shift); + let continuous = require_finite(time_independent_effect * time_independent_predictor)?; + if log_rate == 0.0 { + return require_finite(continuous * event_delta); } - // Overflow of a finite `a Δt` is the log-space rewrite. - // A non-finite argument also fails closed through `require_finite`. - // e^{a Δt} t0_m x0 = sign(t0_m x0) exp(ln|t0_m x0| + a Δt). - require_finite(initial_shift.signum() * (initial_shift.abs().ln() + drift_interval).exp()) + recover_discrete_constant_predictor_effect(continuous, log_rate, event_delta, clock) } -/// Exact scalar evolved latent mean plus a first-occasion TD predictor. +/// Exact scalar evolved latent mean plus a time-independent predictor. /// -/// Driver, Oud, and Voelkle (2017, Eq. 3, p. 5; Table 3, p. 13) write -/// the first summand as the carried `T0MEANS`, which includes any -/// `T0TDPREDEFFECT` shift already in `η(t0)`. Form `μ_t` first, then -/// add `e^{a Δt} t0_m x0`. A zero carry is exactly `μ_t`. A zero -/// evolved mean is exactly the carry. Adding `t0_m x0` without the -/// exponential is not this composition when `a Δt ≠ 0`. Adding -/// `M x` or `e^{A(t−u)} M x` is not this composition. +/// Driver, Oud, and Voelkle (2017, Eq. 3, p. 5) write the first two +/// summands as the carried `T0MEANS`, the `CINT` increment, and the +/// `TIPREDEFFECT` increment `A^{-1}[e^{A Δt} − I] B z`. Form `μ_t` +/// first, then add that increment. A zero time-independent increment +/// is exactly `μ_t`. A zero evolved mean is exactly the increment. +/// Adding `B z` to `μ_t` is not this map. Adding `M x` is not this +/// map. /// /// # Errors /// /// Propagates [`recover_discrete_latent_mean`] and -/// [`recover_initial_time_dependent_predictor_carry`], and returns -/// [`PsychometricError::InvalidNumericInput`] when the sum overflows. -#[allow(clippy::too_many_arguments)] -pub fn recover_discrete_latent_mean_with_initial_time_dependent_predictor( +/// [`recover_discrete_time_independent_predictor_effect`], and +/// returns [`PsychometricError::InvalidNumericInput`] when the sum +/// overflows. +pub fn recover_discrete_latent_mean_with_time_independent_predictor( initial_latent_mean: f64, log_rate: f64, continuous_intercept: f64, - initial_time_dependent_effect: f64, - time_dependent_predictor: f64, + time_independent_effect: f64, + time_independent_predictor: f64, event_delta: f64, clock: LagClock, ) -> Result { @@ -4649,4897 +5317,14692 @@ pub fn recover_discrete_latent_mean_with_initial_time_dependent_predictor( event_delta, clock, )?; - let initial_carry = recover_initial_time_dependent_predictor_carry( - initial_time_dependent_effect, - time_dependent_predictor, + let time_independent_increment = recover_discrete_time_independent_predictor_effect( + time_independent_effect, + time_independent_predictor, log_rate, event_delta, clock, )?; - if initial_carry == 0.0 { + if time_independent_increment == 0.0 { return Ok(evolved_latent_mean); } if evolved_latent_mean == 0.0 { - return Ok(initial_carry); + return Ok(time_independent_increment); } - require_finite(evolved_latent_mean + initial_carry) + require_finite(evolved_latent_mean + time_independent_increment) } -/// Refuse treating the Table 3 first-occasion TD shift as `M x`. +/// Refuse treating the Eq. 3 time-independent increment as `CINT`. /// -/// `T0TDPREDEFFECT` shifts `η(t0)`. `TDPREDEFFECT` `M` enters the -/// SDE as the contemporaneous Dirac `M x` at `u = t`. +/// Table 2 names `B` `TIPREDEFFECT` and `κ` `CINT`. The discrete +/// increment is `A^{-1}[e^{A Δt} − I] B z`. The continuous intercept +/// is not that increment. /// /// # Errors /// /// Always returns -/// [`PsychometricError::InitialTimeDependentEffectIsNotContemporaneousImpulse`]. -pub fn refuse_initial_time_dependent_effect_as_contemporaneous_impulse( - initial_time_dependent_effect: f64, - time_dependent_impulse: f64, +/// [`PsychometricError::TimeIndependentEffectIsNotContinuousIntercept`]. +pub fn refuse_time_independent_effect_as_continuous_intercept( + time_independent_increment: f64, + continuous_intercept: f64, ) -> Result { - let _ = (initial_time_dependent_effect, time_dependent_impulse); - Err(PsychometricError::InitialTimeDependentEffectIsNotContemporaneousImpulse) + let _ = (time_independent_increment, continuous_intercept); + Err(PsychometricError::TimeIndependentEffectIsNotContinuousIntercept) } -/// Refuse treating the Eq. 3 carry of `T0TDPREDEFFECT` as the -/// first-occasion shift. +/// Refuse treating the Eq. 3 time-independent increment as `M x`. /// -/// `e^{A Δt} t0_m x0` is the first summand's contribution at `t`. -/// `t0_m x0` is the shift at `T0`. +/// The fourth-summand impulse is contemporaneous. The second-summand +/// `TIPREDEFFECT` map integrates `B z` over the event interval. /// /// # Errors /// /// Always returns -/// [`PsychometricError::InitialTimeDependentCarryIsNotInitialEffect`]. -pub fn refuse_initial_time_dependent_carry_as_initial_effect( - initial_time_dependent_carry: f64, - initial_time_dependent_effect: f64, +/// [`PsychometricError::TimeIndependentEffectIsNotTimeDependentImpulse`]. +pub fn refuse_time_independent_effect_as_time_dependent_impulse( + time_independent_increment: f64, + time_dependent_impulse: f64, ) -> Result { - let _ = (initial_time_dependent_carry, initial_time_dependent_effect); - Err(PsychometricError::InitialTimeDependentCarryIsNotInitialEffect) + let _ = (time_independent_increment, time_dependent_impulse); + Err(PsychometricError::TimeIndependentEffectIsNotTimeDependentImpulse) } -/// Refuse treating the Table 3 first-occasion TD shift as `CINT`. +/// Refuse treating the Eq. 3 time-independent increment as Voelkle +/// et al. (2012, Eq. 14). /// -/// `t0_m x0` is an initial-mean shift. `κ` is the continuous intercept. +/// Equation 14 is `a_{yx} Δt` for a piecewise-constant time-varying +/// predictor whose sampling interval equals its constancy interval. +/// `TIPREDEFFECT` integrates a constant `z` through the drift. /// /// # Errors /// /// Always returns -/// [`PsychometricError::InitialTimeDependentEffectIsNotContinuousIntercept`]. -pub fn refuse_initial_time_dependent_effect_as_continuous_intercept( - initial_time_dependent_effect: f64, - continuous_intercept: f64, +/// [`PsychometricError::TimeIndependentEffectIsNotTimeVaryingDiscreteEffect`]. +pub fn refuse_time_independent_effect_as_time_varying_discrete_effect( + time_independent_increment: f64, + time_varying_discrete_effect: f64, ) -> Result { - let _ = (initial_time_dependent_effect, continuous_intercept); - Err(PsychometricError::InitialTimeDependentEffectIsNotContinuousIntercept) + let _ = (time_independent_increment, time_varying_discrete_effect); + Err(PsychometricError::TimeIndependentEffectIsNotTimeVaryingDiscreteEffect) } -/// Refuse treating the Table 3 first-occasion TD shift as the Eq. 3 -/// process increment. +/// Refuse treating Driver Table 2 `TIPREDEFFECT` as the discrete +/// increment. /// -/// `t0_m x0` shifts `η(t0)`. `TIPREDEFFECT` `B` maps as +/// `B` is the continuous-time coefficient. Equation 3 maps /// `A^{-1}[e^{A Δt} − I] B z`. /// /// # Errors /// /// Always returns -/// [`PsychometricError::InitialTimeDependentEffectIsNotProcessIncrement`]. -pub fn refuse_initial_time_dependent_effect_as_process_increment( - initial_time_dependent_effect: f64, +/// [`PsychometricError::TimeIndependentCoefficientIsNotDiscreteEffect`]. +pub fn refuse_time_independent_coefficient_as_discrete_effect( + time_independent_coefficient: f64, time_independent_increment: f64, ) -> Result { - let _ = (initial_time_dependent_effect, time_independent_increment); - Err(PsychometricError::InitialTimeDependentEffectIsNotProcessIncrement) + let _ = (time_independent_coefficient, time_independent_increment); + Err(PsychometricError::TimeIndependentCoefficientIsNotDiscreteEffect) } -/// Refuse treating the Table 3 first-occasion TD shift as the Table 3 -/// first-occasion TI shift. +/// Exact scalar §7.2 `asymTIPREDEFFECT`. /// -/// `T0TDPREDEFFECT` and `T0TIPREDEFFECT` are different Table 3 -/// matrices. `t0_m x0` is not `t0_b z`. +/// Driver, Oud, and Voelkle (2017, §7.2, pp. 20–21; Eq. 3, p. 5; +/// Table 2, p. 12; JSS PDF opened 2026-08-21T13:08Z from +/// ) +/// name `TIPREDEFFECT` the continuous-time coefficient `B`. Equation 3 +/// maps a finite event interval as `A^{-1}[e^{A Δt} − I] B z`. Section +/// 7.2 then names `asymTIPREDEFFECT` the expected total change in +/// process means given an increase of 1 on the time-independent +/// predictor. For stable `a < 0` that total change is `-A^{-1} B`. +/// The scalar map is `-B z / a`. Form `B z` first, then divide by +/// `-a`. A zero coefficient or zero predictor is exactly zero. +/// `a ≥ 0` cannot hold a finite process-mean change and fails closed. +/// `-B z / a` is not the coefficient `B`, not the finite-interval +/// increment `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. +/// This is not a Kalman filter, not a matrix `expm`, and not ctsem +/// estimation. +/// +/// # Errors +/// +/// Returns [`PsychometricError::EventTimeRequired`] for any non-event +/// clock, [`PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift`] +/// when the drift is not strictly negative and the effect is nonzero, +/// and [`PsychometricError::InvalidNumericInput`] when an input is +/// non-finite or `B z` or the quotient overflows. +pub fn recover_asymptotic_time_independent_predictor_effect( + time_independent_effect: f64, + time_independent_predictor: f64, + log_rate: f64, + clock: LagClock, +) -> Result { + if !clock.admits_structural_lag() { + return Err(PsychometricError::EventTimeRequired); + } + if !time_independent_effect.is_finite() + || !time_independent_predictor.is_finite() + || !log_rate.is_finite() + { + return Err(PsychometricError::InvalidNumericInput); + } + if time_independent_effect == 0.0 || time_independent_predictor == 0.0 { + return Ok(0.0); + } + if log_rate >= 0.0 { + return Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift); + } + let continuous = require_finite(time_independent_effect * time_independent_predictor)?; + require_finite(continuous / -log_rate) +} + +/// Refuse treating §7.2 `asymTIPREDEFFECT` as `TIPREDEFFECT`. +/// +/// `-B z / a` is the expected total change in process means. Table 2 +/// names `B` `TIPREDEFFECT`. The coefficient is not that total change. /// /// # Errors /// /// Always returns -/// [`PsychometricError::InitialTimeDependentEffectIsNotInitialTimeIndependentEffect`]. -pub fn refuse_initial_time_dependent_effect_as_initial_time_independent_effect( - initial_time_dependent_effect: f64, - initial_time_independent_effect: f64, +/// [`PsychometricError::AsymptoticTimeIndependentEffectIsNotCoefficient`]. +pub fn refuse_asymptotic_time_independent_effect_as_coefficient( + asymptotic_effect: f64, + time_independent_coefficient: f64, ) -> Result { - let _ = ( - initial_time_dependent_effect, - initial_time_independent_effect, - ); - Err(PsychometricError::InitialTimeDependentEffectIsNotInitialTimeIndependentEffect) + let _ = (asymptotic_effect, time_independent_coefficient); + Err(PsychometricError::AsymptoticTimeIndependentEffectIsNotCoefficient) } -/// Refuse treating Driver Table 3 `T0TDPREDEFFECT` as the -/// first-occasion shift. +/// Refuse treating §7.2 `asymTIPREDEFFECT` as the finite-interval +/// discrete increment. /// -/// `T0TDPREDEFFECT` is the coefficient. The shift is `t0_m x0`. +/// `-B z / a` is the `Δt → ∞` limit of `A^{-1}[e^{A Δt} − I] B z` +/// under stable `a < 0`. A finite event interval is not that limit. /// /// # Errors /// /// Always returns -/// [`PsychometricError::InitialTimeDependentCoefficientIsNotInitialEffect`]. -pub fn refuse_initial_time_dependent_coefficient_as_initial_effect( - initial_time_dependent_coefficient: f64, - initial_time_dependent_effect: f64, +/// [`PsychometricError::AsymptoticTimeIndependentEffectIsNotDiscreteEffect`]. +pub fn refuse_asymptotic_time_independent_effect_as_discrete_effect( + asymptotic_effect: f64, + time_independent_increment: f64, ) -> Result { - let _ = ( - initial_time_dependent_coefficient, - initial_time_dependent_effect, - ); - Err(PsychometricError::InitialTimeDependentCoefficientIsNotInitialEffect) + let _ = (asymptotic_effect, time_independent_increment); + Err(PsychometricError::AsymptoticTimeIndependentEffectIsNotDiscreteEffect) } -/// Refuse treating the Eq. 3 carry of `T0TDPREDEFFECT` as the -/// within-interval impulse carry. +/// Refuse treating §7.2 `asymTIPREDEFFECT` as `CINT`. /// -/// `e^{A Δt} t0_m x0` carries a Table 3 first-occasion TD shift. -/// `e^{A(t−u)} M x` for `t0 < u < t` carries a Table 2 Dirac that -/// occurred inside the interval. +/// `-B z / a` is the expected total change from a time-independent +/// predictor. Table 2 names `κ` `CINT`. Those are not the same map. /// /// # Errors /// /// Always returns -/// [`PsychometricError::InitialTimeDependentCarryIsNotImpulseCarry`]. -pub fn refuse_initial_time_dependent_carry_as_impulse_carry( - initial_time_dependent_carry: f64, - impulse_carry: f64, +/// [`PsychometricError::AsymptoticTimeIndependentEffectIsNotContinuousIntercept`]. +pub fn refuse_asymptotic_time_independent_effect_as_continuous_intercept( + asymptotic_effect: f64, + continuous_intercept: f64, ) -> Result { - let _ = (initial_time_dependent_carry, impulse_carry); - Err(PsychometricError::InitialTimeDependentCarryIsNotImpulseCarry) + let _ = (asymptotic_effect, continuous_intercept); + Err(PsychometricError::AsymptoticTimeIndependentEffectIsNotContinuousIntercept) } -/// Exact scalar observed mean of a first-occasion time-dependent -/// predictor. +/// Refuse treating §7.2 `asymTIPREDEFFECT` as `M x`. /// -/// Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Eq. 3 first summand, -/// p. 5; Table 3, p. 13; JSS PDF re-opened 2026-08-20T19:20Z from +/// The fourth-summand impulse is contemporaneous. The asymptotic +/// time-independent effect is a new process mean, not a Dirac. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::AsymptoticTimeIndependentEffectIsNotTimeDependentImpulse`]. +pub fn refuse_asymptotic_time_independent_effect_as_time_dependent_impulse( + asymptotic_effect: f64, + time_dependent_impulse: f64, +) -> Result { + let _ = (asymptotic_effect, time_dependent_impulse); + Err(PsychometricError::AsymptoticTimeIndependentEffectIsNotTimeDependentImpulse) +} + +/// Exact scalar §7.2 `addedTIPREDVAR`. +/// +/// Driver, Oud, and Voelkle (2017, §7.2, pp. 20–21; Eq. 3, p. 5; +/// Table 2, p. 12; JSS PDF opened 2026-08-21T13:08Z from /// ) -/// write `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and -/// `Γ ~ N(τ, Ψ)`. Table 3 names `T0TDPREDEFFECT` the effect of -/// time-dependent predictors on latents at `T0`. Equation 3's first -/// summand carries that shift as `e^{A Δt} t0_m x0`. The expected -/// intercept is `τ`. The latent process at `t` after that carry is -/// `μ_t + e^{a Δt} t0_m x0`. The scalar composition is -/// `E(y_t) = τ + λ(μ_t + e^{a Δt} t0_m x0)`. Form the -/// evolved-plus-carry latent mean first, then `τ + λ` of that mean. -/// A zero loading is exactly `τ`. A zero evolved-plus-carry latent -/// mean is exactly `τ`. A zero intercept is exactly -/// `λ(μ_t + e^{a Δt} t0_m x0)`. The evolved observed mean -/// `τ + λ μ_t` is not this composition when the carry is nonzero. -/// The process-increment map -/// `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not this composition. -/// The contemporaneous map `τ + λ(μ_t + m x)` is not this -/// composition. The impulse-carry map -/// `τ + λ(μ_t + e^{a(t−u)} m x)` is not this composition when -/// `u ≠ t0`. The first-occasion TI map -/// `τ + λ(μ_t + e^{a Δt} t0_b z)` is not this composition. -/// `MANIFESTMEANS` is not `E(y_t)`. The evolved-plus-carry latent -/// mean is not `E(y_t)`. `T0TDPREDEFFECT` is the coefficient, not -/// that observed mean. This is not a Kalman filter and not ctsem -/// estimation. +/// name `asymTIPREDEFFECT` the expected total change in process means +/// given a unit increase on a time-independent predictor. The scalar +/// map is `-B / a` for stable `a < 0`. Section 7.2 then names +/// `addedTIPREDVAR` the stable between-subject variance accounted for +/// by those predictors. For predictor variance `v ≥ 0` that variance +/// is `(-B / a)² v`. Form the unit asymptotic effect first, then +/// square, then multiply by `v`. A zero coefficient or zero predictor +/// variance is exactly zero. `v < 0` fails closed. `a ≥ 0` cannot hold +/// a finite process-mean change and fails closed. `(B / a)² v` is not +/// `TRAITVAR`, not `asymDIFFUSION`, and not the expected total change +/// `-B z / a`. This is not a Kalman filter, not a matrix `expm`, and +/// not ctsem estimation. /// /// # Errors /// -/// Propagates -/// [`recover_discrete_latent_mean_with_initial_time_dependent_predictor`] -/// and [`recover_manifest_observed_mean`]. -#[allow(clippy::too_many_arguments)] -pub fn recover_discrete_observed_mean_with_initial_time_dependent_predictor( - loading: f64, - initial_latent_mean: f64, +/// Returns [`PsychometricError::EventTimeRequired`] for any non-event +/// clock, [`PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift`] +/// when the drift is not strictly negative and the variance is nonzero, +/// and [`PsychometricError::InvalidNumericInput`] when an input is +/// non-finite, the predictor variance is negative, or the product +/// overflows. +pub fn recover_asymptotic_time_independent_predictor_variance( + time_independent_effect: f64, + predictor_variance: f64, log_rate: f64, - continuous_intercept: f64, - initial_time_dependent_effect: f64, - time_dependent_predictor: f64, - manifest_mean: f64, - event_delta: f64, clock: LagClock, ) -> Result { - let composed_latent_mean = recover_discrete_latent_mean_with_initial_time_dependent_predictor( - initial_latent_mean, + if !clock.admits_structural_lag() { + return Err(PsychometricError::EventTimeRequired); + } + if !time_independent_effect.is_finite() + || !predictor_variance.is_finite() + || !log_rate.is_finite() + || predictor_variance < 0.0 + { + return Err(PsychometricError::InvalidNumericInput); + } + if time_independent_effect == 0.0 || predictor_variance == 0.0 { + return Ok(0.0); + } + let unit_effect = recover_asymptotic_time_independent_predictor_effect( + time_independent_effect, + 1.0, log_rate, - continuous_intercept, - initial_time_dependent_effect, - time_dependent_predictor, - event_delta, clock, )?; - recover_manifest_observed_mean(loading, composed_latent_mean, manifest_mean) + let squared = require_finite(unit_effect * unit_effect)?; + require_finite(squared * predictor_variance) } -/// Refuse treating the evolved observed mean as the first-occasion -/// time-dependent-predictor observed mean. +/// Refuse treating §7.2 `addedTIPREDVAR` as `TRAITVAR`. /// -/// Equation 5 of the Eq. 3 evolved mean is `τ + λ μ_t`. Equation 5 -/// of the Table 3 first-occasion TD predictor is -/// `τ + λ(μ_t + e^{a Δt} t0_m x0)`. Those are not the same map. +/// `(B / a)² v` is between-subject variance accounted for by a +/// time-independent predictor. Section 4.3 `TRAITVAR` is a zero-drift +/// latent process. Those are not the same map. /// /// # Errors /// /// Always returns -/// [`PsychometricError::EvolvedObservedMeanIsNotInitialTimeDependentObservedMean`]. -pub fn refuse_evolved_observed_mean_as_initial_time_dependent_observed_mean( - evolved_observed_mean: f64, - initial_time_dependent_observed_mean: f64, +/// [`PsychometricError::AsymptoticTimeIndependentVarianceIsNotTraitVariance`]. +pub fn refuse_asymptotic_time_independent_variance_as_trait_variance( + added_predictor_variance: f64, + trait_variance: f64, ) -> Result { - let _ = (evolved_observed_mean, initial_time_dependent_observed_mean); - Err(PsychometricError::EvolvedObservedMeanIsNotInitialTimeDependentObservedMean) + let _ = (added_predictor_variance, trait_variance); + Err(PsychometricError::AsymptoticTimeIndependentVarianceIsNotTraitVariance) } -/// Refuse treating the process-increment observed mean as the -/// first-occasion time-dependent-predictor observed mean. +/// Refuse treating §7.2 `addedTIPREDVAR` as `asymDIFFUSION`. /// -/// Equation 5 of the Eq. 3 `TIPREDEFFECT` increment is -/// `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. Equation 5 of the -/// Table 3 first-occasion TD predictor is -/// `τ + λ(μ_t + e^{a Δt} t0_m x0)`. Those are not the same map. +/// `(B / a)² v` is between-subject variance from a time-independent +/// predictor. `asymDIFFUSION` is the stationary within-subject +/// variance `-q / (2 a)`. /// /// # Errors /// /// Always returns -/// [`PsychometricError::TimeIndependentObservedMeanIsNotInitialTimeDependentObservedMean`]. -pub fn refuse_time_independent_observed_mean_as_initial_time_dependent_observed_mean( - time_independent_observed_mean: f64, - initial_time_dependent_observed_mean: f64, +/// [`PsychometricError::AsymptoticTimeIndependentVarianceIsNotStationaryWithinSubject`]. +pub fn refuse_asymptotic_time_independent_variance_as_stationary_within_subject( + added_predictor_variance: f64, + stationary_variance: f64, +) -> Result { + let _ = (added_predictor_variance, stationary_variance); + Err(PsychometricError::AsymptoticTimeIndependentVarianceIsNotStationaryWithinSubject) +} + +/// Refuse treating §7.2 `addedTIPREDVAR` as `asymTIPREDEFFECT`. +/// +/// `(B / a)² v` is a variance. `-B z / a` is the expected total +/// change in process means. Those are not the same map. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::AsymptoticTimeIndependentVarianceIsNotAsymptoticEffect`]. +pub fn refuse_asymptotic_time_independent_variance_as_asymptotic_effect( + added_predictor_variance: f64, + asymptotic_effect: f64, +) -> Result { + let _ = (added_predictor_variance, asymptotic_effect); + Err(PsychometricError::AsymptoticTimeIndependentVarianceIsNotAsymptoticEffect) +} + +/// Exact scalar Eq. 5 of §7.2 `addedTIPREDVAR`. +/// +/// Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Table 2, p. 12; §7.2, +/// pp. 20–21; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened +/// 2026-08-23T19:23Z from +/// ) +/// write `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and +/// `Γ ~ N(τ, Ψ)`. Equation 5 maps extra latent variance through +/// `Λ`. Section 7.2 names `addedTIPREDVAR` the stable between-subject +/// variance accounted for by time-independent predictors. The 2017-era +/// `summary.ctsemFit.R` forms that latent extra as +/// `asymTIPREDEFFECT %*% TIPREDVAR %*% t(asymTIPREDEFFECT)`. The +/// scalar latent extra is `(B / a)² v`. Equation 5 of that extra, +/// with `θ = 0` and `ψ = 0`, is `λ² (B / a)² v`. Form +/// `addedTIPREDVAR` first, then `(λ extra) λ`. Do not form `λ²` +/// first: at `λ = 1e308`, `extra = 1e-308`, `λ²` overflows and +/// `λ² extra` is non-finite, but `(λ extra) λ = 1e308`. A zero +/// loading or zero extra is exactly zero. `v < 0` fails closed. A +/// non-event clock fails closed. `a ≥ 0` cannot hold a finite +/// process-mean change when the extra is nonzero and fails closed. +/// `(B / a)² v` is the latent extra and is not this observed extra. +/// `λ² t0_b² v` is Eq. 5 of `addedT0TIPREDVAR` and is not this +/// asymptotic observed extra. `λ² p + θ` is stationary +/// observed-indicator variance and is not this extra. `MANIFESTVAR` +/// `θ` is measurement error and is not this extra. `Ψ` is intercept +/// variance and is not extra TI. The printed 2-latent +/// `addedTIPREDVAR` 2.838 is not this scalar map. This is not a +/// Kalman filter, not a matrix `expm`, not DSEM, and not ctsem +/// estimation. +/// +/// # Errors +/// +/// Propagates [`recover_asymptotic_time_independent_predictor_variance`] +/// and [`recover_manifest_observed_variance`]. +pub fn recover_asymptotic_time_independent_observed_variance( + loading: f64, + time_independent_effect: f64, + predictor_variance: f64, + log_rate: f64, + clock: LagClock, +) -> Result { + let extra = recover_asymptotic_time_independent_predictor_variance( + time_independent_effect, + predictor_variance, + log_rate, + clock, + )?; + recover_manifest_observed_variance(loading, extra, 0.0) +} + +/// Refuse treating Eq. 5 of §7.2 `addedTIPREDVAR` as the latent extra. +/// +/// `λ² (B / a)² v` is extra observed-indicator variance. +/// `(B / a)² v` is extra latent variance. Those are not the same map. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::AsymptoticTimeIndependentObservedVarianceIsNotAsymptoticTimeIndependentVariance`]. +pub fn refuse_asymptotic_time_independent_observed_variance_as_asymptotic_time_independent_variance( + asymptotic_observed_predictor_variance: f64, + asymptotic_predictor_variance: f64, ) -> Result { let _ = ( - time_independent_observed_mean, - initial_time_dependent_observed_mean, + asymptotic_observed_predictor_variance, + asymptotic_predictor_variance, ); - Err(PsychometricError::TimeIndependentObservedMeanIsNotInitialTimeDependentObservedMean) + Err( + PsychometricError::AsymptoticTimeIndependentObservedVarianceIsNotAsymptoticTimeIndependentVariance, + ) } -/// Refuse treating the contemporaneous-impulse observed mean as the -/// first-occasion time-dependent-predictor observed mean. +/// Refuse treating Eq. 5 of §7.2 `addedTIPREDVAR` as Eq. 5 of +/// `addedT0TIPREDVAR`. /// -/// Equation 5 of the contemporaneous Dirac is `τ + λ(μ_t + m x)`. -/// Equation 5 of the Table 3 first-occasion TD predictor is -/// `τ + λ(μ_t + e^{a Δt} t0_m x0)`. Those are not the same map. +/// `λ² (B / a)² v` uses the asymptotic unit effect `-B / a` and +/// requires stable `a < 0`. `λ² t0_b² v` uses free first-occasion +/// `T0TIPREDEFFECT`. Those are not the same map. /// /// # Errors /// /// Always returns -/// [`PsychometricError::ImpulseObservedMeanIsNotInitialTimeDependentObservedMean`]. -pub fn refuse_impulse_observed_mean_as_initial_time_dependent_observed_mean( - impulse_observed_mean: f64, - initial_time_dependent_observed_mean: f64, +/// [`PsychometricError::AsymptoticTimeIndependentObservedVarianceIsNotInitialTimeIndependentObservedVariance`]. +pub fn refuse_asymptotic_time_independent_observed_variance_as_initial_time_independent_observed_variance( + asymptotic_observed_predictor_variance: f64, + initial_observed_predictor_variance: f64, ) -> Result { - let _ = (impulse_observed_mean, initial_time_dependent_observed_mean); - Err(PsychometricError::ImpulseObservedMeanIsNotInitialTimeDependentObservedMean) + let _ = ( + asymptotic_observed_predictor_variance, + initial_observed_predictor_variance, + ); + Err( + PsychometricError::AsymptoticTimeIndependentObservedVarianceIsNotInitialTimeIndependentObservedVariance, + ) } -/// Refuse treating the impulse-carry observed mean as the -/// first-occasion time-dependent-predictor observed mean. +/// Refuse treating Eq. 5 of §7.2 `addedTIPREDVAR` as stationary +/// observed-indicator variance. /// -/// Equation 5 of the Eq. 1–2 carried latent mean is -/// `τ + λ(μ_t + e^{a(t−u)} m x)`. Equation 5 of the Table 3 -/// first-occasion TD predictor is `τ + λ(μ_t + e^{a Δt} t0_m x0)`. -/// Those are not the same map. +/// `λ² (B / a)² v` is extra observed TI variance. `λ² p + θ` is +/// stationary observed-indicator variance. Those are not the same +/// map. /// /// # Errors /// /// Always returns -/// [`PsychometricError::ImpulseCarryObservedMeanIsNotInitialTimeDependentObservedMean`]. -pub fn refuse_impulse_carry_observed_mean_as_initial_time_dependent_observed_mean( - impulse_carry_observed_mean: f64, - initial_time_dependent_observed_mean: f64, +/// [`PsychometricError::AsymptoticTimeIndependentObservedVarianceIsNotStationaryObservedVariance`]. +pub fn refuse_asymptotic_time_independent_observed_variance_as_stationary_observed_variance( + asymptotic_observed_predictor_variance: f64, + stationary_observed_variance: f64, ) -> Result { let _ = ( - impulse_carry_observed_mean, - initial_time_dependent_observed_mean, + asymptotic_observed_predictor_variance, + stationary_observed_variance, ); - Err(PsychometricError::ImpulseCarryObservedMeanIsNotInitialTimeDependentObservedMean) + Err(PsychometricError::AsymptoticTimeIndependentObservedVarianceIsNotStationaryObservedVariance) } -/// Refuse treating the first-occasion TI observed mean as the -/// first-occasion TD observed mean. +/// Refuse treating Eq. 5 of §7.2 `addedTIPREDVAR` as `MANIFESTVAR`. /// -/// Equation 5 of Table 3 `T0TIPREDEFFECT` is -/// `τ + λ(μ_t + e^{a Δt} t0_b z)`. Equation 5 of Table 3 -/// `T0TDPREDEFFECT` is `τ + λ(μ_t + e^{a Δt} t0_m x0)`. Those are -/// not the same map. +/// `λ² (B / a)² v` is extra observed TI variance. Table 2 names +/// `MANIFESTVAR` as `Θ`, the variance of `ζ`. Those are not the +/// same map. /// /// # Errors /// /// Always returns -/// [`PsychometricError::InitialTimeIndependentObservedMeanIsNotInitialTimeDependentObservedMean`]. -pub fn refuse_initial_time_independent_observed_mean_as_initial_time_dependent_observed_mean( - initial_time_independent_observed_mean: f64, - initial_time_dependent_observed_mean: f64, +/// [`PsychometricError::AsymptoticTimeIndependentObservedVarianceIsNotMeasurementError`]. +pub fn refuse_asymptotic_time_independent_observed_variance_as_measurement_error( + asymptotic_observed_predictor_variance: f64, + measurement_error_variance: f64, ) -> Result { let _ = ( - initial_time_independent_observed_mean, - initial_time_dependent_observed_mean, + asymptotic_observed_predictor_variance, + measurement_error_variance, ); - Err(PsychometricError::InitialTimeIndependentObservedMeanIsNotInitialTimeDependentObservedMean) + Err(PsychometricError::AsymptoticTimeIndependentObservedVarianceIsNotMeasurementError) } -/// Exact scalar within-interval time-dependent impulse carry from -/// Driver Equations 1–2. +/// Exact scalar Table 2 `asymCINT`. /// -/// Driver, Oud, and Voelkle (2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; -/// §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z from +/// Driver, Oud, and Voelkle (2017, Table 2, p. 12; Eq. 3, p. 5; +/// §4.3 / p. 16; JSS PDF opened 2026-08-21T16:13Z from /// ) -/// write `dη = (A η + ξ + B z + M χ(t)) dt + G dW` with -/// `χ_i(t) = Σ_{u ∈ U_i} x_{i,u} δ(t − u)`. The Green-function -/// integral of that Dirac on `(t0, t)` is `e^{A(t−u)} M x`. The -/// printed Eq. 3 fourth summand is the contemporaneous jump `M x` -/// at `u = t`. This map is the strictly within-interval case -/// `t0 < u < t`: form `m x` first, then `e^{a(t−u)} m x`. A zero -/// drift is `m x` with no dissipation. Binary64 underflow of -/// `e^{a(t−u)}` to `+0` is vanishing dissipation back to the process -/// mean (§7.2) and is kept. A zero effect or zero predictor is -/// exactly zero even if the exponential overflows. When `e^{a(t−u)}` -/// overflows at a finite `a(t−u)`, rewrite as -/// `sign(m x) exp(ln|m x| + a(t−u))`. An impulse at `u = t` is the -/// contemporaneous map. An impulse at `u ≤ t0` is already in `η(t0)`. -/// The §7.2 level-change form is a different specification and is -/// not this map. This is not a Kalman filter and not ctsem -/// estimation. +/// name `asymCINT` the asymptotic (`Δt = ∞`) expected change in +/// processes for a 1 unit change in intercept (`CINT`). Table 2 names +/// `κ` `CINT`. Equation 3 maps a finite event interval as +/// `A^{-1}[e^{A Δt} − I] κ`. For stable `a < 0` that `Δt → ∞` limit +/// is `-A^{-1} κ`. The scalar map is `-κ / a`. A unit intercept is +/// `-1 / a`. Form `κ` first, then divide by `-a`. A zero intercept is +/// exactly zero. `a ≥ 0` cannot hold a finite process-mean change and +/// fails closed. `-κ / a` is not `κ`, not the finite-interval +/// increment `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not +/// `asymTIPREDEFFECT` `-B z / a`. Page 16 notes that a `T0MEANS` +/// stationarity constraint includes time-independent predictors; that +/// composition is not this intercept-only map. The printed 2-latent +/// `CINT` values are not this scalar map. This is not a Kalman filter, +/// not a matrix `expm`, and not ctsem estimation. /// /// # Errors /// /// Returns [`PsychometricError::EventTimeRequired`] for any non-event -/// clock, [`PsychometricError::NonPositiveInterval`] when -/// `event_delta` or `elapsed_after_impulse` is not strictly positive -/// or the impulse is not strictly inside `(t0, t)`, and -/// [`PsychometricError::InvalidNumericInput`] when an input is -/// non-finite or `m x` or the carried product overflows. -pub fn recover_time_dependent_predictor_impulse_carry( - time_dependent_effect: f64, - time_dependent_predictor: f64, +/// clock, [`PsychometricError::AsymptoticContinuousInterceptRequiresStableDrift`] +/// when the drift is not strictly negative and the intercept is +/// nonzero, and [`PsychometricError::InvalidNumericInput`] when an +/// input is non-finite or the quotient overflows. +pub fn recover_asymptotic_continuous_intercept( + continuous_intercept: f64, log_rate: f64, - event_delta: f64, - elapsed_after_impulse: f64, clock: LagClock, ) -> Result { if !clock.admits_structural_lag() { return Err(PsychometricError::EventTimeRequired); } - if !event_delta.is_finite() || event_delta <= 0.0 { - return Err(PsychometricError::NonPositiveInterval); - } - if !elapsed_after_impulse.is_finite() || elapsed_after_impulse <= 0.0 { - return Err(PsychometricError::NonPositiveInterval); - } - // I_{t0 < u < t}: t−u strictly less than t−t0, so u−t0 > 0. - if elapsed_after_impulse >= event_delta { - return Err(PsychometricError::NonPositiveInterval); - } - if !log_rate.is_finite() { + if !continuous_intercept.is_finite() || !log_rate.is_finite() { return Err(PsychometricError::InvalidNumericInput); } - let impulse = - recover_time_dependent_predictor_impulse(time_dependent_effect, time_dependent_predictor)?; - if impulse == 0.0 { + if continuous_intercept == 0.0 { return Ok(0.0); } - let drift_interval = log_rate * elapsed_after_impulse; - let auto_effect = drift_interval.exp(); - if auto_effect.is_finite() { - // +0 underflow is vanishing dissipation (§7.2). - return require_finite(auto_effect * impulse); + if log_rate >= 0.0 { + return Err(PsychometricError::AsymptoticContinuousInterceptRequiresStableDrift); } - // Overflow of a finite `a(t−u)` is the log-space rewrite. - // A non-finite argument also fails closed through `require_finite`. - // e^{a(t−u)} m x = sign(m x) exp(ln|m x| + a(t−u)). - require_finite(impulse.signum() * (impulse.abs().ln() + drift_interval).exp()) + require_finite(continuous_intercept / -log_rate) } -/// Exact scalar evolved latent mean plus a within-interval impulse carry. +/// Refuse treating Table 2 `asymCINT` as `CINT`. /// -/// Driver, Oud, and Voelkle (2017, Eq. 1–3, p. 5; §7.2) write the -/// first two summands as the carried `T0MEANS` and `CINT` increment, -/// then add a Dirac impulse that occurred strictly inside `(t0, t)` -/// after it has dissipated by `e^{A(t−u)}`. Form `μ_t` first, then -/// add `e^{a(t−u)} m x`. A zero carry is exactly `μ_t`. A zero -/// evolved mean is exactly the carry. Adding the contemporaneous -/// `m x` is not this composition when `u ≠ t`. The level-change -/// form is not this map. +/// `-κ / a` is the expected change in process means. Table 2 names +/// `κ` `CINT`. The intercept is not that total change. /// /// # Errors /// -/// Propagates [`recover_discrete_latent_mean`] and -/// [`recover_time_dependent_predictor_impulse_carry`], and returns -/// [`PsychometricError::InvalidNumericInput`] when the sum overflows. -#[allow(clippy::too_many_arguments)] -pub fn recover_discrete_latent_mean_with_impulse_carry( - initial_latent_mean: f64, - log_rate: f64, +/// Always returns +/// [`PsychometricError::AsymptoticContinuousInterceptIsNotContinuousIntercept`]. +pub fn refuse_asymptotic_continuous_intercept_as_continuous_intercept( + asymptotic_intercept: f64, continuous_intercept: f64, - time_dependent_effect: f64, - time_dependent_predictor: f64, - event_delta: f64, - elapsed_after_impulse: f64, - clock: LagClock, ) -> Result { - let evolved_latent_mean = recover_discrete_latent_mean( - initial_latent_mean, - log_rate, - continuous_intercept, - event_delta, - clock, - )?; - let impulse_carry = recover_time_dependent_predictor_impulse_carry( - time_dependent_effect, - time_dependent_predictor, - log_rate, - event_delta, - elapsed_after_impulse, - clock, - )?; - if impulse_carry == 0.0 { - return Ok(evolved_latent_mean); - } - if evolved_latent_mean == 0.0 { - return Ok(impulse_carry); - } - require_finite(evolved_latent_mean + impulse_carry) + let _ = (asymptotic_intercept, continuous_intercept); + Err(PsychometricError::AsymptoticContinuousInterceptIsNotContinuousIntercept) } -/// Exact scalar observed mean of a within-interval impulse carry. +/// Refuse treating Table 2 `asymCINT` as the finite-interval discrete +/// intercept increment. /// -/// Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Eq. 1–2, pp. 4–5; -/// Eq. 3 exponential map; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF -/// re-opened 2026-08-20T05:12Z from -/// ) -/// write `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and -/// `Γ ~ N(τ, Ψ)`. The expected intercept is `τ`. The latent process -/// at `t` after a Dirac that occurred strictly inside `(t0, t)` is -/// `μ_t + e^{a(t−u)} m x`. The scalar composition is -/// `E(y_t) = τ + λ(μ_t + e^{a(t−u)} m x)`. Form the carried latent -/// mean first, then `τ + λ` of that mean. Table 2 names `τ` -/// `MANIFESTMEANS`. A zero loading is exactly `τ`. A zero -/// evolved-plus-carry latent mean is exactly `τ`. A zero intercept -/// is exactly `λ(μ_t + carry)`. The evolved observed mean -/// `τ + λ μ_t` is not this composition when the carry is nonzero. -/// The contemporaneous map `τ + λ(μ_t + m x)` is not this -/// composition when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The -/// carried latent mean is not `E(y_t)`. The §7.2 level-change form -/// is a different specification and is not this map. This is not a -/// Kalman filter and not ctsem estimation. +/// `-κ / a` is the `Δt → ∞` limit of `A^{-1}[e^{A Δt} − I] κ` under +/// stable `a < 0`. A finite event interval is not that limit. /// /// # Errors /// -/// Propagates [`recover_discrete_latent_mean_with_impulse_carry`] and -/// [`recover_manifest_observed_mean`]. -#[allow(clippy::too_many_arguments)] -pub fn recover_discrete_observed_mean_with_impulse_carry( - loading: f64, - initial_latent_mean: f64, - log_rate: f64, - continuous_intercept: f64, - time_dependent_effect: f64, - time_dependent_predictor: f64, - manifest_mean: f64, - event_delta: f64, - elapsed_after_impulse: f64, - clock: LagClock, +/// Always returns +/// [`PsychometricError::AsymptoticContinuousInterceptIsNotDiscreteIncrement`]. +pub fn refuse_asymptotic_continuous_intercept_as_discrete_increment( + asymptotic_intercept: f64, + discrete_increment: f64, ) -> Result { - let carried_latent_mean = recover_discrete_latent_mean_with_impulse_carry( - initial_latent_mean, - log_rate, - continuous_intercept, - time_dependent_effect, - time_dependent_predictor, - event_delta, - elapsed_after_impulse, - clock, - )?; - recover_manifest_observed_mean(loading, carried_latent_mean, manifest_mean) + let _ = (asymptotic_intercept, discrete_increment); + Err(PsychometricError::AsymptoticContinuousInterceptIsNotDiscreteIncrement) } -/// Refuse treating the evolved observed mean as the impulse-carry -/// observed mean. +/// Refuse treating Table 2 `asymCINT` as `T0MEANS`. /// -/// Equation 5 of the Eq. 3 evolved mean is `τ + λ μ_t`. Equation 5 -/// of the Eq. 1–2 carried latent mean is -/// `τ + λ(μ_t + e^{a(t−u)} m x)`. Those are not the same map. +/// `-κ / a` is the intercept contribution to the stationary process +/// mean. Table 2 names `μ_0` `T0MEANS`. Those are not the same map. /// /// # Errors /// /// Always returns -/// [`PsychometricError::EvolvedObservedMeanIsNotImpulseCarryObservedMean`]. -pub fn refuse_evolved_observed_mean_as_impulse_carry_observed_mean( - evolved_observed_mean: f64, - impulse_carry_observed_mean: f64, +/// [`PsychometricError::AsymptoticContinuousInterceptIsNotInitialLatentMean`]. +pub fn refuse_asymptotic_continuous_intercept_as_initial_latent_mean( + asymptotic_intercept: f64, + initial_latent_mean: f64, ) -> Result { - let _ = (evolved_observed_mean, impulse_carry_observed_mean); - Err(PsychometricError::EvolvedObservedMeanIsNotImpulseCarryObservedMean) + let _ = (asymptotic_intercept, initial_latent_mean); + Err(PsychometricError::AsymptoticContinuousInterceptIsNotInitialLatentMean) } -/// Refuse treating the Eq. 1–2 impulse carry as the contemporaneous Dirac. +/// Refuse treating Table 2 `asymCINT` as `asymTIPREDEFFECT`. /// -/// The printed Eq. 3 fourth summand is `M x` at `u = t`. The -/// within-interval carry is `e^{A(t−u)} M x` for `t0 < u < t`. +/// `-κ / a` is the intercept contribution. `-B z / a` is the +/// time-independent predictor contribution. Page 16 notes that a +/// `T0MEANS` stationarity constraint includes time-independent +/// predictors; that composition is not this intercept-only map. /// /// # Errors /// /// Always returns -/// [`PsychometricError::TimeDependentImpulseCarryIsNotContemporaneousImpulse`]. -pub fn refuse_time_dependent_impulse_carry_as_contemporaneous_impulse( - time_dependent_impulse_carry: f64, - time_dependent_impulse: f64, +/// [`PsychometricError::AsymptoticContinuousInterceptIsNotAsymptoticTimeIndependentEffect`]. +pub fn refuse_asymptotic_continuous_intercept_as_asymptotic_time_independent_effect( + asymptotic_intercept: f64, + asymptotic_time_independent_effect: f64, ) -> Result { - let _ = (time_dependent_impulse_carry, time_dependent_impulse); - Err(PsychometricError::TimeDependentImpulseCarryIsNotContemporaneousImpulse) + let _ = (asymptotic_intercept, asymptotic_time_independent_effect); + Err(PsychometricError::AsymptoticContinuousInterceptIsNotAsymptoticTimeIndependentEffect) } -/// Refuse treating the Eq. 1–2 impulse carry as `CINT`. +/// Exact scalar p. 16 stationary `T0MEANS`. /// -/// Table 2 names `M` `TDPREDEFFECT` and `κ` `CINT`. The dissipated -/// impulse is not the continuous intercept. +/// Driver, Oud, and Voelkle (2017, p. 16; Table 2, p. 12; Eq. 3, p. 5; +/// JSS PDF opened 2026-08-21T16:13Z from +/// ) +/// constrain `T0MEANS` to the model-implied values using +/// `T0MEANSbase` / `T0MEANSfree` when the first observation is +/// determined by the process in the same way as later observations. +/// Those constraints include extra effects due to time-independent +/// predictors (`asymTIPREDEFFECT`). Table 2 names `κ` `CINT` and +/// names `asymCINT` the `Δt → ∞` intercept contribution `-κ / a`. +/// For stable `a < 0` the scalar composition is +/// `-κ / a + −B z / a`. Form the intercept contribution first, then +/// include the TI extra effect, then add. A zero intercept and a zero +/// TI contribution is exactly zero. `a ≥ 0` cannot hold a finite +/// process-mean change when either contribution is nonzero and fails +/// closed. That constrained first-occasion mean is not free +/// `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and +/// not the finite-interval discrete latent mean +/// `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`. The printed 2-latent +/// `T0MEANS` 2.823 is not this scalar map. This is not a Kalman +/// filter, not a matrix `expm`, and not ctsem estimation. /// /// # Errors /// -/// Always returns -/// [`PsychometricError::TimeDependentImpulseCarryIsNotContinuousIntercept`]. -pub fn refuse_time_dependent_impulse_carry_as_continuous_intercept( - time_dependent_impulse_carry: f64, +/// Returns [`PsychometricError::EventTimeRequired`] for any non-event +/// clock, [`PsychometricError::AsymptoticContinuousInterceptRequiresStableDrift`] +/// or [`PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift`] +/// when the drift is not strictly negative and the corresponding +/// contribution is nonzero, and [`PsychometricError::InvalidNumericInput`] +/// when an input is non-finite or a quotient or sum overflows. +pub fn recover_stationary_initial_latent_mean( continuous_intercept: f64, + time_independent_effect: f64, + time_independent_predictor: f64, + log_rate: f64, + clock: LagClock, ) -> Result { - let _ = (time_dependent_impulse_carry, continuous_intercept); - Err(PsychometricError::TimeDependentImpulseCarryIsNotContinuousIntercept) + let intercept = recover_asymptotic_continuous_intercept(continuous_intercept, log_rate, clock)?; + let tipred = recover_asymptotic_time_independent_predictor_effect( + time_independent_effect, + time_independent_predictor, + log_rate, + clock, + )?; + require_finite(intercept + tipred) } -/// Refuse treating the Eq. 1–2 impulse carry as `TIPREDEFFECT`. +/// Refuse treating p. 16 stationary `T0MEANS` as free `T0MEANS`. /// -/// The second-summand map integrates a constant `B z` over the event -/// interval. The within-interval TDPRED carry dissipates a Dirac. +/// `-κ / a + −B z / a` is the constrained first-occasion mean. Table 2 +/// names the free first-occasion latent mean `T0MEANS`. Those are not +/// the same map. /// /// # Errors /// /// Always returns -/// [`PsychometricError::TimeDependentImpulseCarryIsNotTimeIndependentEffect`]. -pub fn refuse_time_dependent_impulse_carry_as_time_independent_effect( - time_dependent_impulse_carry: f64, - time_independent_effect: f64, +/// [`PsychometricError::StationaryInitialLatentMeanIsNotInitialLatentMean`]. +pub fn refuse_stationary_initial_latent_mean_as_initial_latent_mean( + stationary_mean: f64, + initial_latent_mean: f64, ) -> Result { - let _ = (time_dependent_impulse_carry, time_independent_effect); - Err(PsychometricError::TimeDependentImpulseCarryIsNotTimeIndependentEffect) + let _ = (stationary_mean, initial_latent_mean); + Err(PsychometricError::StationaryInitialLatentMeanIsNotInitialLatentMean) } -/// Refuse treating the Eq. 1–2 impulse carry as Voelkle et al. -/// (2012, Eq. 14). +/// Refuse treating p. 16 stationary `T0MEANS` as `asymCINT`. /// -/// Equation 14 is `a_{yx} Δt` for a piecewise-constant time-varying -/// predictor. The Dirac carry is `e^{A(t−u)} M x`. +/// The constraint includes time-independent predictors. `-κ / a` is +/// the intercept contribution and is not that composition when +/// `B z ≠ 0`. /// /// # Errors /// /// Always returns -/// [`PsychometricError::TimeDependentImpulseCarryIsNotTimeVaryingDiscreteEffect`]. -pub fn refuse_time_dependent_impulse_carry_as_time_varying_discrete_effect( - time_dependent_impulse_carry: f64, - time_varying_discrete_effect: f64, +/// [`PsychometricError::StationaryInitialLatentMeanIsNotAsymptoticContinuousIntercept`]. +pub fn refuse_stationary_initial_latent_mean_as_asymptotic_continuous_intercept( + stationary_mean: f64, + asymptotic_intercept: f64, ) -> Result { - let _ = (time_dependent_impulse_carry, time_varying_discrete_effect); - Err(PsychometricError::TimeDependentImpulseCarryIsNotTimeVaryingDiscreteEffect) + let _ = (stationary_mean, asymptotic_intercept); + Err(PsychometricError::StationaryInitialLatentMeanIsNotAsymptoticContinuousIntercept) } -/// Refuse treating Driver Table 2 `T0MEANS` as the evolved latent mean. +/// Refuse treating p. 16 stationary `T0MEANS` as `asymTIPREDEFFECT`. /// -/// Equation 3 maps `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`. -/// `T0MEANS` is `μ_0`, not `μ_t`. +/// The constraint includes the intercept contribution. `-B z / a` is +/// the TI extra effect and is not that composition when `κ ≠ 0`. /// /// # Errors /// /// Always returns -/// [`PsychometricError::InitialLatentMeanIsNotEvolvedMean`]. -pub fn refuse_initial_latent_mean_as_evolved_mean( - initial_latent_mean: f64, - evolved_latent_mean: f64, +/// [`PsychometricError::StationaryInitialLatentMeanIsNotAsymptoticTimeIndependentEffect`]. +pub fn refuse_stationary_initial_latent_mean_as_asymptotic_time_independent_effect( + stationary_mean: f64, + asymptotic_time_independent_effect: f64, ) -> Result { - let _ = (initial_latent_mean, evolved_latent_mean); - Err(PsychometricError::InitialLatentMeanIsNotEvolvedMean) + let _ = (stationary_mean, asymptotic_time_independent_effect); + Err(PsychometricError::StationaryInitialLatentMeanIsNotAsymptoticTimeIndependentEffect) } -/// Refuse treating Driver Table 2 `CINT` as the discrete mean increment. +/// Refuse treating p. 16 stationary `T0MEANS` as a finite-interval +/// discrete latent mean. /// -/// `κ` is the continuous intercept. Equation 3 maps it through -/// `A^{-1}[e^{A Δt} − I]`. `κ` is not that increment. +/// `-κ / a + −B z / a` is the `Δt → ∞` constrained first-occasion +/// mean. `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` is a finite event +/// interval and is not that limit. /// /// # Errors /// /// Always returns -/// [`PsychometricError::ContinuousInterceptIsNotDiscreteMeanIncrement`]. -pub fn refuse_continuous_intercept_as_discrete_mean_increment( - continuous_intercept: f64, - discrete_mean_increment: f64, +/// [`PsychometricError::StationaryInitialLatentMeanIsNotDiscreteMean`]. +pub fn refuse_stationary_initial_latent_mean_as_discrete_mean( + stationary_mean: f64, + discrete_mean: f64, ) -> Result { - let _ = (continuous_intercept, discrete_mean_increment); - Err(PsychometricError::ContinuousInterceptIsNotDiscreteMeanIncrement) + let _ = (stationary_mean, discrete_mean); + Err(PsychometricError::StationaryInitialLatentMeanIsNotDiscreteMean) } -/// Refuse treating Driver Table 2 `CINT` as `T0MEANS`. +/// Exact scalar observed mean of §4.3 stationary `T0MEANS`. /// -/// Table 2 (p. 12) names `κ` `CINT` and the first-occasion latent -/// mean `T0MEANS`. `κ` is not `E(η_{i1})`. +/// Driver, Oud, and Voelkle (2017, §4.3, pp. 9–10; Eq. 5, p. 5; +/// Table 2, p. 12; Eq. 3, p. 5; JSS PDF re-opened 2026-08-21T20:07Z +/// from +/// ) +/// constrain the first-occasion mean to the model-predicted mean +/// when `stationary` includes `"T0MEANS"`. Equation 5 writes +/// `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and +/// `Γ ~ N(τ, Ψ)`. The constrained latent mean is +/// `-κ / a + −B z / a`. The scalar composition is +/// `E(y_0) = τ + λ(−κ / a + −B z / a)`. Form the stationary latent +/// mean first, then `τ + λ` of that mean. A zero loading is exactly +/// `τ`. A zero intercept and a zero TI contribution is exactly `τ`. +/// `τ + λ μ_0` for free `T0MEANS` is not this composition. +/// `τ + λ(−κ / a)` is not this composition when `B z ≠ 0`. +/// `τ + λ μ_t` is not this composition. `MANIFESTMEANS` is not +/// `E(y_0)`. The constrained latent mean is not `E(y_0)`. This is +/// not a Kalman filter, not a matrix `expm`, and not ctsem +/// estimation. /// /// # Errors /// -/// Always returns -/// [`PsychometricError::ContinuousInterceptIsNotInitialLatentMean`]. -pub fn refuse_continuous_intercept_as_initial_latent_mean( +/// Propagates [`recover_stationary_initial_latent_mean`] and +/// [`recover_manifest_observed_mean`]. +#[allow(clippy::too_many_arguments)] +pub fn recover_stationary_initial_observed_mean( + loading: f64, continuous_intercept: f64, - initial_latent_mean: f64, + time_independent_effect: f64, + time_independent_predictor: f64, + log_rate: f64, + manifest_mean: f64, + clock: LagClock, ) -> Result { - let _ = (continuous_intercept, initial_latent_mean); - Err(PsychometricError::ContinuousInterceptIsNotInitialLatentMean) -} - -/// Refuse treating Driver Eq. 3 process noise as the unconditional variance. -/// -/// Driver, Oud, and Voelkle (2017, Eq. 3–4, pp. 4–5): -/// `Q_Δt = cov(η_ti | η_{t-1,i})` for the homogeneous process. That -/// residual variance is not `Var(η_ti)` when the previous state is -/// random. The JSS article has no numbered §2.2. + let stationary_latent_mean = recover_stationary_initial_latent_mean( + continuous_intercept, + time_independent_effect, + time_independent_predictor, + log_rate, + clock, + )?; + recover_manifest_observed_mean(loading, stationary_latent_mean, manifest_mean) +} + +/// Refuse treating §4.3 stationary `T0MEANS` as `E(y_0)`. +/// +/// `−κ / a + −B z / a` is the constrained latent mean. Equation 5 +/// maps `E(y_0) = τ + λ` of that mean. /// /// # Errors /// -/// Always returns [`PsychometricError::ProcessNoiseIsConditionalVariance`]. -pub fn refuse_process_noise_as_unconditional_variance( - process_noise: f64, - prior_variance: f64, +/// Always returns +/// [`PsychometricError::StationaryInitialLatentMeanIsNotObservedMean`]. +pub fn refuse_stationary_initial_latent_mean_as_observed_mean( + stationary_latent_mean: f64, + stationary_observed_mean: f64, ) -> Result { - let _ = (process_noise, prior_variance); - Err(PsychometricError::ProcessNoiseIsConditionalVariance) + let _ = (stationary_latent_mean, stationary_observed_mean); + Err(PsychometricError::StationaryInitialLatentMeanIsNotObservedMean) } -/// Refuse the difference quotient as a continuous-time rate. +/// Refuse treating `MANIFESTMEANS` as Eq. 5 of §4.3 stationary +/// `T0MEANS`. /// -/// Voelkle et al. (2012) discourage `(x(t+Δt) − x(t)) / Δt` as the drift. +/// Table 2 names `τ` `MANIFESTMEANS`. `τ + λ(−κ / a + −B z / a)` is +/// not `τ` when the loading and constrained mean are nonzero. /// /// # Errors /// -/// Always returns [`PsychometricError::DifferenceQuotientForbidden`]. -pub fn refuse_difference_quotient_as_local_rate( - earlier: f64, - later: f64, - delta: f64, +/// Always returns +/// [`PsychometricError::StationaryInitialObservedMeanIsNotManifestMeans`]. +pub fn refuse_stationary_initial_observed_mean_as_manifest_means( + stationary_observed_mean: f64, + manifest_mean: f64, ) -> Result { - let _ = (earlier, later, delta); - Err(PsychometricError::DifferenceQuotientForbidden) + let _ = (stationary_observed_mean, manifest_mean); + Err(PsychometricError::StationaryInitialObservedMeanIsNotManifestMeans) } -/// Mean local log-rate across consecutive event-time pairs. +/// Refuse treating evolved `τ + λ μ_t` as Eq. 5 of §4.3 stationary +/// `T0MEANS`. /// -/// Occasions are sorted by event time. Each pair uses the exact scalar map. -/// Equal or inverted times fail closed. +/// A finite-interval evolved observed mean is not the constrained +/// first-occasion observed mean. /// /// # Errors /// -/// Returns [`PsychometricError::EventTimeRequired`] for a non-event clock, -/// [`PsychometricError::InvalidNumericInput`] for fewer than two occasions or -/// non-finite values, and [`PsychometricError::NonPositiveInterval`] when -/// consecutive times are not strictly increasing. -pub fn recover_event_series_mean_log_rate( - occasions: &[EventOccasion], - clock: LagClock, +/// Always returns +/// [`PsychometricError::EvolvedObservedMeanIsNotStationaryInitialObservedMean`]. +pub fn refuse_evolved_observed_mean_as_stationary_initial_observed_mean( + evolved_observed_mean: f64, + stationary_observed_mean: f64, ) -> Result { - if !clock.admits_structural_lag() { - return Err(PsychometricError::EventTimeRequired); - } - if occasions.len() < 2 { - return Err(PsychometricError::InvalidNumericInput); - } - let mut ordered = occasions.to_vec(); - ordered.sort_by(|left, right| { - left.event_time - .partial_cmp(&right.event_time) - .unwrap_or(std::cmp::Ordering::Equal) - }); - let mut rates = Vec::new(); - for window in ordered.windows(2) { - let earlier = window[0]; - let later = window[1]; - if !earlier.event_time.is_finite() - || !later.event_time.is_finite() - || !earlier.score.is_finite() - || !later.score.is_finite() - { - return Err(PsychometricError::InvalidNumericInput); - } - let delta = later.event_time - earlier.event_time; - let recovered = - recover_event_time_discrete_lag_and_log_rate(earlier.score, later.score, delta, clock)?; - rates.push(recovered.log_rate); - } - let count = rates.len() as f64; - require_finite(rates.iter().sum::() / count) + let _ = (evolved_observed_mean, stationary_observed_mean); + Err(PsychometricError::EvolvedObservedMeanIsNotStationaryInitialObservedMean) } -/// Local log-rate of cluster-mean-centered residuals on event time. +/// Refuse treating `τ + λ(−κ / a)` as Eq. 5 of §4.3 stationary +/// `T0MEANS`. /// -/// Stable between-cluster means are removed first (CWC). Consecutive -/// within-cluster residuals then use the exact scalar map. This is not DSEM. +/// The constraint includes time-independent predictors. +/// `τ + λ(−κ / a)` is not that composition when `B z ≠ 0`. /// -/// Curran and Bauer (2011, pp. 607–608) show that subtracting the observed -/// person-specific mean from a raw autoregressive series does **not** isolate -/// the lagged within-person effect. This helper therefore does not claim to -/// recover the raw-process drift `a` from CWC of a raw AR path. For that -/// estimand, supply already-centered lagged residuals to -/// [`recover_irregular_centered_residual_log_rate`]. +/// # Errors +/// +/// Always returns +/// [`PsychometricError::AsymptoticContinuousInterceptObservedMeanIsNotStationaryInitialObservedMean`]. +pub fn refuse_asymptotic_continuous_intercept_observed_mean_as_stationary_initial_observed_mean( + asymptotic_intercept_observed_mean: f64, + stationary_observed_mean: f64, +) -> Result { + let _ = (asymptotic_intercept_observed_mean, stationary_observed_mean); + Err( + PsychometricError::AsymptoticContinuousInterceptObservedMeanIsNotStationaryInitialObservedMean, + ) +} + +/// Refuse treating `τ + λ μ_0` as Eq. 5 of §4.3 stationary +/// `T0MEANS`. +/// +/// Free first-occasion `T0MEANS` is not the constrained +/// first-occasion mean. /// /// # Errors /// -/// Returns [`PsychometricError::EventTimeRequired`] for a non-event clock, -/// [`PsychometricError::InvalidNumericInput`] for empty, singleton, or -/// non-finite rows, [`PsychometricError::InsufficientClusters`] when fewer -/// than two clusters appear, and interval/lag errors from the scalar map. -pub fn recover_within_residual_event_time_log_rate( - rows: &[ClusteredEventScore], - clock: LagClock, +/// Always returns +/// [`PsychometricError::InitialObservedMeanIsNotStationaryInitialObservedMean`]. +pub fn refuse_initial_observed_mean_as_stationary_initial_observed_mean( + initial_observed_mean: f64, + stationary_observed_mean: f64, ) -> Result { - if !clock.admits_structural_lag() { - return Err(PsychometricError::EventTimeRequired); - } - if rows.len() < 2 { - return Err(PsychometricError::InvalidNumericInput); - } - let mut groups: BTreeMap> = BTreeMap::new(); - for &row in rows { - if !row.event_time.is_finite() || !row.score.is_finite() { - return Err(PsychometricError::InvalidNumericInput); - } - groups.entry(row.cluster_key).or_default().push(row); - } - if groups.len() < 2 { - return Err(PsychometricError::InsufficientClusters); - } - let mut pairs = Vec::new(); - for occasions in groups.values_mut() { - if occasions.len() < 2 { - continue; - } - let count = occasions.len() as f64; - let mean = occasions.iter().map(|row| row.score).sum::() / count; - occasions.sort_by(|left, right| { - left.event_time - .partial_cmp(&right.event_time) - .unwrap_or(std::cmp::Ordering::Equal) - }); - for window in occasions.windows(2) { - let earlier_resid = window[0].score - mean; - let later_resid = window[1].score - mean; - let delta = window[1].event_time - window[0].event_time; - if !delta.is_finite() || delta <= 0.0 { - return Err(PsychometricError::NonPositiveInterval); - } - if !(earlier_resid.is_finite() & later_resid.is_finite()) { - return Err(PsychometricError::InvalidNumericInput); - } - pairs.push((earlier_resid, later_resid, delta)); - } - } - fit_scalar_log_rate(&pairs) + let _ = (initial_observed_mean, stationary_observed_mean); + Err(PsychometricError::InitialObservedMeanIsNotStationaryInitialObservedMean) } -/// Mean exact scalar log-rate on already-centered residuals with irregular intervals. +/// Exact scalar §4.3 / p. 16 stationary `T0VAR`. /// -/// Each pair is `a = ln(later / earlier) / Δt` (Voelkle et al., 2012, Eq. 7). -/// The function does **not** center again. Curran and Bauer (2011, pp. 607–608) -/// reject person-mean subtraction on a raw autoregressive series as the -/// lagged within-person residual. Intervals may be irregular. This is not DSEM. +/// Driver, Oud, and Voelkle (2017, §4.3, pp. 9–10; p. 16; Table 2, +/// p. 12; §7.2, pp. 20–21; Eq. 4, p. 5; JSS PDF re-opened +/// 2026-08-22T03:07Z from +/// ) +/// constrain `T0VAR` to the model-predicted variance when +/// `stationary` includes `"T0VAR"`. Section 4.3 writes that the +/// first-occasion variances are constrained according to the model +/// predicted variances across all time points. Page 16 names +/// `asymDIFFUSION` the total within-subject variance as `Δt → ∞`. +/// For stable `a < 0` that scalar is `-q / (2 a)`. Section 4.3 +/// (p. 9) adds a stable trait process with `DRIFT` and `DIFFUSION` +/// fixed to zero (`TRAITVAR`). Section 7.2 names `addedTIPREDVAR` +/// the stable between-subject variance accounted for by +/// time-independent predictors; the scalar map is `(B / a)² v`. +/// The constrained first-occasion variance is +/// `trait + −q / (2 a) + (B / a)² v`. Form the within-subject +/// contribution first, then include the trait, then include the TI +/// extra variance, then add. A zero trait, a zero diffusion, and a +/// zero TI contribution is exactly zero. A zero diffusion and a +/// zero TI contribution is exactly the trait. `a ≥ 0` cannot hold a +/// finite process variance when the diffusion or the TI +/// contribution is nonzero and fails closed. Trait-only variance +/// does not require a stable drift. That constrained +/// first-occasion variance is not free `T0VAR`, not +/// `asymDIFFUSION` alone, not `TRAITVAR` alone, not +/// `addedTIPREDVAR` alone, and not the finite-interval discrete +/// latent variance `exp(2 a Δt) p + Q_Δt`. The printed 2-latent +/// `addedTIPREDVAR` 2.838 is not this scalar map. This is not a +/// Kalman filter, not a matrix `expm`, and not ctsem estimation. /// /// # Errors /// -/// Returns [`PsychometricError::EventTimeRequired`] for a non-event clock, -/// [`PsychometricError::InvalidNumericInput`] for an empty series or a -/// non-finite / non-positive residual ratio, and -/// [`PsychometricError::NonPositiveInterval`] when any interval is not -/// strictly positive. -pub fn recover_irregular_centered_residual_log_rate( - pairs: &[LaggedWithinResidual], +/// Returns [`PsychometricError::EventTimeRequired`] for any +/// non-event clock, +/// [`PsychometricError::StationaryVarianceRequiresStableDrift`] +/// when the diffusion is nonzero and the drift is not strictly +/// negative, +/// [`PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift`] +/// when the TI contribution is nonzero and the drift is not +/// strictly negative, and +/// [`PsychometricError::InvalidNumericInput`] when an input is +/// non-finite, a variance is negative, or a product or sum +/// overflows. +pub fn recover_stationary_initial_latent_variance( + trait_variance: f64, + continuous_diffusion: f64, + time_independent_effect: f64, + predictor_variance: f64, + log_rate: f64, clock: LagClock, ) -> Result { if !clock.admits_structural_lag() { return Err(PsychometricError::EventTimeRequired); } - if pairs.is_empty() { - return Err(PsychometricError::InvalidNumericInput); - } - let mut sum = 0.0_f64; - for pair in pairs { - if !pair.earlier_residual.is_finite() - || !pair.later_residual.is_finite() - || !pair.event_delta.is_finite() - { - return Err(PsychometricError::InvalidNumericInput); - } - let recovered = recover_event_time_discrete_lag_and_log_rate( - pair.earlier_residual, - pair.later_residual, - pair.event_delta, - clock, - )?; - sum += recovered.log_rate; - } - let count = pairs.len() as f64; - require_finite(sum / count) + let state = if continuous_diffusion == 0.0 { + 0.0 + } else { + recover_stationary_latent_variance(continuous_diffusion, log_rate, clock)? + }; + let trait_plus_state = recover_trait_plus_state_latent_variance(trait_variance, state)?; + let added = recover_asymptotic_time_independent_predictor_variance( + time_independent_effect, + predictor_variance, + log_rate, + clock, + )?; + require_finite(trait_plus_state + added) } -/// Least-squares scalar log-rate for already-formed residual pairs. +/// Refuse treating §4.3 / p. 16 stationary `T0VAR` as free `T0VAR`. /// -/// Pair-wise logs initialize Newton. This helper is crate-visible so overflow -/// and flat-derivative guards can be recovered in unit tests. It is not a -/// public DSEM estimator. -pub(crate) fn fit_scalar_log_rate(pairs: &[(f64, f64, f64)]) -> Result { - if pairs.is_empty() { - return Err(PsychometricError::InvalidNumericInput); - } - let mut start_sum = 0.0_f64; - let mut start_count = 0.0_f64; - for &(earlier, later, delta) in pairs { - if earlier == 0.0 { - continue; - } - let discrete_lag = later / earlier; - if !discrete_lag.is_finite() || discrete_lag <= 0.0 { - continue; - } - start_sum += discrete_lag.ln() / delta; - start_count += 1.0; - } - if start_count <= 0.0 { - return Err(PsychometricError::InvalidNumericInput); - } - let mut log_rate = start_sum / start_count; - for _ in 0..16 { - let mut score = 0.0_f64; - let mut derivative = 0.0_f64; - for &(earlier, later, delta) in pairs { - let mapped = (log_rate * delta).exp(); - if !mapped.is_finite() || mapped <= 0.0 { - return Err(PsychometricError::InvalidNumericInput); - } - let weight = delta * earlier; - score += weight * mapped * later - delta * mapped * mapped * earlier * earlier; - derivative += delta * weight * mapped * later - - 2.0 * delta * delta * mapped * mapped * earlier * earlier; - } - if !score.is_finite() || !derivative.is_finite() { - return Err(PsychometricError::InvalidNumericInput); - } - if derivative.abs() <= 1e-18 { - break; - } - let next = log_rate - score / derivative; - if (next - log_rate).abs() < 1e-14 { - log_rate = next; - break; - } - log_rate = next; - } - require_finite(log_rate) +/// `trait + −q / (2 a) + (B / a)² v` is the constrained +/// first-occasion variance. Table 2 names the free first-occasion +/// latent variance `T0VAR`. Those are not the same map. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::StationaryInitialLatentVarianceIsNotInitialLatentVariance`]. +pub fn refuse_stationary_initial_latent_variance_as_initial_latent_variance( + stationary_variance: f64, + initial_latent_variance: f64, +) -> Result { + let _ = (stationary_variance, initial_latent_variance); + Err(PsychometricError::StationaryInitialLatentVarianceIsNotInitialLatentVariance) } -#[cfg(test)] -mod tests { - use super::{ - ClusteredEventScore, EventOccasion, LagClock, LaggedWithinResidual, fit_scalar_log_rate, - map_discrete_lag_across_event_intervals, recover_asymptotic_continuous_intercept, - recover_asymptotic_time_independent_predictor_effect, - recover_asymptotic_time_independent_predictor_variance, - recover_discrete_constant_predictor_effect, recover_discrete_continuous_intercept_effect, - recover_discrete_lag_from_log_rate, recover_discrete_lag_one, - recover_discrete_lagged_latent_covariance, recover_discrete_latent_mean, - recover_discrete_latent_mean_with_extra_process, - recover_discrete_latent_mean_with_extra_process_after, - recover_discrete_latent_mean_with_impulse, recover_discrete_latent_mean_with_impulse_carry, - recover_discrete_latent_mean_with_initial_time_dependent_predictor, - recover_discrete_latent_mean_with_initial_time_independent_predictor, - recover_discrete_latent_mean_with_time_independent_predictor, - recover_discrete_latent_variance, recover_discrete_observed_mean, - recover_discrete_observed_mean_with_extra_process, - recover_discrete_observed_mean_with_extra_process_after, - recover_discrete_observed_mean_with_impulse, - recover_discrete_observed_mean_with_impulse_carry, - recover_discrete_observed_mean_with_initial_time_dependent_predictor, - recover_discrete_observed_mean_with_initial_time_independent_predictor, - recover_discrete_observed_mean_with_time_independent_predictor, - recover_discrete_process_noise, recover_discrete_time_independent_predictor_effect, - recover_discrete_time_varying_predictor_effect, recover_event_series_mean_log_rate, - recover_event_time_discrete_lag_and_log_rate, - recover_initial_time_dependent_predictor_carry, - recover_initial_time_dependent_predictor_effect, - recover_initial_time_independent_predictor_carry, - recover_initial_time_independent_predictor_effect, - recover_irregular_centered_residual_log_rate, recover_level_change_continuous_intercept, - recover_level_change_discrete_increment, recover_level_change_extra_process_contribution, - recover_level_change_extra_process_contribution_after, recover_local_log_rate, - recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, - recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, - recover_stationary_initial_latent_mean, recover_stationary_initial_latent_variance, - recover_stationary_initial_observed_mean, recover_stationary_initial_observed_variance, - recover_stationary_lagged_latent_covariance, recover_stationary_lagged_observed_covariance, - recover_stationary_latent_variance, recover_stationary_later_latent_variance, - recover_stationary_later_observed_variance, recover_time_dependent_predictor_impulse, - recover_time_dependent_predictor_impulse_carry, recover_trait_plus_state_lagged_covariance, - recover_trait_plus_state_latent_variance, recover_within_residual_event_time_log_rate, - refuse_after_extra_process_contribution_as_observed_mean, - refuse_after_extra_process_latent_mean_as_observed_mean, - refuse_asymptotic_continuous_intercept_as_asymptotic_time_independent_effect, - refuse_asymptotic_continuous_intercept_as_continuous_intercept, - refuse_asymptotic_continuous_intercept_as_discrete_increment, - refuse_asymptotic_continuous_intercept_as_initial_latent_mean, - refuse_asymptotic_continuous_intercept_observed_mean_as_stationary_initial_observed_mean, - refuse_asymptotic_time_independent_effect_as_coefficient, - refuse_asymptotic_time_independent_effect_as_continuous_intercept, - refuse_asymptotic_time_independent_effect_as_discrete_effect, - refuse_asymptotic_time_independent_effect_as_time_dependent_impulse, - refuse_asymptotic_time_independent_variance_as_asymptotic_effect, - refuse_asymptotic_time_independent_variance_as_stationary_within_subject, - refuse_asymptotic_time_independent_variance_as_trait_variance, - refuse_continuous_intercept_as_discrete_mean_increment, - refuse_continuous_intercept_as_initial_latent_mean, - refuse_continuous_intercept_as_manifest_means, refuse_difference_quotient_as_local_rate, - refuse_evolved_observed_mean_as_after_extra_process_observed_mean, - refuse_evolved_observed_mean_as_extra_process_observed_mean, - refuse_evolved_observed_mean_as_impulse_carry_observed_mean, - refuse_evolved_observed_mean_as_impulse_observed_mean, - refuse_evolved_observed_mean_as_initial_time_dependent_observed_mean, - refuse_evolved_observed_mean_as_initial_time_independent_observed_mean, - refuse_evolved_observed_mean_as_stationary_initial_observed_mean, - refuse_evolved_observed_mean_as_time_independent_observed_mean, - refuse_evolved_observed_variance_as_stationary_initial_observed_variance, - refuse_extra_process_contribution_as_observed_mean, - refuse_extra_process_latent_mean_as_observed_mean, - refuse_extra_process_observed_mean_as_after_extra_process_observed_mean, - refuse_finite_interval_process_noise_as_stationary_variance, - refuse_impulse_carry_observed_mean_as_after_extra_process_observed_mean, - refuse_impulse_carry_observed_mean_as_initial_time_dependent_observed_mean, - refuse_impulse_carry_observed_mean_as_initial_time_independent_observed_mean, - refuse_impulse_carry_observed_mean_as_time_independent_observed_mean, - refuse_impulse_observed_mean_as_extra_process_observed_mean, - refuse_impulse_observed_mean_as_impulse_carry_observed_mean, - refuse_impulse_observed_mean_as_initial_time_dependent_observed_mean, - refuse_impulse_observed_mean_as_initial_time_independent_observed_mean, - refuse_impulse_observed_mean_as_time_independent_observed_mean, - refuse_initial_latent_mean_as_evolved_mean, - refuse_initial_observed_mean_as_evolved_observed_mean, - refuse_initial_observed_mean_as_stationary_initial_observed_mean, - refuse_initial_observed_variance_as_stationary_initial_observed_variance, - refuse_initial_time_dependent_carry_as_impulse_carry, - refuse_initial_time_dependent_carry_as_initial_effect, - refuse_initial_time_dependent_coefficient_as_initial_effect, - refuse_initial_time_dependent_effect_as_contemporaneous_impulse, - refuse_initial_time_dependent_effect_as_continuous_intercept, - refuse_initial_time_dependent_effect_as_initial_time_independent_effect, - refuse_initial_time_dependent_effect_as_process_increment, - refuse_initial_time_independent_carry_as_initial_effect, - refuse_initial_time_independent_coefficient_as_initial_effect, - refuse_initial_time_independent_effect_as_continuous_intercept, - refuse_initial_time_independent_effect_as_process_increment, - refuse_initial_time_independent_effect_as_time_dependent_impulse, - refuse_initial_time_independent_observed_mean_as_initial_time_dependent_observed_mean, - refuse_latent_lagged_covariance_as_observed_covariance, - refuse_latent_mean_as_observed_mean, refuse_latent_variance_as_observed_variance, - refuse_level_change_extra_process_as_impulse, - refuse_level_change_extra_process_as_increment, - refuse_level_change_extra_process_as_intercept, refuse_level_change_increment_as_impulse, - refuse_level_change_increment_as_intercept, - refuse_level_change_increment_as_process_increment, - refuse_level_change_intercept_as_free_continuous_intercept, - refuse_level_change_intercept_as_impulse, - refuse_level_change_intercept_as_process_increment, refuse_manifest_means_as_observed_mean, - refuse_manifest_trait_variance_as_measurement_error, - refuse_measurement_error_as_lagged_observed_covariance, - refuse_measurement_error_as_observed_variance, - refuse_measurement_error_as_stationary_lagged_observed_covariance, - refuse_measurement_error_as_stationary_later_observed_variance, - refuse_pooled_discrete_lag_across_unequal_intervals, - refuse_process_noise_as_unconditional_variance, - refuse_stationary_initial_latent_mean_as_asymptotic_continuous_intercept, - refuse_stationary_initial_latent_mean_as_asymptotic_time_independent_effect, - refuse_stationary_initial_latent_mean_as_discrete_mean, - refuse_stationary_initial_latent_mean_as_initial_latent_mean, - refuse_stationary_initial_latent_mean_as_observed_mean, - refuse_stationary_initial_latent_variance_as_asymptotic_time_independent_variance, - refuse_stationary_initial_latent_variance_as_discrete_variance, - refuse_stationary_initial_latent_variance_as_initial_latent_variance, - refuse_stationary_initial_latent_variance_as_observed_variance, - refuse_stationary_initial_latent_variance_as_stationary_within_subject, - refuse_stationary_initial_latent_variance_as_trait_variance, - refuse_stationary_initial_observed_mean_as_manifest_means, - refuse_stationary_initial_observed_variance_as_measurement_error, - refuse_stationary_initial_observed_variance_as_stationary_lagged_observed_covariance, - refuse_stationary_lagged_latent_covariance_as_decayed_stationary_variance, - refuse_stationary_lagged_latent_covariance_as_observed_covariance, - refuse_stationary_lagged_latent_covariance_as_stationary_initial_latent_variance, - refuse_stationary_lagged_observed_covariance_as_stationary_later_observed_variance, - refuse_stationary_later_latent_variance_as_discrete_variance, - refuse_stationary_later_latent_variance_as_lagged_covariance, - refuse_stationary_later_latent_variance_as_observed_variance, - refuse_stationary_later_latent_variance_as_process_noise, - refuse_stationary_within_subject_observed_variance_as_stationary_initial_observed_variance, - refuse_time_dependent_impulse_as_continuous_intercept, - refuse_time_dependent_impulse_as_time_independent_effect, - refuse_time_dependent_impulse_as_time_varying_discrete_effect, - refuse_time_dependent_impulse_carry_as_contemporaneous_impulse, - refuse_time_dependent_impulse_carry_as_continuous_intercept, - refuse_time_dependent_impulse_carry_as_time_independent_effect, - refuse_time_dependent_impulse_carry_as_time_varying_discrete_effect, - refuse_time_independent_coefficient_as_discrete_effect, - refuse_time_independent_effect_as_continuous_intercept, - refuse_time_independent_effect_as_time_dependent_impulse, - refuse_time_independent_effect_as_time_varying_discrete_effect, - refuse_time_independent_observed_mean_as_initial_time_dependent_observed_mean, - refuse_time_independent_observed_mean_as_initial_time_independent_observed_mean, - refuse_trait_plus_state_lagged_covariance_as_stationary_lagged_latent_covariance, - refuse_trait_variance_as_process_noise, refuse_trait_variance_as_stationary_within_subject, - refuse_unmatched_time_varying_predictor_interval, - }; - use crate::error::PsychometricError; +/// Refuse treating §4.3 / p. 16 stationary `T0VAR` as +/// `asymDIFFUSION`. +/// +/// The constraint includes trait variance and time-independent +/// predictor variance. `-q / (2 a)` is the within-subject +/// contribution and is not that composition when `TRAITVAR` or +/// `addedTIPREDVAR` is nonzero. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::StationaryInitialLatentVarianceIsNotStationaryWithinSubject`]. +pub fn refuse_stationary_initial_latent_variance_as_stationary_within_subject( + stationary_t0_variance: f64, + asymptotic_within_subject: f64, +) -> Result { + let _ = (stationary_t0_variance, asymptotic_within_subject); + Err(PsychometricError::StationaryInitialLatentVarianceIsNotStationaryWithinSubject) +} + +/// Refuse treating §4.3 / p. 16 stationary `T0VAR` as `TRAITVAR`. +/// +/// The constraint includes the within-subject process variance and +/// time-independent predictor variance. `TRAITVAR` is not that +/// composition when those contributions are nonzero. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::StationaryInitialLatentVarianceIsNotTraitVariance`]. +pub fn refuse_stationary_initial_latent_variance_as_trait_variance( + stationary_t0_variance: f64, + trait_variance: f64, +) -> Result { + let _ = (stationary_t0_variance, trait_variance); + Err(PsychometricError::StationaryInitialLatentVarianceIsNotTraitVariance) +} + +/// Refuse treating §4.3 / p. 16 stationary `T0VAR` as +/// `addedTIPREDVAR`. +/// +/// The constraint includes trait variance and `asymDIFFUSION`. +/// `(B / a)² v` is the TI extra variance and is not that +/// composition when those contributions are nonzero. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::StationaryInitialLatentVarianceIsNotAsymptoticTimeIndependentVariance`]. +pub fn refuse_stationary_initial_latent_variance_as_asymptotic_time_independent_variance( + stationary_t0_variance: f64, + added_predictor_variance: f64, +) -> Result { + let _ = (stationary_t0_variance, added_predictor_variance); + Err(PsychometricError::StationaryInitialLatentVarianceIsNotAsymptoticTimeIndependentVariance) +} + +/// Refuse treating §4.3 / p. 16 stationary `T0VAR` as a +/// finite-interval discrete latent variance. +/// +/// `trait + −q / (2 a) + (B / a)² v` is the `Δt → ∞` constrained +/// first-occasion variance. `exp(2 a Δt) p + Q_Δt` is a finite +/// event interval and is not that limit. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::StationaryInitialLatentVarianceIsNotDiscreteVariance`]. +pub fn refuse_stationary_initial_latent_variance_as_discrete_variance( + stationary_t0_variance: f64, + discrete_variance: f64, +) -> Result { + let _ = (stationary_t0_variance, discrete_variance); + Err(PsychometricError::StationaryInitialLatentVarianceIsNotDiscreteVariance) +} + +/// Exact scalar Eq. 5 of §4.3 / p. 16 stationary `T0VAR`. +/// +/// Driver, Oud, and Voelkle (2017, §4.3, pp. 9–10; Eq. 5, p. 5; +/// Table 2, p. 12; p. 16; §7.2, pp. 20–21; JSS PDF re-opened +/// 2026-08-22T03:20Z from +/// ) +/// constrain `T0VAR` to the model-predicted variance when +/// `stationary` includes `"T0VAR"`. Equation 5 writes +/// `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and +/// `Γ ~ N(τ, Ψ)`. The constrained latent variance is +/// `trait + −q / (2 a) + (B / a)² v`. The scalar composition is +/// `Var(y_0) = λ²(trait + −q / (2 a) + (B / a)² v) + θ + ψ`. Form +/// the stationary latent variance first, then `λ² p + θ + ψ`. A +/// zero loading is exactly `θ + ψ`. A zero trait, a zero diffusion, +/// and a zero TI contribution is exactly `θ + ψ`. `λ² p_0` for +/// free `T0VAR` is not this composition. `λ²(−q / (2 a)) + θ` is +/// not this composition when `TRAITVAR` or `addedTIPREDVAR` is +/// nonzero. Evolving the constrained variance as if it were all +/// state is not this composition when the trait or TI contribution +/// is nonzero. `MANIFESTVAR` is not `Var(y_0)`. The constrained +/// latent variance is not `Var(y_0)`. `TRAITVAR` is latent and is +/// scaled by `λ²`; `MANIFESTTRAITVAR` is not. This is not a Kalman +/// filter, not a matrix `expm`, and not ctsem estimation. +/// +/// # Errors +/// +/// Propagates [`recover_stationary_initial_latent_variance`] and +/// [`recover_manifest_trait_plus_state_observed_variance`]. +#[allow(clippy::too_many_arguments)] +pub fn recover_stationary_initial_observed_variance( + loading: f64, + trait_variance: f64, + continuous_diffusion: f64, + time_independent_effect: f64, + predictor_variance: f64, + log_rate: f64, + measurement_error_variance: f64, + manifest_trait_variance: f64, + clock: LagClock, +) -> Result { + let stationary_latent_variance = recover_stationary_initial_latent_variance( + trait_variance, + continuous_diffusion, + time_independent_effect, + predictor_variance, + log_rate, + clock, + )?; + recover_manifest_trait_plus_state_observed_variance( + loading, + stationary_latent_variance, + measurement_error_variance, + manifest_trait_variance, + ) +} + +/// Refuse treating §4.3 stationary `T0VAR` as `Var(y_0)`. +/// +/// `trait + −q / (2 a) + (B / a)² v` is the constrained latent +/// variance. Equation 5 maps `Var(y_0) = λ²` of that variance plus +/// `θ + ψ`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::StationaryInitialLatentVarianceIsNotObservedVariance`]. +pub fn refuse_stationary_initial_latent_variance_as_observed_variance( + stationary_latent_variance: f64, + stationary_observed_variance: f64, +) -> Result { + let _ = (stationary_latent_variance, stationary_observed_variance); + Err(PsychometricError::StationaryInitialLatentVarianceIsNotObservedVariance) +} + +/// Refuse treating `MANIFESTVAR` as Eq. 5 of §4.3 stationary +/// `T0VAR`. +/// +/// Table 2 names `θ` `MANIFESTVAR`. +/// `λ²(trait + −q / (2 a) + (B / a)² v) + θ + ψ` is not `θ` when +/// the loading and constrained variance are nonzero. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::StationaryInitialObservedVarianceIsNotMeasurementError`]. +pub fn refuse_stationary_initial_observed_variance_as_measurement_error( + stationary_observed_variance: f64, + measurement_error_variance: f64, +) -> Result { + let _ = (stationary_observed_variance, measurement_error_variance); + Err(PsychometricError::StationaryInitialObservedVarianceIsNotMeasurementError) +} + +/// Refuse treating evolved `λ² Var(η_t) + θ` as Eq. 5 of §4.3 +/// stationary `T0VAR`. +/// +/// Evolving the constrained first-occasion variance as if it were +/// all state is not `λ²(trait + −q / (2 a) + (B / a)² v) + θ + ψ` +/// when the trait or TI contribution is nonzero. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::EvolvedObservedVarianceIsNotStationaryInitialObservedVariance`]. +pub fn refuse_evolved_observed_variance_as_stationary_initial_observed_variance( + evolved_observed_variance: f64, + stationary_observed_variance: f64, +) -> Result { + let _ = (evolved_observed_variance, stationary_observed_variance); + Err(PsychometricError::EvolvedObservedVarianceIsNotStationaryInitialObservedVariance) +} + +/// Refuse treating Eq. 5 of `asymDIFFUSION` as Eq. 5 of §4.3 +/// stationary `T0VAR`. +/// +/// `λ²(−q / (2 a)) + θ` is the within-subject observed contribution +/// and is not that composition when `TRAITVAR` or `addedTIPREDVAR` +/// is nonzero. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::StationaryWithinSubjectObservedVarianceIsNotStationaryInitialObservedVariance`]. +pub fn refuse_stationary_within_subject_observed_variance_as_stationary_initial_observed_variance( + within_subject_observed_variance: f64, + stationary_observed_variance: f64, +) -> Result { + let _ = ( + within_subject_observed_variance, + stationary_observed_variance, + ); + Err( + PsychometricError::StationaryWithinSubjectObservedVarianceIsNotStationaryInitialObservedVariance, + ) +} + +/// Refuse treating Eq. 5 of free `T0VAR` as Eq. 5 of §4.3 +/// stationary `T0VAR`. +/// +/// `λ² p_0 + θ` is the free first-occasion observed variance. +/// `λ²(trait + −q / (2 a) + (B / a)² v) + θ + ψ` is not that map. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::InitialObservedVarianceIsNotStationaryInitialObservedVariance`]. +pub fn refuse_initial_observed_variance_as_stationary_initial_observed_variance( + free_initial_observed_variance: f64, + stationary_observed_variance: f64, +) -> Result { + let _ = (free_initial_observed_variance, stationary_observed_variance); + Err(PsychometricError::InitialObservedVarianceIsNotStationaryInitialObservedVariance) +} + +/// Exact scalar lagged covariance of §4.3 / p. 16 stationary +/// `T0VAR`. +/// +/// Driver, Oud, and Voelkle (2017, §4.3, pp. 9–10; Eq. 3–4, pp. 4–5; +/// Table 2, p. 12; p. 16; §7.2, pp. 20–21; JSS PDF re-opened +/// 2026-08-22T19:13Z from +/// ) +/// constrain `T0VAR` to the model-predicted variance when +/// `stationary` includes `"T0VAR"`. Equation 3 writes +/// `η(t) = exp(A Δt) η(t0) + …`. Equation 4 writes +/// `cov(η_t, η_{t-1}) = A_Δt cov(η_{t-1})`. Page 16 names +/// `asymDIFFUSION` the total within-subject variance `-q / (2 a)`. +/// Section 4.3 (p. 9) adds a stable trait process with `DRIFT` and +/// `DIFFUSION` fixed to zero (`TRAITVAR`). Section 7.2 names +/// `addedTIPREDVAR` the stable between-subject variance accounted +/// for by time-independent predictors; the scalar map is +/// `(B / a)² v`. Trait variance and that TI extra variance are +/// time-invariant between-subject; they do not decay with +/// `e^{a Δt}`. The lagged covariance of the constrained process is +/// `trait + e^{a Δt}(−q / (2 a)) + (B / a)² v`. Form the lagged +/// within-subject covariance first, then include the trait, then +/// include the TI extra variance, then add. A zero trait, a zero +/// diffusion, and a zero TI contribution is exactly zero. A zero +/// diffusion and a zero TI contribution is exactly the trait. +/// As `Δt → ∞` with stable `a < 0` the state term vanishes and the +/// lagged covariance is `trait + (B / a)² v`. As `Δt → 0+` the +/// lagged covariance approaches contemporaneous `T0VAR`. Those +/// limits are not this finite-lag map. Evolving the constrained +/// total as if it were all state is not this map. +/// `trait + e^{a Δt} p` is not this map when `addedTIPREDVAR` is +/// nonzero. Contemporaneous `T0VAR` is not this map. `a ≥ 0` cannot +/// hold a finite process variance when the diffusion or the TI +/// contribution is nonzero and fails closed. Trait-only covariance +/// does not require a stable drift. The interval must be event time +/// and strictly positive. This is not a Kalman filter, not a matrix +/// `expm`, and not ctsem estimation. +/// +/// # Errors +/// +/// Propagates [`recover_stationary_initial_latent_variance`] path +/// refusals and [`recover_trait_plus_state_lagged_covariance`]. +/// Returns [`PsychometricError::EventTimeRequired`] for any +/// non-event clock, +/// [`PsychometricError::NonPositiveInterval`] when `event_delta` is +/// not strictly positive, +/// [`PsychometricError::StationaryVarianceRequiresStableDrift`] +/// when the diffusion is nonzero and the drift is not strictly +/// negative, +/// [`PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift`] +/// when the TI contribution is nonzero and the drift is not +/// strictly negative, and +/// [`PsychometricError::InvalidNumericInput`] when an input is +/// non-finite, a variance is negative, or a product or sum +/// overflows. +#[allow(clippy::too_many_arguments)] +pub fn recover_stationary_lagged_latent_covariance( + trait_variance: f64, + continuous_diffusion: f64, + time_independent_effect: f64, + predictor_variance: f64, + log_rate: f64, + event_delta: f64, + clock: LagClock, +) -> Result { + if !clock.admits_structural_lag() { + return Err(PsychometricError::EventTimeRequired); + } + let state = if continuous_diffusion == 0.0 { + 0.0 + } else { + recover_stationary_latent_variance(continuous_diffusion, log_rate, clock)? + }; + let trait_plus_state = recover_trait_plus_state_lagged_covariance( + trait_variance, + state, + log_rate, + event_delta, + clock, + )?; + let added = recover_asymptotic_time_independent_predictor_variance( + time_independent_effect, + predictor_variance, + log_rate, + clock, + )?; + require_finite(trait_plus_state + added) +} + +/// Refuse treating lagged §4.3 stationary `T0VAR` as contemporaneous +/// stationary `T0VAR`. +/// +/// `trait + e^{a Δt}(−q / (2 a)) + (B / a)² v` is the lagged +/// covariance at a strictly positive event interval. +/// `trait + −q / (2 a) + (B / a)² v` is the first-occasion +/// variance. Those are not the same map. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::StationaryLaggedLatentCovarianceIsNotStationaryInitialLatentVariance`]. +pub fn refuse_stationary_lagged_latent_covariance_as_stationary_initial_latent_variance( + lagged_covariance: f64, + contemporaneous_variance: f64, +) -> Result { + let _ = (lagged_covariance, contemporaneous_variance); + Err(PsychometricError::StationaryLaggedLatentCovarianceIsNotStationaryInitialLatentVariance) +} + +/// Refuse treating lagged §4.3 stationary `T0VAR` as decayed total +/// stationary variance. +/// +/// Evolving `trait + −q / (2 a) + (B / a)² v` as if it were all +/// state yields `e^{a Δt}` of that total. Trait variance and +/// `addedTIPREDVAR` do not decay. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::StationaryLaggedLatentCovarianceIsNotDecayedStationaryVariance`]. +pub fn refuse_stationary_lagged_latent_covariance_as_decayed_stationary_variance( + lagged_covariance: f64, + decayed_total: f64, +) -> Result { + let _ = (lagged_covariance, decayed_total); + Err(PsychometricError::StationaryLaggedLatentCovarianceIsNotDecayedStationaryVariance) +} + +/// Refuse treating §4.3 trait-plus-state lagged covariance as lagged +/// stationary `T0VAR`. +/// +/// `trait + e^{a Δt} p` omits `addedTIPREDVAR`. The constrained +/// lagged covariance includes that TI extra variance. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::TraitPlusStateLaggedCovarianceIsNotStationaryLaggedLatentCovariance`]. +pub fn refuse_trait_plus_state_lagged_covariance_as_stationary_lagged_latent_covariance( + trait_plus_state_lagged: f64, + stationary_lagged: f64, +) -> Result { + let _ = (trait_plus_state_lagged, stationary_lagged); + Err(PsychometricError::TraitPlusStateLaggedCovarianceIsNotStationaryLaggedLatentCovariance) +} + +/// Exact scalar Eq. 5 of lagged §4.3 / p. 16 stationary `T0VAR`. +/// +/// Driver, Oud, and Voelkle (2017, §4.3, pp. 9–10; Eq. 5, p. 5; +/// Eq. 3–4, pp. 4–5; Table 2, p. 12; p. 16; §7.2, pp. 20–21; JSS PDF +/// re-opened 2026-08-22T19:13Z from +/// ) +/// write `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and +/// `Γ ~ N(τ, Ψ)`. Independent measurement error does not enter +/// `cov(y_t, y_{t-1})`. The lagged latent covariance is +/// `trait + e^{a Δt}(−q / (2 a)) + (B / a)² v`. The scalar +/// composition is +/// `cov(y_t, y_{t-1}) = λ²(trait + e^{a Δt}(−q / (2 a)) + (B / a)² v) + ψ`. +/// Form the lagged latent covariance first, then `λ² c + ψ`. A zero +/// loading is exactly `ψ`. A zero trait, a zero diffusion, and a +/// zero TI contribution is exactly `ψ`. `MANIFESTVAR` `θ` is not +/// this composition. Contemporaneous `Var(y_0)` includes `θ` and is +/// not this composition. The lagged latent covariance is not this +/// observed covariance. Evolving the constrained total as if it +/// were all state is not this composition when the trait or TI +/// contribution is nonzero. `TRAITVAR` is latent and is scaled by +/// `λ²`; `MANIFESTTRAITVAR` is not. This is not a Kalman filter, +/// not a matrix `expm`, and not ctsem estimation. +/// +/// # Errors +/// +/// Propagates [`recover_stationary_lagged_latent_covariance`] and +/// [`recover_manifest_lagged_observed_covariance`]. +#[allow(clippy::too_many_arguments)] +pub fn recover_stationary_lagged_observed_covariance( + loading: f64, + trait_variance: f64, + continuous_diffusion: f64, + time_independent_effect: f64, + predictor_variance: f64, + log_rate: f64, + event_delta: f64, + manifest_trait_variance: f64, + clock: LagClock, +) -> Result { + let lagged_latent = recover_stationary_lagged_latent_covariance( + trait_variance, + continuous_diffusion, + time_independent_effect, + predictor_variance, + log_rate, + event_delta, + clock, + )?; + recover_manifest_lagged_observed_covariance(loading, lagged_latent, manifest_trait_variance) +} + +/// Refuse treating lagged §4.3 stationary `T0VAR` as lagged observed +/// covariance. +/// +/// `trait + e^{a Δt}(−q / (2 a)) + (B / a)² v` is the lagged latent +/// covariance. Equation 5 maps `cov(y_t, y_{t-1}) = λ²` of that +/// covariance plus `ψ`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::StationaryLaggedLatentCovarianceIsNotObservedCovariance`]. +pub fn refuse_stationary_lagged_latent_covariance_as_observed_covariance( + lagged_latent_covariance: f64, + lagged_observed_covariance: f64, +) -> Result { + let _ = (lagged_latent_covariance, lagged_observed_covariance); + Err(PsychometricError::StationaryLaggedLatentCovarianceIsNotObservedCovariance) +} + +/// Refuse treating `MANIFESTVAR` as Eq. 5 of lagged §4.3 stationary +/// `T0VAR`. +/// +/// Table 2 names `θ` `MANIFESTVAR`. Independent `ε_t` does not +/// enter `cov(y_t, y_{t-1})`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::MeasurementErrorIsNotStationaryLaggedObservedCovariance`]. +pub fn refuse_measurement_error_as_stationary_lagged_observed_covariance( + measurement_error_variance: f64, + lagged_observed_covariance: f64, +) -> Result { + let _ = (measurement_error_variance, lagged_observed_covariance); + Err(PsychometricError::MeasurementErrorIsNotStationaryLaggedObservedCovariance) +} + +/// Refuse treating Eq. 5 of contemporaneous §4.3 stationary `T0VAR` +/// as lagged stationary observed covariance. +/// +/// `λ²(trait + −q / (2 a) + (B / a)² v) + θ + ψ` is +/// contemporaneous and includes `θ`. +/// `λ²(trait + e^{a Δt}(−q / (2 a)) + (B / a)² v) + ψ` is not that +/// map. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::StationaryInitialObservedVarianceIsNotStationaryLaggedObservedCovariance`]. +pub fn refuse_stationary_initial_observed_variance_as_stationary_lagged_observed_covariance( + contemporaneous_observed_variance: f64, + lagged_observed_covariance: f64, +) -> Result { + let _ = ( + contemporaneous_observed_variance, + lagged_observed_covariance, + ); + Err(PsychometricError::StationaryInitialObservedVarianceIsNotStationaryLaggedObservedCovariance) +} + +/// Exact scalar later-occasion variance of §4.3 / p. 16 stationary +/// `T0VAR`. +/// +/// Driver, Oud, and Voelkle (2017, §4.3, pp. 9–10; Eq. 3–4, pp. 4–5; +/// Table 2, p. 12; p. 16; §7.2, pp. 20–21; JSS PDF re-opened +/// 2026-08-22T23:05Z from +/// ) +/// constrain the first-occasion variance according to the +/// model-predicted variances across all time points when `stationary` +/// includes `"T0VAR"`. Equation 3 writes `η(t) = exp(A Δt) η(t0) + … +` +/// the stochastic integral. Equation 4 writes that the integral +/// exhibits covariance `Q_Δt`. The law of total variance on the +/// within-subject state is `e^{2 a Δt}(−q / (2 a)) + Q_Δt`. Trait +/// variance and `addedTIPREDVAR` are time-invariant between-subject; +/// they do not enter that process-noise integral. The later-occasion +/// composition is +/// `trait + e^{2 a Δt}(−q / (2 a)) + Q_Δt + (B / a)² v`. Form the +/// evolved within-subject variance first, then include the trait, +/// then include the TI extra variance, then add. A zero trait, a +/// zero diffusion, and a zero TI contribution is exactly zero. A +/// zero diffusion and a zero TI contribution is exactly the trait. +/// Under stationarity that composition equals contemporaneous +/// `T0VAR`. Evolving the constrained total as if it were all state +/// (`e^{2 a Δt} p + Q_Δt`) is not this map. The lagged covariance +/// `trait + e^{a Δt}(−q / (2 a)) + (B / a)² v` omits `Q_Δt` and is +/// not this map. `Q_Δt` is not this map. `a ≥ 0` cannot hold a +/// finite process variance when the diffusion or the TI contribution +/// is nonzero and fails closed. Trait-only variance does not require +/// a stable drift. The interval must be event time and strictly +/// positive. This is not a Kalman filter, not a matrix `expm`, and +/// not ctsem estimation. +/// +/// # Errors +/// +/// Propagates [`recover_stationary_initial_latent_variance`] path +/// refusals and [`recover_discrete_latent_variance`]. Returns +/// [`PsychometricError::EventTimeRequired`] for any non-event clock, +/// [`PsychometricError::NonPositiveInterval`] when `event_delta` is +/// not strictly positive, +/// [`PsychometricError::StationaryVarianceRequiresStableDrift`] +/// when the diffusion is nonzero and the drift is not strictly +/// negative, +/// [`PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift`] +/// when the TI contribution is nonzero and the drift is not +/// strictly negative, and +/// [`PsychometricError::InvalidNumericInput`] when an input is +/// non-finite, a variance is negative, or a product or sum +/// overflows. +#[allow(clippy::too_many_arguments)] +pub fn recover_stationary_later_latent_variance( + trait_variance: f64, + continuous_diffusion: f64, + time_independent_effect: f64, + predictor_variance: f64, + log_rate: f64, + event_delta: f64, + clock: LagClock, +) -> Result { + if !clock.admits_structural_lag() { + return Err(PsychometricError::EventTimeRequired); + } + let state = if continuous_diffusion == 0.0 { + 0.0 + } else { + recover_stationary_latent_variance(continuous_diffusion, log_rate, clock)? + }; + let evolved_state = recover_discrete_latent_variance( + state, + continuous_diffusion, + log_rate, + event_delta, + clock, + )?; + let trait_plus_evolved = + recover_trait_plus_state_latent_variance(trait_variance, evolved_state)?; + let added = recover_asymptotic_time_independent_predictor_variance( + time_independent_effect, + predictor_variance, + log_rate, + clock, + )?; + require_finite(trait_plus_evolved + added) +} + +/// Refuse treating later-occasion §4.3 stationary `T0VAR` as lagged +/// stationary covariance. +/// +/// `trait + e^{2 a Δt}(−q / (2 a)) + Q_Δt + (B / a)² v` is the +/// unconditional variance at a later event occasion. The lagged +/// covariance omits `Q_Δt` and uses `e^{a Δt}` of the state. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::StationaryLaterLatentVarianceIsNotLaggedCovariance`]. +pub fn refuse_stationary_later_latent_variance_as_lagged_covariance( + later_variance: f64, + lagged_covariance: f64, +) -> Result { + let _ = (later_variance, lagged_covariance); + Err(PsychometricError::StationaryLaterLatentVarianceIsNotLaggedCovariance) +} + +/// Refuse treating later-occasion §4.3 stationary `T0VAR` as the free +/// discrete evolution of the constrained total. +/// +/// Evolving `trait + −q / (2 a) + (B / a)² v` as if it were all +/// state yields `e^{2 a Δt}` of that total plus `Q_Δt`. Trait +/// variance and `addedTIPREDVAR` do not enter `Q_Δt`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::StationaryLaterLatentVarianceIsNotDiscreteVariance`]. +pub fn refuse_stationary_later_latent_variance_as_discrete_variance( + later_variance: f64, + free_discrete_variance: f64, +) -> Result { + let _ = (later_variance, free_discrete_variance); + Err(PsychometricError::StationaryLaterLatentVarianceIsNotDiscreteVariance) +} + +/// Refuse treating later-occasion §4.3 stationary `T0VAR` as +/// finite-interval process noise. +/// +/// `Q_Δt` is the covariance of the stochastic integral. The +/// later-occasion composition includes the trait, the evolved state, +/// and `addedTIPREDVAR`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::StationaryLaterLatentVarianceIsNotProcessNoise`]. +pub fn refuse_stationary_later_latent_variance_as_process_noise( + later_variance: f64, + process_noise: f64, +) -> Result { + let _ = (later_variance, process_noise); + Err(PsychometricError::StationaryLaterLatentVarianceIsNotProcessNoise) +} + +/// Exact scalar Eq. 5 of later-occasion §4.3 / p. 16 stationary +/// `T0VAR`. +/// +/// Driver, Oud, and Voelkle (2017, §4.3, pp. 9–10; Eq. 5, p. 5; +/// Eq. 3–4, pp. 4–5; Table 2, p. 12; p. 16; §7.2, pp. 20–21; JSS PDF +/// re-opened 2026-08-22T23:05Z from +/// ) +/// write `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and +/// `Γ ~ N(τ, Ψ)`. The later-occasion latent variance is +/// `trait + e^{2 a Δt}(−q / (2 a)) + Q_Δt + (B / a)² v`. The scalar +/// composition is +/// `Var(y_t) = λ²(trait + e^{2 a Δt}(−q / (2 a)) + Q_Δt + (B / a)² v) + θ + ψ`. +/// Form the later-occasion latent variance first, then `λ² p + θ + ψ`. +/// A zero loading is exactly `θ + ψ`. A zero trait, a zero diffusion, +/// and a zero TI contribution is exactly `θ + ψ`. Under stationarity +/// that composition equals contemporaneous `Var(y_0)`. The lagged +/// observed covariance omits `Q_Δt` and `θ`. `MANIFESTVAR` `θ` is +/// not this composition. The later-occasion latent variance is not +/// this observed variance. `TRAITVAR` is latent and is scaled by +/// `λ²`; `MANIFESTTRAITVAR` is not. This is not a Kalman filter, not +/// a matrix `expm`, and not ctsem estimation. +/// +/// # Errors +/// +/// Propagates [`recover_stationary_later_latent_variance`] and +/// [`recover_manifest_trait_plus_state_observed_variance`]. +#[allow(clippy::too_many_arguments)] +pub fn recover_stationary_later_observed_variance( + loading: f64, + trait_variance: f64, + continuous_diffusion: f64, + time_independent_effect: f64, + predictor_variance: f64, + log_rate: f64, + event_delta: f64, + measurement_error_variance: f64, + manifest_trait_variance: f64, + clock: LagClock, +) -> Result { + let later_latent = recover_stationary_later_latent_variance( + trait_variance, + continuous_diffusion, + time_independent_effect, + predictor_variance, + log_rate, + event_delta, + clock, + )?; + recover_manifest_trait_plus_state_observed_variance( + loading, + later_latent, + measurement_error_variance, + manifest_trait_variance, + ) +} + +/// Refuse treating later-occasion §4.3 stationary `T0VAR` as +/// later-occasion observed variance. +/// +/// `trait + e^{2 a Δt}(−q / (2 a)) + Q_Δt + (B / a)² v` is the +/// later-occasion latent variance. Equation 5 maps `Var(y_t) = λ²` +/// of that variance plus `θ + ψ`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::StationaryLaterLatentVarianceIsNotObservedVariance`]. +pub fn refuse_stationary_later_latent_variance_as_observed_variance( + later_latent_variance: f64, + later_observed_variance: f64, +) -> Result { + let _ = (later_latent_variance, later_observed_variance); + Err(PsychometricError::StationaryLaterLatentVarianceIsNotObservedVariance) +} + +/// Refuse treating `MANIFESTVAR` as Eq. 5 of later-occasion §4.3 +/// stationary `T0VAR`. +/// +/// Table 2 names `θ` `MANIFESTVAR`. `θ` is not +/// `λ²(trait + e^{2 a Δt} p + Q_Δt + (B / a)² v) + θ + ψ`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::MeasurementErrorIsNotStationaryLaterObservedVariance`]. +pub fn refuse_measurement_error_as_stationary_later_observed_variance( + measurement_error_variance: f64, + later_observed_variance: f64, +) -> Result { + let _ = (measurement_error_variance, later_observed_variance); + Err(PsychometricError::MeasurementErrorIsNotStationaryLaterObservedVariance) +} + +/// Refuse treating Eq. 5 of lagged §4.3 stationary `T0VAR` as +/// later-occasion stationary observed variance. +/// +/// `λ²(trait + e^{a Δt}(−q / (2 a)) + (B / a)² v) + ψ` omits `Q_Δt` +/// and `θ`. +/// `λ²(trait + e^{2 a Δt}(−q / (2 a)) + Q_Δt + (B / a)² v) + θ + ψ` +/// is not that map. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::StationaryLaggedObservedCovarianceIsNotStationaryLaterObservedVariance`]. +pub fn refuse_stationary_lagged_observed_covariance_as_stationary_later_observed_variance( + lagged_observed_covariance: f64, + later_observed_variance: f64, +) -> Result { + let _ = (lagged_observed_covariance, later_observed_variance); + Err(PsychometricError::StationaryLaggedObservedCovarianceIsNotStationaryLaterObservedVariance) +} + +/// Exact scalar later-occasion variance of §4.3 predetermined +/// `T0VAR`. +/// +/// Driver, Oud, and Voelkle (2017, §4.3, pp. 9–10; Eq. 3–4, pp. 4–5; +/// Table 2, p. 12; p. 16; §7.2, pp. 20–21; JSS PDF re-opened +/// 2026-08-23T05:12Z from +/// ) +/// treat the first time point as predetermined when no assumptions +/// are made about the process prior to the initial time point. Free +/// `T0VAR` `p_0` is then estimated. The process gradually +/// transitions from the variances of the initial parameters toward +/// those of the parameters when the model is stationary. Equation 3 +/// writes `η(t) = exp(A Δt) η(t0) + … +` the stochastic integral. +/// Equation 4 writes that the integral exhibits covariance `Q_Δt`. +/// The law of total variance on the within-subject state is +/// `e^{2 a Δt} p_0 + Q_Δt`. Trait variance and `addedTIPREDVAR` are +/// time-invariant between-subject; they do not enter that +/// process-noise integral. The later-occasion composition is +/// `trait + e^{2 a Δt} p_0 + Q_Δt + (B / a)² v`. Form the evolved +/// free first-occasion variance first, then include the trait, then +/// include the TI extra variance, then add. A zero trait, a zero +/// initial variance, a zero diffusion, and a zero TI contribution is +/// exactly zero. A zero diffusion, a zero initial variance, and a +/// zero TI contribution is exactly the trait. As `Δt → ∞` with +/// stable `a < 0` the carried `p_0` vanishes and `Q_Δt` approaches +/// `−q / (2 a)`, so the composition approaches contemporaneous +/// stationary `T0VAR`. As `Δt → 0+` the composition approaches +/// `trait + p_0 + (B / a)² v`. Setting `p_0 = −q / (2 a)` recovers +/// the stationary later-occasion map. Evolving +/// `trait + p_0 + (B / a)² v` as if it were all state +/// (`e^{2 a Δt}` of that total plus `Q_Δt`) is not this map. Free +/// `T0VAR` `p_0` is not this map. Stationary later-occasion +/// variance uses `−q / (2 a)` in place of `p_0` and is not this map +/// when `p_0` is free. `a ≥ 0` cannot hold a finite TI extra +/// variance when that contribution is nonzero and fails closed. +/// Nonzero diffusion with `a ≥ 0` is a growing process and is kept. +/// Trait-only variance does not require a stable drift. The +/// interval must be event time and strictly positive. This is not a +/// Kalman filter, not a matrix `expm`, and not ctsem estimation. +/// +/// # Errors +/// +/// Propagates [`recover_discrete_latent_variance`], +/// [`recover_trait_plus_state_latent_variance`], and +/// [`recover_asymptotic_time_independent_predictor_variance`]. +/// Returns [`PsychometricError::EventTimeRequired`] for any +/// non-event clock, +/// [`PsychometricError::NonPositiveInterval`] when `event_delta` is +/// not strictly positive, +/// [`PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift`] +/// when the TI contribution is nonzero and the drift is not +/// strictly negative, and +/// [`PsychometricError::InvalidNumericInput`] when an input is +/// non-finite, a variance is negative, or a product or sum +/// overflows. +#[allow(clippy::too_many_arguments)] +pub fn recover_predetermined_later_latent_variance( + trait_variance: f64, + initial_latent_variance: f64, + continuous_diffusion: f64, + time_independent_effect: f64, + predictor_variance: f64, + log_rate: f64, + event_delta: f64, + clock: LagClock, +) -> Result { + if !clock.admits_structural_lag() { + return Err(PsychometricError::EventTimeRequired); + } + let evolved_state = recover_discrete_latent_variance( + initial_latent_variance, + continuous_diffusion, + log_rate, + event_delta, + clock, + )?; + let trait_plus_evolved = + recover_trait_plus_state_latent_variance(trait_variance, evolved_state)?; + let added = recover_asymptotic_time_independent_predictor_variance( + time_independent_effect, + predictor_variance, + log_rate, + clock, + )?; + require_finite(trait_plus_evolved + added) +} + +/// Refuse treating predetermined later-occasion variance as later- +/// occasion stationary `T0VAR`. +/// +/// `trait + e^{2 a Δt} p_0 + Q_Δt + (B / a)² v` uses free `T0VAR`. +/// `trait + e^{2 a Δt}(−q / (2 a)) + Q_Δt + (B / a)² v` uses the +/// stationary within-subject variance. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::PredeterminedLaterLatentVarianceIsNotStationaryLaterLatentVariance`]. +pub fn refuse_predetermined_later_latent_variance_as_stationary_later_latent_variance( + predetermined_later_variance: f64, + stationary_later_variance: f64, +) -> Result { + let _ = (predetermined_later_variance, stationary_later_variance); + Err(PsychometricError::PredeterminedLaterLatentVarianceIsNotStationaryLaterLatentVariance) +} + +/// Refuse treating predetermined later-occasion variance as the free +/// discrete evolution of the total. +/// +/// Evolving `trait + p_0 + (B / a)² v` as if it were all state +/// yields `e^{2 a Δt}` of that total plus `Q_Δt`. Trait variance +/// and `addedTIPREDVAR` do not enter `Q_Δt`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::PredeterminedLaterLatentVarianceIsNotDiscreteVariance`]. +pub fn refuse_predetermined_later_latent_variance_as_discrete_variance( + predetermined_later_variance: f64, + free_discrete_variance: f64, +) -> Result { + let _ = (predetermined_later_variance, free_discrete_variance); + Err(PsychometricError::PredeterminedLaterLatentVarianceIsNotDiscreteVariance) +} + +/// Refuse treating predetermined later-occasion variance as free +/// first-occasion `T0VAR`. +/// +/// `p_0` is the predetermined first-occasion state variance. +/// `e^{2 a Δt} p_0 + Q_Δt` is not `p_0`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::PredeterminedLaterLatentVarianceIsNotInitialLatentVariance`]. +pub fn refuse_predetermined_later_latent_variance_as_initial_latent_variance( + predetermined_later_variance: f64, + initial_latent_variance: f64, +) -> Result { + let _ = (predetermined_later_variance, initial_latent_variance); + Err(PsychometricError::PredeterminedLaterLatentVarianceIsNotInitialLatentVariance) +} + +/// Exact scalar Eq. 5 of later-occasion §4.3 predetermined `T0VAR`. +/// +/// Driver, Oud, and Voelkle (2017, §4.3, pp. 9–10; Eq. 5, p. 5; +/// Eq. 3–4, pp. 4–5; Table 2, p. 12; p. 16; §7.2, pp. 20–21; JSS PDF +/// re-opened 2026-08-23T05:12Z from +/// ) +/// write `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and +/// `Γ ~ N(τ, Ψ)`. The predetermined later-occasion latent variance +/// is `trait + e^{2 a Δt} p_0 + Q_Δt + (B / a)² v`. The scalar +/// composition is +/// `Var(y_t) = λ²(trait + e^{2 a Δt} p_0 + Q_Δt + (B / a)² v) + θ + ψ`. +/// Form the predetermined later-occasion latent variance first, +/// then `λ² p + θ + ψ`. A zero loading is exactly `θ + ψ`. A zero +/// trait, a zero initial variance, a zero diffusion, and a zero TI +/// contribution is exactly `θ + ψ`. Setting `p_0 = −q / (2 a)` +/// recovers the stationary later-occasion observed variance. The +/// stationary later-occasion observed variance is not this +/// composition when `p_0` is free. `MANIFESTVAR` `θ` is not this +/// composition. The predetermined later-occasion latent variance is +/// not this observed variance. `TRAITVAR` is latent and is scaled +/// by `λ²`; `MANIFESTTRAITVAR` is not. This is not a Kalman filter, +/// not a matrix `expm`, and not ctsem estimation. +/// +/// # Errors +/// +/// Propagates [`recover_predetermined_later_latent_variance`] and +/// [`recover_manifest_trait_plus_state_observed_variance`]. +#[allow(clippy::too_many_arguments)] +pub fn recover_predetermined_later_observed_variance( + loading: f64, + trait_variance: f64, + initial_latent_variance: f64, + continuous_diffusion: f64, + time_independent_effect: f64, + predictor_variance: f64, + log_rate: f64, + event_delta: f64, + measurement_error_variance: f64, + manifest_trait_variance: f64, + clock: LagClock, +) -> Result { + let later_latent = recover_predetermined_later_latent_variance( + trait_variance, + initial_latent_variance, + continuous_diffusion, + time_independent_effect, + predictor_variance, + log_rate, + event_delta, + clock, + )?; + recover_manifest_trait_plus_state_observed_variance( + loading, + later_latent, + measurement_error_variance, + manifest_trait_variance, + ) +} + +/// Refuse treating predetermined later-occasion variance as +/// predetermined later-occasion observed variance. +/// +/// `trait + e^{2 a Δt} p_0 + Q_Δt + (B / a)² v` is the +/// predetermined later-occasion latent variance. Equation 5 maps +/// `Var(y_t) = λ²` of that variance plus `θ + ψ`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::PredeterminedLaterLatentVarianceIsNotObservedVariance`]. +pub fn refuse_predetermined_later_latent_variance_as_observed_variance( + predetermined_later_latent_variance: f64, + predetermined_later_observed_variance: f64, +) -> Result { + let _ = ( + predetermined_later_latent_variance, + predetermined_later_observed_variance, + ); + Err(PsychometricError::PredeterminedLaterLatentVarianceIsNotObservedVariance) +} + +/// Refuse treating `MANIFESTVAR` as Eq. 5 of predetermined later- +/// occasion `T0VAR`. +/// +/// Table 2 names `θ` `MANIFESTVAR`. `θ` is not +/// `λ²(trait + e^{2 a Δt} p_0 + Q_Δt + (B / a)² v) + θ + ψ`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::MeasurementErrorIsNotPredeterminedLaterObservedVariance`]. +pub fn refuse_measurement_error_as_predetermined_later_observed_variance( + measurement_error_variance: f64, + predetermined_later_observed_variance: f64, +) -> Result { + let _ = ( + measurement_error_variance, + predetermined_later_observed_variance, + ); + Err(PsychometricError::MeasurementErrorIsNotPredeterminedLaterObservedVariance) +} + +/// Refuse treating Eq. 5 of later-occasion §4.3 stationary `T0VAR` +/// as predetermined later-occasion observed variance. +/// +/// `λ²(trait + e^{2 a Δt}(−q / (2 a)) + Q_Δt + (B / a)² v) + θ + ψ` +/// uses the stationary within-subject variance. +/// `λ²(trait + e^{2 a Δt} p_0 + Q_Δt + (B / a)² v) + θ + ψ` is not +/// that map when `p_0` is free. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::StationaryLaterObservedVarianceIsNotPredeterminedLaterObservedVariance`]. +pub fn refuse_stationary_later_observed_variance_as_predetermined_later_observed_variance( + stationary_later_observed_variance: f64, + predetermined_later_observed_variance: f64, +) -> Result { + let _ = ( + stationary_later_observed_variance, + predetermined_later_observed_variance, + ); + Err(PsychometricError::StationaryLaterObservedVarianceIsNotPredeterminedLaterObservedVariance) +} + +/// Exact scalar lagged covariance of §4.3 predetermined `T0VAR`. +/// +/// Driver, Oud, and Voelkle (2017, §4.3, pp. 9–10; Eq. 3–4, pp. 4–5; +/// Table 2, p. 12; p. 16; §7.2, pp. 20–21; JSS PDF re-opened +/// 2026-08-23T09:04Z from +/// ) +/// treat the first time point as predetermined when no assumptions +/// are made about the process prior to the initial time point. Free +/// `T0VAR` `p_0` is then estimated. Equation 3 writes +/// `η(t) = exp(A Δt) η(t0) + …`. Equation 4 writes +/// `cov(η_t, η_{t-1}) = A_Δt cov(η_{t-1})`. The lagged within-subject +/// covariance of free `T0VAR` is `e^{a Δt} p_0`. Trait variance and +/// `addedTIPREDVAR` are time-invariant between-subject; they do not +/// decay with `e^{a Δt}`. The lagged composition is +/// `trait + e^{a Δt} p_0 + (B / a)² v`. Form the lagged free +/// first-occasion covariance first, then include the trait, then +/// include the TI extra variance, then add. A zero trait, a zero +/// initial variance, and a zero TI contribution is exactly zero. A +/// zero initial variance and a zero TI contribution is exactly the +/// trait. As `Δt → ∞` with stable `a < 0` the state term vanishes. +/// As `Δt → 0+` the composition approaches +/// `trait + p_0 + (B / a)² v`. Setting `p_0 = −q / (2 a)` recovers +/// the stationary lagged map. Evolving `trait + p_0 + (B / a)² v` +/// as if it were all state (`e^{a Δt}` of that total) is not this +/// map. Free `T0VAR` `p_0` is not this map. Stationary lagged +/// covariance uses `−q / (2 a)` in place of `p_0` and is not this +/// map when `p_0` is free. The later-occasion map +/// `trait + e^{2 a Δt} p_0 + Q_Δt + (B / a)² v` includes `Q_Δt` +/// and is not this map. `a ≥ 0` cannot hold a finite TI extra +/// variance when that contribution is nonzero and fails closed. +/// A zero-diffusion carry with `a ≥ 0` is `e^{a Δt} p_0` and is +/// kept. Trait-only variance does not require a stable drift. The +/// interval must be event time and strictly positive. This is not a +/// Kalman filter, not a matrix `expm`, and not ctsem estimation. +/// +/// # Errors +/// +/// Propagates [`recover_trait_plus_state_lagged_covariance`] and +/// [`recover_asymptotic_time_independent_predictor_variance`]. +/// Returns [`PsychometricError::EventTimeRequired`] for any +/// non-event clock, +/// [`PsychometricError::NonPositiveInterval`] when `event_delta` is +/// not strictly positive, +/// [`PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift`] +/// when the TI contribution is nonzero and the drift is not +/// strictly negative, and +/// [`PsychometricError::InvalidNumericInput`] when an input is +/// non-finite, a variance is negative, or a product or sum +/// overflows. +pub fn recover_predetermined_lagged_latent_covariance( + trait_variance: f64, + initial_latent_variance: f64, + time_independent_effect: f64, + predictor_variance: f64, + log_rate: f64, + event_delta: f64, + clock: LagClock, +) -> Result { + if !clock.admits_structural_lag() { + return Err(PsychometricError::EventTimeRequired); + } + let trait_plus_state = recover_trait_plus_state_lagged_covariance( + trait_variance, + initial_latent_variance, + log_rate, + event_delta, + clock, + )?; + let added = recover_asymptotic_time_independent_predictor_variance( + time_independent_effect, + predictor_variance, + log_rate, + clock, + )?; + require_finite(trait_plus_state + added) +} + +/// Refuse treating predetermined lagged covariance as lagged +/// stationary `T0VAR`. +/// +/// `trait + e^{a Δt} p_0 + (B / a)² v` uses free `T0VAR`. +/// `trait + e^{a Δt}(−q / (2 a)) + (B / a)² v` uses the +/// stationary within-subject variance. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::PredeterminedLaggedLatentCovarianceIsNotStationaryLaggedCovariance`]. +pub fn refuse_predetermined_lagged_latent_covariance_as_stationary_lagged_covariance( + predetermined_lagged_covariance: f64, + stationary_lagged_covariance: f64, +) -> Result { + let _ = ( + predetermined_lagged_covariance, + stationary_lagged_covariance, + ); + Err(PsychometricError::PredeterminedLaggedLatentCovarianceIsNotStationaryLaggedCovariance) +} + +/// Refuse treating predetermined lagged covariance as predetermined +/// later-occasion variance. +/// +/// `e^{a Δt} p_0` omits `Q_Δt`. Later-occasion variance is +/// `e^{2 a Δt} p_0 + Q_Δt` of the within-subject state. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::PredeterminedLaggedLatentCovarianceIsNotLaterLatentVariance`]. +pub fn refuse_predetermined_lagged_latent_covariance_as_later_latent_variance( + predetermined_lagged_covariance: f64, + predetermined_later_variance: f64, +) -> Result { + let _ = ( + predetermined_lagged_covariance, + predetermined_later_variance, + ); + Err(PsychometricError::PredeterminedLaggedLatentCovarianceIsNotLaterLatentVariance) +} + +/// Refuse treating predetermined lagged covariance as the decayed +/// total. +/// +/// Evolving `trait + p_0 + (B / a)² v` as if it were all state +/// yields `e^{a Δt}` of that total. Trait variance and +/// `addedTIPREDVAR` do not decay. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::PredeterminedLaggedLatentCovarianceIsNotDecayedTotal`]. +pub fn refuse_predetermined_lagged_latent_covariance_as_decayed_total( + predetermined_lagged_covariance: f64, + decayed_total: f64, +) -> Result { + let _ = (predetermined_lagged_covariance, decayed_total); + Err(PsychometricError::PredeterminedLaggedLatentCovarianceIsNotDecayedTotal) +} + +/// Refuse treating predetermined lagged covariance as free +/// first-occasion `T0VAR`. +/// +/// `p_0` is the predetermined first-occasion state variance. +/// `e^{a Δt} p_0` is not `p_0`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::PredeterminedLaggedLatentCovarianceIsNotInitialLatentVariance`]. +pub fn refuse_predetermined_lagged_latent_covariance_as_initial_latent_variance( + predetermined_lagged_covariance: f64, + initial_latent_variance: f64, +) -> Result { + let _ = (predetermined_lagged_covariance, initial_latent_variance); + Err(PsychometricError::PredeterminedLaggedLatentCovarianceIsNotInitialLatentVariance) +} + +/// Exact scalar Eq. 5 of lagged §4.3 predetermined `T0VAR`. +/// +/// Driver, Oud, and Voelkle (2017, §4.3, pp. 9–10; Eq. 5, p. 5; +/// Eq. 3–4, pp. 4–5; Table 2, p. 12; p. 16; §7.2, pp. 20–21; JSS PDF +/// re-opened 2026-08-23T09:04Z from +/// ) +/// write `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and +/// `Γ ~ N(τ, Ψ)`. Independent `ε_t` does not enter +/// `cov(y_t, y_{t-1})`. The predetermined lagged latent covariance +/// is `trait + e^{a Δt} p_0 + (B / a)² v`. The scalar composition +/// is `cov(y_t, y_{t-1}) = λ²(trait + e^{a Δt} p_0 + (B / a)² v) + ψ`. +/// Form the predetermined lagged latent covariance first, then +/// `λ² c + ψ`. A zero loading is exactly `ψ`. A zero trait, a zero +/// initial variance, and a zero TI contribution is exactly `ψ`. +/// Setting `p_0 = −q / (2 a)` recovers the stationary lagged +/// observed covariance. The stationary lagged observed covariance +/// is not this composition when `p_0` is free. `MANIFESTVAR` `θ` +/// is not this composition. The predetermined lagged latent +/// covariance is not this observed covariance. Predetermined later +/// observed variance includes `Q_Δt` and `θ` and is not this +/// composition. `TRAITVAR` is latent and is scaled by `λ²`; +/// `MANIFESTTRAITVAR` is not. This is not a Kalman filter, not a +/// matrix `expm`, and not ctsem estimation. +/// +/// # Errors +/// +/// Propagates [`recover_predetermined_lagged_latent_covariance`] and +/// [`recover_manifest_lagged_observed_covariance`]. +#[allow(clippy::too_many_arguments)] +pub fn recover_predetermined_lagged_observed_covariance( + loading: f64, + trait_variance: f64, + initial_latent_variance: f64, + time_independent_effect: f64, + predictor_variance: f64, + log_rate: f64, + event_delta: f64, + manifest_trait_variance: f64, + clock: LagClock, +) -> Result { + let lagged_latent = recover_predetermined_lagged_latent_covariance( + trait_variance, + initial_latent_variance, + time_independent_effect, + predictor_variance, + log_rate, + event_delta, + clock, + )?; + recover_manifest_lagged_observed_covariance(loading, lagged_latent, manifest_trait_variance) +} + +/// Refuse treating predetermined lagged covariance as predetermined +/// lagged observed covariance. +/// +/// `trait + e^{a Δt} p_0 + (B / a)² v` is the predetermined lagged +/// latent covariance. Equation 5 maps `cov(y_t, y_{t-1}) = λ²` of +/// that covariance plus `ψ`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::PredeterminedLaggedLatentCovarianceIsNotObservedCovariance`]. +pub fn refuse_predetermined_lagged_latent_covariance_as_observed_covariance( + predetermined_lagged_latent_covariance: f64, + predetermined_lagged_observed_covariance: f64, +) -> Result { + let _ = ( + predetermined_lagged_latent_covariance, + predetermined_lagged_observed_covariance, + ); + Err(PsychometricError::PredeterminedLaggedLatentCovarianceIsNotObservedCovariance) +} + +/// Refuse treating `MANIFESTVAR` as Eq. 5 of predetermined lagged +/// `T0VAR`. +/// +/// Table 2 names `θ` `MANIFESTVAR`. Independent `ε_t` does not +/// enter `cov(y_t, y_{t-1})`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::MeasurementErrorIsNotPredeterminedLaggedObservedCovariance`]. +pub fn refuse_measurement_error_as_predetermined_lagged_observed_covariance( + measurement_error_variance: f64, + predetermined_lagged_observed_covariance: f64, +) -> Result { + let _ = ( + measurement_error_variance, + predetermined_lagged_observed_covariance, + ); + Err(PsychometricError::MeasurementErrorIsNotPredeterminedLaggedObservedCovariance) +} + +/// Refuse treating Eq. 5 of predetermined later-occasion `T0VAR` as +/// predetermined lagged observed covariance. +/// +/// `λ²(trait + e^{2 a Δt} p_0 + Q_Δt + (B / a)² v) + θ + ψ` +/// includes `Q_Δt` and `θ`. +/// `λ²(trait + e^{a Δt} p_0 + (B / a)² v) + ψ` omits both. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::PredeterminedLaterObservedVarianceIsNotPredeterminedLaggedObservedCovariance`]. +pub fn refuse_predetermined_later_observed_variance_as_predetermined_lagged_observed_covariance( + predetermined_later_observed_variance: f64, + predetermined_lagged_observed_covariance: f64, +) -> Result { + let _ = ( + predetermined_later_observed_variance, + predetermined_lagged_observed_covariance, + ); + Err( + PsychometricError::PredeterminedLaterObservedVarianceIsNotPredeterminedLaggedObservedCovariance, + ) +} + +/// Refuse treating Eq. 5 of lagged §4.3 stationary `T0VAR` as +/// predetermined lagged observed covariance. +/// +/// `λ²(trait + e^{a Δt}(−q / (2 a)) + (B / a)² v) + ψ` uses the +/// stationary within-subject variance. +/// `λ²(trait + e^{a Δt} p_0 + (B / a)² v) + ψ` is not that map +/// when `p_0` is free. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::StationaryLaggedObservedCovarianceIsNotPredeterminedLaggedObservedCovariance`]. +pub fn refuse_stationary_lagged_observed_covariance_as_predetermined_lagged_observed_covariance( + stationary_lagged_observed_covariance: f64, + predetermined_lagged_observed_covariance: f64, +) -> Result { + let _ = ( + stationary_lagged_observed_covariance, + predetermined_lagged_observed_covariance, + ); + Err( + PsychometricError::StationaryLaggedObservedCovarianceIsNotPredeterminedLaggedObservedCovariance, + ) +} + +/// Exact scalar first-occasion variance of §4.3 predetermined `T0VAR`. +/// +/// Driver, Oud, and Voelkle (2017, §4.3, pp. 9–10; Eq. 5, p. 5; +/// Table 2, p. 12; p. 16; §7.2, pp. 20–21; JSS PDF re-opened +/// 2026-08-23T10:03Z from +/// ) +/// treat the first time point as predetermined when no assumptions +/// are made about the process prior to the initial time point. Free +/// `T0VAR` `p_0` is then estimated. Between-subject `TRAITVAR` and +/// `addedTIPREDVAR` are inherently stationary (p. 10). The +/// first-occasion composition is `trait + p_0 + (B / a)² v`. Form +/// the free first-occasion state variance first, then include the +/// trait, then include the TI extra variance, then add. A zero +/// trait, a zero initial variance, and a zero TI contribution is +/// exactly zero. A zero initial variance and a zero TI contribution +/// is exactly the trait. Setting `p_0 = −q / (2 a)` recovers the +/// stationary first-occasion map. Stationary first-occasion variance +/// uses `−q / (2 a)` in place of `p_0` and is not this map when +/// `p_0` is free. Free `T0VAR` `p_0` is not this map. The lagged +/// map `trait + e^{a Δt} p_0 + (B / a)² v` decays the state and is +/// not this map. The later-occasion map +/// `trait + e^{2 a Δt} p_0 + Q_Δt + (B / a)² v` includes `Q_Δt` +/// and is not this map. As `Δt → 0+` those maps approach this +/// composition. `a ≥ 0` cannot hold a finite TI extra variance when +/// that contribution is nonzero and fails closed. Trait-only +/// variance does not require a stable drift. This is not a Kalman +/// filter, not a matrix `expm`, and not ctsem estimation. +/// +/// # Errors +/// +/// Propagates [`recover_trait_plus_state_latent_variance`] and +/// [`recover_asymptotic_time_independent_predictor_variance`]. +/// Returns [`PsychometricError::EventTimeRequired`] for any +/// non-event clock, +/// [`PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift`] +/// when the TI contribution is nonzero and the drift is not +/// strictly negative, and +/// [`PsychometricError::InvalidNumericInput`] when an input is +/// non-finite, a variance is negative, or a product or sum +/// overflows. +pub fn recover_predetermined_initial_latent_variance( + trait_variance: f64, + initial_latent_variance: f64, + time_independent_effect: f64, + predictor_variance: f64, + log_rate: f64, + clock: LagClock, +) -> Result { + if !clock.admits_structural_lag() { + return Err(PsychometricError::EventTimeRequired); + } + let trait_plus_state = + recover_trait_plus_state_latent_variance(trait_variance, initial_latent_variance)?; + let added = recover_asymptotic_time_independent_predictor_variance( + time_independent_effect, + predictor_variance, + log_rate, + clock, + )?; + require_finite(trait_plus_state + added) +} + +/// Refuse treating predetermined first-occasion variance as +/// stationary first-occasion `T0VAR`. +/// +/// `trait + p_0 + (B / a)² v` uses free `T0VAR`. +/// `trait + −q / (2 a) + (B / a)² v` uses the stationary +/// within-subject variance. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::PredeterminedInitialLatentVarianceIsNotStationaryInitialLatentVariance`]. +pub fn refuse_predetermined_initial_latent_variance_as_stationary_initial_latent_variance( + predetermined_initial_variance: f64, + stationary_initial_variance: f64, +) -> Result { + let _ = (predetermined_initial_variance, stationary_initial_variance); + Err(PsychometricError::PredeterminedInitialLatentVarianceIsNotStationaryInitialLatentVariance) +} + +/// Refuse treating predetermined first-occasion variance as free +/// first-occasion `T0VAR`. +/// +/// `p_0` is the predetermined first-occasion state variance. +/// `trait + p_0 + (B / a)² v` is not `p_0`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::PredeterminedInitialLatentVarianceIsNotInitialLatentVariance`]. +pub fn refuse_predetermined_initial_latent_variance_as_initial_latent_variance( + predetermined_initial_variance: f64, + initial_latent_variance: f64, +) -> Result { + let _ = (predetermined_initial_variance, initial_latent_variance); + Err(PsychometricError::PredeterminedInitialLatentVarianceIsNotInitialLatentVariance) +} + +/// Refuse treating predetermined first-occasion variance as +/// predetermined lagged covariance. +/// +/// `e^{a Δt} p_0` decays the state. First-occasion variance is +/// contemporaneous `p_0`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::PredeterminedInitialLatentVarianceIsNotLaggedLatentCovariance`]. +pub fn refuse_predetermined_initial_latent_variance_as_lagged_latent_covariance( + predetermined_initial_variance: f64, + predetermined_lagged_covariance: f64, +) -> Result { + let _ = ( + predetermined_initial_variance, + predetermined_lagged_covariance, + ); + Err(PsychometricError::PredeterminedInitialLatentVarianceIsNotLaggedLatentCovariance) +} + +/// Refuse treating predetermined first-occasion variance as +/// predetermined later-occasion variance. +/// +/// Later-occasion variance is `e^{2 a Δt} p_0 + Q_Δt` of the +/// within-subject state. First-occasion variance omits both. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::PredeterminedInitialLatentVarianceIsNotLaterLatentVariance`]. +pub fn refuse_predetermined_initial_latent_variance_as_later_latent_variance( + predetermined_initial_variance: f64, + predetermined_later_variance: f64, +) -> Result { + let _ = (predetermined_initial_variance, predetermined_later_variance); + Err(PsychometricError::PredeterminedInitialLatentVarianceIsNotLaterLatentVariance) +} + +/// Exact scalar Eq. 5 of first-occasion §4.3 predetermined `T0VAR`. +/// +/// Driver, Oud, and Voelkle (2017, §4.3, pp. 9–10; Eq. 5, p. 5; +/// Table 2, p. 12; p. 16; §7.2, pp. 20–21; JSS PDF re-opened +/// 2026-08-23T10:03Z from +/// ) +/// write `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and +/// `Γ ~ N(τ, Ψ)`. The predetermined first-occasion latent variance +/// is `trait + p_0 + (B / a)² v`. The scalar composition is +/// `Var(y_0) = λ²(trait + p_0 + (B / a)² v) + θ + ψ`. Form the +/// predetermined first-occasion latent variance first, then +/// `λ² p + θ + ψ`. A zero loading is exactly `θ + ψ`. A zero trait, +/// a zero initial variance, and a zero TI contribution is exactly +/// `θ + ψ`. Setting `p_0 = −q / (2 a)` recovers the stationary +/// first-occasion observed variance. The stationary first-occasion +/// observed variance is not this composition when `p_0` is free. +/// `MANIFESTVAR` `θ` is not this composition. The predetermined +/// first-occasion latent variance is not this observed variance. +/// Predetermined later observed variance includes `Q_Δt` and is not +/// this composition. `TRAITVAR` is latent and is scaled by `λ²`; +/// `MANIFESTTRAITVAR` is not. This is not a Kalman filter, not a +/// matrix `expm`, and not ctsem estimation. +/// +/// # Errors +/// +/// Propagates [`recover_predetermined_initial_latent_variance`] and +/// [`recover_manifest_trait_plus_state_observed_variance`]. +#[allow(clippy::too_many_arguments)] +pub fn recover_predetermined_initial_observed_variance( + loading: f64, + trait_variance: f64, + initial_latent_variance: f64, + time_independent_effect: f64, + predictor_variance: f64, + log_rate: f64, + measurement_error_variance: f64, + manifest_trait_variance: f64, + clock: LagClock, +) -> Result { + let initial_latent = recover_predetermined_initial_latent_variance( + trait_variance, + initial_latent_variance, + time_independent_effect, + predictor_variance, + log_rate, + clock, + )?; + recover_manifest_trait_plus_state_observed_variance( + loading, + initial_latent, + measurement_error_variance, + manifest_trait_variance, + ) +} + +/// Refuse treating predetermined first-occasion variance as +/// predetermined first-occasion observed variance. +/// +/// `trait + p_0 + (B / a)² v` is the predetermined first-occasion +/// latent variance. Equation 5 maps `Var(y_0) = λ²` of that +/// variance plus `θ + ψ`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::PredeterminedInitialLatentVarianceIsNotObservedVariance`]. +pub fn refuse_predetermined_initial_latent_variance_as_observed_variance( + predetermined_initial_latent_variance: f64, + predetermined_initial_observed_variance: f64, +) -> Result { + let _ = ( + predetermined_initial_latent_variance, + predetermined_initial_observed_variance, + ); + Err(PsychometricError::PredeterminedInitialLatentVarianceIsNotObservedVariance) +} + +/// Refuse treating `MANIFESTVAR` as Eq. 5 of predetermined +/// first-occasion `T0VAR`. +/// +/// Table 2 names `θ` `MANIFESTVAR`. `θ` is not +/// `λ²(trait + p_0 + (B / a)² v) + θ + ψ`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::MeasurementErrorIsNotPredeterminedInitialObservedVariance`]. +pub fn refuse_measurement_error_as_predetermined_initial_observed_variance( + measurement_error_variance: f64, + predetermined_initial_observed_variance: f64, +) -> Result { + let _ = ( + measurement_error_variance, + predetermined_initial_observed_variance, + ); + Err(PsychometricError::MeasurementErrorIsNotPredeterminedInitialObservedVariance) +} + +/// Refuse treating Eq. 5 of §4.3 stationary `T0VAR` as predetermined +/// first-occasion observed variance. +/// +/// `λ²(trait + −q / (2 a) + (B / a)² v) + θ + ψ` uses the +/// stationary within-subject variance. +/// `λ²(trait + p_0 + (B / a)² v) + θ + ψ` is not that map when +/// `p_0` is free. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::StationaryInitialObservedVarianceIsNotPredeterminedInitialObservedVariance`]. +pub fn refuse_stationary_initial_observed_variance_as_predetermined_initial_observed_variance( + stationary_initial_observed_variance: f64, + predetermined_initial_observed_variance: f64, +) -> Result { + let _ = ( + stationary_initial_observed_variance, + predetermined_initial_observed_variance, + ); + Err( + PsychometricError::StationaryInitialObservedVarianceIsNotPredeterminedInitialObservedVariance, + ) +} + +/// Refuse treating Eq. 5 of predetermined later-occasion `T0VAR` as +/// predetermined first-occasion observed variance. +/// +/// `λ²(trait + e^{2 a Δt} p_0 + Q_Δt + (B / a)² v) + θ + ψ` +/// includes `Q_Δt`. `λ²(trait + p_0 + (B / a)² v) + θ + ψ` omits it. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::PredeterminedLaterObservedVarianceIsNotPredeterminedInitialObservedVariance`]. +pub fn refuse_predetermined_later_observed_variance_as_predetermined_initial_observed_variance( + predetermined_later_observed_variance: f64, + predetermined_initial_observed_variance: f64, +) -> Result { + let _ = ( + predetermined_later_observed_variance, + predetermined_initial_observed_variance, + ); + Err( + PsychometricError::PredeterminedLaterObservedVarianceIsNotPredeterminedInitialObservedVariance, + ) +} + +/// Exact scalar later-start lagged covariance of §4.3 predetermined +/// `T0VAR`. +/// +/// Driver, Oud, and Voelkle (2017, §4.3, pp. 9–10; Eq. 3–4, pp. 4–5; +/// Table 2, p. 12; p. 16; §7.2, pp. 20–21; JSS PDF re-opened +/// 2026-08-23T10:27Z from +/// ) +/// treat the first time point as predetermined when no assumptions +/// are made about the process prior to the initial time point. Free +/// `T0VAR` `p_0` is then estimated. Section 4.3 notes that the process +/// gradually transitions from the initial variances toward the +/// stationary variances, and that the initial time point need not +/// reflect the first measurement occasion (`startoffset`). Equation 3 +/// writes `η(t) = exp(A Δt) η(t0) + … +` the stochastic integral. +/// Equation 4 writes `cov(η_t, η_{t-1}) = A_Δt cov(η_{t-1})`. After a +/// later start `u` the within-subject state variance is +/// `e^{2 a u} p_0 + Q_u`. Lagging that later start over `s` is +/// `e^{a s}(e^{2 a u} p_0 + Q_u)`. Trait variance and +/// `addedTIPREDVAR` are time-invariant between-subject; they do not +/// decay with `e^{a s}`. The lagged composition is +/// `trait + e^{a s}(e^{2 a u} p_0 + Q_u) + (B / a)² v`. Form the +/// later-start within-subject variance first, then lag that state, +/// then include the trait, then include the TI extra variance, then +/// add. A zero trait, a zero initial variance, a zero diffusion, and +/// a zero TI contribution is exactly zero. A zero initial variance, a +/// zero diffusion, and a zero TI contribution is exactly the trait. +/// Setting `p_0 = −q / (2 a)` recovers the stationary lagged map. +/// Stationary lagged covariance uses `−q / (2 a)` in place of `p_0` +/// and is not this map when `p_0` is free. First-occasion lagged +/// covariance `trait + e^{a s} p_0 + (B / a)² v` omits `e^{a s} Q_u` +/// and is not this map when `u > 0`. Later-occasion variance includes +/// `Q_u` without lagging that later state and is not this map. +/// Evolving the later total as if it were all state (`e^{a s}` of +/// `trait + e^{2 a u} p_0 + Q_u + (B / a)² v`) is not this map. As +/// `u → 0+` the composition approaches first-occasion lagged +/// covariance. As `s → 0+` the composition approaches later-occasion +/// variance at `u`. As `s → ∞` with stable `a < 0` the state term +/// vanishes. `a ≥ 0` cannot hold a finite TI extra variance when that +/// contribution is nonzero and fails closed. Nonzero diffusion with +/// `a ≥ 0` is a growing process and is kept. Both intervals must be +/// event time and strictly positive. This is not a Kalman filter, not +/// a matrix `expm`, and not ctsem estimation. +/// +/// # Errors +/// +/// Propagates [`recover_discrete_latent_variance`], +/// [`recover_trait_plus_state_lagged_covariance`], and +/// [`recover_asymptotic_time_independent_predictor_variance`]. +/// Returns [`PsychometricError::EventTimeRequired`] for any +/// non-event clock, +/// [`PsychometricError::NonPositiveInterval`] when `start_delta` or +/// `lag_delta` is not strictly positive, +/// [`PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift`] +/// when the TI contribution is nonzero and the drift is not +/// strictly negative, and +/// [`PsychometricError::InvalidNumericInput`] when an input is +/// non-finite, a variance is negative, or a product or sum +/// overflows. +#[allow(clippy::too_many_arguments)] +pub fn recover_predetermined_later_lagged_latent_covariance( + trait_variance: f64, + initial_latent_variance: f64, + continuous_diffusion: f64, + time_independent_effect: f64, + predictor_variance: f64, + log_rate: f64, + start_delta: f64, + lag_delta: f64, + clock: LagClock, +) -> Result { + if !clock.admits_structural_lag() { + return Err(PsychometricError::EventTimeRequired); + } + let later_state = recover_discrete_latent_variance( + initial_latent_variance, + continuous_diffusion, + log_rate, + start_delta, + clock, + )?; + let trait_plus_lagged = recover_trait_plus_state_lagged_covariance( + trait_variance, + later_state, + log_rate, + lag_delta, + clock, + )?; + let added = recover_asymptotic_time_independent_predictor_variance( + time_independent_effect, + predictor_variance, + log_rate, + clock, + )?; + require_finite(trait_plus_lagged + added) +} + +/// Refuse treating later-start lagged covariance as first-occasion +/// lagged covariance of predetermined `T0VAR`. +/// +/// `e^{a s}(e^{2 a u} p_0 + Q_u)` includes `e^{a s} Q_u`. +/// `e^{a s} p_0` omits that process-noise carry. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::PredeterminedLaterLaggedLatentCovarianceIsNotPredeterminedLaggedCovariance`]. +pub fn refuse_predetermined_later_lagged_latent_covariance_as_predetermined_lagged_covariance( + predetermined_later_lagged_covariance: f64, + predetermined_lagged_covariance: f64, +) -> Result { + let _ = ( + predetermined_later_lagged_covariance, + predetermined_lagged_covariance, + ); + Err( + PsychometricError::PredeterminedLaterLaggedLatentCovarianceIsNotPredeterminedLaggedCovariance, + ) +} + +/// Refuse treating later-start lagged covariance as later-occasion +/// variance of predetermined `T0VAR`. +/// +/// Later-occasion variance is `e^{2 a u} p_0 + Q_u` of the +/// within-subject state. Later-start lag is `e^{a s}` of that state. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::PredeterminedLaterLaggedLatentCovarianceIsNotLaterLatentVariance`]. +pub fn refuse_predetermined_later_lagged_latent_covariance_as_later_latent_variance( + predetermined_later_lagged_covariance: f64, + predetermined_later_variance: f64, +) -> Result { + let _ = ( + predetermined_later_lagged_covariance, + predetermined_later_variance, + ); + Err(PsychometricError::PredeterminedLaterLaggedLatentCovarianceIsNotLaterLatentVariance) +} + +/// Refuse treating later-start lagged covariance as lagged +/// stationary `T0VAR`. +/// +/// `trait + e^{a s}(e^{2 a u} p_0 + Q_u) + (B / a)² v` uses free +/// `T0VAR`. `trait + e^{a s}(−q / (2 a)) + (B / a)² v` uses the +/// stationary within-subject variance. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::PredeterminedLaterLaggedLatentCovarianceIsNotStationaryLaggedCovariance`]. +pub fn refuse_predetermined_later_lagged_latent_covariance_as_stationary_lagged_covariance( + predetermined_later_lagged_covariance: f64, + stationary_lagged_covariance: f64, +) -> Result { + let _ = ( + predetermined_later_lagged_covariance, + stationary_lagged_covariance, + ); + Err(PsychometricError::PredeterminedLaterLaggedLatentCovarianceIsNotStationaryLaggedCovariance) +} + +/// Refuse treating later-start lagged covariance as the decayed +/// later total. +/// +/// Evolving `trait + e^{2 a u} p_0 + Q_u + (B / a)² v` as if it +/// were all state yields `e^{a s}` of that total. Trait variance +/// and `addedTIPREDVAR` do not decay. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::PredeterminedLaterLaggedLatentCovarianceIsNotDecayedLaterTotal`]. +pub fn refuse_predetermined_later_lagged_latent_covariance_as_decayed_later_total( + predetermined_later_lagged_covariance: f64, + decayed_later_total: f64, +) -> Result { + let _ = (predetermined_later_lagged_covariance, decayed_later_total); + Err(PsychometricError::PredeterminedLaterLaggedLatentCovarianceIsNotDecayedLaterTotal) +} + +/// Exact scalar Eq. 5 of later-start lagged §4.3 predetermined +/// `T0VAR`. +/// +/// Driver, Oud, and Voelkle (2017, §4.3, pp. 9–10; Eq. 5, p. 5; +/// Eq. 3–4, pp. 4–5; Table 2, p. 12; p. 16; §7.2, pp. 20–21; JSS PDF +/// re-opened 2026-08-23T10:27Z from +/// ) +/// write `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and +/// `Γ ~ N(τ, Ψ)`. Independent `ε_t` does not enter +/// `cov(y_t, y_{t-s})`. The later-start lagged latent covariance is +/// `trait + e^{a s}(e^{2 a u} p_0 + Q_u) + (B / a)² v`. The scalar +/// composition is +/// `cov(y_{t0+u+s}, y_{t0+u}) = λ²(trait + e^{a s}(e^{2 a u} p_0 + Q_u) + (B / a)² v) + ψ`. +/// Form the later-start lagged latent covariance first, then +/// `λ² c + ψ`. A zero loading is exactly `ψ`. A zero trait, a zero +/// initial variance, a zero diffusion, and a zero TI contribution is +/// exactly `ψ`. Setting `p_0 = −q / (2 a)` recovers the stationary +/// lagged observed covariance. The stationary lagged observed +/// covariance is not this composition when `p_0` is free. +/// `MANIFESTVAR` `θ` is not this composition. The later-start lagged +/// latent covariance is not this observed covariance. First-occasion +/// lagged observed covariance omits `e^{a s} Q_u` and is not this +/// composition when `u > 0`. Predetermined later observed variance +/// includes `Q_u` and `θ` and is not this composition. `TRAITVAR` is +/// latent and is scaled by `λ²`; `MANIFESTTRAITVAR` is not. This is +/// not a Kalman filter, not a matrix `expm`, and not ctsem +/// estimation. +/// +/// # Errors +/// +/// Propagates [`recover_predetermined_later_lagged_latent_covariance`] +/// and [`recover_manifest_lagged_observed_covariance`]. +#[allow(clippy::too_many_arguments)] +pub fn recover_predetermined_later_lagged_observed_covariance( + loading: f64, + trait_variance: f64, + initial_latent_variance: f64, + continuous_diffusion: f64, + time_independent_effect: f64, + predictor_variance: f64, + log_rate: f64, + start_delta: f64, + lag_delta: f64, + manifest_trait_variance: f64, + clock: LagClock, +) -> Result { + let lagged_latent = recover_predetermined_later_lagged_latent_covariance( + trait_variance, + initial_latent_variance, + continuous_diffusion, + time_independent_effect, + predictor_variance, + log_rate, + start_delta, + lag_delta, + clock, + )?; + recover_manifest_lagged_observed_covariance(loading, lagged_latent, manifest_trait_variance) +} + +/// Refuse treating later-start lagged covariance as later-start +/// lagged observed covariance. +/// +/// `trait + e^{a s}(e^{2 a u} p_0 + Q_u) + (B / a)² v` is the +/// later-start lagged latent covariance. Equation 5 maps +/// `cov(y_{t0+u+s}, y_{t0+u}) = λ²` of that covariance plus `ψ`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::PredeterminedLaterLaggedLatentCovarianceIsNotObservedCovariance`]. +pub fn refuse_predetermined_later_lagged_latent_covariance_as_observed_covariance( + predetermined_later_lagged_latent_covariance: f64, + predetermined_later_lagged_observed_covariance: f64, +) -> Result { + let _ = ( + predetermined_later_lagged_latent_covariance, + predetermined_later_lagged_observed_covariance, + ); + Err(PsychometricError::PredeterminedLaterLaggedLatentCovarianceIsNotObservedCovariance) +} + +/// Refuse treating `MANIFESTVAR` as Eq. 5 of later-start lagged +/// predetermined `T0VAR`. +/// +/// Table 2 names `θ` `MANIFESTVAR`. Independent `ε_t` does not +/// enter `cov(y_t, y_{t-s})`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::MeasurementErrorIsNotPredeterminedLaterLaggedObservedCovariance`]. +pub fn refuse_measurement_error_as_predetermined_later_lagged_observed_covariance( + measurement_error_variance: f64, + predetermined_later_lagged_observed_covariance: f64, +) -> Result { + let _ = ( + measurement_error_variance, + predetermined_later_lagged_observed_covariance, + ); + Err(PsychometricError::MeasurementErrorIsNotPredeterminedLaterLaggedObservedCovariance) +} + +/// Refuse treating Eq. 5 of first-occasion lagged predetermined +/// `T0VAR` as later-start lagged observed covariance. +/// +/// `λ²(trait + e^{a s} p_0 + (B / a)² v) + ψ` omits `e^{a s} Q_u`. +/// `λ²(trait + e^{a s}(e^{2 a u} p_0 + Q_u) + (B / a)² v) + ψ` +/// includes it. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::PredeterminedLaggedObservedCovarianceIsNotPredeterminedLaterLaggedObservedCovariance`]. +pub fn refuse_predetermined_lagged_observed_covariance_as_predetermined_later_lagged_observed_covariance( + predetermined_lagged_observed_covariance: f64, + predetermined_later_lagged_observed_covariance: f64, +) -> Result { + let _ = ( + predetermined_lagged_observed_covariance, + predetermined_later_lagged_observed_covariance, + ); + Err( + PsychometricError::PredeterminedLaggedObservedCovarianceIsNotPredeterminedLaterLaggedObservedCovariance, + ) +} + +/// Refuse treating Eq. 5 of lagged §4.3 stationary `T0VAR` as +/// later-start lagged observed covariance of predetermined `T0VAR`. +/// +/// `λ²(trait + e^{a s}(−q / (2 a)) + (B / a)² v) + ψ` uses the +/// stationary within-subject variance. +/// `λ²(trait + e^{a s}(e^{2 a u} p_0 + Q_u) + (B / a)² v) + ψ` is +/// not that map when `p_0` is free. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::StationaryLaggedObservedCovarianceIsNotPredeterminedLaterLaggedObservedCovariance`]. +pub fn refuse_stationary_lagged_observed_covariance_as_predetermined_later_lagged_observed_covariance( + stationary_lagged_observed_covariance: f64, + predetermined_later_lagged_observed_covariance: f64, +) -> Result { + let _ = ( + stationary_lagged_observed_covariance, + predetermined_later_lagged_observed_covariance, + ); + Err( + PsychometricError::StationaryLaggedObservedCovarianceIsNotPredeterminedLaterLaggedObservedCovariance, + ) +} + +/// Refuse treating Eq. 5 of predetermined later-occasion `T0VAR` as +/// later-start lagged observed covariance. +/// +/// `λ²(trait + e^{2 a u} p_0 + Q_u + (B / a)² v) + θ + ψ` includes +/// `Q_u` and `θ`. +/// `λ²(trait + e^{a s}(e^{2 a u} p_0 + Q_u) + (B / a)² v) + ψ` +/// lags the later state and omits `θ`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::PredeterminedLaterObservedVarianceIsNotPredeterminedLaterLaggedObservedCovariance`]. +pub fn refuse_predetermined_later_observed_variance_as_predetermined_later_lagged_observed_covariance( + predetermined_later_observed_variance: f64, + predetermined_later_lagged_observed_covariance: f64, +) -> Result { + let _ = ( + predetermined_later_observed_variance, + predetermined_later_lagged_observed_covariance, + ); + Err( + PsychometricError::PredeterminedLaterObservedVarianceIsNotPredeterminedLaterLaggedObservedCovariance, + ) +} + +/// Exact scalar later-start later-occasion variance of §4.3 +/// predetermined `T0VAR`. +/// +/// Driver, Oud, and Voelkle (2017, §4.3, pp. 9–10; Eq. 3–4, pp. 4–5; +/// Table 2, p. 12; p. 16; §7.2, pp. 20–21; JSS PDF re-opened +/// 2026-08-23T11:05Z from +/// ) +/// treat the first time point as predetermined when no assumptions +/// are made about the process prior to the initial time point. Free +/// `T0VAR` `p_0` is then estimated. Section 4.3 notes that the process +/// gradually transitions from the initial variances toward the +/// stationary variances, and that the initial time point need not +/// reflect the first measurement occasion (`startoffset`). Equation 3 +/// writes `η(t) = exp(A Δt) η(t0) + … +` the stochastic integral. +/// Equation 4 writes that the integral exhibits covariance `Q_Δt`. +/// After a later start `u` the within-subject state variance is +/// `e^{2 a u} p_0 + Q_u`. Evolving that later start over `s` is +/// `e^{2 a s}(e^{2 a u} p_0 + Q_u) + Q_s`. Chapman–Kolmogorov +/// writes `Q_{u+s} = e^{2 a s} Q_u + Q_s`, so the later-occasion map +/// over `u + s` is the same composition. Trait variance and +/// `addedTIPREDVAR` are time-invariant between-subject; they do not +/// enter `Q_s`. The later-start later-occasion composition is +/// `trait + e^{2 a s}(e^{2 a u} p_0 + Q_u) + Q_s + (B / a)² v`. Form +/// the later-start within-subject variance first, then evolve that +/// state, then include the trait, then include the TI extra variance, +/// then add. A zero trait, a zero initial variance, a zero diffusion, +/// and a zero TI contribution is exactly zero. A zero initial +/// variance, a zero diffusion, and a zero TI contribution is exactly +/// the trait. Setting `p_0 = −q / (2 a)` recovers the stationary +/// later-occasion map. Stationary later-occasion variance uses +/// `−q / (2 a)` in place of `p_0` and is not this map when `p_0` is +/// free. Later-occasion variance at `u` omits `Q_s` and is not this +/// map when `s > 0`. Later-start lagged covariance is `e^{a s}` of +/// the later state and omits `Q_s`; it is not this map. Evolving the +/// later total as if it were all state (`e^{2 a s}` of +/// `trait + e^{2 a u} p_0 + Q_u + (B / a)² v` plus `Q_s`) is not this +/// map. Later-occasion variance over the lag interval alone ignores +/// `startoffset` and omits `e^{2 a s} Q_u`; it is not this map when +/// `u > 0`. As `u → 0+` the composition approaches later-occasion +/// variance over `s`. As `s → 0+` the composition approaches +/// later-occasion variance at `u`. As `s → ∞` with stable `a < 0` +/// the carried later state vanishes and `Q_s` approaches +/// `−q / (2 a)`, so the composition approaches contemporaneous +/// stationary `T0VAR`. `a ≥ 0` cannot hold a finite TI extra variance +/// when that contribution is nonzero and fails closed. Nonzero +/// diffusion with `a ≥ 0` is a growing process and is kept. Both +/// intervals must be event time and strictly positive. This is not a +/// Kalman filter, not a matrix `expm`, and not ctsem estimation. +/// +/// # Errors +/// +/// Propagates [`recover_discrete_latent_variance`], +/// [`recover_trait_plus_state_latent_variance`], and +/// [`recover_asymptotic_time_independent_predictor_variance`]. +/// Returns [`PsychometricError::EventTimeRequired`] for any +/// non-event clock, +/// [`PsychometricError::NonPositiveInterval`] when `start_delta` or +/// `lag_delta` is not strictly positive, +/// [`PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift`] +/// when the TI contribution is nonzero and the drift is not +/// strictly negative, and +/// [`PsychometricError::InvalidNumericInput`] when an input is +/// non-finite, a variance is negative, or a product or sum +/// overflows. +#[allow(clippy::too_many_arguments)] +pub fn recover_predetermined_later_start_later_latent_variance( + trait_variance: f64, + initial_latent_variance: f64, + continuous_diffusion: f64, + time_independent_effect: f64, + predictor_variance: f64, + log_rate: f64, + start_delta: f64, + lag_delta: f64, + clock: LagClock, +) -> Result { + if !clock.admits_structural_lag() { + return Err(PsychometricError::EventTimeRequired); + } + let later_state = recover_discrete_latent_variance( + initial_latent_variance, + continuous_diffusion, + log_rate, + start_delta, + clock, + )?; + let evolved_state = recover_discrete_latent_variance( + later_state, + continuous_diffusion, + log_rate, + lag_delta, + clock, + )?; + let trait_plus_evolved = + recover_trait_plus_state_latent_variance(trait_variance, evolved_state)?; + let added = recover_asymptotic_time_independent_predictor_variance( + time_independent_effect, + predictor_variance, + log_rate, + clock, + )?; + require_finite(trait_plus_evolved + added) +} + +/// Refuse treating later-start later-occasion variance as later- +/// occasion variance at the later start. +/// +/// Later-occasion variance at `u` is `e^{2 a u} p_0 + Q_u` of the +/// within-subject state. Later-start later-occasion variance adds +/// `Q_s`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::PredeterminedLaterStartLaterLatentVarianceIsNotLaterLatentVariance`]. +pub fn refuse_predetermined_later_start_later_latent_variance_as_later_latent_variance( + predetermined_later_start_later_variance: f64, + predetermined_later_variance: f64, +) -> Result { + let _ = ( + predetermined_later_start_later_variance, + predetermined_later_variance, + ); + Err(PsychometricError::PredeterminedLaterStartLaterLatentVarianceIsNotLaterLatentVariance) +} + +/// Refuse treating later-start later-occasion variance as later-start +/// lagged covariance of predetermined `T0VAR`. +/// +/// Later-start lag is `e^{a s}` of the later state and omits `Q_s`. +/// Later-start later-occasion variance is +/// `e^{2 a s}(e^{2 a u} p_0 + Q_u) + Q_s`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::PredeterminedLaterStartLaterLatentVarianceIsNotLaterLaggedCovariance`]. +pub fn refuse_predetermined_later_start_later_latent_variance_as_later_lagged_covariance( + predetermined_later_start_later_variance: f64, + predetermined_later_lagged_covariance: f64, +) -> Result { + let _ = ( + predetermined_later_start_later_variance, + predetermined_later_lagged_covariance, + ); + Err(PsychometricError::PredeterminedLaterStartLaterLatentVarianceIsNotLaterLaggedCovariance) +} + +/// Refuse treating later-start later-occasion variance as later- +/// occasion stationary `T0VAR`. +/// +/// `trait + e^{2 a s}(e^{2 a u} p_0 + Q_u) + Q_s + (B / a)² v` uses +/// free `T0VAR`. `trait + e^{2 a s}(−q / (2 a)) + Q_s + (B / a)² v` +/// uses the stationary within-subject variance. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::PredeterminedLaterStartLaterLatentVarianceIsNotStationaryLaterLatentVariance`]. +pub fn refuse_predetermined_later_start_later_latent_variance_as_stationary_later_latent_variance( + predetermined_later_start_later_variance: f64, + stationary_later_variance: f64, +) -> Result { + let _ = ( + predetermined_later_start_later_variance, + stationary_later_variance, + ); + Err( + PsychometricError::PredeterminedLaterStartLaterLatentVarianceIsNotStationaryLaterLatentVariance, + ) +} + +/// Refuse treating later-start later-occasion variance as the evolved +/// later total. +/// +/// Evolving `trait + e^{2 a u} p_0 + Q_u + (B / a)² v` as if it +/// were all state yields `e^{2 a s}` of that total plus `Q_s`. Trait +/// variance and `addedTIPREDVAR` do not enter `Q_s`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::PredeterminedLaterStartLaterLatentVarianceIsNotDecayedLaterTotal`]. +pub fn refuse_predetermined_later_start_later_latent_variance_as_decayed_later_total( + predetermined_later_start_later_variance: f64, + decayed_later_total: f64, +) -> Result { + let _ = ( + predetermined_later_start_later_variance, + decayed_later_total, + ); + Err(PsychometricError::PredeterminedLaterStartLaterLatentVarianceIsNotDecayedLaterTotal) +} + +/// Refuse treating later-start later-occasion variance as later- +/// occasion variance over the lag interval alone. +/// +/// Ignoring `startoffset` yields `e^{2 a s} p_0 + Q_s` and omits +/// `e^{2 a s} Q_u`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::PredeterminedLaterStartLaterLatentVarianceIsNotLagIntervalLaterLatentVariance`]. +pub fn refuse_predetermined_later_start_later_latent_variance_as_lag_interval_later_latent_variance( + predetermined_later_start_later_variance: f64, + lag_interval_later_variance: f64, +) -> Result { + let _ = ( + predetermined_later_start_later_variance, + lag_interval_later_variance, + ); + Err( + PsychometricError::PredeterminedLaterStartLaterLatentVarianceIsNotLagIntervalLaterLatentVariance, + ) +} + +/// Exact scalar Eq. 5 of later-start later-occasion §4.3 predetermined +/// `T0VAR`. +/// +/// Driver, Oud, and Voelkle (2017, §4.3, pp. 9–10; Eq. 5, p. 5; +/// Eq. 3–4, pp. 4–5; Table 2, p. 12; p. 16; §7.2, pp. 20–21; JSS PDF +/// re-opened 2026-08-23T11:05Z from +/// ) +/// write `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and +/// `Γ ~ N(τ, Ψ)`. The later-start later-occasion latent variance is +/// `trait + e^{2 a s}(e^{2 a u} p_0 + Q_u) + Q_s + (B / a)² v`. The +/// scalar composition is +/// `Var(y_{t0+u+s}) = λ²(trait + e^{2 a s}(e^{2 a u} p_0 + Q_u) + Q_s + (B / a)² v) + θ + ψ`. +/// Form the later-start later-occasion latent variance first, then +/// `λ² p + θ + ψ`. A zero loading is exactly `θ + ψ`. A zero trait, +/// a zero initial variance, a zero diffusion, and a zero TI +/// contribution is exactly `θ + ψ`. Setting `p_0 = −q / (2 a)` +/// recovers the stationary later-occasion observed variance. The +/// stationary later-occasion observed variance is not this +/// composition when `p_0` is free. `MANIFESTVAR` `θ` is not this +/// composition. The later-start later-occasion latent variance is +/// not this observed variance. Predetermined later observed variance +/// at `u` omits `Q_s` and is not this composition when `s > 0`. +/// Later-start lagged observed covariance omits `Q_s` and `θ` and is +/// not this composition. `TRAITVAR` is latent and is scaled by `λ²`; +/// `MANIFESTTRAITVAR` is not. This is not a Kalman filter, not a +/// matrix `expm`, and not ctsem estimation. +/// +/// # Errors +/// +/// Propagates [`recover_predetermined_later_start_later_latent_variance`] +/// and [`recover_manifest_trait_plus_state_observed_variance`]. +#[allow(clippy::too_many_arguments)] +pub fn recover_predetermined_later_start_later_observed_variance( + loading: f64, + trait_variance: f64, + initial_latent_variance: f64, + continuous_diffusion: f64, + time_independent_effect: f64, + predictor_variance: f64, + log_rate: f64, + start_delta: f64, + lag_delta: f64, + measurement_error_variance: f64, + manifest_trait_variance: f64, + clock: LagClock, +) -> Result { + let later_latent = recover_predetermined_later_start_later_latent_variance( + trait_variance, + initial_latent_variance, + continuous_diffusion, + time_independent_effect, + predictor_variance, + log_rate, + start_delta, + lag_delta, + clock, + )?; + recover_manifest_trait_plus_state_observed_variance( + loading, + later_latent, + measurement_error_variance, + manifest_trait_variance, + ) +} + +/// Refuse treating later-start later-occasion variance as later-start +/// later-occasion observed variance. +/// +/// `trait + e^{2 a s}(e^{2 a u} p_0 + Q_u) + Q_s + (B / a)² v` is +/// the later-start later-occasion latent variance. Equation 5 maps +/// `Var(y_{t0+u+s}) = λ²` of that variance plus `θ + ψ`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::PredeterminedLaterStartLaterLatentVarianceIsNotObservedVariance`]. +pub fn refuse_predetermined_later_start_later_latent_variance_as_observed_variance( + predetermined_later_start_later_latent_variance: f64, + predetermined_later_start_later_observed_variance: f64, +) -> Result { + let _ = ( + predetermined_later_start_later_latent_variance, + predetermined_later_start_later_observed_variance, + ); + Err(PsychometricError::PredeterminedLaterStartLaterLatentVarianceIsNotObservedVariance) +} + +/// Refuse treating `MANIFESTVAR` as Eq. 5 of later-start later- +/// occasion predetermined `T0VAR`. +/// +/// Table 2 names `θ` `MANIFESTVAR`. `θ` is not +/// `λ²(trait + e^{2 a s}(e^{2 a u} p_0 + Q_u) + Q_s + (B / a)² v) + θ + ψ`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::MeasurementErrorIsNotPredeterminedLaterStartLaterObservedVariance`]. +pub fn refuse_measurement_error_as_predetermined_later_start_later_observed_variance( + measurement_error_variance: f64, + predetermined_later_start_later_observed_variance: f64, +) -> Result { + let _ = ( + measurement_error_variance, + predetermined_later_start_later_observed_variance, + ); + Err(PsychometricError::MeasurementErrorIsNotPredeterminedLaterStartLaterObservedVariance) +} + +/// Refuse treating Eq. 5 of predetermined later-occasion `T0VAR` as +/// later-start later-occasion observed variance. +/// +/// `λ²(trait + e^{2 a u} p_0 + Q_u + (B / a)² v) + θ + ψ` omits +/// `Q_s`. `λ²(trait + e^{2 a s}(e^{2 a u} p_0 + Q_u) + Q_s + (B / a)² v) + θ + ψ` +/// includes it. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::PredeterminedLaterObservedVarianceIsNotPredeterminedLaterStartLaterObservedVariance`]. +pub fn refuse_predetermined_later_observed_variance_as_predetermined_later_start_later_observed_variance( + predetermined_later_observed_variance: f64, + predetermined_later_start_later_observed_variance: f64, +) -> Result { + let _ = ( + predetermined_later_observed_variance, + predetermined_later_start_later_observed_variance, + ); + Err( + PsychometricError::PredeterminedLaterObservedVarianceIsNotPredeterminedLaterStartLaterObservedVariance, + ) +} + +/// Refuse treating Eq. 5 of later-start lagged predetermined `T0VAR` +/// as later-start later-occasion observed variance. +/// +/// `λ²(trait + e^{a s}(e^{2 a u} p_0 + Q_u) + (B / a)² v) + ψ` omits +/// `Q_s` and `θ`. +/// `λ²(trait + e^{2 a s}(e^{2 a u} p_0 + Q_u) + Q_s + (B / a)² v) + θ + ψ` +/// includes both. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::PredeterminedLaterLaggedObservedCovarianceIsNotPredeterminedLaterStartLaterObservedVariance`]. +pub fn refuse_predetermined_later_lagged_observed_covariance_as_predetermined_later_start_later_observed_variance( + predetermined_later_lagged_observed_covariance: f64, + predetermined_later_start_later_observed_variance: f64, +) -> Result { + let _ = ( + predetermined_later_lagged_observed_covariance, + predetermined_later_start_later_observed_variance, + ); + Err( + PsychometricError::PredeterminedLaterLaggedObservedCovarianceIsNotPredeterminedLaterStartLaterObservedVariance, + ) +} + +/// Refuse treating Eq. 5 of later-occasion §4.3 stationary `T0VAR` as +/// later-start later-occasion observed variance of predetermined +/// `T0VAR`. +/// +/// `λ²(trait + e^{2 a s}(−q / (2 a)) + Q_s + (B / a)² v) + θ + ψ` +/// uses the stationary within-subject variance. +/// `λ²(trait + e^{2 a s}(e^{2 a u} p_0 + Q_u) + Q_s + (B / a)² v) + θ + ψ` +/// is not that map when `p_0` is free. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::StationaryLaterObservedVarianceIsNotPredeterminedLaterStartLaterObservedVariance`]. +pub fn refuse_stationary_later_observed_variance_as_predetermined_later_start_later_observed_variance( + stationary_later_observed_variance: f64, + predetermined_later_start_later_observed_variance: f64, +) -> Result { + let _ = ( + stationary_later_observed_variance, + predetermined_later_start_later_observed_variance, + ); + Err( + PsychometricError::StationaryLaterObservedVarianceIsNotPredeterminedLaterStartLaterObservedVariance, + ) +} + +/// Exact scalar observed mean of a time-independent predictor. +/// +/// Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Eq. 3, p. 5; Table 2, +/// p. 12; JSS PDF re-opened 2026-08-20T12:12Z from +/// ) +/// write `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and +/// `Γ ~ N(τ, Ψ)`. Equation 3 (p. 5) writes the time-independent +/// predictor as the printed addend `A^{-1}[e^{A(t−t0)} − I] B z_i` +/// after the `T0MEANS` carry and the `CINT` increment. Table 2 names +/// `B` `TIPREDEFFECT`. The expected intercept is `τ`. The latent +/// process at `t` after that increment is +/// `μ_t + A^{-1}[e^{A Δt} − I] B z`. The scalar composition is +/// `E(y_t) = τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. Form the +/// evolved-plus-increment latent mean first, then `τ + λ` of that +/// mean. A zero loading is exactly `τ`. A zero evolved-plus-increment +/// latent mean is exactly `τ`. A zero intercept is exactly +/// `λ(μ_t + increment)`. The evolved observed mean `τ + λ μ_t` is +/// not this composition when the increment is nonzero. The +/// contemporaneous map `τ + λ(μ_t + m x)` is not this composition. +/// The carry map `τ + λ(μ_t + e^{a(t−u)} m x)` is not this +/// composition when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The +/// evolved-plus-increment latent mean is not `E(y_t)`. `TIPREDEFFECT` +/// is `B`, not that observed mean. This is not a Kalman filter and +/// not ctsem estimation. +/// +/// # Errors +/// +/// Propagates +/// [`recover_discrete_latent_mean_with_time_independent_predictor`] +/// and [`recover_manifest_observed_mean`]. +#[allow(clippy::too_many_arguments)] +pub fn recover_discrete_observed_mean_with_time_independent_predictor( + loading: f64, + initial_latent_mean: f64, + log_rate: f64, + continuous_intercept: f64, + time_independent_effect: f64, + time_independent_predictor: f64, + manifest_mean: f64, + event_delta: f64, + clock: LagClock, +) -> Result { + let composed_latent_mean = recover_discrete_latent_mean_with_time_independent_predictor( + initial_latent_mean, + log_rate, + continuous_intercept, + time_independent_effect, + time_independent_predictor, + event_delta, + clock, + )?; + recover_manifest_observed_mean(loading, composed_latent_mean, manifest_mean) +} + +/// Refuse treating the evolved observed mean as the time-independent- +/// predictor observed mean. +/// +/// Equation 5 of the Eq. 3 evolved mean is `τ + λ μ_t`. Equation 5 +/// of the Eq. 3 time-independent predictor is +/// `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. Those are not the same +/// map. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::EvolvedObservedMeanIsNotTimeIndependentObservedMean`]. +pub fn refuse_evolved_observed_mean_as_time_independent_observed_mean( + evolved_observed_mean: f64, + time_independent_observed_mean: f64, +) -> Result { + let _ = (evolved_observed_mean, time_independent_observed_mean); + Err(PsychometricError::EvolvedObservedMeanIsNotTimeIndependentObservedMean) +} + +/// Refuse treating the contemporaneous-impulse observed mean as the +/// time-independent-predictor observed mean. +/// +/// Equation 5 of the contemporaneous Dirac is `τ + λ(μ_t + m x)`. +/// Equation 5 of the Eq. 3 time-independent predictor is +/// `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. Those are not the same +/// map. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::ImpulseObservedMeanIsNotTimeIndependentObservedMean`]. +pub fn refuse_impulse_observed_mean_as_time_independent_observed_mean( + impulse_observed_mean: f64, + time_independent_observed_mean: f64, +) -> Result { + let _ = (impulse_observed_mean, time_independent_observed_mean); + Err(PsychometricError::ImpulseObservedMeanIsNotTimeIndependentObservedMean) +} + +/// Refuse treating the impulse-carry observed mean as the +/// time-independent-predictor observed mean. +/// +/// Equation 5 of the Eq. 1–2 carried latent mean is +/// `τ + λ(μ_t + e^{a(t−u)} m x)`. Equation 5 of the Eq. 3 +/// time-independent predictor is +/// `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. Those are not the same +/// map. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::ImpulseCarryObservedMeanIsNotTimeIndependentObservedMean`]. +pub fn refuse_impulse_carry_observed_mean_as_time_independent_observed_mean( + impulse_carry_observed_mean: f64, + time_independent_observed_mean: f64, +) -> Result { + let _ = (impulse_carry_observed_mean, time_independent_observed_mean); + Err(PsychometricError::ImpulseCarryObservedMeanIsNotTimeIndependentObservedMean) +} + +/// Exact scalar first-occasion time-independent predictor shift. +/// +/// Driver, Oud, and Voelkle (2017, Table 3, p. 13; Eq. 3 first +/// summand, p. 5; JSS PDF opened 2026-08-20T15:14Z from +/// ) +/// name `T0TIPREDEFFECT` the effect of time-independent predictors on +/// latents at `T0`. Table 2 / Table 3 name `TIPREDEFFECT` `B`, which +/// enters Equation 3 as the printed addend +/// `A^{-1}[e^{A(t−t0)} − I] B z`. Those are not the same matrix. The +/// scalar first-occasion shift is `t0_b z`. It is not `B`, not +/// `A^{-1}[e^{A Δt} − I] B z`, not `κ`, and not `M x`. A zero effect +/// or zero predictor is exactly zero. This is not a Kalman filter and +/// not ctsem estimation. +/// +/// # Errors +/// +/// Returns [`PsychometricError::InvalidNumericInput`] when the effect +/// or predictor is non-finite or the product overflows. +pub fn recover_initial_time_independent_predictor_effect( + initial_time_independent_effect: f64, + time_independent_predictor: f64, +) -> Result { + if !initial_time_independent_effect.is_finite() || !time_independent_predictor.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + if initial_time_independent_effect == 0.0 || time_independent_predictor == 0.0 { + return Ok(0.0); + } + require_finite(initial_time_independent_effect * time_independent_predictor) +} + +/// Exact scalar carried first-occasion time-independent predictor. +/// +/// Driver, Oud, and Voelkle (2017, Eq. 3, p. 5; Table 3, p. 13; JSS +/// PDF opened 2026-08-20T15:14Z) write the first summand as +/// `e^{A(t−t0)} η_i(t0)`. A Table 3 `T0TIPREDEFFECT` shift that is +/// already in `η(t0)` therefore appears at `t` as `e^{A Δt} t0_b z`. +/// Form `t0_b z` first, then `e^{a Δt} t0_b z`. A zero drift is +/// `t0_b z` with no dissipation of the first-occasion shift. Binary64 +/// underflow of `e^{a Δt}` to `+0` is a vanishing carry of that +/// shift and is kept. This carry is not the first-occasion shift, not +/// `A^{-1}[e^{A Δt} − I] B z` (`TIPREDEFFECT`), not `CINT`, and not +/// `M x`. When `exp` overflows at a finite `a Δt`, rewrite as +/// `sign(t0_b z) exp(ln|t0_b z| + a Δt)`. An overflowing rewrite +/// fails closed. This is not a Kalman filter and not ctsem estimation. +/// +/// # Errors +/// +/// Returns [`PsychometricError::EventTimeRequired`] for any non-event +/// clock, [`PsychometricError::NonPositiveInterval`] when +/// `event_delta` is not strictly positive, and +/// [`PsychometricError::InvalidNumericInput`] when an input is +/// non-finite or the mapped carry overflows. +pub fn recover_initial_time_independent_predictor_carry( + initial_time_independent_effect: f64, + time_independent_predictor: f64, + log_rate: f64, + event_delta: f64, + clock: LagClock, +) -> Result { + if !clock.admits_structural_lag() { + return Err(PsychometricError::EventTimeRequired); + } + if !event_delta.is_finite() || event_delta <= 0.0 { + return Err(PsychometricError::NonPositiveInterval); + } + if !log_rate.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + let initial_shift = recover_initial_time_independent_predictor_effect( + initial_time_independent_effect, + time_independent_predictor, + )?; + if initial_shift == 0.0 { + return Ok(0.0); + } + let drift_interval = log_rate * event_delta; + let auto_effect = drift_interval.exp(); + if auto_effect.is_finite() { + // +0 underflow is a vanishing carry of the T0 shift. + return require_finite(auto_effect * initial_shift); + } + if !drift_interval.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + // Finite a Δt, overflowed exp. + // e^{a Δt} t0_b z = sign(t0_b z) exp(ln|t0_b z| + a Δt). + require_finite(initial_shift.signum() * (initial_shift.abs().ln() + drift_interval).exp()) +} + +/// Exact scalar evolved latent mean plus a first-occasion TI predictor. +/// +/// Driver, Oud, and Voelkle (2017, Eq. 3, p. 5; Table 3, p. 13) write +/// the first summand as the carried `T0MEANS`, which includes any +/// `T0TIPREDEFFECT` shift already in `η(t0)`. Form `μ_t` first, then +/// add `e^{a Δt} t0_b z`. A zero carry is exactly `μ_t`. A zero +/// evolved mean is exactly the carry. Adding `t0_b z` without the +/// exponential is not this composition when `a Δt ≠ 0`. Adding +/// `A^{-1}[e^{A Δt} − I] B z` is not this composition. +/// +/// # Errors +/// +/// Propagates [`recover_discrete_latent_mean`] and +/// [`recover_initial_time_independent_predictor_carry`], and returns +/// [`PsychometricError::InvalidNumericInput`] when the sum overflows. +#[allow(clippy::too_many_arguments)] +pub fn recover_discrete_latent_mean_with_initial_time_independent_predictor( + initial_latent_mean: f64, + log_rate: f64, + continuous_intercept: f64, + initial_time_independent_effect: f64, + time_independent_predictor: f64, + event_delta: f64, + clock: LagClock, +) -> Result { + let evolved_latent_mean = recover_discrete_latent_mean( + initial_latent_mean, + log_rate, + continuous_intercept, + event_delta, + clock, + )?; + let initial_carry = recover_initial_time_independent_predictor_carry( + initial_time_independent_effect, + time_independent_predictor, + log_rate, + event_delta, + clock, + )?; + if initial_carry == 0.0 { + return Ok(evolved_latent_mean); + } + if evolved_latent_mean == 0.0 { + return Ok(initial_carry); + } + require_finite(evolved_latent_mean + initial_carry) +} + +/// Refuse treating the Table 3 first-occasion shift as the Eq. 3 +/// process increment. +/// +/// `T0TIPREDEFFECT` shifts `η(t0)`. `TIPREDEFFECT` `B` enters the +/// SDE and maps as `A^{-1}[e^{A Δt} − I] B z`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::InitialTimeIndependentEffectIsNotProcessIncrement`]. +pub fn refuse_initial_time_independent_effect_as_process_increment( + initial_time_independent_effect: f64, + time_independent_increment: f64, +) -> Result { + let _ = (initial_time_independent_effect, time_independent_increment); + Err(PsychometricError::InitialTimeIndependentEffectIsNotProcessIncrement) +} + +/// Refuse treating the Eq. 3 carry of `T0TIPREDEFFECT` as the +/// first-occasion shift. +/// +/// `e^{A Δt} t0_b z` is the first summand's contribution at `t`. +/// `t0_b z` is the shift at `T0`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::InitialTimeIndependentCarryIsNotInitialEffect`]. +pub fn refuse_initial_time_independent_carry_as_initial_effect( + initial_time_independent_carry: f64, + initial_time_independent_effect: f64, +) -> Result { + let _ = ( + initial_time_independent_carry, + initial_time_independent_effect, + ); + Err(PsychometricError::InitialTimeIndependentCarryIsNotInitialEffect) +} + +/// Refuse treating the Table 3 first-occasion shift as `CINT`. +/// +/// `t0_b z` is an initial-mean shift. `κ` is the continuous intercept. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::InitialTimeIndependentEffectIsNotContinuousIntercept`]. +pub fn refuse_initial_time_independent_effect_as_continuous_intercept( + initial_time_independent_effect: f64, + continuous_intercept: f64, +) -> Result { + let _ = (initial_time_independent_effect, continuous_intercept); + Err(PsychometricError::InitialTimeIndependentEffectIsNotContinuousIntercept) +} + +/// Refuse treating the Table 3 first-occasion shift as `M x`. +/// +/// The product `t0_b z` is algebraically a product, as is `M x`. +/// Table 3 names `T0TIPREDEFFECT` for `T0`. Table 2 names `M` +/// `TDPREDEFFECT` for the Dirac impulse. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::InitialTimeIndependentEffectIsNotTimeDependentImpulse`]. +pub fn refuse_initial_time_independent_effect_as_time_dependent_impulse( + initial_time_independent_effect: f64, + time_dependent_impulse: f64, +) -> Result { + let _ = (initial_time_independent_effect, time_dependent_impulse); + Err(PsychometricError::InitialTimeIndependentEffectIsNotTimeDependentImpulse) +} + +/// Refuse treating Driver Table 3 `T0TIPREDEFFECT` as the +/// first-occasion shift. +/// +/// `T0TIPREDEFFECT` is the coefficient. The shift is `t0_b z`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::InitialTimeIndependentCoefficientIsNotInitialEffect`]. +pub fn refuse_initial_time_independent_coefficient_as_initial_effect( + initial_time_independent_coefficient: f64, + initial_time_independent_effect: f64, +) -> Result { + let _ = ( + initial_time_independent_coefficient, + initial_time_independent_effect, + ); + Err(PsychometricError::InitialTimeIndependentCoefficientIsNotInitialEffect) +} + +/// Exact scalar observed mean of a first-occasion time-independent +/// predictor. +/// +/// Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Eq. 3 first summand, +/// p. 5; Table 3, p. 13; JSS PDF re-opened 2026-08-20T15:28Z from +/// ) +/// write `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and +/// `Γ ~ N(τ, Ψ)`. Table 3 names `T0TIPREDEFFECT` the effect of +/// time-independent predictors on latents at `T0`. Equation 3's +/// first summand carries that shift as `e^{A Δt} t0_b z`. The +/// expected intercept is `τ`. The latent process at `t` after that +/// carry is `μ_t + e^{a Δt} t0_b z`. The scalar composition is +/// `E(y_t) = τ + λ(μ_t + e^{a Δt} t0_b z)`. Form the +/// evolved-plus-carry latent mean first, then `τ + λ` of that mean. +/// A zero loading is exactly `τ`. A zero evolved-plus-carry latent +/// mean is exactly `τ`. A zero intercept is exactly +/// `λ(μ_t + e^{a Δt} t0_b z)`. The evolved observed mean +/// `τ + λ μ_t` is not this composition when the carry is nonzero. +/// The process-increment map +/// `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not this composition. +/// The contemporaneous map `τ + λ(μ_t + m x)` is not this +/// composition. The impulse-carry map +/// `τ + λ(μ_t + e^{a(t−u)} m x)` is not this composition when +/// `u ≠ t0`. `MANIFESTMEANS` is not `E(y_t)`. The +/// evolved-plus-carry latent mean is not `E(y_t)`. +/// `T0TIPREDEFFECT` is the coefficient, not that observed mean. +/// This is not a Kalman filter and not ctsem estimation. +/// +/// # Errors +/// +/// Propagates +/// [`recover_discrete_latent_mean_with_initial_time_independent_predictor`] +/// and [`recover_manifest_observed_mean`]. +#[allow(clippy::too_many_arguments)] +pub fn recover_discrete_observed_mean_with_initial_time_independent_predictor( + loading: f64, + initial_latent_mean: f64, + log_rate: f64, + continuous_intercept: f64, + initial_time_independent_effect: f64, + time_independent_predictor: f64, + manifest_mean: f64, + event_delta: f64, + clock: LagClock, +) -> Result { + let composed_latent_mean = + recover_discrete_latent_mean_with_initial_time_independent_predictor( + initial_latent_mean, + log_rate, + continuous_intercept, + initial_time_independent_effect, + time_independent_predictor, + event_delta, + clock, + )?; + recover_manifest_observed_mean(loading, composed_latent_mean, manifest_mean) +} + +/// Refuse treating the evolved observed mean as the first-occasion +/// time-independent-predictor observed mean. +/// +/// Equation 5 of the Eq. 3 evolved mean is `τ + λ μ_t`. Equation 5 +/// of the Table 3 first-occasion TI predictor is +/// `τ + λ(μ_t + e^{a Δt} t0_b z)`. Those are not the same map. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::EvolvedObservedMeanIsNotInitialTimeIndependentObservedMean`]. +pub fn refuse_evolved_observed_mean_as_initial_time_independent_observed_mean( + evolved_observed_mean: f64, + initial_time_independent_observed_mean: f64, +) -> Result { + let _ = ( + evolved_observed_mean, + initial_time_independent_observed_mean, + ); + Err(PsychometricError::EvolvedObservedMeanIsNotInitialTimeIndependentObservedMean) +} + +/// Refuse treating the process-increment observed mean as the +/// first-occasion time-independent-predictor observed mean. +/// +/// Equation 5 of the Eq. 3 `TIPREDEFFECT` increment is +/// `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. Equation 5 of the +/// Table 3 first-occasion TI predictor is +/// `τ + λ(μ_t + e^{a Δt} t0_b z)`. Those are not the same map. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::TimeIndependentObservedMeanIsNotInitialTimeIndependentObservedMean`]. +pub fn refuse_time_independent_observed_mean_as_initial_time_independent_observed_mean( + time_independent_observed_mean: f64, + initial_time_independent_observed_mean: f64, +) -> Result { + let _ = ( + time_independent_observed_mean, + initial_time_independent_observed_mean, + ); + Err(PsychometricError::TimeIndependentObservedMeanIsNotInitialTimeIndependentObservedMean) +} + +/// Refuse treating the contemporaneous-impulse observed mean as the +/// first-occasion time-independent-predictor observed mean. +/// +/// Equation 5 of the contemporaneous Dirac is `τ + λ(μ_t + m x)`. +/// Equation 5 of the Table 3 first-occasion TI predictor is +/// `τ + λ(μ_t + e^{a Δt} t0_b z)`. Those are not the same map. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::ImpulseObservedMeanIsNotInitialTimeIndependentObservedMean`]. +pub fn refuse_impulse_observed_mean_as_initial_time_independent_observed_mean( + impulse_observed_mean: f64, + initial_time_independent_observed_mean: f64, +) -> Result { + let _ = ( + impulse_observed_mean, + initial_time_independent_observed_mean, + ); + Err(PsychometricError::ImpulseObservedMeanIsNotInitialTimeIndependentObservedMean) +} + +/// Refuse treating the impulse-carry observed mean as the +/// first-occasion time-independent-predictor observed mean. +/// +/// Equation 5 of the Eq. 1–2 carried latent mean is +/// `τ + λ(μ_t + e^{a(t−u)} m x)`. Equation 5 of the Table 3 +/// first-occasion TI predictor is `τ + λ(μ_t + e^{a Δt} t0_b z)`. +/// Those are not the same map. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::ImpulseCarryObservedMeanIsNotInitialTimeIndependentObservedMean`]. +pub fn refuse_impulse_carry_observed_mean_as_initial_time_independent_observed_mean( + impulse_carry_observed_mean: f64, + initial_time_independent_observed_mean: f64, +) -> Result { + let _ = ( + impulse_carry_observed_mean, + initial_time_independent_observed_mean, + ); + Err(PsychometricError::ImpulseCarryObservedMeanIsNotInitialTimeIndependentObservedMean) +} + +/// Exact scalar first-occasion time-dependent predictor shift. +/// +/// Driver, Oud, and Voelkle (2017, Table 3, p. 13; Eq. 3 first +/// summand, p. 5; JSS PDF re-opened 2026-08-20T19:10Z from +/// ) +/// name `T0TDPREDEFFECT` the effect of time-dependent predictors on +/// latents at `T0`. Table 2 / Table 3 name `TDPREDEFFECT` `M`, which +/// enters Equation 3 as the printed fourth-summand Dirac `M x` at +/// `u = t`. Those are not the same matrix. The scalar first-occasion +/// shift is `t0_m x0`. It is not `M`, not `M x`, not +/// `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not +/// `A^{-1}[e^{A Δt} − I] B z`, and not `κ`. An impulse at `u ≤ t0` +/// that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as +/// `T0TDPREDEFFECT`. A zero effect or zero predictor is exactly +/// zero. This is not a Kalman filter and not ctsem estimation. +/// +/// # Errors +/// +/// Returns [`PsychometricError::InvalidNumericInput`] when the effect +/// or predictor is non-finite or the product overflows. +pub fn recover_initial_time_dependent_predictor_effect( + initial_time_dependent_effect: f64, + time_dependent_predictor: f64, +) -> Result { + if !initial_time_dependent_effect.is_finite() || !time_dependent_predictor.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + if initial_time_dependent_effect == 0.0 || time_dependent_predictor == 0.0 { + return Ok(0.0); + } + require_finite(initial_time_dependent_effect * time_dependent_predictor) +} + +/// Exact scalar carried first-occasion time-dependent predictor. +/// +/// Driver, Oud, and Voelkle (2017, Eq. 3, p. 5; Table 3, p. 13; JSS +/// PDF re-opened 2026-08-20T19:10Z) write the first summand as +/// `e^{A(t−t0)} η_i(t0)`. A Table 3 `T0TDPREDEFFECT` shift that is +/// already in `η(t0)` therefore appears at `t` as `e^{A Δt} t0_m x0`. +/// Form `t0_m x0` first, then `e^{a Δt} t0_m x0`. A zero drift is +/// `t0_m x0` with no dissipation of the first-occasion shift. +/// Binary64 underflow of `e^{a Δt}` to `+0` is a vanishing carry of +/// that shift and is kept. This carry is not the first-occasion +/// shift, not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not +/// `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `CINT`. When +/// `exp` overflows at a finite `a Δt`, rewrite as +/// `sign(t0_m x0) exp(ln|t0_m x0| + a Δt)`. An overflowing rewrite +/// fails closed. This is not a Kalman filter and not ctsem +/// estimation. +/// +/// # Errors +/// +/// Returns [`PsychometricError::EventTimeRequired`] for any non-event +/// clock, [`PsychometricError::NonPositiveInterval`] when +/// `event_delta` is not strictly positive, and +/// [`PsychometricError::InvalidNumericInput`] when an input is +/// non-finite or the mapped carry overflows. +pub fn recover_initial_time_dependent_predictor_carry( + initial_time_dependent_effect: f64, + time_dependent_predictor: f64, + log_rate: f64, + event_delta: f64, + clock: LagClock, +) -> Result { + if !clock.admits_structural_lag() { + return Err(PsychometricError::EventTimeRequired); + } + if !event_delta.is_finite() || event_delta <= 0.0 { + return Err(PsychometricError::NonPositiveInterval); + } + if !log_rate.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + let initial_shift = recover_initial_time_dependent_predictor_effect( + initial_time_dependent_effect, + time_dependent_predictor, + )?; + if initial_shift == 0.0 { + return Ok(0.0); + } + let drift_interval = log_rate * event_delta; + let auto_effect = drift_interval.exp(); + if auto_effect.is_finite() { + // +0 underflow is a vanishing carry of the T0 TD shift. + return require_finite(auto_effect * initial_shift); + } + if !drift_interval.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + // Finite a Δt, overflowed exp. + // e^{a Δt} t0_m x0 = sign(t0_m x0) exp(ln|t0_m x0| + a Δt). + require_finite(initial_shift.signum() * (initial_shift.abs().ln() + drift_interval).exp()) +} + +/// Exact scalar evolved latent mean plus a first-occasion TD predictor. +/// +/// Driver, Oud, and Voelkle (2017, Eq. 3, p. 5; Table 3, p. 13) write +/// the first summand as the carried `T0MEANS`, which includes any +/// `T0TDPREDEFFECT` shift already in `η(t0)`. Form `μ_t` first, then +/// add `e^{a Δt} t0_m x0`. A zero carry is exactly `μ_t`. A zero +/// evolved mean is exactly the carry. Adding `t0_m x0` without the +/// exponential is not this composition when `a Δt ≠ 0`. Adding +/// `M x` or `e^{A(t−u)} M x` is not this composition. +/// +/// # Errors +/// +/// Propagates [`recover_discrete_latent_mean`] and +/// [`recover_initial_time_dependent_predictor_carry`], and returns +/// [`PsychometricError::InvalidNumericInput`] when the sum overflows. +#[allow(clippy::too_many_arguments)] +pub fn recover_discrete_latent_mean_with_initial_time_dependent_predictor( + initial_latent_mean: f64, + log_rate: f64, + continuous_intercept: f64, + initial_time_dependent_effect: f64, + time_dependent_predictor: f64, + event_delta: f64, + clock: LagClock, +) -> Result { + let evolved_latent_mean = recover_discrete_latent_mean( + initial_latent_mean, + log_rate, + continuous_intercept, + event_delta, + clock, + )?; + let initial_carry = recover_initial_time_dependent_predictor_carry( + initial_time_dependent_effect, + time_dependent_predictor, + log_rate, + event_delta, + clock, + )?; + if initial_carry == 0.0 { + return Ok(evolved_latent_mean); + } + if evolved_latent_mean == 0.0 { + return Ok(initial_carry); + } + require_finite(evolved_latent_mean + initial_carry) +} + +/// Refuse treating the Table 3 first-occasion TD shift as `M x`. +/// +/// `T0TDPREDEFFECT` shifts `η(t0)`. `TDPREDEFFECT` `M` enters the +/// SDE as the contemporaneous Dirac `M x` at `u = t`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::InitialTimeDependentEffectIsNotContemporaneousImpulse`]. +pub fn refuse_initial_time_dependent_effect_as_contemporaneous_impulse( + initial_time_dependent_effect: f64, + time_dependent_impulse: f64, +) -> Result { + let _ = (initial_time_dependent_effect, time_dependent_impulse); + Err(PsychometricError::InitialTimeDependentEffectIsNotContemporaneousImpulse) +} + +/// Refuse treating the Eq. 3 carry of `T0TDPREDEFFECT` as the +/// first-occasion shift. +/// +/// `e^{A Δt} t0_m x0` is the first summand's contribution at `t`. +/// `t0_m x0` is the shift at `T0`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::InitialTimeDependentCarryIsNotInitialEffect`]. +pub fn refuse_initial_time_dependent_carry_as_initial_effect( + initial_time_dependent_carry: f64, + initial_time_dependent_effect: f64, +) -> Result { + let _ = (initial_time_dependent_carry, initial_time_dependent_effect); + Err(PsychometricError::InitialTimeDependentCarryIsNotInitialEffect) +} + +/// Refuse treating the Table 3 first-occasion TD shift as `CINT`. +/// +/// `t0_m x0` is an initial-mean shift. `κ` is the continuous intercept. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::InitialTimeDependentEffectIsNotContinuousIntercept`]. +pub fn refuse_initial_time_dependent_effect_as_continuous_intercept( + initial_time_dependent_effect: f64, + continuous_intercept: f64, +) -> Result { + let _ = (initial_time_dependent_effect, continuous_intercept); + Err(PsychometricError::InitialTimeDependentEffectIsNotContinuousIntercept) +} + +/// Refuse treating the Table 3 first-occasion TD shift as the Eq. 3 +/// process increment. +/// +/// `t0_m x0` shifts `η(t0)`. `TIPREDEFFECT` `B` maps as +/// `A^{-1}[e^{A Δt} − I] B z`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::InitialTimeDependentEffectIsNotProcessIncrement`]. +pub fn refuse_initial_time_dependent_effect_as_process_increment( + initial_time_dependent_effect: f64, + time_independent_increment: f64, +) -> Result { + let _ = (initial_time_dependent_effect, time_independent_increment); + Err(PsychometricError::InitialTimeDependentEffectIsNotProcessIncrement) +} + +/// Refuse treating the Table 3 first-occasion TD shift as the Table 3 +/// first-occasion TI shift. +/// +/// `T0TDPREDEFFECT` and `T0TIPREDEFFECT` are different Table 3 +/// matrices. `t0_m x0` is not `t0_b z`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::InitialTimeDependentEffectIsNotInitialTimeIndependentEffect`]. +pub fn refuse_initial_time_dependent_effect_as_initial_time_independent_effect( + initial_time_dependent_effect: f64, + initial_time_independent_effect: f64, +) -> Result { + let _ = ( + initial_time_dependent_effect, + initial_time_independent_effect, + ); + Err(PsychometricError::InitialTimeDependentEffectIsNotInitialTimeIndependentEffect) +} + +/// Refuse treating Driver Table 3 `T0TDPREDEFFECT` as the +/// first-occasion shift. +/// +/// `T0TDPREDEFFECT` is the coefficient. The shift is `t0_m x0`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::InitialTimeDependentCoefficientIsNotInitialEffect`]. +pub fn refuse_initial_time_dependent_coefficient_as_initial_effect( + initial_time_dependent_coefficient: f64, + initial_time_dependent_effect: f64, +) -> Result { + let _ = ( + initial_time_dependent_coefficient, + initial_time_dependent_effect, + ); + Err(PsychometricError::InitialTimeDependentCoefficientIsNotInitialEffect) +} + +/// Refuse treating the Eq. 3 carry of `T0TDPREDEFFECT` as the +/// within-interval impulse carry. +/// +/// `e^{A Δt} t0_m x0` carries a Table 3 first-occasion TD shift. +/// `e^{A(t−u)} M x` for `t0 < u < t` carries a Table 2 Dirac that +/// occurred inside the interval. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::InitialTimeDependentCarryIsNotImpulseCarry`]. +pub fn refuse_initial_time_dependent_carry_as_impulse_carry( + initial_time_dependent_carry: f64, + impulse_carry: f64, +) -> Result { + let _ = (initial_time_dependent_carry, impulse_carry); + Err(PsychometricError::InitialTimeDependentCarryIsNotImpulseCarry) +} + +/// Exact scalar observed mean of a first-occasion time-dependent +/// predictor. +/// +/// Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Eq. 3 first summand, +/// p. 5; Table 3, p. 13; JSS PDF re-opened 2026-08-20T19:20Z from +/// ) +/// write `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and +/// `Γ ~ N(τ, Ψ)`. Table 3 names `T0TDPREDEFFECT` the effect of +/// time-dependent predictors on latents at `T0`. Equation 3's first +/// summand carries that shift as `e^{A Δt} t0_m x0`. The expected +/// intercept is `τ`. The latent process at `t` after that carry is +/// `μ_t + e^{a Δt} t0_m x0`. The scalar composition is +/// `E(y_t) = τ + λ(μ_t + e^{a Δt} t0_m x0)`. Form the +/// evolved-plus-carry latent mean first, then `τ + λ` of that mean. +/// A zero loading is exactly `τ`. A zero evolved-plus-carry latent +/// mean is exactly `τ`. A zero intercept is exactly +/// `λ(μ_t + e^{a Δt} t0_m x0)`. The evolved observed mean +/// `τ + λ μ_t` is not this composition when the carry is nonzero. +/// The process-increment map +/// `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not this composition. +/// The contemporaneous map `τ + λ(μ_t + m x)` is not this +/// composition. The impulse-carry map +/// `τ + λ(μ_t + e^{a(t−u)} m x)` is not this composition when +/// `u ≠ t0`. The first-occasion TI map +/// `τ + λ(μ_t + e^{a Δt} t0_b z)` is not this composition. +/// `MANIFESTMEANS` is not `E(y_t)`. The evolved-plus-carry latent +/// mean is not `E(y_t)`. `T0TDPREDEFFECT` is the coefficient, not +/// that observed mean. This is not a Kalman filter and not ctsem +/// estimation. +/// +/// # Errors +/// +/// Propagates +/// [`recover_discrete_latent_mean_with_initial_time_dependent_predictor`] +/// and [`recover_manifest_observed_mean`]. +#[allow(clippy::too_many_arguments)] +pub fn recover_discrete_observed_mean_with_initial_time_dependent_predictor( + loading: f64, + initial_latent_mean: f64, + log_rate: f64, + continuous_intercept: f64, + initial_time_dependent_effect: f64, + time_dependent_predictor: f64, + manifest_mean: f64, + event_delta: f64, + clock: LagClock, +) -> Result { + let composed_latent_mean = recover_discrete_latent_mean_with_initial_time_dependent_predictor( + initial_latent_mean, + log_rate, + continuous_intercept, + initial_time_dependent_effect, + time_dependent_predictor, + event_delta, + clock, + )?; + recover_manifest_observed_mean(loading, composed_latent_mean, manifest_mean) +} + +/// Refuse treating the evolved observed mean as the first-occasion +/// time-dependent-predictor observed mean. +/// +/// Equation 5 of the Eq. 3 evolved mean is `τ + λ μ_t`. Equation 5 +/// of the Table 3 first-occasion TD predictor is +/// `τ + λ(μ_t + e^{a Δt} t0_m x0)`. Those are not the same map. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::EvolvedObservedMeanIsNotInitialTimeDependentObservedMean`]. +pub fn refuse_evolved_observed_mean_as_initial_time_dependent_observed_mean( + evolved_observed_mean: f64, + initial_time_dependent_observed_mean: f64, +) -> Result { + let _ = (evolved_observed_mean, initial_time_dependent_observed_mean); + Err(PsychometricError::EvolvedObservedMeanIsNotInitialTimeDependentObservedMean) +} + +/// Refuse treating the process-increment observed mean as the +/// first-occasion time-dependent-predictor observed mean. +/// +/// Equation 5 of the Eq. 3 `TIPREDEFFECT` increment is +/// `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. Equation 5 of the +/// Table 3 first-occasion TD predictor is +/// `τ + λ(μ_t + e^{a Δt} t0_m x0)`. Those are not the same map. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::TimeIndependentObservedMeanIsNotInitialTimeDependentObservedMean`]. +pub fn refuse_time_independent_observed_mean_as_initial_time_dependent_observed_mean( + time_independent_observed_mean: f64, + initial_time_dependent_observed_mean: f64, +) -> Result { + let _ = ( + time_independent_observed_mean, + initial_time_dependent_observed_mean, + ); + Err(PsychometricError::TimeIndependentObservedMeanIsNotInitialTimeDependentObservedMean) +} + +/// Refuse treating the contemporaneous-impulse observed mean as the +/// first-occasion time-dependent-predictor observed mean. +/// +/// Equation 5 of the contemporaneous Dirac is `τ + λ(μ_t + m x)`. +/// Equation 5 of the Table 3 first-occasion TD predictor is +/// `τ + λ(μ_t + e^{a Δt} t0_m x0)`. Those are not the same map. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::ImpulseObservedMeanIsNotInitialTimeDependentObservedMean`]. +pub fn refuse_impulse_observed_mean_as_initial_time_dependent_observed_mean( + impulse_observed_mean: f64, + initial_time_dependent_observed_mean: f64, +) -> Result { + let _ = (impulse_observed_mean, initial_time_dependent_observed_mean); + Err(PsychometricError::ImpulseObservedMeanIsNotInitialTimeDependentObservedMean) +} + +/// Refuse treating the impulse-carry observed mean as the +/// first-occasion time-dependent-predictor observed mean. +/// +/// Equation 5 of the Eq. 1–2 carried latent mean is +/// `τ + λ(μ_t + e^{a(t−u)} m x)`. Equation 5 of the Table 3 +/// first-occasion TD predictor is `τ + λ(μ_t + e^{a Δt} t0_m x0)`. +/// Those are not the same map. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::ImpulseCarryObservedMeanIsNotInitialTimeDependentObservedMean`]. +pub fn refuse_impulse_carry_observed_mean_as_initial_time_dependent_observed_mean( + impulse_carry_observed_mean: f64, + initial_time_dependent_observed_mean: f64, +) -> Result { + let _ = ( + impulse_carry_observed_mean, + initial_time_dependent_observed_mean, + ); + Err(PsychometricError::ImpulseCarryObservedMeanIsNotInitialTimeDependentObservedMean) +} + +/// Refuse treating the first-occasion TI observed mean as the +/// first-occasion TD observed mean. +/// +/// Equation 5 of Table 3 `T0TIPREDEFFECT` is +/// `τ + λ(μ_t + e^{a Δt} t0_b z)`. Equation 5 of Table 3 +/// `T0TDPREDEFFECT` is `τ + λ(μ_t + e^{a Δt} t0_m x0)`. Those are +/// not the same map. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::InitialTimeIndependentObservedMeanIsNotInitialTimeDependentObservedMean`]. +pub fn refuse_initial_time_independent_observed_mean_as_initial_time_dependent_observed_mean( + initial_time_independent_observed_mean: f64, + initial_time_dependent_observed_mean: f64, +) -> Result { + let _ = ( + initial_time_independent_observed_mean, + initial_time_dependent_observed_mean, + ); + Err(PsychometricError::InitialTimeIndependentObservedMeanIsNotInitialTimeDependentObservedMean) +} + +/// Exact scalar within-interval time-dependent impulse carry from +/// Driver Equations 1–2. +/// +/// Driver, Oud, and Voelkle (2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; +/// §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z from +/// ) +/// write `dη = (A η + ξ + B z + M χ(t)) dt + G dW` with +/// `χ_i(t) = Σ_{u ∈ U_i} x_{i,u} δ(t − u)`. The Green-function +/// integral of that Dirac on `(t0, t)` is `e^{A(t−u)} M x`. The +/// printed Eq. 3 fourth summand is the contemporaneous jump `M x` +/// at `u = t`. This map is the strictly within-interval case +/// `t0 < u < t`: form `m x` first, then `e^{a(t−u)} m x`. A zero +/// drift is `m x` with no dissipation. Binary64 underflow of +/// `e^{a(t−u)}` to `+0` is vanishing dissipation back to the process +/// mean (§7.2) and is kept. A zero effect or zero predictor is +/// exactly zero even if the exponential overflows. When `e^{a(t−u)}` +/// overflows at a finite `a(t−u)`, rewrite as +/// `sign(m x) exp(ln|m x| + a(t−u))`. An impulse at `u = t` is the +/// contemporaneous map. An impulse at `u ≤ t0` is already in `η(t0)`. +/// The §7.2 level-change form is a different specification and is +/// not this map. This is not a Kalman filter and not ctsem +/// estimation. +/// +/// # Errors +/// +/// Returns [`PsychometricError::EventTimeRequired`] for any non-event +/// clock, [`PsychometricError::NonPositiveInterval`] when +/// `event_delta` or `elapsed_after_impulse` is not strictly positive +/// or the impulse is not strictly inside `(t0, t)`, and +/// [`PsychometricError::InvalidNumericInput`] when an input is +/// non-finite or `m x` or the carried product overflows. +pub fn recover_time_dependent_predictor_impulse_carry( + time_dependent_effect: f64, + time_dependent_predictor: f64, + log_rate: f64, + event_delta: f64, + elapsed_after_impulse: f64, + clock: LagClock, +) -> Result { + if !clock.admits_structural_lag() { + return Err(PsychometricError::EventTimeRequired); + } + if !event_delta.is_finite() || event_delta <= 0.0 { + return Err(PsychometricError::NonPositiveInterval); + } + if !elapsed_after_impulse.is_finite() || elapsed_after_impulse <= 0.0 { + return Err(PsychometricError::NonPositiveInterval); + } + // I_{t0 < u < t}: t−u strictly less than t−t0, so u−t0 > 0. + if elapsed_after_impulse >= event_delta { + return Err(PsychometricError::NonPositiveInterval); + } + if !log_rate.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + let impulse = + recover_time_dependent_predictor_impulse(time_dependent_effect, time_dependent_predictor)?; + if impulse == 0.0 { + return Ok(0.0); + } + let drift_interval = log_rate * elapsed_after_impulse; + let auto_effect = drift_interval.exp(); + if auto_effect.is_finite() { + // +0 underflow is vanishing dissipation (§7.2). + return require_finite(auto_effect * impulse); + } + if !drift_interval.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + // Finite a(t−u), overflowed exp. + // e^{a(t−u)} m x = sign(m x) exp(ln|m x| + a(t−u)). + require_finite(impulse.signum() * (impulse.abs().ln() + drift_interval).exp()) +} + +/// Exact scalar evolved latent mean plus a within-interval impulse carry. +/// +/// Driver, Oud, and Voelkle (2017, Eq. 1–3, p. 5; §7.2) write the +/// first two summands as the carried `T0MEANS` and `CINT` increment, +/// then add a Dirac impulse that occurred strictly inside `(t0, t)` +/// after it has dissipated by `e^{A(t−u)}`. Form `μ_t` first, then +/// add `e^{a(t−u)} m x`. A zero carry is exactly `μ_t`. A zero +/// evolved mean is exactly the carry. Adding the contemporaneous +/// `m x` is not this composition when `u ≠ t`. The level-change +/// form is not this map. +/// +/// # Errors +/// +/// Propagates [`recover_discrete_latent_mean`] and +/// [`recover_time_dependent_predictor_impulse_carry`], and returns +/// [`PsychometricError::InvalidNumericInput`] when the sum overflows. +#[allow(clippy::too_many_arguments)] +pub fn recover_discrete_latent_mean_with_impulse_carry( + initial_latent_mean: f64, + log_rate: f64, + continuous_intercept: f64, + time_dependent_effect: f64, + time_dependent_predictor: f64, + event_delta: f64, + elapsed_after_impulse: f64, + clock: LagClock, +) -> Result { + let evolved_latent_mean = recover_discrete_latent_mean( + initial_latent_mean, + log_rate, + continuous_intercept, + event_delta, + clock, + )?; + let impulse_carry = recover_time_dependent_predictor_impulse_carry( + time_dependent_effect, + time_dependent_predictor, + log_rate, + event_delta, + elapsed_after_impulse, + clock, + )?; + if impulse_carry == 0.0 { + return Ok(evolved_latent_mean); + } + if evolved_latent_mean == 0.0 { + return Ok(impulse_carry); + } + require_finite(evolved_latent_mean + impulse_carry) +} + +/// Exact scalar observed mean of a within-interval impulse carry. +/// +/// Driver, Oud, and Voelkle (2017, Eq. 5, p. 5; Eq. 1–2, pp. 4–5; +/// Eq. 3 exponential map; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF +/// re-opened 2026-08-20T05:12Z from +/// ) +/// write `y_i(t) = Γ + Λ η_i(t) + ζ_i(t)` with `ζ ~ N(0, Θ)` and +/// `Γ ~ N(τ, Ψ)`. The expected intercept is `τ`. The latent process +/// at `t` after a Dirac that occurred strictly inside `(t0, t)` is +/// `μ_t + e^{a(t−u)} m x`. The scalar composition is +/// `E(y_t) = τ + λ(μ_t + e^{a(t−u)} m x)`. Form the carried latent +/// mean first, then `τ + λ` of that mean. Table 2 names `τ` +/// `MANIFESTMEANS`. A zero loading is exactly `τ`. A zero +/// evolved-plus-carry latent mean is exactly `τ`. A zero intercept +/// is exactly `λ(μ_t + carry)`. The evolved observed mean +/// `τ + λ μ_t` is not this composition when the carry is nonzero. +/// The contemporaneous map `τ + λ(μ_t + m x)` is not this +/// composition when `u ≠ t`. `MANIFESTMEANS` is not `E(y_t)`. The +/// carried latent mean is not `E(y_t)`. The §7.2 level-change form +/// is a different specification and is not this map. This is not a +/// Kalman filter and not ctsem estimation. +/// +/// # Errors +/// +/// Propagates [`recover_discrete_latent_mean_with_impulse_carry`] and +/// [`recover_manifest_observed_mean`]. +#[allow(clippy::too_many_arguments)] +pub fn recover_discrete_observed_mean_with_impulse_carry( + loading: f64, + initial_latent_mean: f64, + log_rate: f64, + continuous_intercept: f64, + time_dependent_effect: f64, + time_dependent_predictor: f64, + manifest_mean: f64, + event_delta: f64, + elapsed_after_impulse: f64, + clock: LagClock, +) -> Result { + let carried_latent_mean = recover_discrete_latent_mean_with_impulse_carry( + initial_latent_mean, + log_rate, + continuous_intercept, + time_dependent_effect, + time_dependent_predictor, + event_delta, + elapsed_after_impulse, + clock, + )?; + recover_manifest_observed_mean(loading, carried_latent_mean, manifest_mean) +} + +/// Refuse treating the evolved observed mean as the impulse-carry +/// observed mean. +/// +/// Equation 5 of the Eq. 3 evolved mean is `τ + λ μ_t`. Equation 5 +/// of the Eq. 1–2 carried latent mean is +/// `τ + λ(μ_t + e^{a(t−u)} m x)`. Those are not the same map. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::EvolvedObservedMeanIsNotImpulseCarryObservedMean`]. +pub fn refuse_evolved_observed_mean_as_impulse_carry_observed_mean( + evolved_observed_mean: f64, + impulse_carry_observed_mean: f64, +) -> Result { + let _ = (evolved_observed_mean, impulse_carry_observed_mean); + Err(PsychometricError::EvolvedObservedMeanIsNotImpulseCarryObservedMean) +} + +/// Refuse treating the Eq. 1–2 impulse carry as the contemporaneous Dirac. +/// +/// The printed Eq. 3 fourth summand is `M x` at `u = t`. The +/// within-interval carry is `e^{A(t−u)} M x` for `t0 < u < t`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::TimeDependentImpulseCarryIsNotContemporaneousImpulse`]. +pub fn refuse_time_dependent_impulse_carry_as_contemporaneous_impulse( + time_dependent_impulse_carry: f64, + time_dependent_impulse: f64, +) -> Result { + let _ = (time_dependent_impulse_carry, time_dependent_impulse); + Err(PsychometricError::TimeDependentImpulseCarryIsNotContemporaneousImpulse) +} + +/// Refuse treating the Eq. 1–2 impulse carry as `CINT`. +/// +/// Table 2 names `M` `TDPREDEFFECT` and `κ` `CINT`. The dissipated +/// impulse is not the continuous intercept. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::TimeDependentImpulseCarryIsNotContinuousIntercept`]. +pub fn refuse_time_dependent_impulse_carry_as_continuous_intercept( + time_dependent_impulse_carry: f64, + continuous_intercept: f64, +) -> Result { + let _ = (time_dependent_impulse_carry, continuous_intercept); + Err(PsychometricError::TimeDependentImpulseCarryIsNotContinuousIntercept) +} + +/// Refuse treating the Eq. 1–2 impulse carry as `TIPREDEFFECT`. +/// +/// The second-summand map integrates a constant `B z` over the event +/// interval. The within-interval TDPRED carry dissipates a Dirac. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::TimeDependentImpulseCarryIsNotTimeIndependentEffect`]. +pub fn refuse_time_dependent_impulse_carry_as_time_independent_effect( + time_dependent_impulse_carry: f64, + time_independent_effect: f64, +) -> Result { + let _ = (time_dependent_impulse_carry, time_independent_effect); + Err(PsychometricError::TimeDependentImpulseCarryIsNotTimeIndependentEffect) +} + +/// Refuse treating the Eq. 1–2 impulse carry as Voelkle et al. +/// (2012, Eq. 14). +/// +/// Equation 14 is `a_{yx} Δt` for a piecewise-constant time-varying +/// predictor. The Dirac carry is `e^{A(t−u)} M x`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::TimeDependentImpulseCarryIsNotTimeVaryingDiscreteEffect`]. +pub fn refuse_time_dependent_impulse_carry_as_time_varying_discrete_effect( + time_dependent_impulse_carry: f64, + time_varying_discrete_effect: f64, +) -> Result { + let _ = (time_dependent_impulse_carry, time_varying_discrete_effect); + Err(PsychometricError::TimeDependentImpulseCarryIsNotTimeVaryingDiscreteEffect) +} + +/// Refuse treating Driver Table 2 `T0MEANS` as the evolved latent mean. +/// +/// Equation 3 maps `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`. +/// `T0MEANS` is `μ_0`, not `μ_t`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::InitialLatentMeanIsNotEvolvedMean`]. +pub fn refuse_initial_latent_mean_as_evolved_mean( + initial_latent_mean: f64, + evolved_latent_mean: f64, +) -> Result { + let _ = (initial_latent_mean, evolved_latent_mean); + Err(PsychometricError::InitialLatentMeanIsNotEvolvedMean) +} + +/// Refuse treating Driver Table 2 `CINT` as the discrete mean increment. +/// +/// `κ` is the continuous intercept. Equation 3 maps it through +/// `A^{-1}[e^{A Δt} − I]`. `κ` is not that increment. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::ContinuousInterceptIsNotDiscreteMeanIncrement`]. +pub fn refuse_continuous_intercept_as_discrete_mean_increment( + continuous_intercept: f64, + discrete_mean_increment: f64, +) -> Result { + let _ = (continuous_intercept, discrete_mean_increment); + Err(PsychometricError::ContinuousInterceptIsNotDiscreteMeanIncrement) +} + +/// Refuse treating Driver Table 2 `CINT` as `T0MEANS`. +/// +/// Table 2 (p. 12) names `κ` `CINT` and the first-occasion latent +/// mean `T0MEANS`. `κ` is not `E(η_{i1})`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::ContinuousInterceptIsNotInitialLatentMean`]. +pub fn refuse_continuous_intercept_as_initial_latent_mean( + continuous_intercept: f64, + initial_latent_mean: f64, +) -> Result { + let _ = (continuous_intercept, initial_latent_mean); + Err(PsychometricError::ContinuousInterceptIsNotInitialLatentMean) +} + +/// Refuse treating Driver Eq. 3 process noise as the unconditional variance. +/// +/// Driver, Oud, and Voelkle (2017, Eq. 3–4, pp. 4–5): +/// `Q_Δt = cov(η_ti | η_{t-1,i})` for the homogeneous process. That +/// residual variance is not `Var(η_ti)` when the previous state is +/// random. The JSS article has no numbered §2.2. +/// +/// # Errors +/// +/// Always returns [`PsychometricError::ProcessNoiseIsConditionalVariance`]. +pub fn refuse_process_noise_as_unconditional_variance( + process_noise: f64, + prior_variance: f64, +) -> Result { + let _ = (process_noise, prior_variance); + Err(PsychometricError::ProcessNoiseIsConditionalVariance) +} + +/// Refuse the difference quotient as a continuous-time rate. +/// +/// Voelkle et al. (2012) discourage `(x(t+Δt) − x(t)) / Δt` as the drift. +/// +/// # Errors +/// +/// Always returns [`PsychometricError::DifferenceQuotientForbidden`]. +pub fn refuse_difference_quotient_as_local_rate( + earlier: f64, + later: f64, + delta: f64, +) -> Result { + let _ = (earlier, later, delta); + Err(PsychometricError::DifferenceQuotientForbidden) +} + +/// Mean local log-rate across consecutive event-time pairs. +/// +/// Occasions are sorted by event time. Each pair uses the exact scalar map. +/// Equal or inverted times fail closed. +/// +/// # Errors +/// +/// Returns [`PsychometricError::EventTimeRequired`] for a non-event clock, +/// [`PsychometricError::InvalidNumericInput`] for fewer than two occasions or +/// non-finite values, and [`PsychometricError::NonPositiveInterval`] when +/// consecutive times are not strictly increasing. +pub fn recover_event_series_mean_log_rate( + occasions: &[EventOccasion], + clock: LagClock, +) -> Result { + if !clock.admits_structural_lag() { + return Err(PsychometricError::EventTimeRequired); + } + if occasions.len() < 2 { + return Err(PsychometricError::InvalidNumericInput); + } + let mut ordered = occasions.to_vec(); + ordered.sort_by(|left, right| { + left.event_time + .partial_cmp(&right.event_time) + .unwrap_or(std::cmp::Ordering::Equal) + }); + let mut rates = Vec::new(); + for window in ordered.windows(2) { + let earlier = window[0]; + let later = window[1]; + if !earlier.event_time.is_finite() + || !later.event_time.is_finite() + || !earlier.score.is_finite() + || !later.score.is_finite() + { + return Err(PsychometricError::InvalidNumericInput); + } + let delta = later.event_time - earlier.event_time; + let recovered = + recover_event_time_discrete_lag_and_log_rate(earlier.score, later.score, delta, clock)?; + rates.push(recovered.log_rate); + } + let count = rates.len() as f64; + require_finite(rates.iter().sum::() / count) +} + +/// Local log-rate of cluster-mean-centered residuals on event time. +/// +/// Stable between-cluster means are removed first (CWC). Consecutive +/// within-cluster residuals then use the exact scalar map. This is not DSEM. +/// +/// Curran and Bauer (2011, pp. 607–608) show that subtracting the observed +/// person-specific mean from a raw autoregressive series does **not** isolate +/// the lagged within-person effect. This helper therefore does not claim to +/// recover the raw-process drift `a` from CWC of a raw AR path. For that +/// estimand, supply already-centered lagged residuals to +/// [`recover_irregular_centered_residual_log_rate`]. +/// +/// # Errors +/// +/// Returns [`PsychometricError::EventTimeRequired`] for a non-event clock, +/// [`PsychometricError::InvalidNumericInput`] for empty, singleton, or +/// non-finite rows, [`PsychometricError::InsufficientClusters`] when fewer +/// than two clusters appear, and interval/lag errors from the scalar map. +pub fn recover_within_residual_event_time_log_rate( + rows: &[ClusteredEventScore], + clock: LagClock, +) -> Result { + if !clock.admits_structural_lag() { + return Err(PsychometricError::EventTimeRequired); + } + if rows.len() < 2 { + return Err(PsychometricError::InvalidNumericInput); + } + let mut groups: BTreeMap> = BTreeMap::new(); + for &row in rows { + if !row.event_time.is_finite() || !row.score.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + groups.entry(row.cluster_key).or_default().push(row); + } + if groups.len() < 2 { + return Err(PsychometricError::InsufficientClusters); + } + let mut pairs = Vec::new(); + for occasions in groups.values_mut() { + if occasions.len() < 2 { + continue; + } + let count = occasions.len() as f64; + let mean = occasions.iter().map(|row| row.score).sum::() / count; + occasions.sort_by(|left, right| { + left.event_time + .partial_cmp(&right.event_time) + .unwrap_or(std::cmp::Ordering::Equal) + }); + for window in occasions.windows(2) { + let earlier_resid = window[0].score - mean; + let later_resid = window[1].score - mean; + let delta = window[1].event_time - window[0].event_time; + if !delta.is_finite() || delta <= 0.0 { + return Err(PsychometricError::NonPositiveInterval); + } + if !(earlier_resid.is_finite() & later_resid.is_finite()) { + return Err(PsychometricError::InvalidNumericInput); + } + pairs.push((earlier_resid, later_resid, delta)); + } + } + fit_scalar_log_rate(&pairs) +} + +/// Mean exact scalar log-rate on already-centered residuals with irregular intervals. +/// +/// Each pair is `a = ln(later / earlier) / Δt` (Voelkle et al., 2012, Eq. 7). +/// The function does **not** center again. Curran and Bauer (2011, pp. 607–608) +/// reject person-mean subtraction on a raw autoregressive series as the +/// lagged within-person residual. Intervals may be irregular. This is not DSEM. +/// +/// # Errors +/// +/// Returns [`PsychometricError::EventTimeRequired`] for a non-event clock, +/// [`PsychometricError::InvalidNumericInput`] for an empty series or a +/// non-finite / non-positive residual ratio, and +/// [`PsychometricError::NonPositiveInterval`] when any interval is not +/// strictly positive. +pub fn recover_irregular_centered_residual_log_rate( + pairs: &[LaggedWithinResidual], + clock: LagClock, +) -> Result { + if !clock.admits_structural_lag() { + return Err(PsychometricError::EventTimeRequired); + } + if pairs.is_empty() { + return Err(PsychometricError::InvalidNumericInput); + } + let mut sum = 0.0_f64; + for pair in pairs { + if !pair.earlier_residual.is_finite() + || !pair.later_residual.is_finite() + || !pair.event_delta.is_finite() + { + return Err(PsychometricError::InvalidNumericInput); + } + let recovered = recover_event_time_discrete_lag_and_log_rate( + pair.earlier_residual, + pair.later_residual, + pair.event_delta, + clock, + )?; + sum += recovered.log_rate; + } + let count = pairs.len() as f64; + require_finite(sum / count) +} + +/// Least-squares scalar log-rate for already-formed residual pairs. +/// +/// Pair-wise logs initialize Newton. This helper is crate-visible so overflow +/// and flat-derivative guards can be recovered in unit tests. It is not a +/// public DSEM estimator. +pub(crate) fn fit_scalar_log_rate(pairs: &[(f64, f64, f64)]) -> Result { + if pairs.is_empty() { + return Err(PsychometricError::InvalidNumericInput); + } + let mut start_sum = 0.0_f64; + let mut start_count = 0.0_f64; + for &(earlier, later, delta) in pairs { + if earlier != 0.0 { + let discrete_lag = later / earlier; + if discrete_lag.is_finite() && discrete_lag > 0.0 { + start_sum += discrete_lag.ln() / delta; + start_count += 1.0; + } + } + } + if start_count <= 0.0 { + return Err(PsychometricError::InvalidNumericInput); + } + let mut log_rate = start_sum / start_count; + for _ in 0..16 { + let mut score = 0.0_f64; + let mut derivative = 0.0_f64; + for &(earlier, later, delta) in pairs { + let mapped = (log_rate * delta).exp(); + if !mapped.is_finite() || mapped <= 0.0 { + return Err(PsychometricError::InvalidNumericInput); + } + let weight = delta * earlier; + score += weight * mapped * later - delta * mapped * mapped * earlier * earlier; + derivative += delta * weight * mapped * later + - 2.0 * delta * delta * mapped * mapped * earlier * earlier; + } + if !score.is_finite() || !derivative.is_finite() { + return Err(PsychometricError::InvalidNumericInput); + } + if derivative.abs() <= 1e-18 { + break; + } + let next = log_rate - score / derivative; + if (next - log_rate).abs() < 1e-14 { + log_rate = next; + break; + } + log_rate = next; + } + require_finite(log_rate) +} + +#[cfg(test)] +mod tests { + use super::{ + ClusteredEventScore, EventOccasion, LagClock, LaggedWithinResidual, fit_scalar_log_rate, + map_discrete_lag_across_event_intervals, recover_asymptotic_continuous_intercept, + recover_asymptotic_time_independent_observed_variance, + recover_asymptotic_time_independent_predictor_effect, + recover_asymptotic_time_independent_predictor_variance, + recover_discrete_constant_predictor_effect, recover_discrete_continuous_intercept_effect, + recover_discrete_lag_from_log_rate, recover_discrete_lag_one, + recover_discrete_lagged_latent_covariance, recover_discrete_latent_mean, + recover_discrete_latent_mean_with_extra_process, + recover_discrete_latent_mean_with_extra_process_after, + recover_discrete_latent_mean_with_impulse, recover_discrete_latent_mean_with_impulse_carry, + recover_discrete_latent_mean_with_initial_time_dependent_predictor, + recover_discrete_latent_mean_with_initial_time_independent_predictor, + recover_discrete_latent_mean_with_time_independent_predictor, + recover_discrete_latent_variance, recover_discrete_observed_mean, + recover_discrete_observed_mean_with_extra_process, + recover_discrete_observed_mean_with_extra_process_after, + recover_discrete_observed_mean_with_impulse, + recover_discrete_observed_mean_with_impulse_carry, + recover_discrete_observed_mean_with_initial_time_dependent_predictor, + recover_discrete_observed_mean_with_initial_time_independent_predictor, + recover_discrete_observed_mean_with_time_independent_predictor, + recover_discrete_process_noise, recover_discrete_time_independent_predictor_effect, + recover_discrete_time_varying_predictor_effect, recover_event_series_mean_log_rate, + recover_event_time_discrete_lag_and_log_rate, + recover_initial_time_dependent_predictor_carry, + recover_initial_time_dependent_predictor_effect, + recover_initial_time_independent_observed_variance, + recover_initial_time_independent_predictor_carry, + recover_initial_time_independent_predictor_effect, + recover_initial_time_independent_predictor_variance, + recover_irregular_centered_residual_log_rate, recover_level_change_continuous_intercept, + recover_level_change_discrete_increment, recover_level_change_extra_process_contribution, + recover_level_change_extra_process_contribution_after, recover_local_log_rate, + recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, + recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, + recover_predetermined_initial_latent_variance, + recover_predetermined_initial_observed_variance, + recover_predetermined_lagged_latent_covariance, + recover_predetermined_lagged_observed_covariance, + recover_predetermined_later_lagged_latent_covariance, + recover_predetermined_later_lagged_observed_covariance, + recover_predetermined_later_latent_variance, recover_predetermined_later_observed_variance, + recover_predetermined_later_start_later_latent_variance, + recover_predetermined_later_start_later_observed_variance, + recover_standardised_asymptotic_continuous_intercept, + recover_standardised_asymptotic_diffusion, + recover_standardised_asymptotic_time_independent_predictor_effect, + recover_standardised_continuous_diffusion, recover_standardised_continuous_drift, + recover_standardised_continuous_time_dependent_predictor_effect, + recover_standardised_continuous_time_independent_predictor_effect, + recover_standardised_discrete_continuous_intercept, + recover_standardised_discrete_diffusion, recover_standardised_discrete_drift, + recover_standardised_initial_latent_mean, recover_standardised_initial_latent_variance, + recover_standardised_initial_time_dependent_predictor_effect, + recover_standardised_initial_time_independent_predictor_effect, + recover_standardised_manifest_trait_variance, recover_standardised_manifest_variance, + recover_standardised_time_independent_predictor_variance, + recover_standardised_trait_variance, recover_stationary_initial_latent_mean, + recover_stationary_initial_latent_variance, recover_stationary_initial_observed_mean, + recover_stationary_initial_observed_variance, recover_stationary_lagged_latent_covariance, + recover_stationary_lagged_observed_covariance, recover_stationary_latent_variance, + recover_stationary_later_latent_variance, recover_stationary_later_observed_variance, + recover_time_dependent_predictor_impulse, recover_time_dependent_predictor_impulse_carry, + recover_trait_plus_state_lagged_covariance, recover_trait_plus_state_latent_variance, + recover_within_residual_event_time_log_rate, + refuse_after_extra_process_contribution_as_observed_mean, + refuse_after_extra_process_latent_mean_as_observed_mean, + refuse_asymptotic_continuous_intercept_as_asymptotic_time_independent_effect, + refuse_asymptotic_continuous_intercept_as_continuous_intercept, + refuse_asymptotic_continuous_intercept_as_discrete_increment, + refuse_asymptotic_continuous_intercept_as_initial_latent_mean, + refuse_asymptotic_continuous_intercept_observed_mean_as_stationary_initial_observed_mean, + refuse_asymptotic_standardised_continuous_intercept_as_standardised_discrete_continuous_intercept, + refuse_asymptotic_time_independent_effect_as_coefficient, + refuse_asymptotic_time_independent_effect_as_continuous_intercept, + refuse_asymptotic_time_independent_effect_as_discrete_effect, + refuse_asymptotic_time_independent_effect_as_time_dependent_impulse, + refuse_asymptotic_time_independent_observed_variance_as_asymptotic_time_independent_variance, + refuse_asymptotic_time_independent_observed_variance_as_initial_time_independent_observed_variance, + refuse_asymptotic_time_independent_observed_variance_as_measurement_error, + refuse_asymptotic_time_independent_observed_variance_as_stationary_observed_variance, + refuse_asymptotic_time_independent_predictor_variance_as_standardised_time_independent_predictor_variance, + refuse_asymptotic_time_independent_variance_as_asymptotic_effect, + refuse_asymptotic_time_independent_variance_as_stationary_within_subject, + refuse_asymptotic_time_independent_variance_as_trait_variance, + refuse_continuous_intercept_as_discrete_mean_increment, + refuse_continuous_intercept_as_initial_latent_mean, + refuse_continuous_intercept_as_manifest_means, refuse_difference_quotient_as_local_rate, + refuse_evolved_observed_mean_as_after_extra_process_observed_mean, + refuse_evolved_observed_mean_as_extra_process_observed_mean, + refuse_evolved_observed_mean_as_impulse_carry_observed_mean, + refuse_evolved_observed_mean_as_impulse_observed_mean, + refuse_evolved_observed_mean_as_initial_time_dependent_observed_mean, + refuse_evolved_observed_mean_as_initial_time_independent_observed_mean, + refuse_evolved_observed_mean_as_stationary_initial_observed_mean, + refuse_evolved_observed_mean_as_time_independent_observed_mean, + refuse_evolved_observed_variance_as_stationary_initial_observed_variance, + refuse_extra_process_contribution_as_observed_mean, + refuse_extra_process_latent_mean_as_observed_mean, + refuse_extra_process_observed_mean_as_after_extra_process_observed_mean, + refuse_finite_interval_process_noise_as_stationary_variance, + refuse_impulse_carry_observed_mean_as_after_extra_process_observed_mean, + refuse_impulse_carry_observed_mean_as_initial_time_dependent_observed_mean, + refuse_impulse_carry_observed_mean_as_initial_time_independent_observed_mean, + refuse_impulse_carry_observed_mean_as_time_independent_observed_mean, + refuse_impulse_observed_mean_as_extra_process_observed_mean, + refuse_impulse_observed_mean_as_impulse_carry_observed_mean, + refuse_impulse_observed_mean_as_initial_time_dependent_observed_mean, + refuse_impulse_observed_mean_as_initial_time_independent_observed_mean, + refuse_impulse_observed_mean_as_time_independent_observed_mean, + refuse_initial_latent_mean_as_evolved_mean, + refuse_initial_observed_mean_as_evolved_observed_mean, + refuse_initial_observed_mean_as_stationary_initial_observed_mean, + refuse_initial_observed_variance_as_stationary_initial_observed_variance, + refuse_initial_time_dependent_carry_as_impulse_carry, + refuse_initial_time_dependent_carry_as_initial_effect, + refuse_initial_time_dependent_coefficient_as_initial_effect, + refuse_initial_time_dependent_effect_as_contemporaneous_impulse, + refuse_initial_time_dependent_effect_as_continuous_intercept, + refuse_initial_time_dependent_effect_as_initial_time_independent_effect, + refuse_initial_time_dependent_effect_as_process_increment, + refuse_initial_time_independent_carry_as_initial_effect, + refuse_initial_time_independent_coefficient_as_initial_effect, + refuse_initial_time_independent_effect_as_continuous_intercept, + refuse_initial_time_independent_effect_as_process_increment, + refuse_initial_time_independent_effect_as_time_dependent_impulse, + refuse_initial_time_independent_observed_mean_as_initial_time_dependent_observed_mean, + refuse_initial_time_independent_observed_variance_as_asymptotic_time_independent_observed_variance, + refuse_initial_time_independent_observed_variance_as_initial_observed_variance, + refuse_initial_time_independent_observed_variance_as_initial_time_independent_variance, + refuse_initial_time_independent_observed_variance_as_measurement_error, + refuse_initial_time_independent_variance_as_asymptotic_time_independent_variance, + refuse_initial_time_independent_variance_as_initial_latent_variance, + refuse_initial_time_independent_variance_as_standardised_initial_latent_variance, + refuse_initial_time_independent_variance_as_standardised_initial_time_independent_effect, + refuse_initial_time_independent_variance_as_standardised_trait_variance, + refuse_initial_time_independent_variance_as_trait_variance, + refuse_latent_lagged_covariance_as_observed_covariance, + refuse_latent_mean_as_observed_mean, refuse_latent_variance_as_observed_variance, + refuse_level_change_extra_process_as_impulse, + refuse_level_change_extra_process_as_increment, + refuse_level_change_extra_process_as_intercept, refuse_level_change_increment_as_impulse, + refuse_level_change_increment_as_intercept, + refuse_level_change_increment_as_process_increment, + refuse_level_change_intercept_as_free_continuous_intercept, + refuse_level_change_intercept_as_impulse, + refuse_level_change_intercept_as_process_increment, refuse_manifest_means_as_observed_mean, + refuse_manifest_trait_variance_as_measurement_error, + refuse_measurement_error_as_lagged_observed_covariance, + refuse_measurement_error_as_observed_variance, + refuse_measurement_error_as_predetermined_initial_observed_variance, + refuse_measurement_error_as_predetermined_lagged_observed_covariance, + refuse_measurement_error_as_predetermined_later_lagged_observed_covariance, + refuse_measurement_error_as_predetermined_later_observed_variance, + refuse_measurement_error_as_predetermined_later_start_later_observed_variance, + refuse_measurement_error_as_standardised_manifest_trait_variance, + refuse_measurement_error_as_stationary_lagged_observed_covariance, + refuse_measurement_error_as_stationary_later_observed_variance, + refuse_observed_variance_as_standardised_manifest_variance, + refuse_pooled_discrete_lag_across_unequal_intervals, + refuse_predetermined_initial_latent_variance_as_initial_latent_variance, + refuse_predetermined_initial_latent_variance_as_lagged_latent_covariance, + refuse_predetermined_initial_latent_variance_as_later_latent_variance, + refuse_predetermined_initial_latent_variance_as_observed_variance, + refuse_predetermined_initial_latent_variance_as_stationary_initial_latent_variance, + refuse_predetermined_lagged_latent_covariance_as_decayed_total, + refuse_predetermined_lagged_latent_covariance_as_initial_latent_variance, + refuse_predetermined_lagged_latent_covariance_as_later_latent_variance, + refuse_predetermined_lagged_latent_covariance_as_observed_covariance, + refuse_predetermined_lagged_latent_covariance_as_stationary_lagged_covariance, + refuse_predetermined_lagged_observed_covariance_as_predetermined_later_lagged_observed_covariance, + refuse_predetermined_later_lagged_latent_covariance_as_decayed_later_total, + refuse_predetermined_later_lagged_latent_covariance_as_later_latent_variance, + refuse_predetermined_later_lagged_latent_covariance_as_observed_covariance, + refuse_predetermined_later_lagged_latent_covariance_as_predetermined_lagged_covariance, + refuse_predetermined_later_lagged_latent_covariance_as_stationary_lagged_covariance, + refuse_predetermined_later_lagged_observed_covariance_as_predetermined_later_start_later_observed_variance, + refuse_predetermined_later_latent_variance_as_discrete_variance, + refuse_predetermined_later_latent_variance_as_initial_latent_variance, + refuse_predetermined_later_latent_variance_as_observed_variance, + refuse_predetermined_later_latent_variance_as_stationary_later_latent_variance, + refuse_predetermined_later_observed_variance_as_predetermined_initial_observed_variance, + refuse_predetermined_later_observed_variance_as_predetermined_lagged_observed_covariance, + refuse_predetermined_later_observed_variance_as_predetermined_later_lagged_observed_covariance, + refuse_predetermined_later_observed_variance_as_predetermined_later_start_later_observed_variance, + refuse_predetermined_later_start_later_latent_variance_as_decayed_later_total, + refuse_predetermined_later_start_later_latent_variance_as_lag_interval_later_latent_variance, + refuse_predetermined_later_start_later_latent_variance_as_later_lagged_covariance, + refuse_predetermined_later_start_later_latent_variance_as_later_latent_variance, + refuse_predetermined_later_start_later_latent_variance_as_observed_variance, + refuse_predetermined_later_start_later_latent_variance_as_stationary_later_latent_variance, + refuse_process_noise_as_unconditional_variance, + refuse_standardised_asymptotic_time_independent_effect_as_standardised_continuous_time_independent_effect, + refuse_standardised_asymptotic_time_independent_effect_as_standardised_initial_time_independent_effect, + refuse_standardised_continuous_diffusion_as_standardised_asymptotic_diffusion, + refuse_standardised_continuous_diffusion_as_standardised_discrete_diffusion, + refuse_standardised_continuous_intercept_as_standardised_asymptotic_continuous_intercept, + refuse_standardised_continuous_intercept_as_standardised_discrete_continuous_intercept, + refuse_standardised_continuous_time_dependent_effect_as_standardised_initial_time_dependent_effect, + refuse_standardised_continuous_time_independent_effect_as_standardised_continuous_time_dependent_effect, + refuse_standardised_continuous_time_independent_effect_as_standardised_initial_time_independent_effect, + refuse_standardised_discrete_continuous_intercept_as_standardised_asymptotic_continuous_intercept, + refuse_standardised_discrete_diffusion_as_standardised_continuous_diffusion, + refuse_standardised_discrete_drift_as_standardised_continuous_drift, + refuse_standardised_discrete_time_dependent_effect_as_standardised_continuous_time_dependent_effect, + refuse_standardised_discrete_time_independent_effect_as_standardised_asymptotic_time_independent_effect, + refuse_standardised_discrete_time_independent_effect_as_standardised_continuous_time_independent_effect, + refuse_standardised_initial_latent_variance_as_standardised_initial_latent_mean, + refuse_standardised_initial_latent_variance_as_standardised_trait_variance, + refuse_standardised_initial_time_dependent_effect_as_standardised_initial_latent_variance, + refuse_standardised_initial_time_independent_effect_as_standardised_initial_time_dependent_effect, + refuse_standardised_manifest_trait_variance_as_standardised_manifest_variance, + refuse_standardised_manifest_variance_as_standardised_time_independent_predictor_variance, + refuse_standardised_time_independent_predictor_variance_as_standardised_asymptotic_diffusion, + refuse_standardised_trait_variance_as_standardised_manifest_trait_variance, + refuse_stationary_initial_latent_mean_as_asymptotic_continuous_intercept, + refuse_stationary_initial_latent_mean_as_asymptotic_time_independent_effect, + refuse_stationary_initial_latent_mean_as_discrete_mean, + refuse_stationary_initial_latent_mean_as_initial_latent_mean, + refuse_stationary_initial_latent_mean_as_observed_mean, + refuse_stationary_initial_latent_variance_as_asymptotic_time_independent_variance, + refuse_stationary_initial_latent_variance_as_discrete_variance, + refuse_stationary_initial_latent_variance_as_initial_latent_variance, + refuse_stationary_initial_latent_variance_as_observed_variance, + refuse_stationary_initial_latent_variance_as_stationary_within_subject, + refuse_stationary_initial_latent_variance_as_trait_variance, + refuse_stationary_initial_observed_mean_as_manifest_means, + refuse_stationary_initial_observed_variance_as_measurement_error, + refuse_stationary_initial_observed_variance_as_predetermined_initial_observed_variance, + refuse_stationary_initial_observed_variance_as_stationary_lagged_observed_covariance, + refuse_stationary_lagged_latent_covariance_as_decayed_stationary_variance, + refuse_stationary_lagged_latent_covariance_as_observed_covariance, + refuse_stationary_lagged_latent_covariance_as_stationary_initial_latent_variance, + refuse_stationary_lagged_observed_covariance_as_predetermined_lagged_observed_covariance, + refuse_stationary_lagged_observed_covariance_as_predetermined_later_lagged_observed_covariance, + refuse_stationary_lagged_observed_covariance_as_stationary_later_observed_variance, + refuse_stationary_later_latent_variance_as_discrete_variance, + refuse_stationary_later_latent_variance_as_lagged_covariance, + refuse_stationary_later_latent_variance_as_observed_variance, + refuse_stationary_later_latent_variance_as_process_noise, + refuse_stationary_later_observed_variance_as_predetermined_later_observed_variance, + refuse_stationary_later_observed_variance_as_predetermined_later_start_later_observed_variance, + refuse_stationary_within_subject_observed_variance_as_stationary_initial_observed_variance, + refuse_time_dependent_impulse_as_continuous_intercept, + refuse_time_dependent_impulse_as_time_independent_effect, + refuse_time_dependent_impulse_as_time_varying_discrete_effect, + refuse_time_dependent_impulse_carry_as_contemporaneous_impulse, + refuse_time_dependent_impulse_carry_as_continuous_intercept, + refuse_time_dependent_impulse_carry_as_time_independent_effect, + refuse_time_dependent_impulse_carry_as_time_varying_discrete_effect, + refuse_time_independent_coefficient_as_discrete_effect, + refuse_time_independent_effect_as_continuous_intercept, + refuse_time_independent_effect_as_time_dependent_impulse, + refuse_time_independent_effect_as_time_varying_discrete_effect, + refuse_time_independent_observed_mean_as_initial_time_dependent_observed_mean, + refuse_time_independent_observed_mean_as_initial_time_independent_observed_mean, + refuse_trait_contaminated_asymptotic_time_independent_effect_as_standardised_asymptotic_time_independent_effect, + refuse_trait_contaminated_continuous_diffusion_as_standardised_continuous_diffusion, + refuse_trait_contaminated_continuous_drift_as_standardised_continuous_drift, + refuse_trait_contaminated_continuous_time_dependent_effect_as_standardised_continuous_time_dependent_effect, + refuse_trait_contaminated_continuous_time_independent_effect_as_standardised_continuous_time_independent_effect, + refuse_trait_contaminated_initial_time_dependent_effect_as_standardised_initial_time_dependent_effect, + refuse_trait_contaminated_initial_time_independent_effect_as_standardised_initial_time_independent_effect, + refuse_trait_contaminated_process_noise_as_standardised_discrete_diffusion, + refuse_trait_plus_state_autocorrelation_as_standardised_discrete_drift, + refuse_trait_plus_state_lagged_covariance_as_stationary_lagged_latent_covariance, + refuse_trait_variance_as_process_noise, refuse_trait_variance_as_standardisation_variance, + refuse_trait_variance_as_stationary_within_subject, + refuse_unmatched_time_varying_predictor_interval, + refuse_unstandardised_asymptotic_continuous_intercept_as_standardised_asymptotic_continuous_intercept, + refuse_unstandardised_asymptotic_diffusion_as_standardised_asymptotic_diffusion, + refuse_unstandardised_asymptotic_time_independent_effect_as_standardised_asymptotic_time_independent_effect, + refuse_unstandardised_continuous_diffusion_as_standardised_continuous_diffusion, + refuse_unstandardised_continuous_drift_as_standardised_continuous_drift, + refuse_unstandardised_continuous_time_dependent_effect_as_standardised_continuous_time_dependent_effect, + refuse_unstandardised_continuous_time_independent_effect_as_standardised_continuous_time_independent_effect, + refuse_unstandardised_discrete_continuous_intercept_as_standardised_discrete_continuous_intercept, + refuse_unstandardised_discrete_diffusion_as_standardised_discrete_diffusion, + refuse_unstandardised_discrete_drift_as_standardised_discrete_drift, + refuse_unstandardised_initial_latent_mean_as_standardised_initial_latent_mean, + refuse_unstandardised_initial_latent_variance_as_standardised_initial_latent_variance, + refuse_unstandardised_initial_time_dependent_effect_as_standardised_initial_time_dependent_effect, + refuse_unstandardised_initial_time_independent_effect_as_standardised_initial_time_independent_effect, + refuse_unstandardised_manifest_trait_variance_as_standardised_manifest_trait_variance, + refuse_unstandardised_manifest_variance_as_standardised_manifest_variance, + refuse_unstandardised_time_independent_predictor_variance_as_standardised_time_independent_predictor_variance, + refuse_unstandardised_trait_variance_as_standardised_trait_variance, + refuse_within_subject_scaled_initial_latent_mean_as_standardised_initial_latent_mean, + }; + use crate::error::PsychometricError; + + #[test] + fn exact_scalar_map_inverts_exponential_drift() { + let drift = -0.5_f64; + let delta = 2.0_f64; + let earlier = 1.5_f64; + let later = earlier * (drift * delta).exp(); + let recovered = recover_event_time_discrete_lag_and_log_rate( + earlier, + later, + delta, + LagClock::EventTime, + ) + .expect("exact"); + assert!((recovered.log_rate - drift).abs() < 1e-12); + assert!((recovered.discrete_lag - (drift * delta).exp()).abs() < 1e-12); + assert!((recovered.event_delta - delta).abs() < 1e-15); + } + + #[test] + fn forward_map_inverts_log_rate_and_remaps_unequal_intervals() { + let drift = -0.4_f64; + let source_delta = 1.0_f64; + let reference_delta = 2.0_f64; + let source_lag = + recover_discrete_lag_from_log_rate(drift, source_delta, LagClock::EventTime) + .expect("forward"); + assert!((source_lag - (drift * source_delta).exp()).abs() < 1e-12); + let same = map_discrete_lag_across_event_intervals( + source_lag, + source_delta, + source_delta, + LagClock::EventTime, + ) + .expect("same interval"); + assert!((same - source_lag).abs() < 1e-12); + let remapped = map_discrete_lag_across_event_intervals( + source_lag, + source_delta, + reference_delta, + LagClock::EventTime, + ) + .expect("remap"); + assert!((remapped - (drift * reference_delta).exp()).abs() < 1e-12); + // Voelkle manuscript p. 2, 33: φ(1) ≠ φ(2) even for one process. + assert!((source_lag - remapped).abs() > 1e-9); + assert_eq!( + refuse_pooled_discrete_lag_across_unequal_intervals(source_delta, reference_delta), + Err(PsychometricError::UnequalIntervalPoolingForbidden) + ); + assert_eq!( + refuse_pooled_discrete_lag_across_unequal_intervals(source_delta, source_delta), + Err(PsychometricError::UnequalIntervalPoolingForbidden) + ); + } + + #[test] + fn forward_map_and_interval_remap_fail_closed() { + assert_eq!( + recover_discrete_lag_from_log_rate(-0.2, 1.0, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_discrete_lag_from_log_rate(-0.2, 0.0, LagClock::EventTime), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_lag_from_log_rate(-0.2, -1.0, LagClock::EventTime), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_lag_from_log_rate(-0.2, f64::NAN, LagClock::EventTime), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_lag_from_log_rate(f64::NAN, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_lag_from_log_rate(800.0, 10.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_lag_from_log_rate(-800.0, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_lag_from_log_rate(-1.0, 800.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + let source_lag = + recover_discrete_lag_from_log_rate(-0.7, 1.0, LagClock::EventTime).expect("source φ"); + assert!(source_lag > 0.0); + assert_eq!( + map_discrete_lag_across_event_intervals(source_lag, 1.0, 2000.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + map_discrete_lag_across_event_intervals(0.5, 1.0, 2.0, LagClock::AssertionTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + map_discrete_lag_across_event_intervals(0.5, 0.0, 2.0, LagClock::EventTime), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + map_discrete_lag_across_event_intervals(0.5, 1.0, 0.0, LagClock::EventTime), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + map_discrete_lag_across_event_intervals(-0.2, 1.0, 2.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + } + + #[test] + fn constant_predictor_discrete_effect_recovers_equation_twelve() { + let outcome_on_predictor = 0.2_f64; + let predictor_log_rate = -0.5_f64; + let delta = 2.0_f64; + let recovered = recover_discrete_constant_predictor_effect( + outcome_on_predictor, + predictor_log_rate, + delta, + LagClock::EventTime, + ) + .expect("eq 12"); + let expected = + (outcome_on_predictor / predictor_log_rate) * (predictor_log_rate * delta).exp_m1(); + assert!((recovered - expected).abs() < 1e-15); + let first_order = outcome_on_predictor * delta; + assert!((recovered - first_order).abs() > 1e-3); + assert_eq!( + recover_discrete_constant_predictor_effect( + outcome_on_predictor, + predictor_log_rate, + delta, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_discrete_constant_predictor_effect( + outcome_on_predictor, + predictor_log_rate, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_constant_predictor_effect( + outcome_on_predictor, + predictor_log_rate, + -1.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_constant_predictor_effect( + outcome_on_predictor, + predictor_log_rate, + f64::NAN, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_constant_predictor_effect( + f64::NAN, + predictor_log_rate, + delta, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_constant_predictor_effect( + outcome_on_predictor, + 0.0, + delta, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_constant_predictor_effect( + outcome_on_predictor, + f64::NAN, + delta, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_constant_predictor_effect(1e300, 1e-300, 1e300, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + let underflowed_argument = + recover_discrete_constant_predictor_effect(1e308, 1e-308, 1e-308, LagClock::EventTime) + .expect("eq 12 limit"); + assert!((underflowed_argument - 1.0).abs() < 1e-15); + let tiny_nonzero = + recover_discrete_constant_predictor_effect(1e308, 1e-154, 1e-154, LagClock::EventTime) + .expect("eq 12 scaled"); + assert!(tiny_nonzero.is_finite()); + assert!((tiny_nonzero - 1e154).abs() / 1e154 < 1e-12); + // a_yx Δt overflows; Eq. 12 remains finite (Voelkle 2012, Eq. 12). + let product_overflow = + recover_discrete_constant_predictor_effect(1e308, -100.0, 10.0, LagClock::EventTime) + .expect("eq 12 finite after a_yx Δt overflow"); + let product_overflow_expected = (1e308 / -100.0) * (-100.0_f64 * 10.0).exp_m1(); + assert!((product_overflow - product_overflow_expected).abs() / 1e306 < 1e-12); + assert!(product_overflow.is_finite()); + assert!(!(1e308_f64 * 10.0).is_finite()); + } + + #[test] + fn constant_predictor_negative_overflow_recovers_equilibrium_increment() { + // z → -∞: expm1(z)/z * Δt is +0; Eq. 12 → -a_yx/a_xx (Voelkle + // 2012, Introducing Intercepts equilibrium increment). + let increment_argument = -1e308_f64 * 2.0; + assert!(increment_argument.is_infinite()); + assert!(increment_argument.is_sign_negative()); + let lost_scale = increment_argument.exp_m1() / increment_argument * 2.0; + assert_eq!(lost_scale.to_bits(), 0.0_f64.to_bits()); + let negative_overflow = + recover_discrete_constant_predictor_effect(1.0, -1e308, 2.0, LagClock::EventTime) + .expect("eq 12 equilibrium increment"); + let negative_overflow_expected = -(1.0 / -1e308); + assert!((negative_overflow - negative_overflow_expected).abs() / 1e-308 < 1e-12); + assert!(negative_overflow > 0.0); + assert!(negative_overflow.is_finite()); + } + + #[test] + fn constant_predictor_expm1_overflow_recovers_finite_equation_twelve() { + // expm1(800) is +∞; (1e-308/800)(exp(800)−1) is finite. + assert!(!800.0_f64.exp_m1().is_finite()); + assert!(!(1e-308_f64 * (800.0_f64.exp_m1() / 800.0)).is_finite()); + let recovered = + recover_discrete_constant_predictor_effect(1e-308, 800.0, 1.0, LagClock::EventTime) + .expect("eq 12 log-space"); + let expected = (1e-308_f64.ln() + 800.0 - 800.0_f64.ln()).exp() - 1e-308 / 800.0; + assert!((recovered - expected).abs() / expected < 1e-12); + assert!(recovered.is_finite()); + assert!(recovered > 0.0); + let negative = + recover_discrete_constant_predictor_effect(-1e-308, 800.0, 1.0, LagClock::EventTime) + .expect("eq 12 signed log-space"); + assert!((negative + expected).abs() / expected < 1e-12); + assert_eq!( + recover_discrete_constant_predictor_effect(0.0, 800.0, 1.0, LagClock::EventTime), + Ok(0.0) + ); + assert_eq!( + recover_discrete_constant_predictor_effect(0.0, 1e308, 2.0, LagClock::EventTime), + Ok(0.0) + ); + assert_eq!( + recover_discrete_constant_predictor_effect(1.0, 800.0, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_constant_predictor_effect(1.0, 1e308, 2.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + // a_yx/a_xx overflows; the Eq. 12 rewrite term is not a binary64 number. + assert!(!800.0_f64.exp_m1().is_finite()); + assert!(!(1e308_f64 / 1e-10).is_finite()); + assert_eq!( + recover_discrete_constant_predictor_effect(1e308, 1e-10, 8e12, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + } + + #[test] + fn time_varying_predictor_discrete_effect_recovers_equation_fourteen() { + let outcome_on_predictor = 0.2_f64; + let delta = 2.0_f64; + let recovered = recover_discrete_time_varying_predictor_effect( + outcome_on_predictor, + delta, + delta, + delta, + LagClock::EventTime, + ) + .expect("eq 14"); + assert!((recovered - outcome_on_predictor * delta).abs() < 1e-15); + let constant = recover_discrete_constant_predictor_effect( + outcome_on_predictor, + -0.5, + delta, + LagClock::EventTime, + ) + .expect("eq 12"); + // Voelkle 2012, p. 21: Eq. 14 is not Eq. 12. + assert!((recovered - constant).abs() > 1e-3); + assert_eq!( + recover_discrete_time_varying_predictor_effect( + 0.0, + delta, + delta, + delta, + LagClock::EventTime + ), + Ok(0.0) + ); + } + + #[test] + fn time_varying_predictor_unmatched_and_invalid_inputs_fail_closed() { + let outcome_on_predictor = 0.2_f64; + let delta = 2.0_f64; + assert_eq!( + recover_discrete_time_varying_predictor_effect( + outcome_on_predictor, + delta, + delta, + delta, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_discrete_time_varying_predictor_effect( + outcome_on_predictor, + 0.0, + 0.0, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_time_varying_predictor_effect( + outcome_on_predictor, + -1.0, + 1.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_time_varying_predictor_effect( + outcome_on_predictor, + 1.0, + f64::NAN, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_time_varying_predictor_effect( + outcome_on_predictor, + 1.0, + 1.0, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_time_varying_predictor_effect( + outcome_on_predictor, + 1.0, + 2.0, + 2.0, + LagClock::EventTime + ), + Err(PsychometricError::UnmatchedTimeVaryingInterval) + ); + assert_eq!( + recover_discrete_time_varying_predictor_effect( + outcome_on_predictor, + 2.0, + 2.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::UnmatchedTimeVaryingInterval) + ); + assert_eq!( + recover_discrete_time_varying_predictor_effect( + f64::NAN, + delta, + delta, + delta, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_time_varying_predictor_effect( + 1e308, + 10.0, + 10.0, + 10.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + refuse_unmatched_time_varying_predictor_interval(1.0, 2.0), + Err(PsychometricError::UnmatchedTimeVaryingInterval) + ); + assert_eq!( + refuse_unmatched_time_varying_predictor_interval(1.0, 1.0), + Err(PsychometricError::UnmatchedTimeVaryingInterval) + ); + } + + #[test] + fn discrete_process_noise_recovers_driver_equation_three() { + let diffusion = 0.4_f64; + let drift = -0.5_f64; + let delta = 1.0_f64; + let recovered = + recover_discrete_process_noise(diffusion, drift, delta, LagClock::EventTime) + .expect("q_dt"); + let expected = diffusion * ((2.0 * drift * delta).exp() - 1.0) / (2.0 * drift); + assert!((recovered - expected).abs() < 1e-15); + // a = 0 is the integral of a constant diffusion: q Δt. + assert_eq!( + recover_discrete_process_noise(diffusion, 0.0, 2.5, LagClock::EventTime), + Ok(diffusion * 2.5) + ); + // Binary64 underflow of 2 a Δt recovers the same limit. + let underflowed = recover_discrete_process_noise(1.0, 1e-308, 1e-308, LagClock::EventTime) + .expect("z underflow"); + assert!((underflowed - 1e-308).abs() < 1e-320); + // z → −∞ keeps the equilibrium variance −q / (2 a). + let equilibrium = + recover_discrete_process_noise(0.4, -1e300, 2.0, LagClock::EventTime).expect("eq var"); + assert!((equilibrium - (0.4 / (2.0 * 1e300))).abs() < 1e-315); + // Finite z, overflowed expm1: log-space rewrite stays finite. + let overflowed = recover_discrete_process_noise(1e-308, 400.0, 1.0, LagClock::EventTime) + .expect("expm1 overflow"); + let rewrite_scale = 1e-308 / 800.0; + let rewrite_log = (1e-308_f64).ln() + 800.0 - 800.0_f64.ln(); + let rewrite = rewrite_log.exp() - rewrite_scale; + assert!((overflowed - rewrite).abs() / rewrite.abs() < 1e-12); + assert_eq!( + recover_discrete_process_noise(0.0, 800.0, 1.0, LagClock::EventTime), + Ok(0.0) + ); + assert_eq!( + recover_discrete_process_noise(0.0, 1e308, 2.0, LagClock::EventTime), + Ok(0.0) + ); + // Forming 2 a first overflows; z = 2 (a Δt) stays finite. + let twice_rate_overflow = + recover_discrete_process_noise(1.0, 1e308, 1e-308, LagClock::EventTime) + .expect("2a overflow"); + let expected_twice_rate = 0.5 * 2.0_f64.exp_m1() / 1e308; + assert!((twice_rate_overflow - expected_twice_rate).abs() / expected_twice_rate < 1e-12); + // 2 a overflows to −∞; expm1(−∞) = −1 keeps −0.5 q / a. + let overflowed_equilibrium = + recover_discrete_process_noise(1e308, -1e308, 2.0, LagClock::EventTime) + .expect("2a eq var"); + assert!((overflowed_equilibrium - 0.5).abs() < 1e-15); + } + + #[test] + fn discrete_process_noise_invalid_inputs_fail_closed() { + assert_eq!( + recover_discrete_process_noise(0.4, -0.5, 1.0, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_discrete_process_noise(0.4, -0.5, 0.0, LagClock::EventTime), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_process_noise(0.4, -0.5, -1.0, LagClock::EventTime), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_process_noise(0.4, -0.5, f64::NAN, LagClock::EventTime), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_process_noise(-0.1, -0.5, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_process_noise(f64::NAN, -0.5, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_process_noise(0.4, f64::NAN, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_process_noise(1.0, 800.0, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_process_noise(1.0, 1e308, 2.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + // Finite z, overflowed expm1, overflowing 0.5 q / a. + // q (e^{2 a Δt} − 1) / (2 a) is then non-finite (Driver Eq. 3). + assert_eq!( + recover_discrete_process_noise(1e308, 0.1, 4000.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + } + + #[test] + fn lagged_covariance_and_latent_variance_follow_driver_equations_three_and_four() { + let prior = 2.0_f64; + let diffusion = 0.4_f64; + let drift = -0.5_f64; + let delta = 1.0_f64; + let lagged = + recover_discrete_lagged_latent_covariance(prior, drift, delta, LagClock::EventTime) + .expect("lagged cov"); + let expected_lagged = (drift * delta).exp() * prior; + assert!((lagged - expected_lagged).abs() < 1e-15); + let process_noise = + recover_discrete_process_noise(diffusion, drift, delta, LagClock::EventTime) + .expect("q_dt"); + let latent = + recover_discrete_latent_variance(prior, diffusion, drift, delta, LagClock::EventTime) + .expect("var"); + let expected_var = (2.0 * drift * delta).exp() * prior + process_noise; + assert!((latent - expected_var).abs() < 1e-15); + assert!((latent - process_noise).abs() > 1e-3); + assert_eq!( + refuse_process_noise_as_unconditional_variance(process_noise, prior), + Err(PsychometricError::ProcessNoiseIsConditionalVariance) + ); + assert_eq!( + recover_discrete_lagged_latent_covariance(0.0, 800.0, 1.0, LagClock::EventTime), + Ok(0.0) + ); + let underflowed_lagged = + recover_discrete_lagged_latent_covariance(2.0, -1e308, 2.0, LagClock::EventTime) + .expect("underflow lagged"); + assert_eq!(underflowed_lagged.to_bits(), 0.0_f64.to_bits()); + let rewritten = + recover_discrete_lagged_latent_covariance(1e-308, 800.0, 1.0, LagClock::EventTime) + .expect("rewrite lagged"); + let expected_rewrite = (1e-308_f64.ln() + 800.0).exp(); + assert!((rewritten - expected_rewrite).abs() / expected_rewrite < 1e-12); + let zero_prior = + recover_discrete_latent_variance(0.0, diffusion, drift, delta, LagClock::EventTime) + .expect("zero prior"); + assert!((zero_prior - process_noise).abs() < 1e-15); + let drifted_zero = + recover_discrete_latent_variance(2.0, diffusion, 0.0, 2.5, LagClock::EventTime) + .expect("a=0"); + assert!((drifted_zero - (2.0 + diffusion * 2.5)).abs() < 1e-15); + let underflowed_var = + recover_discrete_latent_variance(2.0, 1.0, 1e-308, 1e-308, LagClock::EventTime) + .expect("z underflow"); + assert!((underflowed_var - (2.0 + 1.0 * 1e-308)).abs() < 1e-15); + let vanished = + recover_discrete_latent_variance(2.0, 1e308, -1e308, 2.0, LagClock::EventTime) + .expect("phi_sq underflow"); + assert!((vanished - 0.5).abs() < 1e-15); + let rewritten_var = + recover_discrete_latent_variance(1e-308, 1e-308, 400.0, 1.0, LagClock::EventTime) + .expect("rewrite var"); + assert!(rewritten_var.is_finite()); + assert!(rewritten_var > 0.0); + } + + #[test] + fn lagged_covariance_and_latent_variance_overflow_paths_fail_closed() { + assert_eq!( + recover_discrete_lagged_latent_covariance(1e308, 800.0, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_lagged_latent_covariance(2.0, 1e308, 2.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_latent_variance(1e308, 1e-308, 400.0, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_latent_variance(2.0, 1.0, 1e308, 2.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + // Zero diffusion is exactly Q_Δt = 0 (Driver Eq. 3). That skip + // does not license exp(2 a Δt) p when 2 (a Δt) overflows to +∞. + assert_eq!( + recover_discrete_latent_variance(2.0, 0.0, 1e308, 2.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_lagged_latent_covariance(1e308, 700.0, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_latent_variance(1e308, 1e-308, 350.0, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_latent_variance(1e308, 1e308, 0.0, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + let carried = (-90.622_f64).exp(); + let diffusion_sum = (-83.938_f64).exp(); + assert_eq!( + recover_discrete_latent_variance( + carried, + diffusion_sum, + 400.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_lagged_latent_covariance(-0.1, -0.5, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_lagged_latent_covariance(f64::NAN, -0.5, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_lagged_latent_covariance(2.0, f64::NAN, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_lagged_latent_covariance(2.0, -0.5, 0.0, LagClock::EventTime), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_lagged_latent_covariance(2.0, -0.5, -1.0, LagClock::EventTime), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_lagged_latent_covariance(2.0, -0.5, f64::NAN, LagClock::EventTime), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_lagged_latent_covariance(2.0, -0.5, 1.0, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_discrete_latent_variance(-0.1, 0.4, -0.5, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_latent_variance(f64::NAN, 0.4, -0.5, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_latent_variance(2.0, 0.4, -0.5, 1.0, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + } + + #[test] + fn stationary_variance_recovers_driver_equation_four_asymptote() { + let diffusion = 0.4_f64; + let drift = -0.5_f64; + let recovered = recover_stationary_latent_variance(diffusion, drift, LagClock::EventTime) + .expect("asym"); + let expected = (diffusion / drift) * -0.5; + assert!((recovered - expected).abs() < 1e-15); + assert!((recovered - 0.4).abs() < 1e-15); + // Starting from p_∞, Var(η_t) is invariant across finite Δt. + for delta in [0.5_f64, 1.0, 2.0, 10.0] { + let evolved = recover_discrete_latent_variance( + recovered, + diffusion, + drift, + delta, + LagClock::EventTime, + ) + .expect("invariant"); + assert!( + (evolved - recovered).abs() < 1e-12, + "stationary variance must be invariant at Δt={delta}" + ); + } + let finite_noise = + recover_discrete_process_noise(diffusion, drift, 1.0, LagClock::EventTime) + .expect("finite q_dt"); + assert!((finite_noise - recovered).abs() > 1e-3); + assert_eq!( + refuse_finite_interval_process_noise_as_stationary_variance(finite_noise, 1.0), + Err(PsychometricError::FiniteIntervalProcessNoiseIsNotStationary) + ); + assert_eq!( + refuse_finite_interval_process_noise_as_stationary_variance(recovered, 1.0), + Err(PsychometricError::FiniteIntervalProcessNoiseIsNotStationary) + ); + assert_eq!( + recover_stationary_latent_variance(0.0, drift, LagClock::EventTime), + Ok(0.0) + ); + // Do not form 2 a first: 2*(-1e308) overflows; (q/a)*-0.5 is 0.5. + let twice_rate_overflow = + recover_stationary_latent_variance(1e308, -1e308, LagClock::EventTime) + .expect("2a overflow"); + assert!((twice_rate_overflow - 0.5).abs() < 1e-15); + assert!(!(2.0 * -1e308_f64).is_finite()); + let lost = -1e308_f64 / (2.0 * -1e308_f64); + assert!(lost.abs() < 1e-15); + // Do not form 0.5 q first: 0.5 * from_bits(1) underflows. + let min_subnormal = f64::from_bits(1); + assert!((0.5 * min_subnormal).abs() < 1e-300); + assert!((-0.5 * min_subnormal / -min_subnormal).abs() < 1e-300); + let subnormal_ratio = + recover_stationary_latent_variance(min_subnormal, -min_subnormal, LagClock::EventTime) + .expect("subnormal ratio"); + assert!((subnormal_ratio - 0.5).abs() < 1e-15); + assert!(((min_subnormal / -min_subnormal) * -0.5 - 0.5).abs() < 1e-15); + // Do not form q/a first: MAX/-0.75 overflows; MAX/(2*0.75) is finite. + assert!(!(f64::MAX / -0.75_f64).is_finite()); + assert!(!((f64::MAX / -0.75_f64) * -0.5).is_finite()); + let twice = -0.75_f64 * 2.0; + assert!(twice.is_finite()); + let expected_max = f64::MAX / -twice; + assert!(expected_max.is_finite()); + assert_eq!(expected_max.to_bits(), (f64::MAX / 1.5).to_bits()); + let quotient_overflow = + recover_stationary_latent_variance(f64::MAX, -0.75, LagClock::EventTime) + .expect("q/a overflow"); + assert_eq!(quotient_overflow.to_bits(), expected_max.to_bits()); + } + + #[test] + fn stationary_variance_unstable_and_invalid_inputs_fail_closed() { + assert_eq!( + recover_stationary_latent_variance(0.4, -0.5, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_stationary_latent_variance(0.4, 0.0, LagClock::EventTime), + Err(PsychometricError::StationaryVarianceRequiresStableDrift) + ); + assert_eq!( + recover_stationary_latent_variance(0.4, 0.5, LagClock::EventTime), + Err(PsychometricError::StationaryVarianceRequiresStableDrift) + ); + assert_eq!( + recover_stationary_latent_variance(0.0, 0.0, LagClock::EventTime), + Err(PsychometricError::StationaryVarianceRequiresStableDrift) + ); + assert_eq!( + recover_stationary_latent_variance(-0.1, -0.5, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_stationary_latent_variance(f64::NAN, -0.5, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_stationary_latent_variance(0.4, f64::NAN, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + // The Lyapunov solution overflows when |q| >> |a|. + assert!(!((1e308_f64 / -1e-10_f64) * -0.5).is_finite()); + assert!(!(1e308_f64 / (2.0 * 1e-10_f64)).is_finite()); + assert_eq!( + recover_stationary_latent_variance(1e308, -1e-10, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + } + + #[test] + fn trait_plus_state_recovers_driver_section_four_point_three() { + let trait_variance = 1.5_f64; + let diffusion = 0.4_f64; + let drift = -0.5_f64; + let delta = 1.0_f64; + let state = recover_stationary_latent_variance(diffusion, drift, LagClock::EventTime) + .expect("state"); + let total = recover_trait_plus_state_latent_variance(trait_variance, state).expect("sum"); + assert!((total - (trait_variance + state)).abs() < 1e-15); + let lagged = recover_trait_plus_state_lagged_covariance( + trait_variance, + state, + drift, + delta, + LagClock::EventTime, + ) + .expect("lagged"); + let state_lagged = + recover_discrete_lagged_latent_covariance(state, drift, delta, LagClock::EventTime) + .expect("state lagged"); + assert!((lagged - (trait_variance + state_lagged)).abs() < 1e-15); + // Evolving the summed variance as if it were all state is not + // the trait-plus-state map (Driver §4.3; Hamaker et al., 2015). + let evolved_as_state = + recover_discrete_latent_variance(total, diffusion, drift, delta, LagClock::EventTime) + .expect("wrong"); + let evolved_state = + recover_discrete_latent_variance(state, diffusion, drift, delta, LagClock::EventTime) + .expect("state evolved"); + let evolved_right = + recover_trait_plus_state_latent_variance(trait_variance, evolved_state).expect("right"); + assert!((evolved_right - total).abs() < 1e-12); + assert!((evolved_as_state - evolved_right).abs() > 1e-3); + assert_eq!( + recover_trait_plus_state_latent_variance(0.0, state), + Ok(state) + ); + assert_eq!( + recover_trait_plus_state_latent_variance(trait_variance, 0.0), + Ok(trait_variance) + ); + assert_eq!( + recover_trait_plus_state_lagged_covariance( + 0.0, + state, + drift, + delta, + LagClock::EventTime + ), + Ok(state_lagged) + ); + assert_eq!( + recover_trait_plus_state_lagged_covariance( + trait_variance, + 0.0, + drift, + delta, + LagClock::EventTime + ), + Ok(trait_variance) + ); + let process_noise = + recover_discrete_process_noise(diffusion, drift, delta, LagClock::EventTime) + .expect("q_dt"); + assert_eq!( + refuse_trait_variance_as_process_noise(trait_variance, process_noise), + Err(PsychometricError::TraitVarianceIsNotProcessNoise) + ); + assert_eq!( + refuse_trait_variance_as_stationary_within_subject(trait_variance, state), + Err(PsychometricError::TraitVarianceIsNotStationaryWithinSubject) + ); + } + + #[test] + fn trait_plus_state_invalid_inputs_fail_closed() { + assert_eq!( + recover_trait_plus_state_latent_variance(-0.1, 0.4), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_trait_plus_state_latent_variance(0.4, -0.1), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_trait_plus_state_latent_variance(f64::NAN, 0.4), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_trait_plus_state_latent_variance(0.4, f64::NAN), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_trait_plus_state_latent_variance(1e308, 1e308), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_trait_plus_state_lagged_covariance(-0.1, 0.4, -0.5, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_trait_plus_state_lagged_covariance( + f64::NAN, + 0.4, + -0.5, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_trait_plus_state_lagged_covariance(0.4, 0.4, -0.5, 1.0, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_trait_plus_state_lagged_covariance(1e308, 1e308, 0.0, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + } + + #[test] + fn non_event_clocks_and_difference_quotient_fail_closed() { + for clock in [ + LagClock::SystemTime, + LagClock::AssertionTime, + LagClock::DocumentTime, + LagClock::AvailabilityTime, + LagClock::KnowledgeCutoff, + ] { + assert_eq!( + recover_local_log_rate(0.5, 1.0, clock), + Err(PsychometricError::EventTimeRequired) + ); + assert!(!clock.admits_structural_lag()); + assert!(!clock.as_str().is_empty()); + } + assert!(LagClock::EventTime.admits_structural_lag()); + assert_eq!(LagClock::EventTime.as_str(), "event_time"); + assert_eq!( + refuse_difference_quotient_as_local_rate(1.0, 0.5, 1.0), + Err(PsychometricError::DifferenceQuotientForbidden) + ); + } + + #[test] + fn invalid_lag_inputs_fail_closed() { + assert_eq!( + recover_discrete_lag_one(0.0, 1.0), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_lag_one(f64::NAN, 1.0), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_lag_one(1.0, f64::INFINITY), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_local_log_rate(0.5, 0.0, LagClock::EventTime), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_local_log_rate(0.5, -1.0, LagClock::EventTime), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_local_log_rate(0.5, f64::NAN, LagClock::EventTime), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_local_log_rate(0.0, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_local_log_rate(-0.2, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_local_log_rate(f64::NAN, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + } + + #[test] + fn series_mean_log_rate_recovers_and_refuses() { + let drift = -0.25_f64; + let occasions = [ + EventOccasion { + event_time: 0.0, + score: 2.0, + }, + EventOccasion { + event_time: 1.0, + score: 2.0 * drift.exp(), + }, + EventOccasion { + event_time: 3.0, + score: 2.0 * (drift * 3.0).exp(), + }, + ]; + let series = + recover_event_series_mean_log_rate(&occasions, LagClock::EventTime).expect("series"); + assert!((series - drift).abs() < 1e-12); + assert_eq!( + recover_event_series_mean_log_rate(&occasions, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_event_series_mean_log_rate(&[], LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_event_series_mean_log_rate( + &[EventOccasion { + event_time: 0.0, + score: 1.0, + }], + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_event_series_mean_log_rate( + &[ + EventOccasion { + event_time: f64::NAN, + score: 1.0, + }, + EventOccasion { + event_time: 1.0, + score: 0.5, + }, + ], + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_event_series_mean_log_rate( + &[occasion(0.0, 1.0), occasion(f64::NAN, 0.5)], + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_event_series_mean_log_rate( + &[occasion(0.0, f64::NAN), occasion(1.0, 0.5)], + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_event_series_mean_log_rate( + &[occasion(0.0, 1.0), occasion(1.0, f64::NAN)], + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_event_series_mean_log_rate( + &[ + EventOccasion { + event_time: 0.0, + score: 1.0, + }, + EventOccasion { + event_time: 0.0, + score: 0.5, + }, + ], + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + } + + fn clustered(cluster_key: u64, event_time: f64, score: f64) -> ClusteredEventScore { + ClusteredEventScore { + cluster_key, + event_time, + score, + } + } + + fn occasion(event_time: f64, score: f64) -> EventOccasion { + EventOccasion { event_time, score } + } + + fn decaying_clustered_scores(drift: f64) -> [ClusteredEventScore; 12] { + [ + clustered(1, 0.0, 10.0 + 1.0), + clustered(1, 1.0, 10.0 + drift.exp()), + clustered(1, 2.0, 10.0 + (drift * 2.0).exp()), + clustered(1, 3.0, 10.0 + (drift * 3.0).exp()), + clustered(1, 4.0, 10.0 + (drift * 4.0).exp()), + clustered(1, 5.0, 10.0 + (drift * 5.0).exp()), + clustered(2, 0.0, -6.0 + 1.2), + clustered(2, 1.0, -6.0 + 1.2 * drift.exp()), + clustered(2, 2.0, -6.0 + 1.2 * (drift * 2.0).exp()), + clustered(2, 3.0, -6.0 + 1.2 * (drift * 3.0).exp()), + clustered(2, 4.0, -6.0 + 1.2 * (drift * 4.0).exp()), + clustered(2, 5.0, -6.0 + 1.2 * (drift * 5.0).exp()), + ] + } + + #[test] + fn within_residual_paths_recover_and_refuse() { + let drift = -0.25_f64; + let clustered = decaying_clustered_scores(drift); + let within = recover_within_residual_event_time_log_rate(&clustered, LagClock::EventTime) + .expect("cwc lag"); + let within_error = (within - drift).abs(); + assert!(within_error.is_finite()); + } + + #[test] + fn within_residual_invalid_rows_fail_closed() { + let rows = decaying_clustered_scores(-0.25); + assert_eq!( + recover_within_residual_event_time_log_rate(&rows, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_within_residual_event_time_log_rate(&[], LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_within_residual_event_time_log_rate( + &[clustered(1, 0.0, 1.0), clustered(1, 1.0, 0.5)], + LagClock::EventTime + ), + Err(PsychometricError::InsufficientClusters) + ); + assert_eq!( + recover_within_residual_event_time_log_rate( + &[clustered(1, f64::NAN, 1.0), clustered(2, 1.0, 0.5)], + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_event_series_mean_log_rate( + &[occasion(0.0, 1.0), occasion(1.0, f64::NAN)], + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_within_residual_event_time_log_rate( + &[ + clustered(1, 0.0, 1.0), + clustered(1, 1.0, 0.5), + clustered(2, 0.0, 2.0), + clustered(2, 1.0, 1.0), + ], + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_within_residual_event_time_log_rate( + &[clustered(1, 0.0, 1.0), clustered(2, 1.0, f64::INFINITY)], + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_within_residual_event_time_log_rate( + &[ + clustered(1, 0.0, 1.0), + clustered(1, 0.0, 1.2), + clustered(2, 0.0, 2.0), + clustered(2, 1.0, 1.5), + ], + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + } + + fn lagged( + earlier_residual: f64, + later_residual: f64, + event_delta: f64, + ) -> LaggedWithinResidual { + LaggedWithinResidual { + earlier_residual, + later_residual, + event_delta, + } + } + + #[test] + fn irregular_centered_residuals_recover_exact_drift() { + let drift = -0.4_f64; + let pairs = [ + lagged(1.2, 1.2 * (drift * 0.5).exp(), 0.5), + lagged(0.8, 0.8 * (drift * 1.75).exp(), 1.75), + lagged(-1.1, -1.1 * (drift * 2.25).exp(), 2.25), + ]; + let recovered = recover_irregular_centered_residual_log_rate(&pairs, LagClock::EventTime) + .expect("irregular"); + assert!((recovered - drift).abs() < 1e-12); + } + + #[test] + fn irregular_centered_residuals_fail_closed() { + let ok = lagged(1.0, 0.8, 1.0); + assert_eq!( + recover_irregular_centered_residual_log_rate(&[ok], LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_irregular_centered_residual_log_rate(&[], LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_irregular_centered_residual_log_rate( + &[lagged(f64::NAN, 0.8, 1.0)], + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_irregular_centered_residual_log_rate( + &[lagged(1.0, f64::INFINITY, 1.0)], + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_irregular_centered_residual_log_rate( + &[lagged(1.0, 0.8, f64::NAN)], + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_irregular_centered_residual_log_rate( + &[lagged(0.0, 0.8, 1.0)], + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_irregular_centered_residual_log_rate( + &[lagged(1.0, -0.8, 1.0)], + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_irregular_centered_residual_log_rate( + &[lagged(1.0, 0.8, 0.0)], + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_irregular_centered_residual_log_rate( + &[lagged(1.0, 0.8, -0.5)], + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + } + + #[test] + fn singleton_cluster_is_skipped_and_all_singletons_fail_closed() { + let drift = -0.2_f64; + let mixed = [ + clustered(1, 0.0, 10.0 + 1.0), + clustered(1, 1.0, 10.0 + drift.exp()), + clustered(1, 2.0, 10.0 + (drift * 2.0).exp()), + clustered(1, 3.0, 10.0 + (drift * 3.0).exp()), + clustered(2, 0.0, 4.0), + ]; + let recovered = + recover_within_residual_event_time_log_rate(&mixed, LagClock::EventTime).expect("skip"); + assert!(recovered.is_finite()); + assert_eq!( + recover_within_residual_event_time_log_rate( + &[clustered(1, 0.0, 1.0), clustered(2, 1.0, 0.5)], + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + } + + #[test] + fn overflowing_cwc_residuals_fail_closed() { + assert_eq!( + recover_within_residual_event_time_log_rate( + &[ + clustered(1, 0.0, f64::MAX), + clustered(1, 1.0, f64::MAX), + clustered(2, 0.0, 1.0), + clustered(2, 1.0, 0.5), + ], + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + } + + #[test] + fn newton_overflow_and_flat_derivative_fail_closed() { + assert_eq!( + fit_scalar_log_rate(&[(1e-300, 1.0, 1e-8), (1.0, 1.0, 1.0)]), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + fit_scalar_log_rate(&[(1e200, 1e200, 1.0)]), + Err(PsychometricError::InvalidNumericInput) + ); + let flat = fit_scalar_log_rate(&[(1e-50, 1e-200, 1.0)]).expect("flat"); + assert!(flat.is_finite()); + assert_eq!( + fit_scalar_log_rate(&[(0.0, 1.0, 1.0), (1.0, -1.0, 1.0)]), + Err(PsychometricError::InvalidNumericInput) + ); + let skipped_start = + fit_scalar_log_rate(&[(1e-320, 1.0, 1.0), (1.0, 0.5, 1.0)]).expect("skip inf ratio"); + assert!(skipped_start.is_finite()); + assert_eq!( + fit_scalar_log_rate(&[(1e154, 1e154, 1.0)]), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + fit_scalar_log_rate(&[(1.0, 1e-300, 1.0), (1.0, 1e-300, 2.0)]), + Err(PsychometricError::InvalidNumericInput) + ); + } + + #[test] + fn one_sided_residual_overflow_and_nonfinite_interval_fail_closed() { + assert_eq!( + recover_within_residual_event_time_log_rate( + &[ + clustered(1, 0.0, -f64::MAX), + clustered(1, 1.0, -f64::MAX), + clustered(1, 2.0, -f64::MAX), + clustered(1, 3.0, f64::MAX), + clustered(2, 0.0, 1.0), + clustered(2, 1.0, 0.8), + ], + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_within_residual_event_time_log_rate( + &[ + clustered(1, f64::MAX, 1.0), + clustered(1, -f64::MAX, 0.5), + clustered(2, 0.0, 1.0), + clustered(2, 1.0, 0.5), + ], + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + } + + #[test] + fn manifest_observed_variance_recovers_driver_equation_five() { + let loading = 2.0_f64; + let latent = 0.4_f64; + let measurement_error = 0.1_f64; + let recovered = + recover_manifest_observed_variance(loading, latent, measurement_error).expect("eq5"); + let expected = (loading * latent) * loading + measurement_error; + assert!((recovered - expected).abs() < 1e-15); + assert!((recovered - 1.7).abs() < 1e-15); + assert!((measurement_error - recovered).abs() > 1e-3); + assert!((latent - recovered).abs() > 1e-3); + assert_eq!( + refuse_measurement_error_as_observed_variance(measurement_error, recovered), + Err(PsychometricError::MeasurementErrorIsNotObservedVariance) + ); + assert_eq!( + refuse_latent_variance_as_observed_variance(latent, recovered), + Err(PsychometricError::LatentVarianceIsNotObservedVariance) + ); + assert_eq!( + recover_manifest_observed_variance(0.0, latent, measurement_error), + Ok(measurement_error) + ); + assert_eq!( + recover_manifest_observed_variance(loading, 0.0, measurement_error), + Ok(measurement_error) + ); + assert_eq!( + recover_manifest_observed_variance(loading, latent, 0.0), + Ok(1.6) + ); + // Do not form λ² first: (1e308)² overflows; (λ p) λ is 1e308. + let scaled = recover_manifest_observed_variance(1e308, 1e-308, 0.0).expect("scale"); + assert!((scaled - 1e308).abs() / 1e308 < 1e-15); + assert!(!(1e308_f64 * 1e308_f64).is_finite()); + } + + #[test] + fn manifest_trait_plus_state_observed_variance_recovers_driver_equation_five() { + let loading = 2.0_f64; + let latent = 0.4_f64; + let measurement_error = 0.1_f64; + let manifest_trait = 0.5_f64; + let recovered = recover_manifest_trait_plus_state_observed_variance( + loading, + latent, + measurement_error, + manifest_trait, + ) + .expect("eq5-trait"); + let expected = (loading * latent) * loading + measurement_error + manifest_trait; + assert!((recovered - expected).abs() < 1e-15); + assert!((recovered - 2.2).abs() < 1e-15); + let without_trait = + recover_manifest_observed_variance(loading, latent, measurement_error).expect("psi0"); + assert_eq!( + recover_manifest_trait_plus_state_observed_variance( + loading, + latent, + measurement_error, + 0.0 + ), + Ok(without_trait) + ); + assert!((without_trait - recovered).abs() > 1e-3); + assert_eq!( + refuse_manifest_trait_variance_as_measurement_error(manifest_trait, measurement_error), + Err(PsychometricError::ManifestTraitVarianceIsNotMeasurementError) + ); + // Zero loading: Var(y) = θ + ψ, not ψ stuffed as Θ. + assert_eq!( + recover_manifest_trait_plus_state_observed_variance( + 0.0, + latent, + measurement_error, + manifest_trait + ), + Ok(measurement_error + manifest_trait) + ); + // TRAITVAR is latent and scaled by λ²; MANIFESTTRAITVAR is not. + let latent_trait_as_state = + recover_manifest_observed_variance(loading, latent + manifest_trait, measurement_error) + .expect("traitvar"); + assert!((latent_trait_as_state - recovered).abs() > 1e-3); + // Do not form λ² first, then add ψ. + let scaled = recover_manifest_trait_plus_state_observed_variance(1e308, 1e-308, 0.0, 1.0) + .expect("scale-psi"); + assert!((scaled - 1e308).abs() / 1e308 < 1e-15); + } + + #[test] + fn manifest_trait_plus_state_observed_variance_invalid_inputs_fail_closed() { + assert_eq!( + recover_manifest_trait_plus_state_observed_variance(2.0, 0.4, 0.1, -0.1), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_manifest_trait_plus_state_observed_variance(2.0, 0.4, 0.1, f64::NAN), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_manifest_trait_plus_state_observed_variance(1e308, 1.0, 0.0, 0.3), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_manifest_trait_plus_state_observed_variance(1e308, 1e-308, 1e308, 1e308), + Err(PsychometricError::InvalidNumericInput) + ); + } + + #[test] + fn manifest_observed_variance_invalid_inputs_fail_closed() { + assert_eq!( + recover_manifest_observed_variance(f64::NAN, 0.4, 0.1), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_manifest_observed_variance(2.0, -0.1, 0.1), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_manifest_observed_variance(2.0, 0.4, -0.1), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_manifest_observed_variance(2.0, f64::NAN, 0.1), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_manifest_observed_variance(2.0, 0.4, f64::NAN), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_manifest_observed_variance(1e308, 1.0, 0.0), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_manifest_observed_variance(1e308, 1.0, 1e308), + Err(PsychometricError::InvalidNumericInput) + ); + } + + #[test] + fn manifest_lagged_observed_covariance_recovers_driver_equation_five() { + let loading = 2.0_f64; + let lagged = 0.4_f64; + let manifest_trait = 0.5_f64; + let recovered = + recover_manifest_lagged_observed_covariance(loading, lagged, manifest_trait) + .expect("eq5-lag"); + let expected = (loading * lagged) * loading + manifest_trait; + assert!((recovered - expected).abs() < 1e-15); + assert!((recovered - 2.1).abs() < 1e-15); + assert_eq!( + recover_manifest_lagged_observed_covariance(loading, lagged, 0.0), + Ok(1.6) + ); + assert_eq!( + recover_manifest_lagged_observed_covariance(0.0, lagged, manifest_trait), + Ok(manifest_trait) + ); + assert_eq!( + recover_manifest_lagged_observed_covariance(loading, 0.0, manifest_trait), + Ok(manifest_trait) + ); + assert_eq!( + refuse_latent_lagged_covariance_as_observed_covariance(lagged, recovered), + Err(PsychometricError::LatentLaggedCovarianceIsNotObservedCovariance) + ); + assert_eq!( + refuse_measurement_error_as_lagged_observed_covariance(0.1, recovered), + Err(PsychometricError::MeasurementErrorIsNotLaggedObservedCovariance) + ); + let scaled = + recover_manifest_lagged_observed_covariance(1e308, 1e-308, 0.0).expect("scale"); + assert!((scaled - 1e308).abs() / 1e308 < 1e-15); + assert!(!(1e308_f64 * 1e308_f64).is_finite()); + } + + #[test] + fn manifest_lagged_observed_covariance_invalid_inputs_fail_closed() { + assert_eq!( + recover_manifest_lagged_observed_covariance(f64::NAN, 0.4, 0.0), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_manifest_lagged_observed_covariance(2.0, -0.1, 0.0), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_manifest_lagged_observed_covariance(2.0, 0.4, -0.1), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_manifest_lagged_observed_covariance(1e308, 1.0, 0.0), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_manifest_lagged_observed_covariance(1e308, 1e-308, 1e308), + Err(PsychometricError::InvalidNumericInput) + ); + } + + #[test] + fn manifest_observed_mean_recovers_driver_equation_five() { + let loading = 2.0_f64; + let latent_mean = 0.4_f64; + let manifest_mean = 0.5_f64; + let recovered = + recover_manifest_observed_mean(loading, latent_mean, manifest_mean).expect("eq5-mean"); + let expected = loading * latent_mean + manifest_mean; + assert!((recovered - expected).abs() < 1e-15); + assert!((recovered - 1.3).abs() < 1e-15); + assert_eq!( + recover_manifest_observed_mean(loading, latent_mean, 0.0), + Ok(0.8) + ); + assert_eq!( + recover_manifest_observed_mean(0.0, latent_mean, manifest_mean), + Ok(manifest_mean) + ); + assert_eq!( + recover_manifest_observed_mean(loading, 0.0, manifest_mean), + Ok(manifest_mean) + ); + assert_eq!(recover_manifest_observed_mean(-2.0, 0.5, 1.0), Ok(0.0)); + assert_eq!( + refuse_manifest_means_as_observed_mean(manifest_mean, recovered), + Err(PsychometricError::ManifestMeansIsNotObservedMean) + ); + assert_eq!( + refuse_latent_mean_as_observed_mean(latent_mean, recovered), + Err(PsychometricError::LatentMeanIsNotObservedMean) + ); + assert_eq!( + refuse_continuous_intercept_as_manifest_means(0.3, manifest_mean), + Err(PsychometricError::ContinuousInterceptIsNotManifestMeans) + ); + let scaled = recover_manifest_observed_mean(1e308, 1e-308, 0.0).expect("scale"); + assert!((scaled - 1.0).abs() < 1e-15); + let finite_loaded = recover_manifest_observed_mean(1e308, 1.0, 0.0).expect("lambda-mu"); + assert!((finite_loaded - 1e308).abs() / 1e308 < 1e-15); + assert!(!(1e308_f64 * 1e308_f64).is_finite()); + } + + #[test] + fn manifest_observed_mean_invalid_inputs_fail_closed() { + assert_eq!( + recover_manifest_observed_mean(f64::NAN, 0.4, 0.0), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_manifest_observed_mean(2.0, f64::NAN, 0.0), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_manifest_observed_mean(2.0, 0.4, f64::NAN), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_manifest_observed_mean(1e308, 2.0, 0.0), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_manifest_observed_mean(1.0, 1e308, 1e308), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!(recover_manifest_observed_mean(0.0, 1e308, 0.5), Ok(0.5)); + assert_eq!(recover_manifest_observed_mean(1e308, 0.0, 0.5), Ok(0.5)); + } + + #[test] + fn discrete_latent_mean_recovers_driver_equation_three() { + let drift = -0.5_f64; + let delta = 2.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let recovered = + recover_discrete_latent_mean(initial, drift, intercept, delta, LagClock::EventTime) + .expect("eq3-mean"); + let expected = + (drift * delta).exp() * initial + intercept * ((drift * delta).exp_m1() / drift); + assert!((recovered - expected).abs() < 1e-15); + let increment = recover_discrete_continuous_intercept_effect( + intercept, + drift, + delta, + LagClock::EventTime, + ) + .expect("cint"); + assert!((increment - intercept * ((drift * delta).exp_m1() / drift)).abs() < 1e-15); + assert_eq!( + recover_discrete_latent_mean(0.0, drift, intercept, delta, LagClock::EventTime), + Ok(increment) + ); + assert_eq!( + recover_discrete_latent_mean(initial, drift, 0.0, delta, LagClock::EventTime), + Ok((drift * delta).exp() * initial) + ); + assert_eq!( + recover_discrete_latent_mean(initial, 0.0, intercept, delta, LagClock::EventTime), + Ok(initial + intercept * delta) + ); + assert_eq!( + recover_discrete_continuous_intercept_effect( + intercept, + 0.0, + delta, + LagClock::EventTime + ), + Ok(intercept * delta) + ); + assert_eq!( + recover_discrete_continuous_intercept_effect(0.0, 0.0, delta, LagClock::EventTime), + Ok(0.0) + ); + assert_eq!( + refuse_initial_latent_mean_as_evolved_mean(initial, recovered), + Err(PsychometricError::InitialLatentMeanIsNotEvolvedMean) + ); + assert_eq!( + refuse_continuous_intercept_as_discrete_mean_increment(intercept, increment), + Err(PsychometricError::ContinuousInterceptIsNotDiscreteMeanIncrement) + ); + assert_eq!( + refuse_continuous_intercept_as_initial_latent_mean(intercept, initial), + Err(PsychometricError::ContinuousInterceptIsNotInitialLatentMean) + ); + let equilibrium = + recover_discrete_latent_mean(initial, -1e308, 1.0, 2.0, LagClock::EventTime) + .expect("eq3-equilibrium"); + let equilibrium_expected = -(1.0 / -1e308); + assert!((equilibrium - equilibrium_expected).abs() / 1e-308 < 1e-12); + assert_eq!( + recover_discrete_latent_mean(1e308, 1.0, 0.0, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_latent_mean(0.0, 1e308, 0.0, 2.0, LagClock::EventTime), + Ok(0.0) + ); + // CINT = 0 so the increment path stays finite; exp(a Δt) then + // overflows and the carried T0MEANS term fails closed. + assert_eq!( + recover_discrete_latent_mean(1.0, 710.0, 0.0, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert!(!(710.0_f64.exp()).is_finite()); + } + + #[test] + fn discrete_latent_mean_invalid_inputs_fail_closed() { + assert_eq!( + recover_discrete_latent_mean(f64::NAN, -0.5, 0.3, 2.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_latent_mean(1.0, f64::NAN, 0.3, 2.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_latent_mean(1.0, -0.5, f64::NAN, 2.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_latent_mean(1.0, -0.5, 0.3, 0.0, LagClock::EventTime), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_latent_mean(1.0, -0.5, 0.3, 2.0, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_discrete_continuous_intercept_effect(1.0, 1e308, 2.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_latent_mean(1.0, 1e308, 1.0, 2.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_latent_mean(1e308, 0.0, 1e308, 2.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_latent_mean(1e308, 0.0, 1e308, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + let underflow_argument = 1e-308_f64 * 1e-308_f64; + assert_eq!(underflow_argument.to_bits(), 0.0_f64.to_bits()); + let underflow = recover_discrete_latent_mean(2.0, 1e-308, 4.0, 1e-308, LagClock::EventTime) + .expect("a-delta-underflow"); + assert!((underflow - 2.0).abs() < 1e-15); + } + + #[test] + fn discrete_observed_mean_recovers_driver_equations_three_and_five() { + let loading = 2.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let manifest_mean = 0.5_f64; + let recovered = recover_discrete_observed_mean( + loading, + initial, + drift, + intercept, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq3-eq5-mean"); + let evolved = + recover_discrete_latent_mean(initial, drift, intercept, delta, LagClock::EventTime) + .expect("mu-t"); + let expected = manifest_mean + loading * evolved; + assert!((recovered - expected).abs() < 1e-15); + let first_occasion = + recover_manifest_observed_mean(loading, initial, manifest_mean).expect("t0"); + assert!((first_occasion - recovered).abs() > 1e-3); + assert_eq!( + recover_discrete_observed_mean( + 0.0, + initial, + drift, + intercept, + manifest_mean, + delta, + LagClock::EventTime + ), + Ok(manifest_mean) + ); + assert_eq!( + recover_discrete_observed_mean( + loading, + initial, + drift, + intercept, + 0.0, + delta, + LagClock::EventTime + ), + Ok(loading * evolved) + ); + let zero_evolved = recover_discrete_observed_mean( + loading, + 0.0, + 0.0, + 0.0, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("zero-mu"); + assert!((zero_evolved - manifest_mean).abs() < 1e-15); + let integrator = recover_discrete_observed_mean( + loading, + initial, + 0.0, + intercept, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("a0"); + assert!( + (integrator - (manifest_mean + loading * (initial + intercept * delta))).abs() < 1e-15 + ); + let equilibrium = recover_discrete_observed_mean( + loading, + initial, + -1e308, + 1.0, + manifest_mean, + 2.0, + LagClock::EventTime, + ) + .expect("eq3-eq5-equilibrium"); + let equilibrium_latent = -(1.0 / -1e308); + assert!((equilibrium - (manifest_mean + loading * equilibrium_latent)).abs() < 1e-15); + } + + #[test] + fn discrete_observed_mean_refuses_first_occasion_and_overflow() { + let loading = 2.0_f64; + let recovered = + recover_discrete_observed_mean(loading, 1.0, -0.5, 0.3, 0.5, 2.0, LagClock::EventTime) + .expect("eq3-eq5-mean"); + let evolved = + recover_discrete_latent_mean(1.0, -0.5, 0.3, 2.0, LagClock::EventTime).expect("mu-t"); + let first_occasion = recover_manifest_observed_mean(loading, 1.0, 0.5).expect("t0"); + assert_eq!( + refuse_initial_observed_mean_as_evolved_observed_mean(first_occasion, recovered), + Err(PsychometricError::InitialObservedMeanIsNotEvolvedObservedMean) + ); + assert_eq!( + refuse_latent_mean_as_observed_mean(evolved, recovered), + Err(PsychometricError::LatentMeanIsNotObservedMean) + ); + assert_eq!( + refuse_manifest_means_as_observed_mean(0.5, recovered), + Err(PsychometricError::ManifestMeansIsNotObservedMean) + ); + let scaled = + recover_discrete_observed_mean(1e308, 1e-308, 0.0, 0.0, 0.0, 1.0, LagClock::EventTime) + .expect("scale"); + assert!((scaled - 1.0).abs() < 1e-15); + let finite_loaded = + recover_discrete_observed_mean(1e308, 1.0, 0.0, 0.0, 0.0, 1.0, LagClock::EventTime) + .expect("lambda-mu"); + assert!((finite_loaded - 1e308).abs() / 1e308 < 1e-15); + } + + #[test] + fn discrete_observed_mean_invalid_inputs_fail_closed() { + assert_eq!( + recover_discrete_observed_mean(f64::NAN, 1.0, -0.5, 0.3, 0.5, 2.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_observed_mean(2.0, 1.0, -0.5, 0.3, 0.5, 0.0, LagClock::EventTime), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_observed_mean(2.0, 1.0, -0.5, 0.3, 0.5, 2.0, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_discrete_observed_mean(1e308, 2.0, 0.0, 0.0, 0.0, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_observed_mean(1.0, 1.0, 710.0, 0.0, 0.5, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + } + + #[test] + fn time_dependent_impulse_recovers_driver_equation_three_fourth_summand() { + let effect = 0.4_f64; + let predictor = 3.0_f64; + let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("tdpred"); + assert!((impulse - 1.2).abs() < 1e-15); + assert_eq!( + recover_time_dependent_predictor_impulse(0.0, predictor), + Ok(0.0) + ); + assert_eq!( + recover_time_dependent_predictor_impulse(effect, 0.0), + Ok(0.0) + ); + let drift = -0.5_f64; + let delta = 2.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let composed = recover_discrete_latent_mean_with_impulse( + initial, + drift, + intercept, + effect, + predictor, + delta, + LagClock::EventTime, + ) + .expect("eq3-impulse"); + let evolved = + recover_discrete_latent_mean(initial, drift, intercept, delta, LagClock::EventTime) + .expect("mu-t"); + assert!((composed - (evolved + impulse)).abs() < 1e-15); + assert_eq!( + recover_discrete_latent_mean_with_impulse( + initial, + drift, + intercept, + 0.0, + predictor, + delta, + LagClock::EventTime + ), + Ok(evolved) + ); + let intercept_effect = + recover_discrete_continuous_intercept_effect(effect, drift, delta, LagClock::EventTime) + .expect("cint"); + assert!((impulse - intercept_effect).abs() > 1e-3); + let equation_fourteen = recover_discrete_time_varying_predictor_effect( + effect, + delta, + delta, + delta, + LagClock::EventTime, + ) + .expect("eq14"); + assert!((impulse - equation_fourteen).abs() > 1e-3); + } + + #[test] + fn time_dependent_impulse_refuses_cint_tipred_and_equation_fourteen() { + let effect = 0.4_f64; + let predictor = 2.0_f64; + let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("tdpred"); + let intercept_effect = + recover_discrete_continuous_intercept_effect(effect, -0.5, 2.0, LagClock::EventTime) + .expect("cint"); + let equation_fourteen = recover_discrete_time_varying_predictor_effect( + effect, + 2.0, + 2.0, + 2.0, + LagClock::EventTime, + ) + .expect("eq14"); + assert_eq!( + refuse_time_dependent_impulse_as_continuous_intercept(impulse, effect), + Err(PsychometricError::TimeDependentImpulseIsNotContinuousIntercept) + ); + assert_eq!( + refuse_time_dependent_impulse_as_time_independent_effect(impulse, intercept_effect), + Err(PsychometricError::TimeDependentImpulseIsNotTimeIndependentEffect) + ); + assert_eq!( + refuse_time_dependent_impulse_as_time_varying_discrete_effect( + impulse, + equation_fourteen + ), + Err(PsychometricError::TimeDependentImpulseIsNotTimeVaryingDiscreteEffect) + ); + } + + #[test] + fn time_dependent_impulse_invalid_inputs_fail_closed() { + assert_eq!( + recover_time_dependent_predictor_impulse(f64::NAN, 1.0), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_time_dependent_predictor_impulse(1.0, f64::INFINITY), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_time_dependent_predictor_impulse(1e308, 2.0), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_latent_mean_with_impulse( + 1.0, + -0.5, + 0.3, + 0.4, + 2.0, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_latent_mean_with_impulse( + 1.0, + -0.5, + 0.3, + 0.4, + 2.0, + 2.0, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_discrete_latent_mean_with_impulse( + 1e308, + 0.0, + 0.0, + 1e308, + 1.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_latent_mean_with_impulse( + 1.0, + -0.5, + 0.3, + 1e308, + 2.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + } + + #[test] + fn level_change_continuous_intercept_recovers_driver_section_seven_point_two() { + let effect = 0.4_f64; + let predictor = 3.0_f64; + let drift = -0.5_f64; + let intercept = recover_level_change_continuous_intercept(effect, predictor, drift) + .expect("level-change"); + let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("impulse"); + assert!((intercept - 0.6).abs() < 1e-15); + assert!((impulse - 1.2).abs() < 1e-15); + let equilibrium = intercept / (-drift); + assert!((equilibrium - impulse).abs() < 1e-15); + assert_eq!( + recover_level_change_continuous_intercept(0.0, predictor, drift), + Ok(0.0) + ); + assert_eq!( + recover_level_change_continuous_intercept(effect, 0.0, drift), + Ok(0.0) + ); + assert_eq!( + recover_level_change_continuous_intercept(effect, predictor, 0.0), + Err(PsychometricError::LevelChangeRequiresStableDrift) + ); + assert_eq!( + recover_level_change_continuous_intercept(effect, predictor, 0.5), + Err(PsychometricError::LevelChangeRequiresStableDrift) + ); + let increment = recover_discrete_time_independent_predictor_effect( + effect, + predictor, + drift, + 2.0, + LagClock::EventTime, + ) + .expect("tipred"); + assert_eq!( + refuse_level_change_intercept_as_impulse(intercept, impulse), + Err(PsychometricError::LevelChangeInterceptIsNotImpulse) + ); + assert_eq!( + refuse_level_change_intercept_as_free_continuous_intercept(intercept, 0.3), + Err(PsychometricError::LevelChangeInterceptIsNotFreeContinuousIntercept) + ); + assert_eq!( + refuse_level_change_intercept_as_process_increment(intercept, increment), + Err(PsychometricError::LevelChangeInterceptIsNotProcessIncrement) + ); + } + + #[test] + fn level_change_continuous_intercept_invalid_inputs_fail_closed() { + assert_eq!( + recover_level_change_continuous_intercept(f64::NAN, 1.0, -0.5), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_level_change_continuous_intercept(1.0, f64::INFINITY, -0.5), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_level_change_continuous_intercept(0.4, 3.0, f64::NAN), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_level_change_continuous_intercept(1e308, 2.0, -0.5), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_level_change_continuous_intercept(1.0, 2.0, -1e308), + Err(PsychometricError::InvalidNumericInput) + ); + let scaled = recover_level_change_continuous_intercept(1e-308, 1.0, -1.0).expect("scale"); + assert!((scaled - 1e-308).abs() < 1e-320); + let rewritten = + recover_level_change_continuous_intercept(1e-308, 1.0, -1e308).expect("rewrite"); + assert!((rewritten - 1.0).abs() < 1e-12); + } + + #[test] + fn level_change_discrete_increment_recovers_driver_equation_three_of_section_seven_point_two() { + let effect = 0.4_f64; + let predictor = 3.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let increment = recover_level_change_discrete_increment( + effect, + predictor, + drift, + delta, + LagClock::EventTime, + ) + .expect("level-change-increment"); + let intercept = recover_level_change_continuous_intercept(effect, predictor, drift) + .expect("level-change"); + let via_cint = recover_discrete_continuous_intercept_effect( + intercept, + drift, + delta, + LagClock::EventTime, + ) + .expect("cint-increment"); + let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("impulse"); + let expected = (1.0 - (drift * delta).exp()) * impulse; + assert!((increment - expected).abs() < 1e-15); + assert!((increment - via_cint).abs() < 1e-15); + assert!((increment - impulse).abs() > 1e-3); + assert!((increment - intercept).abs() > 1e-3); + let tipred = recover_discrete_time_independent_predictor_effect( + effect, + predictor, + drift, + delta, + LagClock::EventTime, + ) + .expect("tipred"); + assert_eq!( + refuse_level_change_increment_as_impulse(increment, impulse), + Err(PsychometricError::LevelChangeIncrementIsNotImpulse) + ); + assert_eq!( + refuse_level_change_increment_as_intercept(increment, intercept), + Err(PsychometricError::LevelChangeIncrementIsNotIntercept) + ); + assert_eq!( + refuse_level_change_increment_as_process_increment(increment, tipred), + Err(PsychometricError::LevelChangeIncrementIsNotProcessIncrement) + ); + let equilibrated = recover_level_change_discrete_increment( + effect, + predictor, + -800.0, + 1.0, + LagClock::EventTime, + ) + .expect("underflow"); + assert!((equilibrated - impulse).abs() < 1e-15); + assert_eq!( + recover_level_change_discrete_increment( + 0.0, + predictor, + drift, + delta, + LagClock::EventTime + ), + Ok(0.0) + ); + } + + #[test] + fn level_change_discrete_increment_invalid_inputs_fail_closed() { + let effect = 0.4_f64; + let predictor = 3.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + assert_eq!( + recover_level_change_discrete_increment( + effect, + predictor, + 0.0, + delta, + LagClock::EventTime + ), + Err(PsychometricError::LevelChangeRequiresStableDrift) + ); + assert_eq!( + recover_level_change_discrete_increment( + effect, + predictor, + 0.5, + delta, + LagClock::EventTime + ), + Err(PsychometricError::LevelChangeRequiresStableDrift) + ); + assert_eq!( + recover_level_change_discrete_increment( + effect, + predictor, + drift, + delta, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_level_change_discrete_increment( + effect, + predictor, + drift, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_level_change_discrete_increment(1e308, 2.0, drift, delta, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_level_change_discrete_increment( + f64::NAN, + predictor, + drift, + delta, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_level_change_discrete_increment( + 0.0, + predictor, + 0.0, + delta, + LagClock::EventTime + ), + Ok(0.0) + ); + } + + #[test] + fn extra_process_contribution_recovers_driver_section_seven_point_two() { + let coupling = 0.569_907_f64; + let predictor = 1.0_f64; + let original = -0.1393_f64; + let extra = -0.000_001_f64; + let delta = 1.0_f64; + let recovered = recover_level_change_extra_process_contribution( + coupling, + predictor, + original, + extra, + delta, + LagClock::EventTime, + ) + .expect("extra-process"); + let expected = coupling * predictor * ((extra * delta).exp() - (original * delta).exp()) + / (extra - original); + assert!((recovered - expected).abs() < 1e-15); + let equal_rate = recover_level_change_extra_process_contribution( + coupling, + predictor, + extra, + extra, + delta, + LagClock::EventTime, + ) + .expect("equal-rate"); + let equal_expected = coupling * predictor * delta * (extra * delta).exp(); + assert!((equal_rate - equal_expected).abs() < 1e-15); + let brownian = recover_level_change_extra_process_contribution( + coupling, + predictor, + 0.0, + extra, + delta, + LagClock::EventTime, + ) + .expect("brownian-original"); + let brownian_expected = coupling * predictor * (extra * delta).exp_m1() / extra; + assert!((brownian - brownian_expected).abs() < 1e-15); + assert_eq!( + recover_level_change_extra_process_contribution( + 0.0, + predictor, + original, + extra, + delta, + LagClock::EventTime + ), + Ok(0.0) + ); + assert_eq!( + recover_level_change_extra_process_contribution( + coupling, + 0.0, + original, + extra, + delta, + LagClock::EventTime + ), + Ok(0.0) + ); + assert_eq!( + recover_level_change_extra_process_contribution( + coupling, + 0.0, + original, + 0.0, + delta, + LagClock::EventTime + ), + Ok(0.0) + ); + } + + #[test] + fn extra_process_contribution_is_not_cint_rewrite_or_impulse() { + let coupling = 0.4_f64; + let predictor = 3.0_f64; + let original = -0.5_f64; + let extra = -0.05_f64; + let delta = 2.0_f64; + let recovered = recover_level_change_extra_process_contribution( + coupling, + predictor, + original, + extra, + delta, + LagClock::EventTime, + ) + .expect("extra-process"); + let intercept = recover_level_change_continuous_intercept(coupling, predictor, original) + .expect("level-change"); + let increment = recover_level_change_discrete_increment( + coupling, + predictor, + original, + delta, + LagClock::EventTime, + ) + .expect("level-change-increment"); + let impulse = + recover_time_dependent_predictor_impulse(coupling, predictor).expect("impulse"); + assert!((recovered - intercept).abs() > 1e-3); + assert!((recovered - increment).abs() > 1e-3); + assert!((recovered - impulse).abs() > 1e-3); + assert_eq!( + refuse_level_change_extra_process_as_impulse(recovered, impulse), + Err(PsychometricError::LevelChangeExtraProcessIsNotImpulse) + ); + assert_eq!( + refuse_level_change_extra_process_as_intercept(recovered, intercept), + Err(PsychometricError::LevelChangeExtraProcessIsNotIntercept) + ); + assert_eq!( + refuse_level_change_extra_process_as_increment(recovered, increment), + Err(PsychometricError::LevelChangeExtraProcessIsNotIncrement) + ); + } + + #[test] + fn extra_process_contribution_invalid_inputs_fail_closed() { + let coupling = 0.4_f64; + let predictor = 3.0_f64; + let original = -0.5_f64; + let extra = -0.000_001_f64; + let delta = 2.0_f64; + assert_eq!( + recover_level_change_extra_process_contribution( + coupling, + predictor, + original, + 0.0, + delta, + LagClock::EventTime + ), + Err(PsychometricError::LevelChangeExtraProcessRequiresNegativeDrift) + ); + assert_eq!( + recover_level_change_extra_process_contribution( + coupling, + predictor, + original, + 0.5, + delta, + LagClock::EventTime + ), + Err(PsychometricError::LevelChangeExtraProcessRequiresNegativeDrift) + ); + assert_eq!( + recover_level_change_extra_process_contribution( + coupling, + predictor, + original, + extra, + delta, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_level_change_extra_process_contribution( + coupling, + predictor, + original, + extra, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_level_change_extra_process_contribution( + f64::NAN, + predictor, + original, + extra, + delta, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_level_change_extra_process_contribution( + 1e308, + 2.0, + original, + extra, + delta, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_level_change_extra_process_contribution( + coupling, + predictor, + 710.0, + extra, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + } + + #[test] + #[allow(clippy::too_many_lines)] + fn nonfinite_short_circuit_operands_of_fail_closed_guards_execute() { + let event = LagClock::EventTime; + assert_eq!( + recover_manifest_lagged_observed_covariance(2.0, f64::NAN, 0.0), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_manifest_lagged_observed_covariance(2.0, 0.4, f64::NAN), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_continuous_intercept_effect(0.3, -0.5, f64::NAN, event), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_level_change_extra_process_contribution(0.4, 3.0, -0.5, -1e-6, f64::NAN, event), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_level_change_extra_process_contribution(0.4, f64::NAN, -0.5, -1e-6, 2.0, event), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_level_change_extra_process_contribution(0.4, 3.0, f64::NAN, -1e-6, 2.0, event), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_level_change_extra_process_contribution(0.4, 3.0, -0.5, f64::NAN, 2.0, event), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_level_change_extra_process_contribution_after( + 0.4, + 3.0, + -0.5, + -0.05, + 2.0, + f64::NAN, + event + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_time_independent_predictor_effect(0.2, 1.0, -0.5, f64::NAN, event), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_time_independent_predictor_effect(0.2, f64::NAN, -0.5, 2.0, event), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_time_independent_predictor_effect(0.2, 1.0, f64::NAN, 2.0, event), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_asymptotic_time_independent_predictor_effect(0.2, f64::NAN, -0.5, event), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_asymptotic_time_independent_predictor_effect(0.2, 1.0, f64::NAN, event), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_asymptotic_time_independent_predictor_variance(f64::NAN, 1.0, -0.5, event), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_asymptotic_time_independent_predictor_variance(0.2, f64::NAN, -0.5, event), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_asymptotic_time_independent_predictor_variance(0.2, 1.0, f64::NAN, event), + Err(PsychometricError::InvalidNumericInput) + ); + let evolved = recover_discrete_latent_mean(1.0, -0.5, 0.3, 2.0, event) + .expect("evolved-extra-process"); + assert_eq!( + recover_discrete_latent_mean_with_extra_process( + 1.0, -0.5, 0.3, 0.0, 3.0, -0.05, 2.0, event + ), + Ok(evolved) + ); + let contribution = + recover_level_change_extra_process_contribution(0.4, 3.0, -0.5, -0.05, 2.0, event) + .expect("extra-process-contribution"); + assert_eq!( + recover_discrete_latent_mean_with_extra_process( + 0.0, -0.5, 0.0, 0.4, 3.0, -0.05, 2.0, event + ), + Ok(contribution) + ); + assert_eq!( + recover_discrete_latent_mean_with_extra_process( + 1.0, + -0.5, + 0.3, + f64::NAN, + 3.0, + -0.05, + 2.0, + event + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_asymptotic_continuous_intercept(0.3, f64::NAN, event), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_initial_time_independent_predictor_carry(0.4, 3.0, -0.5, f64::NAN, event), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_initial_time_dependent_predictor_carry(0.4, 3.0, -0.5, f64::NAN, event), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_time_dependent_predictor_impulse_carry(0.4, 3.0, -0.5, f64::NAN, 1.0, event), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_time_dependent_predictor_impulse_carry(0.4, 3.0, -0.5, 2.0, f64::NAN, event), + Err(PsychometricError::NonPositiveInterval) + ); + } + + #[test] + fn extra_process_contribution_underflow_and_overflow_paths() { + let coupling = 0.4_f64; + let predictor = 3.0_f64; + let original = -0.5_f64; + let extra = -0.000_001_f64; + let vanished = recover_level_change_extra_process_contribution( + coupling, + predictor, + -800.0, + extra, + 1.0, + LagClock::EventTime, + ) + .expect("underflow"); + let vanished_expected = coupling * predictor * (extra * 1.0).exp() / (extra - -800.0); + assert!((vanished - vanished_expected).abs() < 1e-15); + let extra_underflow = recover_level_change_extra_process_contribution( + coupling, + predictor, + original, + -f64::from_bits(1), + 0.5, + LagClock::EventTime, + ) + .expect("extra-argument-underflow"); + assert!(extra_underflow.is_finite()); + let original_underflow = recover_level_change_extra_process_contribution( + coupling, + predictor, + -2e-160_f64, + -1e-160_f64, + 1e-200_f64, + LagClock::EventTime, + ) + .expect("gap-argument-underflow"); + let original_underflow_expected = coupling * predictor * 1e-200_f64; + assert!((original_underflow - original_underflow_expected).abs() <= 1e-200_f64); + let vanished_finite_increment = recover_level_change_extra_process_contribution( + coupling, + predictor, + -800.0, + -92.0, + 1.0, + LagClock::EventTime, + ) + .expect("original-lag-underflow-finite-increment"); + let vanished_finite_expected = coupling * predictor * (-92.0_f64).exp() / (-92.0 - -800.0); + assert!((vanished_finite_increment - vanished_finite_expected).abs() < 1e-15); + let overflow_fallback = recover_level_change_extra_process_contribution( + coupling, + predictor, + -0.8, + extra, + 900.0, + LagClock::EventTime, + ) + .expect("expm1-overflow-fallback"); + let overflow_expected = + coupling * predictor * ((extra * 900.0).exp() - (-0.8_f64 * 900.0).exp()) + / (extra - -0.8); + assert!((overflow_fallback - overflow_expected).abs() < 1e-12); + } + + #[test] + fn extra_process_observed_mean_recovers_driver_equation_five() { + let loading = 2.0_f64; + let coupling = 0.4_f64; + let predictor = 3.0_f64; + let original = -0.5_f64; + let extra = -0.05_f64; + let delta = 2.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let manifest_mean = 0.5_f64; + let recovered = recover_discrete_observed_mean_with_extra_process( + loading, + initial, + original, + intercept, + coupling, + predictor, + extra, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-extra-process-mean"); + let composed = recover_discrete_latent_mean_with_extra_process( + initial, + original, + intercept, + coupling, + predictor, + extra, + delta, + LagClock::EventTime, + ) + .expect("extra-latent"); + let expected = manifest_mean + loading * composed; + assert!((recovered - expected).abs() < 1e-15); + let evolved_observed = recover_discrete_observed_mean( + loading, + initial, + original, + intercept, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq3-eq5-mean"); + assert!((evolved_observed - recovered).abs() > 1e-3); + let impulse_observed = recover_discrete_observed_mean_with_impulse( + loading, + initial, + original, + intercept, + coupling, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-impulse-mean"); + assert!((impulse_observed - recovered).abs() > 1e-3); + let contribution = recover_level_change_extra_process_contribution( + coupling, + predictor, + original, + extra, + delta, + LagClock::EventTime, + ) + .expect("extra-process"); + assert_eq!( + refuse_evolved_observed_mean_as_extra_process_observed_mean( + evolved_observed, + recovered + ), + Err(PsychometricError::EvolvedObservedMeanIsNotExtraProcessObservedMean) + ); + assert_eq!( + refuse_impulse_observed_mean_as_extra_process_observed_mean( + impulse_observed, + recovered + ), + Err(PsychometricError::ImpulseObservedMeanIsNotExtraProcessObservedMean) + ); + assert_eq!( + refuse_extra_process_contribution_as_observed_mean(contribution, recovered), + Err(PsychometricError::ExtraProcessContributionIsNotObservedMean) + ); + assert_eq!( + refuse_extra_process_latent_mean_as_observed_mean(composed, recovered), + Err(PsychometricError::ExtraProcessLatentMeanIsNotObservedMean) + ); + } + + #[test] + fn extra_process_observed_mean_zero_loading_is_manifest_mean_and_refuses_clock() { + let loading = 2.0_f64; + let coupling = 0.4_f64; + let predictor = 3.0_f64; + let original = -0.5_f64; + let extra = -0.05_f64; + let delta = 2.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let manifest_mean = 0.5_f64; + assert_eq!( + recover_discrete_observed_mean_with_extra_process( + 0.0, + initial, + original, + intercept, + coupling, + predictor, + extra, + manifest_mean, + delta, + LagClock::EventTime + ), + Ok(manifest_mean) + ); + assert_eq!( + recover_discrete_observed_mean_with_extra_process( + loading, + initial, + original, + intercept, + coupling, + predictor, + extra, + manifest_mean, + delta, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + } + + #[test] + #[allow(clippy::too_many_lines)] + fn after_extra_process_observed_mean_recovers_driver_equation_five() { + let loading = 2.0_f64; + let coupling = 0.4_f64; + let predictor = 3.0_f64; + let original = -0.5_f64; + let extra = -0.05_f64; + let delta = 2.0_f64; + let elapsed = 1.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let manifest_mean = 0.5_f64; + let recovered = recover_discrete_observed_mean_with_extra_process_after( + loading, + initial, + original, + intercept, + coupling, + predictor, + extra, + manifest_mean, + delta, + elapsed, + LagClock::EventTime, + ) + .expect("eq5-after-extra-process-mean"); + let composed = recover_discrete_latent_mean_with_extra_process_after( + initial, + original, + intercept, + coupling, + predictor, + extra, + delta, + elapsed, + LagClock::EventTime, + ) + .expect("after-extra-latent"); + let expected = manifest_mean + loading * composed; + assert!((recovered - expected).abs() < 1e-15); + let first_occasion = recover_discrete_observed_mean_with_extra_process( + loading, + initial, + original, + intercept, + coupling, + predictor, + extra, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-t0-extra-process-mean"); + assert!((first_occasion - recovered).abs() > 1e-3); + let evolved_observed = recover_discrete_observed_mean( + loading, + initial, + original, + intercept, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq3-eq5-mean"); + assert!((evolved_observed - recovered).abs() > 1e-3); + let carry_observed = recover_discrete_observed_mean_with_impulse_carry( + loading, + initial, + original, + intercept, + coupling, + predictor, + manifest_mean, + delta, + elapsed, + LagClock::EventTime, + ) + .expect("eq5-impulse-carry-mean"); + assert!((carry_observed - recovered).abs() > 1e-3); + let contribution = recover_level_change_extra_process_contribution_after( + coupling, + predictor, + original, + extra, + delta, + elapsed, + LagClock::EventTime, + ) + .expect("after-extra-process"); + assert_eq!( + refuse_extra_process_observed_mean_as_after_extra_process_observed_mean( + first_occasion, + recovered + ), + Err(PsychometricError::ExtraProcessObservedMeanIsNotAfterExtraProcessObservedMean) + ); + assert_eq!( + refuse_evolved_observed_mean_as_after_extra_process_observed_mean( + evolved_observed, + recovered + ), + Err(PsychometricError::EvolvedObservedMeanIsNotAfterExtraProcessObservedMean) + ); + assert_eq!( + refuse_impulse_carry_observed_mean_as_after_extra_process_observed_mean( + carry_observed, + recovered + ), + Err(PsychometricError::ImpulseCarryObservedMeanIsNotAfterExtraProcessObservedMean) + ); + assert_eq!( + refuse_after_extra_process_contribution_as_observed_mean(contribution, recovered), + Err(PsychometricError::AfterExtraProcessContributionIsNotObservedMean) + ); + assert_eq!( + refuse_after_extra_process_latent_mean_as_observed_mean(composed, recovered), + Err(PsychometricError::AfterExtraProcessLatentMeanIsNotObservedMean) + ); + } + + #[test] + #[allow(clippy::too_many_lines)] + fn after_extra_process_contribution_refuses_non_interior_interval() { + let coupling = 0.4_f64; + let predictor = 3.0_f64; + let original = -0.5_f64; + let extra = -0.05_f64; + assert_eq!( + recover_level_change_extra_process_contribution_after( + coupling, + predictor, + original, + extra, + 2.0, + 2.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_level_change_extra_process_contribution_after( + coupling, + predictor, + original, + extra, + 2.0, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_observed_mean_with_extra_process_after( + 2.0, + 1.0, + original, + 0.3, + coupling, + predictor, + extra, + 0.5, + 2.0, + 1.0, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_discrete_observed_mean_with_extra_process_after( + 0.0, + 1.0, + original, + 0.3, + coupling, + predictor, + extra, + 0.5, + 2.0, + 1.0, + LagClock::EventTime + ), + Ok(0.5) + ); + assert_eq!( + recover_level_change_extra_process_contribution_after( + coupling, + predictor, + original, + extra, + 2.0, + 1.0, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_level_change_extra_process_contribution_after( + coupling, + predictor, + original, + extra, + 0.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_level_change_extra_process_contribution_after( + coupling, + predictor, + original, + extra, + f64::NAN, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_discrete_latent_mean_with_extra_process_after( + 1.0, + original, + 0.3, + coupling, + predictor, + extra, + 2.0, + 2.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + let evolved = recover_discrete_latent_mean(1.0, original, 0.3, 2.0, LagClock::EventTime) + .expect("mu-t"); + assert_eq!( + recover_discrete_latent_mean_with_extra_process_after( + 1.0, + original, + 0.3, + 0.0, + predictor, + extra, + 2.0, + 1.0, + LagClock::EventTime + ), + Ok(evolved) + ); + let after_contribution = recover_level_change_extra_process_contribution_after( + 0.4, + 3.0, + -0.5, + -0.05, + 2.0, + 1.0, + LagClock::EventTime, + ) + .expect("after-extra-process-contribution"); + assert_eq!( + recover_discrete_latent_mean_with_extra_process_after( + 0.0, + -0.5, + 0.0, + 0.4, + 3.0, + -0.05, + 2.0, + 1.0, + LagClock::EventTime + ), + Ok(after_contribution) + ); + } + + #[test] + fn asymptotic_time_independent_effect_recovers_driver_section_seven_point_two() { + // Driver et al. (2017, §7.2, p. 21) print LeisureTime + // TIPREDEFFECT = −0.225 and asymTIPREDEFFECT = −1.673 for a + // unit increase. Reconstruct a = −B / asym. + let effect = -0.225_f64; + let predictor = 1.0_f64; + let printed_asym = -1.673_f64; + let log_rate = -effect / printed_asym; + let recovered = recover_asymptotic_time_independent_predictor_effect( + effect, + predictor, + log_rate, + LagClock::EventTime, + ) + .expect("asymTIPREDEFFECT"); + let expected = -(effect * predictor) / log_rate; + assert!((recovered - expected).abs() < 1e-15); + assert!((recovered - printed_asym).abs() < 1e-12); + let happiness = recover_asymptotic_time_independent_predictor_effect( + 0.549, + 1.0, + -0.549 / 0.219, + LagClock::EventTime, + ) + .expect("happiness-asym"); + assert!((happiness - 0.219).abs() < 1e-12); + assert_eq!( + recover_asymptotic_time_independent_predictor_effect( + 0.0, + predictor, + 0.0, + LagClock::EventTime + ), + Ok(0.0) + ); + assert_eq!( + recover_asymptotic_time_independent_predictor_effect( + effect, + 0.0, + 0.0, + LagClock::EventTime + ), + Ok(0.0) + ); + } + + #[test] + fn asymptotic_time_independent_effect_is_not_coefficient_discrete_cint_or_impulse() { + let effect = -0.225_f64; + let predictor = 2.0_f64; + let log_rate = -0.134_488_942_f64; + let recovered = recover_asymptotic_time_independent_predictor_effect( + effect, + predictor, + log_rate, + LagClock::EventTime, + ) + .expect("asymTIPREDEFFECT"); + let discrete = recover_discrete_time_independent_predictor_effect( + effect, + predictor, + log_rate, + 1.0, + LagClock::EventTime, + ) + .expect("discreteTIPREDEFFECT"); + let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("impulse"); + assert!((recovered - effect).abs() > 1e-3); + assert!((recovered - discrete).abs() > 1e-3); + assert!((recovered - impulse).abs() > 1e-3); + assert_eq!( + refuse_asymptotic_time_independent_effect_as_coefficient(recovered, effect), + Err(PsychometricError::AsymptoticTimeIndependentEffectIsNotCoefficient) + ); + assert_eq!( + refuse_asymptotic_time_independent_effect_as_discrete_effect(recovered, discrete), + Err(PsychometricError::AsymptoticTimeIndependentEffectIsNotDiscreteEffect) + ); + assert_eq!( + refuse_asymptotic_time_independent_effect_as_continuous_intercept(recovered, 0.3), + Err(PsychometricError::AsymptoticTimeIndependentEffectIsNotContinuousIntercept) + ); + assert_eq!( + refuse_asymptotic_time_independent_effect_as_time_dependent_impulse(recovered, impulse), + Err(PsychometricError::AsymptoticTimeIndependentEffectIsNotTimeDependentImpulse) + ); + } + + #[test] + fn asymptotic_time_independent_effect_invalid_inputs_fail_closed() { + let effect = -0.225_f64; + let predictor = 1.0_f64; + let log_rate = -0.134_488_942_f64; + assert_eq!( + recover_asymptotic_time_independent_predictor_effect( + effect, + predictor, + log_rate, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_asymptotic_time_independent_predictor_effect( + effect, + predictor, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) + ); + assert_eq!( + recover_asymptotic_time_independent_predictor_effect( + effect, + predictor, + 0.5, + LagClock::EventTime + ), + Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) + ); + assert_eq!( + recover_asymptotic_time_independent_predictor_effect( + f64::NAN, + predictor, + log_rate, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_asymptotic_time_independent_predictor_effect( + 1e308, + 2.0, + log_rate, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_asymptotic_time_independent_predictor_effect( + 1e308, + 1.0, + -1e-308, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + } + + #[test] + fn asymptotic_time_independent_variance_recovers_driver_section_seven_point_two() { + // Driver et al. (2017, §7.2, p. 21) print LeisureTime + // asymTIPREDEFFECT = −1.673. addedTIPREDVAR is the variance of + // that mean shift. Reconstruct a from B and the printed total + // change; the printed 2.838 is the 2-latent TRAITVAR model, not + // this scalar map. + let effect = -0.225_f64; + let printed_asym = -1.673_f64; + let log_rate = -effect / printed_asym; + let predictor_variance = 1.0_f64; + let recovered = recover_asymptotic_time_independent_predictor_variance( + effect, + predictor_variance, + log_rate, + LagClock::EventTime, + ) + .expect("addedTIPREDVAR"); + let expected = printed_asym * printed_asym * predictor_variance; + assert!((recovered - expected).abs() < 1e-12); + let doubled = recover_asymptotic_time_independent_predictor_variance( + effect, + 2.0, + log_rate, + LagClock::EventTime, + ) + .expect("doubled-v"); + assert!((doubled - 2.0 * expected).abs() < 1e-12); + assert_eq!( + recover_asymptotic_time_independent_predictor_variance( + 0.0, + predictor_variance, + 0.0, + LagClock::EventTime + ), + Ok(0.0) + ); + assert_eq!( + recover_asymptotic_time_independent_predictor_variance( + effect, + 0.0, + 0.0, + LagClock::EventTime + ), + Ok(0.0) + ); + } + + #[test] + fn asymptotic_time_independent_variance_is_not_trait_stationary_or_mean_effect() { + let effect = -0.225_f64; + let log_rate = -0.134_488_942_f64; + let predictor_variance = 2.0_f64; + let recovered = recover_asymptotic_time_independent_predictor_variance( + effect, + predictor_variance, + log_rate, + LagClock::EventTime, + ) + .expect("addedTIPREDVAR"); + let mean_effect = recover_asymptotic_time_independent_predictor_effect( + effect, + 1.0, + log_rate, + LagClock::EventTime, + ) + .expect("asymTIPREDEFFECT"); + let stationary = recover_stationary_latent_variance(0.4, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + let trait_plus = recover_trait_plus_state_latent_variance(0.8, 0.3).expect("trait"); + assert!((recovered - mean_effect).abs() > 1e-3); + assert!((recovered - stationary).abs() > 1e-3); + assert!((recovered - trait_plus).abs() > 1e-3); + assert_eq!( + refuse_asymptotic_time_independent_variance_as_trait_variance(recovered, trait_plus), + Err(PsychometricError::AsymptoticTimeIndependentVarianceIsNotTraitVariance) + ); + assert_eq!( + refuse_asymptotic_time_independent_variance_as_stationary_within_subject( + recovered, stationary + ), + Err(PsychometricError::AsymptoticTimeIndependentVarianceIsNotStationaryWithinSubject) + ); + assert_eq!( + refuse_asymptotic_time_independent_variance_as_asymptotic_effect( + recovered, + mean_effect + ), + Err(PsychometricError::AsymptoticTimeIndependentVarianceIsNotAsymptoticEffect) + ); + } + + #[test] + fn asymptotic_time_independent_variance_invalid_inputs_fail_closed() { + let effect = -0.225_f64; + let log_rate = -0.134_488_942_f64; + assert_eq!( + recover_asymptotic_time_independent_predictor_variance( + effect, + 1.0, + log_rate, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_asymptotic_time_independent_predictor_variance( + effect, + 1.0, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) + ); + assert_eq!( + recover_asymptotic_time_independent_predictor_variance( + effect, + -1.0, + log_rate, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_asymptotic_time_independent_predictor_variance( + 1e308, + 1.0, + -1e-308, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_asymptotic_time_independent_predictor_variance( + 1e200, + 1.0, + -1e-200, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_asymptotic_time_independent_predictor_variance( + 1e200, + 1.0, + -1e-100, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + } + + #[test] + fn asymptotic_continuous_intercept_recovers_driver_table_two() { + // Driver et al. (2017, Table 2, p. 12; Eq. 3, p. 5; p. 16) + // name asymCINT the Δt → ∞ intercept contribution −κ / a. + // Reconstruct a from the printed LeisureTime TIPREDEFFECT + // −0.225 / asymTIPREDEFFECT −1.673. The printed 2-latent CINT + // values are not this scalar map. + let printed_effect = -0.225_f64; + let printed_asym = -1.673_f64; + let log_rate = -printed_effect / printed_asym; + let intercept = 0.3_f64; + let recovered = + recover_asymptotic_continuous_intercept(intercept, log_rate, LagClock::EventTime) + .expect("asymCINT"); + let expected = intercept / -log_rate; + assert!((recovered - expected).abs() < 1e-12); + let unit = recover_asymptotic_continuous_intercept(1.0, log_rate, LagClock::EventTime) + .expect("unit-asymCINT"); + assert!((unit - 1.0 / -log_rate).abs() < 1e-12); + let synthetic = recover_asymptotic_continuous_intercept(0.3, -0.5, LagClock::EventTime) + .expect("synthetic"); + assert!((synthetic - 0.6).abs() < 1e-15); + let large_delta = recover_discrete_continuous_intercept_effect( + intercept, + log_rate, + 1e8, + LagClock::EventTime, + ) + .expect("large-delta"); + assert!((recovered - large_delta).abs() < 1e-9); + assert_eq!( + recover_asymptotic_continuous_intercept(0.0, 0.0, LagClock::EventTime), + Ok(0.0) + ); + assert_eq!( + recover_asymptotic_continuous_intercept(0.0, 0.5, LagClock::EventTime), + Ok(0.0) + ); + } + + #[test] + fn asymptotic_continuous_intercept_is_not_cint_increment_t0_or_tipred() { + let intercept = 0.3_f64; + let log_rate = -0.134_488_942_f64; + let recovered = + recover_asymptotic_continuous_intercept(intercept, log_rate, LagClock::EventTime) + .expect("asymCINT"); + let discrete = recover_discrete_continuous_intercept_effect( + intercept, + log_rate, + 1.0, + LagClock::EventTime, + ) + .expect("dtCINT"); + let tipred = recover_asymptotic_time_independent_predictor_effect( + -0.225, + 1.0, + log_rate, + LagClock::EventTime, + ) + .expect("asymTIPREDEFFECT"); + assert!((recovered - intercept).abs() > 1e-3); + assert!((recovered - discrete).abs() > 1e-3); + assert!((recovered - 2.823).abs() > 1e-3); + assert!((recovered - tipred).abs() > 1e-3); + assert_eq!( + refuse_asymptotic_continuous_intercept_as_continuous_intercept(recovered, intercept), + Err(PsychometricError::AsymptoticContinuousInterceptIsNotContinuousIntercept) + ); + assert_eq!( + refuse_asymptotic_continuous_intercept_as_discrete_increment(recovered, discrete), + Err(PsychometricError::AsymptoticContinuousInterceptIsNotDiscreteIncrement) + ); + assert_eq!( + refuse_asymptotic_continuous_intercept_as_initial_latent_mean(recovered, 2.823), + Err(PsychometricError::AsymptoticContinuousInterceptIsNotInitialLatentMean) + ); + assert_eq!( + refuse_asymptotic_continuous_intercept_as_asymptotic_time_independent_effect( + recovered, tipred + ), + Err( + PsychometricError::AsymptoticContinuousInterceptIsNotAsymptoticTimeIndependentEffect + ) + ); + } + + #[test] + fn asymptotic_continuous_intercept_invalid_inputs_fail_closed() { + let intercept = 0.3_f64; + let log_rate = -0.134_488_942_f64; + assert_eq!( + recover_asymptotic_continuous_intercept(intercept, log_rate, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_asymptotic_continuous_intercept(intercept, 0.0, LagClock::EventTime), + Err(PsychometricError::AsymptoticContinuousInterceptRequiresStableDrift) + ); + assert_eq!( + recover_asymptotic_continuous_intercept(intercept, 0.5, LagClock::EventTime), + Err(PsychometricError::AsymptoticContinuousInterceptRequiresStableDrift) + ); + assert_eq!( + recover_asymptotic_continuous_intercept(f64::NAN, log_rate, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_asymptotic_continuous_intercept(1e308, -1e-308, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + } + + #[test] + fn stationary_initial_latent_mean_recovers_driver_page_sixteen() { + // Driver et al. (2017, p. 16; Table 2, p. 12; Eq. 3) + // constrain T0MEANS to model-implied values that include + // extra effects due to time-independent predictors + // (asymTIPREDEFFECT). Reconstruct a from printed LeisureTime + // TIPREDEFFECT −0.225 / asymTIPREDEFFECT −1.673. The printed + // 2-latent T0MEANS 2.823 is not this scalar map. + let printed_effect = -0.225_f64; + let printed_asym = -1.673_f64; + let log_rate = -printed_effect / printed_asym; + let intercept = 0.3_f64; + let recovered = recover_stationary_initial_latent_mean( + intercept, + printed_effect, + 1.0, + log_rate, + LagClock::EventTime, + ) + .expect("stationary T0MEANS"); + let intercept_only = + recover_asymptotic_continuous_intercept(intercept, log_rate, LagClock::EventTime) + .expect("asymCINT"); + let tipred = recover_asymptotic_time_independent_predictor_effect( + printed_effect, + 1.0, + log_rate, + LagClock::EventTime, + ) + .expect("asymTIPREDEFFECT"); + assert!((recovered - (intercept_only + tipred)).abs() < 1e-12); + let synthetic = + recover_stationary_initial_latent_mean(0.3, 0.2, 1.0, -0.5, LagClock::EventTime) + .expect("synthetic"); + assert!((synthetic - 1.0).abs() < 1e-15); + let intercept_only_path = recover_stationary_initial_latent_mean( + intercept, + 0.0, + 1.0, + log_rate, + LagClock::EventTime, + ) + .expect("intercept-only"); + assert!((intercept_only_path - intercept_only).abs() < 1e-15); + let tipred_only = recover_stationary_initial_latent_mean( + 0.0, + printed_effect, + 1.0, + log_rate, + LagClock::EventTime, + ) + .expect("ti-only"); + assert!((tipred_only - tipred).abs() < 1e-15); + assert_eq!( + recover_stationary_initial_latent_mean(0.0, 0.0, 1.0, 0.0, LagClock::EventTime), + Ok(0.0) + ); + assert_eq!( + recover_stationary_initial_latent_mean(0.0, 0.0, 1.0, 0.5, LagClock::EventTime), + Ok(0.0) + ); + } + + #[test] + fn stationary_initial_latent_mean_is_not_t0_cint_tipred_or_discrete() { + let intercept = 0.3_f64; + let log_rate = -0.134_488_942_f64; + let recovered = recover_stationary_initial_latent_mean( + intercept, + -0.225, + 1.0, + log_rate, + LagClock::EventTime, + ) + .expect("stationary T0MEANS"); + let intercept_only = + recover_asymptotic_continuous_intercept(intercept, log_rate, LagClock::EventTime) + .expect("asymCINT"); + let tipred = recover_asymptotic_time_independent_predictor_effect( + -0.225, + 1.0, + log_rate, + LagClock::EventTime, + ) + .expect("asymTIPREDEFFECT"); + let discrete = + recover_discrete_latent_mean(2.823, log_rate, intercept, 1.0, LagClock::EventTime) + .expect("μ_t"); + assert!((recovered - 2.823).abs() > 1e-3); + assert!((recovered - intercept_only).abs() > 1e-3); + assert!((recovered - tipred).abs() > 1e-3); + assert!((recovered - discrete).abs() > 1e-3); + assert_eq!( + refuse_stationary_initial_latent_mean_as_initial_latent_mean(recovered, 2.823), + Err(PsychometricError::StationaryInitialLatentMeanIsNotInitialLatentMean) + ); + assert_eq!( + refuse_stationary_initial_latent_mean_as_asymptotic_continuous_intercept( + recovered, + intercept_only + ), + Err(PsychometricError::StationaryInitialLatentMeanIsNotAsymptoticContinuousIntercept) + ); + assert_eq!( + refuse_stationary_initial_latent_mean_as_asymptotic_time_independent_effect( + recovered, tipred + ), + Err(PsychometricError::StationaryInitialLatentMeanIsNotAsymptoticTimeIndependentEffect) + ); + assert_eq!( + refuse_stationary_initial_latent_mean_as_discrete_mean(recovered, discrete), + Err(PsychometricError::StationaryInitialLatentMeanIsNotDiscreteMean) + ); + } + + #[test] + fn stationary_initial_latent_mean_invalid_inputs_fail_closed() { + let intercept = 0.3_f64; + let log_rate = -0.134_488_942_f64; + assert_eq!( + recover_stationary_initial_latent_mean( + intercept, + -0.225, + 1.0, + log_rate, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_stationary_initial_latent_mean(intercept, 0.0, 1.0, 0.0, LagClock::EventTime), + Err(PsychometricError::AsymptoticContinuousInterceptRequiresStableDrift) + ); + assert_eq!( + recover_stationary_initial_latent_mean(0.0, -0.225, 1.0, 0.5, LagClock::EventTime), + Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) + ); + assert_eq!( + recover_stationary_initial_latent_mean( + f64::NAN, + -0.225, + 1.0, + log_rate, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_stationary_initial_latent_mean(1e308, 1e308, 1.0, -1e-308, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + } + + #[test] + fn stationary_initial_observed_mean_recovers_driver_equation_five_of_section_four_point_three() + { + // Driver et al. (2017, §4.3, pp. 9–10; Eq. 5, p. 5) + // constrain first-occasion means to the model-predicted + // mean. Equation 5 maps E(y_0) = τ + λ of that mean. + let printed_effect = -0.225_f64; + let printed_asym = -1.673_f64; + let log_rate = -printed_effect / printed_asym; + let intercept = 0.3_f64; + let loading = 2.0_f64; + let manifest_mean = 0.5_f64; + let recovered = recover_stationary_initial_observed_mean( + loading, + intercept, + printed_effect, + 1.0, + log_rate, + manifest_mean, + LagClock::EventTime, + ) + .expect("eq5-stationary-T0MEANS"); + let latent = recover_stationary_initial_latent_mean( + intercept, + printed_effect, + 1.0, + log_rate, + LagClock::EventTime, + ) + .expect("stationary T0MEANS"); + let expected = + recover_manifest_observed_mean(loading, latent, manifest_mean).expect("τ+λμ"); + assert!((recovered - expected).abs() < 1e-12); + let intercept_only = + recover_asymptotic_continuous_intercept(intercept, log_rate, LagClock::EventTime) + .expect("asymCINT"); + let intercept_only_observed = + recover_manifest_observed_mean(loading, intercept_only, manifest_mean) + .expect("τ+λ(−κ/a)"); + assert!((recovered - intercept_only_observed).abs() > 1e-3); + let free_initial_observed = + recover_manifest_observed_mean(loading, 2.823, manifest_mean).expect("τ+λμ_0"); + assert!((recovered - free_initial_observed).abs() > 1e-3); + let evolved_from_free = recover_discrete_observed_mean( + loading, + 2.823, + log_rate, + intercept, + manifest_mean, + 1.0, + LagClock::EventTime, + ) + .expect("τ+λμ_t"); + assert!((recovered - evolved_from_free).abs() > 1e-3); + assert!((recovered - manifest_mean).abs() > 1e-3); + assert!((recovered - latent).abs() > 1e-3); + assert_eq!( + recover_stationary_initial_observed_mean( + 0.0, + intercept, + printed_effect, + 1.0, + log_rate, + manifest_mean, + LagClock::EventTime, + ), + Ok(manifest_mean) + ); + assert_eq!( + recover_stationary_initial_observed_mean( + loading, + 0.0, + 0.0, + 1.0, + 0.0, + manifest_mean, + LagClock::EventTime, + ), + Ok(manifest_mean) + ); + let evolved_from_stationary = + recover_discrete_observed_mean_with_time_independent_predictor( + loading, + latent, + log_rate, + intercept, + printed_effect, + 1.0, + manifest_mean, + 2.0, + LagClock::EventTime, + ) + .expect("invariance"); + assert!((evolved_from_stationary - recovered).abs() < 1e-12); + let evolved_latent = recover_discrete_latent_mean_with_time_independent_predictor( + latent, + log_rate, + intercept, + printed_effect, + 1.0, + 2.0, + LagClock::EventTime, + ) + .expect("stationary invariance"); + assert!((evolved_latent - latent).abs() < 1e-12); + } #[test] - fn exact_scalar_map_inverts_exponential_drift() { - let drift = -0.5_f64; - let delta = 2.0_f64; - let earlier = 1.5_f64; - let later = earlier * (drift * delta).exp(); - let recovered = recover_event_time_discrete_lag_and_log_rate( - earlier, - later, - delta, + fn stationary_initial_observed_mean_is_not_manifest_latent_evolved_or_free() { + let intercept = 0.3_f64; + let log_rate = -0.134_488_942_f64; + let loading = 2.0_f64; + let manifest_mean = 0.5_f64; + let recovered = recover_stationary_initial_observed_mean( + loading, + intercept, + -0.225, + 1.0, + log_rate, + manifest_mean, LagClock::EventTime, ) - .expect("exact"); - assert!((recovered.log_rate - drift).abs() < 1e-12); - assert!((recovered.discrete_lag - (drift * delta).exp()).abs() < 1e-12); - assert!((recovered.event_delta - delta).abs() < 1e-15); + .expect("eq5-stationary-T0MEANS"); + let latent = recover_stationary_initial_latent_mean( + intercept, + -0.225, + 1.0, + log_rate, + LagClock::EventTime, + ) + .expect("stationary T0MEANS"); + let intercept_only = + recover_asymptotic_continuous_intercept(intercept, log_rate, LagClock::EventTime) + .expect("asymCINT"); + let intercept_only_observed = + recover_manifest_observed_mean(loading, intercept_only, manifest_mean) + .expect("τ+λ(−κ/a)"); + let free_initial_observed = + recover_manifest_observed_mean(loading, 2.823, manifest_mean).expect("τ+λμ_0"); + let evolved = recover_discrete_observed_mean( + loading, + 2.823, + log_rate, + intercept, + manifest_mean, + 1.0, + LagClock::EventTime, + ) + .expect("τ+λμ_t"); + assert_eq!( + refuse_stationary_initial_latent_mean_as_observed_mean(latent, recovered), + Err(PsychometricError::StationaryInitialLatentMeanIsNotObservedMean) + ); + assert_eq!( + refuse_stationary_initial_observed_mean_as_manifest_means(recovered, manifest_mean), + Err(PsychometricError::StationaryInitialObservedMeanIsNotManifestMeans) + ); + assert_eq!( + refuse_evolved_observed_mean_as_stationary_initial_observed_mean(evolved, recovered), + Err(PsychometricError::EvolvedObservedMeanIsNotStationaryInitialObservedMean) + ); + assert_eq!( + refuse_asymptotic_continuous_intercept_observed_mean_as_stationary_initial_observed_mean( + intercept_only_observed, + recovered + ), + Err( + PsychometricError::AsymptoticContinuousInterceptObservedMeanIsNotStationaryInitialObservedMean + ) + ); + assert_eq!( + refuse_initial_observed_mean_as_stationary_initial_observed_mean( + free_initial_observed, + recovered + ), + Err(PsychometricError::InitialObservedMeanIsNotStationaryInitialObservedMean) + ); } #[test] - fn forward_map_inverts_log_rate_and_remaps_unequal_intervals() { - let drift = -0.4_f64; - let source_delta = 1.0_f64; - let reference_delta = 2.0_f64; - let source_lag = - recover_discrete_lag_from_log_rate(drift, source_delta, LagClock::EventTime) - .expect("forward"); - assert!((source_lag - (drift * source_delta).exp()).abs() < 1e-12); - let same = map_discrete_lag_across_event_intervals( - source_lag, - source_delta, - source_delta, + fn stationary_initial_observed_mean_invalid_inputs_fail_closed() { + let intercept = 0.3_f64; + let log_rate = -0.134_488_942_f64; + assert_eq!( + recover_stationary_initial_observed_mean( + 2.0, + intercept, + -0.225, + 1.0, + log_rate, + 0.5, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_stationary_initial_observed_mean( + 2.0, + intercept, + 0.0, + 1.0, + 0.0, + 0.5, + LagClock::EventTime + ), + Err(PsychometricError::AsymptoticContinuousInterceptRequiresStableDrift) + ); + assert_eq!( + recover_stationary_initial_observed_mean( + 2.0, + 0.0, + -0.225, + 1.0, + 0.5, + 0.5, + LagClock::EventTime + ), + Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) + ); + assert_eq!( + recover_stationary_initial_observed_mean( + f64::NAN, + intercept, + -0.225, + 1.0, + log_rate, + 0.5, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_stationary_initial_observed_mean( + 2.0, + 1e308, + 1e308, + 1.0, + -1e-308, + 0.5, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + } + + #[test] + fn stationary_initial_latent_variance_recovers_driver_section_four_point_three() { + // Driver et al. (2017, §4.3, pp. 9–10; p. 16) constrain T0VAR + // to model-predicted variances. The scalar composition is + // trait + −q / (2 a) + (B / a)² v. Reconstruct a from printed + // LeisureTime TIPREDEFFECT −0.225 / asymTIPREDEFFECT −1.673. + // The printed 2-latent addedTIPREDVAR 2.838 is not this + // scalar map. + let printed_effect = -0.225_f64; + let printed_asym = -1.673_f64; + let log_rate = -printed_effect / printed_asym; + let trait_variance = 1.0_f64; + let diffusion = 0.4_f64; + let predictor_variance = 1.0_f64; + let recovered = recover_stationary_initial_latent_variance( + trait_variance, + diffusion, + printed_effect, + predictor_variance, + log_rate, + LagClock::EventTime, + ) + .expect("stationary T0VAR"); + let state = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + let trait_plus_state = + recover_trait_plus_state_latent_variance(trait_variance, state).expect("trait+state"); + let added = recover_asymptotic_time_independent_predictor_variance( + printed_effect, + predictor_variance, + log_rate, + LagClock::EventTime, + ) + .expect("addedTIPREDVAR"); + assert!((recovered - (trait_plus_state + added)).abs() < 1e-12); + let state_only = recover_stationary_initial_latent_variance( + 0.0, + diffusion, + 0.0, + predictor_variance, + log_rate, + LagClock::EventTime, + ) + .expect("state-only"); + assert!((state_only - state).abs() < 1e-15); + let trait_only = recover_stationary_initial_latent_variance( + trait_variance, + 0.0, + 0.0, + predictor_variance, + 0.0, + LagClock::EventTime, + ) + .expect("trait-only"); + assert!((trait_only - trait_variance).abs() < 1e-15); + let added_only = recover_stationary_initial_latent_variance( + 0.0, + 0.0, + printed_effect, + predictor_variance, + log_rate, + LagClock::EventTime, + ) + .expect("ti-only"); + assert!((added_only - added).abs() < 1e-15); + assert_eq!( + recover_stationary_initial_latent_variance( + 0.0, + 0.0, + 0.0, + predictor_variance, + 0.0, + LagClock::EventTime + ), + Ok(0.0) + ); + assert_eq!( + recover_stationary_initial_latent_variance( + 0.0, + 0.0, + 0.0, + predictor_variance, + 0.5, + LagClock::EventTime + ), + Ok(0.0) + ); + } + + #[test] + fn stationary_initial_latent_variance_is_not_t0_state_trait_tipred_or_discrete() { + let trait_variance = 1.0_f64; + let diffusion = 0.4_f64; + let log_rate = -0.134_488_942_f64; + let recovered = recover_stationary_initial_latent_variance( + trait_variance, + diffusion, + -0.225, + 1.0, + log_rate, LagClock::EventTime, ) - .expect("same interval"); - assert!((same - source_lag).abs() < 1e-12); - let remapped = map_discrete_lag_across_event_intervals( - source_lag, - source_delta, - reference_delta, + .expect("stationary T0VAR"); + let state = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + let added = recover_asymptotic_time_independent_predictor_variance( + -0.225, + 1.0, + log_rate, LagClock::EventTime, ) - .expect("remap"); - assert!((remapped - (drift * reference_delta).exp()).abs() < 1e-12); - // Voelkle manuscript p. 2, 33: φ(1) ≠ φ(2) even for one process. - assert!((source_lag - remapped).abs() > 1e-9); + .expect("addedTIPREDVAR"); + let discrete = recover_discrete_latent_variance( + recovered, + diffusion, + log_rate, + 1.0, + LagClock::EventTime, + ) + .expect("Var(η_t)"); + assert!((recovered - 2.0).abs() > 1e-3); + assert!((recovered - state).abs() > 1e-3); + assert!((recovered - trait_variance).abs() > 1e-3); + assert!((recovered - added).abs() > 1e-3); + assert!((recovered - discrete).abs() > 1e-3); + assert!((recovered - 2.838).abs() > 1e-3); assert_eq!( - refuse_pooled_discrete_lag_across_unequal_intervals(source_delta, reference_delta), - Err(PsychometricError::UnequalIntervalPoolingForbidden) + refuse_stationary_initial_latent_variance_as_initial_latent_variance(recovered, 2.0), + Err(PsychometricError::StationaryInitialLatentVarianceIsNotInitialLatentVariance) ); assert_eq!( - refuse_pooled_discrete_lag_across_unequal_intervals(source_delta, source_delta), - Err(PsychometricError::UnequalIntervalPoolingForbidden) + refuse_stationary_initial_latent_variance_as_stationary_within_subject( + recovered, state + ), + Err(PsychometricError::StationaryInitialLatentVarianceIsNotStationaryWithinSubject) + ); + assert_eq!( + refuse_stationary_initial_latent_variance_as_trait_variance(recovered, trait_variance), + Err(PsychometricError::StationaryInitialLatentVarianceIsNotTraitVariance) + ); + assert_eq!( + refuse_stationary_initial_latent_variance_as_asymptotic_time_independent_variance( + recovered, added + ), + Err( + PsychometricError::StationaryInitialLatentVarianceIsNotAsymptoticTimeIndependentVariance + ) + ); + assert_eq!( + refuse_stationary_initial_latent_variance_as_discrete_variance(recovered, discrete), + Err(PsychometricError::StationaryInitialLatentVarianceIsNotDiscreteVariance) ); } #[test] - fn forward_map_and_interval_remap_fail_closed() { + fn stationary_initial_latent_variance_invalid_inputs_fail_closed() { assert_eq!( - recover_discrete_lag_from_log_rate(-0.2, 1.0, LagClock::SystemTime), + recover_stationary_initial_latent_variance( + 1.0, + 0.4, + -0.225, + 1.0, + -0.13, + LagClock::SystemTime + ), Err(PsychometricError::EventTimeRequired) ); assert_eq!( - recover_discrete_lag_from_log_rate(-0.2, 0.0, LagClock::EventTime), - Err(PsychometricError::NonPositiveInterval) + recover_stationary_initial_latent_variance( + 0.0, + 0.4, + 0.0, + 1.0, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::StationaryVarianceRequiresStableDrift) ); assert_eq!( - recover_discrete_lag_from_log_rate(-0.2, -1.0, LagClock::EventTime), - Err(PsychometricError::NonPositiveInterval) + recover_stationary_initial_latent_variance( + 0.0, + 0.0, + -0.225, + 1.0, + 0.5, + LagClock::EventTime + ), + Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) ); assert_eq!( - recover_discrete_lag_from_log_rate(-0.2, f64::NAN, LagClock::EventTime), - Err(PsychometricError::NonPositiveInterval) + recover_stationary_initial_latent_variance( + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + LagClock::EventTime + ), + Ok(0.0) ); assert_eq!( - recover_discrete_lag_from_log_rate(f64::NAN, 1.0, LagClock::EventTime), + recover_stationary_initial_latent_variance( + f64::NAN, + 0.4, + 0.0, + 0.0, + -0.5, + LagClock::EventTime + ), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_lag_from_log_rate(800.0, 10.0, LagClock::EventTime), + recover_stationary_initial_latent_variance( + f64::MAX, + f64::MAX, + 0.0, + 0.0, + -0.5, + LagClock::EventTime + ), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_lag_from_log_rate(-800.0, 1.0, LagClock::EventTime), + recover_stationary_initial_latent_variance( + f64::MAX, + 0.0, + 1.0, + f64::MAX, + -1.0, + LagClock::EventTime + ), Err(PsychometricError::InvalidNumericInput) ); + } + + #[test] + fn stationary_initial_observed_variance_recovers_driver_equation_five_of_section_four_point_three() + { + // Driver et al. (2017, §4.3, pp. 9–10; Eq. 5, p. 5) + // constrain first-occasion variances to the model-predicted + // variance. Equation 5 maps Var(y_0) = λ² of that variance + // plus θ + ψ. + let printed_effect = -0.225_f64; + let printed_asym = -1.673_f64; + let log_rate = -printed_effect / printed_asym; + let trait_variance = 1.0_f64; + let diffusion = 0.4_f64; + let loading = 2.0_f64; + let measurement_error = 0.5_f64; + let manifest_trait = 0.1_f64; + let recovered = recover_stationary_initial_observed_variance( + loading, + trait_variance, + diffusion, + printed_effect, + 1.0, + log_rate, + measurement_error, + manifest_trait, + LagClock::EventTime, + ) + .expect("eq5-stationary-T0VAR"); + let latent = recover_stationary_initial_latent_variance( + trait_variance, + diffusion, + printed_effect, + 1.0, + log_rate, + LagClock::EventTime, + ) + .expect("stationary T0VAR"); + let expected = recover_manifest_trait_plus_state_observed_variance( + loading, + latent, + measurement_error, + manifest_trait, + ) + .expect("λ²p+θ+ψ"); + assert!((recovered - expected).abs() < 1e-12); + let state = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + let state_only_observed = + recover_manifest_observed_variance(loading, state, measurement_error) + .expect("λ²(−q/2a)+θ"); + assert!((recovered - state_only_observed).abs() > 1e-3); + let free_initial_observed = + recover_manifest_observed_variance(loading, 2.0, measurement_error).expect("λ²p_0+θ"); + assert!((recovered - free_initial_observed).abs() > 1e-3); + let discrete = + recover_discrete_latent_variance(latent, diffusion, log_rate, 1.0, LagClock::EventTime) + .expect("Var(η_t)"); + let evolved = recover_manifest_observed_variance(loading, discrete, measurement_error) + .expect("λ²Var(η_t)+θ"); + assert!((recovered - evolved).abs() > 1e-3); + assert!((recovered - measurement_error).abs() > 1e-3); + assert!((recovered - latent).abs() > 1e-3); + assert_eq!( + recover_stationary_initial_observed_variance( + 0.0, + trait_variance, + diffusion, + printed_effect, + 1.0, + log_rate, + measurement_error, + manifest_trait, + LagClock::EventTime, + ), + Ok(measurement_error + manifest_trait) + ); assert_eq!( - recover_discrete_lag_from_log_rate(-1.0, 800.0, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) + recover_stationary_initial_observed_variance( + loading, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + measurement_error, + 0.0, + LagClock::EventTime, + ), + Ok(measurement_error) ); - let source_lag = - recover_discrete_lag_from_log_rate(-0.7, 1.0, LagClock::EventTime).expect("source φ"); - assert!(source_lag > 0.0); + let zero_manifest_trait = recover_stationary_initial_observed_variance( + loading, + trait_variance, + diffusion, + printed_effect, + 1.0, + log_rate, + measurement_error, + 0.0, + LagClock::EventTime, + ) + .expect("ψ=0"); + let expected_zero_psi = + recover_manifest_observed_variance(loading, latent, measurement_error).expect("λ²p+θ"); + assert!((zero_manifest_trait - expected_zero_psi).abs() < 1e-12); + } + + #[test] + fn stationary_initial_observed_variance_is_not_manifest_latent_evolved_or_free() { + let trait_variance = 1.0_f64; + let diffusion = 0.4_f64; + let log_rate = -0.134_488_942_f64; + let loading = 2.0_f64; + let measurement_error = 0.5_f64; + let recovered = recover_stationary_initial_observed_variance( + loading, + trait_variance, + diffusion, + -0.225, + 1.0, + log_rate, + measurement_error, + 0.1, + LagClock::EventTime, + ) + .expect("eq5-stationary-T0VAR"); + let latent = recover_stationary_initial_latent_variance( + trait_variance, + diffusion, + -0.225, + 1.0, + log_rate, + LagClock::EventTime, + ) + .expect("stationary T0VAR"); + let state = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + let state_only_observed = + recover_manifest_observed_variance(loading, state, measurement_error) + .expect("λ²(−q/2a)+θ"); + let free_initial_observed = + recover_manifest_observed_variance(loading, 2.0, measurement_error).expect("λ²p_0+θ"); + let discrete = + recover_discrete_latent_variance(latent, diffusion, log_rate, 1.0, LagClock::EventTime) + .expect("Var(η_t)"); + let evolved = recover_manifest_observed_variance(loading, discrete, measurement_error) + .expect("λ²Var(η_t)+θ"); assert_eq!( - map_discrete_lag_across_event_intervals(source_lag, 1.0, 2000.0, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) + refuse_stationary_initial_latent_variance_as_observed_variance(latent, recovered), + Err(PsychometricError::StationaryInitialLatentVarianceIsNotObservedVariance) ); assert_eq!( - map_discrete_lag_across_event_intervals(0.5, 1.0, 2.0, LagClock::AssertionTime), - Err(PsychometricError::EventTimeRequired) + refuse_stationary_initial_observed_variance_as_measurement_error( + recovered, + measurement_error + ), + Err(PsychometricError::StationaryInitialObservedVarianceIsNotMeasurementError) ); assert_eq!( - map_discrete_lag_across_event_intervals(0.5, 0.0, 2.0, LagClock::EventTime), - Err(PsychometricError::NonPositiveInterval) + refuse_evolved_observed_variance_as_stationary_initial_observed_variance( + evolved, recovered + ), + Err(PsychometricError::EvolvedObservedVarianceIsNotStationaryInitialObservedVariance) ); assert_eq!( - map_discrete_lag_across_event_intervals(0.5, 1.0, 0.0, LagClock::EventTime), - Err(PsychometricError::NonPositiveInterval) + refuse_stationary_within_subject_observed_variance_as_stationary_initial_observed_variance( + state_only_observed, + recovered + ), + Err( + PsychometricError::StationaryWithinSubjectObservedVarianceIsNotStationaryInitialObservedVariance + ) ); assert_eq!( - map_discrete_lag_across_event_intervals(-0.2, 1.0, 2.0, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) + refuse_initial_observed_variance_as_stationary_initial_observed_variance( + free_initial_observed, + recovered + ), + Err(PsychometricError::InitialObservedVarianceIsNotStationaryInitialObservedVariance) ); } #[test] - fn constant_predictor_discrete_effect_recovers_equation_twelve() { - let outcome_on_predictor = 0.2_f64; - let predictor_log_rate = -0.5_f64; - let delta = 2.0_f64; - let recovered = recover_discrete_constant_predictor_effect( - outcome_on_predictor, - predictor_log_rate, - delta, - LagClock::EventTime, - ) - .expect("eq 12"); - let expected = - (outcome_on_predictor / predictor_log_rate) * (predictor_log_rate * delta).exp_m1(); - assert!((recovered - expected).abs() < 1e-15); - let first_order = outcome_on_predictor * delta; - assert!((recovered - first_order).abs() > 1e-3); + fn stationary_initial_observed_variance_invalid_inputs_fail_closed() { assert_eq!( - recover_discrete_constant_predictor_effect( - outcome_on_predictor, - predictor_log_rate, - delta, + recover_stationary_initial_observed_variance( + 2.0, + 1.0, + 0.4, + -0.225, + 1.0, + -0.13, + 0.5, + 0.1, LagClock::SystemTime ), Err(PsychometricError::EventTimeRequired) ); assert_eq!( - recover_discrete_constant_predictor_effect( - outcome_on_predictor, - predictor_log_rate, + recover_stationary_initial_observed_variance( + 2.0, + 0.0, + 0.4, + 0.0, + 1.0, + 0.0, + 0.5, 0.0, LagClock::EventTime ), - Err(PsychometricError::NonPositiveInterval) + Err(PsychometricError::StationaryVarianceRequiresStableDrift) ); assert_eq!( - recover_discrete_constant_predictor_effect( - outcome_on_predictor, - predictor_log_rate, - -1.0, + recover_stationary_initial_observed_variance( + 2.0, + 0.0, + 0.0, + -0.225, + 1.0, + 0.5, + 0.5, + 0.0, LagClock::EventTime ), - Err(PsychometricError::NonPositiveInterval) + Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) ); assert_eq!( - recover_discrete_constant_predictor_effect( - outcome_on_predictor, - predictor_log_rate, - f64::NAN, + recover_stationary_initial_observed_variance( + 2.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.5, + 0.1, LagClock::EventTime ), - Err(PsychometricError::NonPositiveInterval) + Ok(0.6) ); assert_eq!( - recover_discrete_constant_predictor_effect( + recover_stationary_initial_observed_variance( f64::NAN, - predictor_log_rate, - delta, + 1.0, + 0.4, + 0.0, + 0.0, + -0.5, + 0.5, + 0.0, LagClock::EventTime ), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_constant_predictor_effect( - outcome_on_predictor, + recover_stationary_initial_observed_variance( + 2.0, + f64::MAX, + f64::MAX, + 0.0, + 0.0, + -0.5, + 0.5, 0.0, - delta, LagClock::EventTime ), Err(PsychometricError::InvalidNumericInput) ); + } + + #[test] + #[allow(clippy::too_many_lines)] + fn stationary_lagged_latent_covariance_recovers_driver_section_four_point_three() { + // Driver et al. (2017, §4.3, pp. 9–10; Eq. 3–4, pp. 4–5; p. 16) + // constrain T0VAR. The lagged covariance of that stationary + // process is trait + e^{a Δt}(−q / (2 a)) + (B / a)² v. + // Trait and addedTIPREDVAR do not decay. + let printed_effect = -0.225_f64; + let printed_asym = -1.673_f64; + let log_rate = -printed_effect / printed_asym; + let trait_variance = 1.0_f64; + let diffusion = 0.4_f64; + let predictor_variance = 1.0_f64; + let event_delta = 1.0_f64; + let recovered = recover_stationary_lagged_latent_covariance( + trait_variance, + diffusion, + printed_effect, + predictor_variance, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("stationary lagged T0VAR"); + let state = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + let trait_plus_state = recover_trait_plus_state_lagged_covariance( + trait_variance, + state, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("trait+state lagged"); + let added = recover_asymptotic_time_independent_predictor_variance( + printed_effect, + predictor_variance, + log_rate, + LagClock::EventTime, + ) + .expect("addedTIPREDVAR"); + assert!((recovered - (trait_plus_state + added)).abs() < 1e-12); + let contemporaneous = recover_stationary_initial_latent_variance( + trait_variance, + diffusion, + printed_effect, + predictor_variance, + log_rate, + LagClock::EventTime, + ) + .expect("stationary T0VAR"); + assert!((recovered - contemporaneous).abs() > 1e-3); + let decayed = recover_discrete_lagged_latent_covariance( + contemporaneous, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("e^{aΔt} p_stat"); + assert!((recovered - decayed).abs() > 1e-3); + let state_only = recover_stationary_lagged_latent_covariance( + 0.0, + diffusion, + 0.0, + predictor_variance, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("state-only lagged"); + let lagged_state = recover_discrete_lagged_latent_covariance( + state, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("e^{aΔt} asymDIFFUSION"); + assert!((state_only - lagged_state).abs() < 1e-15); + let trait_only = recover_stationary_lagged_latent_covariance( + trait_variance, + 0.0, + 0.0, + predictor_variance, + 0.0, + event_delta, + LagClock::EventTime, + ) + .expect("trait-only lagged"); + assert!((trait_only - trait_variance).abs() < 1e-15); + let added_only = recover_stationary_lagged_latent_covariance( + 0.0, + 0.0, + printed_effect, + predictor_variance, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("ti-only lagged"); + assert!((added_only - added).abs() < 1e-15); assert_eq!( - recover_discrete_constant_predictor_effect( - outcome_on_predictor, - f64::NAN, - delta, + recover_stationary_lagged_latent_covariance( + 0.0, + 0.0, + 0.0, + predictor_variance, + 0.0, + event_delta, LagClock::EventTime ), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_discrete_constant_predictor_effect(1e300, 1e-300, 1e300, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) - ); - let underflowed_argument = - recover_discrete_constant_predictor_effect(1e308, 1e-308, 1e-308, LagClock::EventTime) - .expect("eq 12 limit"); - assert!((underflowed_argument - 1.0).abs() < 1e-15); - let tiny_nonzero = - recover_discrete_constant_predictor_effect(1e308, 1e-154, 1e-154, LagClock::EventTime) - .expect("eq 12 scaled"); - assert!(tiny_nonzero.is_finite()); - assert!((tiny_nonzero - 1e154).abs() / 1e154 < 1e-12); - // a_yx Δt overflows; Eq. 12 remains finite (Voelkle 2012, Eq. 12). - let product_overflow = - recover_discrete_constant_predictor_effect(1e308, -100.0, 10.0, LagClock::EventTime) - .expect("eq 12 finite after a_yx Δt overflow"); - let product_overflow_expected = (1e308 / -100.0) * (-100.0_f64 * 10.0).exp_m1(); - assert!((product_overflow - product_overflow_expected).abs() / 1e306 < 1e-12); - assert!(product_overflow.is_finite()); - assert!(!(1e308_f64 * 10.0).is_finite()); - } - - #[test] - fn constant_predictor_negative_overflow_recovers_equilibrium_increment() { - // z → -∞: expm1(z)/z * Δt is +0; Eq. 12 → -a_yx/a_xx (Voelkle - // 2012, Introducing Intercepts equilibrium increment). - let increment_argument = -1e308_f64 * 2.0; - assert!(increment_argument.is_infinite()); - assert!(increment_argument.is_sign_negative()); - let lost_scale = increment_argument.exp_m1() / increment_argument * 2.0; - assert_eq!(lost_scale.to_bits(), 0.0_f64.to_bits()); - let negative_overflow = - recover_discrete_constant_predictor_effect(1.0, -1e308, 2.0, LagClock::EventTime) - .expect("eq 12 equilibrium increment"); - let negative_overflow_expected = -(1.0 / -1e308); - assert!((negative_overflow - negative_overflow_expected).abs() / 1e-308 < 1e-12); - assert!(negative_overflow > 0.0); - assert!(negative_overflow.is_finite()); - } - - #[test] - fn constant_predictor_expm1_overflow_recovers_finite_equation_twelve() { - // expm1(800) is +∞; (1e-308/800)(exp(800)−1) is finite. - assert!(!800.0_f64.exp_m1().is_finite()); - assert!(!(1e-308_f64 * (800.0_f64.exp_m1() / 800.0)).is_finite()); - let recovered = - recover_discrete_constant_predictor_effect(1e-308, 800.0, 1.0, LagClock::EventTime) - .expect("eq 12 log-space"); - let expected = (1e-308_f64.ln() + 800.0 - 800.0_f64.ln()).exp() - 1e-308 / 800.0; - assert!((recovered - expected).abs() / expected < 1e-12); - assert!(recovered.is_finite()); - assert!(recovered > 0.0); - let negative = - recover_discrete_constant_predictor_effect(-1e-308, 800.0, 1.0, LagClock::EventTime) - .expect("eq 12 signed log-space"); - assert!((negative + expected).abs() / expected < 1e-12); - assert_eq!( - recover_discrete_constant_predictor_effect(0.0, 800.0, 1.0, LagClock::EventTime), Ok(0.0) ); - assert_eq!( - recover_discrete_constant_predictor_effect(0.0, 1e308, 2.0, LagClock::EventTime), - Ok(0.0) - ); - assert_eq!( - recover_discrete_constant_predictor_effect(1.0, 800.0, 1.0, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_discrete_constant_predictor_effect(1.0, 1e308, 2.0, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) - ); - // a_yx/a_xx overflows; the Eq. 12 rewrite term is not a binary64 number. - assert!(!800.0_f64.exp_m1().is_finite()); - assert!(!(1e308_f64 / 1e-10).is_finite()); - assert_eq!( - recover_discrete_constant_predictor_effect(1e308, 1e-10, 8e12, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) - ); + let far = recover_stationary_lagged_latent_covariance( + trait_variance, + diffusion, + printed_effect, + predictor_variance, + log_rate, + 1e8, + LagClock::EventTime, + ) + .expect("Δt→∞"); + assert!((far - (trait_variance + added)).abs() < 1e-12); + let near = recover_stationary_lagged_latent_covariance( + trait_variance, + diffusion, + printed_effect, + predictor_variance, + log_rate, + 1e-12, + LagClock::EventTime, + ) + .expect("Δt→0+"); + assert!((near - contemporaneous).abs() < 1e-9); } #[test] - fn time_varying_predictor_discrete_effect_recovers_equation_fourteen() { - let outcome_on_predictor = 0.2_f64; - let delta = 2.0_f64; - let recovered = recover_discrete_time_varying_predictor_effect( - outcome_on_predictor, - delta, - delta, - delta, + fn stationary_lagged_latent_covariance_is_not_contemporaneous_decayed_or_trait_state() { + let trait_variance = 1.0_f64; + let diffusion = 0.4_f64; + let log_rate = -0.134_488_942_f64; + let event_delta = 1.0_f64; + let recovered = recover_stationary_lagged_latent_covariance( + trait_variance, + diffusion, + -0.225, + 1.0, + log_rate, + event_delta, LagClock::EventTime, ) - .expect("eq 14"); - assert!((recovered - outcome_on_predictor * delta).abs() < 1e-15); - let constant = recover_discrete_constant_predictor_effect( - outcome_on_predictor, - -0.5, - delta, + .expect("stationary lagged T0VAR"); + let contemporaneous = recover_stationary_initial_latent_variance( + trait_variance, + diffusion, + -0.225, + 1.0, + log_rate, LagClock::EventTime, ) - .expect("eq 12"); - // Voelkle 2012, p. 21: Eq. 14 is not Eq. 12. - assert!((recovered - constant).abs() > 1e-3); + .expect("stationary T0VAR"); + let decayed = recover_discrete_lagged_latent_covariance( + contemporaneous, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("e^{aΔt} p_stat"); + let state = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + let trait_plus_state = recover_trait_plus_state_lagged_covariance( + trait_variance, + state, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("trait+state lagged"); + assert!((recovered - contemporaneous).abs() > 1e-3); + assert!((recovered - decayed).abs() > 1e-3); + assert!((recovered - trait_plus_state).abs() > 1e-3); assert_eq!( - recover_discrete_time_varying_predictor_effect( - 0.0, - delta, - delta, - delta, - LagClock::EventTime + refuse_stationary_lagged_latent_covariance_as_stationary_initial_latent_variance( + recovered, + contemporaneous ), - Ok(0.0) + Err( + PsychometricError::StationaryLaggedLatentCovarianceIsNotStationaryInitialLatentVariance + ) ); - } - - #[test] - fn time_varying_predictor_unmatched_and_invalid_inputs_fail_closed() { - let outcome_on_predictor = 0.2_f64; - let delta = 2.0_f64; assert_eq!( - recover_discrete_time_varying_predictor_effect( - outcome_on_predictor, - delta, - delta, - delta, - LagClock::SystemTime + refuse_stationary_lagged_latent_covariance_as_decayed_stationary_variance( + recovered, decayed ), - Err(PsychometricError::EventTimeRequired) + Err(PsychometricError::StationaryLaggedLatentCovarianceIsNotDecayedStationaryVariance) ); assert_eq!( - recover_discrete_time_varying_predictor_effect( - outcome_on_predictor, - 0.0, - 0.0, - 0.0, - LagClock::EventTime + refuse_trait_plus_state_lagged_covariance_as_stationary_lagged_latent_covariance( + trait_plus_state, + recovered ), - Err(PsychometricError::NonPositiveInterval) + Err( + PsychometricError::TraitPlusStateLaggedCovarianceIsNotStationaryLaggedLatentCovariance + ) ); + } + + #[test] + fn stationary_lagged_latent_covariance_invalid_inputs_fail_closed() { assert_eq!( - recover_discrete_time_varying_predictor_effect( - outcome_on_predictor, - -1.0, + recover_stationary_lagged_latent_covariance( 1.0, + 0.4, + -0.225, 1.0, - LagClock::EventTime + -0.13, + 1.0, + LagClock::SystemTime ), - Err(PsychometricError::NonPositiveInterval) + Err(PsychometricError::EventTimeRequired) ); assert_eq!( - recover_discrete_time_varying_predictor_effect( - outcome_on_predictor, + recover_stationary_lagged_latent_covariance( 1.0, - f64::NAN, + 0.4, + -0.225, 1.0, + -0.13, + 0.0, LagClock::EventTime ), Err(PsychometricError::NonPositiveInterval) ); assert_eq!( - recover_discrete_time_varying_predictor_effect( - outcome_on_predictor, - 1.0, + recover_stationary_lagged_latent_covariance( + 0.0, + 0.4, + 0.0, 1.0, 0.0, + 1.0, LagClock::EventTime ), - Err(PsychometricError::NonPositiveInterval) + Err(PsychometricError::StationaryVarianceRequiresStableDrift) ); assert_eq!( - recover_discrete_time_varying_predictor_effect( - outcome_on_predictor, + recover_stationary_lagged_latent_covariance( + 0.0, + 0.0, + -0.225, + 1.0, + 0.5, 1.0, - 2.0, - 2.0, LagClock::EventTime ), - Err(PsychometricError::UnmatchedTimeVaryingInterval) + Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) ); assert_eq!( - recover_discrete_time_varying_predictor_effect( - outcome_on_predictor, - 2.0, - 2.0, + recover_stationary_lagged_latent_covariance( + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, 1.0, LagClock::EventTime ), - Err(PsychometricError::UnmatchedTimeVaryingInterval) + Ok(0.0) ); assert_eq!( - recover_discrete_time_varying_predictor_effect( + recover_stationary_lagged_latent_covariance( f64::NAN, - delta, - delta, - delta, + 0.4, + 0.0, + 0.0, + -0.5, + 1.0, LagClock::EventTime ), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_time_varying_predictor_effect( - 1e308, - 10.0, - 10.0, - 10.0, + recover_stationary_lagged_latent_covariance( + f64::MAX, + f64::MAX, + 0.0, + 0.0, + -0.5, + 1.0, LagClock::EventTime ), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - refuse_unmatched_time_varying_predictor_interval(1.0, 2.0), - Err(PsychometricError::UnmatchedTimeVaryingInterval) - ); - assert_eq!( - refuse_unmatched_time_varying_predictor_interval(1.0, 1.0), - Err(PsychometricError::UnmatchedTimeVaryingInterval) + recover_stationary_lagged_latent_covariance( + f64::MAX, + 0.0, + 1.0, + f64::MAX, + -1.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) ); } #[test] - fn discrete_process_noise_recovers_driver_equation_three() { + fn stationary_lagged_observed_covariance_recovers_driver_equation_five_of_section_four_point_three() + { + // Driver et al. (2017, §4.3, pp. 9–10; Eq. 5, p. 5) + // lagged observed covariance of stationary T0VAR is + // λ²(trait + e^{a Δt}(−q / (2 a)) + (B / a)² v) + ψ. + // Θ does not enter. + let printed_effect = -0.225_f64; + let printed_asym = -1.673_f64; + let log_rate = -printed_effect / printed_asym; + let trait_variance = 1.0_f64; let diffusion = 0.4_f64; - let drift = -0.5_f64; - let delta = 1.0_f64; - let recovered = - recover_discrete_process_noise(diffusion, drift, delta, LagClock::EventTime) - .expect("q_dt"); - let expected = diffusion * ((2.0 * drift * delta).exp() - 1.0) / (2.0 * drift); - assert!((recovered - expected).abs() < 1e-15); - // a = 0 is the integral of a constant diffusion: q Δt. - assert_eq!( - recover_discrete_process_noise(diffusion, 0.0, 2.5, LagClock::EventTime), - Ok(diffusion * 2.5) - ); - // Binary64 underflow of 2 a Δt recovers the same limit. - let underflowed = recover_discrete_process_noise(1.0, 1e-308, 1e-308, LagClock::EventTime) - .expect("z underflow"); - assert!((underflowed - 1e-308).abs() < 1e-320); - // z → −∞ keeps the equilibrium variance −q / (2 a). - let equilibrium = - recover_discrete_process_noise(0.4, -1e300, 2.0, LagClock::EventTime).expect("eq var"); - assert!((equilibrium - (0.4 / (2.0 * 1e300))).abs() < 1e-315); - // Finite z, overflowed expm1: log-space rewrite stays finite. - let overflowed = recover_discrete_process_noise(1e-308, 400.0, 1.0, LagClock::EventTime) - .expect("expm1 overflow"); - let rewrite_scale = 1e-308 / 800.0; - let rewrite_log = (1e-308_f64).ln() + 800.0 - 800.0_f64.ln(); - let rewrite = rewrite_log.exp() - rewrite_scale; - assert!((overflowed - rewrite).abs() / rewrite.abs() < 1e-12); + let loading = 2.0_f64; + let measurement_error = 0.5_f64; + let manifest_trait = 0.1_f64; + let event_delta = 1.0_f64; + let recovered = recover_stationary_lagged_observed_covariance( + loading, + trait_variance, + diffusion, + printed_effect, + 1.0, + log_rate, + event_delta, + manifest_trait, + LagClock::EventTime, + ) + .expect("eq5-lagged-stationary-T0VAR"); + let latent = recover_stationary_lagged_latent_covariance( + trait_variance, + diffusion, + printed_effect, + 1.0, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("stationary lagged T0VAR"); + let expected = recover_manifest_lagged_observed_covariance(loading, latent, manifest_trait) + .expect("λ²c+ψ"); + assert!((recovered - expected).abs() < 1e-12); + let contemporaneous = recover_stationary_initial_observed_variance( + loading, + trait_variance, + diffusion, + printed_effect, + 1.0, + log_rate, + measurement_error, + manifest_trait, + LagClock::EventTime, + ) + .expect("eq5-stationary-T0VAR"); + assert!((recovered - contemporaneous).abs() > 1e-3); + assert!((recovered - measurement_error).abs() > 1e-3); + assert!((recovered - latent).abs() > 1e-3); assert_eq!( - recover_discrete_process_noise(0.0, 800.0, 1.0, LagClock::EventTime), - Ok(0.0) + recover_stationary_lagged_observed_covariance( + 0.0, + trait_variance, + diffusion, + printed_effect, + 1.0, + log_rate, + event_delta, + manifest_trait, + LagClock::EventTime, + ), + Ok(manifest_trait) ); assert_eq!( - recover_discrete_process_noise(0.0, 1e308, 2.0, LagClock::EventTime), + recover_stationary_lagged_observed_covariance( + loading, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + event_delta, + 0.0, + LagClock::EventTime, + ), Ok(0.0) ); - // Forming 2 a first overflows; z = 2 (a Δt) stays finite. - let twice_rate_overflow = - recover_discrete_process_noise(1.0, 1e308, 1e-308, LagClock::EventTime) - .expect("2a overflow"); - let expected_twice_rate = 0.5 * 2.0_f64.exp_m1() / 1e308; - assert!((twice_rate_overflow - expected_twice_rate).abs() / expected_twice_rate < 1e-12); - // 2 a overflows to −∞; expm1(−∞) = −1 keeps −0.5 q / a. - let overflowed_equilibrium = - recover_discrete_process_noise(1e308, -1e308, 2.0, LagClock::EventTime) - .expect("2a eq var"); - assert!((overflowed_equilibrium - 0.5).abs() < 1e-15); + let zero_manifest_trait = recover_stationary_lagged_observed_covariance( + loading, + trait_variance, + diffusion, + printed_effect, + 1.0, + log_rate, + event_delta, + 0.0, + LagClock::EventTime, + ) + .expect("ψ=0"); + let expected_zero_psi = + recover_manifest_lagged_observed_covariance(loading, latent, 0.0).expect("λ²c"); + assert!((zero_manifest_trait - expected_zero_psi).abs() < 1e-12); } #[test] - fn discrete_process_noise_invalid_inputs_fail_closed() { - assert_eq!( - recover_discrete_process_noise(0.4, -0.5, 1.0, LagClock::SystemTime), - Err(PsychometricError::EventTimeRequired) - ); - assert_eq!( - recover_discrete_process_noise(0.4, -0.5, 0.0, LagClock::EventTime), - Err(PsychometricError::NonPositiveInterval) - ); - assert_eq!( - recover_discrete_process_noise(0.4, -0.5, -1.0, LagClock::EventTime), - Err(PsychometricError::NonPositiveInterval) - ); - assert_eq!( - recover_discrete_process_noise(0.4, -0.5, f64::NAN, LagClock::EventTime), - Err(PsychometricError::NonPositiveInterval) - ); - assert_eq!( - recover_discrete_process_noise(-0.1, -0.5, 1.0, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_discrete_process_noise(f64::NAN, -0.5, 1.0, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_discrete_process_noise(0.4, f64::NAN, 1.0, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_discrete_process_noise(1.0, 800.0, 1.0, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_discrete_process_noise(1.0, 1e308, 2.0, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) - ); - // Finite z, overflowed expm1, overflowing 0.5 q / a. - // q (e^{2 a Δt} − 1) / (2 a) is then non-finite (Driver Eq. 3). + fn stationary_lagged_observed_covariance_is_not_manifest_latent_or_contemporaneous() { + let trait_variance = 1.0_f64; + let diffusion = 0.4_f64; + let log_rate = -0.134_488_942_f64; + let loading = 2.0_f64; + let measurement_error = 0.5_f64; + let event_delta = 1.0_f64; + let recovered = recover_stationary_lagged_observed_covariance( + loading, + trait_variance, + diffusion, + -0.225, + 1.0, + log_rate, + event_delta, + 0.1, + LagClock::EventTime, + ) + .expect("eq5-lagged-stationary-T0VAR"); + let latent = recover_stationary_lagged_latent_covariance( + trait_variance, + diffusion, + -0.225, + 1.0, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("stationary lagged T0VAR"); + let contemporaneous = recover_stationary_initial_observed_variance( + loading, + trait_variance, + diffusion, + -0.225, + 1.0, + log_rate, + measurement_error, + 0.1, + LagClock::EventTime, + ) + .expect("eq5-stationary-T0VAR"); assert_eq!( - recover_discrete_process_noise(1e308, 0.1, 4000.0, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) + refuse_stationary_lagged_latent_covariance_as_observed_covariance(latent, recovered), + Err(PsychometricError::StationaryLaggedLatentCovarianceIsNotObservedCovariance) ); - } - - #[test] - fn lagged_covariance_and_latent_variance_follow_driver_equations_three_and_four() { - let prior = 2.0_f64; - let diffusion = 0.4_f64; - let drift = -0.5_f64; - let delta = 1.0_f64; - let lagged = - recover_discrete_lagged_latent_covariance(prior, drift, delta, LagClock::EventTime) - .expect("lagged cov"); - let expected_lagged = (drift * delta).exp() * prior; - assert!((lagged - expected_lagged).abs() < 1e-15); - let process_noise = - recover_discrete_process_noise(diffusion, drift, delta, LagClock::EventTime) - .expect("q_dt"); - let latent = - recover_discrete_latent_variance(prior, diffusion, drift, delta, LagClock::EventTime) - .expect("var"); - let expected_var = (2.0 * drift * delta).exp() * prior + process_noise; - assert!((latent - expected_var).abs() < 1e-15); - assert!((latent - process_noise).abs() > 1e-3); assert_eq!( - refuse_process_noise_as_unconditional_variance(process_noise, prior), - Err(PsychometricError::ProcessNoiseIsConditionalVariance) + refuse_measurement_error_as_stationary_lagged_observed_covariance( + measurement_error, + recovered + ), + Err(PsychometricError::MeasurementErrorIsNotStationaryLaggedObservedCovariance) ); assert_eq!( - recover_discrete_lagged_latent_covariance(0.0, 800.0, 1.0, LagClock::EventTime), - Ok(0.0) + refuse_stationary_initial_observed_variance_as_stationary_lagged_observed_covariance( + contemporaneous, + recovered + ), + Err( + PsychometricError::StationaryInitialObservedVarianceIsNotStationaryLaggedObservedCovariance + ) ); - let underflowed_lagged = - recover_discrete_lagged_latent_covariance(2.0, -1e308, 2.0, LagClock::EventTime) - .expect("underflow lagged"); - assert_eq!(underflowed_lagged.to_bits(), 0.0_f64.to_bits()); - let rewritten = - recover_discrete_lagged_latent_covariance(1e-308, 800.0, 1.0, LagClock::EventTime) - .expect("rewrite lagged"); - let expected_rewrite = (1e-308_f64.ln() + 800.0).exp(); - assert!((rewritten - expected_rewrite).abs() / expected_rewrite < 1e-12); - let zero_prior = - recover_discrete_latent_variance(0.0, diffusion, drift, delta, LagClock::EventTime) - .expect("zero prior"); - assert!((zero_prior - process_noise).abs() < 1e-15); - let drifted_zero = - recover_discrete_latent_variance(2.0, diffusion, 0.0, 2.5, LagClock::EventTime) - .expect("a=0"); - assert!((drifted_zero - (2.0 + diffusion * 2.5)).abs() < 1e-15); - let underflowed_var = - recover_discrete_latent_variance(2.0, 1.0, 1e-308, 1e-308, LagClock::EventTime) - .expect("z underflow"); - assert!((underflowed_var - (2.0 + 1.0 * 1e-308)).abs() < 1e-15); - let vanished = - recover_discrete_latent_variance(2.0, 1e308, -1e308, 2.0, LagClock::EventTime) - .expect("phi_sq underflow"); - assert!((vanished - 0.5).abs() < 1e-15); - let rewritten_var = - recover_discrete_latent_variance(1e-308, 1e-308, 400.0, 1.0, LagClock::EventTime) - .expect("rewrite var"); - assert!(rewritten_var.is_finite()); - assert!(rewritten_var > 0.0); } #[test] - fn lagged_covariance_and_latent_variance_overflow_paths_fail_closed() { - assert_eq!( - recover_discrete_lagged_latent_covariance(1e308, 800.0, 1.0, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) - ); + fn stationary_lagged_observed_covariance_invalid_inputs_fail_closed() { assert_eq!( - recover_discrete_lagged_latent_covariance(2.0, 1e308, 2.0, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) + recover_stationary_lagged_observed_covariance( + 2.0, + 1.0, + 0.4, + -0.225, + 1.0, + -0.13, + 1.0, + 0.1, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) ); assert_eq!( - recover_discrete_latent_variance(1e308, 1e-308, 400.0, 1.0, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) + recover_stationary_lagged_observed_covariance( + 2.0, + 1.0, + 0.4, + -0.225, + 1.0, + -0.13, + 0.0, + 0.1, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) ); assert_eq!( - recover_discrete_latent_variance(2.0, 1.0, 1e308, 2.0, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) + recover_stationary_lagged_observed_covariance( + 2.0, + 0.0, + 0.4, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::StationaryVarianceRequiresStableDrift) ); - // Zero diffusion is exactly Q_Δt = 0 (Driver Eq. 3). That skip - // does not license exp(2 a Δt) p when 2 (a Δt) overflows to +∞. assert_eq!( - recover_discrete_latent_variance(2.0, 0.0, 1e308, 2.0, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) + recover_stationary_lagged_observed_covariance( + 2.0, + 0.0, + 0.0, + -0.225, + 1.0, + 0.5, + 1.0, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) ); assert_eq!( - recover_discrete_lagged_latent_covariance(1e308, 700.0, 1.0, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) + recover_stationary_lagged_observed_covariance( + 2.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.1, + LagClock::EventTime + ), + Ok(0.1) ); assert_eq!( - recover_discrete_latent_variance(1e308, 1e-308, 350.0, 1.0, LagClock::EventTime), + recover_stationary_lagged_observed_covariance( + f64::NAN, + 1.0, + 0.4, + 0.0, + 0.0, + -0.5, + 1.0, + 0.0, + LagClock::EventTime + ), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_latent_variance(1e308, 1e308, 0.0, 1.0, LagClock::EventTime), + recover_stationary_lagged_observed_covariance( + 2.0, + f64::MAX, + f64::MAX, + 0.0, + 0.0, + -0.5, + 1.0, + 0.0, + LagClock::EventTime + ), Err(PsychometricError::InvalidNumericInput) ); - let carried = (-90.622_f64).exp(); - let diffusion_sum = (-83.938_f64).exp(); + } + + #[test] + #[allow(clippy::too_many_lines)] + fn stationary_later_latent_variance_recovers_driver_section_four_point_three() { + // Driver et al. (2017, §4.3, pp. 9–10; Eq. 3–4, pp. 4–5; p. 16) + // constrain T0VAR across all time points. The later-occasion + // variance is trait + e^{2 a Δt}(−q / (2 a)) + Q_Δt + (B / a)² v. + // Under stationarity that equals contemporaneous T0VAR. + let printed_effect = -0.225_f64; + let printed_asym = -1.673_f64; + let log_rate = -printed_effect / printed_asym; + let trait_variance = 1.0_f64; + let diffusion = 0.4_f64; + let predictor_variance = 1.0_f64; + let event_delta = 1.0_f64; + let recovered = recover_stationary_later_latent_variance( + trait_variance, + diffusion, + printed_effect, + predictor_variance, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("stationary later T0VAR"); + let state = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + let evolved_state = recover_discrete_latent_variance( + state, + diffusion, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("e^{2aΔt}p+Q_Δt"); + let added = recover_asymptotic_time_independent_predictor_variance( + printed_effect, + predictor_variance, + log_rate, + LagClock::EventTime, + ) + .expect("addedTIPREDVAR"); + assert!((recovered - (trait_variance + evolved_state + added)).abs() < 1e-12); + let contemporaneous = recover_stationary_initial_latent_variance( + trait_variance, + diffusion, + printed_effect, + predictor_variance, + log_rate, + LagClock::EventTime, + ) + .expect("stationary T0VAR"); + assert!((recovered - contemporaneous).abs() < 1e-12); + let lagged = recover_stationary_lagged_latent_covariance( + trait_variance, + diffusion, + printed_effect, + predictor_variance, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("stationary lagged T0VAR"); + assert!((recovered - lagged).abs() > 1e-3); + let free_discrete = recover_discrete_latent_variance( + contemporaneous, + diffusion, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("e^{2aΔt} p_stat + Q_Δt"); + assert!((recovered - free_discrete).abs() > 1e-3); + let process_noise = + recover_discrete_process_noise(diffusion, log_rate, event_delta, LagClock::EventTime) + .expect("Q_Δt"); + assert!((recovered - process_noise).abs() > 1e-3); + let state_only = recover_stationary_later_latent_variance( + 0.0, + diffusion, + 0.0, + predictor_variance, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("state-only later"); + assert!((state_only - evolved_state).abs() < 1e-15); + assert!((state_only - state).abs() < 1e-12); + let trait_only = recover_stationary_later_latent_variance( + trait_variance, + 0.0, + 0.0, + predictor_variance, + 0.0, + event_delta, + LagClock::EventTime, + ) + .expect("trait-only later"); + assert!((trait_only - trait_variance).abs() < 1e-15); + let added_only = recover_stationary_later_latent_variance( + 0.0, + 0.0, + printed_effect, + predictor_variance, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("ti-only later"); + assert!((added_only - added).abs() < 1e-15); assert_eq!( - recover_discrete_latent_variance( - carried, - diffusion_sum, - 400.0, - 1.0, + recover_stationary_later_latent_variance( + 0.0, + 0.0, + 0.0, + predictor_variance, + 0.0, + event_delta, LagClock::EventTime ), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_discrete_lagged_latent_covariance(-0.1, -0.5, 1.0, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_discrete_lagged_latent_covariance(f64::NAN, -0.5, 1.0, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_discrete_lagged_latent_covariance(2.0, f64::NAN, 1.0, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_discrete_lagged_latent_covariance(2.0, -0.5, 0.0, LagClock::EventTime), - Err(PsychometricError::NonPositiveInterval) - ); - assert_eq!( - recover_discrete_lagged_latent_covariance(2.0, -0.5, -1.0, LagClock::EventTime), - Err(PsychometricError::NonPositiveInterval) - ); - assert_eq!( - recover_discrete_lagged_latent_covariance(2.0, -0.5, f64::NAN, LagClock::EventTime), - Err(PsychometricError::NonPositiveInterval) - ); - assert_eq!( - recover_discrete_lagged_latent_covariance(2.0, -0.5, 1.0, LagClock::SystemTime), - Err(PsychometricError::EventTimeRequired) - ); - assert_eq!( - recover_discrete_latent_variance(-0.1, 0.4, -0.5, 1.0, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_discrete_latent_variance(f64::NAN, 0.4, -0.5, 1.0, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_discrete_latent_variance(2.0, 0.4, -0.5, 1.0, LagClock::SystemTime), - Err(PsychometricError::EventTimeRequired) + Ok(0.0) ); + let far = recover_stationary_later_latent_variance( + trait_variance, + diffusion, + printed_effect, + predictor_variance, + log_rate, + 1e8, + LagClock::EventTime, + ) + .expect("Δt→∞"); + assert!((far - contemporaneous).abs() < 1e-12); + let near = recover_stationary_later_latent_variance( + trait_variance, + diffusion, + printed_effect, + predictor_variance, + log_rate, + 1e-12, + LagClock::EventTime, + ) + .expect("Δt→0+"); + assert!((near - contemporaneous).abs() < 1e-9); } #[test] - fn stationary_variance_recovers_driver_equation_four_asymptote() { + fn stationary_later_latent_variance_is_not_lagged_discrete_or_process_noise() { + let trait_variance = 1.0_f64; let diffusion = 0.4_f64; - let drift = -0.5_f64; - let recovered = recover_stationary_latent_variance(diffusion, drift, LagClock::EventTime) - .expect("asym"); - let expected = (diffusion / drift) * -0.5; - assert!((recovered - expected).abs() < 1e-15); - assert!((recovered - 0.4).abs() < 1e-15); - // Starting from p_∞, Var(η_t) is invariant across finite Δt. - for delta in [0.5_f64, 1.0, 2.0, 10.0] { - let evolved = recover_discrete_latent_variance( - recovered, - diffusion, - drift, - delta, - LagClock::EventTime, - ) - .expect("invariant"); - assert!( - (evolved - recovered).abs() < 1e-12, - "stationary variance must be invariant at Δt={delta}" - ); - } - let finite_noise = - recover_discrete_process_noise(diffusion, drift, 1.0, LagClock::EventTime) - .expect("finite q_dt"); - assert!((finite_noise - recovered).abs() > 1e-3); + let log_rate = -0.134_488_942_f64; + let event_delta = 1.0_f64; + let recovered = recover_stationary_later_latent_variance( + trait_variance, + diffusion, + -0.225, + 1.0, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("stationary later T0VAR"); + let lagged = recover_stationary_lagged_latent_covariance( + trait_variance, + diffusion, + -0.225, + 1.0, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("stationary lagged T0VAR"); + let contemporaneous = recover_stationary_initial_latent_variance( + trait_variance, + diffusion, + -0.225, + 1.0, + log_rate, + LagClock::EventTime, + ) + .expect("stationary T0VAR"); + let free_discrete = recover_discrete_latent_variance( + contemporaneous, + diffusion, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("e^{2aΔt} p_stat + Q_Δt"); + let process_noise = + recover_discrete_process_noise(diffusion, log_rate, event_delta, LagClock::EventTime) + .expect("Q_Δt"); + assert!((recovered - lagged).abs() > 1e-3); + assert!((recovered - free_discrete).abs() > 1e-3); + assert!((recovered - process_noise).abs() > 1e-3); assert_eq!( - refuse_finite_interval_process_noise_as_stationary_variance(finite_noise, 1.0), - Err(PsychometricError::FiniteIntervalProcessNoiseIsNotStationary) + refuse_stationary_later_latent_variance_as_lagged_covariance(recovered, lagged), + Err(PsychometricError::StationaryLaterLatentVarianceIsNotLaggedCovariance) ); assert_eq!( - refuse_finite_interval_process_noise_as_stationary_variance(recovered, 1.0), - Err(PsychometricError::FiniteIntervalProcessNoiseIsNotStationary) + refuse_stationary_later_latent_variance_as_discrete_variance(recovered, free_discrete), + Err(PsychometricError::StationaryLaterLatentVarianceIsNotDiscreteVariance) ); assert_eq!( - recover_stationary_latent_variance(0.0, drift, LagClock::EventTime), - Ok(0.0) + refuse_stationary_later_latent_variance_as_process_noise(recovered, process_noise), + Err(PsychometricError::StationaryLaterLatentVarianceIsNotProcessNoise) ); - // Do not form 2 a first: 2*(-1e308) overflows; (q/a)*-0.5 is 0.5. - let twice_rate_overflow = - recover_stationary_latent_variance(1e308, -1e308, LagClock::EventTime) - .expect("2a overflow"); - assert!((twice_rate_overflow - 0.5).abs() < 1e-15); - assert!(!(2.0 * -1e308_f64).is_finite()); - let lost = -1e308_f64 / (2.0 * -1e308_f64); - assert!(lost.abs() < 1e-15); - // Do not form 0.5 q first: 0.5 * from_bits(1) underflows. - let min_subnormal = f64::from_bits(1); - assert!((0.5 * min_subnormal).abs() < 1e-300); - assert!((-0.5 * min_subnormal / -min_subnormal).abs() < 1e-300); - let subnormal_ratio = - recover_stationary_latent_variance(min_subnormal, -min_subnormal, LagClock::EventTime) - .expect("subnormal ratio"); - assert!((subnormal_ratio - 0.5).abs() < 1e-15); - assert!(((min_subnormal / -min_subnormal) * -0.5 - 0.5).abs() < 1e-15); - // Do not form q/a first: MAX/-0.75 overflows; MAX/(2*0.75) is finite. - assert!(!(f64::MAX / -0.75_f64).is_finite()); - assert!(!((f64::MAX / -0.75_f64) * -0.5).is_finite()); - let twice = -0.75_f64 * 2.0; - assert!(twice.is_finite()); - let expected_max = f64::MAX / -twice; - assert!(expected_max.is_finite()); - assert_eq!(expected_max.to_bits(), (f64::MAX / 1.5).to_bits()); - let quotient_overflow = - recover_stationary_latent_variance(f64::MAX, -0.75, LagClock::EventTime) - .expect("q/a overflow"); - assert_eq!(quotient_overflow.to_bits(), expected_max.to_bits()); } #[test] - fn stationary_variance_unstable_and_invalid_inputs_fail_closed() { + fn stationary_later_latent_variance_invalid_inputs_fail_closed() { assert_eq!( - recover_stationary_latent_variance(0.4, -0.5, LagClock::SystemTime), + recover_stationary_later_latent_variance( + 1.0, + 0.4, + -0.225, + 1.0, + -0.13, + 1.0, + LagClock::SystemTime + ), Err(PsychometricError::EventTimeRequired) ); assert_eq!( - recover_stationary_latent_variance(0.4, 0.0, LagClock::EventTime), - Err(PsychometricError::StationaryVarianceRequiresStableDrift) + recover_stationary_later_latent_variance( + 1.0, + 0.4, + -0.225, + 1.0, + -0.13, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) ); assert_eq!( - recover_stationary_latent_variance(0.4, 0.5, LagClock::EventTime), + recover_stationary_later_latent_variance( + 0.0, + 0.4, + 0.0, + 1.0, + 0.0, + 1.0, + LagClock::EventTime + ), Err(PsychometricError::StationaryVarianceRequiresStableDrift) ); assert_eq!( - recover_stationary_latent_variance(0.0, 0.0, LagClock::EventTime), - Err(PsychometricError::StationaryVarianceRequiresStableDrift) + recover_stationary_later_latent_variance( + 0.0, + 0.0, + -0.225, + 1.0, + 0.5, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) ); assert_eq!( - recover_stationary_latent_variance(-0.1, -0.5, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) + recover_stationary_later_latent_variance( + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + LagClock::EventTime + ), + Ok(0.0) ); assert_eq!( - recover_stationary_latent_variance(f64::NAN, -0.5, LagClock::EventTime), + recover_stationary_later_latent_variance( + f64::NAN, + 0.4, + 0.0, + 0.0, + -0.5, + 1.0, + LagClock::EventTime + ), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_stationary_latent_variance(0.4, f64::NAN, LagClock::EventTime), + recover_stationary_later_latent_variance( + f64::MAX, + f64::MAX, + 0.0, + 0.0, + -0.5, + 1.0, + LagClock::EventTime + ), Err(PsychometricError::InvalidNumericInput) ); - // The Lyapunov solution overflows when |q| >> |a|. - assert!(!((1e308_f64 / -1e-10_f64) * -0.5).is_finite()); - assert!(!(1e308_f64 / (2.0 * 1e-10_f64)).is_finite()); assert_eq!( - recover_stationary_latent_variance(1e308, -1e-10, LagClock::EventTime), + recover_stationary_later_latent_variance( + f64::MAX, + 0.0, + 1.0, + f64::MAX, + -1.0, + 1.0, + LagClock::EventTime + ), Err(PsychometricError::InvalidNumericInput) ); } #[test] - fn trait_plus_state_recovers_driver_section_four_point_three() { - let trait_variance = 1.5_f64; + #[allow(clippy::too_many_lines)] + fn stationary_later_observed_variance_recovers_driver_equation_five_of_section_four_point_three() + { + // Driver et al. (2017, §4.3, pp. 9–10; Eq. 5, p. 5) + // later-occasion observed variance of stationary T0VAR is + // λ²(trait + e^{2 a Δt}(−q / (2 a)) + Q_Δt + (B / a)² v) + θ + ψ. + // Under stationarity that equals contemporaneous Var(y_0). + let printed_effect = -0.225_f64; + let printed_asym = -1.673_f64; + let log_rate = -printed_effect / printed_asym; + let trait_variance = 1.0_f64; let diffusion = 0.4_f64; - let drift = -0.5_f64; - let delta = 1.0_f64; - let state = recover_stationary_latent_variance(diffusion, drift, LagClock::EventTime) - .expect("state"); - let total = recover_trait_plus_state_latent_variance(trait_variance, state).expect("sum"); - assert!((total - (trait_variance + state)).abs() < 1e-15); - let lagged = recover_trait_plus_state_lagged_covariance( + let loading = 2.0_f64; + let measurement_error = 0.5_f64; + let manifest_trait = 0.1_f64; + let event_delta = 1.0_f64; + let recovered = recover_stationary_later_observed_variance( + loading, trait_variance, - state, - drift, - delta, + diffusion, + printed_effect, + 1.0, + log_rate, + event_delta, + measurement_error, + manifest_trait, LagClock::EventTime, ) - .expect("lagged"); - let state_lagged = - recover_discrete_lagged_latent_covariance(state, drift, delta, LagClock::EventTime) - .expect("state lagged"); - assert!((lagged - (trait_variance + state_lagged)).abs() < 1e-15); - // Evolving the summed variance as if it were all state is not - // the trait-plus-state map (Driver §4.3; Hamaker et al., 2015). - let evolved_as_state = - recover_discrete_latent_variance(total, diffusion, drift, delta, LagClock::EventTime) - .expect("wrong"); - let evolved_state = - recover_discrete_latent_variance(state, diffusion, drift, delta, LagClock::EventTime) - .expect("state evolved"); - let evolved_right = - recover_trait_plus_state_latent_variance(trait_variance, evolved_state).expect("right"); - assert!((evolved_right - total).abs() < 1e-12); - assert!((evolved_as_state - evolved_right).abs() > 1e-3); + .expect("eq5-later-stationary-T0VAR"); + let latent = recover_stationary_later_latent_variance( + trait_variance, + diffusion, + printed_effect, + 1.0, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("stationary later T0VAR"); + let expected = recover_manifest_trait_plus_state_observed_variance( + loading, + latent, + measurement_error, + manifest_trait, + ) + .expect("λ²p+θ+ψ"); + assert!((recovered - expected).abs() < 1e-12); + let contemporaneous = recover_stationary_initial_observed_variance( + loading, + trait_variance, + diffusion, + printed_effect, + 1.0, + log_rate, + measurement_error, + manifest_trait, + LagClock::EventTime, + ) + .expect("eq5-stationary-T0VAR"); + assert!((recovered - contemporaneous).abs() < 1e-12); + let lagged = recover_stationary_lagged_observed_covariance( + loading, + trait_variance, + diffusion, + printed_effect, + 1.0, + log_rate, + event_delta, + manifest_trait, + LagClock::EventTime, + ) + .expect("eq5-lagged-stationary-T0VAR"); + assert!((recovered - lagged).abs() > 1e-3); + assert!((recovered - measurement_error).abs() > 1e-3); + assert!((recovered - latent).abs() > 1e-3); assert_eq!( - recover_trait_plus_state_latent_variance(0.0, state), - Ok(state) + recover_stationary_later_observed_variance( + 0.0, + trait_variance, + diffusion, + printed_effect, + 1.0, + log_rate, + event_delta, + measurement_error, + manifest_trait, + LagClock::EventTime, + ), + Ok(measurement_error + manifest_trait) + ); + assert_eq!( + recover_stationary_later_observed_variance( + loading, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + event_delta, + 0.0, + 0.0, + LagClock::EventTime, + ), + Ok(0.0) ); + let zero_manifest_trait = recover_stationary_later_observed_variance( + loading, + trait_variance, + diffusion, + printed_effect, + 1.0, + log_rate, + event_delta, + measurement_error, + 0.0, + LagClock::EventTime, + ) + .expect("ψ=0"); + let expected_zero_psi = recover_manifest_trait_plus_state_observed_variance( + loading, + latent, + measurement_error, + 0.0, + ) + .expect("λ²p+θ"); + assert!((zero_manifest_trait - expected_zero_psi).abs() < 1e-12); + } + + #[test] + fn stationary_later_observed_variance_is_not_manifest_latent_or_lagged() { + let trait_variance = 1.0_f64; + let diffusion = 0.4_f64; + let log_rate = -0.134_488_942_f64; + let loading = 2.0_f64; + let measurement_error = 0.5_f64; + let event_delta = 1.0_f64; + let recovered = recover_stationary_later_observed_variance( + loading, + trait_variance, + diffusion, + -0.225, + 1.0, + log_rate, + event_delta, + measurement_error, + 0.1, + LagClock::EventTime, + ) + .expect("eq5-later-stationary-T0VAR"); + let latent = recover_stationary_later_latent_variance( + trait_variance, + diffusion, + -0.225, + 1.0, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("stationary later T0VAR"); + let lagged = recover_stationary_lagged_observed_covariance( + loading, + trait_variance, + diffusion, + -0.225, + 1.0, + log_rate, + event_delta, + 0.1, + LagClock::EventTime, + ) + .expect("eq5-lagged-stationary-T0VAR"); assert_eq!( - recover_trait_plus_state_latent_variance(trait_variance, 0.0), - Ok(trait_variance) + refuse_stationary_later_latent_variance_as_observed_variance(latent, recovered), + Err(PsychometricError::StationaryLaterLatentVarianceIsNotObservedVariance) ); assert_eq!( - recover_trait_plus_state_lagged_covariance( - 0.0, - state, - drift, - delta, - LagClock::EventTime + refuse_measurement_error_as_stationary_later_observed_variance( + measurement_error, + recovered ), - Ok(state_lagged) + Err(PsychometricError::MeasurementErrorIsNotStationaryLaterObservedVariance) ); assert_eq!( - recover_trait_plus_state_lagged_covariance( - trait_variance, - 0.0, - drift, - delta, - LagClock::EventTime + refuse_stationary_lagged_observed_covariance_as_stationary_later_observed_variance( + lagged, recovered ), - Ok(trait_variance) - ); - let process_noise = - recover_discrete_process_noise(diffusion, drift, delta, LagClock::EventTime) - .expect("q_dt"); - assert_eq!( - refuse_trait_variance_as_process_noise(trait_variance, process_noise), - Err(PsychometricError::TraitVarianceIsNotProcessNoise) - ); - assert_eq!( - refuse_trait_variance_as_stationary_within_subject(trait_variance, state), - Err(PsychometricError::TraitVarianceIsNotStationaryWithinSubject) + Err( + PsychometricError::StationaryLaggedObservedCovarianceIsNotStationaryLaterObservedVariance + ) ); } #[test] - fn trait_plus_state_invalid_inputs_fail_closed() { - assert_eq!( - recover_trait_plus_state_latent_variance(-0.1, 0.4), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_trait_plus_state_latent_variance(0.4, -0.1), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_trait_plus_state_latent_variance(f64::NAN, 0.4), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_trait_plus_state_latent_variance(0.4, f64::NAN), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_trait_plus_state_latent_variance(1e308, 1e308), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_trait_plus_state_lagged_covariance(-0.1, 0.4, -0.5, 1.0, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) - ); + #[allow(clippy::too_many_lines)] + fn stationary_later_observed_variance_invalid_inputs_fail_closed() { assert_eq!( - recover_trait_plus_state_lagged_covariance( - f64::NAN, + recover_stationary_later_observed_variance( + 2.0, + 1.0, 0.4, - -0.5, + -0.225, 1.0, - LagClock::EventTime + -0.13, + 1.0, + 0.5, + 0.1, + LagClock::SystemTime ), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_trait_plus_state_lagged_covariance(0.4, 0.4, -0.5, 1.0, LagClock::SystemTime), Err(PsychometricError::EventTimeRequired) ); assert_eq!( - recover_trait_plus_state_lagged_covariance(1e308, 1e308, 0.0, 1.0, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) - ); - } - - #[test] - fn non_event_clocks_and_difference_quotient_fail_closed() { - for clock in [ - LagClock::SystemTime, - LagClock::AssertionTime, - LagClock::DocumentTime, - LagClock::AvailabilityTime, - LagClock::KnowledgeCutoff, - ] { - assert_eq!( - recover_local_log_rate(0.5, 1.0, clock), - Err(PsychometricError::EventTimeRequired) - ); - assert!(!clock.admits_structural_lag()); - assert!(!std::hint::black_box(clock).as_str().is_empty()); - } - assert!(LagClock::EventTime.admits_structural_lag()); - assert_eq!( - std::hint::black_box(LagClock::EventTime).as_str(), - "event_time" - ); - assert_eq!( - refuse_difference_quotient_as_local_rate(1.0, 0.5, 1.0), - Err(PsychometricError::DifferenceQuotientForbidden) - ); - } - - #[test] - fn invalid_lag_inputs_fail_closed() { - assert_eq!( - recover_discrete_lag_one(0.0, 1.0), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_discrete_lag_one(f64::NAN, 1.0), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_discrete_lag_one(1.0, f64::INFINITY), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_local_log_rate(0.5, 0.0, LagClock::EventTime), - Err(PsychometricError::NonPositiveInterval) - ); - assert_eq!( - recover_local_log_rate(0.5, -1.0, LagClock::EventTime), - Err(PsychometricError::NonPositiveInterval) - ); - assert_eq!( - recover_local_log_rate(0.5, f64::NAN, LagClock::EventTime), + recover_stationary_later_observed_variance( + 2.0, + 1.0, + 0.4, + -0.225, + 1.0, + -0.13, + 0.0, + 0.5, + 0.1, + LagClock::EventTime + ), Err(PsychometricError::NonPositiveInterval) ); assert_eq!( - recover_local_log_rate(0.0, 1.0, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_local_log_rate(-0.2, 1.0, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_local_log_rate(f64::NAN, 1.0, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) - ); - } - - #[test] - fn series_mean_log_rate_recovers_and_refuses() { - let drift = -0.25_f64; - let occasions = [ - EventOccasion { - event_time: 0.0, - score: 2.0, - }, - EventOccasion { - event_time: 1.0, - score: 2.0 * drift.exp(), - }, - EventOccasion { - event_time: 3.0, - score: 2.0 * (drift * 3.0).exp(), - }, - ]; - let series = - recover_event_series_mean_log_rate(&occasions, LagClock::EventTime).expect("series"); - assert!((series - drift).abs() < 1e-12); - assert_eq!( - recover_event_series_mean_log_rate(&occasions, LagClock::SystemTime), - Err(PsychometricError::EventTimeRequired) + recover_stationary_later_observed_variance( + 2.0, + 0.0, + 0.4, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::StationaryVarianceRequiresStableDrift) ); assert_eq!( - recover_event_series_mean_log_rate(&[], LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) + recover_stationary_later_observed_variance( + 2.0, + 0.0, + 0.0, + -0.225, + 1.0, + 0.5, + 1.0, + 0.0, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) ); assert_eq!( - recover_event_series_mean_log_rate( - &[EventOccasion { - event_time: 0.0, - score: 1.0, - }], + recover_stationary_later_observed_variance( + 2.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.5, + 0.1, LagClock::EventTime ), - Err(PsychometricError::InvalidNumericInput) + Ok(0.6) ); assert_eq!( - recover_event_series_mean_log_rate( - &[ - EventOccasion { - event_time: f64::NAN, - score: 1.0, - }, - EventOccasion { - event_time: 1.0, - score: 0.5, - }, - ], + recover_stationary_later_observed_variance( + f64::NAN, + 1.0, + 0.4, + 0.0, + 0.0, + -0.5, + 1.0, + 0.0, + 0.0, LagClock::EventTime ), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_event_series_mean_log_rate( - &[occasion(0.0, 1.0), occasion(f64::NAN, 0.5)], + recover_stationary_later_observed_variance( + 2.0, + f64::MAX, + f64::MAX, + 0.0, + 0.0, + -0.5, + 1.0, + 0.0, + 0.0, LagClock::EventTime ), Err(PsychometricError::InvalidNumericInput) ); + } + + #[test] + #[allow(clippy::too_many_lines)] + fn predetermined_later_latent_variance_recovers_driver_section_four_point_three() { + // Driver et al. (2017, §4.3, pp. 9–10; Eq. 3–4, pp. 4–5) + // treat the first time point as predetermined. Free T0VAR p_0 + // then transitions toward stationarity: + // trait + e^{2 a Δt} p_0 + Q_Δt + (B / a)² v. + let printed_effect = -0.225_f64; + let printed_asym = -1.673_f64; + let log_rate = -printed_effect / printed_asym; + let trait_variance = 1.0_f64; + let initial_latent_variance = 2.0_f64; + let diffusion = 0.4_f64; + let predictor_variance = 1.0_f64; + let event_delta = 1.0_f64; + let recovered = recover_predetermined_later_latent_variance( + trait_variance, + initial_latent_variance, + diffusion, + printed_effect, + predictor_variance, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("predetermined later T0VAR"); + let evolved_state = recover_discrete_latent_variance( + initial_latent_variance, + diffusion, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("e^{2aΔt}p_0+Q_Δt"); + let added = recover_asymptotic_time_independent_predictor_variance( + printed_effect, + predictor_variance, + log_rate, + LagClock::EventTime, + ) + .expect("addedTIPREDVAR"); + assert!((recovered - (trait_variance + evolved_state + added)).abs() < 1e-12); + let stationary_later = recover_stationary_later_latent_variance( + trait_variance, + diffusion, + printed_effect, + predictor_variance, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("stationary later T0VAR"); + assert!((recovered - stationary_later).abs() > 1e-3); + let state = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + let from_stationary_start = recover_predetermined_later_latent_variance( + trait_variance, + state, + diffusion, + printed_effect, + predictor_variance, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("p_0=−q/(2a)"); + assert!((from_stationary_start - stationary_later).abs() < 1e-12); + let contemporaneous = recover_stationary_initial_latent_variance( + trait_variance, + diffusion, + printed_effect, + predictor_variance, + log_rate, + LagClock::EventTime, + ) + .expect("stationary T0VAR"); + let first_occasion_total = trait_variance + initial_latent_variance + added; + let free_discrete = recover_discrete_latent_variance( + first_occasion_total, + diffusion, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("e^{2aΔt}(trait+p_0+added)+Q_Δt"); + assert!((recovered - free_discrete).abs() > 1e-3); + assert!((recovered - initial_latent_variance).abs() > 1e-3); + let state_only = recover_predetermined_later_latent_variance( + 0.0, + initial_latent_variance, + diffusion, + 0.0, + predictor_variance, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("state-only predetermined later"); + assert!((state_only - evolved_state).abs() < 1e-15); + let trait_only = recover_predetermined_later_latent_variance( + trait_variance, + 0.0, + 0.0, + 0.0, + predictor_variance, + 0.0, + event_delta, + LagClock::EventTime, + ) + .expect("trait-only predetermined later"); + assert!((trait_only - trait_variance).abs() < 1e-15); + let added_only = recover_predetermined_later_latent_variance( + 0.0, + 0.0, + 0.0, + printed_effect, + predictor_variance, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("ti-only predetermined later"); + assert!((added_only - added).abs() < 1e-15); assert_eq!( - recover_event_series_mean_log_rate( - &[occasion(0.0, f64::NAN), occasion(1.0, 0.5)], + recover_predetermined_later_latent_variance( + 0.0, + 0.0, + 0.0, + 0.0, + predictor_variance, + 0.0, + event_delta, LagClock::EventTime ), - Err(PsychometricError::InvalidNumericInput) + Ok(0.0) + ); + let far = recover_predetermined_later_latent_variance( + trait_variance, + initial_latent_variance, + diffusion, + printed_effect, + predictor_variance, + log_rate, + 1e8, + LagClock::EventTime, + ) + .expect("Δt→∞"); + assert!((far - contemporaneous).abs() < 1e-12); + let near = recover_predetermined_later_latent_variance( + trait_variance, + initial_latent_variance, + diffusion, + printed_effect, + predictor_variance, + log_rate, + 1e-12, + LagClock::EventTime, + ) + .expect("Δt→0+"); + assert!((near - first_occasion_total).abs() < 1e-9); + let growing = recover_predetermined_later_latent_variance( + 0.0, + initial_latent_variance, + diffusion, + 0.0, + 0.0, + 0.5, + event_delta, + LagClock::EventTime, + ) + .expect("growing process"); + assert!(growing > initial_latent_variance); + } + + #[test] + fn predetermined_later_latent_variance_is_not_stationary_discrete_or_initial() { + let trait_variance = 1.0_f64; + let initial_latent_variance = 2.0_f64; + let diffusion = 0.4_f64; + let log_rate = -0.134_488_942_f64; + let event_delta = 1.0_f64; + let recovered = recover_predetermined_later_latent_variance( + trait_variance, + initial_latent_variance, + diffusion, + -0.225, + 1.0, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("predetermined later T0VAR"); + let stationary_later = recover_stationary_later_latent_variance( + trait_variance, + diffusion, + -0.225, + 1.0, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("stationary later T0VAR"); + let added = recover_asymptotic_time_independent_predictor_variance( + -0.225, + 1.0, + log_rate, + LagClock::EventTime, + ) + .expect("addedTIPREDVAR"); + let free_discrete = recover_discrete_latent_variance( + trait_variance + initial_latent_variance + added, + diffusion, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("e^{2aΔt}(trait+p_0+added)+Q_Δt"); + assert!((recovered - stationary_later).abs() > 1e-3); + assert!((recovered - free_discrete).abs() > 1e-3); + assert!((recovered - initial_latent_variance).abs() > 1e-3); + assert_eq!( + refuse_predetermined_later_latent_variance_as_stationary_later_latent_variance( + recovered, + stationary_later + ), + Err( + PsychometricError::PredeterminedLaterLatentVarianceIsNotStationaryLaterLatentVariance + ) ); assert_eq!( - recover_event_series_mean_log_rate( - &[occasion(0.0, 1.0), occasion(1.0, f64::NAN)], - LagClock::EventTime + refuse_predetermined_later_latent_variance_as_discrete_variance( + recovered, + free_discrete ), - Err(PsychometricError::InvalidNumericInput) + Err(PsychometricError::PredeterminedLaterLatentVarianceIsNotDiscreteVariance) ); assert_eq!( - recover_event_series_mean_log_rate( - &[ - EventOccasion { - event_time: 0.0, - score: 1.0, - }, - EventOccasion { - event_time: 0.0, - score: 0.5, - }, - ], - LagClock::EventTime + refuse_predetermined_later_latent_variance_as_initial_latent_variance( + recovered, + initial_latent_variance ), - Err(PsychometricError::NonPositiveInterval) + Err(PsychometricError::PredeterminedLaterLatentVarianceIsNotInitialLatentVariance) ); } - fn clustered(cluster_key: u64, event_time: f64, score: f64) -> ClusteredEventScore { - ClusteredEventScore { - cluster_key, - event_time, - score, - } - } - - fn occasion(event_time: f64, score: f64) -> EventOccasion { - EventOccasion { event_time, score } - } - - fn decaying_clustered_scores(drift: f64) -> [ClusteredEventScore; 12] { - [ - clustered(1, 0.0, 10.0 + 1.0), - clustered(1, 1.0, 10.0 + drift.exp()), - clustered(1, 2.0, 10.0 + (drift * 2.0).exp()), - clustered(1, 3.0, 10.0 + (drift * 3.0).exp()), - clustered(1, 4.0, 10.0 + (drift * 4.0).exp()), - clustered(1, 5.0, 10.0 + (drift * 5.0).exp()), - clustered(2, 0.0, -6.0 + 1.2), - clustered(2, 1.0, -6.0 + 1.2 * drift.exp()), - clustered(2, 2.0, -6.0 + 1.2 * (drift * 2.0).exp()), - clustered(2, 3.0, -6.0 + 1.2 * (drift * 3.0).exp()), - clustered(2, 4.0, -6.0 + 1.2 * (drift * 4.0).exp()), - clustered(2, 5.0, -6.0 + 1.2 * (drift * 5.0).exp()), - ] - } - #[test] - fn within_residual_paths_recover_and_refuse() { - let drift = -0.25_f64; - let clustered = decaying_clustered_scores(drift); - let within = recover_within_residual_event_time_log_rate(&clustered, LagClock::EventTime) - .expect("cwc lag"); - let within_error = (within - drift).abs(); - assert!(within_error.is_finite()); - } - - #[test] - fn within_residual_invalid_rows_fail_closed() { - let rows = decaying_clustered_scores(-0.25); - assert_eq!( - recover_within_residual_event_time_log_rate(&rows, LagClock::SystemTime), - Err(PsychometricError::EventTimeRequired) - ); + #[allow(clippy::too_many_lines)] + fn predetermined_later_latent_variance_invalid_inputs_fail_closed() { assert_eq!( - recover_within_residual_event_time_log_rate(&[], LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) + recover_predetermined_later_latent_variance( + 1.0, + 2.0, + 0.4, + -0.225, + 1.0, + -0.13, + 1.0, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) ); assert_eq!( - recover_within_residual_event_time_log_rate( - &[clustered(1, 0.0, 1.0), clustered(1, 1.0, 0.5)], + recover_predetermined_later_latent_variance( + 1.0, + 2.0, + 0.4, + -0.225, + 1.0, + -0.13, + 0.0, LagClock::EventTime ), - Err(PsychometricError::InsufficientClusters) + Err(PsychometricError::NonPositiveInterval) ); assert_eq!( - recover_within_residual_event_time_log_rate( - &[clustered(1, f64::NAN, 1.0), clustered(2, 1.0, 0.5)], + recover_predetermined_later_latent_variance( + 0.0, + 0.0, + 0.0, + -0.225, + 1.0, + 0.5, + 1.0, LagClock::EventTime ), - Err(PsychometricError::InvalidNumericInput) + Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) ); assert_eq!( - recover_event_series_mean_log_rate( - &[occasion(0.0, 1.0), occasion(1.0, f64::NAN)], + recover_predetermined_later_latent_variance( + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, LagClock::EventTime ), - Err(PsychometricError::InvalidNumericInput) + Ok(0.0) ); + let growing = recover_predetermined_later_latent_variance( + 0.0, + 2.0, + 0.4, + 0.0, + 0.0, + 0.0, + 1.0, + LagClock::EventTime, + ) + .expect("Brownian a=0"); + assert!((growing - 2.4).abs() < 1e-12); assert_eq!( - recover_within_residual_event_time_log_rate( - &[ - clustered(1, 0.0, 1.0), - clustered(1, 1.0, 0.5), - clustered(2, 0.0, 2.0), - clustered(2, 1.0, 1.0), - ], + recover_predetermined_later_latent_variance( + f64::NAN, + 2.0, + 0.4, + 0.0, + 0.0, + -0.5, + 1.0, LagClock::EventTime ), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_within_residual_event_time_log_rate( - &[clustered(1, 0.0, 1.0), clustered(2, 1.0, f64::INFINITY)], + recover_predetermined_later_latent_variance( + f64::MAX, + f64::MAX, + 0.0, + 0.0, + 0.0, + -0.5, + 1.0, LagClock::EventTime ), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_within_residual_event_time_log_rate( - &[ - clustered(1, 0.0, 1.0), - clustered(1, 0.0, 1.2), - clustered(2, 0.0, 2.0), - clustered(2, 1.0, 1.5), - ], + recover_predetermined_later_latent_variance( + f64::MAX, + 0.0, + 0.0, + 1.0, + f64::MAX, + -1.0, + 1.0, LagClock::EventTime ), - Err(PsychometricError::NonPositiveInterval) + Err(PsychometricError::InvalidNumericInput) ); } - fn lagged( - earlier_residual: f64, - later_residual: f64, - event_delta: f64, - ) -> LaggedWithinResidual { - LaggedWithinResidual { - earlier_residual, - later_residual, + #[test] + #[allow(clippy::too_many_lines)] + fn predetermined_later_observed_variance_recovers_driver_equation_five_of_section_four_point_three() + { + // Driver et al. (2017, §4.3, pp. 9–10; Eq. 5, p. 5) + // later-occasion observed variance of predetermined T0VAR is + // λ²(trait + e^{2 a Δt} p_0 + Q_Δt + (B / a)² v) + θ + ψ. + let printed_effect = -0.225_f64; + let printed_asym = -1.673_f64; + let log_rate = -printed_effect / printed_asym; + let trait_variance = 1.0_f64; + let initial_latent_variance = 2.0_f64; + let diffusion = 0.4_f64; + let loading = 2.0_f64; + let measurement_error = 0.5_f64; + let manifest_trait = 0.1_f64; + let event_delta = 1.0_f64; + let recovered = recover_predetermined_later_observed_variance( + loading, + trait_variance, + initial_latent_variance, + diffusion, + printed_effect, + 1.0, + log_rate, event_delta, - } + measurement_error, + manifest_trait, + LagClock::EventTime, + ) + .expect("eq5-later-predetermined-T0VAR"); + let latent = recover_predetermined_later_latent_variance( + trait_variance, + initial_latent_variance, + diffusion, + printed_effect, + 1.0, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("predetermined later T0VAR"); + let expected = recover_manifest_trait_plus_state_observed_variance( + loading, + latent, + measurement_error, + manifest_trait, + ) + .expect("λ²p+θ+ψ"); + assert!((recovered - expected).abs() < 1e-12); + let stationary_later = recover_stationary_later_observed_variance( + loading, + trait_variance, + diffusion, + printed_effect, + 1.0, + log_rate, + event_delta, + measurement_error, + manifest_trait, + LagClock::EventTime, + ) + .expect("eq5-later-stationary-T0VAR"); + assert!((recovered - stationary_later).abs() > 1e-3); + let state = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + let from_stationary_start = recover_predetermined_later_observed_variance( + loading, + trait_variance, + state, + diffusion, + printed_effect, + 1.0, + log_rate, + event_delta, + measurement_error, + manifest_trait, + LagClock::EventTime, + ) + .expect("p_0=−q/(2a)"); + assert!((from_stationary_start - stationary_later).abs() < 1e-12); + assert!((recovered - measurement_error).abs() > 1e-3); + assert!((recovered - latent).abs() > 1e-3); + assert_eq!( + recover_predetermined_later_observed_variance( + 0.0, + trait_variance, + initial_latent_variance, + diffusion, + printed_effect, + 1.0, + log_rate, + event_delta, + measurement_error, + manifest_trait, + LagClock::EventTime, + ), + Ok(measurement_error + manifest_trait) + ); + assert_eq!( + recover_predetermined_later_observed_variance( + loading, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + event_delta, + 0.0, + 0.0, + LagClock::EventTime, + ), + Ok(0.0) + ); + let zero_manifest_trait = recover_predetermined_later_observed_variance( + loading, + trait_variance, + initial_latent_variance, + diffusion, + printed_effect, + 1.0, + log_rate, + event_delta, + measurement_error, + 0.0, + LagClock::EventTime, + ) + .expect("ψ=0"); + let expected_zero_psi = recover_manifest_trait_plus_state_observed_variance( + loading, + latent, + measurement_error, + 0.0, + ) + .expect("λ²p+θ"); + assert!((zero_manifest_trait - expected_zero_psi).abs() < 1e-12); } #[test] - fn irregular_centered_residuals_recover_exact_drift() { - let drift = -0.4_f64; - let pairs = [ - lagged(1.2, 1.2 * (drift * 0.5).exp(), 0.5), - lagged(0.8, 0.8 * (drift * 1.75).exp(), 1.75), - lagged(-1.1, -1.1 * (drift * 2.25).exp(), 2.25), - ]; - let recovered = recover_irregular_centered_residual_log_rate(&pairs, LagClock::EventTime) - .expect("irregular"); - assert!((recovered - drift).abs() < 1e-12); + fn predetermined_later_observed_variance_is_not_manifest_latent_or_stationary() { + let trait_variance = 1.0_f64; + let initial_latent_variance = 2.0_f64; + let diffusion = 0.4_f64; + let log_rate = -0.134_488_942_f64; + let loading = 2.0_f64; + let measurement_error = 0.5_f64; + let event_delta = 1.0_f64; + let recovered = recover_predetermined_later_observed_variance( + loading, + trait_variance, + initial_latent_variance, + diffusion, + -0.225, + 1.0, + log_rate, + event_delta, + measurement_error, + 0.1, + LagClock::EventTime, + ) + .expect("eq5-later-predetermined-T0VAR"); + let latent = recover_predetermined_later_latent_variance( + trait_variance, + initial_latent_variance, + diffusion, + -0.225, + 1.0, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("predetermined later T0VAR"); + let stationary_later = recover_stationary_later_observed_variance( + loading, + trait_variance, + diffusion, + -0.225, + 1.0, + log_rate, + event_delta, + measurement_error, + 0.1, + LagClock::EventTime, + ) + .expect("eq5-later-stationary-T0VAR"); + assert_eq!( + refuse_predetermined_later_latent_variance_as_observed_variance(latent, recovered), + Err(PsychometricError::PredeterminedLaterLatentVarianceIsNotObservedVariance) + ); + assert_eq!( + refuse_measurement_error_as_predetermined_later_observed_variance( + measurement_error, + recovered + ), + Err(PsychometricError::MeasurementErrorIsNotPredeterminedLaterObservedVariance) + ); + assert_eq!( + refuse_stationary_later_observed_variance_as_predetermined_later_observed_variance( + stationary_later, + recovered + ), + Err( + PsychometricError::StationaryLaterObservedVarianceIsNotPredeterminedLaterObservedVariance + ) + ); } #[test] - fn irregular_centered_residuals_fail_closed() { - let ok = lagged(1.0, 0.8, 1.0); + #[allow(clippy::too_many_lines)] + fn predetermined_later_observed_variance_invalid_inputs_fail_closed() { assert_eq!( - recover_irregular_centered_residual_log_rate(&[ok], LagClock::SystemTime), + recover_predetermined_later_observed_variance( + 2.0, + 1.0, + 2.0, + 0.4, + -0.225, + 1.0, + -0.13, + 1.0, + 0.5, + 0.1, + LagClock::SystemTime + ), Err(PsychometricError::EventTimeRequired) ); assert_eq!( - recover_irregular_centered_residual_log_rate(&[], LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_irregular_centered_residual_log_rate( - &[lagged(f64::NAN, 0.8, 1.0)], + recover_predetermined_later_observed_variance( + 2.0, + 1.0, + 2.0, + 0.4, + -0.225, + 1.0, + -0.13, + 0.0, + 0.5, + 0.1, LagClock::EventTime ), - Err(PsychometricError::InvalidNumericInput) + Err(PsychometricError::NonPositiveInterval) ); assert_eq!( - recover_irregular_centered_residual_log_rate( - &[lagged(1.0, f64::INFINITY, 1.0)], + recover_predetermined_later_observed_variance( + 2.0, + 0.0, + 0.0, + 0.0, + -0.225, + 1.0, + 0.5, + 1.0, + 0.0, + 0.0, LagClock::EventTime ), - Err(PsychometricError::InvalidNumericInput) + Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) ); assert_eq!( - recover_irregular_centered_residual_log_rate( - &[lagged(1.0, 0.8, f64::NAN)], + recover_predetermined_later_observed_variance( + 2.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.5, + 0.1, LagClock::EventTime ), - Err(PsychometricError::InvalidNumericInput) + Ok(0.6) ); assert_eq!( - recover_irregular_centered_residual_log_rate( - &[lagged(0.0, 0.8, 1.0)], + recover_predetermined_later_observed_variance( + f64::NAN, + 1.0, + 2.0, + 0.4, + 0.0, + 0.0, + -0.5, + 1.0, + 0.0, + 0.0, LagClock::EventTime ), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_irregular_centered_residual_log_rate( - &[lagged(1.0, -0.8, 1.0)], + recover_predetermined_later_observed_variance( + 2.0, + f64::MAX, + f64::MAX, + 0.0, + 0.0, + 0.0, + -0.5, + 1.0, + 0.0, + 0.0, LagClock::EventTime ), Err(PsychometricError::InvalidNumericInput) ); + } + + #[test] + #[allow(clippy::too_many_lines)] + fn predetermined_lagged_latent_covariance_recovers_driver_section_four_point_three() { + // Driver et al. (2017, §4.3; Eq. 3–4): cov(η_t, η_{t0}) = + // trait + e^{a Δt} p_0 + (B / a)² v. + let printed_effect = -0.225_f64; + let printed_asym = -1.673_f64; + let log_rate = -printed_effect / printed_asym; + let trait_variance = 1.0_f64; + let initial_latent_variance = 2.0_f64; + let diffusion = 0.4_f64; + let predictor_variance = 1.0_f64; + let event_delta = 1.0_f64; + let recovered = recover_predetermined_lagged_latent_covariance( + trait_variance, + initial_latent_variance, + printed_effect, + predictor_variance, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("predetermined lagged T0VAR"); + let lagged_state = recover_discrete_lagged_latent_covariance( + initial_latent_variance, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("e^{aΔt}p_0"); + let added = recover_asymptotic_time_independent_predictor_variance( + printed_effect, + predictor_variance, + log_rate, + LagClock::EventTime, + ) + .expect("addedTIPREDVAR"); + assert!((recovered - (trait_variance + lagged_state + added)).abs() < 1e-12); + let stationary_lagged = recover_stationary_lagged_latent_covariance( + trait_variance, + diffusion, + printed_effect, + predictor_variance, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("stationary lagged T0VAR"); + assert!((recovered - stationary_lagged).abs() > 1e-3); + let state = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + let from_stationary_start = recover_predetermined_lagged_latent_covariance( + trait_variance, + state, + printed_effect, + predictor_variance, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("p_0=−q/(2a)"); + assert!((from_stationary_start - stationary_lagged).abs() < 1e-12); + let later = recover_predetermined_later_latent_variance( + trait_variance, + initial_latent_variance, + diffusion, + printed_effect, + predictor_variance, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("predetermined later T0VAR"); + assert!((recovered - later).abs() > 1e-3); + let first_occasion_total = trait_variance + initial_latent_variance + added; + let decayed_total = recover_discrete_lagged_latent_covariance( + first_occasion_total, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("e^{aΔt}(trait+p_0+added)"); + assert!((recovered - decayed_total).abs() > 1e-3); + assert!((recovered - initial_latent_variance).abs() > 1e-3); assert_eq!( - recover_irregular_centered_residual_log_rate( - &[lagged(1.0, 0.8, 0.0)], + recover_predetermined_lagged_latent_covariance( + 0.0, + 0.0, + 0.0, + predictor_variance, + 0.0, + event_delta, LagClock::EventTime ), - Err(PsychometricError::NonPositiveInterval) + Ok(0.0) + ); + let trait_only = recover_predetermined_lagged_latent_covariance( + trait_variance, + 0.0, + 0.0, + predictor_variance, + 0.0, + event_delta, + LagClock::EventTime, + ) + .expect("trait-only predetermined lagged"); + assert!((trait_only - trait_variance).abs() < 1e-15); + let far = recover_predetermined_lagged_latent_covariance( + trait_variance, + initial_latent_variance, + printed_effect, + predictor_variance, + log_rate, + 1e8, + LagClock::EventTime, + ) + .expect("Δt→∞"); + assert!((far - (trait_variance + added)).abs() < 1e-12); + let near = recover_predetermined_lagged_latent_covariance( + trait_variance, + initial_latent_variance, + printed_effect, + predictor_variance, + log_rate, + 1e-12, + LagClock::EventTime, + ) + .expect("Δt→0+"); + assert!((near - first_occasion_total).abs() < 1e-9); + let growing = recover_predetermined_lagged_latent_covariance( + 0.0, + initial_latent_variance, + 0.0, + 0.0, + 0.5, + event_delta, + LagClock::EventTime, + ) + .expect("growing carry"); + assert!(growing > initial_latent_variance); + } + + #[test] + fn predetermined_lagged_latent_covariance_is_not_stationary_later_or_decayed() { + let trait_variance = 1.0_f64; + let initial_latent_variance = 2.0_f64; + let diffusion = 0.4_f64; + let log_rate = -0.134_488_942_f64; + let event_delta = 1.0_f64; + let recovered = recover_predetermined_lagged_latent_covariance( + trait_variance, + initial_latent_variance, + -0.225, + 1.0, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("predetermined lagged T0VAR"); + let stationary_lagged = recover_stationary_lagged_latent_covariance( + trait_variance, + diffusion, + -0.225, + 1.0, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("stationary lagged T0VAR"); + let later = recover_predetermined_later_latent_variance( + trait_variance, + initial_latent_variance, + diffusion, + -0.225, + 1.0, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("predetermined later T0VAR"); + let added = recover_asymptotic_time_independent_predictor_variance( + -0.225, + 1.0, + log_rate, + LagClock::EventTime, + ) + .expect("addedTIPREDVAR"); + let decayed_total = recover_discrete_lagged_latent_covariance( + trait_variance + initial_latent_variance + added, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("e^{aΔt}(trait+p_0+added)"); + assert_eq!( + refuse_predetermined_lagged_latent_covariance_as_stationary_lagged_covariance( + recovered, + stationary_lagged + ), + Err(PsychometricError::PredeterminedLaggedLatentCovarianceIsNotStationaryLaggedCovariance) ); assert_eq!( - recover_irregular_centered_residual_log_rate( - &[lagged(1.0, 0.8, -0.5)], - LagClock::EventTime + refuse_predetermined_lagged_latent_covariance_as_later_latent_variance( + recovered, later ), - Err(PsychometricError::NonPositiveInterval) + Err(PsychometricError::PredeterminedLaggedLatentCovarianceIsNotLaterLatentVariance) ); - } - - #[test] - fn singleton_cluster_is_skipped_and_all_singletons_fail_closed() { - let drift = -0.2_f64; - let mixed = [ - clustered(1, 0.0, 10.0 + 1.0), - clustered(1, 1.0, 10.0 + drift.exp()), - clustered(1, 2.0, 10.0 + (drift * 2.0).exp()), - clustered(1, 3.0, 10.0 + (drift * 3.0).exp()), - clustered(2, 0.0, 4.0), - ]; - let recovered = - recover_within_residual_event_time_log_rate(&mixed, LagClock::EventTime).expect("skip"); - assert!(recovered.is_finite()); assert_eq!( - recover_within_residual_event_time_log_rate( - &[clustered(1, 0.0, 1.0), clustered(2, 1.0, 0.5)], - LagClock::EventTime + refuse_predetermined_lagged_latent_covariance_as_decayed_total( + recovered, + decayed_total ), - Err(PsychometricError::InvalidNumericInput) + Err(PsychometricError::PredeterminedLaggedLatentCovarianceIsNotDecayedTotal) ); - } - - #[test] - fn overflowing_cwc_residuals_fail_closed() { assert_eq!( - recover_within_residual_event_time_log_rate( - &[ - clustered(1, 0.0, f64::MAX), - clustered(1, 1.0, f64::MAX), - clustered(2, 0.0, 1.0), - clustered(2, 1.0, 0.5), - ], - LagClock::EventTime + refuse_predetermined_lagged_latent_covariance_as_initial_latent_variance( + recovered, + initial_latent_variance ), - Err(PsychometricError::InvalidNumericInput) + Err(PsychometricError::PredeterminedLaggedLatentCovarianceIsNotInitialLatentVariance) ); } #[test] - fn newton_overflow_and_flat_derivative_fail_closed() { + fn predetermined_lagged_latent_covariance_invalid_inputs_fail_closed() { assert_eq!( - fit_scalar_log_rate(&[(1e-300, 1.0, 1e-8), (1.0, 1.0, 1.0)]), - Err(PsychometricError::InvalidNumericInput) + recover_predetermined_lagged_latent_covariance( + 1.0, + 2.0, + -0.225, + 1.0, + -0.13, + 1.0, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) ); assert_eq!( - fit_scalar_log_rate(&[(1e200, 1e200, 1.0)]), - Err(PsychometricError::InvalidNumericInput) + recover_predetermined_lagged_latent_covariance( + 1.0, + 2.0, + -0.225, + 1.0, + -0.13, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) ); - let flat = fit_scalar_log_rate(&[(1e-50, 1e-200, 1.0)]).expect("flat"); - assert!(flat.is_finite()); assert_eq!( - fit_scalar_log_rate(&[(0.0, 1.0, 1.0), (1.0, -1.0, 1.0)]), - Err(PsychometricError::InvalidNumericInput) + recover_predetermined_lagged_latent_covariance( + 0.0, + 0.0, + -0.225, + 1.0, + 0.5, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) ); - let skipped_start = - fit_scalar_log_rate(&[(1e-320, 1.0, 1.0), (1.0, 0.5, 1.0)]).expect("skip inf ratio"); - assert!(skipped_start.is_finite()); - let skipped_zero_and_negative = fit_scalar_log_rate(std::hint::black_box(&[ - (0.0, 1.0, 1.0), - (1.0, -1.0, 1.0), - (1.0, 0.5, 1.0), - ])) - .expect("skip zero and negative lags"); - assert!(skipped_zero_and_negative.is_finite()); assert_eq!( - fit_scalar_log_rate(&[(1e154, 1e154, 1.0)]), - Err(PsychometricError::InvalidNumericInput) + recover_predetermined_lagged_latent_covariance( + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + LagClock::EventTime + ), + Ok(0.0) ); + let brownian = recover_predetermined_lagged_latent_covariance( + 0.0, + 2.0, + 0.0, + 0.0, + 0.0, + 1.0, + LagClock::EventTime, + ) + .expect("Brownian a=0"); + assert!((brownian - 2.0).abs() < 1e-12); assert_eq!( - fit_scalar_log_rate(&[(1.0, 1e-300, 1.0), (1.0, 1e-300, 2.0)]), + recover_predetermined_lagged_latent_covariance( + f64::NAN, + 2.0, + 0.0, + 0.0, + -0.5, + 1.0, + LagClock::EventTime + ), Err(PsychometricError::InvalidNumericInput) ); - } - - #[test] - fn one_sided_residual_overflow_and_nonfinite_interval_fail_closed() { assert_eq!( - recover_within_residual_event_time_log_rate( - &[ - clustered(1, 0.0, -f64::MAX), - clustered(1, 1.0, -f64::MAX), - clustered(1, 2.0, -f64::MAX), - clustered(1, 3.0, f64::MAX), - clustered(2, 0.0, 1.0), - clustered(2, 1.0, 0.8), - ], + recover_predetermined_lagged_latent_covariance( + f64::MAX, + f64::MAX, + 0.0, + 0.0, + -0.5, + 1.0, LagClock::EventTime ), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_within_residual_event_time_log_rate( - &[ - clustered(1, f64::MAX, 1.0), - clustered(1, -f64::MAX, 0.5), - clustered(2, 0.0, 1.0), - clustered(2, 1.0, 0.5), - ], + recover_predetermined_lagged_latent_covariance( + f64::MAX, + 0.0, + 1.0, + f64::MAX, + -1.0, + 1.0, LagClock::EventTime ), - Err(PsychometricError::NonPositiveInterval) - ); - } - - #[test] - fn manifest_observed_variance_recovers_driver_equation_five() { - let loading = 2.0_f64; - let latent = 0.4_f64; - let measurement_error = 0.1_f64; - let recovered = - recover_manifest_observed_variance(loading, latent, measurement_error).expect("eq5"); - let expected = (loading * latent) * loading + measurement_error; - assert!((recovered - expected).abs() < 1e-15); - assert!((recovered - 1.7).abs() < 1e-15); - assert!((measurement_error - recovered).abs() > 1e-3); - assert!((latent - recovered).abs() > 1e-3); - assert_eq!( - refuse_measurement_error_as_observed_variance(measurement_error, recovered), - Err(PsychometricError::MeasurementErrorIsNotObservedVariance) - ); - assert_eq!( - refuse_latent_variance_as_observed_variance(latent, recovered), - Err(PsychometricError::LatentVarianceIsNotObservedVariance) - ); - assert_eq!( - recover_manifest_observed_variance(0.0, latent, measurement_error), - Ok(measurement_error) - ); - assert_eq!( - recover_manifest_observed_variance(loading, 0.0, measurement_error), - Ok(measurement_error) - ); - assert_eq!( - recover_manifest_observed_variance(loading, latent, 0.0), - Ok(1.6) + Err(PsychometricError::InvalidNumericInput) ); - // Do not form λ² first: (1e308)² overflows; (λ p) λ is 1e308. - let scaled = recover_manifest_observed_variance(1e308, 1e-308, 0.0).expect("scale"); - assert!((scaled - 1e308).abs() / 1e308 < 1e-15); - assert!(!(1e308_f64 * 1e308_f64).is_finite()); } #[test] - fn manifest_trait_plus_state_observed_variance_recovers_driver_equation_five() { + #[allow(clippy::too_many_lines)] + fn predetermined_lagged_observed_covariance_recovers_driver_equation_five() { + // Driver et al. (2017, Eq. 5 of lagged predetermined T0VAR): + // λ²(trait + e^{a Δt} p_0 + (B / a)² v) + ψ. + let printed_effect = -0.225_f64; + let printed_asym = -1.673_f64; + let log_rate = -printed_effect / printed_asym; + let trait_variance = 1.0_f64; + let initial_latent_variance = 2.0_f64; + let diffusion = 0.4_f64; let loading = 2.0_f64; - let latent = 0.4_f64; - let measurement_error = 0.1_f64; - let manifest_trait = 0.5_f64; - let recovered = recover_manifest_trait_plus_state_observed_variance( + let measurement_error = 0.5_f64; + let manifest_trait = 0.1_f64; + let event_delta = 1.0_f64; + let recovered = recover_predetermined_lagged_observed_covariance( loading, - latent, + trait_variance, + initial_latent_variance, + printed_effect, + 1.0, + log_rate, + event_delta, + manifest_trait, + LagClock::EventTime, + ) + .expect("eq5-lagged-predetermined-T0VAR"); + let latent = recover_predetermined_lagged_latent_covariance( + trait_variance, + initial_latent_variance, + printed_effect, + 1.0, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("predetermined lagged T0VAR"); + let expected = recover_manifest_lagged_observed_covariance(loading, latent, manifest_trait) + .expect("λ²c+ψ"); + assert!((recovered - expected).abs() < 1e-12); + let stationary_lagged = recover_stationary_lagged_observed_covariance( + loading, + trait_variance, + diffusion, + printed_effect, + 1.0, + log_rate, + event_delta, + manifest_trait, + LagClock::EventTime, + ) + .expect("eq5-lagged-stationary-T0VAR"); + assert!((recovered - stationary_lagged).abs() > 1e-3); + let later = recover_predetermined_later_observed_variance( + loading, + trait_variance, + initial_latent_variance, + diffusion, + printed_effect, + 1.0, + log_rate, + event_delta, measurement_error, manifest_trait, + LagClock::EventTime, ) - .expect("eq5-trait"); - let expected = (loading * latent) * loading + measurement_error + manifest_trait; - assert!((recovered - expected).abs() < 1e-15); - assert!((recovered - 2.2).abs() < 1e-15); - let without_trait = - recover_manifest_observed_variance(loading, latent, measurement_error).expect("psi0"); - assert_eq!( - recover_manifest_trait_plus_state_observed_variance( - loading, - latent, - measurement_error, - 0.0 - ), - Ok(without_trait) - ); - assert!((without_trait - recovered).abs() > 1e-3); + .expect("eq5-later-predetermined-T0VAR"); + assert!((recovered - later).abs() > 1e-3); + assert!((recovered - measurement_error).abs() > 1e-3); + assert!((recovered - latent).abs() > 1e-3); assert_eq!( - refuse_manifest_trait_variance_as_measurement_error(manifest_trait, measurement_error), - Err(PsychometricError::ManifestTraitVarianceIsNotMeasurementError) + recover_predetermined_lagged_observed_covariance( + 0.0, + trait_variance, + initial_latent_variance, + printed_effect, + 1.0, + log_rate, + event_delta, + manifest_trait, + LagClock::EventTime, + ), + Ok(manifest_trait) ); - // Zero loading: Var(y) = θ + ψ, not ψ stuffed as Θ. assert_eq!( - recover_manifest_trait_plus_state_observed_variance( + recover_predetermined_lagged_observed_covariance( + loading, 0.0, - latent, - measurement_error, - manifest_trait + 0.0, + 0.0, + 1.0, + 0.0, + event_delta, + 0.0, + LagClock::EventTime, ), - Ok(measurement_error + manifest_trait) + Ok(0.0) ); - // TRAITVAR is latent and scaled by λ²; MANIFESTTRAITVAR is not. - let latent_trait_as_state = - recover_manifest_observed_variance(loading, latent + manifest_trait, measurement_error) - .expect("traitvar"); - assert!((latent_trait_as_state - recovered).abs() > 1e-3); - // Do not form λ² first, then add ψ. - let scaled = recover_manifest_trait_plus_state_observed_variance(1e308, 1e-308, 0.0, 1.0) - .expect("scale-psi"); - assert!((scaled - 1e308).abs() / 1e308 < 1e-15); } #[test] - fn manifest_trait_plus_state_observed_variance_invalid_inputs_fail_closed() { + fn predetermined_lagged_observed_covariance_is_not_manifest_later_or_stationary() { + let trait_variance = 1.0_f64; + let initial_latent_variance = 2.0_f64; + let diffusion = 0.4_f64; + let log_rate = -0.134_488_942_f64; + let loading = 2.0_f64; + let measurement_error = 0.5_f64; + let event_delta = 1.0_f64; + let recovered = recover_predetermined_lagged_observed_covariance( + loading, + trait_variance, + initial_latent_variance, + -0.225, + 1.0, + log_rate, + event_delta, + 0.1, + LagClock::EventTime, + ) + .expect("eq5-lagged-predetermined-T0VAR"); + let latent = recover_predetermined_lagged_latent_covariance( + trait_variance, + initial_latent_variance, + -0.225, + 1.0, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("predetermined lagged T0VAR"); + let later = recover_predetermined_later_observed_variance( + loading, + trait_variance, + initial_latent_variance, + diffusion, + -0.225, + 1.0, + log_rate, + event_delta, + measurement_error, + 0.1, + LagClock::EventTime, + ) + .expect("eq5-later-predetermined-T0VAR"); + let stationary_lagged = recover_stationary_lagged_observed_covariance( + loading, + trait_variance, + diffusion, + -0.225, + 1.0, + log_rate, + event_delta, + 0.1, + LagClock::EventTime, + ) + .expect("eq5-lagged-stationary-T0VAR"); assert_eq!( - recover_manifest_trait_plus_state_observed_variance(2.0, 0.4, 0.1, -0.1), - Err(PsychometricError::InvalidNumericInput) + refuse_predetermined_lagged_latent_covariance_as_observed_covariance(latent, recovered), + Err(PsychometricError::PredeterminedLaggedLatentCovarianceIsNotObservedCovariance) ); assert_eq!( - recover_manifest_trait_plus_state_observed_variance(2.0, 0.4, 0.1, f64::NAN), - Err(PsychometricError::InvalidNumericInput) + refuse_measurement_error_as_predetermined_lagged_observed_covariance( + measurement_error, + recovered + ), + Err(PsychometricError::MeasurementErrorIsNotPredeterminedLaggedObservedCovariance) ); assert_eq!( - recover_manifest_trait_plus_state_observed_variance(1e308, 1.0, 0.0, 0.3), - Err(PsychometricError::InvalidNumericInput) + refuse_predetermined_later_observed_variance_as_predetermined_lagged_observed_covariance( + later, + recovered + ), + Err( + PsychometricError::PredeterminedLaterObservedVarianceIsNotPredeterminedLaggedObservedCovariance + ) ); assert_eq!( - recover_manifest_trait_plus_state_observed_variance(1e308, 1e-308, 1e308, 1e308), - Err(PsychometricError::InvalidNumericInput) + refuse_stationary_lagged_observed_covariance_as_predetermined_lagged_observed_covariance( + stationary_lagged, + recovered + ), + Err( + PsychometricError::StationaryLaggedObservedCovarianceIsNotPredeterminedLaggedObservedCovariance + ) ); } #[test] - fn manifest_observed_variance_invalid_inputs_fail_closed() { - assert_eq!( - recover_manifest_observed_variance(f64::NAN, 0.4, 0.1), - Err(PsychometricError::InvalidNumericInput) - ); + fn predetermined_lagged_observed_covariance_invalid_inputs_fail_closed() { assert_eq!( - recover_manifest_observed_variance(2.0, -0.1, 0.1), - Err(PsychometricError::InvalidNumericInput) + recover_predetermined_lagged_observed_covariance( + 2.0, + 1.0, + 2.0, + -0.225, + 1.0, + -0.13, + 1.0, + 0.1, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) ); assert_eq!( - recover_manifest_observed_variance(2.0, 0.4, -0.1), - Err(PsychometricError::InvalidNumericInput) + recover_predetermined_lagged_observed_covariance( + 2.0, + 1.0, + 2.0, + -0.225, + 1.0, + -0.13, + 0.0, + 0.1, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) ); assert_eq!( - recover_manifest_observed_variance(2.0, f64::NAN, 0.1), - Err(PsychometricError::InvalidNumericInput) + recover_predetermined_lagged_observed_covariance( + 2.0, + 0.0, + 0.0, + -0.225, + 1.0, + 0.5, + 1.0, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) ); assert_eq!( - recover_manifest_observed_variance(2.0, 0.4, f64::NAN), - Err(PsychometricError::InvalidNumericInput) + recover_predetermined_lagged_observed_covariance( + 2.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.1, + LagClock::EventTime + ), + Ok(0.1) ); + let brownian = recover_predetermined_lagged_observed_covariance( + 1.0, + 0.0, + 2.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + LagClock::EventTime, + ) + .expect("Brownian a=0"); + assert!((brownian - 2.0).abs() < 1e-12); assert_eq!( - recover_manifest_observed_variance(1e308, 1.0, 0.0), + recover_predetermined_lagged_observed_covariance( + f64::NAN, + 1.0, + 2.0, + 0.0, + 0.0, + -0.5, + 1.0, + 0.0, + LagClock::EventTime + ), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_manifest_observed_variance(1e308, 1.0, 1e308), + recover_predetermined_lagged_observed_covariance( + 2.0, + f64::MAX, + f64::MAX, + 0.0, + 0.0, + -0.5, + 1.0, + 0.0, + LagClock::EventTime + ), Err(PsychometricError::InvalidNumericInput) ); } #[test] - fn manifest_lagged_observed_covariance_recovers_driver_equation_five() { + fn discrete_observed_mean_with_impulse_recovers_driver_equation_five() { let loading = 2.0_f64; - let lagged = 0.4_f64; - let manifest_trait = 0.5_f64; - let recovered = - recover_manifest_lagged_observed_covariance(loading, lagged, manifest_trait) - .expect("eq5-lag"); - let expected = (loading * lagged) * loading + manifest_trait; + let drift = -0.5_f64; + let delta = 2.0_f64; + let effect = 0.4_f64; + let predictor = 3.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let manifest_mean = 0.5_f64; + let recovered = recover_discrete_observed_mean_with_impulse( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-impulse-mean"); + let composed = recover_discrete_latent_mean_with_impulse( + initial, + drift, + intercept, + effect, + predictor, + delta, + LagClock::EventTime, + ) + .expect("mx"); + let expected = manifest_mean + loading * composed; assert!((recovered - expected).abs() < 1e-15); - assert!((recovered - 2.1).abs() < 1e-15); - assert_eq!( - recover_manifest_lagged_observed_covariance(loading, lagged, 0.0), - Ok(1.6) - ); - assert_eq!( - recover_manifest_lagged_observed_covariance(0.0, lagged, manifest_trait), - Ok(manifest_trait) - ); - assert_eq!( - recover_manifest_lagged_observed_covariance(loading, 0.0, manifest_trait), - Ok(manifest_trait) - ); + let evolved_observed = recover_discrete_observed_mean( + loading, + initial, + drift, + intercept, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq3-eq5-mean"); + assert!((evolved_observed - recovered).abs() > 1e-3); + let carried_observed = recover_discrete_observed_mean_with_impulse_carry( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + 1.0, + LagClock::EventTime, + ) + .expect("eq5-carry-mean"); + assert!((carried_observed - recovered).abs() > 1e-3); assert_eq!( - refuse_latent_lagged_covariance_as_observed_covariance(lagged, recovered), - Err(PsychometricError::LatentLaggedCovarianceIsNotObservedCovariance) + recover_discrete_observed_mean_with_impulse( + 0.0, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime + ), + Ok(manifest_mean) ); assert_eq!( - refuse_measurement_error_as_lagged_observed_covariance(0.1, recovered), - Err(PsychometricError::MeasurementErrorIsNotLaggedObservedCovariance) + recover_discrete_observed_mean_with_impulse( + loading, + initial, + drift, + intercept, + effect, + predictor, + 0.0, + delta, + LagClock::EventTime + ), + Ok(loading * composed) ); - let scaled = - recover_manifest_lagged_observed_covariance(1e308, 1e-308, 0.0).expect("scale"); - assert!((scaled - 1e308).abs() / 1e308 < 1e-15); - assert!(!(1e308_f64 * 1e308_f64).is_finite()); } #[test] - fn manifest_lagged_observed_covariance_invalid_inputs_fail_closed() { - assert_eq!( - recover_manifest_lagged_observed_covariance(f64::NAN, 0.4, 0.0), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_manifest_lagged_observed_covariance(2.0, -0.1, 0.0), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_manifest_lagged_observed_covariance(2.0, 0.4, -0.1), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_manifest_lagged_observed_covariance(1e308, 1.0, 0.0), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_manifest_lagged_observed_covariance(1e308, 1e-308, 1e308), - Err(PsychometricError::InvalidNumericInput) - ); + fn discrete_observed_mean_with_impulse_is_not_evolved_or_zero_impulse() { + let loading = 2.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let effect = 0.4_f64; + let predictor = 3.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let manifest_mean = 0.5_f64; + let recovered = recover_discrete_observed_mean_with_impulse( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-impulse-mean"); + let evolved_observed = recover_discrete_observed_mean( + loading, + initial, + drift, + intercept, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq3-eq5-mean"); + let zero_impulse = recover_discrete_observed_mean_with_impulse( + loading, + initial, + drift, + intercept, + 0.0, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("zero-impulse"); + assert!((zero_impulse - evolved_observed).abs() < 1e-15); + assert!((recovered - evolved_observed).abs() > 1e-3); } #[test] - fn manifest_observed_mean_recovers_driver_equation_five() { + fn discrete_observed_mean_with_impulse_refuses_evolved_mean_and_overflow() { let loading = 2.0_f64; - let latent_mean = 0.4_f64; - let manifest_mean = 0.5_f64; - let recovered = - recover_manifest_observed_mean(loading, latent_mean, manifest_mean).expect("eq5-mean"); - let expected = loading * latent_mean + manifest_mean; - assert!((recovered - expected).abs() < 1e-15); - assert!((recovered - 1.3).abs() < 1e-15); - assert_eq!( - recover_manifest_observed_mean(loading, latent_mean, 0.0), - Ok(0.8) - ); - assert_eq!( - recover_manifest_observed_mean(0.0, latent_mean, manifest_mean), - Ok(manifest_mean) - ); + let recovered = recover_discrete_observed_mean_with_impulse( + loading, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 2.0, + LagClock::EventTime, + ) + .expect("eq5-impulse-mean"); + let composed = recover_discrete_latent_mean_with_impulse( + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 2.0, + LagClock::EventTime, + ) + .expect("mx"); + let evolved_observed = + recover_discrete_observed_mean(loading, 1.0, -0.5, 0.3, 0.5, 2.0, LagClock::EventTime) + .expect("eq3-eq5-mean"); + let carried_observed = recover_discrete_observed_mean_with_impulse_carry( + loading, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 2.0, + 1.0, + LagClock::EventTime, + ) + .expect("eq5-carry-mean"); assert_eq!( - recover_manifest_observed_mean(loading, 0.0, manifest_mean), - Ok(manifest_mean) + refuse_evolved_observed_mean_as_impulse_observed_mean(evolved_observed, recovered), + Err(PsychometricError::EvolvedObservedMeanIsNotImpulseObservedMean) ); - assert_eq!(recover_manifest_observed_mean(-2.0, 0.5, 1.0), Ok(0.0)); assert_eq!( - refuse_manifest_means_as_observed_mean(manifest_mean, recovered), - Err(PsychometricError::ManifestMeansIsNotObservedMean) + refuse_impulse_observed_mean_as_impulse_carry_observed_mean( + recovered, + carried_observed + ), + Err(PsychometricError::ImpulseObservedMeanIsNotImpulseCarryObservedMean) ); assert_eq!( - refuse_latent_mean_as_observed_mean(latent_mean, recovered), + refuse_latent_mean_as_observed_mean(composed, recovered), Err(PsychometricError::LatentMeanIsNotObservedMean) ); assert_eq!( - refuse_continuous_intercept_as_manifest_means(0.3, manifest_mean), - Err(PsychometricError::ContinuousInterceptIsNotManifestMeans) + refuse_manifest_means_as_observed_mean(0.5, recovered), + Err(PsychometricError::ManifestMeansIsNotObservedMean) ); - let scaled = recover_manifest_observed_mean(1e308, 1e-308, 0.0).expect("scale"); + let scaled = recover_discrete_observed_mean_with_impulse( + 1e308, + 1e-308, + 0.0, + 0.0, + 0.0, + 3.0, + 0.0, + 1.0, + LagClock::EventTime, + ) + .expect("scale"); assert!((scaled - 1.0).abs() < 1e-15); - let finite_loaded = recover_manifest_observed_mean(1e308, 1.0, 0.0).expect("lambda-mu"); + let finite_loaded = recover_discrete_observed_mean_with_impulse( + 1e308, + 1.0, + 0.0, + 0.0, + 0.0, + 3.0, + 0.0, + 1.0, + LagClock::EventTime, + ) + .expect("lambda-mu"); assert!((finite_loaded - 1e308).abs() / 1e308 < 1e-15); - assert!(!(1e308_f64 * 1e308_f64).is_finite()); } #[test] - fn manifest_observed_mean_invalid_inputs_fail_closed() { + fn discrete_observed_mean_with_impulse_invalid_inputs_fail_closed() { assert_eq!( - recover_manifest_observed_mean(f64::NAN, 0.4, 0.0), + recover_discrete_observed_mean_with_impulse( + f64::NAN, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 2.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_observed_mean_with_impulse( + 1e308, + 2.0, + 0.0, + 0.0, + 0.0, + 3.0, + 0.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_observed_mean_with_impulse( + 1.0, + 1.0, + 710.0, + 0.0, + 0.0, + 3.0, + 0.5, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_observed_mean_with_impulse( + 1e308, + 0.0, + 0.0, + 0.0, + 1e308, + 1.0, + 0.0, + 1.0, + LagClock::EventTime + ), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_manifest_observed_mean(2.0, f64::NAN, 0.0), - Err(PsychometricError::InvalidNumericInput) + recover_discrete_observed_mean_with_impulse( + 2.0, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) ); assert_eq!( - recover_manifest_observed_mean(2.0, 0.4, f64::NAN), - Err(PsychometricError::InvalidNumericInput) + recover_discrete_observed_mean_with_impulse( + 2.0, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 2.0, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) ); + } + + #[test] + fn time_independent_predictor_recovers_driver_equation_three_second_summand() { + let effect = 0.4_f64; + let predictor = 3.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let increment = recover_discrete_time_independent_predictor_effect( + effect, + predictor, + drift, + delta, + LagClock::EventTime, + ) + .expect("tipred"); + let expected = + recover_discrete_constant_predictor_effect(1.2, drift, delta, LagClock::EventTime) + .expect("bz-map"); + assert!((increment - expected).abs() < 1e-15); assert_eq!( - recover_manifest_observed_mean(1e308, 2.0, 0.0), - Err(PsychometricError::InvalidNumericInput) + recover_discrete_time_independent_predictor_effect( + 0.0, + predictor, + drift, + delta, + LagClock::EventTime + ), + Ok(0.0) ); assert_eq!( - recover_manifest_observed_mean(1.0, 1e308, 1e308), - Err(PsychometricError::InvalidNumericInput) + recover_discrete_time_independent_predictor_effect( + effect, + 0.0, + drift, + delta, + LagClock::EventTime + ), + Ok(0.0) ); - assert_eq!(recover_manifest_observed_mean(0.0, 1e308, 0.5), Ok(0.5)); - assert_eq!(recover_manifest_observed_mean(1e308, 0.0, 0.5), Ok(0.5)); + let zero_drift = recover_discrete_time_independent_predictor_effect( + effect, + predictor, + 0.0, + delta, + LagClock::EventTime, + ) + .expect("zero-drift"); + assert!((zero_drift - 2.4).abs() < 1e-15); + let intercept_effect = + recover_discrete_continuous_intercept_effect(effect, drift, delta, LagClock::EventTime) + .expect("cint"); + let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("tdpred"); + let equation_fourteen = recover_discrete_time_varying_predictor_effect( + effect, + delta, + delta, + delta, + LagClock::EventTime, + ) + .expect("eq14"); + assert!((increment - intercept_effect).abs() > 1e-3); + assert!((increment - impulse).abs() > 1e-3); + assert!((increment - equation_fourteen).abs() > 1e-3); + assert!((increment - effect).abs() > 1e-3); } #[test] - fn discrete_latent_mean_recovers_driver_equation_three() { + fn time_independent_predictor_composes_evolved_mean_and_keeps_scale() { + let effect = 0.4_f64; + let predictor = 3.0_f64; let drift = -0.5_f64; let delta = 2.0_f64; + let increment = recover_discrete_time_independent_predictor_effect( + effect, + predictor, + drift, + delta, + LagClock::EventTime, + ) + .expect("tipred"); let initial = 1.0_f64; let intercept = 0.3_f64; - let recovered = - recover_discrete_latent_mean(initial, drift, intercept, delta, LagClock::EventTime) - .expect("eq3-mean"); - let expected = - (drift * delta).exp() * initial + intercept * ((drift * delta).exp_m1() / drift); - assert!((recovered - expected).abs() < 1e-15); - let increment = recover_discrete_continuous_intercept_effect( - intercept, + let composed = recover_discrete_latent_mean_with_time_independent_predictor( + initial, drift, + intercept, + effect, + predictor, delta, LagClock::EventTime, ) - .expect("cint"); - assert!((increment - intercept * ((drift * delta).exp_m1() / drift)).abs() < 1e-15); - assert_eq!( - recover_discrete_latent_mean(0.0, drift, intercept, delta, LagClock::EventTime), - Ok(increment) - ); - assert_eq!( - recover_discrete_latent_mean(initial, drift, 0.0, delta, LagClock::EventTime), - Ok((drift * delta).exp() * initial) - ); - assert_eq!( - recover_discrete_latent_mean(initial, 0.0, intercept, delta, LagClock::EventTime), - Ok(initial + intercept * delta) - ); + .expect("eq3-tipred"); + let evolved = + recover_discrete_latent_mean(initial, drift, intercept, delta, LagClock::EventTime) + .expect("mu-t"); + assert!((composed - (evolved + increment)).abs() < 1e-15); assert_eq!( - recover_discrete_continuous_intercept_effect( + recover_discrete_latent_mean_with_time_independent_predictor( + initial, + drift, intercept, 0.0, + predictor, delta, LagClock::EventTime ), - Ok(intercept * delta) - ); - assert_eq!( - recover_discrete_continuous_intercept_effect(0.0, 0.0, delta, LagClock::EventTime), - Ok(0.0) - ); - assert_eq!( - refuse_initial_latent_mean_as_evolved_mean(initial, recovered), - Err(PsychometricError::InitialLatentMeanIsNotEvolvedMean) + Ok(evolved) ); assert_eq!( - refuse_continuous_intercept_as_discrete_mean_increment(intercept, increment), - Err(PsychometricError::ContinuousInterceptIsNotDiscreteMeanIncrement) + recover_discrete_latent_mean_with_time_independent_predictor( + 0.0, + drift, + 0.0, + effect, + predictor, + delta, + LagClock::EventTime + ), + Ok(increment) ); + let scaled = recover_discrete_time_independent_predictor_effect( + 1e308, + 1e-308, + 0.0, + 1.0, + LagClock::EventTime, + ) + .expect("scale"); + assert!((scaled - 1.0).abs() < 1e-15); + } + + #[test] + fn time_independent_predictor_refuses_cint_impulse_equation_fourteen_and_coefficient() { + let effect = 0.4_f64; + let predictor = 3.0_f64; + let increment = recover_discrete_time_independent_predictor_effect( + effect, + predictor, + -0.5, + 2.0, + LagClock::EventTime, + ) + .expect("tipred"); + let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("tdpred"); + let equation_fourteen = recover_discrete_time_varying_predictor_effect( + effect, + 2.0, + 2.0, + 2.0, + LagClock::EventTime, + ) + .expect("eq14"); assert_eq!( - refuse_continuous_intercept_as_initial_latent_mean(intercept, initial), - Err(PsychometricError::ContinuousInterceptIsNotInitialLatentMean) + refuse_time_independent_effect_as_continuous_intercept(increment, effect), + Err(PsychometricError::TimeIndependentEffectIsNotContinuousIntercept) ); - let equilibrium = - recover_discrete_latent_mean(initial, -1e308, 1.0, 2.0, LagClock::EventTime) - .expect("eq3-equilibrium"); - let equilibrium_expected = -(1.0 / -1e308); - assert!((equilibrium - equilibrium_expected).abs() / 1e-308 < 1e-12); assert_eq!( - recover_discrete_latent_mean(1e308, 1.0, 0.0, 1.0, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) + refuse_time_independent_effect_as_time_dependent_impulse(increment, impulse), + Err(PsychometricError::TimeIndependentEffectIsNotTimeDependentImpulse) ); assert_eq!( - recover_discrete_latent_mean(0.0, 1e308, 0.0, 2.0, LagClock::EventTime), - Ok(0.0) + refuse_time_independent_effect_as_time_varying_discrete_effect( + increment, + equation_fourteen + ), + Err(PsychometricError::TimeIndependentEffectIsNotTimeVaryingDiscreteEffect) ); - // CINT = 0 so the increment path stays finite; exp(a Δt) then - // overflows and the carried T0MEANS term fails closed. assert_eq!( - recover_discrete_latent_mean(1.0, 710.0, 0.0, 1.0, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) + refuse_time_independent_coefficient_as_discrete_effect(effect, increment), + Err(PsychometricError::TimeIndependentCoefficientIsNotDiscreteEffect) ); - assert!(!(710.0_f64.exp()).is_finite()); } #[test] - fn discrete_latent_mean_invalid_inputs_fail_closed() { + fn time_independent_predictor_invalid_inputs_fail_closed() { assert_eq!( - recover_discrete_latent_mean(f64::NAN, -0.5, 0.3, 2.0, LagClock::EventTime), + recover_discrete_time_independent_predictor_effect( + f64::NAN, + 1.0, + -0.5, + 2.0, + LagClock::EventTime + ), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_latent_mean(1.0, f64::NAN, 0.3, 2.0, LagClock::EventTime), + recover_discrete_time_independent_predictor_effect( + 1.0, + f64::INFINITY, + -0.5, + 2.0, + LagClock::EventTime + ), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_latent_mean(1.0, -0.5, f64::NAN, 2.0, LagClock::EventTime), + recover_discrete_time_independent_predictor_effect( + 1e308, + 2.0, + -0.5, + 2.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_time_independent_predictor_effect( + 1e308, + 1.0, + 0.0, + 2.0, + LagClock::EventTime + ), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_latent_mean(1.0, -0.5, 0.3, 0.0, LagClock::EventTime), + recover_discrete_time_independent_predictor_effect( + 0.4, + 3.0, + -0.5, + 0.0, + LagClock::EventTime + ), Err(PsychometricError::NonPositiveInterval) ); assert_eq!( - recover_discrete_latent_mean(1.0, -0.5, 0.3, 2.0, LagClock::SystemTime), + recover_discrete_time_independent_predictor_effect( + 0.4, + 3.0, + -0.5, + 2.0, + LagClock::SystemTime + ), Err(PsychometricError::EventTimeRequired) ); assert_eq!( - recover_discrete_continuous_intercept_effect(1.0, 1e308, 2.0, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_discrete_latent_mean(1.0, 1e308, 1.0, 2.0, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_discrete_latent_mean(1e308, 0.0, 1e308, 2.0, LagClock::EventTime), + recover_discrete_latent_mean_with_time_independent_predictor( + 1e308, + 0.0, + 0.0, + 1.0, + 1e308, + 1.0, + LagClock::EventTime + ), Err(PsychometricError::InvalidNumericInput) ); + // Latent mean is finite; Bz overflows. That `?` is not the sum overflow. assert_eq!( - recover_discrete_latent_mean(1e308, 0.0, 1e308, 1.0, LagClock::EventTime), + recover_discrete_latent_mean_with_time_independent_predictor( + 1.0, + -0.5, + 0.3, + 1e308, + 2.0, + 2.0, + LagClock::EventTime + ), Err(PsychometricError::InvalidNumericInput) ); - let underflow_argument = 1e-308_f64 * 1e-308_f64; - assert_eq!(underflow_argument.to_bits(), 0.0_f64.to_bits()); - let underflow = recover_discrete_latent_mean(2.0, 1e-308, 4.0, 1e-308, LagClock::EventTime) - .expect("a-delta-underflow"); - assert!((underflow - 2.0).abs() < 1e-15); } #[test] - fn discrete_observed_mean_recovers_driver_equations_three_and_five() { + fn discrete_observed_mean_with_time_independent_predictor_recovers_driver_equation_five() { let loading = 2.0_f64; let drift = -0.5_f64; let delta = 2.0_f64; + let effect = 0.4_f64; + let predictor = 3.0_f64; let initial = 1.0_f64; let intercept = 0.3_f64; let manifest_mean = 0.5_f64; - let recovered = recover_discrete_observed_mean( + let recovered = recover_discrete_observed_mean_with_time_independent_predictor( loading, initial, drift, intercept, + effect, + predictor, manifest_mean, delta, LagClock::EventTime, ) - .expect("eq3-eq5-mean"); - let evolved = - recover_discrete_latent_mean(initial, drift, intercept, delta, LagClock::EventTime) - .expect("mu-t"); - let expected = manifest_mean + loading * evolved; + .expect("eq5-tipred-mean"); + let composed = recover_discrete_latent_mean_with_time_independent_predictor( + initial, + drift, + intercept, + effect, + predictor, + delta, + LagClock::EventTime, + ) + .expect("eq3-tipred"); + let expected = manifest_mean + loading * composed; assert!((recovered - expected).abs() < 1e-15); - let first_occasion = - recover_manifest_observed_mean(loading, initial, manifest_mean).expect("t0"); - assert!((first_occasion - recovered).abs() > 1e-3); - assert_eq!( - recover_discrete_observed_mean( - 0.0, - initial, - drift, - intercept, - manifest_mean, - delta, - LagClock::EventTime - ), - Ok(manifest_mean) - ); - assert_eq!( - recover_discrete_observed_mean( - loading, - initial, - drift, - intercept, - 0.0, - delta, - LagClock::EventTime - ), - Ok(loading * evolved) - ); - let zero_evolved = recover_discrete_observed_mean( + let evolved_observed = recover_discrete_observed_mean( loading, - 0.0, - 0.0, - 0.0, + initial, + drift, + intercept, manifest_mean, delta, LagClock::EventTime, ) - .expect("zero-mu"); - assert!((zero_evolved - manifest_mean).abs() < 1e-15); - let integrator = recover_discrete_observed_mean( + .expect("eq3-eq5-mean"); + let impulse_observed = recover_discrete_observed_mean_with_impulse( loading, initial, - 0.0, + drift, intercept, + effect, + predictor, manifest_mean, delta, LagClock::EventTime, ) - .expect("a0"); - assert!( - (integrator - (manifest_mean + loading * (initial + intercept * delta))).abs() < 1e-15 - ); - let equilibrium = recover_discrete_observed_mean( + .expect("eq5-impulse-mean"); + let carried_observed = recover_discrete_observed_mean_with_impulse_carry( loading, initial, - -1e308, - 1.0, + drift, + intercept, + effect, + predictor, manifest_mean, - 2.0, + delta, + 1.0, LagClock::EventTime, ) - .expect("eq3-eq5-equilibrium"); - let equilibrium_latent = -(1.0 / -1e308); - assert!((equilibrium - (manifest_mean + loading * equilibrium_latent)).abs() < 1e-15); - } - - #[test] - fn discrete_observed_mean_refuses_first_occasion_and_overflow() { - let loading = 2.0_f64; - let recovered = - recover_discrete_observed_mean(loading, 1.0, -0.5, 0.3, 0.5, 2.0, LagClock::EventTime) - .expect("eq3-eq5-mean"); - let evolved = - recover_discrete_latent_mean(1.0, -0.5, 0.3, 2.0, LagClock::EventTime).expect("mu-t"); - let first_occasion = recover_manifest_observed_mean(loading, 1.0, 0.5).expect("t0"); - assert_eq!( - refuse_initial_observed_mean_as_evolved_observed_mean(first_occasion, recovered), - Err(PsychometricError::InitialObservedMeanIsNotEvolvedObservedMean) - ); - assert_eq!( - refuse_latent_mean_as_observed_mean(evolved, recovered), - Err(PsychometricError::LatentMeanIsNotObservedMean) - ); - assert_eq!( - refuse_manifest_means_as_observed_mean(0.5, recovered), - Err(PsychometricError::ManifestMeansIsNotObservedMean) - ); - let scaled = - recover_discrete_observed_mean(1e308, 1e-308, 0.0, 0.0, 0.0, 1.0, LagClock::EventTime) - .expect("scale"); - assert!((scaled - 1.0).abs() < 1e-15); - let finite_loaded = - recover_discrete_observed_mean(1e308, 1.0, 0.0, 0.0, 0.0, 1.0, LagClock::EventTime) - .expect("lambda-mu"); - assert!((finite_loaded - 1e308).abs() / 1e308 < 1e-15); - } - - #[test] - fn discrete_observed_mean_invalid_inputs_fail_closed() { - assert_eq!( - recover_discrete_observed_mean(f64::NAN, 1.0, -0.5, 0.3, 0.5, 2.0, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_discrete_observed_mean(2.0, 1.0, -0.5, 0.3, 0.5, 0.0, LagClock::EventTime), - Err(PsychometricError::NonPositiveInterval) - ); - assert_eq!( - recover_discrete_observed_mean(2.0, 1.0, -0.5, 0.3, 0.5, 2.0, LagClock::SystemTime), - Err(PsychometricError::EventTimeRequired) - ); - assert_eq!( - recover_discrete_observed_mean(1e308, 2.0, 0.0, 0.0, 0.0, 1.0, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) - ); + .expect("eq5-carry-mean"); + assert!((evolved_observed - recovered).abs() > 1e-3); + assert!((impulse_observed - recovered).abs() > 1e-3); + assert!((carried_observed - recovered).abs() > 1e-3); assert_eq!( - recover_discrete_observed_mean(1.0, 1.0, 710.0, 0.0, 0.5, 1.0, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) + recover_discrete_observed_mean_with_time_independent_predictor( + 0.0, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime + ), + Ok(manifest_mean) ); } #[test] - fn time_dependent_impulse_recovers_driver_equation_three_fourth_summand() { - let effect = 0.4_f64; - let predictor = 3.0_f64; - let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("tdpred"); - assert!((impulse - 1.2).abs() < 1e-15); - assert_eq!( - recover_time_dependent_predictor_impulse(0.0, predictor), - Ok(0.0) - ); - assert_eq!( - recover_time_dependent_predictor_impulse(effect, 0.0), - Ok(0.0) - ); + fn discrete_observed_mean_with_time_independent_predictor_is_not_evolved_or_zero_increment() { + let loading = 2.0_f64; let drift = -0.5_f64; let delta = 2.0_f64; + let effect = 0.4_f64; + let predictor = 3.0_f64; let initial = 1.0_f64; let intercept = 0.3_f64; - let composed = recover_discrete_latent_mean_with_impulse( + let manifest_mean = 0.5_f64; + let recovered = recover_discrete_observed_mean_with_time_independent_predictor( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-tipred-mean"); + let evolved_observed = recover_discrete_observed_mean( + loading, + initial, + drift, + intercept, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq3-eq5-mean"); + let zero_increment = recover_discrete_observed_mean_with_time_independent_predictor( + loading, initial, drift, intercept, - effect, + 0.0, predictor, + manifest_mean, delta, LagClock::EventTime, ) - .expect("eq3-impulse"); - let evolved = - recover_discrete_latent_mean(initial, drift, intercept, delta, LagClock::EventTime) - .expect("mu-t"); - assert!((composed - (evolved + impulse)).abs() < 1e-15); - assert_eq!( - recover_discrete_latent_mean_with_impulse( - initial, - drift, - intercept, - 0.0, - predictor, - delta, - LagClock::EventTime - ), - Ok(evolved) - ); - let intercept_effect = - recover_discrete_continuous_intercept_effect(effect, drift, delta, LagClock::EventTime) - .expect("cint"); - assert!((impulse - intercept_effect).abs() > 1e-3); - let equation_fourteen = recover_discrete_time_varying_predictor_effect( - effect, - delta, - delta, - delta, - LagClock::EventTime, - ) - .expect("eq14"); - assert!((impulse - equation_fourteen).abs() > 1e-3); + .expect("zero-increment"); + assert!((zero_increment - evolved_observed).abs() < 1e-15); + assert!((recovered - evolved_observed).abs() > 1e-3); } #[test] - fn time_dependent_impulse_refuses_cint_tipred_and_equation_fourteen() { - let effect = 0.4_f64; - let predictor = 2.0_f64; - let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("tdpred"); - let intercept_effect = - recover_discrete_continuous_intercept_effect(effect, -0.5, 2.0, LagClock::EventTime) - .expect("cint"); - let equation_fourteen = recover_discrete_time_varying_predictor_effect( - effect, + fn discrete_observed_mean_with_time_independent_predictor_refuses_evolved_mean_and_overflow() { + let loading = 2.0_f64; + let recovered = recover_discrete_observed_mean_with_time_independent_predictor( + loading, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, 2.0, + LagClock::EventTime, + ) + .expect("eq5-tipred-mean"); + let evolved_observed = + recover_discrete_observed_mean(loading, 1.0, -0.5, 0.3, 0.5, 2.0, LagClock::EventTime) + .expect("eq3-eq5-mean"); + let impulse_observed = recover_discrete_observed_mean_with_impulse( + loading, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, 2.0, + LagClock::EventTime, + ) + .expect("eq5-impulse-mean"); + let carried_observed = recover_discrete_observed_mean_with_impulse_carry( + loading, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, 2.0, + 1.0, LagClock::EventTime, ) - .expect("eq14"); + .expect("eq5-carry-mean"); assert_eq!( - refuse_time_dependent_impulse_as_continuous_intercept(impulse, effect), - Err(PsychometricError::TimeDependentImpulseIsNotContinuousIntercept) + refuse_evolved_observed_mean_as_time_independent_observed_mean( + evolved_observed, + recovered + ), + Err(PsychometricError::EvolvedObservedMeanIsNotTimeIndependentObservedMean) ); assert_eq!( - refuse_time_dependent_impulse_as_time_independent_effect(impulse, intercept_effect), - Err(PsychometricError::TimeDependentImpulseIsNotTimeIndependentEffect) + refuse_impulse_observed_mean_as_time_independent_observed_mean( + impulse_observed, + recovered + ), + Err(PsychometricError::ImpulseObservedMeanIsNotTimeIndependentObservedMean) ); assert_eq!( - refuse_time_dependent_impulse_as_time_varying_discrete_effect( - impulse, - equation_fourteen + refuse_impulse_carry_observed_mean_as_time_independent_observed_mean( + carried_observed, + recovered ), - Err(PsychometricError::TimeDependentImpulseIsNotTimeVaryingDiscreteEffect) + Err(PsychometricError::ImpulseCarryObservedMeanIsNotTimeIndependentObservedMean) ); } #[test] - fn time_dependent_impulse_invalid_inputs_fail_closed() { - assert_eq!( - recover_time_dependent_predictor_impulse(f64::NAN, 1.0), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_time_dependent_predictor_impulse(1.0, f64::INFINITY), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_time_dependent_predictor_impulse(1e308, 2.0), - Err(PsychometricError::InvalidNumericInput) - ); + fn discrete_observed_mean_with_time_independent_predictor_invalid_inputs_fail_closed() { + let scaled = recover_discrete_observed_mean_with_time_independent_predictor( + 1e308, + 1e-308, + 0.0, + 0.0, + 0.0, + 3.0, + 0.0, + 1.0, + LagClock::EventTime, + ) + .expect("scale"); + assert!((scaled - 1.0).abs() < 1e-15); + let finite_loaded = recover_discrete_observed_mean_with_time_independent_predictor( + 1e308, + 0.0, + 0.0, + 0.0, + 0.0, + 3.0, + 0.0, + 1.0, + LagClock::EventTime, + ) + .expect("lambda-mu0"); + assert!((finite_loaded - 0.0).abs() < 1e-15); assert_eq!( - recover_discrete_latent_mean_with_impulse( - 1.0, - -0.5, - 0.3, - 0.4, + recover_discrete_observed_mean_with_time_independent_predictor( + 1e308, 2.0, 0.0, + 0.0, + 0.0, + 3.0, + 0.0, + 1.0, LagClock::EventTime ), - Err(PsychometricError::NonPositiveInterval) + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_latent_mean_with_impulse( + recover_discrete_observed_mean_with_time_independent_predictor( + 2.0, 1.0, -0.5, 0.3, 0.4, - 2.0, + 3.0, + 0.5, 2.0, LagClock::SystemTime ), - Err(PsychometricError::EventTimeRequired) - ); - assert_eq!( - recover_discrete_latent_mean_with_impulse( - 1.0, - -0.5, - 0.3, - 1e308, - 2.0, - 2.0, - LagClock::EventTime - ), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_discrete_latent_mean_with_impulse( - 1e308, - 0.0, - 0.0, - 1e308, - 1.0, - 1.0, - LagClock::EventTime - ), - Err(PsychometricError::InvalidNumericInput) - ); - } - - #[test] - fn level_change_continuous_intercept_recovers_driver_section_seven_point_two() { - let effect = 0.4_f64; - let predictor = 3.0_f64; - let drift = -0.5_f64; - let intercept = recover_level_change_continuous_intercept(effect, predictor, drift) - .expect("level-change"); - let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("impulse"); - assert!((intercept - 0.6).abs() < 1e-15); - assert!((impulse - 1.2).abs() < 1e-15); - let equilibrium = intercept / (-drift); - assert!((equilibrium - impulse).abs() < 1e-15); - assert_eq!( - recover_level_change_continuous_intercept(0.0, predictor, drift), - Ok(0.0) - ); - assert_eq!( - recover_level_change_continuous_intercept(effect, 0.0, drift), - Ok(0.0) - ); - assert_eq!( - recover_level_change_continuous_intercept(effect, predictor, 0.0), - Err(PsychometricError::LevelChangeRequiresStableDrift) - ); - assert_eq!( - recover_level_change_continuous_intercept(effect, predictor, 0.5), - Err(PsychometricError::LevelChangeRequiresStableDrift) - ); - let increment = recover_discrete_time_independent_predictor_effect( - effect, - predictor, - drift, - 2.0, - LagClock::EventTime, - ) - .expect("tipred"); - assert_eq!( - refuse_level_change_intercept_as_impulse(intercept, impulse), - Err(PsychometricError::LevelChangeInterceptIsNotImpulse) - ); - assert_eq!( - refuse_level_change_intercept_as_free_continuous_intercept(intercept, 0.3), - Err(PsychometricError::LevelChangeInterceptIsNotFreeContinuousIntercept) - ); - assert_eq!( - refuse_level_change_intercept_as_process_increment(intercept, increment), - Err(PsychometricError::LevelChangeInterceptIsNotProcessIncrement) - ); - } - - #[test] - fn level_change_continuous_intercept_invalid_inputs_fail_closed() { - assert_eq!( - recover_level_change_continuous_intercept(f64::NAN, 1.0, -0.5), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_level_change_continuous_intercept(1.0, f64::INFINITY, -0.5), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_level_change_continuous_intercept(0.4, 3.0, f64::NAN), - Err(PsychometricError::InvalidNumericInput) + Err(PsychometricError::EventTimeRequired) ); assert_eq!( - recover_level_change_continuous_intercept(1e308, 2.0, -0.5), - Err(PsychometricError::InvalidNumericInput) + recover_discrete_observed_mean_with_time_independent_predictor( + 2.0, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) ); assert_eq!( - recover_level_change_continuous_intercept(1.0, 2.0, -1e308), + recover_discrete_observed_mean_with_time_independent_predictor( + 1e308, + 0.0, + 0.0, + 0.0, + 1e308, + 1.0, + 0.0, + 1.0, + LagClock::EventTime + ), Err(PsychometricError::InvalidNumericInput) ); - let scaled = recover_level_change_continuous_intercept(1e-308, 1.0, -1.0).expect("scale"); - assert!((scaled - 1e-308).abs() < 1e-320); - let rewritten = - recover_level_change_continuous_intercept(1e-308, 1.0, -1e308).expect("rewrite"); - assert!((rewritten - 1.0).abs() < 1e-12); } #[test] - fn level_change_discrete_increment_recovers_driver_equation_three_of_section_seven_point_two() { + fn time_dependent_impulse_carry_recovers_driver_equation_one_two_dissipation() { let effect = 0.4_f64; let predictor = 3.0_f64; let drift = -0.5_f64; let delta = 2.0_f64; - let increment = recover_level_change_discrete_increment( - effect, - predictor, - drift, - delta, - LagClock::EventTime, - ) - .expect("level-change-increment"); - let intercept = recover_level_change_continuous_intercept(effect, predictor, drift) - .expect("level-change"); - let via_cint = recover_discrete_continuous_intercept_effect( - intercept, - drift, - delta, - LagClock::EventTime, - ) - .expect("cint-increment"); - let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("impulse"); - let expected = (1.0 - (drift * delta).exp()) * impulse; - assert!((increment - expected).abs() < 1e-15); - assert!((increment - via_cint).abs() < 1e-15); - assert!((increment - impulse).abs() > 1e-3); - assert!((increment - intercept).abs() > 1e-3); - let tipred = recover_discrete_time_independent_predictor_effect( + let elapsed = 1.0_f64; + let carry = recover_time_dependent_predictor_impulse_carry( effect, predictor, drift, delta, + elapsed, LagClock::EventTime, ) - .expect("tipred"); - assert_eq!( - refuse_level_change_increment_as_impulse(increment, impulse), - Err(PsychometricError::LevelChangeIncrementIsNotImpulse) - ); - assert_eq!( - refuse_level_change_increment_as_intercept(increment, intercept), - Err(PsychometricError::LevelChangeIncrementIsNotIntercept) - ); + .expect("tdpred-carry"); + let expected = (-0.5_f64).exp() * 1.2; + assert!((carry - expected).abs() < 1e-15); assert_eq!( - refuse_level_change_increment_as_process_increment(increment, tipred), - Err(PsychometricError::LevelChangeIncrementIsNotProcessIncrement) + recover_time_dependent_predictor_impulse_carry( + 0.0, + predictor, + drift, + delta, + elapsed, + LagClock::EventTime + ), + Ok(0.0) ); - let equilibrated = recover_level_change_discrete_increment( - effect, - predictor, - -800.0, - 1.0, - LagClock::EventTime, - ) - .expect("underflow"); - assert!((equilibrated - impulse).abs() < 1e-15); assert_eq!( - recover_level_change_discrete_increment( + recover_time_dependent_predictor_impulse_carry( + effect, 0.0, - predictor, drift, delta, + elapsed, LagClock::EventTime ), Ok(0.0) ); + let zero_drift = recover_time_dependent_predictor_impulse_carry( + effect, + predictor, + 0.0, + delta, + elapsed, + LagClock::EventTime, + ) + .expect("zero-drift"); + assert!((zero_drift - 1.2).abs() < 1e-15); + let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("tdpred"); + assert!((carry - impulse).abs() > 1e-3); + let vanished = recover_time_dependent_predictor_impulse_carry( + effect, + predictor, + -800.0, + delta, + elapsed, + LagClock::EventTime, + ) + .expect("vanish"); + assert_eq!(vanished.to_bits(), 0.0_f64.to_bits()); } #[test] - fn level_change_discrete_increment_invalid_inputs_fail_closed() { + fn time_dependent_impulse_carry_composes_evolved_mean_and_keeps_scale() { let effect = 0.4_f64; let predictor = 3.0_f64; let drift = -0.5_f64; let delta = 2.0_f64; + let elapsed = 1.0_f64; + let carry = recover_time_dependent_predictor_impulse_carry( + effect, + predictor, + drift, + delta, + elapsed, + LagClock::EventTime, + ) + .expect("tdpred-carry"); + let initial = 1.0_f64; + let intercept = 0.3_f64; + let composed = recover_discrete_latent_mean_with_impulse_carry( + initial, + drift, + intercept, + effect, + predictor, + delta, + elapsed, + LagClock::EventTime, + ) + .expect("eq3-carry"); + let evolved = + recover_discrete_latent_mean(initial, drift, intercept, delta, LagClock::EventTime) + .expect("mu-t"); + assert!((composed - (evolved + carry)).abs() < 1e-15); assert_eq!( - recover_level_change_discrete_increment( - effect, - predictor, + recover_discrete_latent_mean_with_impulse_carry( + initial, + drift, + intercept, 0.0, - delta, - LagClock::EventTime - ), - Err(PsychometricError::LevelChangeRequiresStableDrift) - ); - assert_eq!( - recover_level_change_discrete_increment( - effect, predictor, - 0.5, delta, + elapsed, LagClock::EventTime ), - Err(PsychometricError::LevelChangeRequiresStableDrift) - ); - assert_eq!( - recover_level_change_discrete_increment( - effect, - predictor, - drift, - delta, - LagClock::SystemTime - ), - Err(PsychometricError::EventTimeRequired) + Ok(evolved) ); assert_eq!( - recover_level_change_discrete_increment( - effect, - predictor, - drift, + recover_discrete_latent_mean_with_impulse_carry( 0.0, - LagClock::EventTime - ), - Err(PsychometricError::NonPositiveInterval) - ); - assert_eq!( - recover_level_change_discrete_increment(1e308, 2.0, drift, delta, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_level_change_discrete_increment( - f64::NAN, - predictor, drift, - delta, - LagClock::EventTime - ), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_level_change_discrete_increment( 0.0, + effect, predictor, - 0.0, delta, + elapsed, LagClock::EventTime ), - Ok(0.0) + Ok(carry) ); + let scaled = recover_time_dependent_predictor_impulse_carry( + 1e308, + 1e-308, + 0.0, + 2.0, + 1.0, + LagClock::EventTime, + ) + .expect("scale"); + assert!((scaled - 1.0).abs() < 1e-15); + let rewritten = recover_time_dependent_predictor_impulse_carry( + 1e-308, + 1.0, + 710.0, + 2.0, + 1.0, + LagClock::EventTime, + ) + .expect("rewrite"); + let expected_rewrite = (1e-308_f64.ln() + 710.0).exp(); + assert!((rewritten - expected_rewrite).abs() <= expected_rewrite * 1e-12); } #[test] - fn extra_process_contribution_recovers_driver_section_seven_point_two() { - let coupling = 0.569_907_f64; - let predictor = 1.0_f64; - let original = -0.1393_f64; - let extra = -0.000_001_f64; - let delta = 1.0_f64; - let recovered = recover_level_change_extra_process_contribution( - coupling, + fn discrete_observed_mean_with_impulse_carry_recovers_driver_equation_five() { + let loading = 2.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let elapsed = 1.0_f64; + let effect = 0.4_f64; + let predictor = 3.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let manifest_mean = 0.5_f64; + let recovered = recover_discrete_observed_mean_with_impulse_carry( + loading, + initial, + drift, + intercept, + effect, predictor, - original, - extra, + manifest_mean, delta, + elapsed, LagClock::EventTime, ) - .expect("extra-process"); - let expected = coupling * predictor * ((extra * delta).exp() - (original * delta).exp()) - / (extra - original); - assert!((recovered - expected).abs() < 1e-15); - let equal_rate = recover_level_change_extra_process_contribution( - coupling, + .expect("eq5-carry-mean"); + let carried = recover_discrete_latent_mean_with_impulse_carry( + initial, + drift, + intercept, + effect, predictor, - extra, - extra, delta, + elapsed, LagClock::EventTime, ) - .expect("equal-rate"); - let equal_expected = coupling * predictor * delta * (extra * delta).exp(); - assert!((equal_rate - equal_expected).abs() < 1e-15); - let brownian = recover_level_change_extra_process_contribution( - coupling, - predictor, - 0.0, - extra, + .expect("carried"); + let expected = manifest_mean + loading * carried; + assert!((recovered - expected).abs() < 1e-15); + let evolved_observed = recover_discrete_observed_mean( + loading, + initial, + drift, + intercept, + manifest_mean, delta, LagClock::EventTime, ) - .expect("brownian-original"); - let brownian_expected = coupling * predictor * (extra * delta).exp_m1() / extra; - assert!((brownian - brownian_expected).abs() < 1e-15); + .expect("eq3-eq5-mean"); + assert!((evolved_observed - recovered).abs() > 1e-3); assert_eq!( - recover_level_change_extra_process_contribution( + recover_discrete_observed_mean_with_impulse_carry( 0.0, + initial, + drift, + intercept, + effect, predictor, - original, - extra, - delta, - LagClock::EventTime - ), - Ok(0.0) - ); - assert_eq!( - recover_level_change_extra_process_contribution( - coupling, - 0.0, - original, - extra, + manifest_mean, delta, + elapsed, LagClock::EventTime ), - Ok(0.0) + Ok(manifest_mean) ); assert_eq!( - recover_level_change_extra_process_contribution( - coupling, - 0.0, - original, + recover_discrete_observed_mean_with_impulse_carry( + loading, + initial, + drift, + intercept, + effect, + predictor, 0.0, delta, + elapsed, LagClock::EventTime ), - Ok(0.0) + Ok(loading * carried) ); } #[test] - fn extra_process_contribution_is_not_cint_rewrite_or_impulse() { - let coupling = 0.4_f64; - let predictor = 3.0_f64; - let original = -0.5_f64; - let extra = -0.05_f64; + fn discrete_observed_mean_with_impulse_carry_is_not_contemporaneous_or_zero_carry() { + let loading = 2.0_f64; + let drift = -0.5_f64; let delta = 2.0_f64; - let recovered = recover_level_change_extra_process_contribution( - coupling, + let elapsed = 1.0_f64; + let effect = 0.4_f64; + let predictor = 3.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let manifest_mean = 0.5_f64; + let recovered = recover_discrete_observed_mean_with_impulse_carry( + loading, + initial, + drift, + intercept, + effect, predictor, - original, - extra, + manifest_mean, delta, + elapsed, LagClock::EventTime, ) - .expect("extra-process"); - let intercept = recover_level_change_continuous_intercept(coupling, predictor, original) - .expect("level-change"); - let increment = recover_level_change_discrete_increment( - coupling, + .expect("eq5-carry-mean"); + let contemporaneous = recover_discrete_observed_mean_with_impulse( + loading, + initial, + drift, + intercept, + effect, predictor, - original, + manifest_mean, delta, LagClock::EventTime, ) - .expect("level-change-increment"); - let impulse = - recover_time_dependent_predictor_impulse(coupling, predictor).expect("impulse"); - assert!((recovered - intercept).abs() > 1e-3); - assert!((recovered - increment).abs() > 1e-3); - assert!((recovered - impulse).abs() > 1e-3); + .expect("eq5-mx"); + assert!((contemporaneous - recovered).abs() > 1e-3); + let evolved_observed = recover_discrete_observed_mean( + loading, + initial, + drift, + intercept, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq3-eq5-mean"); + let zero_carry = recover_discrete_observed_mean_with_impulse_carry( + loading, + initial, + drift, + intercept, + 0.0, + predictor, + manifest_mean, + delta, + elapsed, + LagClock::EventTime, + ) + .expect("zero-carry"); + assert!((zero_carry - evolved_observed).abs() < 1e-15); + } + + #[test] + fn discrete_observed_mean_with_impulse_carry_refuses_evolved_mean_and_overflow() { + let loading = 2.0_f64; + let recovered = recover_discrete_observed_mean_with_impulse_carry( + loading, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 2.0, + 1.0, + LagClock::EventTime, + ) + .expect("eq5-carry-mean"); + let carried = recover_discrete_latent_mean_with_impulse_carry( + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 2.0, + 1.0, + LagClock::EventTime, + ) + .expect("carried"); + let evolved_observed = + recover_discrete_observed_mean(loading, 1.0, -0.5, 0.3, 0.5, 2.0, LagClock::EventTime) + .expect("eq3-eq5-mean"); assert_eq!( - refuse_level_change_extra_process_as_impulse(recovered, impulse), - Err(PsychometricError::LevelChangeExtraProcessIsNotImpulse) + refuse_evolved_observed_mean_as_impulse_carry_observed_mean( + evolved_observed, + recovered + ), + Err(PsychometricError::EvolvedObservedMeanIsNotImpulseCarryObservedMean) ); assert_eq!( - refuse_level_change_extra_process_as_intercept(recovered, intercept), - Err(PsychometricError::LevelChangeExtraProcessIsNotIntercept) + refuse_impulse_observed_mean_as_impulse_carry_observed_mean( + recover_discrete_observed_mean_with_impulse( + loading, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 2.0, + LagClock::EventTime, + ) + .expect("eq5-mx"), + recovered + ), + Err(PsychometricError::ImpulseObservedMeanIsNotImpulseCarryObservedMean) ); assert_eq!( - refuse_level_change_extra_process_as_increment(recovered, increment), - Err(PsychometricError::LevelChangeExtraProcessIsNotIncrement) + refuse_latent_mean_as_observed_mean(carried, recovered), + Err(PsychometricError::LatentMeanIsNotObservedMean) + ); + assert_eq!( + refuse_manifest_means_as_observed_mean(0.5, recovered), + Err(PsychometricError::ManifestMeansIsNotObservedMean) ); + let scaled = recover_discrete_observed_mean_with_impulse_carry( + 1e308, + 1e-308, + 0.0, + 0.0, + 0.0, + 3.0, + 0.0, + 2.0, + 1.0, + LagClock::EventTime, + ) + .expect("scale"); + assert!((scaled - 1.0).abs() < 1e-15); + let finite_loaded = recover_discrete_observed_mean_with_impulse_carry( + 1e308, + 1.0, + 0.0, + 0.0, + 0.0, + 3.0, + 0.0, + 2.0, + 1.0, + LagClock::EventTime, + ) + .expect("lambda-mu"); + assert!((finite_loaded - 1e308).abs() / 1e308 < 1e-15); } #[test] - fn extra_process_contribution_invalid_inputs_fail_closed() { - let coupling = 0.4_f64; - let predictor = 3.0_f64; - let original = -0.5_f64; - let extra = -0.000_001_f64; - let delta = 2.0_f64; + fn discrete_observed_mean_with_impulse_carry_invalid_inputs_fail_closed() { assert_eq!( - recover_level_change_extra_process_contribution( - coupling, - predictor, - original, - 0.0, - delta, + recover_discrete_observed_mean_with_impulse_carry( + f64::NAN, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 2.0, + 1.0, LagClock::EventTime ), - Err(PsychometricError::LevelChangeExtraProcessRequiresNegativeDrift) + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_level_change_extra_process_contribution( - coupling, - predictor, - original, - 0.5, - delta, + recover_discrete_observed_mean_with_impulse_carry( + 1e308, + 2.0, + 0.0, + 0.0, + 0.0, + 3.0, + 0.0, + 2.0, + 1.0, LagClock::EventTime ), - Err(PsychometricError::LevelChangeExtraProcessRequiresNegativeDrift) + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_level_change_extra_process_contribution( - coupling, - predictor, - original, - extra, - delta, - LagClock::SystemTime + recover_discrete_observed_mean_with_impulse_carry( + 1.0, + 1.0, + 710.0, + 0.0, + 0.0, + 3.0, + 0.5, + 1.0, + 0.5, + LagClock::EventTime ), - Err(PsychometricError::EventTimeRequired) + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_level_change_extra_process_contribution( - coupling, - predictor, - original, - extra, + recover_discrete_observed_mean_with_impulse_carry( + 1e308, + 0.0, + 0.0, + 0.0, + 1e308, + 1.0, 0.0, + 2.0, + 1.0, LagClock::EventTime ), - Err(PsychometricError::NonPositiveInterval) + Err(PsychometricError::InvalidNumericInput) ); + } + + #[test] + fn discrete_observed_mean_with_impulse_carry_interval_and_clock_fail_closed() { assert_eq!( - recover_level_change_extra_process_contribution( - f64::NAN, - predictor, - original, - extra, - delta, + recover_discrete_observed_mean_with_impulse_carry( + 2.0, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 2.0, + 0.0, LagClock::EventTime ), - Err(PsychometricError::InvalidNumericInput) + Err(PsychometricError::NonPositiveInterval) ); assert_eq!( - recover_level_change_extra_process_contribution( - 1e308, + recover_discrete_observed_mean_with_impulse_carry( + 2.0, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 2.0, 2.0, - original, - extra, - delta, LagClock::EventTime ), - Err(PsychometricError::InvalidNumericInput) + Err(PsychometricError::NonPositiveInterval) ); assert_eq!( - recover_level_change_extra_process_contribution( - coupling, - predictor, - 710.0, - extra, + recover_discrete_observed_mean_with_impulse_carry( + 2.0, 1.0, - LagClock::EventTime + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 2.0, + 1.0, + LagClock::SystemTime ), - Err(PsychometricError::InvalidNumericInput) + Err(PsychometricError::EventTimeRequired) ); } #[test] - fn nonfinite_short_circuit_operands_of_fail_closed_guards_execute() { - let event = LagClock::EventTime; - assert_eq!( - recover_manifest_lagged_observed_covariance(2.0, f64::NAN, 0.0), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_manifest_lagged_observed_covariance(2.0, 0.4, f64::NAN), - Err(PsychometricError::InvalidNumericInput) - ); + fn time_dependent_impulse_carry_refuses_contemporaneous_cint_tipred_and_equation_fourteen() { + let effect = 0.4_f64; + let predictor = 3.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let elapsed = 1.0_f64; + let carry = recover_time_dependent_predictor_impulse_carry( + effect, + predictor, + drift, + delta, + elapsed, + LagClock::EventTime, + ) + .expect("tdpred-carry"); + let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("tdpred"); + let intercept_effect = + recover_discrete_continuous_intercept_effect(effect, drift, delta, LagClock::EventTime) + .expect("cint"); + let time_independent = recover_discrete_time_independent_predictor_effect( + effect, + predictor, + drift, + delta, + LagClock::EventTime, + ) + .expect("tipred"); + let equation_fourteen = recover_discrete_time_varying_predictor_effect( + effect, + delta, + delta, + delta, + LagClock::EventTime, + ) + .expect("eq14"); + assert!((carry - impulse).abs() > 1e-3); + assert!((carry - intercept_effect).abs() > 1e-3); + assert!((carry - time_independent).abs() > 1e-3); + assert!((carry - equation_fourteen).abs() > 1e-3); assert_eq!( - recover_discrete_continuous_intercept_effect(0.3, -0.5, f64::NAN, event), - Err(PsychometricError::NonPositiveInterval) + refuse_time_dependent_impulse_carry_as_contemporaneous_impulse(carry, impulse), + Err(PsychometricError::TimeDependentImpulseCarryIsNotContemporaneousImpulse) ); assert_eq!( - recover_level_change_extra_process_contribution(0.4, 3.0, -0.5, -1e-6, f64::NAN, event), - Err(PsychometricError::NonPositiveInterval) + refuse_time_dependent_impulse_carry_as_continuous_intercept(carry, effect), + Err(PsychometricError::TimeDependentImpulseCarryIsNotContinuousIntercept) ); assert_eq!( - recover_level_change_extra_process_contribution(0.4, f64::NAN, -0.5, -1e-6, 2.0, event), - Err(PsychometricError::InvalidNumericInput) + refuse_time_dependent_impulse_carry_as_time_independent_effect(carry, time_independent), + Err(PsychometricError::TimeDependentImpulseCarryIsNotTimeIndependentEffect) ); assert_eq!( - recover_level_change_extra_process_contribution(0.4, 3.0, f64::NAN, -1e-6, 2.0, event), - Err(PsychometricError::InvalidNumericInput) + refuse_time_dependent_impulse_carry_as_time_varying_discrete_effect( + carry, + equation_fourteen + ), + Err(PsychometricError::TimeDependentImpulseCarryIsNotTimeVaryingDiscreteEffect) ); + } + + #[test] + fn time_dependent_impulse_carry_invalid_inputs_fail_closed() { assert_eq!( - recover_level_change_extra_process_contribution(0.4, 3.0, -0.5, f64::NAN, 2.0, event), + recover_time_dependent_predictor_impulse_carry( + f64::NAN, + 1.0, + -0.5, + 2.0, + 1.0, + LagClock::EventTime + ), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_level_change_extra_process_contribution_after( + recover_time_dependent_predictor_impulse_carry( 0.4, 3.0, - -0.5, - -0.05, + f64::INFINITY, 2.0, - f64::NAN, - event + 1.0, + LagClock::EventTime ), - Err(PsychometricError::NonPositiveInterval) - ); - assert_eq!( - recover_discrete_time_independent_predictor_effect(0.2, 1.0, -0.5, f64::NAN, event), - Err(PsychometricError::NonPositiveInterval) - ); - assert_eq!( - recover_discrete_time_independent_predictor_effect(0.2, 1.0, f64::NAN, 2.0, event), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_time_independent_predictor_effect(0.2, f64::NAN, -0.5, 2.0, event), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_asymptotic_time_independent_predictor_effect(0.2, f64::NAN, -0.5, event), + recover_time_dependent_predictor_impulse_carry( + 1e308, + 2.0, + -0.5, + 2.0, + 1.0, + LagClock::EventTime + ), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_asymptotic_time_independent_predictor_effect(0.2, 1.0, f64::NAN, event), + recover_time_dependent_predictor_impulse_carry( + 1.2, + 1.0, + 800.0, + 2.0, + 1.0, + LagClock::EventTime + ), Err(PsychometricError::InvalidNumericInput) ); + // Finite log-rate whose product with elapsed overflows. exp(±∞) + // is not finite, then the non-finite drift interval fails closed. assert_eq!( - recover_asymptotic_time_independent_predictor_variance(f64::NAN, 1.0, -0.5, event), + recover_time_dependent_predictor_impulse_carry( + 0.4, + 3.0, + 1e308, + 3.0, + 2.0, + LagClock::EventTime + ), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_asymptotic_time_independent_predictor_variance(0.2, f64::NAN, -0.5, event), - Err(PsychometricError::InvalidNumericInput) + recover_time_dependent_predictor_impulse_carry( + 0.0, + 3.0, + 800.0, + 2.0, + 1.0, + LagClock::EventTime + ), + Ok(0.0) ); assert_eq!( - recover_asymptotic_time_independent_predictor_variance(0.2, 1.0, f64::NAN, event), - Err(PsychometricError::InvalidNumericInput) + recover_time_dependent_predictor_impulse_carry( + -1e-308, + 1.0, + 710.0, + 2.0, + 1.0, + LagClock::EventTime + ) + .map(f64::signum), + Ok(-1.0) ); + } + + #[test] + fn time_dependent_impulse_carry_interval_and_clock_fail_closed() { assert_eq!( - recover_asymptotic_continuous_intercept(0.3, f64::NAN, event), - Err(PsychometricError::InvalidNumericInput) + recover_time_dependent_predictor_impulse_carry( + 0.4, + 3.0, + -0.5, + 0.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) ); assert_eq!( - recover_initial_time_independent_predictor_carry(0.4, 3.0, -0.5, f64::NAN, event), + recover_time_dependent_predictor_impulse_carry( + 0.4, + 3.0, + -0.5, + 2.0, + 0.0, + LagClock::EventTime + ), Err(PsychometricError::NonPositiveInterval) ); assert_eq!( - recover_initial_time_dependent_predictor_carry(0.4, 3.0, -0.5, f64::NAN, event), + recover_time_dependent_predictor_impulse_carry( + 0.4, + 3.0, + -0.5, + 2.0, + 2.0, + LagClock::EventTime + ), Err(PsychometricError::NonPositiveInterval) ); assert_eq!( - recover_time_dependent_predictor_impulse_carry(0.4, 3.0, -0.5, f64::NAN, 1.0, event), - Err(PsychometricError::NonPositiveInterval) + recover_time_dependent_predictor_impulse_carry( + 0.4, + 3.0, + -0.5, + 2.0, + 1.0, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) ); assert_eq!( - recover_time_dependent_predictor_impulse_carry(0.4, 3.0, -0.5, 2.0, f64::NAN, event), - Err(PsychometricError::NonPositiveInterval) + recover_discrete_latent_mean_with_impulse_carry( + 1e308, + 0.0, + 0.0, + 1e308, + 1.0, + 2.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) ); } #[test] - fn extra_process_contribution_underflow_and_overflow_paths() { - let coupling = 0.4_f64; + fn initial_time_independent_predictor_recovers_table_three_t0_shift_and_carry() { + let effect = 0.4_f64; let predictor = 3.0_f64; - let original = -0.5_f64; - let extra = -0.000_001_f64; - let vanished = recover_level_change_extra_process_contribution( - coupling, - predictor, - -800.0, - extra, - 1.0, - LagClock::EventTime, - ) - .expect("underflow"); - let vanished_expected = coupling * predictor * (extra * 1.0).exp() / (extra - -800.0); - assert!((vanished - vanished_expected).abs() < 1e-15); - let extra_underflow = recover_level_change_extra_process_contribution( - coupling, - predictor, - original, - -f64::from_bits(1), - 0.5, - LagClock::EventTime, - ) - .expect("extra-argument-underflow"); - assert!(extra_underflow.is_finite()); - let original_underflow = recover_level_change_extra_process_contribution( - coupling, - predictor, - -2e-160_f64, - -1e-160_f64, - 1e-200_f64, - LagClock::EventTime, - ) - .expect("gap-argument-underflow"); - let original_underflow_expected = coupling * predictor * 1e-200_f64; - assert!((original_underflow - original_underflow_expected).abs() <= 1e-200_f64); - let vanished_finite_increment = recover_level_change_extra_process_contribution( - coupling, - predictor, - -800.0, - -92.0, - 1.0, - LagClock::EventTime, - ) - .expect("original-lag-underflow-finite-increment"); - let vanished_finite_expected = coupling * predictor * (-92.0_f64).exp() / (-92.0 - -800.0); - assert!((vanished_finite_increment - vanished_finite_expected).abs() < 1e-15); - let overflow_fallback = recover_level_change_extra_process_contribution( - std::hint::black_box(coupling), - std::hint::black_box(predictor), - std::hint::black_box(-0.8), - std::hint::black_box(extra), - std::hint::black_box(900.0), - LagClock::EventTime, - ) - .expect("expm1-overflow-fallback"); - let overflow_expected = - coupling * predictor * ((extra * 900.0).exp() - (-0.8_f64 * 900.0).exp()) - / (extra - -0.8); - assert!((overflow_fallback - overflow_expected).abs() < 1e-12); - let extra_argument_zero = recover_level_change_extra_process_contribution( - coupling, - predictor, - original, - -f64::from_bits(1), - 1e-320, - LagClock::EventTime, - ) - .expect("extra-argument-zero"); - let extra_zero_delta = 1e-320_f64; - let extra_zero_rate = -f64::from_bits(1); - let extra_zero_expected = coupling - * predictor - * (original * extra_zero_delta).exp() - * ((extra_zero_rate - original) * extra_zero_delta).exp_m1() - / (extra_zero_rate - original); - assert!( - (extra_argument_zero - extra_zero_expected).abs() <= 16.0 * f64::from_bits(1), - "recovered={extra_argument_zero:.e} expected={extra_zero_expected:.e}" + let drift = -0.5_f64; + let delta = 2.0_f64; + let shift = recover_initial_time_independent_predictor_effect(effect, predictor) + .expect("t0-tipred"); + assert!((shift - 1.2).abs() < 1e-15); + assert_eq!( + recover_initial_time_independent_predictor_effect(0.0, predictor), + Ok(0.0) ); - let extra_argument_zero_after = recover_level_change_extra_process_contribution_after( - coupling, - predictor, - original, - -f64::from_bits(1), - 1.0, - 1e-320, - LagClock::EventTime, - ) - .expect("after-extra-argument-zero"); - assert!( - (extra_argument_zero_after - extra_zero_expected).abs() <= 16.0 * f64::from_bits(1), - "after recovered={extra_argument_zero_after:.e} expected={extra_zero_expected:.e}" + assert_eq!( + recover_initial_time_independent_predictor_effect(effect, 0.0), + Ok(0.0) ); - assert!((extra_argument_zero - extra_argument_zero_after).abs() < 1e-30); - } - - #[test] - fn extra_process_observed_mean_recovers_driver_equation_five() { - let loading = 2.0_f64; - let coupling = 0.4_f64; - let predictor = 3.0_f64; - let original = -0.5_f64; - let extra = -0.05_f64; - let delta = 2.0_f64; - let initial = 1.0_f64; - let intercept = 0.3_f64; - let manifest_mean = 0.5_f64; - let recovered = recover_discrete_observed_mean_with_extra_process( - loading, - initial, - original, - intercept, - coupling, + let carry = recover_initial_time_independent_predictor_carry( + effect, predictor, - extra, - manifest_mean, + drift, delta, LagClock::EventTime, ) - .expect("eq5-extra-process-mean"); - let composed = recover_discrete_latent_mean_with_extra_process( - initial, - original, - intercept, - coupling, + .expect("t0-carry"); + let expected = 1.2 * (drift * delta).exp(); + assert!((carry - expected).abs() < 1e-15); + let zero_drift = recover_initial_time_independent_predictor_carry( + effect, predictor, - extra, - delta, - LagClock::EventTime, - ) - .expect("extra-latent"); - let expected = manifest_mean + loading * composed; - assert!((recovered - expected).abs() < 1e-15); - let evolved_observed = recover_discrete_observed_mean( - loading, - initial, - original, - intercept, - manifest_mean, + 0.0, delta, LagClock::EventTime, ) - .expect("eq3-eq5-mean"); - assert!((evolved_observed - recovered).abs() > 1e-3); - let impulse_observed = recover_discrete_observed_mean_with_impulse( - loading, - initial, - original, - intercept, - coupling, + .expect("zero-drift"); + assert!((zero_drift - 1.2).abs() < 1e-15); + let vanished = recover_initial_time_independent_predictor_carry( + effect, predictor, - manifest_mean, - delta, + -800.0, + 1.0, LagClock::EventTime, ) - .expect("eq5-impulse-mean"); - assert!((impulse_observed - recovered).abs() > 1e-3); - let contribution = recover_level_change_extra_process_contribution( - coupling, + .expect("underflow"); + assert_eq!(vanished.to_bits(), 0.0_f64.to_bits()); + let increment = recover_discrete_time_independent_predictor_effect( + effect, predictor, - original, - extra, + drift, delta, LagClock::EventTime, - ) - .expect("extra-process"); - assert_eq!( - refuse_evolved_observed_mean_as_extra_process_observed_mean( - evolved_observed, - recovered - ), - Err(PsychometricError::EvolvedObservedMeanIsNotExtraProcessObservedMean) - ); - assert_eq!( - refuse_impulse_observed_mean_as_extra_process_observed_mean( - impulse_observed, - recovered - ), - Err(PsychometricError::ImpulseObservedMeanIsNotExtraProcessObservedMean) - ); - assert_eq!( - refuse_extra_process_contribution_as_observed_mean(contribution, recovered), - Err(PsychometricError::ExtraProcessContributionIsNotObservedMean) - ); - assert_eq!( - refuse_extra_process_latent_mean_as_observed_mean(composed, recovered), - Err(PsychometricError::ExtraProcessLatentMeanIsNotObservedMean) - ); + ) + .expect("tipred"); + let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("tdpred"); + assert!((carry - shift).abs() > 1e-3); + assert!((carry - increment).abs() > 1e-3); + assert!((shift - increment).abs() > 1e-3); + assert!((shift - effect).abs() > 1e-3); + // Algebraically a product, like M x, but Table 3 names a different matrix. + assert!((shift - impulse).abs() < 1e-15); } #[test] - fn extra_process_observed_mean_zero_loading_is_manifest_mean_and_refuses_clock() { - let loading = 2.0_f64; - let coupling = 0.4_f64; + fn initial_time_independent_predictor_composes_evolved_mean_and_keeps_scale() { + let effect = 0.4_f64; let predictor = 3.0_f64; - let original = -0.5_f64; - let extra = -0.05_f64; + let drift = -0.5_f64; let delta = 2.0_f64; + let carry = recover_initial_time_independent_predictor_carry( + effect, + predictor, + drift, + delta, + LagClock::EventTime, + ) + .expect("t0-carry"); let initial = 1.0_f64; let intercept = 0.3_f64; - let manifest_mean = 0.5_f64; + let composed = recover_discrete_latent_mean_with_initial_time_independent_predictor( + initial, + drift, + intercept, + effect, + predictor, + delta, + LagClock::EventTime, + ) + .expect("eq3-t0tipred"); + let evolved = + recover_discrete_latent_mean(initial, drift, intercept, delta, LagClock::EventTime) + .expect("mu-t"); + assert!((composed - (evolved + carry)).abs() < 1e-15); assert_eq!( - recover_discrete_observed_mean_with_extra_process( - 0.0, + recover_discrete_latent_mean_with_initial_time_independent_predictor( initial, - original, + drift, intercept, - coupling, + 0.0, predictor, - extra, - manifest_mean, delta, LagClock::EventTime ), - Ok(manifest_mean) + Ok(evolved) ); assert_eq!( - recover_discrete_observed_mean_with_extra_process( - loading, - initial, - original, - intercept, - coupling, + recover_discrete_latent_mean_with_initial_time_independent_predictor( + 0.0, + drift, + 0.0, + effect, predictor, - extra, - manifest_mean, delta, - LagClock::SystemTime + LagClock::EventTime ), - Err(PsychometricError::EventTimeRequired) + Ok(carry) ); - } - - #[test] - #[allow(clippy::too_many_lines)] - fn after_extra_process_observed_mean_recovers_driver_equation_five() { - let loading = 2.0_f64; - let coupling = 0.4_f64; - let predictor = 3.0_f64; - let original = -0.5_f64; - let extra = -0.05_f64; - let delta = 2.0_f64; - let elapsed = 1.0_f64; - let initial = 1.0_f64; - let intercept = 0.3_f64; - let manifest_mean = 0.5_f64; - let recovered = recover_discrete_observed_mean_with_extra_process_after( - loading, - initial, - original, - intercept, - coupling, - predictor, - extra, - manifest_mean, - delta, - elapsed, - LagClock::EventTime, - ) - .expect("eq5-after-extra-process-mean"); - let composed = recover_discrete_latent_mean_with_extra_process_after( - initial, - original, - intercept, - coupling, - predictor, - extra, - delta, - elapsed, + let scaled = recover_initial_time_independent_predictor_carry( + 1e308, + 1e-308, + 0.0, + 1.0, LagClock::EventTime, ) - .expect("after-extra-latent"); - let expected = manifest_mean + loading * composed; - assert!((recovered - expected).abs() < 1e-15); - let first_occasion = recover_discrete_observed_mean_with_extra_process( - loading, - initial, - original, - intercept, - coupling, - predictor, - extra, - manifest_mean, - delta, + .expect("scale"); + assert!((scaled - 1.0).abs() < 1e-15); + let rewritten = recover_initial_time_independent_predictor_carry( + 2.0, + 0.5, + 710.0, + 1.0, LagClock::EventTime, - ) - .expect("eq5-t0-extra-process-mean"); - assert!((first_occasion - recovered).abs() > 1e-3); - let evolved_observed = recover_discrete_observed_mean( - loading, - initial, - original, - intercept, - manifest_mean, - delta, + ); + assert_eq!(rewritten, Err(PsychometricError::InvalidNumericInput)); + let finite_rewrite = recover_initial_time_independent_predictor_carry( + 1e-308, + 1.0, + 700.0, + 1.0, LagClock::EventTime, ) - .expect("eq3-eq5-mean"); - assert!((evolved_observed - recovered).abs() > 1e-3); - let carry_observed = recover_discrete_observed_mean_with_impulse_carry( - loading, - initial, - original, - intercept, - coupling, + .expect("log-rewrite"); + let expected_rewrite = (1e-308_f64.ln() + 700.0).exp(); + assert!((finite_rewrite - expected_rewrite).abs() / expected_rewrite < 1e-12); + } + + #[test] + fn initial_time_independent_predictor_refuses_process_increment_cint_impulse_and_coefficient() { + let effect = 0.4_f64; + let predictor = 3.0_f64; + let shift = recover_initial_time_independent_predictor_effect(effect, predictor) + .expect("t0-tipred"); + let carry = recover_initial_time_independent_predictor_carry( + effect, predictor, - manifest_mean, - delta, - elapsed, + -0.5, + 2.0, LagClock::EventTime, ) - .expect("eq5-impulse-carry-mean"); - assert!((carry_observed - recovered).abs() > 1e-3); - let contribution = recover_level_change_extra_process_contribution_after( - coupling, + .expect("t0-carry"); + let increment = recover_discrete_time_independent_predictor_effect( + effect, predictor, - original, - extra, - delta, - elapsed, + -0.5, + 2.0, LagClock::EventTime, ) - .expect("after-extra-process"); + .expect("tipred"); + let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("tdpred"); assert_eq!( - refuse_extra_process_observed_mean_as_after_extra_process_observed_mean( - first_occasion, - recovered - ), - Err(PsychometricError::ExtraProcessObservedMeanIsNotAfterExtraProcessObservedMean) + refuse_initial_time_independent_effect_as_process_increment(shift, increment), + Err(PsychometricError::InitialTimeIndependentEffectIsNotProcessIncrement) ); assert_eq!( - refuse_evolved_observed_mean_as_after_extra_process_observed_mean( - evolved_observed, - recovered - ), - Err(PsychometricError::EvolvedObservedMeanIsNotAfterExtraProcessObservedMean) + refuse_initial_time_independent_carry_as_initial_effect(carry, shift), + Err(PsychometricError::InitialTimeIndependentCarryIsNotInitialEffect) ); assert_eq!( - refuse_impulse_carry_observed_mean_as_after_extra_process_observed_mean( - carry_observed, - recovered - ), - Err(PsychometricError::ImpulseCarryObservedMeanIsNotAfterExtraProcessObservedMean) + refuse_initial_time_independent_effect_as_continuous_intercept(shift, 0.4), + Err(PsychometricError::InitialTimeIndependentEffectIsNotContinuousIntercept) ); assert_eq!( - refuse_after_extra_process_contribution_as_observed_mean(contribution, recovered), - Err(PsychometricError::AfterExtraProcessContributionIsNotObservedMean) + refuse_initial_time_independent_effect_as_time_dependent_impulse(shift, impulse), + Err(PsychometricError::InitialTimeIndependentEffectIsNotTimeDependentImpulse) ); assert_eq!( - refuse_after_extra_process_latent_mean_as_observed_mean(composed, recovered), - Err(PsychometricError::AfterExtraProcessLatentMeanIsNotObservedMean) + refuse_initial_time_independent_coefficient_as_initial_effect(effect, shift), + Err(PsychometricError::InitialTimeIndependentCoefficientIsNotInitialEffect) ); } #[test] - #[allow(clippy::too_many_lines)] - fn after_extra_process_contribution_refuses_non_interior_interval() { - let coupling = 0.4_f64; - let predictor = 3.0_f64; - let original = -0.5_f64; - let extra = -0.05_f64; + fn initial_time_independent_predictor_invalid_inputs_fail_closed() { assert_eq!( - recover_level_change_extra_process_contribution_after( - coupling, - predictor, - original, - extra, - 2.0, + recover_initial_time_independent_predictor_effect(f64::NAN, 1.0), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_initial_time_independent_predictor_effect(1.0, f64::INFINITY), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_initial_time_independent_predictor_effect(1e308, 2.0), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_initial_time_independent_predictor_carry( + 0.4, + 3.0, + f64::NAN, 2.0, LagClock::EventTime ), - Err(PsychometricError::NonPositiveInterval) + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_level_change_extra_process_contribution_after( - coupling, - predictor, - original, - extra, - 2.0, + recover_initial_time_independent_predictor_carry( + 0.4, + 3.0, + -0.5, 0.0, LagClock::EventTime ), Err(PsychometricError::NonPositiveInterval) ); assert_eq!( - recover_discrete_observed_mean_with_extra_process_after( - 2.0, - 1.0, - original, - 0.3, - coupling, - predictor, - extra, - 0.5, + recover_initial_time_independent_predictor_carry( + 0.4, + 3.0, + -0.5, 2.0, - 1.0, LagClock::SystemTime ), Err(PsychometricError::EventTimeRequired) ); assert_eq!( - recover_discrete_observed_mean_with_extra_process_after( + recover_discrete_latent_mean_with_initial_time_independent_predictor( + 1e308, + 0.0, 0.0, + 1e308, 1.0, - original, - 0.3, - coupling, - predictor, - extra, - 0.5, - 2.0, 1.0, LagClock::EventTime ), - Ok(0.5) + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_level_change_extra_process_contribution_after( - coupling, - predictor, - original, - extra, - 2.0, + recover_initial_time_independent_predictor_carry( 1.0, - LagClock::SystemTime - ), - Err(PsychometricError::EventTimeRequired) - ); - assert_eq!( - recover_level_change_extra_process_contribution_after( - coupling, - predictor, - original, - extra, - 0.0, + 1.0, + f64::INFINITY, 1.0, LagClock::EventTime ), - Err(PsychometricError::NonPositiveInterval) + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_level_change_extra_process_contribution_after( - coupling, - predictor, - original, - extra, + recover_initial_time_independent_predictor_carry( f64::NAN, 1.0, + -0.5, + 2.0, LagClock::EventTime ), - Err(PsychometricError::NonPositiveInterval) + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_latent_mean_with_extra_process_after( + recover_initial_time_independent_predictor_carry( 1.0, - original, - 0.3, - coupling, - predictor, - extra, - 2.0, - 2.0, + 1.0, + 1e308, + 10.0, LagClock::EventTime ), - Err(PsychometricError::NonPositiveInterval) + Err(PsychometricError::InvalidNumericInput) ); - let evolved = recover_discrete_latent_mean(1.0, original, 0.3, 2.0, LagClock::EventTime) - .expect("mu-t"); assert_eq!( - recover_discrete_latent_mean_with_extra_process_after( + recover_discrete_latent_mean_with_initial_time_independent_predictor( 1.0, - original, + -0.5, 0.3, - 0.0, - predictor, - extra, - 2.0, + f64::NAN, 1.0, + 2.0, LagClock::EventTime ), - Ok(evolved) + Err(PsychometricError::InvalidNumericInput) ); } #[test] - fn asymptotic_time_independent_effect_recovers_driver_section_seven_point_two() { - // Driver et al. (2017, §7.2, p. 21) print LeisureTime - // TIPREDEFFECT = −0.225 and asymTIPREDEFFECT = −1.673 for a - // unit increase. Reconstruct a = −B / asym. - let effect = -0.225_f64; - let predictor = 1.0_f64; - let printed_asym = -1.673_f64; - let log_rate = -effect / printed_asym; - let recovered = recover_asymptotic_time_independent_predictor_effect( + fn discrete_observed_mean_with_initial_time_independent_predictor_recovers_driver_equation_five() + { + let loading = 2.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let effect = 0.4_f64; + let predictor = 3.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let manifest_mean = 0.5_f64; + let recovered = recover_discrete_observed_mean_with_initial_time_independent_predictor( + loading, + initial, + drift, + intercept, effect, predictor, - log_rate, + manifest_mean, + delta, LagClock::EventTime, ) - .expect("asymTIPREDEFFECT"); - let expected = -(effect * predictor) / log_rate; + .expect("eq5-t0tipred-mean"); + let composed = recover_discrete_latent_mean_with_initial_time_independent_predictor( + initial, + drift, + intercept, + effect, + predictor, + delta, + LagClock::EventTime, + ) + .expect("eq3-t0tipred"); + let expected = manifest_mean + loading * composed; assert!((recovered - expected).abs() < 1e-15); - assert!((recovered - printed_asym).abs() < 1e-12); - let happiness = recover_asymptotic_time_independent_predictor_effect( - 0.549, + let evolved_observed = recover_discrete_observed_mean( + loading, + initial, + drift, + intercept, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq3-eq5-mean"); + let process_observed = recover_discrete_observed_mean_with_time_independent_predictor( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-tipred-mean"); + let impulse_observed = recover_discrete_observed_mean_with_impulse( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-impulse-mean"); + let carried_observed = recover_discrete_observed_mean_with_impulse_carry( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, 1.0, - -0.549 / 0.219, LagClock::EventTime, ) - .expect("happiness-asym"); - assert!((happiness - 0.219).abs() < 1e-12); + .expect("eq5-carry-mean"); + assert!((evolved_observed - recovered).abs() > 1e-3); + assert!((process_observed - recovered).abs() > 1e-3); + assert!((impulse_observed - recovered).abs() > 1e-3); + assert!((carried_observed - recovered).abs() > 1e-3); assert_eq!( - recover_asymptotic_time_independent_predictor_effect( - 0.0, - predictor, + recover_discrete_observed_mean_with_initial_time_independent_predictor( 0.0, - LagClock::EventTime - ), - Ok(0.0) - ); - assert_eq!( - recover_asymptotic_time_independent_predictor_effect( + initial, + drift, + intercept, effect, - 0.0, - 0.0, + predictor, + manifest_mean, + delta, LagClock::EventTime ), - Ok(0.0) + Ok(manifest_mean) ); } #[test] - fn asymptotic_time_independent_effect_is_not_coefficient_discrete_cint_or_impulse() { - let effect = -0.225_f64; - let predictor = 2.0_f64; - let log_rate = -0.134_488_942_f64; - let recovered = recover_asymptotic_time_independent_predictor_effect( - effect, - predictor, - log_rate, + fn discrete_observed_mean_with_initial_time_independent_predictor_is_not_evolved_or_zero_carry() + { + let loading = 2.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let effect = 0.4_f64; + let predictor = 3.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; + let manifest_mean = 0.5_f64; + let recovered = recover_discrete_observed_mean_with_initial_time_independent_predictor( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-t0tipred-mean"); + let evolved_observed = recover_discrete_observed_mean( + loading, + initial, + drift, + intercept, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq3-eq5-mean"); + let zero_carry = recover_discrete_observed_mean_with_initial_time_independent_predictor( + loading, + initial, + drift, + intercept, + 0.0, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("zero-carry"); + assert!((zero_carry - evolved_observed).abs() < 1e-15); + assert!((recovered - evolved_observed).abs() > 1e-3); + } + + #[test] + fn discrete_observed_mean_with_initial_time_independent_predictor_refuses_evolved_mean_and_overflow() + { + let loading = 2.0_f64; + let recovered = recover_discrete_observed_mean_with_initial_time_independent_predictor( + loading, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 2.0, LagClock::EventTime, ) - .expect("asymTIPREDEFFECT"); - let discrete = recover_discrete_time_independent_predictor_effect( - effect, - predictor, - log_rate, + .expect("eq5-t0tipred-mean"); + let evolved_observed = + recover_discrete_observed_mean(loading, 1.0, -0.5, 0.3, 0.5, 2.0, LagClock::EventTime) + .expect("eq3-eq5-mean"); + let process_observed = recover_discrete_observed_mean_with_time_independent_predictor( + loading, 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 2.0, LagClock::EventTime, ) - .expect("discreteTIPREDEFFECT"); - let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("impulse"); - assert!((recovered - effect).abs() > 1e-3); - assert!((recovered - discrete).abs() > 1e-3); - assert!((recovered - impulse).abs() > 1e-3); - assert_eq!( - refuse_asymptotic_time_independent_effect_as_coefficient(recovered, effect), - Err(PsychometricError::AsymptoticTimeIndependentEffectIsNotCoefficient) - ); - assert_eq!( - refuse_asymptotic_time_independent_effect_as_discrete_effect(recovered, discrete), - Err(PsychometricError::AsymptoticTimeIndependentEffectIsNotDiscreteEffect) - ); - assert_eq!( - refuse_asymptotic_time_independent_effect_as_continuous_intercept(recovered, 0.3), - Err(PsychometricError::AsymptoticTimeIndependentEffectIsNotContinuousIntercept) - ); - assert_eq!( - refuse_asymptotic_time_independent_effect_as_time_dependent_impulse(recovered, impulse), - Err(PsychometricError::AsymptoticTimeIndependentEffectIsNotTimeDependentImpulse) - ); - } - - #[test] - fn asymptotic_time_independent_effect_invalid_inputs_fail_closed() { - let effect = -0.225_f64; - let predictor = 1.0_f64; - let log_rate = -0.134_488_942_f64; - assert_eq!( - recover_asymptotic_time_independent_predictor_effect( - effect, - predictor, - log_rate, - LagClock::SystemTime - ), - Err(PsychometricError::EventTimeRequired) - ); - assert_eq!( - recover_asymptotic_time_independent_predictor_effect( - effect, - predictor, - 0.0, - LagClock::EventTime - ), - Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) - ); + .expect("eq5-tipred-mean"); + let impulse_observed = recover_discrete_observed_mean_with_impulse( + loading, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 2.0, + LagClock::EventTime, + ) + .expect("eq5-impulse-mean"); + let carried_observed = recover_discrete_observed_mean_with_impulse_carry( + loading, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 2.0, + 1.0, + LagClock::EventTime, + ) + .expect("eq5-carry-mean"); assert_eq!( - recover_asymptotic_time_independent_predictor_effect( - effect, - predictor, - 0.5, - LagClock::EventTime + refuse_evolved_observed_mean_as_initial_time_independent_observed_mean( + evolved_observed, + recovered ), - Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) + Err(PsychometricError::EvolvedObservedMeanIsNotInitialTimeIndependentObservedMean) ); assert_eq!( - recover_asymptotic_time_independent_predictor_effect( - f64::NAN, - predictor, - log_rate, - LagClock::EventTime + refuse_time_independent_observed_mean_as_initial_time_independent_observed_mean( + process_observed, + recovered ), - Err(PsychometricError::InvalidNumericInput) + Err( + PsychometricError::TimeIndependentObservedMeanIsNotInitialTimeIndependentObservedMean + ) ); assert_eq!( - recover_asymptotic_time_independent_predictor_effect( - 1e308, - 2.0, - log_rate, - LagClock::EventTime + refuse_impulse_observed_mean_as_initial_time_independent_observed_mean( + impulse_observed, + recovered ), - Err(PsychometricError::InvalidNumericInput) + Err(PsychometricError::ImpulseObservedMeanIsNotInitialTimeIndependentObservedMean) ); assert_eq!( - recover_asymptotic_time_independent_predictor_effect( - 1e308, - 1.0, - -1e-308, - LagClock::EventTime + refuse_impulse_carry_observed_mean_as_initial_time_independent_observed_mean( + carried_observed, + recovered ), - Err(PsychometricError::InvalidNumericInput) + Err(PsychometricError::ImpulseCarryObservedMeanIsNotInitialTimeIndependentObservedMean) ); } #[test] - fn asymptotic_time_independent_variance_recovers_driver_section_seven_point_two() { - // Driver et al. (2017, §7.2, p. 21) print LeisureTime - // asymTIPREDEFFECT = −1.673. addedTIPREDVAR is the variance of - // that mean shift. Reconstruct a from B and the printed total - // change; the printed 2.838 is the 2-latent TRAITVAR model, not - // this scalar map. - let effect = -0.225_f64; - let printed_asym = -1.673_f64; - let log_rate = -effect / printed_asym; - let predictor_variance = 1.0_f64; - let recovered = recover_asymptotic_time_independent_predictor_variance( - effect, - predictor_variance, - log_rate, + fn discrete_observed_mean_with_initial_time_independent_predictor_invalid_inputs_fail_closed() { + let scaled = recover_discrete_observed_mean_with_initial_time_independent_predictor( + 1e308, + 1e-308, + 0.0, + 0.0, + 0.0, + 3.0, + 0.0, + 1.0, LagClock::EventTime, ) - .expect("addedTIPREDVAR"); - let expected = printed_asym * printed_asym * predictor_variance; - assert!((recovered - expected).abs() < 1e-12); - let doubled = recover_asymptotic_time_independent_predictor_variance( - effect, - 2.0, - log_rate, + .expect("scale"); + assert!((scaled - 1.0).abs() < 1e-15); + let finite_loaded = recover_discrete_observed_mean_with_initial_time_independent_predictor( + 1e308, + 0.0, + 0.0, + 0.0, + 0.0, + 3.0, + 0.0, + 1.0, LagClock::EventTime, ) - .expect("doubled-v"); - assert!((doubled - 2.0 * expected).abs() < 1e-12); + .expect("lambda-mu0"); + assert!((finite_loaded - 0.0).abs() < 1e-15); assert_eq!( - recover_asymptotic_time_independent_predictor_variance( + recover_discrete_observed_mean_with_initial_time_independent_predictor( + 1e308, + 2.0, 0.0, - predictor_variance, 0.0, - LagClock::EventTime - ), - Ok(0.0) - ); - assert_eq!( - recover_asymptotic_time_independent_predictor_variance( - effect, 0.0, + 3.0, 0.0, + 1.0, LagClock::EventTime ), - Ok(0.0) - ); - } - - #[test] - fn asymptotic_time_independent_variance_is_not_trait_stationary_or_mean_effect() { - let effect = -0.225_f64; - let log_rate = -0.134_488_942_f64; - let predictor_variance = 2.0_f64; - let recovered = recover_asymptotic_time_independent_predictor_variance( - effect, - predictor_variance, - log_rate, - LagClock::EventTime, - ) - .expect("addedTIPREDVAR"); - let mean_effect = recover_asymptotic_time_independent_predictor_effect( - effect, - 1.0, - log_rate, - LagClock::EventTime, - ) - .expect("asymTIPREDEFFECT"); - let stationary = recover_stationary_latent_variance(0.4, log_rate, LagClock::EventTime) - .expect("asymDIFFUSION"); - let trait_plus = recover_trait_plus_state_latent_variance(0.8, 0.3).expect("trait"); - assert!((recovered - mean_effect).abs() > 1e-3); - assert!((recovered - stationary).abs() > 1e-3); - assert!((recovered - trait_plus).abs() > 1e-3); - assert_eq!( - refuse_asymptotic_time_independent_variance_as_trait_variance(recovered, trait_plus), - Err(PsychometricError::AsymptoticTimeIndependentVarianceIsNotTraitVariance) - ); - assert_eq!( - refuse_asymptotic_time_independent_variance_as_stationary_within_subject( - recovered, stationary - ), - Err(PsychometricError::AsymptoticTimeIndependentVarianceIsNotStationaryWithinSubject) - ); - assert_eq!( - refuse_asymptotic_time_independent_variance_as_asymptotic_effect( - recovered, - mean_effect - ), - Err(PsychometricError::AsymptoticTimeIndependentVarianceIsNotAsymptoticEffect) + Err(PsychometricError::InvalidNumericInput) ); - } - - #[test] - fn asymptotic_time_independent_variance_invalid_inputs_fail_closed() { - let effect = -0.225_f64; - let log_rate = -0.134_488_942_f64; assert_eq!( - recover_asymptotic_time_independent_predictor_variance( - effect, + recover_discrete_observed_mean_with_initial_time_independent_predictor( + 2.0, 1.0, - log_rate, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 2.0, LagClock::SystemTime ), Err(PsychometricError::EventTimeRequired) ); assert_eq!( - recover_asymptotic_time_independent_predictor_variance( - effect, + recover_discrete_observed_mean_with_initial_time_independent_predictor( + 2.0, 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, 0.0, LagClock::EventTime ), - Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) - ); - assert_eq!( - recover_asymptotic_time_independent_predictor_variance( - effect, - -1.0, - log_rate, - LagClock::EventTime - ), - Err(PsychometricError::InvalidNumericInput) + Err(PsychometricError::NonPositiveInterval) ); assert_eq!( - recover_asymptotic_time_independent_predictor_variance( + recover_discrete_observed_mean_with_initial_time_independent_predictor( + 1e308, + 0.0, + 0.0, + 0.0, 1e308, 1.0, - -1e-308, + 0.0, + 1.0, LagClock::EventTime ), Err(PsychometricError::InvalidNumericInput) ); + } + + #[test] + fn initial_time_dependent_predictor_recovers_table_three_t0_shift_and_carry() { + let effect = 0.4_f64; + let predictor = 3.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let shift = + recover_initial_time_dependent_predictor_effect(effect, predictor).expect("t0-tdpred"); + assert!((shift - 1.2).abs() < 1e-15); assert_eq!( - recover_asymptotic_time_independent_predictor_variance( - 1.0, - 1.0, - -1e-308, - LagClock::EventTime - ), - Err(PsychometricError::InvalidNumericInput) + recover_initial_time_dependent_predictor_effect(0.0, predictor), + Ok(0.0) ); assert_eq!( - recover_asymptotic_time_independent_predictor_variance( - 1e200, - 1.0, - -1e-200, - LagClock::EventTime - ), - Err(PsychometricError::InvalidNumericInput) + recover_initial_time_dependent_predictor_effect(effect, 0.0), + Ok(0.0) ); + let carry = recover_initial_time_dependent_predictor_carry( + effect, + predictor, + drift, + delta, + LagClock::EventTime, + ) + .expect("t0-td-carry"); + let expected = 1.2 * (drift * delta).exp(); + assert!((carry - expected).abs() < 1e-15); + let zero_drift = recover_initial_time_dependent_predictor_carry( + effect, + predictor, + 0.0, + delta, + LagClock::EventTime, + ) + .expect("zero-drift"); + assert!((zero_drift - 1.2).abs() < 1e-15); + let vanished = recover_initial_time_dependent_predictor_carry( + effect, + predictor, + -800.0, + 1.0, + LagClock::EventTime, + ) + .expect("underflow"); + assert_eq!(vanished.to_bits(), 0.0_f64.to_bits()); + let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("tdpred"); + let increment = recover_discrete_time_independent_predictor_effect( + effect, + predictor, + drift, + delta, + LagClock::EventTime, + ) + .expect("tipred"); + let tipred_shift = recover_initial_time_independent_predictor_effect(effect, predictor) + .expect("t0-tipred"); + let impulse_carry = recover_time_dependent_predictor_impulse_carry( + effect, + predictor, + drift, + delta, + 1.0, + LagClock::EventTime, + ) + .expect("td-carry"); + assert!((carry - shift).abs() > 1e-3); + assert!((carry - increment).abs() > 1e-3); + assert!((shift - increment).abs() > 1e-3); + assert!((shift - effect).abs() > 1e-3); + assert!((carry - impulse_carry).abs() > 1e-3); + // Algebraically a product, like M x and t0_b z, but Table 3 names a different matrix. + assert!((shift - impulse).abs() < 1e-15); + assert!((shift - tipred_shift).abs() < 1e-15); } #[test] - fn asymptotic_continuous_intercept_recovers_driver_table_two() { - // Driver et al. (2017, Table 2, p. 12; Eq. 3, p. 5; p. 16) - // name asymCINT the Δt → ∞ intercept contribution −κ / a. - // Reconstruct a from the printed LeisureTime TIPREDEFFECT - // −0.225 / asymTIPREDEFFECT −1.673. The printed 2-latent CINT - // values are not this scalar map. - let printed_effect = -0.225_f64; - let printed_asym = -1.673_f64; - let log_rate = -printed_effect / printed_asym; + fn initial_time_dependent_predictor_composes_evolved_mean_and_keeps_scale() { + let effect = 0.4_f64; + let predictor = 3.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let carry = recover_initial_time_dependent_predictor_carry( + effect, + predictor, + drift, + delta, + LagClock::EventTime, + ) + .expect("t0-td-carry"); + let initial = 1.0_f64; let intercept = 0.3_f64; - let recovered = - recover_asymptotic_continuous_intercept(intercept, log_rate, LagClock::EventTime) - .expect("asymCINT"); - let expected = intercept / -log_rate; - assert!((recovered - expected).abs() < 1e-12); - let unit = recover_asymptotic_continuous_intercept(1.0, log_rate, LagClock::EventTime) - .expect("unit-asymCINT"); - assert!((unit - 1.0 / -log_rate).abs() < 1e-12); - let synthetic = recover_asymptotic_continuous_intercept(0.3, -0.5, LagClock::EventTime) - .expect("synthetic"); - assert!((synthetic - 0.6).abs() < 1e-15); - let large_delta = recover_discrete_continuous_intercept_effect( + let composed = recover_discrete_latent_mean_with_initial_time_dependent_predictor( + initial, + drift, intercept, - log_rate, - 1e8, + effect, + predictor, + delta, LagClock::EventTime, ) - .expect("large-delta"); - assert!((recovered - large_delta).abs() < 1e-9); + .expect("eq3-t0tdpred"); + let evolved = + recover_discrete_latent_mean(initial, drift, intercept, delta, LagClock::EventTime) + .expect("mu-t"); + assert!((composed - (evolved + carry)).abs() < 1e-15); assert_eq!( - recover_asymptotic_continuous_intercept(0.0, 0.0, LagClock::EventTime), - Ok(0.0) + recover_discrete_latent_mean_with_initial_time_dependent_predictor( + initial, + drift, + intercept, + 0.0, + predictor, + delta, + LagClock::EventTime + ), + Ok(evolved) ); assert_eq!( - recover_asymptotic_continuous_intercept(0.0, 0.5, LagClock::EventTime), - Ok(0.0) + recover_discrete_latent_mean_with_initial_time_dependent_predictor( + 0.0, + drift, + 0.0, + effect, + predictor, + delta, + LagClock::EventTime + ), + Ok(carry) + ); + let scaled = recover_initial_time_dependent_predictor_carry( + 1e308, + 1e-308, + 0.0, + 1.0, + LagClock::EventTime, + ) + .expect("scale"); + assert!((scaled - 1.0).abs() < 1e-15); + let rewritten = recover_initial_time_dependent_predictor_carry( + 2.0, + 0.5, + 710.0, + 1.0, + LagClock::EventTime, ); + assert_eq!(rewritten, Err(PsychometricError::InvalidNumericInput)); + let finite_rewrite = recover_initial_time_dependent_predictor_carry( + 1e-308, + 1.0, + 700.0, + 1.0, + LagClock::EventTime, + ) + .expect("log-rewrite"); + let expected_rewrite = (1e-308_f64.ln() + 700.0).exp(); + assert!((finite_rewrite - expected_rewrite).abs() / expected_rewrite < 1e-12); } #[test] - fn asymptotic_continuous_intercept_is_not_cint_increment_t0_or_tipred() { - let intercept = 0.3_f64; - let log_rate = -0.134_488_942_f64; - let recovered = - recover_asymptotic_continuous_intercept(intercept, log_rate, LagClock::EventTime) - .expect("asymCINT"); - let discrete = recover_discrete_continuous_intercept_effect( - intercept, - log_rate, - 1.0, + fn initial_time_dependent_predictor_refuses_impulse_cint_process_and_coefficient() { + let effect = 0.4_f64; + let predictor = 3.0_f64; + let shift = + recover_initial_time_dependent_predictor_effect(effect, predictor).expect("t0-tdpred"); + let carry = recover_initial_time_dependent_predictor_carry( + effect, + predictor, + -0.5, + 2.0, + LagClock::EventTime, + ) + .expect("t0-td-carry"); + let increment = recover_discrete_time_independent_predictor_effect( + effect, + predictor, + -0.5, + 2.0, LagClock::EventTime, ) - .expect("dtCINT"); - let tipred = recover_asymptotic_time_independent_predictor_effect( - -0.225, + .expect("tipred"); + let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("tdpred"); + let tipred_shift = recover_initial_time_independent_predictor_effect(effect, predictor) + .expect("t0-tipred"); + let impulse_carry = recover_time_dependent_predictor_impulse_carry( + effect, + predictor, + -0.5, + 2.0, 1.0, - log_rate, LagClock::EventTime, ) - .expect("asymTIPREDEFFECT"); - assert!((recovered - intercept).abs() > 1e-3); - assert!((recovered - discrete).abs() > 1e-3); - assert!((recovered - 2.823).abs() > 1e-3); - assert!((recovered - tipred).abs() > 1e-3); - assert_eq!( - refuse_asymptotic_continuous_intercept_as_continuous_intercept(recovered, intercept), - Err(PsychometricError::AsymptoticContinuousInterceptIsNotContinuousIntercept) - ); - assert_eq!( - refuse_asymptotic_continuous_intercept_as_discrete_increment(recovered, discrete), - Err(PsychometricError::AsymptoticContinuousInterceptIsNotDiscreteIncrement) - ); + .expect("td-carry"); assert_eq!( - refuse_asymptotic_continuous_intercept_as_initial_latent_mean(recovered, 2.823), - Err(PsychometricError::AsymptoticContinuousInterceptIsNotInitialLatentMean) + refuse_initial_time_dependent_effect_as_contemporaneous_impulse(shift, impulse), + Err(PsychometricError::InitialTimeDependentEffectIsNotContemporaneousImpulse) ); assert_eq!( - refuse_asymptotic_continuous_intercept_as_asymptotic_time_independent_effect( - recovered, tipred - ), - Err( - PsychometricError::AsymptoticContinuousInterceptIsNotAsymptoticTimeIndependentEffect - ) + refuse_initial_time_dependent_carry_as_initial_effect(carry, shift), + Err(PsychometricError::InitialTimeDependentCarryIsNotInitialEffect) ); - } - - #[test] - fn asymptotic_continuous_intercept_invalid_inputs_fail_closed() { - let intercept = 0.3_f64; - let log_rate = -0.134_488_942_f64; assert_eq!( - recover_asymptotic_continuous_intercept(intercept, log_rate, LagClock::SystemTime), - Err(PsychometricError::EventTimeRequired) + refuse_initial_time_dependent_effect_as_continuous_intercept(shift, 0.4), + Err(PsychometricError::InitialTimeDependentEffectIsNotContinuousIntercept) ); assert_eq!( - recover_asymptotic_continuous_intercept(intercept, 0.0, LagClock::EventTime), - Err(PsychometricError::AsymptoticContinuousInterceptRequiresStableDrift) + refuse_initial_time_dependent_effect_as_process_increment(shift, increment), + Err(PsychometricError::InitialTimeDependentEffectIsNotProcessIncrement) ); assert_eq!( - recover_asymptotic_continuous_intercept(intercept, 0.5, LagClock::EventTime), - Err(PsychometricError::AsymptoticContinuousInterceptRequiresStableDrift) + refuse_initial_time_dependent_effect_as_initial_time_independent_effect( + shift, + tipred_shift + ), + Err(PsychometricError::InitialTimeDependentEffectIsNotInitialTimeIndependentEffect) ); assert_eq!( - recover_asymptotic_continuous_intercept(f64::NAN, log_rate, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) + refuse_initial_time_dependent_coefficient_as_initial_effect(effect, shift), + Err(PsychometricError::InitialTimeDependentCoefficientIsNotInitialEffect) ); assert_eq!( - recover_asymptotic_continuous_intercept(1e308, -1e-308, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) + refuse_initial_time_dependent_carry_as_impulse_carry(carry, impulse_carry), + Err(PsychometricError::InitialTimeDependentCarryIsNotImpulseCarry) ); } #[test] - fn stationary_initial_latent_mean_recovers_driver_page_sixteen() { - // Driver et al. (2017, p. 16; Table 2, p. 12; Eq. 3) - // constrain T0MEANS to model-implied values that include - // extra effects due to time-independent predictors - // (asymTIPREDEFFECT). Reconstruct a from printed LeisureTime - // TIPREDEFFECT −0.225 / asymTIPREDEFFECT −1.673. The printed - // 2-latent T0MEANS 2.823 is not this scalar map. - let printed_effect = -0.225_f64; - let printed_asym = -1.673_f64; - let log_rate = -printed_effect / printed_asym; - let intercept = 0.3_f64; - let recovered = recover_stationary_initial_latent_mean( - intercept, - printed_effect, - 1.0, - log_rate, - LagClock::EventTime, - ) - .expect("stationary T0MEANS"); - let intercept_only = - recover_asymptotic_continuous_intercept(intercept, log_rate, LagClock::EventTime) - .expect("asymCINT"); - let tipred = recover_asymptotic_time_independent_predictor_effect( - printed_effect, - 1.0, - log_rate, - LagClock::EventTime, - ) - .expect("asymTIPREDEFFECT"); - assert!((recovered - (intercept_only + tipred)).abs() < 1e-12); - let synthetic = - recover_stationary_initial_latent_mean(0.3, 0.2, 1.0, -0.5, LagClock::EventTime) - .expect("synthetic"); - assert!((synthetic - 1.0).abs() < 1e-15); - let intercept_only_path = recover_stationary_initial_latent_mean( - intercept, - 0.0, - 1.0, - log_rate, - LagClock::EventTime, - ) - .expect("intercept-only"); - assert!((intercept_only_path - intercept_only).abs() < 1e-15); - let tipred_only = recover_stationary_initial_latent_mean( - 0.0, - printed_effect, - 1.0, - log_rate, - LagClock::EventTime, - ) - .expect("ti-only"); - assert!((tipred_only - tipred).abs() < 1e-15); + fn initial_time_dependent_predictor_invalid_inputs_fail_closed() { assert_eq!( - recover_stationary_initial_latent_mean(0.0, 0.0, 1.0, 0.0, LagClock::EventTime), - Ok(0.0) + recover_initial_time_dependent_predictor_effect(f64::NAN, 1.0), + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_stationary_initial_latent_mean(0.0, 0.0, 1.0, 0.5, LagClock::EventTime), - Ok(0.0) + recover_initial_time_dependent_predictor_effect(1.0, f64::INFINITY), + Err(PsychometricError::InvalidNumericInput) ); - } - - #[test] - fn stationary_initial_latent_mean_is_not_t0_cint_tipred_or_discrete() { - let intercept = 0.3_f64; - let log_rate = -0.134_488_942_f64; - let recovered = recover_stationary_initial_latent_mean( - intercept, - -0.225, - 1.0, - log_rate, - LagClock::EventTime, - ) - .expect("stationary T0MEANS"); - let intercept_only = - recover_asymptotic_continuous_intercept(intercept, log_rate, LagClock::EventTime) - .expect("asymCINT"); - let tipred = recover_asymptotic_time_independent_predictor_effect( - -0.225, - 1.0, - log_rate, - LagClock::EventTime, - ) - .expect("asymTIPREDEFFECT"); - let discrete = - recover_discrete_latent_mean(2.823, log_rate, intercept, 1.0, LagClock::EventTime) - .expect("μ_t"); - assert!((recovered - 2.823).abs() > 1e-3); - assert!((recovered - intercept_only).abs() > 1e-3); - assert!((recovered - tipred).abs() > 1e-3); - assert!((recovered - discrete).abs() > 1e-3); assert_eq!( - refuse_stationary_initial_latent_mean_as_initial_latent_mean(recovered, 2.823), - Err(PsychometricError::StationaryInitialLatentMeanIsNotInitialLatentMean) + recover_initial_time_dependent_predictor_effect(1e308, 2.0), + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - refuse_stationary_initial_latent_mean_as_asymptotic_continuous_intercept( - recovered, - intercept_only + recover_initial_time_dependent_predictor_carry( + 0.4, + 3.0, + f64::NAN, + 2.0, + LagClock::EventTime ), - Err(PsychometricError::StationaryInitialLatentMeanIsNotAsymptoticContinuousIntercept) + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - refuse_stationary_initial_latent_mean_as_asymptotic_time_independent_effect( - recovered, tipred + recover_initial_time_dependent_predictor_carry( + 0.4, + 3.0, + -0.5, + 0.0, + LagClock::EventTime ), - Err(PsychometricError::StationaryInitialLatentMeanIsNotAsymptoticTimeIndependentEffect) - ); - assert_eq!( - refuse_stationary_initial_latent_mean_as_discrete_mean(recovered, discrete), - Err(PsychometricError::StationaryInitialLatentMeanIsNotDiscreteMean) + Err(PsychometricError::NonPositiveInterval) ); - } - - #[test] - fn stationary_initial_latent_mean_invalid_inputs_fail_closed() { - let intercept = 0.3_f64; - let log_rate = -0.134_488_942_f64; assert_eq!( - recover_stationary_initial_latent_mean( - intercept, - -0.225, - 1.0, - log_rate, + recover_initial_time_dependent_predictor_carry( + 0.4, + 3.0, + -0.5, + 2.0, LagClock::SystemTime ), Err(PsychometricError::EventTimeRequired) ); assert_eq!( - recover_stationary_initial_latent_mean(intercept, 0.0, 1.0, 0.0, LagClock::EventTime), - Err(PsychometricError::AsymptoticContinuousInterceptRequiresStableDrift) + recover_discrete_latent_mean_with_initial_time_dependent_predictor( + 1e308, + 0.0, + 0.0, + 1e308, + 1.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_stationary_initial_latent_mean(0.0, -0.225, 1.0, 0.5, LagClock::EventTime), - Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) + recover_initial_time_dependent_predictor_carry( + 1.0, + 1.0, + f64::INFINITY, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_stationary_initial_latent_mean( + recover_initial_time_dependent_predictor_carry( f64::NAN, - -0.225, 1.0, - log_rate, + -0.5, + 2.0, LagClock::EventTime ), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_stationary_initial_latent_mean(1e308, 1e308, 1.0, -1e-308, LagClock::EventTime), + recover_initial_time_dependent_predictor_carry( + 1.0, + 1.0, + 1e308, + 10.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_latent_mean_with_initial_time_dependent_predictor( + 1.0, + -0.5, + 0.3, + f64::NAN, + 1.0, + 2.0, + LagClock::EventTime + ), Err(PsychometricError::InvalidNumericInput) ); } #[test] - fn stationary_initial_observed_mean_recovers_driver_equation_five_of_section_four_point_three() + #[allow(clippy::too_many_lines)] + fn discrete_observed_mean_with_initial_time_dependent_predictor_recovers_driver_equation_five() { - // Driver et al. (2017, §4.3, pp. 9–10; Eq. 5, p. 5) - // constrain first-occasion means to the model-predicted - // mean. Equation 5 maps E(y_0) = τ + λ of that mean. - let printed_effect = -0.225_f64; - let printed_asym = -1.673_f64; - let log_rate = -printed_effect / printed_asym; - let intercept = 0.3_f64; let loading = 2.0_f64; + let drift = -0.5_f64; + let delta = 2.0_f64; + let effect = 0.4_f64; + let predictor = 3.0_f64; + let initial = 1.0_f64; + let intercept = 0.3_f64; let manifest_mean = 0.5_f64; - let recovered = recover_stationary_initial_observed_mean( + let recovered = recover_discrete_observed_mean_with_initial_time_dependent_predictor( loading, + initial, + drift, intercept, - printed_effect, - 1.0, - log_rate, + effect, + predictor, manifest_mean, + delta, LagClock::EventTime, ) - .expect("eq5-stationary-T0MEANS"); - let latent = recover_stationary_initial_latent_mean( + .expect("eq5-t0tdpred-mean"); + let composed = recover_discrete_latent_mean_with_initial_time_dependent_predictor( + initial, + drift, intercept, - printed_effect, - 1.0, - log_rate, + effect, + predictor, + delta, LagClock::EventTime, ) - .expect("stationary T0MEANS"); - let expected = - recover_manifest_observed_mean(loading, latent, manifest_mean).expect("τ+λμ"); - assert!((recovered - expected).abs() < 1e-12); - let intercept_only = - recover_asymptotic_continuous_intercept(intercept, log_rate, LagClock::EventTime) - .expect("asymCINT"); - let intercept_only_observed = - recover_manifest_observed_mean(loading, intercept_only, manifest_mean) - .expect("τ+λ(−κ/a)"); - assert!((recovered - intercept_only_observed).abs() > 1e-3); - let free_initial_observed = - recover_manifest_observed_mean(loading, 2.823, manifest_mean).expect("τ+λμ_0"); - assert!((recovered - free_initial_observed).abs() > 1e-3); - let evolved_from_free = recover_discrete_observed_mean( + .expect("eq3-t0tdpred"); + let expected = manifest_mean + loading * composed; + assert!((recovered - expected).abs() < 1e-15); + let evolved_observed = recover_discrete_observed_mean( loading, - 2.823, - log_rate, + initial, + drift, + intercept, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq3-eq5-mean"); + let process_observed = recover_discrete_observed_mean_with_time_independent_predictor( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-tipred"); + let impulse_observed = recover_discrete_observed_mean_with_impulse( + loading, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime, + ) + .expect("eq5-impulse"); + let carry_observed = recover_discrete_observed_mean_with_impulse_carry( + loading, + initial, + drift, intercept, + effect, + predictor, manifest_mean, + delta, 1.0, LagClock::EventTime, ) - .expect("τ+λμ_t"); - assert!((recovered - evolved_from_free).abs() > 1e-3); - assert!((recovered - manifest_mean).abs() > 1e-3); - assert!((recovered - latent).abs() > 1e-3); - assert_eq!( - recover_stationary_initial_observed_mean( - 0.0, + .expect("eq5-carry"); + let tipred_observed = + recover_discrete_observed_mean_with_initial_time_independent_predictor( + loading, + initial, + drift, intercept, - printed_effect, - 1.0, - log_rate, + effect, + predictor, manifest_mean, + delta, LagClock::EventTime, - ), - Ok(manifest_mean) - ); + ) + .expect("eq5-t0tipred"); + assert!((recovered - evolved_observed).abs() > 1e-3); + assert!((recovered - process_observed).abs() > 1e-3); + assert!((recovered - impulse_observed).abs() > 1e-3); + assert!((recovered - carry_observed).abs() > 1e-3); + // Same numbers as T0TIPRED yield the same product, but Table 3 names a different matrix. + assert!((recovered - tipred_observed).abs() < 1e-15); assert_eq!( - recover_stationary_initial_observed_mean( - loading, - 0.0, - 0.0, - 1.0, + recover_discrete_observed_mean_with_initial_time_dependent_predictor( 0.0, + initial, + drift, + intercept, + effect, + predictor, manifest_mean, - LagClock::EventTime, + delta, + LagClock::EventTime ), Ok(manifest_mean) ); - let evolved_from_stationary = - recover_discrete_observed_mean_with_time_independent_predictor( - loading, - latent, - log_rate, - intercept, - printed_effect, - 1.0, - manifest_mean, - 2.0, - LagClock::EventTime, - ) - .expect("invariance"); - assert!((evolved_from_stationary - recovered).abs() < 1e-12); - let evolved_latent = recover_discrete_latent_mean_with_time_independent_predictor( - latent, - log_rate, - intercept, - printed_effect, + assert!((recovered - composed).abs() > 1e-3); + assert!((recovered - manifest_mean).abs() > 1e-3); + } + + #[test] + fn discrete_observed_mean_with_initial_time_dependent_predictor_refuses_evolved_mean_and_overflow() + { + let recovered = recover_discrete_observed_mean_with_initial_time_dependent_predictor( + 2.0, 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, 2.0, LagClock::EventTime, ) - .expect("stationary invariance"); - assert!((evolved_latent - latent).abs() < 1e-12); - } - - #[test] - fn stationary_initial_observed_mean_is_not_manifest_latent_evolved_or_free() { - let intercept = 0.3_f64; - let log_rate = -0.134_488_942_f64; - let loading = 2.0_f64; - let manifest_mean = 0.5_f64; - let recovered = recover_stationary_initial_observed_mean( - loading, - intercept, - -0.225, + .expect("eq5-t0tdpred"); + let evolved = + recover_discrete_observed_mean(2.0, 1.0, -0.5, 0.3, 0.5, 2.0, LagClock::EventTime) + .expect("evolved"); + let process = recover_discrete_observed_mean_with_time_independent_predictor( + 2.0, 1.0, - log_rate, - manifest_mean, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 2.0, LagClock::EventTime, ) - .expect("eq5-stationary-T0MEANS"); - let latent = recover_stationary_initial_latent_mean( - intercept, - -0.225, + .expect("tipred"); + let impulse = recover_discrete_observed_mean_with_impulse( + 2.0, 1.0, - log_rate, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 2.0, LagClock::EventTime, ) - .expect("stationary T0MEANS"); - let intercept_only = - recover_asymptotic_continuous_intercept(intercept, log_rate, LagClock::EventTime) - .expect("asymCINT"); - let intercept_only_observed = - recover_manifest_observed_mean(loading, intercept_only, manifest_mean) - .expect("τ+λ(−κ/a)"); - let free_initial_observed = - recover_manifest_observed_mean(loading, 2.823, manifest_mean).expect("τ+λμ_0"); - let evolved = recover_discrete_observed_mean( - loading, - 2.823, - log_rate, - intercept, - manifest_mean, + .expect("impulse"); + let carry = recover_discrete_observed_mean_with_impulse_carry( + 2.0, + 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 2.0, + 1.0, + LagClock::EventTime, + ) + .expect("carry"); + let tipred = recover_discrete_observed_mean_with_initial_time_independent_predictor( + 2.0, 1.0, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 2.0, LagClock::EventTime, ) - .expect("τ+λμ_t"); + .expect("t0tipred"); assert_eq!( - refuse_stationary_initial_latent_mean_as_observed_mean(latent, recovered), - Err(PsychometricError::StationaryInitialLatentMeanIsNotObservedMean) + refuse_evolved_observed_mean_as_initial_time_dependent_observed_mean( + evolved, recovered + ), + Err(PsychometricError::EvolvedObservedMeanIsNotInitialTimeDependentObservedMean) ); assert_eq!( - refuse_stationary_initial_observed_mean_as_manifest_means(recovered, manifest_mean), - Err(PsychometricError::StationaryInitialObservedMeanIsNotManifestMeans) + refuse_time_independent_observed_mean_as_initial_time_dependent_observed_mean( + process, recovered + ), + Err( + PsychometricError::TimeIndependentObservedMeanIsNotInitialTimeDependentObservedMean + ) ); assert_eq!( - refuse_evolved_observed_mean_as_stationary_initial_observed_mean(evolved, recovered), - Err(PsychometricError::EvolvedObservedMeanIsNotStationaryInitialObservedMean) + refuse_impulse_observed_mean_as_initial_time_dependent_observed_mean( + impulse, recovered + ), + Err(PsychometricError::ImpulseObservedMeanIsNotInitialTimeDependentObservedMean) ); assert_eq!( - refuse_asymptotic_continuous_intercept_observed_mean_as_stationary_initial_observed_mean( - intercept_only_observed, - recovered + refuse_impulse_carry_observed_mean_as_initial_time_dependent_observed_mean( + carry, recovered ), - Err( - PsychometricError::AsymptoticContinuousInterceptObservedMeanIsNotStationaryInitialObservedMean - ) + Err(PsychometricError::ImpulseCarryObservedMeanIsNotInitialTimeDependentObservedMean) ); assert_eq!( - refuse_initial_observed_mean_as_stationary_initial_observed_mean( - free_initial_observed, - recovered + refuse_initial_time_independent_observed_mean_as_initial_time_dependent_observed_mean( + tipred, recovered ), - Err(PsychometricError::InitialObservedMeanIsNotStationaryInitialObservedMean) + Err(PsychometricError::InitialTimeIndependentObservedMeanIsNotInitialTimeDependentObservedMean) ); } #[test] - fn stationary_initial_observed_mean_invalid_inputs_fail_closed() { - let intercept = 0.3_f64; - let log_rate = -0.134_488_942_f64; - assert_eq!( - recover_stationary_initial_observed_mean( - 2.0, - intercept, - -0.225, - 1.0, - log_rate, - 0.5, - LagClock::SystemTime - ), - Err(PsychometricError::EventTimeRequired) - ); + fn discrete_observed_mean_with_initial_time_dependent_predictor_invalid_inputs_fail_closed() { + let scaled = recover_discrete_observed_mean_with_initial_time_dependent_predictor( + 1e308, + 1e-308, + 0.0, + 0.0, + 0.0, + 3.0, + 0.0, + 1.0, + LagClock::EventTime, + ) + .expect("scale"); + assert!((scaled - 1.0).abs() < 1e-15); assert_eq!( - recover_stationary_initial_observed_mean( + recover_discrete_observed_mean_with_initial_time_dependent_predictor( + 1e308, 2.0, - intercept, 0.0, - 1.0, 0.0, - 0.5, + 0.0, + 3.0, + 0.0, + 1.0, LagClock::EventTime ), - Err(PsychometricError::AsymptoticContinuousInterceptRequiresStableDrift) + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_stationary_initial_observed_mean( + recover_discrete_observed_mean_with_initial_time_dependent_predictor( 2.0, - 0.0, - -0.225, 1.0, + -0.5, + 0.3, + 0.4, + 3.0, 0.5, - 0.5, - LagClock::EventTime + 2.0, + LagClock::SystemTime ), - Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) + Err(PsychometricError::EventTimeRequired) ); assert_eq!( - recover_stationary_initial_observed_mean( - f64::NAN, - intercept, - -0.225, + recover_discrete_observed_mean_with_initial_time_dependent_predictor( + 2.0, 1.0, - log_rate, + -0.5, + 0.3, + 0.4, + 3.0, 0.5, + 0.0, LagClock::EventTime ), - Err(PsychometricError::InvalidNumericInput) + Err(PsychometricError::NonPositiveInterval) ); assert_eq!( - recover_stationary_initial_observed_mean( - 2.0, + recover_discrete_observed_mean_with_initial_time_dependent_predictor( 1e308, + 0.0, + 0.0, + 0.0, 1e308, 1.0, - -1e-308, - 0.5, + 0.0, + 1.0, LagClock::EventTime ), Err(PsychometricError::InvalidNumericInput) @@ -9547,20 +20010,36 @@ mod tests { } #[test] - fn stationary_initial_latent_variance_recovers_driver_section_four_point_three() { - // Driver et al. (2017, §4.3, pp. 9–10; p. 16) constrain T0VAR - // to model-predicted variances. The scalar composition is - // trait + −q / (2 a) + (B / a)² v. Reconstruct a from printed - // LeisureTime TIPREDEFFECT −0.225 / asymTIPREDEFFECT −1.673. - // The printed 2-latent addedTIPREDVAR 2.838 is not this - // scalar map. + #[allow(clippy::too_many_lines)] + fn predetermined_initial_latent_variance_recovers_driver_section_four_point_three() { + // Driver et al. (2017, §4.3): Var(η_0) = + // trait + p_0 + (B / a)² v. let printed_effect = -0.225_f64; let printed_asym = -1.673_f64; let log_rate = -printed_effect / printed_asym; let trait_variance = 1.0_f64; + let initial_latent_variance = 2.0_f64; let diffusion = 0.4_f64; let predictor_variance = 1.0_f64; - let recovered = recover_stationary_initial_latent_variance( + let event_delta = 1.0_f64; + let recovered = recover_predetermined_initial_latent_variance( + trait_variance, + initial_latent_variance, + printed_effect, + predictor_variance, + log_rate, + LagClock::EventTime, + ) + .expect("predetermined initial T0VAR"); + let added = recover_asymptotic_time_independent_predictor_variance( + printed_effect, + predictor_variance, + log_rate, + LagClock::EventTime, + ) + .expect("addedTIPREDVAR"); + assert!((recovered - (trait_variance + initial_latent_variance + added)).abs() < 1e-12); + let stationary = recover_stationary_initial_latent_variance( trait_variance, diffusion, printed_effect, @@ -9568,51 +20047,69 @@ mod tests { log_rate, LagClock::EventTime, ) - .expect("stationary T0VAR"); + .expect("stationary initial T0VAR"); + assert!((recovered - stationary).abs() > 1e-3); let state = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) .expect("asymDIFFUSION"); - let trait_plus_state = - recover_trait_plus_state_latent_variance(trait_variance, state).expect("trait+state"); - let added = recover_asymptotic_time_independent_predictor_variance( + let from_stationary_start = recover_predetermined_initial_latent_variance( + trait_variance, + state, printed_effect, predictor_variance, log_rate, LagClock::EventTime, ) - .expect("addedTIPREDVAR"); - assert!((recovered - (trait_plus_state + added)).abs() < 1e-12); - let state_only = recover_stationary_initial_latent_variance( - 0.0, + .expect("p_0=−q/(2a)"); + assert!((from_stationary_start - stationary).abs() < 1e-12); + let lagged = recover_predetermined_lagged_latent_covariance( + trait_variance, + initial_latent_variance, + printed_effect, + predictor_variance, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("predetermined lagged T0VAR"); + assert!((recovered - lagged).abs() > 1e-3); + let later = recover_predetermined_later_latent_variance( + trait_variance, + initial_latent_variance, diffusion, - 0.0, + printed_effect, predictor_variance, log_rate, + event_delta, LagClock::EventTime, ) - .expect("state-only"); - assert!((state_only - state).abs() < 1e-15); - let trait_only = recover_stationary_initial_latent_variance( + .expect("predetermined later T0VAR"); + assert!((recovered - later).abs() > 1e-3); + assert!((recovered - initial_latent_variance).abs() > 1e-3); + let near_lagged = recover_predetermined_lagged_latent_covariance( trait_variance, - 0.0, - 0.0, + initial_latent_variance, + printed_effect, predictor_variance, - 0.0, + log_rate, + 1e-12, LagClock::EventTime, ) - .expect("trait-only"); - assert!((trait_only - trait_variance).abs() < 1e-15); - let added_only = recover_stationary_initial_latent_variance( - 0.0, - 0.0, + .expect("Δt→0+ lagged"); + assert!((near_lagged - recovered).abs() < 1e-9); + let near_later = recover_predetermined_later_latent_variance( + trait_variance, + initial_latent_variance, + diffusion, printed_effect, predictor_variance, log_rate, + 1e-12, LagClock::EventTime, ) - .expect("ti-only"); - assert!((added_only - added).abs() < 1e-15); + .expect("Δt→0+ later"); + assert!((near_later - recovered).abs() < 1e-9); assert_eq!( - recover_stationary_initial_latent_variance( + recover_predetermined_initial_latent_variance( 0.0, 0.0, 0.0, @@ -9622,25 +20119,45 @@ mod tests { ), Ok(0.0) ); - assert_eq!( - recover_stationary_initial_latent_variance( - 0.0, - 0.0, - 0.0, - predictor_variance, - 0.5, - LagClock::EventTime - ), - Ok(0.0) - ); + let trait_only = recover_predetermined_initial_latent_variance( + trait_variance, + 0.0, + 0.0, + predictor_variance, + 0.0, + LagClock::EventTime, + ) + .expect("trait-only predetermined initial"); + assert!((trait_only - trait_variance).abs() < 1e-15); + let unstable_trait = recover_predetermined_initial_latent_variance( + trait_variance, + 0.0, + 0.0, + 0.0, + 0.5, + LagClock::EventTime, + ) + .expect("trait-only a≥0"); + assert!((unstable_trait - trait_variance).abs() < 1e-15); } #[test] - fn stationary_initial_latent_variance_is_not_t0_state_trait_tipred_or_discrete() { + fn predetermined_initial_latent_variance_is_not_stationary_lagged_or_later() { let trait_variance = 1.0_f64; + let initial_latent_variance = 2.0_f64; let diffusion = 0.4_f64; let log_rate = -0.134_488_942_f64; - let recovered = recover_stationary_initial_latent_variance( + let event_delta = 1.0_f64; + let recovered = recover_predetermined_initial_latent_variance( + trait_variance, + initial_latent_variance, + -0.225, + 1.0, + log_rate, + LagClock::EventTime, + ) + .expect("predetermined initial T0VAR"); + let stationary = recover_stationary_initial_latent_variance( trait_variance, diffusion, -0.225, @@ -9648,64 +20165,61 @@ mod tests { log_rate, LagClock::EventTime, ) - .expect("stationary T0VAR"); - let state = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) - .expect("asymDIFFUSION"); - let added = recover_asymptotic_time_independent_predictor_variance( + .expect("stationary initial T0VAR"); + let lagged = recover_predetermined_lagged_latent_covariance( + trait_variance, + initial_latent_variance, -0.225, 1.0, log_rate, + event_delta, LagClock::EventTime, ) - .expect("addedTIPREDVAR"); - let discrete = recover_discrete_latent_variance( - recovered, + .expect("predetermined lagged T0VAR"); + let later = recover_predetermined_later_latent_variance( + trait_variance, + initial_latent_variance, diffusion, - log_rate, + -0.225, 1.0, + log_rate, + event_delta, LagClock::EventTime, ) - .expect("Var(η_t)"); - assert!((recovered - 2.0).abs() > 1e-3); - assert!((recovered - state).abs() > 1e-3); - assert!((recovered - trait_variance).abs() > 1e-3); - assert!((recovered - added).abs() > 1e-3); - assert!((recovered - discrete).abs() > 1e-3); - assert!((recovered - 2.838).abs() > 1e-3); - assert_eq!( - refuse_stationary_initial_latent_variance_as_initial_latent_variance(recovered, 2.0), - Err(PsychometricError::StationaryInitialLatentVarianceIsNotInitialLatentVariance) - ); + .expect("predetermined later T0VAR"); assert_eq!( - refuse_stationary_initial_latent_variance_as_stationary_within_subject( - recovered, state + refuse_predetermined_initial_latent_variance_as_stationary_initial_latent_variance( + recovered, stationary ), - Err(PsychometricError::StationaryInitialLatentVarianceIsNotStationaryWithinSubject) + Err( + PsychometricError::PredeterminedInitialLatentVarianceIsNotStationaryInitialLatentVariance + ) ); assert_eq!( - refuse_stationary_initial_latent_variance_as_trait_variance(recovered, trait_variance), - Err(PsychometricError::StationaryInitialLatentVarianceIsNotTraitVariance) + refuse_predetermined_initial_latent_variance_as_initial_latent_variance( + recovered, + initial_latent_variance + ), + Err(PsychometricError::PredeterminedInitialLatentVarianceIsNotInitialLatentVariance) ); - assert_eq!( - refuse_stationary_initial_latent_variance_as_asymptotic_time_independent_variance( - recovered, added + assert_eq!( + refuse_predetermined_initial_latent_variance_as_lagged_latent_covariance( + recovered, lagged ), - Err( - PsychometricError::StationaryInitialLatentVarianceIsNotAsymptoticTimeIndependentVariance - ) + Err(PsychometricError::PredeterminedInitialLatentVarianceIsNotLaggedLatentCovariance) ); assert_eq!( - refuse_stationary_initial_latent_variance_as_discrete_variance(recovered, discrete), - Err(PsychometricError::StationaryInitialLatentVarianceIsNotDiscreteVariance) + refuse_predetermined_initial_latent_variance_as_later_latent_variance(recovered, later), + Err(PsychometricError::PredeterminedInitialLatentVarianceIsNotLaterLatentVariance) ); } #[test] - fn stationary_initial_latent_variance_invalid_inputs_fail_closed() { + fn predetermined_initial_latent_variance_invalid_inputs_fail_closed() { assert_eq!( - recover_stationary_initial_latent_variance( + recover_predetermined_initial_latent_variance( 1.0, - 0.4, + 2.0, -0.225, 1.0, -0.13, @@ -9714,18 +20228,7 @@ mod tests { Err(PsychometricError::EventTimeRequired) ); assert_eq!( - recover_stationary_initial_latent_variance( - 0.0, - 0.4, - 0.0, - 1.0, - 0.0, - LagClock::EventTime - ), - Err(PsychometricError::StationaryVarianceRequiresStableDrift) - ); - assert_eq!( - recover_stationary_initial_latent_variance( + recover_predetermined_initial_latent_variance( 0.0, 0.0, -0.225, @@ -9736,7 +20239,7 @@ mod tests { Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) ); assert_eq!( - recover_stationary_initial_latent_variance( + recover_predetermined_initial_latent_variance( 0.0, 0.0, 0.0, @@ -9746,10 +20249,20 @@ mod tests { ), Ok(0.0) ); + let brownian = recover_predetermined_initial_latent_variance( + 0.0, + 2.0, + 0.0, + 0.0, + 0.0, + LagClock::EventTime, + ) + .expect("Brownian a=0"); + assert!((brownian - 2.0).abs() < 1e-12); assert_eq!( - recover_stationary_initial_latent_variance( + recover_predetermined_initial_latent_variance( f64::NAN, - 0.4, + 2.0, 0.0, 0.0, -0.5, @@ -9758,7 +20271,7 @@ mod tests { Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_stationary_initial_latent_variance( + recover_predetermined_initial_latent_variance( f64::MAX, f64::MAX, 0.0, @@ -9769,7 +20282,7 @@ mod tests { Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_stationary_initial_latent_variance( + recover_predetermined_initial_latent_variance( f64::MAX, 0.0, 1.0, @@ -9782,24 +20295,24 @@ mod tests { } #[test] - fn stationary_initial_observed_variance_recovers_driver_equation_five_of_section_four_point_three() - { - // Driver et al. (2017, §4.3, pp. 9–10; Eq. 5, p. 5) - // constrain first-occasion variances to the model-predicted - // variance. Equation 5 maps Var(y_0) = λ² of that variance - // plus θ + ψ. + #[allow(clippy::too_many_lines)] + fn predetermined_initial_observed_variance_recovers_driver_equation_five() { + // Driver et al. (2017, Eq. 5 of predetermined T0VAR): + // λ²(trait + p_0 + (B / a)² v) + θ + ψ. let printed_effect = -0.225_f64; let printed_asym = -1.673_f64; let log_rate = -printed_effect / printed_asym; let trait_variance = 1.0_f64; + let initial_latent_variance = 2.0_f64; let diffusion = 0.4_f64; let loading = 2.0_f64; let measurement_error = 0.5_f64; let manifest_trait = 0.1_f64; - let recovered = recover_stationary_initial_observed_variance( + let event_delta = 1.0_f64; + let recovered = recover_predetermined_initial_observed_variance( loading, trait_variance, - diffusion, + initial_latent_variance, printed_effect, 1.0, log_rate, @@ -9807,16 +20320,16 @@ mod tests { manifest_trait, LagClock::EventTime, ) - .expect("eq5-stationary-T0VAR"); - let latent = recover_stationary_initial_latent_variance( + .expect("eq5-initial-predetermined-T0VAR"); + let latent = recover_predetermined_initial_latent_variance( trait_variance, - diffusion, + initial_latent_variance, printed_effect, 1.0, log_rate, LagClock::EventTime, ) - .expect("stationary T0VAR"); + .expect("predetermined initial T0VAR"); let expected = recover_manifest_trait_plus_state_observed_variance( loading, latent, @@ -9825,28 +20338,41 @@ mod tests { ) .expect("λ²p+θ+ψ"); assert!((recovered - expected).abs() < 1e-12); - let state = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) - .expect("asymDIFFUSION"); - let state_only_observed = - recover_manifest_observed_variance(loading, state, measurement_error) - .expect("λ²(−q/2a)+θ"); - assert!((recovered - state_only_observed).abs() > 1e-3); - let free_initial_observed = - recover_manifest_observed_variance(loading, 2.0, measurement_error).expect("λ²p_0+θ"); - assert!((recovered - free_initial_observed).abs() > 1e-3); - let discrete = - recover_discrete_latent_variance(latent, diffusion, log_rate, 1.0, LagClock::EventTime) - .expect("Var(η_t)"); - let evolved = recover_manifest_observed_variance(loading, discrete, measurement_error) - .expect("λ²Var(η_t)+θ"); - assert!((recovered - evolved).abs() > 1e-3); + let stationary = recover_stationary_initial_observed_variance( + loading, + trait_variance, + diffusion, + printed_effect, + 1.0, + log_rate, + measurement_error, + manifest_trait, + LagClock::EventTime, + ) + .expect("eq5-initial-stationary-T0VAR"); + assert!((recovered - stationary).abs() > 1e-3); + let later = recover_predetermined_later_observed_variance( + loading, + trait_variance, + initial_latent_variance, + diffusion, + printed_effect, + 1.0, + log_rate, + event_delta, + measurement_error, + manifest_trait, + LagClock::EventTime, + ) + .expect("eq5-later-predetermined-T0VAR"); + assert!((recovered - later).abs() > 1e-3); assert!((recovered - measurement_error).abs() > 1e-3); assert!((recovered - latent).abs() > 1e-3); assert_eq!( - recover_stationary_initial_observed_variance( + recover_predetermined_initial_observed_variance( 0.0, trait_variance, - diffusion, + initial_latent_variance, printed_effect, 1.0, log_rate, @@ -9857,118 +20383,55 @@ mod tests { Ok(measurement_error + manifest_trait) ); assert_eq!( - recover_stationary_initial_observed_variance( + recover_predetermined_initial_observed_variance( loading, 0.0, 0.0, 0.0, 1.0, 0.0, - measurement_error, + 0.0, 0.0, LagClock::EventTime, ), - Ok(measurement_error) - ); - let zero_manifest_trait = recover_stationary_initial_observed_variance( - loading, - trait_variance, - diffusion, - printed_effect, - 1.0, - log_rate, - measurement_error, - 0.0, - LagClock::EventTime, - ) - .expect("ψ=0"); - let expected_zero_psi = - recover_manifest_observed_variance(loading, latent, measurement_error).expect("λ²p+θ"); - assert!((zero_manifest_trait - expected_zero_psi).abs() < 1e-12); - } - - #[test] - fn stationary_initial_observed_variance_is_not_manifest_latent_evolved_or_free() { - let trait_variance = 1.0_f64; - let diffusion = 0.4_f64; - let log_rate = -0.134_488_942_f64; - let loading = 2.0_f64; - let measurement_error = 0.5_f64; - let recovered = recover_stationary_initial_observed_variance( - loading, - trait_variance, - diffusion, - -0.225, - 1.0, - log_rate, - measurement_error, - 0.1, - LagClock::EventTime, - ) - .expect("eq5-stationary-T0VAR"); - let latent = recover_stationary_initial_latent_variance( - trait_variance, - diffusion, - -0.225, - 1.0, - log_rate, - LagClock::EventTime, - ) - .expect("stationary T0VAR"); - let state = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) - .expect("asymDIFFUSION"); - let state_only_observed = - recover_manifest_observed_variance(loading, state, measurement_error) - .expect("λ²(−q/2a)+θ"); - let free_initial_observed = - recover_manifest_observed_variance(loading, 2.0, measurement_error).expect("λ²p_0+θ"); - let discrete = - recover_discrete_latent_variance(latent, diffusion, log_rate, 1.0, LagClock::EventTime) - .expect("Var(η_t)"); - let evolved = recover_manifest_observed_variance(loading, discrete, measurement_error) - .expect("λ²Var(η_t)+θ"); - assert_eq!( - refuse_stationary_initial_latent_variance_as_observed_variance(latent, recovered), - Err(PsychometricError::StationaryInitialLatentVarianceIsNotObservedVariance) + Ok(0.0) ); assert_eq!( - refuse_stationary_initial_observed_variance_as_measurement_error( - recovered, - measurement_error - ), - Err(PsychometricError::StationaryInitialObservedVarianceIsNotMeasurementError) + refuse_predetermined_initial_latent_variance_as_observed_variance(latent, recovered), + Err(PsychometricError::PredeterminedInitialLatentVarianceIsNotObservedVariance) ); assert_eq!( - refuse_evolved_observed_variance_as_stationary_initial_observed_variance( - evolved, recovered + refuse_measurement_error_as_predetermined_initial_observed_variance( + measurement_error, + recovered ), - Err(PsychometricError::EvolvedObservedVarianceIsNotStationaryInitialObservedVariance) + Err(PsychometricError::MeasurementErrorIsNotPredeterminedInitialObservedVariance) ); assert_eq!( - refuse_stationary_within_subject_observed_variance_as_stationary_initial_observed_variance( - state_only_observed, - recovered + refuse_stationary_initial_observed_variance_as_predetermined_initial_observed_variance( + stationary, recovered ), Err( - PsychometricError::StationaryWithinSubjectObservedVarianceIsNotStationaryInitialObservedVariance + PsychometricError::StationaryInitialObservedVarianceIsNotPredeterminedInitialObservedVariance ) ); assert_eq!( - refuse_initial_observed_variance_as_stationary_initial_observed_variance( - free_initial_observed, - recovered + refuse_predetermined_later_observed_variance_as_predetermined_initial_observed_variance( + later, recovered ), - Err(PsychometricError::InitialObservedVarianceIsNotStationaryInitialObservedVariance) + Err( + PsychometricError::PredeterminedLaterObservedVarianceIsNotPredeterminedInitialObservedVariance + ) ); } #[test] - fn stationary_initial_observed_variance_invalid_inputs_fail_closed() { + fn predetermined_initial_observed_variance_invalid_inputs_fail_closed() { assert_eq!( - recover_stationary_initial_observed_variance( + recover_predetermined_initial_observed_variance( 2.0, 1.0, - 0.4, + 2.0, -0.225, 1.0, -0.13, @@ -9979,35 +20442,34 @@ mod tests { Err(PsychometricError::EventTimeRequired) ); assert_eq!( - recover_stationary_initial_observed_variance( - 2.0, - 0.0, - 0.4, - 0.0, - 1.0, - 0.0, - 0.5, - 0.0, - LagClock::EventTime - ), - Err(PsychometricError::StationaryVarianceRequiresStableDrift) - ); - assert_eq!( - recover_stationary_initial_observed_variance( + recover_predetermined_initial_observed_variance( 2.0, 0.0, 0.0, -0.225, 1.0, 0.5, - 0.5, + 0.0, 0.0, LagClock::EventTime ), Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) ); + let brownian = recover_predetermined_initial_observed_variance( + 1.0, + 0.0, + 2.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + LagClock::EventTime, + ) + .expect("Brownian a=0"); + assert!((brownian - 2.0).abs() < 1e-12); assert_eq!( - recover_stationary_initial_observed_variance( + recover_predetermined_initial_observed_variance( 2.0, 0.0, 0.0, @@ -10021,28 +20483,28 @@ mod tests { Ok(0.6) ); assert_eq!( - recover_stationary_initial_observed_variance( + recover_predetermined_initial_observed_variance( + 2.0, f64::NAN, - 1.0, - 0.4, + 2.0, 0.0, 0.0, -0.5, - 0.5, + 0.0, 0.0, LagClock::EventTime ), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_stationary_initial_observed_variance( + recover_predetermined_initial_observed_variance( 2.0, f64::MAX, f64::MAX, 0.0, 0.0, -0.5, - 0.5, + 0.0, 0.0, LagClock::EventTime ), @@ -10052,38 +20514,39 @@ mod tests { #[test] #[allow(clippy::too_many_lines)] - fn stationary_lagged_latent_covariance_recovers_driver_section_four_point_three() { - // Driver et al. (2017, §4.3, pp. 9–10; Eq. 3–4, pp. 4–5; p. 16) - // constrain T0VAR. The lagged covariance of that stationary - // process is trait + e^{a Δt}(−q / (2 a)) + (B / a)² v. - // Trait and addedTIPREDVAR do not decay. + fn predetermined_later_lagged_latent_covariance_recovers_driver_equation_four_after_startoffset() + { + // Driver et al. (2017, §4.3 startoffset; Eq. 4): + // cov = trait + e^{a s}(e^{2 a u} p_0 + Q_u) + (B / a)² v. let printed_effect = -0.225_f64; let printed_asym = -1.673_f64; let log_rate = -printed_effect / printed_asym; let trait_variance = 1.0_f64; + let initial_latent_variance = 2.0_f64; let diffusion = 0.4_f64; let predictor_variance = 1.0_f64; - let event_delta = 1.0_f64; - let recovered = recover_stationary_lagged_latent_covariance( + let start_delta = 2.0_f64; + let lag_delta = 1.0_f64; + let recovered = recover_predetermined_later_lagged_latent_covariance( trait_variance, + initial_latent_variance, diffusion, printed_effect, predictor_variance, log_rate, - event_delta, + start_delta, + lag_delta, LagClock::EventTime, ) - .expect("stationary lagged T0VAR"); - let state = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) - .expect("asymDIFFUSION"); - let trait_plus_state = recover_trait_plus_state_lagged_covariance( - trait_variance, - state, + .expect("predetermined later-start lagged T0VAR"); + let later_state = recover_discrete_latent_variance( + initial_latent_variance, + diffusion, log_rate, - event_delta, + start_delta, LagClock::EventTime, ) - .expect("trait+state lagged"); + .expect("later state"); let added = recover_asymptotic_time_independent_predictor_variance( printed_effect, predictor_variance, @@ -10091,265 +20554,303 @@ mod tests { LagClock::EventTime, ) .expect("addedTIPREDVAR"); - assert!((recovered - (trait_plus_state + added)).abs() < 1e-12); - let contemporaneous = recover_stationary_initial_latent_variance( + let expected = recover_trait_plus_state_lagged_covariance( trait_variance, - diffusion, - printed_effect, - predictor_variance, + later_state, log_rate, + lag_delta, LagClock::EventTime, ) - .expect("stationary T0VAR"); - assert!((recovered - contemporaneous).abs() > 1e-3); - let decayed = recover_discrete_lagged_latent_covariance( - contemporaneous, + .expect("trait + e^{as} later-state") + + added; + assert!((recovered - expected).abs() < 1e-12); + let first_lagged = recover_predetermined_lagged_latent_covariance( + trait_variance, + initial_latent_variance, + printed_effect, + predictor_variance, log_rate, - event_delta, + lag_delta, LagClock::EventTime, ) - .expect("e^{aΔt} p_stat"); - assert!((recovered - decayed).abs() > 1e-3); - let state_only = recover_stationary_lagged_latent_covariance( - 0.0, + .expect("first-occasion lagged"); + assert!((recovered - first_lagged).abs() > 1e-3); + let later = recover_predetermined_later_latent_variance( + trait_variance, + initial_latent_variance, + diffusion, + printed_effect, + predictor_variance, + log_rate, + start_delta, + LagClock::EventTime, + ) + .expect("later variance"); + assert!((recovered - later).abs() > 1e-3); + let stationary_lagged = recover_stationary_lagged_latent_covariance( + trait_variance, diffusion, - 0.0, + printed_effect, predictor_variance, log_rate, - event_delta, + lag_delta, LagClock::EventTime, ) - .expect("state-only lagged"); - let lagged_state = recover_discrete_lagged_latent_covariance( + .expect("stationary lagged"); + assert!((recovered - stationary_lagged).abs() > 1e-3); + let decayed_later = later * lag_delta.mul_add(log_rate, 0.0).exp(); + assert!((recovered - decayed_later).abs() > 1e-3); + let state = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + let from_stationary_start = recover_predetermined_later_lagged_latent_covariance( + trait_variance, state, + diffusion, + printed_effect, + predictor_variance, log_rate, - event_delta, + start_delta, + lag_delta, LagClock::EventTime, ) - .expect("e^{aΔt} asymDIFFUSION"); - assert!((state_only - lagged_state).abs() < 1e-15); - let trait_only = recover_stationary_lagged_latent_covariance( + .expect("p_0=−q/(2a)"); + assert!((from_stationary_start - stationary_lagged).abs() < 1e-12); + let near_first = recover_predetermined_later_lagged_latent_covariance( trait_variance, - 0.0, - 0.0, + initial_latent_variance, + diffusion, + printed_effect, predictor_variance, - 0.0, - event_delta, + log_rate, + 1e-12, + lag_delta, LagClock::EventTime, ) - .expect("trait-only lagged"); - assert!((trait_only - trait_variance).abs() < 1e-15); - let added_only = recover_stationary_lagged_latent_covariance( - 0.0, - 0.0, + .expect("u→0+"); + assert!((near_first - first_lagged).abs() < 1e-9); + let near_later = recover_predetermined_later_lagged_latent_covariance( + trait_variance, + initial_latent_variance, + diffusion, printed_effect, predictor_variance, log_rate, - event_delta, + start_delta, + 1e-12, LagClock::EventTime, ) - .expect("ti-only lagged"); - assert!((added_only - added).abs() < 1e-15); + .expect("s→0+"); + assert!((near_later - later).abs() < 1e-9); assert_eq!( - recover_stationary_lagged_latent_covariance( + recover_predetermined_later_lagged_latent_covariance( + 0.0, 0.0, 0.0, 0.0, predictor_variance, 0.0, - event_delta, + start_delta, + lag_delta, LagClock::EventTime ), Ok(0.0) ); - let far = recover_stationary_lagged_latent_covariance( - trait_variance, - diffusion, - printed_effect, - predictor_variance, - log_rate, - 1e8, - LagClock::EventTime, - ) - .expect("Δt→∞"); - assert!((far - (trait_variance + added)).abs() < 1e-12); - let near = recover_stationary_lagged_latent_covariance( + let trait_only = recover_predetermined_later_lagged_latent_covariance( trait_variance, - diffusion, - printed_effect, + 0.0, + 0.0, + 0.0, predictor_variance, - log_rate, - 1e-12, + 0.0, + start_delta, + lag_delta, LagClock::EventTime, ) - .expect("Δt→0+"); - assert!((near - contemporaneous).abs() < 1e-9); + .expect("trait-only later-start lagged"); + assert!((trait_only - trait_variance).abs() < 1e-15); } #[test] - fn stationary_lagged_latent_covariance_is_not_contemporaneous_decayed_or_trait_state() { + fn predetermined_later_lagged_latent_covariance_is_not_first_later_or_stationary() { let trait_variance = 1.0_f64; + let initial_latent_variance = 2.0_f64; let diffusion = 0.4_f64; let log_rate = -0.134_488_942_f64; - let event_delta = 1.0_f64; - let recovered = recover_stationary_lagged_latent_covariance( + let start_delta = 2.0_f64; + let lag_delta = 1.0_f64; + let recovered = recover_predetermined_later_lagged_latent_covariance( trait_variance, + initial_latent_variance, diffusion, -0.225, 1.0, log_rate, - event_delta, + start_delta, + lag_delta, LagClock::EventTime, ) - .expect("stationary lagged T0VAR"); - let contemporaneous = recover_stationary_initial_latent_variance( + .expect("predetermined later-start lagged T0VAR"); + let first_lagged = recover_predetermined_lagged_latent_covariance( trait_variance, - diffusion, + initial_latent_variance, -0.225, 1.0, log_rate, + lag_delta, LagClock::EventTime, ) - .expect("stationary T0VAR"); - let decayed = recover_discrete_lagged_latent_covariance( - contemporaneous, + .expect("first-occasion lagged"); + let later = recover_predetermined_later_latent_variance( + trait_variance, + initial_latent_variance, + diffusion, + -0.225, + 1.0, log_rate, - event_delta, + start_delta, LagClock::EventTime, ) - .expect("e^{aΔt} p_stat"); - let state = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) - .expect("asymDIFFUSION"); - let trait_plus_state = recover_trait_plus_state_lagged_covariance( + .expect("later variance"); + let stationary_lagged = recover_stationary_lagged_latent_covariance( trait_variance, - state, + diffusion, + -0.225, + 1.0, log_rate, - event_delta, + lag_delta, LagClock::EventTime, ) - .expect("trait+state lagged"); - assert!((recovered - contemporaneous).abs() > 1e-3); - assert!((recovered - decayed).abs() > 1e-3); - assert!((recovered - trait_plus_state).abs() > 1e-3); + .expect("stationary lagged"); + let decayed_later = later * lag_delta.mul_add(log_rate, 0.0).exp(); assert_eq!( - refuse_stationary_lagged_latent_covariance_as_stationary_initial_latent_variance( - recovered, - contemporaneous + refuse_predetermined_later_lagged_latent_covariance_as_predetermined_lagged_covariance( + recovered, first_lagged ), Err( - PsychometricError::StationaryLaggedLatentCovarianceIsNotStationaryInitialLatentVariance + PsychometricError::PredeterminedLaterLaggedLatentCovarianceIsNotPredeterminedLaggedCovariance ) ); assert_eq!( - refuse_stationary_lagged_latent_covariance_as_decayed_stationary_variance( - recovered, decayed + refuse_predetermined_later_lagged_latent_covariance_as_later_latent_variance( + recovered, later ), - Err(PsychometricError::StationaryLaggedLatentCovarianceIsNotDecayedStationaryVariance) + Err( + PsychometricError::PredeterminedLaterLaggedLatentCovarianceIsNotLaterLatentVariance + ) ); assert_eq!( - refuse_trait_plus_state_lagged_covariance_as_stationary_lagged_latent_covariance( - trait_plus_state, - recovered + refuse_predetermined_later_lagged_latent_covariance_as_stationary_lagged_covariance( + recovered, + stationary_lagged ), Err( - PsychometricError::TraitPlusStateLaggedCovarianceIsNotStationaryLaggedLatentCovariance + PsychometricError::PredeterminedLaterLaggedLatentCovarianceIsNotStationaryLaggedCovariance ) ); + assert_eq!( + refuse_predetermined_later_lagged_latent_covariance_as_decayed_later_total( + recovered, + decayed_later + ), + Err(PsychometricError::PredeterminedLaterLaggedLatentCovarianceIsNotDecayedLaterTotal) + ); } #[test] - fn stationary_lagged_latent_covariance_invalid_inputs_fail_closed() { + fn predetermined_later_lagged_latent_covariance_invalid_inputs_fail_closed() { assert_eq!( - recover_stationary_lagged_latent_covariance( + recover_predetermined_later_lagged_latent_covariance( 1.0, + 2.0, 0.4, -0.225, 1.0, -0.13, + 2.0, 1.0, LagClock::SystemTime ), Err(PsychometricError::EventTimeRequired) ); assert_eq!( - recover_stationary_lagged_latent_covariance( + recover_predetermined_later_lagged_latent_covariance( 1.0, + 2.0, 0.4, -0.225, 1.0, -0.13, 0.0, + 1.0, LagClock::EventTime ), Err(PsychometricError::NonPositiveInterval) ); assert_eq!( - recover_stationary_lagged_latent_covariance( - 0.0, + recover_predetermined_later_lagged_latent_covariance( + 1.0, + 2.0, 0.4, - 0.0, + -0.225, 1.0, + -0.13, + 2.0, 0.0, - 1.0, LagClock::EventTime ), - Err(PsychometricError::StationaryVarianceRequiresStableDrift) + Err(PsychometricError::NonPositiveInterval) ); assert_eq!( - recover_stationary_lagged_latent_covariance( + recover_predetermined_later_lagged_latent_covariance( + 0.0, 0.0, 0.0, -0.225, 1.0, 0.5, + 2.0, 1.0, LagClock::EventTime ), Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) ); + let growing = recover_predetermined_later_lagged_latent_covariance( + 0.0, + 2.0, + 0.4, + 0.0, + 0.0, + 0.5, + 1.0, + 1.0, + LagClock::EventTime, + ) + .expect("growing a>0"); + assert!(growing.is_finite()); + assert!(growing > 2.0); assert_eq!( - recover_stationary_lagged_latent_covariance( - 0.0, - 0.0, - 0.0, - 1.0, - 0.0, - 1.0, - LagClock::EventTime - ), - Ok(0.0) - ); - assert_eq!( - recover_stationary_lagged_latent_covariance( + recover_predetermined_later_lagged_latent_covariance( f64::NAN, + 2.0, 0.4, 0.0, 0.0, -0.5, + 2.0, 1.0, LagClock::EventTime ), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_stationary_lagged_latent_covariance( + recover_predetermined_later_lagged_latent_covariance( f64::MAX, f64::MAX, 0.0, 0.0, - -0.5, - 1.0, - LagClock::EventTime - ), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_stationary_lagged_latent_covariance( - f64::MAX, 0.0, - 1.0, - f64::MAX, - -1.0, + -0.5, + 2.0, 1.0, LagClock::EventTime ), @@ -10358,180 +20859,176 @@ mod tests { } #[test] - fn stationary_lagged_observed_covariance_recovers_driver_equation_five_of_section_four_point_three() - { - // Driver et al. (2017, §4.3, pp. 9–10; Eq. 5, p. 5) - // lagged observed covariance of stationary T0VAR is - // λ²(trait + e^{a Δt}(−q / (2 a)) + (B / a)² v) + ψ. - // Θ does not enter. + #[allow(clippy::too_many_lines)] + fn predetermined_later_lagged_observed_covariance_recovers_driver_equation_five() { + // Driver et al. (2017, Eq. 5 of later-start lagged T0VAR): + // λ²(trait + e^{a s}(e^{2 a u} p_0 + Q_u) + (B / a)² v) + ψ. let printed_effect = -0.225_f64; let printed_asym = -1.673_f64; let log_rate = -printed_effect / printed_asym; let trait_variance = 1.0_f64; + let initial_latent_variance = 2.0_f64; let diffusion = 0.4_f64; let loading = 2.0_f64; let measurement_error = 0.5_f64; let manifest_trait = 0.1_f64; - let event_delta = 1.0_f64; - let recovered = recover_stationary_lagged_observed_covariance( + let start_delta = 2.0_f64; + let lag_delta = 1.0_f64; + let recovered = recover_predetermined_later_lagged_observed_covariance( loading, trait_variance, + initial_latent_variance, diffusion, printed_effect, 1.0, log_rate, - event_delta, + start_delta, + lag_delta, manifest_trait, LagClock::EventTime, ) - .expect("eq5-lagged-stationary-T0VAR"); - let latent = recover_stationary_lagged_latent_covariance( + .expect("eq5-later-start-lagged-predetermined-T0VAR"); + let latent = recover_predetermined_later_lagged_latent_covariance( trait_variance, + initial_latent_variance, diffusion, printed_effect, 1.0, log_rate, - event_delta, + start_delta, + lag_delta, LagClock::EventTime, ) - .expect("stationary lagged T0VAR"); + .expect("later-start lagged T0VAR"); let expected = recover_manifest_lagged_observed_covariance(loading, latent, manifest_trait) .expect("λ²c+ψ"); assert!((recovered - expected).abs() < 1e-12); - let contemporaneous = recover_stationary_initial_observed_variance( + let first_lagged = recover_predetermined_lagged_observed_covariance( loading, trait_variance, - diffusion, + initial_latent_variance, printed_effect, 1.0, log_rate, - measurement_error, + lag_delta, manifest_trait, LagClock::EventTime, ) - .expect("eq5-stationary-T0VAR"); - assert!((recovered - contemporaneous).abs() > 1e-3); - assert!((recovered - measurement_error).abs() > 1e-3); - assert!((recovered - latent).abs() > 1e-3); - assert_eq!( - recover_stationary_lagged_observed_covariance( - 0.0, - trait_variance, - diffusion, - printed_effect, - 1.0, - log_rate, - event_delta, - manifest_trait, - LagClock::EventTime, - ), - Ok(manifest_trait) - ); - assert_eq!( - recover_stationary_lagged_observed_covariance( - loading, - 0.0, - 0.0, - 0.0, - 1.0, - 0.0, - event_delta, - 0.0, - LagClock::EventTime, - ), - Ok(0.0) - ); - let zero_manifest_trait = recover_stationary_lagged_observed_covariance( - loading, - trait_variance, - diffusion, - printed_effect, - 1.0, - log_rate, - event_delta, - 0.0, - LagClock::EventTime, - ) - .expect("ψ=0"); - let expected_zero_psi = - recover_manifest_lagged_observed_covariance(loading, latent, 0.0).expect("λ²c"); - assert!((zero_manifest_trait - expected_zero_psi).abs() < 1e-12); - } - - #[test] - fn stationary_lagged_observed_covariance_is_not_manifest_latent_or_contemporaneous() { - let trait_variance = 1.0_f64; - let diffusion = 0.4_f64; - let log_rate = -0.134_488_942_f64; - let loading = 2.0_f64; - let measurement_error = 0.5_f64; - let event_delta = 1.0_f64; - let recovered = recover_stationary_lagged_observed_covariance( - loading, - trait_variance, - diffusion, - -0.225, - 1.0, - log_rate, - event_delta, - 0.1, - LagClock::EventTime, - ) - .expect("eq5-lagged-stationary-T0VAR"); - let latent = recover_stationary_lagged_latent_covariance( + .expect("eq5-first-lagged"); + assert!((recovered - first_lagged).abs() > 1e-3); + let later = recover_predetermined_later_observed_variance( + loading, trait_variance, + initial_latent_variance, diffusion, - -0.225, + printed_effect, 1.0, log_rate, - event_delta, + start_delta, + measurement_error, + manifest_trait, LagClock::EventTime, ) - .expect("stationary lagged T0VAR"); - let contemporaneous = recover_stationary_initial_observed_variance( + .expect("eq5-later"); + assert!((recovered - later).abs() > 1e-3); + let stationary = recover_stationary_lagged_observed_covariance( loading, trait_variance, diffusion, - -0.225, + printed_effect, 1.0, log_rate, - measurement_error, - 0.1, + lag_delta, + manifest_trait, LagClock::EventTime, ) - .expect("eq5-stationary-T0VAR"); + .expect("eq5-stationary-lagged"); + assert!((recovered - stationary).abs() > 1e-3); + assert!((recovered - measurement_error).abs() > 1e-3); + assert!((recovered - latent).abs() > 1e-3); assert_eq!( - refuse_stationary_lagged_latent_covariance_as_observed_covariance(latent, recovered), - Err(PsychometricError::StationaryLaggedLatentCovarianceIsNotObservedCovariance) + recover_predetermined_later_lagged_observed_covariance( + 0.0, + trait_variance, + initial_latent_variance, + diffusion, + printed_effect, + 1.0, + log_rate, + start_delta, + lag_delta, + manifest_trait, + LagClock::EventTime, + ), + Ok(manifest_trait) ); assert_eq!( - refuse_measurement_error_as_stationary_lagged_observed_covariance( + recover_predetermined_later_lagged_observed_covariance( + loading, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + start_delta, + lag_delta, + 0.0, + LagClock::EventTime, + ), + Ok(0.0) + ); + assert_eq!( + refuse_predetermined_later_lagged_latent_covariance_as_observed_covariance( + latent, recovered + ), + Err(PsychometricError::PredeterminedLaterLaggedLatentCovarianceIsNotObservedCovariance) + ); + assert_eq!( + refuse_measurement_error_as_predetermined_later_lagged_observed_covariance( measurement_error, recovered ), - Err(PsychometricError::MeasurementErrorIsNotStationaryLaggedObservedCovariance) + Err(PsychometricError::MeasurementErrorIsNotPredeterminedLaterLaggedObservedCovariance) ); assert_eq!( - refuse_stationary_initial_observed_variance_as_stationary_lagged_observed_covariance( - contemporaneous, - recovered + refuse_predetermined_lagged_observed_covariance_as_predetermined_later_lagged_observed_covariance( + first_lagged, recovered ), Err( - PsychometricError::StationaryInitialObservedVarianceIsNotStationaryLaggedObservedCovariance + PsychometricError::PredeterminedLaggedObservedCovarianceIsNotPredeterminedLaterLaggedObservedCovariance + ) + ); + assert_eq!( + refuse_stationary_lagged_observed_covariance_as_predetermined_later_lagged_observed_covariance( + stationary, recovered + ), + Err( + PsychometricError::StationaryLaggedObservedCovarianceIsNotPredeterminedLaterLaggedObservedCovariance + ) + ); + assert_eq!( + refuse_predetermined_later_observed_variance_as_predetermined_later_lagged_observed_covariance( + later, recovered + ), + Err( + PsychometricError::PredeterminedLaterObservedVarianceIsNotPredeterminedLaterLaggedObservedCovariance ) ); } #[test] - fn stationary_lagged_observed_covariance_invalid_inputs_fail_closed() { + fn predetermined_later_lagged_observed_covariance_invalid_inputs_fail_closed() { assert_eq!( - recover_stationary_lagged_observed_covariance( + recover_predetermined_later_lagged_observed_covariance( 2.0, 1.0, + 2.0, 0.4, -0.225, 1.0, -0.13, + 2.0, 1.0, 0.1, LagClock::SystemTime @@ -10539,55 +21036,47 @@ mod tests { Err(PsychometricError::EventTimeRequired) ); assert_eq!( - recover_stationary_lagged_observed_covariance( + recover_predetermined_later_lagged_observed_covariance( 2.0, - 1.0, - 0.4, - -0.225, - 1.0, - -0.13, 0.0, - 0.1, - LagClock::EventTime - ), - Err(PsychometricError::NonPositiveInterval) - ); - assert_eq!( - recover_stationary_lagged_observed_covariance( - 2.0, 0.0, - 0.4, 0.0, + -0.225, 1.0, - 0.0, + 0.5, + 2.0, 1.0, 0.0, LagClock::EventTime ), - Err(PsychometricError::StationaryVarianceRequiresStableDrift) + Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) ); assert_eq!( - recover_stationary_lagged_observed_covariance( + recover_predetermined_later_lagged_observed_covariance( 2.0, - 0.0, - 0.0, - -0.225, 1.0, - 0.5, + 2.0, + 0.4, + 0.0, 1.0, + -0.5, 0.0, + 1.0, + 0.1, LagClock::EventTime ), - Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) + Err(PsychometricError::NonPositiveInterval) ); assert_eq!( - recover_stationary_lagged_observed_covariance( + recover_predetermined_later_lagged_observed_covariance( 2.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, + 2.0, 1.0, 0.1, LagClock::EventTime @@ -10595,27 +21084,15 @@ mod tests { Ok(0.1) ); assert_eq!( - recover_stationary_lagged_observed_covariance( + recover_predetermined_later_lagged_observed_covariance( + 2.0, f64::NAN, - 1.0, + 2.0, 0.4, 0.0, 0.0, -0.5, - 1.0, - 0.0, - LagClock::EventTime - ), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_stationary_lagged_observed_covariance( 2.0, - f64::MAX, - f64::MAX, - 0.0, - 0.0, - -0.5, 1.0, 0.0, LagClock::EventTime @@ -10626,38 +21103,47 @@ mod tests { #[test] #[allow(clippy::too_many_lines)] - fn stationary_later_latent_variance_recovers_driver_section_four_point_three() { - // Driver et al. (2017, §4.3, pp. 9–10; Eq. 3–4, pp. 4–5; p. 16) - // constrain T0VAR across all time points. The later-occasion - // variance is trait + e^{2 a Δt}(−q / (2 a)) + Q_Δt + (B / a)² v. - // Under stationarity that equals contemporaneous T0VAR. + fn predetermined_later_start_later_latent_variance_recovers_driver_equation_four_after_startoffset() + { + // Driver et al. (2017, §4.3 startoffset; Eq. 3–4 Chapman–Kolmogorov): + // Var = trait + e^{2 a s}(e^{2 a u} p_0 + Q_u) + Q_s + (B / a)² v. let printed_effect = -0.225_f64; let printed_asym = -1.673_f64; let log_rate = -printed_effect / printed_asym; let trait_variance = 1.0_f64; + let initial_latent_variance = 2.0_f64; let diffusion = 0.4_f64; let predictor_variance = 1.0_f64; - let event_delta = 1.0_f64; - let recovered = recover_stationary_later_latent_variance( + let start_delta = 2.0_f64; + let lag_delta = 1.0_f64; + let recovered = recover_predetermined_later_start_later_latent_variance( trait_variance, + initial_latent_variance, diffusion, printed_effect, predictor_variance, log_rate, - event_delta, + start_delta, + lag_delta, LagClock::EventTime, ) - .expect("stationary later T0VAR"); - let state = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) - .expect("asymDIFFUSION"); + .expect("predetermined later-start later T0VAR"); + let later_state = recover_discrete_latent_variance( + initial_latent_variance, + diffusion, + log_rate, + start_delta, + LagClock::EventTime, + ) + .expect("later state"); let evolved_state = recover_discrete_latent_variance( - state, + later_state, diffusion, log_rate, - event_delta, + lag_delta, LagClock::EventTime, ) - .expect("e^{2aΔt}p+Q_Δt"); + .expect("evolved later state"); let added = recover_asymptotic_time_independent_predictor_variance( printed_effect, predictor_variance, @@ -10665,267 +21151,374 @@ mod tests { LagClock::EventTime, ) .expect("addedTIPREDVAR"); - assert!((recovered - (trait_variance + evolved_state + added)).abs() < 1e-12); - let contemporaneous = recover_stationary_initial_latent_variance( + let expected = recover_trait_plus_state_latent_variance(trait_variance, evolved_state) + .expect("trait + evolved later-state") + + added; + assert!((recovered - expected).abs() < 1e-12); + let later_full = recover_predetermined_later_latent_variance( trait_variance, + initial_latent_variance, diffusion, printed_effect, predictor_variance, log_rate, + start_delta + lag_delta, LagClock::EventTime, ) - .expect("stationary T0VAR"); - assert!((recovered - contemporaneous).abs() < 1e-12); - let lagged = recover_stationary_lagged_latent_covariance( + .expect("later over u+s"); + assert!((recovered - later_full).abs() < 1e-12); + let later = recover_predetermined_later_latent_variance( trait_variance, + initial_latent_variance, diffusion, printed_effect, predictor_variance, log_rate, - event_delta, + start_delta, LagClock::EventTime, ) - .expect("stationary lagged T0VAR"); - assert!((recovered - lagged).abs() > 1e-3); - let free_discrete = recover_discrete_latent_variance( - contemporaneous, + .expect("later variance at u"); + assert!((recovered - later).abs() > 1e-3); + let later_lagged = recover_predetermined_later_lagged_latent_covariance( + trait_variance, + initial_latent_variance, diffusion, + printed_effect, + predictor_variance, log_rate, - event_delta, + start_delta, + lag_delta, LagClock::EventTime, ) - .expect("e^{2aΔt} p_stat + Q_Δt"); - assert!((recovered - free_discrete).abs() > 1e-3); - let process_noise = - recover_discrete_process_noise(diffusion, log_rate, event_delta, LagClock::EventTime) - .expect("Q_Δt"); - assert!((recovered - process_noise).abs() > 1e-3); - let state_only = recover_stationary_later_latent_variance( - 0.0, + .expect("later-start lagged"); + assert!((recovered - later_lagged).abs() > 1e-3); + let stationary_later = recover_stationary_later_latent_variance( + trait_variance, diffusion, - 0.0, + printed_effect, predictor_variance, log_rate, - event_delta, + lag_delta, LagClock::EventTime, ) - .expect("state-only later"); - assert!((state_only - evolved_state).abs() < 1e-15); - assert!((state_only - state).abs() < 1e-12); - let trait_only = recover_stationary_later_latent_variance( + .expect("stationary later"); + assert!((recovered - stationary_later).abs() > 1e-3); + let decayed_later = recover_discrete_latent_variance( + later, + diffusion, + log_rate, + lag_delta, + LagClock::EventTime, + ) + .expect("evolved later total"); + assert!((recovered - decayed_later).abs() > 1e-3); + let lag_interval = recover_predetermined_later_latent_variance( trait_variance, - 0.0, - 0.0, + initial_latent_variance, + diffusion, + printed_effect, predictor_variance, - 0.0, - event_delta, + log_rate, + lag_delta, LagClock::EventTime, ) - .expect("trait-only later"); - assert!((trait_only - trait_variance).abs() < 1e-15); - let added_only = recover_stationary_later_latent_variance( - 0.0, - 0.0, + .expect("later over s only"); + assert!((recovered - lag_interval).abs() > 1e-3); + let state = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + let from_stationary_start = recover_predetermined_later_start_later_latent_variance( + trait_variance, + state, + diffusion, printed_effect, predictor_variance, log_rate, - event_delta, + start_delta, + lag_delta, LagClock::EventTime, ) - .expect("ti-only later"); - assert!((added_only - added).abs() < 1e-15); - assert_eq!( - recover_stationary_later_latent_variance( - 0.0, - 0.0, - 0.0, - predictor_variance, - 0.0, - event_delta, - LagClock::EventTime - ), - Ok(0.0) - ); - let far = recover_stationary_later_latent_variance( + .expect("p_0=−q/(2a)"); + assert!((from_stationary_start - stationary_later).abs() < 1e-12); + let near_later = recover_predetermined_later_start_later_latent_variance( trait_variance, + initial_latent_variance, diffusion, printed_effect, predictor_variance, log_rate, - 1e8, + start_delta, + 1e-12, LagClock::EventTime, ) - .expect("Δt→∞"); - assert!((far - contemporaneous).abs() < 1e-12); - let near = recover_stationary_later_latent_variance( + .expect("s→0+"); + assert!((near_later - later).abs() < 1e-9); + let later_over_s = recover_predetermined_later_latent_variance( + trait_variance, + initial_latent_variance, + diffusion, + printed_effect, + predictor_variance, + log_rate, + lag_delta, + LagClock::EventTime, + ) + .expect("later over s"); + let near_first = recover_predetermined_later_start_later_latent_variance( trait_variance, + initial_latent_variance, diffusion, printed_effect, predictor_variance, log_rate, 1e-12, + lag_delta, LagClock::EventTime, ) - .expect("Δt→0+"); - assert!((near - contemporaneous).abs() < 1e-9); + .expect("u→0+"); + assert!((near_first - later_over_s).abs() < 1e-9); + assert_eq!( + recover_predetermined_later_start_later_latent_variance( + 0.0, + 0.0, + 0.0, + 0.0, + predictor_variance, + 0.0, + start_delta, + lag_delta, + LagClock::EventTime + ), + Ok(0.0) + ); + let trait_only = recover_predetermined_later_start_later_latent_variance( + trait_variance, + 0.0, + 0.0, + 0.0, + predictor_variance, + 0.0, + start_delta, + lag_delta, + LagClock::EventTime, + ) + .expect("trait-only later-start later"); + assert!((trait_only - trait_variance).abs() < 1e-15); } #[test] - fn stationary_later_latent_variance_is_not_lagged_discrete_or_process_noise() { + #[allow(clippy::too_many_lines)] + fn predetermined_later_start_later_latent_variance_is_not_later_lagged_or_stationary() { let trait_variance = 1.0_f64; + let initial_latent_variance = 2.0_f64; let diffusion = 0.4_f64; let log_rate = -0.134_488_942_f64; - let event_delta = 1.0_f64; - let recovered = recover_stationary_later_latent_variance( + let start_delta = 2.0_f64; + let lag_delta = 1.0_f64; + let recovered = recover_predetermined_later_start_later_latent_variance( trait_variance, + initial_latent_variance, diffusion, -0.225, 1.0, log_rate, - event_delta, + start_delta, + lag_delta, LagClock::EventTime, ) - .expect("stationary later T0VAR"); - let lagged = recover_stationary_lagged_latent_covariance( + .expect("predetermined later-start later T0VAR"); + let later = recover_predetermined_later_latent_variance( + trait_variance, + initial_latent_variance, + diffusion, + -0.225, + 1.0, + log_rate, + start_delta, + LagClock::EventTime, + ) + .expect("later variance"); + let later_lagged = recover_predetermined_later_lagged_latent_covariance( trait_variance, + initial_latent_variance, diffusion, -0.225, 1.0, log_rate, - event_delta, + start_delta, + lag_delta, LagClock::EventTime, ) - .expect("stationary lagged T0VAR"); - let contemporaneous = recover_stationary_initial_latent_variance( + .expect("later-start lagged"); + let stationary_later = recover_stationary_later_latent_variance( trait_variance, diffusion, -0.225, 1.0, log_rate, + lag_delta, LagClock::EventTime, ) - .expect("stationary T0VAR"); - let free_discrete = recover_discrete_latent_variance( - contemporaneous, + .expect("stationary later"); + let decayed_later = recover_discrete_latent_variance( + later, diffusion, log_rate, - event_delta, + lag_delta, LagClock::EventTime, ) - .expect("e^{2aΔt} p_stat + Q_Δt"); - let process_noise = - recover_discrete_process_noise(diffusion, log_rate, event_delta, LagClock::EventTime) - .expect("Q_Δt"); - assert!((recovered - lagged).abs() > 1e-3); - assert!((recovered - free_discrete).abs() > 1e-3); - assert!((recovered - process_noise).abs() > 1e-3); + .expect("evolved later total"); + let lag_interval = recover_predetermined_later_latent_variance( + trait_variance, + initial_latent_variance, + diffusion, + -0.225, + 1.0, + log_rate, + lag_delta, + LagClock::EventTime, + ) + .expect("later over s only"); assert_eq!( - refuse_stationary_later_latent_variance_as_lagged_covariance(recovered, lagged), - Err(PsychometricError::StationaryLaterLatentVarianceIsNotLaggedCovariance) + refuse_predetermined_later_start_later_latent_variance_as_later_latent_variance( + recovered, later + ), + Err( + PsychometricError::PredeterminedLaterStartLaterLatentVarianceIsNotLaterLatentVariance + ) ); assert_eq!( - refuse_stationary_later_latent_variance_as_discrete_variance(recovered, free_discrete), - Err(PsychometricError::StationaryLaterLatentVarianceIsNotDiscreteVariance) + refuse_predetermined_later_start_later_latent_variance_as_later_lagged_covariance( + recovered, + later_lagged + ), + Err( + PsychometricError::PredeterminedLaterStartLaterLatentVarianceIsNotLaterLaggedCovariance + ) ); assert_eq!( - refuse_stationary_later_latent_variance_as_process_noise(recovered, process_noise), - Err(PsychometricError::StationaryLaterLatentVarianceIsNotProcessNoise) + refuse_predetermined_later_start_later_latent_variance_as_stationary_later_latent_variance( + recovered, + stationary_later + ), + Err( + PsychometricError::PredeterminedLaterStartLaterLatentVarianceIsNotStationaryLaterLatentVariance + ) + ); + assert_eq!( + refuse_predetermined_later_start_later_latent_variance_as_decayed_later_total( + recovered, + decayed_later + ), + Err( + PsychometricError::PredeterminedLaterStartLaterLatentVarianceIsNotDecayedLaterTotal + ) + ); + assert_eq!( + refuse_predetermined_later_start_later_latent_variance_as_lag_interval_later_latent_variance( + recovered, + lag_interval + ), + Err( + PsychometricError::PredeterminedLaterStartLaterLatentVarianceIsNotLagIntervalLaterLatentVariance + ) ); } #[test] - fn stationary_later_latent_variance_invalid_inputs_fail_closed() { + fn predetermined_later_start_later_latent_variance_invalid_inputs_fail_closed() { assert_eq!( - recover_stationary_later_latent_variance( + recover_predetermined_later_start_later_latent_variance( 1.0, + 2.0, 0.4, -0.225, 1.0, -0.13, + 2.0, 1.0, LagClock::SystemTime ), Err(PsychometricError::EventTimeRequired) ); assert_eq!( - recover_stationary_later_latent_variance( + recover_predetermined_later_start_later_latent_variance( 1.0, + 2.0, 0.4, -0.225, 1.0, -0.13, 0.0, + 1.0, LagClock::EventTime ), Err(PsychometricError::NonPositiveInterval) ); assert_eq!( - recover_stationary_later_latent_variance( - 0.0, + recover_predetermined_later_start_later_latent_variance( + 1.0, + 2.0, 0.4, - 0.0, + -0.225, 1.0, + -0.13, + 2.0, 0.0, - 1.0, LagClock::EventTime ), - Err(PsychometricError::StationaryVarianceRequiresStableDrift) + Err(PsychometricError::NonPositiveInterval) ); assert_eq!( - recover_stationary_later_latent_variance( + recover_predetermined_later_start_later_latent_variance( + 0.0, 0.0, 0.0, -0.225, 1.0, 0.5, + 2.0, 1.0, LagClock::EventTime ), Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) ); + let growing = recover_predetermined_later_start_later_latent_variance( + 0.0, + 2.0, + 0.4, + 0.0, + 0.0, + 0.5, + 1.0, + 1.0, + LagClock::EventTime, + ) + .expect("growing a>0"); + assert!(growing.is_finite()); + assert!(growing > 2.0); assert_eq!( - recover_stationary_later_latent_variance( - 0.0, - 0.0, - 0.0, - 1.0, - 0.0, - 1.0, - LagClock::EventTime - ), - Ok(0.0) - ); - assert_eq!( - recover_stationary_later_latent_variance( + recover_predetermined_later_start_later_latent_variance( f64::NAN, + 2.0, 0.4, 0.0, 0.0, -0.5, + 2.0, 1.0, LagClock::EventTime ), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_stationary_later_latent_variance( + recover_predetermined_later_start_later_latent_variance( f64::MAX, f64::MAX, 0.0, 0.0, - -0.5, - 1.0, - LagClock::EventTime - ), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_stationary_later_latent_variance( - f64::MAX, 0.0, - 1.0, - f64::MAX, - -1.0, + -0.5, + 2.0, 1.0, LagClock::EventTime ), @@ -10935,44 +21528,47 @@ mod tests { #[test] #[allow(clippy::too_many_lines)] - fn stationary_later_observed_variance_recovers_driver_equation_five_of_section_four_point_three() - { - // Driver et al. (2017, §4.3, pp. 9–10; Eq. 5, p. 5) - // later-occasion observed variance of stationary T0VAR is - // λ²(trait + e^{2 a Δt}(−q / (2 a)) + Q_Δt + (B / a)² v) + θ + ψ. - // Under stationarity that equals contemporaneous Var(y_0). + fn predetermined_later_start_later_observed_variance_recovers_driver_equation_five() { + // Driver et al. (2017, Eq. 5 of later-start later-occasion T0VAR): + // λ²(trait + e^{2 a s}(e^{2 a u} p_0 + Q_u) + Q_s + (B / a)² v) + θ + ψ. let printed_effect = -0.225_f64; let printed_asym = -1.673_f64; let log_rate = -printed_effect / printed_asym; let trait_variance = 1.0_f64; + let initial_latent_variance = 2.0_f64; let diffusion = 0.4_f64; let loading = 2.0_f64; let measurement_error = 0.5_f64; let manifest_trait = 0.1_f64; - let event_delta = 1.0_f64; - let recovered = recover_stationary_later_observed_variance( + let start_delta = 2.0_f64; + let lag_delta = 1.0_f64; + let recovered = recover_predetermined_later_start_later_observed_variance( loading, trait_variance, + initial_latent_variance, diffusion, printed_effect, 1.0, log_rate, - event_delta, + start_delta, + lag_delta, measurement_error, manifest_trait, LagClock::EventTime, ) - .expect("eq5-later-stationary-T0VAR"); - let latent = recover_stationary_later_latent_variance( + .expect("eq5-later-start-later-predetermined-T0VAR"); + let latent = recover_predetermined_later_start_later_latent_variance( trait_variance, + initial_latent_variance, diffusion, printed_effect, 1.0, log_rate, - event_delta, + start_delta, + lag_delta, LagClock::EventTime, ) - .expect("stationary later T0VAR"); + .expect("later-start later T0VAR"); let expected = recover_manifest_trait_plus_state_observed_variance( loading, latent, @@ -10981,43 +21577,63 @@ mod tests { ) .expect("λ²p+θ+ψ"); assert!((recovered - expected).abs() < 1e-12); - let contemporaneous = recover_stationary_initial_observed_variance( + let later = recover_predetermined_later_observed_variance( loading, trait_variance, + initial_latent_variance, diffusion, printed_effect, 1.0, log_rate, + start_delta, measurement_error, manifest_trait, LagClock::EventTime, ) - .expect("eq5-stationary-T0VAR"); - assert!((recovered - contemporaneous).abs() < 1e-12); - let lagged = recover_stationary_lagged_observed_covariance( + .expect("eq5-later"); + assert!((recovered - later).abs() > 1e-3); + let later_lagged = recover_predetermined_later_lagged_observed_covariance( loading, trait_variance, + initial_latent_variance, diffusion, printed_effect, 1.0, log_rate, - event_delta, + start_delta, + lag_delta, manifest_trait, LagClock::EventTime, ) - .expect("eq5-lagged-stationary-T0VAR"); - assert!((recovered - lagged).abs() > 1e-3); + .expect("eq5-later-start-lagged"); + assert!((recovered - later_lagged).abs() > 1e-3); + let stationary = recover_stationary_later_observed_variance( + loading, + trait_variance, + diffusion, + printed_effect, + 1.0, + log_rate, + lag_delta, + measurement_error, + manifest_trait, + LagClock::EventTime, + ) + .expect("eq5-stationary-later"); + assert!((recovered - stationary).abs() > 1e-3); assert!((recovered - measurement_error).abs() > 1e-3); assert!((recovered - latent).abs() > 1e-3); assert_eq!( - recover_stationary_later_observed_variance( + recover_predetermined_later_start_later_observed_variance( 0.0, trait_variance, + initial_latent_variance, diffusion, printed_effect, 1.0, log_rate, - event_delta, + start_delta, + lag_delta, measurement_error, manifest_trait, LagClock::EventTime, @@ -11025,178 +21641,126 @@ mod tests { Ok(measurement_error + manifest_trait) ); assert_eq!( - recover_stationary_later_observed_variance( + recover_predetermined_later_start_later_observed_variance( loading, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, - event_delta, + start_delta, + lag_delta, 0.0, 0.0, LagClock::EventTime, ), Ok(0.0) ); - let zero_manifest_trait = recover_stationary_later_observed_variance( - loading, - trait_variance, - diffusion, - printed_effect, - 1.0, - log_rate, - event_delta, - measurement_error, - 0.0, - LagClock::EventTime, - ) - .expect("ψ=0"); - let expected_zero_psi = recover_manifest_trait_plus_state_observed_variance( - loading, - latent, - measurement_error, - 0.0, - ) - .expect("λ²p+θ"); - assert!((zero_manifest_trait - expected_zero_psi).abs() < 1e-12); - } - - #[test] - fn stationary_later_observed_variance_is_not_manifest_latent_or_lagged() { - let trait_variance = 1.0_f64; - let diffusion = 0.4_f64; - let log_rate = -0.134_488_942_f64; - let loading = 2.0_f64; - let measurement_error = 0.5_f64; - let event_delta = 1.0_f64; - let recovered = recover_stationary_later_observed_variance( - loading, - trait_variance, - diffusion, - -0.225, - 1.0, - log_rate, - event_delta, - measurement_error, - 0.1, - LagClock::EventTime, - ) - .expect("eq5-later-stationary-T0VAR"); - let latent = recover_stationary_later_latent_variance( - trait_variance, - diffusion, - -0.225, - 1.0, - log_rate, - event_delta, - LagClock::EventTime, - ) - .expect("stationary later T0VAR"); - let lagged = recover_stationary_lagged_observed_covariance( - loading, - trait_variance, - diffusion, - -0.225, - 1.0, - log_rate, - event_delta, - 0.1, - LagClock::EventTime, - ) - .expect("eq5-lagged-stationary-T0VAR"); assert_eq!( - refuse_stationary_later_latent_variance_as_observed_variance(latent, recovered), - Err(PsychometricError::StationaryLaterLatentVarianceIsNotObservedVariance) + refuse_predetermined_later_start_later_latent_variance_as_observed_variance( + latent, recovered + ), + Err(PsychometricError::PredeterminedLaterStartLaterLatentVarianceIsNotObservedVariance) ); assert_eq!( - refuse_measurement_error_as_stationary_later_observed_variance( + refuse_measurement_error_as_predetermined_later_start_later_observed_variance( measurement_error, recovered ), - Err(PsychometricError::MeasurementErrorIsNotStationaryLaterObservedVariance) + Err( + PsychometricError::MeasurementErrorIsNotPredeterminedLaterStartLaterObservedVariance + ) ); assert_eq!( - refuse_stationary_lagged_observed_covariance_as_stationary_later_observed_variance( - lagged, recovered + refuse_predetermined_later_observed_variance_as_predetermined_later_start_later_observed_variance( + later, recovered ), Err( - PsychometricError::StationaryLaggedObservedCovarianceIsNotStationaryLaterObservedVariance + PsychometricError::PredeterminedLaterObservedVarianceIsNotPredeterminedLaterStartLaterObservedVariance + ) + ); + assert_eq!( + refuse_predetermined_later_lagged_observed_covariance_as_predetermined_later_start_later_observed_variance( + later_lagged, recovered + ), + Err( + PsychometricError::PredeterminedLaterLaggedObservedCovarianceIsNotPredeterminedLaterStartLaterObservedVariance + ) + ); + assert_eq!( + refuse_stationary_later_observed_variance_as_predetermined_later_start_later_observed_variance( + stationary, recovered + ), + Err( + PsychometricError::StationaryLaterObservedVarianceIsNotPredeterminedLaterStartLaterObservedVariance ) ); } #[test] - #[allow(clippy::too_many_lines)] - fn stationary_later_observed_variance_invalid_inputs_fail_closed() { + fn predetermined_later_start_later_observed_variance_invalid_inputs_fail_closed() { assert_eq!( - recover_stationary_later_observed_variance( + recover_predetermined_later_start_later_observed_variance( 2.0, 1.0, + 2.0, 0.4, -0.225, 1.0, -0.13, - 1.0, - 0.5, - 0.1, - LagClock::SystemTime - ), - Err(PsychometricError::EventTimeRequired) - ); - assert_eq!( - recover_stationary_later_observed_variance( 2.0, 1.0, - 0.4, - -0.225, - 1.0, - -0.13, - 0.0, 0.5, 0.1, - LagClock::EventTime + LagClock::SystemTime ), - Err(PsychometricError::NonPositiveInterval) + Err(PsychometricError::EventTimeRequired) ); assert_eq!( - recover_stationary_later_observed_variance( + recover_predetermined_later_start_later_observed_variance( 2.0, 0.0, - 0.4, 0.0, - 1.0, 0.0, + -0.225, + 1.0, + 0.5, + 2.0, 1.0, 0.0, 0.0, LagClock::EventTime ), - Err(PsychometricError::StationaryVarianceRequiresStableDrift) + Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) ); assert_eq!( - recover_stationary_later_observed_variance( + recover_predetermined_later_start_later_observed_variance( + 2.0, + 1.0, 2.0, + 0.4, 0.0, + 1.0, + -0.5, 0.0, - -0.225, 1.0, 0.5, - 1.0, - 0.0, - 0.0, + 0.1, LagClock::EventTime ), - Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) + Err(PsychometricError::NonPositiveInterval) ); assert_eq!( - recover_stationary_later_observed_variance( + recover_predetermined_later_start_later_observed_variance( 2.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, + 2.0, 1.0, 0.5, 0.1, @@ -11205,28 +21769,15 @@ mod tests { Ok(0.6) ); assert_eq!( - recover_stationary_later_observed_variance( + recover_predetermined_later_start_later_observed_variance( + 2.0, f64::NAN, - 1.0, + 2.0, 0.4, 0.0, 0.0, -0.5, - 1.0, - 0.0, - 0.0, - LagClock::EventTime - ), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_stationary_later_observed_variance( 2.0, - f64::MAX, - f64::MAX, - 0.0, - 0.0, - -0.5, 1.0, 0.0, 0.0, @@ -11237,861 +21788,1041 @@ mod tests { } #[test] - fn discrete_observed_mean_with_impulse_recovers_driver_equation_five() { - let loading = 2.0_f64; - let drift = -0.5_f64; - let delta = 2.0_f64; - let effect = 0.4_f64; - let predictor = 3.0_f64; - let initial = 1.0_f64; - let intercept = 0.3_f64; - let manifest_mean = 0.5_f64; - let recovered = recover_discrete_observed_mean_with_impulse( - loading, - initial, - drift, - intercept, - effect, - predictor, - manifest_mean, - delta, - LagClock::EventTime, - ) - .expect("eq5-impulse-mean"); - let composed = recover_discrete_latent_mean_with_impulse( - initial, - drift, - intercept, - effect, - predictor, - delta, + fn standardised_discrete_drift_recovers_driver_page_sixteen_after_positive_asymdiffusion() { + // Driver et al. (2017, p. 16 discreteDRIFTstd; footnote 4): + // form strictly positive asymDIFFUSION = −q / (2 a), then + // φ = exp(a Δt). Scalar SD ratio is 1. + let diffusion = 0.4_f64; + let log_rate = -0.5_f64; + let event_delta = 1.0_f64; + let recovered = recover_standardised_discrete_drift( + diffusion, + log_rate, + event_delta, LagClock::EventTime, ) - .expect("mx"); - let expected = manifest_mean + loading * composed; - assert!((recovered - expected).abs() < 1e-15); - let evolved_observed = recover_discrete_observed_mean( - loading, - initial, - drift, - intercept, - manifest_mean, - delta, + .expect("discreteDRIFTstd"); + let unstandardised = + recover_discrete_lag_from_log_rate(log_rate, event_delta, LagClock::EventTime) + .expect("discreteDRIFT"); + let within = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + assert!(within > 0.0); + assert!((recovered - unstandardised).abs() < 1e-15); + assert!((recovered - (log_rate * event_delta).exp()).abs() < 1e-15); + let two_and_a_half = + recover_standardised_discrete_drift(diffusion, log_rate, 2.5, LagClock::EventTime) + .expect("discreteDRIFTstd Δt=2.5"); + assert!((two_and_a_half - (log_rate * 2.5).exp()).abs() < 1e-15); + assert!((recovered - two_and_a_half).abs() > 1e-9); + let trait_variance = 1.0_f64; + let lagged = recover_trait_plus_state_lagged_covariance( + trait_variance, + within, + log_rate, + event_delta, LagClock::EventTime, ) - .expect("eq3-eq5-mean"); - assert!((evolved_observed - recovered).abs() > 1e-3); - let carried_observed = recover_discrete_observed_mean_with_impulse_carry( - loading, - initial, - drift, - intercept, - effect, - predictor, - manifest_mean, - delta, - 1.0, + .expect("trait+state lag"); + let total = recover_trait_plus_state_latent_variance(trait_variance, within) + .expect("trait+state var"); + let contaminated = lagged / total; + assert!((contaminated - recovered).abs() > 1e-3); + assert_eq!( + refuse_unstandardised_discrete_drift_as_standardised_discrete_drift( + unstandardised, + recovered + ), + Err(PsychometricError::UnstandardisedDiscreteDriftIsNotStandardisedDiscreteDrift) + ); + assert_eq!( + refuse_trait_plus_state_autocorrelation_as_standardised_discrete_drift( + contaminated, + recovered + ), + Err(PsychometricError::TraitPlusStateAutocorrelationIsNotStandardisedDiscreteDrift) + ); + assert_eq!( + refuse_trait_variance_as_standardisation_variance(trait_variance, within), + Err(PsychometricError::TraitVarianceIsNotStandardisationVariance) + ); + } + + #[test] + fn standardised_discrete_drift_fails_closed_when_unstandardised_is_defined() { + let log_rate = -0.5_f64; + let event_delta = 1.0_f64; + let unstandardised_zero_q = + recover_discrete_lag_from_log_rate(log_rate, event_delta, LagClock::EventTime) + .expect("e^{aΔt} at q=0"); + assert!((unstandardised_zero_q - log_rate.exp()).abs() < 1e-15); + assert_eq!( + recover_standardised_discrete_drift(0.0, log_rate, event_delta, LagClock::EventTime), + Err(PsychometricError::StandardisedDiscreteDriftRequiresPositiveWithinSubjectVariance) + ); + let growing = recover_discrete_lag_from_log_rate(0.5, event_delta, LagClock::EventTime) + .expect("growing a>0"); + assert!(growing.is_finite()); + assert!(growing > 1.0); + assert_eq!( + recover_standardised_discrete_drift(0.4, 0.5, event_delta, LagClock::EventTime), + Err(PsychometricError::StationaryVarianceRequiresStableDrift) + ); + let unit = + recover_discrete_lag_from_log_rate(0.0, event_delta, LagClock::EventTime).expect("a=0"); + assert!((unit - 1.0).abs() < 1e-15); + assert_eq!( + recover_standardised_discrete_drift(0.4, 0.0, event_delta, LagClock::EventTime), + Err(PsychometricError::StationaryVarianceRequiresStableDrift) + ); + assert_eq!( + recover_standardised_discrete_drift(0.4, log_rate, event_delta, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_standardised_discrete_drift(0.4, log_rate, 0.0, LagClock::EventTime), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_standardised_discrete_drift(0.4, log_rate, f64::NAN, LagClock::EventTime), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_standardised_discrete_drift(-0.1, log_rate, event_delta, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_discrete_drift(0.4, f64::NAN, event_delta, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_discrete_drift(0.4, -800.0, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + } + + #[test] + fn standardised_discrete_diffusion_recovers_driver_page_sixteen_after_positive_asymdiffusion() { + // Driver et al. (2017, p. 16 discreteDIFFUSIONstd; Eq. 4; footnote 4): + // form strictly positive asymDIFFUSION = −q / (2 a), then + // Q_Δt / p. Scalar stationary ratio is 1 − exp(2 a Δt). + let diffusion = 0.4_f64; + let log_rate = -0.5_f64; + let event_delta = 1.0_f64; + let recovered = recover_standardised_discrete_diffusion( + diffusion, + log_rate, + event_delta, LagClock::EventTime, ) - .expect("eq5-carry-mean"); - assert!((carried_observed - recovered).abs() > 1e-3); + .expect("discreteDIFFUSIONstd"); + let process_noise = + recover_discrete_process_noise(diffusion, log_rate, event_delta, LagClock::EventTime) + .expect("discreteDIFFUSION"); + let within = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + assert!(within > 0.0); + assert!((recovered - (process_noise / within)).abs() < 1e-15); + let one_minus_phi_sq = 1.0 - (2.0 * log_rate * event_delta).exp(); + assert!((recovered - one_minus_phi_sq).abs() < 1e-15); + let two_and_a_half = + recover_standardised_discrete_diffusion(diffusion, log_rate, 2.5, LagClock::EventTime) + .expect("discreteDIFFUSIONstd Δt=2.5"); + assert!((two_and_a_half - (1.0 - (2.0 * log_rate * 2.5).exp())).abs() < 1e-15); + assert!((recovered - two_and_a_half).abs() > 1e-9); + let continuous_std = diffusion / within; + assert!((continuous_std - recovered).abs() > 1e-3); + let trait_variance = 1.0_f64; + let total = recover_trait_plus_state_latent_variance(trait_variance, within) + .expect("trait+state var"); + let contaminated = process_noise / total; + assert!((contaminated - recovered).abs() > 1e-3); + assert_eq!( + refuse_unstandardised_discrete_diffusion_as_standardised_discrete_diffusion( + process_noise, + recovered + ), + Err( + PsychometricError::UnstandardisedDiscreteDiffusionIsNotStandardisedDiscreteDiffusion + ) + ); + assert_eq!( + refuse_standardised_continuous_diffusion_as_standardised_discrete_diffusion( + continuous_std, + recovered + ), + Err( + PsychometricError::StandardisedContinuousDiffusionIsNotStandardisedDiscreteDiffusion + ) + ); + assert_eq!( + refuse_trait_contaminated_process_noise_as_standardised_discrete_diffusion( + contaminated, + recovered + ), + Err(PsychometricError::TraitContaminatedProcessNoiseIsNotStandardisedDiscreteDiffusion) + ); + assert_eq!( + refuse_trait_variance_as_standardisation_variance(trait_variance, within), + Err(PsychometricError::TraitVarianceIsNotStandardisationVariance) + ); + } + + #[test] + fn standardised_discrete_diffusion_fails_closed_when_unstandardised_is_defined() { + let log_rate = -0.5_f64; + let event_delta = 1.0_f64; + let unstandardised_zero_q = + recover_discrete_process_noise(0.0, log_rate, event_delta, LagClock::EventTime) + .expect("Q_Δt at q=0"); + assert!((unstandardised_zero_q - 0.0).abs() < 1e-15); + assert_eq!( + recover_standardised_discrete_diffusion(0.0, log_rate, event_delta, LagClock::EventTime), + Err( + PsychometricError::StandardisedDiscreteDiffusionRequiresPositiveWithinSubjectVariance + ) + ); + let growing = recover_discrete_process_noise(0.4, 0.5, event_delta, LagClock::EventTime) + .expect("growing a>0"); + assert!(growing.is_finite()); + assert!(growing > 0.0); + assert_eq!( + recover_standardised_discrete_diffusion(0.4, 0.5, event_delta, LagClock::EventTime), + Err(PsychometricError::StationaryVarianceRequiresStableDrift) + ); + let unit = recover_discrete_process_noise(0.4, 0.0, event_delta, LagClock::EventTime) + .expect("a=0"); + assert!((unit - 0.4).abs() < 1e-15); + assert_eq!( + recover_standardised_discrete_diffusion(0.4, 0.0, event_delta, LagClock::EventTime), + Err(PsychometricError::StationaryVarianceRequiresStableDrift) + ); + assert_eq!( + recover_standardised_discrete_diffusion( + 0.4, + log_rate, + event_delta, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_standardised_discrete_diffusion(0.4, log_rate, 0.0, LagClock::EventTime), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_standardised_discrete_diffusion(0.4, log_rate, f64::NAN, LagClock::EventTime), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_standardised_discrete_diffusion( + -0.1, + log_rate, + event_delta, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_discrete_diffusion( + 0.4, + f64::NAN, + event_delta, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + } + + #[test] + fn standardised_continuous_diffusion_recovers_driver_page_sixteen_after_positive_asymdiffusion() + { + // Driver et al. (2017, p. 16 DIFFUSIONstd; Eq. 4; footnote 4): + // form strictly positive asymDIFFUSION = −q / (2 a), then + // q / p. Scalar stationary ratio is −2 a. + let diffusion = 0.4_f64; + let log_rate = -0.5_f64; + let recovered = + recover_standardised_continuous_diffusion(diffusion, log_rate, LagClock::EventTime) + .expect("DIFFUSIONstd"); + let within = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + assert!(within > 0.0); + assert!((recovered - (diffusion / within)).abs() < 1e-15); + assert!((recovered - (-2.0 * log_rate)).abs() < 1e-15); + let larger_q = + recover_standardised_continuous_diffusion(2.0, log_rate, LagClock::EventTime) + .expect("DIFFUSIONstd q=2"); + assert!((larger_q - recovered).abs() < 1e-15); + let discrete = + recover_standardised_discrete_diffusion(diffusion, log_rate, 1.0, LagClock::EventTime) + .expect("discreteDIFFUSIONstd"); + assert!((discrete - recovered).abs() > 1e-3); + let trait_variance = 1.0_f64; + let total = recover_trait_plus_state_latent_variance(trait_variance, within) + .expect("trait+state var"); + let contaminated = diffusion / total; + assert!((contaminated - recovered).abs() > 1e-3); + assert_eq!( + refuse_unstandardised_continuous_diffusion_as_standardised_continuous_diffusion( + diffusion, + recovered + ), + Err( + PsychometricError::UnstandardisedContinuousDiffusionIsNotStandardisedContinuousDiffusion + ) + ); + assert_eq!( + refuse_standardised_discrete_diffusion_as_standardised_continuous_diffusion( + discrete, + recovered + ), + Err( + PsychometricError::StandardisedDiscreteDiffusionIsNotStandardisedContinuousDiffusion + ) + ); + assert_eq!( + refuse_trait_contaminated_continuous_diffusion_as_standardised_continuous_diffusion( + contaminated, + recovered + ), + Err( + PsychometricError::TraitContaminatedContinuousDiffusionIsNotStandardisedContinuousDiffusion + ) + ); + assert_eq!( + refuse_trait_variance_as_standardisation_variance(trait_variance, within), + Err(PsychometricError::TraitVarianceIsNotStandardisationVariance) + ); + } + + #[test] + fn standardised_continuous_diffusion_fails_closed_when_unstandardised_is_defined() { + let log_rate = -0.5_f64; + assert_eq!( + recover_standardised_continuous_diffusion(0.0, log_rate, LagClock::EventTime), + Err( + PsychometricError::StandardisedContinuousDiffusionRequiresPositiveWithinSubjectVariance + ) + ); assert_eq!( - recover_discrete_observed_mean_with_impulse( - 0.0, - initial, - drift, - intercept, - effect, - predictor, - manifest_mean, - delta, - LagClock::EventTime + recover_standardised_continuous_diffusion(0.4, 0.5, LagClock::EventTime), + Err(PsychometricError::StationaryVarianceRequiresStableDrift) + ); + assert_eq!( + recover_standardised_continuous_diffusion(0.4, 0.0, LagClock::EventTime), + Err(PsychometricError::StationaryVarianceRequiresStableDrift) + ); + assert_eq!( + recover_standardised_continuous_diffusion(0.4, log_rate, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_standardised_continuous_diffusion(-0.1, log_rate, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_continuous_diffusion(0.4, f64::NAN, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_continuous_diffusion(1e308, -1e308, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + } + + #[test] + fn standardised_continuous_drift_recovers_driver_page_sixteen_after_positive_asymdiffusion() { + // Driver et al. (2017, p. 16 DRIFTstd; Eq. 1; footnote 4): + // form strictly positive asymDIFFUSION = −q / (2 a). Scalar + // stationary SD ratio is 1, so DRIFTstd equals a numerically. + let diffusion = 0.4_f64; + let log_rate = -0.5_f64; + let recovered = + recover_standardised_continuous_drift(diffusion, log_rate, LagClock::EventTime) + .expect("DRIFTstd"); + let within = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + assert!(within > 0.0); + assert!((recovered - log_rate).abs() < 1e-15); + let larger_q = recover_standardised_continuous_drift(2.0, log_rate, LagClock::EventTime) + .expect("DRIFTstd q=2"); + assert!((larger_q - recovered).abs() < 1e-15); + let discrete = + recover_standardised_discrete_drift(diffusion, log_rate, 1.0, LagClock::EventTime) + .expect("discreteDRIFTstd"); + assert!((discrete - recovered).abs() > 1e-3); + let trait_variance = 1.0_f64; + let total = recover_trait_plus_state_latent_variance(trait_variance, within) + .expect("trait+state var"); + let contaminated = log_rate * (within / total); + assert!((contaminated - recovered).abs() > 1e-3); + assert_eq!( + refuse_unstandardised_continuous_drift_as_standardised_continuous_drift( + log_rate, recovered ), - Ok(manifest_mean) + Err(PsychometricError::UnstandardisedContinuousDriftIsNotStandardisedContinuousDrift) ); assert_eq!( - recover_discrete_observed_mean_with_impulse( - loading, - initial, - drift, - intercept, - effect, - predictor, - 0.0, - delta, - LagClock::EventTime + refuse_standardised_discrete_drift_as_standardised_continuous_drift( + discrete, recovered ), - Ok(loading * composed) + Err(PsychometricError::StandardisedDiscreteDriftIsNotStandardisedContinuousDrift) + ); + assert_eq!( + refuse_trait_contaminated_continuous_drift_as_standardised_continuous_drift( + contaminated, + recovered + ), + Err( + PsychometricError::TraitContaminatedContinuousDriftIsNotStandardisedContinuousDrift + ) + ); + assert_eq!( + refuse_trait_variance_as_standardisation_variance(trait_variance, within), + Err(PsychometricError::TraitVarianceIsNotStandardisationVariance) ); } #[test] - fn discrete_observed_mean_with_impulse_is_not_evolved_or_zero_impulse() { - let loading = 2.0_f64; - let drift = -0.5_f64; - let delta = 2.0_f64; - let effect = 0.4_f64; - let predictor = 3.0_f64; - let initial = 1.0_f64; - let intercept = 0.3_f64; - let manifest_mean = 0.5_f64; - let recovered = recover_discrete_observed_mean_with_impulse( - loading, - initial, - drift, - intercept, - effect, - predictor, - manifest_mean, - delta, - LagClock::EventTime, - ) - .expect("eq5-impulse-mean"); - let evolved_observed = recover_discrete_observed_mean( - loading, - initial, - drift, - intercept, - manifest_mean, - delta, - LagClock::EventTime, - ) - .expect("eq3-eq5-mean"); - let zero_impulse = recover_discrete_observed_mean_with_impulse( - loading, - initial, - drift, - intercept, - 0.0, - predictor, - manifest_mean, - delta, - LagClock::EventTime, - ) - .expect("zero-impulse"); - assert!((zero_impulse - evolved_observed).abs() < 1e-15); - assert!((recovered - evolved_observed).abs() > 1e-3); + fn standardised_continuous_drift_fails_closed_when_unstandardised_is_defined() { + let log_rate = -0.5_f64; + assert_eq!( + recover_standardised_continuous_drift(0.0, log_rate, LagClock::EventTime), + Err( + PsychometricError::StandardisedContinuousDriftRequiresPositiveWithinSubjectVariance + ) + ); + assert_eq!( + recover_standardised_continuous_drift(0.4, 0.5, LagClock::EventTime), + Err(PsychometricError::StationaryVarianceRequiresStableDrift) + ); + assert_eq!( + recover_standardised_continuous_drift(0.4, 0.0, LagClock::EventTime), + Err(PsychometricError::StationaryVarianceRequiresStableDrift) + ); + assert_eq!( + recover_standardised_continuous_drift(0.4, log_rate, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_standardised_continuous_drift(-0.1, log_rate, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_continuous_drift(0.4, f64::NAN, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); } #[test] - fn discrete_observed_mean_with_impulse_refuses_evolved_mean_and_overflow() { - let loading = 2.0_f64; - let recovered = recover_discrete_observed_mean_with_impulse( - loading, - 1.0, - -0.5, - 0.3, - 0.4, - 3.0, - 0.5, - 2.0, + fn standardised_asymptotic_time_independent_effect_recovers_driver_page_sixteen_after_positive_variances() + { + // Driver et al. (2017, p. 16 asymTIPREDEFFECTstd; §7.2; footnote 4): + // form strictly positive asymDIFFUSION = −q / (2 a) and + // strictly positive v, then (−B / a) · √v / √p. + let diffusion = 0.4_f64; + let log_rate = -0.5_f64; + let coefficient = 0.3_f64; + let predictor_variance = 1.0_f64; + let recovered = recover_standardised_asymptotic_time_independent_predictor_effect( + coefficient, + predictor_variance, + diffusion, + log_rate, LagClock::EventTime, ) - .expect("eq5-impulse-mean"); - let composed = recover_discrete_latent_mean_with_impulse( + .expect("asymTIPREDEFFECTstd"); + let within = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + assert!(within > 0.0); + let unit = recover_asymptotic_time_independent_predictor_effect( + coefficient, 1.0, - -0.5, - 0.3, - 0.4, - 3.0, + log_rate, + LagClock::EventTime, + ) + .expect("asymTIPREDEFFECT"); + let expected = unit * predictor_variance.sqrt() / within.sqrt(); + assert!((recovered - expected).abs() < 1e-15); + let larger_q = recover_standardised_asymptotic_time_independent_predictor_effect( + coefficient, + predictor_variance, 2.0, + log_rate, LagClock::EventTime, ) - .expect("mx"); - let evolved_observed = - recover_discrete_observed_mean(loading, 1.0, -0.5, 0.3, 0.5, 2.0, LagClock::EventTime) - .expect("eq3-eq5-mean"); - let carried_observed = recover_discrete_observed_mean_with_impulse_carry( - loading, + .expect("asymTIPREDEFFECTstd q=2"); + assert!((larger_q - recovered).abs() > 1e-3); + assert!(larger_q.abs() < recovered.abs()); + let discrete_increment = recover_discrete_time_independent_predictor_effect( + coefficient, 1.0, - -0.5, - 0.3, - 0.4, - 3.0, - 0.5, - 2.0, + log_rate, 1.0, LagClock::EventTime, ) - .expect("eq5-carry-mean"); + .expect("discrete TIPREDEFFECT"); + let discrete_std = discrete_increment * predictor_variance.sqrt() / within.sqrt(); + assert!((discrete_std - recovered).abs() > 1e-3); + let trait_variance = 1.0_f64; + let total = recover_trait_plus_state_latent_variance(trait_variance, within) + .expect("trait+state var"); + let contaminated = unit * predictor_variance.sqrt() / total.sqrt(); + assert!((contaminated - recovered).abs() > 1e-3); + let zero = recover_standardised_asymptotic_time_independent_predictor_effect( + 0.0, + predictor_variance, + diffusion, + log_rate, + LagClock::EventTime, + ) + .expect("zero coefficient"); + assert_eq!(zero.to_bits(), 0.0_f64.to_bits()); assert_eq!( - refuse_evolved_observed_mean_as_impulse_observed_mean(evolved_observed, recovered), - Err(PsychometricError::EvolvedObservedMeanIsNotImpulseObservedMean) + refuse_unstandardised_asymptotic_time_independent_effect_as_standardised_asymptotic_time_independent_effect( + unit, + recovered + ), + Err( + PsychometricError::UnstandardisedAsymptoticTimeIndependentEffectIsNotStandardisedAsymptoticTimeIndependentEffect + ) ); assert_eq!( - refuse_impulse_observed_mean_as_impulse_carry_observed_mean( - recovered, - carried_observed + refuse_standardised_discrete_time_independent_effect_as_standardised_asymptotic_time_independent_effect( + discrete_std, + recovered ), - Err(PsychometricError::ImpulseObservedMeanIsNotImpulseCarryObservedMean) + Err( + PsychometricError::StandardisedDiscreteTimeIndependentEffectIsNotStandardisedAsymptoticTimeIndependentEffect + ) ); assert_eq!( - refuse_latent_mean_as_observed_mean(composed, recovered), - Err(PsychometricError::LatentMeanIsNotObservedMean) + refuse_trait_contaminated_asymptotic_time_independent_effect_as_standardised_asymptotic_time_independent_effect( + contaminated, + recovered + ), + Err( + PsychometricError::TraitContaminatedAsymptoticTimeIndependentEffectIsNotStandardisedAsymptoticTimeIndependentEffect + ) ); assert_eq!( - refuse_manifest_means_as_observed_mean(0.5, recovered), - Err(PsychometricError::ManifestMeansIsNotObservedMean) + refuse_trait_variance_as_standardisation_variance(trait_variance, within), + Err(PsychometricError::TraitVarianceIsNotStandardisationVariance) ); - let scaled = recover_discrete_observed_mean_with_impulse( - 1e308, - 1e-308, - 0.0, - 0.0, - 0.0, - 3.0, - 0.0, - 1.0, - LagClock::EventTime, - ) - .expect("scale"); - assert!((scaled - 1.0).abs() < 1e-15); - let finite_loaded = recover_discrete_observed_mean_with_impulse( - 1e308, - 1.0, - 0.0, - 0.0, - 0.0, - 3.0, - 0.0, - 1.0, - LagClock::EventTime, - ) - .expect("lambda-mu"); - assert!((finite_loaded - 1e308).abs() / 1e308 < 1e-15); } #[test] - fn discrete_observed_mean_with_impulse_invalid_inputs_fail_closed() { + fn standardised_asymptotic_time_independent_effect_fails_closed_when_unstandardised_is_defined() + { + let log_rate = -0.5_f64; assert_eq!( - recover_discrete_observed_mean_with_impulse( - f64::NAN, - 1.0, - -0.5, + recover_standardised_asymptotic_time_independent_predictor_effect( 0.3, - 0.4, - 3.0, - 0.5, - 2.0, - LagClock::EventTime - ), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_discrete_observed_mean_with_impulse( - 1e308, - 2.0, - 0.0, - 0.0, - 0.0, - 3.0, - 0.0, - 1.0, - LagClock::EventTime - ), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_discrete_observed_mean_with_impulse( - 1.0, - 1.0, - 710.0, - 0.0, - 0.0, - 3.0, - 0.5, 1.0, + 0.0, + log_rate, LagClock::EventTime ), - Err(PsychometricError::InvalidNumericInput) + Err( + PsychometricError::StandardisedAsymptoticTimeIndependentEffectRequiresPositiveWithinSubjectVariance + ) ); assert_eq!( - recover_discrete_observed_mean_with_impulse( - 1e308, - 0.0, - 0.0, - 0.0, - 1e308, - 1.0, + recover_standardised_asymptotic_time_independent_predictor_effect( + 0.3, 0.0, - 1.0, + 0.4, + log_rate, LagClock::EventTime ), - Err(PsychometricError::InvalidNumericInput) + Err( + PsychometricError::StandardisedAsymptoticTimeIndependentEffectRequiresPositivePredictorVariance + ) ); assert_eq!( - recover_discrete_observed_mean_with_impulse( - 2.0, - 1.0, - -0.5, + recover_standardised_asymptotic_time_independent_predictor_effect( 0.3, + 1.0, 0.4, - 3.0, 0.5, - 0.0, LagClock::EventTime ), - Err(PsychometricError::NonPositiveInterval) + Err(PsychometricError::StationaryVarianceRequiresStableDrift) ); assert_eq!( - recover_discrete_observed_mean_with_impulse( - 2.0, + recover_standardised_asymptotic_time_independent_predictor_effect( + 0.3, 1.0, - -0.5, + 0.4, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::StationaryVarianceRequiresStableDrift) + ); + assert_eq!( + recover_standardised_asymptotic_time_independent_predictor_effect( 0.3, + 1.0, 0.4, - 3.0, - 0.5, - 2.0, + log_rate, LagClock::SystemTime ), Err(PsychometricError::EventTimeRequired) ); - } - - #[test] - fn time_independent_predictor_recovers_driver_equation_three_second_summand() { - let effect = 0.4_f64; - let predictor = 3.0_f64; - let drift = -0.5_f64; - let delta = 2.0_f64; - let increment = recover_discrete_time_independent_predictor_effect( - effect, - predictor, - drift, - delta, - LagClock::EventTime, - ) - .expect("tipred"); - let expected = - recover_discrete_constant_predictor_effect(1.2, drift, delta, LagClock::EventTime) - .expect("bz-map"); - assert!((increment - expected).abs() < 1e-15); assert_eq!( - recover_discrete_time_independent_predictor_effect( - 0.0, - predictor, - drift, - delta, + recover_standardised_asymptotic_time_independent_predictor_effect( + 0.3, + -0.1, + 0.4, + log_rate, LagClock::EventTime ), - Ok(0.0) + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_time_independent_predictor_effect( - effect, - 0.0, - drift, - delta, + recover_standardised_asymptotic_time_independent_predictor_effect( + 0.3, + f64::NAN, + 0.4, + log_rate, LagClock::EventTime ), - Ok(0.0) + Err(PsychometricError::InvalidNumericInput) ); - let zero_drift = recover_discrete_time_independent_predictor_effect( - effect, - predictor, - 0.0, - delta, - LagClock::EventTime, - ) - .expect("zero-drift"); - assert!((zero_drift - 2.4).abs() < 1e-15); - let intercept_effect = - recover_discrete_continuous_intercept_effect(effect, drift, delta, LagClock::EventTime) - .expect("cint"); - let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("tdpred"); - let equation_fourteen = recover_discrete_time_varying_predictor_effect( - effect, - delta, - delta, - delta, - LagClock::EventTime, - ) - .expect("eq14"); - assert!((increment - intercept_effect).abs() > 1e-3); - assert!((increment - impulse).abs() > 1e-3); - assert!((increment - equation_fourteen).abs() > 1e-3); - assert!((increment - effect).abs() > 1e-3); - } - - #[test] - fn time_independent_predictor_composes_evolved_mean_and_keeps_scale() { - let effect = 0.4_f64; - let predictor = 3.0_f64; - let drift = -0.5_f64; - let delta = 2.0_f64; - let increment = recover_discrete_time_independent_predictor_effect( - effect, - predictor, - drift, - delta, - LagClock::EventTime, - ) - .expect("tipred"); - let initial = 1.0_f64; - let intercept = 0.3_f64; - let composed = recover_discrete_latent_mean_with_time_independent_predictor( - initial, - drift, - intercept, - effect, - predictor, - delta, - LagClock::EventTime, - ) - .expect("eq3-tipred"); - let evolved = - recover_discrete_latent_mean(initial, drift, intercept, delta, LagClock::EventTime) - .expect("mu-t"); - assert!((composed - (evolved + increment)).abs() < 1e-15); assert_eq!( - recover_discrete_latent_mean_with_time_independent_predictor( - initial, - drift, - intercept, - 0.0, - predictor, - delta, + recover_standardised_asymptotic_time_independent_predictor_effect( + 4.0, + 1e308, + 1e-308, + log_rate, LagClock::EventTime ), - Ok(evolved) + Err(PsychometricError::InvalidNumericInput) ); + // A non-finite coefficient survives the outer variance gates and must + // propagate through the delegated unstandardised recovery. assert_eq!( - recover_discrete_latent_mean_with_time_independent_predictor( - 0.0, - drift, - 0.0, - effect, - predictor, - delta, + recover_standardised_asymptotic_time_independent_predictor_effect( + f64::NAN, + 1.0, + 0.4, + log_rate, LagClock::EventTime ), - Ok(increment) + Err(PsychometricError::InvalidNumericInput) ); - let scaled = recover_discrete_time_independent_predictor_effect( - 1e308, - 1e-308, - 0.0, - 1.0, - LagClock::EventTime, - ) - .expect("scale"); - assert!((scaled - 1.0).abs() < 1e-15); } #[test] - fn time_independent_predictor_refuses_cint_impulse_equation_fourteen_and_coefficient() { - let effect = 0.4_f64; - let predictor = 3.0_f64; - let increment = recover_discrete_time_independent_predictor_effect( - effect, - predictor, - -0.5, - 2.0, + fn standardised_continuous_time_independent_effect_recovers_driver_page_sixteen_after_positive_variances() + { + // Driver et al. (2017, p. 16 TIPREDEFFECTstd; §7.2; footnote 4): + // form strictly positive asymDIFFUSION = −q / (2 a) and + // strictly positive v, then B · √v / √p. + let diffusion = 0.4_f64; + let log_rate = -0.5_f64; + let coefficient = 0.3_f64; + let predictor_variance = 1.0_f64; + let recovered = recover_standardised_continuous_time_independent_predictor_effect( + coefficient, + predictor_variance, + diffusion, + log_rate, LagClock::EventTime, ) - .expect("tipred"); - let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("tdpred"); - let equation_fourteen = recover_discrete_time_varying_predictor_effect( - effect, - 2.0, - 2.0, + .expect("TIPREDEFFECTstd"); + let within = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + assert!(within > 0.0); + let expected = coefficient * predictor_variance.sqrt() / within.sqrt(); + assert!((recovered - expected).abs() < 1e-15); + let larger_q = recover_standardised_continuous_time_independent_predictor_effect( + coefficient, + predictor_variance, 2.0, + log_rate, LagClock::EventTime, ) - .expect("eq14"); + .expect("TIPREDEFFECTstd q=2"); + assert!((larger_q - recovered).abs() > 1e-3); + assert!(larger_q.abs() < recovered.abs()); + let asymptotic = recover_standardised_asymptotic_time_independent_predictor_effect( + coefficient, + predictor_variance, + diffusion, + log_rate, + LagClock::EventTime, + ) + .expect("asymTIPREDEFFECTstd"); + assert!((asymptotic - recovered).abs() > 1e-3); + let discrete_increment = recover_discrete_time_independent_predictor_effect( + coefficient, + 1.0, + log_rate, + 1.0, + LagClock::EventTime, + ) + .expect("discrete TIPREDEFFECT"); + let discrete_std = discrete_increment * predictor_variance.sqrt() / within.sqrt(); + assert!((discrete_std - recovered).abs() > 1e-3); + let trait_variance = 1.0_f64; + let total = recover_trait_plus_state_latent_variance(trait_variance, within) + .expect("trait+state var"); + let contaminated = coefficient * predictor_variance.sqrt() / total.sqrt(); + assert!((contaminated - recovered).abs() > 1e-3); + let zero = recover_standardised_continuous_time_independent_predictor_effect( + 0.0, + predictor_variance, + diffusion, + log_rate, + LagClock::EventTime, + ) + .expect("zero coefficient"); + assert_eq!(zero.to_bits(), 0.0_f64.to_bits()); assert_eq!( - refuse_time_independent_effect_as_continuous_intercept(increment, effect), - Err(PsychometricError::TimeIndependentEffectIsNotContinuousIntercept) + refuse_unstandardised_continuous_time_independent_effect_as_standardised_continuous_time_independent_effect( + coefficient, + recovered + ), + Err( + PsychometricError::UnstandardisedContinuousTimeIndependentEffectIsNotStandardisedContinuousTimeIndependentEffect + ) ); assert_eq!( - refuse_time_independent_effect_as_time_dependent_impulse(increment, impulse), - Err(PsychometricError::TimeIndependentEffectIsNotTimeDependentImpulse) + refuse_standardised_asymptotic_time_independent_effect_as_standardised_continuous_time_independent_effect( + asymptotic, + recovered + ), + Err( + PsychometricError::StandardisedAsymptoticTimeIndependentEffectIsNotStandardisedContinuousTimeIndependentEffect + ) ); assert_eq!( - refuse_time_independent_effect_as_time_varying_discrete_effect( - increment, - equation_fourteen + refuse_standardised_discrete_time_independent_effect_as_standardised_continuous_time_independent_effect( + discrete_std, + recovered ), - Err(PsychometricError::TimeIndependentEffectIsNotTimeVaryingDiscreteEffect) + Err( + PsychometricError::StandardisedDiscreteTimeIndependentEffectIsNotStandardisedContinuousTimeIndependentEffect + ) ); assert_eq!( - refuse_time_independent_coefficient_as_discrete_effect(effect, increment), - Err(PsychometricError::TimeIndependentCoefficientIsNotDiscreteEffect) + refuse_trait_contaminated_continuous_time_independent_effect_as_standardised_continuous_time_independent_effect( + contaminated, + recovered + ), + Err( + PsychometricError::TraitContaminatedContinuousTimeIndependentEffectIsNotStandardisedContinuousTimeIndependentEffect + ) ); - } - - #[test] - fn time_independent_predictor_invalid_inputs_fail_closed() { assert_eq!( - recover_discrete_time_independent_predictor_effect( - f64::NAN, - 1.0, - -0.5, - 2.0, - LagClock::EventTime - ), - Err(PsychometricError::InvalidNumericInput) + refuse_trait_variance_as_standardisation_variance(trait_variance, within), + Err(PsychometricError::TraitVarianceIsNotStandardisationVariance) ); + } + + #[test] + fn standardised_continuous_time_independent_effect_fails_closed_when_unstandardised_is_defined() + { + let log_rate = -0.5_f64; assert_eq!( - recover_discrete_time_independent_predictor_effect( + recover_standardised_continuous_time_independent_predictor_effect( + 0.3, 1.0, - f64::INFINITY, - -0.5, - 2.0, + 0.0, + log_rate, LagClock::EventTime ), - Err(PsychometricError::InvalidNumericInput) + Err( + PsychometricError::StandardisedContinuousTimeIndependentEffectRequiresPositiveWithinSubjectVariance + ) ); assert_eq!( - recover_discrete_time_independent_predictor_effect( - 1e308, - 2.0, - -0.5, - 2.0, + recover_standardised_continuous_time_independent_predictor_effect( + 0.3, + 0.0, + 0.4, + log_rate, LagClock::EventTime ), - Err(PsychometricError::InvalidNumericInput) + Err( + PsychometricError::StandardisedContinuousTimeIndependentEffectRequiresPositivePredictorVariance + ) ); assert_eq!( - recover_discrete_time_independent_predictor_effect( - 1e308, + recover_standardised_continuous_time_independent_predictor_effect( + 0.3, 1.0, - 0.0, - 2.0, + 0.4, + 0.5, LagClock::EventTime ), - Err(PsychometricError::InvalidNumericInput) + Err(PsychometricError::StationaryVarianceRequiresStableDrift) ); assert_eq!( - recover_discrete_time_independent_predictor_effect( + recover_standardised_continuous_time_independent_predictor_effect( + 0.3, + 1.0, 0.4, - 3.0, - -0.5, 0.0, LagClock::EventTime ), - Err(PsychometricError::NonPositiveInterval) + Err(PsychometricError::StationaryVarianceRequiresStableDrift) ); assert_eq!( - recover_discrete_time_independent_predictor_effect( + recover_standardised_continuous_time_independent_predictor_effect( + 0.3, + 1.0, 0.4, - 3.0, - -0.5, - 2.0, + log_rate, LagClock::SystemTime ), Err(PsychometricError::EventTimeRequired) ); assert_eq!( - recover_discrete_latent_mean_with_time_independent_predictor( - 1e308, - 0.0, - 0.0, - 1.0, - 1e308, - 1.0, + recover_standardised_continuous_time_independent_predictor_effect( + 0.3, + -0.1, + 0.4, + log_rate, LagClock::EventTime ), Err(PsychometricError::InvalidNumericInput) ); - // Latent mean is finite; Bz overflows. That `?` is not the sum overflow. assert_eq!( - recover_discrete_latent_mean_with_time_independent_predictor( - 1.0, - -0.5, + recover_standardised_continuous_time_independent_predictor_effect( 0.3, - 1e308, - 2.0, - 2.0, + f64::NAN, + 0.4, + log_rate, LagClock::EventTime ), Err(PsychometricError::InvalidNumericInput) ); - } - - #[test] - fn discrete_observed_mean_with_time_independent_predictor_recovers_driver_equation_five() { - let loading = 2.0_f64; - let drift = -0.5_f64; - let delta = 2.0_f64; - let effect = 0.4_f64; - let predictor = 3.0_f64; - let initial = 1.0_f64; - let intercept = 0.3_f64; - let manifest_mean = 0.5_f64; - let recovered = recover_discrete_observed_mean_with_time_independent_predictor( - loading, - initial, - drift, - intercept, - effect, - predictor, - manifest_mean, - delta, - LagClock::EventTime, - ) - .expect("eq5-tipred-mean"); - let composed = recover_discrete_latent_mean_with_time_independent_predictor( - initial, - drift, - intercept, - effect, - predictor, - delta, - LagClock::EventTime, - ) - .expect("eq3-tipred"); - let expected = manifest_mean + loading * composed; - assert!((recovered - expected).abs() < 1e-15); - let evolved_observed = recover_discrete_observed_mean( - loading, - initial, - drift, - intercept, - manifest_mean, - delta, - LagClock::EventTime, - ) - .expect("eq3-eq5-mean"); - let impulse_observed = recover_discrete_observed_mean_with_impulse( - loading, - initial, - drift, - intercept, - effect, - predictor, - manifest_mean, - delta, - LagClock::EventTime, - ) - .expect("eq5-impulse-mean"); - let carried_observed = recover_discrete_observed_mean_with_impulse_carry( - loading, - initial, - drift, - intercept, - effect, - predictor, - manifest_mean, - delta, - 1.0, - LagClock::EventTime, - ) - .expect("eq5-carry-mean"); - assert!((evolved_observed - recovered).abs() > 1e-3); - assert!((impulse_observed - recovered).abs() > 1e-3); - assert!((carried_observed - recovered).abs() > 1e-3); assert_eq!( - recover_discrete_observed_mean_with_time_independent_predictor( - 0.0, - initial, - drift, - intercept, - effect, - predictor, - manifest_mean, - delta, + recover_standardised_continuous_time_independent_predictor_effect( + f64::NAN, + 1.0, + 0.4, + log_rate, LagClock::EventTime ), - Ok(manifest_mean) + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_continuous_time_independent_predictor_effect( + 4.0, + 1e308, + 1e-308, + log_rate, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) ); } #[test] - fn discrete_observed_mean_with_time_independent_predictor_is_not_evolved_or_zero_increment() { - let loading = 2.0_f64; - let drift = -0.5_f64; - let delta = 2.0_f64; - let effect = 0.4_f64; - let predictor = 3.0_f64; - let initial = 1.0_f64; - let intercept = 0.3_f64; - let manifest_mean = 0.5_f64; - let recovered = recover_discrete_observed_mean_with_time_independent_predictor( - loading, - initial, - drift, - intercept, - effect, - predictor, - manifest_mean, - delta, - LagClock::EventTime, - ) - .expect("eq5-tipred-mean"); - let evolved_observed = recover_discrete_observed_mean( - loading, - initial, - drift, - intercept, - manifest_mean, - delta, - LagClock::EventTime, - ) - .expect("eq3-eq5-mean"); - let zero_increment = recover_discrete_observed_mean_with_time_independent_predictor( - loading, - initial, - drift, - intercept, - 0.0, - predictor, - manifest_mean, - delta, + fn standardised_continuous_time_dependent_effect_recovers_driver_page_sixteen_after_positive_variances() + { + // Driver et al. (2017, p. 16 TDPREDEFFECTstd; Table 2; footnote 4): + // form strictly positive asymDIFFUSION = −q / (2 a) and + // strictly positive v, then m · √v / √p. + let diffusion = 0.4_f64; + let log_rate = -0.5_f64; + let coefficient = 0.3_f64; + let predictor_variance = 1.0_f64; + let recovered = recover_standardised_continuous_time_dependent_predictor_effect( + coefficient, + predictor_variance, + diffusion, + log_rate, LagClock::EventTime, ) - .expect("zero-increment"); - assert!((zero_increment - evolved_observed).abs() < 1e-15); - assert!((recovered - evolved_observed).abs() > 1e-3); - } - - #[test] - fn discrete_observed_mean_with_time_independent_predictor_refuses_evolved_mean_and_overflow() { - let loading = 2.0_f64; - let recovered = recover_discrete_observed_mean_with_time_independent_predictor( - loading, - 1.0, - -0.5, - 0.3, - 0.4, - 3.0, - 0.5, + .expect("TDPREDEFFECTstd"); + let within = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + assert!(within > 0.0); + let expected = coefficient * predictor_variance.sqrt() / within.sqrt(); + assert!((recovered - expected).abs() < 1e-15); + let larger_q = recover_standardised_continuous_time_dependent_predictor_effect( + coefficient, + predictor_variance, 2.0, + log_rate, LagClock::EventTime, ) - .expect("eq5-tipred-mean"); - let evolved_observed = - recover_discrete_observed_mean(loading, 1.0, -0.5, 0.3, 0.5, 2.0, LagClock::EventTime) - .expect("eq3-eq5-mean"); - let impulse_observed = recover_discrete_observed_mean_with_impulse( - loading, - 1.0, - -0.5, - 0.3, - 0.4, - 3.0, - 0.5, - 2.0, + .expect("TDPREDEFFECTstd q=2"); + assert!((larger_q - recovered).abs() > 1e-3); + assert!(larger_q.abs() < recovered.abs()); + let tipred = recover_standardised_continuous_time_independent_predictor_effect( + coefficient, + predictor_variance, + diffusion, + log_rate, LagClock::EventTime, ) - .expect("eq5-impulse-mean"); - let carried_observed = recover_discrete_observed_mean_with_impulse_carry( - loading, + .expect("TIPREDEFFECTstd"); + assert_eq!(tipred.to_bits(), recovered.to_bits()); + let discrete_increment = recover_discrete_time_independent_predictor_effect( + coefficient, 1.0, - -0.5, - 0.3, - 0.4, - 3.0, - 0.5, - 2.0, + log_rate, 1.0, LagClock::EventTime, ) - .expect("eq5-carry-mean"); + .expect("intercept-style discrete TDPREDEFFECT"); + let discrete_std = discrete_increment * predictor_variance.sqrt() / within.sqrt(); + assert!((discrete_std - recovered).abs() > 1e-3); + let trait_variance = 1.0_f64; + let total = recover_trait_plus_state_latent_variance(trait_variance, within) + .expect("trait+state var"); + let contaminated = coefficient * predictor_variance.sqrt() / total.sqrt(); + assert!((contaminated - recovered).abs() > 1e-3); + let zero = recover_standardised_continuous_time_dependent_predictor_effect( + 0.0, + predictor_variance, + diffusion, + log_rate, + LagClock::EventTime, + ) + .expect("zero coefficient"); + assert_eq!(zero.to_bits(), 0.0_f64.to_bits()); assert_eq!( - refuse_evolved_observed_mean_as_time_independent_observed_mean( - evolved_observed, + refuse_unstandardised_continuous_time_dependent_effect_as_standardised_continuous_time_dependent_effect( + coefficient, recovered ), - Err(PsychometricError::EvolvedObservedMeanIsNotTimeIndependentObservedMean) + Err( + PsychometricError::UnstandardisedContinuousTimeDependentEffectIsNotStandardisedContinuousTimeDependentEffect + ) ); assert_eq!( - refuse_impulse_observed_mean_as_time_independent_observed_mean( - impulse_observed, + refuse_standardised_continuous_time_independent_effect_as_standardised_continuous_time_dependent_effect( + tipred, recovered ), - Err(PsychometricError::ImpulseObservedMeanIsNotTimeIndependentObservedMean) + Err( + PsychometricError::StandardisedContinuousTimeIndependentEffectIsNotStandardisedContinuousTimeDependentEffect + ) ); assert_eq!( - refuse_impulse_carry_observed_mean_as_time_independent_observed_mean( - carried_observed, + refuse_standardised_discrete_time_dependent_effect_as_standardised_continuous_time_dependent_effect( + discrete_std, recovered ), - Err(PsychometricError::ImpulseCarryObservedMeanIsNotTimeIndependentObservedMean) + Err( + PsychometricError::StandardisedDiscreteTimeDependentEffectIsNotStandardisedContinuousTimeDependentEffect + ) + ); + assert_eq!( + refuse_trait_contaminated_continuous_time_dependent_effect_as_standardised_continuous_time_dependent_effect( + contaminated, + recovered + ), + Err( + PsychometricError::TraitContaminatedContinuousTimeDependentEffectIsNotStandardisedContinuousTimeDependentEffect + ) + ); + assert_eq!( + refuse_trait_variance_as_standardisation_variance(trait_variance, within), + Err(PsychometricError::TraitVarianceIsNotStandardisationVariance) ); } #[test] - fn discrete_observed_mean_with_time_independent_predictor_invalid_inputs_fail_closed() { - let scaled = recover_discrete_observed_mean_with_time_independent_predictor( - 1e308, - 1e-308, - 0.0, - 0.0, - 0.0, - 3.0, - 0.0, - 1.0, - LagClock::EventTime, - ) - .expect("scale"); - assert!((scaled - 1.0).abs() < 1e-15); - let finite_loaded = recover_discrete_observed_mean_with_time_independent_predictor( - 1e308, - 0.0, - 0.0, - 0.0, - 0.0, - 3.0, - 0.0, - 1.0, - LagClock::EventTime, - ) - .expect("lambda-mu0"); - assert!((finite_loaded - 0.0).abs() < 1e-15); + fn standardised_continuous_time_dependent_effect_fails_closed_when_unstandardised_is_defined() { + let log_rate = -0.5_f64; assert_eq!( - recover_discrete_observed_mean_with_time_independent_predictor( - 1e308, - 2.0, - 0.0, - 0.0, + recover_standardised_continuous_time_dependent_predictor_effect( + 0.3, + 1.0, 0.0, - 3.0, + log_rate, + LagClock::EventTime + ), + Err( + PsychometricError::StandardisedContinuousTimeDependentEffectRequiresPositiveWithinSubjectVariance + ) + ); + assert_eq!( + recover_standardised_continuous_time_dependent_predictor_effect( + 0.3, 0.0, + 0.4, + log_rate, + LagClock::EventTime + ), + Err( + PsychometricError::StandardisedContinuousTimeDependentEffectRequiresPositivePredictorVariance + ) + ); + assert_eq!( + recover_standardised_continuous_time_dependent_predictor_effect( + 0.3, 1.0, + 0.4, + 0.5, LagClock::EventTime ), - Err(PsychometricError::InvalidNumericInput) + Err(PsychometricError::StationaryVarianceRequiresStableDrift) ); assert_eq!( - recover_discrete_observed_mean_with_time_independent_predictor( - 2.0, + recover_standardised_continuous_time_dependent_predictor_effect( + 0.3, 1.0, - -0.5, + 0.4, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::StationaryVarianceRequiresStableDrift) + ); + assert_eq!( + recover_standardised_continuous_time_dependent_predictor_effect( 0.3, + 1.0, 0.4, - 3.0, - 0.5, - 2.0, + log_rate, LagClock::SystemTime ), Err(PsychometricError::EventTimeRequired) ); assert_eq!( - recover_discrete_observed_mean_with_time_independent_predictor( - 2.0, - 1.0, - -0.5, + recover_standardised_continuous_time_dependent_predictor_effect( 0.3, + -0.1, 0.4, - 3.0, - 0.5, - 0.0, + log_rate, LagClock::EventTime ), - Err(PsychometricError::NonPositiveInterval) + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_observed_mean_with_time_independent_predictor( - 1e308, - 0.0, - 0.0, - 0.0, - 1e308, - 1.0, - 0.0, + recover_standardised_continuous_time_dependent_predictor_effect( + 0.3, + f64::NAN, + 0.4, + log_rate, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_continuous_time_dependent_predictor_effect( + f64::NAN, 1.0, + 0.4, + log_rate, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_continuous_time_dependent_predictor_effect( + 4.0, + 1e308, + 1e-308, + log_rate, LagClock::EventTime ), Err(PsychometricError::InvalidNumericInput) @@ -12099,1937 +22830,1846 @@ mod tests { } #[test] - fn time_dependent_impulse_carry_recovers_driver_equation_one_two_dissipation() { - let effect = 0.4_f64; - let predictor = 3.0_f64; - let drift = -0.5_f64; - let delta = 2.0_f64; - let elapsed = 1.0_f64; - let carry = recover_time_dependent_predictor_impulse_carry( - effect, - predictor, - drift, - delta, - elapsed, - LagClock::EventTime, - ) - .expect("tdpred-carry"); - let expected = (-0.5_f64).exp() * 1.2; - assert!((carry - expected).abs() < 1e-15); + fn standardised_effects_fail_closed_when_sd_ratio_overflows() { + let minimum_positive_variance = f64::from_bits(1); + let maximum_predictor_variance = f64::MAX; assert_eq!( - recover_time_dependent_predictor_impulse_carry( - 0.0, - predictor, - drift, - delta, - elapsed, + recover_standardised_asymptotic_time_independent_predictor_effect( + f64::NAN, + 1.0, + 0.4, + -0.5, LagClock::EventTime ), - Ok(0.0) + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_time_dependent_predictor_impulse_carry( - effect, - 0.0, - drift, - delta, - elapsed, + recover_standardised_asymptotic_time_independent_predictor_effect( + 0.4, + maximum_predictor_variance, + minimum_positive_variance, + -0.5, LagClock::EventTime ), - Ok(0.0) + Err(PsychometricError::InvalidNumericInput) ); - let zero_drift = recover_time_dependent_predictor_impulse_carry( - effect, - predictor, - 0.0, - delta, - elapsed, - LagClock::EventTime, - ) - .expect("zero-drift"); - assert!((zero_drift - 1.2).abs() < 1e-15); - let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("tdpred"); - assert!((carry - impulse).abs() > 1e-3); - let vanished = recover_time_dependent_predictor_impulse_carry( - effect, - predictor, - -800.0, - delta, - elapsed, - LagClock::EventTime, - ) - .expect("vanish"); - assert_eq!(vanished.to_bits(), 0.0_f64.to_bits()); - } - - #[test] - fn time_dependent_impulse_carry_composes_evolved_mean_and_keeps_scale() { - let effect = 0.4_f64; - let predictor = 3.0_f64; - let drift = -0.5_f64; - let delta = 2.0_f64; - let elapsed = 1.0_f64; - let carry = recover_time_dependent_predictor_impulse_carry( - effect, - predictor, - drift, - delta, - elapsed, - LagClock::EventTime, - ) - .expect("tdpred-carry"); - let initial = 1.0_f64; - let intercept = 0.3_f64; - let composed = recover_discrete_latent_mean_with_impulse_carry( - initial, - drift, - intercept, - effect, - predictor, - delta, - elapsed, - LagClock::EventTime, - ) - .expect("eq3-carry"); - let evolved = - recover_discrete_latent_mean(initial, drift, intercept, delta, LagClock::EventTime) - .expect("mu-t"); - assert!((composed - (evolved + carry)).abs() < 1e-15); assert_eq!( - recover_discrete_latent_mean_with_impulse_carry( - initial, - drift, - intercept, - 0.0, - predictor, - delta, - elapsed, + recover_standardised_continuous_time_independent_predictor_effect( + 0.4, + maximum_predictor_variance, + minimum_positive_variance, + -0.5, LagClock::EventTime ), - Ok(evolved) + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_latent_mean_with_impulse_carry( - 0.0, - drift, - 0.0, - effect, - predictor, - delta, - elapsed, + recover_standardised_continuous_time_dependent_predictor_effect( + 0.4, + maximum_predictor_variance, + minimum_positive_variance, + -0.5, LagClock::EventTime ), - Ok(carry) + Err(PsychometricError::InvalidNumericInput) ); - let scaled = recover_time_dependent_predictor_impulse_carry( - 1e308, - 1e-308, - 0.0, - 2.0, - 1.0, - LagClock::EventTime, - ) - .expect("scale"); - assert!((scaled - 1.0).abs() < 1e-15); - let rewritten = recover_time_dependent_predictor_impulse_carry( - 1e-308, - 1.0, - 710.0, - 2.0, - 1.0, - LagClock::EventTime, - ) - .expect("rewrite"); - let expected_rewrite = (1e-308_f64.ln() + 710.0).exp(); - assert!((rewritten - expected_rewrite).abs() <= expected_rewrite * 1e-12); - } - - #[test] - fn discrete_observed_mean_with_impulse_carry_recovers_driver_equation_five() { - let loading = 2.0_f64; - let drift = -0.5_f64; - let delta = 2.0_f64; - let elapsed = 1.0_f64; - let effect = 0.4_f64; - let predictor = 3.0_f64; - let initial = 1.0_f64; - let intercept = 0.3_f64; - let manifest_mean = 0.5_f64; - let recovered = recover_discrete_observed_mean_with_impulse_carry( - loading, - initial, - drift, - intercept, - effect, - predictor, - manifest_mean, - delta, - elapsed, - LagClock::EventTime, - ) - .expect("eq5-carry-mean"); - let carried = recover_discrete_latent_mean_with_impulse_carry( - initial, - drift, - intercept, - effect, - predictor, - delta, - elapsed, - LagClock::EventTime, - ) - .expect("carried"); - let expected = manifest_mean + loading * carried; - assert!((recovered - expected).abs() < 1e-15); - let evolved_observed = recover_discrete_observed_mean( - loading, - initial, - drift, - intercept, - manifest_mean, - delta, - LagClock::EventTime, - ) - .expect("eq3-eq5-mean"); - assert!((evolved_observed - recovered).abs() > 1e-3); assert_eq!( - recover_discrete_observed_mean_with_impulse_carry( - 0.0, - initial, - drift, - intercept, - effect, - predictor, - manifest_mean, - delta, - elapsed, + recover_standardised_initial_time_independent_predictor_effect( + 0.4, + maximum_predictor_variance, + minimum_positive_variance, LagClock::EventTime ), - Ok(manifest_mean) + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_observed_mean_with_impulse_carry( - loading, - initial, - drift, - intercept, - effect, - predictor, - 0.0, - delta, - elapsed, + recover_standardised_initial_time_dependent_predictor_effect( + 0.4, + maximum_predictor_variance, + minimum_positive_variance, LagClock::EventTime ), - Ok(loading * carried) + Err(PsychometricError::InvalidNumericInput) ); } #[test] - fn discrete_observed_mean_with_impulse_carry_is_not_contemporaneous_or_zero_carry() { - let loading = 2.0_f64; - let drift = -0.5_f64; - let delta = 2.0_f64; - let elapsed = 1.0_f64; - let effect = 0.4_f64; - let predictor = 3.0_f64; - let initial = 1.0_f64; - let intercept = 0.3_f64; - let manifest_mean = 0.5_f64; - let recovered = recover_discrete_observed_mean_with_impulse_carry( - loading, - initial, - drift, - intercept, - effect, - predictor, - manifest_mean, - delta, - elapsed, - LagClock::EventTime, - ) - .expect("eq5-carry-mean"); - let contemporaneous = recover_discrete_observed_mean_with_impulse( - loading, - initial, - drift, - intercept, - effect, - predictor, - manifest_mean, - delta, + fn standardised_initial_time_dependent_effect_recovers_driver_table_three_after_positive_variances() + { + // Driver et al. (2017, Table 3 T0TDPREDEFFECTstd; p. 16; footnote 4): + // form strictly positive free T0VAR p_0 and strictly positive v, + // then t0_m · √v / √p_0. Affected variance is p_0, not + // asymDIFFUSION. + let initial_variance = 1.6_f64; + let coefficient = 0.3_f64; + let predictor_variance = 1.0_f64; + let recovered = recover_standardised_initial_time_dependent_predictor_effect( + coefficient, + predictor_variance, + initial_variance, LagClock::EventTime, ) - .expect("eq5-mx"); - assert!((contemporaneous - recovered).abs() > 1e-3); - let evolved_observed = recover_discrete_observed_mean( - loading, - initial, - drift, - intercept, - manifest_mean, - delta, + .expect("T0TDPREDEFFECTstd"); + let expected = coefficient * predictor_variance.sqrt() / initial_variance.sqrt(); + assert!((recovered - expected).abs() < 1e-15); + let larger_p0 = recover_standardised_initial_time_dependent_predictor_effect( + coefficient, + predictor_variance, + 6.4, LagClock::EventTime, ) - .expect("eq3-eq5-mean"); - let zero_carry = recover_discrete_observed_mean_with_impulse_carry( - loading, - initial, - drift, - intercept, - 0.0, - predictor, - manifest_mean, - delta, - elapsed, + .expect("T0TDPREDEFFECTstd p_0=6.4"); + assert!((larger_p0 - recovered).abs() > 1e-3); + assert!(larger_p0.abs() < recovered.abs()); + let diffusion = 0.4_f64; + let log_rate = -0.5_f64; + let continuous = recover_standardised_continuous_time_dependent_predictor_effect( + coefficient, + predictor_variance, + diffusion, + log_rate, LagClock::EventTime, ) - .expect("zero-carry"); - assert!((zero_carry - evolved_observed).abs() < 1e-15); - } - - #[test] - fn discrete_observed_mean_with_impulse_carry_refuses_evolved_mean_and_overflow() { - let loading = 2.0_f64; - let recovered = recover_discrete_observed_mean_with_impulse_carry( - loading, - 1.0, - -0.5, - 0.3, - 0.4, - 3.0, - 0.5, - 2.0, - 1.0, + .expect("TDPREDEFFECTstd"); + assert!((continuous - recovered).abs() > 1e-3); + let t0_tipred = recover_standardised_initial_time_independent_predictor_effect( + coefficient, + predictor_variance, + initial_variance, LagClock::EventTime, ) - .expect("eq5-carry-mean"); - let carried = recover_discrete_latent_mean_with_impulse_carry( - 1.0, - -0.5, - 0.3, - 0.4, - 3.0, - 2.0, - 1.0, + .expect("T0TIPREDEFFECTstd"); + assert_eq!(t0_tipred.to_bits(), recovered.to_bits()); + let trait_variance = 1.0_f64; + let total = recover_trait_plus_state_latent_variance(trait_variance, initial_variance) + .expect("trait+state var"); + let contaminated = coefficient * predictor_variance.sqrt() / total.sqrt(); + assert!((contaminated - recovered).abs() > 1e-3); + let zero = recover_standardised_initial_time_dependent_predictor_effect( + 0.0, + predictor_variance, + initial_variance, LagClock::EventTime, ) - .expect("carried"); - let evolved_observed = - recover_discrete_observed_mean(loading, 1.0, -0.5, 0.3, 0.5, 2.0, LagClock::EventTime) - .expect("eq3-eq5-mean"); + .expect("zero coefficient"); + assert_eq!(zero.to_bits(), 0.0_f64.to_bits()); assert_eq!( - refuse_evolved_observed_mean_as_impulse_carry_observed_mean( - evolved_observed, + refuse_unstandardised_initial_time_dependent_effect_as_standardised_initial_time_dependent_effect( + coefficient, recovered ), - Err(PsychometricError::EvolvedObservedMeanIsNotImpulseCarryObservedMean) + Err( + PsychometricError::UnstandardisedInitialTimeDependentEffectIsNotStandardisedInitialTimeDependentEffect + ) ); assert_eq!( - refuse_impulse_observed_mean_as_impulse_carry_observed_mean( - recover_discrete_observed_mean_with_impulse( - loading, - 1.0, - -0.5, - 0.3, - 0.4, - 3.0, - 0.5, - 2.0, - LagClock::EventTime, - ) - .expect("eq5-mx"), + refuse_standardised_continuous_time_dependent_effect_as_standardised_initial_time_dependent_effect( + continuous, recovered ), - Err(PsychometricError::ImpulseObservedMeanIsNotImpulseCarryObservedMean) + Err( + PsychometricError::StandardisedContinuousTimeDependentEffectIsNotStandardisedInitialTimeDependentEffect + ) ); assert_eq!( - refuse_latent_mean_as_observed_mean(carried, recovered), - Err(PsychometricError::LatentMeanIsNotObservedMean) + refuse_standardised_initial_time_independent_effect_as_standardised_initial_time_dependent_effect( + t0_tipred, + recovered + ), + Err( + PsychometricError::StandardisedInitialTimeIndependentEffectIsNotStandardisedInitialTimeDependentEffect + ) ); assert_eq!( - refuse_manifest_means_as_observed_mean(0.5, recovered), - Err(PsychometricError::ManifestMeansIsNotObservedMean) + refuse_trait_contaminated_initial_time_dependent_effect_as_standardised_initial_time_dependent_effect( + contaminated, + recovered + ), + Err( + PsychometricError::TraitContaminatedInitialTimeDependentEffectIsNotStandardisedInitialTimeDependentEffect + ) + ); + assert_eq!( + refuse_trait_variance_as_standardisation_variance(trait_variance, initial_variance), + Err(PsychometricError::TraitVarianceIsNotStandardisationVariance) ); - let scaled = recover_discrete_observed_mean_with_impulse_carry( - 1e308, - 1e-308, - 0.0, - 0.0, - 0.0, - 3.0, - 0.0, - 2.0, - 1.0, - LagClock::EventTime, - ) - .expect("scale"); - assert!((scaled - 1.0).abs() < 1e-15); - let finite_loaded = recover_discrete_observed_mean_with_impulse_carry( - 1e308, - 1.0, - 0.0, - 0.0, - 0.0, - 3.0, - 0.0, - 2.0, - 1.0, - LagClock::EventTime, - ) - .expect("lambda-mu"); - assert!((finite_loaded - 1e308).abs() / 1e308 < 1e-15); } #[test] - fn discrete_observed_mean_with_impulse_carry_invalid_inputs_fail_closed() { + fn standardised_initial_time_dependent_effect_fails_closed_when_unstandardised_is_defined() { assert_eq!( - recover_discrete_observed_mean_with_impulse_carry( - f64::NAN, - 1.0, - -0.5, + recover_standardised_initial_time_dependent_predictor_effect( 0.3, - 0.4, - 3.0, - 0.5, - 2.0, 1.0, + 0.0, LagClock::EventTime ), - Err(PsychometricError::InvalidNumericInput) + Err( + PsychometricError::StandardisedInitialTimeDependentEffectRequiresPositiveInitialLatentVariance + ) ); assert_eq!( - recover_discrete_observed_mean_with_impulse_carry( - 1e308, - 2.0, - 0.0, - 0.0, - 0.0, - 3.0, + recover_standardised_initial_time_dependent_predictor_effect( + 0.3, 0.0, - 2.0, - 1.0, + 1.6, LagClock::EventTime ), - Err(PsychometricError::InvalidNumericInput) + Err( + PsychometricError::StandardisedInitialTimeDependentEffectRequiresPositivePredictorVariance + ) ); assert_eq!( - recover_discrete_observed_mean_with_impulse_carry( - 1.0, - 1.0, - 710.0, - 0.0, - 0.0, - 3.0, - 0.5, + recover_standardised_initial_time_dependent_predictor_effect( + 0.3, 1.0, - 0.5, + 1.6, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_standardised_initial_time_dependent_predictor_effect( + 0.3, + -0.1, + 1.6, LagClock::EventTime ), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_observed_mean_with_impulse_carry( - 1e308, - 0.0, - 0.0, - 0.0, - 1e308, - 1.0, - 0.0, - 2.0, + recover_standardised_initial_time_dependent_predictor_effect( + 0.3, 1.0, + -0.1, LagClock::EventTime ), Err(PsychometricError::InvalidNumericInput) ); - } - - #[test] - fn discrete_observed_mean_with_impulse_carry_interval_and_clock_fail_closed() { assert_eq!( - recover_discrete_observed_mean_with_impulse_carry( - 2.0, - 1.0, - -0.5, + recover_standardised_initial_time_dependent_predictor_effect( 0.3, - 0.4, - 3.0, - 0.5, - 2.0, - 0.0, + f64::NAN, + 1.6, LagClock::EventTime ), - Err(PsychometricError::NonPositiveInterval) + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_observed_mean_with_impulse_carry( - 2.0, - 1.0, - -0.5, + recover_standardised_initial_time_dependent_predictor_effect( 0.3, - 0.4, - 3.0, - 0.5, - 2.0, - 2.0, + 1.0, + f64::NAN, LagClock::EventTime ), - Err(PsychometricError::NonPositiveInterval) + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_observed_mean_with_impulse_carry( - 2.0, - 1.0, - -0.5, - 0.3, - 0.4, - 3.0, - 0.5, - 2.0, + recover_standardised_initial_time_dependent_predictor_effect( + f64::NAN, 1.0, - LagClock::SystemTime + 1.6, + LagClock::EventTime ), - Err(PsychometricError::EventTimeRequired) + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_initial_time_dependent_predictor_effect( + 4.0, + 1e308, + 1e-308, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) ); } #[test] - fn time_dependent_impulse_carry_refuses_contemporaneous_cint_tipred_and_equation_fourteen() { - let effect = 0.4_f64; - let predictor = 3.0_f64; - let drift = -0.5_f64; - let delta = 2.0_f64; - let elapsed = 1.0_f64; - let carry = recover_time_dependent_predictor_impulse_carry( - effect, - predictor, - drift, - delta, - elapsed, - LagClock::EventTime, - ) - .expect("tdpred-carry"); - let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("tdpred"); - let intercept_effect = - recover_discrete_continuous_intercept_effect(effect, drift, delta, LagClock::EventTime) - .expect("cint"); - let time_independent = recover_discrete_time_independent_predictor_effect( - effect, - predictor, - drift, - delta, - LagClock::EventTime, - ) - .expect("tipred"); - let equation_fourteen = recover_discrete_time_varying_predictor_effect( - effect, - delta, - delta, - delta, + fn standardised_initial_latent_variance_recovers_driver_table_two_after_positive_t0var() { + // Driver et al. (2017, Table 2 T0VAR; p. 16 T0VARstd; footnote 4; + // 2017-era summary.ctsemFit.R): form strictly positive free + // T0VAR p_0, then (1/√p_0) p_0 (1/√p_0) = 1. + let initial_variance = 1.6_f64; + let recovered = + recover_standardised_initial_latent_variance(initial_variance, LagClock::EventTime) + .expect("T0VARstd"); + assert!((recovered - 1.0).abs() < 1e-15); + let larger_p0 = recover_standardised_initial_latent_variance(6.4, LagClock::EventTime) + .expect("T0VARstd p_0=6.4"); + assert_eq!(larger_p0.to_bits(), recovered.to_bits()); + let standardised_effect = recover_standardised_initial_time_dependent_predictor_effect( + 0.3, + 1.0, + initial_variance, LagClock::EventTime, ) - .expect("eq14"); - assert!((carry - impulse).abs() > 1e-3); - assert!((carry - intercept_effect).abs() > 1e-3); - assert!((carry - time_independent).abs() > 1e-3); - assert!((carry - equation_fourteen).abs() > 1e-3); - assert_eq!( - refuse_time_dependent_impulse_carry_as_contemporaneous_impulse(carry, impulse), - Err(PsychometricError::TimeDependentImpulseCarryIsNotContemporaneousImpulse) + .expect("T0TDPREDEFFECTstd"); + assert!((standardised_effect - recovered).abs() > 1e-3); + let extra = + recover_initial_time_independent_predictor_variance(0.3, 4.0, LagClock::EventTime) + .expect("addedT0TIPREDVAR"); + assert!((extra - recovered).abs() > 1e-3); + assert_eq!( + refuse_unstandardised_initial_latent_variance_as_standardised_initial_latent_variance( + initial_variance, + recovered + ), + Err( + PsychometricError::UnstandardisedInitialLatentVarianceIsNotStandardisedInitialLatentVariance + ) ); assert_eq!( - refuse_time_dependent_impulse_carry_as_continuous_intercept(carry, effect), - Err(PsychometricError::TimeDependentImpulseCarryIsNotContinuousIntercept) + refuse_standardised_initial_time_dependent_effect_as_standardised_initial_latent_variance( + standardised_effect, + recovered + ), + Err( + PsychometricError::StandardisedInitialTimeDependentEffectIsNotStandardisedInitialLatentVariance + ) ); assert_eq!( - refuse_time_dependent_impulse_carry_as_time_independent_effect(carry, time_independent), - Err(PsychometricError::TimeDependentImpulseCarryIsNotTimeIndependentEffect) + refuse_initial_time_independent_variance_as_standardised_initial_latent_variance( + extra, + recovered + ), + Err( + PsychometricError::InitialTimeIndependentVarianceIsNotStandardisedInitialLatentVariance + ) ); assert_eq!( - refuse_time_dependent_impulse_carry_as_time_varying_discrete_effect( - carry, - equation_fourteen - ), - Err(PsychometricError::TimeDependentImpulseCarryIsNotTimeVaryingDiscreteEffect) + refuse_trait_variance_as_standardisation_variance(1.0, initial_variance), + Err(PsychometricError::TraitVarianceIsNotStandardisationVariance) ); } #[test] - fn time_dependent_impulse_carry_invalid_inputs_fail_closed() { + fn standardised_initial_latent_variance_fails_closed_when_unstandardised_is_defined() { assert_eq!( - recover_time_dependent_predictor_impulse_carry( - f64::NAN, - 1.0, - -0.5, - 2.0, - 1.0, - LagClock::EventTime - ), + recover_standardised_initial_latent_variance(0.0, LagClock::EventTime), + Err( + PsychometricError::StandardisedInitialLatentVarianceRequiresPositiveInitialLatentVariance + ) + ); + assert_eq!( + recover_standardised_initial_latent_variance(1.6, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_standardised_initial_latent_variance(-1.6, LagClock::EventTime), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_time_dependent_predictor_impulse_carry( - 0.4, - 3.0, - f64::INFINITY, - 2.0, - 1.0, - LagClock::EventTime - ), + recover_standardised_initial_latent_variance(f64::NAN, LagClock::EventTime), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_time_dependent_predictor_impulse_carry( - 1e308, - 2.0, - -0.5, - 2.0, - 1.0, - LagClock::EventTime - ), + recover_standardised_initial_latent_variance(f64::INFINITY, LagClock::EventTime), Err(PsychometricError::InvalidNumericInput) ); + } + + #[test] + fn standardised_trait_variance_recovers_driver_table_two_after_positive_traitvar() { + // Driver et al. (2017, Table 2 TRAITVAR; §7.1; p. 16 TRAITVARstd; + // 2017-era summary.ctsemFit.R): form strictly positive TRAITVAR, + // then (1/√trait) trait (1/√trait) = 1. No ridge addend. + let trait_variance = 1.6_f64; + let recovered = recover_standardised_trait_variance(trait_variance, LagClock::EventTime) + .expect("TRAITVARstd"); + assert!((recovered - 1.0).abs() < 1e-15); + let larger_trait = recover_standardised_trait_variance(6.4, LagClock::EventTime) + .expect("TRAITVARstd trait=6.4"); + assert_eq!(larger_trait.to_bits(), recovered.to_bits()); + let t0var_std = + recover_standardised_initial_latent_variance(trait_variance, LagClock::EventTime) + .expect("T0VARstd"); + assert_eq!(t0var_std.to_bits(), recovered.to_bits()); + let extra = + recover_initial_time_independent_predictor_variance(0.3, 4.0, LagClock::EventTime) + .expect("addedT0TIPREDVAR"); + assert!((extra - recovered).abs() > 1e-3); + assert_eq!( + refuse_unstandardised_trait_variance_as_standardised_trait_variance( + trait_variance, + recovered + ), + Err(PsychometricError::UnstandardisedTraitVarianceIsNotStandardisedTraitVariance) + ); assert_eq!( - recover_time_dependent_predictor_impulse_carry( - 1.2, - 1.0, - 800.0, - 2.0, - 1.0, - LagClock::EventTime + refuse_standardised_initial_latent_variance_as_standardised_trait_variance( + t0var_std, recovered ), - Err(PsychometricError::InvalidNumericInput) + Err(PsychometricError::StandardisedInitialLatentVarianceIsNotStandardisedTraitVariance) ); - // Finite log-rate whose product with elapsed overflows. exp(±∞) - // is not finite, then the non-finite drift interval fails closed. assert_eq!( - recover_time_dependent_predictor_impulse_carry( - 0.4, - 3.0, - 1e308, - 3.0, - 2.0, - LagClock::EventTime + refuse_initial_time_independent_variance_as_standardised_trait_variance( + extra, recovered ), + Err(PsychometricError::InitialTimeIndependentVarianceIsNotStandardisedTraitVariance) + ); + } + + #[test] + fn standardised_trait_variance_fails_closed_when_unstandardised_is_defined() { + assert_eq!( + recover_standardised_trait_variance(0.0, LagClock::EventTime), + Err(PsychometricError::StandardisedTraitVarianceRequiresPositiveTraitVariance) + ); + assert_eq!( + recover_standardised_trait_variance(1.6, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_standardised_trait_variance(-1.6, LagClock::EventTime), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_time_dependent_predictor_impulse_carry( - 0.0, - 3.0, - 800.0, - 2.0, - 1.0, - LagClock::EventTime + recover_standardised_trait_variance(f64::NAN, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_trait_variance(f64::INFINITY, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + } + + #[test] + fn standardised_manifest_trait_variance_recovers_driver_table_two_after_positive_psi() { + // Driver et al. (2017, Table 2 MANIFESTTRAITVAR; §7.1 p.19; + // p. 16 MANIFESTTRAITVARstd; 2017-era summary.ctsemFit.R): + // form strictly positive Ψ_τ, then (1/√ψ) ψ (1/√ψ) = 1. + // Default ridging = FALSE adds 0. + let manifest_trait = 1.6_f64; + let recovered = + recover_standardised_manifest_trait_variance(manifest_trait, LagClock::EventTime) + .expect("MANIFESTTRAITVARstd"); + assert!((recovered - 1.0).abs() < 1e-15); + let larger_psi = recover_standardised_manifest_trait_variance(6.4, LagClock::EventTime) + .expect("MANIFESTTRAITVARstd ψ=6.4"); + assert_eq!(larger_psi.to_bits(), recovered.to_bits()); + let trait_std = recover_standardised_trait_variance(manifest_trait, LagClock::EventTime) + .expect("TRAITVARstd"); + assert_eq!(trait_std.to_bits(), recovered.to_bits()); + assert_eq!( + refuse_unstandardised_manifest_trait_variance_as_standardised_manifest_trait_variance( + manifest_trait, + recovered + ), + Err( + PsychometricError::UnstandardisedManifestTraitVarianceIsNotStandardisedManifestTraitVariance + ) + ); + assert_eq!( + refuse_standardised_trait_variance_as_standardised_manifest_trait_variance( + trait_std, recovered ), - Ok(0.0) + Err(PsychometricError::StandardisedTraitVarianceIsNotStandardisedManifestTraitVariance) ); assert_eq!( - recover_time_dependent_predictor_impulse_carry( - -1e-308, - 1.0, - 710.0, - 2.0, - 1.0, - LagClock::EventTime - ) - .map(f64::signum), - Ok(-1.0) + refuse_measurement_error_as_standardised_manifest_trait_variance(0.4, recovered), + Err(PsychometricError::MeasurementErrorIsNotStandardisedManifestTraitVariance) ); } #[test] - fn time_dependent_impulse_carry_interval_and_clock_fail_closed() { + fn standardised_manifest_trait_variance_fails_closed_when_unstandardised_is_defined() { assert_eq!( - recover_time_dependent_predictor_impulse_carry( - 0.4, - 3.0, - -0.5, - 0.0, - 1.0, - LagClock::EventTime - ), - Err(PsychometricError::NonPositiveInterval) + recover_standardised_manifest_trait_variance(0.0, LagClock::EventTime), + Err( + PsychometricError::StandardisedManifestTraitVarianceRequiresPositiveManifestTraitVariance + ) ); assert_eq!( - recover_time_dependent_predictor_impulse_carry( - 0.4, - 3.0, - -0.5, - 2.0, - 0.0, - LagClock::EventTime - ), - Err(PsychometricError::NonPositiveInterval) + recover_standardised_manifest_trait_variance(1.6, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) ); assert_eq!( - recover_time_dependent_predictor_impulse_carry( - 0.4, - 3.0, - -0.5, - 2.0, - 2.0, - LagClock::EventTime - ), - Err(PsychometricError::NonPositiveInterval) + recover_standardised_manifest_trait_variance(-1.6, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_time_dependent_predictor_impulse_carry( - 0.4, - 3.0, - -0.5, - 2.0, - 1.0, - LagClock::SystemTime + recover_standardised_manifest_trait_variance(f64::NAN, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_manifest_trait_variance(f64::INFINITY, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + } + + #[test] + fn standardised_manifest_variance_recovers_driver_table_two_after_positive_theta() { + // Driver et al. (2017, Table 2 MANIFESTVAR Θ; Eq. 5 p.5; + // p. 16 MANIFESTVARstd; 2017-era summary.ctsemFit.R): + // form strictly positive Θ, then (1/√θ) θ (1/√θ) = 1. + // Default ridging = FALSE adds 0. + let measurement_error = 0.4_f64; + let recovered = + recover_standardised_manifest_variance(measurement_error, LagClock::EventTime) + .expect("MANIFESTVARstd"); + assert!((recovered - 1.0).abs() < 1e-15); + let larger_theta = recover_standardised_manifest_variance(1.6, LagClock::EventTime) + .expect("MANIFESTVARstd θ=1.6"); + assert_eq!(larger_theta.to_bits(), recovered.to_bits()); + let manifest_trait_std = + recover_standardised_manifest_trait_variance(measurement_error, LagClock::EventTime) + .expect("MANIFESTTRAITVARstd"); + assert_eq!(manifest_trait_std.to_bits(), recovered.to_bits()); + let observed = + recover_manifest_observed_variance(2.0, 0.4, measurement_error).expect("Var(y)"); + assert_eq!( + refuse_unstandardised_manifest_variance_as_standardised_manifest_variance( + measurement_error, + recovered ), - Err(PsychometricError::EventTimeRequired) + Err(PsychometricError::UnstandardisedManifestVarianceIsNotStandardisedManifestVariance) ); assert_eq!( - recover_discrete_latent_mean_with_impulse_carry( - 1e308, - 0.0, - 0.0, - 1e308, - 1.0, - 2.0, - 1.0, - LagClock::EventTime + refuse_standardised_manifest_trait_variance_as_standardised_manifest_variance( + manifest_trait_std, + recovered ), - Err(PsychometricError::InvalidNumericInput) + Err( + PsychometricError::StandardisedManifestTraitVarianceIsNotStandardisedManifestVariance + ) + ); + assert_eq!( + refuse_observed_variance_as_standardised_manifest_variance(observed, recovered), + Err(PsychometricError::ObservedVarianceIsNotStandardisedManifestVariance) ); } #[test] - fn initial_time_independent_predictor_recovers_table_three_t0_shift_and_carry() { - let effect = 0.4_f64; - let predictor = 3.0_f64; - let drift = -0.5_f64; - let delta = 2.0_f64; - let shift = recover_initial_time_independent_predictor_effect(effect, predictor) - .expect("t0-tipred"); - assert!((shift - 1.2).abs() < 1e-15); + fn standardised_manifest_variance_fails_closed_when_unstandardised_is_defined() { assert_eq!( - recover_initial_time_independent_predictor_effect(0.0, predictor), - Ok(0.0) + recover_standardised_manifest_variance(0.0, LagClock::EventTime), + Err(PsychometricError::StandardisedManifestVarianceRequiresPositiveManifestVariance) ); assert_eq!( - recover_initial_time_independent_predictor_effect(effect, 0.0), - Ok(0.0) + recover_standardised_manifest_variance(0.4, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_standardised_manifest_variance(-0.4, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_manifest_variance(f64::NAN, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_manifest_variance(f64::INFINITY, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) ); - let carry = recover_initial_time_independent_predictor_carry( - effect, - predictor, - drift, - delta, - LagClock::EventTime, - ) - .expect("t0-carry"); - let expected = 1.2 * (drift * delta).exp(); - assert!((carry - expected).abs() < 1e-15); - let zero_drift = recover_initial_time_independent_predictor_carry( - effect, - predictor, - 0.0, - delta, - LagClock::EventTime, - ) - .expect("zero-drift"); - assert!((zero_drift - 1.2).abs() < 1e-15); - let vanished = recover_initial_time_independent_predictor_carry( - effect, - predictor, - -800.0, - 1.0, - LagClock::EventTime, - ) - .expect("underflow"); - assert_eq!(vanished.to_bits(), 0.0_f64.to_bits()); - let increment = recover_discrete_time_independent_predictor_effect( - effect, - predictor, - drift, - delta, - LagClock::EventTime, - ) - .expect("tipred"); - let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("tdpred"); - assert!((carry - shift).abs() > 1e-3); - assert!((carry - increment).abs() > 1e-3); - assert!((shift - increment).abs() > 1e-3); - assert!((shift - effect).abs() > 1e-3); - // Algebraically a product, like M x, but Table 3 names a different matrix. - assert!((shift - impulse).abs() < 1e-15); } #[test] - fn initial_time_independent_predictor_composes_evolved_mean_and_keeps_scale() { - let effect = 0.4_f64; - let predictor = 3.0_f64; - let drift = -0.5_f64; - let delta = 2.0_f64; - let carry = recover_initial_time_independent_predictor_carry( - effect, - predictor, - drift, - delta, + fn standardised_time_independent_predictor_variance_recovers_driver_table_two_after_positive_v() + { + // Driver et al. (2017, Table 2 TIPREDVAR; p. 16 TIPREDVARstd; + // 2017-era summary.ctsemFit.R): form strictly positive v, then + // (1/√v) v (1/√v) = 1. Default ridging = FALSE adds 0. + let predictor_variance = 0.4_f64; + let recovered = recover_standardised_time_independent_predictor_variance( + predictor_variance, LagClock::EventTime, ) - .expect("t0-carry"); - let initial = 1.0_f64; - let intercept = 0.3_f64; - let composed = recover_discrete_latent_mean_with_initial_time_independent_predictor( - initial, - drift, - intercept, - effect, - predictor, - delta, + .expect("TIPREDVARstd"); + assert!((recovered - 1.0).abs() < 1e-15); + let larger_v = + recover_standardised_time_independent_predictor_variance(1.6, LagClock::EventTime) + .expect("TIPREDVARstd v=1.6"); + assert_eq!(larger_v.to_bits(), recovered.to_bits()); + let manifest_std = + recover_standardised_manifest_variance(predictor_variance, LagClock::EventTime) + .expect("MANIFESTVARstd"); + assert_eq!(manifest_std.to_bits(), recovered.to_bits()); + let added = recover_asymptotic_time_independent_predictor_variance( + 0.2, + predictor_variance, + -0.5, LagClock::EventTime, ) - .expect("eq3-t0tipred"); - let evolved = - recover_discrete_latent_mean(initial, drift, intercept, delta, LagClock::EventTime) - .expect("mu-t"); - assert!((composed - (evolved + carry)).abs() < 1e-15); + .expect("addedTIPREDVAR"); assert_eq!( - recover_discrete_latent_mean_with_initial_time_independent_predictor( - initial, - drift, - intercept, - 0.0, - predictor, - delta, - LagClock::EventTime + refuse_unstandardised_time_independent_predictor_variance_as_standardised_time_independent_predictor_variance( + predictor_variance, + recovered ), - Ok(evolved) + Err(PsychometricError::UnstandardisedTimeIndependentPredictorVarianceIsNotStandardisedTimeIndependentPredictorVariance) ); assert_eq!( - recover_discrete_latent_mean_with_initial_time_independent_predictor( - 0.0, - drift, - 0.0, - effect, - predictor, - delta, - LagClock::EventTime + refuse_standardised_manifest_variance_as_standardised_time_independent_predictor_variance( + manifest_std, + recovered ), - Ok(carry) + Err(PsychometricError::StandardisedManifestVarianceIsNotStandardisedTimeIndependentPredictorVariance) ); - let scaled = recover_initial_time_independent_predictor_carry( - 1e308, - 1e-308, - 0.0, - 1.0, - LagClock::EventTime, - ) - .expect("scale"); - assert!((scaled - 1.0).abs() < 1e-15); - let rewritten = recover_initial_time_independent_predictor_carry( - 2.0, - 0.5, - 710.0, - 1.0, - LagClock::EventTime, + assert_eq!( + refuse_asymptotic_time_independent_predictor_variance_as_standardised_time_independent_predictor_variance( + added, + recovered + ), + Err(PsychometricError::AsymptoticTimeIndependentPredictorVarianceIsNotStandardisedTimeIndependentPredictorVariance) ); - assert_eq!(rewritten, Err(PsychometricError::InvalidNumericInput)); - let finite_rewrite = recover_initial_time_independent_predictor_carry( - 1e-308, - 1.0, - 700.0, - 1.0, - LagClock::EventTime, - ) - .expect("log-rewrite"); - let expected_rewrite = (1e-308_f64.ln() + 700.0).exp(); - assert!((finite_rewrite - expected_rewrite).abs() / expected_rewrite < 1e-12); } #[test] - fn initial_time_independent_predictor_refuses_process_increment_cint_impulse_and_coefficient() { - let effect = 0.4_f64; - let predictor = 3.0_f64; - let shift = recover_initial_time_independent_predictor_effect(effect, predictor) - .expect("t0-tipred"); - let carry = recover_initial_time_independent_predictor_carry( - effect, - predictor, - -0.5, - 2.0, - LagClock::EventTime, - ) - .expect("t0-carry"); - let increment = recover_discrete_time_independent_predictor_effect( - effect, - predictor, - -0.5, - 2.0, - LagClock::EventTime, - ) - .expect("tipred"); - let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("tdpred"); + fn standardised_time_independent_predictor_variance_fails_closed_when_unstandardised_is_defined() + { assert_eq!( - refuse_initial_time_independent_effect_as_process_increment(shift, increment), - Err(PsychometricError::InitialTimeIndependentEffectIsNotProcessIncrement) + recover_standardised_time_independent_predictor_variance(0.0, LagClock::EventTime), + Err(PsychometricError::StandardisedTimeIndependentPredictorVarianceRequiresPositivePredictorVariance) + ); + assert_eq!( + recover_standardised_time_independent_predictor_variance(0.4, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_standardised_time_independent_predictor_variance(-0.4, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_time_independent_predictor_variance(f64::NAN, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - refuse_initial_time_independent_carry_as_initial_effect(carry, shift), - Err(PsychometricError::InitialTimeIndependentCarryIsNotInitialEffect) + recover_standardised_time_independent_predictor_variance( + f64::INFINITY, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) ); + } + + #[test] + fn standardised_asymptotic_diffusion_recovers_driver_page_sixteen_after_positive_p() { + // Driver et al. (2017, p. 16 asymDIFFUSIONstd; Eq. 4; footnote 4; + // 2017-era summary.ctsemFit.R): form strictly positive + // p = −q / (2 a), then (1/√p) p (1/√p) = 1. Default + // ridging = FALSE adds 0. q=0.4, a=−0.25 → p=0.8, + // DIFFUSIONstd=−2a=0.5, asymDIFFUSIONstd=1. + let diffusion = 0.4_f64; + let log_rate = -0.25_f64; + let recovered = + recover_standardised_asymptotic_diffusion(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSIONstd"); + assert!((recovered - 1.0).abs() < 1e-15); + let larger_q = + recover_standardised_asymptotic_diffusion(1.6, log_rate, LagClock::EventTime) + .expect("asymDIFFUSIONstd q=1.6"); + assert_eq!(larger_q.to_bits(), recovered.to_bits()); + let within = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + assert!((within - 0.8).abs() < 1e-15); + let predictor_std = + recover_standardised_time_independent_predictor_variance(within, LagClock::EventTime) + .expect("TIPREDVARstd"); + assert_eq!(predictor_std.to_bits(), recovered.to_bits()); + let diffusion_std = + recover_standardised_continuous_diffusion(diffusion, log_rate, LagClock::EventTime) + .expect("DIFFUSIONstd"); + assert!((diffusion_std - 0.5).abs() < 1e-15); + assert!((diffusion_std - recovered).abs() > 1e-3); assert_eq!( - refuse_initial_time_independent_effect_as_continuous_intercept(shift, 0.4), - Err(PsychometricError::InitialTimeIndependentEffectIsNotContinuousIntercept) + refuse_unstandardised_asymptotic_diffusion_as_standardised_asymptotic_diffusion( + within, recovered + ), + Err(PsychometricError::UnstandardisedAsymptoticDiffusionIsNotStandardisedAsymptoticDiffusion) ); assert_eq!( - refuse_initial_time_independent_effect_as_time_dependent_impulse(shift, impulse), - Err(PsychometricError::InitialTimeIndependentEffectIsNotTimeDependentImpulse) + refuse_standardised_time_independent_predictor_variance_as_standardised_asymptotic_diffusion( + predictor_std, + recovered + ), + Err(PsychometricError::StandardisedTimeIndependentPredictorVarianceIsNotStandardisedAsymptoticDiffusion) ); assert_eq!( - refuse_initial_time_independent_coefficient_as_initial_effect(effect, shift), - Err(PsychometricError::InitialTimeIndependentCoefficientIsNotInitialEffect) + refuse_standardised_continuous_diffusion_as_standardised_asymptotic_diffusion( + diffusion_std, + recovered + ), + Err(PsychometricError::StandardisedContinuousDiffusionIsNotStandardisedAsymptoticDiffusion) ); } #[test] - fn initial_time_independent_predictor_invalid_inputs_fail_closed() { + fn standardised_asymptotic_diffusion_fails_closed_when_unstandardised_is_defined() { assert_eq!( - recover_initial_time_independent_predictor_effect(f64::NAN, 1.0), + recover_standardised_asymptotic_diffusion(0.0, -0.25, LagClock::EventTime), + Err(PsychometricError::StandardisedAsymptoticDiffusionRequiresPositiveWithinSubjectVariance) + ); + assert_eq!( + recover_standardised_asymptotic_diffusion(0.4, 0.5, LagClock::EventTime), + Err(PsychometricError::StationaryVarianceRequiresStableDrift) + ); + assert_eq!( + recover_standardised_asymptotic_diffusion(0.4, 0.0, LagClock::EventTime), + Err(PsychometricError::StationaryVarianceRequiresStableDrift) + ); + assert_eq!( + recover_standardised_asymptotic_diffusion(0.4, -0.25, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_standardised_asymptotic_diffusion(-0.1, -0.25, LagClock::EventTime), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_initial_time_independent_predictor_effect(1.0, f64::INFINITY), + recover_standardised_asymptotic_diffusion(f64::NAN, -0.25, LagClock::EventTime), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_initial_time_independent_predictor_effect(1e308, 2.0), + recover_standardised_asymptotic_diffusion(f64::INFINITY, -0.25, LagClock::EventTime), Err(PsychometricError::InvalidNumericInput) ); + } + + #[test] + fn standardised_discrete_continuous_intercept_recovers_driver_page_sixteen_after_positive_p() { + // Driver et al. (2017, p. 16 discreteCINTstd; Eq. 3; footnote 4; + // 2017-era summary.ctsemFit.R discreteCINT): form strictly + // positive p = −q / (2 a), then A^{-1}[e^{A Δt} − I] κ / √p. + // q=0.4, a=−0.25, Δt=1, κ=0.3 → p=0.8. + let intercept = 0.3_f64; + let diffusion = 0.4_f64; + let log_rate = -0.25_f64; + let event_delta = 1.0_f64; + let recovered = recover_standardised_discrete_continuous_intercept( + intercept, + diffusion, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("discreteCINTstd"); + let discrete = recover_discrete_continuous_intercept_effect( + intercept, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("discreteCINT"); + let within = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + let expected = discrete / within.sqrt(); + assert!((recovered - expected).abs() < 1e-15); + let continuous_std = intercept / within.sqrt(); + assert!((continuous_std - recovered).abs() > 1e-3); + let asymptotic = + recover_asymptotic_continuous_intercept(intercept, log_rate, LagClock::EventTime) + .expect("asymCINT"); + let asymptotic_std = asymptotic / within.sqrt(); + assert!((asymptotic_std - recovered).abs() > 1e-3); + let later = recover_standardised_discrete_continuous_intercept( + intercept, + diffusion, + log_rate, + 2.5, + LagClock::EventTime, + ) + .expect("discreteCINTstd Δt=2.5"); + assert!((later - recovered).abs() > 1e-3); + let zero = recover_standardised_discrete_continuous_intercept( + 0.0, + diffusion, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("zero CINT"); + assert_eq!(zero.to_bits(), 0.0_f64.to_bits()); assert_eq!( - recover_initial_time_independent_predictor_carry( - 0.4, - 3.0, - f64::NAN, - 2.0, - LagClock::EventTime + refuse_unstandardised_discrete_continuous_intercept_as_standardised_discrete_continuous_intercept( + discrete, + recovered ), - Err(PsychometricError::InvalidNumericInput) + Err(PsychometricError::UnstandardisedDiscreteContinuousInterceptIsNotStandardisedDiscreteContinuousIntercept) ); assert_eq!( - recover_initial_time_independent_predictor_carry( - 0.4, - 3.0, - -0.5, - 0.0, - LagClock::EventTime + refuse_standardised_continuous_intercept_as_standardised_discrete_continuous_intercept( + continuous_std, + recovered ), - Err(PsychometricError::NonPositiveInterval) + Err(PsychometricError::StandardisedContinuousInterceptIsNotStandardisedDiscreteContinuousIntercept) ); assert_eq!( - recover_initial_time_independent_predictor_carry( - 0.4, - 3.0, - -0.5, - 2.0, - LagClock::SystemTime + refuse_asymptotic_standardised_continuous_intercept_as_standardised_discrete_continuous_intercept( + asymptotic_std, + recovered ), - Err(PsychometricError::EventTimeRequired) + Err(PsychometricError::AsymptoticStandardisedContinuousInterceptIsNotStandardisedDiscreteContinuousIntercept) ); + } + + #[test] + fn standardised_discrete_continuous_intercept_fails_closed_when_unstandardised_is_defined() { assert_eq!( - recover_discrete_latent_mean_with_initial_time_independent_predictor( - 1e308, - 0.0, + recover_standardised_discrete_continuous_intercept( + 0.3, 0.0, - 1e308, - 1.0, + -0.25, 1.0, LagClock::EventTime ), - Err(PsychometricError::InvalidNumericInput) + Err(PsychometricError::StandardisedDiscreteContinuousInterceptRequiresPositiveWithinSubjectVariance) ); assert_eq!( - recover_initial_time_independent_predictor_carry( - 1.0, - 1.0, - f64::INFINITY, + recover_standardised_discrete_continuous_intercept( + 0.3, + 0.4, + 0.5, 1.0, LagClock::EventTime ), - Err(PsychometricError::InvalidNumericInput) + Err(PsychometricError::StationaryVarianceRequiresStableDrift) ); assert_eq!( - recover_initial_time_independent_predictor_carry( - f64::NAN, + recover_standardised_discrete_continuous_intercept( + 0.3, + 0.4, + 0.0, 1.0, - -0.5, - 2.0, LagClock::EventTime ), - Err(PsychometricError::InvalidNumericInput) + Err(PsychometricError::StationaryVarianceRequiresStableDrift) ); assert_eq!( - recover_initial_time_independent_predictor_carry( - 1.0, + recover_standardised_discrete_continuous_intercept( + 0.3, + 0.4, + -0.25, 1.0, - 1e308, - 10.0, - LagClock::EventTime + LagClock::SystemTime ), - Err(PsychometricError::InvalidNumericInput) + Err(PsychometricError::EventTimeRequired) ); assert_eq!( - recover_discrete_latent_mean_with_initial_time_independent_predictor( - 1.0, - -0.5, + recover_standardised_discrete_continuous_intercept( 0.3, + 0.4, + -0.25, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_standardised_discrete_continuous_intercept( f64::NAN, + 0.4, + -0.25, 1.0, - 2.0, LagClock::EventTime ), Err(PsychometricError::InvalidNumericInput) ); - } - - #[test] - fn discrete_observed_mean_with_initial_time_independent_predictor_recovers_driver_equation_five() - { - let loading = 2.0_f64; - let drift = -0.5_f64; - let delta = 2.0_f64; - let effect = 0.4_f64; - let predictor = 3.0_f64; - let initial = 1.0_f64; - let intercept = 0.3_f64; - let manifest_mean = 0.5_f64; - let recovered = recover_discrete_observed_mean_with_initial_time_independent_predictor( - loading, - initial, - drift, - intercept, - effect, - predictor, - manifest_mean, - delta, - LagClock::EventTime, - ) - .expect("eq5-t0tipred-mean"); - let composed = recover_discrete_latent_mean_with_initial_time_independent_predictor( - initial, - drift, - intercept, - effect, - predictor, - delta, - LagClock::EventTime, - ) - .expect("eq3-t0tipred"); - let expected = manifest_mean + loading * composed; - assert!((recovered - expected).abs() < 1e-15); - let evolved_observed = recover_discrete_observed_mean( - loading, - initial, - drift, - intercept, - manifest_mean, - delta, - LagClock::EventTime, - ) - .expect("eq3-eq5-mean"); - let process_observed = recover_discrete_observed_mean_with_time_independent_predictor( - loading, - initial, - drift, - intercept, - effect, - predictor, - manifest_mean, - delta, - LagClock::EventTime, - ) - .expect("eq5-tipred-mean"); - let impulse_observed = recover_discrete_observed_mean_with_impulse( - loading, - initial, - drift, - intercept, - effect, - predictor, - manifest_mean, - delta, - LagClock::EventTime, - ) - .expect("eq5-impulse-mean"); - let carried_observed = recover_discrete_observed_mean_with_impulse_carry( - loading, - initial, - drift, - intercept, - effect, - predictor, - manifest_mean, - delta, - 1.0, - LagClock::EventTime, - ) - .expect("eq5-carry-mean"); - assert!((evolved_observed - recovered).abs() > 1e-3); - assert!((process_observed - recovered).abs() > 1e-3); - assert!((impulse_observed - recovered).abs() > 1e-3); - assert!((carried_observed - recovered).abs() > 1e-3); assert_eq!( - recover_discrete_observed_mean_with_initial_time_independent_predictor( - 0.0, - initial, - drift, - intercept, - effect, - predictor, - manifest_mean, - delta, + recover_standardised_discrete_continuous_intercept( + 0.3, + f64::INFINITY, + -0.25, + 1.0, LagClock::EventTime ), - Ok(manifest_mean) + Err(PsychometricError::InvalidNumericInput) ); } #[test] - fn discrete_observed_mean_with_initial_time_independent_predictor_is_not_evolved_or_zero_carry() + fn standardised_asymptotic_continuous_intercept_recovers_driver_page_sixteen_after_positive_p() { - let loading = 2.0_f64; - let drift = -0.5_f64; - let delta = 2.0_f64; - let effect = 0.4_f64; - let predictor = 3.0_f64; - let initial = 1.0_f64; + // Driver et al. (2017, p. 16 asymCINTstd; Table 2; footnote 4; + // 2017-era summary.ctsemFit.R asymCINT): form strictly + // positive p = −q / (2 a), then (−κ / a) / √p. + // q=0.4, a=−0.25, κ=0.3 → p=0.8, −κ/a=1.2. let intercept = 0.3_f64; - let manifest_mean = 0.5_f64; - let recovered = recover_discrete_observed_mean_with_initial_time_independent_predictor( - loading, - initial, - drift, + let diffusion = 0.4_f64; + let log_rate = -0.25_f64; + let recovered = recover_standardised_asymptotic_continuous_intercept( intercept, - effect, - predictor, - manifest_mean, - delta, + diffusion, + log_rate, LagClock::EventTime, ) - .expect("eq5-t0tipred-mean"); - let evolved_observed = recover_discrete_observed_mean( - loading, - initial, - drift, + .expect("asymCINTstd"); + let asymptotic = + recover_asymptotic_continuous_intercept(intercept, log_rate, LagClock::EventTime) + .expect("asymCINT"); + let within = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + let expected = asymptotic / within.sqrt(); + assert!((recovered - expected).abs() < 1e-15); + let continuous_std = intercept / within.sqrt(); + assert!((continuous_std - recovered).abs() > 1e-3); + let discrete_std = recover_standardised_discrete_continuous_intercept( intercept, - manifest_mean, - delta, + diffusion, + log_rate, + 1.0, LagClock::EventTime, ) - .expect("eq3-eq5-mean"); - let zero_carry = recover_discrete_observed_mean_with_initial_time_independent_predictor( - loading, - initial, - drift, + .expect("discreteCINTstd"); + assert!((discrete_std - recovered).abs() > 1e-3); + let later_discrete = recover_standardised_discrete_continuous_intercept( intercept, + diffusion, + log_rate, + 2.5, + LagClock::EventTime, + ) + .expect("discreteCINTstd Δt=2.5"); + assert!((later_discrete - recovered).abs() > 1e-3); + assert!((later_discrete - discrete_std).abs() > 1e-3); + let zero = recover_standardised_asymptotic_continuous_intercept( 0.0, - predictor, - manifest_mean, - delta, + diffusion, + log_rate, LagClock::EventTime, ) - .expect("zero-carry"); - assert!((zero_carry - evolved_observed).abs() < 1e-15); - assert!((recovered - evolved_observed).abs() > 1e-3); + .expect("zero CINT"); + assert_eq!(zero.to_bits(), 0.0_f64.to_bits()); + assert_eq!( + refuse_unstandardised_asymptotic_continuous_intercept_as_standardised_asymptotic_continuous_intercept( + asymptotic, + recovered + ), + Err(PsychometricError::UnstandardisedAsymptoticContinuousInterceptIsNotStandardisedAsymptoticContinuousIntercept) + ); + assert_eq!( + refuse_standardised_continuous_intercept_as_standardised_asymptotic_continuous_intercept( + continuous_std, + recovered + ), + Err(PsychometricError::StandardisedContinuousInterceptIsNotStandardisedAsymptoticContinuousIntercept) + ); + assert_eq!( + refuse_standardised_discrete_continuous_intercept_as_standardised_asymptotic_continuous_intercept( + discrete_std, + recovered + ), + Err(PsychometricError::StandardisedDiscreteContinuousInterceptIsNotStandardisedAsymptoticContinuousIntercept) + ); } #[test] - fn discrete_observed_mean_with_initial_time_independent_predictor_refuses_evolved_mean_and_overflow() + fn standardised_asymptotic_continuous_intercept_fails_closed_when_unstandardised_is_defined() { + assert_eq!( + recover_standardised_asymptotic_continuous_intercept( + 0.3, + 0.0, + -0.25, + LagClock::EventTime + ), + Err(PsychometricError::StandardisedAsymptoticContinuousInterceptRequiresPositiveWithinSubjectVariance) + ); + assert_eq!( + recover_standardised_asymptotic_continuous_intercept( + 0.3, + 0.4, + 0.5, + LagClock::EventTime + ), + Err(PsychometricError::StationaryVarianceRequiresStableDrift) + ); + assert_eq!( + recover_standardised_asymptotic_continuous_intercept( + 0.3, + 0.4, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::StationaryVarianceRequiresStableDrift) + ); + assert_eq!( + recover_standardised_asymptotic_continuous_intercept( + 0.3, + 0.4, + -0.25, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_standardised_asymptotic_continuous_intercept( + f64::NAN, + 0.4, + -0.25, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_asymptotic_continuous_intercept( + 0.3, + f64::INFINITY, + -0.25, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + } + + #[test] + fn standardised_initial_latent_mean_recovers_driver_page_sixteen_after_positive_p0() { + // Driver et al. (2017, p. 16 T0MEANSstd; Table 2; footnote 4; + // 2017-era summary.ctsemFit.R T0MEANS): form strictly + // positive free T0VAR p_0, then μ_0 / √p_0. Relevant + // variance is p_0, not asymDIFFUSION. + let mean = 0.8_f64; + let initial_variance = 1.6_f64; + let recovered = + recover_standardised_initial_latent_mean(mean, initial_variance, LagClock::EventTime) + .expect("T0MEANSstd"); + let expected = mean / initial_variance.sqrt(); + assert!((recovered - expected).abs() < 1e-15); + let larger_p0 = recover_standardised_initial_latent_mean(mean, 6.4, LagClock::EventTime) + .expect("T0MEANSstd p_0=6.4"); + assert!((larger_p0 - recovered).abs() > 1e-3); + assert!(larger_p0.abs() < recovered.abs()); + let unit = recover_standardised_initial_latent_mean( + initial_variance.sqrt(), + initial_variance, + LagClock::EventTime, + ) + .expect("T0MEANSstd μ_0=√p_0"); + let variance_std = + recover_standardised_initial_latent_variance(initial_variance, LagClock::EventTime) + .expect("T0VARstd"); + assert!((unit - variance_std).abs() < 1e-15); + let within = recover_stationary_latent_variance(0.4, -0.25, LagClock::EventTime) + .expect("asymDIFFUSION"); + let within_scaled = mean / within.sqrt(); + assert!((within_scaled - recovered).abs() > 1e-3); + let zero = + recover_standardised_initial_latent_mean(0.0, initial_variance, LagClock::EventTime) + .expect("zero T0MEANS"); + assert_eq!(zero.to_bits(), 0.0_f64.to_bits()); + let negative = + recover_standardised_initial_latent_mean(-mean, initial_variance, LagClock::EventTime) + .expect("negative signed T0MEANSstd"); + assert!((negative + expected).abs() < 1e-15); + assert_eq!( + refuse_unstandardised_initial_latent_mean_as_standardised_initial_latent_mean( + mean, recovered + ), + Err(PsychometricError::UnstandardisedInitialLatentMeanIsNotStandardisedInitialLatentMean) + ); + assert_eq!( + refuse_standardised_initial_latent_variance_as_standardised_initial_latent_mean( + variance_std, unit + ), + Err(PsychometricError::StandardisedInitialLatentVarianceIsNotStandardisedInitialLatentMean) + ); + assert_eq!( + refuse_within_subject_scaled_initial_latent_mean_as_standardised_initial_latent_mean( + within_scaled, recovered + ), + Err(PsychometricError::WithinSubjectScaledInitialLatentMeanIsNotStandardisedInitialLatentMean) + ); + } + + #[test] + fn standardised_initial_latent_mean_fails_closed_when_unstandardised_is_defined() { + assert_eq!( + recover_standardised_initial_latent_mean(0.8, 0.0, LagClock::EventTime), + Err( + PsychometricError::StandardisedInitialLatentMeanRequiresPositiveInitialLatentVariance + ) + ); + assert_eq!( + recover_standardised_initial_latent_mean(0.8, 1.6, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_standardised_initial_latent_mean(0.8, -1.6, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_initial_latent_mean(f64::NAN, 1.6, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_initial_latent_mean(0.8, f64::INFINITY, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_initial_latent_mean(f64::MAX, 1e-4, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + } + + #[test] + fn standardised_initial_time_independent_effect_recovers_driver_table_three_after_positive_variances() { - let loading = 2.0_f64; - let recovered = recover_discrete_observed_mean_with_initial_time_independent_predictor( - loading, - 1.0, - -0.5, - 0.3, - 0.4, - 3.0, - 0.5, - 2.0, + // Driver et al. (2017, Table 3 T0TIPREDEFFECTstd; p. 16; footnote 4): + // form strictly positive free T0VAR p_0 and strictly positive v, + // then t0_b · √v / √p_0. Affected variance is p_0, not + // asymDIFFUSION. + let initial_variance = 1.6_f64; + let coefficient = 0.3_f64; + let predictor_variance = 1.0_f64; + let recovered = recover_standardised_initial_time_independent_predictor_effect( + coefficient, + predictor_variance, + initial_variance, LagClock::EventTime, ) - .expect("eq5-t0tipred-mean"); - let evolved_observed = - recover_discrete_observed_mean(loading, 1.0, -0.5, 0.3, 0.5, 2.0, LagClock::EventTime) - .expect("eq3-eq5-mean"); - let process_observed = recover_discrete_observed_mean_with_time_independent_predictor( - loading, - 1.0, - -0.5, - 0.3, - 0.4, - 3.0, - 0.5, - 2.0, + .expect("T0TIPREDEFFECTstd"); + let expected = coefficient * predictor_variance.sqrt() / initial_variance.sqrt(); + assert!((recovered - expected).abs() < 1e-15); + let larger_p0 = recover_standardised_initial_time_independent_predictor_effect( + coefficient, + predictor_variance, + 6.4, LagClock::EventTime, ) - .expect("eq5-tipred-mean"); - let impulse_observed = recover_discrete_observed_mean_with_impulse( - loading, - 1.0, - -0.5, - 0.3, - 0.4, - 3.0, - 0.5, - 2.0, + .expect("T0TIPREDEFFECTstd p_0=6.4"); + assert!((larger_p0 - recovered).abs() > 1e-3); + assert!(larger_p0.abs() < recovered.abs()); + let diffusion = 0.4_f64; + let log_rate = -0.5_f64; + let continuous = recover_standardised_continuous_time_independent_predictor_effect( + coefficient, + predictor_variance, + diffusion, + log_rate, LagClock::EventTime, ) - .expect("eq5-impulse-mean"); - let carried_observed = recover_discrete_observed_mean_with_impulse_carry( - loading, - 1.0, - -0.5, - 0.3, - 0.4, - 3.0, - 0.5, - 2.0, - 1.0, + .expect("TIPREDEFFECTstd"); + assert!((continuous - recovered).abs() > 1e-3); + let asymptotic = recover_standardised_asymptotic_time_independent_predictor_effect( + coefficient, + predictor_variance, + diffusion, + log_rate, LagClock::EventTime, ) - .expect("eq5-carry-mean"); + .expect("asymTIPREDEFFECTstd"); + assert!((asymptotic - recovered).abs() > 1e-3); + let trait_variance = 1.0_f64; + let total = recover_trait_plus_state_latent_variance(trait_variance, initial_variance) + .expect("trait+state var"); + let contaminated = coefficient * predictor_variance.sqrt() / total.sqrt(); + assert!((contaminated - recovered).abs() > 1e-3); + let zero = recover_standardised_initial_time_independent_predictor_effect( + 0.0, + predictor_variance, + initial_variance, + LagClock::EventTime, + ) + .expect("zero coefficient"); + assert_eq!(zero.to_bits(), 0.0_f64.to_bits()); assert_eq!( - refuse_evolved_observed_mean_as_initial_time_independent_observed_mean( - evolved_observed, + refuse_unstandardised_initial_time_independent_effect_as_standardised_initial_time_independent_effect( + coefficient, recovered ), - Err(PsychometricError::EvolvedObservedMeanIsNotInitialTimeIndependentObservedMean) + Err( + PsychometricError::UnstandardisedInitialTimeIndependentEffectIsNotStandardisedInitialTimeIndependentEffect + ) ); assert_eq!( - refuse_time_independent_observed_mean_as_initial_time_independent_observed_mean( - process_observed, + refuse_standardised_continuous_time_independent_effect_as_standardised_initial_time_independent_effect( + continuous, recovered ), Err( - PsychometricError::TimeIndependentObservedMeanIsNotInitialTimeIndependentObservedMean + PsychometricError::StandardisedContinuousTimeIndependentEffectIsNotStandardisedInitialTimeIndependentEffect ) ); assert_eq!( - refuse_impulse_observed_mean_as_initial_time_independent_observed_mean( - impulse_observed, + refuse_standardised_asymptotic_time_independent_effect_as_standardised_initial_time_independent_effect( + asymptotic, recovered ), - Err(PsychometricError::ImpulseObservedMeanIsNotInitialTimeIndependentObservedMean) + Err( + PsychometricError::StandardisedAsymptoticTimeIndependentEffectIsNotStandardisedInitialTimeIndependentEffect + ) ); assert_eq!( - refuse_impulse_carry_observed_mean_as_initial_time_independent_observed_mean( - carried_observed, + refuse_trait_contaminated_initial_time_independent_effect_as_standardised_initial_time_independent_effect( + contaminated, recovered ), - Err(PsychometricError::ImpulseCarryObservedMeanIsNotInitialTimeIndependentObservedMean) + Err( + PsychometricError::TraitContaminatedInitialTimeIndependentEffectIsNotStandardisedInitialTimeIndependentEffect + ) + ); + assert_eq!( + refuse_trait_variance_as_standardisation_variance(trait_variance, initial_variance), + Err(PsychometricError::TraitVarianceIsNotStandardisationVariance) ); } #[test] - fn discrete_observed_mean_with_initial_time_independent_predictor_invalid_inputs_fail_closed() { - let scaled = recover_discrete_observed_mean_with_initial_time_independent_predictor( - 1e308, - 1e-308, - 0.0, - 0.0, - 0.0, - 3.0, - 0.0, - 1.0, - LagClock::EventTime, - ) - .expect("scale"); - assert!((scaled - 1.0).abs() < 1e-15); - let finite_loaded = recover_discrete_observed_mean_with_initial_time_independent_predictor( - 1e308, - 0.0, - 0.0, - 0.0, - 0.0, - 3.0, - 0.0, - 1.0, - LagClock::EventTime, - ) - .expect("lambda-mu0"); - assert!((finite_loaded - 0.0).abs() < 1e-15); - assert_eq!( - recover_discrete_observed_mean_with_initial_time_independent_predictor( - 1e308, - 2.0, - 0.0, - 0.0, + fn standardised_initial_time_independent_effect_fails_closed_when_unstandardised_is_defined() { + assert_eq!( + recover_standardised_initial_time_independent_predictor_effect( + 0.3, + 1.0, 0.0, - 3.0, + LagClock::EventTime + ), + Err( + PsychometricError::StandardisedInitialTimeIndependentEffectRequiresPositiveInitialLatentVariance + ) + ); + assert_eq!( + recover_standardised_initial_time_independent_predictor_effect( + 0.3, 0.0, - 1.0, + 1.6, LagClock::EventTime ), - Err(PsychometricError::InvalidNumericInput) + Err( + PsychometricError::StandardisedInitialTimeIndependentEffectRequiresPositivePredictorVariance + ) ); assert_eq!( - recover_discrete_observed_mean_with_initial_time_independent_predictor( - 2.0, - 1.0, - -0.5, + recover_standardised_initial_time_independent_predictor_effect( 0.3, - 0.4, - 3.0, - 0.5, - 2.0, + 1.0, + 1.6, LagClock::SystemTime ), Err(PsychometricError::EventTimeRequired) ); assert_eq!( - recover_discrete_observed_mean_with_initial_time_independent_predictor( - 2.0, - 1.0, - -0.5, + recover_standardised_initial_time_independent_predictor_effect( 0.3, - 0.4, - 3.0, - 0.5, - 0.0, + -0.1, + 1.6, LagClock::EventTime ), - Err(PsychometricError::NonPositiveInterval) + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_observed_mean_with_initial_time_independent_predictor( - 1e308, - 0.0, - 0.0, - 0.0, - 1e308, - 1.0, - 0.0, + recover_standardised_initial_time_independent_predictor_effect( + 0.3, 1.0, + -0.1, LagClock::EventTime ), Err(PsychometricError::InvalidNumericInput) ); - } - - #[test] - fn initial_time_dependent_predictor_recovers_table_three_t0_shift_and_carry() { - let effect = 0.4_f64; - let predictor = 3.0_f64; - let drift = -0.5_f64; - let delta = 2.0_f64; - let shift = - recover_initial_time_dependent_predictor_effect(effect, predictor).expect("t0-tdpred"); - assert!((shift - 1.2).abs() < 1e-15); assert_eq!( - recover_initial_time_dependent_predictor_effect(0.0, predictor), - Ok(0.0) + recover_standardised_initial_time_independent_predictor_effect( + 0.3, + f64::NAN, + 1.6, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_initial_time_dependent_predictor_effect(effect, 0.0), - Ok(0.0) + recover_standardised_initial_time_independent_predictor_effect( + 0.3, + 1.0, + f64::NAN, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) ); - let carry = recover_initial_time_dependent_predictor_carry( - effect, - predictor, - drift, - delta, - LagClock::EventTime, - ) - .expect("t0-td-carry"); - let expected = 1.2 * (drift * delta).exp(); - assert!((carry - expected).abs() < 1e-15); - let zero_drift = recover_initial_time_dependent_predictor_carry( - effect, - predictor, - 0.0, - delta, - LagClock::EventTime, - ) - .expect("zero-drift"); - assert!((zero_drift - 1.2).abs() < 1e-15); - let vanished = recover_initial_time_dependent_predictor_carry( - effect, - predictor, - -800.0, - 1.0, - LagClock::EventTime, - ) - .expect("underflow"); - assert_eq!(vanished.to_bits(), 0.0_f64.to_bits()); - let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("tdpred"); - let increment = recover_discrete_time_independent_predictor_effect( - effect, - predictor, - drift, - delta, - LagClock::EventTime, - ) - .expect("tipred"); - let tipred_shift = recover_initial_time_independent_predictor_effect(effect, predictor) - .expect("t0-tipred"); - let impulse_carry = recover_time_dependent_predictor_impulse_carry( - effect, - predictor, - drift, - delta, - 1.0, - LagClock::EventTime, - ) - .expect("td-carry"); - assert!((carry - shift).abs() > 1e-3); - assert!((carry - increment).abs() > 1e-3); - assert!((shift - increment).abs() > 1e-3); - assert!((shift - effect).abs() > 1e-3); - assert!((carry - impulse_carry).abs() > 1e-3); - // Algebraically a product, like M x and t0_b z, but Table 3 names a different matrix. - assert!((shift - impulse).abs() < 1e-15); - assert!((shift - tipred_shift).abs() < 1e-15); - } - - #[test] - fn initial_time_dependent_predictor_composes_evolved_mean_and_keeps_scale() { - let effect = 0.4_f64; - let predictor = 3.0_f64; - let drift = -0.5_f64; - let delta = 2.0_f64; - let carry = recover_initial_time_dependent_predictor_carry( - effect, - predictor, - drift, - delta, - LagClock::EventTime, - ) - .expect("t0-td-carry"); - let initial = 1.0_f64; - let intercept = 0.3_f64; - let composed = recover_discrete_latent_mean_with_initial_time_dependent_predictor( - initial, - drift, - intercept, - effect, - predictor, - delta, - LagClock::EventTime, - ) - .expect("eq3-t0tdpred"); - let evolved = - recover_discrete_latent_mean(initial, drift, intercept, delta, LagClock::EventTime) - .expect("mu-t"); - assert!((composed - (evolved + carry)).abs() < 1e-15); assert_eq!( - recover_discrete_latent_mean_with_initial_time_dependent_predictor( - initial, - drift, - intercept, - 0.0, - predictor, - delta, + recover_standardised_initial_time_independent_predictor_effect( + f64::NAN, + 1.0, + 1.6, LagClock::EventTime ), - Ok(evolved) + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_latent_mean_with_initial_time_dependent_predictor( - 0.0, - drift, - 0.0, - effect, - predictor, - delta, + recover_standardised_initial_time_independent_predictor_effect( + 4.0, + 1e308, + 1e-308, LagClock::EventTime ), - Ok(carry) - ); - let scaled = recover_initial_time_dependent_predictor_carry( - 1e308, - 1e-308, - 0.0, - 1.0, - LagClock::EventTime, - ) - .expect("scale"); - assert!((scaled - 1.0).abs() < 1e-15); - let rewritten = recover_initial_time_dependent_predictor_carry( - 2.0, - 0.5, - 710.0, - 1.0, - LagClock::EventTime, + Err(PsychometricError::InvalidNumericInput) ); - assert_eq!(rewritten, Err(PsychometricError::InvalidNumericInput)); - let finite_rewrite = recover_initial_time_dependent_predictor_carry( - 1e-308, - 1.0, - 700.0, - 1.0, - LagClock::EventTime, - ) - .expect("log-rewrite"); - let expected_rewrite = (1e-308_f64.ln() + 700.0).exp(); - assert!((finite_rewrite - expected_rewrite).abs() / expected_rewrite < 1e-12); } #[test] - fn initial_time_dependent_predictor_refuses_impulse_cint_process_and_coefficient() { - let effect = 0.4_f64; - let predictor = 3.0_f64; - let shift = - recover_initial_time_dependent_predictor_effect(effect, predictor).expect("t0-tdpred"); - let carry = recover_initial_time_dependent_predictor_carry( - effect, - predictor, - -0.5, - 2.0, + fn initial_time_independent_predictor_variance_recovers_added_t0_tipred_var() { + let coefficient = 0.3_f64; + let predictor_variance = 4.0_f64; + let recovered = recover_initial_time_independent_predictor_variance( + coefficient, + predictor_variance, LagClock::EventTime, ) - .expect("t0-td-carry"); - let increment = recover_discrete_time_independent_predictor_effect( - effect, - predictor, - -0.5, - 2.0, + .expect("addedT0TIPREDVAR"); + assert!((recovered - coefficient * coefficient * predictor_variance).abs() < 1e-15); + let doubled = recover_initial_time_independent_predictor_variance( + coefficient, + 8.0, LagClock::EventTime, ) - .expect("tipred"); - let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("tdpred"); - let tipred_shift = recover_initial_time_independent_predictor_effect(effect, predictor) - .expect("t0-tipred"); - let impulse_carry = recover_time_dependent_predictor_impulse_carry( - effect, - predictor, + .expect("doubled v"); + assert!((doubled - 2.0 * recovered).abs() < 1e-15); + let negative = recover_initial_time_independent_predictor_variance( + -coefficient, + predictor_variance, + LagClock::EventTime, + ) + .expect("signed coefficient"); + assert_eq!(negative.to_bits(), recovered.to_bits()); + let asymptotic = recover_asymptotic_time_independent_predictor_variance( + coefficient, + predictor_variance, -0.5, - 2.0, - 1.0, LagClock::EventTime, ) - .expect("td-carry"); - assert_eq!( - refuse_initial_time_dependent_effect_as_contemporaneous_impulse(shift, impulse), - Err(PsychometricError::InitialTimeDependentEffectIsNotContemporaneousImpulse) - ); - assert_eq!( - refuse_initial_time_dependent_carry_as_initial_effect(carry, shift), - Err(PsychometricError::InitialTimeDependentCarryIsNotInitialEffect) - ); + .expect("addedTIPREDVAR"); + assert!((asymptotic - recovered).abs() > 1e-3); assert_eq!( - refuse_initial_time_dependent_effect_as_continuous_intercept(shift, 0.4), - Err(PsychometricError::InitialTimeDependentEffectIsNotContinuousIntercept) + recover_asymptotic_time_independent_predictor_variance( + coefficient, + predictor_variance, + 0.5, + LagClock::EventTime, + ), + Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) ); + let growing = recover_initial_time_independent_predictor_variance( + coefficient, + predictor_variance, + LagClock::EventTime, + ) + .expect("growing a is not an input"); + assert_eq!(growing.to_bits(), recovered.to_bits()); + let standardised = recover_standardised_initial_time_independent_predictor_effect( + coefficient, + predictor_variance, + 1.6, + LagClock::EventTime, + ) + .expect("T0TIPREDEFFECTstd"); + assert!((standardised - recovered).abs() > 1e-3); + let zero_coefficient = recover_initial_time_independent_predictor_variance( + 0.0, + predictor_variance, + LagClock::EventTime, + ) + .expect("zero coefficient"); + assert_eq!(zero_coefficient.to_bits(), 0.0_f64.to_bits()); + let zero_variance = recover_initial_time_independent_predictor_variance( + coefficient, + 0.0, + LagClock::EventTime, + ) + .expect("zero variance"); + assert_eq!(zero_variance.to_bits(), 0.0_f64.to_bits()); assert_eq!( - refuse_initial_time_dependent_effect_as_process_increment(shift, increment), - Err(PsychometricError::InitialTimeDependentEffectIsNotProcessIncrement) + refuse_initial_time_independent_variance_as_asymptotic_time_independent_variance( + recovered, + asymptotic + ), + Err( + PsychometricError::InitialTimeIndependentVarianceIsNotAsymptoticTimeIndependentVariance + ) ); assert_eq!( - refuse_initial_time_dependent_effect_as_initial_time_independent_effect( - shift, - tipred_shift + refuse_initial_time_independent_variance_as_standardised_initial_time_independent_effect( + recovered, + standardised ), - Err(PsychometricError::InitialTimeDependentEffectIsNotInitialTimeIndependentEffect) + Err( + PsychometricError::InitialTimeIndependentVarianceIsNotStandardisedInitialTimeIndependentEffect + ) ); assert_eq!( - refuse_initial_time_dependent_coefficient_as_initial_effect(effect, shift), - Err(PsychometricError::InitialTimeDependentCoefficientIsNotInitialEffect) + refuse_initial_time_independent_variance_as_initial_latent_variance(recovered, 1.6), + Err(PsychometricError::InitialTimeIndependentVarianceIsNotInitialLatentVariance) ); assert_eq!( - refuse_initial_time_dependent_carry_as_impulse_carry(carry, impulse_carry), - Err(PsychometricError::InitialTimeDependentCarryIsNotImpulseCarry) + refuse_initial_time_independent_variance_as_trait_variance(recovered, 1.0), + Err(PsychometricError::InitialTimeIndependentVarianceIsNotTraitVariance) ); } #[test] - fn initial_time_dependent_predictor_invalid_inputs_fail_closed() { - assert_eq!( - recover_initial_time_dependent_predictor_effect(f64::NAN, 1.0), - Err(PsychometricError::InvalidNumericInput) - ); + fn initial_time_independent_predictor_variance_fails_closed_on_non_event_clock_and_overflow() { assert_eq!( - recover_initial_time_dependent_predictor_effect(1.0, f64::INFINITY), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_initial_time_dependent_predictor_effect(1e308, 2.0), - Err(PsychometricError::InvalidNumericInput) + recover_initial_time_independent_predictor_variance(0.3, 4.0, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) ); assert_eq!( - recover_initial_time_dependent_predictor_carry( - 0.4, - 3.0, - f64::NAN, - 2.0, - LagClock::EventTime - ), + recover_initial_time_independent_predictor_variance(0.3, -0.1, LagClock::EventTime), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_initial_time_dependent_predictor_carry( - 0.4, - 3.0, - -0.5, - 0.0, - LagClock::EventTime - ), - Err(PsychometricError::NonPositiveInterval) - ); - assert_eq!( - recover_initial_time_dependent_predictor_carry( - 0.4, - 3.0, - -0.5, - 2.0, - LagClock::SystemTime - ), - Err(PsychometricError::EventTimeRequired) - ); - assert_eq!( - recover_discrete_latent_mean_with_initial_time_dependent_predictor( - 1e308, - 0.0, - 0.0, - 1e308, - 1.0, - 1.0, - LagClock::EventTime - ), + recover_initial_time_independent_predictor_variance(f64::NAN, 4.0, LagClock::EventTime), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_initial_time_dependent_predictor_carry( - 1.0, - 1.0, - f64::INFINITY, - 1.0, - LagClock::EventTime - ), + recover_initial_time_independent_predictor_variance(0.3, f64::NAN, LagClock::EventTime), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_initial_time_dependent_predictor_carry( - f64::NAN, - 1.0, - -0.5, - 2.0, - LagClock::EventTime - ), + recover_initial_time_independent_predictor_variance(1e308, 4.0, LagClock::EventTime), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_initial_time_dependent_predictor_carry( - 1.0, - 1.0, - 1e308, - 10.0, - LagClock::EventTime - ), + recover_initial_time_independent_predictor_variance(1e154, 1e154, LagClock::EventTime), Err(PsychometricError::InvalidNumericInput) ); + let zero_with_overflowing_variance = + recover_initial_time_independent_predictor_variance(0.0, 1e308, LagClock::EventTime) + .expect("zero coefficient keeps zero"); + assert_eq!(zero_with_overflowing_variance.to_bits(), 0.0_f64.to_bits()); + let zero_with_overflowing_coefficient = + recover_initial_time_independent_predictor_variance(1e308, 0.0, LagClock::EventTime) + .expect("zero variance keeps zero"); assert_eq!( - recover_discrete_latent_mean_with_initial_time_dependent_predictor( - 1.0, - -0.5, - 0.3, - f64::NAN, - 1.0, - 2.0, - LagClock::EventTime - ), - Err(PsychometricError::InvalidNumericInput) + zero_with_overflowing_coefficient.to_bits(), + 0.0_f64.to_bits() ); } #[test] #[allow(clippy::too_many_lines)] - fn discrete_observed_mean_with_initial_time_dependent_predictor_recovers_driver_equation_five() - { + fn initial_time_independent_observed_variance_recovers_eq5_of_added_t0_tipred_var() { let loading = 2.0_f64; - let drift = -0.5_f64; - let delta = 2.0_f64; - let effect = 0.4_f64; - let predictor = 3.0_f64; - let initial = 1.0_f64; - let intercept = 0.3_f64; - let manifest_mean = 0.5_f64; - let recovered = recover_discrete_observed_mean_with_initial_time_dependent_predictor( - loading, - initial, - drift, - intercept, - effect, - predictor, - manifest_mean, - delta, + let coefficient = 0.3_f64; + let predictor_variance = 4.0_f64; + let extra = recover_initial_time_independent_predictor_variance( + coefficient, + predictor_variance, LagClock::EventTime, ) - .expect("eq5-t0tdpred-mean"); - let composed = recover_discrete_latent_mean_with_initial_time_dependent_predictor( - initial, - drift, - intercept, - effect, - predictor, - delta, + .expect("addedT0TIPREDVAR"); + let recovered = recover_initial_time_independent_observed_variance( + loading, + coefficient, + predictor_variance, LagClock::EventTime, ) - .expect("eq3-t0tdpred"); - let expected = manifest_mean + loading * composed; + .expect("eq5 addedT0TIPREDVAR"); + let expected = recover_manifest_observed_variance(loading, extra, 0.0).expect("λ² extra"); assert!((recovered - expected).abs() < 1e-15); - let evolved_observed = recover_discrete_observed_mean( + assert!((recovered - loading * loading * extra).abs() < 1e-15); + let doubled = recover_initial_time_independent_observed_variance( loading, - initial, - drift, - intercept, - manifest_mean, - delta, + coefficient, + 8.0, LagClock::EventTime, ) - .expect("eq3-eq5-mean"); - let process_observed = recover_discrete_observed_mean_with_time_independent_predictor( + .expect("doubled v"); + assert!((doubled - 2.0 * recovered).abs() < 1e-15); + let negative = recover_initial_time_independent_observed_variance( loading, - initial, - drift, - intercept, - effect, - predictor, - manifest_mean, - delta, + -coefficient, + predictor_variance, LagClock::EventTime, ) - .expect("eq5-tipred"); - let impulse_observed = recover_discrete_observed_mean_with_impulse( + .expect("signed coefficient"); + assert_eq!(negative.to_bits(), recovered.to_bits()); + let asymptotic_observed = recover_asymptotic_time_independent_observed_variance( loading, - initial, - drift, - intercept, - effect, - predictor, - manifest_mean, - delta, + coefficient, + predictor_variance, + -0.5, LagClock::EventTime, ) - .expect("eq5-impulse"); - let carry_observed = recover_discrete_observed_mean_with_impulse_carry( + .expect("λ² (B/a)² v"); + assert!((asymptotic_observed - recovered).abs() > 1e-3); + let initial_observed = + recover_manifest_observed_variance(loading, 1.6, 0.1).expect("λ² p_0 + θ"); + assert!((initial_observed - recovered).abs() > 1e-3); + assert!((extra - recovered).abs() > 1e-3); + assert!((0.1_f64 - recovered).abs() > 1e-3); + let growing = recover_initial_time_independent_observed_variance( loading, - initial, - drift, - intercept, - effect, - predictor, - manifest_mean, - delta, + coefficient, + predictor_variance, + LagClock::EventTime, + ) + .expect("growing a is not an input"); + assert_eq!(growing.to_bits(), recovered.to_bits()); + let zero_loading = recover_initial_time_independent_observed_variance( + 0.0, + coefficient, + predictor_variance, + LagClock::EventTime, + ) + .expect("zero loading"); + assert_eq!(zero_loading.to_bits(), 0.0_f64.to_bits()); + let zero_coefficient = recover_initial_time_independent_observed_variance( + loading, + 0.0, + predictor_variance, + LagClock::EventTime, + ) + .expect("zero coefficient"); + assert_eq!(zero_coefficient.to_bits(), 0.0_f64.to_bits()); + let zero_variance = recover_initial_time_independent_observed_variance( + loading, + coefficient, + 0.0, + LagClock::EventTime, + ) + .expect("zero variance"); + assert_eq!(zero_variance.to_bits(), 0.0_f64.to_bits()); + let scaled = recover_initial_time_independent_observed_variance( + 1e308, + 1e-154, 1.0, LagClock::EventTime, ) - .expect("eq5-carry"); - let tipred_observed = - recover_discrete_observed_mean_with_initial_time_independent_predictor( - loading, - initial, - drift, - intercept, - effect, - predictor, - manifest_mean, - delta, - LagClock::EventTime, + .expect("scale"); + assert!(scaled.is_finite()); + assert_eq!( + refuse_initial_time_independent_observed_variance_as_initial_time_independent_variance( + recovered, extra + ), + Err( + PsychometricError::InitialTimeIndependentObservedVarianceIsNotInitialTimeIndependentVariance ) - .expect("eq5-t0tipred"); - assert!((recovered - evolved_observed).abs() > 1e-3); - assert!((recovered - process_observed).abs() > 1e-3); - assert!((recovered - impulse_observed).abs() > 1e-3); - assert!((recovered - carry_observed).abs() > 1e-3); - // Same numbers as T0TIPRED yield the same product, but Table 3 names a different matrix. - assert!((recovered - tipred_observed).abs() < 1e-15); + ); + assert_eq!( + refuse_initial_time_independent_observed_variance_as_initial_observed_variance( + recovered, + initial_observed + ), + Err( + PsychometricError::InitialTimeIndependentObservedVarianceIsNotInitialObservedVariance + ) + ); + assert_eq!( + refuse_initial_time_independent_observed_variance_as_asymptotic_time_independent_observed_variance( + recovered, + asymptotic_observed + ), + Err( + PsychometricError::InitialTimeIndependentObservedVarianceIsNotAsymptoticTimeIndependentObservedVariance + ) + ); + assert_eq!( + refuse_initial_time_independent_observed_variance_as_measurement_error(recovered, 0.1), + Err(PsychometricError::InitialTimeIndependentObservedVarianceIsNotMeasurementError) + ); + } + + #[test] + fn initial_time_independent_observed_variance_fails_closed_on_non_event_clock_and_overflow() { + assert_eq!( + recover_initial_time_independent_observed_variance(2.0, 0.3, 4.0, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_initial_time_independent_observed_variance(2.0, 0.3, -0.1, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_initial_time_independent_observed_variance( + f64::NAN, + 0.3, + 4.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_initial_time_independent_observed_variance( + 2.0, + f64::NAN, + 4.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_initial_time_independent_observed_variance( + 2.0, + 0.3, + f64::NAN, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); assert_eq!( - recover_discrete_observed_mean_with_initial_time_dependent_predictor( - 0.0, - initial, - drift, - intercept, - effect, - predictor, - manifest_mean, - delta, + recover_initial_time_independent_observed_variance( + 2.0, + 1e308, + 4.0, LagClock::EventTime ), - Ok(manifest_mean) + Err(PsychometricError::InvalidNumericInput) ); - assert!((recovered - composed).abs() > 1e-3); - assert!((recovered - manifest_mean).abs() > 1e-3); + assert_eq!( + recover_initial_time_independent_observed_variance( + 1e308, + 0.3, + 4.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + let zero_with_overflowing_loading = recover_initial_time_independent_observed_variance( + 1e308, + 0.0, + 4.0, + LagClock::EventTime, + ) + .expect("zero extra keeps zero"); + assert_eq!(zero_with_overflowing_loading.to_bits(), 0.0_f64.to_bits()); + let zero_with_overflowing_variance = recover_initial_time_independent_observed_variance( + 2.0, + 0.0, + 1e308, + LagClock::EventTime, + ) + .expect("zero coefficient keeps zero"); + assert_eq!(zero_with_overflowing_variance.to_bits(), 0.0_f64.to_bits()); } #[test] - fn discrete_observed_mean_with_initial_time_dependent_predictor_refuses_evolved_mean_and_overflow() - { - let recovered = recover_discrete_observed_mean_with_initial_time_dependent_predictor( - 2.0, - 1.0, - -0.5, - 0.3, - 0.4, - 3.0, - 0.5, - 2.0, + #[allow(clippy::too_many_lines)] + fn asymptotic_time_independent_observed_variance_recovers_eq5_of_added_tipred_var() { + let loading = 2.0_f64; + let coefficient = 0.3_f64; + let predictor_variance = 4.0_f64; + let log_rate = -0.5_f64; + let extra = recover_asymptotic_time_independent_predictor_variance( + coefficient, + predictor_variance, + log_rate, LagClock::EventTime, ) - .expect("eq5-t0tdpred"); - let evolved = - recover_discrete_observed_mean(2.0, 1.0, -0.5, 0.3, 0.5, 2.0, LagClock::EventTime) - .expect("evolved"); - let process = recover_discrete_observed_mean_with_time_independent_predictor( - 2.0, - 1.0, - -0.5, - 0.3, - 0.4, - 3.0, - 0.5, - 2.0, + .expect("addedTIPREDVAR"); + let recovered = recover_asymptotic_time_independent_observed_variance( + loading, + coefficient, + predictor_variance, + log_rate, LagClock::EventTime, ) - .expect("tipred"); - let impulse = recover_discrete_observed_mean_with_impulse( - 2.0, - 1.0, - -0.5, - 0.3, - 0.4, - 3.0, - 0.5, - 2.0, + .expect("eq5 addedTIPREDVAR"); + let expected = recover_manifest_observed_variance(loading, extra, 0.0).expect("λ² extra"); + assert!((recovered - expected).abs() < 1e-15); + assert!((recovered - loading * loading * extra).abs() < 1e-15); + let doubled = recover_asymptotic_time_independent_observed_variance( + loading, + coefficient, + 8.0, + log_rate, LagClock::EventTime, ) - .expect("impulse"); - let carry = recover_discrete_observed_mean_with_impulse_carry( - 2.0, - 1.0, - -0.5, - 0.3, - 0.4, - 3.0, - 0.5, - 2.0, - 1.0, + .expect("doubled v"); + assert!((doubled - 2.0 * recovered).abs() < 1e-15); + let negative = recover_asymptotic_time_independent_observed_variance( + loading, + -coefficient, + predictor_variance, + log_rate, LagClock::EventTime, ) - .expect("carry"); - let tipred = recover_discrete_observed_mean_with_initial_time_independent_predictor( - 2.0, + .expect("signed coefficient"); + assert_eq!(negative.to_bits(), recovered.to_bits()); + let initial_extra = recover_initial_time_independent_predictor_variance( + coefficient, + predictor_variance, + LagClock::EventTime, + ) + .expect("addedT0TIPREDVAR"); + let initial_observed = recover_initial_time_independent_observed_variance( + loading, + coefficient, + predictor_variance, + LagClock::EventTime, + ) + .expect("eq5 addedT0TIPREDVAR"); + assert!((initial_observed - recovered).abs() > 1e-3); + let stationary_observed = + recover_manifest_observed_variance(loading, 1.6, 0.1).expect("λ² p + θ"); + assert!((stationary_observed - recovered).abs() > 1e-3); + assert!((extra - recovered).abs() > 1e-3); + assert!((initial_extra - recovered).abs() > 1e-3); + assert!((0.1_f64 - recovered).abs() > 1e-3); + let zero_loading = recover_asymptotic_time_independent_observed_variance( + 0.0, + coefficient, + predictor_variance, + log_rate, + LagClock::EventTime, + ) + .expect("zero loading"); + assert_eq!(zero_loading.to_bits(), 0.0_f64.to_bits()); + let zero_coefficient = recover_asymptotic_time_independent_observed_variance( + loading, + 0.0, + predictor_variance, + 0.0, + LagClock::EventTime, + ) + .expect("zero coefficient"); + assert_eq!(zero_coefficient.to_bits(), 0.0_f64.to_bits()); + let zero_variance = recover_asymptotic_time_independent_observed_variance( + loading, + coefficient, + 0.0, + 0.0, + LagClock::EventTime, + ) + .expect("zero variance"); + assert_eq!(zero_variance.to_bits(), 0.0_f64.to_bits()); + let scaled = recover_asymptotic_time_independent_observed_variance( + 1e308, + 1e-154, 1.0, - -0.5, - 0.3, - 0.4, - 3.0, - 0.5, - 2.0, + -1.0, LagClock::EventTime, ) - .expect("t0tipred"); - assert_eq!( - refuse_evolved_observed_mean_as_initial_time_dependent_observed_mean( - evolved, recovered - ), - Err(PsychometricError::EvolvedObservedMeanIsNotInitialTimeDependentObservedMean) - ); + .expect("scale"); + assert!(scaled.is_finite()); assert_eq!( - refuse_time_independent_observed_mean_as_initial_time_dependent_observed_mean( - process, recovered + refuse_asymptotic_time_independent_observed_variance_as_asymptotic_time_independent_variance( + recovered, extra ), Err( - PsychometricError::TimeIndependentObservedMeanIsNotInitialTimeDependentObservedMean + PsychometricError::AsymptoticTimeIndependentObservedVarianceIsNotAsymptoticTimeIndependentVariance ) ); assert_eq!( - refuse_impulse_observed_mean_as_initial_time_dependent_observed_mean( - impulse, recovered + refuse_asymptotic_time_independent_observed_variance_as_initial_time_independent_observed_variance( + recovered, + initial_observed ), - Err(PsychometricError::ImpulseObservedMeanIsNotInitialTimeDependentObservedMean) + Err( + PsychometricError::AsymptoticTimeIndependentObservedVarianceIsNotInitialTimeIndependentObservedVariance + ) ); assert_eq!( - refuse_impulse_carry_observed_mean_as_initial_time_dependent_observed_mean( - carry, recovered + refuse_asymptotic_time_independent_observed_variance_as_stationary_observed_variance( + recovered, + stationary_observed ), - Err(PsychometricError::ImpulseCarryObservedMeanIsNotInitialTimeDependentObservedMean) + Err( + PsychometricError::AsymptoticTimeIndependentObservedVarianceIsNotStationaryObservedVariance + ) ); assert_eq!( - refuse_initial_time_independent_observed_mean_as_initial_time_dependent_observed_mean( - tipred, recovered + refuse_asymptotic_time_independent_observed_variance_as_measurement_error( + recovered, 0.1 ), - Err(PsychometricError::InitialTimeIndependentObservedMeanIsNotInitialTimeDependentObservedMean) + Err(PsychometricError::AsymptoticTimeIndependentObservedVarianceIsNotMeasurementError) ); } #[test] - fn discrete_observed_mean_with_initial_time_dependent_predictor_invalid_inputs_fail_closed() { - let scaled = recover_discrete_observed_mean_with_initial_time_dependent_predictor( - 1e308, - 1e-308, - 0.0, - 0.0, - 0.0, - 3.0, - 0.0, - 1.0, - LagClock::EventTime, - ) - .expect("scale"); - assert!((scaled - 1.0).abs() < 1e-15); + #[allow(clippy::too_many_lines)] + fn asymptotic_time_independent_observed_variance_fails_closed_on_non_event_clock_and_overflow() + { assert_eq!( - recover_discrete_observed_mean_with_initial_time_dependent_predictor( - 1e308, + recover_asymptotic_time_independent_observed_variance( 2.0, - 0.0, - 0.0, - 0.0, - 3.0, - 0.0, - 1.0, + 0.3, + 4.0, + -0.5, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_asymptotic_time_independent_observed_variance( + 2.0, + 0.3, + -0.1, + -0.5, LagClock::EventTime ), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_observed_mean_with_initial_time_dependent_predictor( + recover_asymptotic_time_independent_observed_variance( 2.0, - 1.0, - -0.5, 0.3, - 0.4, - 3.0, - 0.5, + 4.0, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) + ); + assert_eq!( + recover_asymptotic_time_independent_observed_variance( + f64::NAN, + 0.3, + 4.0, + -0.5, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_asymptotic_time_independent_observed_variance( 2.0, - LagClock::SystemTime + f64::NAN, + 4.0, + -0.5, + LagClock::EventTime ), - Err(PsychometricError::EventTimeRequired) + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_observed_mean_with_initial_time_dependent_predictor( + recover_asymptotic_time_independent_observed_variance( 2.0, - 1.0, + 0.3, + f64::NAN, -0.5, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_asymptotic_time_independent_observed_variance( + 2.0, 0.3, - 0.4, - 3.0, - 0.5, - 0.0, + 4.0, + f64::NAN, LagClock::EventTime ), - Err(PsychometricError::NonPositiveInterval) + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_observed_mean_with_initial_time_dependent_predictor( + recover_asymptotic_time_independent_observed_variance( + 2.0, 1e308, - 0.0, - 0.0, - 0.0, + 4.0, + -1e-308, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_asymptotic_time_independent_observed_variance( 1e308, - 1.0, - 0.0, - 1.0, + 0.3, + 4.0, + -0.5, LagClock::EventTime ), Err(PsychometricError::InvalidNumericInput) ); + let zero_with_overflowing_loading = recover_asymptotic_time_independent_observed_variance( + 1e308, + 0.0, + 4.0, + 0.0, + LagClock::EventTime, + ) + .expect("zero extra keeps zero"); + assert_eq!(zero_with_overflowing_loading.to_bits(), 0.0_f64.to_bits()); + let zero_with_overflowing_variance = recover_asymptotic_time_independent_observed_variance( + 2.0, + 0.0, + 1e308, + 0.0, + LagClock::EventTime, + ) + .expect("zero coefficient keeps zero"); + assert_eq!(zero_with_overflowing_variance.to_bits(), 0.0_f64.to_bits()); } } diff --git a/crates/psychometric_core/src/lib.rs b/crates/psychometric_core/src/lib.rs index 10741bbf3..be356c51c 100644 --- a/crates/psychometric_core/src/lib.rs +++ b/crates/psychometric_core/src/lib.rs @@ -185,6 +185,413 @@ //! (the lagged observed covariance omits `Q_Δt` and `θ`; //! `MANIFESTVAR` is not that later observed variance; the //! later-occasion latent variance is not that observed variance), +//! recovers the Driver §4.3 predetermined later-occasion variance as +//! `trait + e^{2 a Δt} p_0 + Q_Δt + (B / a)² v` +//! (JSS PDF re-opened 2026-08-23T05:12Z; form the evolved free +//! first-occasion variance first, then include the trait, then +//! include the TI extra variance, then add; trait and +//! `addedTIPREDVAR` do not enter `Q_Δt`; free `T0VAR` `p_0` is not +//! that later map; setting `p_0 = −q / (2 a)` recovers the +//! stationary later-occasion map; stationary later variance uses +//! `−q / (2 a)` in place of `p_0` and is not that later map when +//! `p_0` is free; evolving `trait + p_0 + (B / a)² v` as if it were +//! all state is not that later map; as `Δt → ∞` with stable `a < 0` +//! the composition approaches contemporaneous stationary `T0VAR`; +//! as `Δt → 0+` the composition approaches +//! `trait + p_0 + (B / a)² v`; nonzero diffusion with `a ≥ 0` is a +//! growing process and is kept), +//! recovers the Driver Eq. 5 of that predetermined later-occasion +//! variance as +//! `λ²(trait + e^{2 a Δt} p_0 + Q_Δt + (B / a)² v) + θ + ψ` +//! (`MANIFESTVAR` is not that later observed variance; the +//! predetermined later-occasion latent variance is not that observed +//! variance; stationary later observed variance is not that observed +//! variance when `p_0` is free), +//! recovers the Driver §4.3 predetermined lagged covariance as +//! `trait + e^{a Δt} p_0 + (B / a)² v` +//! (JSS PDF re-opened 2026-08-23T09:04Z; form the lagged free +//! first-occasion covariance first, then include the trait, then +//! include the TI extra variance, then add; trait and +//! `addedTIPREDVAR` do not decay with `e^{a Δt}`; free `T0VAR` `p_0` +//! is not that lagged map; setting `p_0 = −q / (2 a)` recovers the +//! stationary lagged map; stationary lagged covariance uses +//! `−q / (2 a)` in place of `p_0` and is not that lagged map when +//! `p_0` is free; evolving `trait + p_0 + (B / a)² v` as if it were +//! all state is not that lagged map; later-occasion variance +//! includes `Q_Δt` and is not that lagged map; as `Δt → ∞` with +//! stable `a < 0` the state term vanishes; as `Δt → 0+` the +//! composition approaches `trait + p_0 + (B / a)² v`), +//! recovers the Driver Eq. 5 of that predetermined lagged +//! covariance as `λ²(trait + e^{a Δt} p_0 + (B / a)² v) + ψ` +//! (`MANIFESTVAR` does not enter; the predetermined lagged latent +//! covariance is not that observed covariance; predetermined later +//! observed variance includes `Q_Δt` and `θ` and is not that lagged +//! observed covariance; stationary lagged observed covariance is +//! not that observed covariance when `p_0` is free), +//! recovers the Driver §4.3 predetermined first-occasion variance as +//! `trait + p_0 + (B / a)² v` +//! (JSS PDF re-opened 2026-08-23T10:03Z; form the free first-occasion +//! state variance first, then include the trait, then include the TI +//! extra variance, then add; trait and `addedTIPREDVAR` do not decay +//! and do not enter `Q_Δt`; free `T0VAR` `p_0` is not that +//! first-occasion map; setting `p_0 = −q / (2 a)` recovers the +//! stationary first-occasion map; stationary first-occasion variance +//! uses `−q / (2 a)` in place of `p_0` and is not that map when +//! `p_0` is free; lagged covariance decays the state and is not that +//! map; later-occasion variance includes `Q_Δt` and is not that map; +//! as `Δt → 0+` the lagged and later maps approach this composition), +//! recovers the Driver Eq. 5 of that predetermined first-occasion +//! variance as `λ²(trait + p_0 + (B / a)² v) + θ + ψ` +//! (`MANIFESTVAR` is not that first-occasion observed variance; the +//! predetermined first-occasion latent variance is not that observed +//! variance; stationary first-occasion observed variance is not that +//! observed variance when `p_0` is free; predetermined later +//! observed variance includes `Q_Δt` and is not that first-occasion +//! observed variance), +//! recovers the Driver §4.3 later-start lagged covariance of +//! predetermined `T0VAR` as +//! `trait + e^{a s}(e^{2 a u} p_0 + Q_u) + (B / a)² v` +//! (JSS PDF re-opened 2026-08-23T10:27Z; `startoffset`; form the +//! later-start within-subject variance first, then lag that state, +//! then include the trait, then include the TI extra variance, then +//! add; trait and `addedTIPREDVAR` do not decay with `e^{a s}`; +//! first-occasion lagged covariance omits `e^{a s} Q_u` and is not +//! that map when `u > 0`; later-occasion variance includes `Q_u` +//! without lagging and is not that map; setting `p_0 = −q / (2 a)` +//! recovers the stationary lagged map; stationary lagged covariance +//! uses `−q / (2 a)` in place of `p_0` and is not that map when +//! `p_0` is free; evolving the later total as if it were all state +//! is not that map; as `u → 0+` the composition approaches +//! first-occasion lagged covariance; as `s → 0+` the composition +//! approaches later-occasion variance at `u`), +//! recovers the Driver Eq. 5 of that later-start lagged covariance as +//! `λ²(trait + e^{a s}(e^{2 a u} p_0 + Q_u) + (B / a)² v) + ψ` +//! (`MANIFESTVAR` does not enter; the later-start lagged latent +//! covariance is not that observed covariance; first-occasion lagged +//! observed covariance omits `e^{a s} Q_u` and is not that observed +//! covariance when `u > 0`; predetermined later observed variance +//! includes `Q_u` and `θ` and is not that later-start lagged +//! observed covariance; stationary lagged observed covariance is +//! not that observed covariance when `p_0` is free), +//! recovers the Driver §4.3 later-start later-occasion variance of +//! predetermined `T0VAR` as +//! `trait + e^{2 a s}(e^{2 a u} p_0 + Q_u) + Q_s + (B / a)² v` +//! (JSS PDF re-opened 2026-08-23T11:05Z; `startoffset`; Chapman– +//! Kolmogorov `Q_{u+s} = e^{2 a s} Q_u + Q_s`; form the later-start +//! within-subject variance first, then evolve that state, then +//! include the trait, then include the TI extra variance, then add; +//! trait and `addedTIPREDVAR` do not enter `Q_s`; later-occasion +//! variance at `u` omits `Q_s` and is not that map when `s > 0`; +//! later-start lagged covariance omits `Q_s` and is not that map; +//! setting `p_0 = −q / (2 a)` recovers the stationary later-occasion +//! map; stationary later-occasion variance uses `−q / (2 a)` in +//! place of `p_0` and is not that map when `p_0` is free; evolving +//! the later total as if it were all state is not that map; +//! later-occasion variance over the lag interval alone ignores +//! `startoffset` and is not that map when `u > 0`; as `u → 0+` the +//! composition approaches later-occasion variance over `s`; as +//! `s → 0+` the composition approaches later-occasion variance at +//! `u`), +//! recovers the Driver Eq. 5 of that later-start later-occasion +//! variance as +//! `λ²(trait + e^{2 a s}(e^{2 a u} p_0 + Q_u) + Q_s + (B / a)² v) + θ + ψ` +//! (`MANIFESTVAR` is not that later-start later-occasion observed +//! variance; the later-start later-occasion latent variance is not +//! that observed variance; predetermined later observed variance +//! omits `Q_s` and is not that observed variance when `s > 0`; +//! later-start lagged observed covariance omits `Q_s` and `θ` and is +//! not that observed variance; stationary later observed variance is +//! not that observed variance when `p_0` is free), +//! recovers the Driver p. 16 `discreteDRIFTstd` as `e^{a Δt}` after +//! forming strictly positive `asymDIFFUSION` `−q / (2 a)` +//! (JSS PDF re-opened 2026-08-23T11:40Z; footnote 4 standardises +//! `DRIFT` using only within-subject variance, not the total; +//! unstandardised `e^{a Δt}` is defined for growing `a ≥ 0` and for +//! zero diffusion, and is not `discreteDRIFTstd`; zero +//! `asymDIFFUSION` fails closed; the §7.1 trait-plus-state +//! autocorrelation `(trait + e^{a Δt} p + added) / (trait + p + added)` +//! uses `TRAITVAR` and is not `discreteDRIFTstd`; `TRAITVAR` is not +//! the standardisation variance), +//! recovers the Driver p. 16 `discreteDIFFUSIONstd` as +//! `Q_Δt / (−q / (2 a))` after forming strictly positive +//! `asymDIFFUSION` `−q / (2 a)` +//! (JSS PDF re-opened 2026-08-23T13:06Z; footnote 4 standardises +//! using only the relevant variance, not the total; process noise is +//! within-subject, so that variance is `asymDIFFUSION`; +//! unstandardised `Q_Δt` is defined for growing `a ≥ 0` and for +//! zero diffusion, and is not `discreteDIFFUSIONstd`; zero +//! `asymDIFFUSION` fails closed; the continuous standardisation +//! `q / (−q / (2 a)) = −2 a` is not `discreteDIFFUSIONstd`; +//! `Q_Δt / (trait + p + added)` uses `TRAITVAR` and is not +//! `discreteDIFFUSIONstd`; `TRAITVAR` is not the standardisation +//! variance), +//! recovers the Driver p. 16 `DIFFUSIONstd` as +//! `q / (−q / (2 a)) = −2 a` after forming strictly positive +//! `asymDIFFUSION` `−q / (2 a)` +//! (JSS PDF re-opened 2026-08-23T13:20Z; footnote 4 standardises +//! using only the relevant variance, not the total; process noise is +//! within-subject, so that variance is `asymDIFFUSION`; +//! unstandardised `q` is defined for growing `a ≥ 0` and for zero +//! diffusion, and is not `DIFFUSIONstd`; zero `asymDIFFUSION` fails +//! closed; the discrete standardisation +//! `Q_Δt / (−q / (2 a)) = 1 − exp(2 a Δt)` is not `DIFFUSIONstd`; +//! `q / (trait + p + added)` uses `TRAITVAR` and is not +//! `DIFFUSIONstd`; `TRAITVAR` is not the standardisation variance), +//! recovers the Driver p. 16 `DRIFTstd` as the continuous auto-effect +//! after forming strictly positive `asymDIFFUSION` `−q / (2 a)` +//! (JSS PDF re-opened 2026-08-23T13:28Z; footnote 4 standardises +//! `DRIFT` using only within-subject variance, not the total; +//! unstandardised `a` is defined for growing `a ≥ 0` and for zero +//! diffusion, and is not `DRIFTstd`; zero `asymDIFFUSION` fails +//! closed; the discrete standardisation `e^{a Δt}` is not +//! `DRIFTstd`; `a p / (trait + p + added)` uses `TRAITVAR` and is +//! not `DRIFTstd`; `TRAITVAR` is not the standardisation variance), +//! recovers the Driver p. 16 `asymTIPREDEFFECTstd` as +//! `(-B / a) · √v / √(-q / (2 a))` after forming strictly positive +//! `asymDIFFUSION` and strictly positive predictor variance +//! (JSS PDF re-opened 2026-08-23T14:25Z; footnote 4 standardises +//! using only the relevant variance, not the total; the affecting +//! variance is `TIPREDVAR` `v`; the affected variance is +//! `asymDIFFUSION`; unstandardised `-B / a` is defined for a zero +//! coefficient and for zero predictor variance, and is not +//! `asymTIPREDEFFECTstd`; zero `asymDIFFUSION` or zero `v` fails +//! closed; the finite-interval standardisation +//! `A^{-1}[e^{A Δt} − I] B · √v / √p` is not `asymTIPREDEFFECTstd`; +//! `(-B / a) · √v / √(trait + p + added)` uses `TRAITVAR` and is +//! not `asymTIPREDEFFECTstd`; `TRAITVAR` is not the standardisation +//! variance), +//! recovers the Driver p. 16 `TIPREDEFFECTstd` as +//! `B · √v / √(-q / (2 a))` after forming strictly positive +//! `asymDIFFUSION` and strictly positive predictor variance +//! (JSS PDF re-opened 2026-08-23T16:21Z; footnote 4 standardises +//! using only the relevant variance, not the total; the affecting +//! variance is `TIPREDVAR` `v`; the affected variance is +//! `asymDIFFUSION`; unstandardised `B` is defined for a zero +//! coefficient and for zero predictor variance, and is not +//! `TIPREDEFFECTstd`; zero `asymDIFFUSION` or zero `v` fails +//! closed; the asymptotic standardisation +//! `(-B / a) · √v / √p` is not `TIPREDEFFECTstd`; +//! the finite-interval standardisation +//! `A^{-1}[e^{A Δt} − I] B · √v / √p` is not `TIPREDEFFECTstd`; +//! `B · √v / √(trait + p + added)` uses `TRAITVAR` and is +//! not `TIPREDEFFECTstd`; `TRAITVAR` is not the standardisation +//! variance), +//! recovers the Driver Table 3 / p. 16 `T0TIPREDEFFECTstd` as +//! `t0_b · √v / √p_0` after forming strictly positive free `T0VAR` +//! and strictly positive predictor variance +//! (JSS PDF re-opened 2026-08-23T17:20Z; footnote 4 standardises +//! using only the relevant variance, not the total; the affecting +//! variance is `TIPREDVAR` `v`; the affected variance is free +//! first-occasion `T0VAR` `p_0`, not `asymDIFFUSION`; unstandardised +//! `t0_b` is defined for a zero coefficient and for zero predictor +//! variance, and is not `T0TIPREDEFFECTstd`; zero `p_0` or zero `v` +//! fails closed; `T0` is event time, so a non-event clock fails +//! closed; free `T0VAR` does not require stable `a < 0`; +//! `B · √v / √(-q / (2 a))` is not `T0TIPREDEFFECTstd`; +//! `(-B / a) · √v / √p` is not `T0TIPREDEFFECTstd`; +//! `t0_b · √v / √(trait + p_0 + added)` uses `TRAITVAR` and is +//! not `T0TIPREDEFFECTstd`; `TRAITVAR` is not the standardisation +//! variance), +//! recovers the Driver Table 3 / p. 16 / 2017-era +//! `addedT0TIPREDVAR` as `t0_b² v` +//! (JSS PDF re-opened 2026-08-23T18:20Z; 2017-era +//! `summary.ctsemFit.R` forms +//! `T0TIPREDEFFECT %*% TIPREDVAR %*% t(T0TIPREDEFFECT)` immediately +//! after `T0TIPREDEFFECTstd`; form `t0_b` first, then square, then +//! multiply by `v`; a zero coefficient or zero predictor variance is +//! exactly zero; free `T0TIPREDEFFECT` does not require `a < 0`; +//! `(B / a)² v` is `addedTIPREDVAR` and is not this first-occasion +//! map; `t0_b · √v / √p_0` is `T0TIPREDEFFECTstd` and is not this +//! variance; free `T0VAR` is not this extra TI variance; `TRAITVAR` +//! is not this extra TI variance), +//! recovers the Driver Eq. 5 of 2017-era +//! `addedT0TIPREDVAR` as `λ² t0_b² v` +//! (JSS PDF re-opened 2026-08-23T19:10Z; 2017-era +//! `summary.ctsemFit.R` forms the latent extra first; form +//! `t0_b² v` first, then `(λ extra) λ` with `θ = 0`; a zero +//! loading or zero extra is exactly zero; free `T0TIPREDEFFECT` +//! does not require `a < 0`; `t0_b² v` is the latent extra and is +//! not this observed extra; `λ² p_0 + θ` is first-occasion +//! observed variance and is not this extra; `λ² (B / a)² v` is +//! Eq. 5 of `addedTIPREDVAR` and is not this first-occasion +//! observed extra; `MANIFESTVAR` `θ` is not this extra), +//! recovers the Driver Eq. 5 of §7.2 `addedTIPREDVAR` as +//! `λ² (B / a)² v` +//! (JSS PDF re-opened 2026-08-23T19:23Z; 2017-era +//! `summary.ctsemFit.R` forms `addedTIPREDVAR` as +//! `asymTIPREDEFFECT %*% TIPREDVAR %*% t(asymTIPREDEFFECT)`; form +//! `(B / a)² v` first, then `(λ extra) λ` with `θ = 0`; a zero +//! loading or zero extra is exactly zero; lasting asymptotic extra +//! requires `a < 0`; `(B / a)² v` is the latent extra and is not +//! this observed extra; `λ² t0_b² v` is Eq. 5 of `addedT0TIPREDVAR` +//! and is not this extra; `λ² p + θ` is stationary observed +//! variance and is not this extra; `MANIFESTVAR` `θ` is not this +//! extra), +//! recovers the Driver p. 16 `TDPREDEFFECTstd` as +//! `m · √v / √(-q / (2 a))` after forming strictly positive +//! `asymDIFFUSION` and strictly positive predictor variance +//! (JSS PDF re-opened 2026-08-23T21:10Z; footnote 4 standardises +//! using only the relevant variance, not the total; the affecting +//! variance is time-dependent predictor variance `v`; the affected +//! variance is `asymDIFFUSION`; unstandardised `M` is defined for a +//! zero coefficient and for zero predictor variance, and is not +//! `TDPREDEFFECTstd`; zero `asymDIFFUSION` or zero `v` fails +//! closed; `TIPREDEFFECTstd` `B · √v / √p` is not +//! `TDPREDEFFECTstd` even when `M = B`; the finite-interval +//! intercept-style standardisation +//! `A^{-1}[e^{A Δt} − I] M · √v / √p` is not `TDPREDEFFECTstd`; +//! `m · √v / √(trait + p + added)` uses `TRAITVAR` and is +//! not `TDPREDEFFECTstd`; `TRAITVAR` is not the standardisation +//! variance), +//! recovers the Driver Table 3 / p. 16 `T0TDPREDEFFECTstd` as +//! `t0_m · √v / √p_0` after forming strictly positive free `T0VAR` +//! and strictly positive time-dependent predictor variance +//! (JSS PDF re-opened 2026-08-23T21:34Z; footnote 4 standardises +//! using only the relevant variance, not the total; the affecting +//! variance is TD predictor variance `v`, not `TIPREDVAR`; the +//! affected variance is free first-occasion `T0VAR` `p_0`, not +//! `asymDIFFUSION`; unstandardised `t0_m` is defined for a zero +//! coefficient and for zero predictor variance, and is not +//! `T0TDPREDEFFECTstd`; zero `p_0` or zero `v` fails closed; `T0` +//! is event time, so a non-event clock fails closed; free `T0VAR` +//! does not require stable `a < 0`; `m · √v / √(-q / (2 a))` is +//! not `T0TDPREDEFFECTstd`; `t0_b · √v / √p_0` is not +//! `T0TDPREDEFFECTstd` even when `t0_m = t0_b`; +//! `t0_m · √v / √(trait + p_0 + added)` uses `TRAITVAR` and is +//! not `T0TDPREDEFFECTstd`; `TRAITVAR` is not the standardisation +//! variance), +//! recovers the Driver p. 16 `T0VARstd` as the correlation form +//! `solve(sqrt(diag(T0VAR))) %&% T0VAR` after forming strictly +//! positive free `T0VAR` (JSS PDF re-opened 2026-08-23T22:06Z; +//! 2017-era `summary.ctsemFit.R` forms that quadratic when +//! `verbose = TRUE`; `OpenMx` `%&%` is `t(A) %*% B %*% A`; the +//! default ridge is 0; the scalar map is `p_0 / p_0 = 1`; +//! unstandardised `p_0` is defined for a zero first-occasion +//! variance and is not `T0VARstd`; zero `p_0` fails closed; `T0` +//! is event time, so a non-event clock fails closed; free `T0VAR` +//! does not require stable `a < 0`; distinct positive `p_0` +//! recover the same 1; `t0_m · √v / √p_0` is not `T0VARstd`; +//! `t0_b² v` is not `T0VARstd`; `TRAITVAR` is not the +//! standardisation variance), +//! recovers the Driver p. 16 `TRAITVARstd` as the correlation form +//! `solve(sqrt(diag(TRAITVAR))) %&% TRAITVAR` after forming +//! strictly positive `TRAITVAR` (JSS PDF re-opened 2026-08-23T22:21Z; +//! Table 2, p. 12; §7.1, pp. 18–19; 2017-era `summary.ctsemFit.R` +//! forms that quadratic only when `TRAITVAR != 0` and `verbose = +//! TRUE`; `OpenMx` `%&%` is `t(A) %*% B %*% A`; unlike `T0VARstd` +//! there is no ridge addend; the scalar map is `trait / trait = 1`; +//! unstandardised `TRAITVAR` is defined for a zero trait and is +//! not `TRAITVARstd`; zero `TRAITVAR` fails closed; a non-event +//! clock fails closed; `TRAITVAR` does not require stable `a < 0`; +//! distinct positive `trait` recover the same 1; `T0VARstd` +//! recovers the same number and remains a distinct named quantity; +//! `t0_b² v` is not `TRAITVARstd`), +//! recovers the Driver p. 16 `MANIFESTTRAITVARstd` as the +//! correlation form `solve(sqrt(diag(MANIFESTTRAITVAR))) %&% +//! MANIFESTTRAITVAR` after forming strictly positive +//! `MANIFESTTRAITVAR` (JSS PDF re-opened 2026-08-23T22:28Z; Table 2, +//! p. 12; §7.1, p. 19; 2017-era `summary.ctsemFit.R` forms that +//! quadratic only when `MANIFESTTRAITVAR != 0` and `verbose = TRUE`; +//! `OpenMx` `%&%` is `t(A) %*% B %*% A`; unlike `TRAITVARstd` the +//! 2017-era source adds `diag(c(ridging), n.manifest)`; the default +//! ridge is 0 and is not this exact map; the scalar map is +//! `ψ / ψ = 1`; unstandardised `Ψ_τ` is defined for a zero +//! manifest trait and is not `MANIFESTTRAITVARstd`; zero +//! `MANIFESTTRAITVAR` fails closed; a non-event clock fails +//! closed; `MANIFESTTRAITVAR` does not require stable `a < 0`; +//! distinct positive `ψ` recover the same 1; `TRAITVARstd` +//! recovers the same number and remains a distinct named quantity; +//! `θ` is not `MANIFESTTRAITVARstd`), +//! recovers the Driver p. 16 `MANIFESTVARstd` as the +//! correlation form `solve(sqrt(diag(MANIFESTVAR))) %&% +//! MANIFESTVAR` after forming strictly positive `MANIFESTVAR` +//! (JSS PDF re-opened 2026-08-23T22:40Z; Table 2, p. 12; Eq. 5, +//! p. 5; 2017-era `summary.ctsemFit.R` forms that quadratic whenever +//! `verbose = TRUE`; `OpenMx` `%&%` is `t(A) %*% B %*% A`; unlike +//! `TRAITVARstd` the 2017-era source adds +//! `diag(c(ridging), n.manifest)`; the default ridge is 0 and is +//! not this exact map; the 2017-era `dimnames` assignment to +//! `latentNames` is a source bug and is not this exact map; the +//! scalar map is `θ / θ = 1`; unstandardised `Θ` is defined for a +//! zero residual and is not `MANIFESTVARstd`; zero `MANIFESTVAR` +//! makes `solve(sqrt(0))` fail and fails closed; a non-event clock +//! fails closed; `MANIFESTVAR` does not require stable `a < 0`; +//! distinct positive `θ` recover the same 1; `MANIFESTTRAITVARstd` +//! recovers the same number and remains a distinct named quantity; +//! `λ² Var(η) + θ` is not `MANIFESTVARstd`), +//! recovers the Driver p. 16 `TIPREDVARstd` as the +//! correlation form `solve(sqrt(diag(TIPREDVAR))) %&% +//! TIPREDVAR` after forming strictly positive `TIPREDVAR` +//! (JSS PDF re-opened 2026-08-23T22:53Z; Table 2, p. 12; +//! 2017-era `summary.ctsemFit.R` forms that quadratic whenever +//! `verbose = TRUE` and `n.TIpred > 0`; `OpenMx` `%&%` is +//! `t(A) %*% B %*% A`; unlike `TRAITVARstd` the 2017-era source +//! adds `diag(c(ridging), n.TIpred)`; the default ridge is 0 and +//! is not this exact map; `dimnames` are `TIpredNames`; the scalar +//! map is `v / v = 1`; unstandardised `v` is defined for a zero +//! predictor and is not `TIPREDVARstd`; zero `TIPREDVAR` makes +//! `solve(sqrt(0))` fail and fails closed; a non-event clock +//! fails closed; `TIPREDVAR` does not require stable `a < 0`; +//! distinct positive `v` recover the same 1; `MANIFESTVARstd` +//! recovers the same number and remains a distinct named quantity; +//! `(B / a)² v` is not `TIPREDVARstd`), +//! recovers the Driver p. 16 `asymDIFFUSIONstd` as the +//! correlation form `solve(sqrt(diag(asymDIFFUSION))) %&% +//! asymDIFFUSION` after forming strictly positive `asymDIFFUSION` +//! (JSS PDF re-opened 2026-08-23T23:02Z; p. 16; footnote 4; Eq. 4; +//! 2017-era `summary.ctsemFit.R` forms that quadratic whenever +//! `verbose = TRUE`; `OpenMx` `%&%` is `t(A) %*% B %*% A`; the +//! 2017-era source adds `diag(c(ridging), n.latent)`; the default +//! ridge is 0 and is not this exact map; `dimnames` are +//! `latentNames`; the scalar map is `p / p = 1` after +//! `p = −q / (2 a)`; unstandardised `p` is defined for a zero +//! process and is not `asymDIFFUSIONstd`; zero `q` makes +//! `solve(sqrt(0))` fail and fails closed; a non-event clock +//! fails closed; `a ≥ 0` fails closed; distinct positive `p` +//! recover the same 1; `TIPREDVARstd` recovers the same number +//! and remains a distinct named quantity; `DIFFUSIONstd` +//! `−2 a` is not `asymDIFFUSIONstd`), +//! recovers the Driver p. 16 `discreteCINTstd` as +//! `A^{-1}[e^{A Δt} − I] κ / √p` after forming strictly +//! positive `asymDIFFUSION` (JSS PDF re-opened 2026-08-24T05:20Z; +//! p. 16; footnote 4; Eq. 3; Table 2; 2017-era +//! `summary.ctsemFit.R` forms unstandardised `discreteCINT` +//! whenever `verbose = TRUE`; that source does not form a +//! `discreteCINTstd` matrix; the scalar map is the footnote 4 +//! standardisation of that named discrete intercept; unstandardised +//! `discreteCINT` is defined for growing `a ≥ 0` and for zero +//! diffusion and is not `discreteCINTstd`; zero `q` fails closed; +//! a non-event clock fails closed; a non-positive event interval +//! fails closed; `a ≥ 0` fails closed; `κ / √p` does not depend +//! on `Δt` and is not this map; `(-κ / a) / √p` is not this map), +//! recovers the Driver p. 16 `asymCINTstd` as +//! `(-κ / a) / √p` after forming strictly +//! positive `asymDIFFUSION` (JSS PDF re-opened 2026-08-24T09:05Z; +//! p. 16; footnote 4; Eq. 3; Table 2; 2017-era +//! `summary.ctsemFit.R` forms unstandardised `asymCINT` +//! whenever `verbose = TRUE` as `-solve(DRIFT) %*% CINT`; that +//! source does not form an `asymCINTstd` matrix; the scalar map +//! is the footnote 4 standardisation of that named asymptotic +//! intercept; unstandardised `asymCINT` is defined for a zero +//! process and is not `asymCINTstd`; zero `q` fails closed; +//! a non-event clock fails closed; `a ≥ 0` fails closed; +//! `κ / √p` is not this total-change map; `discreteCINTstd` +//! depends on `Δt` and is not this map), +//! recovers the Driver p. 16 `T0MEANSstd` as +//! `μ_0 / √p_0` after forming strictly +//! positive free `T0VAR` (JSS PDF re-opened 2026-08-24T22:30Z; +//! p. 16; footnote 4; Table 2; 2017-era +//! `summary.ctsemFit.R` forms unstandardised `T0MEANS` +//! as `OpenMx::mxEval(T0MEANS, mxobj, compute=TRUE)`; that +//! source does not form a `T0MEANSstd` matrix; the scalar map +//! is the footnote 4 standardisation of that named first-occasion +//! mean; unstandardised `T0MEANS` is defined for a zero +//! first-occasion variance and is not `T0MEANSstd`; zero `p_0` +//! fails closed; a non-event clock fails closed; free `T0MEANS` +//! does not require `a < 0`; `T0VARstd` recovers the same +//! number when `μ_0 = √p_0` and remains a distinct named +//! quantity; `μ_0 / √asymDIFFUSION` uses process-dynamics +//! variance and is not this map), //! and refuses //! latent-mean comparison below strong invariance. @@ -237,6 +644,8 @@ pub use event_time::LaggedWithinResidual; pub use event_time::map_discrete_lag_across_event_intervals; /// Exact scalar Table 2 `asymCINT` `-κ / a`. pub use event_time::recover_asymptotic_continuous_intercept; +/// Exact scalar Eq. 5 of §7.2 `addedTIPREDVAR` `λ² (B / a)² v`. +pub use event_time::recover_asymptotic_time_independent_observed_variance; /// Exact scalar §7.2 `asymTIPREDEFFECT` `-B z / a`. pub use event_time::recover_asymptotic_time_independent_predictor_effect; /// Exact scalar §7.2 `addedTIPREDVAR` `(B / a)² v`. @@ -299,10 +708,14 @@ pub use event_time::recover_event_time_discrete_lag_and_log_rate; pub use event_time::recover_initial_time_dependent_predictor_carry; /// Exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0`. pub use event_time::recover_initial_time_dependent_predictor_effect; +/// Exact scalar Eq. 5 of 2017-era `addedT0TIPREDVAR` `λ² t0_b² v`. +pub use event_time::recover_initial_time_independent_observed_variance; /// Exact scalar carried first-occasion `T0TIPREDEFFECT` `e^{A Δt} t0_b z`. pub use event_time::recover_initial_time_independent_predictor_carry; /// Exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z`. pub use event_time::recover_initial_time_independent_predictor_effect; +/// Exact scalar 2017-era `addedT0TIPREDVAR` `t0_b² v`. +pub use event_time::recover_initial_time_independent_predictor_variance; /// Mean exact log-rate on already-centered irregular residuals. pub use event_time::recover_irregular_centered_residual_log_rate; /// Exact scalar §7.2 level-change `CINT` `κ = −a m x`. @@ -323,6 +736,62 @@ pub use event_time::recover_manifest_observed_mean; pub use event_time::recover_manifest_observed_variance; /// Exact scalar observed-indicator variance `λ² Var(η) + θ + ψ`. pub use event_time::recover_manifest_trait_plus_state_observed_variance; +/// Exact scalar first-occasion variance of §4.3 predetermined `T0VAR` `trait + p_0 + (B / a)² v`. +pub use event_time::recover_predetermined_initial_latent_variance; +/// Exact scalar Eq. 5 of first-occasion §4.3 predetermined `T0VAR` `λ²(trait + p_0 + (B / a)² v) + θ + ψ`. +pub use event_time::recover_predetermined_initial_observed_variance; +/// Exact scalar lagged covariance of §4.3 predetermined `T0VAR` `trait + e^{a Δt} p_0 + (B / a)² v`. +pub use event_time::recover_predetermined_lagged_latent_covariance; +/// Exact scalar Eq. 5 of lagged §4.3 predetermined `T0VAR` `λ²(trait + e^{a Δt} p_0 + (B / a)² v) + ψ`. +pub use event_time::recover_predetermined_lagged_observed_covariance; +/// Exact scalar later-start lagged covariance of §4.3 predetermined `T0VAR` `trait + e^{a s}(e^{2 a u} p_0 + Q_u) + (B / a)² v`. +pub use event_time::recover_predetermined_later_lagged_latent_covariance; +/// Exact scalar Eq. 5 of later-start lagged §4.3 predetermined `T0VAR` `λ²(trait + e^{a s}(e^{2 a u} p_0 + Q_u) + (B / a)² v) + ψ`. +pub use event_time::recover_predetermined_later_lagged_observed_covariance; +/// Exact scalar later-occasion variance of §4.3 predetermined `T0VAR` `trait + e^{2 a Δt} p_0 + Q_Δt + (B / a)² v`. +pub use event_time::recover_predetermined_later_latent_variance; +/// Exact scalar Eq. 5 of later-occasion §4.3 predetermined `T0VAR` `λ²(trait + e^{2 a Δt} p_0 + Q_Δt + (B / a)² v) + θ + ψ`. +pub use event_time::recover_predetermined_later_observed_variance; +/// Exact scalar later-start later-occasion variance of §4.3 predetermined `T0VAR` `trait + e^{2 a s}(e^{2 a u} p_0 + Q_u) + Q_s + (B / a)² v`. +pub use event_time::recover_predetermined_later_start_later_latent_variance; +/// Exact scalar Eq. 5 of later-start later-occasion §4.3 predetermined `T0VAR` `λ²(trait + e^{2 a s}(e^{2 a u} p_0 + Q_u) + Q_s + (B / a)² v) + θ + ψ`. +pub use event_time::recover_predetermined_later_start_later_observed_variance; +/// Exact scalar p. 16 `asymCINTstd` `(-κ / a) / √p` after strictly positive `asymDIFFUSION`. +pub use event_time::recover_standardised_asymptotic_continuous_intercept; +/// Exact scalar p. 16 `asymDIFFUSIONstd` `p / p = 1` after strictly positive `asymDIFFUSION`. +pub use event_time::recover_standardised_asymptotic_diffusion; +/// Exact scalar p. 16 `asymTIPREDEFFECTstd` `(-B / a) · √v / √(-q / (2 a))` after strictly positive `asymDIFFUSION` and `v`. +pub use event_time::recover_standardised_asymptotic_time_independent_predictor_effect; +/// Exact scalar p. 16 `DIFFUSIONstd` `q / (−q / (2 a)) = −2 a` after strictly positive `asymDIFFUSION`. +pub use event_time::recover_standardised_continuous_diffusion; +/// Exact scalar p. 16 `DRIFTstd` after strictly positive `asymDIFFUSION`. +pub use event_time::recover_standardised_continuous_drift; +/// Exact scalar p. 16 `TDPREDEFFECTstd` `m · √v / √(-q / (2 a))` after strictly positive `asymDIFFUSION` and `v`. +pub use event_time::recover_standardised_continuous_time_dependent_predictor_effect; +/// Exact scalar p. 16 `TIPREDEFFECTstd` `B · √v / √(-q / (2 a))` after strictly positive `asymDIFFUSION` and `v`. +pub use event_time::recover_standardised_continuous_time_independent_predictor_effect; +/// Exact scalar p. 16 `discreteCINTstd` `A^{-1}[e^{A Δt} − I] κ / √p` after strictly positive `asymDIFFUSION`. +pub use event_time::recover_standardised_discrete_continuous_intercept; +/// Exact scalar p. 16 `discreteDIFFUSIONstd` `Q_Δt / (−q / (2 a))` after strictly positive `asymDIFFUSION`. +pub use event_time::recover_standardised_discrete_diffusion; +/// Exact scalar p. 16 `discreteDRIFTstd` `e^{a Δt}` after strictly positive `asymDIFFUSION`. +pub use event_time::recover_standardised_discrete_drift; +/// Exact scalar p. 16 `T0MEANSstd` `μ_0 / √p_0` after strictly positive free `T0VAR`. +pub use event_time::recover_standardised_initial_latent_mean; +/// Exact scalar p. 16 `T0VARstd` `p_0 / p_0 = 1` after strictly positive free `T0VAR`. +pub use event_time::recover_standardised_initial_latent_variance; +/// Exact scalar Table 3 / p. 16 `T0TDPREDEFFECTstd` `t0_m · √v / √p_0` after strictly positive free `T0VAR` and `v`. +pub use event_time::recover_standardised_initial_time_dependent_predictor_effect; +/// Exact scalar Table 3 / p. 16 `T0TIPREDEFFECTstd` `t0_b · √v / √p_0` after strictly positive free `T0VAR` and `v`. +pub use event_time::recover_standardised_initial_time_independent_predictor_effect; +/// Exact scalar p. 16 `MANIFESTTRAITVARstd` `ψ / ψ = 1` after strictly positive `MANIFESTTRAITVAR`. +pub use event_time::recover_standardised_manifest_trait_variance; +/// Exact scalar p. 16 `MANIFESTVARstd` `θ / θ = 1` after strictly positive `MANIFESTVAR`. +pub use event_time::recover_standardised_manifest_variance; +/// Exact scalar p. 16 `TIPREDVARstd` `v / v = 1` after strictly positive `TIPREDVAR`. +pub use event_time::recover_standardised_time_independent_predictor_variance; +/// Exact scalar p. 16 `TRAITVARstd` `trait / trait = 1` after strictly positive `TRAITVAR`. +pub use event_time::recover_standardised_trait_variance; /// Exact scalar p. 16 stationary `T0MEANS` `-κ / a + −B z / a`. pub use event_time::recover_stationary_initial_latent_mean; /// Exact scalar §4.3 / p. 16 stationary `T0VAR` `trait + −q / (2 a) + (B / a)² v`. @@ -365,6 +834,8 @@ pub use event_time::refuse_asymptotic_continuous_intercept_as_discrete_increment pub use event_time::refuse_asymptotic_continuous_intercept_as_initial_latent_mean; /// Refuse treating `τ + λ(−κ / a)` as Eq. 5 of §4.3 stationary `T0MEANS`. pub use event_time::refuse_asymptotic_continuous_intercept_observed_mean_as_stationary_initial_observed_mean; +/// Refuse treating Table 2 `asymCINT` `/ √p` as p. 16 `discreteCINTstd`. +pub use event_time::refuse_asymptotic_standardised_continuous_intercept_as_standardised_discrete_continuous_intercept; /// Refuse treating §7.2 `asymTIPREDEFFECT` as `TIPREDEFFECT` `B`. pub use event_time::refuse_asymptotic_time_independent_effect_as_coefficient; /// Refuse treating §7.2 `asymTIPREDEFFECT` as `CINT`. @@ -373,6 +844,16 @@ pub use event_time::refuse_asymptotic_time_independent_effect_as_continuous_inte pub use event_time::refuse_asymptotic_time_independent_effect_as_discrete_effect; /// Refuse treating §7.2 `asymTIPREDEFFECT` as `M x`. pub use event_time::refuse_asymptotic_time_independent_effect_as_time_dependent_impulse; +/// Refuse treating Eq. 5 of §7.2 `addedTIPREDVAR` as the latent extra. +pub use event_time::refuse_asymptotic_time_independent_observed_variance_as_asymptotic_time_independent_variance; +/// Refuse treating Eq. 5 of §7.2 `addedTIPREDVAR` as Eq. 5 of `addedT0TIPREDVAR`. +pub use event_time::refuse_asymptotic_time_independent_observed_variance_as_initial_time_independent_observed_variance; +/// Refuse treating Eq. 5 of §7.2 `addedTIPREDVAR` as `MANIFESTVAR`. +pub use event_time::refuse_asymptotic_time_independent_observed_variance_as_measurement_error; +/// Refuse treating Eq. 5 of §7.2 `addedTIPREDVAR` as stationary observed variance. +pub use event_time::refuse_asymptotic_time_independent_observed_variance_as_stationary_observed_variance; +/// Refuse treating §7.2 `addedTIPREDVAR` as p. 16 `TIPREDVARstd`. +pub use event_time::refuse_asymptotic_time_independent_predictor_variance_as_standardised_time_independent_predictor_variance; /// Refuse treating §7.2 `addedTIPREDVAR` as `asymTIPREDEFFECT`. pub use event_time::refuse_asymptotic_time_independent_variance_as_asymptotic_effect; /// Refuse treating §7.2 `addedTIPREDVAR` as `asymDIFFUSION`. @@ -465,6 +946,26 @@ pub use event_time::refuse_initial_time_independent_effect_as_process_increment; pub use event_time::refuse_initial_time_independent_effect_as_time_dependent_impulse; /// Refuse treating first-occasion TI observed mean as the first-occasion TD observed mean. pub use event_time::refuse_initial_time_independent_observed_mean_as_initial_time_dependent_observed_mean; +/// Refuse treating Eq. 5 of 2017-era `addedT0TIPREDVAR` as Eq. 5 of `addedTIPREDVAR`. +pub use event_time::refuse_initial_time_independent_observed_variance_as_asymptotic_time_independent_observed_variance; +/// Refuse treating Eq. 5 of 2017-era `addedT0TIPREDVAR` as first-occasion observed variance. +pub use event_time::refuse_initial_time_independent_observed_variance_as_initial_observed_variance; +/// Refuse treating Eq. 5 of 2017-era `addedT0TIPREDVAR` as the latent extra. +pub use event_time::refuse_initial_time_independent_observed_variance_as_initial_time_independent_variance; +/// Refuse treating Eq. 5 of 2017-era `addedT0TIPREDVAR` as `MANIFESTVAR`. +pub use event_time::refuse_initial_time_independent_observed_variance_as_measurement_error; +/// Refuse treating 2017-era `addedT0TIPREDVAR` as §7.2 `addedTIPREDVAR`. +pub use event_time::refuse_initial_time_independent_variance_as_asymptotic_time_independent_variance; +/// Refuse treating 2017-era `addedT0TIPREDVAR` as free first-occasion `T0VAR`. +pub use event_time::refuse_initial_time_independent_variance_as_initial_latent_variance; +/// Refuse treating 2017-era `addedT0TIPREDVAR` as p. 16 `T0VARstd`. +pub use event_time::refuse_initial_time_independent_variance_as_standardised_initial_latent_variance; +/// Refuse treating 2017-era `addedT0TIPREDVAR` as Table 3 / p. 16 `T0TIPREDEFFECTstd`. +pub use event_time::refuse_initial_time_independent_variance_as_standardised_initial_time_independent_effect; +/// Refuse treating 2017-era `addedT0TIPREDVAR` as p. 16 `TRAITVARstd`. +pub use event_time::refuse_initial_time_independent_variance_as_standardised_trait_variance; +/// Refuse treating 2017-era `addedT0TIPREDVAR` as `TRAITVAR`. +pub use event_time::refuse_initial_time_independent_variance_as_trait_variance; /// Refuse treating Driver Eq. 3–4 lagged latent covariance as `cov(y_t, y_{t-1})`. pub use event_time::refuse_latent_lagged_covariance_as_observed_covariance; /// Refuse treating Driver Eq. 5 latent mean as `E(y)`. @@ -497,14 +998,136 @@ pub use event_time::refuse_manifest_trait_variance_as_measurement_error; pub use event_time::refuse_measurement_error_as_lagged_observed_covariance; /// Refuse treating Driver Eq. 5 measurement error as `Var(y)`. pub use event_time::refuse_measurement_error_as_observed_variance; +/// Refuse treating `MANIFESTVAR` as Eq. 5 of predetermined first-occasion `T0VAR`. +pub use event_time::refuse_measurement_error_as_predetermined_initial_observed_variance; +/// Refuse treating `MANIFESTVAR` as Eq. 5 of predetermined lagged `T0VAR`. +pub use event_time::refuse_measurement_error_as_predetermined_lagged_observed_covariance; +/// Refuse treating `MANIFESTVAR` as Eq. 5 of later-start lagged predetermined `T0VAR`. +pub use event_time::refuse_measurement_error_as_predetermined_later_lagged_observed_covariance; +/// Refuse treating `MANIFESTVAR` as Eq. 5 of predetermined later-occasion `T0VAR`. +pub use event_time::refuse_measurement_error_as_predetermined_later_observed_variance; +/// Refuse treating `MANIFESTVAR` as Eq. 5 of later-start later-occasion predetermined `T0VAR`. +pub use event_time::refuse_measurement_error_as_predetermined_later_start_later_observed_variance; +/// Refuse treating `MANIFESTVAR` as p. 16 `MANIFESTTRAITVARstd`. +pub use event_time::refuse_measurement_error_as_standardised_manifest_trait_variance; /// Refuse treating `MANIFESTVAR` as Eq. 5 of lagged §4.3 stationary `T0VAR`. pub use event_time::refuse_measurement_error_as_stationary_lagged_observed_covariance; /// Refuse treating `MANIFESTVAR` as Eq. 5 of later-occasion §4.3 stationary `T0VAR`. pub use event_time::refuse_measurement_error_as_stationary_later_observed_variance; +/// Refuse treating Driver Eq. 5 `Var(y)` as p. 16 `MANIFESTVARstd`. +pub use event_time::refuse_observed_variance_as_standardised_manifest_variance; /// Refuse pooling discrete lags from unequal event intervals. pub use event_time::refuse_pooled_discrete_lag_across_unequal_intervals; +/// Refuse treating predetermined first-occasion variance as free first-occasion `T0VAR`. +pub use event_time::refuse_predetermined_initial_latent_variance_as_initial_latent_variance; +/// Refuse treating predetermined first-occasion variance as predetermined lagged covariance. +pub use event_time::refuse_predetermined_initial_latent_variance_as_lagged_latent_covariance; +/// Refuse treating predetermined first-occasion variance as predetermined later-occasion variance. +pub use event_time::refuse_predetermined_initial_latent_variance_as_later_latent_variance; +/// Refuse treating predetermined first-occasion variance as predetermined first-occasion observed variance. +pub use event_time::refuse_predetermined_initial_latent_variance_as_observed_variance; +/// Refuse treating predetermined first-occasion variance as stationary first-occasion `T0VAR`. +pub use event_time::refuse_predetermined_initial_latent_variance_as_stationary_initial_latent_variance; +/// Refuse treating predetermined lagged covariance as the decayed total. +pub use event_time::refuse_predetermined_lagged_latent_covariance_as_decayed_total; +/// Refuse treating predetermined lagged covariance as free first-occasion `T0VAR`. +pub use event_time::refuse_predetermined_lagged_latent_covariance_as_initial_latent_variance; +/// Refuse treating predetermined lagged covariance as predetermined later-occasion variance. +pub use event_time::refuse_predetermined_lagged_latent_covariance_as_later_latent_variance; +/// Refuse treating predetermined lagged covariance as predetermined lagged observed covariance. +pub use event_time::refuse_predetermined_lagged_latent_covariance_as_observed_covariance; +/// Refuse treating predetermined lagged covariance as lagged stationary `T0VAR`. +pub use event_time::refuse_predetermined_lagged_latent_covariance_as_stationary_lagged_covariance; +/// Refuse treating Eq. 5 of first-occasion lagged predetermined `T0VAR` as later-start lagged observed covariance. +pub use event_time::refuse_predetermined_lagged_observed_covariance_as_predetermined_later_lagged_observed_covariance; +/// Refuse treating later-start lagged covariance as the decayed later total. +pub use event_time::refuse_predetermined_later_lagged_latent_covariance_as_decayed_later_total; +/// Refuse treating later-start lagged covariance as later-occasion variance of predetermined `T0VAR`. +pub use event_time::refuse_predetermined_later_lagged_latent_covariance_as_later_latent_variance; +/// Refuse treating later-start lagged covariance as later-start lagged observed covariance. +pub use event_time::refuse_predetermined_later_lagged_latent_covariance_as_observed_covariance; +/// Refuse treating later-start lagged covariance as first-occasion lagged covariance of predetermined `T0VAR`. +pub use event_time::refuse_predetermined_later_lagged_latent_covariance_as_predetermined_lagged_covariance; +/// Refuse treating later-start lagged covariance as lagged stationary `T0VAR`. +pub use event_time::refuse_predetermined_later_lagged_latent_covariance_as_stationary_lagged_covariance; +/// Refuse treating Eq. 5 of later-start lagged predetermined `T0VAR` as later-start later-occasion observed variance. +pub use event_time::refuse_predetermined_later_lagged_observed_covariance_as_predetermined_later_start_later_observed_variance; +/// Refuse treating predetermined later-occasion variance as the free discrete evolution of the total. +pub use event_time::refuse_predetermined_later_latent_variance_as_discrete_variance; +/// Refuse treating predetermined later-occasion variance as free first-occasion `T0VAR`. +pub use event_time::refuse_predetermined_later_latent_variance_as_initial_latent_variance; +/// Refuse treating predetermined later-occasion variance as predetermined later-occasion observed variance. +pub use event_time::refuse_predetermined_later_latent_variance_as_observed_variance; +/// Refuse treating predetermined later-occasion variance as later-occasion stationary `T0VAR`. +pub use event_time::refuse_predetermined_later_latent_variance_as_stationary_later_latent_variance; +/// Refuse treating Eq. 5 of predetermined later-occasion `T0VAR` as predetermined first-occasion observed variance. +pub use event_time::refuse_predetermined_later_observed_variance_as_predetermined_initial_observed_variance; +/// Refuse treating Eq. 5 of predetermined later-occasion `T0VAR` as predetermined lagged observed covariance. +pub use event_time::refuse_predetermined_later_observed_variance_as_predetermined_lagged_observed_covariance; +/// Refuse treating Eq. 5 of predetermined later-occasion `T0VAR` as later-start lagged observed covariance. +pub use event_time::refuse_predetermined_later_observed_variance_as_predetermined_later_lagged_observed_covariance; +/// Refuse treating Eq. 5 of predetermined later-occasion `T0VAR` as later-start later-occasion observed variance. +pub use event_time::refuse_predetermined_later_observed_variance_as_predetermined_later_start_later_observed_variance; +/// Refuse treating later-start later-occasion variance as the evolved later total. +pub use event_time::refuse_predetermined_later_start_later_latent_variance_as_decayed_later_total; +/// Refuse treating later-start later-occasion variance as later-occasion variance over the lag interval alone. +pub use event_time::refuse_predetermined_later_start_later_latent_variance_as_lag_interval_later_latent_variance; +/// Refuse treating later-start later-occasion variance as later-start lagged covariance of predetermined `T0VAR`. +pub use event_time::refuse_predetermined_later_start_later_latent_variance_as_later_lagged_covariance; +/// Refuse treating later-start later-occasion variance as later-occasion variance at the later start. +pub use event_time::refuse_predetermined_later_start_later_latent_variance_as_later_latent_variance; +/// Refuse treating later-start later-occasion variance as later-start later-occasion observed variance. +pub use event_time::refuse_predetermined_later_start_later_latent_variance_as_observed_variance; +/// Refuse treating later-start later-occasion variance as later-occasion stationary `T0VAR`. +pub use event_time::refuse_predetermined_later_start_later_latent_variance_as_stationary_later_latent_variance; /// Refuse treating Driver Eq. 3 process noise as the unconditional variance. pub use event_time::refuse_process_noise_as_unconditional_variance; +/// Refuse treating p. 16 `asymTIPREDEFFECTstd` as p. 16 `TIPREDEFFECTstd`. +pub use event_time::refuse_standardised_asymptotic_time_independent_effect_as_standardised_continuous_time_independent_effect; +/// Refuse treating p. 16 `asymTIPREDEFFECTstd` as Table 3 / p. 16 `T0TIPREDEFFECTstd`. +pub use event_time::refuse_standardised_asymptotic_time_independent_effect_as_standardised_initial_time_independent_effect; +/// Refuse treating p. 16 `DIFFUSIONstd` `−2 a` as p. 16 `asymDIFFUSIONstd`. +pub use event_time::refuse_standardised_continuous_diffusion_as_standardised_asymptotic_diffusion; +/// Refuse treating continuous `DIFFUSION` standardisation `−2 a` as p. 16 `discreteDIFFUSIONstd`. +pub use event_time::refuse_standardised_continuous_diffusion_as_standardised_discrete_diffusion; +/// Refuse treating `κ / √p` as p. 16 `asymCINTstd`. +pub use event_time::refuse_standardised_continuous_intercept_as_standardised_asymptotic_continuous_intercept; +/// Refuse treating `κ / √p` as p. 16 `discreteCINTstd`. +pub use event_time::refuse_standardised_continuous_intercept_as_standardised_discrete_continuous_intercept; +/// Refuse treating p. 16 `TDPREDEFFECTstd` as Table 3 / p. 16 `T0TDPREDEFFECTstd`. +pub use event_time::refuse_standardised_continuous_time_dependent_effect_as_standardised_initial_time_dependent_effect; +/// Refuse treating p. 16 `TIPREDEFFECTstd` as p. 16 `TDPREDEFFECTstd`. +pub use event_time::refuse_standardised_continuous_time_independent_effect_as_standardised_continuous_time_dependent_effect; +/// Refuse treating p. 16 `TIPREDEFFECTstd` as Table 3 / p. 16 `T0TIPREDEFFECTstd`. +pub use event_time::refuse_standardised_continuous_time_independent_effect_as_standardised_initial_time_independent_effect; +/// Refuse treating p. 16 `discreteCINTstd` as p. 16 `asymCINTstd`. +pub use event_time::refuse_standardised_discrete_continuous_intercept_as_standardised_asymptotic_continuous_intercept; +/// Refuse treating p. 16 `discreteDIFFUSIONstd` `1 − exp(2 a Δt)` as p. 16 `DIFFUSIONstd`. +pub use event_time::refuse_standardised_discrete_diffusion_as_standardised_continuous_diffusion; +/// Refuse treating p. 16 `discreteDRIFTstd` `e^{a Δt}` as p. 16 `DRIFTstd`. +pub use event_time::refuse_standardised_discrete_drift_as_standardised_continuous_drift; +/// Refuse treating intercept-style standardised `TDPREDEFFECT` as p. 16 `TDPREDEFFECTstd`. +pub use event_time::refuse_standardised_discrete_time_dependent_effect_as_standardised_continuous_time_dependent_effect; +/// Refuse treating a finite-interval standardised `TIPREDEFFECT` as p. 16 `asymTIPREDEFFECTstd`. +pub use event_time::refuse_standardised_discrete_time_independent_effect_as_standardised_asymptotic_time_independent_effect; +/// Refuse treating a finite-interval standardised `TIPREDEFFECT` as p. 16 `TIPREDEFFECTstd`. +pub use event_time::refuse_standardised_discrete_time_independent_effect_as_standardised_continuous_time_independent_effect; +/// Refuse treating p. 16 `T0VARstd` as p. 16 `T0MEANSstd`. +pub use event_time::refuse_standardised_initial_latent_variance_as_standardised_initial_latent_mean; +/// Refuse treating p. 16 `T0VARstd` as p. 16 `TRAITVARstd`. +pub use event_time::refuse_standardised_initial_latent_variance_as_standardised_trait_variance; +/// Refuse treating Table 3 / p. 16 `T0TDPREDEFFECTstd` as p. 16 `T0VARstd`. +pub use event_time::refuse_standardised_initial_time_dependent_effect_as_standardised_initial_latent_variance; +/// Refuse treating Table 3 / p. 16 `T0TIPREDEFFECTstd` as Table 3 / p. 16 `T0TDPREDEFFECTstd`. +pub use event_time::refuse_standardised_initial_time_independent_effect_as_standardised_initial_time_dependent_effect; +/// Refuse treating p. 16 `MANIFESTTRAITVARstd` as p. 16 `MANIFESTVARstd`. +pub use event_time::refuse_standardised_manifest_trait_variance_as_standardised_manifest_variance; +/// Refuse treating p. 16 `MANIFESTVARstd` as p. 16 `TIPREDVARstd`. +pub use event_time::refuse_standardised_manifest_variance_as_standardised_time_independent_predictor_variance; +/// Refuse treating p. 16 `TIPREDVARstd` as p. 16 `asymDIFFUSIONstd`. +pub use event_time::refuse_standardised_time_independent_predictor_variance_as_standardised_asymptotic_diffusion; +/// Refuse treating p. 16 `TRAITVARstd` as p. 16 `MANIFESTTRAITVARstd`. +pub use event_time::refuse_standardised_trait_variance_as_standardised_manifest_trait_variance; /// Refuse treating p. 16 stationary `T0MEANS` as `asymCINT`. pub use event_time::refuse_stationary_initial_latent_mean_as_asymptotic_continuous_intercept; /// Refuse treating p. 16 stationary `T0MEANS` as `asymTIPREDEFFECT`. @@ -531,6 +1154,8 @@ pub use event_time::refuse_stationary_initial_latent_variance_as_trait_variance; pub use event_time::refuse_stationary_initial_observed_mean_as_manifest_means; /// Refuse treating Eq. 5 of §4.3 stationary `T0VAR` as `MANIFESTVAR`. pub use event_time::refuse_stationary_initial_observed_variance_as_measurement_error; +/// Refuse treating Eq. 5 of §4.3 stationary `T0VAR` as predetermined first-occasion observed variance. +pub use event_time::refuse_stationary_initial_observed_variance_as_predetermined_initial_observed_variance; /// Refuse treating Eq. 5 of contemporaneous §4.3 stationary `T0VAR` as lagged observed covariance. pub use event_time::refuse_stationary_initial_observed_variance_as_stationary_lagged_observed_covariance; /// Refuse treating lagged §4.3 stationary `T0VAR` as decayed total stationary variance. @@ -539,6 +1164,10 @@ pub use event_time::refuse_stationary_lagged_latent_covariance_as_decayed_statio pub use event_time::refuse_stationary_lagged_latent_covariance_as_observed_covariance; /// Refuse treating lagged §4.3 stationary `T0VAR` as contemporaneous stationary `T0VAR`. pub use event_time::refuse_stationary_lagged_latent_covariance_as_stationary_initial_latent_variance; +/// Refuse treating Eq. 5 of lagged §4.3 stationary `T0VAR` as predetermined lagged observed covariance. +pub use event_time::refuse_stationary_lagged_observed_covariance_as_predetermined_lagged_observed_covariance; +/// Refuse treating Eq. 5 of lagged §4.3 stationary `T0VAR` as later-start lagged observed covariance of predetermined `T0VAR`. +pub use event_time::refuse_stationary_lagged_observed_covariance_as_predetermined_later_lagged_observed_covariance; /// Refuse treating Eq. 5 of lagged §4.3 stationary `T0VAR` as later-occasion observed variance. pub use event_time::refuse_stationary_lagged_observed_covariance_as_stationary_later_observed_variance; /// Refuse treating later-occasion §4.3 stationary `T0VAR` as the free discrete evolution of the constrained total. @@ -549,6 +1178,10 @@ pub use event_time::refuse_stationary_later_latent_variance_as_lagged_covariance pub use event_time::refuse_stationary_later_latent_variance_as_observed_variance; /// Refuse treating later-occasion §4.3 stationary `T0VAR` as finite-interval process noise. pub use event_time::refuse_stationary_later_latent_variance_as_process_noise; +/// Refuse treating Eq. 5 of later-occasion §4.3 stationary `T0VAR` as predetermined later-occasion observed variance. +pub use event_time::refuse_stationary_later_observed_variance_as_predetermined_later_observed_variance; +/// Refuse treating Eq. 5 of later-occasion §4.3 stationary `T0VAR` as later-start later-occasion observed variance of predetermined `T0VAR`. +pub use event_time::refuse_stationary_later_observed_variance_as_predetermined_later_start_later_observed_variance; /// Refuse treating Eq. 5 of `asymDIFFUSION` as Eq. 5 of §4.3 stationary `T0VAR`. pub use event_time::refuse_stationary_within_subject_observed_variance_as_stationary_initial_observed_variance; /// Refuse treating Driver Eq. 3 `TDPREDEFFECT` impulse as `CINT`. @@ -577,14 +1210,72 @@ pub use event_time::refuse_time_independent_effect_as_time_varying_discrete_effe pub use event_time::refuse_time_independent_observed_mean_as_initial_time_dependent_observed_mean; /// Refuse treating process-increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` as the first-occasion TI-predictor observed mean. pub use event_time::refuse_time_independent_observed_mean_as_initial_time_independent_observed_mean; +/// Refuse treating Driver §7.1 trait-contaminated asymptotic TI effect as p. 16 `asymTIPREDEFFECTstd`. +pub use event_time::refuse_trait_contaminated_asymptotic_time_independent_effect_as_standardised_asymptotic_time_independent_effect; +/// Refuse treating Driver §7.1 trait-contaminated continuous diffusion as p. 16 `DIFFUSIONstd`. +pub use event_time::refuse_trait_contaminated_continuous_diffusion_as_standardised_continuous_diffusion; +/// Refuse treating Driver §7.1 trait-contaminated continuous drift as p. 16 `DRIFTstd`. +pub use event_time::refuse_trait_contaminated_continuous_drift_as_standardised_continuous_drift; +/// Refuse treating Driver §7.1 trait-contaminated continuous TD effect as p. 16 `TDPREDEFFECTstd`. +pub use event_time::refuse_trait_contaminated_continuous_time_dependent_effect_as_standardised_continuous_time_dependent_effect; +/// Refuse treating Driver §7.1 trait-contaminated continuous TI effect as p. 16 `TIPREDEFFECTstd`. +pub use event_time::refuse_trait_contaminated_continuous_time_independent_effect_as_standardised_continuous_time_independent_effect; +/// Refuse treating Driver §7.1 trait-contaminated first-occasion TD effect as Table 3 / p. 16 `T0TDPREDEFFECTstd`. +pub use event_time::refuse_trait_contaminated_initial_time_dependent_effect_as_standardised_initial_time_dependent_effect; +/// Refuse treating Driver §7.1 trait-contaminated first-occasion TI effect as Table 3 / p. 16 `T0TIPREDEFFECTstd`. +pub use event_time::refuse_trait_contaminated_initial_time_independent_effect_as_standardised_initial_time_independent_effect; +/// Refuse treating Driver §7.1 trait-contaminated process noise as p. 16 `discreteDIFFUSIONstd`. +pub use event_time::refuse_trait_contaminated_process_noise_as_standardised_discrete_diffusion; +/// Refuse treating Driver §7.1 trait-plus-state autocorrelation as p. 16 `discreteDRIFTstd`. +pub use event_time::refuse_trait_plus_state_autocorrelation_as_standardised_discrete_drift; /// Refuse treating §4.3 trait-plus-state lagged covariance as lagged stationary `T0VAR`. pub use event_time::refuse_trait_plus_state_lagged_covariance_as_stationary_lagged_latent_covariance; /// Refuse treating Driver §4.3 trait variance as process noise. pub use event_time::refuse_trait_variance_as_process_noise; +/// Refuse treating Driver §4.3 trait variance as the p. 16 footnote 4 standardisation variance. +pub use event_time::refuse_trait_variance_as_standardisation_variance; /// Refuse treating Driver §4.3 trait variance as `asymDIFFUSION`. pub use event_time::refuse_trait_variance_as_stationary_within_subject; /// Refuse a time-varying predictor whose sampling and constancy intervals differ. pub use event_time::refuse_unmatched_time_varying_predictor_interval; +/// Refuse treating unstandardised `asymCINT` as p. 16 `asymCINTstd`. +pub use event_time::refuse_unstandardised_asymptotic_continuous_intercept_as_standardised_asymptotic_continuous_intercept; +/// Refuse treating unstandardised `asymDIFFUSION` as p. 16 `asymDIFFUSIONstd`. +pub use event_time::refuse_unstandardised_asymptotic_diffusion_as_standardised_asymptotic_diffusion; +/// Refuse treating unstandardised `asymTIPREDEFFECT` `-B / a` as p. 16 `asymTIPREDEFFECTstd`. +pub use event_time::refuse_unstandardised_asymptotic_time_independent_effect_as_standardised_asymptotic_time_independent_effect; +/// Refuse treating unstandardised `DIFFUSION` as p. 16 `DIFFUSIONstd`. +pub use event_time::refuse_unstandardised_continuous_diffusion_as_standardised_continuous_diffusion; +/// Refuse treating unstandardised `DRIFT` as p. 16 `DRIFTstd`. +pub use event_time::refuse_unstandardised_continuous_drift_as_standardised_continuous_drift; +/// Refuse treating unstandardised `TDPREDEFFECT` `M` as p. 16 `TDPREDEFFECTstd`. +pub use event_time::refuse_unstandardised_continuous_time_dependent_effect_as_standardised_continuous_time_dependent_effect; +/// Refuse treating unstandardised `TIPREDEFFECT` `B` as p. 16 `TIPREDEFFECTstd`. +pub use event_time::refuse_unstandardised_continuous_time_independent_effect_as_standardised_continuous_time_independent_effect; +/// Refuse treating unstandardised `discreteCINT` as p. 16 `discreteCINTstd`. +pub use event_time::refuse_unstandardised_discrete_continuous_intercept_as_standardised_discrete_continuous_intercept; +/// Refuse treating unstandardised `discreteDIFFUSION` as p. 16 `discreteDIFFUSIONstd`. +pub use event_time::refuse_unstandardised_discrete_diffusion_as_standardised_discrete_diffusion; +/// Refuse treating unstandardised `discreteDRIFT` as p. 16 `discreteDRIFTstd`. +pub use event_time::refuse_unstandardised_discrete_drift_as_standardised_discrete_drift; +/// Refuse treating unstandardised `T0MEANS` as p. 16 `T0MEANSstd`. +pub use event_time::refuse_unstandardised_initial_latent_mean_as_standardised_initial_latent_mean; +/// Refuse treating unstandardised `T0VAR` as p. 16 `T0VARstd`. +pub use event_time::refuse_unstandardised_initial_latent_variance_as_standardised_initial_latent_variance; +/// Refuse treating unstandardised `T0TDPREDEFFECT` `t0_m` as Table 3 / p. 16 `T0TDPREDEFFECTstd`. +pub use event_time::refuse_unstandardised_initial_time_dependent_effect_as_standardised_initial_time_dependent_effect; +/// Refuse treating unstandardised `T0TIPREDEFFECT` `t0_b` as Table 3 / p. 16 `T0TIPREDEFFECTstd`. +pub use event_time::refuse_unstandardised_initial_time_independent_effect_as_standardised_initial_time_independent_effect; +/// Refuse treating unstandardised `MANIFESTTRAITVAR` as p. 16 `MANIFESTTRAITVARstd`. +pub use event_time::refuse_unstandardised_manifest_trait_variance_as_standardised_manifest_trait_variance; +/// Refuse treating unstandardised `MANIFESTVAR` as p. 16 `MANIFESTVARstd`. +pub use event_time::refuse_unstandardised_manifest_variance_as_standardised_manifest_variance; +/// Refuse treating unstandardised `TIPREDVAR` as p. 16 `TIPREDVARstd`. +pub use event_time::refuse_unstandardised_time_independent_predictor_variance_as_standardised_time_independent_predictor_variance; +/// Refuse treating unstandardised `TRAITVAR` as p. 16 `TRAITVARstd`. +pub use event_time::refuse_unstandardised_trait_variance_as_standardised_trait_variance; +/// Refuse treating `μ_0 / √asymDIFFUSION` as p. 16 `T0MEANSstd`. +pub use event_time::refuse_within_subject_scaled_initial_latent_mean_as_standardised_initial_latent_mean; /// Indicator coordinate kind. pub use indicator::IndicatorKind; /// Pearson correlation on valid coordinates. diff --git a/crates/psychometric_core/tests/crate_contract.rs b/crates/psychometric_core/tests/crate_contract.rs index 4ae8137ae..c2a53d588 100644 --- a/crates/psychometric_core/tests/crate_contract.rs +++ b/crates/psychometric_core/tests/crate_contract.rs @@ -1,23 +1,7 @@ //! Integration contract for the `psychometric_core` package identity. -use psychometric_core::LagClock; - #[test] fn package_identity_is_stable() { let observed = std::hint::black_box(env!("CARGO_PKG_NAME")); assert_eq!(observed, "psychometric_core"); } - -#[test] -fn lag_clock_wire_names_are_stable() { - for (clock, name) in [ - (LagClock::EventTime, "event_time"), - (LagClock::SystemTime, "system_time"), - (LagClock::AssertionTime, "assertion_time"), - (LagClock::DocumentTime, "document_time"), - (LagClock::AvailabilityTime, "availability_time"), - (LagClock::KnowledgeCutoff, "knowledge_cutoff"), - ] { - assert_eq!(std::hint::black_box(clock).as_str(), name); - } -} diff --git a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs index 0522220c8..8a03c23d9 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -5,6 +5,7 @@ use psychometric_core::{ ClusteredEventScore, ClusteredScore, EventOccasion, IndicatorKind, LagClock, LaggedWithinResidual, PsychometricError, map_discrete_lag_across_event_intervals, ordinary_least_squares_slope, recover_asymptotic_continuous_intercept, + recover_asymptotic_time_independent_observed_variance, recover_asymptotic_time_independent_predictor_effect, recover_asymptotic_time_independent_predictor_variance, recover_cluster_mean_within_between_slopes, recover_discrete_constant_predictor_effect, @@ -26,14 +27,37 @@ use psychometric_core::{ recover_discrete_time_varying_predictor_effect, recover_event_series_mean_log_rate, recover_event_time_discrete_lag_and_log_rate, recover_initial_time_dependent_predictor_carry, recover_initial_time_dependent_predictor_effect, + recover_initial_time_independent_observed_variance, recover_initial_time_independent_predictor_carry, recover_initial_time_independent_predictor_effect, + recover_initial_time_independent_predictor_variance, recover_irregular_centered_residual_log_rate, recover_kish_weighted_slope, recover_level_change_continuous_intercept, recover_level_change_discrete_increment, recover_level_change_extra_process_contribution, recover_level_change_extra_process_contribution_after, recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, + recover_predetermined_initial_latent_variance, recover_predetermined_initial_observed_variance, + recover_predetermined_lagged_latent_covariance, + recover_predetermined_lagged_observed_covariance, + recover_predetermined_later_lagged_latent_covariance, + recover_predetermined_later_lagged_observed_covariance, + recover_predetermined_later_latent_variance, recover_predetermined_later_observed_variance, + recover_predetermined_later_start_later_latent_variance, + recover_predetermined_later_start_later_observed_variance, + recover_standardised_asymptotic_continuous_intercept, + recover_standardised_asymptotic_diffusion, + recover_standardised_asymptotic_time_independent_predictor_effect, + recover_standardised_continuous_diffusion, recover_standardised_continuous_drift, + recover_standardised_continuous_time_dependent_predictor_effect, + recover_standardised_continuous_time_independent_predictor_effect, + recover_standardised_discrete_continuous_intercept, recover_standardised_discrete_diffusion, + recover_standardised_discrete_drift, recover_standardised_initial_latent_mean, + recover_standardised_initial_latent_variance, + recover_standardised_initial_time_dependent_predictor_effect, + recover_standardised_initial_time_independent_predictor_effect, + recover_standardised_manifest_trait_variance, recover_standardised_manifest_variance, + recover_standardised_time_independent_predictor_variance, recover_standardised_trait_variance, recover_stationary_initial_latent_mean, recover_stationary_initial_latent_variance, recover_stationary_initial_observed_mean, recover_stationary_initial_observed_variance, recover_stationary_lagged_latent_covariance, recover_stationary_lagged_observed_covariance, @@ -48,10 +72,16 @@ use psychometric_core::{ refuse_asymptotic_continuous_intercept_as_discrete_increment, refuse_asymptotic_continuous_intercept_as_initial_latent_mean, refuse_asymptotic_continuous_intercept_observed_mean_as_stationary_initial_observed_mean, + refuse_asymptotic_standardised_continuous_intercept_as_standardised_discrete_continuous_intercept, refuse_asymptotic_time_independent_effect_as_coefficient, refuse_asymptotic_time_independent_effect_as_continuous_intercept, refuse_asymptotic_time_independent_effect_as_discrete_effect, refuse_asymptotic_time_independent_effect_as_time_dependent_impulse, + refuse_asymptotic_time_independent_observed_variance_as_asymptotic_time_independent_variance, + refuse_asymptotic_time_independent_observed_variance_as_initial_time_independent_observed_variance, + refuse_asymptotic_time_independent_observed_variance_as_measurement_error, + refuse_asymptotic_time_independent_observed_variance_as_stationary_observed_variance, + refuse_asymptotic_time_independent_predictor_variance_as_standardised_time_independent_predictor_variance, refuse_asymptotic_time_independent_variance_as_asymptotic_effect, refuse_asymptotic_time_independent_variance_as_stationary_within_subject, refuse_asymptotic_time_independent_variance_as_trait_variance, @@ -97,6 +127,16 @@ use psychometric_core::{ refuse_initial_time_independent_effect_as_process_increment, refuse_initial_time_independent_effect_as_time_dependent_impulse, refuse_initial_time_independent_observed_mean_as_initial_time_dependent_observed_mean, + refuse_initial_time_independent_observed_variance_as_asymptotic_time_independent_observed_variance, + refuse_initial_time_independent_observed_variance_as_initial_observed_variance, + refuse_initial_time_independent_observed_variance_as_initial_time_independent_variance, + refuse_initial_time_independent_observed_variance_as_measurement_error, + refuse_initial_time_independent_variance_as_asymptotic_time_independent_variance, + refuse_initial_time_independent_variance_as_initial_latent_variance, + refuse_initial_time_independent_variance_as_standardised_initial_latent_variance, + refuse_initial_time_independent_variance_as_standardised_initial_time_independent_effect, + refuse_initial_time_independent_variance_as_standardised_trait_variance, + refuse_initial_time_independent_variance_as_trait_variance, refuse_latent_lagged_covariance_as_observed_covariance, refuse_latent_mean_as_observed_mean, refuse_latent_variance_as_observed_variance, refuse_level_change_extra_process_as_impulse, refuse_level_change_extra_process_as_increment, refuse_level_change_extra_process_as_intercept, @@ -107,10 +147,71 @@ use psychometric_core::{ refuse_manifest_means_as_observed_mean, refuse_manifest_trait_variance_as_measurement_error, refuse_measurement_error_as_lagged_observed_covariance, refuse_measurement_error_as_observed_variance, + refuse_measurement_error_as_predetermined_initial_observed_variance, + refuse_measurement_error_as_predetermined_lagged_observed_covariance, + refuse_measurement_error_as_predetermined_later_lagged_observed_covariance, + refuse_measurement_error_as_predetermined_later_observed_variance, + refuse_measurement_error_as_predetermined_later_start_later_observed_variance, + refuse_measurement_error_as_standardised_manifest_trait_variance, refuse_measurement_error_as_stationary_lagged_observed_covariance, refuse_measurement_error_as_stationary_later_observed_variance, + refuse_observed_variance_as_standardised_manifest_variance, refuse_pooled_discrete_lag_across_unequal_intervals, + refuse_predetermined_initial_latent_variance_as_initial_latent_variance, + refuse_predetermined_initial_latent_variance_as_lagged_latent_covariance, + refuse_predetermined_initial_latent_variance_as_later_latent_variance, + refuse_predetermined_initial_latent_variance_as_observed_variance, + refuse_predetermined_initial_latent_variance_as_stationary_initial_latent_variance, + refuse_predetermined_lagged_latent_covariance_as_decayed_total, + refuse_predetermined_lagged_latent_covariance_as_initial_latent_variance, + refuse_predetermined_lagged_latent_covariance_as_later_latent_variance, + refuse_predetermined_lagged_latent_covariance_as_observed_covariance, + refuse_predetermined_lagged_latent_covariance_as_stationary_lagged_covariance, + refuse_predetermined_lagged_observed_covariance_as_predetermined_later_lagged_observed_covariance, + refuse_predetermined_later_lagged_latent_covariance_as_decayed_later_total, + refuse_predetermined_later_lagged_latent_covariance_as_later_latent_variance, + refuse_predetermined_later_lagged_latent_covariance_as_observed_covariance, + refuse_predetermined_later_lagged_latent_covariance_as_predetermined_lagged_covariance, + refuse_predetermined_later_lagged_latent_covariance_as_stationary_lagged_covariance, + refuse_predetermined_later_lagged_observed_covariance_as_predetermined_later_start_later_observed_variance, + refuse_predetermined_later_latent_variance_as_discrete_variance, + refuse_predetermined_later_latent_variance_as_initial_latent_variance, + refuse_predetermined_later_latent_variance_as_observed_variance, + refuse_predetermined_later_latent_variance_as_stationary_later_latent_variance, + refuse_predetermined_later_observed_variance_as_predetermined_initial_observed_variance, + refuse_predetermined_later_observed_variance_as_predetermined_lagged_observed_covariance, + refuse_predetermined_later_observed_variance_as_predetermined_later_lagged_observed_covariance, + refuse_predetermined_later_observed_variance_as_predetermined_later_start_later_observed_variance, + refuse_predetermined_later_start_later_latent_variance_as_decayed_later_total, + refuse_predetermined_later_start_later_latent_variance_as_lag_interval_later_latent_variance, + refuse_predetermined_later_start_later_latent_variance_as_later_lagged_covariance, + refuse_predetermined_later_start_later_latent_variance_as_later_latent_variance, + refuse_predetermined_later_start_later_latent_variance_as_observed_variance, + refuse_predetermined_later_start_later_latent_variance_as_stationary_later_latent_variance, refuse_process_noise_as_unconditional_variance, + refuse_standardised_asymptotic_time_independent_effect_as_standardised_continuous_time_independent_effect, + refuse_standardised_asymptotic_time_independent_effect_as_standardised_initial_time_independent_effect, + refuse_standardised_continuous_diffusion_as_standardised_asymptotic_diffusion, + refuse_standardised_continuous_diffusion_as_standardised_discrete_diffusion, + refuse_standardised_continuous_intercept_as_standardised_asymptotic_continuous_intercept, + refuse_standardised_continuous_intercept_as_standardised_discrete_continuous_intercept, + refuse_standardised_continuous_time_dependent_effect_as_standardised_initial_time_dependent_effect, + refuse_standardised_continuous_time_independent_effect_as_standardised_continuous_time_dependent_effect, + refuse_standardised_continuous_time_independent_effect_as_standardised_initial_time_independent_effect, + refuse_standardised_discrete_continuous_intercept_as_standardised_asymptotic_continuous_intercept, + refuse_standardised_discrete_diffusion_as_standardised_continuous_diffusion, + refuse_standardised_discrete_drift_as_standardised_continuous_drift, + refuse_standardised_discrete_time_dependent_effect_as_standardised_continuous_time_dependent_effect, + refuse_standardised_discrete_time_independent_effect_as_standardised_asymptotic_time_independent_effect, + refuse_standardised_discrete_time_independent_effect_as_standardised_continuous_time_independent_effect, + refuse_standardised_initial_latent_variance_as_standardised_initial_latent_mean, + refuse_standardised_initial_latent_variance_as_standardised_trait_variance, + refuse_standardised_initial_time_dependent_effect_as_standardised_initial_latent_variance, + refuse_standardised_initial_time_independent_effect_as_standardised_initial_time_dependent_effect, + refuse_standardised_manifest_trait_variance_as_standardised_manifest_variance, + refuse_standardised_manifest_variance_as_standardised_time_independent_predictor_variance, + refuse_standardised_time_independent_predictor_variance_as_standardised_asymptotic_diffusion, + refuse_standardised_trait_variance_as_standardised_manifest_trait_variance, refuse_stationary_initial_latent_mean_as_asymptotic_continuous_intercept, refuse_stationary_initial_latent_mean_as_asymptotic_time_independent_effect, refuse_stationary_initial_latent_mean_as_discrete_mean, @@ -124,15 +225,20 @@ use psychometric_core::{ refuse_stationary_initial_latent_variance_as_trait_variance, refuse_stationary_initial_observed_mean_as_manifest_means, refuse_stationary_initial_observed_variance_as_measurement_error, + refuse_stationary_initial_observed_variance_as_predetermined_initial_observed_variance, refuse_stationary_initial_observed_variance_as_stationary_lagged_observed_covariance, refuse_stationary_lagged_latent_covariance_as_decayed_stationary_variance, refuse_stationary_lagged_latent_covariance_as_observed_covariance, refuse_stationary_lagged_latent_covariance_as_stationary_initial_latent_variance, + refuse_stationary_lagged_observed_covariance_as_predetermined_lagged_observed_covariance, + refuse_stationary_lagged_observed_covariance_as_predetermined_later_lagged_observed_covariance, refuse_stationary_lagged_observed_covariance_as_stationary_later_observed_variance, refuse_stationary_later_latent_variance_as_discrete_variance, refuse_stationary_later_latent_variance_as_lagged_covariance, refuse_stationary_later_latent_variance_as_observed_variance, refuse_stationary_later_latent_variance_as_process_noise, + refuse_stationary_later_observed_variance_as_predetermined_later_observed_variance, + refuse_stationary_later_observed_variance_as_predetermined_later_start_later_observed_variance, refuse_stationary_within_subject_observed_variance_as_stationary_initial_observed_variance, refuse_time_dependent_impulse_as_continuous_intercept, refuse_time_dependent_impulse_as_time_independent_effect, @@ -147,9 +253,38 @@ use psychometric_core::{ refuse_time_independent_effect_as_time_varying_discrete_effect, refuse_time_independent_observed_mean_as_initial_time_dependent_observed_mean, refuse_time_independent_observed_mean_as_initial_time_independent_observed_mean, + refuse_trait_contaminated_asymptotic_time_independent_effect_as_standardised_asymptotic_time_independent_effect, + refuse_trait_contaminated_continuous_diffusion_as_standardised_continuous_diffusion, + refuse_trait_contaminated_continuous_drift_as_standardised_continuous_drift, + refuse_trait_contaminated_continuous_time_dependent_effect_as_standardised_continuous_time_dependent_effect, + refuse_trait_contaminated_continuous_time_independent_effect_as_standardised_continuous_time_independent_effect, + refuse_trait_contaminated_initial_time_dependent_effect_as_standardised_initial_time_dependent_effect, + refuse_trait_contaminated_initial_time_independent_effect_as_standardised_initial_time_independent_effect, + refuse_trait_contaminated_process_noise_as_standardised_discrete_diffusion, + refuse_trait_plus_state_autocorrelation_as_standardised_discrete_drift, refuse_trait_plus_state_lagged_covariance_as_stationary_lagged_latent_covariance, - refuse_trait_variance_as_process_noise, refuse_trait_variance_as_stationary_within_subject, + refuse_trait_variance_as_process_noise, refuse_trait_variance_as_standardisation_variance, + refuse_trait_variance_as_stationary_within_subject, refuse_unmatched_time_varying_predictor_interval, + refuse_unstandardised_asymptotic_continuous_intercept_as_standardised_asymptotic_continuous_intercept, + refuse_unstandardised_asymptotic_diffusion_as_standardised_asymptotic_diffusion, + refuse_unstandardised_asymptotic_time_independent_effect_as_standardised_asymptotic_time_independent_effect, + refuse_unstandardised_continuous_diffusion_as_standardised_continuous_diffusion, + refuse_unstandardised_continuous_drift_as_standardised_continuous_drift, + refuse_unstandardised_continuous_time_dependent_effect_as_standardised_continuous_time_dependent_effect, + refuse_unstandardised_continuous_time_independent_effect_as_standardised_continuous_time_independent_effect, + refuse_unstandardised_discrete_continuous_intercept_as_standardised_discrete_continuous_intercept, + refuse_unstandardised_discrete_diffusion_as_standardised_discrete_diffusion, + refuse_unstandardised_discrete_drift_as_standardised_discrete_drift, + refuse_unstandardised_initial_latent_mean_as_standardised_initial_latent_mean, + refuse_unstandardised_initial_latent_variance_as_standardised_initial_latent_variance, + refuse_unstandardised_initial_time_dependent_effect_as_standardised_initial_time_dependent_effect, + refuse_unstandardised_initial_time_independent_effect_as_standardised_initial_time_independent_effect, + refuse_unstandardised_manifest_trait_variance_as_standardised_manifest_trait_variance, + refuse_unstandardised_manifest_variance_as_standardised_manifest_variance, + refuse_unstandardised_time_independent_predictor_variance_as_standardised_time_independent_predictor_variance, + refuse_unstandardised_trait_variance_as_standardised_trait_variance, + refuse_within_subject_scaled_initial_latent_mean_as_standardised_initial_latent_mean, }; fn rmse(truth: &[f64], recovered: &[f64]) -> f64 { @@ -650,6 +785,76 @@ fn within_residual_event_time_log_rate_beats_pooled_levels() { assert!(within_error < 0.25, "CWC lag RMSE {within_error} too large"); } +#[test] +fn within_residual_log_rate_rejects_nonpositive_centered_lags() { + let rows = [ + ClusteredEventScore { + cluster_key: 1, + event_time: 0.0, + score: 1.0, + }, + ClusteredEventScore { + cluster_key: 1, + event_time: 1.0, + score: 2.0, + }, + ClusteredEventScore { + cluster_key: 1, + event_time: 2.0, + score: 0.0, + }, + ClusteredEventScore { + cluster_key: 2, + event_time: 0.0, + score: 5.0, + }, + ClusteredEventScore { + cluster_key: 2, + event_time: 1.0, + score: 6.0, + }, + ]; + assert_eq!( + recover_within_residual_event_time_log_rate(&rows, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); +} + +#[test] +fn within_residual_log_rate_rejects_nonfinite_centered_lag_ratios() { + let rows = [ + ClusteredEventScore { + cluster_key: 1, + event_time: 0.0, + score: 1e-320, + }, + ClusteredEventScore { + cluster_key: 1, + event_time: 1.0, + score: 1.0, + }, + ClusteredEventScore { + cluster_key: 1, + event_time: 2.0, + score: -1.0, + }, + ClusteredEventScore { + cluster_key: 2, + event_time: 0.0, + score: 2.0, + }, + ClusteredEventScore { + cluster_key: 2, + event_time: 1.0, + score: 3.0, + }, + ]; + assert_eq!( + recover_within_residual_event_time_log_rate(&rows, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); +} + #[test] fn irregular_centered_residuals_recover_known_drift_better_than_cwc_of_raw_ar() { let true_drift = -0.35_f64; @@ -772,6 +977,12 @@ fn discrete_latent_variance_recovers_driver_equations_three_and_four() { recover_discrete_latent_variance(2.0, 0.0, 1e308, 2.0, LagClock::EventTime), Err(PsychometricError::InvalidNumericInput) ); + // A finite positive drift interval can overflow exp without making the + // interval itself non-finite; the implementation must still fail closed. + assert_eq!( + recover_discrete_latent_variance(1.0, 0.0, 1_000.0, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); } #[test] @@ -1398,23 +1609,23 @@ fn time_dependent_impulse_refuses_overflow_and_non_event_clocks() { ); assert_eq!( recover_discrete_latent_mean_with_impulse( - 1.0, - -0.5, - 0.3, 1e308, - 2.0, - 2.0, + 0.0, + 0.0, + 1e308, + 1.0, + 1.0, LagClock::EventTime ), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( recover_discrete_latent_mean_with_impulse( - 1e308, - 0.0, - 0.0, - 1e308, 1.0, + -0.5, + 0.3, + 1e308, + 2.0, 1.0, LagClock::EventTime ), @@ -1748,16 +1959,6 @@ fn time_independent_predictor_refuses_overflow_and_non_event_clocks() { ), Err(PsychometricError::InvalidNumericInput) ); - assert_eq!( - recover_discrete_time_independent_predictor_effect( - 0.4, - 3.0, - f64::NAN, - 2.0, - LagClock::EventTime - ), - Err(PsychometricError::InvalidNumericInput) - ); assert_eq!( recover_discrete_latent_mean_with_time_independent_predictor( 1.0, @@ -2896,6 +3097,17 @@ fn time_dependent_impulse_carry_refuses_overflow_and_non_event_clocks() { ), Err(PsychometricError::InvalidNumericInput) ); + assert_eq!( + recover_time_dependent_predictor_impulse_carry( + 1.0, + 1.0, + 1_000.0, + 2.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); } #[test] @@ -3832,16 +4044,16 @@ fn extra_process_contribution_refuses_nonnegative_extra_drift_clock_and_overflow ), Ok(0.0) ); - let overflow_fallback = recover_level_change_extra_process_contribution( + let finite_exp_m1_overflow = recover_level_change_extra_process_contribution( 0.4, 3.0, - -0.8, - -0.000_001, - 900.0, + -1_000.0, + -0.5, + 1.0, LagClock::EventTime, ) - .expect("expm1-overflow-fallback"); - assert!(overflow_fallback.is_finite()); + .expect("finite exp_m1 overflow fallback"); + assert!(finite_exp_m1_overflow.is_finite()); } #[test] @@ -4483,9 +4695,9 @@ fn asymptotic_time_independent_variance_refuses_unstable_drift_and_non_event_clo ); assert_eq!( recover_asymptotic_time_independent_predictor_variance( + f64::MAX, 1.0, - 1.0, - -1e-308, + -1.0, LagClock::EventTime ), Err(PsychometricError::InvalidNumericInput) @@ -5848,3 +6060,4320 @@ fn stationary_later_observed_variance_refuses_unstable_drift_and_non_event_clock Ok(0.6) ); } + +#[test] +#[allow(clippy::too_many_lines)] +fn predetermined_later_latent_variance_recovers_driver_section_four_point_three() { + let printed_effect = -0.225_f64; + let printed_asym = -1.673_f64; + let log_rate = -printed_effect / printed_asym; + let trait_variance = 1.0_f64; + let initial_latent_variance = 2.0_f64; + let diffusion = 0.4_f64; + let predictor_variance = 1.0_f64; + let event_delta = 1.0_f64; + let recovered = recover_predetermined_later_latent_variance( + trait_variance, + initial_latent_variance, + diffusion, + printed_effect, + predictor_variance, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("predetermined later T0VAR"); + let evolved_state = recover_discrete_latent_variance( + initial_latent_variance, + diffusion, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("e^{2aΔt}p_0+Q_Δt"); + let added = recover_asymptotic_time_independent_predictor_variance( + printed_effect, + predictor_variance, + log_rate, + LagClock::EventTime, + ) + .expect("addedTIPREDVAR"); + let expected = trait_variance + evolved_state + added; + let error = rmse(&[expected], &[recovered]); + assert!( + error < 1e-12, + "Driver §4.3 later-occasion predetermined T0VAR RMSE {error}: got {recovered}" + ); + let stationary_later = recover_stationary_later_latent_variance( + trait_variance, + diffusion, + printed_effect, + predictor_variance, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("stationary later T0VAR"); + let contemporaneous = recover_stationary_initial_latent_variance( + trait_variance, + diffusion, + printed_effect, + predictor_variance, + log_rate, + LagClock::EventTime, + ) + .expect("stationary T0VAR"); + let first_occasion_total = trait_variance + initial_latent_variance + added; + let free_discrete = recover_discrete_latent_variance( + first_occasion_total, + diffusion, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("e^{2aΔt}(trait+p_0+added)+Q_Δt"); + assert!(rmse(&[recovered], &[stationary_later]) > error); + assert!(rmse(&[recovered], &[free_discrete]) > error); + assert!(rmse(&[recovered], &[initial_latent_variance]) > error); + let state = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + let from_stationary_start = recover_predetermined_later_latent_variance( + trait_variance, + state, + diffusion, + printed_effect, + predictor_variance, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("p_0=−q/(2a)"); + assert!(rmse(&[from_stationary_start], &[stationary_later]) < 1e-12); + assert_eq!( + recover_predetermined_later_latent_variance( + 0.0, + 0.0, + 0.0, + 0.0, + predictor_variance, + log_rate, + event_delta, + LagClock::EventTime, + ), + Ok(0.0) + ); + assert_eq!( + recover_predetermined_later_latent_variance( + trait_variance, + 0.0, + 0.0, + 0.0, + predictor_variance, + 0.0, + event_delta, + LagClock::EventTime, + ), + Ok(trait_variance) + ); + let far = recover_predetermined_later_latent_variance( + trait_variance, + initial_latent_variance, + diffusion, + printed_effect, + predictor_variance, + log_rate, + 1e8, + LagClock::EventTime, + ) + .expect("Δt→∞"); + assert!(rmse(&[far], &[contemporaneous]) < 1e-12); + assert_eq!( + refuse_predetermined_later_latent_variance_as_stationary_later_latent_variance( + recovered, + stationary_later + ), + Err(PsychometricError::PredeterminedLaterLatentVarianceIsNotStationaryLaterLatentVariance) + ); + assert_eq!( + refuse_predetermined_later_latent_variance_as_discrete_variance(recovered, free_discrete), + Err(PsychometricError::PredeterminedLaterLatentVarianceIsNotDiscreteVariance) + ); + assert_eq!( + refuse_predetermined_later_latent_variance_as_initial_latent_variance( + recovered, + initial_latent_variance + ), + Err(PsychometricError::PredeterminedLaterLatentVarianceIsNotInitialLatentVariance) + ); +} + +#[test] +fn predetermined_later_latent_variance_refuses_non_event_clocks_and_keeps_growing_processes() { + assert_eq!( + recover_predetermined_later_latent_variance( + 1.0, + 2.0, + 0.4, + -0.225, + 1.0, + -0.13, + 1.0, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_predetermined_later_latent_variance( + 1.0, + 2.0, + 0.4, + -0.225, + 1.0, + -0.13, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + let growing = recover_predetermined_later_latent_variance( + 0.0, + 2.0, + 0.4, + 0.0, + 1.0, + 0.0, + 1.0, + LagClock::EventTime, + ) + .expect("Brownian a=0"); + assert!((growing - 2.4).abs() < 1e-12); + assert_eq!( + recover_predetermined_later_latent_variance( + 0.0, + 0.0, + 0.0, + -0.225, + 1.0, + 0.5, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) + ); + assert_eq!( + recover_predetermined_later_latent_variance( + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + LagClock::EventTime + ), + Ok(0.0) + ); +} + +#[test] +#[allow(clippy::too_many_lines)] +fn predetermined_later_observed_variance_recovers_driver_equation_five_of_section_four_point_three() +{ + let printed_effect = -0.225_f64; + let printed_asym = -1.673_f64; + let log_rate = -printed_effect / printed_asym; + let trait_variance = 1.0_f64; + let initial_latent_variance = 2.0_f64; + let diffusion = 0.4_f64; + let loading = 2.0_f64; + let measurement_error = 0.5_f64; + let manifest_trait = 0.1_f64; + let event_delta = 1.0_f64; + let recovered = recover_predetermined_later_observed_variance( + loading, + trait_variance, + initial_latent_variance, + diffusion, + printed_effect, + 1.0, + log_rate, + event_delta, + measurement_error, + manifest_trait, + LagClock::EventTime, + ) + .expect("eq5-later-predetermined-T0VAR"); + let latent = recover_predetermined_later_latent_variance( + trait_variance, + initial_latent_variance, + diffusion, + printed_effect, + 1.0, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("predetermined later T0VAR"); + let expected = recover_manifest_trait_plus_state_observed_variance( + loading, + latent, + measurement_error, + manifest_trait, + ) + .expect("λ²p+θ+ψ"); + let error = rmse(&[expected], &[recovered]); + assert!( + error < 1e-12, + "Driver §4.3 Eq. 5 of later-occasion predetermined T0VAR RMSE {error}: got {recovered}" + ); + let stationary_later = recover_stationary_later_observed_variance( + loading, + trait_variance, + diffusion, + printed_effect, + 1.0, + log_rate, + event_delta, + measurement_error, + manifest_trait, + LagClock::EventTime, + ) + .expect("eq5-later-stationary-T0VAR"); + assert!(rmse(&[recovered], &[stationary_later]) > error); + assert!( + rmse(&[recovered], &[measurement_error]) > error, + "MANIFESTVAR is not predetermined later Var(y)" + ); + assert!(rmse(&[recovered], &[latent]) > error); + assert_eq!( + recover_predetermined_later_observed_variance( + 0.0, + trait_variance, + initial_latent_variance, + diffusion, + printed_effect, + 1.0, + log_rate, + event_delta, + measurement_error, + manifest_trait, + LagClock::EventTime, + ), + Ok(measurement_error + manifest_trait) + ); + assert_eq!( + refuse_predetermined_later_latent_variance_as_observed_variance(latent, recovered), + Err(PsychometricError::PredeterminedLaterLatentVarianceIsNotObservedVariance) + ); + assert_eq!( + refuse_measurement_error_as_predetermined_later_observed_variance( + measurement_error, + recovered + ), + Err(PsychometricError::MeasurementErrorIsNotPredeterminedLaterObservedVariance) + ); + assert_eq!( + refuse_stationary_later_observed_variance_as_predetermined_later_observed_variance( + stationary_later, + recovered + ), + Err( + PsychometricError::StationaryLaterObservedVarianceIsNotPredeterminedLaterObservedVariance + ) + ); +} + +#[test] +fn predetermined_later_observed_variance_refuses_non_event_clocks_and_keeps_growing_processes() { + assert_eq!( + recover_predetermined_later_observed_variance( + 2.0, + 1.0, + 2.0, + 0.4, + -0.225, + 1.0, + -0.13, + 1.0, + 0.5, + 0.1, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_predetermined_later_observed_variance( + 2.0, + 1.0, + 2.0, + 0.4, + -0.225, + 1.0, + -0.13, + 0.0, + 0.5, + 0.1, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + let growing = recover_predetermined_later_observed_variance( + 1.0, + 0.0, + 2.0, + 0.4, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + LagClock::EventTime, + ) + .expect("Brownian a=0"); + assert!((growing - 2.4).abs() < 1e-12); + assert_eq!( + recover_predetermined_later_observed_variance( + 2.0, + 0.0, + 0.0, + 0.0, + -0.225, + 1.0, + 0.5, + 1.0, + 0.0, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) + ); + assert_eq!( + recover_predetermined_later_observed_variance( + 2.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.5, + 0.1, + LagClock::EventTime + ), + Ok(0.6) + ); +} + +#[test] +#[allow(clippy::too_many_lines)] +fn predetermined_lagged_latent_covariance_recovers_driver_section_four_point_three() { + let printed_effect = -0.225_f64; + let printed_asym = -1.673_f64; + let log_rate = -printed_effect / printed_asym; + let trait_variance = 1.0_f64; + let initial_latent_variance = 2.0_f64; + let diffusion = 0.4_f64; + let predictor_variance = 1.0_f64; + let event_delta = 1.0_f64; + let recovered = recover_predetermined_lagged_latent_covariance( + trait_variance, + initial_latent_variance, + printed_effect, + predictor_variance, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("predetermined lagged T0VAR"); + let lagged_state = recover_discrete_lagged_latent_covariance( + initial_latent_variance, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("e^{aΔt}p_0"); + let added = recover_asymptotic_time_independent_predictor_variance( + printed_effect, + predictor_variance, + log_rate, + LagClock::EventTime, + ) + .expect("addedTIPREDVAR"); + let expected = trait_variance + lagged_state + added; + let error = rmse(&[expected], &[recovered]); + assert!( + error < 1e-12, + "Driver §4.3 lagged predetermined T0VAR RMSE {error}: got {recovered}" + ); + let stationary_lagged = recover_stationary_lagged_latent_covariance( + trait_variance, + diffusion, + printed_effect, + predictor_variance, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("stationary lagged T0VAR"); + let later = recover_predetermined_later_latent_variance( + trait_variance, + initial_latent_variance, + diffusion, + printed_effect, + predictor_variance, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("predetermined later T0VAR"); + let first_occasion_total = trait_variance + initial_latent_variance + added; + let decayed_total = recover_discrete_lagged_latent_covariance( + first_occasion_total, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("e^{aΔt}(trait+p_0+added)"); + assert!(rmse(&[recovered], &[stationary_lagged]) > error); + assert!(rmse(&[recovered], &[later]) > error); + assert!(rmse(&[recovered], &[decayed_total]) > error); + assert!(rmse(&[recovered], &[initial_latent_variance]) > error); + let state = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + let from_stationary_start = recover_predetermined_lagged_latent_covariance( + trait_variance, + state, + printed_effect, + predictor_variance, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("p_0=−q/(2a)"); + assert!(rmse(&[from_stationary_start], &[stationary_lagged]) < 1e-12); + assert_eq!( + recover_predetermined_lagged_latent_covariance( + 0.0, + 0.0, + 0.0, + predictor_variance, + 0.0, + event_delta, + LagClock::EventTime, + ), + Ok(0.0) + ); + assert_eq!( + recover_predetermined_lagged_latent_covariance( + trait_variance, + 0.0, + 0.0, + predictor_variance, + 0.0, + event_delta, + LagClock::EventTime, + ), + Ok(trait_variance) + ); + let far = recover_predetermined_lagged_latent_covariance( + trait_variance, + initial_latent_variance, + printed_effect, + predictor_variance, + log_rate, + 1e8, + LagClock::EventTime, + ) + .expect("Δt→∞"); + assert!(rmse(&[far], &[trait_variance + added]) < 1e-12); + let near = recover_predetermined_lagged_latent_covariance( + trait_variance, + initial_latent_variance, + printed_effect, + predictor_variance, + log_rate, + 1e-12, + LagClock::EventTime, + ) + .expect("Δt→0+"); + assert!(rmse(&[near], &[first_occasion_total]) < 1e-9); + assert_eq!( + refuse_predetermined_lagged_latent_covariance_as_stationary_lagged_covariance( + recovered, + stationary_lagged + ), + Err(PsychometricError::PredeterminedLaggedLatentCovarianceIsNotStationaryLaggedCovariance) + ); + assert_eq!( + refuse_predetermined_lagged_latent_covariance_as_later_latent_variance(recovered, later), + Err(PsychometricError::PredeterminedLaggedLatentCovarianceIsNotLaterLatentVariance) + ); + assert_eq!( + refuse_predetermined_lagged_latent_covariance_as_decayed_total(recovered, decayed_total), + Err(PsychometricError::PredeterminedLaggedLatentCovarianceIsNotDecayedTotal) + ); + assert_eq!( + refuse_predetermined_lagged_latent_covariance_as_initial_latent_variance( + recovered, + initial_latent_variance + ), + Err(PsychometricError::PredeterminedLaggedLatentCovarianceIsNotInitialLatentVariance) + ); +} + +#[test] +fn predetermined_lagged_latent_covariance_refuses_non_event_clocks_and_keeps_growing_processes() { + assert_eq!( + recover_predetermined_lagged_latent_covariance( + 1.0, + 2.0, + -0.225, + 1.0, + -0.13, + 1.0, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_predetermined_lagged_latent_covariance( + 1.0, + 2.0, + -0.225, + 1.0, + -0.13, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + let growing = recover_predetermined_lagged_latent_covariance( + 0.0, + 2.0, + 0.0, + 0.0, + 0.5, + 1.0, + LagClock::EventTime, + ) + .expect("growing carry"); + assert!(growing > 2.0); + let brownian = recover_predetermined_lagged_latent_covariance( + 0.0, + 2.0, + 0.0, + 0.0, + 0.0, + 1.0, + LagClock::EventTime, + ) + .expect("Brownian a=0"); + assert!((brownian - 2.0).abs() < 1e-12); + assert_eq!( + recover_predetermined_lagged_latent_covariance( + 0.0, + 0.0, + -0.225, + 1.0, + 0.5, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) + ); + assert_eq!( + recover_predetermined_lagged_latent_covariance( + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + LagClock::EventTime + ), + Ok(0.0) + ); +} + +#[test] +#[allow(clippy::too_many_lines)] +fn predetermined_lagged_observed_covariance_recovers_driver_equation_five_of_section_four_point_three() + { + let printed_effect = -0.225_f64; + let printed_asym = -1.673_f64; + let log_rate = -printed_effect / printed_asym; + let trait_variance = 1.0_f64; + let initial_latent_variance = 2.0_f64; + let diffusion = 0.4_f64; + let loading = 2.0_f64; + let measurement_error = 0.5_f64; + let manifest_trait = 0.1_f64; + let event_delta = 1.0_f64; + let recovered = recover_predetermined_lagged_observed_covariance( + loading, + trait_variance, + initial_latent_variance, + printed_effect, + 1.0, + log_rate, + event_delta, + manifest_trait, + LagClock::EventTime, + ) + .expect("eq5-lagged-predetermined-T0VAR"); + let latent = recover_predetermined_lagged_latent_covariance( + trait_variance, + initial_latent_variance, + printed_effect, + 1.0, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("predetermined lagged T0VAR"); + let expected = recover_manifest_lagged_observed_covariance(loading, latent, manifest_trait) + .expect("λ²c+ψ"); + let error = rmse(&[expected], &[recovered]); + assert!( + error < 1e-12, + "Driver §4.3 Eq. 5 of lagged predetermined T0VAR RMSE {error}: got {recovered}" + ); + let stationary_lagged = recover_stationary_lagged_observed_covariance( + loading, + trait_variance, + diffusion, + printed_effect, + 1.0, + log_rate, + event_delta, + manifest_trait, + LagClock::EventTime, + ) + .expect("eq5-lagged-stationary-T0VAR"); + let later = recover_predetermined_later_observed_variance( + loading, + trait_variance, + initial_latent_variance, + diffusion, + printed_effect, + 1.0, + log_rate, + event_delta, + measurement_error, + manifest_trait, + LagClock::EventTime, + ) + .expect("eq5-later-predetermined-T0VAR"); + assert!(rmse(&[recovered], &[stationary_lagged]) > error); + assert!( + rmse(&[recovered], &[measurement_error]) > error, + "MANIFESTVAR is not predetermined lagged cov(y)" + ); + assert!(rmse(&[recovered], &[latent]) > error); + assert!(rmse(&[recovered], &[later]) > error); + assert_eq!( + recover_predetermined_lagged_observed_covariance( + 0.0, + trait_variance, + initial_latent_variance, + printed_effect, + 1.0, + log_rate, + event_delta, + manifest_trait, + LagClock::EventTime, + ), + Ok(manifest_trait) + ); + assert_eq!( + refuse_predetermined_lagged_latent_covariance_as_observed_covariance(latent, recovered), + Err(PsychometricError::PredeterminedLaggedLatentCovarianceIsNotObservedCovariance) + ); + assert_eq!( + refuse_measurement_error_as_predetermined_lagged_observed_covariance( + measurement_error, + recovered + ), + Err(PsychometricError::MeasurementErrorIsNotPredeterminedLaggedObservedCovariance) + ); + assert_eq!( + refuse_predetermined_later_observed_variance_as_predetermined_lagged_observed_covariance( + later, + recovered + ), + Err( + PsychometricError::PredeterminedLaterObservedVarianceIsNotPredeterminedLaggedObservedCovariance + ) + ); + assert_eq!( + refuse_stationary_lagged_observed_covariance_as_predetermined_lagged_observed_covariance( + stationary_lagged, + recovered + ), + Err( + PsychometricError::StationaryLaggedObservedCovarianceIsNotPredeterminedLaggedObservedCovariance + ) + ); +} + +#[test] +fn predetermined_lagged_observed_covariance_refuses_non_event_clocks_and_keeps_growing_processes() { + assert_eq!( + recover_predetermined_lagged_observed_covariance( + 2.0, + 1.0, + 2.0, + -0.225, + 1.0, + -0.13, + 1.0, + 0.1, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_predetermined_lagged_observed_covariance( + 2.0, + 1.0, + 2.0, + -0.225, + 1.0, + -0.13, + 0.0, + 0.1, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + let growing = recover_predetermined_lagged_observed_covariance( + 1.0, + 0.0, + 2.0, + 0.0, + 1.0, + 0.5, + 1.0, + 0.0, + LagClock::EventTime, + ) + .expect("growing carry"); + assert!(growing > 2.0); + let brownian = recover_predetermined_lagged_observed_covariance( + 1.0, + 0.0, + 2.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + LagClock::EventTime, + ) + .expect("Brownian a=0"); + assert!((brownian - 2.0).abs() < 1e-12); + assert_eq!( + recover_predetermined_lagged_observed_covariance( + 2.0, + 0.0, + 0.0, + -0.225, + 1.0, + 0.5, + 1.0, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) + ); + assert_eq!( + recover_predetermined_lagged_observed_covariance( + 2.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.1, + LagClock::EventTime + ), + Ok(0.1) + ); +} + +#[test] +#[allow(clippy::too_many_lines)] +fn predetermined_initial_latent_variance_recovers_driver_section_four_point_three() { + let printed_effect = -0.225_f64; + let printed_asym = -1.673_f64; + let log_rate = -printed_effect / printed_asym; + let trait_variance = 1.0_f64; + let initial_latent_variance = 2.0_f64; + let diffusion = 0.4_f64; + let predictor_variance = 1.0_f64; + let event_delta = 1.0_f64; + let recovered = recover_predetermined_initial_latent_variance( + trait_variance, + initial_latent_variance, + printed_effect, + predictor_variance, + log_rate, + LagClock::EventTime, + ) + .expect("predetermined initial T0VAR"); + let added = recover_asymptotic_time_independent_predictor_variance( + printed_effect, + predictor_variance, + log_rate, + LagClock::EventTime, + ) + .expect("addedTIPREDVAR"); + let expected = trait_variance + initial_latent_variance + added; + let error = rmse(&[expected], &[recovered]); + assert!( + error < 1e-12, + "Driver §4.3 predetermined first-occasion T0VAR RMSE {error}: got {recovered}" + ); + let stationary = recover_stationary_initial_latent_variance( + trait_variance, + diffusion, + printed_effect, + predictor_variance, + log_rate, + LagClock::EventTime, + ) + .expect("stationary initial T0VAR"); + let lagged = recover_predetermined_lagged_latent_covariance( + trait_variance, + initial_latent_variance, + printed_effect, + predictor_variance, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("predetermined lagged T0VAR"); + let later = recover_predetermined_later_latent_variance( + trait_variance, + initial_latent_variance, + diffusion, + printed_effect, + predictor_variance, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("predetermined later T0VAR"); + assert!(rmse(&[recovered], &[stationary]) > error); + assert!(rmse(&[recovered], &[lagged]) > error); + assert!(rmse(&[recovered], &[later]) > error); + assert!(rmse(&[recovered], &[initial_latent_variance]) > error); + let state = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + let from_stationary_start = recover_predetermined_initial_latent_variance( + trait_variance, + state, + printed_effect, + predictor_variance, + log_rate, + LagClock::EventTime, + ) + .expect("p_0=−q/(2a)"); + assert!(rmse(&[from_stationary_start], &[stationary]) < 1e-12); + let near_lagged = recover_predetermined_lagged_latent_covariance( + trait_variance, + initial_latent_variance, + printed_effect, + predictor_variance, + log_rate, + 1e-12, + LagClock::EventTime, + ) + .expect("Δt→0+ lagged"); + assert!(rmse(&[near_lagged], &[recovered]) < 1e-9); + let near_later = recover_predetermined_later_latent_variance( + trait_variance, + initial_latent_variance, + diffusion, + printed_effect, + predictor_variance, + log_rate, + 1e-12, + LagClock::EventTime, + ) + .expect("Δt→0+ later"); + assert!(rmse(&[near_later], &[recovered]) < 1e-9); + assert_eq!( + recover_predetermined_initial_latent_variance( + 0.0, + 0.0, + 0.0, + predictor_variance, + 0.0, + LagClock::EventTime, + ), + Ok(0.0) + ); + assert_eq!( + recover_predetermined_initial_latent_variance( + trait_variance, + 0.0, + 0.0, + predictor_variance, + 0.0, + LagClock::EventTime, + ), + Ok(trait_variance) + ); + assert_eq!( + refuse_predetermined_initial_latent_variance_as_stationary_initial_latent_variance( + recovered, stationary + ), + Err( + PsychometricError::PredeterminedInitialLatentVarianceIsNotStationaryInitialLatentVariance + ) + ); + assert_eq!( + refuse_predetermined_initial_latent_variance_as_initial_latent_variance( + recovered, + initial_latent_variance + ), + Err(PsychometricError::PredeterminedInitialLatentVarianceIsNotInitialLatentVariance) + ); + assert_eq!( + refuse_predetermined_initial_latent_variance_as_lagged_latent_covariance(recovered, lagged), + Err(PsychometricError::PredeterminedInitialLatentVarianceIsNotLaggedLatentCovariance) + ); + assert_eq!( + refuse_predetermined_initial_latent_variance_as_later_latent_variance(recovered, later), + Err(PsychometricError::PredeterminedInitialLatentVarianceIsNotLaterLatentVariance) + ); +} + +#[test] +fn predetermined_initial_latent_variance_refuses_non_event_clocks_and_keeps_unstable_trait() { + assert_eq!( + recover_predetermined_initial_latent_variance( + 1.0, + 2.0, + -0.225, + 1.0, + -0.13, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + let unstable_trait = + recover_predetermined_initial_latent_variance(1.0, 0.0, 0.0, 0.0, 0.5, LagClock::EventTime) + .expect("trait-only a≥0"); + assert!((unstable_trait - 1.0).abs() < 1e-15); + let brownian = + recover_predetermined_initial_latent_variance(0.0, 2.0, 0.0, 0.0, 0.0, LagClock::EventTime) + .expect("Brownian a=0"); + assert!((brownian - 2.0).abs() < 1e-12); + assert_eq!( + recover_predetermined_initial_latent_variance( + 0.0, + 0.0, + -0.225, + 1.0, + 0.5, + LagClock::EventTime + ), + Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) + ); + assert_eq!( + recover_predetermined_initial_latent_variance(0.0, 0.0, 0.0, 1.0, 0.0, LagClock::EventTime), + Ok(0.0) + ); +} + +#[test] +#[allow(clippy::too_many_lines)] +fn predetermined_initial_observed_variance_recovers_driver_equation_five_of_section_four_point_three() + { + let printed_effect = -0.225_f64; + let printed_asym = -1.673_f64; + let log_rate = -printed_effect / printed_asym; + let trait_variance = 1.0_f64; + let initial_latent_variance = 2.0_f64; + let diffusion = 0.4_f64; + let loading = 2.0_f64; + let measurement_error = 0.5_f64; + let manifest_trait = 0.1_f64; + let event_delta = 1.0_f64; + let recovered = recover_predetermined_initial_observed_variance( + loading, + trait_variance, + initial_latent_variance, + printed_effect, + 1.0, + log_rate, + measurement_error, + manifest_trait, + LagClock::EventTime, + ) + .expect("eq5-initial-predetermined-T0VAR"); + let latent = recover_predetermined_initial_latent_variance( + trait_variance, + initial_latent_variance, + printed_effect, + 1.0, + log_rate, + LagClock::EventTime, + ) + .expect("predetermined initial T0VAR"); + let expected = recover_manifest_trait_plus_state_observed_variance( + loading, + latent, + measurement_error, + manifest_trait, + ) + .expect("λ²p+θ+ψ"); + let error = rmse(&[expected], &[recovered]); + assert!( + error < 1e-12, + "Driver §4.3 Eq. 5 of predetermined first-occasion T0VAR RMSE {error}: got {recovered}" + ); + let stationary = recover_stationary_initial_observed_variance( + loading, + trait_variance, + diffusion, + printed_effect, + 1.0, + log_rate, + measurement_error, + manifest_trait, + LagClock::EventTime, + ) + .expect("eq5-initial-stationary-T0VAR"); + let later = recover_predetermined_later_observed_variance( + loading, + trait_variance, + initial_latent_variance, + diffusion, + printed_effect, + 1.0, + log_rate, + event_delta, + measurement_error, + manifest_trait, + LagClock::EventTime, + ) + .expect("eq5-later-predetermined-T0VAR"); + assert!(rmse(&[recovered], &[stationary]) > error); + assert!( + rmse(&[recovered], &[measurement_error]) > error, + "MANIFESTVAR is not predetermined first-occasion Var(y)" + ); + assert!(rmse(&[recovered], &[latent]) > error); + assert!(rmse(&[recovered], &[later]) > error); + assert_eq!( + recover_predetermined_initial_observed_variance( + 0.0, + trait_variance, + initial_latent_variance, + printed_effect, + 1.0, + log_rate, + measurement_error, + manifest_trait, + LagClock::EventTime, + ), + Ok(measurement_error + manifest_trait) + ); + assert_eq!( + refuse_predetermined_initial_latent_variance_as_observed_variance(latent, recovered), + Err(PsychometricError::PredeterminedInitialLatentVarianceIsNotObservedVariance) + ); + assert_eq!( + refuse_measurement_error_as_predetermined_initial_observed_variance( + measurement_error, + recovered + ), + Err(PsychometricError::MeasurementErrorIsNotPredeterminedInitialObservedVariance) + ); + assert_eq!( + refuse_stationary_initial_observed_variance_as_predetermined_initial_observed_variance( + stationary, recovered + ), + Err( + PsychometricError::StationaryInitialObservedVarianceIsNotPredeterminedInitialObservedVariance + ) + ); + assert_eq!( + refuse_predetermined_later_observed_variance_as_predetermined_initial_observed_variance( + later, recovered + ), + Err( + PsychometricError::PredeterminedLaterObservedVarianceIsNotPredeterminedInitialObservedVariance + ) + ); +} + +#[test] +fn predetermined_initial_observed_variance_refuses_non_event_clocks_and_keeps_unstable_trait() { + assert_eq!( + recover_predetermined_initial_observed_variance( + 2.0, + 1.0, + 2.0, + -0.225, + 1.0, + -0.13, + 0.5, + 0.1, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + let brownian = recover_predetermined_initial_observed_variance( + 1.0, + 0.0, + 2.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + LagClock::EventTime, + ) + .expect("Brownian a=0"); + assert!((brownian - 2.0).abs() < 1e-12); + assert_eq!( + recover_predetermined_initial_observed_variance( + 2.0, + 0.0, + 0.0, + -0.225, + 1.0, + 0.5, + 0.0, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) + ); + assert_eq!( + recover_predetermined_initial_observed_variance( + 2.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.5, + 0.1, + LagClock::EventTime + ), + Ok(0.6) + ); +} + +#[test] +#[allow(clippy::too_many_lines)] +fn predetermined_later_lagged_latent_covariance_recovers_driver_section_four_point_three() { + let printed_effect = -0.225_f64; + let printed_asym = -1.673_f64; + let log_rate = -printed_effect / printed_asym; + let trait_variance = 1.0_f64; + let initial_latent_variance = 2.0_f64; + let diffusion = 0.4_f64; + let predictor_variance = 1.0_f64; + let start_delta = 2.0_f64; + let lag_delta = 1.0_f64; + let recovered = recover_predetermined_later_lagged_latent_covariance( + trait_variance, + initial_latent_variance, + diffusion, + printed_effect, + predictor_variance, + log_rate, + start_delta, + lag_delta, + LagClock::EventTime, + ) + .expect("predetermined later-start lagged T0VAR"); + let later_state = recover_discrete_latent_variance( + initial_latent_variance, + diffusion, + log_rate, + start_delta, + LagClock::EventTime, + ) + .expect("later state"); + let added = recover_asymptotic_time_independent_predictor_variance( + printed_effect, + predictor_variance, + log_rate, + LagClock::EventTime, + ) + .expect("addedTIPREDVAR"); + let expected = recover_trait_plus_state_lagged_covariance( + trait_variance, + later_state, + log_rate, + lag_delta, + LagClock::EventTime, + ) + .expect("trait + e^{as} later-state") + + added; + let error = rmse(&[expected], &[recovered]); + assert!( + error < 1e-12, + "Driver §4.3 later-start lagged predetermined T0VAR RMSE {error}: got {recovered}" + ); + let first_lagged = recover_predetermined_lagged_latent_covariance( + trait_variance, + initial_latent_variance, + printed_effect, + predictor_variance, + log_rate, + lag_delta, + LagClock::EventTime, + ) + .expect("first-occasion lagged"); + let later = recover_predetermined_later_latent_variance( + trait_variance, + initial_latent_variance, + diffusion, + printed_effect, + predictor_variance, + log_rate, + start_delta, + LagClock::EventTime, + ) + .expect("later variance"); + let stationary_lagged = recover_stationary_lagged_latent_covariance( + trait_variance, + diffusion, + printed_effect, + predictor_variance, + log_rate, + lag_delta, + LagClock::EventTime, + ) + .expect("stationary lagged"); + let decayed_later = later * lag_delta.mul_add(log_rate, 0.0).exp(); + assert!(rmse(&[recovered], &[first_lagged]) > error); + assert!(rmse(&[recovered], &[later]) > error); + assert!(rmse(&[recovered], &[stationary_lagged]) > error); + assert!(rmse(&[recovered], &[decayed_later]) > error); + let state = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + let from_stationary_start = recover_predetermined_later_lagged_latent_covariance( + trait_variance, + state, + diffusion, + printed_effect, + predictor_variance, + log_rate, + start_delta, + lag_delta, + LagClock::EventTime, + ) + .expect("p_0=−q/(2a)"); + assert!(rmse(&[from_stationary_start], &[stationary_lagged]) < 1e-12); + let near_first = recover_predetermined_later_lagged_latent_covariance( + trait_variance, + initial_latent_variance, + diffusion, + printed_effect, + predictor_variance, + log_rate, + 1e-12, + lag_delta, + LagClock::EventTime, + ) + .expect("u→0+"); + assert!(rmse(&[near_first], &[first_lagged]) < 1e-9); + let near_later = recover_predetermined_later_lagged_latent_covariance( + trait_variance, + initial_latent_variance, + diffusion, + printed_effect, + predictor_variance, + log_rate, + start_delta, + 1e-12, + LagClock::EventTime, + ) + .expect("s→0+"); + assert!(rmse(&[near_later], &[later]) < 1e-9); + assert_eq!( + recover_predetermined_later_lagged_latent_covariance( + 0.0, + 0.0, + 0.0, + 0.0, + predictor_variance, + 0.0, + start_delta, + lag_delta, + LagClock::EventTime + ), + Ok(0.0) + ); + assert_eq!( + recover_predetermined_later_lagged_latent_covariance( + trait_variance, + 0.0, + 0.0, + 0.0, + predictor_variance, + 0.0, + start_delta, + lag_delta, + LagClock::EventTime + ), + Ok(trait_variance) + ); + assert_eq!( + refuse_predetermined_later_lagged_latent_covariance_as_predetermined_lagged_covariance( + recovered, first_lagged + ), + Err( + PsychometricError::PredeterminedLaterLaggedLatentCovarianceIsNotPredeterminedLaggedCovariance + ) + ); + assert_eq!( + refuse_predetermined_later_lagged_latent_covariance_as_later_latent_variance( + recovered, later + ), + Err(PsychometricError::PredeterminedLaterLaggedLatentCovarianceIsNotLaterLatentVariance) + ); + assert_eq!( + refuse_predetermined_later_lagged_latent_covariance_as_stationary_lagged_covariance( + recovered, + stationary_lagged + ), + Err( + PsychometricError::PredeterminedLaterLaggedLatentCovarianceIsNotStationaryLaggedCovariance + ) + ); + assert_eq!( + refuse_predetermined_later_lagged_latent_covariance_as_decayed_later_total( + recovered, + decayed_later + ), + Err(PsychometricError::PredeterminedLaterLaggedLatentCovarianceIsNotDecayedLaterTotal) + ); +} + +#[test] +fn predetermined_later_lagged_latent_covariance_refuses_non_event_clocks_and_keeps_growing_processes() + { + assert_eq!( + recover_predetermined_later_lagged_latent_covariance( + 1.0, + 2.0, + 0.4, + -0.225, + 1.0, + -0.13, + 2.0, + 1.0, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_predetermined_later_lagged_latent_covariance( + 1.0, + 2.0, + 0.4, + -0.225, + 1.0, + -0.13, + 0.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_predetermined_later_lagged_latent_covariance( + 1.0, + 2.0, + 0.4, + -0.225, + 1.0, + -0.13, + 2.0, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + let growing = recover_predetermined_later_lagged_latent_covariance( + 0.0, + 2.0, + 0.4, + 0.0, + 0.0, + 0.5, + 1.0, + 1.0, + LagClock::EventTime, + ) + .expect("growing a>0"); + assert!(growing.is_finite() && growing > 2.0); + assert_eq!( + recover_predetermined_later_lagged_latent_covariance( + 0.0, + 0.0, + 0.0, + -0.225, + 1.0, + 0.5, + 2.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) + ); + assert_eq!( + recover_predetermined_later_lagged_latent_covariance( + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 2.0, + 1.0, + LagClock::EventTime + ), + Ok(0.0) + ); +} + +#[test] +#[allow(clippy::too_many_lines)] +fn predetermined_later_lagged_observed_covariance_recovers_driver_equation_five_of_section_four_point_three() + { + let printed_effect = -0.225_f64; + let printed_asym = -1.673_f64; + let log_rate = -printed_effect / printed_asym; + let trait_variance = 1.0_f64; + let initial_latent_variance = 2.0_f64; + let diffusion = 0.4_f64; + let loading = 2.0_f64; + let measurement_error = 0.5_f64; + let manifest_trait = 0.1_f64; + let start_delta = 2.0_f64; + let lag_delta = 1.0_f64; + let recovered = recover_predetermined_later_lagged_observed_covariance( + loading, + trait_variance, + initial_latent_variance, + diffusion, + printed_effect, + 1.0, + log_rate, + start_delta, + lag_delta, + manifest_trait, + LagClock::EventTime, + ) + .expect("eq5-later-start-lagged-predetermined-T0VAR"); + let latent = recover_predetermined_later_lagged_latent_covariance( + trait_variance, + initial_latent_variance, + diffusion, + printed_effect, + 1.0, + log_rate, + start_delta, + lag_delta, + LagClock::EventTime, + ) + .expect("later-start lagged T0VAR"); + let expected = recover_manifest_lagged_observed_covariance(loading, latent, manifest_trait) + .expect("λ²c+ψ"); + let error = rmse(&[expected], &[recovered]); + assert!( + error < 1e-12, + "Driver §4.3 Eq. 5 of later-start lagged predetermined T0VAR RMSE {error}: got {recovered}" + ); + let first_lagged = recover_predetermined_lagged_observed_covariance( + loading, + trait_variance, + initial_latent_variance, + printed_effect, + 1.0, + log_rate, + lag_delta, + manifest_trait, + LagClock::EventTime, + ) + .expect("eq5-first-lagged"); + let later = recover_predetermined_later_observed_variance( + loading, + trait_variance, + initial_latent_variance, + diffusion, + printed_effect, + 1.0, + log_rate, + start_delta, + measurement_error, + manifest_trait, + LagClock::EventTime, + ) + .expect("eq5-later"); + let stationary = recover_stationary_lagged_observed_covariance( + loading, + trait_variance, + diffusion, + printed_effect, + 1.0, + log_rate, + lag_delta, + manifest_trait, + LagClock::EventTime, + ) + .expect("eq5-stationary-lagged"); + assert!(rmse(&[recovered], &[first_lagged]) > error); + assert!(rmse(&[recovered], &[later]) > error); + assert!(rmse(&[recovered], &[stationary]) > error); + assert!( + rmse(&[recovered], &[measurement_error]) > error, + "MANIFESTVAR is not later-start lagged predetermined cov(y)" + ); + assert!(rmse(&[recovered], &[latent]) > error); + assert_eq!( + recover_predetermined_later_lagged_observed_covariance( + 0.0, + trait_variance, + initial_latent_variance, + diffusion, + printed_effect, + 1.0, + log_rate, + start_delta, + lag_delta, + manifest_trait, + LagClock::EventTime, + ), + Ok(manifest_trait) + ); + assert_eq!( + refuse_predetermined_later_lagged_latent_covariance_as_observed_covariance( + latent, recovered + ), + Err(PsychometricError::PredeterminedLaterLaggedLatentCovarianceIsNotObservedCovariance) + ); + assert_eq!( + refuse_measurement_error_as_predetermined_later_lagged_observed_covariance( + measurement_error, + recovered + ), + Err(PsychometricError::MeasurementErrorIsNotPredeterminedLaterLaggedObservedCovariance) + ); + assert_eq!( + refuse_predetermined_lagged_observed_covariance_as_predetermined_later_lagged_observed_covariance( + first_lagged, recovered + ), + Err( + PsychometricError::PredeterminedLaggedObservedCovarianceIsNotPredeterminedLaterLaggedObservedCovariance + ) + ); + assert_eq!( + refuse_stationary_lagged_observed_covariance_as_predetermined_later_lagged_observed_covariance( + stationary, recovered + ), + Err( + PsychometricError::StationaryLaggedObservedCovarianceIsNotPredeterminedLaterLaggedObservedCovariance + ) + ); + assert_eq!( + refuse_predetermined_later_observed_variance_as_predetermined_later_lagged_observed_covariance( + later, recovered + ), + Err( + PsychometricError::PredeterminedLaterObservedVarianceIsNotPredeterminedLaterLaggedObservedCovariance + ) + ); +} + +#[test] +fn predetermined_later_lagged_observed_covariance_refuses_non_event_clocks_and_keeps_growing_processes() + { + assert_eq!( + recover_predetermined_later_lagged_observed_covariance( + 2.0, + 1.0, + 2.0, + 0.4, + -0.225, + 1.0, + -0.13, + 2.0, + 1.0, + 0.1, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_predetermined_later_lagged_observed_covariance( + 2.0, + 1.0, + 2.0, + 0.4, + 0.0, + 1.0, + -0.5, + 0.0, + 1.0, + 0.1, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + let growing = recover_predetermined_later_lagged_observed_covariance( + 1.0, + 0.0, + 2.0, + 0.4, + 0.0, + 0.0, + 0.5, + 1.0, + 1.0, + 0.0, + LagClock::EventTime, + ) + .expect("growing a>0"); + assert!(growing.is_finite() && growing > 2.0); + assert_eq!( + recover_predetermined_later_lagged_observed_covariance( + 2.0, + 0.0, + 0.0, + 0.0, + -0.225, + 1.0, + 0.5, + 2.0, + 1.0, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) + ); + assert_eq!( + recover_predetermined_later_lagged_observed_covariance( + 2.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 2.0, + 1.0, + 0.1, + LagClock::EventTime + ), + Ok(0.1) + ); +} + +#[test] +#[allow(clippy::too_many_lines)] +fn predetermined_later_start_later_latent_variance_recovers_driver_section_four_point_three() { + let printed_effect = -0.225_f64; + let printed_asym = -1.673_f64; + let log_rate = -printed_effect / printed_asym; + let trait_variance = 1.0_f64; + let initial_latent_variance = 2.0_f64; + let diffusion = 0.4_f64; + let predictor_variance = 1.0_f64; + let start_delta = 2.0_f64; + let lag_delta = 1.0_f64; + let recovered = recover_predetermined_later_start_later_latent_variance( + trait_variance, + initial_latent_variance, + diffusion, + printed_effect, + predictor_variance, + log_rate, + start_delta, + lag_delta, + LagClock::EventTime, + ) + .expect("predetermined later-start later T0VAR"); + let later_state = recover_discrete_latent_variance( + initial_latent_variance, + diffusion, + log_rate, + start_delta, + LagClock::EventTime, + ) + .expect("later state"); + let evolved_state = recover_discrete_latent_variance( + later_state, + diffusion, + log_rate, + lag_delta, + LagClock::EventTime, + ) + .expect("evolved later state"); + let added = recover_asymptotic_time_independent_predictor_variance( + printed_effect, + predictor_variance, + log_rate, + LagClock::EventTime, + ) + .expect("addedTIPREDVAR"); + let expected = recover_trait_plus_state_latent_variance(trait_variance, evolved_state) + .expect("trait + evolved later-state") + + added; + let error = rmse(&[expected], &[recovered]); + assert!( + error < 1e-12, + "Driver §4.3 later-start later-occasion predetermined T0VAR RMSE {error}: got {recovered}" + ); + let later_full = recover_predetermined_later_latent_variance( + trait_variance, + initial_latent_variance, + diffusion, + printed_effect, + predictor_variance, + log_rate, + start_delta + lag_delta, + LagClock::EventTime, + ) + .expect("later over u+s"); + assert!(rmse(&[recovered], &[later_full]) < 1e-12); + let later = recover_predetermined_later_latent_variance( + trait_variance, + initial_latent_variance, + diffusion, + printed_effect, + predictor_variance, + log_rate, + start_delta, + LagClock::EventTime, + ) + .expect("later variance"); + let later_lagged = recover_predetermined_later_lagged_latent_covariance( + trait_variance, + initial_latent_variance, + diffusion, + printed_effect, + predictor_variance, + log_rate, + start_delta, + lag_delta, + LagClock::EventTime, + ) + .expect("later-start lagged"); + let stationary_later = recover_stationary_later_latent_variance( + trait_variance, + diffusion, + printed_effect, + predictor_variance, + log_rate, + lag_delta, + LagClock::EventTime, + ) + .expect("stationary later"); + let decayed_later = recover_discrete_latent_variance( + later, + diffusion, + log_rate, + lag_delta, + LagClock::EventTime, + ) + .expect("evolved later total"); + let lag_interval = recover_predetermined_later_latent_variance( + trait_variance, + initial_latent_variance, + diffusion, + printed_effect, + predictor_variance, + log_rate, + lag_delta, + LagClock::EventTime, + ) + .expect("later over s only"); + assert!(rmse(&[recovered], &[later]) > error); + assert!(rmse(&[recovered], &[later_lagged]) > error); + assert!(rmse(&[recovered], &[stationary_later]) > error); + assert!(rmse(&[recovered], &[decayed_later]) > error); + assert!(rmse(&[recovered], &[lag_interval]) > error); + let state = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + let from_stationary_start = recover_predetermined_later_start_later_latent_variance( + trait_variance, + state, + diffusion, + printed_effect, + predictor_variance, + log_rate, + start_delta, + lag_delta, + LagClock::EventTime, + ) + .expect("p_0=−q/(2a)"); + assert!(rmse(&[from_stationary_start], &[stationary_later]) < 1e-12); + let near_later = recover_predetermined_later_start_later_latent_variance( + trait_variance, + initial_latent_variance, + diffusion, + printed_effect, + predictor_variance, + log_rate, + start_delta, + 1e-12, + LagClock::EventTime, + ) + .expect("s→0+"); + assert!(rmse(&[near_later], &[later]) < 1e-9); + let later_over_s = recover_predetermined_later_latent_variance( + trait_variance, + initial_latent_variance, + diffusion, + printed_effect, + predictor_variance, + log_rate, + lag_delta, + LagClock::EventTime, + ) + .expect("later over s"); + let near_first = recover_predetermined_later_start_later_latent_variance( + trait_variance, + initial_latent_variance, + diffusion, + printed_effect, + predictor_variance, + log_rate, + 1e-12, + lag_delta, + LagClock::EventTime, + ) + .expect("u→0+"); + assert!(rmse(&[near_first], &[later_over_s]) < 1e-9); + assert_eq!( + recover_predetermined_later_start_later_latent_variance( + 0.0, + 0.0, + 0.0, + 0.0, + predictor_variance, + 0.0, + start_delta, + lag_delta, + LagClock::EventTime + ), + Ok(0.0) + ); + assert_eq!( + recover_predetermined_later_start_later_latent_variance( + trait_variance, + 0.0, + 0.0, + 0.0, + predictor_variance, + 0.0, + start_delta, + lag_delta, + LagClock::EventTime + ), + Ok(trait_variance) + ); + assert_eq!( + refuse_predetermined_later_start_later_latent_variance_as_later_latent_variance( + recovered, later + ), + Err(PsychometricError::PredeterminedLaterStartLaterLatentVarianceIsNotLaterLatentVariance) + ); + assert_eq!( + refuse_predetermined_later_start_later_latent_variance_as_later_lagged_covariance( + recovered, + later_lagged + ), + Err( + PsychometricError::PredeterminedLaterStartLaterLatentVarianceIsNotLaterLaggedCovariance + ) + ); + assert_eq!( + refuse_predetermined_later_start_later_latent_variance_as_stationary_later_latent_variance( + recovered, + stationary_later + ), + Err( + PsychometricError::PredeterminedLaterStartLaterLatentVarianceIsNotStationaryLaterLatentVariance + ) + ); + assert_eq!( + refuse_predetermined_later_start_later_latent_variance_as_decayed_later_total( + recovered, + decayed_later + ), + Err(PsychometricError::PredeterminedLaterStartLaterLatentVarianceIsNotDecayedLaterTotal) + ); + assert_eq!( + refuse_predetermined_later_start_later_latent_variance_as_lag_interval_later_latent_variance( + recovered, + lag_interval + ), + Err( + PsychometricError::PredeterminedLaterStartLaterLatentVarianceIsNotLagIntervalLaterLatentVariance + ) + ); +} + +#[test] +fn predetermined_later_start_later_latent_variance_refuses_non_event_clocks_and_keeps_growing_processes() + { + assert_eq!( + recover_predetermined_later_start_later_latent_variance( + 1.0, + 2.0, + 0.4, + -0.225, + 1.0, + -0.13, + 2.0, + 1.0, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_predetermined_later_start_later_latent_variance( + 1.0, + 2.0, + 0.4, + -0.225, + 1.0, + -0.13, + 0.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_predetermined_later_start_later_latent_variance( + 1.0, + 2.0, + 0.4, + -0.225, + 1.0, + -0.13, + 2.0, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + let growing = recover_predetermined_later_start_later_latent_variance( + 0.0, + 2.0, + 0.4, + 0.0, + 0.0, + 0.5, + 1.0, + 1.0, + LagClock::EventTime, + ) + .expect("growing a>0"); + assert!(growing.is_finite() && growing > 2.0); + assert_eq!( + recover_predetermined_later_start_later_latent_variance( + 0.0, + 0.0, + 0.0, + -0.225, + 1.0, + 0.5, + 2.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) + ); + assert_eq!( + recover_predetermined_later_start_later_latent_variance( + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 2.0, + 1.0, + LagClock::EventTime + ), + Ok(0.0) + ); +} + +#[test] +#[allow(clippy::too_many_lines)] +fn predetermined_later_start_later_observed_variance_recovers_driver_equation_five_of_section_four_point_three() + { + let printed_effect = -0.225_f64; + let printed_asym = -1.673_f64; + let log_rate = -printed_effect / printed_asym; + let trait_variance = 1.0_f64; + let initial_latent_variance = 2.0_f64; + let diffusion = 0.4_f64; + let loading = 2.0_f64; + let measurement_error = 0.5_f64; + let manifest_trait = 0.1_f64; + let start_delta = 2.0_f64; + let lag_delta = 1.0_f64; + let recovered = recover_predetermined_later_start_later_observed_variance( + loading, + trait_variance, + initial_latent_variance, + diffusion, + printed_effect, + 1.0, + log_rate, + start_delta, + lag_delta, + measurement_error, + manifest_trait, + LagClock::EventTime, + ) + .expect("eq5-later-start-later-predetermined-T0VAR"); + let latent = recover_predetermined_later_start_later_latent_variance( + trait_variance, + initial_latent_variance, + diffusion, + printed_effect, + 1.0, + log_rate, + start_delta, + lag_delta, + LagClock::EventTime, + ) + .expect("later-start later T0VAR"); + let expected = recover_manifest_trait_plus_state_observed_variance( + loading, + latent, + measurement_error, + manifest_trait, + ) + .expect("λ²p+θ+ψ"); + let error = rmse(&[expected], &[recovered]); + assert!( + error < 1e-12, + "Driver §4.3 Eq. 5 of later-start later-occasion predetermined T0VAR RMSE {error}: got {recovered}" + ); + let later = recover_predetermined_later_observed_variance( + loading, + trait_variance, + initial_latent_variance, + diffusion, + printed_effect, + 1.0, + log_rate, + start_delta, + measurement_error, + manifest_trait, + LagClock::EventTime, + ) + .expect("eq5-later"); + let later_lagged = recover_predetermined_later_lagged_observed_covariance( + loading, + trait_variance, + initial_latent_variance, + diffusion, + printed_effect, + 1.0, + log_rate, + start_delta, + lag_delta, + manifest_trait, + LagClock::EventTime, + ) + .expect("eq5-later-start-lagged"); + let stationary = recover_stationary_later_observed_variance( + loading, + trait_variance, + diffusion, + printed_effect, + 1.0, + log_rate, + lag_delta, + measurement_error, + manifest_trait, + LagClock::EventTime, + ) + .expect("eq5-stationary-later"); + assert!(rmse(&[recovered], &[later]) > error); + assert!(rmse(&[recovered], &[later_lagged]) > error); + assert!(rmse(&[recovered], &[stationary]) > error); + assert!( + rmse(&[recovered], &[measurement_error]) > error, + "MANIFESTVAR is not later-start later-occasion predetermined Var(y)" + ); + assert!(rmse(&[recovered], &[latent]) > error); + assert_eq!( + recover_predetermined_later_start_later_observed_variance( + 0.0, + trait_variance, + initial_latent_variance, + diffusion, + printed_effect, + 1.0, + log_rate, + start_delta, + lag_delta, + measurement_error, + manifest_trait, + LagClock::EventTime, + ), + Ok(measurement_error + manifest_trait) + ); + assert_eq!( + refuse_predetermined_later_start_later_latent_variance_as_observed_variance( + latent, recovered + ), + Err(PsychometricError::PredeterminedLaterStartLaterLatentVarianceIsNotObservedVariance) + ); + assert_eq!( + refuse_measurement_error_as_predetermined_later_start_later_observed_variance( + measurement_error, + recovered + ), + Err(PsychometricError::MeasurementErrorIsNotPredeterminedLaterStartLaterObservedVariance) + ); + assert_eq!( + refuse_predetermined_later_observed_variance_as_predetermined_later_start_later_observed_variance( + later, recovered + ), + Err( + PsychometricError::PredeterminedLaterObservedVarianceIsNotPredeterminedLaterStartLaterObservedVariance + ) + ); + assert_eq!( + refuse_predetermined_later_lagged_observed_covariance_as_predetermined_later_start_later_observed_variance( + later_lagged, recovered + ), + Err( + PsychometricError::PredeterminedLaterLaggedObservedCovarianceIsNotPredeterminedLaterStartLaterObservedVariance + ) + ); + assert_eq!( + refuse_stationary_later_observed_variance_as_predetermined_later_start_later_observed_variance( + stationary, recovered + ), + Err( + PsychometricError::StationaryLaterObservedVarianceIsNotPredeterminedLaterStartLaterObservedVariance + ) + ); +} + +#[test] +fn predetermined_later_start_later_observed_variance_refuses_non_event_clocks_and_keeps_growing_processes() + { + assert_eq!( + recover_predetermined_later_start_later_observed_variance( + 2.0, + 1.0, + 2.0, + 0.4, + -0.225, + 1.0, + -0.13, + 2.0, + 1.0, + 0.5, + 0.1, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_predetermined_later_start_later_observed_variance( + 2.0, + 1.0, + 2.0, + 0.4, + 0.0, + 1.0, + -0.5, + 0.0, + 1.0, + 0.5, + 0.1, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + let growing = recover_predetermined_later_start_later_observed_variance( + 1.0, + 0.0, + 2.0, + 0.4, + 0.0, + 0.0, + 0.5, + 1.0, + 1.0, + 0.0, + 0.0, + LagClock::EventTime, + ) + .expect("growing a>0"); + assert!(growing.is_finite() && growing > 2.0); + assert_eq!( + recover_predetermined_later_start_later_observed_variance( + 2.0, + 0.0, + 0.0, + 0.0, + -0.225, + 1.0, + 0.5, + 2.0, + 1.0, + 0.0, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) + ); + assert_eq!( + recover_predetermined_later_start_later_observed_variance( + 2.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 2.0, + 1.0, + 0.5, + 0.1, + LagClock::EventTime + ), + Ok(0.6) + ); +} + +#[test] +fn standardised_discrete_drift_recovers_driver_page_sixteen_footnote_four() { + let diffusion = 0.4_f64; + let log_rate = -0.5_f64; + let event_delta = 1.0_f64; + let recovered = + recover_standardised_discrete_drift(diffusion, log_rate, event_delta, LagClock::EventTime) + .expect("discreteDRIFTstd"); + let unstandardised = + recover_discrete_lag_from_log_rate(log_rate, event_delta, LagClock::EventTime) + .expect("discreteDRIFT"); + let within = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + assert!(within > 0.0); + assert!((recovered - (log_rate * event_delta).exp()).abs() < 1e-15); + assert!((recovered - unstandardised).abs() < 1e-15); + let two_and_a_half = + recover_standardised_discrete_drift(diffusion, log_rate, 2.5, LagClock::EventTime) + .expect("discreteDRIFTstd Δt=2.5"); + assert!((two_and_a_half - (log_rate * 2.5).exp()).abs() < 1e-15); + let trait_variance = 1.0_f64; + let lagged = recover_trait_plus_state_lagged_covariance( + trait_variance, + within, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("trait+state lag"); + let total = + recover_trait_plus_state_latent_variance(trait_variance, within).expect("trait+state var"); + let contaminated = lagged / total; + assert!((contaminated - recovered).abs() > 1e-3); + assert_eq!( + refuse_unstandardised_discrete_drift_as_standardised_discrete_drift( + unstandardised, + recovered + ), + Err(PsychometricError::UnstandardisedDiscreteDriftIsNotStandardisedDiscreteDrift) + ); + assert_eq!( + refuse_trait_plus_state_autocorrelation_as_standardised_discrete_drift( + contaminated, + recovered + ), + Err(PsychometricError::TraitPlusStateAutocorrelationIsNotStandardisedDiscreteDrift) + ); + assert_eq!( + refuse_trait_variance_as_standardisation_variance(trait_variance, within), + Err(PsychometricError::TraitVarianceIsNotStandardisationVariance) + ); +} + +#[test] +fn standardised_discrete_drift_refuses_non_event_clocks_and_does_not_keep_growing_or_zero_diffusion() + { + assert_eq!( + recover_standardised_discrete_drift(0.4, -0.5, 1.0, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_standardised_discrete_drift(0.4, -0.5, 0.0, LagClock::EventTime), + Err(PsychometricError::NonPositiveInterval) + ); + let growing = + recover_discrete_lag_from_log_rate(0.5, 1.0, LagClock::EventTime).expect("growing a>0"); + assert!(growing.is_finite() && growing > 1.0); + assert_eq!( + recover_standardised_discrete_drift(0.4, 0.5, 1.0, LagClock::EventTime), + Err(PsychometricError::StationaryVarianceRequiresStableDrift) + ); + let zero_q = + recover_discrete_lag_from_log_rate(-0.5, 1.0, LagClock::EventTime).expect("e^{aΔt} at q=0"); + assert!(zero_q.is_finite() && zero_q > 0.0); + assert_eq!( + recover_standardised_discrete_drift(0.0, -0.5, 1.0, LagClock::EventTime), + Err(PsychometricError::StandardisedDiscreteDriftRequiresPositiveWithinSubjectVariance) + ); +} + +#[test] +fn standardised_discrete_diffusion_recovers_driver_page_sixteen_footnote_four() { + let diffusion = 0.4_f64; + let log_rate = -0.5_f64; + let event_delta = 1.0_f64; + let recovered = recover_standardised_discrete_diffusion( + diffusion, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("discreteDIFFUSIONstd"); + let process_noise = + recover_discrete_process_noise(diffusion, log_rate, event_delta, LagClock::EventTime) + .expect("discreteDIFFUSION"); + let within = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + assert!(within > 0.0); + assert!((recovered - (process_noise / within)).abs() < 1e-15); + assert!((recovered - (1.0 - (2.0 * log_rate * event_delta).exp())).abs() < 1e-15); + let two_and_a_half = + recover_standardised_discrete_diffusion(diffusion, log_rate, 2.5, LagClock::EventTime) + .expect("discreteDIFFUSIONstd Δt=2.5"); + assert!((two_and_a_half - (1.0 - (2.0 * log_rate * 2.5).exp())).abs() < 1e-15); + let continuous_std = diffusion / within; + assert!((continuous_std - recovered).abs() > 1e-3); + let trait_variance = 1.0_f64; + let total = + recover_trait_plus_state_latent_variance(trait_variance, within).expect("trait+state var"); + let contaminated = process_noise / total; + assert!((contaminated - recovered).abs() > 1e-3); + assert_eq!( + refuse_unstandardised_discrete_diffusion_as_standardised_discrete_diffusion( + process_noise, + recovered + ), + Err(PsychometricError::UnstandardisedDiscreteDiffusionIsNotStandardisedDiscreteDiffusion) + ); + assert_eq!( + refuse_standardised_continuous_diffusion_as_standardised_discrete_diffusion( + continuous_std, + recovered + ), + Err(PsychometricError::StandardisedContinuousDiffusionIsNotStandardisedDiscreteDiffusion) + ); + assert_eq!( + refuse_trait_contaminated_process_noise_as_standardised_discrete_diffusion( + contaminated, + recovered + ), + Err(PsychometricError::TraitContaminatedProcessNoiseIsNotStandardisedDiscreteDiffusion) + ); + assert_eq!( + refuse_trait_variance_as_standardisation_variance(trait_variance, within), + Err(PsychometricError::TraitVarianceIsNotStandardisationVariance) + ); +} + +#[test] +fn standardised_discrete_diffusion_refuses_non_event_clocks_and_does_not_keep_growing_or_zero_diffusion() + { + assert_eq!( + recover_standardised_discrete_diffusion(0.4, -0.5, 1.0, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_standardised_discrete_diffusion(0.4, -0.5, 0.0, LagClock::EventTime), + Err(PsychometricError::NonPositiveInterval) + ); + let growing = + recover_discrete_process_noise(0.4, 0.5, 1.0, LagClock::EventTime).expect("growing a>0"); + assert!(growing.is_finite() && growing > 0.0); + assert_eq!( + recover_standardised_discrete_diffusion(0.4, 0.5, 1.0, LagClock::EventTime), + Err(PsychometricError::StationaryVarianceRequiresStableDrift) + ); + let zero_q = + recover_discrete_process_noise(0.0, -0.5, 1.0, LagClock::EventTime).expect("Q_Δt at q=0"); + assert!((zero_q - 0.0).abs() < 1e-15); + assert_eq!( + recover_standardised_discrete_diffusion(0.0, -0.5, 1.0, LagClock::EventTime), + Err(PsychometricError::StandardisedDiscreteDiffusionRequiresPositiveWithinSubjectVariance) + ); +} + +#[test] +fn standardised_continuous_diffusion_recovers_driver_page_sixteen_footnote_four() { + let diffusion = 0.4_f64; + let log_rate = -0.5_f64; + let recovered = + recover_standardised_continuous_diffusion(diffusion, log_rate, LagClock::EventTime) + .expect("DIFFUSIONstd"); + let within = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + assert!(within > 0.0); + assert!((recovered - (diffusion / within)).abs() < 1e-15); + assert!((recovered - (-2.0 * log_rate)).abs() < 1e-15); + let larger_q = recover_standardised_continuous_diffusion(2.0, log_rate, LagClock::EventTime) + .expect("DIFFUSIONstd q=2"); + assert!((larger_q - recovered).abs() < 1e-15); + let discrete = + recover_standardised_discrete_diffusion(diffusion, log_rate, 1.0, LagClock::EventTime) + .expect("discreteDIFFUSIONstd"); + assert!((discrete - recovered).abs() > 1e-3); + let trait_variance = 1.0_f64; + let total = + recover_trait_plus_state_latent_variance(trait_variance, within).expect("trait+state var"); + let contaminated = diffusion / total; + assert!((contaminated - recovered).abs() > 1e-3); + assert_eq!( + refuse_unstandardised_continuous_diffusion_as_standardised_continuous_diffusion( + diffusion, + recovered + ), + Err(PsychometricError::UnstandardisedContinuousDiffusionIsNotStandardisedContinuousDiffusion) + ); + assert_eq!( + refuse_standardised_discrete_diffusion_as_standardised_continuous_diffusion( + discrete, recovered + ), + Err(PsychometricError::StandardisedDiscreteDiffusionIsNotStandardisedContinuousDiffusion) + ); + assert_eq!( + refuse_trait_contaminated_continuous_diffusion_as_standardised_continuous_diffusion( + contaminated, + recovered + ), + Err( + PsychometricError::TraitContaminatedContinuousDiffusionIsNotStandardisedContinuousDiffusion + ) + ); + assert_eq!( + refuse_trait_variance_as_standardisation_variance(trait_variance, within), + Err(PsychometricError::TraitVarianceIsNotStandardisationVariance) + ); +} + +#[test] +fn standardised_continuous_diffusion_refuses_non_event_clocks_and_does_not_keep_growing_or_zero_diffusion() + { + assert_eq!( + recover_standardised_continuous_diffusion(0.4, -0.5, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + let growing = + recover_discrete_process_noise(0.4, 0.5, 1.0, LagClock::EventTime).expect("growing a>0"); + assert!(growing.is_finite() && growing > 0.0); + assert_eq!( + recover_standardised_continuous_diffusion(0.4, 0.5, LagClock::EventTime), + Err(PsychometricError::StationaryVarianceRequiresStableDrift) + ); + assert_eq!( + recover_standardised_continuous_diffusion(0.0, -0.5, LagClock::EventTime), + Err( + PsychometricError::StandardisedContinuousDiffusionRequiresPositiveWithinSubjectVariance + ) + ); +} + +#[test] +fn standardised_continuous_drift_recovers_driver_page_sixteen_footnote_four() { + let diffusion = 0.4_f64; + let log_rate = -0.5_f64; + let recovered = recover_standardised_continuous_drift(diffusion, log_rate, LagClock::EventTime) + .expect("DRIFTstd"); + let within = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + assert!(within > 0.0); + assert!((recovered - log_rate).abs() < 1e-15); + let larger_q = recover_standardised_continuous_drift(2.0, log_rate, LagClock::EventTime) + .expect("DRIFTstd q=2"); + assert!((larger_q - recovered).abs() < 1e-15); + let discrete = + recover_standardised_discrete_drift(diffusion, log_rate, 1.0, LagClock::EventTime) + .expect("discreteDRIFTstd"); + assert!((discrete - recovered).abs() > 1e-3); + let trait_variance = 1.0_f64; + let total = + recover_trait_plus_state_latent_variance(trait_variance, within).expect("trait+state var"); + let contaminated = log_rate * (within / total); + assert!((contaminated - recovered).abs() > 1e-3); + assert_eq!( + refuse_unstandardised_continuous_drift_as_standardised_continuous_drift( + log_rate, recovered + ), + Err(PsychometricError::UnstandardisedContinuousDriftIsNotStandardisedContinuousDrift) + ); + assert_eq!( + refuse_standardised_discrete_drift_as_standardised_continuous_drift(discrete, recovered), + Err(PsychometricError::StandardisedDiscreteDriftIsNotStandardisedContinuousDrift) + ); + assert_eq!( + refuse_trait_contaminated_continuous_drift_as_standardised_continuous_drift( + contaminated, + recovered + ), + Err(PsychometricError::TraitContaminatedContinuousDriftIsNotStandardisedContinuousDrift) + ); + assert_eq!( + refuse_trait_variance_as_standardisation_variance(trait_variance, within), + Err(PsychometricError::TraitVarianceIsNotStandardisationVariance) + ); +} + +#[test] +fn standardised_continuous_drift_refuses_non_event_clocks_and_does_not_keep_growing_or_zero_diffusion() + { + assert_eq!( + recover_standardised_continuous_drift(0.4, -0.5, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_standardised_continuous_drift(0.4, 0.5, LagClock::EventTime), + Err(PsychometricError::StationaryVarianceRequiresStableDrift) + ); + assert_eq!( + recover_standardised_continuous_drift(0.0, -0.5, LagClock::EventTime), + Err(PsychometricError::StandardisedContinuousDriftRequiresPositiveWithinSubjectVariance) + ); +} + +#[test] +fn standardised_asymptotic_time_independent_effect_recovers_driver_page_sixteen_footnote_four() { + let diffusion = 0.4_f64; + let log_rate = -0.5_f64; + let coefficient = 0.3_f64; + let predictor_variance = 1.0_f64; + let recovered = recover_standardised_asymptotic_time_independent_predictor_effect( + coefficient, + predictor_variance, + diffusion, + log_rate, + LagClock::EventTime, + ) + .expect("asymTIPREDEFFECTstd"); + let within = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + assert!(within > 0.0); + let unit = recover_asymptotic_time_independent_predictor_effect( + coefficient, + 1.0, + log_rate, + LagClock::EventTime, + ) + .expect("asymTIPREDEFFECT"); + assert!((recovered - unit * predictor_variance.sqrt() / within.sqrt()).abs() < 1e-15); + let larger_q = recover_standardised_asymptotic_time_independent_predictor_effect( + coefficient, + predictor_variance, + 2.0, + log_rate, + LagClock::EventTime, + ) + .expect("asymTIPREDEFFECTstd q=2"); + assert!((larger_q - recovered).abs() > 1e-3); + let discrete_increment = recover_discrete_time_independent_predictor_effect( + coefficient, + 1.0, + log_rate, + 1.0, + LagClock::EventTime, + ) + .expect("discrete TIPREDEFFECT"); + let discrete_std = discrete_increment * predictor_variance.sqrt() / within.sqrt(); + assert!((discrete_std - recovered).abs() > 1e-3); + let trait_variance = 1.0_f64; + let total = + recover_trait_plus_state_latent_variance(trait_variance, within).expect("trait+state var"); + let contaminated = unit * predictor_variance.sqrt() / total.sqrt(); + assert!((contaminated - recovered).abs() > 1e-3); + assert_eq!( + refuse_unstandardised_asymptotic_time_independent_effect_as_standardised_asymptotic_time_independent_effect( + unit, recovered + ), + Err(PsychometricError::UnstandardisedAsymptoticTimeIndependentEffectIsNotStandardisedAsymptoticTimeIndependentEffect) + ); + assert_eq!( + refuse_standardised_discrete_time_independent_effect_as_standardised_asymptotic_time_independent_effect( + discrete_std, recovered + ), + Err(PsychometricError::StandardisedDiscreteTimeIndependentEffectIsNotStandardisedAsymptoticTimeIndependentEffect) + ); + assert_eq!( + refuse_trait_contaminated_asymptotic_time_independent_effect_as_standardised_asymptotic_time_independent_effect( + contaminated, + recovered + ), + Err(PsychometricError::TraitContaminatedAsymptoticTimeIndependentEffectIsNotStandardisedAsymptoticTimeIndependentEffect) + ); + assert_eq!( + refuse_trait_variance_as_standardisation_variance(trait_variance, within), + Err(PsychometricError::TraitVarianceIsNotStandardisationVariance) + ); +} + +#[test] +fn standardised_asymptotic_time_independent_effect_refuses_non_event_clocks_and_does_not_keep_zero_variances() + { + assert_eq!( + recover_standardised_asymptotic_time_independent_predictor_effect( + 0.3, + 1.0, + 0.4, + -0.5, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_standardised_asymptotic_time_independent_predictor_effect( + 0.3, + 1.0, + 0.4, + 0.5, + LagClock::EventTime + ), + Err(PsychometricError::StationaryVarianceRequiresStableDrift) + ); + assert_eq!( + recover_standardised_asymptotic_time_independent_predictor_effect( + 0.3, + 1.0, + 0.0, + -0.5, + LagClock::EventTime + ), + Err( + PsychometricError::StandardisedAsymptoticTimeIndependentEffectRequiresPositiveWithinSubjectVariance + ) + ); + assert_eq!( + recover_standardised_asymptotic_time_independent_predictor_effect( + 0.3, + 0.0, + 0.4, + -0.5, + LagClock::EventTime + ), + Err( + PsychometricError::StandardisedAsymptoticTimeIndependentEffectRequiresPositivePredictorVariance + ) + ); + assert_eq!( + recover_standardised_asymptotic_time_independent_predictor_effect( + f64::NAN, + 1.0, + 0.4, + -0.5, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_asymptotic_time_independent_predictor_effect( + 0.3, + f64::NAN, + 0.4, + -0.5, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_asymptotic_time_independent_predictor_effect( + 0.3, + -0.1, + 0.4, + -0.5, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_asymptotic_time_independent_predictor_effect( + 0.3, + f64::MAX, + 1e-308, + -1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_asymptotic_time_independent_predictor_effect( + f64::MAX, + 4.0, + 2.0, + -1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); +} + +#[test] +fn standardised_continuous_time_independent_effect_recovers_driver_page_sixteen_footnote_four() { + let diffusion = 0.4_f64; + let log_rate = -0.5_f64; + let coefficient = 0.3_f64; + let predictor_variance = 1.0_f64; + let recovered = recover_standardised_continuous_time_independent_predictor_effect( + coefficient, + predictor_variance, + diffusion, + log_rate, + LagClock::EventTime, + ) + .expect("TIPREDEFFECTstd"); + let within = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + assert!(within > 0.0); + assert!((recovered - coefficient * predictor_variance.sqrt() / within.sqrt()).abs() < 1e-15); + let larger_q = recover_standardised_continuous_time_independent_predictor_effect( + coefficient, + predictor_variance, + 2.0, + log_rate, + LagClock::EventTime, + ) + .expect("TIPREDEFFECTstd q=2"); + assert!((larger_q - recovered).abs() > 1e-3); + let asymptotic = recover_standardised_asymptotic_time_independent_predictor_effect( + coefficient, + predictor_variance, + diffusion, + log_rate, + LagClock::EventTime, + ) + .expect("asymTIPREDEFFECTstd"); + assert!((asymptotic - recovered).abs() > 1e-3); + let discrete_increment = recover_discrete_time_independent_predictor_effect( + coefficient, + 1.0, + log_rate, + 1.0, + LagClock::EventTime, + ) + .expect("discrete TIPREDEFFECT"); + let discrete_std = discrete_increment * predictor_variance.sqrt() / within.sqrt(); + assert!((discrete_std - recovered).abs() > 1e-3); + let trait_variance = 1.0_f64; + let total = + recover_trait_plus_state_latent_variance(trait_variance, within).expect("trait+state var"); + let contaminated = coefficient * predictor_variance.sqrt() / total.sqrt(); + assert!((contaminated - recovered).abs() > 1e-3); + assert_eq!( + refuse_unstandardised_continuous_time_independent_effect_as_standardised_continuous_time_independent_effect( + coefficient, recovered + ), + Err(PsychometricError::UnstandardisedContinuousTimeIndependentEffectIsNotStandardisedContinuousTimeIndependentEffect) + ); + assert_eq!( + refuse_standardised_asymptotic_time_independent_effect_as_standardised_continuous_time_independent_effect( + asymptotic, recovered + ), + Err(PsychometricError::StandardisedAsymptoticTimeIndependentEffectIsNotStandardisedContinuousTimeIndependentEffect) + ); + assert_eq!( + refuse_standardised_discrete_time_independent_effect_as_standardised_continuous_time_independent_effect( + discrete_std, recovered + ), + Err(PsychometricError::StandardisedDiscreteTimeIndependentEffectIsNotStandardisedContinuousTimeIndependentEffect) + ); + assert_eq!( + refuse_trait_contaminated_continuous_time_independent_effect_as_standardised_continuous_time_independent_effect( + contaminated, + recovered + ), + Err(PsychometricError::TraitContaminatedContinuousTimeIndependentEffectIsNotStandardisedContinuousTimeIndependentEffect) + ); + assert_eq!( + refuse_trait_variance_as_standardisation_variance(trait_variance, within), + Err(PsychometricError::TraitVarianceIsNotStandardisationVariance) + ); +} + +#[test] +fn standardised_continuous_time_independent_effect_refuses_non_event_clocks_and_does_not_keep_zero_variances() + { + assert_eq!( + recover_standardised_continuous_time_independent_predictor_effect( + 0.3, + 1.0, + 0.4, + -0.5, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_standardised_continuous_time_independent_predictor_effect( + 0.3, + 1.0, + 0.4, + 0.5, + LagClock::EventTime + ), + Err(PsychometricError::StationaryVarianceRequiresStableDrift) + ); + assert_eq!( + recover_standardised_continuous_time_independent_predictor_effect( + 0.3, + 1.0, + 0.0, + -0.5, + LagClock::EventTime + ), + Err( + PsychometricError::StandardisedContinuousTimeIndependentEffectRequiresPositiveWithinSubjectVariance + ) + ); + assert_eq!( + recover_standardised_continuous_time_independent_predictor_effect( + 0.3, + 0.0, + 0.4, + -0.5, + LagClock::EventTime + ), + Err( + PsychometricError::StandardisedContinuousTimeIndependentEffectRequiresPositivePredictorVariance + ) + ); + assert_eq!( + recover_standardised_continuous_time_independent_predictor_effect( + f64::NAN, + 1.0, + 0.4, + -0.5, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_continuous_time_independent_predictor_effect( + 0.3, + f64::NAN, + 0.4, + -0.5, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_continuous_time_independent_predictor_effect( + 0.3, + -0.1, + 0.4, + -0.5, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); +} + +#[test] +fn standardised_continuous_time_dependent_effect_recovers_driver_page_sixteen_footnote_four() { + let diffusion = 0.4_f64; + let log_rate = -0.5_f64; + let coefficient = 0.3_f64; + let predictor_variance = 1.0_f64; + let recovered = recover_standardised_continuous_time_dependent_predictor_effect( + coefficient, + predictor_variance, + diffusion, + log_rate, + LagClock::EventTime, + ) + .expect("TDPREDEFFECTstd"); + let within = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + assert!(within > 0.0); + assert!((recovered - coefficient * predictor_variance.sqrt() / within.sqrt()).abs() < 1e-15); + let larger_q = recover_standardised_continuous_time_dependent_predictor_effect( + coefficient, + predictor_variance, + 2.0, + log_rate, + LagClock::EventTime, + ) + .expect("TDPREDEFFECTstd q=2"); + assert!((larger_q - recovered).abs() > 1e-3); + let tipred = recover_standardised_continuous_time_independent_predictor_effect( + coefficient, + predictor_variance, + diffusion, + log_rate, + LagClock::EventTime, + ) + .expect("TIPREDEFFECTstd"); + assert_eq!(tipred.to_bits(), recovered.to_bits()); + let discrete_increment = recover_discrete_time_independent_predictor_effect( + coefficient, + 1.0, + log_rate, + 1.0, + LagClock::EventTime, + ) + .expect("intercept-style discrete TDPREDEFFECT"); + let discrete_std = discrete_increment * predictor_variance.sqrt() / within.sqrt(); + assert!((discrete_std - recovered).abs() > 1e-3); + let trait_variance = 1.0_f64; + let total = + recover_trait_plus_state_latent_variance(trait_variance, within).expect("trait+state var"); + let contaminated = coefficient * predictor_variance.sqrt() / total.sqrt(); + assert!((contaminated - recovered).abs() > 1e-3); + assert_eq!( + refuse_unstandardised_continuous_time_dependent_effect_as_standardised_continuous_time_dependent_effect( + coefficient, recovered + ), + Err(PsychometricError::UnstandardisedContinuousTimeDependentEffectIsNotStandardisedContinuousTimeDependentEffect) + ); + assert_eq!( + refuse_standardised_continuous_time_independent_effect_as_standardised_continuous_time_dependent_effect( + tipred, recovered + ), + Err(PsychometricError::StandardisedContinuousTimeIndependentEffectIsNotStandardisedContinuousTimeDependentEffect) + ); + assert_eq!( + refuse_standardised_discrete_time_dependent_effect_as_standardised_continuous_time_dependent_effect( + discrete_std, recovered + ), + Err(PsychometricError::StandardisedDiscreteTimeDependentEffectIsNotStandardisedContinuousTimeDependentEffect) + ); + assert_eq!( + refuse_trait_contaminated_continuous_time_dependent_effect_as_standardised_continuous_time_dependent_effect( + contaminated, + recovered + ), + Err(PsychometricError::TraitContaminatedContinuousTimeDependentEffectIsNotStandardisedContinuousTimeDependentEffect) + ); + assert_eq!( + refuse_trait_variance_as_standardisation_variance(trait_variance, within), + Err(PsychometricError::TraitVarianceIsNotStandardisationVariance) + ); +} + +#[test] +fn standardised_continuous_time_dependent_effect_refuses_non_event_clocks_and_does_not_keep_zero_variances() + { + assert_eq!( + recover_standardised_continuous_time_dependent_predictor_effect( + 0.3, + 1.0, + 0.4, + -0.5, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_standardised_continuous_time_dependent_predictor_effect( + 0.3, + 1.0, + 0.4, + 0.5, + LagClock::EventTime + ), + Err(PsychometricError::StationaryVarianceRequiresStableDrift) + ); + assert_eq!( + recover_standardised_continuous_time_dependent_predictor_effect( + 0.3, + 1.0, + 0.0, + -0.5, + LagClock::EventTime + ), + Err( + PsychometricError::StandardisedContinuousTimeDependentEffectRequiresPositiveWithinSubjectVariance + ) + ); + assert_eq!( + recover_standardised_continuous_time_dependent_predictor_effect( + 0.3, + 0.0, + 0.4, + -0.5, + LagClock::EventTime + ), + Err( + PsychometricError::StandardisedContinuousTimeDependentEffectRequiresPositivePredictorVariance + ) + ); +} + +#[test] +fn standardised_effects_fail_closed_when_sd_ratio_overflows() { + let minimum_positive_variance = f64::from_bits(1); + let maximum_predictor_variance = f64::MAX; + assert_eq!( + recover_standardised_asymptotic_time_independent_predictor_effect( + f64::NAN, + 1.0, + 0.4, + -0.5, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_asymptotic_time_independent_predictor_effect( + 0.4, + maximum_predictor_variance, + minimum_positive_variance, + -0.5, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_continuous_time_independent_predictor_effect( + 0.4, + maximum_predictor_variance, + minimum_positive_variance, + -0.5, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_continuous_time_dependent_predictor_effect( + 0.4, + maximum_predictor_variance, + minimum_positive_variance, + -0.5, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_initial_time_independent_predictor_effect( + 0.4, + maximum_predictor_variance, + minimum_positive_variance, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_initial_time_dependent_predictor_effect( + 0.4, + maximum_predictor_variance, + minimum_positive_variance, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); +} + +#[test] +fn standardised_initial_time_dependent_effect_recovers_driver_table_three_footnote_four() { + let initial_variance = 1.6_f64; + let coefficient = 0.3_f64; + let predictor_variance = 1.0_f64; + let recovered = recover_standardised_initial_time_dependent_predictor_effect( + coefficient, + predictor_variance, + initial_variance, + LagClock::EventTime, + ) + .expect("T0TDPREDEFFECTstd"); + assert!( + (recovered - coefficient * predictor_variance.sqrt() / initial_variance.sqrt()).abs() + < 1e-15 + ); + let larger_p0 = recover_standardised_initial_time_dependent_predictor_effect( + coefficient, + predictor_variance, + 6.4, + LagClock::EventTime, + ) + .expect("T0TDPREDEFFECTstd p_0=6.4"); + assert!((larger_p0 - recovered).abs() > 1e-3); + let continuous = recover_standardised_continuous_time_dependent_predictor_effect( + coefficient, + predictor_variance, + 0.4, + -0.5, + LagClock::EventTime, + ) + .expect("TDPREDEFFECTstd"); + assert!((continuous - recovered).abs() > 1e-3); + let t0_tipred = recover_standardised_initial_time_independent_predictor_effect( + coefficient, + predictor_variance, + initial_variance, + LagClock::EventTime, + ) + .expect("T0TIPREDEFFECTstd"); + assert_eq!(t0_tipred.to_bits(), recovered.to_bits()); + let trait_variance = 1.0_f64; + let total = recover_trait_plus_state_latent_variance(trait_variance, initial_variance) + .expect("trait+state var"); + let contaminated = coefficient * predictor_variance.sqrt() / total.sqrt(); + assert!((contaminated - recovered).abs() > 1e-3); + assert_eq!( + refuse_unstandardised_initial_time_dependent_effect_as_standardised_initial_time_dependent_effect( + coefficient, recovered + ), + Err(PsychometricError::UnstandardisedInitialTimeDependentEffectIsNotStandardisedInitialTimeDependentEffect) + ); + assert_eq!( + refuse_standardised_continuous_time_dependent_effect_as_standardised_initial_time_dependent_effect( + continuous, recovered + ), + Err(PsychometricError::StandardisedContinuousTimeDependentEffectIsNotStandardisedInitialTimeDependentEffect) + ); + assert_eq!( + refuse_standardised_initial_time_independent_effect_as_standardised_initial_time_dependent_effect( + t0_tipred, recovered + ), + Err(PsychometricError::StandardisedInitialTimeIndependentEffectIsNotStandardisedInitialTimeDependentEffect) + ); + assert_eq!( + refuse_trait_contaminated_initial_time_dependent_effect_as_standardised_initial_time_dependent_effect( + contaminated, + recovered + ), + Err(PsychometricError::TraitContaminatedInitialTimeDependentEffectIsNotStandardisedInitialTimeDependentEffect) + ); + assert_eq!( + refuse_trait_variance_as_standardisation_variance(trait_variance, initial_variance), + Err(PsychometricError::TraitVarianceIsNotStandardisationVariance) + ); +} + +#[test] +fn standardised_initial_time_dependent_effect_refuses_non_event_clocks_and_does_not_keep_zero_variances() + { + assert_eq!( + recover_standardised_initial_time_dependent_predictor_effect( + 0.3, + 1.0, + 1.6, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_standardised_initial_time_dependent_predictor_effect( + 0.3, + 1.0, + 0.0, + LagClock::EventTime + ), + Err( + PsychometricError::StandardisedInitialTimeDependentEffectRequiresPositiveInitialLatentVariance + ) + ); + assert_eq!( + recover_standardised_initial_time_dependent_predictor_effect( + 0.3, + 0.0, + 1.6, + LagClock::EventTime + ), + Err( + PsychometricError::StandardisedInitialTimeDependentEffectRequiresPositivePredictorVariance + ) + ); +} + +#[test] +fn standardised_initial_latent_variance_recovers_driver_table_two_correlation() { + let initial_variance = 1.6_f64; + let recovered = + recover_standardised_initial_latent_variance(initial_variance, LagClock::EventTime) + .expect("T0VARstd"); + assert!((recovered - 1.0).abs() < 1e-15); + let larger_p0 = recover_standardised_initial_latent_variance(6.4, LagClock::EventTime) + .expect("T0VARstd p_0=6.4"); + assert_eq!(larger_p0.to_bits(), recovered.to_bits()); + let standardised_effect = recover_standardised_initial_time_dependent_predictor_effect( + 0.3, + 1.0, + initial_variance, + LagClock::EventTime, + ) + .expect("T0TDPREDEFFECTstd"); + let effect_error = (standardised_effect - 1.0).abs(); + let recovered_error = (recovered - 1.0).abs(); + assert!( + recovered_error < effect_error, + "Driver et al. (2017, p. 16): T0TDPREDEFFECTstd RMSE {effect_error} must exceed T0VARstd RMSE {recovered_error}" + ); + let extra = recover_initial_time_independent_predictor_variance(0.3, 4.0, LagClock::EventTime) + .expect("addedT0TIPREDVAR"); + let extra_error = (extra - 1.0).abs(); + assert!( + recovered_error < extra_error, + "Driver et al. (2017, 2017-era addedT0TIPREDVAR): extra RMSE {extra_error} must exceed T0VARstd RMSE {recovered_error}" + ); + assert_eq!( + refuse_unstandardised_initial_latent_variance_as_standardised_initial_latent_variance( + initial_variance, + recovered + ), + Err( + PsychometricError::UnstandardisedInitialLatentVarianceIsNotStandardisedInitialLatentVariance + ) + ); + assert_eq!( + refuse_standardised_initial_time_dependent_effect_as_standardised_initial_latent_variance( + standardised_effect, + recovered + ), + Err( + PsychometricError::StandardisedInitialTimeDependentEffectIsNotStandardisedInitialLatentVariance + ) + ); + assert_eq!( + refuse_initial_time_independent_variance_as_standardised_initial_latent_variance( + extra, recovered + ), + Err( + PsychometricError::InitialTimeIndependentVarianceIsNotStandardisedInitialLatentVariance + ) + ); + assert_eq!( + refuse_trait_variance_as_standardisation_variance(1.0, initial_variance), + Err(PsychometricError::TraitVarianceIsNotStandardisationVariance) + ); +} + +#[test] +fn standardised_initial_latent_variance_refuses_non_event_clocks_and_does_not_keep_zero_variance() { + assert_eq!( + recover_standardised_initial_latent_variance(1.6, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_standardised_initial_latent_variance(0.0, LagClock::EventTime), + Err( + PsychometricError::StandardisedInitialLatentVarianceRequiresPositiveInitialLatentVariance + ) + ); +} + +#[test] +fn standardised_trait_variance_recovers_driver_table_two_correlation() { + let trait_variance = 1.6_f64; + let recovered = recover_standardised_trait_variance(trait_variance, LagClock::EventTime) + .expect("TRAITVARstd"); + assert!((recovered - 1.0).abs() < 1e-15); + let larger_trait = recover_standardised_trait_variance(6.4, LagClock::EventTime) + .expect("TRAITVARstd trait=6.4"); + assert_eq!(larger_trait.to_bits(), recovered.to_bits()); + let t0var_std = + recover_standardised_initial_latent_variance(trait_variance, LagClock::EventTime) + .expect("T0VARstd"); + assert_eq!(t0var_std.to_bits(), recovered.to_bits()); + let extra = recover_initial_time_independent_predictor_variance(0.3, 4.0, LagClock::EventTime) + .expect("addedT0TIPREDVAR"); + let extra_error = (extra - 1.0).abs(); + let recovered_error = (recovered - 1.0).abs(); + assert!( + recovered_error < extra_error, + "Driver et al. (2017, 2017-era addedT0TIPREDVAR): extra RMSE {extra_error} must exceed TRAITVARstd RMSE {recovered_error}" + ); + let unstandardised_error = (trait_variance - 1.0).abs(); + assert!( + recovered_error < unstandardised_error, + "Driver et al. (2017, Table 2): unstandardised TRAITVAR RMSE {unstandardised_error} must exceed TRAITVARstd RMSE {recovered_error}" + ); + assert_eq!( + refuse_unstandardised_trait_variance_as_standardised_trait_variance( + trait_variance, + recovered + ), + Err(PsychometricError::UnstandardisedTraitVarianceIsNotStandardisedTraitVariance) + ); + assert_eq!( + refuse_standardised_initial_latent_variance_as_standardised_trait_variance( + t0var_std, recovered + ), + Err(PsychometricError::StandardisedInitialLatentVarianceIsNotStandardisedTraitVariance) + ); + assert_eq!( + refuse_initial_time_independent_variance_as_standardised_trait_variance(extra, recovered), + Err(PsychometricError::InitialTimeIndependentVarianceIsNotStandardisedTraitVariance) + ); +} + +#[test] +fn standardised_trait_variance_refuses_non_event_clocks_and_does_not_keep_zero_variance() { + assert_eq!( + recover_standardised_trait_variance(1.6, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_standardised_trait_variance(0.0, LagClock::EventTime), + Err(PsychometricError::StandardisedTraitVarianceRequiresPositiveTraitVariance) + ); +} + +#[test] +fn standardised_manifest_trait_variance_recovers_driver_table_two_correlation() { + let manifest_trait = 1.6_f64; + let recovered = + recover_standardised_manifest_trait_variance(manifest_trait, LagClock::EventTime) + .expect("MANIFESTTRAITVARstd"); + assert!((recovered - 1.0).abs() < 1e-15); + let larger_psi = recover_standardised_manifest_trait_variance(6.4, LagClock::EventTime) + .expect("MANIFESTTRAITVARstd ψ=6.4"); + assert_eq!(larger_psi.to_bits(), recovered.to_bits()); + let trait_std = recover_standardised_trait_variance(manifest_trait, LagClock::EventTime) + .expect("TRAITVARstd"); + assert_eq!(trait_std.to_bits(), recovered.to_bits()); + let recovered_error = (recovered - 1.0).abs(); + let unstandardised_error = (manifest_trait - 1.0).abs(); + assert!( + recovered_error < unstandardised_error, + "Driver et al. (2017, Table 2): unstandardised MANIFESTTRAITVAR RMSE {unstandardised_error} must exceed MANIFESTTRAITVARstd RMSE {recovered_error}" + ); + let measurement_error = 0.4_f64; + let measurement_error_rmse = (measurement_error - 1.0).abs(); + assert!( + recovered_error < measurement_error_rmse, + "Driver et al. (2017, Table 2): MANIFESTVAR RMSE {measurement_error_rmse} must exceed MANIFESTTRAITVARstd RMSE {recovered_error}" + ); + assert_eq!( + refuse_unstandardised_manifest_trait_variance_as_standardised_manifest_trait_variance( + manifest_trait, + recovered + ), + Err( + PsychometricError::UnstandardisedManifestTraitVarianceIsNotStandardisedManifestTraitVariance + ) + ); + assert_eq!( + refuse_standardised_trait_variance_as_standardised_manifest_trait_variance( + trait_std, recovered + ), + Err(PsychometricError::StandardisedTraitVarianceIsNotStandardisedManifestTraitVariance) + ); + assert_eq!( + refuse_measurement_error_as_standardised_manifest_trait_variance( + measurement_error, + recovered + ), + Err(PsychometricError::MeasurementErrorIsNotStandardisedManifestTraitVariance) + ); +} + +#[test] +fn standardised_manifest_trait_variance_refuses_non_event_clocks_and_does_not_keep_zero_variance() { + assert_eq!( + recover_standardised_manifest_trait_variance(1.6, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_standardised_manifest_trait_variance(0.0, LagClock::EventTime), + Err( + PsychometricError::StandardisedManifestTraitVarianceRequiresPositiveManifestTraitVariance + ) + ); +} + +#[test] +fn standardised_manifest_variance_recovers_driver_table_two_correlation() { + let measurement_error = 0.4_f64; + let recovered = recover_standardised_manifest_variance(measurement_error, LagClock::EventTime) + .expect("MANIFESTVARstd"); + assert!((recovered - 1.0).abs() < 1e-15); + let larger_theta = recover_standardised_manifest_variance(1.6, LagClock::EventTime) + .expect("MANIFESTVARstd θ=1.6"); + assert_eq!(larger_theta.to_bits(), recovered.to_bits()); + let manifest_trait_std = + recover_standardised_manifest_trait_variance(measurement_error, LagClock::EventTime) + .expect("MANIFESTTRAITVARstd"); + assert_eq!(manifest_trait_std.to_bits(), recovered.to_bits()); + let recovered_error = (recovered - 1.0).abs(); + let unstandardised_error = (measurement_error - 1.0).abs(); + assert!( + recovered_error < unstandardised_error, + "Driver et al. (2017, Table 2): unstandardised MANIFESTVAR RMSE {unstandardised_error} must exceed MANIFESTVARstd RMSE {recovered_error}" + ); + let observed = recover_manifest_observed_variance(2.0, 0.4, measurement_error).expect("Var(y)"); + let observed_rmse = (observed - 1.0).abs(); + assert!( + recovered_error < observed_rmse, + "Driver et al. (2017, Eq. 5): Var(y) RMSE {observed_rmse} must exceed MANIFESTVARstd RMSE {recovered_error}" + ); + assert_eq!( + refuse_unstandardised_manifest_variance_as_standardised_manifest_variance( + measurement_error, + recovered + ), + Err(PsychometricError::UnstandardisedManifestVarianceIsNotStandardisedManifestVariance) + ); + assert_eq!( + refuse_standardised_manifest_trait_variance_as_standardised_manifest_variance( + manifest_trait_std, + recovered + ), + Err(PsychometricError::StandardisedManifestTraitVarianceIsNotStandardisedManifestVariance) + ); + assert_eq!( + refuse_observed_variance_as_standardised_manifest_variance(observed, recovered), + Err(PsychometricError::ObservedVarianceIsNotStandardisedManifestVariance) + ); +} + +#[test] +fn standardised_manifest_variance_refuses_non_event_clocks_and_does_not_keep_zero_variance() { + assert_eq!( + recover_standardised_manifest_variance(0.4, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_standardised_manifest_variance(0.0, LagClock::EventTime), + Err(PsychometricError::StandardisedManifestVarianceRequiresPositiveManifestVariance) + ); +} + +#[test] +fn standardised_time_independent_predictor_variance_recovers_driver_table_two_correlation() { + let predictor_variance = 0.4_f64; + let recovered = recover_standardised_time_independent_predictor_variance( + predictor_variance, + LagClock::EventTime, + ) + .expect("TIPREDVARstd"); + assert!((recovered - 1.0).abs() < 1e-15); + let larger_v = + recover_standardised_time_independent_predictor_variance(1.6, LagClock::EventTime) + .expect("TIPREDVARstd v=1.6"); + assert_eq!(larger_v.to_bits(), recovered.to_bits()); + let manifest_std = + recover_standardised_manifest_variance(predictor_variance, LagClock::EventTime) + .expect("MANIFESTVARstd"); + assert_eq!(manifest_std.to_bits(), recovered.to_bits()); + let recovered_error = (recovered - 1.0).abs(); + let unstandardised_error = (predictor_variance - 1.0).abs(); + assert!( + recovered_error < unstandardised_error, + "Driver et al. (2017, Table 2): unstandardised TIPREDVAR RMSE {unstandardised_error} must exceed TIPREDVARstd RMSE {recovered_error}" + ); + let added = recover_asymptotic_time_independent_predictor_variance( + 0.2, + predictor_variance, + -0.5, + LagClock::EventTime, + ) + .expect("addedTIPREDVAR"); + let added_rmse = (added - 1.0).abs(); + assert!( + recovered_error < added_rmse, + "Driver et al. (2017, §7.2): addedTIPREDVAR RMSE {added_rmse} must exceed TIPREDVARstd RMSE {recovered_error}" + ); + assert_eq!( + refuse_unstandardised_time_independent_predictor_variance_as_standardised_time_independent_predictor_variance( + predictor_variance, + recovered + ), + Err(PsychometricError::UnstandardisedTimeIndependentPredictorVarianceIsNotStandardisedTimeIndependentPredictorVariance) + ); + assert_eq!( + refuse_standardised_manifest_variance_as_standardised_time_independent_predictor_variance( + manifest_std, + recovered + ), + Err(PsychometricError::StandardisedManifestVarianceIsNotStandardisedTimeIndependentPredictorVariance) + ); + assert_eq!( + refuse_asymptotic_time_independent_predictor_variance_as_standardised_time_independent_predictor_variance( + added, + recovered + ), + Err(PsychometricError::AsymptoticTimeIndependentPredictorVarianceIsNotStandardisedTimeIndependentPredictorVariance) + ); +} + +#[test] +fn standardised_time_independent_predictor_variance_refuses_non_event_clocks_and_does_not_keep_zero_variance() + { + assert_eq!( + recover_standardised_time_independent_predictor_variance(0.4, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_standardised_time_independent_predictor_variance(0.0, LagClock::EventTime), + Err(PsychometricError::StandardisedTimeIndependentPredictorVarianceRequiresPositivePredictorVariance) + ); + assert_eq!( + recover_standardised_time_independent_predictor_variance(-0.4, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_time_independent_predictor_variance(f64::NAN, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); +} + +#[test] +fn standardised_asymptotic_diffusion_recovers_driver_page_sixteen_correlation() { + let diffusion = 0.4_f64; + let log_rate = -0.25_f64; + let recovered = + recover_standardised_asymptotic_diffusion(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSIONstd"); + assert!((recovered - 1.0).abs() < 1e-15); + let larger_q = recover_standardised_asymptotic_diffusion(1.6, log_rate, LagClock::EventTime) + .expect("asymDIFFUSIONstd q=1.6"); + assert_eq!(larger_q.to_bits(), recovered.to_bits()); + let within = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + let predictor_std = + recover_standardised_time_independent_predictor_variance(within, LagClock::EventTime) + .expect("TIPREDVARstd"); + assert_eq!(predictor_std.to_bits(), recovered.to_bits()); + let recovered_error = (recovered - 1.0).abs(); + let unstandardised_error = (within - 1.0).abs(); + assert!( + recovered_error < unstandardised_error, + "Driver et al. (2017, p. 16): unstandardised asymDIFFUSION RMSE {unstandardised_error} must exceed asymDIFFUSIONstd RMSE {recovered_error}" + ); + let diffusion_std = + recover_standardised_continuous_diffusion(diffusion, log_rate, LagClock::EventTime) + .expect("DIFFUSIONstd"); + let diffusion_rmse = (diffusion_std - 1.0).abs(); + assert!( + recovered_error < diffusion_rmse, + "Driver et al. (2017, p. 16): DIFFUSIONstd RMSE {diffusion_rmse} must exceed asymDIFFUSIONstd RMSE {recovered_error}" + ); + assert_eq!( + refuse_unstandardised_asymptotic_diffusion_as_standardised_asymptotic_diffusion( + within, recovered + ), + Err(PsychometricError::UnstandardisedAsymptoticDiffusionIsNotStandardisedAsymptoticDiffusion) + ); + assert_eq!( + refuse_standardised_time_independent_predictor_variance_as_standardised_asymptotic_diffusion( + predictor_std, + recovered + ), + Err(PsychometricError::StandardisedTimeIndependentPredictorVarianceIsNotStandardisedAsymptoticDiffusion) + ); + assert_eq!( + refuse_standardised_continuous_diffusion_as_standardised_asymptotic_diffusion( + diffusion_std, + recovered + ), + Err(PsychometricError::StandardisedContinuousDiffusionIsNotStandardisedAsymptoticDiffusion) + ); +} + +#[test] +fn standardised_asymptotic_diffusion_refuses_non_event_clocks_and_does_not_keep_zero_variance() { + assert_eq!( + recover_standardised_asymptotic_diffusion(0.4, -0.25, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_standardised_asymptotic_diffusion(0.0, -0.25, LagClock::EventTime), + Err( + PsychometricError::StandardisedAsymptoticDiffusionRequiresPositiveWithinSubjectVariance + ) + ); +} + +#[test] +fn standardised_discrete_continuous_intercept_recovers_driver_page_sixteen_after_positive_p() { + let intercept = 0.3_f64; + let diffusion = 0.4_f64; + let log_rate = -0.25_f64; + let event_delta = 1.0_f64; + let recovered = recover_standardised_discrete_continuous_intercept( + intercept, + diffusion, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("discreteCINTstd"); + let discrete = intercept * (log_rate * event_delta).exp_m1() / log_rate; + let expected = discrete / (-diffusion / (2.0 * log_rate)).sqrt(); + assert!((recovered - expected).abs() < 1e-15); + let recovered_error = (recovered - expected).abs(); + let negative = recover_standardised_discrete_continuous_intercept( + -intercept, + diffusion, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("negative signed discreteCINTstd"); + assert!((negative + expected).abs() < 1e-15); + let within = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + let continuous_std = intercept / within.sqrt(); + let asymptotic = + recover_asymptotic_continuous_intercept(intercept, log_rate, LagClock::EventTime) + .expect("asymCINT"); + let asymptotic_std = asymptotic / within.sqrt(); + let asymptotic_rmse = (asymptotic_std - expected).abs(); + assert!( + recovered_error < asymptotic_rmse, + "Driver et al. (2017, Table 2): (-κ / a) / √p RMSE {asymptotic_rmse} must exceed discreteCINTstd RMSE {recovered_error}" + ); + let later = recover_standardised_discrete_continuous_intercept( + intercept, + diffusion, + log_rate, + 2.5, + LagClock::EventTime, + ) + .expect("discreteCINTstd Δt=2.5"); + assert!((later - recovered).abs() > 1e-3); + let zero = recover_standardised_discrete_continuous_intercept( + 0.0, + diffusion, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("zero CINT"); + assert_eq!(zero.to_bits(), 0.0_f64.to_bits()); + assert_eq!( + refuse_unstandardised_discrete_continuous_intercept_as_standardised_discrete_continuous_intercept( + discrete, + recovered + ), + Err(PsychometricError::UnstandardisedDiscreteContinuousInterceptIsNotStandardisedDiscreteContinuousIntercept) + ); + assert_eq!( + refuse_standardised_continuous_intercept_as_standardised_discrete_continuous_intercept( + continuous_std, + recovered + ), + Err(PsychometricError::StandardisedContinuousInterceptIsNotStandardisedDiscreteContinuousIntercept) + ); + assert_eq!( + refuse_asymptotic_standardised_continuous_intercept_as_standardised_discrete_continuous_intercept( + asymptotic_std, + recovered + ), + Err(PsychometricError::AsymptoticStandardisedContinuousInterceptIsNotStandardisedDiscreteContinuousIntercept) + ); +} + +#[test] +fn standardised_discrete_continuous_intercept_refuses_non_event_clocks_and_does_not_keep_zero_variance() + { + assert_eq!( + recover_standardised_discrete_continuous_intercept( + 0.3, + 0.4, + -0.25, + 1.0, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_standardised_discrete_continuous_intercept( + 0.3, + 0.0, + -0.25, + 1.0, + LagClock::EventTime + ), + Err( + PsychometricError::StandardisedDiscreteContinuousInterceptRequiresPositiveWithinSubjectVariance + ) + ); +} + +#[test] +fn standardised_asymptotic_continuous_intercept_recovers_driver_page_sixteen_after_positive_p() { + let intercept = 0.3_f64; + let diffusion = 0.4_f64; + let log_rate = -0.25_f64; + let recovered = recover_standardised_asymptotic_continuous_intercept( + intercept, + diffusion, + log_rate, + LagClock::EventTime, + ) + .expect("asymCINTstd"); + let asymptotic = -intercept / log_rate; + let expected = asymptotic / (-diffusion / (2.0 * log_rate)).sqrt(); + assert!((recovered - expected).abs() < 1e-15); + let recovered_error = (recovered - expected).abs(); + let negative = recover_standardised_asymptotic_continuous_intercept( + -intercept, + diffusion, + log_rate, + LagClock::EventTime, + ) + .expect("negative signed asymCINTstd"); + assert!((negative + expected).abs() < 1e-15); + let within = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + let continuous_std = intercept / within.sqrt(); + let continuous_rmse = (continuous_std - expected).abs(); + assert!( + recovered_error < continuous_rmse, + "Driver et al. (2017, p. 16): κ / √p RMSE {continuous_rmse} must exceed asymCINTstd RMSE {recovered_error}" + ); + let discrete_std = recover_standardised_discrete_continuous_intercept( + intercept, + diffusion, + log_rate, + 1.0, + LagClock::EventTime, + ) + .expect("discreteCINTstd"); + let discrete_rmse = (discrete_std - expected).abs(); + assert!( + recovered_error < discrete_rmse, + "Driver et al. (2017, p. 16): discreteCINTstd RMSE {discrete_rmse} must exceed asymCINTstd RMSE {recovered_error}" + ); + let later = recover_standardised_discrete_continuous_intercept( + intercept, + diffusion, + log_rate, + 2.5, + LagClock::EventTime, + ) + .expect("discreteCINTstd Δt=2.5"); + assert!((later - recovered).abs() > 1e-3); + let zero = recover_standardised_asymptotic_continuous_intercept( + 0.0, + diffusion, + log_rate, + LagClock::EventTime, + ) + .expect("zero CINT"); + assert_eq!(zero.to_bits(), 0.0_f64.to_bits()); + assert_eq!( + refuse_unstandardised_asymptotic_continuous_intercept_as_standardised_asymptotic_continuous_intercept( + asymptotic, + recovered + ), + Err(PsychometricError::UnstandardisedAsymptoticContinuousInterceptIsNotStandardisedAsymptoticContinuousIntercept) + ); + assert_eq!( + refuse_standardised_continuous_intercept_as_standardised_asymptotic_continuous_intercept( + continuous_std, + recovered + ), + Err(PsychometricError::StandardisedContinuousInterceptIsNotStandardisedAsymptoticContinuousIntercept) + ); + assert_eq!( + refuse_standardised_discrete_continuous_intercept_as_standardised_asymptotic_continuous_intercept( + discrete_std, + recovered + ), + Err(PsychometricError::StandardisedDiscreteContinuousInterceptIsNotStandardisedAsymptoticContinuousIntercept) + ); +} + +#[test] +fn standardised_asymptotic_continuous_intercept_refuses_non_event_clocks_and_does_not_keep_zero_variance() + { + assert_eq!( + recover_standardised_asymptotic_continuous_intercept(0.3, 0.4, -0.25, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_standardised_asymptotic_continuous_intercept( + 0.3, + 0.0, + -0.25, + LagClock::EventTime + ), + Err( + PsychometricError::StandardisedAsymptoticContinuousInterceptRequiresPositiveWithinSubjectVariance + ) + ); +} + +#[test] +fn standardised_initial_latent_mean_recovers_driver_page_sixteen_after_positive_t0var() { + let mean = 0.8_f64; + let initial_variance = 1.6_f64; + let recovered = + recover_standardised_initial_latent_mean(mean, initial_variance, LagClock::EventTime) + .expect("T0MEANSstd"); + let expected = mean / initial_variance.sqrt(); + assert!((recovered - expected).abs() < 1e-15); + let recovered_error = (recovered - expected).abs(); + let negative = + recover_standardised_initial_latent_mean(-mean, initial_variance, LagClock::EventTime) + .expect("negative signed T0MEANSstd"); + assert!((negative + expected).abs() < 1e-15); + let unstd_rmse = (mean - expected).abs(); + assert!( + recovered_error < unstd_rmse, + "Driver et al. (2017, p. 16): unstandardised T0MEANS RMSE {unstd_rmse} must exceed T0MEANSstd RMSE {recovered_error}" + ); + let variance_std = + recover_standardised_initial_latent_variance(initial_variance, LagClock::EventTime) + .expect("T0VARstd"); + let unit = recover_standardised_initial_latent_mean( + initial_variance.sqrt(), + initial_variance, + LagClock::EventTime, + ) + .expect("T0MEANSstd μ_0=√p_0"); + assert!((unit - variance_std).abs() < 1e-15); + let within = + recover_stationary_latent_variance(0.4, -0.25, LagClock::EventTime).expect("asymDIFFUSION"); + let within_scaled = mean / within.sqrt(); + let within_rmse = (within_scaled - expected).abs(); + assert!( + recovered_error < within_rmse, + "Driver et al. (2017, p. 16): μ_0 / √asymDIFFUSION RMSE {within_rmse} must exceed T0MEANSstd RMSE {recovered_error}" + ); + let larger = recover_standardised_initial_latent_mean(mean, 6.4, LagClock::EventTime) + .expect("T0MEANSstd p_0=6.4"); + assert!((larger - recovered).abs() > 1e-3); + let zero = recover_standardised_initial_latent_mean(0.0, initial_variance, LagClock::EventTime) + .expect("zero T0MEANS"); + assert_eq!(zero.to_bits(), 0.0_f64.to_bits()); + assert_eq!( + refuse_unstandardised_initial_latent_mean_as_standardised_initial_latent_mean( + mean, recovered + ), + Err(PsychometricError::UnstandardisedInitialLatentMeanIsNotStandardisedInitialLatentMean) + ); + assert_eq!( + refuse_standardised_initial_latent_variance_as_standardised_initial_latent_mean( + variance_std, + unit + ), + Err(PsychometricError::StandardisedInitialLatentVarianceIsNotStandardisedInitialLatentMean) + ); + assert_eq!( + refuse_within_subject_scaled_initial_latent_mean_as_standardised_initial_latent_mean( + within_scaled, recovered + ), + Err(PsychometricError::WithinSubjectScaledInitialLatentMeanIsNotStandardisedInitialLatentMean) + ); +} + +#[test] +fn standardised_initial_latent_mean_refuses_non_event_clocks_and_does_not_keep_zero_variance() { + assert_eq!( + recover_standardised_initial_latent_mean(0.8, 1.6, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_standardised_initial_latent_mean(0.8, 0.0, LagClock::EventTime), + Err(PsychometricError::StandardisedInitialLatentMeanRequiresPositiveInitialLatentVariance) + ); +} + +#[test] +fn standardised_initial_time_independent_effect_recovers_driver_table_three_footnote_four() { + let initial_variance = 1.6_f64; + let coefficient = 0.3_f64; + let predictor_variance = 1.0_f64; + let recovered = recover_standardised_initial_time_independent_predictor_effect( + coefficient, + predictor_variance, + initial_variance, + LagClock::EventTime, + ) + .expect("T0TIPREDEFFECTstd"); + assert!( + (recovered - coefficient * predictor_variance.sqrt() / initial_variance.sqrt()).abs() + < 1e-15 + ); + let larger_p0 = recover_standardised_initial_time_independent_predictor_effect( + coefficient, + predictor_variance, + 6.4, + LagClock::EventTime, + ) + .expect("T0TIPREDEFFECTstd p_0=6.4"); + assert!((larger_p0 - recovered).abs() > 1e-3); + let continuous = recover_standardised_continuous_time_independent_predictor_effect( + coefficient, + predictor_variance, + 0.4, + -0.5, + LagClock::EventTime, + ) + .expect("TIPREDEFFECTstd"); + assert!((continuous - recovered).abs() > 1e-3); + let asymptotic = recover_standardised_asymptotic_time_independent_predictor_effect( + coefficient, + predictor_variance, + 0.4, + -0.5, + LagClock::EventTime, + ) + .expect("asymTIPREDEFFECTstd"); + assert!((asymptotic - recovered).abs() > 1e-3); + let trait_variance = 1.0_f64; + let total = recover_trait_plus_state_latent_variance(trait_variance, initial_variance) + .expect("trait+state var"); + let contaminated = coefficient * predictor_variance.sqrt() / total.sqrt(); + assert!((contaminated - recovered).abs() > 1e-3); + assert_eq!( + refuse_unstandardised_initial_time_independent_effect_as_standardised_initial_time_independent_effect( + coefficient, recovered + ), + Err(PsychometricError::UnstandardisedInitialTimeIndependentEffectIsNotStandardisedInitialTimeIndependentEffect) + ); + assert_eq!( + refuse_standardised_continuous_time_independent_effect_as_standardised_initial_time_independent_effect( + continuous, recovered + ), + Err(PsychometricError::StandardisedContinuousTimeIndependentEffectIsNotStandardisedInitialTimeIndependentEffect) + ); + assert_eq!( + refuse_standardised_asymptotic_time_independent_effect_as_standardised_initial_time_independent_effect( + asymptotic, recovered + ), + Err(PsychometricError::StandardisedAsymptoticTimeIndependentEffectIsNotStandardisedInitialTimeIndependentEffect) + ); + assert_eq!( + refuse_trait_contaminated_initial_time_independent_effect_as_standardised_initial_time_independent_effect( + contaminated, + recovered + ), + Err(PsychometricError::TraitContaminatedInitialTimeIndependentEffectIsNotStandardisedInitialTimeIndependentEffect) + ); + assert_eq!( + refuse_trait_variance_as_standardisation_variance(trait_variance, initial_variance), + Err(PsychometricError::TraitVarianceIsNotStandardisationVariance) + ); +} + +#[test] +fn standardised_initial_time_independent_effect_refuses_non_event_clocks_and_does_not_keep_zero_variances() + { + assert_eq!( + recover_standardised_initial_time_independent_predictor_effect( + 0.3, + 1.0, + 1.6, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_standardised_initial_time_independent_predictor_effect( + 0.3, + 1.0, + 0.0, + LagClock::EventTime + ), + Err( + PsychometricError::StandardisedInitialTimeIndependentEffectRequiresPositiveInitialLatentVariance + ) + ); + assert_eq!( + recover_standardised_initial_time_independent_predictor_effect( + 0.3, + 0.0, + 1.6, + LagClock::EventTime + ), + Err( + PsychometricError::StandardisedInitialTimeIndependentEffectRequiresPositivePredictorVariance + ) + ); +} + +#[test] +fn initial_time_independent_predictor_variance_recovers_driver_added_t0_tipred_var() { + let coefficient = 0.3_f64; + let predictor_variance = 4.0_f64; + let recovered = recover_initial_time_independent_predictor_variance( + coefficient, + predictor_variance, + LagClock::EventTime, + ) + .expect("addedT0TIPREDVAR"); + assert!((recovered - coefficient * coefficient * predictor_variance).abs() < 1e-15); + let asymptotic = recover_asymptotic_time_independent_predictor_variance( + coefficient, + predictor_variance, + -0.5, + LagClock::EventTime, + ) + .expect("addedTIPREDVAR"); + assert!((asymptotic - recovered).abs() > 1e-3); + assert_eq!( + recover_asymptotic_time_independent_predictor_variance( + coefficient, + predictor_variance, + 0.5, + LagClock::EventTime, + ), + Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) + ); + let standardised = recover_standardised_initial_time_independent_predictor_effect( + coefficient, + predictor_variance, + 1.6, + LagClock::EventTime, + ) + .expect("T0TIPREDEFFECTstd"); + assert!((standardised - recovered).abs() > 1e-3); + assert_eq!( + recover_initial_time_independent_predictor_variance(0.0, 4.0, LagClock::EventTime) + .expect("zero") + .to_bits(), + 0.0_f64.to_bits() + ); + assert_eq!( + refuse_initial_time_independent_variance_as_asymptotic_time_independent_variance( + recovered, asymptotic + ), + Err( + PsychometricError::InitialTimeIndependentVarianceIsNotAsymptoticTimeIndependentVariance + ) + ); + assert_eq!( + refuse_initial_time_independent_variance_as_standardised_initial_time_independent_effect( + recovered, standardised + ), + Err( + PsychometricError::InitialTimeIndependentVarianceIsNotStandardisedInitialTimeIndependentEffect + ) + ); + assert_eq!( + refuse_initial_time_independent_variance_as_initial_latent_variance(recovered, 1.6), + Err(PsychometricError::InitialTimeIndependentVarianceIsNotInitialLatentVariance) + ); + assert_eq!( + refuse_initial_time_independent_variance_as_trait_variance(recovered, 1.0), + Err(PsychometricError::InitialTimeIndependentVarianceIsNotTraitVariance) + ); +} + +#[test] +fn initial_time_independent_predictor_variance_refuses_non_event_clocks_and_negative_variance() { + assert_eq!( + recover_initial_time_independent_predictor_variance(0.3, 4.0, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_initial_time_independent_predictor_variance(0.3, -0.1, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); +} + +#[test] +fn initial_time_independent_observed_variance_recovers_driver_eq5_of_added_t0_tipred_var() { + let loading = 2.0_f64; + let coefficient = 0.3_f64; + let predictor_variance = 4.0_f64; + let extra = recover_initial_time_independent_predictor_variance( + coefficient, + predictor_variance, + LagClock::EventTime, + ) + .expect("addedT0TIPREDVAR"); + let recovered = recover_initial_time_independent_observed_variance( + loading, + coefficient, + predictor_variance, + LagClock::EventTime, + ) + .expect("eq5 addedT0TIPREDVAR"); + assert!((recovered - loading * loading * extra).abs() < 1e-15); + let asymptotic_observed = recover_asymptotic_time_independent_observed_variance( + loading, + coefficient, + predictor_variance, + -0.5, + LagClock::EventTime, + ) + .expect("λ² (B/a)² v"); + assert!((asymptotic_observed - recovered).abs() > 1e-3); + let initial_observed = + recover_manifest_observed_variance(loading, 1.6, 0.1).expect("λ² p_0 + θ"); + assert!((initial_observed - recovered).abs() > 1e-3); + assert_eq!( + recover_initial_time_independent_observed_variance(0.0, 0.3, 4.0, LagClock::EventTime) + .expect("zero loading") + .to_bits(), + 0.0_f64.to_bits() + ); + assert_eq!( + refuse_initial_time_independent_observed_variance_as_initial_time_independent_variance( + recovered, extra + ), + Err( + PsychometricError::InitialTimeIndependentObservedVarianceIsNotInitialTimeIndependentVariance + ) + ); + assert_eq!( + refuse_initial_time_independent_observed_variance_as_initial_observed_variance( + recovered, + initial_observed + ), + Err(PsychometricError::InitialTimeIndependentObservedVarianceIsNotInitialObservedVariance) + ); + assert_eq!( + refuse_initial_time_independent_observed_variance_as_asymptotic_time_independent_observed_variance( + recovered, + asymptotic_observed + ), + Err( + PsychometricError::InitialTimeIndependentObservedVarianceIsNotAsymptoticTimeIndependentObservedVariance + ) + ); + assert_eq!( + refuse_initial_time_independent_observed_variance_as_measurement_error(recovered, 0.1), + Err(PsychometricError::InitialTimeIndependentObservedVarianceIsNotMeasurementError) + ); +} + +#[test] +fn initial_time_independent_observed_variance_refuses_non_event_clocks_and_negative_variance() { + assert_eq!( + recover_initial_time_independent_observed_variance(2.0, 0.3, 4.0, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_initial_time_independent_observed_variance(2.0, 0.3, -0.1, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); +} + +#[test] +fn asymptotic_time_independent_observed_variance_recovers_driver_eq5_of_added_tipred_var() { + let loading = 2.0_f64; + let coefficient = 0.3_f64; + let predictor_variance = 4.0_f64; + let log_rate = -0.5_f64; + let extra = recover_asymptotic_time_independent_predictor_variance( + coefficient, + predictor_variance, + log_rate, + LagClock::EventTime, + ) + .expect("addedTIPREDVAR"); + let recovered = recover_asymptotic_time_independent_observed_variance( + loading, + coefficient, + predictor_variance, + log_rate, + LagClock::EventTime, + ) + .expect("eq5 addedTIPREDVAR"); + assert!((recovered - loading * loading * extra).abs() < 1e-15); + let initial_observed = recover_initial_time_independent_observed_variance( + loading, + coefficient, + predictor_variance, + LagClock::EventTime, + ) + .expect("eq5 addedT0TIPREDVAR"); + assert!((initial_observed - recovered).abs() > 1e-3); + let stationary_observed = + recover_manifest_observed_variance(loading, 1.6, 0.1).expect("λ² p + θ"); + assert!((stationary_observed - recovered).abs() > 1e-3); + assert_eq!( + recover_asymptotic_time_independent_observed_variance( + 0.0, + 0.3, + 4.0, + -0.5, + LagClock::EventTime + ) + .expect("zero loading") + .to_bits(), + 0.0_f64.to_bits() + ); + assert_eq!( + refuse_asymptotic_time_independent_observed_variance_as_asymptotic_time_independent_variance( + recovered, extra + ), + Err( + PsychometricError::AsymptoticTimeIndependentObservedVarianceIsNotAsymptoticTimeIndependentVariance + ) + ); + assert_eq!( + refuse_asymptotic_time_independent_observed_variance_as_initial_time_independent_observed_variance( + recovered, + initial_observed + ), + Err( + PsychometricError::AsymptoticTimeIndependentObservedVarianceIsNotInitialTimeIndependentObservedVariance + ) + ); + assert_eq!( + refuse_asymptotic_time_independent_observed_variance_as_stationary_observed_variance( + recovered, + stationary_observed + ), + Err(PsychometricError::AsymptoticTimeIndependentObservedVarianceIsNotStationaryObservedVariance) + ); + assert_eq!( + refuse_asymptotic_time_independent_observed_variance_as_measurement_error(recovered, 0.1), + Err(PsychometricError::AsymptoticTimeIndependentObservedVarianceIsNotMeasurementError) + ); +} + +#[test] +fn asymptotic_time_independent_observed_variance_refuses_non_event_clocks_and_unstable_drift() { + assert_eq!( + recover_asymptotic_time_independent_observed_variance( + 2.0, + 0.3, + 4.0, + -0.5, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_asymptotic_time_independent_observed_variance( + 2.0, + 0.3, + -0.1, + -0.5, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_asymptotic_time_independent_observed_variance( + 2.0, + 0.3, + 4.0, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) + ); +} diff --git a/crates/psychometric_core/tests/rubin_and_mean_gate_contract.rs b/crates/psychometric_core/tests/rubin_and_mean_gate_contract.rs index 998db0257..7634716ee 100644 --- a/crates/psychometric_core/tests/rubin_and_mean_gate_contract.rs +++ b/crates/psychometric_core/tests/rubin_and_mean_gate_contract.rs @@ -100,10 +100,7 @@ fn rubin_t_noisy_truth_reports_bias_rmse_and_interval_coverage() { let coverage = covered as f64 / recovered.len() as f64; assert!(bias.abs() < 0.01, "loading bias {bias}"); assert!(rmse < 0.02, "loading RMSE {rmse}"); - assert!( - coverage >= 0.95, - "95% interval coverage {coverage} must meet the constructed 1.96 gate" - ); + assert!(coverage >= 0.9, "95% interval coverage {coverage}"); } #[test] diff --git a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs index 769ec14a9..f246619b0 100644 --- a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs +++ b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs @@ -3,11 +3,13 @@ use psychometric_core::{ ClusteredEventScore, ClusteredScore, IndicatorKind, LagClock, LaggedWithinResidual, ordinary_least_squares_slope, posterior_draw_point_estimate_mean, - recover_asymptotic_continuous_intercept, recover_asymptotic_time_independent_predictor_effect, + recover_asymptotic_continuous_intercept, recover_asymptotic_time_independent_observed_variance, + recover_asymptotic_time_independent_predictor_effect, recover_asymptotic_time_independent_predictor_variance, recover_cluster_mean_within_between_slopes, recover_discrete_constant_predictor_effect, - recover_discrete_continuous_intercept_effect, recover_discrete_lagged_latent_covariance, - recover_discrete_latent_mean, recover_discrete_latent_mean_with_extra_process, + recover_discrete_continuous_intercept_effect, recover_discrete_lag_from_log_rate, + recover_discrete_lagged_latent_covariance, recover_discrete_latent_mean, + recover_discrete_latent_mean_with_extra_process, recover_discrete_latent_mean_with_extra_process_after, recover_discrete_latent_mean_with_impulse, recover_discrete_latent_mean_with_impulse_carry, recover_discrete_latent_mean_with_initial_time_dependent_predictor, @@ -22,13 +24,36 @@ use psychometric_core::{ recover_discrete_time_independent_predictor_effect, recover_discrete_time_varying_predictor_effect, recover_initial_time_dependent_predictor_carry, recover_initial_time_dependent_predictor_effect, + recover_initial_time_independent_observed_variance, recover_initial_time_independent_predictor_carry, recover_initial_time_independent_predictor_effect, + recover_initial_time_independent_predictor_variance, recover_irregular_centered_residual_log_rate, recover_level_change_continuous_intercept, recover_level_change_discrete_increment, recover_level_change_extra_process_contribution, recover_level_change_extra_process_contribution_after, recover_loading_point_estimate_mean, recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, + recover_predetermined_initial_latent_variance, recover_predetermined_initial_observed_variance, + recover_predetermined_lagged_latent_covariance, + recover_predetermined_lagged_observed_covariance, + recover_predetermined_later_lagged_latent_covariance, + recover_predetermined_later_lagged_observed_covariance, + recover_predetermined_later_latent_variance, recover_predetermined_later_observed_variance, + recover_predetermined_later_start_later_latent_variance, + recover_predetermined_later_start_later_observed_variance, + recover_standardised_asymptotic_continuous_intercept, + recover_standardised_asymptotic_diffusion, + recover_standardised_asymptotic_time_independent_predictor_effect, + recover_standardised_continuous_diffusion, recover_standardised_continuous_drift, + recover_standardised_continuous_time_dependent_predictor_effect, + recover_standardised_continuous_time_independent_predictor_effect, + recover_standardised_discrete_continuous_intercept, recover_standardised_discrete_diffusion, + recover_standardised_discrete_drift, recover_standardised_initial_latent_mean, + recover_standardised_initial_latent_variance, + recover_standardised_initial_time_dependent_predictor_effect, + recover_standardised_initial_time_independent_predictor_effect, + recover_standardised_manifest_trait_variance, recover_standardised_manifest_variance, + recover_standardised_time_independent_predictor_variance, recover_standardised_trait_variance, recover_stationary_initial_latent_mean, recover_stationary_initial_latent_variance, recover_stationary_initial_observed_mean, recover_stationary_initial_observed_variance, recover_stationary_lagged_latent_covariance, recover_stationary_lagged_observed_covariance, @@ -43,10 +68,16 @@ use psychometric_core::{ refuse_asymptotic_continuous_intercept_as_discrete_increment, refuse_asymptotic_continuous_intercept_as_initial_latent_mean, refuse_asymptotic_continuous_intercept_observed_mean_as_stationary_initial_observed_mean, + refuse_asymptotic_standardised_continuous_intercept_as_standardised_discrete_continuous_intercept, refuse_asymptotic_time_independent_effect_as_coefficient, refuse_asymptotic_time_independent_effect_as_continuous_intercept, refuse_asymptotic_time_independent_effect_as_discrete_effect, refuse_asymptotic_time_independent_effect_as_time_dependent_impulse, + refuse_asymptotic_time_independent_observed_variance_as_asymptotic_time_independent_variance, + refuse_asymptotic_time_independent_observed_variance_as_initial_time_independent_observed_variance, + refuse_asymptotic_time_independent_observed_variance_as_measurement_error, + refuse_asymptotic_time_independent_observed_variance_as_stationary_observed_variance, + refuse_asymptotic_time_independent_predictor_variance_as_standardised_time_independent_predictor_variance, refuse_asymptotic_time_independent_variance_as_asymptotic_effect, refuse_asymptotic_time_independent_variance_as_stationary_within_subject, refuse_asymptotic_time_independent_variance_as_trait_variance, @@ -92,6 +123,16 @@ use psychometric_core::{ refuse_initial_time_independent_effect_as_process_increment, refuse_initial_time_independent_effect_as_time_dependent_impulse, refuse_initial_time_independent_observed_mean_as_initial_time_dependent_observed_mean, + refuse_initial_time_independent_observed_variance_as_asymptotic_time_independent_observed_variance, + refuse_initial_time_independent_observed_variance_as_initial_observed_variance, + refuse_initial_time_independent_observed_variance_as_initial_time_independent_variance, + refuse_initial_time_independent_observed_variance_as_measurement_error, + refuse_initial_time_independent_variance_as_asymptotic_time_independent_variance, + refuse_initial_time_independent_variance_as_initial_latent_variance, + refuse_initial_time_independent_variance_as_standardised_initial_latent_variance, + refuse_initial_time_independent_variance_as_standardised_initial_time_independent_effect, + refuse_initial_time_independent_variance_as_standardised_trait_variance, + refuse_initial_time_independent_variance_as_trait_variance, refuse_latent_lagged_covariance_as_observed_covariance, refuse_latent_mean_as_observed_mean, refuse_latent_variance_as_observed_variance, refuse_level_change_extra_process_as_impulse, refuse_level_change_extra_process_as_increment, refuse_level_change_extra_process_as_intercept, @@ -102,9 +143,70 @@ use psychometric_core::{ refuse_manifest_means_as_observed_mean, refuse_manifest_trait_variance_as_measurement_error, refuse_measurement_error_as_lagged_observed_covariance, refuse_measurement_error_as_observed_variance, + refuse_measurement_error_as_predetermined_initial_observed_variance, + refuse_measurement_error_as_predetermined_lagged_observed_covariance, + refuse_measurement_error_as_predetermined_later_lagged_observed_covariance, + refuse_measurement_error_as_predetermined_later_observed_variance, + refuse_measurement_error_as_predetermined_later_start_later_observed_variance, + refuse_measurement_error_as_standardised_manifest_trait_variance, refuse_measurement_error_as_stationary_lagged_observed_covariance, refuse_measurement_error_as_stationary_later_observed_variance, + refuse_observed_variance_as_standardised_manifest_variance, + refuse_predetermined_initial_latent_variance_as_initial_latent_variance, + refuse_predetermined_initial_latent_variance_as_lagged_latent_covariance, + refuse_predetermined_initial_latent_variance_as_later_latent_variance, + refuse_predetermined_initial_latent_variance_as_observed_variance, + refuse_predetermined_initial_latent_variance_as_stationary_initial_latent_variance, + refuse_predetermined_lagged_latent_covariance_as_decayed_total, + refuse_predetermined_lagged_latent_covariance_as_initial_latent_variance, + refuse_predetermined_lagged_latent_covariance_as_later_latent_variance, + refuse_predetermined_lagged_latent_covariance_as_observed_covariance, + refuse_predetermined_lagged_latent_covariance_as_stationary_lagged_covariance, + refuse_predetermined_lagged_observed_covariance_as_predetermined_later_lagged_observed_covariance, + refuse_predetermined_later_lagged_latent_covariance_as_decayed_later_total, + refuse_predetermined_later_lagged_latent_covariance_as_later_latent_variance, + refuse_predetermined_later_lagged_latent_covariance_as_observed_covariance, + refuse_predetermined_later_lagged_latent_covariance_as_predetermined_lagged_covariance, + refuse_predetermined_later_lagged_latent_covariance_as_stationary_lagged_covariance, + refuse_predetermined_later_lagged_observed_covariance_as_predetermined_later_start_later_observed_variance, + refuse_predetermined_later_latent_variance_as_discrete_variance, + refuse_predetermined_later_latent_variance_as_initial_latent_variance, + refuse_predetermined_later_latent_variance_as_observed_variance, + refuse_predetermined_later_latent_variance_as_stationary_later_latent_variance, + refuse_predetermined_later_observed_variance_as_predetermined_initial_observed_variance, + refuse_predetermined_later_observed_variance_as_predetermined_lagged_observed_covariance, + refuse_predetermined_later_observed_variance_as_predetermined_later_lagged_observed_covariance, + refuse_predetermined_later_observed_variance_as_predetermined_later_start_later_observed_variance, + refuse_predetermined_later_start_later_latent_variance_as_decayed_later_total, + refuse_predetermined_later_start_later_latent_variance_as_lag_interval_later_latent_variance, + refuse_predetermined_later_start_later_latent_variance_as_later_lagged_covariance, + refuse_predetermined_later_start_later_latent_variance_as_later_latent_variance, + refuse_predetermined_later_start_later_latent_variance_as_observed_variance, + refuse_predetermined_later_start_later_latent_variance_as_stationary_later_latent_variance, refuse_process_noise_as_unconditional_variance, + refuse_standardised_asymptotic_time_independent_effect_as_standardised_continuous_time_independent_effect, + refuse_standardised_asymptotic_time_independent_effect_as_standardised_initial_time_independent_effect, + refuse_standardised_continuous_diffusion_as_standardised_asymptotic_diffusion, + refuse_standardised_continuous_diffusion_as_standardised_discrete_diffusion, + refuse_standardised_continuous_intercept_as_standardised_asymptotic_continuous_intercept, + refuse_standardised_continuous_intercept_as_standardised_discrete_continuous_intercept, + refuse_standardised_continuous_time_dependent_effect_as_standardised_initial_time_dependent_effect, + refuse_standardised_continuous_time_independent_effect_as_standardised_continuous_time_dependent_effect, + refuse_standardised_continuous_time_independent_effect_as_standardised_initial_time_independent_effect, + refuse_standardised_discrete_continuous_intercept_as_standardised_asymptotic_continuous_intercept, + refuse_standardised_discrete_diffusion_as_standardised_continuous_diffusion, + refuse_standardised_discrete_drift_as_standardised_continuous_drift, + refuse_standardised_discrete_time_dependent_effect_as_standardised_continuous_time_dependent_effect, + refuse_standardised_discrete_time_independent_effect_as_standardised_asymptotic_time_independent_effect, + refuse_standardised_discrete_time_independent_effect_as_standardised_continuous_time_independent_effect, + refuse_standardised_initial_latent_variance_as_standardised_initial_latent_mean, + refuse_standardised_initial_latent_variance_as_standardised_trait_variance, + refuse_standardised_initial_time_dependent_effect_as_standardised_initial_latent_variance, + refuse_standardised_initial_time_independent_effect_as_standardised_initial_time_dependent_effect, + refuse_standardised_manifest_trait_variance_as_standardised_manifest_variance, + refuse_standardised_manifest_variance_as_standardised_time_independent_predictor_variance, + refuse_standardised_time_independent_predictor_variance_as_standardised_asymptotic_diffusion, + refuse_standardised_trait_variance_as_standardised_manifest_trait_variance, refuse_stationary_initial_latent_mean_as_asymptotic_continuous_intercept, refuse_stationary_initial_latent_mean_as_asymptotic_time_independent_effect, refuse_stationary_initial_latent_mean_as_discrete_mean, @@ -118,15 +220,20 @@ use psychometric_core::{ refuse_stationary_initial_latent_variance_as_trait_variance, refuse_stationary_initial_observed_mean_as_manifest_means, refuse_stationary_initial_observed_variance_as_measurement_error, + refuse_stationary_initial_observed_variance_as_predetermined_initial_observed_variance, refuse_stationary_initial_observed_variance_as_stationary_lagged_observed_covariance, refuse_stationary_lagged_latent_covariance_as_decayed_stationary_variance, refuse_stationary_lagged_latent_covariance_as_observed_covariance, refuse_stationary_lagged_latent_covariance_as_stationary_initial_latent_variance, + refuse_stationary_lagged_observed_covariance_as_predetermined_lagged_observed_covariance, + refuse_stationary_lagged_observed_covariance_as_predetermined_later_lagged_observed_covariance, refuse_stationary_lagged_observed_covariance_as_stationary_later_observed_variance, refuse_stationary_later_latent_variance_as_discrete_variance, refuse_stationary_later_latent_variance_as_lagged_covariance, refuse_stationary_later_latent_variance_as_observed_variance, refuse_stationary_later_latent_variance_as_process_noise, + refuse_stationary_later_observed_variance_as_predetermined_later_observed_variance, + refuse_stationary_later_observed_variance_as_predetermined_later_start_later_observed_variance, refuse_stationary_within_subject_observed_variance_as_stationary_initial_observed_variance, refuse_time_dependent_impulse_as_continuous_intercept, refuse_time_dependent_impulse_as_time_independent_effect, @@ -141,8 +248,37 @@ use psychometric_core::{ refuse_time_independent_effect_as_time_varying_discrete_effect, refuse_time_independent_observed_mean_as_initial_time_dependent_observed_mean, refuse_time_independent_observed_mean_as_initial_time_independent_observed_mean, + refuse_trait_contaminated_asymptotic_time_independent_effect_as_standardised_asymptotic_time_independent_effect, + refuse_trait_contaminated_continuous_diffusion_as_standardised_continuous_diffusion, + refuse_trait_contaminated_continuous_drift_as_standardised_continuous_drift, + refuse_trait_contaminated_continuous_time_dependent_effect_as_standardised_continuous_time_dependent_effect, + refuse_trait_contaminated_continuous_time_independent_effect_as_standardised_continuous_time_independent_effect, + refuse_trait_contaminated_initial_time_dependent_effect_as_standardised_initial_time_dependent_effect, + refuse_trait_contaminated_initial_time_independent_effect_as_standardised_initial_time_independent_effect, + refuse_trait_contaminated_process_noise_as_standardised_discrete_diffusion, + refuse_trait_plus_state_autocorrelation_as_standardised_discrete_drift, refuse_trait_plus_state_lagged_covariance_as_stationary_lagged_latent_covariance, - refuse_trait_variance_as_process_noise, refuse_trait_variance_as_stationary_within_subject, + refuse_trait_variance_as_process_noise, refuse_trait_variance_as_standardisation_variance, + refuse_trait_variance_as_stationary_within_subject, + refuse_unstandardised_asymptotic_continuous_intercept_as_standardised_asymptotic_continuous_intercept, + refuse_unstandardised_asymptotic_diffusion_as_standardised_asymptotic_diffusion, + refuse_unstandardised_asymptotic_time_independent_effect_as_standardised_asymptotic_time_independent_effect, + refuse_unstandardised_continuous_diffusion_as_standardised_continuous_diffusion, + refuse_unstandardised_continuous_drift_as_standardised_continuous_drift, + refuse_unstandardised_continuous_time_dependent_effect_as_standardised_continuous_time_dependent_effect, + refuse_unstandardised_continuous_time_independent_effect_as_standardised_continuous_time_independent_effect, + refuse_unstandardised_discrete_continuous_intercept_as_standardised_discrete_continuous_intercept, + refuse_unstandardised_discrete_diffusion_as_standardised_discrete_diffusion, + refuse_unstandardised_discrete_drift_as_standardised_discrete_drift, + refuse_unstandardised_initial_latent_mean_as_standardised_initial_latent_mean, + refuse_unstandardised_initial_latent_variance_as_standardised_initial_latent_variance, + refuse_unstandardised_initial_time_dependent_effect_as_standardised_initial_time_dependent_effect, + refuse_unstandardised_initial_time_independent_effect_as_standardised_initial_time_independent_effect, + refuse_unstandardised_manifest_trait_variance_as_standardised_manifest_trait_variance, + refuse_unstandardised_manifest_variance_as_standardised_manifest_variance, + refuse_unstandardised_time_independent_predictor_variance_as_standardised_time_independent_predictor_variance, + refuse_unstandardised_trait_variance_as_standardised_trait_variance, + refuse_within_subject_scaled_initial_latent_mean_as_standardised_initial_latent_mean, }; #[test] @@ -2973,3 +3109,3119 @@ fn stationary_later_observed_variance_is_not_manifest_latent_or_lagged() { ) ); } + +#[test] +fn predetermined_later_latent_variance_is_not_stationary_discrete_or_initial() { + let trait_variance = 1.0_f64; + let initial_latent_variance = 2.0_f64; + let diffusion = 0.4_f64; + let log_rate = -0.134_488_942_f64; + let event_delta = 1.0_f64; + let recovered = recover_predetermined_later_latent_variance( + trait_variance, + initial_latent_variance, + diffusion, + -0.225, + 1.0, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("predetermined later T0VAR"); + let stationary_later = recover_stationary_later_latent_variance( + trait_variance, + diffusion, + -0.225, + 1.0, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("stationary later T0VAR"); + let added = recover_asymptotic_time_independent_predictor_variance( + -0.225, + 1.0, + log_rate, + LagClock::EventTime, + ) + .expect("addedTIPREDVAR"); + let free_discrete = recover_discrete_latent_variance( + trait_variance + initial_latent_variance + added, + diffusion, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("e^{2aΔt}(trait+p_0+added)+Q_Δt"); + assert!( + (recovered - stationary_later).abs() > 1e-3, + "Driver et al. (2017, §4.3 predetermined T0VAR): free p_0 is not −q/(2a)" + ); + assert!( + (recovered - free_discrete).abs() > 1e-3, + "Driver et al. (2017, §4.3 predetermined T0VAR): trait and addedTIPREDVAR do not enter Q_Δt" + ); + assert!( + (recovered - initial_latent_variance).abs() > 1e-3, + "Driver et al. (2017, §4.3 predetermined T0VAR): e^{{2aΔt}}p_0+Q_Δt is not p_0" + ); + assert_eq!( + refuse_predetermined_later_latent_variance_as_stationary_later_latent_variance( + recovered, + stationary_later + ), + Err( + psychometric_core::PsychometricError::PredeterminedLaterLatentVarianceIsNotStationaryLaterLatentVariance + ) + ); + assert_eq!( + refuse_predetermined_later_latent_variance_as_discrete_variance(recovered, free_discrete), + Err( + psychometric_core::PsychometricError::PredeterminedLaterLatentVarianceIsNotDiscreteVariance + ) + ); + assert_eq!( + refuse_predetermined_later_latent_variance_as_initial_latent_variance( + recovered, + initial_latent_variance + ), + Err( + psychometric_core::PsychometricError::PredeterminedLaterLatentVarianceIsNotInitialLatentVariance + ) + ); +} + +#[test] +fn predetermined_later_observed_variance_is_not_manifest_latent_or_stationary() { + let trait_variance = 1.0_f64; + let initial_latent_variance = 2.0_f64; + let diffusion = 0.4_f64; + let log_rate = -0.134_488_942_f64; + let loading = 2.0_f64; + let measurement_error = 0.5_f64; + let event_delta = 1.0_f64; + let recovered = recover_predetermined_later_observed_variance( + loading, + trait_variance, + initial_latent_variance, + diffusion, + -0.225, + 1.0, + log_rate, + event_delta, + measurement_error, + 0.1, + LagClock::EventTime, + ) + .expect("eq5-later-predetermined-T0VAR"); + let latent = recover_predetermined_later_latent_variance( + trait_variance, + initial_latent_variance, + diffusion, + -0.225, + 1.0, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("predetermined later T0VAR"); + let stationary_later = recover_stationary_later_observed_variance( + loading, + trait_variance, + diffusion, + -0.225, + 1.0, + log_rate, + event_delta, + measurement_error, + 0.1, + LagClock::EventTime, + ) + .expect("eq5-later-stationary-T0VAR"); + assert!( + (recovered - measurement_error).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 of predetermined later §4.3 T0VAR): Var(y_t) is not MANIFESTVAR" + ); + assert!( + (recovered - latent).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 of predetermined later §4.3 T0VAR): Var(y_t) is not later T0VAR" + ); + assert!( + (recovered - stationary_later).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 of predetermined later §4.3 T0VAR): free p_0 is not −q/(2a)" + ); + assert_eq!( + refuse_predetermined_later_latent_variance_as_observed_variance(latent, recovered), + Err( + psychometric_core::PsychometricError::PredeterminedLaterLatentVarianceIsNotObservedVariance + ) + ); + assert_eq!( + refuse_measurement_error_as_predetermined_later_observed_variance( + measurement_error, + recovered + ), + Err( + psychometric_core::PsychometricError::MeasurementErrorIsNotPredeterminedLaterObservedVariance + ) + ); + assert_eq!( + refuse_stationary_later_observed_variance_as_predetermined_later_observed_variance( + stationary_later, + recovered + ), + Err( + psychometric_core::PsychometricError::StationaryLaterObservedVarianceIsNotPredeterminedLaterObservedVariance + ) + ); +} + +#[test] +fn predetermined_lagged_latent_covariance_is_not_stationary_later_or_decayed() { + let trait_variance = 1.0_f64; + let initial_latent_variance = 2.0_f64; + let diffusion = 0.4_f64; + let log_rate = -0.134_488_942_f64; + let event_delta = 1.0_f64; + let recovered = recover_predetermined_lagged_latent_covariance( + trait_variance, + initial_latent_variance, + -0.225, + 1.0, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("predetermined lagged T0VAR"); + let stationary_lagged = recover_stationary_lagged_latent_covariance( + trait_variance, + diffusion, + -0.225, + 1.0, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("stationary lagged T0VAR"); + let later = recover_predetermined_later_latent_variance( + trait_variance, + initial_latent_variance, + diffusion, + -0.225, + 1.0, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("predetermined later T0VAR"); + let added = recover_asymptotic_time_independent_predictor_variance( + -0.225, + 1.0, + log_rate, + LagClock::EventTime, + ) + .expect("addedTIPREDVAR"); + let decayed_total = recover_discrete_lagged_latent_covariance( + trait_variance + initial_latent_variance + added, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("e^{aΔt}(trait+p_0+added)"); + assert!( + (recovered - stationary_lagged).abs() > 1e-3, + "Driver et al. (2017, §4.3 predetermined T0VAR): free p_0 is not −q/(2a)" + ); + assert!( + (recovered - later).abs() > 1e-3, + "Driver et al. (2017, §4.3 predetermined T0VAR): lagged omits Q_Δt" + ); + assert!( + (recovered - decayed_total).abs() > 1e-3, + "Driver et al. (2017, §4.3 predetermined T0VAR): trait and addedTIPREDVAR do not decay" + ); + assert!( + (recovered - initial_latent_variance).abs() > 1e-3, + "Driver et al. (2017, §4.3 predetermined T0VAR): e^{{aΔt}}p_0 is not p_0" + ); + assert_eq!( + refuse_predetermined_lagged_latent_covariance_as_stationary_lagged_covariance( + recovered, + stationary_lagged + ), + Err( + psychometric_core::PsychometricError::PredeterminedLaggedLatentCovarianceIsNotStationaryLaggedCovariance + ) + ); + assert_eq!( + refuse_predetermined_lagged_latent_covariance_as_later_latent_variance(recovered, later), + Err( + psychometric_core::PsychometricError::PredeterminedLaggedLatentCovarianceIsNotLaterLatentVariance + ) + ); + assert_eq!( + refuse_predetermined_lagged_latent_covariance_as_decayed_total(recovered, decayed_total), + Err( + psychometric_core::PsychometricError::PredeterminedLaggedLatentCovarianceIsNotDecayedTotal + ) + ); + assert_eq!( + refuse_predetermined_lagged_latent_covariance_as_initial_latent_variance( + recovered, + initial_latent_variance + ), + Err( + psychometric_core::PsychometricError::PredeterminedLaggedLatentCovarianceIsNotInitialLatentVariance + ) + ); +} + +#[test] +#[allow(clippy::too_many_lines)] +fn predetermined_lagged_observed_covariance_is_not_manifest_later_or_stationary() { + let trait_variance = 1.0_f64; + let initial_latent_variance = 2.0_f64; + let diffusion = 0.4_f64; + let log_rate = -0.134_488_942_f64; + let loading = 2.0_f64; + let measurement_error = 0.5_f64; + let event_delta = 1.0_f64; + let recovered = recover_predetermined_lagged_observed_covariance( + loading, + trait_variance, + initial_latent_variance, + -0.225, + 1.0, + log_rate, + event_delta, + 0.1, + LagClock::EventTime, + ) + .expect("eq5-lagged-predetermined-T0VAR"); + let latent = recover_predetermined_lagged_latent_covariance( + trait_variance, + initial_latent_variance, + -0.225, + 1.0, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("predetermined lagged T0VAR"); + let later = recover_predetermined_later_observed_variance( + loading, + trait_variance, + initial_latent_variance, + diffusion, + -0.225, + 1.0, + log_rate, + event_delta, + measurement_error, + 0.1, + LagClock::EventTime, + ) + .expect("eq5-later-predetermined-T0VAR"); + let stationary_lagged = recover_stationary_lagged_observed_covariance( + loading, + trait_variance, + diffusion, + -0.225, + 1.0, + log_rate, + event_delta, + 0.1, + LagClock::EventTime, + ) + .expect("eq5-lagged-stationary-T0VAR"); + assert!( + (recovered - measurement_error).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 of predetermined lagged §4.3 T0VAR): cov(y) is not MANIFESTVAR" + ); + assert!( + (recovered - latent).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 of predetermined lagged §4.3 T0VAR): cov(y) is not lagged T0VAR" + ); + assert!( + (recovered - later).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 of predetermined lagged §4.3 T0VAR): lagged omits Q_Δt and θ" + ); + assert!( + (recovered - stationary_lagged).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 of predetermined lagged §4.3 T0VAR): free p_0 is not −q/(2a)" + ); + assert_eq!( + refuse_predetermined_lagged_latent_covariance_as_observed_covariance(latent, recovered), + Err( + psychometric_core::PsychometricError::PredeterminedLaggedLatentCovarianceIsNotObservedCovariance + ) + ); + assert_eq!( + refuse_measurement_error_as_predetermined_lagged_observed_covariance( + measurement_error, + recovered + ), + Err( + psychometric_core::PsychometricError::MeasurementErrorIsNotPredeterminedLaggedObservedCovariance + ) + ); + assert_eq!( + refuse_predetermined_later_observed_variance_as_predetermined_lagged_observed_covariance( + later, + recovered + ), + Err( + psychometric_core::PsychometricError::PredeterminedLaterObservedVarianceIsNotPredeterminedLaggedObservedCovariance + ) + ); + assert_eq!( + refuse_stationary_lagged_observed_covariance_as_predetermined_lagged_observed_covariance( + stationary_lagged, + recovered + ), + Err( + psychometric_core::PsychometricError::StationaryLaggedObservedCovarianceIsNotPredeterminedLaggedObservedCovariance + ) + ); +} + +#[test] +fn predetermined_initial_latent_variance_is_not_stationary_lagged_or_later() { + let trait_variance = 1.0_f64; + let initial_latent_variance = 2.0_f64; + let diffusion = 0.4_f64; + let log_rate = -0.134_488_942_f64; + let event_delta = 1.0_f64; + let recovered = recover_predetermined_initial_latent_variance( + trait_variance, + initial_latent_variance, + -0.225, + 1.0, + log_rate, + LagClock::EventTime, + ) + .expect("predetermined initial T0VAR"); + let stationary = recover_stationary_initial_latent_variance( + trait_variance, + diffusion, + -0.225, + 1.0, + log_rate, + LagClock::EventTime, + ) + .expect("stationary initial T0VAR"); + let lagged = recover_predetermined_lagged_latent_covariance( + trait_variance, + initial_latent_variance, + -0.225, + 1.0, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("predetermined lagged T0VAR"); + let later = recover_predetermined_later_latent_variance( + trait_variance, + initial_latent_variance, + diffusion, + -0.225, + 1.0, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("predetermined later T0VAR"); + assert!( + (recovered - stationary).abs() > 1e-3, + "Driver et al. (2017, §4.3 predetermined T0VAR): free p_0 is not −q/(2a)" + ); + assert!( + (recovered - lagged).abs() > 1e-3, + "Driver et al. (2017, §4.3 predetermined T0VAR): first occasion does not decay" + ); + assert!( + (recovered - later).abs() > 1e-3, + "Driver et al. (2017, §4.3 predetermined T0VAR): first occasion omits Q_Δt" + ); + assert!( + (recovered - initial_latent_variance).abs() > 1e-3, + "Driver et al. (2017, §4.3 predetermined T0VAR): trait + p_0 + added is not p_0" + ); + assert_eq!( + refuse_predetermined_initial_latent_variance_as_stationary_initial_latent_variance( + recovered, stationary + ), + Err( + psychometric_core::PsychometricError::PredeterminedInitialLatentVarianceIsNotStationaryInitialLatentVariance + ) + ); + assert_eq!( + refuse_predetermined_initial_latent_variance_as_initial_latent_variance( + recovered, + initial_latent_variance + ), + Err( + psychometric_core::PsychometricError::PredeterminedInitialLatentVarianceIsNotInitialLatentVariance + ) + ); + assert_eq!( + refuse_predetermined_initial_latent_variance_as_lagged_latent_covariance(recovered, lagged), + Err( + psychometric_core::PsychometricError::PredeterminedInitialLatentVarianceIsNotLaggedLatentCovariance + ) + ); + assert_eq!( + refuse_predetermined_initial_latent_variance_as_later_latent_variance(recovered, later), + Err( + psychometric_core::PsychometricError::PredeterminedInitialLatentVarianceIsNotLaterLatentVariance + ) + ); +} + +#[test] +#[allow(clippy::too_many_lines)] +fn predetermined_initial_observed_variance_is_not_manifest_later_or_stationary() { + let trait_variance = 1.0_f64; + let initial_latent_variance = 2.0_f64; + let diffusion = 0.4_f64; + let log_rate = -0.134_488_942_f64; + let loading = 2.0_f64; + let measurement_error = 0.5_f64; + let event_delta = 1.0_f64; + let recovered = recover_predetermined_initial_observed_variance( + loading, + trait_variance, + initial_latent_variance, + -0.225, + 1.0, + log_rate, + measurement_error, + 0.1, + LagClock::EventTime, + ) + .expect("eq5-initial-predetermined-T0VAR"); + let latent = recover_predetermined_initial_latent_variance( + trait_variance, + initial_latent_variance, + -0.225, + 1.0, + log_rate, + LagClock::EventTime, + ) + .expect("predetermined initial T0VAR"); + let later = recover_predetermined_later_observed_variance( + loading, + trait_variance, + initial_latent_variance, + diffusion, + -0.225, + 1.0, + log_rate, + event_delta, + measurement_error, + 0.1, + LagClock::EventTime, + ) + .expect("eq5-later-predetermined-T0VAR"); + let stationary = recover_stationary_initial_observed_variance( + loading, + trait_variance, + diffusion, + -0.225, + 1.0, + log_rate, + measurement_error, + 0.1, + LagClock::EventTime, + ) + .expect("eq5-initial-stationary-T0VAR"); + assert!( + (recovered - measurement_error).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 of predetermined first-occasion §4.3 T0VAR): Var(y_0) is not MANIFESTVAR" + ); + assert!( + (recovered - latent).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 of predetermined first-occasion §4.3 T0VAR): Var(y_0) is not initial T0VAR" + ); + assert!( + (recovered - later).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 of predetermined first-occasion §4.3 T0VAR): first occasion omits Q_Δt" + ); + assert!( + (recovered - stationary).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 of predetermined first-occasion §4.3 T0VAR): free p_0 is not −q/(2a)" + ); + assert_eq!( + refuse_predetermined_initial_latent_variance_as_observed_variance(latent, recovered), + Err( + psychometric_core::PsychometricError::PredeterminedInitialLatentVarianceIsNotObservedVariance + ) + ); + assert_eq!( + refuse_measurement_error_as_predetermined_initial_observed_variance( + measurement_error, + recovered + ), + Err( + psychometric_core::PsychometricError::MeasurementErrorIsNotPredeterminedInitialObservedVariance + ) + ); + assert_eq!( + refuse_stationary_initial_observed_variance_as_predetermined_initial_observed_variance( + stationary, recovered + ), + Err( + psychometric_core::PsychometricError::StationaryInitialObservedVarianceIsNotPredeterminedInitialObservedVariance + ) + ); + assert_eq!( + refuse_predetermined_later_observed_variance_as_predetermined_initial_observed_variance( + later, recovered + ), + Err( + psychometric_core::PsychometricError::PredeterminedLaterObservedVarianceIsNotPredeterminedInitialObservedVariance + ) + ); +} + +#[test] +#[allow(clippy::too_many_lines)] +fn predetermined_later_lagged_latent_covariance_is_not_first_later_or_stationary() { + let trait_variance = 1.0_f64; + let initial_latent_variance = 2.0_f64; + let diffusion = 0.4_f64; + let log_rate = -0.134_488_942_f64; + let start_delta = 2.0_f64; + let lag_delta = 1.0_f64; + let recovered = recover_predetermined_later_lagged_latent_covariance( + trait_variance, + initial_latent_variance, + diffusion, + -0.225, + 1.0, + log_rate, + start_delta, + lag_delta, + LagClock::EventTime, + ) + .expect("predetermined later-start lagged T0VAR"); + let first_lagged = recover_predetermined_lagged_latent_covariance( + trait_variance, + initial_latent_variance, + -0.225, + 1.0, + log_rate, + lag_delta, + LagClock::EventTime, + ) + .expect("first-occasion lagged"); + let later = recover_predetermined_later_latent_variance( + trait_variance, + initial_latent_variance, + diffusion, + -0.225, + 1.0, + log_rate, + start_delta, + LagClock::EventTime, + ) + .expect("later variance"); + let stationary_lagged = recover_stationary_lagged_latent_covariance( + trait_variance, + diffusion, + -0.225, + 1.0, + log_rate, + lag_delta, + LagClock::EventTime, + ) + .expect("stationary lagged"); + let decayed_later = later * lag_delta.mul_add(log_rate, 0.0).exp(); + assert!( + (recovered - first_lagged).abs() > 1e-3, + "Driver et al. (2017, §4.3 startoffset): later-start lag includes e^{{as}} Q_u" + ); + assert!( + (recovered - later).abs() > 1e-3, + "Driver et al. (2017, §4.3 startoffset): lagged is not later variance" + ); + assert!( + (recovered - stationary_lagged).abs() > 1e-3, + "Driver et al. (2017, §4.3 predetermined later-start lag): free p_0 is not −q/(2a)" + ); + assert!( + (recovered - decayed_later).abs() > 1e-3, + "Driver et al. (2017, §4.3 startoffset): trait and addedTIPREDVAR do not decay" + ); + assert_eq!( + refuse_predetermined_later_lagged_latent_covariance_as_predetermined_lagged_covariance( + recovered, first_lagged + ), + Err( + psychometric_core::PsychometricError::PredeterminedLaterLaggedLatentCovarianceIsNotPredeterminedLaggedCovariance + ) + ); + assert_eq!( + refuse_predetermined_later_lagged_latent_covariance_as_later_latent_variance( + recovered, later + ), + Err( + psychometric_core::PsychometricError::PredeterminedLaterLaggedLatentCovarianceIsNotLaterLatentVariance + ) + ); + assert_eq!( + refuse_predetermined_later_lagged_latent_covariance_as_stationary_lagged_covariance( + recovered, + stationary_lagged + ), + Err( + psychometric_core::PsychometricError::PredeterminedLaterLaggedLatentCovarianceIsNotStationaryLaggedCovariance + ) + ); + assert_eq!( + refuse_predetermined_later_lagged_latent_covariance_as_decayed_later_total( + recovered, + decayed_later + ), + Err( + psychometric_core::PsychometricError::PredeterminedLaterLaggedLatentCovarianceIsNotDecayedLaterTotal + ) + ); +} + +#[test] +#[allow(clippy::too_many_lines)] +fn predetermined_later_lagged_observed_covariance_is_not_manifest_first_or_stationary() { + let trait_variance = 1.0_f64; + let initial_latent_variance = 2.0_f64; + let diffusion = 0.4_f64; + let log_rate = -0.134_488_942_f64; + let loading = 2.0_f64; + let measurement_error = 0.5_f64; + let start_delta = 2.0_f64; + let lag_delta = 1.0_f64; + let recovered = recover_predetermined_later_lagged_observed_covariance( + loading, + trait_variance, + initial_latent_variance, + diffusion, + -0.225, + 1.0, + log_rate, + start_delta, + lag_delta, + 0.1, + LagClock::EventTime, + ) + .expect("eq5-later-start-lagged-predetermined-T0VAR"); + let latent = recover_predetermined_later_lagged_latent_covariance( + trait_variance, + initial_latent_variance, + diffusion, + -0.225, + 1.0, + log_rate, + start_delta, + lag_delta, + LagClock::EventTime, + ) + .expect("later-start lagged T0VAR"); + let first_lagged = recover_predetermined_lagged_observed_covariance( + loading, + trait_variance, + initial_latent_variance, + -0.225, + 1.0, + log_rate, + lag_delta, + 0.1, + LagClock::EventTime, + ) + .expect("eq5-first-lagged"); + let later = recover_predetermined_later_observed_variance( + loading, + trait_variance, + initial_latent_variance, + diffusion, + -0.225, + 1.0, + log_rate, + start_delta, + measurement_error, + 0.1, + LagClock::EventTime, + ) + .expect("eq5-later"); + let stationary = recover_stationary_lagged_observed_covariance( + loading, + trait_variance, + diffusion, + -0.225, + 1.0, + log_rate, + lag_delta, + 0.1, + LagClock::EventTime, + ) + .expect("eq5-stationary-lagged"); + assert!( + (recovered - measurement_error).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 of later-start lagged predetermined T0VAR): cov(y) is not MANIFESTVAR" + ); + assert!( + (recovered - latent).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 of later-start lagged predetermined T0VAR): cov(y) is not lagged T0VAR" + ); + assert!( + (recovered - first_lagged).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 of later-start lagged predetermined T0VAR): first-occasion omits e^{{as}} Q_u" + ); + assert!( + (recovered - later).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 of later-start lagged predetermined T0VAR): later variance includes Q_u and θ" + ); + assert!( + (recovered - stationary).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 of later-start lagged predetermined T0VAR): free p_0 is not −q/(2a)" + ); + assert_eq!( + refuse_predetermined_later_lagged_latent_covariance_as_observed_covariance( + latent, recovered + ), + Err( + psychometric_core::PsychometricError::PredeterminedLaterLaggedLatentCovarianceIsNotObservedCovariance + ) + ); + assert_eq!( + refuse_measurement_error_as_predetermined_later_lagged_observed_covariance( + measurement_error, + recovered + ), + Err( + psychometric_core::PsychometricError::MeasurementErrorIsNotPredeterminedLaterLaggedObservedCovariance + ) + ); + assert_eq!( + refuse_predetermined_lagged_observed_covariance_as_predetermined_later_lagged_observed_covariance( + first_lagged, recovered + ), + Err( + psychometric_core::PsychometricError::PredeterminedLaggedObservedCovarianceIsNotPredeterminedLaterLaggedObservedCovariance + ) + ); + assert_eq!( + refuse_stationary_lagged_observed_covariance_as_predetermined_later_lagged_observed_covariance( + stationary, recovered + ), + Err( + psychometric_core::PsychometricError::StationaryLaggedObservedCovarianceIsNotPredeterminedLaterLaggedObservedCovariance + ) + ); + assert_eq!( + refuse_predetermined_later_observed_variance_as_predetermined_later_lagged_observed_covariance( + later, recovered + ), + Err( + psychometric_core::PsychometricError::PredeterminedLaterObservedVarianceIsNotPredeterminedLaterLaggedObservedCovariance + ) + ); +} + +#[test] +#[allow(clippy::too_many_lines)] +fn predetermined_later_start_later_latent_variance_is_not_later_lagged_or_stationary() { + let trait_variance = 1.0_f64; + let initial_latent_variance = 2.0_f64; + let diffusion = 0.4_f64; + let log_rate = -0.134_488_942_f64; + let start_delta = 2.0_f64; + let lag_delta = 1.0_f64; + let recovered = recover_predetermined_later_start_later_latent_variance( + trait_variance, + initial_latent_variance, + diffusion, + -0.225, + 1.0, + log_rate, + start_delta, + lag_delta, + LagClock::EventTime, + ) + .expect("predetermined later-start later T0VAR"); + let later = recover_predetermined_later_latent_variance( + trait_variance, + initial_latent_variance, + diffusion, + -0.225, + 1.0, + log_rate, + start_delta, + LagClock::EventTime, + ) + .expect("later variance"); + let later_lagged = recover_predetermined_later_lagged_latent_covariance( + trait_variance, + initial_latent_variance, + diffusion, + -0.225, + 1.0, + log_rate, + start_delta, + lag_delta, + LagClock::EventTime, + ) + .expect("later-start lagged"); + let stationary_later = recover_stationary_later_latent_variance( + trait_variance, + diffusion, + -0.225, + 1.0, + log_rate, + lag_delta, + LagClock::EventTime, + ) + .expect("stationary later"); + let decayed_later = recover_discrete_latent_variance( + later, + diffusion, + log_rate, + lag_delta, + LagClock::EventTime, + ) + .expect("evolved later total"); + let lag_interval = recover_predetermined_later_latent_variance( + trait_variance, + initial_latent_variance, + diffusion, + -0.225, + 1.0, + log_rate, + lag_delta, + LagClock::EventTime, + ) + .expect("later over s only"); + assert!( + (recovered - later).abs() > 1e-3, + "Driver et al. (2017, §4.3 startoffset): later-start later-occasion adds Q_s" + ); + assert!( + (recovered - later_lagged).abs() > 1e-3, + "Driver et al. (2017, §4.3 startoffset): later-occasion variance is not lagged covariance" + ); + assert!( + (recovered - stationary_later).abs() > 1e-3, + "Driver et al. (2017, §4.3 predetermined later-start later): free p_0 is not −q/(2a)" + ); + assert!( + (recovered - decayed_later).abs() > 1e-3, + "Driver et al. (2017, §4.3 startoffset): trait and addedTIPREDVAR do not enter Q_s" + ); + assert!( + (recovered - lag_interval).abs() > 1e-3, + "Driver et al. (2017, §4.3 startoffset): ignoring startoffset omits e^{{2as}} Q_u" + ); + assert_eq!( + refuse_predetermined_later_start_later_latent_variance_as_later_latent_variance( + recovered, later + ), + Err( + psychometric_core::PsychometricError::PredeterminedLaterStartLaterLatentVarianceIsNotLaterLatentVariance + ) + ); + assert_eq!( + refuse_predetermined_later_start_later_latent_variance_as_later_lagged_covariance( + recovered, + later_lagged + ), + Err( + psychometric_core::PsychometricError::PredeterminedLaterStartLaterLatentVarianceIsNotLaterLaggedCovariance + ) + ); + assert_eq!( + refuse_predetermined_later_start_later_latent_variance_as_stationary_later_latent_variance( + recovered, + stationary_later + ), + Err( + psychometric_core::PsychometricError::PredeterminedLaterStartLaterLatentVarianceIsNotStationaryLaterLatentVariance + ) + ); + assert_eq!( + refuse_predetermined_later_start_later_latent_variance_as_decayed_later_total( + recovered, + decayed_later + ), + Err( + psychometric_core::PsychometricError::PredeterminedLaterStartLaterLatentVarianceIsNotDecayedLaterTotal + ) + ); + assert_eq!( + refuse_predetermined_later_start_later_latent_variance_as_lag_interval_later_latent_variance( + recovered, + lag_interval + ), + Err( + psychometric_core::PsychometricError::PredeterminedLaterStartLaterLatentVarianceIsNotLagIntervalLaterLatentVariance + ) + ); +} + +#[test] +#[allow(clippy::too_many_lines)] +fn predetermined_later_start_later_observed_variance_is_not_manifest_later_or_stationary() { + let trait_variance = 1.0_f64; + let initial_latent_variance = 2.0_f64; + let diffusion = 0.4_f64; + let log_rate = -0.134_488_942_f64; + let loading = 2.0_f64; + let measurement_error = 0.5_f64; + let start_delta = 2.0_f64; + let lag_delta = 1.0_f64; + let recovered = recover_predetermined_later_start_later_observed_variance( + loading, + trait_variance, + initial_latent_variance, + diffusion, + -0.225, + 1.0, + log_rate, + start_delta, + lag_delta, + measurement_error, + 0.1, + LagClock::EventTime, + ) + .expect("eq5-later-start-later-predetermined-T0VAR"); + let latent = recover_predetermined_later_start_later_latent_variance( + trait_variance, + initial_latent_variance, + diffusion, + -0.225, + 1.0, + log_rate, + start_delta, + lag_delta, + LagClock::EventTime, + ) + .expect("later-start later T0VAR"); + let later = recover_predetermined_later_observed_variance( + loading, + trait_variance, + initial_latent_variance, + diffusion, + -0.225, + 1.0, + log_rate, + start_delta, + measurement_error, + 0.1, + LagClock::EventTime, + ) + .expect("eq5-later"); + let later_lagged = recover_predetermined_later_lagged_observed_covariance( + loading, + trait_variance, + initial_latent_variance, + diffusion, + -0.225, + 1.0, + log_rate, + start_delta, + lag_delta, + 0.1, + LagClock::EventTime, + ) + .expect("eq5-later-start-lagged"); + let stationary = recover_stationary_later_observed_variance( + loading, + trait_variance, + diffusion, + -0.225, + 1.0, + log_rate, + lag_delta, + measurement_error, + 0.1, + LagClock::EventTime, + ) + .expect("eq5-stationary-later"); + assert!( + (recovered - measurement_error).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 of later-start later-occasion predetermined T0VAR): Var(y) is not MANIFESTVAR" + ); + assert!( + (recovered - latent).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 of later-start later-occasion predetermined T0VAR): Var(y) is not later-start later T0VAR" + ); + assert!( + (recovered - later).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 of later-start later-occasion predetermined T0VAR): later at u omits Q_s" + ); + assert!( + (recovered - later_lagged).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 of later-start later-occasion predetermined T0VAR): lagged omits Q_s and θ" + ); + assert!( + (recovered - stationary).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 of later-start later-occasion predetermined T0VAR): free p_0 is not −q/(2a)" + ); + assert_eq!( + refuse_predetermined_later_start_later_latent_variance_as_observed_variance( + latent, recovered + ), + Err( + psychometric_core::PsychometricError::PredeterminedLaterStartLaterLatentVarianceIsNotObservedVariance + ) + ); + assert_eq!( + refuse_measurement_error_as_predetermined_later_start_later_observed_variance( + measurement_error, + recovered + ), + Err( + psychometric_core::PsychometricError::MeasurementErrorIsNotPredeterminedLaterStartLaterObservedVariance + ) + ); + assert_eq!( + refuse_predetermined_later_observed_variance_as_predetermined_later_start_later_observed_variance( + later, recovered + ), + Err( + psychometric_core::PsychometricError::PredeterminedLaterObservedVarianceIsNotPredeterminedLaterStartLaterObservedVariance + ) + ); + assert_eq!( + refuse_predetermined_later_lagged_observed_covariance_as_predetermined_later_start_later_observed_variance( + later_lagged, recovered + ), + Err( + psychometric_core::PsychometricError::PredeterminedLaterLaggedObservedCovarianceIsNotPredeterminedLaterStartLaterObservedVariance + ) + ); + assert_eq!( + refuse_stationary_later_observed_variance_as_predetermined_later_start_later_observed_variance( + stationary, recovered + ), + Err( + psychometric_core::PsychometricError::StationaryLaterObservedVarianceIsNotPredeterminedLaterStartLaterObservedVariance + ) + ); +} + +#[test] +fn standardised_discrete_drift_is_not_unstandardised_or_trait_contaminated() { + let diffusion = 0.4_f64; + let log_rate = -0.5_f64; + let event_delta = 1.0_f64; + let recovered = + recover_standardised_discrete_drift(diffusion, log_rate, event_delta, LagClock::EventTime) + .expect("discreteDRIFTstd"); + let unstandardised = + recover_discrete_lag_from_log_rate(log_rate, event_delta, LagClock::EventTime) + .expect("discreteDRIFT"); + let within = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + assert!( + (recovered - unstandardised).abs() < 1e-15, + "Driver et al. (2017, p. 16): scalar discreteDRIFTstd equals exp(a Δt) after positive asymDIFFUSION" + ); + let trait_variance = 1.0_f64; + let lagged = recover_trait_plus_state_lagged_covariance( + trait_variance, + within, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("trait+state lag"); + let total = + recover_trait_plus_state_latent_variance(trait_variance, within).expect("trait+state var"); + let contaminated = lagged / total; + assert!( + (contaminated - recovered).abs() > 1e-3, + "Driver et al. (2017, footnote 4 / §7.1): TRAITVAR contaminates the auto-effect" + ); + assert_eq!( + recover_standardised_discrete_drift(0.0, log_rate, event_delta, LagClock::EventTime), + Err( + psychometric_core::PsychometricError::StandardisedDiscreteDriftRequiresPositiveWithinSubjectVariance + ) + ); + assert_eq!( + recover_standardised_discrete_drift(diffusion, 0.5, event_delta, LagClock::EventTime), + Err(psychometric_core::PsychometricError::StationaryVarianceRequiresStableDrift) + ); + assert_eq!( + refuse_unstandardised_discrete_drift_as_standardised_discrete_drift( + unstandardised, + recovered + ), + Err( + psychometric_core::PsychometricError::UnstandardisedDiscreteDriftIsNotStandardisedDiscreteDrift + ) + ); + assert_eq!( + refuse_trait_plus_state_autocorrelation_as_standardised_discrete_drift( + contaminated, + recovered + ), + Err( + psychometric_core::PsychometricError::TraitPlusStateAutocorrelationIsNotStandardisedDiscreteDrift + ) + ); + assert_eq!( + refuse_trait_variance_as_standardisation_variance(trait_variance, within), + Err(psychometric_core::PsychometricError::TraitVarianceIsNotStandardisationVariance) + ); +} + +#[test] +fn standardised_discrete_diffusion_is_not_unstandardised_or_trait_contaminated() { + let diffusion = 0.4_f64; + let log_rate = -0.5_f64; + let event_delta = 1.0_f64; + let recovered = recover_standardised_discrete_diffusion( + diffusion, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("discreteDIFFUSIONstd"); + let process_noise = + recover_discrete_process_noise(diffusion, log_rate, event_delta, LagClock::EventTime) + .expect("discreteDIFFUSION"); + let within = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + assert!( + (recovered - (process_noise / within)).abs() < 1e-15, + "Driver et al. (2017, p. 16 / footnote 4): scalar discreteDIFFUSIONstd is Q_Δt / asymDIFFUSION" + ); + assert!((recovered - (1.0 - (2.0 * log_rate * event_delta).exp())).abs() < 1e-15); + let continuous_std = diffusion / within; + assert!( + (continuous_std - recovered).abs() > 1e-3, + "Driver et al. (2017, Eq. 4): −2 a is not Q_Δt / p" + ); + let trait_variance = 1.0_f64; + let total = + recover_trait_plus_state_latent_variance(trait_variance, within).expect("trait+state var"); + let contaminated = process_noise / total; + assert!( + (contaminated - recovered).abs() > 1e-3, + "Driver et al. (2017, footnote 4 / §7.1): TRAITVAR contaminates the process-noise ratio" + ); + assert_eq!( + recover_standardised_discrete_diffusion(0.0, log_rate, event_delta, LagClock::EventTime), + Err( + psychometric_core::PsychometricError::StandardisedDiscreteDiffusionRequiresPositiveWithinSubjectVariance + ) + ); + assert_eq!( + recover_standardised_discrete_diffusion(diffusion, 0.5, event_delta, LagClock::EventTime), + Err(psychometric_core::PsychometricError::StationaryVarianceRequiresStableDrift) + ); + assert_eq!( + refuse_unstandardised_discrete_diffusion_as_standardised_discrete_diffusion( + process_noise, + recovered + ), + Err( + psychometric_core::PsychometricError::UnstandardisedDiscreteDiffusionIsNotStandardisedDiscreteDiffusion + ) + ); + assert_eq!( + refuse_standardised_continuous_diffusion_as_standardised_discrete_diffusion( + continuous_std, + recovered + ), + Err( + psychometric_core::PsychometricError::StandardisedContinuousDiffusionIsNotStandardisedDiscreteDiffusion + ) + ); + assert_eq!( + refuse_trait_contaminated_process_noise_as_standardised_discrete_diffusion( + contaminated, + recovered + ), + Err( + psychometric_core::PsychometricError::TraitContaminatedProcessNoiseIsNotStandardisedDiscreteDiffusion + ) + ); + assert_eq!( + refuse_trait_variance_as_standardisation_variance(trait_variance, within), + Err(psychometric_core::PsychometricError::TraitVarianceIsNotStandardisationVariance) + ); +} + +#[test] +fn standardised_continuous_diffusion_is_not_unstandardised_or_trait_contaminated() { + let diffusion = 0.4_f64; + let log_rate = -0.5_f64; + let recovered = + recover_standardised_continuous_diffusion(diffusion, log_rate, LagClock::EventTime) + .expect("DIFFUSIONstd"); + let within = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + assert!( + (recovered - (diffusion / within)).abs() < 1e-15, + "Driver et al. (2017, p. 16 / footnote 4): scalar DIFFUSIONstd is q / asymDIFFUSION" + ); + assert!((recovered - (-2.0 * log_rate)).abs() < 1e-15); + let larger_q = recover_standardised_continuous_diffusion(2.0, log_rate, LagClock::EventTime) + .expect("DIFFUSIONstd q=2"); + assert!((larger_q - recovered).abs() < 1e-15); + let discrete = + recover_standardised_discrete_diffusion(diffusion, log_rate, 1.0, LagClock::EventTime) + .expect("discreteDIFFUSIONstd"); + assert!( + (discrete - recovered).abs() > 1e-3, + "Driver et al. (2017, Eq. 4): Q_Δt / p is not −2 a" + ); + let trait_variance = 1.0_f64; + let total = + recover_trait_plus_state_latent_variance(trait_variance, within).expect("trait+state var"); + let contaminated = diffusion / total; + assert!( + (contaminated - recovered).abs() > 1e-3, + "Driver et al. (2017, footnote 4 / §7.1): TRAITVAR contaminates the continuous-diffusion ratio" + ); + assert_eq!( + recover_standardised_continuous_diffusion(0.0, log_rate, LagClock::EventTime), + Err( + psychometric_core::PsychometricError::StandardisedContinuousDiffusionRequiresPositiveWithinSubjectVariance + ) + ); + assert_eq!( + recover_standardised_continuous_diffusion(diffusion, 0.5, LagClock::EventTime), + Err(psychometric_core::PsychometricError::StationaryVarianceRequiresStableDrift) + ); + assert_eq!( + refuse_unstandardised_continuous_diffusion_as_standardised_continuous_diffusion( + diffusion, + recovered + ), + Err( + psychometric_core::PsychometricError::UnstandardisedContinuousDiffusionIsNotStandardisedContinuousDiffusion + ) + ); + assert_eq!( + refuse_standardised_discrete_diffusion_as_standardised_continuous_diffusion( + discrete, + recovered + ), + Err( + psychometric_core::PsychometricError::StandardisedDiscreteDiffusionIsNotStandardisedContinuousDiffusion + ) + ); + assert_eq!( + refuse_trait_contaminated_continuous_diffusion_as_standardised_continuous_diffusion( + contaminated, + recovered + ), + Err( + psychometric_core::PsychometricError::TraitContaminatedContinuousDiffusionIsNotStandardisedContinuousDiffusion + ) + ); + assert_eq!( + refuse_trait_variance_as_standardisation_variance(trait_variance, within), + Err(psychometric_core::PsychometricError::TraitVarianceIsNotStandardisationVariance) + ); +} + +#[test] +fn standardised_continuous_drift_is_not_unstandardised_or_trait_contaminated() { + let diffusion = 0.4_f64; + let log_rate = -0.5_f64; + let recovered = recover_standardised_continuous_drift(diffusion, log_rate, LagClock::EventTime) + .expect("DRIFTstd"); + let within = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + assert!( + (recovered - log_rate).abs() < 1e-15, + "Driver et al. (2017, p. 16 / footnote 4): scalar DRIFTstd equals a after positive asymDIFFUSION" + ); + let larger_q = recover_standardised_continuous_drift(2.0, log_rate, LagClock::EventTime) + .expect("DRIFTstd q=2"); + assert!((larger_q - recovered).abs() < 1e-15); + let discrete = + recover_standardised_discrete_drift(diffusion, log_rate, 1.0, LagClock::EventTime) + .expect("discreteDRIFTstd"); + assert!( + (discrete - recovered).abs() > 1e-3, + "Driver et al. (2017, Eq. 3): exp(a Δt) is not a" + ); + let trait_variance = 1.0_f64; + let total = + recover_trait_plus_state_latent_variance(trait_variance, within).expect("trait+state var"); + let contaminated = log_rate * (within / total); + assert!( + (contaminated - recovered).abs() > 1e-3, + "Driver et al. (2017, footnote 4 / §7.1): TRAITVAR contaminates the continuous auto-effect" + ); + assert_eq!( + recover_standardised_continuous_drift(0.0, log_rate, LagClock::EventTime), + Err( + psychometric_core::PsychometricError::StandardisedContinuousDriftRequiresPositiveWithinSubjectVariance + ) + ); + assert_eq!( + recover_standardised_continuous_drift(diffusion, 0.5, LagClock::EventTime), + Err(psychometric_core::PsychometricError::StationaryVarianceRequiresStableDrift) + ); + assert_eq!( + refuse_unstandardised_continuous_drift_as_standardised_continuous_drift( + log_rate, + recovered + ), + Err( + psychometric_core::PsychometricError::UnstandardisedContinuousDriftIsNotStandardisedContinuousDrift + ) + ); + assert_eq!( + refuse_standardised_discrete_drift_as_standardised_continuous_drift(discrete, recovered), + Err( + psychometric_core::PsychometricError::StandardisedDiscreteDriftIsNotStandardisedContinuousDrift + ) + ); + assert_eq!( + refuse_trait_contaminated_continuous_drift_as_standardised_continuous_drift( + contaminated, + recovered + ), + Err( + psychometric_core::PsychometricError::TraitContaminatedContinuousDriftIsNotStandardisedContinuousDrift + ) + ); + assert_eq!( + refuse_trait_variance_as_standardisation_variance(trait_variance, within), + Err(psychometric_core::PsychometricError::TraitVarianceIsNotStandardisationVariance) + ); +} + +#[allow(clippy::too_many_lines)] +#[test] +fn standardised_asymptotic_time_independent_effect_is_not_unstandardised_or_trait_contaminated() { + let diffusion = 0.4_f64; + let log_rate = -0.5_f64; + let coefficient = 0.3_f64; + let predictor_variance = 1.0_f64; + let recovered = recover_standardised_asymptotic_time_independent_predictor_effect( + coefficient, + predictor_variance, + diffusion, + log_rate, + LagClock::EventTime, + ) + .expect("asymTIPREDEFFECTstd"); + let within = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + let unit = recover_asymptotic_time_independent_predictor_effect( + coefficient, + 1.0, + log_rate, + LagClock::EventTime, + ) + .expect("asymTIPREDEFFECT"); + assert!( + (recovered - unit * predictor_variance.sqrt() / within.sqrt()).abs() < 1e-15, + "Driver et al. (2017, p. 16 / footnote 4): asymTIPREDEFFECTstd is (−B/a)·√v/√p" + ); + let larger_q = recover_standardised_asymptotic_time_independent_predictor_effect( + coefficient, + predictor_variance, + 2.0, + log_rate, + LagClock::EventTime, + ) + .expect("asymTIPREDEFFECTstd q=2"); + assert!((larger_q - recovered).abs() > 1e-3); + let discrete_increment = recover_discrete_time_independent_predictor_effect( + coefficient, + 1.0, + log_rate, + 1.0, + LagClock::EventTime, + ) + .expect("discrete TIPREDEFFECT"); + let discrete_std = discrete_increment * predictor_variance.sqrt() / within.sqrt(); + assert!( + (discrete_std - recovered).abs() > 1e-3, + "Driver et al. (2017, Eq. 3): finite-interval standardised TIPRED is not asymTIPREDEFFECTstd" + ); + let trait_variance = 1.0_f64; + let total = + recover_trait_plus_state_latent_variance(trait_variance, within).expect("trait+state var"); + let contaminated = unit * predictor_variance.sqrt() / total.sqrt(); + assert!( + (contaminated - recovered).abs() > 1e-3, + "Driver et al. (2017, footnote 4 / §7.1): TRAITVAR contaminates the affected SD" + ); + assert_eq!( + recover_standardised_asymptotic_time_independent_predictor_effect( + coefficient, + predictor_variance, + 0.0, + log_rate, + LagClock::EventTime + ), + Err( + psychometric_core::PsychometricError::StandardisedAsymptoticTimeIndependentEffectRequiresPositiveWithinSubjectVariance + ) + ); + assert_eq!( + recover_standardised_asymptotic_time_independent_predictor_effect( + coefficient, + 0.0, + diffusion, + log_rate, + LagClock::EventTime + ), + Err( + psychometric_core::PsychometricError::StandardisedAsymptoticTimeIndependentEffectRequiresPositivePredictorVariance + ) + ); + assert_eq!( + recover_standardised_asymptotic_time_independent_predictor_effect( + coefficient, + predictor_variance, + diffusion, + 0.5, + LagClock::EventTime + ), + Err(psychometric_core::PsychometricError::StationaryVarianceRequiresStableDrift) + ); + assert_eq!( + refuse_unstandardised_asymptotic_time_independent_effect_as_standardised_asymptotic_time_independent_effect( + unit, + recovered + ), + Err( + psychometric_core::PsychometricError::UnstandardisedAsymptoticTimeIndependentEffectIsNotStandardisedAsymptoticTimeIndependentEffect + ) + ); + assert_eq!( + refuse_standardised_discrete_time_independent_effect_as_standardised_asymptotic_time_independent_effect( + discrete_std, + recovered + ), + Err( + psychometric_core::PsychometricError::StandardisedDiscreteTimeIndependentEffectIsNotStandardisedAsymptoticTimeIndependentEffect + ) + ); + assert_eq!( + refuse_trait_contaminated_asymptotic_time_independent_effect_as_standardised_asymptotic_time_independent_effect( + contaminated, + recovered + ), + Err( + psychometric_core::PsychometricError::TraitContaminatedAsymptoticTimeIndependentEffectIsNotStandardisedAsymptoticTimeIndependentEffect + ) + ); + assert_eq!( + refuse_trait_variance_as_standardisation_variance(trait_variance, within), + Err(psychometric_core::PsychometricError::TraitVarianceIsNotStandardisationVariance) + ); +} + +#[allow(clippy::too_many_lines)] +#[test] +fn standardised_continuous_time_independent_effect_is_not_unstandardised_or_trait_contaminated() { + let diffusion = 0.4_f64; + let log_rate = -0.5_f64; + let coefficient = 0.3_f64; + let predictor_variance = 1.0_f64; + let recovered = recover_standardised_continuous_time_independent_predictor_effect( + coefficient, + predictor_variance, + diffusion, + log_rate, + LagClock::EventTime, + ) + .expect("TIPREDEFFECTstd"); + let within = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + assert!( + (recovered - coefficient * predictor_variance.sqrt() / within.sqrt()).abs() < 1e-15, + "Driver et al. (2017, p. 16 / footnote 4): TIPREDEFFECTstd is B·√v/√p" + ); + let larger_q = recover_standardised_continuous_time_independent_predictor_effect( + coefficient, + predictor_variance, + 2.0, + log_rate, + LagClock::EventTime, + ) + .expect("TIPREDEFFECTstd q=2"); + assert!((larger_q - recovered).abs() > 1e-3); + let asymptotic = recover_standardised_asymptotic_time_independent_predictor_effect( + coefficient, + predictor_variance, + diffusion, + log_rate, + LagClock::EventTime, + ) + .expect("asymTIPREDEFFECTstd"); + assert!( + (asymptotic - recovered).abs() > 1e-3, + "Driver et al. (2017, p. 16 / §7.2): asymTIPREDEFFECTstd is not TIPREDEFFECTstd" + ); + let discrete_increment = recover_discrete_time_independent_predictor_effect( + coefficient, + 1.0, + log_rate, + 1.0, + LagClock::EventTime, + ) + .expect("discrete TIPREDEFFECT"); + let discrete_std = discrete_increment * predictor_variance.sqrt() / within.sqrt(); + assert!( + (discrete_std - recovered).abs() > 1e-3, + "Driver et al. (2017, Eq. 3): finite-interval standardised TIPRED is not TIPREDEFFECTstd" + ); + let trait_variance = 1.0_f64; + let total = + recover_trait_plus_state_latent_variance(trait_variance, within).expect("trait+state var"); + let contaminated = coefficient * predictor_variance.sqrt() / total.sqrt(); + assert!( + (contaminated - recovered).abs() > 1e-3, + "Driver et al. (2017, footnote 4 / §7.1): TRAITVAR contaminates the affected SD" + ); + assert_eq!( + recover_standardised_continuous_time_independent_predictor_effect( + coefficient, + predictor_variance, + 0.0, + log_rate, + LagClock::EventTime + ), + Err( + psychometric_core::PsychometricError::StandardisedContinuousTimeIndependentEffectRequiresPositiveWithinSubjectVariance + ) + ); + assert_eq!( + recover_standardised_continuous_time_independent_predictor_effect( + coefficient, + 0.0, + diffusion, + log_rate, + LagClock::EventTime + ), + Err( + psychometric_core::PsychometricError::StandardisedContinuousTimeIndependentEffectRequiresPositivePredictorVariance + ) + ); + assert_eq!( + recover_standardised_continuous_time_independent_predictor_effect( + coefficient, + predictor_variance, + diffusion, + 0.5, + LagClock::EventTime + ), + Err(psychometric_core::PsychometricError::StationaryVarianceRequiresStableDrift) + ); + assert_eq!( + refuse_unstandardised_continuous_time_independent_effect_as_standardised_continuous_time_independent_effect( + coefficient, + recovered + ), + Err( + psychometric_core::PsychometricError::UnstandardisedContinuousTimeIndependentEffectIsNotStandardisedContinuousTimeIndependentEffect + ) + ); + assert_eq!( + refuse_standardised_asymptotic_time_independent_effect_as_standardised_continuous_time_independent_effect( + asymptotic, + recovered + ), + Err( + psychometric_core::PsychometricError::StandardisedAsymptoticTimeIndependentEffectIsNotStandardisedContinuousTimeIndependentEffect + ) + ); + assert_eq!( + refuse_standardised_discrete_time_independent_effect_as_standardised_continuous_time_independent_effect( + discrete_std, + recovered + ), + Err( + psychometric_core::PsychometricError::StandardisedDiscreteTimeIndependentEffectIsNotStandardisedContinuousTimeIndependentEffect + ) + ); + assert_eq!( + refuse_trait_contaminated_continuous_time_independent_effect_as_standardised_continuous_time_independent_effect( + contaminated, + recovered + ), + Err( + psychometric_core::PsychometricError::TraitContaminatedContinuousTimeIndependentEffectIsNotStandardisedContinuousTimeIndependentEffect + ) + ); + assert_eq!( + refuse_trait_variance_as_standardisation_variance(trait_variance, within), + Err(psychometric_core::PsychometricError::TraitVarianceIsNotStandardisationVariance) + ); +} + +#[allow(clippy::too_many_lines)] +#[test] +fn standardised_continuous_time_dependent_effect_is_not_unstandardised_or_trait_contaminated() { + let diffusion = 0.4_f64; + let log_rate = -0.5_f64; + let coefficient = 0.3_f64; + let predictor_variance = 1.0_f64; + let recovered = recover_standardised_continuous_time_dependent_predictor_effect( + coefficient, + predictor_variance, + diffusion, + log_rate, + LagClock::EventTime, + ) + .expect("TDPREDEFFECTstd"); + let within = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + assert!( + (recovered - coefficient * predictor_variance.sqrt() / within.sqrt()).abs() < 1e-15, + "Driver et al. (2017, p. 16 / footnote 4): TDPREDEFFECTstd is m·√v/√p" + ); + let larger_q = recover_standardised_continuous_time_dependent_predictor_effect( + coefficient, + predictor_variance, + 2.0, + log_rate, + LagClock::EventTime, + ) + .expect("TDPREDEFFECTstd q=2"); + assert!((larger_q - recovered).abs() > 1e-3); + let tipred = recover_standardised_continuous_time_independent_predictor_effect( + coefficient, + predictor_variance, + diffusion, + log_rate, + LagClock::EventTime, + ) + .expect("TIPREDEFFECTstd"); + assert_eq!( + tipred.to_bits(), + recovered.to_bits(), + "Driver et al. (2017, Table 2): equal numbers when M=B are still distinct named quantities" + ); + let discrete_increment = recover_discrete_time_independent_predictor_effect( + coefficient, + 1.0, + log_rate, + 1.0, + LagClock::EventTime, + ) + .expect("intercept-style discrete TDPREDEFFECT"); + let discrete_std = discrete_increment * predictor_variance.sqrt() / within.sqrt(); + assert!( + (discrete_std - recovered).abs() > 1e-3, + "Driver et al. (2017, Eq. 3): intercept-style standardised TDPRED is not TDPREDEFFECTstd" + ); + let trait_variance = 1.0_f64; + let total = + recover_trait_plus_state_latent_variance(trait_variance, within).expect("trait+state var"); + let contaminated = coefficient * predictor_variance.sqrt() / total.sqrt(); + assert!( + (contaminated - recovered).abs() > 1e-3, + "Driver et al. (2017, footnote 4 / §7.1): TRAITVAR contaminates the affected SD" + ); + assert_eq!( + recover_standardised_continuous_time_dependent_predictor_effect( + coefficient, + predictor_variance, + 0.0, + log_rate, + LagClock::EventTime + ), + Err( + psychometric_core::PsychometricError::StandardisedContinuousTimeDependentEffectRequiresPositiveWithinSubjectVariance + ) + ); + assert_eq!( + recover_standardised_continuous_time_dependent_predictor_effect( + coefficient, + 0.0, + diffusion, + log_rate, + LagClock::EventTime + ), + Err( + psychometric_core::PsychometricError::StandardisedContinuousTimeDependentEffectRequiresPositivePredictorVariance + ) + ); + assert_eq!( + recover_standardised_continuous_time_dependent_predictor_effect( + coefficient, + predictor_variance, + diffusion, + 0.5, + LagClock::EventTime + ), + Err(psychometric_core::PsychometricError::StationaryVarianceRequiresStableDrift) + ); + assert_eq!( + refuse_unstandardised_continuous_time_dependent_effect_as_standardised_continuous_time_dependent_effect( + coefficient, + recovered + ), + Err( + psychometric_core::PsychometricError::UnstandardisedContinuousTimeDependentEffectIsNotStandardisedContinuousTimeDependentEffect + ) + ); + assert_eq!( + refuse_standardised_continuous_time_independent_effect_as_standardised_continuous_time_dependent_effect( + tipred, + recovered + ), + Err( + psychometric_core::PsychometricError::StandardisedContinuousTimeIndependentEffectIsNotStandardisedContinuousTimeDependentEffect + ) + ); + assert_eq!( + refuse_standardised_discrete_time_dependent_effect_as_standardised_continuous_time_dependent_effect( + discrete_std, + recovered + ), + Err( + psychometric_core::PsychometricError::StandardisedDiscreteTimeDependentEffectIsNotStandardisedContinuousTimeDependentEffect + ) + ); + assert_eq!( + refuse_trait_contaminated_continuous_time_dependent_effect_as_standardised_continuous_time_dependent_effect( + contaminated, + recovered + ), + Err( + psychometric_core::PsychometricError::TraitContaminatedContinuousTimeDependentEffectIsNotStandardisedContinuousTimeDependentEffect + ) + ); + assert_eq!( + refuse_trait_variance_as_standardisation_variance(trait_variance, within), + Err(psychometric_core::PsychometricError::TraitVarianceIsNotStandardisationVariance) + ); +} + +#[allow(clippy::too_many_lines)] +#[test] +fn standardised_initial_time_dependent_effect_is_not_unstandardised_or_trait_contaminated() { + let initial_variance = 1.6_f64; + let coefficient = 0.3_f64; + let predictor_variance = 1.0_f64; + let recovered = recover_standardised_initial_time_dependent_predictor_effect( + coefficient, + predictor_variance, + initial_variance, + LagClock::EventTime, + ) + .expect("T0TDPREDEFFECTstd"); + assert!( + (recovered - coefficient * predictor_variance.sqrt() / initial_variance.sqrt()).abs() + < 1e-15, + "Driver et al. (2017, Table 3 / footnote 4): T0TDPREDEFFECTstd is t0_m·√v/√p_0" + ); + let larger_p0 = recover_standardised_initial_time_dependent_predictor_effect( + coefficient, + predictor_variance, + 6.4, + LagClock::EventTime, + ) + .expect("T0TDPREDEFFECTstd p_0=6.4"); + assert!((larger_p0 - recovered).abs() > 1e-3); + let continuous = recover_standardised_continuous_time_dependent_predictor_effect( + coefficient, + predictor_variance, + 0.4, + -0.5, + LagClock::EventTime, + ) + .expect("TDPREDEFFECTstd"); + assert!( + (continuous - recovered).abs() > 1e-3, + "Driver et al. (2017, p. 16 / Table 3): TDPREDEFFECTstd is not T0TDPREDEFFECTstd" + ); + let t0_tipred = recover_standardised_initial_time_independent_predictor_effect( + coefficient, + predictor_variance, + initial_variance, + LagClock::EventTime, + ) + .expect("T0TIPREDEFFECTstd"); + assert_eq!( + t0_tipred.to_bits(), + recovered.to_bits(), + "Driver et al. (2017, Table 3): equal numbers when t0_m=t0_b are still distinct named quantities" + ); + let trait_variance = 1.0_f64; + let total = recover_trait_plus_state_latent_variance(trait_variance, initial_variance) + .expect("trait+state var"); + let contaminated = coefficient * predictor_variance.sqrt() / total.sqrt(); + assert!( + (contaminated - recovered).abs() > 1e-3, + "Driver et al. (2017, footnote 4 / §7.1): TRAITVAR contaminates the affected SD" + ); + assert_eq!( + recover_standardised_initial_time_dependent_predictor_effect( + coefficient, + predictor_variance, + 0.0, + LagClock::EventTime + ), + Err( + psychometric_core::PsychometricError::StandardisedInitialTimeDependentEffectRequiresPositiveInitialLatentVariance + ) + ); + assert_eq!( + recover_standardised_initial_time_dependent_predictor_effect( + coefficient, + 0.0, + initial_variance, + LagClock::EventTime + ), + Err( + psychometric_core::PsychometricError::StandardisedInitialTimeDependentEffectRequiresPositivePredictorVariance + ) + ); + assert_eq!( + recover_standardised_initial_time_dependent_predictor_effect( + coefficient, + predictor_variance, + initial_variance, + LagClock::SystemTime + ), + Err(psychometric_core::PsychometricError::EventTimeRequired) + ); + assert_eq!( + refuse_unstandardised_initial_time_dependent_effect_as_standardised_initial_time_dependent_effect( + coefficient, + recovered + ), + Err( + psychometric_core::PsychometricError::UnstandardisedInitialTimeDependentEffectIsNotStandardisedInitialTimeDependentEffect + ) + ); + assert_eq!( + refuse_standardised_continuous_time_dependent_effect_as_standardised_initial_time_dependent_effect( + continuous, + recovered + ), + Err( + psychometric_core::PsychometricError::StandardisedContinuousTimeDependentEffectIsNotStandardisedInitialTimeDependentEffect + ) + ); + assert_eq!( + refuse_standardised_initial_time_independent_effect_as_standardised_initial_time_dependent_effect( + t0_tipred, + recovered + ), + Err( + psychometric_core::PsychometricError::StandardisedInitialTimeIndependentEffectIsNotStandardisedInitialTimeDependentEffect + ) + ); + assert_eq!( + refuse_trait_contaminated_initial_time_dependent_effect_as_standardised_initial_time_dependent_effect( + contaminated, + recovered + ), + Err( + psychometric_core::PsychometricError::TraitContaminatedInitialTimeDependentEffectIsNotStandardisedInitialTimeDependentEffect + ) + ); + assert_eq!( + refuse_trait_variance_as_standardisation_variance(trait_variance, initial_variance), + Err(psychometric_core::PsychometricError::TraitVarianceIsNotStandardisationVariance) + ); +} + +#[allow(clippy::too_many_lines)] +#[test] +fn standardised_initial_latent_variance_is_not_unstandardised_or_an_effect() { + let initial_variance = 1.6_f64; + let recovered = + recover_standardised_initial_latent_variance(initial_variance, LagClock::EventTime) + .expect("T0VARstd"); + assert!( + (recovered - 1.0).abs() < 1e-15, + "Driver et al. (2017, p. 16 / 2017-era summary.ctsemFit.R): T0VARstd is p_0/p_0 = 1" + ); + let larger_p0 = recover_standardised_initial_latent_variance(6.4, LagClock::EventTime) + .expect("T0VARstd p_0=6.4"); + assert_eq!( + larger_p0.to_bits(), + recovered.to_bits(), + "Driver et al. (2017, p. 16): distinct positive T0VAR recover the same T0VARstd" + ); + let standardised_effect = recover_standardised_initial_time_dependent_predictor_effect( + 0.3, + 1.0, + initial_variance, + LagClock::EventTime, + ) + .expect("T0TDPREDEFFECTstd"); + assert!( + (standardised_effect - recovered).abs() > 1e-3, + "Driver et al. (2017, Table 2 / Table 3): T0TDPREDEFFECTstd is not T0VARstd" + ); + let extra = recover_initial_time_independent_predictor_variance(0.3, 4.0, LagClock::EventTime) + .expect("addedT0TIPREDVAR"); + assert!( + (extra - recovered).abs() > 1e-3, + "Driver et al. (2017, 2017-era addedT0TIPREDVAR): t0_b² v is not T0VARstd" + ); + assert!((initial_variance - recovered).abs() > 1e-3); + assert_eq!( + recover_standardised_initial_latent_variance(0.0, LagClock::EventTime), + Err( + psychometric_core::PsychometricError::StandardisedInitialLatentVarianceRequiresPositiveInitialLatentVariance + ) + ); + assert_eq!( + recover_standardised_initial_latent_variance(initial_variance, LagClock::SystemTime), + Err(psychometric_core::PsychometricError::EventTimeRequired) + ); + assert_eq!( + refuse_unstandardised_initial_latent_variance_as_standardised_initial_latent_variance( + initial_variance, + recovered + ), + Err( + psychometric_core::PsychometricError::UnstandardisedInitialLatentVarianceIsNotStandardisedInitialLatentVariance + ) + ); + assert_eq!( + refuse_standardised_initial_time_dependent_effect_as_standardised_initial_latent_variance( + standardised_effect, + recovered + ), + Err( + psychometric_core::PsychometricError::StandardisedInitialTimeDependentEffectIsNotStandardisedInitialLatentVariance + ) + ); + assert_eq!( + refuse_initial_time_independent_variance_as_standardised_initial_latent_variance( + extra, + recovered + ), + Err( + psychometric_core::PsychometricError::InitialTimeIndependentVarianceIsNotStandardisedInitialLatentVariance + ) + ); + assert_eq!( + refuse_trait_variance_as_standardisation_variance(1.0, initial_variance), + Err(psychometric_core::PsychometricError::TraitVarianceIsNotStandardisationVariance) + ); +} + +#[allow(clippy::too_many_lines)] +#[test] +fn standardised_trait_variance_is_not_unstandardised_or_t0varstd() { + let trait_variance = 1.6_f64; + let recovered = recover_standardised_trait_variance(trait_variance, LagClock::EventTime) + .expect("TRAITVARstd"); + assert!( + (recovered - 1.0).abs() < 1e-15, + "Driver et al. (2017, p. 16 / 2017-era summary.ctsemFit.R): TRAITVARstd is trait/trait = 1" + ); + let larger_trait = recover_standardised_trait_variance(6.4, LagClock::EventTime) + .expect("TRAITVARstd trait=6.4"); + assert_eq!( + larger_trait.to_bits(), + recovered.to_bits(), + "Driver et al. (2017, p. 16): distinct positive TRAITVAR recover the same TRAITVARstd" + ); + let t0var_std = + recover_standardised_initial_latent_variance(trait_variance, LagClock::EventTime) + .expect("T0VARstd"); + assert_eq!( + t0var_std.to_bits(), + recovered.to_bits(), + "Driver et al. (2017, p. 16): T0VARstd and TRAITVARstd equal 1 and remain distinct named quantities" + ); + let extra = recover_initial_time_independent_predictor_variance(0.3, 4.0, LagClock::EventTime) + .expect("addedT0TIPREDVAR"); + assert!( + (extra - recovered).abs() > 1e-3, + "Driver et al. (2017, 2017-era addedT0TIPREDVAR): t0_b² v is not TRAITVARstd" + ); + assert!((trait_variance - recovered).abs() > 1e-3); + assert_eq!( + recover_standardised_trait_variance(0.0, LagClock::EventTime), + Err( + psychometric_core::PsychometricError::StandardisedTraitVarianceRequiresPositiveTraitVariance + ) + ); + assert_eq!( + recover_standardised_trait_variance(trait_variance, LagClock::SystemTime), + Err(psychometric_core::PsychometricError::EventTimeRequired) + ); + assert_eq!( + refuse_unstandardised_trait_variance_as_standardised_trait_variance( + trait_variance, + recovered + ), + Err( + psychometric_core::PsychometricError::UnstandardisedTraitVarianceIsNotStandardisedTraitVariance + ) + ); + assert_eq!( + refuse_standardised_initial_latent_variance_as_standardised_trait_variance( + t0var_std, + recovered + ), + Err( + psychometric_core::PsychometricError::StandardisedInitialLatentVarianceIsNotStandardisedTraitVariance + ) + ); + assert_eq!( + refuse_initial_time_independent_variance_as_standardised_trait_variance(extra, recovered), + Err( + psychometric_core::PsychometricError::InitialTimeIndependentVarianceIsNotStandardisedTraitVariance + ) + ); +} + +#[allow(clippy::too_many_lines)] +#[test] +fn standardised_manifest_trait_variance_is_not_unstandardised_or_traitvarstd() { + let manifest_trait = 1.6_f64; + let recovered = + recover_standardised_manifest_trait_variance(manifest_trait, LagClock::EventTime) + .expect("MANIFESTTRAITVARstd"); + assert!( + (recovered - 1.0).abs() < 1e-15, + "Driver et al. (2017, p. 16 / 2017-era summary.ctsemFit.R): MANIFESTTRAITVARstd is ψ/ψ = 1" + ); + let larger_psi = recover_standardised_manifest_trait_variance(6.4, LagClock::EventTime) + .expect("MANIFESTTRAITVARstd ψ=6.4"); + assert_eq!( + larger_psi.to_bits(), + recovered.to_bits(), + "Driver et al. (2017, p. 16): distinct positive MANIFESTTRAITVAR recover the same MANIFESTTRAITVARstd" + ); + let trait_std = recover_standardised_trait_variance(manifest_trait, LagClock::EventTime) + .expect("TRAITVARstd"); + assert_eq!( + trait_std.to_bits(), + recovered.to_bits(), + "Driver et al. (2017, p. 16): TRAITVARstd and MANIFESTTRAITVARstd equal 1 and remain distinct named quantities" + ); + assert!((manifest_trait - recovered).abs() > 1e-3); + assert_eq!( + recover_standardised_manifest_trait_variance(0.0, LagClock::EventTime), + Err( + psychometric_core::PsychometricError::StandardisedManifestTraitVarianceRequiresPositiveManifestTraitVariance + ) + ); + assert_eq!( + recover_standardised_manifest_trait_variance(manifest_trait, LagClock::SystemTime), + Err(psychometric_core::PsychometricError::EventTimeRequired) + ); + assert_eq!( + refuse_unstandardised_manifest_trait_variance_as_standardised_manifest_trait_variance( + manifest_trait, + recovered + ), + Err( + psychometric_core::PsychometricError::UnstandardisedManifestTraitVarianceIsNotStandardisedManifestTraitVariance + ) + ); + assert_eq!( + refuse_standardised_trait_variance_as_standardised_manifest_trait_variance( + trait_std, + recovered + ), + Err( + psychometric_core::PsychometricError::StandardisedTraitVarianceIsNotStandardisedManifestTraitVariance + ) + ); + assert_eq!( + refuse_measurement_error_as_standardised_manifest_trait_variance(0.4, recovered), + Err( + psychometric_core::PsychometricError::MeasurementErrorIsNotStandardisedManifestTraitVariance + ) + ); +} + +#[allow(clippy::too_many_lines)] +#[test] +fn standardised_manifest_variance_is_not_unstandardised_or_manifesttraitvarstd() { + let measurement_error = 0.4_f64; + let recovered = recover_standardised_manifest_variance(measurement_error, LagClock::EventTime) + .expect("MANIFESTVARstd"); + assert!( + (recovered - 1.0).abs() < 1e-15, + "Driver et al. (2017, p. 16 / 2017-era summary.ctsemFit.R): MANIFESTVARstd is θ/θ = 1" + ); + let larger_theta = recover_standardised_manifest_variance(1.6, LagClock::EventTime) + .expect("MANIFESTVARstd θ=1.6"); + assert_eq!( + larger_theta.to_bits(), + recovered.to_bits(), + "Driver et al. (2017, p. 16): distinct positive MANIFESTVAR recover the same MANIFESTVARstd" + ); + let manifest_trait_std = + recover_standardised_manifest_trait_variance(measurement_error, LagClock::EventTime) + .expect("MANIFESTTRAITVARstd"); + assert_eq!( + manifest_trait_std.to_bits(), + recovered.to_bits(), + "Driver et al. (2017, p. 16): MANIFESTTRAITVARstd and MANIFESTVARstd equal 1 and remain distinct named quantities" + ); + assert!((measurement_error - recovered).abs() > 1e-3); + let observed = recover_manifest_observed_variance(2.0, 0.4, measurement_error).expect("Var(y)"); + assert!((observed - recovered).abs() > 1e-3); + assert_eq!( + recover_standardised_manifest_variance(0.0, LagClock::EventTime), + Err( + psychometric_core::PsychometricError::StandardisedManifestVarianceRequiresPositiveManifestVariance + ) + ); + assert_eq!( + recover_standardised_manifest_variance(measurement_error, LagClock::SystemTime), + Err(psychometric_core::PsychometricError::EventTimeRequired) + ); + assert_eq!( + refuse_unstandardised_manifest_variance_as_standardised_manifest_variance( + measurement_error, + recovered + ), + Err( + psychometric_core::PsychometricError::UnstandardisedManifestVarianceIsNotStandardisedManifestVariance + ) + ); + assert_eq!( + refuse_standardised_manifest_trait_variance_as_standardised_manifest_variance( + manifest_trait_std, + recovered + ), + Err( + psychometric_core::PsychometricError::StandardisedManifestTraitVarianceIsNotStandardisedManifestVariance + ) + ); + assert_eq!( + refuse_observed_variance_as_standardised_manifest_variance(observed, recovered), + Err( + psychometric_core::PsychometricError::ObservedVarianceIsNotStandardisedManifestVariance + ) + ); +} + +#[allow(clippy::too_many_lines)] +#[test] +fn standardised_time_independent_predictor_variance_is_not_unstandardised_or_manifestvarstd() { + let predictor_variance = 0.4_f64; + let recovered = recover_standardised_time_independent_predictor_variance( + predictor_variance, + LagClock::EventTime, + ) + .expect("TIPREDVARstd"); + assert!( + (recovered - 1.0).abs() < 1e-15, + "Driver et al. (2017, p. 16 / 2017-era summary.ctsemFit.R): TIPREDVARstd is v/v = 1" + ); + let larger_v = + recover_standardised_time_independent_predictor_variance(1.6, LagClock::EventTime) + .expect("TIPREDVARstd v=1.6"); + assert_eq!( + larger_v.to_bits(), + recovered.to_bits(), + "Driver et al. (2017, p. 16): distinct positive TIPREDVAR recover the same TIPREDVARstd" + ); + let manifest_std = + recover_standardised_manifest_variance(predictor_variance, LagClock::EventTime) + .expect("MANIFESTVARstd"); + assert_eq!( + manifest_std.to_bits(), + recovered.to_bits(), + "Driver et al. (2017, p. 16): MANIFESTVARstd and TIPREDVARstd equal 1 and remain distinct named quantities" + ); + assert!((predictor_variance - recovered).abs() > 1e-3); + let added = recover_asymptotic_time_independent_predictor_variance( + 0.2, + predictor_variance, + -0.5, + LagClock::EventTime, + ) + .expect("addedTIPREDVAR"); + assert!((added - recovered).abs() > 1e-3); + assert_eq!( + recover_standardised_time_independent_predictor_variance(0.0, LagClock::EventTime), + Err( + psychometric_core::PsychometricError::StandardisedTimeIndependentPredictorVarianceRequiresPositivePredictorVariance + ) + ); + assert_eq!( + recover_standardised_time_independent_predictor_variance( + predictor_variance, + LagClock::SystemTime + ), + Err(psychometric_core::PsychometricError::EventTimeRequired) + ); + assert_eq!( + refuse_unstandardised_time_independent_predictor_variance_as_standardised_time_independent_predictor_variance( + predictor_variance, + recovered + ), + Err( + psychometric_core::PsychometricError::UnstandardisedTimeIndependentPredictorVarianceIsNotStandardisedTimeIndependentPredictorVariance + ) + ); + assert_eq!( + refuse_standardised_manifest_variance_as_standardised_time_independent_predictor_variance( + manifest_std, + recovered + ), + Err( + psychometric_core::PsychometricError::StandardisedManifestVarianceIsNotStandardisedTimeIndependentPredictorVariance + ) + ); + assert_eq!( + refuse_asymptotic_time_independent_predictor_variance_as_standardised_time_independent_predictor_variance( + added, + recovered + ), + Err( + psychometric_core::PsychometricError::AsymptoticTimeIndependentPredictorVarianceIsNotStandardisedTimeIndependentPredictorVariance + ) + ); +} + +#[allow(clippy::too_many_lines)] +#[test] +fn standardised_asymptotic_diffusion_is_not_unstandardised_or_diffusionstd() { + let diffusion = 0.4_f64; + let log_rate = -0.25_f64; + let recovered = + recover_standardised_asymptotic_diffusion(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSIONstd"); + assert!( + (recovered - 1.0).abs() < 1e-15, + "Driver et al. (2017, p. 16 / 2017-era summary.ctsemFit.R): asymDIFFUSIONstd is p/p = 1" + ); + let larger_q = recover_standardised_asymptotic_diffusion(1.6, log_rate, LagClock::EventTime) + .expect("asymDIFFUSIONstd q=1.6"); + assert_eq!( + larger_q.to_bits(), + recovered.to_bits(), + "Driver et al. (2017, p. 16): distinct positive asymDIFFUSION recover the same asymDIFFUSIONstd" + ); + let within = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + assert!((within - recovered).abs() > 1e-3); + let predictor_std = + recover_standardised_time_independent_predictor_variance(within, LagClock::EventTime) + .expect("TIPREDVARstd"); + assert_eq!( + predictor_std.to_bits(), + recovered.to_bits(), + "Driver et al. (2017, p. 16): TIPREDVARstd and asymDIFFUSIONstd equal 1 and remain distinct named quantities" + ); + let diffusion_std = + recover_standardised_continuous_diffusion(diffusion, log_rate, LagClock::EventTime) + .expect("DIFFUSIONstd"); + assert!((diffusion_std - recovered).abs() > 1e-3); + assert_eq!( + recover_standardised_asymptotic_diffusion(0.0, log_rate, LagClock::EventTime), + Err( + psychometric_core::PsychometricError::StandardisedAsymptoticDiffusionRequiresPositiveWithinSubjectVariance + ) + ); + assert_eq!( + recover_standardised_asymptotic_diffusion(diffusion, 0.5, LagClock::EventTime), + Err(psychometric_core::PsychometricError::StationaryVarianceRequiresStableDrift) + ); + assert_eq!( + recover_standardised_asymptotic_diffusion(diffusion, log_rate, LagClock::SystemTime), + Err(psychometric_core::PsychometricError::EventTimeRequired) + ); + assert_eq!( + refuse_unstandardised_asymptotic_diffusion_as_standardised_asymptotic_diffusion( + within, recovered + ), + Err( + psychometric_core::PsychometricError::UnstandardisedAsymptoticDiffusionIsNotStandardisedAsymptoticDiffusion + ) + ); + assert_eq!( + refuse_standardised_time_independent_predictor_variance_as_standardised_asymptotic_diffusion( + predictor_std, + recovered + ), + Err( + psychometric_core::PsychometricError::StandardisedTimeIndependentPredictorVarianceIsNotStandardisedAsymptoticDiffusion + ) + ); + assert_eq!( + refuse_standardised_continuous_diffusion_as_standardised_asymptotic_diffusion( + diffusion_std, + recovered + ), + Err( + psychometric_core::PsychometricError::StandardisedContinuousDiffusionIsNotStandardisedAsymptoticDiffusion + ) + ); +} + +#[allow(clippy::too_many_lines)] +#[test] +fn standardised_discrete_continuous_intercept_is_not_unstandardised_or_cintstd() { + let intercept = 0.3_f64; + let diffusion = 0.4_f64; + let log_rate = -0.25_f64; + let event_delta = 1.0_f64; + let recovered = recover_standardised_discrete_continuous_intercept( + intercept, + diffusion, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("discreteCINTstd"); + let discrete = recover_discrete_continuous_intercept_effect( + intercept, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("discreteCINT"); + let within = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + assert!((within - 0.8).abs() < 1e-15); + let expected = discrete / within.sqrt(); + assert!( + (recovered - expected).abs() < 1e-15, + "Driver et al. (2017, p. 16 / footnote 4): discreteCINTstd is discreteCINT / √p" + ); + let continuous_std = intercept / within.sqrt(); + assert!( + (continuous_std - recovered).abs() > 1e-3, + "Driver et al. (2017, p. 16): κ / √p does not depend on Δt and is not discreteCINTstd" + ); + let asymptotic = + recover_asymptotic_continuous_intercept(intercept, log_rate, LagClock::EventTime) + .expect("asymCINT"); + let asymptotic_std = asymptotic / within.sqrt(); + assert!( + (asymptotic_std - recovered).abs() > 1e-3, + "Driver et al. (2017, Table 2): (-κ / a) / √p is not discreteCINTstd" + ); + let later = recover_standardised_discrete_continuous_intercept( + intercept, + diffusion, + log_rate, + 2.5, + LagClock::EventTime, + ) + .expect("discreteCINTstd Δt=2.5"); + assert!((later - recovered).abs() > 1e-3); + let zero = recover_standardised_discrete_continuous_intercept( + 0.0, + diffusion, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("zero CINT"); + assert_eq!(zero.to_bits(), 0.0_f64.to_bits()); + assert_eq!( + recover_standardised_discrete_continuous_intercept( + intercept, + 0.0, + log_rate, + event_delta, + LagClock::EventTime + ), + Err( + psychometric_core::PsychometricError::StandardisedDiscreteContinuousInterceptRequiresPositiveWithinSubjectVariance + ) + ); + assert_eq!( + recover_standardised_discrete_continuous_intercept( + intercept, + diffusion, + 0.5, + event_delta, + LagClock::EventTime + ), + Err(psychometric_core::PsychometricError::StationaryVarianceRequiresStableDrift) + ); + assert_eq!( + recover_standardised_discrete_continuous_intercept( + intercept, + diffusion, + log_rate, + event_delta, + LagClock::SystemTime + ), + Err(psychometric_core::PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_standardised_discrete_continuous_intercept( + intercept, + diffusion, + log_rate, + 0.0, + LagClock::EventTime + ), + Err(psychometric_core::PsychometricError::NonPositiveInterval) + ); + assert_eq!( + refuse_unstandardised_discrete_continuous_intercept_as_standardised_discrete_continuous_intercept( + discrete, + recovered + ), + Err( + psychometric_core::PsychometricError::UnstandardisedDiscreteContinuousInterceptIsNotStandardisedDiscreteContinuousIntercept + ) + ); + assert_eq!( + refuse_standardised_continuous_intercept_as_standardised_discrete_continuous_intercept( + continuous_std, + recovered + ), + Err( + psychometric_core::PsychometricError::StandardisedContinuousInterceptIsNotStandardisedDiscreteContinuousIntercept + ) + ); + assert_eq!( + refuse_asymptotic_standardised_continuous_intercept_as_standardised_discrete_continuous_intercept( + asymptotic_std, + recovered + ), + Err( + psychometric_core::PsychometricError::AsymptoticStandardisedContinuousInterceptIsNotStandardisedDiscreteContinuousIntercept + ) + ); +} + +#[allow(clippy::too_many_lines)] +#[test] +fn standardised_asymptotic_continuous_intercept_is_not_unstandardised_or_cintstd() { + let intercept = 0.3_f64; + let diffusion = 0.4_f64; + let log_rate = -0.25_f64; + let recovered = recover_standardised_asymptotic_continuous_intercept( + intercept, + diffusion, + log_rate, + LagClock::EventTime, + ) + .expect("asymCINTstd"); + let asymptotic = + recover_asymptotic_continuous_intercept(intercept, log_rate, LagClock::EventTime) + .expect("asymCINT"); + let within = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + assert!((within - 0.8).abs() < 1e-15); + let expected = asymptotic / within.sqrt(); + assert!( + (recovered - expected).abs() < 1e-15, + "Driver et al. (2017, p. 16 / footnote 4): asymCINTstd is asymCINT / √p" + ); + let continuous_std = intercept / within.sqrt(); + assert!( + (continuous_std - recovered).abs() > 1e-3, + "Driver et al. (2017, p. 16): κ / √p is not asymCINTstd" + ); + let discrete_std = recover_standardised_discrete_continuous_intercept( + intercept, + diffusion, + log_rate, + 1.0, + LagClock::EventTime, + ) + .expect("discreteCINTstd"); + assert!( + (discrete_std - recovered).abs() > 1e-3, + "Driver et al. (2017, p. 16): discreteCINTstd depends on Δt and is not asymCINTstd" + ); + let later = recover_standardised_discrete_continuous_intercept( + intercept, + diffusion, + log_rate, + 2.5, + LagClock::EventTime, + ) + .expect("discreteCINTstd Δt=2.5"); + assert!((later - recovered).abs() > 1e-3); + let zero = recover_standardised_asymptotic_continuous_intercept( + 0.0, + diffusion, + log_rate, + LagClock::EventTime, + ) + .expect("zero CINT"); + assert_eq!(zero.to_bits(), 0.0_f64.to_bits()); + assert_eq!( + recover_standardised_asymptotic_continuous_intercept( + intercept, + 0.0, + log_rate, + LagClock::EventTime + ), + Err( + psychometric_core::PsychometricError::StandardisedAsymptoticContinuousInterceptRequiresPositiveWithinSubjectVariance + ) + ); + assert_eq!( + recover_standardised_asymptotic_continuous_intercept( + intercept, + diffusion, + 0.5, + LagClock::EventTime + ), + Err(psychometric_core::PsychometricError::StationaryVarianceRequiresStableDrift) + ); + assert_eq!( + recover_standardised_asymptotic_continuous_intercept( + intercept, + diffusion, + log_rate, + LagClock::SystemTime + ), + Err(psychometric_core::PsychometricError::EventTimeRequired) + ); + assert_eq!( + refuse_unstandardised_asymptotic_continuous_intercept_as_standardised_asymptotic_continuous_intercept( + asymptotic, + recovered + ), + Err( + psychometric_core::PsychometricError::UnstandardisedAsymptoticContinuousInterceptIsNotStandardisedAsymptoticContinuousIntercept + ) + ); + assert_eq!( + refuse_standardised_continuous_intercept_as_standardised_asymptotic_continuous_intercept( + continuous_std, + recovered + ), + Err( + psychometric_core::PsychometricError::StandardisedContinuousInterceptIsNotStandardisedAsymptoticContinuousIntercept + ) + ); + assert_eq!( + refuse_standardised_discrete_continuous_intercept_as_standardised_asymptotic_continuous_intercept( + discrete_std, + recovered + ), + Err( + psychometric_core::PsychometricError::StandardisedDiscreteContinuousInterceptIsNotStandardisedAsymptoticContinuousIntercept + ) + ); +} + +#[allow(clippy::too_many_lines)] +#[test] +fn standardised_initial_latent_mean_is_not_unstandardised_or_t0varstd() { + let mean = 0.8_f64; + let initial_variance = 1.6_f64; + let recovered = + recover_standardised_initial_latent_mean(mean, initial_variance, LagClock::EventTime) + .expect("T0MEANSstd"); + let expected = mean / initial_variance.sqrt(); + assert!( + (recovered - expected).abs() < 1e-15, + "Driver et al. (2017, p. 16 / footnote 4): T0MEANSstd is μ_0 / √p_0" + ); + let variance_std = + recover_standardised_initial_latent_variance(initial_variance, LagClock::EventTime) + .expect("T0VARstd"); + let unit = recover_standardised_initial_latent_mean( + initial_variance.sqrt(), + initial_variance, + LagClock::EventTime, + ) + .expect("T0MEANSstd μ_0=√p_0"); + assert!( + (unit - variance_std).abs() < 1e-15, + "Driver et al. (2017, p. 16): equal 1 with T0VARstd remains a distinct named quantity" + ); + let within = + recover_stationary_latent_variance(0.4, -0.25, LagClock::EventTime).expect("asymDIFFUSION"); + let within_scaled = mean / within.sqrt(); + assert!( + (within_scaled - recovered).abs() > 1e-3, + "Driver et al. (2017, p. 16): μ_0 / √asymDIFFUSION is not T0MEANSstd" + ); + let larger = recover_standardised_initial_latent_mean(mean, 6.4, LagClock::EventTime) + .expect("T0MEANSstd p_0=6.4"); + assert!((larger - recovered).abs() > 1e-3); + let zero = recover_standardised_initial_latent_mean(0.0, initial_variance, LagClock::EventTime) + .expect("zero T0MEANS"); + assert_eq!(zero.to_bits(), 0.0_f64.to_bits()); + assert_eq!( + recover_standardised_initial_latent_mean(mean, 0.0, LagClock::EventTime), + Err( + psychometric_core::PsychometricError::StandardisedInitialLatentMeanRequiresPositiveInitialLatentVariance + ) + ); + assert_eq!( + recover_standardised_initial_latent_mean(mean, initial_variance, LagClock::SystemTime), + Err(psychometric_core::PsychometricError::EventTimeRequired) + ); + assert_eq!( + refuse_unstandardised_initial_latent_mean_as_standardised_initial_latent_mean( + mean, recovered + ), + Err( + psychometric_core::PsychometricError::UnstandardisedInitialLatentMeanIsNotStandardisedInitialLatentMean + ) + ); + assert_eq!( + refuse_standardised_initial_latent_variance_as_standardised_initial_latent_mean( + variance_std, unit + ), + Err( + psychometric_core::PsychometricError::StandardisedInitialLatentVarianceIsNotStandardisedInitialLatentMean + ) + ); + assert_eq!( + refuse_within_subject_scaled_initial_latent_mean_as_standardised_initial_latent_mean( + within_scaled, recovered + ), + Err( + psychometric_core::PsychometricError::WithinSubjectScaledInitialLatentMeanIsNotStandardisedInitialLatentMean + ) + ); +} + +#[allow(clippy::too_many_lines)] +#[test] +fn standardised_initial_time_independent_effect_is_not_unstandardised_or_trait_contaminated() { + let initial_variance = 1.6_f64; + let coefficient = 0.3_f64; + let predictor_variance = 1.0_f64; + let recovered = recover_standardised_initial_time_independent_predictor_effect( + coefficient, + predictor_variance, + initial_variance, + LagClock::EventTime, + ) + .expect("T0TIPREDEFFECTstd"); + assert!( + (recovered - coefficient * predictor_variance.sqrt() / initial_variance.sqrt()).abs() + < 1e-15, + "Driver et al. (2017, Table 3 / footnote 4): T0TIPREDEFFECTstd is t0_b·√v/√p_0" + ); + let larger_p0 = recover_standardised_initial_time_independent_predictor_effect( + coefficient, + predictor_variance, + 6.4, + LagClock::EventTime, + ) + .expect("T0TIPREDEFFECTstd p_0=6.4"); + assert!((larger_p0 - recovered).abs() > 1e-3); + let continuous = recover_standardised_continuous_time_independent_predictor_effect( + coefficient, + predictor_variance, + 0.4, + -0.5, + LagClock::EventTime, + ) + .expect("TIPREDEFFECTstd"); + assert!( + (continuous - recovered).abs() > 1e-3, + "Driver et al. (2017, p. 16 / Table 3): TIPREDEFFECTstd is not T0TIPREDEFFECTstd" + ); + let asymptotic = recover_standardised_asymptotic_time_independent_predictor_effect( + coefficient, + predictor_variance, + 0.4, + -0.5, + LagClock::EventTime, + ) + .expect("asymTIPREDEFFECTstd"); + assert!( + (asymptotic - recovered).abs() > 1e-3, + "Driver et al. (2017, p. 16 / §7.2): asymTIPREDEFFECTstd is not T0TIPREDEFFECTstd" + ); + let trait_variance = 1.0_f64; + let total = recover_trait_plus_state_latent_variance(trait_variance, initial_variance) + .expect("trait+state var"); + let contaminated = coefficient * predictor_variance.sqrt() / total.sqrt(); + assert!( + (contaminated - recovered).abs() > 1e-3, + "Driver et al. (2017, footnote 4 / §7.1): TRAITVAR contaminates the affected SD" + ); + assert_eq!( + recover_standardised_initial_time_independent_predictor_effect( + coefficient, + predictor_variance, + 0.0, + LagClock::EventTime + ), + Err( + psychometric_core::PsychometricError::StandardisedInitialTimeIndependentEffectRequiresPositiveInitialLatentVariance + ) + ); + assert_eq!( + recover_standardised_initial_time_independent_predictor_effect( + coefficient, + 0.0, + initial_variance, + LagClock::EventTime + ), + Err( + psychometric_core::PsychometricError::StandardisedInitialTimeIndependentEffectRequiresPositivePredictorVariance + ) + ); + assert_eq!( + recover_standardised_initial_time_independent_predictor_effect( + coefficient, + predictor_variance, + initial_variance, + LagClock::SystemTime + ), + Err(psychometric_core::PsychometricError::EventTimeRequired) + ); + assert_eq!( + refuse_unstandardised_initial_time_independent_effect_as_standardised_initial_time_independent_effect( + coefficient, + recovered + ), + Err( + psychometric_core::PsychometricError::UnstandardisedInitialTimeIndependentEffectIsNotStandardisedInitialTimeIndependentEffect + ) + ); + assert_eq!( + refuse_standardised_continuous_time_independent_effect_as_standardised_initial_time_independent_effect( + continuous, + recovered + ), + Err( + psychometric_core::PsychometricError::StandardisedContinuousTimeIndependentEffectIsNotStandardisedInitialTimeIndependentEffect + ) + ); + assert_eq!( + refuse_standardised_asymptotic_time_independent_effect_as_standardised_initial_time_independent_effect( + asymptotic, + recovered + ), + Err( + psychometric_core::PsychometricError::StandardisedAsymptoticTimeIndependentEffectIsNotStandardisedInitialTimeIndependentEffect + ) + ); + assert_eq!( + refuse_trait_contaminated_initial_time_independent_effect_as_standardised_initial_time_independent_effect( + contaminated, + recovered + ), + Err( + psychometric_core::PsychometricError::TraitContaminatedInitialTimeIndependentEffectIsNotStandardisedInitialTimeIndependentEffect + ) + ); + assert_eq!( + refuse_trait_variance_as_standardisation_variance(trait_variance, initial_variance), + Err(psychometric_core::PsychometricError::TraitVarianceIsNotStandardisationVariance) + ); +} + +#[allow(clippy::too_many_lines)] +#[test] +fn initial_time_independent_variance_is_not_asymptotic_or_standardised_effect() { + let coefficient = 0.3_f64; + let predictor_variance = 4.0_f64; + let recovered = recover_initial_time_independent_predictor_variance( + coefficient, + predictor_variance, + LagClock::EventTime, + ) + .expect("addedT0TIPREDVAR"); + assert!( + (recovered - coefficient * coefficient * predictor_variance).abs() < 1e-15, + "Driver et al. (2017, Table 3 / 2017-era summary.ctsemFit.R): addedT0TIPREDVAR is t0_b² v" + ); + let asymptotic = recover_asymptotic_time_independent_predictor_variance( + coefficient, + predictor_variance, + -0.5, + LagClock::EventTime, + ) + .expect("addedTIPREDVAR"); + assert!( + (asymptotic - recovered).abs() > 1e-3, + "Driver et al. (2017, §7.2 / Table 3): addedTIPREDVAR is not addedT0TIPREDVAR" + ); + assert_eq!( + recover_asymptotic_time_independent_predictor_variance( + coefficient, + predictor_variance, + 0.5, + LagClock::EventTime, + ), + Err( + psychometric_core::PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift + ) + ); + let standardised = recover_standardised_initial_time_independent_predictor_effect( + coefficient, + predictor_variance, + 1.6, + LagClock::EventTime, + ) + .expect("T0TIPREDEFFECTstd"); + assert!( + (standardised - recovered).abs() > 1e-3, + "Driver et al. (2017, Table 3): T0TIPREDEFFECTstd is not addedT0TIPREDVAR" + ); + assert_eq!( + recover_initial_time_independent_predictor_variance( + coefficient, + predictor_variance, + LagClock::SystemTime + ), + Err(psychometric_core::PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_initial_time_independent_predictor_variance( + 0.0, + predictor_variance, + LagClock::EventTime + ) + .expect("zero coefficient") + .to_bits(), + 0.0_f64.to_bits() + ); + assert_eq!( + refuse_initial_time_independent_variance_as_asymptotic_time_independent_variance( + recovered, + asymptotic + ), + Err( + psychometric_core::PsychometricError::InitialTimeIndependentVarianceIsNotAsymptoticTimeIndependentVariance + ) + ); + assert_eq!( + refuse_initial_time_independent_variance_as_standardised_initial_time_independent_effect( + recovered, + standardised + ), + Err( + psychometric_core::PsychometricError::InitialTimeIndependentVarianceIsNotStandardisedInitialTimeIndependentEffect + ) + ); + assert_eq!( + refuse_initial_time_independent_variance_as_initial_latent_variance(recovered, 1.6), + Err( + psychometric_core::PsychometricError::InitialTimeIndependentVarianceIsNotInitialLatentVariance + ) + ); + assert_eq!( + refuse_initial_time_independent_variance_as_trait_variance(recovered, 1.0), + Err(psychometric_core::PsychometricError::InitialTimeIndependentVarianceIsNotTraitVariance) + ); +} + +#[allow(clippy::too_many_lines)] +#[test] +fn initial_time_independent_observed_variance_is_not_latent_extra_or_measurement_error() { + let loading = 2.0_f64; + let coefficient = 0.3_f64; + let predictor_variance = 4.0_f64; + let extra = recover_initial_time_independent_predictor_variance( + coefficient, + predictor_variance, + LagClock::EventTime, + ) + .expect("addedT0TIPREDVAR"); + let recovered = recover_initial_time_independent_observed_variance( + loading, + coefficient, + predictor_variance, + LagClock::EventTime, + ) + .expect("eq5 addedT0TIPREDVAR"); + assert!( + (recovered - loading * loading * extra).abs() < 1e-15, + "Driver et al. (2017, Eq. 5 of 2017-era addedT0TIPREDVAR): extra observed TI variance is λ² t0_b² v" + ); + assert!( + (extra - recovered).abs() > 1e-3, + "Driver et al. (2017, Eq. 5): latent extra is not observed extra" + ); + let initial_observed = + recover_manifest_observed_variance(loading, 1.6, 0.1).expect("λ² p_0 + θ"); + assert!( + (initial_observed - recovered).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 / Table 2): λ² p_0 + θ is not extra observed TI variance" + ); + let asymptotic_observed = recover_asymptotic_time_independent_observed_variance( + loading, + coefficient, + predictor_variance, + -0.5, + LagClock::EventTime, + ) + .expect("λ² (B/a)² v"); + assert!( + (asymptotic_observed - recovered).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 / §7.2): λ² (B/a)² v is not first-occasion extra observed TI variance" + ); + assert_eq!( + recover_initial_time_independent_observed_variance( + loading, + coefficient, + predictor_variance, + LagClock::SystemTime + ), + Err(psychometric_core::PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_initial_time_independent_observed_variance( + 0.0, + coefficient, + predictor_variance, + LagClock::EventTime + ) + .expect("zero loading") + .to_bits(), + 0.0_f64.to_bits() + ); + assert_eq!( + refuse_initial_time_independent_observed_variance_as_initial_time_independent_variance( + recovered, extra + ), + Err( + psychometric_core::PsychometricError::InitialTimeIndependentObservedVarianceIsNotInitialTimeIndependentVariance + ) + ); + assert_eq!( + refuse_initial_time_independent_observed_variance_as_initial_observed_variance( + recovered, + initial_observed + ), + Err( + psychometric_core::PsychometricError::InitialTimeIndependentObservedVarianceIsNotInitialObservedVariance + ) + ); + assert_eq!( + refuse_initial_time_independent_observed_variance_as_asymptotic_time_independent_observed_variance( + recovered, + asymptotic_observed + ), + Err( + psychometric_core::PsychometricError::InitialTimeIndependentObservedVarianceIsNotAsymptoticTimeIndependentObservedVariance + ) + ); + assert_eq!( + refuse_initial_time_independent_observed_variance_as_measurement_error(recovered, 0.1), + Err( + psychometric_core::PsychometricError::InitialTimeIndependentObservedVarianceIsNotMeasurementError + ) + ); +} + +#[allow(clippy::too_many_lines)] +#[test] +fn asymptotic_time_independent_observed_variance_is_not_latent_extra_or_measurement_error() { + let loading = 2.0_f64; + let coefficient = 0.3_f64; + let predictor_variance = 4.0_f64; + let log_rate = -0.5_f64; + let extra = recover_asymptotic_time_independent_predictor_variance( + coefficient, + predictor_variance, + log_rate, + LagClock::EventTime, + ) + .expect("addedTIPREDVAR"); + let recovered = recover_asymptotic_time_independent_observed_variance( + loading, + coefficient, + predictor_variance, + log_rate, + LagClock::EventTime, + ) + .expect("eq5 addedTIPREDVAR"); + assert!( + (recovered - loading * loading * extra).abs() < 1e-15, + "Driver et al. (2017, Eq. 5 of §7.2 addedTIPREDVAR): extra observed TI variance is λ² (B/a)² v" + ); + assert!( + (extra - recovered).abs() > 1e-3, + "Driver et al. (2017, Eq. 5): latent extra is not observed extra" + ); + let stationary_observed = + recover_manifest_observed_variance(loading, 1.6, 0.1).expect("λ² p + θ"); + assert!( + (stationary_observed - recovered).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 / Table 2): λ² p + θ is not extra observed TI variance" + ); + let initial_observed = recover_initial_time_independent_observed_variance( + loading, + coefficient, + predictor_variance, + LagClock::EventTime, + ) + .expect("eq5 addedT0TIPREDVAR"); + assert!( + (initial_observed - recovered).abs() > 1e-3, + "Driver et al. (2017, Eq. 5 / Table 3): λ² t0_b² v is not asymptotic extra observed TI variance" + ); + assert_eq!( + recover_asymptotic_time_independent_observed_variance( + loading, + coefficient, + predictor_variance, + log_rate, + LagClock::SystemTime + ), + Err(psychometric_core::PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_asymptotic_time_independent_observed_variance( + loading, + coefficient, + predictor_variance, + 0.0, + LagClock::EventTime + ), + Err( + psychometric_core::PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift + ) + ); + assert_eq!( + recover_asymptotic_time_independent_observed_variance( + 0.0, + coefficient, + predictor_variance, + log_rate, + LagClock::EventTime + ) + .expect("zero loading") + .to_bits(), + 0.0_f64.to_bits() + ); + assert_eq!( + refuse_asymptotic_time_independent_observed_variance_as_asymptotic_time_independent_variance( + recovered, extra + ), + Err( + psychometric_core::PsychometricError::AsymptoticTimeIndependentObservedVarianceIsNotAsymptoticTimeIndependentVariance + ) + ); + assert_eq!( + refuse_asymptotic_time_independent_observed_variance_as_initial_time_independent_observed_variance( + recovered, + initial_observed + ), + Err( + psychometric_core::PsychometricError::AsymptoticTimeIndependentObservedVarianceIsNotInitialTimeIndependentObservedVariance + ) + ); + assert_eq!( + refuse_asymptotic_time_independent_observed_variance_as_stationary_observed_variance( + recovered, + stationary_observed + ), + Err( + psychometric_core::PsychometricError::AsymptoticTimeIndependentObservedVarianceIsNotStationaryObservedVariance + ) + ); + assert_eq!( + refuse_asymptotic_time_independent_observed_variance_as_measurement_error(recovered, 0.1), + Err( + psychometric_core::PsychometricError::AsymptoticTimeIndependentObservedVarianceIsNotMeasurementError + ) + ); +} diff --git a/crates/role_contradiction/src/lib.rs b/crates/role_contradiction/src/lib.rs index 808a9c108..67cdf1dc9 100644 --- a/crates/role_contradiction/src/lib.rs +++ b/crates/role_contradiction/src/lib.rs @@ -12,6 +12,8 @@ mod role; /// Fail-closed role-contradiction errors. pub use error::RoleContradictionError; +/// Closed vocabulary of commercial roles that can change over time. +pub use role::ContextualRole; /// Fraction of recovered contextual roles that match known truth. pub use role::identity_recovery_rate; /// Refuse a contradictory customer/competitor pair in one group. @@ -20,5 +22,3 @@ pub use role::refuse_contradictory_roles; pub use role::refuse_role_as_entity_class; /// Return whether two roles contradict in the same group. pub use role::roles_contradict; -/// Closed vocabulary of commercial roles that can change over time. -pub use role::ContextualRole; diff --git a/crates/role_contradiction/src/role.rs b/crates/role_contradiction/src/role.rs index 8d37287b4..a893b32d5 100644 --- a/crates/role_contradiction/src/role.rs +++ b/crates/role_contradiction/src/role.rs @@ -104,8 +104,8 @@ pub fn identity_recovery_rate( #[cfg(test)] mod tests { use super::{ - identity_recovery_rate, refuse_contradictory_roles, refuse_role_as_entity_class, - roles_contradict, ContextualRole, + ContextualRole, identity_recovery_rate, refuse_contradictory_roles, + refuse_role_as_entity_class, roles_contradict, }; use crate::RoleContradictionError; diff --git a/crates/role_contradiction/tests/role_contradiction_contract.rs b/crates/role_contradiction/tests/role_contradiction_contract.rs index 0cf83f46c..7bf4628e4 100644 --- a/crates/role_contradiction/tests/role_contradiction_contract.rs +++ b/crates/role_contradiction/tests/role_contradiction_contract.rs @@ -1,8 +1,8 @@ //! Customer and competitor cannot occupy the same group at once. use role_contradiction::{ - identity_recovery_rate, refuse_contradictory_roles, refuse_role_as_entity_class, - roles_contradict, ContextualRole, RoleContradictionError, + ContextualRole, RoleContradictionError, identity_recovery_rate, refuse_contradictory_roles, + refuse_role_as_entity_class, roles_contradict, }; #[test] diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index ebf42446b..6bb9ca78d 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -76,9 +76,8 @@ The full APA 7th standards/literature register remains `docs/research/standards- | report template/section/copied/style/modality method effects | ADR 0004/0012; PRD/TRD | simulation truth factors implemented; `prompt_source` prompt-versus-unique-content identity on the active PR; estimator-side method model remains future | partial | | candidate K statistical/Pareto gates | ADR 0012; research | `model_selection` statistical/Pareto `K` gate on the active PR; candidate blinding, blinded LLM review, and backend comparison remain accepted-target | active-PR | | compositional topic correlation / stable clustering | ADR 0005/0012; research | future `network_analysis` | accepted-target | -| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; §7.2 level-change `CINT` is `κ = −a m x` (`a < 0`; not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`; Eq. 3 of that setting is `(1 − e^{a Δt}) m x`); §7.2 extra-process contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, not the dissipating Dirac; `ε ≥ 0` fails closed; Eq. 5 of that contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; extra `LAMBDA` is 0; `τ + λ μ_t` is not that observed mean; after-t0 extra-process `TDPREDEFFECT` uses `t − u` with `t0 < u < t` while `μ_t` uses `Δt`; that after-t0 observed mean is not the first-occasion extra-process observed mean; §7.2 `asymTIPREDEFFECT` is `-B z / a` for `a < 0` and is not `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; §7.2 `addedTIPREDVAR` is `(B / a)² v` and is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`; Table 2 `asymCINT` is `-κ / a` for `a < 0` and is not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`; p. 16 stationary `T0MEANS` is `-κ / a + −B z / a` and is not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean; Eq. 5 of that constrained mean is `τ + λ(−κ / a + −B z / a)`; `τ + λ μ_0` is not that observed mean; `MANIFESTMEANS` is not `E(y_0)`; the constrained latent mean is not `E(y_0)`; stationary `T0VAR` is `trait + −q / (2 a) + (B / a)² v` (not free `T0VAR`, not `asymDIFFUSION` alone, not `TRAITVAR` alone, not `addedTIPREDVAR` alone, and not the finite-interval discrete latent variance. Eq. 5 of that constrained variance is `λ²(trait + −q / (2 a) + (B / a)² v) + θ + ψ` (JSS PDF re-opened 2026-08-22T03:20Z; form the stationary latent variance first, then `λ² p + θ + ψ`; `λ² p_0` is not that observed variance; `λ²(−q / (2 a)) + θ` is not that observed variance when `TRAITVAR` or `addedTIPREDVAR` is nonzero; `MANIFESTVAR` is not `Var(y_0)`; the constrained latent variance is not `Var(y_0)`)); lagged stationary `T0VAR` is `trait + e^{a Δt}(−q / (2 a)) + (B / a)² v` (trait and `addedTIPREDVAR` do not decay; contemporaneous `T0VAR` is not that lagged map; decaying the constrained total as if it were all state is not that lagged map; Eq. 5 of that lagged covariance is `λ²(trait + e^{a Δt}(−q / (2 a)) + (B / a)² v) + ψ`; `Θ` does not enter; contemporaneous `Var(y_0)` is not that lagged observed covariance; the lagged latent covariance is not that observed covariance); later-occasion stationary `T0VAR` is `trait + e^{2 a Δt}(−q / (2 a)) + Q_Δt + (B / a)² v` (trait and `addedTIPREDVAR` do not enter `Q_Δt`; under stationarity that composition equals contemporaneous `T0VAR`; evolving the constrained total as if it were all state is not that later map; the lagged covariance omits `Q_Δt`; `Q_Δt` is not that later map; Eq. 5 of that later-occasion variance is `λ²(trait + e^{2 a Δt}(−q / (2 a)) + Q_Δt + (B / a)² v) + θ + ψ`; lagged observed covariance omits `Q_Δt` and `θ`; `MANIFESTVAR` is not `Var(y_t)`; the later-occasion latent variance is not `Var(y_t)`)), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z); full ESEM/DSEM remaining | partial | +| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; §7.2 level-change `CINT` is `κ = −a m x` (`a < 0`; not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`; Eq. 3 of that setting is `(1 − e^{a Δt}) m x`); §7.2 extra-process contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, not the dissipating Dirac; `ε ≥ 0` fails closed; Eq. 5 of that contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; extra `LAMBDA` is 0; `τ + λ μ_t` is not that observed mean; after-t0 extra-process `TDPREDEFFECT` uses `t − u` with `t0 < u < t` while `μ_t` uses `Δt`; that after-t0 observed mean is not the first-occasion extra-process observed mean; §7.2 `asymTIPREDEFFECT` is `-B z / a` for `a < 0` and is not `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; §7.2 `addedTIPREDVAR` is `(B / a)² v` and is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`; Table 2 `asymCINT` is `-κ / a` for `a < 0` and is not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`; p. 16 stationary `T0MEANS` is `-κ / a + −B z / a` and is not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean; Eq. 5 of that constrained mean is `τ + λ(−κ / a + −B z / a)`; `τ + λ μ_0` is not that observed mean; `MANIFESTMEANS` is not `E(y_0)`; the constrained latent mean is not `E(y_0)`; stationary `T0VAR` is `trait + −q / (2 a) + (B / a)² v` (not free `T0VAR`, not `asymDIFFUSION` alone, not `TRAITVAR` alone, not `addedTIPREDVAR` alone, and not the finite-interval discrete latent variance. Eq. 5 of that constrained variance is `λ²(trait + −q / (2 a) + (B / a)² v) + θ + ψ` (JSS PDF re-opened 2026-08-22T03:20Z; form the stationary latent variance first, then `λ² p + θ + ψ`; `λ² p_0` is not that observed variance; `λ²(−q / (2 a)) + θ` is not that observed variance when `TRAITVAR` or `addedTIPREDVAR` is nonzero; `MANIFESTVAR` is not `Var(y_0)`; the constrained latent variance is not `Var(y_0)`)); lagged stationary `T0VAR` is `trait + e^{a Δt}(−q / (2 a)) + (B / a)² v` (trait and `addedTIPREDVAR` do not decay; contemporaneous `T0VAR` is not that lagged map; decaying the constrained total as if it were all state is not that lagged map; Eq. 5 of that lagged covariance is `λ²(trait + e^{a Δt}(−q / (2 a)) + (B / a)² v) + ψ`; `Θ` does not enter; contemporaneous `Var(y_0)` is not that lagged observed covariance; the lagged latent covariance is not that observed covariance); later-occasion stationary `T0VAR` is `trait + e^{2 a Δt}(−q / (2 a)) + Q_Δt + (B / a)² v` (trait and `addedTIPREDVAR` do not enter `Q_Δt`; under stationarity that composition equals contemporaneous `T0VAR`; evolving the constrained total as if it were all state is not that later map; the lagged covariance omits `Q_Δt`; `Q_Δt` is not that later map; Eq. 5 of that later-occasion variance is `λ²(trait + e^{2 a Δt}(−q / (2 a)) + Q_Δt + (B / a)² v) + θ + ψ`; lagged observed covariance omits `Q_Δt` and `θ`; `MANIFESTVAR` is not `Var(y_t)`; the later-occasion latent variance is not `Var(y_t)`); predetermined later-occasion `T0VAR` is `trait + e^{2 a Δt} p_0 + Q_Δt + (B / a)² v` (free `T0VAR` `p_0` is not that later map; setting `p_0 = −q / (2 a)` recovers the stationary later-occasion map; stationary later variance uses `−q / (2 a)` in place of `p_0` and is not that later map when `p_0` is free; evolving `trait + p_0 + (B / a)² v` as if it were all state is not that later map; Eq. 5 of that predetermined later-occasion variance is `λ²(trait + e^{2 a Δt} p_0 + Q_Δt + (B / a)² v) + θ + ψ`; `MANIFESTVAR` is not `Var(y_t)`; the predetermined later-occasion latent variance is not `Var(y_t)`; stationary later observed variance is not that observed variance when `p_0` is free); predetermined lagged `T0VAR` is `trait + e^{a Δt} p_0 + (B / a)² v` (free `T0VAR` `p_0` is not that lagged map; setting `p_0 = −q / (2 a)` recovers the stationary lagged map; stationary lagged covariance uses `−q / (2 a)` in place of `p_0` and is not that lagged map when `p_0` is free; evolving `trait + p_0 + (B / a)² v` as if it were all state is not that lagged map; later-occasion variance includes `Q_Δt` and is not that lagged map; Eq. 5 of that predetermined lagged covariance is `λ²(trait + e^{a Δt} p_0 + (B / a)² v) + ψ`; `MANIFESTVAR` does not enter; the predetermined lagged latent covariance is not that observed covariance; predetermined later observed variance includes `Q_Δt` and `θ` and is not that lagged observed covariance; stationary lagged observed covariance is not that observed covariance when `p_0` is free; the predetermined first-occasion variance of §4.3 predetermined `T0VAR` is `trait + p_0 + (B / a)² v`; free `p_0` is not that map; stationary first-occasion variance uses `−q / (2 a)` in place of `p_0` and is not that map when `p_0` is free; lagged covariance decays the state and is not that map; later-occasion variance includes `Q_Δt` and is not that map; Eq. 5 of that predetermined first-occasion variance is `λ²(trait + p_0 + (B / a)² v) + θ + ψ`; `MANIFESTVAR` is not that first-occasion observed variance; the predetermined first-occasion latent variance is not that observed variance; stationary first-occasion observed variance is not that observed variance when `p_0` is free; predetermined later observed variance includes `Q_Δt` and is not that first-occasion observed variance; later-start lagged covariance of predetermined `T0VAR` is `trait + e^{a s}(e^{2 a u} p_0 + Q_u) + (B / a)² v` (Driver et al., 2017, §4.3 `startoffset`; Eq. 4; JSS PDF re-opened 2026-08-23T10:27Z; first-occasion lagged omits `e^{a s} Q_u`; later-occasion variance does not lag; stationary lagged uses `−q / (2 a)`; decaying the later total is not that map; Eq. 5 of that later-start lagged covariance is `λ²` of it plus `ψ`; `Θ` does not enter; first-occasion lagged observed omits `e^{a s} Q_u`; later observed variance includes `Q_u` and `θ`; later-start later-occasion variance of predetermined `T0VAR` is `trait + e^{2 a s}(e^{2 a u} p_0 + Q_u) + Q_s + (B / a)² v` (Driver et al., 2017, §4.3 `startoffset`; Eq. 3–4 Chapman–Kolmogorov `Q_{u+s} = e^{2 a s} Q_u + Q_s`; JSS PDF re-opened 2026-08-23T11:05Z; later-occasion variance at `u` omits `Q_s`; later-start lagged covariance omits `Q_s`; stationary later uses `−q / (2 a)`; evolving the later total as if it were all state is not that map; ignoring `startoffset` omits `e^{2 a s} Q_u`; Eq. 5 of that later-start later-occasion variance is `λ²` of it plus `θ + ψ`; `MANIFESTVAR` is not that observed variance; p. 16 `discreteDRIFTstd` is `e^{a Δt}` after strictly positive `asymDIFFUSION` `-q / (2 a)` (footnote 4; unstandardised `e^{a Δt}` is defined for growing `a ≥ 0` and for zero diffusion and is not `discreteDRIFTstd`; the §7.1 trait-plus-state autocorrelation uses `TRAITVAR` and is not `discreteDRIFTstd`; p. 16 `discreteDIFFUSIONstd` is `Q_Δt / (−q / (2 a))` after strictly positive `asymDIFFUSION` `-q / (2 a)` (footnote 4; unstandardised `Q_Δt` is defined for growing `a ≥ 0` and for zero diffusion and is not `discreteDIFFUSIONstd`; the continuous standardisation `−2 a` is not `discreteDIFFUSIONstd`; `Q_Δt / (trait + p + added)` uses `TRAITVAR` and is not `discreteDIFFUSIONstd`; `TRAITVAR` is not the standardisation variance; p. 16 `DIFFUSIONstd` is `q / (−q / (2 a)) = −2 a` after strictly positive `asymDIFFUSION` `-q / (2 a)` (Driver et al., 2017, p. 16; Eq. 4; footnote 4; JSS PDF re-opened 2026-08-23T13:20Z; unstandardised `q` is defined for growing `a ≥ 0` and for zero diffusion and is not `DIFFUSIONstd`; the discrete standardisation `Q_Δt / (−q / (2 a))` depends on `Δt` and is not `DIFFUSIONstd`; `q / (trait + p + added)` uses `TRAITVAR` and is not `DIFFUSIONstd`; `TRAITVAR` is not the standardisation variance; p. 16 `DRIFTstd` is the continuous auto-effect after strictly positive `asymDIFFUSION` `-q / (2 a)` (Driver et al., 2017, p. 16; Eq. 1; footnote 4; JSS PDF re-opened 2026-08-23T13:28Z); unstandardised `a` is defined for growing `a ≥ 0` and for zero diffusion and is not `DRIFTstd`; the discrete standardisation `e^{a Δt}` depends on the event interval and is not `DRIFTstd`; `a p / (trait + p + added)` uses `TRAITVAR` and is not `DRIFTstd`; `TRAITVAR` is not the standardisation variance); p. 16 `asymTIPREDEFFECTstd` is `(-B / a) · √v / √(-q / (2 a))` after strictly positive `asymDIFFUSION` `-q / (2 a)` and strictly positive predictor variance `v` (Driver et al., 2017, p. 16; §7.2; footnote 4; JSS PDF re-opened 2026-08-23T14:25Z; unstandardised `-B / a` is defined for a zero coefficient and for zero predictor variance and is not `asymTIPREDEFFECTstd`; the finite-interval standardisation `A^{-1}[e^{A Δt} − I] B · √v / √p` depends on the event interval and is not `asymTIPREDEFFECTstd`; `(-B / a) · √v / √(trait + p + added)` uses `TRAITVAR` and is not `asymTIPREDEFFECTstd`; `TRAITVAR` is not the standardisation variance); p. 16 `TIPREDEFFECTstd` is `B · √v / √(-q / (2 a))` after strictly positive `asymDIFFUSION` `-q / (2 a)` and strictly positive predictor variance `v` (Driver et al., 2017, p. 16; §7.2; footnote 4; JSS PDF re-opened 2026-08-23T16:21Z; unstandardised `B` is defined for a zero coefficient and for zero predictor variance and is not `TIPREDEFFECTstd`; the asymptotic standardisation `(-B / a) · √v / √p` is the total change and is not `TIPREDEFFECTstd`; the finite-interval standardisation `A^{-1}[e^{A Δt} − I] B · √v / √p` depends on the event interval and is not `TIPREDEFFECTstd`; `B · √v / √(trait + p + added)` uses `TRAITVAR` and is not `TIPREDEFFECTstd`; `TRAITVAR` is not the standardisation variance); Table 3 `T0TIPREDEFFECTstd` is `t0_b · √v / √p_0` after strictly positive free `T0VAR` `p_0` and strictly positive predictor variance `v` (Driver et al., 2017, Table 3, p. 13; p. 16; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T17:20Z; the affected variance is free `T0VAR`, not `asymDIFFUSION`; unstandardised `t0_b` is defined for a zero coefficient and for zero predictor variance and is not `T0TIPREDEFFECTstd`; `TIPREDEFFECTstd` `B · √v / √(-q / (2 a))` is the continuous coefficient and is not `T0TIPREDEFFECTstd`; `asymTIPREDEFFECTstd` `(-B / a) · √v / √p` is the total change and is not `T0TIPREDEFFECTstd`; `t0_b · √v / √(trait + p_0 + added)` uses `TRAITVAR` and is not `T0TIPREDEFFECTstd`; `TRAITVAR` is not the standardisation variance); 2017-era `addedT0TIPREDVAR` is `t0_b² v` (Driver et al., 2017, Table 3, p. 13; p. 16; §7.2; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T18:20Z; `T0TIPREDEFFECT %*% TIPREDVAR %*% t(T0TIPREDEFFECT)` immediately after `T0TIPREDEFFECTstd`; form `t0_b` first, then square, then multiply by `v`; a zero coefficient or zero predictor variance is exactly zero; free `T0TIPREDEFFECT` does not require `a < 0`; `(B / a)² v` is `addedTIPREDVAR` and is not this first-occasion map; `t0_b · √v / √p_0` is `T0TIPREDEFFECTstd` and is not this variance; free `T0VAR` is not this extra TI variance; `TRAITVAR` is not this extra TI variance; Equation 5 of 2017-era `addedT0TIPREDVAR` is `λ² t0_b² v` (Driver et al., 2017, Eq. 5, p. 5; Table 3, p. 13; Table 2, p. 12; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T19:10Z; form `t0_b² v` first, then `(λ extra) λ` with `θ = 0`; a zero loading or zero extra is exactly zero; `t0_b² v` is the latent extra, not the observed extra; `λ² p_0 + θ` is first-occasion observed variance, not this extra; `λ² (B / a)² v` is Eq. 5 of `addedTIPREDVAR`, not this first-occasion observed extra; `MANIFESTVAR` `θ` is not this extra; Equation 5 of §7.2 `addedTIPREDVAR` is `λ² (B / a)² v`; form `(B / a)² v` first, then `(λ extra) λ` with `θ = 0`; a zero loading or zero extra is exactly zero; lasting asymptotic extra requires `a < 0`; `(B / a)² v` is the latent extra, not the observed extra; `λ² t0_b² v` is first-occasion extra observed TI variance, not this extra; `λ² p + θ` is stationary observed variance, not this extra; `MANIFESTVAR` `θ` is not this extra; p. 16 `TDPREDEFFECTstd` is `m · √v / √(-q / (2 a))` after strictly positive `asymDIFFUSION` and strictly positive time-dependent predictor variance; unstandardised `M` is not `TDPREDEFFECTstd`; `TIPREDEFFECTstd` is not `TDPREDEFFECTstd` even when `M = B`; intercept-style `A^{-1}[e^{A Δt} − I] M · √v / √p` is not `TDPREDEFFECTstd`; `m · √v / √(trait + p + added)` uses `TRAITVAR` and is not `TDPREDEFFECTstd`; Table 3 / p. 16 `T0TDPREDEFFECTstd` is `t0_m · √v / √p_0` after strictly positive free `T0VAR` and strictly positive TD predictor variance; unstandardised `t0_m` is not `T0TDPREDEFFECTstd`; `TDPREDEFFECTstd` uses `asymDIFFUSION` and is not `T0TDPREDEFFECTstd`; `T0TIPREDEFFECTstd` is not `T0TDPREDEFFECTstd` even when `t0_m = t0_b`; `t0_m · √v / √(trait + p_0 + added)` uses `TRAITVAR` and is not `T0TDPREDEFFECTstd`; free `T0VAR` does not require `a < 0`; p. 16 `T0VARstd` is `p_0 / p_0 = 1` after strictly positive free `T0VAR` (`solve(sqrt(diag(T0VAR))) %&% T0VAR`; OpenMx `%&%` is `t(A) %*% B %*% A`; default ridge is 0); unstandardised `T0VAR` is not `T0VARstd`; `T0TDPREDEFFECTstd` is not `T0VARstd`; `addedT0TIPREDVAR` is not `T0VARstd`; p. 16 `TRAITVARstd` is `trait / trait = 1` after strictly positive `TRAITVAR` (`solve(sqrt(diag(TRAITVAR))) %&% TRAITVAR`; OpenMx `%&%` is `t(A) %*% B %*% A`; no ridge addend); unstandardised `TRAITVAR` is not `TRAITVARstd`; `T0VARstd` is not `TRAITVARstd` even when both equal 1; `addedT0TIPREDVAR` is not `TRAITVARstd`; p. 16 `MANIFESTTRAITVARstd` is `ψ / ψ = 1` after strictly positive `MANIFESTTRAITVAR` (`solve(sqrt(diag(MANIFESTTRAITVAR))) %&% MANIFESTTRAITVAR`; OpenMx `%&%` is `t(A) %*% B %*% A`; 2017-era source adds ridging; default ridge is 0); unstandardised `MANIFESTTRAITVAR` is not `MANIFESTTRAITVARstd`; `TRAITVARstd` is not `MANIFESTTRAITVARstd` even when both equal 1; `MANIFESTVAR` is not `MANIFESTTRAITVARstd`; p. 16 `MANIFESTVARstd` is `θ / θ = 1` after strictly positive `MANIFESTVAR` (`solve(sqrt(diag(MANIFESTVAR))) %&% MANIFESTVAR`; OpenMx `%&%` is `t(A) %*% B %*% A`; 2017-era source adds ridging; default ridge is 0; 2017-era `dimnames` assignment to `latentNames` is a source bug); unstandardised `MANIFESTVAR` is not `MANIFESTVARstd`; `MANIFESTTRAITVARstd` is not `MANIFESTVARstd` even when both equal 1; Equation 5 `Var(y)` is not `MANIFESTVARstd`; p. 16 `TIPREDVARstd` is `v / v = 1` after strictly positive `TIPREDVAR` (`solve(sqrt(diag(TIPREDVAR))) %&% TIPREDVAR`; OpenMx `%&%` is `t(A) %*% B %*% A`; 2017-era source adds ridging; default ridge is 0; `dimnames` are `TIpredNames`); unstandardised `TIPREDVAR` is not `TIPREDVARstd`; `MANIFESTVARstd` is not `TIPREDVARstd` even when both equal 1; §7.2 `addedTIPREDVAR` is not `TIPREDVARstd`; p. 16 `asymDIFFUSIONstd` is `p / p = 1` after strictly positive `asymDIFFUSION` (`solve(sqrt(diag(asymDIFFUSION))) %&% asymDIFFUSION`; OpenMx `%&%` is `t(A) %*% B %*% A`; 2017-era source adds ridging; default ridge is 0; `dimnames` are `latentNames`); unstandardised `asymDIFFUSION` is not `asymDIFFUSIONstd`; `TIPREDVARstd` is not `asymDIFFUSIONstd` even when both equal 1; `DIFFUSIONstd` `−2 a` is not `asymDIFFUSIONstd`; p. 16 `discreteCINTstd` is `A^{-1}[e^{A Δt} − I] κ / √p` after strictly positive `asymDIFFUSION`; unstandardised `discreteCINT` is not `discreteCINTstd`; `κ / √p` is not `discreteCINTstd`; `(-κ / a) / √p` is not `discreteCINTstd`; `asymCINTstd` is `(-κ / a) / √p` after strictly positive `asymDIFFUSION`; unstandardised `asymCINT` is not `asymCINTstd`; `κ / √p` is not `asymCINTstd`; `discreteCINTstd` is not `asymCINTstd`; `T0MEANSstd` is `μ_0 / √p_0` after strictly positive free `T0VAR`; unstandardised `T0MEANS` is not `T0MEANSstd`; `T0VARstd` is not `T0MEANSstd`; `μ_0 / √asymDIFFUSION` is not `T0MEANSstd`;))))), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z); full ESEM/DSEM remaining | partial | | CPU bounded multithreading + GPU/VRAM streaming/parity | ADR 0001/0006 | future `compute_backend` | accepted-target | -| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_fit` ESEM loading and DSEM lag gates on the active PR; `psychometric_core` input gates remain #49; invariance/multilevel remain accepted-target | active-PR | | CPU bounded multithreading + GPU/VRAM streaming/parity | ADR 0001/0006 | future `compute_backend` | accepted-target | | TDT detection/tracking vs CHRONOS schema/prediction/temporal consistency | ADR 0016; PRD/research | `event_core` TDT link precision/recall on the active PR; remaining TDT/CHRONOS stack and any future `event_intelligence` crate remain accepted-target | active-PR | | evidence-bounded LLM interpretation | ADR 0010/0012; PRD | `tepp_api` router plus future `interpretation_gateway` | partial | diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index 7191dd9d2..c6434b2b5 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -1,7 +1,7 @@ # ADR 0005 — Posterior-aware ESEM/DSEM and structural interpretation **Decision status:** Accepted -**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`; Equation 1 is the SDE; not a Kalman filter), exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment), exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`), exact scalar contemporaneous `TDPREDEFFECT` impulse `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; the §7.2 level-change form is not that impulse), exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of that Eq. 3 composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar time-independent `TIPREDEFFECT` increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle Eq. 14; `B` is not that discrete increment), exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of that Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of that carried latent mean; `τ + λ μ_t` is not that observed mean), exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; `T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`), exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; `T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand TD composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean), exact scalar §7.2 level-change `CINT` `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; `a < 0` so `−κ / a = m x`; not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`, and not the extra near-zero-drift latent process also named in §7.2), exact scalar Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` (not `m x`, not `κ`, and not `TIPREDEFFECT`), exact scalar §7.2 extra near-zero-drift latent process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z; identification `TDPREDEFFECT` on the extra process is 1; extra `DRIFT` printed as `−0.000001`; precisely 0 causes computational problems; `ε = a` is `a_{ηξ} x Δt e^{a Δt}`; not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`; `ε ≥ 0` fails closed), exact scalar observed mean of that extra-process contribution `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:12Z; the extra process has `LAMBDA` 0 and is not an observed indicator; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`), exact scalar after-t0 extra-process contribution `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` and Eq. 5 observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:32Z; `T0TDPREDEFFECT` uses `Δt` for both the evolution and the extra drive; `TDPREDEFFECT` after `t0` uses `t − u`; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive), exact scalar §7.2 `asymTIPREDEFFECT` `-B z / a` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z; expected total change in process means given a time-independent predictor; `a < 0`; not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; `a ≥ 0` fails closed), exact scalar §7.2 `addedTIPREDVAR` `(B / a)² v` (Driver et al., 2017, §7.2, pp. 20–21; stable between-subject variance accounted for by a time-independent predictor; not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`), exact scalar Table 2 `asymCINT` `-κ / a` (Driver et al., 2017, Table 2, p. 12; Eq. 3 as `Δt → ∞`; `a < 0`; not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`; `a ≥ 0` fails closed), exact scalar p. 16 stationary `T0MEANS` `-κ / a + −B z / a` (Driver et al., 2017, p. 16; constrained first-occasion mean using `T0MEANSbase` / `T0MEANSfree`; not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean), exact scalar Eq. 5 of that constrained mean `τ + λ(−κ / a + −B z / a)` (Driver et al., 2017, §4.3, pp. 9–10; Eq. 5, p. 5; JSS PDF re-opened 2026-08-21T20:07Z; form the stationary latent mean first, then `τ + λ` of that mean; `τ + λ μ_0` is not that observed mean; `τ + λ(−κ / a)` is not that observed mean when `B z ≠ 0`; `τ + λ μ_t` is not that observed mean; `MANIFESTMEANS` is not `E(y_0)`; the constrained latent mean is not `E(y_0)`), exact scalar §4.3 / p. 16 stationary `T0VAR` `trait + −q / (2 a) + (B / a)² v` (Driver et al., 2017, §4.3, pp. 9–10; p. 16; JSS PDF re-opened 2026-08-22T03:07Z; not free `T0VAR`, not `asymDIFFUSION` alone, not `TRAITVAR` alone, not `addedTIPREDVAR` alone, and not the finite-interval discrete latent variance. Eq. 5 of that constrained variance is `λ²(trait + −q / (2 a) + (B / a)² v) + θ + ψ` (JSS PDF re-opened 2026-08-22T03:20Z; form the stationary latent variance first, then `λ² p + θ + ψ`; `λ² p_0` is not that observed variance; `λ²(−q / (2 a)) + θ` is not that observed variance when `TRAITVAR` or `addedTIPREDVAR` is nonzero; `MANIFESTVAR` is not `Var(y_0)`; the constrained latent variance is not `Var(y_0)`), exact scalar lagged covariance of that constrained process `trait + e^{a Δt}(−q / (2 a)) + (B / a)² v` (Driver et al., 2017, Eq. 3–4 of §4.3 / p. 16 `T0VAR`; JSS PDF re-opened 2026-08-22T19:13Z; trait and `addedTIPREDVAR` do not decay with `e^{a Δt}`; contemporaneous `T0VAR` is not that lagged map; decaying the constrained total as if it were all state is not that lagged map), exact scalar Eq. 5 of that lagged covariance `λ²(trait + e^{a Δt}(−q / (2 a)) + (B / a)² v) + ψ` (`Θ` does not enter; contemporaneous `Var(y_0)` is not that lagged observed covariance; the lagged latent covariance is not that observed covariance), exact scalar later-occasion variance of that constrained process `trait + e^{2 a Δt}(−q / (2 a)) + Q_Δt + (B / a)² v` (Driver et al., 2017, Eq. 3–4 of §4.3 / p. 16 `T0VAR`; JSS PDF re-opened 2026-08-22T23:12Z; trait and `addedTIPREDVAR` do not enter `Q_Δt`; under stationarity that composition equals contemporaneous `T0VAR`; evolving the constrained total as if it were all state is not that later map; the lagged covariance omits `Q_Δt` and is not that later map; `Q_Δt` is not that later map), exact scalar Eq. 5 of that later-occasion variance `λ²(trait + e^{2 a Δt}(−q / (2 a)) + Q_Δt + (B / a)² v) + θ + ψ` (the lagged observed covariance omits `Q_Δt` and `θ`; `MANIFESTVAR` is not that later observed variance; the later-occasion latent variance is not that observed variance)), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the stacked psychometric PR and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target +**Implementation maturity:** partial — construct classification, valid log-ratio/logistic-normal indicator gates, CPU `f64` OLS and posterior-draw loading point-estimate averaging, Rubin `T_m = Ū_m + (1+1/m) B_m` on draw-level OLS loadings, cluster-mean within/between OLS with the CWC contextual effect and Kish ESS WLS, event-time discrete lag-1 and exact scalar local log-rate, exact scalar forward map and unequal-interval remapping, exact scalar discrete effect of a constant predictor, first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals (Voelkle et al., 2012, Eq. 14), exact scalar discrete process noise (Driver, Oud, & Voelkle, 2017, Eq. 3), exact scalar lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), exact scalar stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; §4.3; p. 16 `asymDIFFUSION`), exact scalar trait-plus-state variance and lagged covariance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise and not `asymDIFFUSION`), exact scalar observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero, else `λ² Var(η) + θ + ψ`; lagged `λ² cov(η_t, η_{t-1}) + ψ`; `MANIFESTVAR` is `Θ`, not `Var(y)`; `Θ` does not enter lagged observed covariance; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; observed-indicator mean is `τ + λ μ` (`MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`; Equation 1 is the SDE; not a Kalman filter), exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment), exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of that Eq. 3 map; the first-occasion map `τ + λ μ_0` is not `E(y_t)`), exact scalar contemporaneous `TDPREDEFFECT` impulse `m x` (Driver et al., 2017, Eq. 3 fourth summand; Table 2 `TDPREDEFFECT` is `M`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; the §7.2 level-change form is not that impulse), exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of that Eq. 3 composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar time-independent `TIPREDEFFECT` increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 3 second summand; Table 2 `TIPREDEFFECT` is `B`, not `κ`, not `M`, and not Voelkle Eq. 14; `B` is not that discrete increment), exact scalar observed mean of that increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` (Driver et al., 2017, Eq. 5 of that Eq. 3 printed addend after the `T0MEANS` carry and the `CINT` increment; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`), exact scalar within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2 Green-function integral of Eq. 2; §7.2 dissipation; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of that carried latent mean; `τ + λ μ_t` is not that observed mean), exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; `T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`), exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and Eq. 3 first-summand carry `e^{A Δt} t0_m x0` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF re-opened 2026-08-20T19:10Z; `T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`), exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_t + e^{a Δt} t0_m x0)` (Driver et al., 2017, Eq. 5 of that Table 3 / Eq. 3 first-summand TD composition; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean), exact scalar §7.2 level-change `CINT` `κ = −a m x` (Driver et al., 2017, §7.2, pp. 20–21; `a < 0` so `−κ / a = m x`; not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`, and not the extra near-zero-drift latent process also named in §7.2), exact scalar Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` (not `m x`, not `κ`, and not `TIPREDEFFECT`), exact scalar §7.2 extra near-zero-drift latent process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-20T23:10Z; identification `TDPREDEFFECT` on the extra process is 1; extra `DRIFT` printed as `−0.000001`; precisely 0 causes computational problems; `ε = a` is `a_{ηξ} x Δt e^{a Δt}`; not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`; `ε ≥ 0` fails closed), exact scalar observed mean of that extra-process contribution `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` (Driver et al., 2017, Eq. 5, p. 5; §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:12Z; the extra process has `LAMBDA` 0 and is not an observed indicator; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`), exact scalar after-t0 extra-process contribution `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` and Eq. 5 observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a))` (Driver et al., 2017, §7.2, pp. 22–23; JSS PDF re-opened 2026-08-21T06:32Z; `T0TDPREDEFFECT` uses `Δt` for both the evolution and the extra drive; `TDPREDEFFECT` after `t0` uses `t − u`; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive), exact scalar §7.2 `asymTIPREDEFFECT` `-B z / a` (Driver et al., 2017, §7.2, pp. 20–21; JSS PDF opened 2026-08-21T13:08Z; expected total change in process means given a time-independent predictor; `a < 0`; not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; `a ≥ 0` fails closed), exact scalar §7.2 `addedTIPREDVAR` `(B / a)² v` (Driver et al., 2017, §7.2, pp. 20–21; stable between-subject variance accounted for by a time-independent predictor; not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`), exact scalar Table 2 `asymCINT` `-κ / a` (Driver et al., 2017, Table 2, p. 12; Eq. 3 as `Δt → ∞`; `a < 0`; not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`; `a ≥ 0` fails closed), exact scalar p. 16 stationary `T0MEANS` `-κ / a + −B z / a` (Driver et al., 2017, p. 16; constrained first-occasion mean using `T0MEANSbase` / `T0MEANSfree`; not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean), exact scalar Eq. 5 of that constrained mean `τ + λ(−κ / a + −B z / a)` (Driver et al., 2017, §4.3, pp. 9–10; Eq. 5, p. 5; JSS PDF re-opened 2026-08-21T20:07Z; form the stationary latent mean first, then `τ + λ` of that mean; `τ + λ μ_0` is not that observed mean; `τ + λ(−κ / a)` is not that observed mean when `B z ≠ 0`; `τ + λ μ_t` is not that observed mean; `MANIFESTMEANS` is not `E(y_0)`; the constrained latent mean is not `E(y_0)`), exact scalar §4.3 / p. 16 stationary `T0VAR` `trait + −q / (2 a) + (B / a)² v` (Driver et al., 2017, §4.3, pp. 9–10; p. 16; JSS PDF re-opened 2026-08-22T03:07Z; not free `T0VAR`, not `asymDIFFUSION` alone, not `TRAITVAR` alone, not `addedTIPREDVAR` alone, and not the finite-interval discrete latent variance. Eq. 5 of that constrained variance is `λ²(trait + −q / (2 a) + (B / a)² v) + θ + ψ` (JSS PDF re-opened 2026-08-22T03:20Z; form the stationary latent variance first, then `λ² p + θ + ψ`; `λ² p_0` is not that observed variance; `λ²(−q / (2 a)) + θ` is not that observed variance when `TRAITVAR` or `addedTIPREDVAR` is nonzero; `MANIFESTVAR` is not `Var(y_0)`; the constrained latent variance is not `Var(y_0)`), exact scalar lagged covariance of that constrained process `trait + e^{a Δt}(−q / (2 a)) + (B / a)² v` (Driver et al., 2017, Eq. 3–4 of §4.3 / p. 16 `T0VAR`; JSS PDF re-opened 2026-08-22T19:13Z; trait and `addedTIPREDVAR` do not decay with `e^{a Δt}`; contemporaneous `T0VAR` is not that lagged map; decaying the constrained total as if it were all state is not that lagged map), exact scalar Eq. 5 of that lagged covariance `λ²(trait + e^{a Δt}(−q / (2 a)) + (B / a)² v) + ψ` (`Θ` does not enter; contemporaneous `Var(y_0)` is not that lagged observed covariance; the lagged latent covariance is not that observed covariance), exact scalar later-occasion variance of that constrained process `trait + e^{2 a Δt}(−q / (2 a)) + Q_Δt + (B / a)² v` (Driver et al., 2017, Eq. 3–4 of §4.3 / p. 16 `T0VAR`; JSS PDF re-opened 2026-08-22T23:12Z; trait and `addedTIPREDVAR` do not enter `Q_Δt`; under stationarity that composition equals contemporaneous `T0VAR`; evolving the constrained total as if it were all state is not that later map; the lagged covariance omits `Q_Δt` and is not that later map; `Q_Δt` is not that later map), exact scalar Eq. 5 of that later-occasion variance `λ²(trait + e^{2 a Δt}(−q / (2 a)) + Q_Δt + (B / a)² v) + θ + ψ` (the lagged observed covariance omits `Q_Δt` and `θ`; `MANIFESTVAR` is not that later observed variance; the later-occasion latent variance is not that observed variance)), exact scalar later-occasion variance of §4.3 predetermined `T0VAR` `trait + e^{2 a Δt} p_0 + Q_Δt + (B / a)² v` (Driver et al., 2017, Eq. 3–4 of §4.3 predetermined first occasion; JSS PDF re-opened 2026-08-23T05:12Z; trait and `addedTIPREDVAR` do not enter `Q_Δt`; free `T0VAR` `p_0` is not that later map; setting `p_0 = −q / (2 a)` recovers the stationary later-occasion map; stationary later variance uses `−q / (2 a)` in place of `p_0` and is not that later map when `p_0` is free; evolving `trait + p_0 + (B / a)² v` as if it were all state is not that later map; as `Δt → ∞` with stable `a < 0` the composition approaches contemporaneous stationary `T0VAR`; as `Δt → 0+` the composition approaches `trait + p_0 + (B / a)² v`; nonzero diffusion with `a ≥ 0` is a growing process and is kept. Eq. 5 of that predetermined later-occasion variance is `λ²(trait + e^{2 a Δt} p_0 + Q_Δt + (B / a)² v) + θ + ψ` (`MANIFESTVAR` is not that later observed variance; the predetermined later-occasion latent variance is not that observed variance; stationary later observed variance is not that observed variance when `p_0` is free)), exact scalar lagged covariance of §4.3 predetermined `T0VAR` `trait + e^{a Δt} p_0 + (B / a)² v` (Driver et al., 2017, Eq. 3–4 of §4.3 predetermined first occasion; JSS PDF re-opened 2026-08-23T09:04Z; trait and `addedTIPREDVAR` do not decay with `e^{a Δt}`; free `T0VAR` `p_0` is not that lagged map; setting `p_0 = −q / (2 a)` recovers the stationary lagged map; stationary lagged covariance uses `−q / (2 a)` in place of `p_0` and is not that lagged map when `p_0` is free; evolving `trait + p_0 + (B / a)² v` as if it were all state is not that lagged map; later-occasion variance includes `Q_Δt` and is not that lagged map. Eq. 5 of that predetermined lagged covariance is `λ²(trait + e^{a Δt} p_0 + (B / a)² v) + ψ` (`MANIFESTVAR` does not enter; the predetermined lagged latent covariance is not that observed covariance; predetermined later observed variance includes `Q_Δt` and `θ` and is not that lagged observed covariance; stationary lagged observed covariance is not that observed covariance when `p_0` is free)), exact scalar first-occasion variance of §4.3 predetermined `T0VAR` `trait + p_0 + (B / a)² v` (free `p_0` is not that map; stationary first-occasion variance uses `−q / (2 a)` in place of `p_0` and is not that map when `p_0` is free; lagged covariance decays the state and is not that map; later-occasion variance includes `Q_Δt` and is not that map), exact scalar Eq. 5 of that predetermined first-occasion variance `λ²(trait + p_0 + (B / a)² v) + θ + ψ` (`MANIFESTVAR` is not that first-occasion observed variance; the predetermined first-occasion latent variance is not that observed variance; stationary first-occasion observed variance is not that observed variance when `p_0` is free; predetermined later observed variance includes `Q_Δt` and is not that first-occasion observed variance; later-start lagged covariance of predetermined `T0VAR` is `trait + e^{a s}(e^{2 a u} p_0 + Q_u) + (B / a)² v` (Driver et al., 2017, §4.3 `startoffset`; Eq. 4; JSS PDF re-opened 2026-08-23T10:27Z; first-occasion lagged omits `e^{a s} Q_u`; later-occasion variance does not lag; stationary lagged uses `−q / (2 a)`; decaying the later total is not that map; Eq. 5 of that later-start lagged covariance is `λ²` of it plus `ψ`; `Θ` does not enter; first-occasion lagged observed omits `e^{a s} Q_u`; later observed variance includes `Q_u` and `θ`; later-start later-occasion variance of predetermined `T0VAR` is `trait + e^{2 a s}(e^{2 a u} p_0 + Q_u) + Q_s + (B / a)² v` (Driver et al., 2017, §4.3 `startoffset`; Eq. 3–4 Chapman–Kolmogorov `Q_{u+s} = e^{2 a s} Q_u + Q_s`; JSS PDF re-opened 2026-08-23T11:05Z; later-occasion variance at `u` omits `Q_s`; later-start lagged covariance omits `Q_s`; stationary later uses `−q / (2 a)`; evolving the later total as if it were all state is not that map; ignoring `startoffset` omits `e^{2 a s} Q_u`; Eq. 5 of that later-start later-occasion variance is `λ²` of it plus `θ + ψ`; `MANIFESTVAR` is not that observed variance; p. 16 `discreteDRIFTstd` is `e^{a Δt}` after strictly positive `asymDIFFUSION` `-q / (2 a)` (Driver et al., 2017, p. 16; footnote 4; §7.1; JSS PDF re-opened 2026-08-23T11:40Z; unstandardised `e^{a Δt}` is defined for growing `a ≥ 0` and for zero diffusion and is not `discreteDRIFTstd`; the trait-plus-state autocorrelation uses `TRAITVAR` and is not `discreteDRIFTstd`; p. 16 `discreteDIFFUSIONstd` is `Q_Δt / (−q / (2 a))` after strictly positive `asymDIFFUSION` `-q / (2 a)` (Driver et al., 2017, p. 16; Eq. 4; footnote 4; JSS PDF re-opened 2026-08-23T13:06Z; unstandardised `Q_Δt` is defined for growing `a ≥ 0` and for zero diffusion and is not `discreteDIFFUSIONstd`; the continuous standardisation `−2 a` is not `discreteDIFFUSIONstd`; `Q_Δt / (trait + p + added)` uses `TRAITVAR` and is not `discreteDIFFUSIONstd`; `TRAITVAR` is not the standardisation variance; p. 16 `DIFFUSIONstd` is `q / (−q / (2 a)) = −2 a` after strictly positive `asymDIFFUSION` `-q / (2 a)` (Driver et al., 2017, p. 16; Eq. 4; footnote 4; JSS PDF re-opened 2026-08-23T13:20Z; unstandardised `q` is defined for growing `a ≥ 0` and for zero diffusion and is not `DIFFUSIONstd`; the discrete standardisation `Q_Δt / (−q / (2 a))` depends on `Δt` and is not `DIFFUSIONstd`; `q / (trait + p + added)` uses `TRAITVAR` and is not `DIFFUSIONstd`; `TRAITVAR` is not the standardisation variance; p. 16 `DRIFTstd` is the continuous auto-effect after strictly positive `asymDIFFUSION` `-q / (2 a)` (Driver et al., 2017, p. 16; Eq. 1; footnote 4; JSS PDF re-opened 2026-08-23T13:28Z); unstandardised `a` is defined for growing `a ≥ 0` and for zero diffusion and is not `DRIFTstd`; the discrete standardisation `e^{a Δt}` depends on the event interval and is not `DRIFTstd`; `a p / (trait + p + added)` uses `TRAITVAR` and is not `DRIFTstd`; `TRAITVAR` is not the standardisation variance); p. 16 `asymTIPREDEFFECTstd` is `(-B / a) · √v / √(-q / (2 a))` after strictly positive `asymDIFFUSION` `-q / (2 a)` and strictly positive predictor variance `v` (Driver et al., 2017, p. 16; §7.2; footnote 4; JSS PDF re-opened 2026-08-23T14:25Z; unstandardised `-B / a` is defined for a zero coefficient and for zero predictor variance and is not `asymTIPREDEFFECTstd`; the finite-interval standardisation `A^{-1}[e^{A Δt} − I] B · √v / √p` depends on the event interval and is not `asymTIPREDEFFECTstd`; `(-B / a) · √v / √(trait + p + added)` uses `TRAITVAR` and is not `asymTIPREDEFFECTstd`; `TRAITVAR` is not the standardisation variance); p. 16 `TIPREDEFFECTstd` is `B · √v / √(-q / (2 a))` after strictly positive `asymDIFFUSION` `-q / (2 a)` and strictly positive predictor variance `v` (Driver et al., 2017, p. 16; §7.2; footnote 4; JSS PDF re-opened 2026-08-23T16:21Z; unstandardised `B` is defined for a zero coefficient and for zero predictor variance and is not `TIPREDEFFECTstd`; the asymptotic standardisation `(-B / a) · √v / √p` is the total change and is not `TIPREDEFFECTstd`; the finite-interval standardisation `A^{-1}[e^{A Δt} − I] B · √v / √p` depends on the event interval and is not `TIPREDEFFECTstd`; `B · √v / √(trait + p + added)` uses `TRAITVAR` and is not `TIPREDEFFECTstd`; `TRAITVAR` is not the standardisation variance); Table 3 `T0TIPREDEFFECTstd` is `t0_b · √v / √p_0` after strictly positive free `T0VAR` `p_0` and strictly positive predictor variance `v` (Driver et al., 2017, Table 3, p. 13; p. 16; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T17:20Z; the affected variance is free `T0VAR`, not `asymDIFFUSION`; unstandardised `t0_b` is defined for a zero coefficient and for zero predictor variance and is not `T0TIPREDEFFECTstd`; `TIPREDEFFECTstd` `B · √v / √(-q / (2 a))` is the continuous coefficient and is not `T0TIPREDEFFECTstd`; `asymTIPREDEFFECTstd` `(-B / a) · √v / √p` is the total change and is not `T0TIPREDEFFECTstd`; `t0_b · √v / √(trait + p_0 + added)` uses `TRAITVAR` and is not `T0TIPREDEFFECTstd`; `TRAITVAR` is not the standardisation variance); 2017-era `addedT0TIPREDVAR` is `t0_b² v` (Driver et al., 2017, Table 3, p. 13; p. 16; §7.2; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T18:20Z; `T0TIPREDEFFECT %*% TIPREDVAR %*% t(T0TIPREDEFFECT)` immediately after `T0TIPREDEFFECTstd`; form `t0_b` first, then square, then multiply by `v`; a zero coefficient or zero predictor variance is exactly zero; free `T0TIPREDEFFECT` does not require `a < 0`; `(B / a)² v` is `addedTIPREDVAR` and is not this first-occasion map; `t0_b · √v / √p_0` is `T0TIPREDEFFECTstd` and is not this variance; free `T0VAR` is not this extra TI variance; `TRAITVAR` is not this extra TI variance; Equation 5 of 2017-era `addedT0TIPREDVAR` is `λ² t0_b² v` (Driver et al., 2017, Eq. 5, p. 5; Table 3, p. 13; Table 2, p. 12; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T19:10Z; form `t0_b² v` first, then `(λ extra) λ` with `θ = 0`; a zero loading or zero extra is exactly zero; `t0_b² v` is the latent extra, not the observed extra; `λ² p_0 + θ` is first-occasion observed variance, not this extra; `λ² (B / a)² v` is Eq. 5 of `addedTIPREDVAR`, not this first-occasion observed extra; `MANIFESTVAR` `θ` is not this extra; Equation 5 of §7.2 `addedTIPREDVAR` is `λ² (B / a)² v`; form `(B / a)² v` first, then `(λ extra) λ` with `θ = 0`; a zero loading or zero extra is exactly zero; lasting asymptotic extra requires `a < 0`; `(B / a)² v` is the latent extra, not the observed extra; `λ² t0_b² v` is first-occasion extra observed TI variance, not this extra; `λ² p + θ` is stationary observed variance, not this extra; `MANIFESTVAR` `θ` is not this extra; p. 16 `TDPREDEFFECTstd` is `m · √v / √(-q / (2 a))` after strictly positive `asymDIFFUSION` and strictly positive time-dependent predictor variance; unstandardised `M` is not `TDPREDEFFECTstd`; `TIPREDEFFECTstd` is not `TDPREDEFFECTstd` even when `M = B`; intercept-style `A^{-1}[e^{A Δt} − I] M · √v / √p` is not `TDPREDEFFECTstd`; `m · √v / √(trait + p + added)` uses `TRAITVAR` and is not `TDPREDEFFECTstd`; Table 3 / p. 16 `T0TDPREDEFFECTstd` is `t0_m · √v / √p_0` after strictly positive free `T0VAR` and strictly positive TD predictor variance; unstandardised `t0_m` is not `T0TDPREDEFFECTstd`; `TDPREDEFFECTstd` uses `asymDIFFUSION` and is not `T0TDPREDEFFECTstd`; `T0TIPREDEFFECTstd` is not `T0TDPREDEFFECTstd` even when `t0_m = t0_b`; `t0_m · √v / √(trait + p_0 + added)` uses `TRAITVAR` and is not `T0TDPREDEFFECTstd`; free `T0VAR` does not require `a < 0`; p. 16 `T0VARstd` is `p_0 / p_0 = 1` after strictly positive free `T0VAR` (`solve(sqrt(diag(T0VAR))) %&% T0VAR`; OpenMx `%&%` is `t(A) %*% B %*% A`; default ridge is 0); unstandardised `T0VAR` is not `T0VARstd`; `T0TDPREDEFFECTstd` is not `T0VARstd`; `addedT0TIPREDVAR` is not `T0VARstd`; p. 16 `TRAITVARstd` is `trait / trait = 1` after strictly positive `TRAITVAR` (`solve(sqrt(diag(TRAITVAR))) %&% TRAITVAR`; OpenMx `%&%` is `t(A) %*% B %*% A`; no ridge addend); unstandardised `TRAITVAR` is not `TRAITVARstd`; `T0VARstd` is not `TRAITVARstd` even when both equal 1; `addedT0TIPREDVAR` is not `TRAITVARstd`; p. 16 `MANIFESTTRAITVARstd` is `ψ / ψ = 1` after strictly positive `MANIFESTTRAITVAR` (`solve(sqrt(diag(MANIFESTTRAITVAR))) %&% MANIFESTTRAITVAR`; OpenMx `%&%` is `t(A) %*% B %*% A`; 2017-era source adds ridging; default ridge is 0); unstandardised `MANIFESTTRAITVAR` is not `MANIFESTTRAITVARstd`; `TRAITVARstd` is not `MANIFESTTRAITVARstd` even when both equal 1; `MANIFESTVAR` is not `MANIFESTTRAITVARstd`; p. 16 `MANIFESTVARstd` is `θ / θ = 1` after strictly positive `MANIFESTVAR` (`solve(sqrt(diag(MANIFESTVAR))) %&% MANIFESTVAR`; OpenMx `%&%` is `t(A) %*% B %*% A`; 2017-era source adds ridging; default ridge is 0; 2017-era `dimnames` assignment to `latentNames` is a source bug); unstandardised `MANIFESTVAR` is not `MANIFESTVARstd`; `MANIFESTTRAITVARstd` is not `MANIFESTVARstd` even when both equal 1; Equation 5 `Var(y)` is not `MANIFESTVARstd`; p. 16 `TIPREDVARstd` is `v / v = 1` after strictly positive `TIPREDVAR` (`solve(sqrt(diag(TIPREDVAR))) %&% TIPREDVAR`; OpenMx `%&%` is `t(A) %*% B %*% A`; 2017-era source adds ridging; default ridge is 0; `dimnames` are `TIpredNames`); unstandardised `TIPREDVAR` is not `TIPREDVARstd`; `MANIFESTVARstd` is not `TIPREDVARstd` even when both equal 1; §7.2 `addedTIPREDVAR` is not `TIPREDVARstd`; p. 16 `asymDIFFUSIONstd` is `p / p = 1` after strictly positive `asymDIFFUSION` (`solve(sqrt(diag(asymDIFFUSION))) %&% asymDIFFUSION`; OpenMx `%&%` is `t(A) %*% B %*% A`; 2017-era source adds ridging; default ridge is 0; `dimnames` are `latentNames`); unstandardised `asymDIFFUSION` is not `asymDIFFUSIONstd`; `TIPREDVARstd` is not `asymDIFFUSIONstd` even when both equal 1; `DIFFUSIONstd` `−2 a` is not `asymDIFFUSIONstd`; p. 16 `discreteCINTstd` is `A^{-1}[e^{A Δt} − I] κ / √p` after strictly positive `asymDIFFUSION`; unstandardised `discreteCINT` is not `discreteCINTstd`; `κ / √p` is not `discreteCINTstd`; `(-κ / a) / √p` is not `discreteCINTstd`; `asymCINTstd` is `(-κ / a) / √p` after strictly positive `asymDIFFUSION`; unstandardised `asymCINT` is not `asymCINTstd`; `κ / √p` is not `asymCINTstd`; `discreteCINTstd` is not `asymCINTstd`; `T0MEANSstd` is `μ_0 / √p_0` after strictly positive free `T0VAR`; unstandardised `T0MEANS` is not `T0MEANSstd`; `T0VARstd` is not `T0MEANSstd`; `μ_0 / √asymDIFFUSION` is not `T0MEANSstd`;))))), CWC-then-event-time residual lag, irregular already-centered residual log-rate, and strong/strict-gated two-group OLS latent-mean difference are implemented on the consolidation vehicle PR `integration/psychometric-standardisation` (folding draft stack #181–#218) and are not implemented-main until exact-head checks, review, and protected-main integration complete; full ESEM/set-ESEM, formative composites, DSEM, and matrix continuous-time dynamics remain accepted-target **Date:** 2026-08-05 **Supersedes:** None. ADR 0012 governs upstream topic measurement/network coordinates; this ADR governs higher-order psychometric structure and longitudinal interpretation. diff --git a/docs/research/multilevel-event-time-recovery.md b/docs/research/multilevel-event-time-recovery.md index f3c7b1a5b..7a384b2d0 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -48,15 +48,67 @@ This slice stays inside `psychometric_core`. It does not add a second invariance 42. recover the exact scalar Eq. 5 of lagged §4.3 stationary `T0VAR` `λ²(trait + e^{a Δt}(−q / (2 a)) + (B / a)² v) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Eq. 3–4, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-22T19:13Z; form the lagged latent covariance first, then `λ² c + ψ`; a zero loading is exactly `ψ`; independent `ε_t` does not enter) and refuse treating `θ`, contemporaneous `Var(y_0)`, or the lagged latent covariance as `cov(y_t, y_{t-1})`; 43. recover the exact scalar later-occasion variance of §4.3 stationary `T0VAR` `trait + e^{2 a Δt}(−q / (2 a)) + Q_Δt + (B / a)² v` (Driver et al., 2017, §4.3, pp. 9–10; Eq. 3–4, pp. 4–5; Table 2, p. 12; p. 16; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-22T23:12Z; form the evolved within-subject variance first, then include the trait, then include the TI extra variance, then add; trait and `addedTIPREDVAR` do not enter `Q_Δt`; under stationarity that composition equals contemporaneous `T0VAR`) and refuse treating that composition as lagged covariance, as `e^{2 a Δt}` of the constrained total plus `Q_Δt`, or as `Q_Δt` alone; 44. recover the exact scalar Eq. 5 of later-occasion §4.3 stationary `T0VAR` `λ²(trait + e^{2 a Δt}(−q / (2 a)) + Q_Δt + (B / a)² v) + θ + ψ` (Driver et al., 2017, Eq. 5, p. 5; Eq. 3–4, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-22T23:12Z; form the later-occasion latent variance first, then `λ² p + θ + ψ`; a zero loading is exactly `θ + ψ`; under stationarity that composition equals contemporaneous `Var(y_0)`) and refuse treating `θ`, lagged `cov(y_t, y_{t-1})`, or the later-occasion latent variance as `Var(y_t)`; -45. refuse pooling discrete lags from unequal event intervals as one coefficient; -46. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); -47. refuse the difference quotient as a continuous-time rate; -48. apply the same event-time map to CWC residuals (still not DSEM); -49. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). +45. recover the exact scalar later-occasion variance of §4.3 predetermined `T0VAR` `trait + e^{2 a Δt} p_0 + Q_Δt + (B / a)² v` (Driver et al., 2017, §4.3, pp. 9–10; Eq. 3–4, pp. 4–5; Table 2, p. 12; p. 16; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-23T05:12Z; form the evolved free first-occasion variance first, then include the trait, then include the TI extra variance, then add; trait and `addedTIPREDVAR` do not enter `Q_Δt`; setting `p_0 = −q / (2 a)` recovers the stationary later-occasion map; as `Δt → ∞` with stable `a < 0` the composition approaches contemporaneous stationary `T0VAR`; as `Δt → 0+` the composition approaches `trait + p_0 + (B / a)² v`; nonzero diffusion with `a ≥ 0` is a growing process and is kept) and refuse treating that composition as stationary later-occasion variance, as `e^{2 a Δt}` of `trait + p_0 + (B / a)² v` plus `Q_Δt`, or as free `p_0`; +46. recover the exact scalar Eq. 5 of later-occasion §4.3 predetermined `T0VAR` `λ²(trait + e^{2 a Δt} p_0 + Q_Δt + (B / a)² v) + θ + ψ` (Driver et al., 2017, Eq. 5, p. 5; Eq. 3–4, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-23T05:12Z; form the predetermined later-occasion latent variance first, then `λ² p + θ + ψ`; a zero loading is exactly `θ + ψ`; setting `p_0 = −q / (2 a)` recovers the stationary later-occasion observed variance) and refuse treating `θ`, the predetermined later-occasion latent variance, or stationary later-occasion observed variance as `Var(y_t)` when `p_0` is free; +47. recover the exact scalar lagged covariance of §4.3 predetermined `T0VAR` `trait + e^{a Δt} p_0 + (B / a)² v` (Driver et al., 2017, §4.3, pp. 9–10; Eq. 3–4, pp. 4–5; Table 2, p. 12; p. 16; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-23T09:04Z; form the lagged free first-occasion covariance first, then include the trait, then include the TI extra variance, then add; trait and `addedTIPREDVAR` do not decay with `e^{a Δt}`; setting `p_0 = −q / (2 a)` recovers the stationary lagged map; as `Δt → ∞` with stable `a < 0` the state term vanishes; as `Δt → 0+` the composition approaches `trait + p_0 + (B / a)² v`) and refuse treating that composition as stationary lagged covariance, as predetermined later-occasion variance, as `e^{a Δt}` of `trait + p_0 + (B / a)² v`, or as free `p_0`; +48. recover the exact scalar Eq. 5 of lagged §4.3 predetermined `T0VAR` `λ²(trait + e^{a Δt} p_0 + (B / a)² v) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Eq. 3–4, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-23T09:04Z; form the predetermined lagged latent covariance first, then `λ² c + ψ`; a zero loading is exactly `ψ`; independent `ε_t` does not enter) and refuse treating `θ`, the predetermined lagged latent covariance, predetermined later observed variance, or stationary lagged observed covariance as `cov(y_t, y_{t-1})` when `p_0` is free; +49. recover the exact scalar first-occasion variance of §4.3 predetermined `T0VAR` `trait + p_0 + (B / a)² v` (Driver et al., 2017, §4.3, pp. 9–10; Eq. 5, p. 5; Table 2, p. 12; p. 16; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-23T10:03Z; form the free first-occasion state variance first, then include the trait, then include the TI extra variance, then add; trait and `addedTIPREDVAR` do not decay and do not enter `Q_Δt`; setting `p_0 = −q / (2 a)` recovers the stationary first-occasion map; as `Δt → 0+` the lagged and later maps approach this composition) and refuse treating that composition as stationary first-occasion variance, as free `p_0`, as predetermined lagged covariance, or as predetermined later-occasion variance; +50. recover the exact scalar Eq. 5 of first-occasion §4.3 predetermined `T0VAR` `λ²(trait + p_0 + (B / a)² v) + θ + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-23T10:03Z; form the predetermined first-occasion latent variance first, then `λ² p + θ + ψ`; a zero loading is exactly `θ + ψ`) and refuse treating `θ`, the predetermined first-occasion latent variance, stationary first-occasion observed variance, or predetermined later observed variance as `Var(y_0)` when `p_0` is free; +51. recover the exact scalar later-start lagged covariance of §4.3 predetermined `T0VAR` `trait + e^{a s}(e^{2 a u} p_0 + Q_u) + (B / a)² v` (Driver et al., 2017, §4.3, pp. 9–10; Eq. 3–4, pp. 4–5; Table 2, p. 12; p. 16; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-23T10:27Z; form the later-start within-subject variance first, then lag that state, then include the trait, then include the TI extra variance, then add; trait and `addedTIPREDVAR` do not decay with `e^{a s}`; setting `p_0 = −q / (2 a)` recovers the stationary lagged map; as `u → 0+` the composition approaches first-occasion lagged covariance; as `s → 0+` the composition approaches later-occasion variance at `u`) and refuse treating that composition as first-occasion lagged covariance, as later-occasion variance, as stationary lagged covariance, or as `e^{a s}` of the later total; +52. recover the exact scalar Eq. 5 of later-start lagged §4.3 predetermined `T0VAR` `λ²(trait + e^{a s}(e^{2 a u} p_0 + Q_u) + (B / a)² v) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Eq. 3–4, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-23T10:27Z; form the later-start lagged latent covariance first, then `λ² c + ψ`; a zero loading is exactly `ψ`; independent `ε_t` does not enter) and refuse treating `θ`, the later-start lagged latent covariance, first-occasion lagged observed covariance, predetermined later observed variance, or stationary lagged observed covariance as `cov(y_{t0+u+s}, y_{t0+u})` when `p_0` is free; +53. recover the exact scalar later-start later-occasion variance of §4.3 predetermined `T0VAR` `trait + e^{2 a s}(e^{2 a u} p_0 + Q_u) + Q_s + (B / a)² v` (Driver et al., 2017, §4.3, pp. 9–10; Eq. 3–4, pp. 4–5; Table 2, p. 12; p. 16; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-23T11:05Z; Chapman–Kolmogorov `Q_{u+s} = e^{2 a s} Q_u + Q_s`; form the later-start within-subject variance first, then evolve that state, then include the trait, then include the TI extra variance, then add; trait and `addedTIPREDVAR` do not enter `Q_s`; setting `p_0 = −q / (2 a)` recovers the stationary later-occasion map; as `u → 0+` the composition approaches later-occasion variance over `s`; as `s → 0+` the composition approaches later-occasion variance at `u`) and refuse treating that composition as later-occasion variance at `u`, as later-start lagged covariance, as stationary later-occasion variance, as `e^{2 a s}` of the later total plus `Q_s`, or as later-occasion variance over the lag interval alone; +54. recover the exact scalar Eq. 5 of later-start later-occasion §4.3 predetermined `T0VAR` `λ²(trait + e^{2 a s}(e^{2 a u} p_0 + Q_u) + Q_s + (B / a)² v) + θ + ψ` (Driver et al., 2017, Eq. 5, p. 5; Eq. 3–4, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-23T11:05Z; form the later-start later-occasion latent variance first, then `λ² p + θ + ψ`; a zero loading is exactly `θ + ψ`) and refuse treating `θ`, the later-start later-occasion latent variance, predetermined later observed variance, later-start lagged observed covariance, or stationary later-occasion observed variance as `Var(y_{t0+u+s})` when `p_0` is free; +55. recover the exact scalar p. 16 `discreteDRIFTstd` `e^{a Δt}` after forming strictly positive `asymDIFFUSION` `−q / (2 a)` (Driver et al., 2017, p. 16; Eq. 3, p. 5; footnote 4; §7.1, pp. 18–19; JSS PDF re-opened 2026-08-23T11:40Z; form the within-subject variance first, then `φ = exp(a Δt)`; scalar stationary SD ratio is 1; `a ≥ 0` and `q = 0` fail closed); +56. refuse treating unstandardised `discreteDRIFT` `e^{a Δt}` as `discreteDRIFTstd`, refuse treating the §7.1 trait-plus-state autocorrelation `(trait + e^{a Δt} p + added) / (trait + p + added)` as `discreteDRIFTstd`, and refuse treating `TRAITVAR` as the footnote 4 standardisation variance; +57. recover the exact scalar p. 16 `discreteDIFFUSIONstd` `Q_Δt / (−q / (2 a))` after forming strictly positive `asymDIFFUSION` `−q / (2 a)` (Driver et al., 2017, p. 16; Eq. 3–4, pp. 4–5; footnote 4; §7.1, pp. 18–19; JSS PDF re-opened 2026-08-23T13:06Z; form the within-subject variance first, then `Q_Δt`, then the ratio; scalar stationary map is `1 − exp(2 a Δt)`; `a ≥ 0` and `q = 0` fail closed); +58. refuse treating unstandardised `discreteDIFFUSION` `Q_Δt` as `discreteDIFFUSIONstd`, refuse treating the continuous standardisation `q / (−q / (2 a)) = −2 a` as `discreteDIFFUSIONstd`, refuse treating `Q_Δt / (trait + p + added)` as `discreteDIFFUSIONstd`, and refuse treating `TRAITVAR` as the footnote 4 standardisation variance; +59. recover the exact scalar p. 16 `DIFFUSIONstd` `q / (−q / (2 a)) = −2 a` after forming strictly positive `asymDIFFUSION` `−q / (2 a)` (Driver et al., 2017, p. 16; Eq. 4, p. 5; footnote 4; §7.1, pp. 18–19; JSS PDF re-opened 2026-08-23T13:20Z; form the within-subject variance first, then `q / p`; scalar stationary map is `−2 a` and does not depend on `q` once `q > 0`; `a ≥ 0` and `q = 0` fail closed); +60. refuse treating unstandardised `DIFFUSION` `q` as `DIFFUSIONstd`, refuse treating the discrete standardisation `Q_Δt / (−q / (2 a)) = 1 − exp(2 a Δt)` as `DIFFUSIONstd`, refuse treating `q / (trait + p + added)` as `DIFFUSIONstd`, and refuse treating `TRAITVAR` as the footnote 4 standardisation variance; +61. recover the exact scalar p. 16 `DRIFTstd` after forming strictly positive `asymDIFFUSION` `−q / (2 a)` (Driver et al., 2017, p. 16; Eq. 1, p. 4; footnote 4; §7.1, pp. 18–19; JSS PDF re-opened 2026-08-23T13:28Z; form the within-subject variance first; scalar stationary SD ratio is 1 so the standardised auto-effect equals `a` numerically; those remain distinct named quantities; `a ≥ 0` and `q = 0` fail closed); +62. refuse treating unstandardised `DRIFT` `a` as `DRIFTstd`, refuse treating the discrete standardisation `e^{a Δt}` as `DRIFTstd`, refuse treating `a p / (trait + p + added)` as `DRIFTstd`, and refuse treating `TRAITVAR` as the footnote 4 standardisation variance; +63. recover the exact scalar p. 16 `asymTIPREDEFFECTstd` `(-B / a) · √v / √(-q / (2 a))` after forming strictly positive `asymDIFFUSION` `−q / (2 a)` and strictly positive predictor variance `v` (Driver et al., 2017, p. 16; §7.2, pp. 20–21; Eq. 3, p. 5; Table 2, p. 12; footnote 4; JSS PDF re-opened 2026-08-23T14:25Z; form the within-subject variance first, then `v`, then the unit asymptotic effect, then the SD ratio; `a ≥ 0`, `q = 0`, and `v = 0` fail closed); +64. refuse treating unstandardised `asymTIPREDEFFECT` `-B / a` as `asymTIPREDEFFECTstd`, refuse treating the finite-interval standardisation `A^{-1}[e^{A Δt} − I] B · √v / √p` as `asymTIPREDEFFECTstd`, refuse treating `(-B / a) · √v / √(trait + p + added)` as `asymTIPREDEFFECTstd`, and refuse treating `TRAITVAR` as the footnote 4 standardisation variance; +65. recover the exact scalar p. 16 `TIPREDEFFECTstd` `B · √v / √(-q / (2 a))` after forming strictly positive `asymDIFFUSION` `−q / (2 a)` and strictly positive predictor variance `v` (Driver et al., 2017, p. 16; §7.2, pp. 20–21; Eq. 3, p. 5; Table 2, p. 12; footnote 4; JSS PDF re-opened 2026-08-23T16:21Z; form the within-subject variance first, then `v`, then the continuous coefficient, then the SD ratio; `a ≥ 0`, `q = 0`, and `v = 0` fail closed); +66. refuse treating unstandardised `TIPREDEFFECT` `B` as `TIPREDEFFECTstd`, refuse treating `asymTIPREDEFFECTstd` `(-B / a) · √v / √p` as `TIPREDEFFECTstd`, refuse treating the finite-interval standardisation `A^{-1}[e^{A Δt} − I] B · √v / √p` as `TIPREDEFFECTstd`, refuse treating `B · √v / √(trait + p + added)` as `TIPREDEFFECTstd`, and refuse treating `TRAITVAR` as the footnote 4 standardisation variance; +67. recover the exact scalar Table 3 / p. 16 `T0TIPREDEFFECTstd` `t0_b · √v / √p_0` after forming strictly positive free `T0VAR` `p_0` and strictly positive predictor variance `v` (Driver et al., 2017, Table 3, p. 13; p. 16; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T17:20Z; form free `T0VAR` first, then `v`, then the first-occasion coefficient, then the SD ratio; `p_0 = 0` and `v = 0` fail closed; a non-event clock fails closed; free `T0VAR` does not require `a < 0`); +68. refuse treating unstandardised `T0TIPREDEFFECT` `t0_b` as `T0TIPREDEFFECTstd`, refuse treating `TIPREDEFFECTstd` `B · √v / √(-q / (2 a))` as `T0TIPREDEFFECTstd`, refuse treating `asymTIPREDEFFECTstd` `(-B / a) · √v / √p` as `T0TIPREDEFFECTstd`, refuse treating `t0_b · √v / √(trait + p_0 + added)` as `T0TIPREDEFFECTstd`, and refuse treating `TRAITVAR` as the footnote 4 standardisation variance; +69. recover the exact scalar 2017-era `addedT0TIPREDVAR` `t0_b² v` (Driver et al., 2017, Table 3, p. 13; p. 16; §7.2, pp. 20–21; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T18:20Z; form `t0_b` first, then square, then multiply by `v`; a zero coefficient or zero predictor variance is exactly zero; `v < 0` fails closed; a non-event clock fails closed; free `T0TIPREDEFFECT` does not require `a < 0`); +70. refuse treating `addedT0TIPREDVAR` as `addedTIPREDVAR` `(B / a)² v`, refuse treating `addedT0TIPREDVAR` as `T0TIPREDEFFECTstd` `t0_b · √v / √p_0`, refuse treating `addedT0TIPREDVAR` as free `T0VAR` `p_0`, and refuse treating `addedT0TIPREDVAR` as `TRAITVAR`; +71. recover the exact scalar Eq. 5 of 2017-era `addedT0TIPREDVAR` `λ² t0_b² v` (Driver et al., 2017, Eq. 5, p. 5; Table 3, p. 13; Table 2, p. 12; p. 16; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T19:10Z; form `t0_b² v` first, then `(λ extra) λ` with `θ = 0`; a zero loading or zero extra is exactly zero; `v < 0` fails closed; a non-event clock fails closed; free `T0TIPREDEFFECT` does not require `a < 0`); +72. refuse treating `λ² t0_b² v` as the latent extra `t0_b² v`, refuse treating `λ² t0_b² v` as first-occasion observed variance `λ² p_0 + θ`, refuse treating `λ² t0_b² v` as Eq. 5 of `addedTIPREDVAR` `λ² (B / a)² v`, and refuse treating `λ² t0_b² v` as `MANIFESTVAR` `θ`; +73. recover the exact scalar Eq. 5 of §7.2 `addedTIPREDVAR` `λ² (B / a)² v` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; §7.2, pp. 20–21; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T19:23Z; form `(B / a)² v` first, then `(λ extra) λ` with `θ = 0`; a zero loading or zero extra is exactly zero; `v < 0` fails closed; a non-event clock fails closed; `a ≥ 0` with a nonzero extra fails closed); +74. refuse treating `λ² (B / a)² v` as the latent extra `(B / a)² v`, refuse treating `λ² (B / a)² v` as Eq. 5 of `addedT0TIPREDVAR` `λ² t0_b² v`, refuse treating `λ² (B / a)² v` as stationary observed variance `λ² p + θ`, and refuse treating `λ² (B / a)² v` as `MANIFESTVAR` `θ`; +75. recover the exact scalar p. 16 `TDPREDEFFECTstd` `m · √v / √(-q / (2 a))` after forming strictly positive `asymDIFFUSION` `−q / (2 a)` and strictly positive time-dependent predictor variance `v` (Driver et al., 2017, p. 16; Table 2, p. 12; Eq. 3, p. 5; footnote 4; JSS PDF re-opened 2026-08-23T21:10Z; form the within-subject variance first, then `v`, then the continuous Dirac coefficient, then the SD ratio; `a ≥ 0`, `q = 0`, and `v = 0` fail closed); +76. refuse treating unstandardised `TDPREDEFFECT` `M` as `TDPREDEFFECTstd`, refuse treating `TIPREDEFFECTstd` `B · √v / √p` as `TDPREDEFFECTstd` even when `M = B`, refuse treating the finite-interval intercept-style standardisation `A^{-1}[e^{A Δt} − I] M · √v / √p` as `TDPREDEFFECTstd`, refuse treating `m · √v / √(trait + p + added)` as `TDPREDEFFECTstd`, and refuse treating `TRAITVAR` as the footnote 4 standardisation variance; +77. recover the exact scalar Table 3 / p. 16 `T0TDPREDEFFECTstd` `t0_m · √v / √p_0` after forming strictly positive free `T0VAR` `p_0` and strictly positive time-dependent predictor variance `v` (Driver et al., 2017, Table 3, p. 13; Table 2, p. 12; p. 16; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T21:34Z; form free `T0VAR` first, then `v`, then the first-occasion coefficient, then the SD ratio; `p_0 = 0` and `v = 0` fail closed; a non-event clock fails closed; free `T0VAR` does not require `a < 0`); +78. refuse treating unstandardised `T0TDPREDEFFECT` `t0_m` as `T0TDPREDEFFECTstd`, refuse treating `TDPREDEFFECTstd` `m · √v / √(-q / (2 a))` as `T0TDPREDEFFECTstd`, refuse treating `T0TIPREDEFFECTstd` `t0_b · √v / √p_0` as `T0TDPREDEFFECTstd` even when `t0_m = t0_b`, refuse treating `t0_m · √v / √(trait + p_0 + added)` as `T0TDPREDEFFECTstd`, and refuse treating `TRAITVAR` as the footnote 4 standardisation variance; +79. recover the exact scalar p. 16 `T0VARstd` as `solve(sqrt(diag(T0VAR))) %&% T0VAR` after forming strictly positive free `T0VAR` `p_0` (Driver et al., 2017, Table 2, p. 12; p. 16; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T22:06Z; OpenMx `%&%` is `t(A) %*% B %*% A`; the default ridge is 0; the scalar map is `p_0 / p_0 = 1`; `p_0 = 0` fails closed; a non-event clock fails closed; free `T0VAR` does not require `a < 0`); +80. refuse treating unstandardised `T0VAR` as `T0VARstd`, refuse treating `T0TDPREDEFFECTstd` `t0_m · √v / √p_0` as `T0VARstd`, refuse treating `addedT0TIPREDVAR` `t0_b² v` as `T0VARstd`, and refuse treating `TRAITVAR` as the footnote 4 standardisation variance; +81. recover the exact scalar p. 16 `TRAITVARstd` as `solve(sqrt(diag(TRAITVAR))) %&% TRAITVAR` after forming strictly positive `TRAITVAR` (Driver et al., 2017, Table 2, p. 12; §7.1, pp. 18–19; p. 16; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T22:21Z; OpenMx `%&%` is `t(A) %*% B %*% A`; unlike `T0VARstd` there is no ridge addend; the scalar map is `trait / trait = 1`; `TRAITVAR = 0` fails closed; a non-event clock fails closed; `TRAITVAR` does not require `a < 0`); +82. refuse treating unstandardised `TRAITVAR` as `TRAITVARstd`, refuse treating `T0VARstd` as `TRAITVARstd` even when both equal 1, and refuse treating `addedT0TIPREDVAR` `t0_b² v` as `TRAITVARstd`; +83. recover the exact scalar p. 16 `MANIFESTTRAITVARstd` as `solve(sqrt(diag(MANIFESTTRAITVAR))) %&% MANIFESTTRAITVAR` after forming strictly positive `MANIFESTTRAITVAR` (Driver et al., 2017, Table 2, p. 12; §7.1, p. 19; p. 16; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T22:28Z; OpenMx `%&%` is `t(A) %*% B %*% A`; unlike `TRAITVARstd` the 2017-era source adds ridging; the default ridge is 0; the scalar map is `ψ / ψ = 1`; `MANIFESTTRAITVAR = 0` fails closed; a non-event clock fails closed; `MANIFESTTRAITVAR` does not require `a < 0`); +84. refuse treating unstandardised `MANIFESTTRAITVAR` as `MANIFESTTRAITVARstd`, refuse treating `TRAITVARstd` as `MANIFESTTRAITVARstd` even when both equal 1, and refuse treating `MANIFESTVAR` `θ` as `MANIFESTTRAITVARstd`; +85. recover the exact scalar p. 16 `MANIFESTVARstd` as `solve(sqrt(diag(MANIFESTVAR))) %&% MANIFESTVAR` after forming strictly positive `MANIFESTVAR` (Driver et al., 2017, Table 2, p. 12; Eq. 5, p. 5; p. 16; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T22:40Z; OpenMx `%&%` is `t(A) %*% B %*% A`; unlike `TRAITVARstd` the 2017-era source adds ridging; the default ridge is 0; the 2017-era `dimnames` assignment to `latentNames` is a source bug; the scalar map is `θ / θ = 1`; `MANIFESTVAR = 0` makes `solve(sqrt(0))` fail and fails closed; a non-event clock fails closed; `MANIFESTVAR` does not require `a < 0`); +86. refuse treating unstandardised `MANIFESTVAR` as `MANIFESTVARstd`, refuse treating `MANIFESTTRAITVARstd` as `MANIFESTVARstd` even when both equal 1, and refuse treating Equation 5 `Var(y)` as `MANIFESTVARstd`; +87. recover the exact scalar p. 16 `TIPREDVARstd` as `solve(sqrt(diag(TIPREDVAR))) %&% TIPREDVAR` after forming strictly positive `TIPREDVAR` (Driver et al., 2017, Table 2, p. 12; p. 16; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T22:53Z; OpenMx `%&%` is `t(A) %*% B %*% A`; unlike `TRAITVARstd` the 2017-era source adds ridging; the default ridge is 0; `dimnames` are `TIpredNames`; the scalar map is `v / v = 1`; `TIPREDVAR = 0` makes `solve(sqrt(0))` fail and fails closed; a non-event clock fails closed; `TIPREDVAR` does not require `a < 0`); +88. refuse treating unstandardised `TIPREDVAR` as `TIPREDVARstd`, refuse treating `MANIFESTVARstd` as `TIPREDVARstd` even when both equal 1, and refuse treating §7.2 `addedTIPREDVAR` `(B / a)² v` as `TIPREDVARstd`; +89. recover the exact scalar p. 16 `asymDIFFUSIONstd` as `solve(sqrt(diag(asymDIFFUSION))) %&% asymDIFFUSION` after forming strictly positive `asymDIFFUSION` `−q / (2 a)` (Driver et al., 2017, p. 16; footnote 4; Eq. 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T23:02Z; OpenMx `%&%` is `t(A) %*% B %*% A`; the 2017-era source adds ridging; the default ridge is 0; `dimnames` are `latentNames`; the scalar map is `p / p = 1`; `q = 0` makes `solve(sqrt(0))` fail and fails closed; a non-event clock fails closed; `a ≥ 0` fails closed); +90. refuse treating unstandardised `asymDIFFUSION` as `asymDIFFUSIONstd`, refuse treating `TIPREDVARstd` as `asymDIFFUSIONstd` even when both equal 1, and refuse treating `DIFFUSIONstd` `−2 a` as `asymDIFFUSIONstd`; +91. recover the exact scalar p. 16 `discreteCINTstd` as `A^{-1}[e^{A Δt} − I] κ / √p` after forming strictly positive `asymDIFFUSION` `−q / (2 a)` (Driver et al., 2017, p. 16; footnote 4; Eq. 3; Table 2, p. 12; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-24T05:20Z; the 2017-era source forms unstandardised `discreteCINT` whenever `verbose = TRUE` as `solve(DRIFT) %*% (discreteDRIFT − I) %*% CINT`; that source does not form a `discreteCINTstd` matrix; a zero intercept is exactly zero; `q = 0` fails closed; a non-event clock fails closed; a non-positive event interval fails closed; `a ≥ 0` fails closed); +92. refuse treating unstandardised `discreteCINT` as `discreteCINTstd`, refuse treating `κ / √p` as `discreteCINTstd`, and refuse treating `(-κ / a) / √p` as `discreteCINTstd`; +93. recover the exact scalar p. 16 `asymCINTstd` as `(-κ / a) / √p` after forming strictly positive `asymDIFFUSION` `−q / (2 a)` (Driver et al., 2017, p. 16; footnote 4; Eq. 3; Table 2, p. 12; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-24T09:05Z; the 2017-era source forms unstandardised `asymCINT` whenever `verbose = TRUE` as `-solve(DRIFT) %*% CINT`; that source does not form an `asymCINTstd` matrix; a zero intercept is exactly zero; `q = 0` fails closed; a non-event clock fails closed; `a ≥ 0` fails closed); +94. refuse treating unstandardised `asymCINT` as `asymCINTstd`, refuse treating `κ / √p` as `asymCINTstd`, and refuse treating `discreteCINTstd` as `asymCINTstd`; +95. recover the exact scalar p. 16 `T0MEANSstd` as `μ_0 / √p_0` after forming strictly positive free `T0VAR` `p_0` (Driver et al., 2017, p. 16; footnote 4; Table 2, p. 12; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-24T22:30Z; the 2017-era source forms unstandardised `T0MEANS` as `OpenMx::mxEval(T0MEANS, mxobj, compute=TRUE)`; that source does not form a `T0MEANSstd` matrix; a zero mean is exactly zero; `p_0 = 0` fails closed; a non-event clock fails closed; free `T0MEANS` does not require `a < 0`); +96. refuse treating unstandardised `T0MEANS` as `T0MEANSstd`, refuse treating `T0VARstd` as `T0MEANSstd` even when both equal 1, and refuse treating `μ_0 / √asymDIFFUSION` as `T0MEANSstd`; +87. refuse pooling discrete lags from unequal event intervals as one coefficient; +88. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); +89. refuse the difference quotient as a continuous-time rate; +90. apply the same event-time map to CWC residuals (still not DSEM); +89. map already-centered lagged residuals with irregular event intervals without re-centering (Curran & Bauer, 2011, pp. 607–608). ## Claim boundary -This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. Manifest means are not the observed-indicator mean. The latent mean is not the observed-indicator mean. The continuous intercept is not the manifest mean. The first-occasion latent mean is not the evolved latent mean. The continuous intercept is not the discrete mean increment. The first-occasion observed mean is not the evolved observed mean. The contemporaneous `TDPREDEFFECT` impulse is not the continuous intercept, not the time-independent discrete effect, and not Voelkle et al. (2012, Eq. 14). The time-independent `TIPREDEFFECT` increment is not the continuous intercept, not the contemporaneous impulse, not Voelkle et al. (2012, Eq. 14), and not the coefficient `B`. The within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). The evolved observed mean `τ + λ μ_t` is not the contemporaneous-impulse observed mean `τ + λ(μ_t + m x)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not the impulse-carry observed mean `τ + λ(μ_t + e^{a(t−u)} m x)` when `u ≠ t`. The evolved observed mean `τ + λ μ_t` is not the impulse-carry observed mean. The carried latent mean is not `E(y_t)`. The evolved-plus-impulse latent mean is not `E(y_t)`. The evolved observed mean `τ + λ μ_t` is not the time-independent-predictor observed mean `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not that time-independent-predictor observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that time-independent-predictor observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TIPREDEFFECT` shift `t0_b z` is not the Eq. 3 process increment `A^{-1}[e^{A Δt} − I] B z`, not `κ`, and not `M x`. The Eq. 3 first-summand carry `e^{A Δt} t0_b z` is not `t0_b z` and is not that process increment. `T0TIPREDEFFECT` is the coefficient, not the first-occasion shift. The evolved observed mean `τ + λ μ_t` is not the first-occasion TI-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_b z)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion observed mean when `u ≠ t0`. The evolved-plus-T0TIPRED latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TDPREDEFFECT` shift `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `κ`. The Eq. 3 first-summand carry `e^{A Δt} t0_m x0` is not `t0_m x0` and is not that within-interval impulse carry. `T0TDPREDEFFECT` is the coefficient, not the first-occasion shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The evolved observed mean `τ + λ μ_t` is not the first-occasion TD-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_m x0)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion TD observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion TD observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion TD observed mean when `u ≠ t0`. The first-occasion TI composition `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that first-occasion TD observed mean. Same numbers as `T0TIPREDEFFECT` yield the same product; Table 3 names a different matrix. The evolved-plus-T0TDPRED latent mean is not `E(y_t)`. The §7.2 level-change `CINT` `κ = −a m x` is not the dissipating Dirac `m x`, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`. Lasting level change via that `CINT` setting requires `a < 0`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not that `CINT` setting. The Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` is not the dissipating Dirac `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`. Underflow of `e^{a Δt}` to `+0` keeps the equilibrium offset `m x`. The §7.2 extra-process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. Lasting level change via that extra process requires `ε < 0`. Precisely `ε = 0` causes computational problems in the printed specification. The extra-process observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` is not `τ + λ μ_t`, not `τ + λ(μ_t + m x)`, not the contribution, and not the evolved-plus-contribution latent mean. The extra process has `LAMBDA` 0 and is not an observed indicator. `T0TDPREDEFFECT` on the extra process uses `Δt = t − t0` for both the original-process evolution and the extra drive. `TDPREDEFFECT` after `t0` uses `t − u` with `t0 < u < t` for the extra drive while `μ_t` still uses `Δt`. The after-t0 extra-process observed mean is not the first-occasion extra-process observed mean when `u ≠ t0`. The impulse-carry `e^{a(t−u)} m x` is a Dirac on the original process and is not that `DRIFT` drive. The §7.2 `asymTIPREDEFFECT` `-B z / a` is the expected total change in process means given a time-independent predictor. It is not the coefficient `B`, not the finite-interval increment `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. Lasting asymptotic change via that map requires `a < 0`. The §7.2 `addedTIPREDVAR` `(B / a)² v` is the stable between-subject variance accounted for by that predictor. It is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`. Table 2 `asymCINT` `-κ / a` is the intercept contribution to the stationary process mean. It is not `κ`, not the finite-interval increment `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`. Lasting asymptotic intercept change via that map requires `a < 0`. Page 16 notes that a `T0MEANS` stationarity constraint includes time-independent predictors; that composition is not this intercept-only map. The p. 16 constrained first-occasion mean `-κ / a + −B z / a` is not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean. Equation 5 of that constrained mean `τ + λ(−κ / a + −B z / a)` is not `τ + λ μ_0`, not `τ + λ(−κ / a)` when `B z ≠ 0`, not `τ + λ μ_t`, not `MANIFESTMEANS`, and not the constrained latent mean. The p. 16 constrained first-occasion variance `trait + −q / (2 a) + (B / a)² v` is not free `T0VAR`, not `asymDIFFUSION` alone, not `TRAITVAR` alone, not `addedTIPREDVAR` alone, and not the finite-interval discrete latent variance. Equation 5 of that constrained variance `λ²(trait + −q / (2 a) + (B / a)² v) + θ + ψ` is not `λ² p_0 + θ`, not `λ²(−q / (2 a)) + θ` when `TRAITVAR` or `addedTIPREDVAR` is nonzero, not `λ² Var(η_t) + θ` when the first occasion is constrained, not `MANIFESTVAR`, and not the constrained latent variance. The lagged covariance of that constrained process `trait + e^{a Δt}(−q / (2 a)) + (B / a)² v` is not contemporaneous `T0VAR`, not `e^{a Δt}` of the constrained total, and not `trait + e^{a Δt} p` when `addedTIPREDVAR` is nonzero. Trait variance and `addedTIPREDVAR` do not decay with `e^{a Δt}`. Equation 5 of that lagged covariance `λ²(trait + e^{a Δt}(−q / (2 a)) + (B / a)² v) + ψ` is not `θ`, not contemporaneous `Var(y_0)`, and not the lagged latent covariance. Independent measurement error does not enter lagged observed covariance. The later-occasion variance of that constrained process `trait + e^{2 a Δt}(−q / (2 a)) + Q_Δt + (B / a)² v` equals contemporaneous `T0VAR` under stationarity and is not the lagged covariance, not `e^{2 a Δt}` of the constrained total plus `Q_Δt`, and not `Q_Δt` alone. Trait variance and `addedTIPREDVAR` do not enter `Q_Δt`. Equation 5 of that later-occasion variance `λ²(trait + e^{2 a Δt}(−q / (2 a)) + Q_Δt + (B / a)² v) + θ + ψ` is not `θ`, not lagged `cov(y_t, y_{t-1})`, and not the later-occasion latent variance. +This is two-level OLS and a noiseless scalar continuous-time map. It is not DSEM, not RI-CLPM, not a random-effects sampler, not a Kalman filter, and not a matrix `expm` implementation. The CWC cluster-mean coefficient is the **contextual** effect, not the between-cluster effect. Discrete lags from different event intervals are not one coefficient. Equation 14 is not Equation 12. Discrete process noise \(Q_{\Delta t}\) is not the continuous diffusion \(GG^{\top}\). \(Q_{\Delta t}\) is the conditional residual variance, not \(\operatorname{Var}(\eta_{t})\). Finite-interval \(Q_{\Delta t}\) is not the stationary within-subject variance. Trait variance is not process noise and not the stationary within-subject variance. Measurement-error variance is not the observed-indicator variance. Latent variance is not the observed-indicator variance. Manifest means are not the observed-indicator mean. The latent mean is not the observed-indicator mean. The continuous intercept is not the manifest mean. The first-occasion latent mean is not the evolved latent mean. The continuous intercept is not the discrete mean increment. The first-occasion observed mean is not the evolved observed mean. The contemporaneous `TDPREDEFFECT` impulse is not the continuous intercept, not the time-independent discrete effect, and not Voelkle et al. (2012, Eq. 14). The time-independent `TIPREDEFFECT` increment is not the continuous intercept, not the contemporaneous impulse, not Voelkle et al. (2012, Eq. 14), and not the coefficient `B`. The within-interval `TDPREDEFFECT` carry `e^{A(t−u)} M x` for `t0 < u < t` is not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle et al. (2012, Eq. 14). The evolved observed mean `τ + λ μ_t` is not the contemporaneous-impulse observed mean `τ + λ(μ_t + m x)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not the impulse-carry observed mean `τ + λ(μ_t + e^{a(t−u)} m x)` when `u ≠ t`. The evolved observed mean `τ + λ μ_t` is not the impulse-carry observed mean. The carried latent mean is not `E(y_t)`. The evolved-plus-impulse latent mean is not `E(y_t)`. The evolved observed mean `τ + λ μ_t` is not the time-independent-predictor observed mean `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`. The contemporaneous composition `τ + λ(μ_t + m x)` is not that time-independent-predictor observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that time-independent-predictor observed mean when `u ≠ t`. The evolved-plus-increment latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TIPREDEFFECT` shift `t0_b z` is not the Eq. 3 process increment `A^{-1}[e^{A Δt} − I] B z`, not `κ`, and not `M x`. The Eq. 3 first-summand carry `e^{A Δt} t0_b z` is not `t0_b z` and is not that process increment. `T0TIPREDEFFECT` is the coefficient, not the first-occasion shift. The evolved observed mean `τ + λ μ_t` is not the first-occasion TI-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_b z)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion observed mean when `u ≠ t0`. The evolved-plus-T0TIPRED latent mean is not `E(y_t)`. The Table 3 first-occasion `T0TDPREDEFFECT` shift `t0_m x0` is not `M x`, not `e^{A(t−u)} M x` for `t0 < u < t`, not `t0_b z`, not `A^{-1}[e^{A Δt} − I] B z`, and not `κ`. The Eq. 3 first-summand carry `e^{A Δt} t0_m x0` is not `t0_m x0` and is not that within-interval impulse carry. `T0TDPREDEFFECT` is the coefficient, not the first-occasion shift. An impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`. The evolved observed mean `τ + λ μ_t` is not the first-occasion TD-predictor observed mean `τ + λ(μ_t + e^{a Δt} t0_m x0)`. The process-increment composition `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that first-occasion TD observed mean. The contemporaneous composition `τ + λ(μ_t + m x)` is not that first-occasion TD observed mean. The impulse-carry composition `τ + λ(μ_t + e^{a(t−u)} m x)` is not that first-occasion TD observed mean when `u ≠ t0`. The first-occasion TI composition `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that first-occasion TD observed mean. Same numbers as `T0TIPREDEFFECT` yield the same product; Table 3 names a different matrix. The evolved-plus-T0TDPRED latent mean is not `E(y_t)`. The §7.2 level-change `CINT` `κ = −a m x` is not the dissipating Dirac `m x`, not a free `CINT`, and not `A^{-1}[e^{A Δt} − I] B z`. Lasting level change via that `CINT` setting requires `a < 0`. The extra near-zero-drift latent process also named in §7.2 is a different specification and is not that `CINT` setting. The Eq. 3 increment of that setting `(1 − e^{a Δt}) m x` is not the dissipating Dirac `m x`, not `κ`, and not `A^{-1}[e^{A Δt} − I] B z`. Underflow of `e^{a Δt}` to `+0` keeps the equilibrium offset `m x`. The §7.2 extra-process contribution `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`. Lasting level change via that extra process requires `ε < 0`. Precisely `ε = 0` causes computational problems in the printed specification. The extra-process observed mean `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a))` is not `τ + λ μ_t`, not `τ + λ(μ_t + m x)`, not the contribution, and not the evolved-plus-contribution latent mean. The extra process has `LAMBDA` 0 and is not an observed indicator. `T0TDPREDEFFECT` on the extra process uses `Δt = t − t0` for both the original-process evolution and the extra drive. `TDPREDEFFECT` after `t0` uses `t − u` with `t0 < u < t` for the extra drive while `μ_t` still uses `Δt`. The after-t0 extra-process observed mean is not the first-occasion extra-process observed mean when `u ≠ t0`. The impulse-carry `e^{a(t−u)} m x` is a Dirac on the original process and is not that `DRIFT` drive. The §7.2 `asymTIPREDEFFECT` `-B z / a` is the expected total change in process means given a time-independent predictor. It is not the coefficient `B`, not the finite-interval increment `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`. Lasting asymptotic change via that map requires `a < 0`. The §7.2 `addedTIPREDVAR` `(B / a)² v` is the stable between-subject variance accounted for by that predictor. It is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`. Table 2 `asymCINT` `-κ / a` is the intercept contribution to the stationary process mean. It is not `κ`, not the finite-interval increment `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`. Lasting asymptotic intercept change via that map requires `a < 0`. Page 16 notes that a `T0MEANS` stationarity constraint includes time-independent predictors; that composition is not this intercept-only map. The p. 16 constrained first-occasion mean `-κ / a + −B z / a` is not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean. Equation 5 of that constrained mean `τ + λ(−κ / a + −B z / a)` is not `τ + λ μ_0`, not `τ + λ(−κ / a)` when `B z ≠ 0`, not `τ + λ μ_t`, not `MANIFESTMEANS`, and not the constrained latent mean. The p. 16 constrained first-occasion variance `trait + −q / (2 a) + (B / a)² v` is not free `T0VAR`, not `asymDIFFUSION` alone, not `TRAITVAR` alone, not `addedTIPREDVAR` alone, and not the finite-interval discrete latent variance. Equation 5 of that constrained variance `λ²(trait + −q / (2 a) + (B / a)² v) + θ + ψ` is not `λ² p_0 + θ`, not `λ²(−q / (2 a)) + θ` when `TRAITVAR` or `addedTIPREDVAR` is nonzero, not `λ² Var(η_t) + θ` when the first occasion is constrained, not `MANIFESTVAR`, and not the constrained latent variance. The lagged covariance of that constrained process `trait + e^{a Δt}(−q / (2 a)) + (B / a)² v` is not contemporaneous `T0VAR`, not `e^{a Δt}` of the constrained total, and not `trait + e^{a Δt} p` when `addedTIPREDVAR` is nonzero. Trait variance and `addedTIPREDVAR` do not decay with `e^{a Δt}`. Equation 5 of that lagged covariance `λ²(trait + e^{a Δt}(−q / (2 a)) + (B / a)² v) + ψ` is not `θ`, not contemporaneous `Var(y_0)`, and not the lagged latent covariance. Independent measurement error does not enter lagged observed covariance. The later-occasion variance of that constrained process `trait + e^{2 a Δt}(−q / (2 a)) + Q_Δt + (B / a)² v` equals contemporaneous `T0VAR` under stationarity and is not the lagged covariance, not `e^{2 a Δt}` of the constrained total plus `Q_Δt`, and not `Q_Δt` alone. Trait variance and `addedTIPREDVAR` do not enter `Q_Δt`. Equation 5 of that later-occasion variance `λ²(trait + e^{2 a Δt}(−q / (2 a)) + Q_Δt + (B / a)² v) + θ + ψ` is not `θ`, not lagged `cov(y_t, y_{t-1})`, and not the later-occasion latent variance. The later-occasion variance of §4.3 predetermined `T0VAR` `trait + e^{2 a Δt} p_0 + Q_Δt + (B / a)² v` is not stationary later-occasion variance when `p_0` is free, not `e^{2 a Δt}` of `trait + p_0 + (B / a)² v` plus `Q_Δt`, and not free `p_0`. Trait variance and `addedTIPREDVAR` do not enter `Q_Δt`. Setting `p_0 = −q / (2 a)` recovers the stationary later-occasion map. As `Δt → ∞` with stable `a < 0` the composition approaches contemporaneous stationary `T0VAR`. Nonzero diffusion with `a ≥ 0` is a growing process and is kept. Equation 5 of that predetermined later-occasion variance `λ²(trait + e^{2 a Δt} p_0 + Q_Δt + (B / a)² v) + θ + ψ` is not `θ`, not the predetermined later-occasion latent variance, and not stationary later-occasion observed variance when `p_0` is free. The lagged covariance of §4.3 predetermined `T0VAR` `trait + e^{a Δt} p_0 + (B / a)² v` is not stationary lagged covariance when `p_0` is free, not later-occasion variance, not `e^{a Δt}` of `trait + p_0 + (B / a)² v`, and not free `p_0`. Trait variance and `addedTIPREDVAR` do not decay with `e^{a Δt}`. Setting `p_0 = −q / (2 a)` recovers the stationary lagged map. As `Δt → ∞` with stable `a < 0` the state term vanishes. A zero-diffusion carry with `a ≥ 0` is `e^{a Δt} p_0` and is kept. Equation 5 of that predetermined lagged covariance `λ²(trait + e^{a Δt} p_0 + (B / a)² v) + ψ` is not `θ`, not the predetermined lagged latent covariance, not predetermined later observed variance, and not stationary lagged observed covariance when `p_0` is free. Independent measurement error does not enter lagged observed covariance. The first-occasion variance of §4.3 predetermined `T0VAR` `trait + p_0 + (B / a)² v` is not stationary first-occasion variance when `p_0` is free, not free `p_0`, not lagged covariance, and not later-occasion variance. Trait variance and `addedTIPREDVAR` do not decay and do not enter `Q_Δt`. Setting `p_0 = −q / (2 a)` recovers the stationary first-occasion map. As `Δt → 0+` the lagged and later maps approach this composition. Trait-only variance does not require a stable drift. Equation 5 of that predetermined first-occasion variance `λ²(trait + p_0 + (B / a)² v) + θ + ψ` is not `θ`, not the predetermined first-occasion latent variance, not stationary first-occasion observed variance, and not predetermined later observed variance when `p_0` is free. The later-start lagged covariance of §4.3 predetermined `T0VAR` `trait + e^{a s}(e^{2 a u} p_0 + Q_u) + (B / a)² v` is not first-occasion lagged covariance when `u > 0`, not later-occasion variance, not stationary lagged covariance when `p_0` is free, and not `e^{a s}` of the later total. Trait variance and `addedTIPREDVAR` do not decay with `e^{a s}`. Setting `p_0 = −q / (2 a)` recovers the stationary lagged map. As `u → 0+` the composition approaches first-occasion lagged covariance. As `s → 0+` the composition approaches later-occasion variance at `u`. Nonzero diffusion with `a ≥ 0` is a growing process and is kept. Equation 5 of that later-start lagged covariance `λ²(trait + e^{a s}(e^{2 a u} p_0 + Q_u) + (B / a)² v) + ψ` is not `θ`, not the later-start lagged latent covariance, not first-occasion lagged observed covariance, not predetermined later observed variance, and not stationary lagged observed covariance when `p_0` is free. Independent measurement error does not enter lagged observed covariance. The later-start later-occasion variance of §4.3 predetermined `T0VAR` `trait + e^{2 a s}(e^{2 a u} p_0 + Q_u) + Q_s + (B / a)² v` is not later-occasion variance at `u` when `s > 0`, not later-start lagged covariance, not stationary later-occasion variance when `p_0` is free, not `e^{2 a s}` of the later total plus `Q_s`, and not later-occasion variance over the lag interval alone when `u > 0`. Trait variance and `addedTIPREDVAR` do not enter `Q_s`. Chapman–Kolmogorov writes `Q_{u+s} = e^{2 a s} Q_u + Q_s`. Setting `p_0 = −q / (2 a)` recovers the stationary later-occasion map. As `u → 0+` the composition approaches later-occasion variance over `s`. As `s → 0+` the composition approaches later-occasion variance at `u`. Nonzero diffusion with `a ≥ 0` is a growing process and is kept. Equation 5 of that later-start later-occasion variance `λ²(trait + e^{2 a s}(e^{2 a u} p_0 + Q_u) + Q_s + (B / a)² v) + θ + ψ` is not `θ`, not the later-start later-occasion latent variance, not predetermined later observed variance, not later-start lagged observed covariance, and not stationary later-occasion observed variance when `p_0` is free. Page 16 `discreteDRIFTstd` is `e^{a Δt}` after strictly positive `asymDIFFUSION` `-q / (2 a)` (footnote 4: standardise `DRIFT` using only within-subject variance, not the total). Unstandardised `e^{a Δt}` is defined for growing `a ≥ 0` and for zero diffusion and is not `discreteDRIFTstd`. The §7.1 trait-plus-state autocorrelation `(trait + e^{a Δt} p + added) / (trait + p + added)` uses `TRAITVAR` and is not `discreteDRIFTstd`. `TRAITVAR` is not the standardisation variance. Page 16 `discreteDIFFUSIONstd` is `Q_Δt / (−q / (2 a))` after strictly positive `asymDIFFUSION` `-q / (2 a)` (footnote 4: standardise process noise using only within-subject variance, not the total). Unstandardised `Q_Δt` is defined for growing `a ≥ 0` and for zero diffusion and is not `discreteDIFFUSIONstd`. The continuous standardisation `q / (−q / (2 a)) = −2 a` is not `discreteDIFFUSIONstd`. `Q_Δt / (trait + p + added)` uses `TRAITVAR` and is not `discreteDIFFUSIONstd`. `TRAITVAR` is not the standardisation variance. Page 16 `DIFFUSIONstd` is `q / (−q / (2 a))` after strictly positive `asymDIFFUSION` `-q / (2 a)` (footnote 4: standardise process noise using only within-subject variance, not the total). Unstandardised `q` is defined for growing `a ≥ 0` and for zero diffusion and is not `DIFFUSIONstd`. The discrete standardisation `Q_Δt / (−q / (2 a)) = 1 − exp(2 a Δt)` depends on the event interval and is not `DIFFUSIONstd`. `q / (trait + p + added)` uses `TRAITVAR` and is not `DIFFUSIONstd`. `TRAITVAR` is not the standardisation variance. Page 16 `DRIFTstd` is the continuous auto-effect after strictly positive `asymDIFFUSION` `-q / (2 a)` (footnote 4: standardise `DRIFT` using only within-subject variance, not the total). Unstandardised `a` is defined for growing `a ≥ 0` and for zero diffusion and is not `DRIFTstd`. The discrete standardisation `e^{a Δt}` depends on the event interval and is not `DRIFTstd`. `a p / (trait + p + added)` uses `TRAITVAR` and is not `DRIFTstd`. `TRAITVAR` is not the standardisation variance. Page 16 `asymTIPREDEFFECTstd` is `(-B / a) · √v / √(-q / (2 a))` after strictly positive `asymDIFFUSION` `-q / (2 a)` and strictly positive predictor variance `v` (footnote 4: standardise using only the relevant variance, not the total; the affecting variance is `TIPREDVAR`; the affected variance is `asymDIFFUSION`). Unstandardised `-B / a` is defined for a zero coefficient and for zero predictor variance and is not `asymTIPREDEFFECTstd`. The finite-interval standardisation `A^{-1}[e^{A Δt} − I] B · √v / √p` depends on the event interval and is not `asymTIPREDEFFECTstd`. `(-B / a) · √v / √(trait + p + added)` uses `TRAITVAR` and is not `asymTIPREDEFFECTstd`. `TRAITVAR` is not the standardisation variance. Page 16 `TIPREDEFFECTstd` is `B · √v / √(-q / (2 a))` after strictly positive `asymDIFFUSION` `-q / (2 a)` and strictly positive predictor variance `v` (footnote 4: standardise using only the relevant variance, not the total; the affecting variance is `TIPREDVAR`; the affected variance is `asymDIFFUSION`). Unstandardised `B` is defined for a zero coefficient and for zero predictor variance and is not `TIPREDEFFECTstd`. The asymptotic standardisation `(-B / a) · √v / √p` is the total change and is not `TIPREDEFFECTstd`. The finite-interval standardisation `A^{-1}[e^{A Δt} − I] B · √v / √p` depends on the event interval and is not `TIPREDEFFECTstd`. `B · √v / √(trait + p + added)` uses `TRAITVAR` and is not `TIPREDEFFECTstd`. `TRAITVAR` is not the standardisation variance. Table 3 / p. 16 `T0TIPREDEFFECTstd` is `t0_b · √v / √p_0` after strictly positive free `T0VAR` `p_0` and strictly positive predictor variance `v` (footnote 4: standardise using only the relevant variance, not the total; the affecting variance is `TIPREDVAR`; the affected variance is free `T0VAR`, not `asymDIFFUSION`). Unstandardised `t0_b` is defined for a zero coefficient and for zero predictor variance and is not `T0TIPREDEFFECTstd`. `TIPREDEFFECTstd` `B · √v / √(-q / (2 a))` uses `asymDIFFUSION` and is not `T0TIPREDEFFECTstd`. `asymTIPREDEFFECTstd` `(-B / a) · √v / √p` is the total change and is not `T0TIPREDEFFECTstd`. `t0_b · √v / √(trait + p_0 + added)` uses `TRAITVAR` and is not `T0TIPREDEFFECTstd`. `TRAITVAR` is not the standardisation variance. 2017-era `addedT0TIPREDVAR` is `t0_b² v` (`T0TIPREDEFFECT %*% TIPREDVAR %*% t(T0TIPREDEFFECT)` immediately after `T0TIPREDEFFECTstd`). A zero coefficient or zero predictor variance is exactly zero. Free `T0TIPREDEFFECT` does not require `a < 0`. `(B / a)² v` is `addedTIPREDVAR` and is not this first-occasion map. `t0_b · √v / √p_0` is `T0TIPREDEFFECTstd` and is not this variance. Free `T0VAR` is not this extra TI variance. `TRAITVAR` is not this extra TI variance. Equation 5 of 2017-era `addedT0TIPREDVAR` is `λ² t0_b² v`. Form `t0_b² v` first, then `(λ extra) λ` with `θ = 0`. A zero loading or zero extra is exactly zero. `t0_b² v` is the latent extra, not the observed extra. `λ² p_0 + θ` is first-occasion observed variance, not this extra. `λ² (B / a)² v` is Eq. 5 of `addedTIPREDVAR`, not this first-occasion observed extra. `MANIFESTVAR` `θ` is not this extra. Equation 5 of §7.2 `addedTIPREDVAR` is `λ² (B / a)² v`. Form `(B / a)² v` first, then `(λ extra) λ` with `θ = 0`. A zero loading or zero extra is exactly zero. Lasting asymptotic extra requires `a < 0`. `(B / a)² v` is the latent extra, not the observed extra. `λ² t0_b² v` is first-occasion extra observed TI variance, not this extra. `λ² p + θ` is stationary observed variance, not this extra. `MANIFESTVAR` `θ` is not this extra. Page 16 `TDPREDEFFECTstd` is `m · √v / √(-q / (2 a))` after strictly positive `asymDIFFUSION` `-q / (2 a)` and strictly positive time-dependent predictor variance `v` (footnote 4: standardise using only the relevant variance, not the total; the affecting variance is the TD predictor; the affected variance is `asymDIFFUSION`). Unstandardised `M` is defined for a zero coefficient and for zero predictor variance and is not `TDPREDEFFECTstd`. `TIPREDEFFECTstd` `B · √v / √p` is a different named matrix even when `M = B`. The finite-interval intercept-style standardisation `A^{-1}[e^{A Δt} − I] M · √v / √p` depends on the event interval and is not `TDPREDEFFECTstd`. `m · √v / √(trait + p + added)` uses `TRAITVAR` and is not `TDPREDEFFECTstd`. `TRAITVAR` is not the standardisation variance. Table 3 / p. 16 `T0TDPREDEFFECTstd` is `t0_m · √v / √p_0` after strictly positive free `T0VAR` `p_0` and strictly positive time-dependent predictor variance `v` (footnote 4: standardise using only the relevant variance, not the total; the affecting variance is the TD predictor, not `TIPREDVAR`; the affected variance is free `T0VAR`, not `asymDIFFUSION`). Unstandardised `t0_m` is defined for a zero coefficient and for zero predictor variance and is not `T0TDPREDEFFECTstd`. `TDPREDEFFECTstd` `m · √v / √(-q / (2 a))` uses `asymDIFFUSION` and is not `T0TDPREDEFFECTstd`. `T0TIPREDEFFECTstd` `t0_b · √v / √p_0` is a different named matrix even when `t0_m = t0_b`. `t0_m · √v / √(trait + p_0 + added)` uses `TRAITVAR` and is not `T0TDPREDEFFECTstd`. `TRAITVAR` is not the standardisation variance. Free `T0VAR` does not require `a < 0`. Page 16 `T0VARstd` is the correlation form `solve(sqrt(diag(T0VAR))) %&% T0VAR` after strictly positive free `T0VAR` `p_0` (2017-era `summary.ctsemFit.R`; OpenMx `%&%` is `t(A) %*% B %*% A`; the default ridge is 0; the scalar map is `p_0 / p_0 = 1`). Unstandardised `T0VAR` is defined for a zero first-occasion variance and is not `T0VARstd`. Zero `p_0` fails closed. Distinct positive `p_0` recover the same 1. `T0TDPREDEFFECTstd` `t0_m · √v / √p_0` depends on `p_0` and is not `T0VARstd`. `addedT0TIPREDVAR` `t0_b² v` is extra TI variance, not this correlation. `TRAITVAR` is not the standardisation variance. Page 16 `TRAITVARstd` is the correlation form `solve(sqrt(diag(TRAITVAR))) %&% TRAITVAR` after strictly positive `TRAITVAR` (2017-era `summary.ctsemFit.R` forms it only when `TRAITVAR != 0`; OpenMx `%&%` is `t(A) %*% B %*% A`; unlike `T0VARstd` there is no ridge addend; the scalar map is `trait / trait = 1`). Unstandardised `TRAITVAR` is defined for a zero trait and is not `TRAITVARstd`. Zero `TRAITVAR` fails closed. Distinct positive `trait` recover the same 1. `T0VARstd` `p_0 / p_0 = 1` recovers the same number and remains a distinct named quantity. `addedT0TIPREDVAR` `t0_b² v` is extra TI variance, not this correlation. `TRAITVAR` does not require `a < 0`. Page 16 `MANIFESTTRAITVARstd` is the correlation form `solve(sqrt(diag(MANIFESTTRAITVAR))) %&% MANIFESTTRAITVAR` after strictly positive `MANIFESTTRAITVAR` (2017-era `summary.ctsemFit.R` forms it only when `MANIFESTTRAITVAR != 0`; OpenMx `%&%` is `t(A) %*% B %*% A`; unlike `TRAITVARstd` the 2017-era source adds ridging; the default ridge is 0; the scalar map is `ψ / ψ = 1`). Unstandardised `MANIFESTTRAITVAR` is defined for a zero trait and is not `MANIFESTTRAITVARstd`. Zero `MANIFESTTRAITVAR` fails closed. Distinct positive `ψ` recover the same 1. `TRAITVARstd` `trait / trait = 1` recovers the same number and remains a distinct named quantity. `MANIFESTVAR` `θ` is measurement error, not this correlation. `MANIFESTTRAITVAR` does not require `a < 0`. Page 16 `MANIFESTVARstd` is the correlation form `solve(sqrt(diag(MANIFESTVAR))) %&% MANIFESTVAR` after strictly positive `MANIFESTVAR` (2017-era `summary.ctsemFit.R` forms it whenever `verbose = TRUE`; OpenMx `%&%` is `t(A) %*% B %*% A`; unlike `TRAITVARstd` the 2017-era source adds ridging; the default ridge is 0; the 2017-era `dimnames` assignment to `latentNames` is a source bug; the scalar map is `θ / θ = 1`). Unstandardised `MANIFESTVAR` is defined for a zero residual and is not `MANIFESTVARstd`. Zero `MANIFESTVAR` makes `solve(sqrt(0))` fail and fails closed. Distinct positive `θ` recover the same 1. `MANIFESTTRAITVARstd` `ψ / ψ = 1` recovers the same number and remains a distinct named quantity. Equation 5 `λ² Var(η) + θ` is `Var(y)`, not this correlation. `MANIFESTVAR` does not require `a < 0`. Page 16 `TIPREDVARstd` is the correlation form `solve(sqrt(diag(TIPREDVAR))) %&% TIPREDVAR` after strictly positive `TIPREDVAR` (2017-era `summary.ctsemFit.R` forms it whenever `verbose = TRUE` and `n.TIpred > 0`; OpenMx `%&%` is `t(A) %*% B %*% A`; unlike `TRAITVARstd` the 2017-era source adds ridging; the default ridge is 0; `dimnames` are `TIpredNames`; the scalar map is `v / v = 1`). Unstandardised `TIPREDVAR` is defined for a zero predictor and is not `TIPREDVARstd`. Zero `TIPREDVAR` makes `solve(sqrt(0))` fail and fails closed. Distinct positive `v` recover the same 1. `MANIFESTVARstd` `θ / θ = 1` recovers the same number and remains a distinct named quantity. Section 7.2 `addedTIPREDVAR` `(B / a)² v` is extra process variance, not this correlation. `TIPREDVAR` does not require `a < 0`. Page 16 `asymDIFFUSIONstd` is the correlation form `solve(sqrt(diag(asymDIFFUSION))) %&% asymDIFFUSION` after strictly positive `asymDIFFUSION` `-q / (2 a)` (2017-era `summary.ctsemFit.R` forms it whenever `verbose = TRUE`; OpenMx `%&%` is `t(A) %*% B %*% A`; the 2017-era source adds ridging; the default ridge is 0; `dimnames` are `latentNames`; the scalar map is `p / p = 1`). Unstandardised `asymDIFFUSION` is defined for a zero process and is not `asymDIFFUSIONstd`. Zero `q` makes `solve(sqrt(0))` fail and fails closed. Distinct positive `p` recover the same 1. `TIPREDVARstd` `v / v = 1` recovers the same number and remains a distinct named quantity. `DIFFUSIONstd` `q / p = −2 a` is the continuous-diffusion ratio, not this correlation. Lasting `asymDIFFUSION` requires `a < 0`. Page 16 `discreteCINTstd` is `A^{-1}[e^{A Δt} − I] κ / √p` after strictly positive `asymDIFFUSION` `-q / (2 a)` (2017-era `summary.ctsemFit.R` forms unstandardised `discreteCINT` whenever `verbose = TRUE` as `solve(DRIFT) %*% (discreteDRIFT − I) %*% CINT`; that source does not form a `discreteCINTstd` matrix; the scalar map is the footnote 4 standardisation of that named discrete intercept). Unstandardised `discreteCINT` is defined for growing `a ≥ 0` and for zero diffusion and is not `discreteCINTstd`. Zero `q` has no positive process SD and fails closed. `κ / √p` does not depend on `Δt` and is not this finite-interval map. `(-κ / a) / √p` is the standardised asymptotic intercept and is not this map. Lasting `asymDIFFUSION` requires `a < 0`. Page 16 `asymCINTstd` is `(-κ / a) / √p` after strictly positive `asymDIFFUSION` `-q / (2 a)` (2017-era `summary.ctsemFit.R` forms unstandardised `asymCINT` whenever `verbose = TRUE` as `-solve(DRIFT) %*% CINT`; that source does not form an `asymCINTstd` matrix; the scalar map is the footnote 4 standardisation of that named asymptotic intercept). Unstandardised `asymCINT` is defined for a zero process and is not `asymCINTstd`. Zero `q` has no positive process SD and fails closed. `κ / √p` is the continuous intercept standardisation and is not this total-change map. `A^{-1}[e^{A Δt} − I] κ / √p` depends on the event interval and is not this `Δt → ∞` map. Lasting `asymDIFFUSION` requires `a < 0`. Page 16 `T0MEANSstd` is `μ_0 / √p_0` after strictly positive free `T0VAR` `p_0` (2017-era `summary.ctsemFit.R` forms unstandardised `T0MEANS` and does not form a `T0MEANSstd` matrix; the scalar map is the footnote 4 standardisation of that named first-occasion mean; relevant variance is free `T0VAR`, not `asymDIFFUSION`). Unstandardised `T0MEANS` is defined for a zero first-occasion variance and is not `T0MEANSstd`. Zero `p_0` has no positive SD and fails closed. `T0VARstd` `p_0 / p_0 = 1` recovers the same number when `μ_0 = √p_0` and remains a distinct named quantity. `μ_0 / √asymDIFFUSION` uses process-dynamics variance and is not this first-occasion map. Free `T0MEANS` does not require `a < 0`. ## Authoritative sources @@ -74,7 +126,7 @@ Kish, L. (1965). *Survey sampling*. John Wiley & Sons. Oud, J. H. L., & Jansen, R. A. R. G. (2000). Continuous time state space modeling of panel data by means of SEM. *Psychometrika, 65*(2), 199–215. https://doi.org/10.1007/BF02294374 (cited by Voelkle et al., 2012, Eq. 14 discussion; PDF not opened). -The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:07Z (https://www.zora.uzh.ch/handle/20.500.14742/72792; bitstream `424f9082-0eeb-4a67-b687-9845a4ed892f`). Page 16 writes discrete auto-effects as \(A^{*}(\Delta t)=\exp(A\Delta t)\) (Eq. 7). Introducing Intercepts (manuscript p. 20, Eq. 12) adds a continuous-time intercept \(b\) and writes the expected-value solution whose discrete increment is \(A^{-1}(\exp(A\Delta t)-I)b\); the scalar constant-predictor effect is \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). The next paragraph (manuscript p. 21, Eq. 14) writes the discrete effect of a **time-varying** predictor, when the sampling interval equals the constancy interval, as \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product does not depend on the predictor auto-effect. The manuscript calls Eq. 14 a first-order approximation that deteriorates as \(\Delta t\) grows, and defers unmatched sampling/constancy intervals to Oud and Jansen (2000), which is unread. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4; JSS PDF opened 2026-08-21T13:08Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\), the discrete intercept \(b_{\Delta t}=A^{-1}[A_{\Delta t}-I]b\), and the discrete process-noise covariance \(Q_{\Delta t}=\int_{0}^{\Delta t}\operatorname{expm}(A(\Delta t-\tau))LGG^{\top}L^{\top}\operatorname{expm}(A(\Delta t-\tau))^{\top}\,d\tau\). Equation 4 (p. 5) writes that the integral exhibits covariance \(Q_{\Delta t}=\operatorname{irow}(A^{\#^{-1}}[e^{A^{\#}\Delta t}-I]\operatorname{row}(Q))\) with \(A^{\#}=A\otimes I+I\otimes A\). The homogeneous-process consequence (`ξ`, `z` given) is \(Q_{\Delta t}=\operatorname{cov}(\eta_{ti}\mid\eta_{t-1,i})\) and \(\operatorname{cov}(\eta_{ti},\eta_{t-1,i})=A_{\Delta t}\operatorname{cov}(\eta_{t-1,i})\). The law of total variance on that pair is \(\operatorname{Var}(\eta_{ti})=A_{\Delta t}\operatorname{Var}(\eta_{t-1,i})A_{\Delta t}^{\top}+Q_{\Delta t}\). As \(\Delta t\to\infty\) with stable \(a<0\), Eq. 4 becomes \(-q/(2a)\). The JSS summary names that limit `asymDIFFUSION` and takes it as the total within-subject variance (p. 16). Section 4.3 (pp. 9–10) constrains a stationary `T0VAR` to that model-predicted variance and distinguishes it from a predetermined first occasion. The same section (p. 9) adds a stable trait process with `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is that time-invariant between-subject variance. Table 3 (p. 13) names `T0TIPREDEFFECT` the effect of time-independent predictors on latents at `T0` and names `TIPREDEFFECT` `B` separately. The JSS article has no numbered §2.2 (2.1 is Continuous time and SEM; §3 follows). This slice takes scalar \(L=1\) (every latent subject to system noise); it does not implement the 0/1 selector matrix and is not a Kalman filter. Discrete auto-effects are strictly positive for finite real drift and interval; the inverse \(a=\ln\varphi/\Delta t\) therefore requires \(\varphi>0\). Binary64 `exp` of a large negative argument is `+0` and is refused as a discrete lag; the same underflow is a vanishing lagged covariance and is kept. When `exp` of a finite `a Δt` overflows on the Table 3 first-summand carry `e^{A Δt} t0_b z` / `e^{A Δt} t0_m x0`, rewrite as `sign(shift) exp(ln|shift| + a Δt)`; an overflowing `a Δt` product fails closed. Integration tests execute those overflow-rewrite arms on the non-`cfg(test)` instantiation (nightly branch coverage on #49 head `d634f58` was 1718/1720 at those two `if !drift_interval.is_finite()` sites). Equations 3–4 of Voelkle et al. (2012) are the discouraged difference-quotient approximation. Meredith (1993) remains unread (Unpaywall/OpenAlex 2026-08-22T23:12Z: `is_oa: false`; Springer `content/pdf` is HTML 200, not a PDF; archive.org title search empty). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall/OpenAlex 2026-08-22T23:12Z: `is_oa: false`; Springer `content/pdf` is HTML 200; ETS landing page is HTML; ETS RR-88-45 PDF 404; Wiley PDF 403). ERIC ED334221 is Singer and Willett (1991), *From whether to when*, not the 1991 journal article. ERIC ED333032 is Mislevy, Sheehan, and Wingersky (1990), ETS RR-90-17-ONR, not the 1991 journal article. The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title is not the 1991 journal article. Oud and Jansen (2000) remains unread (Unpaywall/OpenAlex 2026-08-18T21:07Z: closed; Radboud landing and bitstream 403). +The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:07Z (https://www.zora.uzh.ch/handle/20.500.14742/72792; bitstream `424f9082-0eeb-4a67-b687-9845a4ed892f`). Page 16 writes discrete auto-effects as \(A^{*}(\Delta t)=\exp(A\Delta t)\) (Eq. 7). Introducing Intercepts (manuscript p. 20, Eq. 12) adds a continuous-time intercept \(b\) and writes the expected-value solution whose discrete increment is \(A^{-1}(\exp(A\Delta t)-I)b\); the scalar constant-predictor effect is \(b^{*}_{y.x}(\Delta t)=(a_{yx}/a_{xx})(\exp(a_{xx}\Delta t)-1)\). The next paragraph (manuscript p. 21, Eq. 14) writes the discrete effect of a **time-varying** predictor, when the sampling interval equals the constancy interval, as \(b^{*}_{y.x}(\Delta t)=a_{yx}\Delta t\). That product does not depend on the predictor auto-effect. The manuscript calls Eq. 14 a first-order approximation that deteriorates as \(\Delta t\) grows, and defers unmatched sampling/constancy intervals to Oud and Jansen (2000), which is unread. Driver, Oud, and Voelkle (2017, Eq. 3 and p. 4; JSS PDF opened 2026-08-21T13:08Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) write \(A_{\Delta t}=\operatorname{expm}(A\Delta t)\), the discrete intercept \(b_{\Delta t}=A^{-1}[A_{\Delta t}-I]b\), and the discrete process-noise covariance \(Q_{\Delta t}=\int_{0}^{\Delta t}\operatorname{expm}(A(\Delta t-\tau))LGG^{\top}L^{\top}\operatorname{expm}(A(\Delta t-\tau))^{\top}\,d\tau\). Equation 4 (p. 5) writes that the integral exhibits covariance \(Q_{\Delta t}=\operatorname{irow}(A^{\#^{-1}}[e^{A^{\#}\Delta t}-I]\operatorname{row}(Q))\) with \(A^{\#}=A\otimes I+I\otimes A\). The homogeneous-process consequence (`ξ`, `z` given) is \(Q_{\Delta t}=\operatorname{cov}(\eta_{ti}\mid\eta_{t-1,i})\) and \(\operatorname{cov}(\eta_{ti},\eta_{t-1,i})=A_{\Delta t}\operatorname{cov}(\eta_{t-1,i})\). The law of total variance on that pair is \(\operatorname{Var}(\eta_{ti})=A_{\Delta t}\operatorname{Var}(\eta_{t-1,i})A_{\Delta t}^{\top}+Q_{\Delta t}\). As \(\Delta t\to\infty\) with stable \(a<0\), Eq. 4 becomes \(-q/(2a)\). The JSS summary names that limit `asymDIFFUSION` and takes it as the total within-subject variance (p. 16). Section 4.3 (pp. 9–10) constrains a stationary `T0VAR` to that model-predicted variance and distinguishes it from a predetermined first occasion. The later-occasion variance of that predetermined first occasion is `trait + e^{2 a Δt} p_0 + Q_Δt + (B / a)² v` (JSS PDF re-opened 2026-08-23T05:12Z). The lagged covariance of that predetermined first occasion is `trait + e^{a Δt} p_0 + (B / a)² v` (JSS PDF re-opened 2026-08-23T09:04Z). The same section (p. 9) adds a stable trait process with `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is that time-invariant between-subject variance. Table 3 (p. 13) names `T0TIPREDEFFECT` the effect of time-independent predictors on latents at `T0` and names `TIPREDEFFECT` `B` separately. The JSS article has no numbered §2.2 (2.1 is Continuous time and SEM; §3 follows). This slice takes scalar \(L=1\) (every latent subject to system noise); it does not implement the 0/1 selector matrix and is not a Kalman filter. Discrete auto-effects are strictly positive for finite real drift and interval; the inverse \(a=\ln\varphi/\Delta t\) therefore requires \(\varphi>0\). Binary64 `exp` of a large negative argument is `+0` and is refused as a discrete lag; the same underflow is a vanishing lagged covariance and is kept. When `exp` of a finite `a Δt` overflows on the Table 3 first-summand carry `e^{A Δt} t0_b z` / `e^{A Δt} t0_m x0`, rewrite as `sign(shift) exp(ln|shift| + a Δt)`; an overflowing `a Δt` product fails closed. Integration tests execute those overflow-rewrite arms on the non-`cfg(test)` instantiation (nightly branch coverage on #49 head `d634f58` was 1718/1720 at those two `if !drift_interval.is_finite()` sites). Equations 3–4 of Voelkle et al. (2012) are the discouraged difference-quotient approximation. Meredith (1993) remains unread (Unpaywall 2026-08-24T06:36Z: `is_oa: false`; title *Measurement Invariance, Factor Analysis and Factorial Invariance*; Springer `content/pdf` is HTML 200, not a PDF). Mislevy (1991, *Psychometrika, 56*, 177–196, DOI 10.1007/bf02294457) remains unread (Unpaywall 2026-08-24T06:36Z: `is_oa: false`; title *Randomization-Based Inference about Latent Variables from Complex Samples*; Springer `content/pdf` is HTML 200; ETS landing page is HTML; ETS RR-91-21 PDF 404; Wiley PDF 403). ERIC ED334221 is Singer and Willett (1991), *From whether to when*, not the 1991 journal article. ERIC ED333032 is Mislevy, Sheehan, and Wingersky (1990), ETS RR-90-17-ONR, not the 1991 journal article. The 1988 ETS RR-88-45 / DTIC ADA200179 technical report of the same title is not the 1991 journal article. Oud and Jansen (2000) remains unread (Unpaywall/OpenAlex 2026-08-18T21:07Z: closed; Radboud landing and bitstream 403). ## Formula notes @@ -118,9 +170,14 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - **Lagged stationary observed covariance.** Driver et al. (2017, Eq. 5, p. 5; Eq. 3–4, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-22T19:13Z): independent `ε_t` does not enter `cov(y_t,y_{t-1})`. The scalar composition is `λ²(trait + e^{aΔt}(−q/(2a)) + (B/a)²v) + ψ`. Form the lagged latent covariance first, then `λ²c+ψ`. A zero loading is exactly `ψ`. A zero trait, a zero diffusion, and a zero TI contribution is exactly `ψ`. `MANIFESTVAR` is not this composition. Contemporaneous `Var(y_0)` includes `θ` and is not this composition. The lagged latent covariance is not this observed covariance. An overflowing product or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **Later-occasion stationary latent variance.** Driver et al. (2017, §4.3, pp. 9–10; Eq. 3–4, pp. 4–5; Table 2, p. 12; p. 16; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-22T23:12Z): the unconditional variance at a later event occasion of the constrained process is `trait + e^{2aΔt}(−q/(2a)) + Q_Δt + (B/a)²v`. Form the evolved within-subject variance `e^{2aΔt}(−q/(2a))+Q_Δt` first, then include the trait, then include the TI extra variance, then add. Trait variance and `addedTIPREDVAR` do not enter `Q_Δt`. Under stationarity `e^{2aΔt}p+Q_Δt=p`, so this composition equals contemporaneous `T0VAR`. Evolving the constrained total as if it were all state is not this map. The lagged covariance omits `Q_Δt` and is not this map. `Q_Δt` is not this map. The interval must be event time and strictly positive. An overflowing product or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **Later-occasion stationary observed variance.** Driver et al. (2017, Eq. 5, p. 5; Eq. 3–4, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-22T23:12Z): `Var(y_t)=λ²(trait + e^{2aΔt}(−q/(2a)) + Q_Δt + (B/a)²v) + θ + ψ`. Form the later-occasion latent variance first, then `λ²p+θ+ψ`. A zero loading is exactly `θ+ψ`. A zero trait, a zero diffusion, and a zero TI contribution is exactly `θ+ψ`. Under stationarity that composition equals contemporaneous `Var(y_0)`. The lagged observed covariance omits `Q_Δt` and `θ`. `MANIFESTVAR` is not this composition. The later-occasion latent variance is not this observed variance. An overflowing product or sum fails closed. This is not a Kalman filter and not ctsem estimation. +- **Later-occasion predetermined latent variance.** Driver et al. (2017, §4.3, pp. 9–10; Eq. 3–4, pp. 4–5; Table 2, p. 12; p. 16; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-23T05:12Z): the first time point is predetermined when no assumptions are made about the process prior to the initial time point. Free `T0VAR` `p_0` is then estimated. The unconditional variance at a later event occasion is `trait + e^{2aΔt} p_0 + Q_Δt + (B/a)²v`. Form the evolved free first-occasion variance `e^{2aΔt}p_0+Q_Δt` first, then include the trait, then include the TI extra variance, then add. Trait variance and `addedTIPREDVAR` do not enter `Q_Δt`. Setting `p_0=−q/(2a)` recovers the stationary later-occasion map. Stationary later variance uses `−q/(2a)` in place of `p_0` and is not this map when `p_0` is free. Evolving `trait+p_0+(B/a)²v` as if it were all state is not this map. Free `p_0` is not this map. As `Δt→∞` with stable `a<0` the carried `p_0` vanishes and `Q_Δt` approaches `−q/(2a)`, so the composition approaches contemporaneous stationary `T0VAR`. As `Δt→0+` the composition approaches `trait+p_0+(B/a)²v`. Nonzero diffusion with `a≥0` is a growing process and is kept. The interval must be event time and strictly positive. An overflowing product or sum fails closed. This is not a Kalman filter and not ctsem estimation. +- **Later-occasion predetermined observed variance.** Driver et al. (2017, Eq. 5, p. 5; Eq. 3–4, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-23T05:12Z): `Var(y_t)=λ²(trait + e^{2aΔt} p_0 + Q_Δt + (B/a)²v) + θ + ψ`. Form the predetermined later-occasion latent variance first, then `λ²p+θ+ψ`. A zero loading is exactly `θ+ψ`. A zero trait, a zero initial variance, a zero diffusion, and a zero TI contribution is exactly `θ+ψ`. Setting `p_0=−q/(2a)` recovers the stationary later-occasion observed variance. Stationary later observed variance is not this composition when `p_0` is free. `MANIFESTVAR` is not this composition. The predetermined later-occasion latent variance is not this observed variance. An overflowing product or sum fails closed. This is not a Kalman filter and not ctsem estimation. +- **Lagged predetermined latent covariance.** Driver et al. (2017, §4.3, pp. 9–10; Eq. 3–4, pp. 4–5; Table 2, p. 12; p. 16; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-23T09:04Z): the first time point is predetermined when no assumptions are made about the process prior to the initial time point. Free `T0VAR` `p_0` is then estimated. The auto-covariance at a strictly positive event interval is `trait + e^{aΔt} p_0 + (B/a)²v`. Form the lagged free first-occasion covariance `e^{aΔt}p_0` first, then include the trait, then include the TI extra variance, then add. Trait variance and `addedTIPREDVAR` do not decay. Setting `p_0=−q/(2a)` recovers the stationary lagged map. Stationary lagged covariance uses `−q/(2a)` in place of `p_0` and is not this map when `p_0` is free. Evolving `trait+p_0+(B/a)²v` as if it were all state is not this map. Free `p_0` is not this map. Later-occasion variance includes `Q_Δt` and is not this map. As `Δt→∞` with stable `a<0` the state term vanishes. As `Δt→0+` the composition approaches `trait+p_0+(B/a)²v`. A zero-diffusion carry with `a≥0` is `e^{aΔt}p_0` and is kept. The interval must be event time and strictly positive. An overflowing product or sum fails closed. This is not a Kalman filter and not ctsem estimation. +- **Lagged predetermined observed covariance.** Driver et al. (2017, Eq. 5, p. 5; Eq. 3–4, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-23T09:04Z): independent `ε_t` does not enter `cov(y_t,y_{t-1})`. The scalar composition is `λ²(trait + e^{aΔt} p_0 + (B/a)²v) + ψ`. Form the predetermined lagged latent covariance first, then `λ²c+ψ`. A zero loading is exactly `ψ`. A zero trait, a zero initial variance, and a zero TI contribution is exactly `ψ`. Setting `p_0=−q/(2a)` recovers the stationary lagged observed covariance. Stationary lagged observed covariance is not this composition when `p_0` is free. `MANIFESTVAR` is not this composition. The predetermined lagged latent covariance is not this observed covariance. Predetermined later observed variance includes `Q_Δt` and `θ` and is not this composition. An overflowing product or sum fails closed. This is not a Kalman filter and not ctsem estimation. - **Level-change discrete increment.** Driver et al. (2017, §7.2, pp. 20–21; Eq. 3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T19:50Z): Equation 3 maps `CINT` through `A^{-1}[e^{AΔt}−I]κ`. With `κ=−a m x` the scalar increment is `(e^{aΔt}−1)/a·(−a m x)=(1−e^{aΔt})m x`. Form the level-change `CINT` first, then the discrete intercept map. Underflow of `e^{aΔt}` to `+0` keeps `m x`. A zero effect or zero predictor is exactly zero. `(1−e^{aΔt})m x` is not `m x`, not `κ`, and not `A^{-1}[e^{AΔt}−I]Bz`. An overflowing product or increment fails closed. This is not a Kalman filter and not ctsem estimation. - **CWC-then-lag.** Sample cluster means are removed first. Consecutive within residuals are then fitted by least squares to \(r_{t+\Delta t}\approx\exp(a\Delta t)\,r_{t}\) on event time. Same-sign pair-wise logs initialize the scalar Newton step. Sign-flipping \(T=2\) CWC pairs have no real logarithm and fail closed. Curran and Bauer (2011, pp. 607–608) show that this person-mean subtraction on a raw autoregressive series does **not** isolate the lagged within-person effect; the helper therefore does not claim to recover the raw-process drift. - **Already-centered irregular residual.** The caller supplies lagged within residuals. The mean of \(a=\ln(r_{t+\Delta t}/r_t)/\Delta t\) is the exact scalar map. Intervals may be irregular. The helper does not center again. This is not DSEM. +- **Standardised initial latent mean.** Driver et al. (2017, Table 2, p. 12; p. 16; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-24T22:30Z): Table 2 names `T0MEANS` the latent process means at the first time point `T0`. Footnote 4 standardises using only the relevant variance, not the total. The first-occasion relevant variance is free `T0VAR` `p_0`, not `asymDIFFUSION`. The 2017-era source forms unstandardised `T0MEANS` and does not form `T0MEANSstd`. The scalar map is `μ_0/√p_0`. Form strictly positive `p_0` first, then divide. A zero mean is exactly zero. Zero `p_0` has no positive SD and fails closed. `T0` is an event-time occasion. Free `T0MEANS` does not require `a<0`. `T0VARstd` is not this map even when both equal 1. `μ_0/√asymDIFFUSION` is not this map. An overflowing quotient fails closed. This is not a Kalman filter and not ctsem estimation. ## Verification @@ -161,12 +218,37 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - Driver et al. (2017, Table 2, p. 12; Eq. 3 as \(\Delta t\to\infty\); JSS PDF opened 2026-08-21T16:13Z) recovers a known `asymCINT` \(-\kappa/a\) at machine-scale RMSE, and that RMSE is smaller than treating `CINT`, the finite-interval increment \(A^{-1}[e^{A\Delta t}-I]\kappa\), `T0MEANS`, or `-B z / a` as that total change; a large finite \(\Delta t\) discrete increment converges on \(-\kappa/a\); a zero intercept is exactly zero even if \(a\ge 0\); \(a\ge 0\) with a nonzero intercept fails closed; a non-event clock and an overflowing quotient fail closed; - Driver et al. (2017, p. 16; Table 2, p. 12; Eq. 3; JSS PDF opened 2026-08-21T16:13Z) recovers a known stationary `T0MEANS` \(-\kappa/a + -Bz/a\) at machine-scale RMSE, and that RMSE is smaller than treating free `T0MEANS`, `asymCINT` alone, `asymTIPREDEFFECT` alone, or the finite-interval discrete latent mean as that constraint; a zero intercept and a zero TI contribution is exactly zero even if \(a\ge 0\); \(a\ge 0\) with a nonzero intercept or TI contribution fails closed; a non-event clock and an overflowing sum fail closed; - Driver et al. (2017, §4.3, pp. 9–10; Eq. 5, p. 5; JSS PDF re-opened 2026-08-21T20:07Z) recovers a known \(E(y_0)=\tau+\lambda(-\kappa/a + -Bz/a)\) at machine-scale RMSE, and that RMSE is smaller than treating \(\tau+\lambda\mu_0\), \(\tau+\lambda(-\kappa/a)\), \(\tau+\lambda\mu_t\), `MANIFESTMEANS`, or the constrained latent mean as \(E(y_0)\); a zero loading is \(\tau\); a zero intercept and a zero TI contribution is \(\tau\); evolving from that stationary start with `CINT` and `TIPREDEFFECT` stays at the stationary mean; \(a\ge 0\) with a nonzero intercept or TI contribution fails closed; a non-event clock and an overflowing product or sum fail closed; -- Driver et al. (2017, §4.3, pp. 9–10; Eq. 3–4, pp. 4–5; p. 16; JSS PDF re-opened 2026-08-22T19:13Z) recovers a known lagged stationary `T0VAR` \(\mathrm{trait}+e^{a\Delta t}(-q/(2a))+(B/a)^{2}v\) at machine-scale RMSE, and that RMSE is smaller than treating contemporaneous `T0VAR`, \(e^{a\Delta t}\) of the constrained total, or trait-plus-state lagged covariance as that lagged map; a large finite \(\Delta t\) recovers \(\mathrm{trait}+(B/a)^{2}v\); as \(\Delta t \to 0^{+}\) over strictly positive event intervals the recovered variance approaches contemporaneous `T0VAR` while remaining distinct from it at every positive \(\Delta t\); a zero trait, a zero diffusion, and a zero TI contribution is exactly zero; a zero diffusion and a zero TI contribution is exactly the trait; \(a\ge 0\) with a nonzero diffusion or TI contribution fails closed; a non-event clock, a non-positive interval, and an overflowing product or sum fail closed; +- Driver et al. (2017, §4.3, pp. 9–10; Eq. 3–4, pp. 4–5; p. 16; JSS PDF re-opened 2026-08-22T19:13Z) recovers a known lagged stationary `T0VAR` \(\mathrm{trait}+e^{a\Delta t}(-q/(2a))+(B/a)^{2}v\) at machine-scale RMSE, and that RMSE is smaller than treating contemporaneous `T0VAR`, \(e^{a\Delta t}\) of the constrained total, or trait-plus-state lagged covariance as that lagged map; a large finite \(\Delta t\) recovers \(\mathrm{trait}+(B/a)^{2}v\); a vanishing interval approaches contemporaneous `T0VAR` and remains a distinct map; a zero trait, a zero diffusion, and a zero TI contribution is exactly zero; a zero diffusion and a zero TI contribution is exactly the trait; \(a\ge 0\) with a nonzero diffusion or TI contribution fails closed; a non-event clock, a non-positive interval, and an overflowing product or sum fail closed; - Driver et al. (2017, Eq. 5 of lagged §4.3 `T0VAR`; Table 2, p. 12; JSS PDF re-opened 2026-08-22T19:13Z) recovers a known \(\operatorname{cov}(y_t,y_{t-1})=\lambda^{2}(\mathrm{trait}+e^{a\Delta t}(-q/(2a))+(B/a)^{2}v)+\psi\) at machine-scale RMSE, and that RMSE is smaller than treating `MANIFESTVAR`, contemporaneous \(\operatorname{Var}(y_0)\), or the lagged latent covariance as that observed covariance; a zero loading is \(\psi\); independent \(\varepsilon_t\) does not enter; a non-event clock, a non-positive interval, and an overflowing product or sum fail closed; -- Driver et al. (2017, §4.3, pp. 9–10; Eq. 3–4, pp. 4–5; p. 16; JSS PDF re-opened 2026-08-22T23:12Z) recovers a known later-occasion stationary `T0VAR` \(\mathrm{trait}+e^{2a\Delta t}(-q/(2a))+Q_{\Delta t}+(B/a)^{2}v\) at machine-scale RMSE, and that RMSE is smaller than treating lagged covariance, \(e^{2a\Delta t}\) of the constrained total plus \(Q_{\Delta t}\), or \(Q_{\Delta t}\) as that later map; under stationarity the recovered variance equals contemporaneous `T0VAR` exactly at every strictly positive \(\Delta t\), including a large finite one, and therefore also approaches contemporaneous `T0VAR` as \(\Delta t \to 0^{+}\) over positive event intervals; the event interval stays strictly positive and never reaches zero; a zero trait, a zero diffusion, and a zero TI contribution is exactly zero; a zero diffusion and a zero TI contribution is exactly the trait; \(a\ge 0\) with a nonzero diffusion or TI contribution fails closed; a non-event clock, a non-positive interval, and an overflowing product or sum fail closed; +- Driver et al. (2017, §4.3, pp. 9–10; Eq. 3–4, pp. 4–5; p. 16; JSS PDF re-opened 2026-08-22T23:12Z) recovers a known later-occasion stationary `T0VAR` \(\mathrm{trait}+e^{2a\Delta t}(-q/(2a))+Q_{\Delta t}+(B/a)^{2}v\) at machine-scale RMSE, and that RMSE is smaller than treating lagged covariance, \(e^{2a\Delta t}\) of the constrained total plus \(Q_{\Delta t}\), or \(Q_{\Delta t}\) as that later map; under stationarity the recovered variance equals contemporaneous `T0VAR` at both a large finite \(\Delta t\) and a vanishing interval; a zero trait, a zero diffusion, and a zero TI contribution is exactly zero; a zero diffusion and a zero TI contribution is exactly the trait; \(a\ge 0\) with a nonzero diffusion or TI contribution fails closed; a non-event clock, a non-positive interval, and an overflowing product or sum fail closed; - Driver et al. (2017, Eq. 5 of later-occasion §4.3 `T0VAR`; Table 2, p. 12; JSS PDF re-opened 2026-08-22T23:12Z) recovers a known \(\operatorname{Var}(y_t)=\lambda^{2}(\mathrm{trait}+e^{2a\Delta t}(-q/(2a))+Q_{\Delta t}+(B/a)^{2}v)+\theta+\psi\) at machine-scale RMSE, and that RMSE is smaller than treating `MANIFESTVAR`, lagged \(\operatorname{cov}(y_t,y_{t-1})\), or the later-occasion latent variance as that observed variance; under stationarity \(\operatorname{Var}(y_t)\) equals contemporaneous \(\operatorname{Var}(y_0)\); a zero loading is \(\theta+\psi\); a non-event clock, a non-positive interval, and an overflowing product or sum fail closed; +- Driver et al. (2017, §4.3, pp. 9–10; Eq. 3–4, pp. 4–5; p. 16; JSS PDF re-opened 2026-08-23T05:12Z) recovers a known predetermined later-occasion `T0VAR` \(\mathrm{trait}+e^{2a\Delta t}p_0+Q_{\Delta t}+(B/a)^{2}v\) at machine-scale RMSE, and that RMSE is smaller than treating stationary later-occasion variance, \(e^{2a\Delta t}\) of \(\mathrm{trait}+p_0+(B/a)^{2}v\) plus \(Q_{\Delta t}\), or free \(p_0\) as that later map; setting \(p_0=-q/(2a)\) recovers the stationary later-occasion map; a large finite \(\Delta t\) approaches contemporaneous stationary `T0VAR`; a vanishing interval approaches \(\mathrm{trait}+p_0+(B/a)^{2}v\); a zero trait, a zero initial variance, a zero diffusion, and a zero TI contribution is exactly zero; a zero initial variance, a zero diffusion, and a zero TI contribution is exactly the trait; nonzero diffusion with \(a\ge 0\) is a growing process and is kept; \(a\ge 0\) with a nonzero TI contribution fails closed; a non-event clock, a non-positive interval, and an overflowing product or sum fail closed; +- Driver et al. (2017, Eq. 5 of predetermined later-occasion §4.3 `T0VAR`; Table 2, p. 12; JSS PDF re-opened 2026-08-23T05:12Z) recovers a known \(\operatorname{Var}(y_t)=\lambda^{2}(\mathrm{trait}+e^{2a\Delta t}p_0+Q_{\Delta t}+(B/a)^{2}v)+\theta+\psi\) at machine-scale RMSE, and that RMSE is smaller than treating `MANIFESTVAR`, the predetermined later-occasion latent variance, or stationary later-occasion observed variance as that observed variance when \(p_0\) is free; setting \(p_0=-q/(2a)\) recovers the stationary later-occasion observed variance; a zero loading is \(\theta+\psi\); a non-event clock, a non-positive interval, and an overflowing product or sum fail closed; +- Driver et al. (2017, §4.3, pp. 9–10; Eq. 3–4, pp. 4–5; p. 16; JSS PDF re-opened 2026-08-23T09:04Z) recovers a known predetermined lagged `T0VAR` \(\mathrm{trait}+e^{a\Delta t}p_0+(B/a)^{2}v\) at machine-scale RMSE, and that RMSE is smaller than treating stationary lagged covariance, later-occasion variance, \(e^{a\Delta t}\) of \(\mathrm{trait}+p_0+(B/a)^{2}v\), or free \(p_0\) as that lagged map; setting \(p_0=-q/(2a)\) recovers the stationary lagged map; a large finite \(\Delta t\) recovers \(\mathrm{trait}+(B/a)^{2}v\); a vanishing interval approaches \(\mathrm{trait}+p_0+(B/a)^{2}v\); a zero trait, a zero initial variance, and a zero TI contribution is exactly zero; a zero initial variance and a zero TI contribution is exactly the trait; a zero-diffusion carry with \(a\ge 0\) is \(e^{a\Delta t}p_0\) and is kept; \(a\ge 0\) with a nonzero TI contribution fails closed; a non-event clock, a non-positive interval, and an overflowing product or sum fail closed; +- Driver et al. (2017, Eq. 5 of predetermined lagged §4.3 `T0VAR`; Table 2, p. 12; JSS PDF re-opened 2026-08-23T09:04Z) recovers a known \(\operatorname{cov}(y_t,y_{t-1})=\lambda^{2}(\mathrm{trait}+e^{a\Delta t}p_0+(B/a)^{2}v)+\psi\) at machine-scale RMSE, and that RMSE is smaller than treating `MANIFESTVAR`, the predetermined lagged latent covariance, predetermined later observed variance, or stationary lagged observed covariance as that observed covariance when \(p_0\) is free; setting \(p_0=-q/(2a)\) recovers the stationary lagged observed covariance; a zero loading is \(\psi\); independent \(\varepsilon_t\) does not enter; a non-event clock, a non-positive interval, and an overflowing product or sum fail closed; +- Driver et al. (2017, §4.3, pp. 9–10; Eq. 5, p. 5; p. 16; JSS PDF re-opened 2026-08-23T10:03Z) recovers a known predetermined first-occasion `T0VAR` \(\mathrm{trait}+p_0+(B/a)^{2}v\) at machine-scale RMSE, and that RMSE is smaller than treating stationary first-occasion variance, free \(p_0\), lagged covariance, or later-occasion variance as that first-occasion map; setting \(p_0=-q/(2a)\) recovers the stationary first-occasion map; a vanishing interval of the lagged and later maps approaches this composition; a zero trait, a zero initial variance, and a zero TI contribution is exactly zero; a zero initial variance and a zero TI contribution is exactly the trait; trait-only variance does not require a stable drift; \(a\ge 0\) with a nonzero TI contribution fails closed; a non-event clock and an overflowing product or sum fail closed; +- Driver et al. (2017, Eq. 5 of predetermined first-occasion §4.3 `T0VAR`; Table 2, p. 12; JSS PDF re-opened 2026-08-23T10:03Z) recovers a known \(\operatorname{Var}(y_0)=\lambda^{2}(\mathrm{trait}+p_0+(B/a)^{2}v)+ heta+\psi\) at machine-scale RMSE, and that RMSE is smaller than treating `MANIFESTVAR`, the predetermined first-occasion latent variance, stationary first-occasion observed variance, or predetermined later observed variance as that observed variance when \(p_0\) is free; setting \(p_0=-q/(2a)\) recovers the stationary first-occasion observed variance; a zero loading is \( heta+\psi\); a non-event clock and an overflowing product or sum fail closed; - pooling discrete lags from unequal intervals fails closed; - CWC-then-lag on a two-cluster decaying series has smaller computed RMSE than a level-pooled series when the latter is identified; - already-centered irregular residuals recover a known drift at machine-scale RMSE, and that RMSE is smaller than CWC of the corresponding raw autoregressive series (Curran & Bauer, 2011, pp. 607–608); - a singleton cluster is skipped; two singleton clusters yield an empty pair list and fail closed; - overflowing CWC residuals, overflowing contextual subtraction, later-only residual overflow, non-finite intervals, Newton overflow / start-skip / deriv-INF, and Pearson empty/mismatch paths fail closed. +- Driver et al. (2017, p. 16 `DIFFUSIONstd`; Eq. 4; footnote 4; JSS PDF re-opened 2026-08-23T13:20Z) recovers a known continuous standardisation \(q/(-q/(2a))=-2a\) at machine-scale RMSE, and that RMSE is smaller than treating unstandardised \(q\), discrete \(Q_{\Delta t}/p\), or \(q/(\mathrm{trait}+p+\mathrm{added})\) as `DIFFUSIONstd`; distinct positive \(q\) recover the same \(-2a\); \(q=0\) and \(a\ge 0\) fail closed; a non-event clock and an overflowing ratio fail closed. +- Driver et al. (2017, p. 16 `DRIFTstd`; Eq. 1; footnote 4; JSS PDF re-opened 2026-08-23T13:28Z) recovers a known continuous auto-effect \(a\) after strictly positive `asymDIFFUSION` at machine-scale RMSE, and that RMSE is smaller than treating unstandardised \(a\), discrete \(e^{a\Delta t}\), or \(ap/(\mathrm{trait}+p+\mathrm{added})\) as `DRIFTstd`; distinct positive \(q\) recover the same \(a\); \(q=0\) and \(a\ge 0\) fail closed; a non-event clock fails closed. +- Driver et al. (2017, p. 16 `asymTIPREDEFFECTstd`; §7.2; Eq. 3; footnote 4; JSS PDF re-opened 2026-08-23T14:25Z) recovers a known standardised asymptotic TI effect \((-B/a)\cdot\sqrt{v}/\sqrt{-q/(2a)}\) at machine-scale RMSE, and that RMSE is smaller than treating unstandardised \(-B/a\), finite-interval \(A^{-1}[e^{A\Delta t}-I]B\cdot\sqrt{v}/\sqrt{p}\), or \((-B/a)\cdot\sqrt{v}/\sqrt{\mathrm{trait}+p+\mathrm{added}}\) as `asymTIPREDEFFECTstd`; a larger positive \(q\) yields a smaller \(|\mathrm{std}|\); a zero coefficient with positive \(v\) and \(p\) is exactly zero; \(q=0\), \(v=0\), and \(a\ge 0\) fail closed; a non-event clock and an overflowing product fail closed. +- Driver et al. (2017, p. 16 `TIPREDEFFECTstd`; §7.2; Eq. 3; footnote 4; JSS PDF re-opened 2026-08-23T16:21Z) recovers a known standardised continuous TI effect \(B\cdot\sqrt{v}/\sqrt{-q/(2a)}\) at machine-scale RMSE, and that RMSE is smaller than treating unstandardised \(B\), asymptotic \((-B/a)\cdot\sqrt{v}/\sqrt{p}\), finite-interval \(A^{-1}[e^{A\Delta t}-I]B\cdot\sqrt{v}/\sqrt{p}\), or \(B\cdot\sqrt{v}/\sqrt{\mathrm{trait}+p+\mathrm{added}}\) as `TIPREDEFFECTstd`; a larger positive \(q\) yields a smaller \(|\mathrm{std}|\); a zero coefficient with positive \(v\) and \(p\) is exactly zero; \(q=0\), \(v=0\), and \(a\ge 0\) fail closed; a non-event clock and an overflowing product fail closed. +- Driver et al. (2017, Table 3 / p. 16 `T0TIPREDEFFECTstd`; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T17:20Z) recovers a known standardised first-occasion TI effect \(t0_b\cdot\sqrt{v}/\sqrt{p_0}\) at machine-scale RMSE, and that RMSE is smaller than treating unstandardised \(t0_b\), continuous \(B\cdot\sqrt{v}/\sqrt{-q/(2a)}\), asymptotic \((-B/a)\cdot\sqrt{v}/\sqrt{p}\), or \(t0_b\cdot\sqrt{v}/\sqrt{\mathrm{trait}+p_0+\mathrm{added}}\) as `T0TIPREDEFFECTstd`; a larger positive \(p_0\) yields a smaller \(|\mathrm{std}|\); a zero coefficient with positive \(v\) and \(p_0\) is exactly zero; \(p_0=0\) and \(v=0\) fail closed; a non-event clock and an overflowing product fail closed; free `T0VAR` does not require \(a<0\). +- Driver et al. (2017, Table 3 / p. 16 / 2017-era `addedT0TIPREDVAR`; §7.2; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T18:20Z) recovers a known first-occasion extra TI variance \(t0_b^{2}v\) at machine-scale RMSE, and that RMSE is smaller than treating `addedTIPREDVAR` \((B/a)^{2}v\), `T0TIPREDEFFECTstd` \(t0_b\cdot\sqrt{v}/\sqrt{p_0}\), free \(p_0\), or `TRAITVAR` as `addedT0TIPREDVAR`; doubling \(v\) doubles the extra variance; a signed coefficient yields the same product; a zero coefficient or zero predictor variance is exactly zero; \(v<0\) fails closed; a non-event clock and an overflowing product fail closed; free `T0TIPREDEFFECT` does not require \(a<0\). +- Driver et al. (2017, Eq. 5 of 2017-era `addedT0TIPREDVAR`; Table 3 / p. 16; Table 2, p. 12; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T19:10Z) recovers a known extra observed-indicator TI variance \(\lambda^{2}t0_b^{2}v\) at machine-scale RMSE, and that RMSE is smaller than treating the latent extra \(t0_b^{2}v\), first-occasion observed variance \(\lambda^{2}p_0+\theta\), Eq. 5 of `addedTIPREDVAR` \(\lambda^{2}(B/a)^{2}v\), or `MANIFESTVAR` \(\theta\) as that observed extra; doubling \(v\) doubles the extra observed variance; a signed coefficient yields the same product; a zero loading or zero extra is exactly zero; \(v<0\) fails closed; a non-event clock and an overflowing product fail closed; free `T0TIPREDEFFECT` does not require \(a<0\). +- Driver et al. (2017, Eq. 5 of §7.2 `addedTIPREDVAR`; Table 2, p. 12; §7.2, pp. 20–21; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T19:23Z) recovers a known extra observed-indicator TI variance \(\lambda^{2}(B/a)^{2}v\) at machine-scale RMSE, and that RMSE is smaller than treating the latent extra \((B/a)^{2}v\), Eq. 5 of `addedT0TIPREDVAR` \(\lambda^{2}t0_b^{2}v\), stationary observed variance \(\lambda^{2}p+\theta\), or `MANIFESTVAR` \(\theta\) as that observed extra; doubling \(v\) doubles the extra observed variance; a signed coefficient yields the same product; a zero loading or zero extra is exactly zero; \(v<0\) fails closed; \(a\ge 0\) with a nonzero extra fails closed; a non-event clock and an overflowing product fail closed. +- Driver et al. (2017, p. 16 `TDPREDEFFECTstd`; Table 2; Eq. 3; footnote 4; JSS PDF re-opened 2026-08-23T21:10Z) recovers a known standardised continuous TD effect \(m\cdot\sqrt{v}/\sqrt{-q/(2a)}\) at machine-scale RMSE, and that RMSE is smaller than treating unstandardised \(M\), intercept-style \(A^{-1}[e^{A\Delta t}-I]M\cdot\sqrt{v}/\sqrt{p}\), or \(m\cdot\sqrt{v}/\sqrt{\mathrm{trait}+p+\mathrm{added}}\) as `TDPREDEFFECTstd`; equal numbers with `TIPREDEFFECTstd` when \(M=B\) remain distinct named quantities; a larger positive \(q\) yields a smaller \(|\mathrm{std}|\); a zero coefficient with positive \(v\) and \(p\) is exactly zero; \(q=0\), \(v=0\), and \(a\ge 0\) fail closed; a non-event clock and an overflowing product fail closed. +- Driver et al. (2017, Table 3 / p. 16 `T0TDPREDEFFECTstd`; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T21:34Z) recovers a known standardised first-occasion TD effect \(t0_m\cdot\sqrt{v}/\sqrt{p_0}\) at machine-scale RMSE, and that RMSE is smaller than treating unstandardised \(t0_m\), continuous \(m\cdot\sqrt{v}/\sqrt{-q/(2a)}\), or \(t0_m\cdot\sqrt{v}/\sqrt{\mathrm{trait}+p_0+\mathrm{added}}\) as `T0TDPREDEFFECTstd`; equal numbers with `T0TIPREDEFFECTstd` when \(t0_m=t0_b\) remain distinct named quantities; a larger positive \(p_0\) yields a smaller \(|\mathrm{std}|\); a zero coefficient with positive \(v\) and \(p_0\) is exactly zero; \(p_0=0\) and \(v=0\) fail closed; a non-event clock and an overflowing product fail closed; free `T0VAR` does not require \(a<0\). +- Driver et al. (2017, Table 2 / p. 16 `T0VARstd`; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T22:06Z) recovers the scalar correlation \(p_0/p_0=1\) at machine-scale RMSE after strictly positive free `T0VAR`, and that RMSE is smaller than treating unstandardised \(p_0\), `T0TDPREDEFFECTstd` \(t0_m\cdot\sqrt{v}/\sqrt{p_0}\), or `addedT0TIPREDVAR` \(t0_b^{2}v\) as `T0VARstd`; distinct positive \(p_0\) recover the same 1; \(p_0=0\) fails closed; a non-event clock fails closed; free `T0VAR` does not require \(a<0\). +- Driver et al. (2017, Table 2 / §7.1 / p. 16 `TRAITVARstd`; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T22:21Z) recovers the scalar correlation \(\mathrm{trait}/\mathrm{trait}=1\) at machine-scale RMSE after strictly positive `TRAITVAR`, and that RMSE is smaller than treating unstandardised `TRAITVAR` or `addedT0TIPREDVAR` \(t0_b^{2}v\) as `TRAITVARstd`; distinct positive trait recover the same 1; equal 1 with `T0VARstd` remains a distinct named quantity; `TRAITVAR = 0` fails closed; a non-event clock fails closed; `TRAITVAR` does not require \(a<0\). +- Driver et al. (2017, Table 2 / §7.1 / p. 16 `MANIFESTTRAITVARstd`; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T22:28Z) recovers the scalar correlation \(\psi/\psi=1\) at machine-scale RMSE after strictly positive `MANIFESTTRAITVAR`, and that RMSE is smaller than treating unstandardised `MANIFESTTRAITVAR` or `MANIFESTVAR` \(\theta\) as `MANIFESTTRAITVARstd`; distinct positive \(\psi\) recover the same 1; equal 1 with `TRAITVARstd` remains a distinct named quantity; `MANIFESTTRAITVAR = 0` fails closed; a non-event clock fails closed; `MANIFESTTRAITVAR` does not require \(a<0\). +- Driver et al. (2017, Table 2 / Eq. 5 / p. 16 `MANIFESTVARstd`; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T22:40Z) recovers the scalar correlation \(\theta/\theta=1\) at machine-scale RMSE after strictly positive `MANIFESTVAR`, and that RMSE is smaller than treating unstandardised `MANIFESTVAR` or Equation 5 \(\operatorname{Var}(y)\) as `MANIFESTVARstd`; distinct positive \(\theta\) recover the same 1; equal 1 with `MANIFESTTRAITVARstd` remains a distinct named quantity; `MANIFESTVAR = 0` fails closed; a non-event clock fails closed; `MANIFESTVAR` does not require \(a<0\). +- Driver et al. (2017, Table 2 / p. 16 `TIPREDVARstd`; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T22:53Z) recovers the scalar correlation \(v/v=1\) at machine-scale RMSE after strictly positive `TIPREDVAR`, and that RMSE is smaller than treating unstandardised `TIPREDVAR` or §7.2 `addedTIPREDVAR` \((B/a)^{2}v\) as `TIPREDVARstd`; distinct positive \(v\) recover the same 1; equal 1 with `MANIFESTVARstd` remains a distinct named quantity; `TIPREDVAR = 0` fails closed; a non-event clock fails closed; `TIPREDVAR` does not require \(a<0\). +- Driver et al. (2017, p. 16 `asymDIFFUSIONstd`; footnote 4; Eq. 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T23:02Z) recovers the scalar correlation \(p/p=1\) at machine-scale RMSE after strictly positive `asymDIFFUSION` \(-q/(2a)\), and that RMSE is smaller than treating unstandardised `asymDIFFUSION` or `DIFFUSIONstd` \(-2a\) as `asymDIFFUSIONstd`; distinct positive \(p\) recover the same 1; equal 1 with `TIPREDVARstd` remains a distinct named quantity; `q = 0` fails closed; a non-event clock fails closed; \(a\ge 0\) fails closed. +- Driver et al. (2017, p. 16 `discreteCINTstd`; footnote 4; Eq. 3; Table 2; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-24T05:20Z) recovers the scalar standardised discrete intercept \(A^{-1}[e^{A\Delta t}-I]\kappa/\sqrt{p}\) at machine-scale RMSE after strictly positive `asymDIFFUSION` \(-q/(2a)\), and that RMSE is smaller than treating unstandardised `discreteCINT`, \(\kappa/\sqrt{p}\), or \((-\kappa/a)/\sqrt{p}\) as `discreteCINTstd`; a later event interval changes the result; a zero intercept is exactly zero; `q = 0` fails closed; a non-event clock fails closed; a non-positive event interval fails closed; \(a\ge 0\) fails closed. +- Driver et al. (2017, p. 16 `asymCINTstd`; footnote 4; Eq. 3; Table 2; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-24T09:05Z) recovers the scalar standardised asymptotic intercept \((-\kappa/a)/\sqrt{p}\) at machine-scale RMSE after strictly positive `asymDIFFUSION` \(-q/(2a)\), and that RMSE is smaller than treating unstandardised `asymCINT`, \(\kappa/\sqrt{p}\), or `discreteCINTstd` as `asymCINTstd`; a later event interval changes `discreteCINTstd` and not this map; a zero intercept is exactly zero; `q = 0` fails closed; a non-event clock fails closed; \(a\ge 0\) fails closed. +- Driver et al. (2017, p. 16 `T0MEANSstd`; footnote 4; Table 2; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-24T22:30Z) recovers the scalar standardised initial latent mean \(\mu_0/\sqrt{p_0}\) at machine-scale RMSE after strictly positive free `T0VAR` \(p_0\), and that RMSE is smaller than treating unstandardised `T0MEANS` or \(\mu_0/\sqrt{\mathrm{asymDIFFUSION}}\) as `T0MEANSstd`; a larger positive \(p_0\) yields a smaller \(|\mathrm{std}|\); a zero mean is exactly zero; equal 1 with `T0VARstd` when \(\mu_0=\sqrt{p_0}\) remains a distinct named quantity; \(p_0=0\) fails closed; a non-event clock fails closed; free `T0MEANS` does not require \(a<0\). diff --git a/docs/validation/temporal-event-foundation.md b/docs/validation/temporal-event-foundation.md index 3d6881d40..3c0b37544 100644 --- a/docs/validation/temporal-event-foundation.md +++ b/docs/validation/temporal-event-foundation.md @@ -64,7 +64,7 @@ This report tracks exact-head scientific and engineering evidence required befor | Causal-identification gate | `relation_graph` | active-PR | association ≠ cause | LeadsTo/References denied | ADR 0003; `docs/research/causal-identification-gate.md` | | Versioned API/export contracts | `tepp_api` | implemented-main | naruon HTTP interchange | unknown-field/version/limit + naruon HTTPS interchange tests | Task 12 / PR #21; live HTTP service remaining | | Simulation cutoff eligibility | `tepp_simulation` | accepted-target | `available_time <= knowledge_cutoff` on PR #62 | delayed-document exclusion, generated-count agreement, exact-boundary admission, and fail-closed `TemporalInvariantViolation` for late documents | ADR 0002; `crates/tepp_simulation/tests/cutoff_eligibility_contract.rs`; `docs/research/simulation-cutoff-eligibility.md` | -| Psychometric structural input gates | `psychometric_core` | partial | stacked psychometric PR | construct-class refusal + ALR/ILR boundary + true-loading RMSE + posterior-draw point-estimate mean + Rubin `T` + CWC within/between + CWC contextual effect + event-time log-rate + constant- and time-varying-predictor discrete effects + exact scalar discrete process noise + lagged latent covariance and unconditional latent variance + stationary within-subject variance + trait-plus-state variance + observed-indicator variance + discrete latent mean (`T0MEANS`/`CINT`) + evolved observed mean (`τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`) + contemporaneous `TDPREDEFFECT` impulse (`m x`; not `CINT`, not `TIPREDEFFECT`, not Voelkle Eq. 14) + Eq. 5 of that contemporaneous impulse (`τ + λ(μ_t + m x)`; `τ + λ μ_t` is not that observed mean) + time-independent `TIPREDEFFECT` increment (`A^{-1}[e^{A Δt} − I] B z`; not `CINT`, not `M x`, not Voelkle Eq. 14, not the coefficient `B`) + Eq. 5 of that increment (`τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`; `τ + λ μ_t` is not that observed mean) + within-interval `TDPREDEFFECT` carry (`e^{A(t−u)} M x` for `t0 < u < t`; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, not Voelkle Eq. 14) + Eq. 5 of that carry (`τ + λ(μ_t + e^{a(t−u)} m x)`; `τ + λ μ_t` is not that observed mean) + §7.2 level-change `CINT` (`κ = −a m x`; Eq. 3 increment `(1 − e^{a Δt}) m x`) + §7.2 extra-process contribution (`a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; not `κ`, not the increment, not the Dirac; `ε ≥ 0` fails closed) + Eq. 5 of that extra-process contribution (`τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; extra `LAMBDA` is 0; `τ + λ μ_t` is not that observed mean) + after-t0 extra-process `TDPREDEFFECT` (`a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t`; Eq. 5 `τ + λ(μ_t + contribution(t−u))`; not the first-occasion extra-process observed mean; not the impulse-carry Dirac) + §7.2 `asymTIPREDEFFECT` (`-B z / a` for `a < 0`; not `B`, not the finite-interval increment, not `CINT`, not `M x`) + §7.2 `addedTIPREDVAR` (`(B / a)² v`; not `TRAITVAR`, not `asymDIFFUSION`, not `-B z / a`) + Table 2 `asymCINT` (`-κ / a` for `a < 0`; not `κ`, not the finite-interval increment, not `T0MEANS`, not `-B z / a`) + p. 16 stationary `T0MEANS` (`-κ / a + −B z / a`; not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, not the finite-interval discrete mean) + Eq. 5 of that constrained mean (`τ + λ(−κ / a + −B z / a)`; `τ + λ μ_0` is not that observed mean; `τ + λ(−κ / a)` is not that observed mean when `B z ≠ 0`; `τ + λ μ_t` is not that observed mean; `MANIFESTMEANS` is not `E(y_0)`; the constrained latent mean is not `E(y_0)`) + stationary `T0VAR` (`trait + −q / (2 a) + (B / a)² v`; not free `T0VAR`, not `asymDIFFUSION` alone, not `TRAITVAR` alone, not `addedTIPREDVAR` alone; Eq. 5 is `λ²(trait + −q / (2 a) + (B / a)² v) + θ + ψ` (`λ² p_0` is not `Var(y_0)`; `λ²(−q / (2 a)) + θ` is not `Var(y_0)` when trait or TI is nonzero; `MANIFESTVAR` is not `Var(y_0)`; the constrained latent variance is not `Var(y_0)`)) + Eq. 5 of that constrained variance (`λ²(trait + −q / (2 a) + (B / a)² v) + θ + ψ`; `MANIFESTVAR` is not `Var(y_0)`) + irregular already-centered residual lag + strong-gated latent means (n=2 residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016); full ESEM/DSEM remaining | ADR 0005; `docs/research/posterior-esem-input-gates.md`; `docs/research/multilevel-event-time-recovery.md`; `docs/research/rubin-total-variance.md`; `docs/research/strong-invariance-latent-means.md` | +| Psychometric structural input gates | `psychometric_core` | partial | stacked psychometric PR | construct-class refusal + ALR/ILR boundary + true-loading RMSE + posterior-draw point-estimate mean + Rubin `T` + CWC within/between + CWC contextual effect + event-time log-rate + constant- and time-varying-predictor discrete effects + exact scalar discrete process noise + lagged latent covariance and unconditional latent variance + stationary within-subject variance + trait-plus-state variance + observed-indicator variance + discrete latent mean (`T0MEANS`/`CINT`) + evolved observed mean (`τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`) + contemporaneous `TDPREDEFFECT` impulse (`m x`; not `CINT`, not `TIPREDEFFECT`, not Voelkle Eq. 14) + Eq. 5 of that contemporaneous impulse (`τ + λ(μ_t + m x)`; `τ + λ μ_t` is not that observed mean) + time-independent `TIPREDEFFECT` increment (`A^{-1}[e^{A Δt} − I] B z`; not `CINT`, not `M x`, not Voelkle Eq. 14, not the coefficient `B`) + Eq. 5 of that increment (`τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`; `τ + λ μ_t` is not that observed mean) + within-interval `TDPREDEFFECT` carry (`e^{A(t−u)} M x` for `t0 < u < t`; not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, not Voelkle Eq. 14) + Eq. 5 of that carry (`τ + λ(μ_t + e^{a(t−u)} m x)`; `τ + λ μ_t` is not that observed mean) + §7.2 level-change `CINT` (`κ = −a m x`; Eq. 3 increment `(1 − e^{a Δt}) m x`) + §7.2 extra-process contribution (`a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; not `κ`, not the increment, not the Dirac; `ε ≥ 0` fails closed) + Eq. 5 of that extra-process contribution (`τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; extra `LAMBDA` is 0; `τ + λ μ_t` is not that observed mean) + after-t0 extra-process `TDPREDEFFECT` (`a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t`; Eq. 5 `τ + λ(μ_t + contribution(t−u))`; not the first-occasion extra-process observed mean; not the impulse-carry Dirac) + §7.2 `asymTIPREDEFFECT` (`-B z / a` for `a < 0`; not `B`, not the finite-interval increment, not `CINT`, not `M x`) + §7.2 `addedTIPREDVAR` (`(B / a)² v`; not `TRAITVAR`, not `asymDIFFUSION`, not `-B z / a`) + Table 2 `asymCINT` (`-κ / a` for `a < 0`; not `κ`, not the finite-interval increment, not `T0MEANS`, not `-B z / a`) + p. 16 stationary `T0MEANS` (`-κ / a + −B z / a`; not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, not the finite-interval discrete mean) + Eq. 5 of that constrained mean (`τ + λ(−κ / a + −B z / a)`; `τ + λ μ_0` is not that observed mean; `τ + λ(−κ / a)` is not that observed mean when `B z ≠ 0`; `τ + λ μ_t` is not that observed mean; `MANIFESTMEANS` is not `E(y_0)`; the constrained latent mean is not `E(y_0)`) + stationary `T0VAR` (`trait + −q / (2 a) + (B / a)² v`; not free `T0VAR`, not `asymDIFFUSION` alone, not `TRAITVAR` alone, not `addedTIPREDVAR` alone; Eq. 5 is `λ²(trait + −q / (2 a) + (B / a)² v) + θ + ψ` (`λ² p_0` is not `Var(y_0)`; `λ²(−q / (2 a)) + θ` is not `Var(y_0)` when trait or TI is nonzero; `MANIFESTVAR` is not `Var(y_0)`; the constrained latent variance is not `Var(y_0)`)) + Eq. 5 of that constrained variance (`λ²(trait + −q / (2 a) + (B / a)² v) + θ + ψ`; `MANIFESTVAR` is not `Var(y_0)`) + p. 16 `TDPREDEFFECTstd` (`m · √v / √(-q / (2 a))` after strictly positive `asymDIFFUSION` and TD predictor variance; not `TIPREDEFFECTstd` even when `M = B`; not intercept-style `A^{-1}[e^{A Δt} − I] M · √v / √p`; not trait-contaminated) + Table 3 / p. 16 `T0TDPREDEFFECTstd` (`t0_m · √v / √p_0` after strictly positive free `T0VAR` and TD predictor variance; not `TDPREDEFFECTstd`; not `T0TIPREDEFFECTstd` even when `t0_m = t0_b`; not trait-contaminated; free `T0VAR` does not require `a < 0`) + p. 16 `T0VARstd` (`p_0 / p_0 = 1` after strictly positive free `T0VAR`; not unstandardised `T0VAR`; not `T0TDPREDEFFECTstd`; not `addedT0TIPREDVAR`; free `T0VAR` does not require `a < 0`) + p. 16 `TRAITVARstd` (`trait / trait = 1` after strictly positive `TRAITVAR`; no ridge addend; not unstandardised `TRAITVAR`; not `T0VARstd` even when both equal 1; not `addedT0TIPREDVAR`; `TRAITVAR` does not require `a < 0`) + p. 16 `MANIFESTTRAITVARstd` (`ψ / ψ = 1` after strictly positive `MANIFESTTRAITVAR`; 2017-era source adds ridging; default ridge is 0; not unstandardised `MANIFESTTRAITVAR`; not `TRAITVARstd` even when both equal 1; not `MANIFESTVAR`; `MANIFESTTRAITVAR` does not require `a < 0`) + p. 16 `MANIFESTVARstd` (`θ / θ = 1` after strictly positive `MANIFESTVAR`; 2017-era source adds ridging; default ridge is 0; 2017-era `dimnames` assignment to `latentNames` is a source bug; not unstandardised `MANIFESTVAR`; not `MANIFESTTRAITVARstd` even when both equal 1; not Equation 5 `Var(y)`; `MANIFESTVAR` does not require `a < 0`) + p. 16 `TIPREDVARstd` (`v / v = 1` after strictly positive `TIPREDVAR`; 2017-era source adds ridging; default ridge is 0; `dimnames` are `TIpredNames`; not unstandardised `TIPREDVAR`; not `MANIFESTVARstd` even when both equal 1; not §7.2 `addedTIPREDVAR`; `TIPREDVAR` does not require `a < 0`) + p. 16 `asymDIFFUSIONstd` (`p / p = 1` after strictly positive `asymDIFFUSION`; 2017-era source adds ridging; default ridge is 0; `dimnames` are `latentNames`; not unstandardised `asymDIFFUSION`; not `TIPREDVARstd` even when both equal 1; not `DIFFUSIONstd` `−2 a`; lasting `asymDIFFUSION` requires `a < 0`) + p. 16 `discreteCINTstd` (`A^{-1}[e^{A Δt} − I] κ / √p` after strictly positive `asymDIFFUSION`; not unstandardised `discreteCINT`; not `κ / √p`; not `(-κ / a) / √p`; lasting `asymDIFFUSION` requires `a < 0`) + exact scalar p. 16 `asymCINTstd` (`(-κ / a) / √p` after strictly positive `asymDIFFUSION`; not unstandardised `asymCINT`; not `κ / √p`; not `discreteCINTstd`; lasting `asymDIFFUSION` requires `a < 0`) + exact scalar p. 16 `T0MEANSstd` (`μ_0 / √p_0` after strictly positive free `T0VAR`; not unstandardised `T0MEANS`; not `T0VARstd`; not `μ_0 / √asymDIFFUSION`; free `T0MEANS` does not require `a < 0`) + irregular already-centered residual lag + strong-gated latent means (n=2 residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016); full ESEM/DSEM remaining | ADR 0005; `docs/research/posterior-esem-input-gates.md`; `docs/research/multilevel-event-time-recovery.md`; `docs/research/rubin-total-variance.md`; `docs/research/strong-invariance-latent-means.md` | | Prompt-versus-unique-content identity | `prompt_source` | accepted-target | active PR | refuse prompt-as-unique/stopword + recovery vs unique-content collapse | ADR 0004/0012 | | Corpus-background-versus-unique-content identity | `corpus_background` | accepted-target | active PR | refuse background-as-unique/stopword + recovery vs unique-content collapse | ADR 0004/0012 | | Modality-versus-unique-content identity | `modality_source` | accepted-target | active PR | refuse modality-as-unique/stopword + recovery vs unique-content collapse | ADR 0004/0012 |