diff --git a/CHANGELOG.md b/CHANGELOG.md index 062a69412..32b5b162b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,8 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang - `event_core` adds bounded Allen interval-consistency classification, atomic path-consistency closure, contradiction/resource refusals, and an explicit dependency-error fallback without claiming unrestricted global satisfiability. +- `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, p. 16 `TDPREDEFFECTstd`; Table 2, p. 12; Eq. 3, p. 5; footnote 4; §7.1, pp. 18–19; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-30T04:11Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar standardised continuous time-dependent predictor effect on current main after `0ce16e8` dropped the pre-consolidation code while research notes already named the map (register items 75–76). Page 16 prints continuous-time parameters and, when appropriate, standardised matrices with the suffix `std`. The printed example on p. 16 is `discreteDRIFTstd`, not `TDPREDEFFECTstd`. Footnote 4: standardisations use only the relevant variance, not the total. Table 2 names `M` `TDPREDEFFECT` and names `TDPREDVAR` the time-dependent predictor variance. The affecting variance is `TDPREDVAR` `v`, not `TIPREDVAR`. The affected variance is within-subject `asymDIFFUSION` `-q / (2 a)`, because the process dynamics are individual, or average individual, temporal dynamics. The 2017-era `summary.ctsemFit.R` `n.TDpred` section forms unstandardised `TDPREDEFFECT` only and comments out `TDPREDVAR` and `TDPREDVARstd`. That source does not form a `TDPREDEFFECTstd` matrix; the scalar map is the footnote 4 standardisation of that named coefficient: `m · √v / √(-q / (2 a))`. Form strictly positive `asymDIFFUSION` first, then strictly positive `v`, then the continuous Dirac coefficient, then the SD ratio. Form the SD ratio before multiplying so an overflowing `m √v` does not lose a finite std. A zero coefficient after strictly positive SDs is exactly zero (`0 · Inf` is NaN). When that ratio overflows, form `(m / √p) √v`. Unstandardised `M` is defined for a zero coefficient and for zero predictor variance; standardised `TDPREDEFFECT` is not. Zero `q` or zero `v` has no positive SD and fails closed. Lasting `p` requires stable `a < 0`. A non-event clock fails closed. `TIPREDEFFECTstd` `B · √v / √p` is a different named matrix even when `M = B`. The finite-interval intercept-style standardisation `A^{-1}[e^{A Δt} − I] M · √v / √p` depends on the event interval and is not this continuous Dirac coefficient. `m · √v / √(trait + p + added)` uses the total, not `asymDIFFUSION`, and is not `TDPREDEFFECTstd` when `TRAITVAR` is nonzero. `TRAITVAR` is not the standardisation variance. Independent of open `#303` `T0TDPREDEFFECTstd`, `#300` `TIPREDEFFECTstd`, `#299` `asymTIPREDEFFECTstd`, `#298` `DRIFTstd`, `#297` `DIFFUSIONstd`, `#296` `discreteDIFFUSIONstd`, `#280` `discreteDRIFTstd`, and `#272` `TIPREDVARstd`. Meredith (1993) remains unread (web search 2026-08-30T04:11Z: Springer/Cambridge Core paywalled; Unpaywall historically `is_oa: false`; Springer `content/pdf` is 403). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread on the same terms (DOI `10.1007/bf02294457`). 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, Table 2, p. 12 `MANIFESTTRAITVAR`; §7.1, p. 19; p. 16 `MANIFESTTRAITVARstd`; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-27T14:20Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar standardised manifest-trait variance on current main after `0ce16e8` dropped the pre-consolidation code while research notes already named the map (register items 83–84). Table 2 names `MANIFESTTRAITVAR` `Ψ_τ` the additional time-invariant variance-covariance on the measurement level and sets it `NULL` when there is no manifest trait. Equation 5 writes `Γ ~ N(τ, Ψ)` and names that covariance the manifest traits. Section 7.1 names manifest traits stable individual differences in indicator levels, distinct from process-level `TRAITVAR` `φ_ξ`. Page 16 prints standardised matrices with the suffix `std` when appropriate. The printed example on p. 16 is `discreteDRIFTstd`, not `MANIFESTTRAITVARstd`. Footnote 4 standardises using only the relevant variance, not the total. The relevant variance for that named indicator-level correlation is `MANIFESTTRAITVAR`, not process-level `TRAITVAR` and not residual `MANIFESTVAR` `θ`. The 2017-era source forms `MANIFESTTRAITVARstd` only when `MANIFESTTRAITVAR != 0`, as `solve(sqrt(diag(MANIFESTTRAITVAR) + ridging)) %&% MANIFESTTRAITVAR` when `verbose = TRUE`. OpenMx `%&%` is `t(A) %*% B %*% A`. Unlike `TRAITVARstd`, that formation adds `diag(c(ridging), n.manifest)`. The default `ridging = FALSE` adds 0, not `0.0001`; that ridge is a numerical hack and is not this exact map. The scalar correlation is `ψ / ψ = 1` after strictly positive `MANIFESTTRAITVAR`. Form strictly positive `ψ` first, then `1 / √ψ`, then `(1 / √ψ) ψ (1 / √ψ)`. Unstandardised `MANIFESTTRAITVAR` is defined for a zero trait; standardised `MANIFESTTRAITVAR` is not. Zero `MANIFESTTRAITVAR` skips forming `MANIFESTTRAITVARstd` in the 2017-era source and fails closed here. Indicator-level trait variance is an event-time structural quantity, so a non-event clock fails closed. `MANIFESTTRAITVAR` does not require stable `a < 0`. Distinct positive `ψ` recover the same 1. `trait / trait = 1` is `TRAITVARstd` and recovers the same number and remains a distinct named quantity. `θ` is `MANIFESTVAR` and is measurement error, not this correlation. Meredith (1993) remains unread (web search 2026-08-27T14:20Z: Springer/Cambridge Core paywalled; Unpaywall historically `is_oa: false`; Springer `content/pdf` is an HTML stub). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread on the same terms (DOI `10.1007/bf02294457`). 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, Table 2, p. 12 `TRAITVAR`; §7.1, pp. 18–19; p. 16 `TRAITVARstd`; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-26T17:45Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar standardised trait variance on current main after `0ce16e8` dropped the pre-consolidation code while research notes already named the map (register items 81–82). Table 2 names `TRAITVAR` `φ_ξ` the latent trait variance/covariance and sets it `NULL` when there is no trait. Section 7.1 names traits the stable between-subject differences (unit-level unobserved heterogeneity). Page 16 prints standardised matrices with the suffix `std` when appropriate. The printed example on p. 16 is `discreteDRIFTstd`, not `TRAITVARstd`. Footnote 4 standardises using only the relevant variance, not the total. The relevant variance for that named between-subject correlation is `TRAITVAR`, not free first-occasion `T0VAR` and not process-dynamics `asymDIFFUSION`. The 2017-era source forms `TRAITVARstd` only when `TRAITVAR != 0`, as `solve(sqrt(diag(TRAITVAR))) %&% TRAITVAR` when `verbose = TRUE`. OpenMx `%&%` is `t(A) %*% B %*% A`. Unlike `T0VARstd`, that formation uses `diag(diag(TRAITVAR))` and does not add `diag(c(ridging))`. The ridge is a `T0VAR` numerical hack and is not this exact map. The scalar correlation is `trait / trait = 1` after strictly positive `TRAITVAR`. Form strictly positive `trait` first, then `1 / √trait`, then `(1 / √trait) trait (1 / √trait)`. Unstandardised `TRAITVAR` is defined for a zero trait; standardised `TRAITVAR` is not. Zero `TRAITVAR` skips forming `TRAITVARstd` in the 2017-era source and fails closed here. Between-subject variance is an event-time structural quantity, so a non-event clock fails closed. `TRAITVAR` does not require stable `a < 0`. Distinct positive `trait` recover the same 1. `p_0 / p_0 = 1` is `T0VARstd` and recovers the same number and remains a distinct named quantity. `t0_b² v` is `addedT0TIPREDVAR` and is extra first-occasion TI variance, not this correlation. Meredith (1993) remains unread (Unpaywall 2026-08-26T17:20Z: `is_oa: false`; OpenAlex closed; Springer `content/pdf` is an HTML stub). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread on the same terms (DOI `10.1007/bf02294457`; Unpaywall `is_oa: false`). Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. diff --git a/crates/psychometric_core/src/error.rs b/crates/psychometric_core/src/error.rs index 4ab2695e0..a2cc67de6 100644 --- a/crates/psychometric_core/src/error.rs +++ b/crates/psychometric_core/src/error.rs @@ -710,6 +710,36 @@ pub enum PsychometricError { /// `MANIFESTVARstd`. `λ² Var(η) + θ` is `Var(y)`, not the /// correlation form of `Θ`. ObservedVarianceIsNotStandardisedManifestVariance, + + /// Driver p. 16 `TDPREDEFFECTstd` was requested without a + /// strictly positive `asymDIFFUSION`. Footnote 4 standardises + /// using only the relevant within-subject variance; zero `q` + /// has no positive process SD. + StandardisedTimeDependentEffectRequiresPositiveWithinSubjectVariance, + /// Driver p. 16 `TDPREDEFFECTstd` was requested without a + /// strictly positive time-dependent predictor variance. + /// Footnote 4 `m · √v / √p` has no positive affecting SD when + /// `v = 0`. + StandardisedTimeDependentEffectRequiresPositivePredictorVariance, + /// Driver Table 2 unstandardised `TDPREDEFFECT` `M` was treated + /// as `TDPREDEFFECTstd`. Unstandardised `M` is defined for a + /// zero coefficient and for zero predictor variance; + /// standardised `TDPREDEFFECT` is not. + UnstandardisedTimeDependentEffectIsNotStandardisedTimeDependentEffect, + /// Driver p. 16 `TIPREDEFFECTstd` `B · √v / √p` was treated as + /// `TDPREDEFFECTstd`. Equal numbers when `M = B` remain distinct + /// named quantities. + StandardisedTimeIndependentEffectIsNotStandardisedTimeDependentEffect, + /// Finite-interval intercept-style + /// `A^{-1}[e^{A Δt} − I] M · √v / √p` was treated as p. 16 + /// `TDPREDEFFECTstd`. That map depends on `Δt` and is not the + /// continuous Dirac coefficient. + DiscreteStandardisedTimeDependentEffectIsNotStandardisedTimeDependentEffect, + /// `m · √v / √(trait + p + added)` was treated as p. 16 + /// `TDPREDEFFECTstd`. Footnote 4 uses only `asymDIFFUSION`, not + /// total variance. `TRAITVAR` is not the standardisation + /// variance. + TraitContaminatedTimeDependentEffectIsNotStandardisedTimeDependentEffect, } impl fmt::Display for PsychometricError { @@ -1235,6 +1265,24 @@ impl fmt::Display for PsychometricError { Self::ObservedVarianceIsNotStandardisedManifestVariance => { "observed-indicator variance is not standardised measurement-error variance" } + Self::StandardisedTimeDependentEffectRequiresPositiveWithinSubjectVariance => { + "standardised time-dependent effect requires strictly positive within-subject variance" + } + Self::StandardisedTimeDependentEffectRequiresPositivePredictorVariance => { + "standardised time-dependent effect requires strictly positive predictor variance" + } + Self::UnstandardisedTimeDependentEffectIsNotStandardisedTimeDependentEffect => { + "unstandardised time-dependent effect is not standardised time-dependent effect" + } + Self::StandardisedTimeIndependentEffectIsNotStandardisedTimeDependentEffect => { + "standardised time-independent effect is not standardised time-dependent effect" + } + Self::DiscreteStandardisedTimeDependentEffectIsNotStandardisedTimeDependentEffect => { + "discrete standardised time-dependent effect is not standardised time-dependent effect" + } + Self::TraitContaminatedTimeDependentEffectIsNotStandardisedTimeDependentEffect => { + "trait-contaminated time-dependent effect is not standardised time-dependent effect" + } }; formatter.write_str(message) } @@ -2073,4 +2121,38 @@ mod tests { "measurement error is not standardised manifest-trait variance" ); } + + #[test] + fn standardised_time_dependent_effect_boundary_messages_are_stable() { + assert_eq!( + PsychometricError::StandardisedTimeDependentEffectRequiresPositiveWithinSubjectVariance + .to_string(), + "standardised time-dependent effect requires strictly positive within-subject variance" + ); + assert_eq!( + PsychometricError::StandardisedTimeDependentEffectRequiresPositivePredictorVariance + .to_string(), + "standardised time-dependent effect requires strictly positive predictor variance" + ); + assert_eq!( + PsychometricError::UnstandardisedTimeDependentEffectIsNotStandardisedTimeDependentEffect + .to_string(), + "unstandardised time-dependent effect is not standardised time-dependent effect" + ); + assert_eq!( + PsychometricError::StandardisedTimeIndependentEffectIsNotStandardisedTimeDependentEffect + .to_string(), + "standardised time-independent effect is not standardised time-dependent effect" + ); + assert_eq!( + PsychometricError::DiscreteStandardisedTimeDependentEffectIsNotStandardisedTimeDependentEffect + .to_string(), + "discrete standardised time-dependent effect is not standardised time-dependent effect" + ); + assert_eq!( + PsychometricError::TraitContaminatedTimeDependentEffectIsNotStandardisedTimeDependentEffect + .to_string(), + "trait-contaminated time-dependent effect is not standardised time-dependent effect" + ); + } } diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index a29bc5c18..ea969de4a 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -2630,6 +2630,205 @@ pub fn refuse_discrete_standardised_continuous_intercept_as_standardised_asympto ) } +/// Exact scalar p. 16 `TDPREDEFFECTstd` after strictly positive +/// `asymDIFFUSION` and `TDPREDVAR`. +/// +/// Driver, Oud, and Voelkle (2017, p. 16 `TDPREDEFFECTstd`; Table 2, +/// p. 12; Eq. 3, p. 5; footnote 4; §7.1, pp. 18–19; 2017-era ctsem +/// `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-30T04:11Z from +/// ) +/// print continuous-time parameters and, when appropriate, +/// standardised matrices with the suffix `std`. The printed example +/// on p. 16 is `discreteDRIFTstd`, not `TDPREDEFFECTstd`. Footnote 4 +/// standardises using only the relevant variance, not the total. +/// Table 2 names `M` `TDPREDEFFECT` the `n.latent × n.TDpred` effect +/// of time-dependent predictors on latent processes, and names +/// `TDPREDVAR` the time-dependent predictor variance. The affecting +/// variance is `TDPREDVAR` `v`, not `TIPREDVAR`. The affected +/// variance is within-subject `asymDIFFUSION` `p = −q / (2 a)`, +/// because the process dynamics are individual, or average +/// individual, temporal dynamics. The 2017-era +/// `summary.ctsemFit.R` `n.TDpred` section forms unstandardised +/// `TDPREDEFFECT` only and comments out `TDPREDVAR` and +/// `TDPREDVARstd`. That source does not form a `TDPREDEFFECTstd` +/// matrix; the scalar map here is the footnote 4 standardisation of +/// the named Table 2 coefficient: `m · √v / √p`. Form strictly +/// positive `p` first, then strictly positive `v`, then the +/// continuous Dirac coefficient, then the SD ratio. Form the SD +/// ratio before multiplying by the coefficient so that a finite +/// `m · √v / √p` is not lost to an overflowing `m √v`. A zero +/// coefficient after strictly positive variances is exactly zero +/// even when `√v / √p` overflows (`0 · Inf` is NaN). When that +/// ratio overflows, form `(m / √p) √v` so a tiny coefficient is +/// not lost to `Inf`. Unstandardised `M` is defined for a zero +/// coefficient and for zero predictor variance; standardised +/// `TDPREDEFFECT` is not. Zero `v` has no positive predictor SD +/// and fails closed. Zero `q` has no positive process SD and +/// fails closed. Lasting `p` requires stable `a < 0`. +/// `TDPREDEFFECT` is an event-time process-dynamics quantity, so a +/// non-event clock fails closed. A larger positive `q` yields a +/// smaller `|std|`. `TIPREDEFFECTstd` `B · √v / √p` is a different +/// named matrix even when `M = B` and the predictor variances +/// match. This crate does not currently export `TIPREDEFFECTstd` +/// (open `#300`); the refuse still names that quantity. The +/// finite-interval intercept-style standardisation +/// `A^{-1}[e^{A Δt} − I] M · √v / √p` treats the Dirac coefficient +/// as an integrated intercept and depends on the event interval. +/// `t0_m · √v / √p_0` is Table 3 `T0TDPREDEFFECTstd` against free +/// `T0VAR` and is not this process-dynamics map. Independent of +/// open `#303` `T0TDPREDEFFECTstd`, `#300` `TIPREDEFFECTstd`, +/// `#299` `asymTIPREDEFFECTstd`, `#298` `DRIFTstd`, `#297` +/// `DIFFUSIONstd`, `#296` `discreteDIFFUSIONstd`, `#280` +/// `discreteDRIFTstd`, and `#272` `TIPREDVARstd`. Section 7.1 +/// warns that omitting trait variance confounds between- and +/// within-person information. `m · √v / √(trait + p + added)` uses +/// the total, not `asymDIFFUSION`, and is not `TDPREDEFFECTstd` +/// when `TRAITVAR` is nonzero. `TRAITVAR` is not the +/// standardisation variance. This is not a Kalman filter, not a +/// matrix `expm`, not DSEM, and not ctsem estimation. +/// +/// # Errors +/// +/// Returns [`PsychometricError::EventTimeRequired`] for any +/// non-event clock, +/// [`PsychometricError::StationaryVarianceRequiresStableDrift`] +/// when `a ≥ 0`, +/// [`PsychometricError::StandardisedTimeDependentEffectRequiresPositiveWithinSubjectVariance`] +/// when `q = 0`, +/// [`PsychometricError::StandardisedTimeDependentEffectRequiresPositivePredictorVariance`] +/// when `v = 0`, and +/// [`PsychometricError::InvalidNumericInput`] when an input is +/// non-finite, the predictor variance is negative, or the mapped +/// product overflows. +pub fn recover_standardised_time_dependent_predictor_effect( + time_dependent_effect: f64, + predictor_variance: f64, + continuous_diffusion: f64, + log_rate: f64, + clock: LagClock, +) -> Result { + let within = recover_stationary_latent_variance(continuous_diffusion, log_rate, clock)?; + if within == 0.0 { + return Err( + PsychometricError::StandardisedTimeDependentEffectRequiresPositiveWithinSubjectVariance, + ); + } + let predictor_variance = require_finite(predictor_variance)?; + if predictor_variance < 0.0 { + return Err(PsychometricError::InvalidNumericInput); + } + if predictor_variance == 0.0 { + return Err( + PsychometricError::StandardisedTimeDependentEffectRequiresPositivePredictorVariance, + ); + } + let coefficient = require_finite(time_dependent_effect)?; + // A zero coefficient with strictly positive SDs is exactly zero + // even when `√v / √p` overflows (`0 · Inf` is NaN). + if coefficient == 0.0 { + return Ok(0.0); + } + let process_sd = within.sqrt(); + let predictor_sd = predictor_variance.sqrt(); + // Form the SD ratio first. `m √v` overflows at large `|m|` and + // large `v` even when `m √v / √p` is a binary64 number. + let ratio = predictor_sd / process_sd; + if ratio.is_finite() { + return require_finite(coefficient * ratio); + } + // Ratio overflowed. `(m / √p) √v` keeps a finite std when the + // coefficient is tiny and `p` is subnormal. + let scaled_coefficient = coefficient / process_sd; + if scaled_coefficient.is_finite() { + return require_finite(scaled_coefficient * predictor_sd); + } + Err(PsychometricError::InvalidNumericInput) +} + +/// Refuse treating unstandardised `TDPREDEFFECT` as p. 16 +/// `TDPREDEFFECTstd`. +/// +/// Unstandardised `M` is defined for a zero coefficient and for +/// zero predictor variance. Footnote 4 `TDPREDEFFECTstd` requires +/// strictly positive `asymDIFFUSION` and strictly positive +/// `TDPREDVAR`. Equal numbers when `v = p` remain distinct named +/// quantities. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::UnstandardisedTimeDependentEffectIsNotStandardisedTimeDependentEffect`]. +pub fn refuse_unstandardised_time_dependent_effect_as_standardised_time_dependent_effect( + unstandardised_effect: f64, + standardised_effect: f64, +) -> Result { + let _ = (unstandardised_effect, standardised_effect); + Err(PsychometricError::UnstandardisedTimeDependentEffectIsNotStandardisedTimeDependentEffect) +} + +/// Refuse treating p. 16 `TIPREDEFFECTstd` as p. 16 +/// `TDPREDEFFECTstd`. +/// +/// `B · √v / √p` standardises Table 2 `TIPREDEFFECT`. Footnote 4 +/// `TDPREDEFFECTstd` is `m · √v / √p`. Equal numbers when `M = B` +/// remain distinct named quantities. This crate does not currently +/// export `TIPREDEFFECTstd`; the refuse still names that quantity. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::StandardisedTimeIndependentEffectIsNotStandardisedTimeDependentEffect`]. +pub fn refuse_standardised_time_independent_effect_as_standardised_time_dependent_effect( + standardised_time_independent_effect: f64, + standardised_time_dependent_effect: f64, +) -> Result { + let _ = ( + standardised_time_independent_effect, + standardised_time_dependent_effect, + ); + Err(PsychometricError::StandardisedTimeIndependentEffectIsNotStandardisedTimeDependentEffect) +} + +/// Refuse treating the finite-interval intercept-style +/// standardisation as p. 16 `TDPREDEFFECTstd`. +/// +/// `A^{-1}[e^{A Δt} − I] M · √v / √p` treats the Dirac coefficient +/// as an integrated intercept and depends on the event interval. +/// `m · √v / √p` is the continuous coefficient and does not. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::DiscreteStandardisedTimeDependentEffectIsNotStandardisedTimeDependentEffect`]. +pub fn refuse_discrete_standardised_time_dependent_effect_as_standardised_time_dependent_effect( + discrete_standardised_effect: f64, + standardised_effect: f64, +) -> Result { + let _ = (discrete_standardised_effect, standardised_effect); + Err( + PsychometricError::DiscreteStandardisedTimeDependentEffectIsNotStandardisedTimeDependentEffect, + ) +} + +/// Refuse treating `m · √v / √(trait + p + added)` as p. 16 +/// `TDPREDEFFECTstd`. +/// +/// Footnote 4 measurement of the continuous TD effect uses +/// `asymDIFFUSION`, not total variance. `TRAITVAR` is not the +/// standardisation variance. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::TraitContaminatedTimeDependentEffectIsNotStandardisedTimeDependentEffect`]. +pub fn refuse_trait_contaminated_time_dependent_effect_as_standardised_time_dependent_effect( + trait_contaminated_effect: f64, + standardised_effect: f64, +) -> Result { + let _ = (trait_contaminated_effect, standardised_effect); + Err(PsychometricError::TraitContaminatedTimeDependentEffectIsNotStandardisedTimeDependentEffect) +} + /// Exact scalar discrete latent mean from Driver Equation 3. /// /// Driver, Oud, and Voelkle (2017, Eq. 3, p. 4; Table 2, p. 12; JSS @@ -6891,14 +7090,14 @@ mod tests { recover_standardised_discrete_continuous_intercept, recover_standardised_initial_latent_mean, recover_standardised_initial_latent_variance, recover_standardised_manifest_mean, recover_standardised_manifest_trait_variance, - recover_standardised_trait_variance, recover_stationary_initial_latent_mean, - recover_stationary_initial_latent_variance, recover_stationary_initial_observed_mean, - recover_stationary_initial_observed_variance, recover_stationary_lagged_latent_covariance, - recover_stationary_lagged_observed_covariance, recover_stationary_latent_variance, - recover_stationary_later_latent_variance, recover_stationary_later_observed_variance, - recover_time_dependent_predictor_impulse, recover_time_dependent_predictor_impulse_carry, - recover_trait_plus_state_lagged_covariance, recover_trait_plus_state_latent_variance, - recover_within_residual_event_time_log_rate, + recover_standardised_time_dependent_predictor_effect, recover_standardised_trait_variance, + recover_stationary_initial_latent_mean, recover_stationary_initial_latent_variance, + recover_stationary_initial_observed_mean, recover_stationary_initial_observed_variance, + recover_stationary_lagged_latent_covariance, recover_stationary_lagged_observed_covariance, + recover_stationary_latent_variance, recover_stationary_later_latent_variance, + recover_stationary_later_observed_variance, recover_time_dependent_predictor_impulse, + recover_time_dependent_predictor_impulse_carry, recover_trait_plus_state_lagged_covariance, + recover_trait_plus_state_latent_variance, recover_within_residual_event_time_log_rate, refuse_after_extra_process_contribution_as_observed_mean, refuse_after_extra_process_latent_mean_as_observed_mean, refuse_asymptotic_continuous_intercept_as_asymptotic_time_independent_effect, @@ -6920,6 +7119,7 @@ mod tests { refuse_continuous_intercept_as_manifest_means, refuse_difference_quotient_as_local_rate, refuse_discrete_standardised_continuous_intercept_as_standardised_asymptotic_continuous_intercept, refuse_discrete_standardised_continuous_intercept_as_standardised_continuous_intercept, + refuse_discrete_standardised_time_dependent_effect_as_standardised_time_dependent_effect, 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, @@ -6988,6 +7188,7 @@ mod tests { refuse_standardised_initial_latent_variance_as_standardised_initial_latent_mean, refuse_standardised_initial_latent_variance_as_standardised_trait_variance, refuse_standardised_manifest_variance_as_standardised_manifest_mean, + refuse_standardised_time_independent_effect_as_standardised_time_dependent_effect, refuse_standardised_time_independent_predictor_variance_as_standardised_asymptotic_diffusion, refuse_standardised_trait_variance_as_standardised_manifest_trait_variance, refuse_stationary_initial_latent_mean_as_asymptotic_continuous_intercept, @@ -7026,6 +7227,7 @@ mod tests { refuse_time_independent_effect_as_time_varying_discrete_effect, refuse_time_independent_observed_mean_as_initial_time_dependent_observed_mean, refuse_time_independent_observed_mean_as_initial_time_independent_observed_mean, + refuse_trait_contaminated_time_dependent_effect_as_standardised_time_dependent_effect, refuse_trait_plus_state_lagged_covariance_as_stationary_lagged_latent_covariance, refuse_trait_scaled_continuous_intercept_as_standardised_continuous_intercept, refuse_trait_variance_as_process_noise, refuse_trait_variance_as_stationary_within_subject, @@ -7038,6 +7240,7 @@ mod tests { refuse_unstandardised_initial_latent_variance_as_standardised_initial_latent_variance, refuse_unstandardised_manifest_mean_as_standardised_manifest_mean, refuse_unstandardised_manifest_trait_variance_as_standardised_manifest_trait_variance, + refuse_unstandardised_time_dependent_effect_as_standardised_time_dependent_effect, refuse_unstandardised_trait_variance_as_standardised_trait_variance, refuse_within_subject_scaled_initial_latent_mean_as_standardised_initial_latent_mean, }; @@ -16353,4 +16556,348 @@ mod tests { Err(PsychometricError::InvalidNumericInput) ); } + + #[allow(clippy::too_many_lines)] + #[test] + fn standardised_time_dependent_effect_recovers_driver_page_sixteen_after_positive_p_and_v() { + // Driver et al. (2017, p. 16 TDPREDEFFECTstd; Table 2; Eq. 3; + // footnote 4): form strictly positive p = −q / (2 a), then + // strictly positive v, then m · √v / √p. Affected variance is + // asymDIFFUSION, not free T0VAR. Affecting variance is + // TDPREDVAR, not TIPREDVAR. 2017-era summary.ctsemFit.R + // comments out TDPREDVAR / TDPREDVARstd and does not form + // TDPREDEFFECTstd. + let coefficient = 0.3_f64; + let predictor_variance = 1.6_f64; + let diffusion = 0.8_f64; + let log_rate = -0.5_f64; + let recovered = recover_standardised_time_dependent_predictor_effect( + coefficient, + predictor_variance, + diffusion, + log_rate, + LagClock::EventTime, + ) + .expect("TDPREDEFFECTstd"); + let stationary = + recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("p"); + let expected = coefficient * predictor_variance.sqrt() / stationary.sqrt(); + assert!((recovered - expected).abs() < 1e-15); + assert!((coefficient - recovered).abs() > 1e-3); + let time_independent = coefficient * predictor_variance.sqrt() / stationary.sqrt(); + assert!((time_independent - recovered).abs() < 1e-15); + let discrete = recover_discrete_time_independent_predictor_effect( + coefficient, + 1.0, + log_rate, + 1.0, + LagClock::EventTime, + ) + .expect("intercept-style discrete TD") + * predictor_variance.sqrt() + / stationary.sqrt(); + assert!((discrete - recovered).abs() > 1e-3); + let later = recover_discrete_time_independent_predictor_effect( + coefficient, + 1.0, + log_rate, + 2.5, + LagClock::EventTime, + ) + .expect("intercept-style discrete TD Δt=2.5") + * predictor_variance.sqrt() + / stationary.sqrt(); + assert!((later - recovered).abs() > 1e-3); + assert!((later - discrete).abs() > 1e-3); + let larger_q = recover_standardised_time_dependent_predictor_effect( + coefficient, + predictor_variance, + 3.2, + log_rate, + LagClock::EventTime, + ) + .expect("larger q"); + assert!((larger_q - recovered).abs() > 1e-3); + assert!(larger_q.abs() < recovered.abs()); + let added = recover_asymptotic_time_independent_predictor_variance( + coefficient, + predictor_variance, + log_rate, + LagClock::EventTime, + ) + .expect("added analog"); + let trait_plus_state = + recover_trait_plus_state_latent_variance(0.5, stationary).expect("trait + p"); + let contaminated = + coefficient * predictor_variance.sqrt() / (trait_plus_state + added).sqrt(); + assert!((contaminated - recovered).abs() > 1e-3); + let zero = recover_standardised_time_dependent_predictor_effect( + 0.0, + predictor_variance, + diffusion, + log_rate, + LagClock::EventTime, + ) + .expect("zero coefficient"); + assert_eq!(zero.to_bits(), 0.0_f64.to_bits()); + let negative = recover_standardised_time_dependent_predictor_effect( + -coefficient, + predictor_variance, + diffusion, + log_rate, + LagClock::EventTime, + ) + .expect("signed coefficient"); + assert!((negative + recovered).abs() < 1e-15); + let equal_ratio = recover_standardised_time_dependent_predictor_effect( + coefficient, + stationary, + diffusion, + log_rate, + LagClock::EventTime, + ) + .expect("v = p"); + assert!((equal_ratio - coefficient).abs() < 1e-15); + let overflow_safe = recover_standardised_time_dependent_predictor_effect( + 1e300, + 1e40, + 1e40, + -0.5, + LagClock::EventTime, + ) + .expect("SD ratio first stays finite"); + assert!((overflow_safe - 1e300).abs() / 1e300 < 1e-15); + let min_subnormal = f64::from_bits(1); + let zero_extreme = recover_standardised_time_dependent_predictor_effect( + 0.0, + f64::MAX, + min_subnormal, + -0.5, + LagClock::EventTime, + ) + .expect("zero coefficient after extreme positive SDs"); + assert_eq!(zero_extreme.to_bits(), 0.0_f64.to_bits()); + let tiny_extreme = recover_standardised_time_dependent_predictor_effect( + 1e-300, + f64::MAX, + min_subnormal, + -0.5, + LagClock::EventTime, + ) + .expect("tiny coefficient after ratio overflow"); + let tiny_p = recover_stationary_latent_variance(min_subnormal, -0.5, LagClock::EventTime) + .expect("subnormal p"); + let tiny_expected = (1e-300_f64.ln() + 0.5 * f64::MAX.ln() - 0.5 * tiny_p.ln()).exp(); + assert!(tiny_extreme.is_finite()); + assert!((tiny_extreme - tiny_expected).abs() / tiny_expected < 1e-12); + assert_eq!( + refuse_unstandardised_time_dependent_effect_as_standardised_time_dependent_effect( + coefficient, + recovered + ), + Err( + PsychometricError::UnstandardisedTimeDependentEffectIsNotStandardisedTimeDependentEffect + ) + ); + assert_eq!( + refuse_unstandardised_time_dependent_effect_as_standardised_time_dependent_effect( + coefficient, + equal_ratio + ), + Err( + PsychometricError::UnstandardisedTimeDependentEffectIsNotStandardisedTimeDependentEffect + ) + ); + assert_eq!( + refuse_standardised_time_independent_effect_as_standardised_time_dependent_effect( + time_independent, + recovered + ), + Err( + PsychometricError::StandardisedTimeIndependentEffectIsNotStandardisedTimeDependentEffect + ) + ); + assert_eq!( + refuse_discrete_standardised_time_dependent_effect_as_standardised_time_dependent_effect( + discrete, + recovered + ), + Err( + PsychometricError::DiscreteStandardisedTimeDependentEffectIsNotStandardisedTimeDependentEffect + ) + ); + assert_eq!( + refuse_trait_contaminated_time_dependent_effect_as_standardised_time_dependent_effect( + contaminated, + recovered + ), + Err( + PsychometricError::TraitContaminatedTimeDependentEffectIsNotStandardisedTimeDependentEffect + ) + ); + } + + #[allow(clippy::too_many_lines)] + #[test] + fn standardised_time_dependent_effect_fails_closed_when_unstandardised_is_defined() { + assert_eq!( + recover_standardised_time_dependent_predictor_effect( + 0.3, + 1.6, + 0.0, + -0.5, + LagClock::EventTime + ), + Err( + PsychometricError::StandardisedTimeDependentEffectRequiresPositiveWithinSubjectVariance + ) + ); + assert_eq!( + recover_standardised_time_dependent_predictor_effect( + 0.3, + 0.0, + 0.8, + -0.5, + LagClock::EventTime + ), + Err( + PsychometricError::StandardisedTimeDependentEffectRequiresPositivePredictorVariance + ) + ); + assert_eq!( + recover_standardised_time_dependent_predictor_effect( + 0.3, + -1.6, + 0.8, + -0.5, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_time_dependent_predictor_effect( + 0.3, + 1.6, + 0.8, + 0.5, + LagClock::EventTime + ), + Err(PsychometricError::StationaryVarianceRequiresStableDrift) + ); + assert_eq!( + recover_standardised_time_dependent_predictor_effect( + 0.3, + 1.6, + 0.8, + -0.5, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_standardised_time_dependent_predictor_effect( + 0.3, + 1.6, + -0.8, + -0.5, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_time_dependent_predictor_effect( + f64::NAN, + 1.6, + 0.8, + -0.5, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_time_dependent_predictor_effect( + 0.3, + f64::NAN, + 0.8, + -0.5, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_time_dependent_predictor_effect( + 0.3, + f64::INFINITY, + 0.8, + -0.5, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_time_dependent_predictor_effect( + 4.0, + 1e308, + 1e-308, + -0.5, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_time_dependent_predictor_effect( + 1.0, + f64::MAX, + f64::from_bits(1), + -0.5, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_time_dependent_predictor_effect( + f64::MAX, + f64::MAX, + f64::from_bits(1), + -0.5, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_time_dependent_predictor_effect( + 0.0, + 0.0, + 0.8, + -0.5, + LagClock::EventTime + ), + Err( + PsychometricError::StandardisedTimeDependentEffectRequiresPositivePredictorVariance + ) + ); + assert_eq!( + recover_standardised_time_dependent_predictor_effect( + 0.0, + 1.6, + 0.0, + -0.5, + LagClock::EventTime + ), + Err( + PsychometricError::StandardisedTimeDependentEffectRequiresPositiveWithinSubjectVariance + ) + ); + assert_eq!( + recover_standardised_time_dependent_predictor_effect( + f64::MAX, + 1.0, + 0.5, + -1.0, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + } } diff --git a/crates/psychometric_core/src/lib.rs b/crates/psychometric_core/src/lib.rs index c081a63f6..1c6b5bb35 100644 --- a/crates/psychometric_core/src/lib.rs +++ b/crates/psychometric_core/src/lib.rs @@ -36,6 +36,10 @@ //! first-occasion map `τ + λ μ_0` is not `E(y_t)`), recovers the //! Driver Eq. 3 fourth-summand impulse `m x` (Table 2 `TDPREDEFFECT` //! is `M`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14), +//! recovers the Driver p. 16 `TDPREDEFFECTstd` `m · √v / √(-q / (2 a))` +//! after strictly positive `asymDIFFUSION` and `TDPREDVAR` (footnote 4; +//! unstandardised `M` is not `TDPREDEFFECTstd`; `TIPREDEFFECTstd` is +//! not `TDPREDEFFECTstd` even when `M = B`), //! recovers the Driver Eq. 5 of that contemporaneous impulse as //! `τ + λ(μ_t + m x)` (`τ + λ μ_t` is not that observed mean; //! `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when @@ -417,6 +421,8 @@ pub use event_time::recover_standardised_manifest_mean; pub use event_time::recover_standardised_manifest_trait_variance; /// Exact scalar p. 16 `MANIFESTVARstd` `θ/...` after strictly positive `MANIFESTVAR`. pub use event_time::recover_standardised_manifest_variance; +/// Exact scalar p. 16 `TDPREDEFFECTstd` `m · √v / √p` after strictly positive `asymDIFFUSION` and `TDPREDVAR`. +pub use event_time::recover_standardised_time_dependent_predictor_effect; /// Exact scalar p. 16 `TRAITVARstd` `trait / trait = 1` after strictly positive `TRAITVAR`. pub use event_time::recover_standardised_trait_variance; @@ -492,6 +498,8 @@ pub use event_time::refuse_difference_quotient_as_local_rate; pub use event_time::refuse_discrete_standardised_continuous_intercept_as_standardised_asymptotic_continuous_intercept; /// Refuse treating p. 16 `discreteCINTstd` as `CINTstd`. pub use event_time::refuse_discrete_standardised_continuous_intercept_as_standardised_continuous_intercept; +/// Refuse treating intercept-style `A^{-1}[e^{A Δt} − I] M · √v / √p` as `TDPREDEFFECTstd`. +pub use event_time::refuse_discrete_standardised_time_dependent_effect_as_standardised_time_dependent_effect; /// Refuse treating evolved `τ + λ μ_t` as the after-t0 extra-process observed mean. pub use event_time::refuse_evolved_observed_mean_as_after_extra_process_observed_mean; /// Refuse treating evolved `τ + λ μ_t` as the extra-process observed mean. @@ -639,6 +647,8 @@ pub use event_time::refuse_standardised_manifest_variance_as_standardised_manife /// Refuse treating observed θ as p. 16 `MANIFESTVARstd`. pub use event_time::refuse_observed_variance_as_standardised_manifest_variance; +/// Refuse treating p. 16 `TIPREDEFFECTstd` as `TDPREDEFFECTstd`. +pub use event_time::refuse_standardised_time_independent_effect_as_standardised_time_dependent_effect; /// Refuse treating p. 16 `TIPREDVARstd` as `asymDIFFUSIONstd`. pub use event_time::refuse_standardised_time_independent_predictor_variance_as_standardised_asymptotic_diffusion; /// Refuse treating p. 16 `TRAITVARstd` as `MANIFESTTRAITVARstd`. @@ -715,6 +725,8 @@ pub use event_time::refuse_time_independent_effect_as_time_varying_discrete_effe pub use event_time::refuse_time_independent_observed_mean_as_initial_time_dependent_observed_mean; /// Refuse treating process-increment `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` as the first-occasion TI-predictor observed mean. pub use event_time::refuse_time_independent_observed_mean_as_initial_time_independent_observed_mean; +/// Refuse treating `m · √v / √(trait + p + added)` as `TDPREDEFFECTstd`. +pub use event_time::refuse_trait_contaminated_time_dependent_effect_as_standardised_time_dependent_effect; /// Refuse treating §4.3 trait-plus-state lagged covariance as lagged stationary `T0VAR`. pub use event_time::refuse_trait_plus_state_lagged_covariance_as_stationary_lagged_latent_covariance; /// Refuse treating `κ / √(trait + p + added)` as `CINTstd`. @@ -743,6 +755,8 @@ pub use event_time::refuse_unstandardised_manifest_mean_as_standardised_manifest pub use event_time::refuse_unstandardised_manifest_trait_variance_as_standardised_manifest_trait_variance; /// Refuse treating unstandardised `MANIFESTVAR` as p. 16 `MANIFESTVARstd`. pub use event_time::refuse_unstandardised_manifest_variance_as_standardised_manifest_variance; +/// Refuse treating unstandardised `TDPREDEFFECT` as p. 16 `TDPREDEFFECTstd`. +pub use event_time::refuse_unstandardised_time_dependent_effect_as_standardised_time_dependent_effect; /// Refuse treating unstandardised `TRAITVAR` as p. 16 `TRAITVARstd`. pub use event_time::refuse_unstandardised_trait_variance_as_standardised_trait_variance; 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 1c0027f44..7dc7fc50a 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -39,14 +39,14 @@ use psychometric_core::{ recover_standardised_discrete_continuous_intercept, recover_standardised_initial_latent_mean, recover_standardised_initial_latent_variance, recover_standardised_manifest_mean, recover_standardised_manifest_trait_variance, recover_standardised_manifest_variance, - recover_standardised_trait_variance, recover_stationary_initial_latent_mean, - recover_stationary_initial_latent_variance, recover_stationary_initial_observed_mean, - recover_stationary_initial_observed_variance, recover_stationary_lagged_latent_covariance, - recover_stationary_lagged_observed_covariance, recover_stationary_latent_variance, - recover_stationary_later_latent_variance, recover_stationary_later_observed_variance, - recover_time_dependent_predictor_impulse, recover_time_dependent_predictor_impulse_carry, - recover_trait_plus_state_lagged_covariance, recover_trait_plus_state_latent_variance, - recover_within_residual_event_time_log_rate, + recover_standardised_time_dependent_predictor_effect, recover_standardised_trait_variance, + recover_stationary_initial_latent_mean, recover_stationary_initial_latent_variance, + recover_stationary_initial_observed_mean, recover_stationary_initial_observed_variance, + recover_stationary_lagged_latent_covariance, recover_stationary_lagged_observed_covariance, + recover_stationary_latent_variance, recover_stationary_later_latent_variance, + recover_stationary_later_observed_variance, recover_time_dependent_predictor_impulse, + recover_time_dependent_predictor_impulse_carry, recover_trait_plus_state_lagged_covariance, + recover_trait_plus_state_latent_variance, recover_within_residual_event_time_log_rate, refuse_after_extra_process_contribution_as_observed_mean, refuse_after_extra_process_latent_mean_as_observed_mean, refuse_asymptotic_continuous_intercept_as_asymptotic_time_independent_effect, @@ -64,6 +64,7 @@ use psychometric_core::{ 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_discrete_standardised_time_dependent_effect_as_standardised_time_dependent_effect, 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, @@ -122,6 +123,7 @@ use psychometric_core::{ refuse_process_noise_as_unconditional_variance, refuse_standardised_initial_latent_variance_as_standardised_trait_variance, refuse_standardised_manifest_trait_variance_as_standardised_manifest_variance, + refuse_standardised_time_independent_effect_as_standardised_time_dependent_effect, refuse_standardised_trait_variance_as_standardised_manifest_trait_variance, refuse_stationary_initial_latent_mean_as_asymptotic_continuous_intercept, refuse_stationary_initial_latent_mean_as_asymptotic_time_independent_effect, @@ -159,11 +161,13 @@ use psychometric_core::{ refuse_time_independent_effect_as_time_varying_discrete_effect, refuse_time_independent_observed_mean_as_initial_time_dependent_observed_mean, refuse_time_independent_observed_mean_as_initial_time_independent_observed_mean, + refuse_trait_contaminated_time_dependent_effect_as_standardised_time_dependent_effect, refuse_trait_plus_state_lagged_covariance_as_stationary_lagged_latent_covariance, refuse_trait_variance_as_process_noise, refuse_trait_variance_as_stationary_within_subject, refuse_unmatched_time_varying_predictor_interval, refuse_unstandardised_manifest_trait_variance_as_standardised_manifest_trait_variance, refuse_unstandardised_manifest_variance_as_standardised_manifest_variance, + refuse_unstandardised_time_dependent_effect_as_standardised_time_dependent_effect, refuse_unstandardised_trait_variance_as_standardised_trait_variance, }; @@ -6540,3 +6544,166 @@ fn manifest_variance_std_clock_path_is_runtime_opaque() { Err(PsychometricError::EventTimeRequired) ); } + +#[test] +#[allow(clippy::too_many_lines)] +fn standardised_time_dependent_effect_recovers_driver_page_sixteen_after_positive_p_and_v() { + let coefficient = 0.3_f64; + let predictor_variance = 1.6_f64; + let diffusion = 0.8_f64; + let log_rate = -0.5_f64; + let recovered = recover_standardised_time_dependent_predictor_effect( + coefficient, + predictor_variance, + diffusion, + log_rate, + LagClock::EventTime, + ) + .expect("TDPREDEFFECTstd"); + let stationary = + recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime).expect("p"); + let expected = coefficient * predictor_variance.sqrt() / stationary.sqrt(); + let error = (recovered - expected).abs(); + assert!( + error < 1e-15, + "Driver et al. (2017, p. 16 TDPREDEFFECTstd): RMSE {error} for m · √v / √p" + ); + let unstd_rmse = (coefficient - expected).abs(); + assert!( + error < unstd_rmse, + "Driver et al. (2017, Table 2): unstandardised TDPREDEFFECT RMSE {unstd_rmse} must exceed TDPREDEFFECTstd RMSE {error}" + ); + let discrete = recover_discrete_time_independent_predictor_effect( + coefficient, + 1.0, + log_rate, + 1.0, + LagClock::EventTime, + ) + .expect("intercept-style discrete") + * predictor_variance.sqrt() + / stationary.sqrt(); + let discrete_rmse = (discrete - expected).abs(); + assert!( + error < discrete_rmse, + "Driver et al. (2017, footnote 4): intercept-style RMSE {discrete_rmse} must exceed TDPREDEFFECTstd RMSE {error}" + ); + let added = recover_asymptotic_time_independent_predictor_variance( + coefficient, + predictor_variance, + log_rate, + LagClock::EventTime, + ) + .expect("added analog"); + let trait_plus_state = + recover_trait_plus_state_latent_variance(0.5, stationary).expect("trait + p"); + let contaminated = coefficient * predictor_variance.sqrt() / (trait_plus_state + added).sqrt(); + let contaminated_rmse = (contaminated - expected).abs(); + assert!( + error < contaminated_rmse, + "Driver et al. (2017, footnote 4): trait-contaminated RMSE {contaminated_rmse} must exceed TDPREDEFFECTstd RMSE {error}" + ); + let larger = recover_standardised_time_dependent_predictor_effect( + coefficient, + predictor_variance, + 3.2, + log_rate, + LagClock::EventTime, + ) + .expect("larger q"); + assert!( + larger.abs() < recovered.abs(), + "Driver et al. (2017, footnote 4): larger q shrinks |TDPREDEFFECTstd|" + ); + let time_independent = coefficient * predictor_variance.sqrt() / stationary.sqrt(); + assert_eq!( + refuse_unstandardised_time_dependent_effect_as_standardised_time_dependent_effect( + coefficient, recovered + ), + Err(PsychometricError::UnstandardisedTimeDependentEffectIsNotStandardisedTimeDependentEffect) + ); + assert_eq!( + refuse_standardised_time_independent_effect_as_standardised_time_dependent_effect( + time_independent, + recovered + ), + Err(PsychometricError::StandardisedTimeIndependentEffectIsNotStandardisedTimeDependentEffect) + ); + assert_eq!( + refuse_discrete_standardised_time_dependent_effect_as_standardised_time_dependent_effect( + discrete, recovered + ), + Err(PsychometricError::DiscreteStandardisedTimeDependentEffectIsNotStandardisedTimeDependentEffect) + ); + assert_eq!( + refuse_trait_contaminated_time_dependent_effect_as_standardised_time_dependent_effect( + contaminated, recovered + ), + Err(PsychometricError::TraitContaminatedTimeDependentEffectIsNotStandardisedTimeDependentEffect) + ); +} + +#[test] +fn standardised_time_dependent_effect_refuses_non_event_clocks_and_does_not_keep_zero_q_or_v() { + assert_eq!( + recover_standardised_time_dependent_predictor_effect( + 0.3, + 1.6, + 0.8, + -0.5, + LagClock::AssertionTime + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_standardised_time_dependent_predictor_effect( + 0.3, + 1.6, + 0.8, + -0.5, + LagClock::KnowledgeCutoff + ), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_standardised_time_dependent_predictor_effect( + 0.3, + 1.6, + 0.0, + -0.5, + LagClock::EventTime + ), + Err( + PsychometricError::StandardisedTimeDependentEffectRequiresPositiveWithinSubjectVariance + ) + ); + assert_eq!( + recover_standardised_time_dependent_predictor_effect( + 0.3, + 0.0, + 0.8, + -0.5, + LagClock::EventTime + ), + Err(PsychometricError::StandardisedTimeDependentEffectRequiresPositivePredictorVariance) + ); + assert_eq!( + recover_standardised_time_dependent_predictor_effect( + 0.3, + 1.6, + 0.8, + 0.5, + LagClock::EventTime + ), + Err(PsychometricError::StationaryVarianceRequiresStableDrift) + ); + let zero = recover_standardised_time_dependent_predictor_effect( + 0.0, + 1.6, + 0.8, + -0.5, + LagClock::EventTime, + ) + .expect("zero TDPREDEFFECT"); + assert_eq!(zero.to_bits(), 0.0_f64.to_bits()); +} diff --git a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs index 6ccf7f38b..58cf7c446 100644 --- a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs +++ b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs @@ -34,14 +34,14 @@ use psychometric_core::{ recover_standardised_discrete_continuous_intercept, recover_standardised_initial_latent_mean, recover_standardised_initial_latent_variance, recover_standardised_manifest_mean, recover_standardised_manifest_trait_variance, recover_standardised_manifest_variance, - recover_standardised_trait_variance, recover_stationary_initial_latent_mean, - recover_stationary_initial_latent_variance, recover_stationary_initial_observed_mean, - recover_stationary_initial_observed_variance, recover_stationary_lagged_latent_covariance, - recover_stationary_lagged_observed_covariance, recover_stationary_latent_variance, - recover_stationary_later_latent_variance, recover_stationary_later_observed_variance, - recover_time_dependent_predictor_impulse, recover_time_dependent_predictor_impulse_carry, - recover_trait_plus_state_lagged_covariance, recover_trait_plus_state_latent_variance, - recover_within_residual_event_time_log_rate, + recover_standardised_time_dependent_predictor_effect, recover_standardised_trait_variance, + recover_stationary_initial_latent_mean, recover_stationary_initial_latent_variance, + recover_stationary_initial_observed_mean, recover_stationary_initial_observed_variance, + recover_stationary_lagged_latent_covariance, recover_stationary_lagged_observed_covariance, + recover_stationary_latent_variance, recover_stationary_later_latent_variance, + recover_stationary_later_observed_variance, recover_time_dependent_predictor_impulse, + recover_time_dependent_predictor_impulse_carry, recover_trait_plus_state_lagged_covariance, + recover_trait_plus_state_latent_variance, recover_within_residual_event_time_log_rate, refuse_after_extra_process_contribution_as_observed_mean, refuse_after_extra_process_latent_mean_as_observed_mean, refuse_asymptotic_continuous_intercept_as_asymptotic_time_independent_effect, @@ -63,6 +63,7 @@ use psychometric_core::{ refuse_continuous_intercept_as_manifest_means, refuse_discrete_standardised_continuous_intercept_as_standardised_asymptotic_continuous_intercept, refuse_discrete_standardised_continuous_intercept_as_standardised_continuous_intercept, + refuse_discrete_standardised_time_dependent_effect_as_standardised_time_dependent_effect, 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, @@ -129,6 +130,7 @@ use psychometric_core::{ refuse_standardised_initial_latent_variance_as_standardised_trait_variance, refuse_standardised_manifest_trait_variance_as_standardised_manifest_variance, refuse_standardised_manifest_variance_as_standardised_manifest_mean, + refuse_standardised_time_independent_effect_as_standardised_time_dependent_effect, refuse_standardised_time_independent_predictor_variance_as_standardised_asymptotic_diffusion, refuse_standardised_trait_variance_as_standardised_manifest_trait_variance, refuse_stationary_initial_latent_mean_as_asymptotic_continuous_intercept, @@ -167,6 +169,7 @@ use psychometric_core::{ refuse_time_independent_effect_as_time_varying_discrete_effect, refuse_time_independent_observed_mean_as_initial_time_dependent_observed_mean, refuse_time_independent_observed_mean_as_initial_time_independent_observed_mean, + refuse_trait_contaminated_time_dependent_effect_as_standardised_time_dependent_effect, refuse_trait_plus_state_lagged_covariance_as_stationary_lagged_latent_covariance, refuse_trait_scaled_continuous_intercept_as_standardised_continuous_intercept, refuse_trait_variance_as_process_noise, refuse_trait_variance_as_stationary_within_subject, @@ -179,6 +182,7 @@ use psychometric_core::{ refuse_unstandardised_manifest_mean_as_standardised_manifest_mean, refuse_unstandardised_manifest_trait_variance_as_standardised_manifest_trait_variance, refuse_unstandardised_manifest_variance_as_standardised_manifest_variance, + refuse_unstandardised_time_dependent_effect_as_standardised_time_dependent_effect, refuse_unstandardised_trait_variance_as_standardised_trait_variance, refuse_within_subject_scaled_initial_latent_mean_as_standardised_initial_latent_mean, }; @@ -3782,3 +3786,162 @@ fn standardised_manifest_variance_is_not_unstandardised_traitstd_or_observed_var ) ); } + +#[test] +#[allow(clippy::too_many_lines)] +fn standardised_time_dependent_effect_is_not_unstandardised_or_tipred_or_discrete_or_trait() { + let coefficient = 0.3_f64; + let predictor_variance = 1.6_f64; + let diffusion = 0.8_f64; + let log_rate = -0.5_f64; + let recovered = recover_standardised_time_dependent_predictor_effect( + coefficient, + predictor_variance, + diffusion, + log_rate, + LagClock::EventTime, + ) + .expect("TDPREDEFFECTstd"); + let stationary = + recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime).expect("p"); + let expected = coefficient * predictor_variance.sqrt() / stationary.sqrt(); + assert!( + (recovered - expected).abs() < 1e-15, + "Driver et al. (2017, p. 16 footnote 4): TDPREDEFFECTstd is m · √v / √p" + ); + assert!( + (recovered - coefficient).abs() > 1e-3, + "Driver et al. (2017, Table 2): unstandardised TDPREDEFFECT is not TDPREDEFFECTstd" + ); + let time_independent = coefficient * predictor_variance.sqrt() / stationary.sqrt(); + assert!( + (time_independent - recovered).abs() < 1e-15, + "equal numbers when M = B remain distinct named quantities" + ); + let discrete = recover_discrete_time_independent_predictor_effect( + coefficient, + 1.0, + log_rate, + 1.0, + LagClock::EventTime, + ) + .expect("intercept-style discrete") + * predictor_variance.sqrt() + / stationary.sqrt(); + assert!( + (discrete - recovered).abs() > 1e-3, + "Driver et al. (2017, footnote 4): A^{{-1}}[e^{{A Δt}} − I] M · √v / √p is not TDPREDEFFECTstd" + ); + let added = recover_asymptotic_time_independent_predictor_variance( + coefficient, + predictor_variance, + log_rate, + LagClock::EventTime, + ) + .expect("added analog"); + let trait_plus_state = + recover_trait_plus_state_latent_variance(0.5, stationary).expect("trait + p"); + let contaminated = coefficient * predictor_variance.sqrt() / (trait_plus_state + added).sqrt(); + assert!( + (contaminated - recovered).abs() > 1e-3, + "Driver et al. (2017, footnote 4): m · √v / √(trait + p + added) is not TDPREDEFFECTstd" + ); + let larger_q = recover_standardised_time_dependent_predictor_effect( + coefficient, + predictor_variance, + 3.2, + log_rate, + LagClock::EventTime, + ) + .expect("larger q"); + assert!( + larger_q.abs() < recovered.abs(), + "Driver et al. (2017, footnote 4): larger q shrinks |TDPREDEFFECTstd|" + ); + let zero = recover_standardised_time_dependent_predictor_effect( + 0.0, + predictor_variance, + diffusion, + log_rate, + LagClock::EventTime, + ) + .expect("zero coefficient"); + assert_eq!(zero.to_bits(), 0.0_f64.to_bits()); + assert_eq!( + refuse_unstandardised_time_dependent_effect_as_standardised_time_dependent_effect( + coefficient, recovered + ), + Err( + psychometric_core::PsychometricError::UnstandardisedTimeDependentEffectIsNotStandardisedTimeDependentEffect + ) + ); + assert_eq!( + refuse_standardised_time_independent_effect_as_standardised_time_dependent_effect( + time_independent, + recovered + ), + Err( + psychometric_core::PsychometricError::StandardisedTimeIndependentEffectIsNotStandardisedTimeDependentEffect + ) + ); + assert_eq!( + refuse_discrete_standardised_time_dependent_effect_as_standardised_time_dependent_effect( + discrete, recovered + ), + Err( + psychometric_core::PsychometricError::DiscreteStandardisedTimeDependentEffectIsNotStandardisedTimeDependentEffect + ) + ); + assert_eq!( + refuse_trait_contaminated_time_dependent_effect_as_standardised_time_dependent_effect( + contaminated, recovered + ), + Err( + psychometric_core::PsychometricError::TraitContaminatedTimeDependentEffectIsNotStandardisedTimeDependentEffect + ) + ); + assert_eq!( + recover_standardised_time_dependent_predictor_effect( + coefficient, + predictor_variance, + 0.0, + log_rate, + LagClock::EventTime + ), + Err( + psychometric_core::PsychometricError::StandardisedTimeDependentEffectRequiresPositiveWithinSubjectVariance + ) + ); + assert_eq!( + recover_standardised_time_dependent_predictor_effect( + coefficient, + 0.0, + diffusion, + log_rate, + LagClock::EventTime + ), + Err( + psychometric_core::PsychometricError::StandardisedTimeDependentEffectRequiresPositivePredictorVariance + ) + ); + assert_eq!( + recover_standardised_time_dependent_predictor_effect( + coefficient, + predictor_variance, + diffusion, + 0.5, + LagClock::EventTime + ), + Err(psychometric_core::PsychometricError::StationaryVarianceRequiresStableDrift) + ); + assert_eq!( + recover_standardised_time_dependent_predictor_effect( + coefficient, + predictor_variance, + diffusion, + log_rate, + LagClock::DocumentTime + ), + Err(psychometric_core::PsychometricError::EventTimeRequired) + ); +} diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index ee1e6cf0d..5582fc8e0 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -37,6 +37,7 @@ The executable standardised-initial-variance slice recovers Driver et al. (2017, The executable standardised-asymptotic-diffusion slice recovers Driver et al. (2017, p. 16 `asymDIFFUSIONstd`) as `p / p = 1` after strictly positive `asymDIFFUSION` `p = −q / (2 a)` (footnote 4; 2017-era `summary.ctsemFit.R` `solve(sqrt(diag(asymDIFFUSION))) %&% asymDIFFUSION`; JSS PDF re-opened 2026-08-26T17:20Z). Unstandardised `p` is defined for a zero process and is not that map. `p_0 / p_0 = 1` is the named `T0VARstd` first-occasion correlation and is not `asymDIFFUSIONstd` even when both equal 1. `q / p = −2 a` is the named `DIFFUSIONstd` continuous-diffusion ratio and is not this correlation. `v / v = 1` is the named `TIPREDVARstd` predictor correlation and is not this map even when both equal 1. Zero `q` and `a ≥ 0` fail closed. This is not ctsem estimation. The executable standardised-manifest-trait-variance slice recovers Driver et al. (2017, p. 16 `MANIFESTTRAITVARstd`) as `ψ / ψ = 1` after strictly positive `MANIFESTTRAITVAR` (Table 2 `Ψ_τ`; §7.1, p. 19; footnote 4; 2017-era `summary.ctsemFit.R` `solve(sqrt(diag(MANIFESTTRAITVAR) + ridging)) %&% MANIFESTTRAITVAR`; JSS PDF re-opened 2026-08-27T14:20Z). Unstandardised `ψ` is defined for a zero manifest trait and is not that map. `trait / trait = 1` is the named `TRAITVARstd` process-level correlation and is not `MANIFESTTRAITVARstd` even when both equal 1. `θ` is `MANIFESTVAR` and is not that correlation. `MANIFESTTRAITVAR` does not require `a < 0`. This is not ctsem estimation. The executable standardised-manifest-variance slice recovers Driver et al. (2017, p. 16 `MANIFESTVARstd`) as `θ / θ = 1` after strictly positive `MANIFESTVAR` (Table 2 measurement-error Cholesky; Eq. 5 `ε ~ N(0, Θ)`; footnote 4; 2017-era `summary.ctsemFit.R` `solve(sqrt(diag(MANIFESTVAR) + ridging)) %&% MANIFESTVAR`; default ridge 0; JSS PDF re-opened 2026-08-27T14:25Z). Unstandardised `θ` is defined for a zero residual and is not that map. `ψ / ψ = 1` is `MANIFESTTRAITVARstd` and is not `MANIFESTVARstd` even when both equal 1. `MANIFESTVAR` does not require `a < 0`. +The executable standardised-time-dependent-effect slice recovers Driver et al. (2017, p. 16 `TDPREDEFFECTstd`) as `m · √v / √p` after strictly positive `asymDIFFUSION` `p = −q / (2 a)` and strictly positive `TDPREDVAR` `v` (Table 2 `M`; footnote 4; Eq. 3; 2017-era `summary.ctsemFit.R` comments out `TDPREDVAR` / `TDPREDVARstd` and does not form `TDPREDEFFECTstd`; JSS PDF re-opened 2026-08-30T04:11Z). Unstandardised `M` is defined for a zero coefficient and for zero predictor variance and is not that map. `TIPREDEFFECTstd` `B · √v / √p` is not `TDPREDEFFECTstd` even when `M = B`. `A^{-1}[e^{A Δt} − I] M · √v / √p` depends on the event interval and is not this continuous Dirac coefficient. `m · √v / √(trait + p + added)` uses `TRAITVAR` and is not `TDPREDEFFECTstd`. Zero `q`, zero `v`, and `a ≥ 0` fail closed. This is not ctsem estimation. The executable standardised-trait-variance slice recovers Driver et al. (2017, p. 16 `TRAITVARstd`) as `trait / trait = 1` after strictly positive `TRAITVAR` (Table 2 `φ_ξ`; §7.1; footnote 4; 2017-era `summary.ctsemFit.R` `solve(sqrt(diag(TRAITVAR))) %&% TRAITVAR`; JSS PDF re-opened 2026-08-26T17:45Z). Unlike `T0VARstd` there is no ridge addend. Unstandardised `TRAITVAR` is defined for a zero trait and is not that map. `p_0 / p_0 = 1` is the named `T0VARstd` first-occasion correlation and is not `TRAITVARstd` even when both equal 1. `t0_b² v` is `addedT0TIPREDVAR` and is not this correlation. Zero `TRAITVAR` and a non-event clock fail closed. `TRAITVAR` does not require `a < 0`. This is not ctsem estimation. diff --git a/docs/research/multilevel-event-time-recovery.md b/docs/research/multilevel-event-time-recovery.md index 3701dcb4b..8904b2cd8 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -78,7 +78,7 @@ This slice stays inside `psychometric_core`. It does not add a second invariance 72. refuse treating `λ² t0_b² v` as the latent extra `t0_b² v`, refuse treating `λ² t0_b² v` as first-occasion observed variance `λ² p_0 + θ`, refuse treating `λ² t0_b² v` as Eq. 5 of `addedTIPREDVAR` `λ² (B / a)² v`, and refuse treating `λ² t0_b² v` as `MANIFESTVAR` `θ`; 73. recover the exact scalar Eq. 5 of §7.2 `addedTIPREDVAR` `λ² (B / a)² v` (Driver et al., 2017, Eq. 5, p. 5; Table 2, p. 12; §7.2, pp. 20–21; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T19:23Z; form `(B / a)² v` first, then `(λ extra) λ` with `θ = 0`; a zero loading or zero extra is exactly zero; `v < 0` fails closed; a non-event clock fails closed; `a ≥ 0` with a nonzero extra fails closed); 74. refuse treating `λ² (B / a)² v` as the latent extra `(B / a)² v`, refuse treating `λ² (B / a)² v` as Eq. 5 of `addedT0TIPREDVAR` `λ² t0_b² v`, refuse treating `λ² (B / a)² v` as stationary observed variance `λ² p + θ`, and refuse treating `λ² (B / a)² v` as `MANIFESTVAR` `θ`; -75. recover the exact scalar p. 16 `TDPREDEFFECTstd` `m · √v / √(-q / (2 a))` after forming strictly positive `asymDIFFUSION` `−q / (2 a)` and strictly positive time-dependent predictor variance `v` (Driver et al., 2017, p. 16; Table 2, p. 12; Eq. 3, p. 5; footnote 4; JSS PDF re-opened 2026-08-23T21:10Z; form the within-subject variance first, then `v`, then the continuous Dirac coefficient, then the SD ratio; `a ≥ 0`, `q = 0`, and `v = 0` fail closed); +75. recover the exact scalar p. 16 `TDPREDEFFECTstd` `m · √v / √(-q / (2 a))` after forming strictly positive `asymDIFFUSION` `−q / (2 a)` and strictly positive time-dependent predictor variance `v` (Driver et al., 2017, p. 16; Table 2, p. 12; Eq. 3, p. 5; footnote 4; JSS PDF re-opened 2026-08-30T04:11Z; form the within-subject variance first, then `v`, then the continuous Dirac coefficient, then the SD ratio; `a ≥ 0`, `q = 0`, and `v = 0` fail closed); 76. refuse treating unstandardised `TDPREDEFFECT` `M` as `TDPREDEFFECTstd`, refuse treating `TIPREDEFFECTstd` `B · √v / √p` as `TDPREDEFFECTstd` even when `M = B`, refuse treating the finite-interval intercept-style standardisation `A^{-1}[e^{A Δt} − I] M · √v / √p` as `TDPREDEFFECTstd`, refuse treating `m · √v / √(trait + p + added)` as `TDPREDEFFECTstd`, and refuse treating `TRAITVAR` as the footnote 4 standardisation variance; 77. recover the exact scalar Table 3 / p. 16 `T0TDPREDEFFECTstd` `t0_m · √v / √p_0` after forming strictly positive free `T0VAR` `p_0` and strictly positive time-dependent predictor variance `v` (Driver et al., 2017, Table 3, p. 13; Table 2, p. 12; p. 16; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T21:34Z; form free `T0VAR` first, then `v`, then the first-occasion coefficient, then the SD ratio; `p_0 = 0` and `v = 0` fail closed; a non-event clock fails closed; free `T0VAR` does not require `a < 0`); 78. refuse treating unstandardised `T0TDPREDEFFECT` `t0_m` as `T0TDPREDEFFECTstd`, refuse treating `TDPREDEFFECTstd` `m · √v / √(-q / (2 a))` as `T0TDPREDEFFECTstd`, refuse treating `T0TIPREDEFFECTstd` `t0_b · √v / √p_0` as `T0TDPREDEFFECTstd` even when `t0_m = t0_b`, refuse treating `t0_m · √v / √(trait + p_0 + added)` as `T0TDPREDEFFECTstd`, and refuse treating `TRAITVAR` as the footnote 4 standardisation variance; @@ -248,7 +248,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - Driver et al. (2017, Table 3 / p. 16 / 2017-era `addedT0TIPREDVAR`; §7.2; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T18:20Z) recovers a known first-occasion extra TI variance \(t0_b^{2}v\) at machine-scale RMSE, and that RMSE is smaller than treating `addedTIPREDVAR` \((B/a)^{2}v\), `T0TIPREDEFFECTstd` \(t0_b\cdot\sqrt{v}/\sqrt{p_0}\), free \(p_0\), or `TRAITVAR` as `addedT0TIPREDVAR`; doubling \(v\) doubles the extra variance; a signed coefficient yields the same product; a zero coefficient or zero predictor variance is exactly zero; \(v<0\) fails closed; a non-event clock and an overflowing product fail closed; free `T0TIPREDEFFECT` does not require \(a<0\). - Driver et al. (2017, Eq. 5 of 2017-era `addedT0TIPREDVAR`; Table 3 / p. 16; Table 2, p. 12; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T19:10Z) recovers a known extra observed-indicator TI variance \(\lambda^{2}t0_b^{2}v\) at machine-scale RMSE, and that RMSE is smaller than treating the latent extra \(t0_b^{2}v\), first-occasion observed variance \(\lambda^{2}p_0+\theta\), Eq. 5 of `addedTIPREDVAR` \(\lambda^{2}(B/a)^{2}v\), or `MANIFESTVAR` \(\theta\) as that observed extra; doubling \(v\) doubles the extra observed variance; a signed coefficient yields the same product; a zero loading or zero extra is exactly zero; \(v<0\) fails closed; a non-event clock and an overflowing product fail closed; free `T0TIPREDEFFECT` does not require \(a<0\). - Driver et al. (2017, Eq. 5 of §7.2 `addedTIPREDVAR`; Table 2, p. 12; §7.2, pp. 20–21; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T19:23Z) recovers a known extra observed-indicator TI variance \(\lambda^{2}(B/a)^{2}v\) at machine-scale RMSE, and that RMSE is smaller than treating the latent extra \((B/a)^{2}v\), Eq. 5 of `addedT0TIPREDVAR` \(\lambda^{2}t0_b^{2}v\), stationary observed variance \(\lambda^{2}p+\theta\), or `MANIFESTVAR` \(\theta\) as that observed extra; doubling \(v\) doubles the extra observed variance; a signed coefficient yields the same product; a zero loading or zero extra is exactly zero; \(v<0\) fails closed; \(a\ge 0\) with a nonzero extra fails closed; a non-event clock and an overflowing product fail closed. -- Driver et al. (2017, p. 16 `TDPREDEFFECTstd`; Table 2; Eq. 3; footnote 4; JSS PDF re-opened 2026-08-23T21:10Z) recovers a known standardised continuous TD effect \(m\cdot\sqrt{v}/\sqrt{-q/(2a)}\) at machine-scale RMSE, and that RMSE is smaller than treating unstandardised \(M\), intercept-style \(A^{-1}[e^{A\Delta t}-I]M\cdot\sqrt{v}/\sqrt{p}\), or \(m\cdot\sqrt{v}/\sqrt{\mathrm{trait}+p+\mathrm{added}}\) as `TDPREDEFFECTstd`; equal numbers with `TIPREDEFFECTstd` when \(M=B\) remain distinct named quantities; a larger positive \(q\) yields a smaller \(|\mathrm{std}|\); a zero coefficient with positive \(v\) and \(p\) is exactly zero; \(q=0\), \(v=0\), and \(a\ge 0\) fail closed; a non-event clock and an overflowing product fail closed. +- Driver et al. (2017, p. 16 `TDPREDEFFECTstd`; Table 2; Eq. 3; footnote 4; JSS PDF re-opened 2026-08-30T04:11Z) recovers a known standardised continuous TD effect \(m\cdot\sqrt{v}/\sqrt{-q/(2a)}\) at machine-scale RMSE, and that RMSE is smaller than treating unstandardised \(M\), intercept-style \(A^{-1}[e^{A\Delta t}-I]M\cdot\sqrt{v}/\sqrt{p}\), or \(m\cdot\sqrt{v}/\sqrt{\mathrm{trait}+p+\mathrm{added}}\) as `TDPREDEFFECTstd`; equal numbers with `TIPREDEFFECTstd` when \(M=B\) remain distinct named quantities; a larger positive \(q\) yields a smaller \(|\mathrm{std}|\); a zero coefficient with positive \(v\) and \(p\) is exactly zero; \(q=0\), \(v=0\), and \(a\ge 0\) fail closed; a non-event clock and an overflowing product fail closed. - Driver et al. (2017, Table 3 / p. 16 `T0TDPREDEFFECTstd`; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T21:34Z) recovers a known standardised first-occasion TD effect \(t0_m\cdot\sqrt{v}/\sqrt{p_0}\) at machine-scale RMSE, and that RMSE is smaller than treating unstandardised \(t0_m\), continuous \(m\cdot\sqrt{v}/\sqrt{-q/(2a)}\), or \(t0_m\cdot\sqrt{v}/\sqrt{\mathrm{trait}+p_0+\mathrm{added}}\) as `T0TDPREDEFFECTstd`; equal numbers with `T0TIPREDEFFECTstd` when \(t0_m=t0_b\) remain distinct named quantities; a larger positive \(p_0\) yields a smaller \(|\mathrm{std}|\); a zero coefficient with positive \(v\) and \(p_0\) is exactly zero; \(p_0=0\) and \(v=0\) fail closed; a non-event clock and an overflowing product fail closed; free `T0VAR` does not require \(a<0\). - Driver et al. (2017, Table 2 / p. 16 `T0VARstd`; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-26T07:17Z) recovers the scalar correlation \(p_0/p_0=1\) at machine-scale RMSE after strictly positive free `T0VAR`, and that RMSE is smaller than treating unstandardised \(p_0\), `T0MEANSstd` \(\mu_0/\sqrt{p_0}\), or `asymDIFFUSIONstd` \(p/p=1\) as `T0VARstd`; distinct positive \(p_0\) recover the same 1; equal 1 with `T0MEANSstd` when \(\mu_0=\sqrt{p_0}\) remains a distinct named quantity; equal 1 with `asymDIFFUSIONstd` remains a distinct named quantity; \(p_0=0\) fails closed; a non-event clock fails closed; free `T0VAR` does not require \(a<0\). - Driver et al. (2017, Table 2 / §7.1 / p. 16 `TRAITVARstd`; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T22:21Z) recovers the scalar correlation \(\mathrm{trait}/\mathrm{trait}=1\) at machine-scale RMSE after strictly positive `TRAITVAR`, and that RMSE is smaller than treating unstandardised `TRAITVAR` or `addedT0TIPREDVAR` \(t0_b^{2}v\) as `TRAITVARstd`; distinct positive trait recover the same 1; equal 1 with `T0VARstd` remains a distinct named quantity; `TRAITVAR = 0` fails closed; a non-event clock fails closed; `TRAITVAR` does not require \(a<0\).