From c46931a2ab0ed9486faf382457a3a8c9c84505b9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 00:27:14 +0000 Subject: [PATCH] feat(psychometric): restore Driver p.16 DRIFTstd on main Restores recover_standardised_drift after strictly positive asymDIFFUSION. Scalar SD ratio is 1 so the standardised auto-effect equals unstandardised a numerically; those remain distinct named quantities. Refuses unstandardised a, discreteDRIFTstd, and trait-contaminated a p/(trait+p+added). Independent of #280/#296/#297. Driver, Oud, and Voelkle (2017, p. 16; Eq. 1; footnote 4). JSS PDF re-opened 2026-08-29T00:25Z. --- CHANGELOG.md | 3 + crates/psychometric_core/src/error.rs | 45 ++++ crates/psychometric_core/src/event_time.rs | 204 +++++++++++++++++- crates/psychometric_core/src/lib.rs | 20 ++ ...multilevel_event_time_recovery_contract.rs | 137 +++++++++++- .../scientific_claim_boundary_contract.rs | 88 ++++++-- docs/adr/0005-posterior-esem-dsem.md | 1 + .../multilevel-event-time-recovery.md | 6 +- 8 files changed, 476 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 613f576c6..1b83f7799 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang - **Analysis engine**: deterministic end-to-end analysis-run execution with cutoff-safe eligibility, immutable evidence binding, and reproducibility manifests (`analysis_engine` crate). - **Restore Driver p.16 `MANIFESTVARstd`**: `recover_standardised_manifest_variance` maps `θ / θ = 1` with strictly positive `MANIFESTVAR`, refusing unstandardised manifest-variance, `MANIFESTTRAITVARstd`, and Equation 5 `Var(y)` substitutions (`psychometric_core`). +- **Restore Driver p.16 `DRIFTstd`**: `recover_standardised_drift` maps the continuous auto-effect after strictly positive `asymDIFFUSION`, refusing unstandardised drift, `discreteDRIFTstd`, and trait-contaminated substitutions (`psychometric_core`). - **Posterior network estimator**: cross-draw Pearson correlations in ILR space, jackknife SE and CI, Benjamini–Hochberg FDR edge admission, nonparametric bootstrap stability, greedy modularity consensus clustering (`network_analysis` crate). - **Topic measurement reference estimator**: bounded deterministic CPU `f64` TRSL-TM fitting with ALR/ILR coordinates, Aitchison distance, and lexical-inferential-weight refusal gates (`topic_measurement` crate). - **Psychometric core**: Driver et al. (2017) SDE discrete-time recovery suite including drift, diffusion, T0VAR, TIPRED/TDPRED effects, standardised parameters, trait/state variance decomposition, and observed-indicator mapping — 18 K lines of production Rust with true-parameter RMSE tests. @@ -37,6 +38,8 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang ## [Unreleased] +- `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, p. 16 `DRIFTstd`; Eq. 1, p. 4; footnote 4; Table 2, p. 12; §7.1, pp. 18–19; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-29T00:25Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar standardised continuous `DRIFT` on current main after `b03cc378` restored `MANIFESTVARstd` while research notes already named this map (register items 61–62). Page 16 prints continuous-time parameters (e.g., `DRIFT`, `DIFFUSION`) and, when appropriate, standardised matrices with the suffix `std`. The printed example on p. 16 is `discreteDRIFTstd`, not `DRIFTstd`. Footnote 4 standardises using only the relevant variance, not the total. For `DRIFT` that relevant variance is within-subject `asymDIFFUSION` `p = −q / (2 a)`, because `DRIFT` is intended to represent individual, or average individual, temporal dynamics. The 2017-era source forms `discreteDRIFTstd` as `discreteDRIFT * standardiser` after the `asymDIFFUSION` SD ratio and does **not** form a `DRIFTstd` matrix. The scalar map here is the footnote 4 standardisation of the named continuous auto-effect: form strictly positive `p` first, then `DRIFT * standardiser`. In the scalar stationary case that SD ratio is 1 after strictly positive `p`, so the standardised auto-effect equals the unstandardised log-rate `a` numerically; those remain distinct named quantities. Unstandardised `a` is defined for growing `a ≥ 0` and for zero diffusion; standardised `DRIFT` is not. Zero `q` has no positive SD and fails closed. Lasting `p` requires stable `a < 0`. A non-event clock fails closed. Distinct positive `q` recover the same `a`. `e^{a Δt}` is `discreteDRIFTstd` and depends on the event interval. This crate does not currently export `discreteDRIFTstd`; the refuse still names that quantity. Independent of open `#280` `discreteDRIFTstd`, `#296` `discreteDIFFUSIONstd`, and `#297` `DIFFUSIONstd`. Section 7.1 warns that omitting trait variance confounds between- and within-person information. `a p / (trait + p + added)` uses the total, not `asymDIFFUSION`, and is not `DRIFTstd` when `TRAITVAR` is nonzero. `TRAITVAR` is not the standardisation variance. Meredith (1993) remains unread (web search 2026-08-29T00:25Z: 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 `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..f160a3310 100644 --- a/crates/psychometric_core/src/error.rs +++ b/crates/psychometric_core/src/error.rs @@ -613,6 +613,23 @@ pub enum PsychometricError { /// correlation form of `TIPREDVAR`. StandardisedTimeIndependentPredictorVarianceIsNotStandardisedAsymptoticDiffusion, + /// Driver p. 16 `DRIFTstd` was requested with a non-positive + /// within-subject variance. Footnote 4 standardises continuous + /// `DRIFT` using only strictly positive `asymDIFFUSION`. + StandardisedDriftRequiresPositiveWithinSubjectVariance, + /// Driver p. 16 unstandardised `DRIFT` `a` was treated as + /// `DRIFTstd`. Unstandardised `a` is defined for growing or + /// zero-diffusion processes; standardised `DRIFT` is not. + UnstandardisedDriftIsNotStandardisedDrift, + /// Driver p. 16 `discreteDRIFTstd` `e^{a Δt}` was treated as + /// `DRIFTstd`. The discrete auto-effect depends on the event + /// interval; the continuous auto-effect does not. + StandardisedDiscreteDriftIsNotStandardisedDrift, + /// Driver §7.1 trait-contaminated continuous auto-effect + /// `a p / (trait + p + added)` was treated as p. 16 `DRIFTstd`. + /// Footnote 4 uses only `asymDIFFUSION`, not `TRAITVAR`. + TraitContaminatedDriftIsNotStandardisedDrift, + /// Driver p. 16 `TRAITVARstd` was requested with a non-positive /// trait variance. The 2017-era source skips forming /// `TRAITVARstd` when `TRAITVAR == 0`; footnote 4 @@ -1171,6 +1188,18 @@ impl fmt::Display for PsychometricError { Self::StandardisedTimeIndependentPredictorVarianceIsNotStandardisedAsymptoticDiffusion => { "standardised time-independent predictor variance is not standardised asymptotic diffusion" } + Self::StandardisedDriftRequiresPositiveWithinSubjectVariance => { + "standardised drift requires strictly positive within-subject variance" + } + Self::UnstandardisedDriftIsNotStandardisedDrift => { + "unstandardised drift is not standardised drift" + } + Self::StandardisedDiscreteDriftIsNotStandardisedDrift => { + "standardised discrete drift is not standardised drift" + } + Self::TraitContaminatedDriftIsNotStandardisedDrift => { + "trait-contaminated drift is not standardised drift" + } Self::StandardisedTraitVarianceRequiresPositiveTraitVariance => { "standardised trait variance requires strictly positive trait variance" } @@ -1977,6 +2006,22 @@ mod tests { .to_string(), "standardised time-independent predictor variance is not standardised asymptotic diffusion" ); + assert_eq!( + PsychometricError::StandardisedDriftRequiresPositiveWithinSubjectVariance.to_string(), + "standardised drift requires strictly positive within-subject variance" + ); + assert_eq!( + PsychometricError::UnstandardisedDriftIsNotStandardisedDrift.to_string(), + "unstandardised drift is not standardised drift" + ); + assert_eq!( + PsychometricError::StandardisedDiscreteDriftIsNotStandardisedDrift.to_string(), + "standardised discrete drift is not standardised drift" + ); + assert_eq!( + PsychometricError::TraitContaminatedDriftIsNotStandardisedDrift.to_string(), + "trait-contaminated drift is not standardised drift" + ); } #[test] diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index a29bc5c18..ec4d58262 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -2125,6 +2125,126 @@ pub fn refuse_standardised_continuous_diffusion_as_standardised_asymptotic_diffu Err(PsychometricError::StandardisedContinuousDiffusionIsNotStandardisedAsymptoticDiffusion) } +/// Exact scalar p. 16 `DRIFTstd` after strictly positive +/// `asymDIFFUSION`. +/// +/// Driver, Oud, and Voelkle (2017, p. 16; Eq. 1, p. 4; footnote 4; +/// Table 2, p. 12; §7.1, pp. 18–19; 2017-era ctsem +/// `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-29T00:25Z from +/// ) +/// print continuous-time parameters (e.g., `DRIFT`, `DIFFUSION`) +/// and, when appropriate, standardised matrices with the suffix +/// `std`. The printed example on p. 16 is `discreteDRIFTstd`, not +/// `DRIFTstd`. Footnote 4: standardisations use only the relevant +/// variance, not the total. For `DRIFT` that relevant variance is +/// within-subject `asymDIFFUSION` `p = −q / (2 a)`, because `DRIFT` +/// is intended to represent individual, or average individual, +/// temporal dynamics. The 2017-era `summary.ctsemFit.R` forms +/// `discreteDRIFTstd` whenever `verbose = TRUE`, as +/// `discreteDRIFT * standardiser` after +/// `standardiser <- rep(sqrt(diag(asymDIFFUSION)), each = n.latent) / +/// rep(diag(sqrt(asymDIFFUSION)), times = n.latent)` (comment: "std +/// dev of affecting latent divided by std dev of affected latent"). +/// That source does **not** form a `DRIFTstd` matrix. The scalar map +/// here is the footnote 4 standardisation of the named continuous +/// auto-effect: form strictly positive `p` first, then +/// `DRIFT * standardiser`. In the scalar stationary case that SD +/// ratio is 1 after strictly positive `p`, so the standardised +/// auto-effect equals the unstandardised log-rate `a` numerically; +/// those remain distinct named quantities. Unstandardised `a` is +/// defined for growing `a ≥ 0` and for zero diffusion; standardised +/// `DRIFT` is not. Zero `q` has no positive SD and fails closed. +/// Lasting `p` requires stable `a < 0`. `DRIFT` is an event-time +/// process-dynamics quantity, so a non-event clock fails closed. +/// Distinct positive `q` recover the same `a`. `discreteDRIFTstd` +/// `e^{a Δt}` depends on the event interval and is not this +/// continuous map. This crate does not currently export +/// `discreteDRIFTstd`; the refuse still names that quantity. +/// Section 7.1 warns that omitting trait variance confounds +/// between- and within-person information. +/// `a p / (trait + p + added)` uses the total, not `asymDIFFUSION`, +/// and is not `DRIFTstd` when `TRAITVAR` is nonzero. `TRAITVAR` is +/// not the standardisation variance. 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::StandardisedDriftRequiresPositiveWithinSubjectVariance`] +/// when `q = 0`, and +/// [`PsychometricError::InvalidNumericInput`] when the diffusion or +/// log-rate is non-finite or the diffusion is negative. +pub fn recover_standardised_drift( + 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::StandardisedDriftRequiresPositiveWithinSubjectVariance); + } + Ok(log_rate) +} + +/// Refuse treating unstandardised `DRIFT` as p. 16 `DRIFTstd`. +/// +/// Unstandardised `a` is defined for growing `a ≥ 0` and for a zero +/// process. Footnote 4 `DRIFTstd` requires strictly positive +/// `asymDIFFUSION`. Equal numbers in the scalar stationary case +/// remain distinct named quantities. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::UnstandardisedDriftIsNotStandardisedDrift`]. +pub fn refuse_unstandardised_drift_as_standardised_drift( + unstandardised_drift: f64, + standardised_drift: f64, +) -> Result { + let _ = (unstandardised_drift, standardised_drift); + Err(PsychometricError::UnstandardisedDriftIsNotStandardisedDrift) +} + +/// Refuse treating p. 16 `discreteDRIFTstd` as p. 16 `DRIFTstd`. +/// +/// `e^{a Δt}` depends on the event interval. `DRIFTstd` does not. +/// Equal numbers at a particular `Δt` remain distinct named +/// quantities. This crate does not currently export +/// `discreteDRIFTstd`; the refuse still names that quantity. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::StandardisedDiscreteDriftIsNotStandardisedDrift`]. +pub fn refuse_standardised_discrete_drift_as_standardised_drift( + standardised_discrete_drift: f64, + standardised_drift: f64, +) -> Result { + let _ = (standardised_discrete_drift, standardised_drift); + Err(PsychometricError::StandardisedDiscreteDriftIsNotStandardisedDrift) +} + +/// Refuse treating `a p / (trait + p + added)` as p. 16 `DRIFTstd`. +/// +/// Footnote 4 measurement of the auto-effect uses `asymDIFFUSION`, +/// not total variance. `TRAITVAR` is not the standardisation +/// variance. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::TraitContaminatedDriftIsNotStandardisedDrift`]. +pub fn refuse_trait_contaminated_drift_as_standardised_drift( + trait_contaminated_drift: f64, + standardised_drift: f64, +) -> Result { + let _ = (trait_contaminated_drift, standardised_drift); + Err(PsychometricError::TraitContaminatedDriftIsNotStandardisedDrift) +} + /// Refuse treating p. 16 `TIPREDVARstd` as p. 16 /// `asymDIFFUSIONstd`. /// @@ -6888,7 +7008,7 @@ mod tests { recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, recover_standardised_asymptotic_continuous_intercept, recover_standardised_asymptotic_diffusion, recover_standardised_continuous_intercept, - recover_standardised_discrete_continuous_intercept, + recover_standardised_discrete_continuous_intercept, recover_standardised_drift, 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, @@ -6983,6 +7103,7 @@ mod tests { refuse_standardised_continuous_diffusion_as_standardised_asymptotic_diffusion, refuse_standardised_continuous_intercept_as_standardised_asymptotic_continuous_intercept, refuse_standardised_continuous_intercept_as_standardised_discrete_continuous_intercept, + refuse_standardised_discrete_drift_as_standardised_drift, refuse_standardised_initial_latent_mean_as_standardised_initial_latent_variance, refuse_standardised_initial_latent_variance_as_standardised_asymptotic_diffusion, refuse_standardised_initial_latent_variance_as_standardised_initial_latent_mean, @@ -7026,6 +7147,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_drift_as_standardised_drift, 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, @@ -7034,6 +7156,7 @@ mod tests { refuse_unstandardised_asymptotic_diffusion_as_standardised_asymptotic_diffusion, refuse_unstandardised_continuous_intercept_as_standardised_continuous_intercept, refuse_unstandardised_discrete_continuous_intercept_as_standardised_discrete_continuous_intercept, + refuse_unstandardised_drift_as_standardised_drift, refuse_unstandardised_initial_latent_mean_as_standardised_initial_latent_mean, refuse_unstandardised_initial_latent_variance_as_standardised_initial_latent_variance, refuse_unstandardised_manifest_mean_as_standardised_manifest_mean, @@ -16033,6 +16156,85 @@ mod tests { ); } + #[test] + fn standardised_drift_recovers_driver_page_sixteen_auto_effect_after_positive_asymdiffusion() { + // Driver et al. (2017, p. 16 DRIFTstd; Eq. 1; footnote 4): + // form strictly positive asymDIFFUSION p = −q/(2a), then + // DRIFT * standardiser. Scalar SD ratio is 1, so the + // standardised auto-effect equals a numerically. JSS PDF + // re-opened 2026-08-29T00:25Z. + let diffusion = 0.4_f64; + let log_rate = -0.5_f64; + let recovered = + recover_standardised_drift(diffusion, log_rate, LagClock::EventTime).expect("DRIFTstd"); + assert!((recovered - log_rate).abs() < 1e-15); + let larger_q = + recover_standardised_drift(1.6, log_rate, LagClock::EventTime).expect("DRIFTstd q=1.6"); + assert!((larger_q - recovered).abs() < 1e-15); + let within = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + assert!(within > 0.0); + let discrete = recover_discrete_lag_from_log_rate(log_rate, 1.0, LagClock::EventTime) + .expect("e^{aΔt}"); + assert!((discrete - recovered).abs() > 1e-3); + let trait_variance = 1.0_f64; + let added = 0.1_f64; + let total = recover_trait_plus_state_latent_variance(trait_variance, within) + .expect("trait+state") + + added; + let contaminated = log_rate * within / total; + assert!((contaminated - recovered).abs() > 1e-3); + assert_eq!( + refuse_unstandardised_drift_as_standardised_drift(log_rate, recovered), + Err(PsychometricError::UnstandardisedDriftIsNotStandardisedDrift) + ); + assert_eq!( + refuse_standardised_discrete_drift_as_standardised_drift(discrete, recovered), + Err(PsychometricError::StandardisedDiscreteDriftIsNotStandardisedDrift) + ); + assert_eq!( + refuse_trait_contaminated_drift_as_standardised_drift(contaminated, recovered), + Err(PsychometricError::TraitContaminatedDriftIsNotStandardisedDrift) + ); + } + + #[test] + fn standardised_drift_fails_closed_when_unstandardised_is_defined() { + let log_rate = -0.5_f64; + assert_eq!( + recover_standardised_drift(0.0, log_rate, LagClock::EventTime), + Err(PsychometricError::StandardisedDriftRequiresPositiveWithinSubjectVariance) + ); + assert_eq!( + recover_standardised_drift(0.4, 0.5, LagClock::EventTime), + Err(PsychometricError::StationaryVarianceRequiresStableDrift) + ); + assert_eq!( + recover_standardised_drift(0.4, 0.0, LagClock::EventTime), + Err(PsychometricError::StationaryVarianceRequiresStableDrift) + ); + assert_eq!( + recover_standardised_drift(0.4, log_rate, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_standardised_drift(-0.1, log_rate, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_drift(f64::NAN, log_rate, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_drift(0.4, f64::NAN, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_drift(f64::INFINITY, log_rate, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + } + #[test] fn standardised_manifest_trait_variance_recovers_driver_table_two_after_positive_psi() { // Driver et al. (2017, Table 2 MANIFESTTRAITVAR; §7.1; p. 16 diff --git a/crates/psychometric_core/src/lib.rs b/crates/psychometric_core/src/lib.rs index c081a63f6..279a3abe5 100644 --- a/crates/psychometric_core/src/lib.rs +++ b/crates/psychometric_core/src/lib.rs @@ -261,6 +261,18 @@ //! correlation; zero `MANIFESTTRAITVAR` fails closed; a non-event //! clock fails closed; `MANIFESTTRAITVAR` does not require `a < 0`; //! JSS PDF re-opened 2026-08-27T14:20Z), +//! recovers the Driver p. 16 `DRIFTstd` as the continuous auto-effect +//! after strictly positive `asymDIFFUSION` `p = −q / (2 a)` (footnote 4 +//! uses only the relevant within-subject variance; 2017-era +//! `summary.ctsemFit.R` forms `discreteDRIFTstd` and does not form a +//! `DRIFTstd` matrix; scalar stationary SD ratio is 1 so the +//! standardised auto-effect equals `a` numerically; those remain +//! distinct named quantities; unstandardised `a` is defined for growing +//! `a ≥ 0` and for zero diffusion and is not that map; `e^{a Δt}` is +//! `discreteDRIFTstd` and depends on `Δt`; `a p / (trait + p + added)` +//! uses `TRAITVAR` and is not that map; zero `q` fails closed; a +//! non-event clock fails closed; `a ≥ 0` fails closed; JSS PDF +//! re-opened 2026-08-29T00:25Z), //! and refuses //! latent-mean comparison below strong invariance. @@ -407,6 +419,8 @@ pub use event_time::recover_standardised_asymptotic_diffusion; pub use event_time::recover_standardised_continuous_intercept; /// Exact scalar p. 16 `discreteCINTstd` `A^{-1}[e^{A Δt} − I] κ / √p`. pub use event_time::recover_standardised_discrete_continuous_intercept; +/// Exact scalar p. 16 `DRIFTstd` after strictly positive `asymDIFFUSION`. +pub use event_time::recover_standardised_drift; /// Exact scalar p. 16 `T0MEANSstd` `μ_0 / √p_0`. pub use event_time::recover_standardised_initial_latent_mean; /// Exact scalar p. 16 `T0VARstd` `p_0 / p_0 = 1` after strictly positive free `T0VAR`. @@ -624,6 +638,8 @@ pub use event_time::refuse_standardised_continuous_diffusion_as_standardised_asy pub use event_time::refuse_standardised_continuous_intercept_as_standardised_asymptotic_continuous_intercept; /// Refuse treating p. 16 `CINTstd` as `discreteCINTstd`. pub use event_time::refuse_standardised_continuous_intercept_as_standardised_discrete_continuous_intercept; +/// Refuse treating p. 16 `discreteDRIFTstd` as `DRIFTstd`. +pub use event_time::refuse_standardised_discrete_drift_as_standardised_drift; /// Refuse treating p. 16 `T0MEANSstd` as `T0VARstd`. pub use event_time::refuse_standardised_initial_latent_mean_as_standardised_initial_latent_variance; /// Refuse treating p. 16 `T0VARstd` as `asymDIFFUSIONstd`. @@ -715,6 +731,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 `a p / (trait + p + added)` as p. 16 `DRIFTstd`. +pub use event_time::refuse_trait_contaminated_drift_as_standardised_drift; /// Refuse treating §4.3 trait-plus-state lagged covariance as lagged stationary `T0VAR`. pub use event_time::refuse_trait_plus_state_lagged_covariance_as_stationary_lagged_latent_covariance; /// Refuse treating `κ / √(trait + p + added)` as `CINTstd`. @@ -733,6 +751,8 @@ pub use event_time::refuse_unstandardised_asymptotic_diffusion_as_standardised_a pub use event_time::refuse_unstandardised_continuous_intercept_as_standardised_continuous_intercept; /// Refuse treating unstandardised `discreteCINT` as `discreteCINTstd`. pub use event_time::refuse_unstandardised_discrete_continuous_intercept_as_standardised_discrete_continuous_intercept; +/// Refuse treating unstandardised `DRIFT` as p. 16 `DRIFTstd`. +pub use event_time::refuse_unstandardised_drift_as_standardised_drift; /// Refuse treating unstandardised `T0MEANS` as `T0MEANSstd`. pub use event_time::refuse_unstandardised_initial_latent_mean_as_standardised_initial_latent_mean; /// Refuse treating unstandardised `T0VAR` as `T0VARstd`. 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..e7ece1fa1 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -36,17 +36,17 @@ use psychometric_core::{ recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, recover_standardised_asymptotic_continuous_intercept, recover_standardised_asymptotic_diffusion, recover_standardised_continuous_intercept, - 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_discrete_continuous_intercept, recover_standardised_drift, + 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, 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, @@ -120,6 +120,7 @@ use psychometric_core::{ refuse_observed_variance_as_standardised_manifest_variance, refuse_pooled_discrete_lag_across_unequal_intervals, refuse_process_noise_as_unconditional_variance, + refuse_standardised_discrete_drift_as_standardised_drift, refuse_standardised_initial_latent_variance_as_standardised_trait_variance, refuse_standardised_manifest_trait_variance_as_standardised_manifest_variance, refuse_standardised_trait_variance_as_standardised_manifest_trait_variance, @@ -159,9 +160,11 @@ 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_drift_as_standardised_drift, refuse_trait_plus_state_lagged_covariance_as_stationary_lagged_latent_covariance, refuse_trait_variance_as_process_noise, refuse_trait_variance_as_stationary_within_subject, refuse_unmatched_time_varying_predictor_interval, + refuse_unstandardised_drift_as_standardised_drift, refuse_unstandardised_manifest_trait_variance_as_standardised_manifest_trait_variance, refuse_unstandardised_manifest_variance_as_standardised_manifest_variance, refuse_unstandardised_trait_variance_as_standardised_trait_variance, @@ -6540,3 +6543,115 @@ fn manifest_variance_std_clock_path_is_runtime_opaque() { Err(PsychometricError::EventTimeRequired) ); } + +#[test] +fn standardised_drift_recovers_driver_page_sixteen_auto_effect() { + let diffusion = 0.4_f64; + let log_rate = -0.5_f64; + let recovered = + recover_standardised_drift(diffusion, log_rate, LagClock::EventTime).expect("DRIFTstd"); + let recovered_error = (recovered - log_rate).abs(); + assert!( + recovered_error < 1e-15, + "Driver et al. (2017, p. 16 DRIFTstd): RMSE {recovered_error} for a after positive asymDIFFUSION" + ); + assert_eq!( + recovered.to_bits(), + log_rate.to_bits(), + "Driver et al. (2017, p. 16): scalar SD ratio is 1 so DRIFTstd equals unstandardised a numerically" + ); + let larger_q = + recover_standardised_drift(1.6, log_rate, LagClock::EventTime).expect("DRIFTstd q=1.6"); + assert_eq!( + larger_q.to_bits(), + recovered.to_bits(), + "Driver et al. (2017, p. 16): distinct positive q recover the same DRIFTstd" + ); + let discrete = + recover_discrete_lag_from_log_rate(log_rate, 1.0, LagClock::EventTime).expect("e^{aΔt}"); + let discrete_error = (discrete - log_rate).abs(); + assert!( + recovered_error < discrete_error, + "Driver et al. (2017, p. 16): discrete e^{{aΔt}} RMSE {discrete_error} must exceed DRIFTstd RMSE {recovered_error}" + ); + let within = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + let trait_variance = 1.0_f64; + let added = 0.1_f64; + let total = recover_trait_plus_state_latent_variance(trait_variance, within) + .expect("trait+state") + + added; + let contaminated = log_rate * within / total; + let contaminated_error = (contaminated - log_rate).abs(); + assert!( + recovered_error < contaminated_error, + "Driver et al. (2017, §7.1): trait-contaminated RMSE {contaminated_error} must exceed DRIFTstd RMSE {recovered_error}" + ); + assert_eq!( + refuse_unstandardised_drift_as_standardised_drift(log_rate, recovered), + Err(PsychometricError::UnstandardisedDriftIsNotStandardisedDrift) + ); + assert_eq!( + refuse_standardised_discrete_drift_as_standardised_drift(discrete, recovered), + Err(PsychometricError::StandardisedDiscreteDriftIsNotStandardisedDrift) + ); + assert_eq!( + refuse_trait_contaminated_drift_as_standardised_drift(contaminated, recovered), + Err(PsychometricError::TraitContaminatedDriftIsNotStandardisedDrift) + ); + assert_eq!( + recover_standardised_drift(diffusion, log_rate, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); +} + +#[test] +fn standardised_drift_refuses_non_event_clocks_and_does_not_keep_zero_diffusion() { + assert_eq!( + recover_standardised_drift(0.4, -0.5, LagClock::AssertionTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_standardised_drift(0.4, -0.5, LagClock::KnowledgeCutoff), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_standardised_drift(-0.4, -0.5, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_drift(f64::NAN, -0.5, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_drift(f64::INFINITY, -0.5, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_drift(0.0, -0.5, LagClock::EventTime), + Err(PsychometricError::StandardisedDriftRequiresPositiveWithinSubjectVariance) + ); + assert_eq!( + recover_standardised_drift(0.4, 0.5, LagClock::EventTime), + Err(PsychometricError::StationaryVarianceRequiresStableDrift) + ); + assert_eq!( + recover_standardised_drift(0.4, 0.0, LagClock::EventTime), + Err(PsychometricError::StationaryVarianceRequiresStableDrift) + ); +} + +#[test] +fn drift_std_clock_path_is_runtime_opaque() { + let clocks = [ + LagClock::SystemTime, + LagClock::DocumentTime, + LagClock::AssertionTime, + ]; + let non_event_index = std::process::id() as usize % clocks.len(); + let non_event = clocks[non_event_index]; + assert_eq!( + recover_standardised_drift(0.4, -0.5, non_event), + Err(PsychometricError::EventTimeRequired) + ); +} diff --git a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs index 6ccf7f38b..4b83c867c 100644 --- a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs +++ b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs @@ -6,8 +6,9 @@ use psychometric_core::{ recover_asymptotic_continuous_intercept, recover_asymptotic_time_independent_predictor_effect, recover_asymptotic_time_independent_predictor_variance, recover_cluster_mean_within_between_slopes, recover_discrete_constant_predictor_effect, - recover_discrete_continuous_intercept_effect, recover_discrete_lagged_latent_covariance, - recover_discrete_latent_mean, recover_discrete_latent_mean_with_extra_process, + recover_discrete_continuous_intercept_effect, recover_discrete_lag_from_log_rate, + recover_discrete_lagged_latent_covariance, recover_discrete_latent_mean, + recover_discrete_latent_mean_with_extra_process, recover_discrete_latent_mean_with_extra_process_after, recover_discrete_latent_mean_with_impulse, recover_discrete_latent_mean_with_impulse_carry, recover_discrete_latent_mean_with_initial_time_dependent_predictor, @@ -31,17 +32,17 @@ use psychometric_core::{ recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, recover_standardised_asymptotic_continuous_intercept, recover_standardised_asymptotic_diffusion, recover_standardised_continuous_intercept, - 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_discrete_continuous_intercept, recover_standardised_drift, + 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, 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, @@ -123,6 +124,7 @@ use psychometric_core::{ refuse_standardised_continuous_diffusion_as_standardised_asymptotic_diffusion, refuse_standardised_continuous_intercept_as_standardised_asymptotic_continuous_intercept, refuse_standardised_continuous_intercept_as_standardised_discrete_continuous_intercept, + refuse_standardised_discrete_drift_as_standardised_drift, refuse_standardised_initial_latent_mean_as_standardised_initial_latent_variance, refuse_standardised_initial_latent_variance_as_standardised_asymptotic_diffusion, refuse_standardised_initial_latent_variance_as_standardised_initial_latent_mean, @@ -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_drift_as_standardised_drift, 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, @@ -174,6 +177,7 @@ use psychometric_core::{ refuse_unstandardised_asymptotic_diffusion_as_standardised_asymptotic_diffusion, refuse_unstandardised_continuous_intercept_as_standardised_continuous_intercept, refuse_unstandardised_discrete_continuous_intercept_as_standardised_discrete_continuous_intercept, + refuse_unstandardised_drift_as_standardised_drift, refuse_unstandardised_initial_latent_mean_as_standardised_initial_latent_mean, refuse_unstandardised_initial_latent_variance_as_standardised_initial_latent_variance, refuse_unstandardised_manifest_mean_as_standardised_manifest_mean, @@ -3782,3 +3786,61 @@ fn standardised_manifest_variance_is_not_unstandardised_traitstd_or_observed_var ) ); } + +#[test] +fn standardised_drift_is_not_unstandardised_discrete_or_trait_contaminated() { + let diffusion = 0.4_f64; + let log_rate = -0.5_f64; + let recovered = + recover_standardised_drift(diffusion, log_rate, LagClock::EventTime).expect("DRIFTstd"); + assert!( + (recovered - log_rate).abs() < 1e-15, + "Driver et al. (2017, p. 16 / footnote 4): DRIFTstd equals a after strictly positive asymDIFFUSION" + ); + let larger_q = + recover_standardised_drift(1.6, log_rate, LagClock::EventTime).expect("DRIFTstd q=1.6"); + assert_eq!( + larger_q.to_bits(), + recovered.to_bits(), + "Driver et al. (2017, p. 16): distinct positive q recover the same DRIFTstd" + ); + let discrete = + recover_discrete_lag_from_log_rate(log_rate, 1.0, LagClock::EventTime).expect("e^{aΔt}"); + assert!( + (discrete - recovered).abs() > 1e-3, + "Driver et al. (2017, p. 16): discreteDRIFTstd is not DRIFTstd" + ); + let within = recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + let contaminated = log_rate * within / (1.0 + within + 0.1); + assert!( + (contaminated - recovered).abs() > 1e-3, + "Driver et al. (2017, §7.1): a p / (trait + p + added) is not DRIFTstd" + ); + assert_eq!( + recover_standardised_drift(0.0, log_rate, LagClock::EventTime), + Err( + psychometric_core::PsychometricError::StandardisedDriftRequiresPositiveWithinSubjectVariance + ) + ); + assert_eq!( + recover_standardised_drift(diffusion, log_rate, LagClock::DocumentTime), + Err(psychometric_core::PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_standardised_drift(diffusion, 0.5, LagClock::EventTime), + Err(psychometric_core::PsychometricError::StationaryVarianceRequiresStableDrift) + ); + assert_eq!( + refuse_unstandardised_drift_as_standardised_drift(log_rate, recovered), + Err(psychometric_core::PsychometricError::UnstandardisedDriftIsNotStandardisedDrift) + ); + assert_eq!( + refuse_standardised_discrete_drift_as_standardised_drift(discrete, recovered), + Err(psychometric_core::PsychometricError::StandardisedDiscreteDriftIsNotStandardisedDrift) + ); + assert_eq!( + refuse_trait_contaminated_drift_as_standardised_drift(contaminated, recovered), + Err(psychometric_core::PsychometricError::TraitContaminatedDriftIsNotStandardisedDrift) + ); +} diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index ee1e6cf0d..942fe8567 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-drift slice recovers Driver et al. (2017, p. 16 `DRIFTstd`) as the continuous auto-effect after strictly positive `asymDIFFUSION` `p = −q / (2 a)` (footnote 4; Eq. 1; 2017-era `summary.ctsemFit.R` forms `discreteDRIFTstd` and does not form a `DRIFTstd` matrix; JSS PDF re-opened 2026-08-29T00:25Z). Scalar stationary SD ratio is 1, so the standardised auto-effect equals unstandardised `a` numerically; those remain distinct named quantities. Unstandardised `a` is defined for growing `a ≥ 0` and for zero diffusion and is not that map. `e^{a Δt}` is `discreteDRIFTstd` and depends on the event interval. `a p / (trait + p + added)` uses `TRAITVAR` and is not that map. Zero `q` 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..545a59cd8 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -64,8 +64,8 @@ This slice stays inside `psychometric_core`. It does not add a second invariance 58. refuse treating unstandardised `discreteDIFFUSION` `Q_Δt` as `discreteDIFFUSIONstd`, refuse treating the continuous standardisation `q / (−q / (2 a)) = −2 a` as `discreteDIFFUSIONstd`, refuse treating `Q_Δt / (trait + p + added)` as `discreteDIFFUSIONstd`, and refuse treating `TRAITVAR` as the footnote 4 standardisation variance; 59. recover the exact scalar p. 16 `DIFFUSIONstd` `q / (−q / (2 a)) = −2 a` after forming strictly positive `asymDIFFUSION` `−q / (2 a)` (Driver et al., 2017, p. 16; Eq. 4, p. 5; footnote 4; §7.1, pp. 18–19; JSS PDF re-opened 2026-08-23T13:20Z; form the within-subject variance first, then `q / p`; scalar stationary map is `−2 a` and does not depend on `q` once `q > 0`; `a ≥ 0` and `q = 0` fail closed); 60. refuse treating unstandardised `DIFFUSION` `q` as `DIFFUSIONstd`, refuse treating the discrete standardisation `Q_Δt / (−q / (2 a)) = 1 − exp(2 a Δt)` as `DIFFUSIONstd`, refuse treating `q / (trait + p + added)` as `DIFFUSIONstd`, and refuse treating `TRAITVAR` as the footnote 4 standardisation variance; -61. recover the exact scalar p. 16 `DRIFTstd` after forming strictly positive `asymDIFFUSION` `−q / (2 a)` (Driver et al., 2017, p. 16; Eq. 1, p. 4; footnote 4; §7.1, pp. 18–19; JSS PDF re-opened 2026-08-23T13:28Z; form the within-subject variance first; scalar stationary SD ratio is 1 so the standardised auto-effect equals `a` numerically; those remain distinct named quantities; `a ≥ 0` and `q = 0` fail closed); -62. refuse treating unstandardised `DRIFT` `a` as `DRIFTstd`, refuse treating the discrete standardisation `e^{a Δt}` as `DRIFTstd`, refuse treating `a p / (trait + p + added)` as `DRIFTstd`, and refuse treating `TRAITVAR` as the footnote 4 standardisation variance; +61. recover the exact scalar p. 16 `DRIFTstd` after forming strictly positive `asymDIFFUSION` `−q / (2 a)` (Driver et al., 2017, p. 16; Eq. 1, p. 4; footnote 4; §7.1, pp. 18–19; JSS PDF re-opened 2026-08-29T00:25Z; form the within-subject variance first; scalar stationary SD ratio is 1 so the standardised auto-effect equals `a` numerically; those remain distinct named quantities; `a ≥ 0` and `q = 0` fail closed); +62. refuse treating unstandardised `DRIFT` `a` as `DRIFTstd` even when the numbers are equal, refuse treating the discrete standardisation `e^{a Δt}` as `DRIFTstd`, refuse treating `a p / (trait + p + added)` as `DRIFTstd`, and refuse treating `TRAITVAR` as the footnote 4 standardisation variance; 63. recover the exact scalar p. 16 `asymTIPREDEFFECTstd` `(-B / a) · √v / √(-q / (2 a))` after forming strictly positive `asymDIFFUSION` `−q / (2 a)` and strictly positive predictor variance `v` (Driver et al., 2017, p. 16; §7.2, pp. 20–21; Eq. 3, p. 5; Table 2, p. 12; footnote 4; JSS PDF re-opened 2026-08-23T14:25Z; form the within-subject variance first, then `v`, then the unit asymptotic effect, then the SD ratio; `a ≥ 0`, `q = 0`, and `v = 0` fail closed); 64. refuse treating unstandardised `asymTIPREDEFFECT` `-B / a` as `asymTIPREDEFFECTstd`, refuse treating the finite-interval standardisation `A^{-1}[e^{A Δt} − I] B · √v / √p` as `asymTIPREDEFFECTstd`, refuse treating `(-B / a) · √v / √(trait + p + added)` as `asymTIPREDEFFECTstd`, and refuse treating `TRAITVAR` as the footnote 4 standardisation variance; 65. recover the exact scalar p. 16 `TIPREDEFFECTstd` `B · √v / √(-q / (2 a))` after forming strictly positive `asymDIFFUSION` `−q / (2 a)` and strictly positive predictor variance `v` (Driver et al., 2017, p. 16; §7.2, pp. 20–21; Eq. 3, p. 5; Table 2, p. 12; footnote 4; JSS PDF re-opened 2026-08-23T16:21Z; form the within-subject variance first, then `v`, then the continuous coefficient, then the SD ratio; `a ≥ 0`, `q = 0`, and `v = 0` fail closed); @@ -241,7 +241,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - a singleton cluster is skipped; two singleton clusters yield an empty pair list and fail closed; - overflowing CWC residuals, overflowing contextual subtraction, later-only residual overflow, non-finite intervals, Newton overflow / start-skip / deriv-INF, and Pearson empty/mismatch paths fail closed. - Driver et al. (2017, p. 16 `DIFFUSIONstd`; Eq. 4; footnote 4; JSS PDF re-opened 2026-08-23T13:20Z) recovers a known continuous standardisation \(q/(-q/(2a))=-2a\) at machine-scale RMSE, and that RMSE is smaller than treating unstandardised \(q\), discrete \(Q_{\Delta t}/p\), or \(q/(\mathrm{trait}+p+\mathrm{added})\) as `DIFFUSIONstd`; distinct positive \(q\) recover the same \(-2a\); \(q=0\) and \(a\ge 0\) fail closed; a non-event clock and an overflowing ratio fail closed. -- Driver et al. (2017, p. 16 `DRIFTstd`; Eq. 1; footnote 4; JSS PDF re-opened 2026-08-23T13:28Z) recovers a known continuous auto-effect \(a\) after strictly positive `asymDIFFUSION` at machine-scale RMSE, and that RMSE is smaller than treating unstandardised \(a\), discrete \(e^{a\Delta t}\), or \(ap/(\mathrm{trait}+p+\mathrm{added})\) as `DRIFTstd`; distinct positive \(q\) recover the same \(a\); \(q=0\) and \(a\ge 0\) fail closed; a non-event clock fails closed. +- Driver et al. (2017, p. 16 `DRIFTstd`; Eq. 1; footnote 4; JSS PDF re-opened 2026-08-29T00:25Z) recovers a known continuous auto-effect \(a\) after strictly positive `asymDIFFUSION` at machine-scale RMSE; equal numbers with unstandardised \(a\) remain distinct named quantities; that RMSE is smaller than treating discrete \(e^{a\Delta t}\) or \(ap/(\mathrm{trait}+p+\mathrm{added})\) as `DRIFTstd`; distinct positive \(q\) recover the same \(a\); \(q=0\) and \(a\ge 0\) fail closed; a non-event clock fails closed. - Driver et al. (2017, p. 16 `asymTIPREDEFFECTstd`; §7.2; Eq. 3; footnote 4; JSS PDF re-opened 2026-08-23T14:25Z) recovers a known standardised asymptotic TI effect \((-B/a)\cdot\sqrt{v}/\sqrt{-q/(2a)}\) at machine-scale RMSE, and that RMSE is smaller than treating unstandardised \(-B/a\), finite-interval \(A^{-1}[e^{A\Delta t}-I]B\cdot\sqrt{v}/\sqrt{p}\), or \((-B/a)\cdot\sqrt{v}/\sqrt{\mathrm{trait}+p+\mathrm{added}}\) as `asymTIPREDEFFECTstd`; a larger positive \(q\) yields a smaller \(|\mathrm{std}|\); a zero coefficient with positive \(v\) and \(p\) is exactly zero; \(q=0\), \(v=0\), and \(a\ge 0\) fail closed; a non-event clock and an overflowing product fail closed. - Driver et al. (2017, p. 16 `TIPREDEFFECTstd`; §7.2; Eq. 3; footnote 4; JSS PDF re-opened 2026-08-23T16:21Z) recovers a known standardised continuous TI effect \(B\cdot\sqrt{v}/\sqrt{-q/(2a)}\) at machine-scale RMSE, and that RMSE is smaller than treating unstandardised \(B\), asymptotic \((-B/a)\cdot\sqrt{v}/\sqrt{p}\), finite-interval \(A^{-1}[e^{A\Delta t}-I]B\cdot\sqrt{v}/\sqrt{p}\), or \(B\cdot\sqrt{v}/\sqrt{\mathrm{trait}+p+\mathrm{added}}\) as `TIPREDEFFECTstd`; a larger positive \(q\) yields a smaller \(|\mathrm{std}|\); a zero coefficient with positive \(v\) and \(p\) is exactly zero; \(q=0\), \(v=0\), and \(a\ge 0\) fail closed; a non-event clock and an overflowing product fail closed. - Driver et al. (2017, Table 3 / p. 16 `T0TIPREDEFFECTstd`; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T17:20Z) recovers a known standardised first-occasion TI effect \(t0_b\cdot\sqrt{v}/\sqrt{p_0}\) at machine-scale RMSE, and that RMSE is smaller than treating unstandardised \(t0_b\), continuous \(B\cdot\sqrt{v}/\sqrt{-q/(2a)}\), asymptotic \((-B/a)\cdot\sqrt{v}/\sqrt{p}\), or \(t0_b\cdot\sqrt{v}/\sqrt{\mathrm{trait}+p_0+\mathrm{added}}\) as `T0TIPREDEFFECTstd`; a larger positive \(p_0\) yields a smaller \(|\mathrm{std}|\); a zero coefficient with positive \(v\) and \(p_0\) is exactly zero; \(p_0=0\) and \(v=0\) fail closed; a non-event clock and an overflowing product fail closed; free `T0VAR` does not require \(a<0\).