diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 2f42564a..0193cd50 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -61,7 +61,7 @@ boundaries above remain the target modular MSA architecture. | `tepp_simulation` | known-truth temporal/event data generation | | `validation_core` | RMSE, bias, coverage, graph, and Monte Carlo metrics | | `tepp_api` | versioned DTO, schema, and export contracts | -| `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))), 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) | No crate exposes placeholder production behavior in Task 1. This prevents an empty façade from becoming a de facto public API before its invariants and tests diff --git a/CHANGELOG.md b/CHANGELOG.md index ce7024d8..c35e80c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang ## [Unreleased] +- `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-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*). - `psychometric_core` integration tests now execute the Driver, Oud, and Voelkle (2017, Eq. 3 first-summand carry) overflow rewrite of Table 3 `T0TIPREDEFFECT` / `T0TDPREDEFFECT` (`sign(t0_b z) exp(ln|t0_b z| + a Δt)` and the same form for `t0_m x0`). Nightly branch coverage on #49 head `d634f5849ed8e9f75af1b43c2e59d8e7d6301b45` was 1718/1720: the two missing records were the unused non-`cfg(test)` instantiations of `if !drift_interval.is_finite()` at the T0 TI and T0 TD carry overflow rewrites (`event_time.rs` L4245 and L4628). Lib tests already covered both sides; integration tests now take overflowing `a Δt` (`1e308 * 2`) and finite-`a Δt` overflowed `exp` (`710`) on those public maps. Meredith (1993) remains unread (Unpaywall 2026-08-22T23:12Z: `is_oa: false`; title *Measurement Invariance, Factor Analysis and Factorial Invariance*). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread (Unpaywall 2026-08-22T23:12Z: `is_oa: false`; title *Randomization-Based Inference about Latent Variables from Complex Samples*). Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. - `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-22T23:12Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar later-occasion variance of §4.3 stationary `T0VAR`. Section 4.3 constrains first-occasion variance according to the model-predicted variances across all time points. 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 and 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. Under stationarity that composition equals contemporaneous `T0VAR`. Evolving the constrained total as if it were all state (`e^{2 a Δt} p_stat + 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. The interval must be event time and strictly positive. 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 that later-occasion observed variance. The later-occasion latent variance is not the later-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-22T23:12Z: `is_oa: false`; title *Measurement Invariance, Factor Analysis and Factorial Invariance*). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread (Unpaywall 2026-08-22T23:12Z: `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-22T19:13Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar lagged covariance of §4.3 stationary `T0VAR`. Equation 3 writes `η(t) = exp(A Δt) η(t0) + …`. Equation 4 writes `cov(η_t, η_{t-1}) = A_Δt cov(η_{t-1})`. The contemporaneous constraint is `trait + −q / (2 a) + (B / a)² v`. 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}(−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. As `Δt → ∞` with stable `a < 0` the state term vanishes. As `Δt → 0+` the lagged map 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. The interval must be event time and strictly positive. Equation 5 of that lagged covariance is `λ²(trait + e^{a Δt}(−q / (2 a)) + (B / a)² v) + ψ`. Independent `ε_t` does not enter. `MANIFESTVAR` is not that lagged observed covariance. Contemporaneous `Var(y_0)` includes `θ` and is not that lagged observed covariance. The lagged latent covariance is not the lagged observed covariance. 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-22T19:13Z: `is_oa: false`; title *Measurement Invariance, Factor Analysis and Factorial Invariance*). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread (Unpaywall 2026-08-22T19:13Z: `is_oa: false`; title *Randomization-Based Inference about Latent Variables from Complex Samples*). diff --git a/CLAUDE.md b/CLAUDE.md index a7a04122..a49d4368 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. 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 bea873ad..1bb6b7d6 100644 --- a/crates/psychometric_core/src/error.rs +++ b/crates/psychometric_core/src/error.rs @@ -504,6 +504,91 @@ 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, } impl fmt::Display for PsychometricError { @@ -897,6 +982,72 @@ 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" + } }; formatter.write_str(message) } @@ -1513,4 +1664,121 @@ 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" + ); + } } diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index 6e267d80..deedbeb4 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -138,6 +138,50 @@ //! 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. //! 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 @@ -4045,177 +4089,328 @@ pub fn refuse_stationary_lagged_observed_covariance_as_stationary_later_observed Err(PsychometricError::StationaryLaggedObservedCovarianceIsNotStationaryLaterObservedVariance) } -/// Exact scalar observed mean of a time-independent predictor. +/// Exact scalar later-occasion variance of §4.3 predetermined +/// `T0VAR`. /// -/// 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 +/// 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 /// ) -/// 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. +/// 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_mean_with_time_independent_predictor`] -/// and [`recover_manifest_observed_mean`]. +/// 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_discrete_observed_mean_with_time_independent_predictor( - loading: f64, - initial_latent_mean: f64, - log_rate: f64, - continuous_intercept: f64, +pub fn recover_predetermined_later_latent_variance( + trait_variance: f64, + initial_latent_variance: f64, + continuous_diffusion: f64, time_independent_effect: f64, - time_independent_predictor: f64, - manifest_mean: f64, + predictor_variance: f64, + log_rate: f64, event_delta: f64, clock: LagClock, ) -> Result { - let composed_latent_mean = recover_discrete_latent_mean_with_time_independent_predictor( - initial_latent_mean, + if !clock.admits_structural_lag() { + return Err(PsychometricError::EventTimeRequired); + } + let evolved_state = recover_discrete_latent_variance( + initial_latent_variance, + continuous_diffusion, log_rate, - continuous_intercept, - time_independent_effect, - time_independent_predictor, event_delta, clock, )?; - recover_manifest_observed_mean(loading, composed_latent_mean, manifest_mean) + 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 the evolved observed mean as the time-independent- -/// predictor observed mean. +/// Refuse treating predetermined later-occasion variance as later- +/// occasion stationary `T0VAR`. /// -/// 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. +/// `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::EvolvedObservedMeanIsNotTimeIndependentObservedMean`]. -pub fn refuse_evolved_observed_mean_as_time_independent_observed_mean( - evolved_observed_mean: f64, - time_independent_observed_mean: f64, +/// [`PsychometricError::PredeterminedLaterLatentVarianceIsNotStationaryLaterLatentVariance`]. +pub fn refuse_predetermined_later_latent_variance_as_stationary_later_latent_variance( + predetermined_later_variance: f64, + stationary_later_variance: f64, ) -> Result { - let _ = (evolved_observed_mean, time_independent_observed_mean); - Err(PsychometricError::EvolvedObservedMeanIsNotTimeIndependentObservedMean) + let _ = (predetermined_later_variance, stationary_later_variance); + Err(PsychometricError::PredeterminedLaterLatentVarianceIsNotStationaryLaterLatentVariance) } -/// Refuse treating the contemporaneous-impulse observed mean as the -/// time-independent-predictor observed mean. +/// Refuse treating predetermined later-occasion variance as the free +/// discrete evolution of the total. /// -/// 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. +/// 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::ImpulseObservedMeanIsNotTimeIndependentObservedMean`]. -pub fn refuse_impulse_observed_mean_as_time_independent_observed_mean( - impulse_observed_mean: f64, - time_independent_observed_mean: f64, +/// [`PsychometricError::PredeterminedLaterLatentVarianceIsNotDiscreteVariance`]. +pub fn refuse_predetermined_later_latent_variance_as_discrete_variance( + predetermined_later_variance: f64, + free_discrete_variance: f64, ) -> Result { - let _ = (impulse_observed_mean, time_independent_observed_mean); - Err(PsychometricError::ImpulseObservedMeanIsNotTimeIndependentObservedMean) + let _ = (predetermined_later_variance, free_discrete_variance); + Err(PsychometricError::PredeterminedLaterLatentVarianceIsNotDiscreteVariance) } -/// Refuse treating the impulse-carry observed mean as the -/// time-independent-predictor observed mean. +/// Refuse treating predetermined later-occasion variance as free +/// first-occasion `T0VAR`. /// -/// 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. +/// `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::ImpulseCarryObservedMeanIsNotTimeIndependentObservedMean`]. -pub fn refuse_impulse_carry_observed_mean_as_time_independent_observed_mean( - impulse_carry_observed_mean: f64, - time_independent_observed_mean: f64, +/// [`PsychometricError::PredeterminedLaterLatentVarianceIsNotInitialLatentVariance`]. +pub fn refuse_predetermined_later_latent_variance_as_initial_latent_variance( + predetermined_later_variance: f64, + initial_latent_variance: f64, ) -> Result { - let _ = (impulse_carry_observed_mean, time_independent_observed_mean); - Err(PsychometricError::ImpulseCarryObservedMeanIsNotTimeIndependentObservedMean) + let _ = (predetermined_later_variance, initial_latent_variance); + Err(PsychometricError::PredeterminedLaterLatentVarianceIsNotInitialLatentVariance) } -/// Exact scalar first-occasion time-independent predictor shift. +/// Exact scalar Eq. 5 of later-occasion §4.3 predetermined `T0VAR`. /// -/// Driver, Oud, and Voelkle (2017, Table 3, p. 13; Eq. 3 first -/// summand, p. 5; JSS PDF opened 2026-08-20T15:14Z from +/// 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 /// ) -/// 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. +/// 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 /// -/// 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, +/// 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 { - 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) + 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, + ) } -/// Exact scalar carried first-occasion time-independent predictor. +/// Refuse treating predetermined later-occasion variance as +/// predetermined later-occasion observed variance. /// -/// 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. +/// `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 /// -/// Returns [`PsychometricError::EventTimeRequired`] for any non-event -/// clock, [`PsychometricError::NonPositiveInterval`] when -/// `event_delta` is not strictly positive, and +/// 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 or the mapped carry overflows. -pub fn recover_initial_time_independent_predictor_carry( - initial_time_independent_effect: f64, - time_independent_predictor: f64, +/// 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, @@ -4223,796 +4418,703 @@ pub fn recover_initial_time_independent_predictor_carry( 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, + let trait_plus_state = recover_trait_plus_state_lagged_covariance( + trait_variance, + initial_latent_variance, + log_rate, + event_delta, + clock, )?; - 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, + let added = recover_asymptotic_time_independent_predictor_variance( + time_independent_effect, + predictor_variance, 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) + require_finite(trait_plus_state + added) } -/// Refuse treating the Eq. 3 carry of `T0TIPREDEFFECT` as the -/// first-occasion shift. +/// Refuse treating predetermined lagged covariance as lagged +/// stationary `T0VAR`. /// -/// `e^{A Δt} t0_b z` is the first summand's contribution at `t`. -/// `t0_b z` is the shift at `T0`. +/// `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::InitialTimeIndependentCarryIsNotInitialEffect`]. -pub fn refuse_initial_time_independent_carry_as_initial_effect( - initial_time_independent_carry: f64, - initial_time_independent_effect: f64, +/// [`PsychometricError::PredeterminedLaggedLatentCovarianceIsNotStationaryLaggedCovariance`]. +pub fn refuse_predetermined_lagged_latent_covariance_as_stationary_lagged_covariance( + predetermined_lagged_covariance: f64, + stationary_lagged_covariance: f64, ) -> Result { let _ = ( - initial_time_independent_carry, - initial_time_independent_effect, + predetermined_lagged_covariance, + stationary_lagged_covariance, ); - Err(PsychometricError::InitialTimeIndependentCarryIsNotInitialEffect) + Err(PsychometricError::PredeterminedLaggedLatentCovarianceIsNotStationaryLaggedCovariance) } -/// Refuse treating the Table 3 first-occasion shift as `CINT`. +/// Refuse treating predetermined lagged covariance as predetermined +/// later-occasion variance. /// -/// `t0_b z` is an initial-mean shift. `κ` is the continuous intercept. +/// `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::InitialTimeIndependentEffectIsNotContinuousIntercept`]. -pub fn refuse_initial_time_independent_effect_as_continuous_intercept( - initial_time_independent_effect: f64, - continuous_intercept: f64, +/// [`PsychometricError::PredeterminedLaggedLatentCovarianceIsNotLaterLatentVariance`]. +pub fn refuse_predetermined_lagged_latent_covariance_as_later_latent_variance( + predetermined_lagged_covariance: f64, + predetermined_later_variance: f64, ) -> Result { - let _ = (initial_time_independent_effect, continuous_intercept); - Err(PsychometricError::InitialTimeIndependentEffectIsNotContinuousIntercept) + let _ = ( + predetermined_lagged_covariance, + predetermined_later_variance, + ); + Err(PsychometricError::PredeterminedLaggedLatentCovarianceIsNotLaterLatentVariance) } -/// Refuse treating the Table 3 first-occasion shift as `M x`. +/// Refuse treating predetermined lagged covariance as the decayed +/// total. /// -/// 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. +/// 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::InitialTimeIndependentEffectIsNotTimeDependentImpulse`]. -pub fn refuse_initial_time_independent_effect_as_time_dependent_impulse( - initial_time_independent_effect: f64, - time_dependent_impulse: f64, +/// [`PsychometricError::PredeterminedLaggedLatentCovarianceIsNotDecayedTotal`]. +pub fn refuse_predetermined_lagged_latent_covariance_as_decayed_total( + predetermined_lagged_covariance: f64, + decayed_total: f64, ) -> Result { - let _ = (initial_time_independent_effect, time_dependent_impulse); - Err(PsychometricError::InitialTimeIndependentEffectIsNotTimeDependentImpulse) + let _ = (predetermined_lagged_covariance, decayed_total); + Err(PsychometricError::PredeterminedLaggedLatentCovarianceIsNotDecayedTotal) } -/// Refuse treating Driver Table 3 `T0TIPREDEFFECT` as the -/// first-occasion shift. +/// Refuse treating predetermined lagged covariance as free +/// first-occasion `T0VAR`. /// -/// `T0TIPREDEFFECT` is the coefficient. The shift is `t0_b z`. +/// `p_0` is the predetermined first-occasion state variance. +/// `e^{a Δt} p_0` is not `p_0`. /// /// # 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::PredeterminedLaggedLatentCovarianceIsNotInitialLatentVariance`]. +pub fn refuse_predetermined_lagged_latent_covariance_as_initial_latent_variance( + predetermined_lagged_covariance: f64, + initial_latent_variance: f64, ) -> Result { - let _ = ( - initial_time_independent_coefficient, - initial_time_independent_effect, - ); - Err(PsychometricError::InitialTimeIndependentCoefficientIsNotInitialEffect) + let _ = (predetermined_lagged_covariance, initial_latent_variance); + Err(PsychometricError::PredeterminedLaggedLatentCovarianceIsNotInitialLatentVariance) } -/// Exact scalar observed mean of a first-occasion time-independent -/// predictor. +/// Exact scalar Eq. 5 of lagged §4.3 predetermined `T0VAR`. /// -/// 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 +/// 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(τ, Ψ)`. 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. +/// `Γ ~ 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_discrete_latent_mean_with_initial_time_independent_predictor`] -/// and [`recover_manifest_observed_mean`]. +/// Propagates [`recover_predetermined_lagged_latent_covariance`] and +/// [`recover_manifest_lagged_observed_covariance`]. #[allow(clippy::too_many_arguments)] -pub fn recover_discrete_observed_mean_with_initial_time_independent_predictor( +pub fn recover_predetermined_lagged_observed_covariance( loading: f64, - initial_latent_mean: f64, + trait_variance: f64, + initial_latent_variance: f64, + time_independent_effect: f64, + predictor_variance: f64, log_rate: f64, - continuous_intercept: f64, - initial_time_independent_effect: f64, - time_independent_predictor: f64, - manifest_mean: f64, event_delta: f64, + manifest_trait_variance: 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 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 the evolved observed mean as the first-occasion -/// time-independent-predictor observed mean. +/// Refuse treating predetermined lagged covariance as predetermined +/// lagged observed covariance. /// -/// 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. +/// `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::EvolvedObservedMeanIsNotInitialTimeIndependentObservedMean`]. -pub fn refuse_evolved_observed_mean_as_initial_time_independent_observed_mean( - evolved_observed_mean: f64, - initial_time_independent_observed_mean: f64, +/// [`PsychometricError::PredeterminedLaggedLatentCovarianceIsNotObservedCovariance`]. +pub fn refuse_predetermined_lagged_latent_covariance_as_observed_covariance( + predetermined_lagged_latent_covariance: f64, + predetermined_lagged_observed_covariance: f64, ) -> Result { let _ = ( - evolved_observed_mean, - initial_time_independent_observed_mean, + predetermined_lagged_latent_covariance, + predetermined_lagged_observed_covariance, ); - Err(PsychometricError::EvolvedObservedMeanIsNotInitialTimeIndependentObservedMean) + Err(PsychometricError::PredeterminedLaggedLatentCovarianceIsNotObservedCovariance) } -/// Refuse treating the process-increment observed mean as the -/// first-occasion time-independent-predictor observed mean. +/// Refuse treating `MANIFESTVAR` as Eq. 5 of predetermined lagged +/// `T0VAR`. /// -/// 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. +/// Table 2 names `θ` `MANIFESTVAR`. Independent `ε_t` does not +/// enter `cov(y_t, y_{t-1})`. /// /// # 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::MeasurementErrorIsNotPredeterminedLaggedObservedCovariance`]. +pub fn refuse_measurement_error_as_predetermined_lagged_observed_covariance( + measurement_error_variance: f64, + predetermined_lagged_observed_covariance: f64, ) -> Result { let _ = ( - time_independent_observed_mean, - initial_time_independent_observed_mean, + measurement_error_variance, + predetermined_lagged_observed_covariance, ); - Err(PsychometricError::TimeIndependentObservedMeanIsNotInitialTimeIndependentObservedMean) + Err(PsychometricError::MeasurementErrorIsNotPredeterminedLaggedObservedCovariance) } -/// Refuse treating the contemporaneous-impulse observed mean as the -/// first-occasion time-independent-predictor observed mean. +/// Refuse treating Eq. 5 of predetermined later-occasion `T0VAR` as +/// predetermined lagged observed covariance. /// -/// 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. +/// `λ²(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::ImpulseObservedMeanIsNotInitialTimeIndependentObservedMean`]. -pub fn refuse_impulse_observed_mean_as_initial_time_independent_observed_mean( - impulse_observed_mean: f64, - initial_time_independent_observed_mean: f64, +/// [`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 _ = ( - impulse_observed_mean, - initial_time_independent_observed_mean, + predetermined_later_observed_variance, + predetermined_lagged_observed_covariance, ); - Err(PsychometricError::ImpulseObservedMeanIsNotInitialTimeIndependentObservedMean) + Err( + PsychometricError::PredeterminedLaterObservedVarianceIsNotPredeterminedLaggedObservedCovariance, + ) } -/// Refuse treating the impulse-carry observed mean as the -/// first-occasion time-independent-predictor observed mean. +/// Refuse treating Eq. 5 of lagged §4.3 stationary `T0VAR` as +/// predetermined lagged observed covariance. /// -/// 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. +/// `λ²(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::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::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 _ = ( - impulse_carry_observed_mean, - initial_time_independent_observed_mean, + stationary_lagged_observed_covariance, + predetermined_lagged_observed_covariance, ); - Err(PsychometricError::ImpulseCarryObservedMeanIsNotInitialTimeIndependentObservedMean) + Err( + PsychometricError::StationaryLaggedObservedCovarianceIsNotPredeterminedLaggedObservedCovariance, + ) } -/// Exact scalar first-occasion time-dependent predictor shift. +/// Exact scalar first-occasion variance of §4.3 predetermined `T0VAR`. /// -/// Driver, Oud, and Voelkle (2017, Table 3, p. 13; Eq. 3 first -/// summand, p. 5; JSS PDF re-opened 2026-08-20T19:10Z from +/// 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 /// ) -/// 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. +/// 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 /// -/// Returns [`PsychometricError::EventTimeRequired`] for any non-event -/// clock, [`PsychometricError::NonPositiveInterval`] when -/// `event_delta` is not strictly positive, and +/// 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 or the mapped carry overflows. -pub fn recover_initial_time_dependent_predictor_carry( - initial_time_dependent_effect: f64, - time_dependent_predictor: f64, +/// 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, - 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, + 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, )?; - 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()) + require_finite(trait_plus_state + added) } -/// Exact scalar evolved latent mean plus a first-occasion TD predictor. +/// Refuse treating predetermined first-occasion variance as +/// stationary first-occasion `T0VAR`. /// -/// 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. +/// `trait + p_0 + (B / a)² v` uses free `T0VAR`. +/// `trait + −q / (2 a) + (B / a)² v` uses the stationary +/// within-subject variance. /// /// # 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, +/// 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 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) + let _ = (predetermined_initial_variance, stationary_initial_variance); + Err(PsychometricError::PredeterminedInitialLatentVarianceIsNotStationaryInitialLatentVariance) } -/// Refuse treating the Table 3 first-occasion TD shift as `M x`. +/// Refuse treating predetermined first-occasion variance as free +/// first-occasion `T0VAR`. /// -/// `T0TDPREDEFFECT` shifts `η(t0)`. `TDPREDEFFECT` `M` enters the -/// SDE as the contemporaneous Dirac `M x` at `u = t`. +/// `p_0` is the predetermined first-occasion state variance. +/// `trait + p_0 + (B / a)² v` is not `p_0`. /// /// # 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::PredeterminedInitialLatentVarianceIsNotInitialLatentVariance`]. +pub fn refuse_predetermined_initial_latent_variance_as_initial_latent_variance( + predetermined_initial_variance: f64, + initial_latent_variance: f64, ) -> Result { - let _ = (initial_time_dependent_effect, time_dependent_impulse); - Err(PsychometricError::InitialTimeDependentEffectIsNotContemporaneousImpulse) + let _ = (predetermined_initial_variance, initial_latent_variance); + Err(PsychometricError::PredeterminedInitialLatentVarianceIsNotInitialLatentVariance) } -/// Refuse treating the Eq. 3 carry of `T0TDPREDEFFECT` as the -/// first-occasion shift. +/// Refuse treating predetermined first-occasion variance as +/// predetermined lagged covariance. /// -/// `e^{A Δt} t0_m x0` is the first summand's contribution at `t`. -/// `t0_m x0` is the shift at `T0`. +/// `e^{a Δt} p_0` decays the state. First-occasion variance is +/// contemporaneous `p_0`. /// /// # 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::PredeterminedInitialLatentVarianceIsNotLaggedLatentCovariance`]. +pub fn refuse_predetermined_initial_latent_variance_as_lagged_latent_covariance( + predetermined_initial_variance: f64, + predetermined_lagged_covariance: f64, ) -> Result { - let _ = (initial_time_dependent_carry, initial_time_dependent_effect); - Err(PsychometricError::InitialTimeDependentCarryIsNotInitialEffect) + let _ = ( + predetermined_initial_variance, + predetermined_lagged_covariance, + ); + Err(PsychometricError::PredeterminedInitialLatentVarianceIsNotLaggedLatentCovariance) } -/// Refuse treating the Table 3 first-occasion TD shift as `CINT`. +/// Refuse treating predetermined first-occasion variance as +/// predetermined later-occasion variance. /// -/// `t0_m x0` is an initial-mean shift. `κ` is the continuous intercept. +/// 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::InitialTimeDependentEffectIsNotContinuousIntercept`]. -pub fn refuse_initial_time_dependent_effect_as_continuous_intercept( - initial_time_dependent_effect: f64, - continuous_intercept: f64, +/// [`PsychometricError::PredeterminedInitialLatentVarianceIsNotLaterLatentVariance`]. +pub fn refuse_predetermined_initial_latent_variance_as_later_latent_variance( + predetermined_initial_variance: f64, + predetermined_later_variance: f64, ) -> Result { - let _ = (initial_time_dependent_effect, continuous_intercept); - Err(PsychometricError::InitialTimeDependentEffectIsNotContinuousIntercept) + let _ = (predetermined_initial_variance, predetermined_later_variance); + Err(PsychometricError::PredeterminedInitialLatentVarianceIsNotLaterLatentVariance) } -/// Refuse treating the Table 3 first-occasion TD shift as the Eq. 3 -/// process increment. +/// Exact scalar Eq. 5 of first-occasion §4.3 predetermined `T0VAR`. /// -/// `t0_m x0` shifts `η(t0)`. `TIPREDEFFECT` `B` maps as -/// `A^{-1}[e^{A Δt} − I] B z`. +/// 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::InitialTimeDependentEffectIsNotProcessIncrement`]. -pub fn refuse_initial_time_dependent_effect_as_process_increment( - initial_time_dependent_effect: f64, - time_independent_increment: f64, +/// [`PsychometricError::PredeterminedInitialLatentVarianceIsNotObservedVariance`]. +pub fn refuse_predetermined_initial_latent_variance_as_observed_variance( + predetermined_initial_latent_variance: f64, + predetermined_initial_observed_variance: f64, ) -> Result { - let _ = (initial_time_dependent_effect, time_independent_increment); - Err(PsychometricError::InitialTimeDependentEffectIsNotProcessIncrement) + let _ = ( + predetermined_initial_latent_variance, + predetermined_initial_observed_variance, + ); + Err(PsychometricError::PredeterminedInitialLatentVarianceIsNotObservedVariance) } -/// Refuse treating the Table 3 first-occasion TD shift as the Table 3 -/// first-occasion TI shift. +/// Refuse treating `MANIFESTVAR` as Eq. 5 of predetermined +/// first-occasion `T0VAR`. /// -/// `T0TDPREDEFFECT` and `T0TIPREDEFFECT` are different Table 3 -/// matrices. `t0_m x0` is not `t0_b z`. +/// Table 2 names `θ` `MANIFESTVAR`. `θ` is not +/// `λ²(trait + p_0 + (B / a)² v) + θ + ψ`. /// /// # 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::MeasurementErrorIsNotPredeterminedInitialObservedVariance`]. +pub fn refuse_measurement_error_as_predetermined_initial_observed_variance( + measurement_error_variance: f64, + predetermined_initial_observed_variance: f64, ) -> Result { let _ = ( - initial_time_dependent_effect, - initial_time_independent_effect, + measurement_error_variance, + predetermined_initial_observed_variance, ); - Err(PsychometricError::InitialTimeDependentEffectIsNotInitialTimeIndependentEffect) + Err(PsychometricError::MeasurementErrorIsNotPredeterminedInitialObservedVariance) } -/// Refuse treating Driver Table 3 `T0TDPREDEFFECT` as the -/// first-occasion shift. +/// Refuse treating Eq. 5 of §4.3 stationary `T0VAR` as predetermined +/// first-occasion observed variance. /// -/// `T0TDPREDEFFECT` is the coefficient. The shift is `t0_m x0`. +/// `λ²(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::InitialTimeDependentCoefficientIsNotInitialEffect`]. -pub fn refuse_initial_time_dependent_coefficient_as_initial_effect( - initial_time_dependent_coefficient: f64, - initial_time_dependent_effect: f64, +/// [`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 _ = ( - initial_time_dependent_coefficient, - initial_time_dependent_effect, + stationary_initial_observed_variance, + predetermined_initial_observed_variance, ); - Err(PsychometricError::InitialTimeDependentCoefficientIsNotInitialEffect) + Err( + PsychometricError::StationaryInitialObservedVarianceIsNotPredeterminedInitialObservedVariance, + ) } -/// Refuse treating the Eq. 3 carry of `T0TDPREDEFFECT` as the -/// within-interval impulse carry. +/// Refuse treating Eq. 5 of predetermined later-occasion `T0VAR` as +/// predetermined first-occasion observed variance. /// -/// `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. +/// `λ²(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::InitialTimeDependentCarryIsNotImpulseCarry`]. -pub fn refuse_initial_time_dependent_carry_as_impulse_carry( - initial_time_dependent_carry: f64, - impulse_carry: f64, +/// [`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 _ = (initial_time_dependent_carry, impulse_carry); - Err(PsychometricError::InitialTimeDependentCarryIsNotImpulseCarry) + let _ = ( + predetermined_later_observed_variance, + predetermined_initial_observed_variance, + ); + Err( + PsychometricError::PredeterminedLaterObservedVarianceIsNotPredeterminedInitialObservedVariance, + ) } -/// Exact scalar observed mean of a first-occasion time-dependent -/// predictor. +/// Exact scalar observed mean of a 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-20T19:20Z from +/// 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(τ, Ψ)`. 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. +/// `Γ ~ 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_initial_time_dependent_predictor`] +/// [`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_initial_time_dependent_predictor( +pub fn recover_discrete_observed_mean_with_time_independent_predictor( loading: f64, 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, manifest_mean: f64, event_delta: f64, clock: LagClock, ) -> Result { - let composed_latent_mean = recover_discrete_latent_mean_with_initial_time_dependent_predictor( + let composed_latent_mean = recover_discrete_latent_mean_with_time_independent_predictor( initial_latent_mean, log_rate, continuous_intercept, - initial_time_dependent_effect, - time_dependent_predictor, + 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-dependent-predictor observed 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 Table 3 first-occasion TD predictor is -/// `τ + λ(μ_t + e^{a Δt} t0_m x0)`. Those are not the same map. +/// 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::EvolvedObservedMeanIsNotInitialTimeDependentObservedMean`]. -pub fn refuse_evolved_observed_mean_as_initial_time_dependent_observed_mean( +/// [`PsychometricError::EvolvedObservedMeanIsNotTimeIndependentObservedMean`]. +pub fn refuse_evolved_observed_mean_as_time_independent_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) + let _ = (evolved_observed_mean, time_independent_observed_mean); + Err(PsychometricError::EvolvedObservedMeanIsNotTimeIndependentObservedMean) } /// Refuse treating the contemporaneous-impulse observed mean as the -/// first-occasion time-dependent-predictor observed mean. +/// time-independent-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. +/// 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::ImpulseObservedMeanIsNotInitialTimeDependentObservedMean`]. -pub fn refuse_impulse_observed_mean_as_initial_time_dependent_observed_mean( +/// [`PsychometricError::ImpulseObservedMeanIsNotTimeIndependentObservedMean`]. +pub fn refuse_impulse_observed_mean_as_time_independent_observed_mean( impulse_observed_mean: f64, - initial_time_dependent_observed_mean: f64, + time_independent_observed_mean: f64, ) -> Result { - let _ = (impulse_observed_mean, initial_time_dependent_observed_mean); - Err(PsychometricError::ImpulseObservedMeanIsNotInitialTimeDependentObservedMean) + let _ = (impulse_observed_mean, time_independent_observed_mean); + Err(PsychometricError::ImpulseObservedMeanIsNotTimeIndependentObservedMean) } /// Refuse treating the impulse-carry observed mean as the -/// first-occasion time-dependent-predictor observed mean. +/// 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 TD predictor is `τ + λ(μ_t + e^{a Δt} t0_m x0)`. -/// Those are not the same map. +/// `τ + λ(μ_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::ImpulseCarryObservedMeanIsNotInitialTimeDependentObservedMean`]. -pub fn refuse_impulse_carry_observed_mean_as_initial_time_dependent_observed_mean( +/// [`PsychometricError::ImpulseCarryObservedMeanIsNotTimeIndependentObservedMean`]. +pub fn refuse_impulse_carry_observed_mean_as_time_independent_observed_mean( impulse_carry_observed_mean: f64, - initial_time_dependent_observed_mean: f64, + time_independent_observed_mean: f64, ) -> Result { - let _ = ( - impulse_carry_observed_mean, - initial_time_dependent_observed_mean, - ); - Err(PsychometricError::ImpulseCarryObservedMeanIsNotInitialTimeDependentObservedMean) + let _ = (impulse_carry_observed_mean, time_independent_observed_mean); + Err(PsychometricError::ImpulseCarryObservedMeanIsNotTimeIndependentObservedMean) } -/// Refuse treating the first-occasion TI observed mean as the -/// first-occasion TD observed mean. +/// Exact scalar first-occasion time-independent predictor shift. /// -/// 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. +/// 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 /// -/// 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, +/// 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 { - let _ = ( - initial_time_independent_observed_mean, - initial_time_dependent_observed_mean, - ); - Err(PsychometricError::InitialTimeIndependentObservedMeanIsNotInitialTimeDependentObservedMean) + 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 within-interval time-dependent impulse carry from -/// Driver Equations 1–2. +/// Exact scalar carried first-occasion time-independent predictor. /// -/// 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. +/// 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` or `elapsed_after_impulse` is not strictly positive -/// or the impulse is not strictly inside `(t0, t)`, and +/// `event_delta` is not strictly positive, 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, +/// 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, - elapsed_after_impulse: f64, clock: LagClock, ) -> Result { if !clock.admits_structural_lag() { @@ -5021,60 +5123,53 @@ pub fn recover_time_dependent_predictor_impulse_carry( 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 { + 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 * elapsed_after_impulse; + let drift_interval = log_rate * event_delta; let auto_effect = drift_interval.exp(); if auto_effect.is_finite() { - // +0 underflow is vanishing dissipation (§7.2). - return require_finite(auto_effect * impulse); + // +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−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()) + // 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 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. +/// Exact scalar evolved latent mean plus a first-occasion TI predictor. /// -/// # Errors +/// 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_time_dependent_predictor_impulse_carry`], and returns +/// [`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_impulse_carry( +pub fn recover_discrete_latent_mean_with_initial_time_independent_predictor( initial_latent_mean: f64, log_rate: f64, continuous_intercept: f64, - time_dependent_effect: f64, - time_dependent_predictor: f64, + initial_time_independent_effect: f64, + time_independent_predictor: f64, event_delta: f64, - elapsed_after_impulse: f64, clock: LagClock, ) -> Result { let evolved_latent_mean = recover_discrete_latent_mean( @@ -5084,596 +5179,1425 @@ pub fn recover_discrete_latent_mean_with_impulse_carry( event_delta, clock, )?; - let impulse_carry = recover_time_dependent_predictor_impulse_carry( - time_dependent_effect, - time_dependent_predictor, + let initial_carry = recover_initial_time_independent_predictor_carry( + initial_time_independent_effect, + time_independent_predictor, log_rate, event_delta, - elapsed_after_impulse, clock, )?; - if impulse_carry == 0.0 { + if initial_carry == 0.0 { return Ok(evolved_latent_mean); } if evolved_latent_mean == 0.0 { - return Ok(impulse_carry); + return Ok(initial_carry); } - require_finite(evolved_latent_mean + impulse_carry) + require_finite(evolved_latent_mean + initial_carry) } -/// Exact scalar observed mean of a within-interval impulse carry. +/// Refuse treating the Table 3 first-occasion shift as the Eq. 3 +/// process 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. +/// `T0TIPREDEFFECT` shifts `η(t0)`. `TIPREDEFFECT` `B` enters the +/// SDE and maps as `A^{-1}[e^{A Δt} − I] B z`. /// /// # 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::InitialTimeIndependentEffectIsNotProcessIncrement`]. +pub fn refuse_initial_time_independent_effect_as_process_increment( + initial_time_independent_effect: f64, + time_independent_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 _ = (initial_time_independent_effect, time_independent_increment); + Err(PsychometricError::InitialTimeIndependentEffectIsNotProcessIncrement) } -/// Refuse treating the evolved observed mean as the impulse-carry -/// observed mean. +/// Refuse treating the Eq. 3 carry of `T0TIPREDEFFECT` as the +/// first-occasion shift. /// -/// 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. +/// `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::EvolvedObservedMeanIsNotImpulseCarryObservedMean`]. -pub fn refuse_evolved_observed_mean_as_impulse_carry_observed_mean( - evolved_observed_mean: f64, - impulse_carry_observed_mean: f64, +/// [`PsychometricError::InitialTimeIndependentCarryIsNotInitialEffect`]. +pub fn refuse_initial_time_independent_carry_as_initial_effect( + initial_time_independent_carry: f64, + initial_time_independent_effect: f64, ) -> Result { - let _ = (evolved_observed_mean, impulse_carry_observed_mean); - Err(PsychometricError::EvolvedObservedMeanIsNotImpulseCarryObservedMean) + let _ = ( + initial_time_independent_carry, + initial_time_independent_effect, + ); + Err(PsychometricError::InitialTimeIndependentCarryIsNotInitialEffect) } -/// Refuse treating the Eq. 1–2 impulse carry as the contemporaneous Dirac. +/// Refuse treating the Table 3 first-occasion shift as `CINT`. /// -/// 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`. +/// `t0_b z` is an initial-mean shift. `κ` is the continuous intercept. /// /// # 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::InitialTimeIndependentEffectIsNotContinuousIntercept`]. +pub fn refuse_initial_time_independent_effect_as_continuous_intercept( + initial_time_independent_effect: f64, + continuous_intercept: f64, ) -> Result { - let _ = (time_dependent_impulse_carry, time_dependent_impulse); - Err(PsychometricError::TimeDependentImpulseCarryIsNotContemporaneousImpulse) + let _ = (initial_time_independent_effect, continuous_intercept); + Err(PsychometricError::InitialTimeIndependentEffectIsNotContinuousIntercept) } -/// Refuse treating the Eq. 1–2 impulse carry as `CINT`. +/// Refuse treating the Table 3 first-occasion shift as `M x`. /// -/// Table 2 names `M` `TDPREDEFFECT` and `κ` `CINT`. The dissipated -/// impulse is not the continuous intercept. +/// 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::TimeDependentImpulseCarryIsNotContinuousIntercept`]. -pub fn refuse_time_dependent_impulse_carry_as_continuous_intercept( - time_dependent_impulse_carry: f64, - continuous_intercept: f64, +/// [`PsychometricError::InitialTimeIndependentEffectIsNotTimeDependentImpulse`]. +pub fn refuse_initial_time_independent_effect_as_time_dependent_impulse( + initial_time_independent_effect: f64, + time_dependent_impulse: f64, ) -> Result { - let _ = (time_dependent_impulse_carry, continuous_intercept); - Err(PsychometricError::TimeDependentImpulseCarryIsNotContinuousIntercept) + let _ = (initial_time_independent_effect, time_dependent_impulse); + Err(PsychometricError::InitialTimeIndependentEffectIsNotTimeDependentImpulse) } -/// Refuse treating the Eq. 1–2 impulse carry as `TIPREDEFFECT`. +/// Refuse treating Driver Table 3 `T0TIPREDEFFECT` as the +/// first-occasion shift. /// -/// The second-summand map integrates a constant `B z` over the event -/// interval. The within-interval TDPRED carry dissipates a Dirac. +/// `T0TIPREDEFFECT` is the coefficient. The shift is `t0_b z`. /// /// # 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::InitialTimeIndependentCoefficientIsNotInitialEffect`]. +pub fn refuse_initial_time_independent_coefficient_as_initial_effect( + initial_time_independent_coefficient: f64, + initial_time_independent_effect: f64, ) -> Result { - let _ = (time_dependent_impulse_carry, time_independent_effect); - Err(PsychometricError::TimeDependentImpulseCarryIsNotTimeIndependentEffect) + let _ = ( + initial_time_independent_coefficient, + initial_time_independent_effect, + ); + Err(PsychometricError::InitialTimeIndependentCoefficientIsNotInitialEffect) } -/// Refuse treating the Eq. 1–2 impulse carry as Voelkle et al. -/// (2012, Eq. 14). +/// Exact scalar observed mean of a first-occasion time-independent +/// predictor. /// -/// Equation 14 is `a_{yx} Δt` for a piecewise-constant time-varying -/// predictor. The Dirac carry is `e^{A(t−u)} 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-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 /// -/// 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, +/// 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 _ = (time_dependent_impulse_carry, time_varying_discrete_effect); - Err(PsychometricError::TimeDependentImpulseCarryIsNotTimeVaryingDiscreteEffect) + 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 Driver Table 2 `T0MEANS` as the evolved latent mean. +/// Refuse treating the evolved observed mean as the first-occasion +/// time-independent-predictor observed mean. /// -/// Equation 3 maps `μ_t = exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`. -/// `T0MEANS` is `μ_0`, not `μ_t`. +/// 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::InitialLatentMeanIsNotEvolvedMean`]. -pub fn refuse_initial_latent_mean_as_evolved_mean( - initial_latent_mean: f64, - evolved_latent_mean: f64, +/// [`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 _ = (initial_latent_mean, evolved_latent_mean); - Err(PsychometricError::InitialLatentMeanIsNotEvolvedMean) + let _ = ( + evolved_observed_mean, + initial_time_independent_observed_mean, + ); + Err(PsychometricError::EvolvedObservedMeanIsNotInitialTimeIndependentObservedMean) } -/// Refuse treating Driver Table 2 `CINT` as the discrete mean increment. +/// Refuse treating the process-increment observed mean as the +/// first-occasion time-independent-predictor observed mean. /// -/// `κ` is the continuous intercept. Equation 3 maps it through -/// `A^{-1}[e^{A Δt} − I]`. `κ` is not that increment. +/// 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::ContinuousInterceptIsNotDiscreteMeanIncrement`]. -pub fn refuse_continuous_intercept_as_discrete_mean_increment( - continuous_intercept: f64, - discrete_mean_increment: f64, +/// [`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 _ = (continuous_intercept, discrete_mean_increment); - Err(PsychometricError::ContinuousInterceptIsNotDiscreteMeanIncrement) + let _ = ( + time_independent_observed_mean, + initial_time_independent_observed_mean, + ); + Err(PsychometricError::TimeIndependentObservedMeanIsNotInitialTimeIndependentObservedMean) } -/// Refuse treating Driver Table 2 `CINT` as `T0MEANS`. +/// Refuse treating the contemporaneous-impulse observed mean as the +/// first-occasion time-independent-predictor observed mean. /// -/// Table 2 (p. 12) names `κ` `CINT` and the first-occasion latent -/// mean `T0MEANS`. `κ` is not `E(η_{i1})`. +/// 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::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, +/// [`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 _ = (process_noise, prior_variance); - Err(PsychometricError::ProcessNoiseIsConditionalVariance) + let _ = ( + impulse_observed_mean, + initial_time_independent_observed_mean, + ); + Err(PsychometricError::ImpulseObservedMeanIsNotInitialTimeIndependentObservedMean) } -/// Refuse the difference quotient as a continuous-time rate. +/// Refuse treating the impulse-carry observed mean as the +/// first-occasion time-independent-predictor observed mean. /// -/// Voelkle et al. (2012) discourage `(x(t+Δt) − x(t)) / Δt` as the drift. +/// 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::DifferenceQuotientForbidden`]. -pub fn refuse_difference_quotient_as_local_rate( - earlier: f64, - later: f64, - delta: f64, +/// 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 _ = (earlier, later, delta); - Err(PsychometricError::DifferenceQuotientForbidden) + let _ = ( + impulse_carry_observed_mean, + initial_time_independent_observed_mean, + ); + Err(PsychometricError::ImpulseCarryObservedMeanIsNotInitialTimeIndependentObservedMean) } -/// Mean local log-rate across consecutive event-time pairs. +/// Exact scalar first-occasion time-dependent predictor shift. /// -/// Occasions are sorted by event time. Each pair uses the exact scalar map. -/// Equal or inverted times fail closed. +/// 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::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, +/// 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 !clock.admits_structural_lag() { - return Err(PsychometricError::EventTimeRequired); - } - if occasions.len() < 2 { + if !initial_time_dependent_effect.is_finite() || !time_dependent_predictor.is_finite() { 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); + if initial_time_dependent_effect == 0.0 || time_dependent_predictor == 0.0 { + return Ok(0.0); } - let count = rates.len() as f64; - require_finite(rates.iter().sum::() / count) + require_finite(initial_time_dependent_effect * time_dependent_predictor) } -/// 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. +/// Exact scalar carried first-occasion time-dependent predictor. /// -/// 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`]. +/// 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 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], +/// 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 rows.len() < 2 { + if !event_delta.is_finite() || event_delta <= 0.0 { + return Err(PsychometricError::NonPositiveInterval); + } + if !log_rate.is_finite() { 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); + 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); } - if groups.len() < 2 { - return Err(PsychometricError::InsufficientClusters); + 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 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)); - } + if !drift_interval.is_finite() { + return Err(PsychometricError::InvalidNumericInput); } - fit_scalar_log_rate(&pairs) + // 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()) } -/// Mean exact scalar log-rate on already-centered residuals with irregular intervals. +/// Exact scalar evolved latent mean plus a first-occasion TD predictor. /// -/// 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, 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 /// -/// 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], +/// 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 { - if !clock.admits_structural_lag() { - return Err(PsychometricError::EventTimeRequired); - } - if pairs.is_empty() { - return Err(PsychometricError::InvalidNumericInput); + 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); } - 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; + if evolved_latent_mean == 0.0 { + return Ok(initial_carry); } - let count = pairs.len() as f64; - require_finite(sum / count) + require_finite(evolved_latent_mean + initial_carry) } -/// Least-squares scalar log-rate for already-formed residual pairs. +/// Refuse treating the Table 3 first-occasion TD shift as `M x`. /// -/// 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) +/// `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) } -#[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 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_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_predetermined_initial_latent_variance, + recover_predetermined_initial_observed_variance, + recover_predetermined_lagged_latent_covariance, + recover_predetermined_lagged_observed_covariance, + recover_predetermined_later_latent_variance, recover_predetermined_later_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_predetermined_initial_observed_variance, + refuse_measurement_error_as_predetermined_lagged_observed_covariance, + refuse_measurement_error_as_predetermined_later_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_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_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_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_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_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_within_subject_observed_variance_as_stationary_initial_observed_variance, refuse_time_dependent_impulse_as_continuous_intercept, refuse_time_dependent_impulse_as_time_independent_effect, @@ -5695,1770 +6619,2724 @@ mod tests { use crate::error::PsychometricError; #[test] - fn exact_scalar_map_inverts_exponential_drift() { - let drift = -0.5_f64; + 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 earlier = 1.5_f64; - let later = earlier * (drift * delta).exp(); - let recovered = recover_event_time_discrete_lag_and_log_rate( - earlier, - later, + let recovered = recover_discrete_time_varying_predictor_effect( + outcome_on_predictor, + delta, + delta, 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); + .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 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); + 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_pooled_discrete_lag_across_unequal_intervals(source_delta, reference_delta), - Err(PsychometricError::UnequalIntervalPoolingForbidden) + refuse_process_noise_as_unconditional_variance(process_noise, prior), + Err(PsychometricError::ProcessNoiseIsConditionalVariance) ); assert_eq!( - refuse_pooled_discrete_lag_across_unequal_intervals(source_delta, source_delta), - Err(PsychometricError::UnequalIntervalPoolingForbidden) + 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 forward_map_and_interval_remap_fail_closed() { + fn lagged_covariance_and_latent_variance_overflow_paths_fail_closed() { assert_eq!( - recover_discrete_lag_from_log_rate(-0.2, 1.0, LagClock::SystemTime), - Err(PsychometricError::EventTimeRequired) + recover_discrete_lagged_latent_covariance(1e308, 800.0, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_lag_from_log_rate(-0.2, 0.0, LagClock::EventTime), + 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_lag_from_log_rate(-0.2, -1.0, LagClock::EventTime), + recover_discrete_lagged_latent_covariance(2.0, -0.5, -1.0, LagClock::EventTime), Err(PsychometricError::NonPositiveInterval) ); assert_eq!( - recover_discrete_lag_from_log_rate(-0.2, f64::NAN, LagClock::EventTime), + recover_discrete_lagged_latent_covariance(2.0, -0.5, f64::NAN, LagClock::EventTime), Err(PsychometricError::NonPositiveInterval) ); assert_eq!( - recover_discrete_lag_from_log_rate(f64::NAN, 1.0, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) + recover_discrete_lagged_latent_covariance(2.0, -0.5, 1.0, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) ); assert_eq!( - recover_discrete_lag_from_log_rate(800.0, 10.0, LagClock::EventTime), + recover_discrete_latent_variance(-0.1, 0.4, -0.5, 1.0, LagClock::EventTime), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_lag_from_log_rate(-800.0, 1.0, LagClock::EventTime), + recover_discrete_latent_variance(f64::NAN, 0.4, -0.5, 1.0, LagClock::EventTime), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_lag_from_log_rate(-1.0, 800.0, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) + 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) ); - 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) + recover_stationary_latent_variance(0.0, 0.0, LagClock::EventTime), + Err(PsychometricError::StationaryVarianceRequiresStableDrift) ); assert_eq!( - map_discrete_lag_across_event_intervals(0.5, 1.0, 2.0, LagClock::AssertionTime), - Err(PsychometricError::EventTimeRequired) + recover_stationary_latent_variance(-0.1, -0.5, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - map_discrete_lag_across_event_intervals(0.5, 0.0, 2.0, LagClock::EventTime), - Err(PsychometricError::NonPositiveInterval) + recover_stationary_latent_variance(f64::NAN, -0.5, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - map_discrete_lag_across_event_intervals(0.5, 1.0, 0.0, LagClock::EventTime), - Err(PsychometricError::NonPositiveInterval) + 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!( - map_discrete_lag_across_event_intervals(-0.2, 1.0, 2.0, LagClock::EventTime), + recover_stationary_latent_variance(1e308, -1e-10, 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, + 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("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); + .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_discrete_constant_predictor_effect( - outcome_on_predictor, - predictor_log_rate, - delta, - LagClock::SystemTime - ), - Err(PsychometricError::EventTimeRequired) + recover_trait_plus_state_latent_variance(0.0, state), + Ok(state) ); assert_eq!( - recover_discrete_constant_predictor_effect( - outcome_on_predictor, - predictor_log_rate, + 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 ), - Err(PsychometricError::NonPositiveInterval) + Ok(state_lagged) ); assert_eq!( - recover_discrete_constant_predictor_effect( - outcome_on_predictor, - predictor_log_rate, - -1.0, + recover_trait_plus_state_lagged_covariance( + trait_variance, + 0.0, + drift, + delta, LagClock::EventTime ), - Err(PsychometricError::NonPositiveInterval) + Ok(trait_variance) ); + let process_noise = + recover_discrete_process_noise(diffusion, drift, delta, LagClock::EventTime) + .expect("q_dt"); assert_eq!( - recover_discrete_constant_predictor_effect( - outcome_on_predictor, - predictor_log_rate, - f64::NAN, - LagClock::EventTime - ), - Err(PsychometricError::NonPositiveInterval) + refuse_trait_variance_as_process_noise(trait_variance, process_noise), + Err(PsychometricError::TraitVarianceIsNotProcessNoise) ); assert_eq!( - recover_discrete_constant_predictor_effect( - f64::NAN, - predictor_log_rate, - delta, - LagClock::EventTime - ), + 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_discrete_constant_predictor_effect( - outcome_on_predictor, - 0.0, - delta, - LagClock::EventTime - ), + recover_trait_plus_state_latent_variance(0.4, -0.1), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_constant_predictor_effect( - outcome_on_predictor, + 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, - delta, + 0.4, + -0.5, + 1.0, LagClock::EventTime ), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_constant_predictor_effect(1e300, 1e-300, 1e300, LagClock::EventTime), + 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) ); - 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()); + 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 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); + fn invalid_lag_inputs_fail_closed() { assert_eq!( - recover_discrete_constant_predictor_effect(0.0, 800.0, 1.0, LagClock::EventTime), - Ok(0.0) + 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_discrete_constant_predictor_effect(0.0, 1e308, 2.0, LagClock::EventTime), - Ok(0.0) + recover_local_log_rate(0.5, f64::NAN, LagClock::EventTime), + Err(PsychometricError::NonPositiveInterval) ); assert_eq!( - recover_discrete_constant_predictor_effect(1.0, 800.0, 1.0, LagClock::EventTime), + recover_local_log_rate(0.0, 1.0, LagClock::EventTime), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_constant_predictor_effect(1.0, 1e308, 2.0, LagClock::EventTime), + recover_local_log_rate(-0.2, 1.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), + recover_local_log_rate(f64::NAN, 1.0, 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; + 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_discrete_time_varying_predictor_effect( - outcome_on_predictor, - delta, - delta, - delta, - LagClock::SystemTime - ), + recover_event_series_mean_log_rate(&occasions, 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) + recover_event_series_mean_log_rate(&[], LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_time_varying_predictor_effect( - outcome_on_predictor, - 1.0, - f64::NAN, - 1.0, + recover_event_series_mean_log_rate( + &[EventOccasion { + event_time: 0.0, + score: 1.0, + }], LagClock::EventTime ), - Err(PsychometricError::NonPositiveInterval) + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_time_varying_predictor_effect( - outcome_on_predictor, - 1.0, - 1.0, - 0.0, + 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::NonPositiveInterval) + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_time_varying_predictor_effect( - outcome_on_predictor, - 1.0, - 2.0, - 2.0, + recover_event_series_mean_log_rate( + &[occasion(0.0, 1.0), occasion(f64::NAN, 0.5)], LagClock::EventTime ), - Err(PsychometricError::UnmatchedTimeVaryingInterval) + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_time_varying_predictor_effect( - outcome_on_predictor, - 2.0, - 2.0, - 1.0, + recover_event_series_mean_log_rate( + &[occasion(0.0, f64::NAN), occasion(1.0, 0.5)], LagClock::EventTime ), - Err(PsychometricError::UnmatchedTimeVaryingInterval) + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_time_varying_predictor_effect( - f64::NAN, - delta, - delta, - delta, + recover_event_series_mean_log_rate( + &[occasion(0.0, 1.0), occasion(1.0, f64::NAN)], LagClock::EventTime ), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_time_varying_predictor_effect( - 1e308, - 10.0, - 10.0, - 10.0, + 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::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) + 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 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); + 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 discrete_process_noise_invalid_inputs_fail_closed() { + fn within_residual_invalid_rows_fail_closed() { + let rows = decaying_clustered_scores(-0.25); assert_eq!( - recover_discrete_process_noise(0.4, -0.5, 1.0, LagClock::SystemTime), + recover_within_residual_event_time_log_rate(&rows, 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) + recover_within_residual_event_time_log_rate(&[], LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_process_noise(-0.1, -0.5, 1.0, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) + 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_discrete_process_noise(f64::NAN, -0.5, 1.0, LagClock::EventTime), + 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_discrete_process_noise(0.4, f64::NAN, 1.0, LagClock::EventTime), + recover_event_series_mean_log_rate( + &[occasion(0.0, 1.0), occasion(1.0, f64::NAN)], + LagClock::EventTime + ), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_process_noise(1.0, 800.0, 1.0, LagClock::EventTime), + 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_discrete_process_noise(1.0, 1e308, 2.0, LagClock::EventTime), + recover_within_residual_event_time_log_rate( + &[clustered(1, 0.0, 1.0), clustered(2, 1.0, f64::INFINITY)], + 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) + 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 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); + 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 lagged_covariance_and_latent_variance_overflow_paths_fail_closed() { + fn irregular_centered_residuals_fail_closed() { + let ok = lagged(1.0, 0.8, 1.0); assert_eq!( - recover_discrete_lagged_latent_covariance(1e308, 800.0, 1.0, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) + recover_irregular_centered_residual_log_rate(&[ok], LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) ); assert_eq!( - recover_discrete_lagged_latent_covariance(2.0, 1e308, 2.0, LagClock::EventTime), + recover_irregular_centered_residual_log_rate(&[], LagClock::EventTime), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_latent_variance(1e308, 1e-308, 400.0, 1.0, LagClock::EventTime), + recover_irregular_centered_residual_log_rate( + &[lagged(f64::NAN, 0.8, 1.0)], + LagClock::EventTime + ), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_latent_variance(2.0, 1.0, 1e308, 2.0, LagClock::EventTime), + recover_irregular_centered_residual_log_rate( + &[lagged(1.0, f64::INFINITY, 1.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), + recover_irregular_centered_residual_log_rate( + &[lagged(1.0, 0.8, f64::NAN)], + LagClock::EventTime + ), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_lagged_latent_covariance(1e308, 700.0, 1.0, LagClock::EventTime), + recover_irregular_centered_residual_log_rate( + &[lagged(0.0, 0.8, 1.0)], + LagClock::EventTime + ), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_latent_variance(1e308, 1e-308, 350.0, 1.0, LagClock::EventTime), + recover_irregular_centered_residual_log_rate( + &[lagged(1.0, -0.8, 1.0)], + LagClock::EventTime + ), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_latent_variance(1e308, 1e308, 0.0, 1.0, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) + recover_irregular_centered_residual_log_rate( + &[lagged(1.0, 0.8, 0.0)], + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) ); - 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, + recover_irregular_centered_residual_log_rate( + &[lagged(1.0, 0.8, -0.5)], LagClock::EventTime ), - Err(PsychometricError::InvalidNumericInput) + 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_discrete_lagged_latent_covariance(-0.1, -0.5, 1.0, LagClock::EventTime), + 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_discrete_lagged_latent_covariance(f64::NAN, -0.5, 1.0, LagClock::EventTime), + 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!( - recover_discrete_lagged_latent_covariance(2.0, f64::NAN, 1.0, LagClock::EventTime), + fit_scalar_log_rate(&[(1e-300, 1.0, 1e-8), (1.0, 1.0, 1.0)]), 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) + 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!( - recover_discrete_latent_variance(-0.1, 0.4, -0.5, 1.0, LagClock::EventTime), + 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!( - recover_discrete_latent_variance(f64::NAN, 0.4, -0.5, 1.0, LagClock::EventTime), + fit_scalar_log_rate(&[(1e154, 1e154, 1.0)]), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_latent_variance(2.0, 0.4, -0.5, 1.0, LagClock::SystemTime), - Err(PsychometricError::EventTimeRequired) + fit_scalar_log_rate(&[(1.0, 1e-300, 1.0), (1.0, 1e-300, 2.0)]), + Err(PsychometricError::InvalidNumericInput) ); } #[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) - ); + fn one_sided_residual_overflow_and_nonfinite_interval_fail_closed() { assert_eq!( - refuse_finite_interval_process_noise_as_stationary_variance(recovered, 1.0), - Err(PsychometricError::FiniteIntervalProcessNoiseIsNotStationary) + 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_stationary_latent_variance(0.0, drift, LagClock::EventTime), - Ok(0.0) + 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) ); - // 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) - ); + 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!( - recover_stationary_latent_variance(0.0, 0.0, LagClock::EventTime), - Err(PsychometricError::StationaryVarianceRequiresStableDrift) + refuse_measurement_error_as_observed_variance(measurement_error, recovered), + Err(PsychometricError::MeasurementErrorIsNotObservedVariance) ); assert_eq!( - recover_stationary_latent_variance(-0.1, -0.5, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) + refuse_latent_variance_as_observed_variance(latent, recovered), + Err(PsychometricError::LatentVarianceIsNotObservedVariance) ); assert_eq!( - recover_stationary_latent_variance(f64::NAN, -0.5, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) + recover_manifest_observed_variance(0.0, latent, measurement_error), + Ok(measurement_error) ); assert_eq!( - recover_stationary_latent_variance(0.4, f64::NAN, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) + recover_manifest_observed_variance(loading, 0.0, measurement_error), + Ok(measurement_error) ); - // 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) + 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 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, + 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("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) - ); + .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_trait_plus_state_latent_variance(trait_variance, 0.0), - Ok(trait_variance) + 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!( - recover_trait_plus_state_lagged_covariance( - 0.0, - state, - drift, - delta, - LagClock::EventTime - ), - Ok(state_lagged) + refuse_manifest_trait_variance_as_measurement_error(manifest_trait, measurement_error), + Err(PsychometricError::ManifestTraitVarianceIsNotMeasurementError) ); + // Zero loading: Var(y) = θ + ψ, not ψ stuffed as Θ. assert_eq!( - recover_trait_plus_state_lagged_covariance( - trait_variance, + recover_manifest_trait_plus_state_observed_variance( 0.0, - drift, - delta, - LagClock::EventTime + latent, + measurement_error, + manifest_trait ), - Ok(trait_variance) + Ok(measurement_error + manifest_trait) ); - let process_noise = - recover_discrete_process_noise(diffusion, drift, delta, LagClock::EventTime) - .expect("q_dt"); + // 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!( - refuse_trait_variance_as_process_noise(trait_variance, process_noise), - Err(PsychometricError::TraitVarianceIsNotProcessNoise) + recover_manifest_trait_plus_state_observed_variance(2.0, 0.4, 0.1, -0.1), + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - refuse_trait_variance_as_stationary_within_subject(trait_variance, state), - Err(PsychometricError::TraitVarianceIsNotStationaryWithinSubject) + recover_manifest_trait_plus_state_observed_variance(2.0, 0.4, 0.1, f64::NAN), + Err(PsychometricError::InvalidNumericInput) ); - } - - #[test] - fn trait_plus_state_invalid_inputs_fail_closed() { assert_eq!( - recover_trait_plus_state_latent_variance(-0.1, 0.4), + recover_manifest_trait_plus_state_observed_variance(1e308, 1.0, 0.0, 0.3), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_trait_plus_state_latent_variance(0.4, -0.1), + 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_trait_plus_state_latent_variance(f64::NAN, 0.4), + recover_manifest_observed_variance(f64::NAN, 0.4, 0.1), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_trait_plus_state_latent_variance(0.4, f64::NAN), + recover_manifest_observed_variance(2.0, -0.1, 0.1), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_trait_plus_state_latent_variance(1e308, 1e308), + recover_manifest_observed_variance(2.0, 0.4, -0.1), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_trait_plus_state_lagged_covariance(-0.1, 0.4, -0.5, 1.0, LagClock::EventTime), + recover_manifest_observed_variance(2.0, f64::NAN, 0.1), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_trait_plus_state_lagged_covariance( - f64::NAN, - 0.4, - -0.5, - 1.0, - LagClock::EventTime - ), + recover_manifest_observed_variance(2.0, 0.4, f64::NAN), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_trait_plus_state_lagged_covariance(0.4, 0.4, -0.5, 1.0, LagClock::SystemTime), - Err(PsychometricError::EventTimeRequired) + recover_manifest_observed_variance(1e308, 1.0, 0.0), + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_trait_plus_state_lagged_covariance(1e308, 1e308, 0.0, 1.0, LagClock::EventTime), + recover_manifest_observed_variance(1e308, 1.0, 1e308), 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"); + 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!( - refuse_difference_quotient_as_local_rate(1.0, 0.5, 1.0), - Err(PsychometricError::DifferenceQuotientForbidden) + recover_manifest_lagged_observed_covariance(loading, lagged, 0.0), + Ok(1.6) ); - } - - #[test] - fn invalid_lag_inputs_fail_closed() { assert_eq!( - recover_discrete_lag_one(0.0, 1.0), - Err(PsychometricError::InvalidNumericInput) + recover_manifest_lagged_observed_covariance(0.0, lagged, manifest_trait), + Ok(manifest_trait) ); assert_eq!( - recover_discrete_lag_one(f64::NAN, 1.0), - Err(PsychometricError::InvalidNumericInput) + recover_manifest_lagged_observed_covariance(loading, 0.0, manifest_trait), + Ok(manifest_trait) ); assert_eq!( - recover_discrete_lag_one(1.0, f64::INFINITY), - Err(PsychometricError::InvalidNumericInput) + refuse_latent_lagged_covariance_as_observed_covariance(lagged, recovered), + Err(PsychometricError::LatentLaggedCovarianceIsNotObservedCovariance) ); assert_eq!( - recover_local_log_rate(0.5, 0.0, LagClock::EventTime), - Err(PsychometricError::NonPositiveInterval) + 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_local_log_rate(0.5, -1.0, LagClock::EventTime), - Err(PsychometricError::NonPositiveInterval) + recover_manifest_lagged_observed_covariance(f64::NAN, 0.4, 0.0), + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_local_log_rate(0.5, f64::NAN, LagClock::EventTime), - Err(PsychometricError::NonPositiveInterval) + recover_manifest_lagged_observed_covariance(2.0, -0.1, 0.0), + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_local_log_rate(0.0, 1.0, LagClock::EventTime), + recover_manifest_lagged_observed_covariance(2.0, 0.4, -0.1), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_local_log_rate(-0.2, 1.0, LagClock::EventTime), + recover_manifest_lagged_observed_covariance(1e308, 1.0, 0.0), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_local_log_rate(f64::NAN, 1.0, LagClock::EventTime), + recover_manifest_lagged_observed_covariance(1e308, 1e-308, 1e308), 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) - ); + 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_event_series_mean_log_rate( - &[EventOccasion { - event_time: 0.0, - score: 1.0, - }], - LagClock::EventTime - ), - Err(PsychometricError::InvalidNumericInput) + recover_manifest_observed_mean(loading, latent_mean, 0.0), + Ok(0.8) ); 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) + recover_manifest_observed_mean(0.0, latent_mean, manifest_mean), + Ok(manifest_mean) ); assert_eq!( - recover_event_series_mean_log_rate( - &[occasion(0.0, 1.0), occasion(f64::NAN, 0.5)], - LagClock::EventTime - ), - Err(PsychometricError::InvalidNumericInput) + 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!( - recover_event_series_mean_log_rate( - &[occasion(0.0, f64::NAN), occasion(1.0, 0.5)], - LagClock::EventTime - ), - Err(PsychometricError::InvalidNumericInput) + refuse_manifest_means_as_observed_mean(manifest_mean, recovered), + Err(PsychometricError::ManifestMeansIsNotObservedMean) ); assert_eq!( - recover_event_series_mean_log_rate( - &[occasion(0.0, 1.0), occasion(1.0, f64::NAN)], - LagClock::EventTime - ), - Err(PsychometricError::InvalidNumericInput) + refuse_latent_mean_as_observed_mean(latent_mean, recovered), + Err(PsychometricError::LatentMeanIsNotObservedMean) ); 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) + refuse_continuous_intercept_as_manifest_means(0.3, manifest_mean), + Err(PsychometricError::ContinuousInterceptIsNotManifestMeans) ); - } - - 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()); + 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 within_residual_invalid_rows_fail_closed() { - let rows = decaying_clustered_scores(-0.25); + fn manifest_observed_mean_invalid_inputs_fail_closed() { assert_eq!( - recover_within_residual_event_time_log_rate(&rows, LagClock::SystemTime), - Err(PsychometricError::EventTimeRequired) + recover_manifest_observed_mean(f64::NAN, 0.4, 0.0), + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_within_residual_event_time_log_rate(&[], LagClock::EventTime), + recover_manifest_observed_mean(2.0, f64::NAN, 0.0), 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) + recover_manifest_observed_mean(2.0, 0.4, f64::NAN), + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_within_residual_event_time_log_rate( - &[clustered(1, f64::NAN, 1.0), clustered(2, 1.0, 0.5)], - LagClock::EventTime - ), + recover_manifest_observed_mean(1e308, 2.0, 0.0), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_event_series_mean_log_rate( - &[occasion(0.0, 1.0), occasion(1.0, f64::NAN)], - LagClock::EventTime - ), + 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_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) + recover_discrete_latent_mean(0.0, drift, intercept, delta, LagClock::EventTime), + Ok(increment) ); assert_eq!( - recover_within_residual_event_time_log_rate( - &[clustered(1, 0.0, 1.0), clustered(2, 1.0, f64::INFINITY)], + 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_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) + recover_discrete_latent_mean(0.0, 1e308, 0.0, 2.0, LagClock::EventTime), + Ok(0.0) ); - } - - 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); + // 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 irregular_centered_residuals_fail_closed() { - let ok = lagged(1.0, 0.8, 1.0); + fn discrete_latent_mean_invalid_inputs_fail_closed() { assert_eq!( - recover_irregular_centered_residual_log_rate(&[ok], LagClock::SystemTime), - Err(PsychometricError::EventTimeRequired) + recover_discrete_latent_mean(f64::NAN, -0.5, 0.3, 2.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_irregular_centered_residual_log_rate(&[], LagClock::EventTime), + recover_discrete_latent_mean(1.0, f64::NAN, 0.3, 2.0, LagClock::EventTime), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_irregular_centered_residual_log_rate( - &[lagged(f64::NAN, 0.8, 1.0)], - LagClock::EventTime - ), + recover_discrete_latent_mean(1.0, -0.5, f64::NAN, 2.0, LagClock::EventTime), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_irregular_centered_residual_log_rate( - &[lagged(1.0, f64::INFINITY, 1.0)], - LagClock::EventTime - ), + 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_irregular_centered_residual_log_rate( - &[lagged(1.0, 0.8, f64::NAN)], - LagClock::EventTime - ), + recover_discrete_latent_mean(1.0, 1e308, 1.0, 2.0, LagClock::EventTime), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_irregular_centered_residual_log_rate( - &[lagged(0.0, 0.8, 1.0)], - LagClock::EventTime - ), + recover_discrete_latent_mean(1e308, 0.0, 1e308, 2.0, LagClock::EventTime), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_irregular_centered_residual_log_rate( - &[lagged(1.0, -0.8, 1.0)], - LagClock::EventTime - ), + 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_irregular_centered_residual_log_rate( - &[lagged(1.0, 0.8, 0.0)], + recover_discrete_observed_mean( + 0.0, + initial, + drift, + intercept, + manifest_mean, + delta, LagClock::EventTime ), - Err(PsychometricError::NonPositiveInterval) + Ok(manifest_mean) ); assert_eq!( - recover_irregular_centered_residual_log_rate( - &[lagged(1.0, 0.8, -0.5)], + recover_discrete_observed_mean( + loading, + initial, + drift, + intercept, + 0.0, + delta, LagClock::EventTime ), - Err(PsychometricError::NonPositiveInterval) + 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 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), - ]; + fn discrete_observed_mean_refuses_first_occasion_and_overflow() { + let loading = 2.0_f64; let recovered = - recover_within_residual_event_time_log_rate(&mixed, LagClock::EventTime).expect("skip"); - assert!(recovered.is_finite()); + 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!( - recover_within_residual_event_time_log_rate( - &[clustered(1, 0.0, 1.0), clustered(2, 1.0, 0.5)], - LagClock::EventTime - ), - Err(PsychometricError::InvalidNumericInput) + refuse_initial_observed_mean_as_evolved_observed_mean(first_occasion, recovered), + Err(PsychometricError::InitialObservedMeanIsNotEvolvedObservedMean) ); - } - - #[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) + 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 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) - ); + fn discrete_observed_mean_invalid_inputs_fail_closed() { assert_eq!( - fit_scalar_log_rate(&[(1e200, 1e200, 1.0)]), + recover_discrete_observed_mean(f64::NAN, 1.0, -0.5, 0.3, 0.5, 2.0, LagClock::EventTime), 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) + recover_discrete_observed_mean(2.0, 1.0, -0.5, 0.3, 0.5, 0.0, LagClock::EventTime), + Err(PsychometricError::NonPositiveInterval) ); - 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) + recover_discrete_observed_mean(2.0, 1.0, -0.5, 0.3, 0.5, 2.0, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) ); assert_eq!( - fit_scalar_log_rate(&[(1.0, 1e-300, 1.0), (1.0, 1e-300, 2.0)]), + recover_discrete_observed_mean(1e308, 2.0, 0.0, 0.0, 0.0, 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), - ], - LagClock::EventTime - ), + recover_discrete_observed_mean(1.0, 1.0, 710.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), - ], - 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) - ); + 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_manifest_observed_variance(0.0, latent, measurement_error), - Ok(measurement_error) + recover_time_dependent_predictor_impulse(0.0, predictor), + Ok(0.0) ); assert_eq!( - recover_manifest_observed_variance(loading, 0.0, measurement_error), - Ok(measurement_error) + 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_manifest_observed_variance(loading, latent, 0.0), - Ok(1.6) + recover_discrete_latent_mean_with_impulse( + initial, + drift, + intercept, + 0.0, + predictor, + delta, + LagClock::EventTime + ), + Ok(evolved) ); - // 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()); + 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 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, + 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("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"); + .expect("eq14"); assert_eq!( - recover_manifest_trait_plus_state_observed_variance( - loading, - latent, - measurement_error, - 0.0 - ), - Ok(without_trait) + refuse_time_dependent_impulse_as_continuous_intercept(impulse, effect), + Err(PsychometricError::TimeDependentImpulseIsNotContinuousIntercept) ); - assert!((without_trait - recovered).abs() > 1e-3); assert_eq!( - refuse_manifest_trait_variance_as_measurement_error(manifest_trait, measurement_error), - Err(PsychometricError::ManifestTraitVarianceIsNotMeasurementError) + refuse_time_dependent_impulse_as_time_independent_effect(impulse, intercept_effect), + Err(PsychometricError::TimeDependentImpulseIsNotTimeIndependentEffect) ); - // Zero loading: Var(y) = θ + ψ, not ψ stuffed as Θ. assert_eq!( - recover_manifest_trait_plus_state_observed_variance( - 0.0, - latent, - measurement_error, - manifest_trait + refuse_time_dependent_impulse_as_time_varying_discrete_effect( + impulse, + equation_fourteen ), - Ok(measurement_error + manifest_trait) + Err(PsychometricError::TimeDependentImpulseIsNotTimeVaryingDiscreteEffect) ); - // 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 time_dependent_impulse_invalid_inputs_fail_closed() { assert_eq!( - recover_manifest_trait_plus_state_observed_variance(2.0, 0.4, 0.1, -0.1), + recover_time_dependent_predictor_impulse(f64::NAN, 1.0), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_manifest_trait_plus_state_observed_variance(2.0, 0.4, 0.1, f64::NAN), + recover_time_dependent_predictor_impulse(1.0, f64::INFINITY), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_manifest_trait_plus_state_observed_variance(1e308, 1.0, 0.0, 0.3), + recover_time_dependent_predictor_impulse(1e308, 2.0), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_manifest_trait_plus_state_observed_variance(1e308, 1e-308, 1e308, 1e308), + 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) ); } #[test] - fn manifest_observed_variance_invalid_inputs_fail_closed() { + 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_manifest_observed_variance(f64::NAN, 0.4, 0.1), - Err(PsychometricError::InvalidNumericInput) + recover_level_change_continuous_intercept(0.0, predictor, drift), + Ok(0.0) ); assert_eq!( - recover_manifest_observed_variance(2.0, -0.1, 0.1), - Err(PsychometricError::InvalidNumericInput) + recover_level_change_continuous_intercept(effect, 0.0, drift), + Ok(0.0) ); assert_eq!( - recover_manifest_observed_variance(2.0, 0.4, -0.1), + 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_manifest_observed_variance(2.0, f64::NAN, 0.1), + recover_level_change_continuous_intercept(1.0, f64::INFINITY, -0.5), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_manifest_observed_variance(2.0, 0.4, f64::NAN), + recover_level_change_continuous_intercept(0.4, 3.0, f64::NAN), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_manifest_observed_variance(1e308, 1.0, 0.0), + recover_level_change_continuous_intercept(1e308, 2.0, -0.5), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_manifest_observed_variance(1e308, 1.0, 1e308), + 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 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) - ); + 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!( - recover_manifest_lagged_observed_covariance(0.0, lagged, manifest_trait), - Ok(manifest_trait) + refuse_level_change_increment_as_impulse(increment, impulse), + Err(PsychometricError::LevelChangeIncrementIsNotImpulse) ); assert_eq!( - recover_manifest_lagged_observed_covariance(loading, 0.0, manifest_trait), - Ok(manifest_trait) + refuse_level_change_increment_as_intercept(increment, intercept), + Err(PsychometricError::LevelChangeIncrementIsNotIntercept) ); assert_eq!( - refuse_latent_lagged_covariance_as_observed_covariance(lagged, recovered), - Err(PsychometricError::LatentLaggedCovarianceIsNotObservedCovariance) + 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!( - refuse_measurement_error_as_lagged_observed_covariance(0.1, recovered), - Err(PsychometricError::MeasurementErrorIsNotLaggedObservedCovariance) + recover_level_change_discrete_increment( + 0.0, + predictor, + drift, + delta, + LagClock::EventTime + ), + Ok(0.0) ); - 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() { + 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_manifest_lagged_observed_covariance(f64::NAN, 0.4, 0.0), - Err(PsychometricError::InvalidNumericInput) + recover_level_change_discrete_increment( + effect, + predictor, + 0.0, + delta, + LagClock::EventTime + ), + Err(PsychometricError::LevelChangeRequiresStableDrift) ); assert_eq!( - recover_manifest_lagged_observed_covariance(2.0, -0.1, 0.0), - Err(PsychometricError::InvalidNumericInput) + recover_level_change_discrete_increment( + effect, + predictor, + 0.5, + delta, + LagClock::EventTime + ), + Err(PsychometricError::LevelChangeRequiresStableDrift) ); assert_eq!( - recover_manifest_lagged_observed_covariance(2.0, 0.4, -0.1), + 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_manifest_lagged_observed_covariance(1e308, 1.0, 0.0), + recover_level_change_discrete_increment( + f64::NAN, + predictor, + drift, + delta, + LagClock::EventTime + ), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_manifest_lagged_observed_covariance(1e308, 1e-308, 1e308), - Err(PsychometricError::InvalidNumericInput) + recover_level_change_discrete_increment( + 0.0, + predictor, + 0.0, + delta, + LagClock::EventTime + ), + Ok(0.0) ); } #[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; + 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); - assert!((recovered - 1.3).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_manifest_observed_mean(loading, latent_mean, 0.0), - Ok(0.8) + recover_level_change_extra_process_contribution( + 0.0, + predictor, + original, + extra, + delta, + LagClock::EventTime + ), + Ok(0.0) ); assert_eq!( - recover_manifest_observed_mean(0.0, latent_mean, manifest_mean), - Ok(manifest_mean) + recover_level_change_extra_process_contribution( + coupling, + 0.0, + original, + extra, + delta, + LagClock::EventTime + ), + Ok(0.0) ); assert_eq!( - recover_manifest_observed_mean(loading, 0.0, manifest_mean), - Ok(manifest_mean) + recover_level_change_extra_process_contribution( + coupling, + 0.0, + original, + 0.0, + delta, + LagClock::EventTime + ), + Ok(0.0) ); - assert_eq!(recover_manifest_observed_mean(-2.0, 0.5, 1.0), 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_manifest_means_as_observed_mean(manifest_mean, recovered), - Err(PsychometricError::ManifestMeansIsNotObservedMean) + refuse_level_change_extra_process_as_impulse(recovered, impulse), + Err(PsychometricError::LevelChangeExtraProcessIsNotImpulse) ); assert_eq!( - refuse_latent_mean_as_observed_mean(latent_mean, recovered), - Err(PsychometricError::LatentMeanIsNotObservedMean) + refuse_level_change_extra_process_as_intercept(recovered, intercept), + Err(PsychometricError::LevelChangeExtraProcessIsNotIntercept) ); assert_eq!( - refuse_continuous_intercept_as_manifest_means(0.3, manifest_mean), - Err(PsychometricError::ContinuousInterceptIsNotManifestMeans) + refuse_level_change_extra_process_as_increment(recovered, increment), + Err(PsychometricError::LevelChangeExtraProcessIsNotIncrement) ); - 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() { + 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_manifest_observed_mean(f64::NAN, 0.4, 0.0), - Err(PsychometricError::InvalidNumericInput) + recover_level_change_extra_process_contribution( + coupling, + predictor, + original, + 0.0, + delta, + LagClock::EventTime + ), + Err(PsychometricError::LevelChangeExtraProcessRequiresNegativeDrift) ); assert_eq!( - recover_manifest_observed_mean(2.0, f64::NAN, 0.0), - Err(PsychometricError::InvalidNumericInput) + recover_level_change_extra_process_contribution( + coupling, + predictor, + original, + 0.5, + delta, + LagClock::EventTime + ), + Err(PsychometricError::LevelChangeExtraProcessRequiresNegativeDrift) ); assert_eq!( - recover_manifest_observed_mean(2.0, 0.4, f64::NAN), + 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_manifest_observed_mean(1e308, 2.0, 0.0), + recover_level_change_extra_process_contribution( + 1e308, + 2.0, + original, + extra, + delta, + LagClock::EventTime + ), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_manifest_observed_mean(1.0, 1e308, 1e308), + recover_level_change_extra_process_contribution( + coupling, + predictor, + 710.0, + extra, + 1.0, + LagClock::EventTime + ), 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) - ); + fn nonfinite_short_circuit_operands_of_fail_closed_guards_execute() { + let event = LagClock::EventTime; assert_eq!( - recover_discrete_latent_mean(initial, drift, 0.0, delta, LagClock::EventTime), - Ok((drift * delta).exp() * initial) + recover_manifest_lagged_observed_covariance(2.0, f64::NAN, 0.0), + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_latent_mean(initial, 0.0, intercept, delta, LagClock::EventTime), - Ok(initial + intercept * delta) + recover_manifest_lagged_observed_covariance(2.0, 0.4, f64::NAN), + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_continuous_intercept_effect( - intercept, - 0.0, - delta, - LagClock::EventTime - ), - Ok(intercept * delta) + recover_discrete_continuous_intercept_effect(0.3, -0.5, f64::NAN, event), + Err(PsychometricError::NonPositiveInterval) ); assert_eq!( - recover_discrete_continuous_intercept_effect(0.0, 0.0, delta, LagClock::EventTime), - Ok(0.0) + recover_level_change_extra_process_contribution(0.4, 3.0, -0.5, -1e-6, f64::NAN, event), + Err(PsychometricError::NonPositiveInterval) ); assert_eq!( - refuse_initial_latent_mean_as_evolved_mean(initial, recovered), - Err(PsychometricError::InitialLatentMeanIsNotEvolvedMean) + recover_level_change_extra_process_contribution(0.4, f64::NAN, -0.5, -1e-6, 2.0, event), + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - refuse_continuous_intercept_as_discrete_mean_increment(intercept, increment), - Err(PsychometricError::ContinuousInterceptIsNotDiscreteMeanIncrement) + recover_level_change_extra_process_contribution(0.4, 3.0, f64::NAN, -1e-6, 2.0, event), + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - refuse_continuous_intercept_as_initial_latent_mean(intercept, initial), - Err(PsychometricError::ContinuousInterceptIsNotInitialLatentMean) + recover_level_change_extra_process_contribution(0.4, 3.0, -0.5, f64::NAN, 2.0, event), + Err(PsychometricError::InvalidNumericInput) ); - 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) + 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_latent_mean(0.0, 1e308, 0.0, 2.0, LagClock::EventTime), - Ok(0.0) + recover_discrete_time_independent_predictor_effect(0.2, 1.0, -0.5, f64::NAN, event), + Err(PsychometricError::NonPositiveInterval) ); - // 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), + recover_discrete_time_independent_predictor_effect(0.2, f64::NAN, -0.5, 2.0, event), 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), + recover_asymptotic_time_independent_predictor_effect(0.2, f64::NAN, -0.5, event), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_latent_mean(1.0, f64::NAN, 0.3, 2.0, LagClock::EventTime), + recover_asymptotic_time_independent_predictor_effect(0.2, 1.0, f64::NAN, event), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_latent_mean(1.0, -0.5, f64::NAN, 2.0, LagClock::EventTime), + recover_asymptotic_time_independent_predictor_variance(f64::NAN, 1.0, -0.5, event), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_latent_mean(1.0, -0.5, 0.3, 0.0, LagClock::EventTime), - Err(PsychometricError::NonPositiveInterval) + recover_asymptotic_time_independent_predictor_variance(0.2, f64::NAN, -0.5, event), + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_latent_mean(1.0, -0.5, 0.3, 2.0, LagClock::SystemTime), - Err(PsychometricError::EventTimeRequired) + recover_asymptotic_time_independent_predictor_variance(0.2, 1.0, f64::NAN, event), + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_continuous_intercept_effect(1.0, 1e308, 2.0, LagClock::EventTime), + recover_asymptotic_continuous_intercept(0.3, f64::NAN, event), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_latent_mean(1.0, 1e308, 1.0, 2.0, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) + recover_initial_time_independent_predictor_carry(0.4, 3.0, -0.5, f64::NAN, event), + Err(PsychometricError::NonPositiveInterval) ); assert_eq!( - recover_discrete_latent_mean(1e308, 0.0, 1e308, 2.0, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) + recover_initial_time_dependent_predictor_carry(0.4, 3.0, -0.5, f64::NAN, event), + Err(PsychometricError::NonPositiveInterval) ); assert_eq!( - recover_discrete_latent_mean(1e308, 0.0, 1e308, 1.0, LagClock::EventTime), - Err(PsychometricError::InvalidNumericInput) + 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) ); - 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 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 drift = -0.5_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( + let recovered = recover_discrete_observed_mean_with_extra_process( loading, initial, - drift, + original, intercept, + coupling, + predictor, + extra, 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-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 first_occasion = - recover_manifest_observed_mean(loading, initial, manifest_mean).expect("t0"); - 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 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( + recover_discrete_observed_mean_with_extra_process( 0.0, initial, - drift, + original, intercept, + coupling, + predictor, + extra, manifest_mean, delta, LagClock::EventTime @@ -7466,222 +9344,168 @@ mod tests { Ok(manifest_mean) ); assert_eq!( - recover_discrete_observed_mean( + recover_discrete_observed_mean_with_extra_process( loading, initial, - drift, + original, intercept, - 0.0, + coupling, + predictor, + extra, + manifest_mean, delta, - LagClock::EventTime + LagClock::SystemTime ), - Ok(loading * evolved) + Err(PsychometricError::EventTimeRequired) ); - let zero_evolved = recover_discrete_observed_mean( + } + + #[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, - 0.0, - 0.0, - 0.0, + initial, + original, + intercept, + coupling, + predictor, + extra, manifest_mean, delta, + elapsed, LagClock::EventTime, ) - .expect("zero-mu"); - assert!((zero_evolved - manifest_mean).abs() < 1e-15); - let integrator = recover_discrete_observed_mean( + .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, - 0.0, + original, intercept, + coupling, + predictor, + extra, 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-t0-extra-process-mean"); + assert!((first_occasion - recovered).abs() > 1e-3); + let evolved_observed = recover_discrete_observed_mean( loading, initial, - -1e308, - 1.0, + original, + intercept, manifest_mean, - 2.0, + delta, 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( + .expect("eq3-eq5-mean"); + assert!((evolved_observed - recovered).abs() > 1e-3); + let carry_observed = recover_discrete_observed_mean_with_impulse_carry( + loading, initial, - drift, + original, intercept, - effect, + coupling, predictor, + manifest_mean, delta, + elapsed, 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, + .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("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"); + .expect("after-extra-process"); assert_eq!( - refuse_time_dependent_impulse_as_continuous_intercept(impulse, effect), - Err(PsychometricError::TimeDependentImpulseIsNotContinuousIntercept) + 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_time_dependent_impulse_as_time_independent_effect(impulse, intercept_effect), - Err(PsychometricError::TimeDependentImpulseIsNotTimeIndependentEffect) + refuse_after_extra_process_contribution_as_observed_mean(contribution, recovered), + Err(PsychometricError::AfterExtraProcessContributionIsNotObservedMean) ); assert_eq!( - refuse_time_dependent_impulse_as_time_varying_discrete_effect( - impulse, - equation_fourteen - ), - Err(PsychometricError::TimeDependentImpulseIsNotTimeVaryingDiscreteEffect) + refuse_after_extra_process_latent_mean_as_observed_mean(composed, recovered), + Err(PsychometricError::AfterExtraProcessLatentMeanIsNotObservedMean) ); } #[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) - ); + #[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_time_dependent_predictor_impulse(1e308, 2.0), - Err(PsychometricError::InvalidNumericInput) + recover_level_change_extra_process_contribution_after( + coupling, + predictor, + original, + extra, + 2.0, + 2.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) ); assert_eq!( - recover_discrete_latent_mean_with_impulse( - 1.0, - -0.5, - 0.3, - 0.4, + recover_level_change_extra_process_contribution_after( + coupling, + predictor, + original, + extra, 2.0, 0.0, LagClock::EventTime @@ -7689,322 +9513,297 @@ mod tests { Err(PsychometricError::NonPositiveInterval) ); assert_eq!( - recover_discrete_latent_mean_with_impulse( + recover_discrete_observed_mean_with_extra_process_after( + 2.0, 1.0, - -0.5, + original, 0.3, - 0.4, - 2.0, + coupling, + predictor, + extra, + 0.5, 2.0, + 1.0, LagClock::SystemTime ), Err(PsychometricError::EventTimeRequired) ); assert_eq!( - recover_discrete_latent_mean_with_impulse( - 1e308, - 0.0, + recover_discrete_observed_mean_with_extra_process_after( 0.0, - 1e308, 1.0, + original, + 0.3, + coupling, + predictor, + extra, + 0.5, + 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) + Ok(0.5) ); assert_eq!( - recover_level_change_continuous_intercept(effect, predictor, 0.0), - Err(PsychometricError::LevelChangeRequiresStableDrift) + 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_continuous_intercept(effect, predictor, 0.5), - Err(PsychometricError::LevelChangeRequiresStableDrift) + recover_level_change_extra_process_contribution_after( + coupling, + predictor, + original, + extra, + 0.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) ); - 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) + recover_level_change_extra_process_contribution_after( + coupling, + predictor, + original, + extra, + f64::NAN, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) ); assert_eq!( - refuse_level_change_intercept_as_free_continuous_intercept(intercept, 0.3), - Err(PsychometricError::LevelChangeInterceptIsNotFreeContinuousIntercept) + 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!( - refuse_level_change_intercept_as_process_increment(intercept, increment), - Err(PsychometricError::LevelChangeInterceptIsNotProcessIncrement) + 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) ); } #[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) - ); + 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_level_change_continuous_intercept(1e308, 2.0, -0.5), - Err(PsychometricError::InvalidNumericInput) + recover_asymptotic_time_independent_predictor_effect( + 0.0, + predictor, + 0.0, + LagClock::EventTime + ), + Ok(0.0) ); assert_eq!( - recover_level_change_continuous_intercept(1.0, 2.0, -1e308), - Err(PsychometricError::InvalidNumericInput) + recover_asymptotic_time_independent_predictor_effect( + effect, + 0.0, + 0.0, + LagClock::EventTime + ), + Ok(0.0) ); - 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( + 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, - 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, + predictor, + log_rate, 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( + .expect("asymTIPREDEFFECT"); + let discrete = recover_discrete_time_independent_predictor_effect( effect, predictor, - drift, - delta, + log_rate, + 1.0, LagClock::EventTime, ) - .expect("tipred"); + .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_level_change_increment_as_impulse(increment, impulse), - Err(PsychometricError::LevelChangeIncrementIsNotImpulse) + refuse_asymptotic_time_independent_effect_as_coefficient(recovered, effect), + Err(PsychometricError::AsymptoticTimeIndependentEffectIsNotCoefficient) ); assert_eq!( - refuse_level_change_increment_as_intercept(increment, intercept), - Err(PsychometricError::LevelChangeIncrementIsNotIntercept) + refuse_asymptotic_time_independent_effect_as_discrete_effect(recovered, discrete), + Err(PsychometricError::AsymptoticTimeIndependentEffectIsNotDiscreteEffect) ); assert_eq!( - refuse_level_change_increment_as_process_increment(increment, tipred), - Err(PsychometricError::LevelChangeIncrementIsNotProcessIncrement) + refuse_asymptotic_time_independent_effect_as_continuous_intercept(recovered, 0.3), + Err(PsychometricError::AsymptoticTimeIndependentEffectIsNotContinuousIntercept) ); - 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) + refuse_asymptotic_time_independent_effect_as_time_dependent_impulse(recovered, impulse), + Err(PsychometricError::AsymptoticTimeIndependentEffectIsNotTimeDependentImpulse) ); } #[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; + 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_level_change_discrete_increment( + recover_asymptotic_time_independent_predictor_effect( effect, predictor, - 0.0, - delta, - LagClock::EventTime + log_rate, + LagClock::SystemTime ), - Err(PsychometricError::LevelChangeRequiresStableDrift) + Err(PsychometricError::EventTimeRequired) ); assert_eq!( - recover_level_change_discrete_increment( + recover_asymptotic_time_independent_predictor_effect( effect, predictor, - 0.5, - delta, + 0.0, LagClock::EventTime ), - Err(PsychometricError::LevelChangeRequiresStableDrift) + Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) ); assert_eq!( - recover_level_change_discrete_increment( + recover_asymptotic_time_independent_predictor_effect( effect, predictor, - drift, - delta, - LagClock::SystemTime + 0.5, + LagClock::EventTime ), - Err(PsychometricError::EventTimeRequired) + Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) ); assert_eq!( - recover_level_change_discrete_increment( - effect, + recover_asymptotic_time_independent_predictor_effect( + f64::NAN, predictor, - drift, - 0.0, + log_rate, 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, + recover_asymptotic_time_independent_predictor_effect( + 1e308, + 2.0, + log_rate, LagClock::EventTime ), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_level_change_discrete_increment( - 0.0, - predictor, - 0.0, - delta, + recover_asymptotic_time_independent_predictor_effect( + 1e308, + 1.0, + -1e-308, LagClock::EventTime ), - Ok(0.0) + Err(PsychometricError::InvalidNumericInput) ); } #[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, + 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("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, + .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("brownian-original"); - let brownian_expected = coupling * predictor * (extra * delta).exp_m1() / extra; - assert!((brownian - brownian_expected).abs() < 1e-15); + .expect("doubled-v"); + assert!((doubled - 2.0 * expected).abs() < 1e-12); assert_eq!( - recover_level_change_extra_process_contribution( + recover_asymptotic_time_independent_predictor_variance( 0.0, - predictor, - original, - extra, - delta, - LagClock::EventTime - ), - Ok(0.0) - ); - assert_eq!( - recover_level_change_extra_process_contribution( - coupling, + predictor_variance, 0.0, - original, - extra, - delta, LagClock::EventTime ), Ok(0.0) ); assert_eq!( - recover_level_change_extra_process_contribution( - coupling, + recover_asymptotic_time_independent_predictor_variance( + effect, 0.0, - original, 0.0, - delta, LagClock::EventTime ), Ok(0.0) @@ -8012,130 +9811,94 @@ mod tests { } #[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, + 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("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, + .expect("addedTIPREDVAR"); + let mean_effect = recover_asymptotic_time_independent_predictor_effect( + effect, + 1.0, + log_rate, 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) - ); + .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_level_change_extra_process_as_increment(recovered, increment), - Err(PsychometricError::LevelChangeExtraProcessIsNotIncrement) + refuse_asymptotic_time_independent_variance_as_trait_variance(recovered, trait_plus), + Err(PsychometricError::AsymptoticTimeIndependentVarianceIsNotTraitVariance) ); - } - - #[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 + refuse_asymptotic_time_independent_variance_as_stationary_within_subject( + recovered, stationary ), - Err(PsychometricError::LevelChangeExtraProcessRequiresNegativeDrift) + Err(PsychometricError::AsymptoticTimeIndependentVarianceIsNotStationaryWithinSubject) ); assert_eq!( - recover_level_change_extra_process_contribution( - coupling, - predictor, - original, - 0.5, - delta, - LagClock::EventTime + refuse_asymptotic_time_independent_variance_as_asymptotic_effect( + recovered, + mean_effect ), - Err(PsychometricError::LevelChangeExtraProcessRequiresNegativeDrift) + 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_level_change_extra_process_contribution( - coupling, - predictor, - original, - extra, - delta, + recover_asymptotic_time_independent_predictor_variance( + effect, + 1.0, + log_rate, LagClock::SystemTime ), Err(PsychometricError::EventTimeRequired) ); assert_eq!( - recover_level_change_extra_process_contribution( - coupling, - predictor, - original, - extra, + recover_asymptotic_time_independent_predictor_variance( + effect, + 1.0, 0.0, LagClock::EventTime ), - Err(PsychometricError::NonPositiveInterval) + Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) ); assert_eq!( - recover_level_change_extra_process_contribution( - f64::NAN, - predictor, - original, - extra, - delta, + recover_asymptotic_time_independent_predictor_variance( + effect, + -1.0, + log_rate, LagClock::EventTime ), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_level_change_extra_process_contribution( + recover_asymptotic_time_independent_predictor_variance( 1e308, - 2.0, - original, - extra, - delta, + 1.0, + -1e-308, LagClock::EventTime ), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_level_change_extra_process_contribution( - coupling, - predictor, - 710.0, - extra, + recover_asymptotic_time_independent_predictor_variance( + 1e200, 1.0, + -1e-200, LagClock::EventTime ), Err(PsychometricError::InvalidNumericInput) @@ -8143,695 +9906,1014 @@ mod tests { } #[test] - fn nonfinite_short_circuit_operands_of_fail_closed_guards_execute() { - let event = LagClock::EventTime; + 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_manifest_lagged_observed_covariance(2.0, f64::NAN, 0.0), - Err(PsychometricError::InvalidNumericInput) + recover_asymptotic_continuous_intercept(0.0, 0.0, LagClock::EventTime), + Ok(0.0) ); assert_eq!( - recover_manifest_lagged_observed_covariance(2.0, 0.4, f64::NAN), - Err(PsychometricError::InvalidNumericInput) + 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!( - recover_discrete_continuous_intercept_effect(0.3, -0.5, f64::NAN, event), - Err(PsychometricError::NonPositiveInterval) + refuse_asymptotic_continuous_intercept_as_discrete_increment(recovered, discrete), + Err(PsychometricError::AsymptoticContinuousInterceptIsNotDiscreteIncrement) ); assert_eq!( - recover_level_change_extra_process_contribution(0.4, 3.0, -0.5, -1e-6, f64::NAN, event), - Err(PsychometricError::NonPositiveInterval) + refuse_asymptotic_continuous_intercept_as_initial_latent_mean(recovered, 2.823), + Err(PsychometricError::AsymptoticContinuousInterceptIsNotInitialLatentMean) ); assert_eq!( - recover_level_change_extra_process_contribution(0.4, f64::NAN, -0.5, -1e-6, 2.0, event), - Err(PsychometricError::InvalidNumericInput) + 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_level_change_extra_process_contribution(0.4, 3.0, f64::NAN, -1e-6, 2.0, event), - Err(PsychometricError::InvalidNumericInput) + recover_asymptotic_continuous_intercept(intercept, log_rate, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) ); assert_eq!( - recover_level_change_extra_process_contribution(0.4, 3.0, -0.5, f64::NAN, 2.0, event), - Err(PsychometricError::InvalidNumericInput) + recover_asymptotic_continuous_intercept(intercept, 0.0, LagClock::EventTime), + Err(PsychometricError::AsymptoticContinuousInterceptRequiresStableDrift) ); 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) + recover_asymptotic_continuous_intercept(intercept, 0.5, LagClock::EventTime), + Err(PsychometricError::AsymptoticContinuousInterceptRequiresStableDrift) ); assert_eq!( - recover_discrete_time_independent_predictor_effect(0.2, 1.0, -0.5, f64::NAN, event), - Err(PsychometricError::NonPositiveInterval) + 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_discrete_time_independent_predictor_effect(0.2, f64::NAN, -0.5, 2.0, event), - Err(PsychometricError::InvalidNumericInput) + recover_stationary_initial_latent_mean(0.0, 0.0, 1.0, 0.0, LagClock::EventTime), + Ok(0.0) ); assert_eq!( - recover_asymptotic_time_independent_predictor_effect(0.2, f64::NAN, -0.5, event), - Err(PsychometricError::InvalidNumericInput) + 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!( - recover_asymptotic_time_independent_predictor_effect(0.2, 1.0, f64::NAN, event), - Err(PsychometricError::InvalidNumericInput) + refuse_stationary_initial_latent_mean_as_initial_latent_mean(recovered, 2.823), + Err(PsychometricError::StationaryInitialLatentMeanIsNotInitialLatentMean) ); assert_eq!( - recover_asymptotic_time_independent_predictor_variance(f64::NAN, 1.0, -0.5, event), - Err(PsychometricError::InvalidNumericInput) + refuse_stationary_initial_latent_mean_as_asymptotic_continuous_intercept( + recovered, + intercept_only + ), + Err(PsychometricError::StationaryInitialLatentMeanIsNotAsymptoticContinuousIntercept) ); assert_eq!( - recover_asymptotic_time_independent_predictor_variance(0.2, f64::NAN, -0.5, event), - Err(PsychometricError::InvalidNumericInput) + refuse_stationary_initial_latent_mean_as_asymptotic_time_independent_effect( + recovered, tipred + ), + Err(PsychometricError::StationaryInitialLatentMeanIsNotAsymptoticTimeIndependentEffect) ); assert_eq!( - recover_asymptotic_time_independent_predictor_variance(0.2, 1.0, f64::NAN, event), - Err(PsychometricError::InvalidNumericInput) + 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_asymptotic_continuous_intercept(0.3, f64::NAN, event), - Err(PsychometricError::InvalidNumericInput) + recover_stationary_initial_latent_mean( + intercept, + -0.225, + 1.0, + log_rate, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) ); assert_eq!( - recover_initial_time_independent_predictor_carry(0.4, 3.0, -0.5, f64::NAN, event), - Err(PsychometricError::NonPositiveInterval) + recover_stationary_initial_latent_mean(intercept, 0.0, 1.0, 0.0, LagClock::EventTime), + Err(PsychometricError::AsymptoticContinuousInterceptRequiresStableDrift) ); assert_eq!( - recover_initial_time_dependent_predictor_carry(0.4, 3.0, -0.5, f64::NAN, event), - Err(PsychometricError::NonPositiveInterval) + recover_stationary_initial_latent_mean(0.0, -0.225, 1.0, 0.5, LagClock::EventTime), + Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) ); assert_eq!( - recover_time_dependent_predictor_impulse_carry(0.4, 3.0, -0.5, f64::NAN, 1.0, event), - Err(PsychometricError::NonPositiveInterval) + recover_stationary_initial_latent_mean( + f64::NAN, + -0.225, + 1.0, + log_rate, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_time_dependent_predictor_impulse_carry(0.4, 3.0, -0.5, 2.0, f64::NAN, event), - Err(PsychometricError::NonPositiveInterval) + recover_stationary_initial_latent_mean(1e308, 1e308, 1.0, -1e-308, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) ); } #[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, + 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("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, + .expect("eq5-stationary-T0MEANS"); + let latent = recover_stationary_initial_latent_mean( + intercept, + printed_effect, + 1.0, + log_rate, 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, + .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("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, + .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("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); + .expect("stationary invariance"); + assert!((evolved_latent - latent).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; + 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_discrete_observed_mean_with_extra_process( + let recovered = recover_stationary_initial_observed_mean( loading, - initial, - original, intercept, - coupling, - predictor, - extra, + -0.225, + 1.0, + log_rate, 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, + .expect("eq5-stationary-T0MEANS"); + let latent = recover_stationary_initial_latent_mean( intercept, - manifest_mean, - delta, + -0.225, + 1.0, + log_rate, LagClock::EventTime, ) - .expect("eq3-eq5-mean"); - assert!((evolved_observed - recovered).abs() > 1e-3); - let impulse_observed = recover_discrete_observed_mean_with_impulse( + .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, - initial, - original, + 2.823, + log_rate, 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, + 1.0, LagClock::EventTime, ) - .expect("extra-process"); + .expect("τ+λμ_t"); assert_eq!( - refuse_evolved_observed_mean_as_extra_process_observed_mean( - evolved_observed, - recovered - ), - Err(PsychometricError::EvolvedObservedMeanIsNotExtraProcessObservedMean) + refuse_stationary_initial_latent_mean_as_observed_mean(latent, recovered), + Err(PsychometricError::StationaryInitialLatentMeanIsNotObservedMean) ); assert_eq!( - refuse_impulse_observed_mean_as_extra_process_observed_mean( - impulse_observed, - recovered - ), - Err(PsychometricError::ImpulseObservedMeanIsNotExtraProcessObservedMean) + refuse_stationary_initial_observed_mean_as_manifest_means(recovered, manifest_mean), + Err(PsychometricError::StationaryInitialObservedMeanIsNotManifestMeans) ); assert_eq!( - refuse_extra_process_contribution_as_observed_mean(contribution, recovered), - Err(PsychometricError::ExtraProcessContributionIsNotObservedMean) + refuse_evolved_observed_mean_as_stationary_initial_observed_mean(evolved, recovered), + Err(PsychometricError::EvolvedObservedMeanIsNotStationaryInitialObservedMean) ); assert_eq!( - refuse_extra_process_latent_mean_as_observed_mean(composed, recovered), - Err(PsychometricError::ExtraProcessLatentMeanIsNotObservedMean) + 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 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; + fn stationary_initial_observed_mean_invalid_inputs_fail_closed() { let intercept = 0.3_f64; - let manifest_mean = 0.5_f64; + let log_rate = -0.134_488_942_f64; assert_eq!( - recover_discrete_observed_mean_with_extra_process( - 0.0, - initial, - original, + recover_stationary_initial_observed_mean( + 2.0, intercept, - coupling, - predictor, - extra, - manifest_mean, - delta, + -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 ), - Ok(manifest_mean) + Err(PsychometricError::AsymptoticContinuousInterceptRequiresStableDrift) ); assert_eq!( - recover_discrete_observed_mean_with_extra_process( - loading, - initial, - original, + 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, - coupling, - predictor, - extra, - manifest_mean, - delta, - LagClock::SystemTime + -0.225, + 1.0, + log_rate, + 0.5, + LagClock::EventTime ), - Err(PsychometricError::EventTimeRequired) + 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] - #[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, + 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("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, + .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("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, + .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("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, + .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("after-extra-process"); + .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!( - refuse_extra_process_observed_mean_as_after_extra_process_observed_mean( - first_occasion, - recovered + recover_stationary_initial_latent_variance( + 0.0, + 0.0, + 0.0, + predictor_variance, + 0.0, + LagClock::EventTime ), - Err(PsychometricError::ExtraProcessObservedMeanIsNotAfterExtraProcessObservedMean) + Ok(0.0) ); assert_eq!( - refuse_evolved_observed_mean_as_after_extra_process_observed_mean( - evolved_observed, - recovered + recover_stationary_initial_latent_variance( + 0.0, + 0.0, + 0.0, + predictor_variance, + 0.5, + LagClock::EventTime ), - Err(PsychometricError::EvolvedObservedMeanIsNotAfterExtraProcessObservedMean) + 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("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("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_impulse_carry_observed_mean_as_after_extra_process_observed_mean( - carry_observed, - recovered - ), - Err(PsychometricError::ImpulseCarryObservedMeanIsNotAfterExtraProcessObservedMean) + refuse_stationary_initial_latent_variance_as_initial_latent_variance(recovered, 2.0), + Err(PsychometricError::StationaryInitialLatentVarianceIsNotInitialLatentVariance) ); assert_eq!( - refuse_after_extra_process_contribution_as_observed_mean(contribution, recovered), - Err(PsychometricError::AfterExtraProcessContributionIsNotObservedMean) + refuse_stationary_initial_latent_variance_as_stationary_within_subject( + recovered, state + ), + Err(PsychometricError::StationaryInitialLatentVarianceIsNotStationaryWithinSubject) ); assert_eq!( - refuse_after_extra_process_latent_mean_as_observed_mean(composed, recovered), - Err(PsychometricError::AfterExtraProcessLatentMeanIsNotObservedMean) + refuse_stationary_initial_latent_variance_as_trait_variance(recovered, trait_variance), + Err(PsychometricError::StationaryInitialLatentVarianceIsNotTraitVariance) ); - } - - #[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 + refuse_stationary_initial_latent_variance_as_asymptotic_time_independent_variance( + recovered, added ), - Err(PsychometricError::NonPositiveInterval) + Err( + PsychometricError::StationaryInitialLatentVarianceIsNotAsymptoticTimeIndependentVariance + ) ); assert_eq!( - recover_level_change_extra_process_contribution_after( - coupling, - predictor, - original, - extra, - 2.0, - 0.0, - LagClock::EventTime - ), - Err(PsychometricError::NonPositiveInterval) + refuse_stationary_initial_latent_variance_as_discrete_variance(recovered, discrete), + Err(PsychometricError::StationaryInitialLatentVarianceIsNotDiscreteVariance) ); + } + + #[test] + fn stationary_initial_latent_variance_invalid_inputs_fail_closed() { assert_eq!( - recover_discrete_observed_mean_with_extra_process_after( - 2.0, + recover_stationary_initial_latent_variance( 1.0, - original, - 0.3, - coupling, - predictor, - extra, - 0.5, - 2.0, + 0.4, + -0.225, 1.0, + -0.13, LagClock::SystemTime ), Err(PsychometricError::EventTimeRequired) ); assert_eq!( - recover_discrete_observed_mean_with_extra_process_after( + recover_stationary_initial_latent_variance( + 0.0, + 0.4, 0.0, 1.0, - original, - 0.3, - coupling, - predictor, - extra, - 0.5, - 2.0, - 1.0, + 0.0, LagClock::EventTime ), - Ok(0.5) + Err(PsychometricError::StationaryVarianceRequiresStableDrift) ); assert_eq!( - recover_level_change_extra_process_contribution_after( - coupling, - predictor, - original, - extra, - 2.0, + recover_stationary_initial_latent_variance( + 0.0, + 0.0, + -0.225, 1.0, - LagClock::SystemTime + 0.5, + LagClock::EventTime ), - Err(PsychometricError::EventTimeRequired) + Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) ); assert_eq!( - recover_level_change_extra_process_contribution_after( - coupling, - predictor, - original, - extra, + recover_stationary_initial_latent_variance( + 0.0, + 0.0, 0.0, 1.0, + 0.0, LagClock::EventTime ), - Err(PsychometricError::NonPositiveInterval) + Ok(0.0) ); assert_eq!( - recover_level_change_extra_process_contribution_after( - coupling, - predictor, - original, - extra, + recover_stationary_initial_latent_variance( f64::NAN, - 1.0, + 0.4, + 0.0, + 0.0, + -0.5, LagClock::EventTime ), - Err(PsychometricError::NonPositiveInterval) + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_latent_mean_with_extra_process_after( - 1.0, - original, - 0.3, - coupling, - predictor, - extra, - 2.0, - 2.0, + recover_stationary_initial_latent_variance( + f64::MAX, + f64::MAX, + 0.0, + 0.0, + -0.5, 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( - 1.0, - original, - 0.3, + recover_stationary_initial_latent_variance( + f64::MAX, 0.0, - predictor, - extra, - 2.0, 1.0, + f64::MAX, + -1.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; + 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 = -effect / printed_asym; - let recovered = recover_asymptotic_time_independent_predictor_effect( - effect, - predictor, + 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("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, + .expect("eq5-stationary-T0VAR"); + let latent = recover_stationary_initial_latent_variance( + trait_variance, + diffusion, + printed_effect, 1.0, - -0.549 / 0.219, + log_rate, LagClock::EventTime, ) - .expect("happiness-asym"); - assert!((happiness - 0.219).abs() < 1e-12); + .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_asymptotic_time_independent_predictor_effect( - 0.0, - predictor, + recover_stationary_initial_observed_variance( 0.0, - LagClock::EventTime + trait_variance, + diffusion, + printed_effect, + 1.0, + log_rate, + measurement_error, + manifest_trait, + LagClock::EventTime, ), - Ok(0.0) + Ok(measurement_error + manifest_trait) ); assert_eq!( - recover_asymptotic_time_independent_predictor_effect( - effect, + recover_stationary_initial_observed_variance( + loading, 0.0, 0.0, - LagClock::EventTime + 0.0, + 1.0, + 0.0, + measurement_error, + 0.0, + LagClock::EventTime, ), - Ok(0.0) + 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 asymptotic_time_independent_effect_is_not_coefficient_discrete_cint_or_impulse() { - let effect = -0.225_f64; - let predictor = 2.0_f64; + 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 recovered = recover_asymptotic_time_independent_predictor_effect( - effect, - predictor, + 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("asymTIPREDEFFECT"); - let discrete = recover_discrete_time_independent_predictor_effect( - effect, - predictor, - log_rate, + .expect("eq5-stationary-T0VAR"); + let latent = recover_stationary_initial_latent_variance( + trait_variance, + diffusion, + -0.225, 1.0, + log_rate, 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); + .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_asymptotic_time_independent_effect_as_coefficient(recovered, effect), - Err(PsychometricError::AsymptoticTimeIndependentEffectIsNotCoefficient) + refuse_stationary_initial_latent_variance_as_observed_variance(latent, recovered), + Err(PsychometricError::StationaryInitialLatentVarianceIsNotObservedVariance) ); assert_eq!( - refuse_asymptotic_time_independent_effect_as_discrete_effect(recovered, discrete), - Err(PsychometricError::AsymptoticTimeIndependentEffectIsNotDiscreteEffect) + refuse_stationary_initial_observed_variance_as_measurement_error( + recovered, + measurement_error + ), + Err(PsychometricError::StationaryInitialObservedVarianceIsNotMeasurementError) ); assert_eq!( - refuse_asymptotic_time_independent_effect_as_continuous_intercept(recovered, 0.3), - Err(PsychometricError::AsymptoticTimeIndependentEffectIsNotContinuousIntercept) + refuse_evolved_observed_variance_as_stationary_initial_observed_variance( + evolved, recovered + ), + Err(PsychometricError::EvolvedObservedVarianceIsNotStationaryInitialObservedVariance) ); assert_eq!( - refuse_asymptotic_time_independent_effect_as_time_dependent_impulse(recovered, impulse), - Err(PsychometricError::AsymptoticTimeIndependentEffectIsNotTimeDependentImpulse) + refuse_stationary_within_subject_observed_variance_as_stationary_initial_observed_variance( + state_only_observed, + recovered + ), + Err( + PsychometricError::StationaryWithinSubjectObservedVarianceIsNotStationaryInitialObservedVariance + ) + ); + assert_eq!( + refuse_initial_observed_variance_as_stationary_initial_observed_variance( + free_initial_observed, + recovered + ), + Err(PsychometricError::InitialObservedVarianceIsNotStationaryInitialObservedVariance) ); } #[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; + fn stationary_initial_observed_variance_invalid_inputs_fail_closed() { assert_eq!( - recover_asymptotic_time_independent_predictor_effect( - effect, - predictor, - log_rate, + 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_asymptotic_time_independent_predictor_effect( - effect, - predictor, + 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::AsymptoticTimeIndependentEffectRequiresStableDrift) + Err(PsychometricError::StationaryVarianceRequiresStableDrift) ); assert_eq!( - recover_asymptotic_time_independent_predictor_effect( - effect, - predictor, + 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::AsymptoticTimeIndependentEffectRequiresStableDrift) ); assert_eq!( - recover_asymptotic_time_independent_predictor_effect( - f64::NAN, - predictor, - log_rate, + 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::InvalidNumericInput) + Ok(0.6) ); assert_eq!( - recover_asymptotic_time_independent_predictor_effect( - 1e308, - 2.0, - log_rate, + recover_stationary_initial_observed_variance( + f64::NAN, + 1.0, + 0.4, + 0.0, + 0.0, + -0.5, + 0.5, + 0.0, LagClock::EventTime ), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_asymptotic_time_independent_predictor_effect( - 1e308, - 1.0, - -1e-308, + recover_stationary_initial_observed_variance( + 2.0, + f64::MAX, + f64::MAX, + 0.0, + 0.0, + -0.5, + 0.5, + 0.0, LagClock::EventTime ), Err(PsychometricError::InvalidNumericInput) @@ -8839,142 +10921,306 @@ mod tests { } #[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; + #[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 = -effect / printed_asym; + 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_asymptotic_time_independent_predictor_variance( - effect, + 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"); - 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, + 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("doubled-v"); - assert!((doubled - 2.0 * expected).abs() < 1e-12); + .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_asymptotic_time_independent_predictor_variance( + recover_stationary_lagged_latent_covariance( 0.0, - predictor_variance, 0.0, - LagClock::EventTime - ), - Ok(0.0) - ); - assert_eq!( - recover_asymptotic_time_independent_predictor_variance( - effect, 0.0, + predictor_variance, 0.0, + event_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( + trait_variance, + diffusion, + printed_effect, + predictor_variance, + log_rate, + 1e-12, + LagClock::EventTime, + ) + .expect("Δt→0+"); + assert!((near - contemporaneous).abs() < 1e-9); } #[test] - fn asymptotic_time_independent_variance_is_not_trait_stationary_or_mean_effect() { - let effect = -0.225_f64; + 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 predictor_variance = 2.0_f64; - let recovered = recover_asymptotic_time_independent_predictor_variance( - effect, - predictor_variance, + 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("addedTIPREDVAR"); - let mean_effect = recover_asymptotic_time_independent_predictor_effect( - effect, + .expect("stationary lagged T0VAR"); + let contemporaneous = recover_stationary_initial_latent_variance( + trait_variance, + diffusion, + -0.225, 1.0, log_rate, LagClock::EventTime, ) - .expect("asymTIPREDEFFECT"); - let stationary = recover_stationary_latent_variance(0.4, log_rate, LagClock::EventTime) + .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 = 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); + 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!( - refuse_asymptotic_time_independent_variance_as_trait_variance(recovered, trait_plus), - Err(PsychometricError::AsymptoticTimeIndependentVarianceIsNotTraitVariance) + refuse_stationary_lagged_latent_covariance_as_stationary_initial_latent_variance( + recovered, + contemporaneous + ), + Err( + PsychometricError::StationaryLaggedLatentCovarianceIsNotStationaryInitialLatentVariance + ) ); assert_eq!( - refuse_asymptotic_time_independent_variance_as_stationary_within_subject( - recovered, stationary + refuse_stationary_lagged_latent_covariance_as_decayed_stationary_variance( + recovered, decayed ), - Err(PsychometricError::AsymptoticTimeIndependentVarianceIsNotStationaryWithinSubject) + Err(PsychometricError::StationaryLaggedLatentCovarianceIsNotDecayedStationaryVariance) ); assert_eq!( - refuse_asymptotic_time_independent_variance_as_asymptotic_effect( - recovered, - mean_effect + refuse_trait_plus_state_lagged_covariance_as_stationary_lagged_latent_covariance( + trait_plus_state, + recovered ), - Err(PsychometricError::AsymptoticTimeIndependentVarianceIsNotAsymptoticEffect) + Err( + PsychometricError::TraitPlusStateLaggedCovarianceIsNotStationaryLaggedLatentCovariance + ) ); } #[test] - fn asymptotic_time_independent_variance_invalid_inputs_fail_closed() { - let effect = -0.225_f64; - let log_rate = -0.134_488_942_f64; + fn stationary_lagged_latent_covariance_invalid_inputs_fail_closed() { assert_eq!( - recover_asymptotic_time_independent_predictor_variance( - effect, + recover_stationary_lagged_latent_covariance( + 1.0, + 0.4, + -0.225, + 1.0, + -0.13, 1.0, - log_rate, LagClock::SystemTime ), Err(PsychometricError::EventTimeRequired) ); assert_eq!( - recover_asymptotic_time_independent_predictor_variance( - effect, + recover_stationary_lagged_latent_covariance( + 1.0, + 0.4, + -0.225, + 1.0, + -0.13, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_stationary_lagged_latent_covariance( + 0.0, + 0.4, + 0.0, + 1.0, + 0.0, 1.0, + LagClock::EventTime + ), + Err(PsychometricError::StationaryVarianceRequiresStableDrift) + ); + assert_eq!( + recover_stationary_lagged_latent_covariance( + 0.0, 0.0, + -0.225, + 1.0, + 0.5, + 1.0, LagClock::EventTime ), Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) ); assert_eq!( - recover_asymptotic_time_independent_predictor_variance( - effect, - -1.0, - log_rate, + 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( + f64::NAN, + 0.4, + 0.0, + 0.0, + -0.5, + 1.0, LagClock::EventTime ), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_asymptotic_time_independent_predictor_variance( - 1e308, + recover_stationary_lagged_latent_covariance( + f64::MAX, + f64::MAX, + 0.0, + 0.0, + -0.5, 1.0, - -1e-308, LagClock::EventTime ), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_asymptotic_time_independent_predictor_variance( - 1e200, + recover_stationary_lagged_latent_covariance( + f64::MAX, + 0.0, + 1.0, + f64::MAX, + -1.0, 1.0, - -1e-200, LagClock::EventTime ), Err(PsychometricError::InvalidNumericInput) @@ -8982,543 +11228,918 @@ mod tests { } #[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. + 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 intercept = 0.3_f64; - let recovered = - recover_asymptotic_continuous_intercept(intercept, log_rate, LagClock::EventTime) - .expect("asymCINT"); - let expected = intercept / -log_rate; + 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 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 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, + let contemporaneous = recover_stationary_initial_observed_variance( + loading, + trait_variance, + diffusion, + printed_effect, + 1.0, log_rate, - 1e8, + measurement_error, + manifest_trait, LagClock::EventTime, ) - .expect("large-delta"); - assert!((recovered - large_delta).abs() < 1e-9); + .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_asymptotic_continuous_intercept(0.0, 0.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_asymptotic_continuous_intercept(0.0, 0.5, 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) ); + 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 asymptotic_continuous_intercept_is_not_cint_increment_t0_or_tipred() { - let intercept = 0.3_f64; + 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 recovered = - recover_asymptotic_continuous_intercept(intercept, log_rate, LagClock::EventTime) - .expect("asymCINT"); - let discrete = recover_discrete_continuous_intercept_effect( - intercept, - log_rate, + 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("dtCINT"); - let tipred = recover_asymptotic_time_independent_predictor_effect( + .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("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) - ); + .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!( - refuse_asymptotic_continuous_intercept_as_discrete_increment(recovered, discrete), - Err(PsychometricError::AsymptoticContinuousInterceptIsNotDiscreteIncrement) + refuse_stationary_lagged_latent_covariance_as_observed_covariance(latent, recovered), + Err(PsychometricError::StationaryLaggedLatentCovarianceIsNotObservedCovariance) ); assert_eq!( - refuse_asymptotic_continuous_intercept_as_initial_latent_mean(recovered, 2.823), - Err(PsychometricError::AsymptoticContinuousInterceptIsNotInitialLatentMean) + refuse_measurement_error_as_stationary_lagged_observed_covariance( + measurement_error, + recovered + ), + Err(PsychometricError::MeasurementErrorIsNotStationaryLaggedObservedCovariance) ); assert_eq!( - refuse_asymptotic_continuous_intercept_as_asymptotic_time_independent_effect( - recovered, tipred + refuse_stationary_initial_observed_variance_as_stationary_lagged_observed_covariance( + contemporaneous, + recovered ), Err( - PsychometricError::AsymptoticContinuousInterceptIsNotAsymptoticTimeIndependentEffect + PsychometricError::StationaryInitialObservedVarianceIsNotStationaryLaggedObservedCovariance ) ); } #[test] - fn asymptotic_continuous_intercept_invalid_inputs_fail_closed() { - let intercept = 0.3_f64; - let log_rate = -0.134_488_942_f64; + fn stationary_lagged_observed_covariance_invalid_inputs_fail_closed() { assert_eq!( - recover_asymptotic_continuous_intercept(intercept, log_rate, LagClock::SystemTime), + 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_asymptotic_continuous_intercept(intercept, 0.0, LagClock::EventTime), - Err(PsychometricError::AsymptoticContinuousInterceptRequiresStableDrift) + 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_asymptotic_continuous_intercept(intercept, 0.5, LagClock::EventTime), - Err(PsychometricError::AsymptoticContinuousInterceptRequiresStableDrift) + 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) ); assert_eq!( - recover_asymptotic_continuous_intercept(f64::NAN, log_rate, LagClock::EventTime), + 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_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_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_asymptotic_continuous_intercept(1e308, -1e-308, 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) ); } #[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. + #[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 intercept = 0.3_f64; - let recovered = recover_stationary_initial_latent_mean( - intercept, + 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, - 1.0, + predictor_variance, log_rate, + event_delta, 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( + .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, - 1.0, + predictor_variance, 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, + .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, - 1.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_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( + trait_variance, + diffusion, + printed_effect, + predictor_variance, log_rate, + 1e8, LagClock::EventTime, ) - .expect("intercept-only"); - assert!((intercept_only_path - intercept_only).abs() < 1e-15); - let tipred_only = recover_stationary_initial_latent_mean( - 0.0, + .expect("Δt→∞"); + assert!((far - contemporaneous).abs() < 1e-12); + let near = recover_stationary_later_latent_variance( + trait_variance, + diffusion, printed_effect, - 1.0, + predictor_variance, log_rate, + 1e-12, 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) - ); + .expect("Δt→0+"); + assert!((near - contemporaneous).abs() < 1e-9); } #[test] - fn stationary_initial_latent_mean_is_not_t0_cint_tipred_or_discrete() { - let intercept = 0.3_f64; + fn stationary_later_latent_variance_is_not_lagged_discrete_or_process_noise() { + 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_mean( - intercept, + 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 T0MEANS"); - let intercept_only = - recover_asymptotic_continuous_intercept(intercept, log_rate, LagClock::EventTime) - .expect("asymCINT"); - let tipred = recover_asymptotic_time_independent_predictor_effect( + .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("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) - ); + .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_stationary_initial_latent_mean_as_asymptotic_continuous_intercept( - recovered, - intercept_only - ), - Err(PsychometricError::StationaryInitialLatentMeanIsNotAsymptoticContinuousIntercept) + refuse_stationary_later_latent_variance_as_lagged_covariance(recovered, lagged), + Err(PsychometricError::StationaryLaterLatentVarianceIsNotLaggedCovariance) ); assert_eq!( - refuse_stationary_initial_latent_mean_as_asymptotic_time_independent_effect( - recovered, tipred - ), - Err(PsychometricError::StationaryInitialLatentMeanIsNotAsymptoticTimeIndependentEffect) + refuse_stationary_later_latent_variance_as_discrete_variance(recovered, free_discrete), + Err(PsychometricError::StationaryLaterLatentVarianceIsNotDiscreteVariance) ); assert_eq!( - refuse_stationary_initial_latent_mean_as_discrete_mean(recovered, discrete), - Err(PsychometricError::StationaryInitialLatentMeanIsNotDiscreteMean) + refuse_stationary_later_latent_variance_as_process_noise(recovered, process_noise), + Err(PsychometricError::StationaryLaterLatentVarianceIsNotProcessNoise) ); } #[test] - fn stationary_initial_latent_mean_invalid_inputs_fail_closed() { - let intercept = 0.3_f64; - let log_rate = -0.134_488_942_f64; + fn stationary_later_latent_variance_invalid_inputs_fail_closed() { assert_eq!( - recover_stationary_initial_latent_mean( - intercept, + recover_stationary_later_latent_variance( + 1.0, + 0.4, -0.225, 1.0, - log_rate, + -0.13, + 1.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_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_initial_latent_mean(0.0, -0.225, 1.0, 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_later_latent_variance( + 0.0, + 0.0, + -0.225, + 1.0, + 0.5, + 1.0, + LagClock::EventTime + ), Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) ); assert_eq!( - recover_stationary_initial_latent_mean( + 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( f64::NAN, - -0.225, + 0.4, + 0.0, + 0.0, + -0.5, 1.0, - log_rate, LagClock::EventTime ), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_stationary_initial_latent_mean(1e308, 1e308, 1.0, -1e-308, LagClock::EventTime), + recover_stationary_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, + 1.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 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) - // constrain first-occasion means to the model-predicted - // mean. Equation 5 maps E(y_0) = τ + λ of that mean. + // 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 intercept = 0.3_f64; + let trait_variance = 1.0_f64; + let diffusion = 0.4_f64; let loading = 2.0_f64; - let manifest_mean = 0.5_f64; - let recovered = recover_stationary_initial_observed_mean( + 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, - intercept, + trait_variance, + diffusion, printed_effect, 1.0, log_rate, - manifest_mean, + event_delta, + measurement_error, + manifest_trait, LagClock::EventTime, ) - .expect("eq5-stationary-T0MEANS"); - let latent = recover_stationary_initial_latent_mean( - intercept, + .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 T0MEANS"); - let expected = - recover_manifest_observed_mean(loading, latent, manifest_mean).expect("τ+λμ"); + .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 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( + 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, - 2.823, - log_rate, - intercept, - manifest_mean, + trait_variance, + diffusion, + printed_effect, 1.0, + log_rate, + event_delta, + manifest_trait, LagClock::EventTime, ) - .expect("τ+λμ_t"); - assert!((recovered - evolved_from_free).abs() > 1e-3); - assert!((recovered - manifest_mean).abs() > 1e-3); + .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_stationary_initial_observed_mean( + recover_stationary_later_observed_variance( 0.0, - intercept, + trait_variance, + diffusion, printed_effect, 1.0, log_rate, - manifest_mean, + event_delta, + measurement_error, + manifest_trait, LagClock::EventTime, ), - Ok(manifest_mean) + Ok(measurement_error + manifest_trait) ); assert_eq!( - recover_stationary_initial_observed_mean( + recover_stationary_later_observed_variance( loading, 0.0, 0.0, + 0.0, 1.0, 0.0, - manifest_mean, + event_delta, + 0.0, + 0.0, LagClock::EventTime, ), - Ok(manifest_mean) + Ok(0.0) ); - 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, + let zero_manifest_trait = recover_stationary_later_observed_variance( + loading, + trait_variance, + diffusion, printed_effect, 1.0, - 2.0, + log_rate, + event_delta, + measurement_error, + 0.0, LagClock::EventTime, ) - .expect("stationary invariance"); - assert!((evolved_latent - latent).abs() < 1e-12); + .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_initial_observed_mean_is_not_manifest_latent_evolved_or_free() { - let intercept = 0.3_f64; + 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 manifest_mean = 0.5_f64; - let recovered = recover_stationary_initial_observed_mean( + let measurement_error = 0.5_f64; + let event_delta = 1.0_f64; + let recovered = recover_stationary_later_observed_variance( loading, - intercept, + trait_variance, + diffusion, -0.225, 1.0, log_rate, - manifest_mean, + event_delta, + measurement_error, + 0.1, LagClock::EventTime, ) - .expect("eq5-stationary-T0MEANS"); - let latent = recover_stationary_initial_latent_mean( - intercept, + .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 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( + .expect("stationary later T0VAR"); + let lagged = recover_stationary_lagged_observed_covariance( loading, - 2.823, - log_rate, - intercept, - manifest_mean, + trait_variance, + diffusion, + -0.225, 1.0, + log_rate, + event_delta, + 0.1, 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) - ); + .expect("eq5-lagged-stationary-T0VAR"); assert_eq!( - refuse_evolved_observed_mean_as_stationary_initial_observed_mean(evolved, recovered), - Err(PsychometricError::EvolvedObservedMeanIsNotStationaryInitialObservedMean) + refuse_stationary_later_latent_variance_as_observed_variance(latent, recovered), + Err(PsychometricError::StationaryLaterLatentVarianceIsNotObservedVariance) ); assert_eq!( - refuse_asymptotic_continuous_intercept_observed_mean_as_stationary_initial_observed_mean( - intercept_only_observed, + refuse_measurement_error_as_stationary_later_observed_variance( + measurement_error, recovered ), - Err( - PsychometricError::AsymptoticContinuousInterceptObservedMeanIsNotStationaryInitialObservedMean - ) + Err(PsychometricError::MeasurementErrorIsNotStationaryLaterObservedVariance) ); assert_eq!( - refuse_initial_observed_mean_as_stationary_initial_observed_mean( - free_initial_observed, - recovered + refuse_stationary_lagged_observed_covariance_as_stationary_later_observed_variance( + lagged, recovered ), - Err(PsychometricError::InitialObservedMeanIsNotStationaryInitialObservedMean) + Err( + PsychometricError::StationaryLaggedObservedCovarianceIsNotStationaryLaterObservedVariance + ) ); } #[test] - fn stationary_initial_observed_mean_invalid_inputs_fail_closed() { - let intercept = 0.3_f64; - let log_rate = -0.134_488_942_f64; + #[allow(clippy::too_many_lines)] + fn stationary_later_observed_variance_invalid_inputs_fail_closed() { assert_eq!( - recover_stationary_initial_observed_mean( + recover_stationary_later_observed_variance( 2.0, - intercept, + 1.0, + 0.4, -0.225, 1.0, - log_rate, + -0.13, + 1.0, 0.5, + 0.1, LagClock::SystemTime ), Err(PsychometricError::EventTimeRequired) ); assert_eq!( - recover_stationary_initial_observed_mean( + recover_stationary_later_observed_variance( 2.0, - intercept, - 0.0, 1.0, + 0.4, + -0.225, + 1.0, + -0.13, 0.0, 0.5, + 0.1, LagClock::EventTime ), - Err(PsychometricError::AsymptoticContinuousInterceptRequiresStableDrift) + Err(PsychometricError::NonPositiveInterval) ); assert_eq!( - recover_stationary_initial_observed_mean( + 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_stationary_later_observed_variance( 2.0, 0.0, + 0.0, -0.225, 1.0, 0.5, - 0.5, + 1.0, + 0.0, + 0.0, LagClock::EventTime ), Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) ); assert_eq!( - recover_stationary_initial_observed_mean( - f64::NAN, - intercept, - -0.225, + recover_stationary_later_observed_variance( + 2.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, 1.0, - log_rate, 0.5, + 0.1, + LagClock::EventTime + ), + Ok(0.6) + ); + assert_eq!( + 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_stationary_initial_observed_mean( + recover_stationary_later_observed_variance( 2.0, - 1e308, - 1e308, + f64::MAX, + f64::MAX, + 0.0, + 0.0, + -0.5, 1.0, - -1e-308, - 0.5, + 0.0, + 0.0, 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. + ); + } + + #[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 recovered = recover_stationary_initial_latent_variance( + 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("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"); + .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, @@ -9526,77 +12147,167 @@ mod tests { LagClock::EventTime, ) .expect("addedTIPREDVAR"); - assert!((recovered - (trait_plus_state + added)).abs() < 1e-12); - let state_only = recover_stationary_initial_latent_variance( + 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"); - assert!((state_only - state).abs() < 1e-15); - let trait_only = recover_stationary_initial_latent_variance( + .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"); + .expect("trait-only predetermined later"); assert!((trait_only - trait_variance).abs() < 1e-15); - let added_only = recover_stationary_initial_latent_variance( + 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"); + .expect("ti-only predetermined later"); assert!((added_only - added).abs() < 1e-15); assert_eq!( - recover_stationary_initial_latent_variance( - 0.0, + recover_predetermined_later_latent_variance( 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, + 0.0, + event_delta, LagClock::EventTime ), 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 stationary_initial_latent_variance_is_not_t0_state_trait_tipred_or_discrete() { + 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 recovered = recover_stationary_initial_latent_variance( + 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("stationary T0VAR"); - let state = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) - .expect("asymDIFFUSION"); + .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, @@ -9604,123 +12315,144 @@ mod tests { LagClock::EventTime, ) .expect("addedTIPREDVAR"); - let discrete = recover_discrete_latent_variance( - recovered, + let free_discrete = recover_discrete_latent_variance( + trait_variance + initial_latent_variance + added, diffusion, log_rate, - 1.0, + 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("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_stationary_initial_latent_variance_as_stationary_within_subject( - recovered, state + refuse_predetermined_later_latent_variance_as_stationary_later_latent_variance( + recovered, + stationary_later ), - Err(PsychometricError::StationaryInitialLatentVarianceIsNotStationaryWithinSubject) - ); - assert_eq!( - refuse_stationary_initial_latent_variance_as_trait_variance(recovered, trait_variance), - Err(PsychometricError::StationaryInitialLatentVarianceIsNotTraitVariance) + Err( + PsychometricError::PredeterminedLaterLatentVarianceIsNotStationaryLaterLatentVariance + ) ); assert_eq!( - refuse_stationary_initial_latent_variance_as_asymptotic_time_independent_variance( - recovered, added + refuse_predetermined_later_latent_variance_as_discrete_variance( + recovered, + free_discrete ), - Err( - PsychometricError::StationaryInitialLatentVarianceIsNotAsymptoticTimeIndependentVariance - ) + Err(PsychometricError::PredeterminedLaterLatentVarianceIsNotDiscreteVariance) ); assert_eq!( - refuse_stationary_initial_latent_variance_as_discrete_variance(recovered, discrete), - Err(PsychometricError::StationaryInitialLatentVarianceIsNotDiscreteVariance) + refuse_predetermined_later_latent_variance_as_initial_latent_variance( + recovered, + initial_latent_variance + ), + Err(PsychometricError::PredeterminedLaterLatentVarianceIsNotInitialLatentVariance) ); } #[test] - fn stationary_initial_latent_variance_invalid_inputs_fail_closed() { + #[allow(clippy::too_many_lines)] + fn predetermined_later_latent_variance_invalid_inputs_fail_closed() { assert_eq!( - recover_stationary_initial_latent_variance( + 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_stationary_initial_latent_variance( - 0.0, + recover_predetermined_later_latent_variance( + 1.0, + 2.0, 0.4, - 0.0, + -0.225, 1.0, + -0.13, 0.0, LagClock::EventTime ), - Err(PsychometricError::StationaryVarianceRequiresStableDrift) + Err(PsychometricError::NonPositiveInterval) ); assert_eq!( - recover_stationary_initial_latent_variance( + 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_stationary_initial_latent_variance( + recover_predetermined_later_latent_variance( + 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, + 1.0, LagClock::EventTime ), 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_stationary_initial_latent_variance( + 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_stationary_initial_latent_variance( + 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_stationary_initial_latent_variance( + recover_predetermined_later_latent_variance( f64::MAX, 0.0, + 0.0, 1.0, f64::MAX, -1.0, + 1.0, LagClock::EventTime ), Err(PsychometricError::InvalidNumericInput) @@ -9728,41 +12460,47 @@ mod tests { } #[test] - fn stationary_initial_observed_variance_recovers_driver_equation_five_of_section_four_point_three() + #[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) - // constrain first-occasion variances to the model-predicted - // variance. Equation 5 maps Var(y_0) = λ² of that variance - // plus θ + ψ. + // 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 recovered = recover_stationary_initial_observed_variance( + 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-stationary-T0VAR"); - let latent = recover_stationary_initial_latent_variance( + .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("stationary T0VAR"); + .expect("predetermined later T0VAR"); let expected = recover_manifest_trait_plus_state_observed_variance( loading, latent, @@ -9771,31 +12509,49 @@ mod tests { ) .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 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 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_stationary_initial_observed_variance( + 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, @@ -9803,121 +12559,127 @@ mod tests { Ok(measurement_error + manifest_trait) ); assert_eq!( - recover_stationary_initial_observed_variance( + recover_predetermined_later_observed_variance( loading, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, - measurement_error, + event_delta, + 0.0, 0.0, LagClock::EventTime, ), - Ok(measurement_error) + Ok(0.0) ); - let zero_manifest_trait = recover_stationary_initial_observed_variance( + 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_observed_variance(loading, latent, measurement_error).expect("λ²p+θ"); + 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_initial_observed_variance_is_not_manifest_latent_evolved_or_free() { + 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 recovered = recover_stationary_initial_observed_variance( + 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-stationary-T0VAR"); - let latent = recover_stationary_initial_latent_variance( + .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("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) - ); + .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_stationary_initial_observed_variance_as_measurement_error( - recovered, - measurement_error - ), - Err(PsychometricError::StationaryInitialObservedVarianceIsNotMeasurementError) + refuse_predetermined_later_latent_variance_as_observed_variance(latent, recovered), + Err(PsychometricError::PredeterminedLaterLatentVarianceIsNotObservedVariance) ); assert_eq!( - refuse_evolved_observed_variance_as_stationary_initial_observed_variance( - evolved, recovered + refuse_measurement_error_as_predetermined_later_observed_variance( + measurement_error, + recovered ), - Err(PsychometricError::EvolvedObservedVarianceIsNotStationaryInitialObservedVariance) + Err(PsychometricError::MeasurementErrorIsNotPredeterminedLaterObservedVariance) ); assert_eq!( - refuse_stationary_within_subject_observed_variance_as_stationary_initial_observed_variance( - state_only_observed, + refuse_stationary_later_observed_variance_as_predetermined_later_observed_variance( + stationary_later, recovered ), Err( - PsychometricError::StationaryWithinSubjectObservedVarianceIsNotStationaryInitialObservedVariance + PsychometricError::StationaryLaterObservedVarianceIsNotPredeterminedLaterObservedVariance ) ); - assert_eq!( - refuse_initial_observed_variance_as_stationary_initial_observed_variance( - free_initial_observed, - recovered - ), - Err(PsychometricError::InitialObservedVarianceIsNotStationaryInitialObservedVariance) - ); } #[test] - fn stationary_initial_observed_variance_invalid_inputs_fail_closed() { + #[allow(clippy::too_many_lines)] + fn predetermined_later_observed_variance_invalid_inputs_fail_closed() { assert_eq!( - recover_stationary_initial_observed_variance( + 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 @@ -9925,41 +12687,47 @@ mod tests { Err(PsychometricError::EventTimeRequired) ); assert_eq!( - recover_stationary_initial_observed_variance( + recover_predetermined_later_observed_variance( + 2.0, + 1.0, 2.0, - 0.0, 0.4, - 0.0, + -0.225, 1.0, + -0.13, 0.0, 0.5, - 0.0, + 0.1, LagClock::EventTime ), - Err(PsychometricError::StationaryVarianceRequiresStableDrift) + Err(PsychometricError::NonPositiveInterval) ); assert_eq!( - recover_stationary_initial_observed_variance( + recover_predetermined_later_observed_variance( 2.0, 0.0, 0.0, + 0.0, -0.225, 1.0, 0.5, - 0.5, + 1.0, + 0.0, 0.0, LagClock::EventTime ), Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) ); assert_eq!( - recover_stationary_initial_observed_variance( + 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 @@ -9967,28 +12735,32 @@ mod tests { Ok(0.6) ); assert_eq!( - recover_stationary_initial_observed_variance( + recover_predetermined_later_observed_variance( f64::NAN, 1.0, + 2.0, 0.4, 0.0, 0.0, -0.5, - 0.5, + 1.0, + 0.0, 0.0, LagClock::EventTime ), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_stationary_initial_observed_variance( + recover_predetermined_later_observed_variance( 2.0, f64::MAX, f64::MAX, 0.0, 0.0, + 0.0, -0.5, - 0.5, + 1.0, + 0.0, 0.0, LagClock::EventTime ), @@ -9998,38 +12770,34 @@ 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_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_stationary_lagged_latent_covariance( + let recovered = recover_predetermined_lagged_latent_covariance( trait_variance, - diffusion, + initial_latent_variance, 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, + .expect("predetermined lagged T0VAR"); + let lagged_state = recover_discrete_lagged_latent_covariance( + initial_latent_variance, log_rate, event_delta, LagClock::EventTime, ) - .expect("trait+state lagged"); + .expect("e^{aΔt}p_0"); let added = recover_asymptotic_time_independent_predictor_variance( printed_effect, predictor_variance, @@ -10037,67 +12805,55 @@ mod tests { LagClock::EventTime, ) .expect("addedTIPREDVAR"); - assert!((recovered - (trait_plus_state + added)).abs() < 1e-12); - let contemporaneous = recover_stationary_initial_latent_variance( + 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, - 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( + .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("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 later = recover_predetermined_later_latent_variance( trait_variance, - 0.0, - 0.0, + initial_latent_variance, + diffusion, + printed_effect, predictor_variance, - 0.0, + log_rate, 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, + .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("ti-only lagged"); - assert!((added_only - added).abs() < 1e-15); + .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_stationary_lagged_latent_covariance( + recover_predetermined_lagged_latent_covariance( 0.0, 0.0, 0.0, @@ -10108,9 +12864,20 @@ mod tests { ), Ok(0.0) ); - let far = recover_stationary_lagged_latent_covariance( + let trait_only = recover_predetermined_lagged_latent_covariance( trait_variance, - diffusion, + 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, @@ -10119,9 +12886,9 @@ mod tests { ) .expect("Δt→∞"); assert!((far - (trait_variance + added)).abs() < 1e-12); - let near = recover_stationary_lagged_latent_covariance( + let near = recover_predetermined_lagged_latent_covariance( trait_variance, - diffusion, + initial_latent_variance, printed_effect, predictor_variance, log_rate, @@ -10129,16 +12896,38 @@ mod tests { LagClock::EventTime, ) .expect("Δt→0+"); - assert!((near - contemporaneous).abs() < 1e-9); + 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 stationary_lagged_latent_covariance_is_not_contemporaneous_decayed_or_trait_state() { + 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_stationary_lagged_latent_covariance( + 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, @@ -10148,67 +12937,66 @@ mod tests { LagClock::EventTime, ) .expect("stationary lagged T0VAR"); - let contemporaneous = recover_stationary_initial_latent_variance( + let later = recover_predetermined_later_latent_variance( trait_variance, + initial_latent_variance, diffusion, -0.225, 1.0, log_rate, + event_delta, LagClock::EventTime, ) - .expect("stationary T0VAR"); - let decayed = recover_discrete_lagged_latent_covariance( - contemporaneous, + .expect("predetermined later T0VAR"); + let added = recover_asymptotic_time_independent_predictor_variance( + -0.225, + 1.0, 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, + .expect("addedTIPREDVAR"); + let decayed_total = recover_discrete_lagged_latent_covariance( + trait_variance + initial_latent_variance + added, 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); + .expect("e^{aΔt}(trait+p_0+added)"); assert_eq!( - refuse_stationary_lagged_latent_covariance_as_stationary_initial_latent_variance( + refuse_predetermined_lagged_latent_covariance_as_stationary_lagged_covariance( recovered, - contemporaneous + stationary_lagged ), - Err( - PsychometricError::StationaryLaggedLatentCovarianceIsNotStationaryInitialLatentVariance - ) + Err(PsychometricError::PredeterminedLaggedLatentCovarianceIsNotStationaryLaggedCovariance) ); assert_eq!( - refuse_stationary_lagged_latent_covariance_as_decayed_stationary_variance( - recovered, decayed + refuse_predetermined_lagged_latent_covariance_as_later_latent_variance( + recovered, later ), - Err(PsychometricError::StationaryLaggedLatentCovarianceIsNotDecayedStationaryVariance) + Err(PsychometricError::PredeterminedLaggedLatentCovarianceIsNotLaterLatentVariance) ); assert_eq!( - refuse_trait_plus_state_lagged_covariance_as_stationary_lagged_latent_covariance( - trait_plus_state, - recovered + refuse_predetermined_lagged_latent_covariance_as_decayed_total( + recovered, + decayed_total ), - Err( - PsychometricError::TraitPlusStateLaggedCovarianceIsNotStationaryLaggedLatentCovariance - ) + Err(PsychometricError::PredeterminedLaggedLatentCovarianceIsNotDecayedTotal) + ); + assert_eq!( + refuse_predetermined_lagged_latent_covariance_as_initial_latent_variance( + recovered, + initial_latent_variance + ), + Err(PsychometricError::PredeterminedLaggedLatentCovarianceIsNotInitialLatentVariance) ); } #[test] - fn stationary_lagged_latent_covariance_invalid_inputs_fail_closed() { + fn predetermined_lagged_latent_covariance_invalid_inputs_fail_closed() { assert_eq!( - recover_stationary_lagged_latent_covariance( + recover_predetermined_lagged_latent_covariance( 1.0, - 0.4, + 2.0, -0.225, 1.0, -0.13, @@ -10218,9 +13006,9 @@ mod tests { Err(PsychometricError::EventTimeRequired) ); assert_eq!( - recover_stationary_lagged_latent_covariance( + recover_predetermined_lagged_latent_covariance( 1.0, - 0.4, + 2.0, -0.225, 1.0, -0.13, @@ -10230,19 +13018,7 @@ mod tests { Err(PsychometricError::NonPositiveInterval) ); assert_eq!( - recover_stationary_lagged_latent_covariance( - 0.0, - 0.4, - 0.0, - 1.0, - 0.0, - 1.0, - LagClock::EventTime - ), - Err(PsychometricError::StationaryVarianceRequiresStableDrift) - ); - assert_eq!( - recover_stationary_lagged_latent_covariance( + recover_predetermined_lagged_latent_covariance( 0.0, 0.0, -0.225, @@ -10254,7 +13030,7 @@ mod tests { Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) ); assert_eq!( - recover_stationary_lagged_latent_covariance( + recover_predetermined_lagged_latent_covariance( 0.0, 0.0, 0.0, @@ -10265,10 +13041,21 @@ mod tests { ), 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!( - recover_stationary_lagged_latent_covariance( + recover_predetermined_lagged_latent_covariance( f64::NAN, - 0.4, + 2.0, 0.0, 0.0, -0.5, @@ -10278,7 +13065,7 @@ mod tests { Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_stationary_lagged_latent_covariance( + recover_predetermined_lagged_latent_covariance( f64::MAX, f64::MAX, 0.0, @@ -10290,7 +13077,7 @@ mod tests { Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_stationary_lagged_latent_covariance( + recover_predetermined_lagged_latent_covariance( f64::MAX, 0.0, 1.0, @@ -10304,25 +13091,24 @@ 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_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 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 recovered = recover_predetermined_lagged_observed_covariance( loading, trait_variance, - diffusion, + initial_latent_variance, printed_effect, 1.0, log_rate, @@ -10330,40 +13116,55 @@ mod tests { manifest_trait, LagClock::EventTime, ) - .expect("eq5-lagged-stationary-T0VAR"); - let latent = recover_stationary_lagged_latent_covariance( + .expect("eq5-lagged-predetermined-T0VAR"); + let latent = recover_predetermined_lagged_latent_covariance( trait_variance, - diffusion, + initial_latent_variance, printed_effect, 1.0, log_rate, event_delta, LagClock::EventTime, ) - .expect("stationary lagged T0VAR"); + .expect("predetermined 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 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-stationary-T0VAR"); - assert!((recovered - contemporaneous).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!( - recover_stationary_lagged_observed_covariance( + recover_predetermined_lagged_observed_covariance( 0.0, trait_variance, - diffusion, + initial_latent_variance, printed_effect, 1.0, log_rate, @@ -10374,7 +13175,7 @@ mod tests { Ok(manifest_trait) ); assert_eq!( - recover_stationary_lagged_observed_covariance( + recover_predetermined_lagged_observed_covariance( loading, 0.0, 0.0, @@ -10387,35 +13188,21 @@ mod tests { ), 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() { + 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_stationary_lagged_observed_covariance( + let recovered = recover_predetermined_lagged_observed_covariance( loading, trait_variance, - diffusion, + initial_latent_variance, -0.225, 1.0, log_rate, @@ -10423,58 +13210,81 @@ mod tests { 0.1, LagClock::EventTime, ) - .expect("eq5-lagged-stationary-T0VAR"); - let latent = recover_stationary_lagged_latent_covariance( + .expect("eq5-lagged-predetermined-T0VAR"); + let latent = recover_predetermined_lagged_latent_covariance( trait_variance, - diffusion, + initial_latent_variance, -0.225, 1.0, log_rate, event_delta, LagClock::EventTime, ) - .expect("stationary lagged T0VAR"); - let contemporaneous = recover_stationary_initial_observed_variance( + .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-stationary-T0VAR"); + .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!( - refuse_stationary_lagged_latent_covariance_as_observed_covariance(latent, recovered), - Err(PsychometricError::StationaryLaggedLatentCovarianceIsNotObservedCovariance) + refuse_predetermined_lagged_latent_covariance_as_observed_covariance(latent, recovered), + Err(PsychometricError::PredeterminedLaggedLatentCovarianceIsNotObservedCovariance) ); assert_eq!( - refuse_measurement_error_as_stationary_lagged_observed_covariance( + refuse_measurement_error_as_predetermined_lagged_observed_covariance( measurement_error, recovered ), - Err(PsychometricError::MeasurementErrorIsNotStationaryLaggedObservedCovariance) + Err(PsychometricError::MeasurementErrorIsNotPredeterminedLaggedObservedCovariance) ); assert_eq!( - refuse_stationary_initial_observed_variance_as_stationary_lagged_observed_covariance( - contemporaneous, + refuse_predetermined_later_observed_variance_as_predetermined_lagged_observed_covariance( + later, recovered ), Err( - PsychometricError::StationaryInitialObservedVarianceIsNotStationaryLaggedObservedCovariance + PsychometricError::PredeterminedLaterObservedVarianceIsNotPredeterminedLaggedObservedCovariance + ) + ); + assert_eq!( + refuse_stationary_lagged_observed_covariance_as_predetermined_lagged_observed_covariance( + stationary_lagged, + recovered + ), + Err( + PsychometricError::StationaryLaggedObservedCovarianceIsNotPredeterminedLaggedObservedCovariance ) ); } #[test] - fn stationary_lagged_observed_covariance_invalid_inputs_fail_closed() { + fn predetermined_lagged_observed_covariance_invalid_inputs_fail_closed() { assert_eq!( - recover_stationary_lagged_observed_covariance( + recover_predetermined_lagged_observed_covariance( 2.0, 1.0, - 0.4, + 2.0, -0.225, 1.0, -0.13, @@ -10485,10 +13295,10 @@ mod tests { Err(PsychometricError::EventTimeRequired) ); assert_eq!( - recover_stationary_lagged_observed_covariance( + recover_predetermined_lagged_observed_covariance( 2.0, 1.0, - 0.4, + 2.0, -0.225, 1.0, -0.13, @@ -10499,21 +13309,7 @@ mod tests { Err(PsychometricError::NonPositiveInterval) ); assert_eq!( - 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) - ); - assert_eq!( - recover_stationary_lagged_observed_covariance( + recover_predetermined_lagged_observed_covariance( 2.0, 0.0, 0.0, @@ -10527,7 +13323,7 @@ mod tests { Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) ); assert_eq!( - recover_stationary_lagged_observed_covariance( + recover_predetermined_lagged_observed_covariance( 2.0, 0.0, 0.0, @@ -10540,11 +13336,24 @@ mod tests { ), 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_stationary_lagged_observed_covariance( + recover_predetermined_lagged_observed_covariance( f64::NAN, 1.0, - 0.4, + 2.0, 0.0, 0.0, -0.5, @@ -10555,7 +13364,7 @@ mod tests { Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_stationary_lagged_observed_covariance( + recover_predetermined_lagged_observed_covariance( 2.0, f64::MAX, f64::MAX, @@ -10571,611 +13380,573 @@ 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. - 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, + 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("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, + .expect("eq5-impulse-mean"); + let composed = recover_discrete_latent_mean_with_impulse( + initial, + drift, + intercept, + effect, + predictor, + 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, + .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, 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, + .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("ti-only later"); - assert!((added_only - added).abs() < 1e-15); + .expect("eq5-carry-mean"); + assert!((carried_observed - recovered).abs() > 1e-3); assert_eq!( - recover_stationary_later_latent_variance( - 0.0, - 0.0, + recover_discrete_observed_mean_with_impulse( 0.0, - predictor_variance, + initial, + drift, + intercept, + effect, + predictor, + manifest_mean, + delta, + LagClock::EventTime + ), + Ok(manifest_mean) + ); + assert_eq!( + recover_discrete_observed_mean_with_impulse( + loading, + initial, + drift, + intercept, + effect, + predictor, 0.0, - event_delta, + delta, LagClock::EventTime ), - Ok(0.0) + Ok(loading * composed) ); - let far = recover_stationary_later_latent_variance( - trait_variance, - diffusion, - printed_effect, - predictor_variance, - log_rate, - 1e8, + } + + #[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("Δ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, + .expect("eq5-impulse-mean"); + let evolved_observed = recover_discrete_observed_mean( + loading, + initial, + drift, + intercept, + manifest_mean, + delta, LagClock::EventTime, ) - .expect("Δt→0+"); - assert!((near - contemporaneous).abs() < 1e-9); + .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 stationary_later_latent_variance_is_not_lagged_discrete_or_process_noise() { - 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_later_latent_variance( - trait_variance, - diffusion, - -0.225, + 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, - log_rate, - event_delta, + -0.5, + 0.3, + 0.4, + 3.0, + 0.5, + 2.0, LagClock::EventTime, ) - .expect("stationary later T0VAR"); - let lagged = recover_stationary_lagged_latent_covariance( - trait_variance, - diffusion, - -0.225, + .expect("eq5-impulse-mean"); + let composed = recover_discrete_latent_mean_with_impulse( 1.0, - log_rate, - event_delta, + -0.5, + 0.3, + 0.4, + 3.0, + 2.0, LagClock::EventTime, ) - .expect("stationary lagged T0VAR"); - let contemporaneous = recover_stationary_initial_latent_variance( - trait_variance, - diffusion, - -0.225, + .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, - 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); + .expect("eq5-carry-mean"); assert_eq!( - refuse_stationary_later_latent_variance_as_lagged_covariance(recovered, lagged), - Err(PsychometricError::StationaryLaterLatentVarianceIsNotLaggedCovariance) + refuse_evolved_observed_mean_as_impulse_observed_mean(evolved_observed, recovered), + Err(PsychometricError::EvolvedObservedMeanIsNotImpulseObservedMean) ); assert_eq!( - refuse_stationary_later_latent_variance_as_discrete_variance(recovered, free_discrete), - Err(PsychometricError::StationaryLaterLatentVarianceIsNotDiscreteVariance) + refuse_impulse_observed_mean_as_impulse_carry_observed_mean( + recovered, + carried_observed + ), + Err(PsychometricError::ImpulseObservedMeanIsNotImpulseCarryObservedMean) ); assert_eq!( - refuse_stationary_later_latent_variance_as_process_noise(recovered, process_noise), - Err(PsychometricError::StationaryLaterLatentVarianceIsNotProcessNoise) + refuse_latent_mean_as_observed_mean(composed, 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( + 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 stationary_later_latent_variance_invalid_inputs_fail_closed() { - assert_eq!( - recover_stationary_later_latent_variance( - 1.0, - 0.4, - -0.225, - 1.0, - -0.13, - 1.0, - LagClock::SystemTime - ), - Err(PsychometricError::EventTimeRequired) - ); + fn discrete_observed_mean_with_impulse_invalid_inputs_fail_closed() { assert_eq!( - recover_stationary_later_latent_variance( + recover_discrete_observed_mean_with_impulse( + f64::NAN, 1.0, + -0.5, + 0.3, 0.4, - -0.225, - 1.0, - -0.13, - 0.0, + 3.0, + 0.5, + 2.0, LagClock::EventTime ), - Err(PsychometricError::NonPositiveInterval) + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_stationary_later_latent_variance( + recover_discrete_observed_mean_with_impulse( + 1e308, + 2.0, 0.0, - 0.4, 0.0, - 1.0, + 0.0, + 3.0, 0.0, 1.0, LagClock::EventTime ), - Err(PsychometricError::StationaryVarianceRequiresStableDrift) + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_stationary_later_latent_variance( + recover_discrete_observed_mean_with_impulse( + 1.0, + 1.0, + 710.0, 0.0, 0.0, - -0.225, - 1.0, + 3.0, 0.5, 1.0, LagClock::EventTime ), - Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_stationary_later_latent_variance( + recover_discrete_observed_mean_with_impulse( + 1e308, 0.0, 0.0, 0.0, + 1e308, 1.0, 0.0, 1.0, LagClock::EventTime ), - Ok(0.0) + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_stationary_later_latent_variance( - f64::NAN, + recover_discrete_observed_mean_with_impulse( + 2.0, + 1.0, + -0.5, + 0.3, 0.4, + 3.0, + 0.5, 0.0, - 0.0, - -0.5, - 1.0, LagClock::EventTime ), - Err(PsychometricError::InvalidNumericInput) + Err(PsychometricError::NonPositiveInterval) ); assert_eq!( - recover_stationary_later_latent_variance( - f64::MAX, - f64::MAX, - 0.0, - 0.0, - -0.5, + 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_discrete_time_independent_predictor_effect( + 0.0, + predictor, + drift, + delta, LagClock::EventTime ), - Err(PsychometricError::InvalidNumericInput) + Ok(0.0) ); assert_eq!( - recover_stationary_later_latent_variance( - f64::MAX, + recover_discrete_time_independent_predictor_effect( + effect, 0.0, - 1.0, - f64::MAX, - -1.0, - 1.0, + drift, + delta, LagClock::EventTime ), - Err(PsychometricError::InvalidNumericInput) + Ok(0.0) ); - } - - #[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). - 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 event_delta = 1.0_f64; - let recovered = recover_stationary_later_observed_variance( - loading, - trait_variance, - diffusion, - printed_effect, - 1.0, - log_rate, - event_delta, - measurement_error, - manifest_trait, + let zero_drift = recover_discrete_time_independent_predictor_effect( + effect, + predictor, + 0.0, + delta, LagClock::EventTime, ) - .expect("eq5-later-stationary-T0VAR"); - let latent = recover_stationary_later_latent_variance( - trait_variance, - diffusion, - printed_effect, - 1.0, - log_rate, - event_delta, + .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("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, + .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("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, + .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("eq5-lagged-stationary-T0VAR"); - assert!((recovered - lagged).abs() > 1e-3); - assert!((recovered - measurement_error).abs() > 1e-3); - assert!((recovered - latent).abs() > 1e-3); + .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_stationary_later_observed_variance( + recover_discrete_latent_mean_with_time_independent_predictor( + initial, + drift, + intercept, 0.0, - trait_variance, - diffusion, - printed_effect, - 1.0, - log_rate, - event_delta, - measurement_error, - manifest_trait, - LagClock::EventTime, + predictor, + delta, + LagClock::EventTime ), - Ok(measurement_error + manifest_trait) + Ok(evolved) ); assert_eq!( - recover_stationary_later_observed_variance( - loading, - 0.0, - 0.0, - 0.0, - 1.0, - 0.0, - event_delta, + recover_discrete_latent_mean_with_time_independent_predictor( 0.0, + drift, 0.0, - LagClock::EventTime, + effect, + predictor, + delta, + LagClock::EventTime ), - Ok(0.0) + Ok(increment) ); - let zero_manifest_trait = recover_stationary_later_observed_variance( - loading, - trait_variance, - diffusion, - printed_effect, - 1.0, - log_rate, - event_delta, - measurement_error, + let scaled = recover_discrete_time_independent_predictor_effect( + 1e308, + 1e-308, 0.0, + 1.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); + .expect("scale"); + assert!((scaled - 1.0).abs() < 1e-15); } #[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, + 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("stationary later T0VAR"); - let lagged = recover_stationary_lagged_observed_covariance( - loading, - trait_variance, - diffusion, - -0.225, - 1.0, - log_rate, - event_delta, - 0.1, + .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("eq5-lagged-stationary-T0VAR"); + .expect("eq14"); assert_eq!( - refuse_stationary_later_latent_variance_as_observed_variance(latent, recovered), - Err(PsychometricError::StationaryLaterLatentVarianceIsNotObservedVariance) + refuse_time_independent_effect_as_continuous_intercept(increment, effect), + Err(PsychometricError::TimeIndependentEffectIsNotContinuousIntercept) ); assert_eq!( - refuse_measurement_error_as_stationary_later_observed_variance( - measurement_error, - recovered - ), - Err(PsychometricError::MeasurementErrorIsNotStationaryLaterObservedVariance) + refuse_time_independent_effect_as_time_dependent_impulse(increment, impulse), + Err(PsychometricError::TimeIndependentEffectIsNotTimeDependentImpulse) ); assert_eq!( - refuse_stationary_lagged_observed_covariance_as_stationary_later_observed_variance( - lagged, recovered + refuse_time_independent_effect_as_time_varying_discrete_effect( + increment, + equation_fourteen ), - Err( - PsychometricError::StationaryLaggedObservedCovarianceIsNotStationaryLaterObservedVariance - ) + Err(PsychometricError::TimeIndependentEffectIsNotTimeVaryingDiscreteEffect) + ); + assert_eq!( + refuse_time_independent_coefficient_as_discrete_effect(effect, increment), + Err(PsychometricError::TimeIndependentCoefficientIsNotDiscreteEffect) ); } #[test] - #[allow(clippy::too_many_lines)] - fn stationary_later_observed_variance_invalid_inputs_fail_closed() { + fn time_independent_predictor_invalid_inputs_fail_closed() { assert_eq!( - recover_stationary_later_observed_variance( - 2.0, - 1.0, - 0.4, - -0.225, - 1.0, - -0.13, + recover_discrete_time_independent_predictor_effect( + f64::NAN, 1.0, - 0.5, - 0.1, - LagClock::SystemTime + -0.5, + 2.0, + LagClock::EventTime ), - Err(PsychometricError::EventTimeRequired) + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_stationary_later_observed_variance( - 2.0, - 1.0, - 0.4, - -0.225, + recover_discrete_time_independent_predictor_effect( 1.0, - -0.13, - 0.0, - 0.5, - 0.1, + f64::INFINITY, + -0.5, + 2.0, LagClock::EventTime ), - Err(PsychometricError::NonPositiveInterval) + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_stationary_later_observed_variance( + recover_discrete_time_independent_predictor_effect( + 1e308, + 2.0, + -0.5, 2.0, - 0.0, - 0.4, - 0.0, - 1.0, - 0.0, - 1.0, - 0.0, - 0.0, LagClock::EventTime ), - Err(PsychometricError::StationaryVarianceRequiresStableDrift) + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_stationary_later_observed_variance( - 2.0, - 0.0, - 0.0, - -0.225, - 1.0, - 0.5, + recover_discrete_time_independent_predictor_effect( + 1e308, 1.0, 0.0, - 0.0, + 2.0, LagClock::EventTime ), - Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_stationary_later_observed_variance( - 2.0, - 0.0, - 0.0, - 0.0, - 1.0, + recover_discrete_time_independent_predictor_effect( + 0.4, + 3.0, + -0.5, 0.0, - 1.0, - 0.5, - 0.1, LagClock::EventTime ), - Ok(0.6) + Err(PsychometricError::NonPositiveInterval) ); assert_eq!( - recover_stationary_later_observed_variance( - f64::NAN, - 1.0, + recover_discrete_time_independent_predictor_effect( 0.4, - 0.0, - 0.0, + 3.0, -0.5, - 1.0, + 2.0, + 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, LagClock::EventTime ), Err(PsychometricError::InvalidNumericInput) ); + // Latent mean is finite; Bz overflows. That `?` is not the sum overflow. assert_eq!( - recover_stationary_later_observed_variance( - 2.0, - f64::MAX, - f64::MAX, - 0.0, - 0.0, - -0.5, + recover_discrete_latent_mean_with_time_independent_predictor( 1.0, - 0.0, - 0.0, + -0.5, + 0.3, + 1e308, + 2.0, + 2.0, LagClock::EventTime ), Err(PsychometricError::InvalidNumericInput) @@ -11183,7 +13954,7 @@ mod tests { } #[test] - fn discrete_observed_mean_with_impulse_recovers_driver_equation_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; @@ -11192,7 +13963,7 @@ mod tests { let initial = 1.0_f64; let intercept = 0.3_f64; let manifest_mean = 0.5_f64; - let recovered = recover_discrete_observed_mean_with_impulse( + let recovered = recover_discrete_observed_mean_with_time_independent_predictor( loading, initial, drift, @@ -11203,8 +13974,8 @@ mod tests { delta, LagClock::EventTime, ) - .expect("eq5-impulse-mean"); - let composed = recover_discrete_latent_mean_with_impulse( + .expect("eq5-tipred-mean"); + let composed = recover_discrete_latent_mean_with_time_independent_predictor( initial, drift, intercept, @@ -11213,7 +13984,7 @@ mod tests { delta, LagClock::EventTime, ) - .expect("mx"); + .expect("eq3-tipred"); let expected = manifest_mean + loading * composed; assert!((recovered - expected).abs() < 1e-15); let evolved_observed = recover_discrete_observed_mean( @@ -11226,7 +13997,18 @@ mod tests { LagClock::EventTime, ) .expect("eq3-eq5-mean"); - assert!((evolved_observed - recovered).abs() > 1e-3); + 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, @@ -11240,39 +14022,27 @@ mod tests { 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_impulse( + recover_discrete_observed_mean_with_time_independent_predictor( 0.0, initial, drift, intercept, effect, predictor, - manifest_mean, - delta, - LagClock::EventTime - ), - Ok(manifest_mean) - ); - assert_eq!( - recover_discrete_observed_mean_with_impulse( - loading, - initial, - drift, - intercept, - effect, - predictor, - 0.0, + manifest_mean, delta, LagClock::EventTime ), - Ok(loading * composed) + Ok(manifest_mean) ); } #[test] - fn discrete_observed_mean_with_impulse_is_not_evolved_or_zero_impulse() { + 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; @@ -11281,7 +14051,7 @@ mod tests { let initial = 1.0_f64; let intercept = 0.3_f64; let manifest_mean = 0.5_f64; - let recovered = recover_discrete_observed_mean_with_impulse( + let recovered = recover_discrete_observed_mean_with_time_independent_predictor( loading, initial, drift, @@ -11292,7 +14062,7 @@ mod tests { delta, LagClock::EventTime, ) - .expect("eq5-impulse-mean"); + .expect("eq5-tipred-mean"); let evolved_observed = recover_discrete_observed_mean( loading, initial, @@ -11303,7 +14073,7 @@ mod tests { LagClock::EventTime, ) .expect("eq3-eq5-mean"); - let zero_impulse = recover_discrete_observed_mean_with_impulse( + let zero_increment = recover_discrete_observed_mean_with_time_independent_predictor( loading, initial, drift, @@ -11314,15 +14084,15 @@ mod tests { delta, LagClock::EventTime, ) - .expect("zero-impulse"); - assert!((zero_impulse - evolved_observed).abs() < 1e-15); + .expect("zero-increment"); + assert!((zero_increment - evolved_observed).abs() < 1e-15); assert!((recovered - evolved_observed).abs() > 1e-3); } #[test] - fn discrete_observed_mean_with_impulse_refuses_evolved_mean_and_overflow() { + 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_impulse( + let recovered = recover_discrete_observed_mean_with_time_independent_predictor( loading, 1.0, -0.5, @@ -11333,20 +14103,22 @@ mod tests { 2.0, LagClock::EventTime, ) - .expect("eq5-impulse-mean"); - let composed = recover_discrete_latent_mean_with_impulse( + .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("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"); + .expect("eq5-impulse-mean"); let carried_observed = recover_discrete_observed_mean_with_impulse_carry( loading, 1.0, @@ -11361,25 +14133,31 @@ mod tests { ) .expect("eq5-carry-mean"); assert_eq!( - refuse_evolved_observed_mean_as_impulse_observed_mean(evolved_observed, recovered), - Err(PsychometricError::EvolvedObservedMeanIsNotImpulseObservedMean) - ); - assert_eq!( - refuse_impulse_observed_mean_as_impulse_carry_observed_mean( - recovered, - carried_observed + refuse_evolved_observed_mean_as_time_independent_observed_mean( + evolved_observed, + recovered ), - Err(PsychometricError::ImpulseObservedMeanIsNotImpulseCarryObservedMean) + Err(PsychometricError::EvolvedObservedMeanIsNotTimeIndependentObservedMean) ); assert_eq!( - refuse_latent_mean_as_observed_mean(composed, recovered), - Err(PsychometricError::LatentMeanIsNotObservedMean) + refuse_impulse_observed_mean_as_time_independent_observed_mean( + impulse_observed, + recovered + ), + Err(PsychometricError::ImpulseObservedMeanIsNotTimeIndependentObservedMean) ); assert_eq!( - refuse_manifest_means_as_observed_mean(0.5, recovered), - Err(PsychometricError::ManifestMeansIsNotObservedMean) + refuse_impulse_carry_observed_mean_as_time_independent_observed_mean( + carried_observed, + recovered + ), + Err(PsychometricError::ImpulseCarryObservedMeanIsNotTimeIndependentObservedMean) ); - let scaled = recover_discrete_observed_mean_with_impulse( + } + + #[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, @@ -11392,9 +14170,9 @@ mod tests { ) .expect("scale"); assert!((scaled - 1.0).abs() < 1e-15); - let finite_loaded = recover_discrete_observed_mean_with_impulse( + let finite_loaded = recover_discrete_observed_mean_with_time_independent_predictor( 1e308, - 1.0, + 0.0, 0.0, 0.0, 0.0, @@ -11403,28 +14181,10 @@ mod tests { 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() { - assert_eq!( - 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) - ); + .expect("lambda-mu0"); + assert!((finite_loaded - 0.0).abs() < 1e-15); assert_eq!( - recover_discrete_observed_mean_with_impulse( + recover_discrete_observed_mean_with_time_independent_predictor( 1e308, 2.0, 0.0, @@ -11438,35 +14198,21 @@ mod tests { Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_observed_mean_with_impulse( - 1.0, + recover_discrete_observed_mean_with_time_independent_predictor( + 2.0, 1.0, - 710.0, - 0.0, - 0.0, + -0.5, + 0.3, + 0.4, 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 + 2.0, + LagClock::SystemTime ), - Err(PsychometricError::InvalidNumericInput) + Err(PsychometricError::EventTimeRequired) ); assert_eq!( - recover_discrete_observed_mean_with_impulse( + recover_discrete_observed_mean_with_time_independent_predictor( 2.0, 1.0, -0.5, @@ -11480,563 +14226,715 @@ mod tests { Err(PsychometricError::NonPositiveInterval) ); assert_eq!( - recover_discrete_observed_mean_with_impulse( - 2.0, + recover_discrete_observed_mean_with_time_independent_predictor( + 1e308, + 0.0, + 0.0, + 0.0, + 1e308, 1.0, - -0.5, - 0.3, - 0.4, - 3.0, - 0.5, - 2.0, - LagClock::SystemTime + 0.0, + 1.0, + LagClock::EventTime ), - Err(PsychometricError::EventTimeRequired) + Err(PsychometricError::InvalidNumericInput) ); } #[test] - fn time_independent_predictor_recovers_driver_equation_three_second_summand() { + 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_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"); - let expected = - recover_discrete_constant_predictor_effect(1.2, drift, delta, LagClock::EventTime) - .expect("bz-map"); - assert!((increment - expected).abs() < 1e-15); + .expect("tdpred-carry"); + let expected = (-0.5_f64).exp() * 1.2; + assert!((carry - expected).abs() < 1e-15); assert_eq!( - recover_discrete_time_independent_predictor_effect( + recover_time_dependent_predictor_impulse_carry( 0.0, predictor, drift, delta, + elapsed, LagClock::EventTime ), Ok(0.0) ); assert_eq!( - recover_discrete_time_independent_predictor_effect( + recover_time_dependent_predictor_impulse_carry( effect, 0.0, drift, delta, + elapsed, LagClock::EventTime ), Ok(0.0) ); - let zero_drift = recover_discrete_time_independent_predictor_effect( + let zero_drift = recover_time_dependent_predictor_impulse_carry( effect, predictor, 0.0, delta, + elapsed, 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"); + assert!((zero_drift - 1.2).abs() < 1e-15); let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("tdpred"); - let equation_fourteen = recover_discrete_time_varying_predictor_effect( + 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, + LagClock::EventTime + ), + Ok(evolved) + ); + assert_eq!( + recover_discrete_latent_mean_with_impulse_carry( + 0.0, + drift, + 0.0, + effect, + predictor, + delta, + elapsed, + LagClock::EventTime + ), + 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("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); + .expect("rewrite"); + let expected_rewrite = (1e-308_f64.ln() + 710.0).exp(); + assert!((rewritten - expected_rewrite).abs() <= expected_rewrite * 1e-12); } #[test] - fn time_independent_predictor_composes_evolved_mean_and_keeps_scale() { - let effect = 0.4_f64; - let predictor = 3.0_f64; + 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 increment = recover_discrete_time_independent_predictor_effect( + 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, - drift, + manifest_mean, delta, + elapsed, LagClock::EventTime, ) - .expect("tipred"); - let initial = 1.0_f64; - let intercept = 0.3_f64; - let composed = recover_discrete_latent_mean_with_time_independent_predictor( + .expect("eq5-carry-mean"); + let carried = recover_discrete_latent_mean_with_impulse_carry( initial, drift, intercept, effect, predictor, delta, + elapsed, 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); + .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_latent_mean_with_time_independent_predictor( + recover_discrete_observed_mean_with_impulse_carry( + 0.0, initial, drift, intercept, - 0.0, + effect, predictor, + manifest_mean, delta, + elapsed, LagClock::EventTime ), - Ok(evolved) + Ok(manifest_mean) ); assert_eq!( - recover_discrete_latent_mean_with_time_independent_predictor( - 0.0, + recover_discrete_observed_mean_with_impulse_carry( + loading, + initial, drift, - 0.0, + intercept, effect, predictor, + 0.0, delta, + elapsed, LagClock::EventTime ), - Ok(increment) + Ok(loading * carried) ); - 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() { + 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 increment = recover_discrete_time_independent_predictor_effect( + 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, - -0.5, - 2.0, + manifest_mean, + delta, + elapsed, LagClock::EventTime, ) - .expect("tipred"); - let impulse = recover_time_dependent_predictor_impulse(effect, predictor).expect("tdpred"); - let equation_fourteen = recover_discrete_time_varying_predictor_effect( + .expect("eq5-carry-mean"); + let contemporaneous = recover_discrete_observed_mean_with_impulse( + loading, + initial, + drift, + intercept, effect, + predictor, + manifest_mean, + delta, + 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, + 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, - 2.0, + 1.0, LagClock::EventTime, ) - .expect("eq14"); + .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_time_independent_effect_as_continuous_intercept(increment, effect), - Err(PsychometricError::TimeIndependentEffectIsNotContinuousIntercept) + refuse_evolved_observed_mean_as_impulse_carry_observed_mean( + evolved_observed, + recovered + ), + Err(PsychometricError::EvolvedObservedMeanIsNotImpulseCarryObservedMean) ); assert_eq!( - refuse_time_independent_effect_as_time_dependent_impulse(increment, impulse), - Err(PsychometricError::TimeIndependentEffectIsNotTimeDependentImpulse) + 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_time_independent_effect_as_time_varying_discrete_effect( - increment, - equation_fourteen - ), - Err(PsychometricError::TimeIndependentEffectIsNotTimeVaryingDiscreteEffect) + refuse_latent_mean_as_observed_mean(carried, recovered), + Err(PsychometricError::LatentMeanIsNotObservedMean) ); assert_eq!( - refuse_time_independent_coefficient_as_discrete_effect(effect, increment), - Err(PsychometricError::TimeIndependentCoefficientIsNotDiscreteEffect) + 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 time_independent_predictor_invalid_inputs_fail_closed() { + fn discrete_observed_mean_with_impulse_carry_invalid_inputs_fail_closed() { assert_eq!( - recover_discrete_time_independent_predictor_effect( + 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::InvalidNumericInput) ); assert_eq!( - recover_discrete_time_independent_predictor_effect( - 1.0, - f64::INFINITY, - -0.5, - 2.0, - LagClock::EventTime - ), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_discrete_time_independent_predictor_effect( + recover_discrete_observed_mean_with_impulse_carry( 1e308, 2.0, - -0.5, + 0.0, + 0.0, + 0.0, + 3.0, + 0.0, 2.0, + 1.0, LagClock::EventTime ), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_time_independent_predictor_effect( + 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::InvalidNumericInput) + ); + assert_eq!( + 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::InvalidNumericInput) ); + } + + #[test] + fn discrete_observed_mean_with_impulse_carry_interval_and_clock_fail_closed() { assert_eq!( - recover_discrete_time_independent_predictor_effect( + recover_discrete_observed_mean_with_impulse_carry( + 2.0, + 1.0, + -0.5, + 0.3, 0.4, 3.0, - -0.5, + 0.5, + 2.0, 0.0, LagClock::EventTime ), Err(PsychometricError::NonPositiveInterval) ); assert_eq!( - recover_discrete_time_independent_predictor_effect( + recover_discrete_observed_mean_with_impulse_carry( + 2.0, + 1.0, + -0.5, + 0.3, 0.4, 3.0, - -0.5, + 0.5, + 2.0, 2.0, - 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, LagClock::EventTime ), - Err(PsychometricError::InvalidNumericInput) + Err(PsychometricError::NonPositiveInterval) ); - // Latent mean is finite; Bz overflows. That `?` is not the sum overflow. assert_eq!( - recover_discrete_latent_mean_with_time_independent_predictor( + recover_discrete_observed_mean_with_impulse_carry( + 2.0, 1.0, -0.5, 0.3, - 1e308, - 2.0, + 0.4, + 3.0, + 0.5, 2.0, - LagClock::EventTime + 1.0, + LagClock::SystemTime ), - Err(PsychometricError::InvalidNumericInput) + Err(PsychometricError::EventTimeRequired) ); } #[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; + fn time_dependent_impulse_carry_refuses_contemporaneous_cint_tipred_and_equation_fourteen() { 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, + 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, - 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, + elapsed, LagClock::EventTime, ) - .expect("eq3-eq5-mean"); - let impulse_observed = recover_discrete_observed_mean_with_impulse( - loading, - initial, - drift, - intercept, + .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, - 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, - LagClock::EventTime - ), - Ok(manifest_mean) - ); - } - - #[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, + .expect("tipred"); + let equation_fourteen = recover_discrete_time_varying_predictor_effect( 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, delta, 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, - 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("eq5-carry-mean"); + .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_evolved_observed_mean_as_time_independent_observed_mean( - evolved_observed, - recovered - ), - Err(PsychometricError::EvolvedObservedMeanIsNotTimeIndependentObservedMean) + refuse_time_dependent_impulse_carry_as_contemporaneous_impulse(carry, impulse), + Err(PsychometricError::TimeDependentImpulseCarryIsNotContemporaneousImpulse) ); assert_eq!( - refuse_impulse_observed_mean_as_time_independent_observed_mean( - impulse_observed, - recovered - ), - Err(PsychometricError::ImpulseObservedMeanIsNotTimeIndependentObservedMean) + refuse_time_dependent_impulse_carry_as_continuous_intercept(carry, effect), + Err(PsychometricError::TimeDependentImpulseCarryIsNotContinuousIntercept) ); assert_eq!( - refuse_impulse_carry_observed_mean_as_time_independent_observed_mean( - carried_observed, - recovered + refuse_time_dependent_impulse_carry_as_time_independent_effect(carry, time_independent), + Err(PsychometricError::TimeDependentImpulseCarryIsNotTimeIndependentEffect) + ); + assert_eq!( + refuse_time_dependent_impulse_carry_as_time_varying_discrete_effect( + carry, + equation_fourteen ), - Err(PsychometricError::ImpulseCarryObservedMeanIsNotTimeIndependentObservedMean) + Err(PsychometricError::TimeDependentImpulseCarryIsNotTimeVaryingDiscreteEffect) ); } #[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 time_dependent_impulse_carry_invalid_inputs_fail_closed() { assert_eq!( - recover_discrete_observed_mean_with_time_independent_predictor( - 1e308, + recover_time_dependent_predictor_impulse_carry( + f64::NAN, + 1.0, + -0.5, 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_time_independent_predictor( + recover_time_dependent_predictor_impulse_carry( + 0.4, + 3.0, + f64::INFINITY, 2.0, 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_time_dependent_predictor_impulse_carry( + 1e308, + 2.0, -0.5, - 0.3, + 2.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + 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_time_dependent_predictor_impulse_carry( 0.4, 3.0, - 0.5, + 1e308, + 3.0, 2.0, - LagClock::SystemTime + LagClock::EventTime ), - Err(PsychometricError::EventTimeRequired) + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_observed_mean_with_time_independent_predictor( + recover_time_dependent_predictor_impulse_carry( + 0.0, + 3.0, + 800.0, + 2.0, + 1.0, + LagClock::EventTime + ), + Ok(0.0) + ); + 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) + ); + } + + #[test] + fn time_dependent_impulse_carry_interval_and_clock_fail_closed() { + assert_eq!( + recover_time_dependent_predictor_impulse_carry( + 0.4, + 3.0, -0.5, - 0.3, + 0.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_time_dependent_predictor_impulse_carry( 0.4, 3.0, - 0.5, + -0.5, + 2.0, 0.0, LagClock::EventTime ), Err(PsychometricError::NonPositiveInterval) ); assert_eq!( - recover_discrete_observed_mean_with_time_independent_predictor( + 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, + 2.0, + 1.0, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_discrete_latent_mean_with_impulse_carry( 1e308, 0.0, 0.0, - 0.0, 1e308, 1.0, - 0.0, + 2.0, 1.0, LagClock::EventTime ), @@ -12045,163 +14943,307 @@ mod tests { } #[test] - fn time_dependent_impulse_carry_recovers_driver_equation_one_two_dissipation() { + 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 elapsed = 1.0_f64; - let carry = recover_time_dependent_predictor_impulse_carry( + 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) + ); + assert_eq!( + recover_initial_time_independent_predictor_effect(effect, 0.0), + Ok(0.0) + ); + let carry = recover_initial_time_independent_predictor_carry( effect, predictor, drift, delta, - elapsed, LagClock::EventTime, ) - .expect("tdpred-carry"); - let expected = (-0.5_f64).exp() * 1.2; + .expect("t0-carry"); + let expected = 1.2 * (drift * delta).exp(); assert!((carry - expected).abs() < 1e-15); - assert_eq!( - recover_time_dependent_predictor_impulse_carry( - 0.0, - predictor, - drift, - delta, - elapsed, - LagClock::EventTime - ), - Ok(0.0) - ); - assert_eq!( - recover_time_dependent_predictor_impulse_carry( - effect, - 0.0, - drift, - delta, - elapsed, - LagClock::EventTime - ), - Ok(0.0) - ); - let zero_drift = recover_time_dependent_predictor_impulse_carry( + let zero_drift = recover_initial_time_independent_predictor_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( + let vanished = recover_initial_time_independent_predictor_carry( effect, predictor, -800.0, - delta, - elapsed, + 1.0, LagClock::EventTime, ) - .expect("vanish"); + .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 time_dependent_impulse_carry_composes_evolved_mean_and_keeps_scale() { + 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 elapsed = 1.0_f64; - let carry = recover_time_dependent_predictor_impulse_carry( + let carry = recover_initial_time_independent_predictor_carry( effect, predictor, drift, delta, - elapsed, LagClock::EventTime, ) - .expect("tdpred-carry"); + .expect("t0-carry"); let initial = 1.0_f64; let intercept = 0.3_f64; - let composed = recover_discrete_latent_mean_with_impulse_carry( + let composed = recover_discrete_latent_mean_with_initial_time_independent_predictor( initial, drift, intercept, effect, predictor, delta, - elapsed, LagClock::EventTime, ) - .expect("eq3-carry"); + .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_latent_mean_with_impulse_carry( + recover_discrete_latent_mean_with_initial_time_independent_predictor( initial, drift, intercept, 0.0, predictor, delta, - elapsed, LagClock::EventTime ), Ok(evolved) ); assert_eq!( - recover_discrete_latent_mean_with_impulse_carry( + recover_discrete_latent_mean_with_initial_time_independent_predictor( 0.0, drift, 0.0, effect, predictor, delta, - elapsed, LagClock::EventTime ), Ok(carry) ); - let scaled = recover_time_dependent_predictor_impulse_carry( + let scaled = recover_initial_time_independent_predictor_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( + let rewritten = recover_initial_time_independent_predictor_carry( + 2.0, + 0.5, + 710.0, + 1.0, + LagClock::EventTime, + ); + assert_eq!(rewritten, Err(PsychometricError::InvalidNumericInput)); + let finite_rewrite = recover_initial_time_independent_predictor_carry( 1e-308, 1.0, - 710.0, - 2.0, + 700.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); + .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 discrete_observed_mean_with_impulse_carry_recovers_driver_equation_five() { + 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"); + assert_eq!( + refuse_initial_time_independent_effect_as_process_increment(shift, increment), + Err(PsychometricError::InitialTimeIndependentEffectIsNotProcessIncrement) + ); + assert_eq!( + refuse_initial_time_independent_carry_as_initial_effect(carry, shift), + Err(PsychometricError::InitialTimeIndependentCarryIsNotInitialEffect) + ); + assert_eq!( + refuse_initial_time_independent_effect_as_continuous_intercept(shift, 0.4), + Err(PsychometricError::InitialTimeIndependentEffectIsNotContinuousIntercept) + ); + assert_eq!( + refuse_initial_time_independent_effect_as_time_dependent_impulse(shift, impulse), + Err(PsychometricError::InitialTimeIndependentEffectIsNotTimeDependentImpulse) + ); + assert_eq!( + refuse_initial_time_independent_coefficient_as_initial_effect(effect, shift), + Err(PsychometricError::InitialTimeIndependentCoefficientIsNotInitialEffect) + ); + } + + #[test] + fn initial_time_independent_predictor_invalid_inputs_fail_closed() { + assert_eq!( + 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::InvalidNumericInput) + ); + assert_eq!( + recover_initial_time_independent_predictor_carry( + 0.4, + 3.0, + -0.5, + 0.0, + LagClock::EventTime + ), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_initial_time_independent_predictor_carry( + 0.4, + 3.0, + -0.5, + 2.0, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_discrete_latent_mean_with_initial_time_independent_predictor( + 1e308, + 0.0, + 0.0, + 1e308, + 1.0, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_initial_time_independent_predictor_carry( + 1.0, + 1.0, + f64::INFINITY, + 1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_initial_time_independent_predictor_carry( + f64::NAN, + 1.0, + -0.5, + 2.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_initial_time_independent_predictor_carry( + 1.0, + 1.0, + 1e308, + 10.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_discrete_latent_mean_with_initial_time_independent_predictor( + 1.0, + -0.5, + 0.3, + f64::NAN, + 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 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( + let recovered = recover_discrete_observed_mean_with_initial_time_independent_predictor( loading, initial, drift, @@ -12210,22 +15252,20 @@ mod tests { predictor, manifest_mean, delta, - elapsed, LagClock::EventTime, ) - .expect("eq5-carry-mean"); - let carried = recover_discrete_latent_mean_with_impulse_carry( + .expect("eq5-t0tipred-mean"); + let composed = recover_discrete_latent_mean_with_initial_time_independent_predictor( initial, drift, intercept, effect, predictor, delta, - elapsed, LagClock::EventTime, ) - .expect("carried"); - let expected = manifest_mean + loading * carried; + .expect("eq3-t0tipred"); + let expected = manifest_mean + loading * composed; assert!((recovered - expected).abs() < 1e-15); let evolved_observed = recover_discrete_observed_mean( loading, @@ -12237,64 +15277,75 @@ mod tests { 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_impulse_carry( - 0.0, - initial, - drift, - intercept, - effect, - predictor, - manifest_mean, - delta, - elapsed, - LagClock::EventTime - ), - Ok(manifest_mean) - ); - assert_eq!( - recover_discrete_observed_mean_with_impulse_carry( - loading, + recover_discrete_observed_mean_with_initial_time_independent_predictor( + 0.0, initial, drift, intercept, effect, predictor, - 0.0, + manifest_mean, delta, - elapsed, LagClock::EventTime ), - Ok(loading * carried) + Ok(manifest_mean) ); } #[test] - fn discrete_observed_mean_with_impulse_carry_is_not_contemporaneous_or_zero_carry() { + 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 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( + let recovered = recover_discrete_observed_mean_with_initial_time_independent_predictor( loading, initial, drift, @@ -12305,8 +15356,7 @@ mod tests { delta, LagClock::EventTime, ) - .expect("eq5-mx"); - assert!((contemporaneous - recovered).abs() > 1e-3); + .expect("eq5-t0tipred-mean"); let evolved_observed = recover_discrete_observed_mean( loading, initial, @@ -12317,7 +15367,7 @@ mod tests { LagClock::EventTime, ) .expect("eq3-eq5-mean"); - let zero_carry = recover_discrete_observed_mean_with_impulse_carry( + let zero_carry = recover_discrete_observed_mean_with_initial_time_independent_predictor( loading, initial, drift, @@ -12326,17 +15376,18 @@ mod tests { predictor, manifest_mean, delta, - elapsed, 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_impulse_carry_refuses_evolved_mean_and_overflow() { + 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_impulse_carry( + let recovered = recover_discrete_observed_mean_with_initial_time_independent_predictor( loading, 1.0, -0.5, @@ -12345,399 +15396,160 @@ mod tests { 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"); + .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"); - assert_eq!( - refuse_evolved_observed_mean_as_impulse_carry_observed_mean( - evolved_observed, - recovered - ), - Err(PsychometricError::EvolvedObservedMeanIsNotImpulseCarryObservedMean) - ); - 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"), - recovered - ), - Err(PsychometricError::ImpulseObservedMeanIsNotImpulseCarryObservedMean) - ); - assert_eq!( - 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, + 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("lambda-mu"); - assert!((finite_loaded - 1e308).abs() / 1e308 < 1e-15); - } - - #[test] - fn discrete_observed_mean_with_impulse_carry_invalid_inputs_fail_closed() { - assert_eq!( - 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::InvalidNumericInput) - ); - assert_eq!( - 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::InvalidNumericInput) - ); - assert_eq!( - 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::InvalidNumericInput) - ); - assert_eq!( - 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::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, - 0.3, - 0.4, - 3.0, - 0.5, - 2.0, - 0.0, - LagClock::EventTime - ), - Err(PsychometricError::NonPositiveInterval) - ); - 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, - 2.0, - LagClock::EventTime - ), - Err(PsychometricError::NonPositiveInterval) - ); - 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, - 1.0, - LagClock::SystemTime - ), - Err(PsychometricError::EventTimeRequired) - ); - } - - #[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, + .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("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); + .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!( - refuse_time_dependent_impulse_carry_as_contemporaneous_impulse(carry, impulse), - Err(PsychometricError::TimeDependentImpulseCarryIsNotContemporaneousImpulse) + refuse_evolved_observed_mean_as_initial_time_independent_observed_mean( + evolved_observed, + recovered + ), + Err(PsychometricError::EvolvedObservedMeanIsNotInitialTimeIndependentObservedMean) ); assert_eq!( - refuse_time_dependent_impulse_carry_as_continuous_intercept(carry, effect), - Err(PsychometricError::TimeDependentImpulseCarryIsNotContinuousIntercept) + refuse_time_independent_observed_mean_as_initial_time_independent_observed_mean( + process_observed, + recovered + ), + Err( + PsychometricError::TimeIndependentObservedMeanIsNotInitialTimeIndependentObservedMean + ) ); assert_eq!( - refuse_time_dependent_impulse_carry_as_time_independent_effect(carry, time_independent), - Err(PsychometricError::TimeDependentImpulseCarryIsNotTimeIndependentEffect) + refuse_impulse_observed_mean_as_initial_time_independent_observed_mean( + impulse_observed, + recovered + ), + Err(PsychometricError::ImpulseObservedMeanIsNotInitialTimeIndependentObservedMean) ); assert_eq!( - refuse_time_dependent_impulse_carry_as_time_varying_discrete_effect( - carry, - equation_fourteen + refuse_impulse_carry_observed_mean_as_initial_time_independent_observed_mean( + carried_observed, + recovered ), - Err(PsychometricError::TimeDependentImpulseCarryIsNotTimeVaryingDiscreteEffect) + Err(PsychometricError::ImpulseCarryObservedMeanIsNotInitialTimeIndependentObservedMean) ); } #[test] - fn time_dependent_impulse_carry_invalid_inputs_fail_closed() { - assert_eq!( - recover_time_dependent_predictor_impulse_carry( - f64::NAN, - 1.0, - -0.5, - 2.0, - 1.0, - 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 - ), - Err(PsychometricError::InvalidNumericInput) - ); + 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_time_dependent_predictor_impulse_carry( + recover_discrete_observed_mean_with_initial_time_independent_predictor( 1e308, 2.0, - -0.5, - 2.0, + 0.0, + 0.0, + 0.0, + 3.0, + 0.0, 1.0, LagClock::EventTime ), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_time_dependent_predictor_impulse_carry( - 1.2, - 1.0, - 800.0, + recover_discrete_observed_mean_with_initial_time_independent_predictor( 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_time_dependent_predictor_impulse_carry( + -0.5, + 0.3, 0.4, 3.0, - 1e308, - 3.0, - 2.0, - LagClock::EventTime - ), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_time_dependent_predictor_impulse_carry( - 0.0, - 3.0, - 800.0, + 0.5, 2.0, - 1.0, - LagClock::EventTime + LagClock::SystemTime ), - Ok(0.0) + Err(PsychometricError::EventTimeRequired) ); assert_eq!( - recover_time_dependent_predictor_impulse_carry( - -1e-308, - 1.0, - 710.0, + recover_discrete_observed_mean_with_initial_time_independent_predictor( 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_time_dependent_predictor_impulse_carry( - 0.4, - 3.0, -0.5, - 0.0, - 1.0, - LagClock::EventTime - ), - Err(PsychometricError::NonPositiveInterval) - ); - assert_eq!( - recover_time_dependent_predictor_impulse_carry( + 0.3, 0.4, 3.0, - -0.5, - 2.0, + 0.5, 0.0, LagClock::EventTime ), Err(PsychometricError::NonPositiveInterval) ); assert_eq!( - 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, - 2.0, - 1.0, - LagClock::SystemTime - ), - Err(PsychometricError::EventTimeRequired) - ); - assert_eq!( - recover_discrete_latent_mean_with_impulse_carry( + recover_discrete_observed_mean_with_initial_time_independent_predictor( 1e308, 0.0, 0.0, + 0.0, 1e308, 1.0, - 2.0, + 0.0, 1.0, LagClock::EventTime ), @@ -12746,33 +15558,33 @@ mod tests { } #[test] - fn initial_time_independent_predictor_recovers_table_three_t0_shift_and_carry() { + 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_independent_predictor_effect(effect, predictor) - .expect("t0-tipred"); + 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_independent_predictor_effect(0.0, predictor), + recover_initial_time_dependent_predictor_effect(0.0, predictor), Ok(0.0) ); assert_eq!( - recover_initial_time_independent_predictor_effect(effect, 0.0), + recover_initial_time_dependent_predictor_effect(effect, 0.0), Ok(0.0) ); - let carry = recover_initial_time_independent_predictor_carry( + let carry = recover_initial_time_dependent_predictor_carry( effect, predictor, drift, delta, LagClock::EventTime, ) - .expect("t0-carry"); + .expect("t0-td-carry"); let expected = 1.2 * (drift * delta).exp(); assert!((carry - expected).abs() < 1e-15); - let zero_drift = recover_initial_time_independent_predictor_carry( + let zero_drift = recover_initial_time_dependent_predictor_carry( effect, predictor, 0.0, @@ -12781,7 +15593,7 @@ mod tests { ) .expect("zero-drift"); assert!((zero_drift - 1.2).abs() < 1e-15); - let vanished = recover_initial_time_independent_predictor_carry( + let vanished = recover_initial_time_dependent_predictor_carry( effect, predictor, -800.0, @@ -12790,6 +15602,7 @@ mod tests { ) .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, @@ -12798,32 +15611,44 @@ mod tests { 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, + 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); - // Algebraically a product, like M x, but Table 3 names a different matrix. + 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_independent_predictor_composes_evolved_mean_and_keeps_scale() { + 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_independent_predictor_carry( + let carry = recover_initial_time_dependent_predictor_carry( effect, predictor, drift, delta, LagClock::EventTime, ) - .expect("t0-carry"); + .expect("t0-td-carry"); let initial = 1.0_f64; let intercept = 0.3_f64; - let composed = recover_discrete_latent_mean_with_initial_time_independent_predictor( + let composed = recover_discrete_latent_mean_with_initial_time_dependent_predictor( initial, drift, intercept, @@ -12832,13 +15657,13 @@ mod tests { delta, LagClock::EventTime, ) - .expect("eq3-t0tipred"); + .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_independent_predictor( + recover_discrete_latent_mean_with_initial_time_dependent_predictor( initial, drift, intercept, @@ -12850,7 +15675,7 @@ mod tests { Ok(evolved) ); assert_eq!( - recover_discrete_latent_mean_with_initial_time_independent_predictor( + recover_discrete_latent_mean_with_initial_time_dependent_predictor( 0.0, drift, 0.0, @@ -12861,7 +15686,7 @@ mod tests { ), Ok(carry) ); - let scaled = recover_initial_time_independent_predictor_carry( + let scaled = recover_initial_time_dependent_predictor_carry( 1e308, 1e-308, 0.0, @@ -12870,7 +15695,7 @@ mod tests { ) .expect("scale"); assert!((scaled - 1.0).abs() < 1e-15); - let rewritten = recover_initial_time_independent_predictor_carry( + let rewritten = recover_initial_time_dependent_predictor_carry( 2.0, 0.5, 710.0, @@ -12878,7 +15703,7 @@ mod tests { LagClock::EventTime, ); assert_eq!(rewritten, Err(PsychometricError::InvalidNumericInput)); - let finite_rewrite = recover_initial_time_independent_predictor_carry( + let finite_rewrite = recover_initial_time_dependent_predictor_carry( 1e-308, 1.0, 700.0, @@ -12891,19 +15716,19 @@ mod tests { } #[test] - fn initial_time_independent_predictor_refuses_process_increment_cint_impulse_and_coefficient() { + 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_independent_predictor_effect(effect, predictor) - .expect("t0-tipred"); - let carry = recover_initial_time_independent_predictor_carry( + 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-carry"); + .expect("t0-td-carry"); let increment = recover_discrete_time_independent_predictor_effect( effect, predictor, @@ -12913,44 +15738,66 @@ mod tests { ) .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, + LagClock::EventTime, + ) + .expect("td-carry"); assert_eq!( - refuse_initial_time_independent_effect_as_process_increment(shift, increment), - Err(PsychometricError::InitialTimeIndependentEffectIsNotProcessIncrement) + refuse_initial_time_dependent_effect_as_contemporaneous_impulse(shift, impulse), + Err(PsychometricError::InitialTimeDependentEffectIsNotContemporaneousImpulse) ); assert_eq!( - refuse_initial_time_independent_carry_as_initial_effect(carry, shift), - Err(PsychometricError::InitialTimeIndependentCarryIsNotInitialEffect) + refuse_initial_time_dependent_carry_as_initial_effect(carry, shift), + Err(PsychometricError::InitialTimeDependentCarryIsNotInitialEffect) ); assert_eq!( - refuse_initial_time_independent_effect_as_continuous_intercept(shift, 0.4), - Err(PsychometricError::InitialTimeIndependentEffectIsNotContinuousIntercept) + refuse_initial_time_dependent_effect_as_continuous_intercept(shift, 0.4), + Err(PsychometricError::InitialTimeDependentEffectIsNotContinuousIntercept) ); assert_eq!( - refuse_initial_time_independent_effect_as_time_dependent_impulse(shift, impulse), - Err(PsychometricError::InitialTimeIndependentEffectIsNotTimeDependentImpulse) + refuse_initial_time_dependent_effect_as_process_increment(shift, increment), + Err(PsychometricError::InitialTimeDependentEffectIsNotProcessIncrement) ); assert_eq!( - refuse_initial_time_independent_coefficient_as_initial_effect(effect, shift), - Err(PsychometricError::InitialTimeIndependentCoefficientIsNotInitialEffect) + refuse_initial_time_dependent_effect_as_initial_time_independent_effect( + shift, + tipred_shift + ), + Err(PsychometricError::InitialTimeDependentEffectIsNotInitialTimeIndependentEffect) + ); + assert_eq!( + refuse_initial_time_dependent_coefficient_as_initial_effect(effect, shift), + Err(PsychometricError::InitialTimeDependentCoefficientIsNotInitialEffect) + ); + assert_eq!( + refuse_initial_time_dependent_carry_as_impulse_carry(carry, impulse_carry), + Err(PsychometricError::InitialTimeDependentCarryIsNotImpulseCarry) ); } #[test] - fn initial_time_independent_predictor_invalid_inputs_fail_closed() { + fn initial_time_dependent_predictor_invalid_inputs_fail_closed() { assert_eq!( - recover_initial_time_independent_predictor_effect(f64::NAN, 1.0), + recover_initial_time_dependent_predictor_effect(f64::NAN, 1.0), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_initial_time_independent_predictor_effect(1.0, f64::INFINITY), + recover_initial_time_dependent_predictor_effect(1.0, f64::INFINITY), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_initial_time_independent_predictor_effect(1e308, 2.0), + recover_initial_time_dependent_predictor_effect(1e308, 2.0), Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_initial_time_independent_predictor_carry( + recover_initial_time_dependent_predictor_carry( 0.4, 3.0, f64::NAN, @@ -12960,7 +15807,7 @@ mod tests { Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_initial_time_independent_predictor_carry( + recover_initial_time_dependent_predictor_carry( 0.4, 3.0, -0.5, @@ -12970,7 +15817,7 @@ mod tests { Err(PsychometricError::NonPositiveInterval) ); assert_eq!( - recover_initial_time_independent_predictor_carry( + recover_initial_time_dependent_predictor_carry( 0.4, 3.0, -0.5, @@ -12980,7 +15827,7 @@ mod tests { Err(PsychometricError::EventTimeRequired) ); assert_eq!( - recover_discrete_latent_mean_with_initial_time_independent_predictor( + recover_discrete_latent_mean_with_initial_time_dependent_predictor( 1e308, 0.0, 0.0, @@ -12992,7 +15839,7 @@ mod tests { Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_initial_time_independent_predictor_carry( + recover_initial_time_dependent_predictor_carry( 1.0, 1.0, f64::INFINITY, @@ -13002,7 +15849,7 @@ mod tests { Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_initial_time_independent_predictor_carry( + recover_initial_time_dependent_predictor_carry( f64::NAN, 1.0, -0.5, @@ -13012,7 +15859,7 @@ mod tests { Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_initial_time_independent_predictor_carry( + recover_initial_time_dependent_predictor_carry( 1.0, 1.0, 1e308, @@ -13022,7 +15869,7 @@ mod tests { Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_latent_mean_with_initial_time_independent_predictor( + recover_discrete_latent_mean_with_initial_time_dependent_predictor( 1.0, -0.5, 0.3, @@ -13036,8 +15883,9 @@ mod tests { } #[test] - fn discrete_observed_mean_with_initial_time_independent_predictor_recovers_driver_equation_five() - { + #[allow(clippy::too_many_lines)] + fn discrete_observed_mean_with_initial_time_dependent_predictor_recovers_driver_equation_five() + { let loading = 2.0_f64; let drift = -0.5_f64; let delta = 2.0_f64; @@ -13046,7 +15894,7 @@ mod tests { 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( + let recovered = recover_discrete_observed_mean_with_initial_time_dependent_predictor( loading, initial, drift, @@ -13057,8 +15905,8 @@ mod tests { delta, LagClock::EventTime, ) - .expect("eq5-t0tipred-mean"); - let composed = recover_discrete_latent_mean_with_initial_time_independent_predictor( + .expect("eq5-t0tdpred-mean"); + let composed = recover_discrete_latent_mean_with_initial_time_dependent_predictor( initial, drift, intercept, @@ -13067,7 +15915,7 @@ mod tests { delta, LagClock::EventTime, ) - .expect("eq3-t0tipred"); + .expect("eq3-t0tdpred"); let expected = manifest_mean + loading * composed; assert!((recovered - expected).abs() < 1e-15); let evolved_observed = recover_discrete_observed_mean( @@ -13091,7 +15939,7 @@ mod tests { delta, LagClock::EventTime, ) - .expect("eq5-tipred-mean"); + .expect("eq5-tipred"); let impulse_observed = recover_discrete_observed_mean_with_impulse( loading, initial, @@ -13103,8 +15951,8 @@ mod tests { delta, LagClock::EventTime, ) - .expect("eq5-impulse-mean"); - let carried_observed = recover_discrete_observed_mean_with_impulse_carry( + .expect("eq5-impulse"); + let carry_observed = recover_discrete_observed_mean_with_impulse_carry( loading, initial, drift, @@ -13116,13 +15964,28 @@ mod tests { 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!( + .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("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_discrete_observed_mean_with_initial_time_dependent_predictor( 0.0, initial, drift, @@ -13135,63 +15998,15 @@ mod tests { ), Ok(manifest_mean) ); + assert!((recovered - composed).abs() > 1e-3); + assert!((recovered - manifest_mean).abs() > 1e-3); } #[test] - 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() + fn discrete_observed_mean_with_initial_time_dependent_predictor_refuses_evolved_mean_and_overflow() { - let loading = 2.0_f64; - let recovered = recover_discrete_observed_mean_with_initial_time_independent_predictor( - loading, + let recovered = recover_discrete_observed_mean_with_initial_time_dependent_predictor( + 2.0, 1.0, -0.5, 0.3, @@ -13201,12 +16016,12 @@ mod tests { 2.0, 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, + .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, @@ -13216,9 +16031,9 @@ mod tests { 2.0, LagClock::EventTime, ) - .expect("eq5-tipred-mean"); - let impulse_observed = recover_discrete_observed_mean_with_impulse( - loading, + .expect("tipred"); + let impulse = recover_discrete_observed_mean_with_impulse( + 2.0, 1.0, -0.5, 0.3, @@ -13228,9 +16043,9 @@ mod tests { 2.0, LagClock::EventTime, ) - .expect("eq5-impulse-mean"); - let carried_observed = recover_discrete_observed_mean_with_impulse_carry( - loading, + .expect("impulse"); + let carry = recover_discrete_observed_mean_with_impulse_carry( + 2.0, 1.0, -0.5, 0.3, @@ -13241,42 +16056,56 @@ mod tests { 1.0, LagClock::EventTime, ) - .expect("eq5-carry-mean"); + .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("t0tipred"); assert_eq!( - refuse_evolved_observed_mean_as_initial_time_independent_observed_mean( - evolved_observed, - recovered + refuse_evolved_observed_mean_as_initial_time_dependent_observed_mean( + evolved, recovered ), - Err(PsychometricError::EvolvedObservedMeanIsNotInitialTimeIndependentObservedMean) + Err(PsychometricError::EvolvedObservedMeanIsNotInitialTimeDependentObservedMean) ); assert_eq!( - refuse_time_independent_observed_mean_as_initial_time_independent_observed_mean( - process_observed, - recovered + refuse_time_independent_observed_mean_as_initial_time_dependent_observed_mean( + process, recovered ), Err( - PsychometricError::TimeIndependentObservedMeanIsNotInitialTimeIndependentObservedMean + PsychometricError::TimeIndependentObservedMeanIsNotInitialTimeDependentObservedMean ) ); assert_eq!( - refuse_impulse_observed_mean_as_initial_time_independent_observed_mean( - impulse_observed, - recovered + refuse_impulse_observed_mean_as_initial_time_dependent_observed_mean( + impulse, recovered + ), + Err(PsychometricError::ImpulseObservedMeanIsNotInitialTimeDependentObservedMean) + ); + assert_eq!( + refuse_impulse_carry_observed_mean_as_initial_time_dependent_observed_mean( + carry, recovered ), - Err(PsychometricError::ImpulseObservedMeanIsNotInitialTimeIndependentObservedMean) + Err(PsychometricError::ImpulseCarryObservedMeanIsNotInitialTimeDependentObservedMean) ); assert_eq!( - refuse_impulse_carry_observed_mean_as_initial_time_independent_observed_mean( - carried_observed, - recovered + refuse_initial_time_independent_observed_mean_as_initial_time_dependent_observed_mean( + tipred, recovered ), - Err(PsychometricError::ImpulseCarryObservedMeanIsNotInitialTimeIndependentObservedMean) + Err(PsychometricError::InitialTimeIndependentObservedMeanIsNotInitialTimeDependentObservedMean) ); } #[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( + 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, @@ -13289,21 +16118,8 @@ mod tests { ) .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( + recover_discrete_observed_mean_with_initial_time_dependent_predictor( 1e308, 2.0, 0.0, @@ -13317,7 +16133,7 @@ mod tests { Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_observed_mean_with_initial_time_independent_predictor( + recover_discrete_observed_mean_with_initial_time_dependent_predictor( 2.0, 1.0, -0.5, @@ -13331,7 +16147,7 @@ mod tests { Err(PsychometricError::EventTimeRequired) ); assert_eq!( - recover_discrete_observed_mean_with_initial_time_independent_predictor( + recover_discrete_observed_mean_with_initial_time_dependent_predictor( 2.0, 1.0, -0.5, @@ -13345,7 +16161,7 @@ mod tests { Err(PsychometricError::NonPositiveInterval) ); assert_eq!( - recover_discrete_observed_mean_with_initial_time_independent_predictor( + recover_discrete_observed_mean_with_initial_time_dependent_predictor( 1e308, 0.0, 0.0, @@ -13361,618 +16177,502 @@ mod tests { } #[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) - ); - assert_eq!( - recover_initial_time_dependent_predictor_effect(effect, 0.0), - Ok(0.0) - ); - let carry = recover_initial_time_dependent_predictor_carry( - effect, - predictor, - drift, - delta, + #[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 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("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, + .expect("predetermined initial T0VAR"); + let added = recover_asymptotic_time_independent_predictor_variance( + printed_effect, + predictor_variance, + log_rate, 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, + .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, + predictor_variance, + log_rate, 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, + .expect("stationary initial T0VAR"); + assert!((recovered - stationary).abs() > 1e-3); + 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("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, + .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("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, + .expect("predetermined lagged T0VAR"); + assert!((recovered - lagged).abs() > 1e-3); + let later = recover_predetermined_later_latent_variance( + trait_variance, + initial_latent_variance, + diffusion, + printed_effect, + predictor_variance, + log_rate, + event_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, + .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, + initial_latent_variance, + printed_effect, + predictor_variance, + log_rate, + 1e-12, 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); + .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("Δt→0+ later"); + assert!((near_later - recovered).abs() < 1e-9); assert_eq!( - recover_discrete_latent_mean_with_initial_time_dependent_predictor( - initial, - drift, - intercept, + recover_predetermined_initial_latent_variance( 0.0, - predictor, - delta, - LagClock::EventTime - ), - Ok(evolved) - ); - assert_eq!( - recover_discrete_latent_mean_with_initial_time_dependent_predictor( 0.0, - drift, 0.0, - effect, - predictor, - delta, + predictor_variance, + 0.0, LagClock::EventTime ), - Ok(carry) + Ok(0.0) ); - let scaled = recover_initial_time_dependent_predictor_carry( - 1e308, - 1e-308, + let trait_only = recover_predetermined_initial_latent_variance( + trait_variance, + 0.0, + 0.0, + predictor_variance, 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, + .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, - 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); + .expect("trait-only a≥0"); + assert!((unstable_trait - trait_variance).abs() < 1e-15); } #[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 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("t0-td-carry"); - let increment = recover_discrete_time_independent_predictor_effect( - effect, - predictor, - -0.5, - 2.0, + .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("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, + .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("td-carry"); - assert_eq!( - refuse_initial_time_dependent_effect_as_contemporaneous_impulse(shift, impulse), - Err(PsychometricError::InitialTimeDependentEffectIsNotContemporaneousImpulse) - ); + .expect("predetermined later T0VAR"); assert_eq!( - refuse_initial_time_dependent_carry_as_initial_effect(carry, shift), - Err(PsychometricError::InitialTimeDependentCarryIsNotInitialEffect) - ); - assert_eq!( - refuse_initial_time_dependent_effect_as_continuous_intercept(shift, 0.4), - Err(PsychometricError::InitialTimeDependentEffectIsNotContinuousIntercept) - ); - assert_eq!( - refuse_initial_time_dependent_effect_as_process_increment(shift, increment), - Err(PsychometricError::InitialTimeDependentEffectIsNotProcessIncrement) + refuse_predetermined_initial_latent_variance_as_stationary_initial_latent_variance( + recovered, stationary + ), + Err( + PsychometricError::PredeterminedInitialLatentVarianceIsNotStationaryInitialLatentVariance + ) ); assert_eq!( - refuse_initial_time_dependent_effect_as_initial_time_independent_effect( - shift, - tipred_shift + refuse_predetermined_initial_latent_variance_as_initial_latent_variance( + recovered, + initial_latent_variance ), - Err(PsychometricError::InitialTimeDependentEffectIsNotInitialTimeIndependentEffect) + Err(PsychometricError::PredeterminedInitialLatentVarianceIsNotInitialLatentVariance) ); assert_eq!( - refuse_initial_time_dependent_coefficient_as_initial_effect(effect, shift), - Err(PsychometricError::InitialTimeDependentCoefficientIsNotInitialEffect) + refuse_predetermined_initial_latent_variance_as_lagged_latent_covariance( + recovered, lagged + ), + Err(PsychometricError::PredeterminedInitialLatentVarianceIsNotLaggedLatentCovariance) ); assert_eq!( - refuse_initial_time_dependent_carry_as_impulse_carry(carry, impulse_carry), - Err(PsychometricError::InitialTimeDependentCarryIsNotImpulseCarry) + refuse_predetermined_initial_latent_variance_as_later_latent_variance(recovered, later), + Err(PsychometricError::PredeterminedInitialLatentVarianceIsNotLaterLatentVariance) ); } #[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) - ); - 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) - ); - assert_eq!( - recover_initial_time_dependent_predictor_carry( - 0.4, - 3.0, - f64::NAN, - 2.0, - 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) - ); + fn predetermined_initial_latent_variance_invalid_inputs_fail_closed() { assert_eq!( - recover_initial_time_dependent_predictor_carry( - 0.4, - 3.0, - -0.5, + recover_predetermined_initial_latent_variance( + 1.0, 2.0, + -0.225, + 1.0, + -0.13, LagClock::SystemTime ), Err(PsychometricError::EventTimeRequired) ); assert_eq!( - recover_discrete_latent_mean_with_initial_time_dependent_predictor( - 1e308, + recover_predetermined_initial_latent_variance( 0.0, 0.0, - 1e308, - 1.0, - 1.0, - LagClock::EventTime - ), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_initial_time_dependent_predictor_carry( - 1.0, - 1.0, - f64::INFINITY, - 1.0, - LagClock::EventTime - ), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_initial_time_dependent_predictor_carry( - f64::NAN, - 1.0, - -0.5, - 2.0, - LagClock::EventTime - ), - Err(PsychometricError::InvalidNumericInput) - ); - assert_eq!( - recover_initial_time_dependent_predictor_carry( - 1.0, + -0.225, 1.0, - 1e308, - 10.0, + 0.5, LagClock::EventTime ), - Err(PsychometricError::InvalidNumericInput) + Err(PsychometricError::AsymptoticTimeIndependentEffectRequiresStableDrift) ); assert_eq!( - recover_discrete_latent_mean_with_initial_time_dependent_predictor( - 1.0, - -0.5, - 0.3, - f64::NAN, + recover_predetermined_initial_latent_variance( + 0.0, + 0.0, + 0.0, 1.0, - 2.0, + 0.0, LagClock::EventTime ), - Err(PsychometricError::InvalidNumericInput) + Ok(0.0) ); - } - - #[test] - #[allow(clippy::too_many_lines)] - fn discrete_observed_mean_with_initial_time_dependent_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_dependent_predictor( - loading, - initial, - drift, - intercept, - effect, - predictor, - manifest_mean, - delta, - LagClock::EventTime, - ) - .expect("eq5-t0tdpred-mean"); - let composed = recover_discrete_latent_mean_with_initial_time_dependent_predictor( - initial, - drift, - intercept, - effect, - predictor, - delta, - LagClock::EventTime, - ) - .expect("eq3-t0tdpred"); - 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"); - 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, + let brownian = recover_predetermined_initial_latent_variance( + 0.0, + 2.0, + 0.0, + 0.0, + 0.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("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); + .expect("Brownian a=0"); + assert!((brownian - 2.0).abs() < 1e-12); + assert_eq!( + recover_predetermined_initial_latent_variance( + f64::NAN, + 2.0, + 0.0, + 0.0, + -0.5, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); assert_eq!( - recover_discrete_observed_mean_with_initial_time_dependent_predictor( + recover_predetermined_initial_latent_variance( + f64::MAX, + f64::MAX, 0.0, - initial, - drift, - intercept, - effect, - predictor, - manifest_mean, - delta, + 0.0, + -0.5, LagClock::EventTime ), - Ok(manifest_mean) + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_predetermined_initial_latent_variance( + f64::MAX, + 0.0, + 1.0, + f64::MAX, + -1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) ); - 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, + #[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 event_delta = 1.0_f64; + let recovered = recover_predetermined_initial_observed_variance( + loading, + trait_variance, + initial_latent_variance, + printed_effect, 1.0, - -0.5, - 0.3, - 0.4, - 3.0, - 0.5, - 2.0, + log_rate, + measurement_error, + manifest_trait, 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, + .expect("eq5-initial-predetermined-T0VAR"); + let latent = recover_predetermined_initial_latent_variance( + trait_variance, + initial_latent_variance, + printed_effect, 1.0, - -0.5, - 0.3, - 0.4, - 3.0, - 0.5, - 2.0, + 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, - LagClock::EventTime, + .expect("predetermined initial T0VAR"); + let expected = recover_manifest_trait_plus_state_observed_variance( + loading, + latent, + measurement_error, + manifest_trait, ) - .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, + .expect("λ²p+θ+ψ"); + assert!((recovered - expected).abs() < 1e-12); + let stationary = recover_stationary_initial_observed_variance( + loading, + trait_variance, + diffusion, + printed_effect, 1.0, + log_rate, + measurement_error, + manifest_trait, LagClock::EventTime, ) - .expect("carry"); - let tipred = recover_discrete_observed_mean_with_initial_time_independent_predictor( - 2.0, + .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, - -0.5, - 0.3, - 0.4, - 3.0, - 0.5, - 2.0, + log_rate, + event_delta, + measurement_error, + manifest_trait, LagClock::EventTime, ) - .expect("t0tipred"); + .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_evolved_observed_mean_as_initial_time_dependent_observed_mean( - evolved, recovered + recover_predetermined_initial_observed_variance( + 0.0, + trait_variance, + initial_latent_variance, + printed_effect, + 1.0, + log_rate, + measurement_error, + manifest_trait, + LagClock::EventTime, ), - Err(PsychometricError::EvolvedObservedMeanIsNotInitialTimeDependentObservedMean) + Ok(measurement_error + manifest_trait) ); assert_eq!( - refuse_time_independent_observed_mean_as_initial_time_dependent_observed_mean( - process, recovered + recover_predetermined_initial_observed_variance( + loading, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + LagClock::EventTime, ), - Err( - PsychometricError::TimeIndependentObservedMeanIsNotInitialTimeDependentObservedMean - ) + Ok(0.0) ); assert_eq!( - refuse_impulse_observed_mean_as_initial_time_dependent_observed_mean( - impulse, recovered + 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::ImpulseObservedMeanIsNotInitialTimeDependentObservedMean) + Err(PsychometricError::MeasurementErrorIsNotPredeterminedInitialObservedVariance) ); assert_eq!( - refuse_impulse_carry_observed_mean_as_initial_time_dependent_observed_mean( - carry, recovered + refuse_stationary_initial_observed_variance_as_predetermined_initial_observed_variance( + stationary, recovered ), - Err(PsychometricError::ImpulseCarryObservedMeanIsNotInitialTimeDependentObservedMean) + Err( + PsychometricError::StationaryInitialObservedVarianceIsNotPredeterminedInitialObservedVariance + ) ); assert_eq!( - refuse_initial_time_independent_observed_mean_as_initial_time_dependent_observed_mean( - tipred, recovered + refuse_predetermined_later_observed_variance_as_predetermined_initial_observed_variance( + later, recovered ), - Err(PsychometricError::InitialTimeIndependentObservedMeanIsNotInitialTimeDependentObservedMean) + Err( + PsychometricError::PredeterminedLaterObservedVarianceIsNotPredeterminedInitialObservedVariance + ) ); } #[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, + fn predetermined_initial_observed_variance_invalid_inputs_fail_closed() { + 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) + ); + 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) + ); + let brownian = recover_predetermined_initial_observed_variance( + 1.0, + 0.0, + 2.0, 0.0, + 1.0, 0.0, 0.0, - 3.0, 0.0, - 1.0, LagClock::EventTime, ) - .expect("scale"); - assert!((scaled - 1.0).abs() < 1e-15); + .expect("Brownian a=0"); + assert!((brownian - 2.0).abs() < 1e-12); assert_eq!( - recover_discrete_observed_mean_with_initial_time_dependent_predictor( - 1e308, + recover_predetermined_initial_observed_variance( 2.0, 0.0, 0.0, 0.0, - 3.0, - 0.0, 1.0, + 0.0, + 0.5, + 0.1, LagClock::EventTime ), - Err(PsychometricError::InvalidNumericInput) + Ok(0.6) ); assert_eq!( - recover_discrete_observed_mean_with_initial_time_dependent_predictor( - 2.0, - 1.0, - -0.5, - 0.3, - 0.4, - 3.0, - 0.5, + recover_predetermined_initial_observed_variance( 2.0, - LagClock::SystemTime - ), - Err(PsychometricError::EventTimeRequired) - ); - assert_eq!( - recover_discrete_observed_mean_with_initial_time_dependent_predictor( + f64::NAN, 2.0, - 1.0, + 0.0, + 0.0, -0.5, - 0.3, - 0.4, - 3.0, - 0.5, + 0.0, 0.0, LagClock::EventTime ), - Err(PsychometricError::NonPositiveInterval) + Err(PsychometricError::InvalidNumericInput) ); assert_eq!( - recover_discrete_observed_mean_with_initial_time_dependent_predictor( - 1e308, + recover_predetermined_initial_observed_variance( + 2.0, + f64::MAX, + f64::MAX, 0.0, 0.0, + -0.5, 0.0, - 1e308, - 1.0, 0.0, - 1.0, LagClock::EventTime ), Err(PsychometricError::InvalidNumericInput) diff --git a/crates/psychometric_core/src/lib.rs b/crates/psychometric_core/src/lib.rs index 3d77eeb2..4e1db6d9 100644 --- a/crates/psychometric_core/src/lib.rs +++ b/crates/psychometric_core/src/lib.rs @@ -185,6 +185,69 @@ //! (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), //! and refuses //! latent-mean comparison below strong invariance. @@ -321,6 +384,18 @@ 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-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 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`. @@ -495,12 +570,50 @@ 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 predetermined later-occasion `T0VAR`. +pub use event_time::refuse_measurement_error_as_predetermined_later_observed_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 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 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 Driver Eq. 3 process noise as the unconditional variance. pub use event_time::refuse_process_noise_as_unconditional_variance; /// Refuse treating p. 16 stationary `T0MEANS` as `asymCINT`. @@ -529,6 +642,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. @@ -537,6 +652,8 @@ 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-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. @@ -547,6 +664,8 @@ 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 `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`. 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 e5c3c990..2b964ac2 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -34,13 +34,17 @@ use psychometric_core::{ 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_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, + recover_predetermined_initial_latent_variance, recover_predetermined_initial_observed_variance, + recover_predetermined_lagged_latent_covariance, + recover_predetermined_lagged_observed_covariance, recover_predetermined_later_latent_variance, + recover_predetermined_later_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, @@ -107,9 +111,28 @@ 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_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_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_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_process_noise_as_unconditional_variance, refuse_stationary_initial_latent_mean_as_asymptotic_continuous_intercept, refuse_stationary_initial_latent_mean_as_asymptotic_time_independent_effect, @@ -124,15 +147,18 @@ 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_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_within_subject_observed_variance_as_stationary_initial_observed_variance, refuse_time_dependent_impulse_as_continuous_intercept, refuse_time_dependent_impulse_as_time_independent_effect, @@ -5807,3 +5833,1215 @@ 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) + ); +} diff --git a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs index 769ec14a..70692aba 100644 --- a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs +++ b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs @@ -29,13 +29,17 @@ use psychometric_core::{ 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_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, + recover_predetermined_initial_latent_variance, recover_predetermined_initial_observed_variance, + recover_predetermined_lagged_latent_covariance, + recover_predetermined_lagged_observed_covariance, recover_predetermined_later_latent_variance, + recover_predetermined_later_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, @@ -102,8 +106,27 @@ 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_observed_variance, refuse_measurement_error_as_stationary_lagged_observed_covariance, refuse_measurement_error_as_stationary_later_observed_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_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_process_noise_as_unconditional_variance, refuse_stationary_initial_latent_mean_as_asymptotic_continuous_intercept, refuse_stationary_initial_latent_mean_as_asymptotic_time_independent_effect, @@ -118,15 +141,18 @@ 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_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_within_subject_observed_variance_as_stationary_initial_observed_variance, refuse_time_dependent_impulse_as_continuous_intercept, refuse_time_dependent_impulse_as_time_independent_effect, @@ -2973,3 +2999,577 @@ 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 + ) + ); +} diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index 50dcf2a8..e27444bc 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -28,7 +28,7 @@ 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; estimator-side method model remains future | partial | | candidate K statistical/Pareto gates + blinded LLM review | ADR 0012; research | future `model_selection` | accepted-target | | 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)), 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 | | TDT detection/tracking vs CHRONOS schema/prediction/temporal consistency | ADR 0016; PRD/research | future `event_intelligence` | accepted-target | | 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 936510f4..e684c6b0 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)), 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 **Date:** 2026-08-05 **Supersedes:** None. ADR 0012 governs upstream topic measurement/network coordinates; this ADR governs higher-order psychometric structure and longitudinal interpretation. @@ -20,7 +20,7 @@ Before ESEM/SEM interpretation, each higher-order construct is classified as ref Longitudinal analysis evaluates measurement invariance at the level needed for the claimed comparison, supports partial/approximate or time-varying loadings where scientifically justified, separates stable between-unit components from within-unit temporal change, and handles irregular intervals through appropriate discrete- or continuous-time dynamics. -The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) 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, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; 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`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), recovers the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; a zero drift is `κ Δt`; underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`), recovers the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; JSS PDF re-opened 2026-08-19T22:10Z; the first-occasion map `τ + λ μ_0` is not `E(y_t)`; `MANIFESTMEANS` is not `E(y_t)`; `μ_t` is not `E(y_t)`), recovers the exact scalar contemporaneous time-dependent predictor impulse `m x` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z; `TDPREDEFFECT` is `M`, not `CINT`; `M x` is not `A^{-1}[e^{A Δt} − I] B z`; `M x` is not Voelkle et al., 2012, Eq. 14; the level-change form is not that impulse), recovers the exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition; JSS PDF re-opened 2026-08-20T09:01Z; 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`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-impulse latent mean is not `E(y_t)`), recovers the exact scalar time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z; `TIPREDEFFECT` is `B`, not `κ`; form `B z` first, then the discrete intercept map; a zero drift is `B z Δt`; `B` is not the discrete increment; `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle Eq. 14), recovers the exact scalar observed mean of that increment `τ + λ(μ_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; JSS PDF re-opened 2026-08-20T12:12Z; 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`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-increment latent mean is not `E(y_t)`), recovers the exact scalar within-interval time-dependent impulse carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2, pp. 4–5; Eq. 3 exponential map; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z; form `m x` first, then `e^{a(t−u)} m x`; a zero drift is `m x`; 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 Eq. 14; an impulse at `u = t` is the contemporaneous map; an impulse at `u ≤ t0` is already in `η(t0)`), recovers the exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean; JSS PDF re-opened 2026-08-20T05:12Z; 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)`), recovers the exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and its Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF opened 2026-08-20T15:14Z; form `t0_b z` first, then `e^{a Δt} t0_b z`; a zero drift is `t0_b z`; underflow of `e^{a Δt}` to `+0` is a vanishing carry 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), recovers the exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition; JSS PDF re-opened 2026-08-20T15:28Z; 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`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and its 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; form `t0_m x0` first, then `e^{a Δt} t0_m x0`; a zero drift is `t0_m x0`; underflow of `e^{a Δt}` to `+0` is a vanishing carry 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`), recovers the exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_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; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar §7.2 level-change `CINT` setting `κ = −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`; `a < 0` so `−κ / a = m x`; `a ≥ 0` cannot hold a new process mean; `−a m x` is not the dissipating Dirac, 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), recovers the exact scalar Eq. 3 increment of that setting `(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`), recovers the 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; form `a_{ηξ} x` first; `ε = a` is `a_{ηξ} x Δt e^{a Δt}`; a zero coupling or zero predictor is exactly zero; `ε ≥ 0` cannot hold a lasting extra state; that contribution is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`), recovers the 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; form the evolved-plus-contribution latent mean first, then `τ + λ` of that mean; 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)`), recovers the exact scalar after-t0 extra-process contribution `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` and its 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` while `μ_t` still uses `Δt`; an impulse at `u = t0` or `u = t` is not interior; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive), recovers the exact scalar §7.2 `asymTIPREDEFFECT` `-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`; `a < 0`; a zero coefficient or zero predictor is exactly zero; `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`), recovers the exact scalar §7.2 `addedTIPREDVAR` `(B / a)² v` (Driver et al., 2017, §7.2, pp. 20–21; form the unit asymptotic effect first, then square, then multiply by `v`; `v ≥ 0`; a zero coefficient or zero predictor variance is exactly zero; `(B / a)² v` is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`), recovers the exact scalar Table 2 `asymCINT` `-κ / a` (Driver et al., 2017, Table 2, p. 12; Eq. 3 as `Δt → ∞`; JSS PDF opened 2026-08-21T16:13Z; form `κ` first, then divide by `-a`; `a < 0`; a zero intercept is exactly zero; `-κ / a` is not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`), recovers the exact scalar p. 16 stationary `T0MEANS` `-κ / a + −B z / a` (Driver et al., 2017, p. 16; form the intercept contribution first, then include the TI extra effect, then add; not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean), recovers the 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)`), recovers the 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; form the within-subject contribution first, then include the trait, then include the TI extra variance, then add; not free `T0VAR`, not `asymDIFFUSION` alone, not `TRAITVAR` alone, not `addedTIPREDVAR` alone, and not the finite-interval discrete latent variance), recovers the exact scalar Eq. 5 of that constrained variance `λ²(trait + −q / (2 a) + (B / a)² v) + θ + ψ` (Driver et al., 2017, §4.3, pp. 9–10; Eq. 5, p. 5; Table 2, p. 12; 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)`), recovers the 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), recovers the 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), recovers the 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; 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`; 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), recovers the 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), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance (Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z: scalar is required for latent means; residual invariance is not). Two-observation series have no residual degrees of freedom and cap at strong/scalar; identically-zero OLS residual variance is not strict. This two-group OLS gate is not MGCFA. +The executable multilevel slice is cluster-mean centering (CWC) plus within/between OLS, the CWC contextual effect (`between − within`), and Kish-weighted slopes. Enders and Tofighi (2007, Table 2, pp. 124–127) show that the CWC cluster-mean coefficient is the contextual effect, not the between-cluster effect. It is not DSEM and not RI-CLPM. The executable temporal slice maps a discrete lag through the exact scalar exponential `a = ln(φ) / Δt` on event time only (Voelkle, Oud, Davidov, & Schmidt, 2012, Eq. 7; Driver, Oud, & Voelkle, 2017, Eq. 3), recovers the forward map `φ(Δt) = exp(a Δt)`, remaps a discrete lag onto another event interval through that log-rate, recovers the exact scalar discrete effect of a constant predictor (Voelkle et al., 2012, Eq. 12) 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, and in log space when `expm1(z)` overflows at a finite `z` (`a_yx = 0` is exactly zero; an overflowing `a_yx/a_xx` rewrite term fails closed), recovers the first-order discrete effect of a time-varying predictor with matched sampling and constancy intervals as `a_yx Δt` (Voelkle et al., 2012, Eq. 14; ZORA accepted manuscript p. 21; not Eq. 12; unmatched intervals fail closed because Oud & Jansen, 2000, is unread), recovers the exact scalar discrete process noise `Q_Δt = 0.5 q (expm1(z) / a)` with `z = 2 (a Δt)` and `q = G G⊤ ≥ 0` (Driver et al., 2017, Eq. 3; JSS PDF re-opened 2026-08-18T07:06Z, p. 4; scalar `L = 1`; do not form `2 a` first; `a = 0` and `z → 0` recover `q Δt`; `z → −∞` keeps `−0.5 q / a`; a zero diffusion is exactly zero; `z → +∞` and an overflowing rewrite scale `0.5 q / a` fail closed), recovers the exact scalar lagged latent covariance `exp(a Δt) p` and the law-of-total-variance map `Var(η_t) = exp(2 a Δt) p + Q_Δt` (Driver et al., 2017, Eq. 3–4, pp. 4–5; JSS PDF re-opened 2026-08-18T18:03Z; JSS has no numbered §2.2; `Q_Δt` is `cov(η_t | η_{t-1})` and is refused as the unconditional variance; a zero diffusion whose `2 (a Δt)` overflows to `+∞` fails closed), recovers the exact scalar stationary within-subject variance `-q / (2 a)` as the `Δt → ∞` limit of Eq. 4 for stable `a < 0` (JSS p. 16 `asymDIFFUSION`; §4.3 T0VAR stationarity; 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`; CodeRabbit on `75ecdd3`); when `2 a` overflows, form `(q / a) * -0.5`; do not form `0.5 q` first (`q = from_bits(1)`, `a = -from_bits(1)` → `0.5`); `a ≥ 0` and finite-interval `Q_Δt` fail closed as that limit), recovers the exact scalar trait-plus-state variance `trait + state` and lagged covariance `trait + exp(a Δt) p` (Driver et al., 2017, §4.3, p. 9; JSS PDF re-opened 2026-08-18T21:07Z; a stable trait has `DRIFT` and `DIFFUSION` fixed to zero; `TRAITVAR` is not process noise and not `asymDIFFUSION`; evolving the summed variance as if it were all state fails the claim boundary; this is not RI-CLPM), recovers the exact scalar observed-indicator variance `λ² Var(η) + θ` when `MANIFESTTRAITVAR` is zero and `λ² Var(η) + θ + ψ` otherwise, and the lagged observed covariance `λ² cov(η_t, η_{t-1}) + ψ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T04:18Z; Equation 1 is the latent SDE; form `(λ p) λ` then add `θ`, then add `ψ`; do not form `λ²` first; `MANIFESTVAR` is `Θ`, not `Var(y)` and does not enter lagged observed covariance; `MANIFESTTRAITVAR` is `Ψ_τ`, not `Θ`; `TRAITVAR` is latent and scaled by `λ²`; `Var(η)` is not `Var(y)`), recovers the exact scalar observed-indicator mean `τ + λ μ` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; JSS PDF re-opened 2026-08-19T14:08Z; `MANIFESTMEANS` is `τ`, not `E(y)`; `CINT` is not `MANIFESTMEANS`; `T0MEANS` is not `E(y)`), recovers the exact scalar discrete latent mean `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ` (Driver et al., 2017, Eq. 3, p. 4; Table 2, p. 12; JSS PDF re-opened 2026-08-19T18:10Z; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; a zero drift is `κ Δt`; underflow of `exp(a Δt)` to `+0` drops the carried `T0MEANS` and keeps `−κ / a`), recovers the exact scalar evolved observed-indicator mean `τ + λ μ_t` (Driver et al., 2017, Eq. 5 of the Eq. 3 expected-value map; JSS PDF re-opened 2026-08-19T22:10Z; the first-occasion map `τ + λ μ_0` is not `E(y_t)`; `MANIFESTMEANS` is not `E(y_t)`; `μ_t` is not `E(y_t)`), recovers the exact scalar contemporaneous time-dependent predictor impulse `m x` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T07:10Z; `TDPREDEFFECT` is `M`, not `CINT`; `M x` is not `A^{-1}[e^{A Δt} − I] B z`; `M x` is not Voelkle et al., 2012, Eq. 14; the level-change form is not that impulse), recovers the exact scalar observed mean of that contemporaneous impulse `τ + λ(μ_t + m x)` (Driver et al., 2017, Eq. 5 of the Eq. 3 fourth-summand composition; JSS PDF re-opened 2026-08-20T09:01Z; 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`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-impulse latent mean is not `E(y_t)`), recovers the exact scalar time-independent predictor increment `A^{-1}[e^{A Δt} − I] B z` (Driver et al., 2017, Eq. 1–3, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-20T10:13Z; `TIPREDEFFECT` is `B`, not `κ`; form `B z` first, then the discrete intercept map; a zero drift is `B z Δt`; `B` is not the discrete increment; `A^{-1}[e^{A Δt} − I] B z` is not `CINT`, not `M x`, and not Voelkle Eq. 14), recovers the exact scalar observed mean of that increment `τ + λ(μ_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; JSS PDF re-opened 2026-08-20T12:12Z; 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`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-increment latent mean is not `E(y_t)`), recovers the exact scalar within-interval time-dependent impulse carry `e^{A(t−u)} M x` for `t0 < u < t` (Driver et al., 2017, Eq. 1–2, pp. 4–5; Eq. 3 exponential map; §7.2, pp. 20–21; JSS PDF re-opened 2026-08-20T10:33Z; form `m x` first, then `e^{a(t−u)} m x`; a zero drift is `m x`; 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 Eq. 14; an impulse at `u = t` is the contemporaneous map; an impulse at `u ≤ t0` is already in `η(t0)`), recovers the exact scalar observed mean of that carry `τ + λ(μ_t + e^{a(t−u)} m x)` (Driver et al., 2017, Eq. 5 of the Eq. 1–2 carried latent mean; JSS PDF re-opened 2026-08-20T05:12Z; 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)`), recovers the exact scalar first-occasion `T0TIPREDEFFECT` shift `t0_b z` and its Eq. 3 first-summand carry `e^{A Δt} t0_b z` (Driver et al., 2017, Table 3, p. 13; Eq. 3, p. 5; JSS PDF opened 2026-08-20T15:14Z; form `t0_b z` first, then `e^{a Δt} t0_b z`; a zero drift is `t0_b z`; underflow of `e^{a Δt}` to `+0` is a vanishing carry 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), recovers the exact scalar observed mean of that first-occasion carry `τ + λ(μ_t + e^{a Δt} t0_b z)` (Driver et al., 2017, Eq. 5 of the Table 3 / Eq. 3 first-summand composition; JSS PDF re-opened 2026-08-20T15:28Z; 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`; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar first-occasion `T0TDPREDEFFECT` shift `t0_m x0` and its 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; form `t0_m x0` first, then `e^{a Δt} t0_m x0`; a zero drift is `t0_m x0`; underflow of `e^{a Δt}` to `+0` is a vanishing carry 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`), recovers the exact scalar observed mean of that first-occasion TD carry `τ + λ(μ_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; `MANIFESTMEANS` is not `E(y_t)`; the evolved-plus-carry latent mean is not `E(y_t)`), recovers the exact scalar §7.2 level-change `CINT` setting `κ = −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`; `a < 0` so `−κ / a = m x`; `a ≥ 0` cannot hold a new process mean; `−a m x` is not the dissipating Dirac, 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), recovers the exact scalar Eq. 3 increment of that setting `(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`), recovers the 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; form `a_{ηξ} x` first; `ε = a` is `a_{ηξ} x Δt e^{a Δt}`; a zero coupling or zero predictor is exactly zero; `ε ≥ 0` cannot hold a lasting extra state; that contribution is not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`), recovers the 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; form the evolved-plus-contribution latent mean first, then `τ + λ` of that mean; 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)`), recovers the exact scalar after-t0 extra-process contribution `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` and its 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` while `μ_t` still uses `Δt`; an impulse at `u = t0` or `u = t` is not interior; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive), recovers the exact scalar §7.2 `asymTIPREDEFFECT` `-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`; `a < 0`; a zero coefficient or zero predictor is exactly zero; `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`), recovers the exact scalar §7.2 `addedTIPREDVAR` `(B / a)² v` (Driver et al., 2017, §7.2, pp. 20–21; form the unit asymptotic effect first, then square, then multiply by `v`; `v ≥ 0`; a zero coefficient or zero predictor variance is exactly zero; `(B / a)² v` is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`), recovers the exact scalar Table 2 `asymCINT` `-κ / a` (Driver et al., 2017, Table 2, p. 12; Eq. 3 as `Δt → ∞`; JSS PDF opened 2026-08-21T16:13Z; form `κ` first, then divide by `-a`; `a < 0`; a zero intercept is exactly zero; `-κ / a` is not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`), recovers the exact scalar p. 16 stationary `T0MEANS` `-κ / a + −B z / a` (Driver et al., 2017, p. 16; form the intercept contribution first, then include the TI extra effect, then add; not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean), recovers the 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)`), recovers the 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; form the within-subject contribution first, then include the trait, then include the TI extra variance, then add; not free `T0VAR`, not `asymDIFFUSION` alone, not `TRAITVAR` alone, not `addedTIPREDVAR` alone, and not the finite-interval discrete latent variance), recovers the exact scalar Eq. 5 of that constrained variance `λ²(trait + −q / (2 a) + (B / a)² v) + θ + ψ` (Driver et al., 2017, §4.3, pp. 9–10; Eq. 5, p. 5; Table 2, p. 12; 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)`), recovers the 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), recovers the 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), recovers the 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; 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`; 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), recovers the 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), recovers 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, 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 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), recovers the exact scalar Eq. 5 of that predetermined later-occasion variance `λ²(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 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; 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), recovers the exact scalar Eq. 5 of that predetermined lagged covariance `λ²(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 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), recovers the 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), and refuses the difference quotient, pooling discrete lags from unequal intervals, and a binary64 underflow of that exponential to `+0` (not a discrete lag). The first-order product `a_yx Δt` is Eq. 14 and the underflow limit of Eq. 12, not the general constant-predictor discrete effect. Already-centered residuals may have irregular event intervals. Subtracting the person-specific mean from a raw autoregressive series is not the lagged within-person residual (Curran & Bauer, 2011, pp. 607–608). Metric/weak invariance licenses shared metric meaning only. Latent-mean comparison requires strong (equal loading and intercept) or strict invariance (Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z: scalar is required for latent means; residual invariance is not). Two-observation series have no residual degrees of freedom and cap at strong/scalar; identically-zero OLS residual variance is not strict. This two-group OLS gate is not MGCFA. Input/process/intervention/outcome paths obey event-time order. Temporal precedence, document linkage, event tracking, or model prediction alone do not justify causal language. diff --git a/docs/research/multilevel-event-time-recovery.md b/docs/research/multilevel-event-time-recovery.md index 12f25297..051ab453 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -48,15 +48,21 @@ 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. refuse pooling discrete lags from unequal event intervals as one coefficient; +52. refuse unmatched sampling and constancy intervals for a time-varying predictor (Oud & Jansen, 2000, unread); +53. refuse the difference quotient as a continuous-time rate; +54. apply the same event-time map to CWC residuals (still not DSEM); +55. 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. ## Authoritative sources @@ -74,7 +80,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/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). ## Formula notes @@ -118,6 +124,10 @@ 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. @@ -165,6 +175,12 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - 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` 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);