diff --git a/CHANGELOG.md b/CHANGELOG.md index 062a69412..61bfafa1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ 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 `discreteDIFFUSIONstd`; Eq. 3–4, pp. 4–5; Table 2, p. 12; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-09-01T19:20Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar standardised discrete diffusion on current main after `0ce16e8` dropped the pre-consolidation code while research notes already named the map (register items 57–58). Page 16 prints discrete-time transformations for a chosen event interval (`discreteDRIFT`, `discreteDIFFUSION`) and, when appropriate, standardised matrices with the suffix `std`. The printed example on p. 16 is `discreteDRIFTstd`, not `discreteDIFFUSIONstd`. Footnote 4 standardises using only the relevant variance, not the total. Table 2 names `DIFFUSION` `Q` the lower-triangular `n.latent × n.latent` Cholesky of diffusion variance/covariance. Process noise is within-subject stochastic input, so that relevant variance is within-subject `asymDIFFUSION` `p = −q / (2 a)`. The 2017-era source forms unstandardised `discreteDIFFUSION` whenever `verbose = TRUE`. That source does not form a `discreteDIFFUSIONstd` matrix; the scalar map is the footnote 4 standardisation of that named discrete diffusion: `Q_Δt / p` after strictly positive `p`. Form strictly positive `p` first, then `Q_Δt` from Equation 3–4, then divide `Q_Δt` by `p`. In the scalar stationary case that ratio equals `1 − exp(2 a Δt)`. Unstandardised `Q_Δt` is defined for growing `a ≥ 0` and for a zero process; standardised discrete diffusion is not. Zero `q` has no positive process SD and fails closed. Lasting `p` requires stable `a < 0`. A non-event clock fails closed. A non-positive event interval fails closed. `q / p = −2 a` is `DIFFUSIONstd` and does not depend on `Δt`. `Q_Δt / (trait + p + added)` uses the total and is not this map when `TRAITVAR` is nonzero. 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. - `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..a18a3ab3f 100644 --- a/crates/psychometric_core/src/error.rs +++ b/crates/psychometric_core/src/error.rs @@ -650,6 +650,24 @@ pub enum PsychometricError { /// `discreteCINTstd`. The asymptotic map is the total change, not /// the finite-interval intercept. AsymptoticStandardisedContinuousInterceptIsNotStandardisedDiscreteContinuousIntercept, + /// Driver p. 16 `discreteDIFFUSIONstd` was requested without a + /// strictly positive `asymDIFFUSION`. Footnote 4 standardises + /// using only the relevant variance; zero `q` has no positive + /// process SD. + StandardisedDiscreteDiffusionRequiresPositiveStationaryVariance, + /// Unstandardised `discreteDIFFUSION` `Q_Δt` was treated as + /// `discreteDIFFUSIONstd`. Unstandardised process noise is + /// defined for growing `a ≥ 0` and for zero diffusion; + /// standardised discrete diffusion is not. + UnstandardisedDiscreteDiffusionIsNotStandardisedDiscreteDiffusion, + /// Driver p. 16 `DIFFUSIONstd` `q / p = −2 a` was treated as + /// `discreteDIFFUSIONstd`. The continuous ratio does not depend + /// on the event interval. + StandardisedContinuousDiffusionIsNotStandardisedDiscreteDiffusion, + /// Driver §7.1 total `Q_Δt / (trait + p + added)` was treated as + /// p. 16 `discreteDIFFUSIONstd`. Footnote 4 uses only + /// within-subject `asymDIFFUSION`, not the total. + TotalVarianceScaledDiscreteDiffusionIsNotStandardisedDiscreteDiffusion, /// Driver p. 16 `asymCINTstd` was requested without a strictly /// positive `asymDIFFUSION`. Footnote 4 standardises using only /// the relevant variance; zero `q` has no positive process SD. @@ -1198,6 +1216,18 @@ impl fmt::Display for PsychometricError { Self::AsymptoticStandardisedContinuousInterceptIsNotStandardisedDiscreteContinuousIntercept => { "asymptotic standardised continuous intercept is not standardised discrete continuous intercept" } + Self::StandardisedDiscreteDiffusionRequiresPositiveStationaryVariance => { + "standardised discrete diffusion requires strictly positive stationary within-subject variance" + } + Self::UnstandardisedDiscreteDiffusionIsNotStandardisedDiscreteDiffusion => { + "unstandardised discrete diffusion is not standardised discrete diffusion" + } + Self::StandardisedContinuousDiffusionIsNotStandardisedDiscreteDiffusion => { + "standardised continuous diffusion is not standardised discrete diffusion" + } + Self::TotalVarianceScaledDiscreteDiffusionIsNotStandardisedDiscreteDiffusion => { + "total-variance scaled discrete diffusion is not standardised discrete diffusion" + } Self::StandardisedAsymptoticContinuousInterceptRequiresPositiveStationaryVariance => { "standardised asymptotic continuous intercept requires strictly positive stationary within-subject variance" } @@ -2027,6 +2057,30 @@ mod tests { ); } + #[test] + fn standardised_discrete_diffusion_boundary_messages_are_stable() { + assert_eq!( + PsychometricError::StandardisedDiscreteDiffusionRequiresPositiveStationaryVariance + .to_string(), + "standardised discrete diffusion requires strictly positive stationary within-subject variance" + ); + assert_eq!( + PsychometricError::UnstandardisedDiscreteDiffusionIsNotStandardisedDiscreteDiffusion + .to_string(), + "unstandardised discrete diffusion is not standardised discrete diffusion" + ); + assert_eq!( + PsychometricError::StandardisedContinuousDiffusionIsNotStandardisedDiscreteDiffusion + .to_string(), + "standardised continuous diffusion is not standardised discrete diffusion" + ); + assert_eq!( + PsychometricError::TotalVarianceScaledDiscreteDiffusionIsNotStandardisedDiscreteDiffusion + .to_string(), + "total-variance scaled discrete diffusion is not standardised discrete diffusion" + ); + } + #[test] fn standardised_asymptotic_continuous_intercept_boundary_messages_are_stable() { assert_eq!( diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index a29bc5c18..8456efdfd 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -2501,6 +2501,137 @@ pub fn refuse_asymptotic_standardised_continuous_intercept_as_standardised_discr ) } +/// Exact scalar p. 16 `discreteDIFFUSIONstd` after strictly positive +/// `asymDIFFUSION`. +/// +/// Driver, Oud, and Voelkle (2017, Eq. 3–4, pp. 4–5; Table 2, p. 12; +/// p. 16; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF +/// re-opened 2026-09-01T19:20Z from +/// ) +/// print discrete-time transformations for a chosen event interval +/// (`discreteDRIFT`, `discreteDIFFUSION`) and, when appropriate, +/// standardised matrices with the suffix `std`. The printed example +/// on p. 16 is `discreteDRIFTstd`, not `discreteDIFFUSIONstd`. +/// Footnote 4: standardisations use only the relevant variance, not +/// the total. Process noise is within-subject stochastic input, so +/// that relevant variance is within-subject `asymDIFFUSION` +/// `p = −q / (2 a)`. The 2017-era `summary.ctsemFit.R` forms +/// unstandardised `discreteDIFFUSION` whenever `verbose = TRUE`. +/// That source does not form a `discreteDIFFUSIONstd` matrix; the +/// scalar map here is the footnote 4 standardisation of that named +/// discrete diffusion: `Q_Δt / p` after strictly positive `p`. Form +/// strictly positive `p` first, then `Q_Δt` from Equation 3–4, then +/// divide `Q_Δt` by `p`. In the scalar stationary case that ratio +/// equals `1 − exp(2 a Δt)`. Unstandardised `Q_Δt` is defined for +/// growing `a ≥ 0` and for zero diffusion; standardised discrete +/// diffusion is not. Zero `q` has no positive process SD and fails +/// closed. Lasting `p` requires stable `a < 0`. A non-event clock +/// fails closed. A non-positive event interval fails closed. +/// `q / p = −2 a` is `DIFFUSIONstd` and does not depend on `Δt`. +/// This crate does not currently export `DIFFUSIONstd`; the refuse +/// still names that quantity. `Q_Δt / (trait + p + added)` uses the +/// total, not `asymDIFFUSION`, and is not this map when `TRAITVAR` +/// is nonzero. 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::StandardisedDiscreteDiffusionRequiresPositiveStationaryVariance`] +/// when `q = 0`, +/// [`PsychometricError::NonPositiveInterval`] when `event_delta` is +/// not strictly positive, and +/// [`PsychometricError::InvalidNumericInput`] when the diffusion or +/// log-rate is non-finite, the diffusion is negative, or the +/// quotient overflows. +pub fn recover_standardised_discrete_diffusion( + continuous_diffusion: f64, + log_rate: f64, + event_delta: f64, + clock: LagClock, +) -> Result { + let stationary = recover_stationary_latent_variance(continuous_diffusion, log_rate, clock)?; + if stationary == 0.0 { + return Err( + PsychometricError::StandardisedDiscreteDiffusionRequiresPositiveStationaryVariance, + ); + } + let process_noise = + recover_discrete_process_noise(continuous_diffusion, log_rate, event_delta, clock)?; + require_finite(process_noise / stationary) +} + +/// Refuse treating unstandardised `discreteDIFFUSION` as p. 16 +/// `discreteDIFFUSIONstd`. +/// +/// Unstandardised `Q_Δt` is defined for growing `a ≥ 0` and for a +/// zero process. Footnote 4 `discreteDIFFUSIONstd` requires +/// strictly positive `asymDIFFUSION`. Equal numbers when `p = 1` +/// are still distinct named quantities. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::UnstandardisedDiscreteDiffusionIsNotStandardisedDiscreteDiffusion`]. +pub fn refuse_unstandardised_discrete_diffusion_as_standardised_discrete_diffusion( + unstandardised_discrete_diffusion: f64, + standardised_discrete_diffusion: f64, +) -> Result { + let _ = ( + unstandardised_discrete_diffusion, + standardised_discrete_diffusion, + ); + Err(PsychometricError::UnstandardisedDiscreteDiffusionIsNotStandardisedDiscreteDiffusion) +} + +/// Refuse treating p. 16 `DIFFUSIONstd` as p. 16 +/// `discreteDIFFUSIONstd`. +/// +/// `q / p = −2 a` does not depend on the event interval. +/// `discreteDIFFUSIONstd` `Q_Δt / p` does. This crate does not +/// currently export `DIFFUSIONstd`; the refuse still names that +/// quantity. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::StandardisedContinuousDiffusionIsNotStandardisedDiscreteDiffusion`]. +pub fn refuse_standardised_continuous_diffusion_as_standardised_discrete_diffusion( + standardised_continuous_diffusion: f64, + standardised_discrete_diffusion: f64, +) -> Result { + let _ = ( + standardised_continuous_diffusion, + standardised_discrete_diffusion, + ); + Err(PsychometricError::StandardisedContinuousDiffusionIsNotStandardisedDiscreteDiffusion) +} + +/// Refuse treating `Q_Δt / (trait + p + added)` as p. 16 +/// `discreteDIFFUSIONstd`. +/// +/// Footnote 4 uses only within-subject `asymDIFFUSION`. The total +/// includes `TRAITVAR` and `addedTIPREDVAR`. Equal numbers when +/// those extras are zero remain distinct named quantities. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::TotalVarianceScaledDiscreteDiffusionIsNotStandardisedDiscreteDiffusion`]. +pub fn refuse_total_variance_scaled_discrete_diffusion_as_standardised_discrete_diffusion( + total_scaled_discrete_diffusion: f64, + standardised_discrete_diffusion: f64, +) -> Result { + let _ = ( + total_scaled_discrete_diffusion, + standardised_discrete_diffusion, + ); + Err(PsychometricError::TotalVarianceScaledDiscreteDiffusionIsNotStandardisedDiscreteDiffusion) +} + /// Exact scalar p. 16 `asymCINTstd` after strictly positive /// `asymDIFFUSION`. /// @@ -6889,16 +7020,16 @@ mod tests { 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_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_diffusion, 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, 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, @@ -6981,6 +7112,7 @@ mod tests { refuse_process_noise_as_unconditional_variance, refuse_standardised_asymptotic_diffusion_as_standardised_initial_latent_variance, refuse_standardised_continuous_diffusion_as_standardised_asymptotic_diffusion, + refuse_standardised_continuous_diffusion_as_standardised_discrete_diffusion, refuse_standardised_continuous_intercept_as_standardised_asymptotic_continuous_intercept, refuse_standardised_continuous_intercept_as_standardised_discrete_continuous_intercept, refuse_standardised_initial_latent_mean_as_standardised_initial_latent_variance, @@ -7026,6 +7158,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_total_variance_scaled_discrete_diffusion_as_standardised_discrete_diffusion, 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 +7167,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_discrete_diffusion_as_standardised_discrete_diffusion, 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, @@ -16207,6 +16341,116 @@ mod tests { ); } + #[test] + fn standardised_discrete_diffusion_recovers_driver_page_sixteen_ratio() { + // Driver et al. (2017, p. 16 discreteDIFFUSIONstd; Table 2; + // footnote 4; Eq. 3–4; 2017-era summary.ctsemFit.R): form + // strictly positive asymDIFFUSION p = −q/(2a), then Q_Δt, + // then Q_Δt / p. Scalar stationary map is 1 − exp(2 a Δt). + // That source does not form a discreteDIFFUSIONstd matrix. + let diffusion = 0.4_f64; + let log_rate = -0.25_f64; + let event_delta = 1.0_f64; + let recovered = recover_standardised_discrete_diffusion( + diffusion, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("discreteDIFFUSIONstd"); + let stationary = + recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + let process_noise = + recover_discrete_process_noise(diffusion, log_rate, event_delta, LagClock::EventTime) + .expect("discreteDIFFUSION"); + let expected = process_noise / stationary; + assert!((recovered - expected).abs() < 1e-15); + let identity = 1.0 - (2.0 * log_rate * event_delta).exp(); + assert!((recovered - identity).abs() < 1e-15); + let later = + recover_standardised_discrete_diffusion(diffusion, log_rate, 2.0, LagClock::EventTime) + .expect("later Δt"); + assert!((later - recovered).abs() > 1e-3); + let continuous_std = -2.0 * log_rate; + assert!((continuous_std - recovered).abs() > 1e-3); + assert!((process_noise - recovered).abs() > 1e-3); + let total = recover_trait_plus_state_latent_variance(0.5, stationary).expect("trait + p"); + let total_scaled = process_noise / total; + assert!((total_scaled - recovered).abs() > 1e-3); + let larger_q = recover_standardised_discrete_diffusion( + 1.6, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("q=1.6"); + assert!((larger_q - recovered).abs() < 1e-15); + assert_eq!( + refuse_unstandardised_discrete_diffusion_as_standardised_discrete_diffusion( + process_noise, + recovered + ), + Err( + PsychometricError::UnstandardisedDiscreteDiffusionIsNotStandardisedDiscreteDiffusion + ) + ); + assert_eq!( + refuse_standardised_continuous_diffusion_as_standardised_discrete_diffusion( + continuous_std, + recovered + ), + Err( + PsychometricError::StandardisedContinuousDiffusionIsNotStandardisedDiscreteDiffusion + ) + ); + assert_eq!( + refuse_total_variance_scaled_discrete_diffusion_as_standardised_discrete_diffusion( + total_scaled, + recovered + ), + Err( + PsychometricError::TotalVarianceScaledDiscreteDiffusionIsNotStandardisedDiscreteDiffusion + ) + ); + } + + #[test] + fn standardised_discrete_diffusion_fails_closed_when_unstandardised_is_defined() { + assert_eq!( + recover_standardised_discrete_diffusion(0.0, -0.25, 1.0, LagClock::EventTime), + Err(PsychometricError::StandardisedDiscreteDiffusionRequiresPositiveStationaryVariance) + ); + assert_eq!( + recover_standardised_discrete_diffusion(0.4, 0.25, 1.0, LagClock::EventTime), + Err(PsychometricError::StationaryVarianceRequiresStableDrift) + ); + assert_eq!( + recover_standardised_discrete_diffusion(0.4, -0.25, 1.0, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_standardised_discrete_diffusion(0.4, -0.25, 0.0, LagClock::EventTime), + Err(PsychometricError::NonPositiveInterval) + ); + assert_eq!( + recover_standardised_discrete_diffusion(-0.4, -0.25, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_discrete_diffusion(f64::NAN, -0.25, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_discrete_diffusion(0.4, f64::NAN, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_discrete_diffusion(f64::INFINITY, -0.25, 1.0, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + } + #[test] fn standardised_asymptotic_continuous_intercept_recovers_driver_page_sixteen_after_positive_p() { diff --git a/crates/psychometric_core/src/lib.rs b/crates/psychometric_core/src/lib.rs index c081a63f6..78dca2c86 100644 --- a/crates/psychometric_core/src/lib.rs +++ b/crates/psychometric_core/src/lib.rs @@ -246,6 +246,19 @@ //! is `TIPREDVARstd` and is not that map even when both equal 1; //! zero `q` fails closed; a non-event clock fails closed; `a ≥ 0` //! fails closed; JSS PDF re-opened 2026-08-26T17:20Z), +//! recovers the Driver p. 16 `discreteDIFFUSIONstd` as +//! `Q_Δt / p = 1 − exp(2 a Δt)` after strictly positive +//! `asymDIFFUSION` `p = −q / (2 a)` (footnote 4 uses only the +//! relevant within-subject variance; 2017-era +//! `summary.ctsemFit.R` forms unstandardised `discreteDIFFUSION` +//! and does not form a `discreteDIFFUSIONstd` matrix; +//! unstandardised `Q_Δt` is defined for growing `a ≥ 0` and for a +//! zero process and is not that map; `q / p = −2 a` is +//! `DIFFUSIONstd` and is not that finite-interval ratio; +//! `Q_Δt / (trait + p + added)` uses the total and is not that map; +//! zero `q` fails closed; a non-event clock fails closed; a +//! non-positive event interval fails closed; `a ≥ 0` fails closed; +//! JSS PDF re-opened 2026-09-01T19:20Z), //! recovers the Driver p. 16 `MANIFESTTRAITVARstd` as `ψ / ψ = 1` //! after strictly positive `MANIFESTTRAITVAR` (Table 2 names //! `MANIFESTTRAITVAR` `Ψ_τ`; §7.1 names manifest traits stable @@ -407,6 +420,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 `discreteDIFFUSIONstd` `Q_Δt / p = 1 − exp(2 a Δt)` after strictly positive `asymDIFFUSION`. +pub use event_time::recover_standardised_discrete_diffusion; /// 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`. @@ -620,6 +635,8 @@ pub use event_time::refuse_process_noise_as_unconditional_variance; pub use event_time::refuse_standardised_asymptotic_diffusion_as_standardised_initial_latent_variance; /// Refuse treating p. 16 `DIFFUSIONstd` as `asymDIFFUSIONstd`. pub use event_time::refuse_standardised_continuous_diffusion_as_standardised_asymptotic_diffusion; +/// Refuse treating p. 16 `DIFFUSIONstd` as `discreteDIFFUSIONstd`. +pub use event_time::refuse_standardised_continuous_diffusion_as_standardised_discrete_diffusion; /// Refuse treating p. 16 `CINTstd` as `asymCINTstd`. pub use event_time::refuse_standardised_continuous_intercept_as_standardised_asymptotic_continuous_intercept; /// Refuse treating p. 16 `CINTstd` as `discreteCINTstd`. @@ -636,6 +653,10 @@ pub use event_time::refuse_standardised_initial_latent_variance_as_standardised_ pub use event_time::refuse_standardised_manifest_trait_variance_as_standardised_manifest_variance; /// Refuse treating `MANIFESTVARstd` as `MANIFESTMEANSstd`. pub use event_time::refuse_standardised_manifest_variance_as_standardised_manifest_mean; +/// Refuse treating total-variance scaled `Q_Δt / (trait + p + added)` as `discreteDIFFUSIONstd`. +pub use event_time::refuse_total_variance_scaled_discrete_diffusion_as_standardised_discrete_diffusion; +/// Refuse treating unstandardised `discreteDIFFUSION` as p. 16 `discreteDIFFUSIONstd`. +pub use event_time::refuse_unstandardised_discrete_diffusion_as_standardised_discrete_diffusion; /// Refuse treating observed θ as p. 16 `MANIFESTVARstd`. pub use event_time::refuse_observed_variance_as_standardised_manifest_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..6e7612130 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_discrete_diffusion, + 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_continuous_diffusion_as_standardised_discrete_diffusion, 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_total_variance_scaled_discrete_diffusion_as_standardised_discrete_diffusion, 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_discrete_diffusion_as_standardised_discrete_diffusion, 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, @@ -6352,6 +6355,103 @@ fn standardised_discrete_continuous_intercept_refuses_non_event_clocks_and_does_ assert_eq!(zero.to_bits(), 0.0_f64.to_bits()); } +#[test] +fn standardised_discrete_diffusion_recovers_driver_page_sixteen_ratio() { + let diffusion = 0.4_f64; + let log_rate = -0.25_f64; + let event_delta = 1.0_f64; + let recovered = recover_standardised_discrete_diffusion( + diffusion, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("discreteDIFFUSIONstd"); + let identity = 1.0 - (2.0 * log_rate * event_delta).exp(); + let recovered_error = (recovered - identity).abs(); + assert!( + recovered_error < 1e-15, + "Driver et al. (2017, p. 16 discreteDIFFUSIONstd): RMSE {recovered_error} for Q_Δt / p = 1 − exp(2 a Δt)" + ); + let later = + recover_standardised_discrete_diffusion(diffusion, log_rate, 2.0, LagClock::EventTime) + .expect("later Δt"); + assert!( + (later - recovered).abs() > 1e-3, + "Driver et al. (2017, p. 16): a later event interval changes discreteDIFFUSIONstd" + ); + let continuous_std = -2.0 * log_rate; + let continuous_error = (continuous_std - identity).abs(); + assert!( + continuous_error > recovered_error, + "Driver et al. (2017, p. 16): DIFFUSIONstd RMSE {continuous_error} must exceed discreteDIFFUSIONstd RMSE {recovered_error}" + ); + let process_noise = + recover_discrete_process_noise(diffusion, log_rate, event_delta, LagClock::EventTime) + .expect("discreteDIFFUSION"); + let unstandardised_error = (process_noise - identity).abs(); + assert!( + unstandardised_error > recovered_error, + "Driver et al. (2017, Eq. 3): unstandardised Q_Δt RMSE {unstandardised_error} must exceed discreteDIFFUSIONstd RMSE {recovered_error}" + ); + let stationary = + recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime).expect("p"); + let total = recover_trait_plus_state_latent_variance(0.5, stationary).expect("trait + p"); + let total_scaled = process_noise / total; + let total_error = (total_scaled - identity).abs(); + assert!( + total_error > recovered_error, + "Driver et al. (2017, footnote 4): total-scaled RMSE {total_error} must exceed discreteDIFFUSIONstd RMSE {recovered_error}" + ); + assert_eq!( + refuse_unstandardised_discrete_diffusion_as_standardised_discrete_diffusion( + process_noise, + recovered + ), + Err(PsychometricError::UnstandardisedDiscreteDiffusionIsNotStandardisedDiscreteDiffusion) + ); + assert_eq!( + refuse_standardised_continuous_diffusion_as_standardised_discrete_diffusion( + continuous_std, + recovered + ), + Err(PsychometricError::StandardisedContinuousDiffusionIsNotStandardisedDiscreteDiffusion) + ); + assert_eq!( + refuse_total_variance_scaled_discrete_diffusion_as_standardised_discrete_diffusion( + total_scaled, + recovered + ), + Err( + PsychometricError::TotalVarianceScaledDiscreteDiffusionIsNotStandardisedDiscreteDiffusion + ) + ); +} + +#[test] +fn standardised_discrete_diffusion_refuses_non_event_clocks_and_does_not_keep_zero_q() { + assert_eq!( + recover_standardised_discrete_diffusion(0.4, -0.25, 1.0, LagClock::AssertionTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_standardised_discrete_diffusion(0.4, -0.25, 1.0, LagClock::KnowledgeCutoff), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_standardised_discrete_diffusion(0.0, -0.25, 1.0, LagClock::EventTime), + Err(PsychometricError::StandardisedDiscreteDiffusionRequiresPositiveStationaryVariance) + ); + assert_eq!( + recover_standardised_discrete_diffusion(0.4, 0.25, 1.0, LagClock::EventTime), + Err(PsychometricError::StationaryVarianceRequiresStableDrift) + ); + assert_eq!( + recover_standardised_discrete_diffusion(0.4, -0.25, 0.0, LagClock::EventTime), + Err(PsychometricError::NonPositiveInterval) + ); +} + #[test] fn standardised_asymptotic_continuous_intercept_recovers_driver_page_sixteen_after_positive_p() { let intercept = 0.4_f64; diff --git a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs index 6ccf7f38b..d0897ae5f 100644 --- a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs +++ b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs @@ -31,17 +31,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_discrete_diffusion, + 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, @@ -121,6 +121,7 @@ use psychometric_core::{ refuse_process_noise_as_unconditional_variance, refuse_standardised_asymptotic_diffusion_as_standardised_initial_latent_variance, refuse_standardised_continuous_diffusion_as_standardised_asymptotic_diffusion, + refuse_standardised_continuous_diffusion_as_standardised_discrete_diffusion, refuse_standardised_continuous_intercept_as_standardised_asymptotic_continuous_intercept, refuse_standardised_continuous_intercept_as_standardised_discrete_continuous_intercept, refuse_standardised_initial_latent_mean_as_standardised_initial_latent_variance, @@ -167,6 +168,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_total_variance_scaled_discrete_diffusion_as_standardised_discrete_diffusion, 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 +176,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_discrete_diffusion_as_standardised_discrete_diffusion, 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, @@ -3611,6 +3614,98 @@ fn standardised_discrete_continuous_intercept_is_not_unstandardised_continuous_o ); } +#[test] +fn standardised_discrete_diffusion_is_not_unstandardised_continuous_or_total() { + let diffusion = 0.4_f64; + let log_rate = -0.25_f64; + let event_delta = 1.0_f64; + let recovered = recover_standardised_discrete_diffusion( + diffusion, + log_rate, + event_delta, + LagClock::EventTime, + ) + .expect("discreteDIFFUSIONstd"); + let identity = 1.0 - (2.0 * log_rate * event_delta).exp(); + assert!( + (recovered - identity).abs() < 1e-15, + "Driver et al. (2017, p. 16 footnote 4): discreteDIFFUSIONstd is Q_Δt / p = 1 − exp(2 a Δt)" + ); + let later = + recover_standardised_discrete_diffusion(diffusion, log_rate, 2.0, LagClock::EventTime) + .expect("later Δt"); + assert!( + (later - recovered).abs() > 1e-3, + "Driver et al. (2017, p. 16): a later event interval changes discreteDIFFUSIONstd" + ); + let process_noise = + recover_discrete_process_noise(diffusion, log_rate, event_delta, LagClock::EventTime) + .expect("discreteDIFFUSION"); + assert!( + (process_noise - recovered).abs() > 1e-3, + "Driver et al. (2017, Eq. 3): unstandardised discreteDIFFUSION is not discreteDIFFUSIONstd" + ); + let continuous_std = -2.0 * log_rate; + assert!( + (continuous_std - recovered).abs() > 1e-3, + "Driver et al. (2017, p. 16): DIFFUSIONstd −2a is not discreteDIFFUSIONstd" + ); + let stationary = + recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime).expect("p"); + let total = recover_trait_plus_state_latent_variance(0.5, stationary).expect("trait + p"); + let total_scaled = process_noise / total; + assert!( + (total_scaled - recovered).abs() > 1e-3, + "Driver et al. (2017, footnote 4): Q_Δt / (trait + p + added) is not discreteDIFFUSIONstd" + ); + assert_eq!( + refuse_unstandardised_discrete_diffusion_as_standardised_discrete_diffusion( + process_noise, + recovered + ), + Err( + psychometric_core::PsychometricError::UnstandardisedDiscreteDiffusionIsNotStandardisedDiscreteDiffusion + ) + ); + assert_eq!( + refuse_standardised_continuous_diffusion_as_standardised_discrete_diffusion( + continuous_std, + recovered + ), + Err( + psychometric_core::PsychometricError::StandardisedContinuousDiffusionIsNotStandardisedDiscreteDiffusion + ) + ); + assert_eq!( + refuse_total_variance_scaled_discrete_diffusion_as_standardised_discrete_diffusion( + total_scaled, + recovered + ), + Err( + psychometric_core::PsychometricError::TotalVarianceScaledDiscreteDiffusionIsNotStandardisedDiscreteDiffusion + ) + ); + assert_eq!( + recover_standardised_discrete_diffusion(0.0, log_rate, event_delta, LagClock::EventTime), + Err( + psychometric_core::PsychometricError::StandardisedDiscreteDiffusionRequiresPositiveStationaryVariance + ) + ); + assert_eq!( + recover_standardised_discrete_diffusion( + diffusion, + log_rate, + event_delta, + LagClock::DocumentTime + ), + Err(psychometric_core::PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_standardised_discrete_diffusion(diffusion, 0.25, event_delta, LagClock::EventTime), + Err(psychometric_core::PsychometricError::StationaryVarianceRequiresStableDrift) + ); +} + #[allow(clippy::too_many_lines)] #[test] fn standardised_asymptotic_continuous_intercept_is_not_unstandardised_or_cintstd() { diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index ee1e6cf0d..8708a8589 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -35,6 +35,7 @@ The executable standardised-asymptotic-intercept slice recovers Driver et al. (2 The executable standardised-initial-mean slice recovers Driver et al. (2017, p. 16 `T0MEANSstd`) as `μ_0 / √p_0` after strictly positive free `T0VAR` (footnote 4; JSS PDF re-opened 2026-08-26T04:09Z). Unstandardised `μ_0` is defined for a zero first-occasion variance and is not that map. `p_0 / p_0 = 1` is the named `T0VARstd` correlation form and is not `T0MEANSstd` even when `μ_0 = √p_0`. `μ_0 / √asymDIFFUSION` uses process-dynamics variance and is not the first-occasion map. Free `T0MEANS` does not require `a < 0`. This is not ctsem estimation. The executable standardised-initial-variance slice recovers Driver et al. (2017, p. 16 `T0VARstd`) as `p_0 / p_0 = 1` after strictly positive free `T0VAR` (footnote 4; 2017-era `summary.ctsemFit.R` `solve(sqrt(diag(T0VAR))) %&% T0VAR`; JSS PDF re-opened 2026-08-26T07:17Z). Unstandardised `p_0` is defined for a zero first-occasion variance and is not that map. `μ_0 / √p_0` is the named `T0MEANSstd` first-occasion mean and is not `T0VARstd` even when `μ_0 = √p_0`. `p / p = 1` is the named `asymDIFFUSIONstd` correlation form and is not `T0VARstd` even when both equal 1. Free `T0VAR` does not require `a < 0`. This is not ctsem estimation. 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-discrete-diffusion slice recovers Driver et al. (2017, p. 16 `discreteDIFFUSIONstd`) as `Q_Δt / p = 1 − exp(2 a Δt)` after strictly positive `asymDIFFUSION` `p = −q / (2 a)` (footnote 4; Eq. 3–4; 2017-era `summary.ctsemFit.R` forms unstandardised `discreteDIFFUSION` and does not form a `discreteDIFFUSIONstd` matrix; JSS PDF re-opened 2026-09-01T19:20Z). Unstandardised `Q_Δt` is defined for growing `a ≥ 0` and for a zero process and is not that map. `q / p = −2 a` is `DIFFUSIONstd` and is not this finite-interval ratio. `Q_Δt / (trait + p + added)` uses the total and is not this map. Zero `q`, `a ≥ 0`, and a non-positive event interval 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`. diff --git a/docs/research/multilevel-event-time-recovery.md b/docs/research/multilevel-event-time-recovery.md index 3701dcb4b..c84745c61 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -60,7 +60,7 @@ This slice stays inside `psychometric_core`. It does not add a second invariance 54. recover the exact scalar Eq. 5 of later-start later-occasion §4.3 predetermined `T0VAR` `λ²(trait + e^{2 a s}(e^{2 a u} p_0 + Q_u) + Q_s + (B / a)² v) + θ + ψ` (Driver et al., 2017, Eq. 5, p. 5; Eq. 3–4, pp. 4–5; Table 2, p. 12; JSS PDF re-opened 2026-08-23T11:05Z; form the later-start later-occasion latent variance first, then `λ² p + θ + ψ`; a zero loading is exactly `θ + ψ`) and refuse treating `θ`, the later-start later-occasion latent variance, predetermined later observed variance, later-start lagged observed covariance, or stationary later-occasion observed variance as `Var(y_{t0+u+s})` when `p_0` is free; 55. recover the exact scalar p. 16 `discreteDRIFTstd` `e^{a Δt}` after forming strictly positive `asymDIFFUSION` `−q / (2 a)` (Driver et al., 2017, p. 16; Eq. 3, p. 5; footnote 4; §7.1, pp. 18–19; JSS PDF re-opened 2026-08-23T11:40Z; form the within-subject variance first, then `φ = exp(a Δt)`; scalar stationary SD ratio is 1; `a ≥ 0` and `q = 0` fail closed); 56. refuse treating unstandardised `discreteDRIFT` `e^{a Δt}` as `discreteDRIFTstd`, refuse treating the §7.1 trait-plus-state autocorrelation `(trait + e^{a Δt} p + added) / (trait + p + added)` as `discreteDRIFTstd`, and refuse treating `TRAITVAR` as the footnote 4 standardisation variance; -57. recover the exact scalar p. 16 `discreteDIFFUSIONstd` `Q_Δt / (−q / (2 a))` after forming strictly positive `asymDIFFUSION` `−q / (2 a)` (Driver et al., 2017, p. 16; Eq. 3–4, pp. 4–5; footnote 4; §7.1, pp. 18–19; JSS PDF re-opened 2026-08-23T13:06Z; form the within-subject variance first, then `Q_Δt`, then the ratio; scalar stationary map is `1 − exp(2 a Δt)`; `a ≥ 0` and `q = 0` fail closed); +57. recover the exact scalar p. 16 `discreteDIFFUSIONstd` `Q_Δt / (−q / (2 a))` after forming strictly positive `asymDIFFUSION` `−q / (2 a)` (Driver et al., 2017, p. 16; Eq. 3–4, pp. 4–5; Table 2, p. 12; footnote 4; §7.1, pp. 18–19; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-09-01T19:20Z; that source forms unstandardised `discreteDIFFUSION` and does not form a `discreteDIFFUSIONstd` matrix; form the within-subject variance first, then `Q_Δt`, then the ratio; scalar stationary map is `1 − exp(2 a Δt)`; `a ≥ 0` and `q = 0` fail closed; a non-positive event interval fails closed); 58. refuse treating unstandardised `discreteDIFFUSION` `Q_Δt` as `discreteDIFFUSIONstd`, refuse treating the continuous standardisation `q / (−q / (2 a)) = −2 a` as `discreteDIFFUSIONstd`, refuse treating `Q_Δt / (trait + p + added)` as `discreteDIFFUSIONstd`, and refuse treating `TRAITVAR` as the footnote 4 standardisation variance; 59. recover the exact scalar p. 16 `DIFFUSIONstd` `q / (−q / (2 a)) = −2 a` after forming strictly positive `asymDIFFUSION` `−q / (2 a)` (Driver et al., 2017, p. 16; Eq. 4, p. 5; footnote 4; §7.1, pp. 18–19; JSS PDF re-opened 2026-08-23T13:20Z; form the within-subject variance first, then `q / p`; scalar stationary map is `−2 a` and does not depend on `q` once `q > 0`; `a ≥ 0` and `q = 0` fail closed); 60. refuse treating unstandardised `DIFFUSION` `q` as `DIFFUSIONstd`, refuse treating the discrete standardisation `Q_Δt / (−q / (2 a)) = 1 − exp(2 a Δt)` as `DIFFUSIONstd`, refuse treating `q / (trait + p + added)` as `DIFFUSIONstd`, and refuse treating `TRAITVAR` as the footnote 4 standardisation variance; @@ -241,6 +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 `discreteDIFFUSIONstd`; Eq. 3–4; Table 2; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-09-01T19:20Z) recovers a known discrete standardisation \(Q_{\Delta t}/(-q/(2a))=1-\exp(2a\Delta t)\) at machine-scale RMSE after strictly positive `asymDIFFUSION`, and that RMSE is smaller than treating unstandardised \(Q_{\Delta t}\), continuous \(q/p=-2a\), or \(Q_{\Delta t}/(\mathrm{trait}+p+\mathrm{added})\) as `discreteDIFFUSIONstd`; that 2017-era source does not form a `discreteDIFFUSIONstd` matrix; a later event interval changes the result; distinct positive \(q\) recover the same \(1-\exp(2a\Delta t)\); \(q=0\) and \(a\ge 0\) fail closed; a non-event clock, a non-positive interval, and an overflowing ratio fail closed. - Driver et al. (2017, p. 16 `DRIFTstd`; Eq. 1; footnote 4; JSS PDF re-opened 2026-08-23T13:28Z) recovers a known continuous auto-effect \(a\) after strictly positive `asymDIFFUSION` at machine-scale RMSE, and that RMSE is smaller than treating unstandardised \(a\), discrete \(e^{a\Delta t}\), or \(ap/(\mathrm{trait}+p+\mathrm{added})\) as `DRIFTstd`; distinct positive \(q\) recover the same \(a\); \(q=0\) and \(a\ge 0\) fail closed; a non-event clock fails closed. - Driver et al. (2017, p. 16 `asymTIPREDEFFECTstd`; §7.2; Eq. 3; footnote 4; JSS PDF re-opened 2026-08-23T14:25Z) recovers a known standardised asymptotic TI effect \((-B/a)\cdot\sqrt{v}/\sqrt{-q/(2a)}\) at machine-scale RMSE, and that RMSE is smaller than treating unstandardised \(-B/a\), finite-interval \(A^{-1}[e^{A\Delta t}-I]B\cdot\sqrt{v}/\sqrt{p}\), or \((-B/a)\cdot\sqrt{v}/\sqrt{\mathrm{trait}+p+\mathrm{added}}\) as `asymTIPREDEFFECTstd`; a larger positive \(q\) yields a smaller \(|\mathrm{std}|\); a zero coefficient with positive \(v\) and \(p\) is exactly zero; \(q=0\), \(v=0\), and \(a\ge 0\) fail closed; a non-event clock and an overflowing product fail closed. - Driver et al. (2017, p. 16 `TIPREDEFFECTstd`; §7.2; Eq. 3; footnote 4; JSS PDF re-opened 2026-08-23T16:21Z) recovers a known standardised continuous TI effect \(B\cdot\sqrt{v}/\sqrt{-q/(2a)}\) at machine-scale RMSE, and that RMSE is smaller than treating unstandardised \(B\), asymptotic \((-B/a)\cdot\sqrt{v}/\sqrt{p}\), finite-interval \(A^{-1}[e^{A\Delta t}-I]B\cdot\sqrt{v}/\sqrt{p}\), or \(B\cdot\sqrt{v}/\sqrt{\mathrm{trait}+p+\mathrm{added}}\) as `TIPREDEFFECTstd`; a larger positive \(q\) yields a smaller \(|\mathrm{std}|\); a zero coefficient with positive \(v\) and \(p\) is exactly zero; \(q=0\), \(v=0\), and \(a\ge 0\) fail closed; a non-event clock and an overflowing product fail closed.