From ae90cc53cfb3fc1564a6b6d2ee19d231034bdc38 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 17:36:47 +0000 Subject: [PATCH 1/6] feat(psychometric): restore Driver p.16 TRAITVARstd trait/trait=1 on main MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restore recover_standardised_trait_variance on current main after 0ce16e8 dropped the pre-consolidation code while research notes already named the map (register items 81–82). Scalar is trait/trait=1 after strictly positive TRAITVAR. Distinct from T0VARstd and addedT0TIPREDVAR even when T0VARstd equals 1. No ridge addend. --- CHANGELOG.md | 1 + crates/psychometric_core/src/error.rs | 54 +++++ crates/psychometric_core/src/event_time.rs | 196 +++++++++++++++++- crates/psychometric_core/src/lib.rs | 20 ++ ...multilevel_event_time_recovery_contract.rs | 74 ++++++- .../scientific_claim_boundary_contract.rs | 69 +++++- docs/adr/0005-posterior-esem-dsem.md | 1 + .../multilevel-event-time-recovery.md | 4 +- 8 files changed, 412 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aad5f4ee..4e4be5e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang ## [Unreleased] +- `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. - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, p. 16 `T0VARstd`; Table 2, p. 12; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-26T07:17Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar standardised initial latent variance on current main after `0ce16e8` dropped the pre-consolidation code while research notes already named the map (register items 79–80). Page 16 prints standardised matrices with the suffix `std` when appropriate. The printed example on p. 16 is `discreteDRIFTstd`, not `T0VARstd`. Footnote 4 standardises using only the relevant variance, not the total. Table 2 names `T0VAR` the latent process initial variance/covariance. The first-occasion relevant variance is free `T0VAR` `p_0`, not process-dynamics `asymDIFFUSION` `-q / (2 a)`. The 2017-era source forms `T0VARstd` as `solve(sqrt(diag(T0VAR))) %&% T0VAR` when `verbose = TRUE`. OpenMx `%&%` is `t(A) %*% B %*% A`. The default `ridging = FALSE` adds 0, not `0.0001`; that ridge is a numerical hack and is not this exact map. The scalar correlation is `p_0 / p_0 = 1` after strictly positive `p_0`. Form strictly positive `p_0` first, then `1 / √p_0`, then `(1 / √p_0) p_0 (1 / √p_0)`. A zero first-occasion variance has no positive SD and fails closed. `T0` is an event-time occasion, so a non-event clock fails closed. Free `T0VAR` does not require stable `a < 0`. Distinct positive `p_0` recover the same 1. `μ_0 / √p_0` is `T0MEANSstd` and recovers the same number when `μ_0 = √p_0` and remains a distinct named quantity. `p / p = 1` is `asymDIFFUSIONstd` and recovers the same number and remains a distinct named quantity. Meredith (1993) remains unread (Unpaywall 2026-08-26T07:17Z: `is_oa: false`; OpenAlex closed; Springer `content/pdf` is a 3038-byte 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, p. 16 `asymCINTstd`; Eq. 3, p. 4; Table 2, p. 12; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-26T00:20Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar standardised asymptotic continuous intercept on current main after consolidation dropped the pre-consolidation `(-κ / a) / √p` slice. Page 16 prints standardised matrices with the suffix `std` when appropriate, and asymptotic values as `Δt → ∞`. Footnote 4 standardises using only the relevant variance, not the total. Table 2 names `κ` `CINT`. The relevant variance for that process intercept is within-subject `asymDIFFUSION` `p = −q / (2 a)`. The 2017-era source forms unstandardised `asymCINT` whenever `verbose = TRUE` as `-solve(DRIFT) %*% CINT` and does not form an `asymCINTstd` matrix. Form strictly positive `p` first, then the asymptotic intercept, then divide by `√p`. A zero intercept is exactly zero after that positive SD. Zero `q` has no positive process SD and fails closed. Lasting `p` requires stable `a < 0`. A non-event clock fails closed. `κ / √p` is `CINTstd` and is not this total-change map. `A^{-1}[e^{A Δt} − I] κ / √p` is `discreteCINTstd` and depends on the event interval. Meredith (1993) remains unread (Unpaywall 2026-08-25T18:22Z: `is_oa: false`; Springer `content/pdf` is a 3038-byte HTML stub). Mislevy (1991) remains unread on the same terms. 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, p. 16 `T0MEANSstd`; Table 2, p. 12; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-26T04:09Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar standardised initial latent mean on current main after `0ce16e8` dropped the pre-consolidation code while research notes already named the map. Page 16 prints standardised matrices with the suffix `std` when appropriate. Footnote 4 standardises using only the relevant variance, not the total. Table 2 names `T0MEANS` the `n.latent × 1` matrix of latent process means at the first time point `T0` and names `T0VAR` the latent process initial variance/covariance. The first-occasion relevant variance is free `T0VAR` `p_0`, not process-dynamics `asymDIFFUSION` `-q / (2 a)`. The 2017-era source forms unstandardised `T0MEANS` and does not form a `T0MEANSstd` matrix; the scalar map is `μ_0 / √p_0` after strictly positive `p_0`. A zero mean is exactly zero. Zero `p_0` has no positive SD and fails closed. `T0` is an event-time occasion, so a non-event clock fails closed. Free `T0MEANS` does not require stable `a < 0`. `p_0 / p_0 = 1` recovers the same number when `μ_0 = √p_0` and remains a distinct named quantity. `μ_0 / √asymDIFFUSION` uses process-dynamics variance and is not this first-occasion map. Meredith (1993) remains unread (Unpaywall 2026-08-26T00:22Z: `is_oa: false`; OpenAlex closed; Springer `content/pdf` is a 3038-byte HTML stub). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread on the same terms. 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 e83d1a43..1151a48f 100644 --- a/crates/psychometric_core/src/error.rs +++ b/crates/psychometric_core/src/error.rs @@ -585,6 +585,25 @@ pub enum PsychometricError { /// `asymDIFFUSIONstd` is the correlation form of process- /// dynamics `asymDIFFUSION`. StandardisedAsymptoticDiffusionIsNotStandardisedInitialLatentVariance, + /// Driver p. 16 `TRAITVARstd` was requested with a non-positive + /// trait variance. The 2017-era source skips forming + /// `TRAITVARstd` when `TRAITVAR == 0`; footnote 4 + /// standardisation requires strictly positive `TRAITVAR`. + StandardisedTraitVarianceRequiresPositiveTraitVariance, + /// Driver Table 2 unstandardised `TRAITVAR` was treated as + /// p. 16 `TRAITVARstd`. Unstandardised trait variance is + /// defined for a zero trait; standardised `TRAITVAR` is not. + UnstandardisedTraitVarianceIsNotStandardisedTraitVariance, + /// Driver p. 16 `T0VARstd` was treated as p. 16 `TRAITVARstd`. + /// Equal numbers when both correlations equal 1 are still + /// distinct named quantities. `TRAITVARstd` is the correlation + /// form of between-subject `TRAITVAR`; `T0VARstd` is the + /// correlation form of free first-occasion `T0VAR`. + StandardisedInitialLatentVarianceIsNotStandardisedTraitVariance, + /// Driver 2017-era `addedT0TIPREDVAR` `t0_b² v` was treated as + /// p. 16 `TRAITVARstd`. Extra first-occasion TI variance is not + /// the correlation form of between-subject `TRAITVAR`. + InitialTimeIndependentVarianceIsNotStandardisedTraitVariance, /// Driver p. 16 `discreteCINTstd` was requested without a strictly /// positive `asymDIFFUSION`. Footnote 4 standardises using only the /// relevant variance; zero `q` has no positive process SD. @@ -1063,6 +1082,18 @@ impl fmt::Display for PsychometricError { Self::StandardisedAsymptoticDiffusionIsNotStandardisedInitialLatentVariance => { "standardised asymptotic diffusion is not standardised initial latent variance" } + Self::StandardisedTraitVarianceRequiresPositiveTraitVariance => { + "standardised trait variance requires strictly positive trait variance" + } + Self::UnstandardisedTraitVarianceIsNotStandardisedTraitVariance => { + "unstandardised trait variance is not standardised trait variance" + } + Self::StandardisedInitialLatentVarianceIsNotStandardisedTraitVariance => { + "standardised initial latent variance is not standardised trait variance" + } + Self::InitialTimeIndependentVarianceIsNotStandardisedTraitVariance => { + "initial time-independent predictor variance is not standardised trait variance" + } Self::StandardisedDiscreteContinuousInterceptRequiresPositiveStationaryVariance => { "standardised discrete continuous intercept requires strictly positive stationary within-subject variance" } @@ -1802,6 +1833,29 @@ mod tests { ); } + #[test] + fn standardised_trait_variance_boundary_messages_are_stable() { + assert_eq!( + PsychometricError::StandardisedTraitVarianceRequiresPositiveTraitVariance.to_string(), + "standardised trait variance requires strictly positive trait variance" + ); + assert_eq!( + PsychometricError::UnstandardisedTraitVarianceIsNotStandardisedTraitVariance + .to_string(), + "unstandardised trait variance is not standardised trait variance" + ); + assert_eq!( + PsychometricError::StandardisedInitialLatentVarianceIsNotStandardisedTraitVariance + .to_string(), + "standardised initial latent variance is not standardised trait variance" + ); + assert_eq!( + PsychometricError::InitialTimeIndependentVarianceIsNotStandardisedTraitVariance + .to_string(), + "initial time-independent predictor variance is not standardised trait variance" + ); + } + #[test] fn standardised_discrete_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 89492353..4e5fd8c0 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -1760,6 +1760,129 @@ pub fn refuse_standardised_asymptotic_diffusion_as_standardised_initial_latent_v Err(PsychometricError::StandardisedAsymptoticDiffusionIsNotStandardisedInitialLatentVariance) } +/// Exact scalar p. 16 `TRAITVARstd` after strictly positive `TRAITVAR`. +/// +/// Driver, Oud, and Voelkle (2017, Table 2, p. 12; §7.1, pp. 18–19; +/// p. 16; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF +/// re-opened 2026-08-26T17:45Z from +/// ) +/// name `TRAITVAR` `φ_ξ` the latent trait variance/covariance. +/// Table 2 sets it `NULL` when there is no trait variance. +/// Section 7.1 names traits the stable between-subject differences +/// (unit-level unobserved heterogeneity) and estimates `φ_ξ` of the +/// intercepts `ξ` across individuals. Page 16 prints standardised +/// matrices with the suffix `std` when appropriate. The printed +/// example on p. 16 is `discreteDRIFTstd`, not `TRAITVARstd`. +/// Footnote 4: standardisations use 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 +/// `summary.ctsemFit.R` forms `TRAITVARstd` only when +/// `TRAITVAR != 0`, as `solve(sqrt(diag(TRAITVAR))) %&% TRAITVAR` +/// when `verbose = TRUE`. `OpenMx` `%&%` is the quadratic form +/// `t(A) %*% B %*% A`. Unlike `T0VARstd`, that formation uses +/// `diag(diag(TRAITVAR))` and does not add `diag(c(ridging))`. The +/// ridge is a `T0VAR` numerical hack and is not this exact map. The +/// scalar correlation is `trait / trait = 1` after strictly +/// positive `TRAITVAR`. Form strictly positive `trait` first, then +/// `1 / √trait`, then `(1 / √trait) trait (1 / √trait)`. +/// Unstandardised `TRAITVAR` is defined for a zero trait; +/// standardised `TRAITVAR` is not. Zero `TRAITVAR` skips forming +/// `TRAITVARstd` in the 2017-era source and fails closed here. +/// Between-subject variance is an event-time structural quantity, +/// so a non-event clock fails closed. `TRAITVAR` does not require +/// stable `a < 0`. Distinct positive `trait` recover the same 1. +/// `T0VARstd` `p_0 / p_0 = 1` recovers the same number and remains +/// a distinct named quantity. This crate already exports +/// `T0VARstd`. `addedT0TIPREDVAR` `t0_b² v` is extra first-occasion +/// TI variance, not this correlation. This crate does not currently +/// export `addedT0TIPREDVAR`; the refuse still names that quantity. +/// 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::StandardisedTraitVarianceRequiresPositiveTraitVariance`] +/// when `TRAITVAR` is zero, and +/// [`PsychometricError::InvalidNumericInput`] when the variance is +/// non-finite, negative, or the quadratic form overflows. +pub fn recover_standardised_trait_variance( + trait_variance: f64, + clock: LagClock, +) -> Result { + if !clock.admits_structural_lag() { + return Err(PsychometricError::EventTimeRequired); + } + if !trait_variance.is_finite() || trait_variance < 0.0 { + return Err(PsychometricError::InvalidNumericInput); + } + if trait_variance == 0.0 { + return Err(PsychometricError::StandardisedTraitVarianceRequiresPositiveTraitVariance); + } + let process_sd = trait_variance.sqrt(); + let inverse_sd = require_finite(1.0 / process_sd)?; + let scaled = require_finite(inverse_sd * trait_variance)?; + require_finite(scaled * inverse_sd) +} + +/// Refuse treating unstandardised `TRAITVAR` as p. 16 `TRAITVARstd`. +/// +/// Unstandardised `TRAITVAR` is defined for a zero trait. Footnote +/// 4 `TRAITVARstd` requires strictly positive `TRAITVAR`. Equal +/// numbers when `trait = 1` are still distinct named quantities. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::UnstandardisedTraitVarianceIsNotStandardisedTraitVariance`]. +pub fn refuse_unstandardised_trait_variance_as_standardised_trait_variance( + unstandardised_trait_variance: f64, + standardised_trait_variance: f64, +) -> Result { + let _ = (unstandardised_trait_variance, standardised_trait_variance); + Err(PsychometricError::UnstandardisedTraitVarianceIsNotStandardisedTraitVariance) +} + +/// Refuse treating p. 16 `T0VARstd` as p. 16 `TRAITVARstd`. +/// +/// Both scalar correlations equal 1 after strictly positive +/// variances. `T0VARstd` standardises free first-occasion `T0VAR`. +/// `TRAITVARstd` standardises between-subject `TRAITVAR`. Equal +/// numbers remain distinct named quantities. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::StandardisedInitialLatentVarianceIsNotStandardisedTraitVariance`]. +pub fn refuse_standardised_initial_latent_variance_as_standardised_trait_variance( + standardised_initial_variance: f64, + standardised_trait_variance: f64, +) -> Result { + let _ = (standardised_initial_variance, standardised_trait_variance); + Err(PsychometricError::StandardisedInitialLatentVarianceIsNotStandardisedTraitVariance) +} + +/// Refuse treating 2017-era `addedT0TIPREDVAR` as p. 16 `TRAITVARstd`. +/// +/// `t0_b² v` is extra first-occasion TI variance. `TRAITVARstd` is +/// the correlation form of between-subject `TRAITVAR`. Those are +/// not the same map. This crate does not currently export +/// `addedT0TIPREDVAR`; the refuse still names that quantity. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::InitialTimeIndependentVarianceIsNotStandardisedTraitVariance`]. +pub fn refuse_initial_time_independent_variance_as_standardised_trait_variance( + initial_predictor_variance: f64, + standardised_trait_variance: f64, +) -> Result { + let _ = (initial_predictor_variance, standardised_trait_variance); + Err(PsychometricError::InitialTimeIndependentVarianceIsNotStandardisedTraitVariance) +} + /// Exact scalar discrete intercept increment from Driver Equation 3. /// /// Driver, Oud, and Voelkle (2017, Eq. 3, p. 4; Table 2, p. 12; JSS @@ -6321,9 +6444,8 @@ mod tests { recover_standardised_asymptotic_continuous_intercept, 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_initial_latent_mean, recover_standardised_initial_latent_variance, + recover_standardised_manifest_mean, 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, @@ -6391,6 +6513,7 @@ mod tests { refuse_initial_time_independent_effect_as_process_increment, refuse_initial_time_independent_effect_as_time_dependent_impulse, refuse_initial_time_independent_observed_mean_as_initial_time_dependent_observed_mean, + refuse_initial_time_independent_variance_as_standardised_trait_variance, refuse_latent_lagged_covariance_as_observed_covariance, refuse_latent_mean_as_observed_mean, refuse_latent_variance_as_observed_variance, refuse_level_change_extra_process_as_impulse, @@ -6414,6 +6537,7 @@ mod tests { refuse_standardised_continuous_intercept_as_standardised_discrete_continuous_intercept, refuse_standardised_initial_latent_mean_as_standardised_initial_latent_variance, refuse_standardised_initial_latent_variance_as_standardised_initial_latent_mean, + refuse_standardised_initial_latent_variance_as_standardised_trait_variance, refuse_standardised_manifest_variance_as_standardised_manifest_mean, refuse_stationary_initial_latent_mean_as_asymptotic_continuous_intercept, refuse_stationary_initial_latent_mean_as_asymptotic_time_independent_effect, @@ -6461,6 +6585,7 @@ mod tests { 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, + refuse_unstandardised_trait_variance_as_standardised_trait_variance, refuse_within_subject_scaled_initial_latent_mean_as_standardised_initial_latent_mean, }; use crate::error::PsychometricError; @@ -15298,6 +15423,71 @@ mod tests { ); } + #[test] + fn standardised_trait_variance_recovers_driver_table_two_after_positive_traitvar() { + // Driver et al. (2017, Table 2 TRAITVAR; §7.1; p. 16 TRAITVARstd; + // 2017-era summary.ctsemFit.R): form strictly positive TRAITVAR, + // then (1/√trait) trait (1/√trait) = 1. No ridge addend. + let trait_variance = 1.6_f64; + let recovered = recover_standardised_trait_variance(trait_variance, LagClock::EventTime) + .expect("TRAITVARstd"); + assert!((recovered - 1.0).abs() < 1e-15); + let larger_trait = recover_standardised_trait_variance(6.4, LagClock::EventTime) + .expect("TRAITVARstd trait=6.4"); + assert!((larger_trait - recovered).abs() < 1e-15); + let t0var_std = + recover_standardised_initial_latent_variance(trait_variance, LagClock::EventTime) + .expect("T0VARstd"); + assert!((t0var_std - recovered).abs() < 1e-15); + // 2017-era addedT0TIPREDVAR is t0_b² v. This crate does not + // currently export that map; the refuse still names it. + let extra = 0.3_f64 * 0.3_f64 * 4.0_f64; + assert!((extra - recovered).abs() > 1e-3); + assert_eq!( + refuse_unstandardised_trait_variance_as_standardised_trait_variance( + trait_variance, + recovered + ), + Err(PsychometricError::UnstandardisedTraitVarianceIsNotStandardisedTraitVariance) + ); + assert_eq!( + refuse_standardised_initial_latent_variance_as_standardised_trait_variance( + t0var_std, recovered + ), + Err(PsychometricError::StandardisedInitialLatentVarianceIsNotStandardisedTraitVariance) + ); + assert_eq!( + refuse_initial_time_independent_variance_as_standardised_trait_variance( + extra, recovered + ), + Err(PsychometricError::InitialTimeIndependentVarianceIsNotStandardisedTraitVariance) + ); + } + + #[test] + fn standardised_trait_variance_fails_closed_when_unstandardised_is_defined() { + assert_eq!( + recover_standardised_trait_variance(0.0, LagClock::EventTime), + Err(PsychometricError::StandardisedTraitVarianceRequiresPositiveTraitVariance) + ); + assert_eq!( + recover_standardised_trait_variance(1.6, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_standardised_trait_variance(-1.6, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_trait_variance(f64::NAN, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_trait_variance(f64::INFINITY, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + } + #[test] fn standardised_discrete_continuous_intercept_fails_closed_when_unstandardised_is_defined() { assert_eq!( diff --git a/crates/psychometric_core/src/lib.rs b/crates/psychometric_core/src/lib.rs index 2c9c2b0f..7651aab6 100644 --- a/crates/psychometric_core/src/lib.rs +++ b/crates/psychometric_core/src/lib.rs @@ -234,6 +234,18 @@ //! `μ_0 / √p_0` is `T0MEANSstd` and is not that map even when //! `μ_0 = √p_0`; `p / p = 1` is `asymDIFFUSIONstd` and is not that //! map even when both equal 1; JSS PDF re-opened 2026-08-26T07:17Z), +//! recovers the Driver p. 16 `TRAITVARstd` as `trait / trait = 1` +//! after strictly positive `TRAITVAR` (Table 2 names `TRAITVAR` +//! `φ_ξ`; §7.1 names traits the stable between-subject differences; +//! 2017-era `summary.ctsemFit.R` forms `TRAITVARstd` only when +//! `TRAITVAR != 0` as `solve(sqrt(diag(TRAITVAR))) %&% TRAITVAR`; +//! 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 `T0VARstd` and is not that map even when both +//! equal 1; `t0_b² v` is `addedT0TIPREDVAR` and is not that +//! correlation; zero `TRAITVAR` fails closed; a non-event clock +//! fails closed; `TRAITVAR` does not require `a < 0`; JSS PDF +//! re-opened 2026-08-26T17:45Z), //! and refuses //! latent-mean comparison below strong invariance. @@ -384,6 +396,8 @@ pub use event_time::recover_standardised_initial_latent_mean; pub use event_time::recover_standardised_initial_latent_variance; /// Exact scalar p. 16 `MANIFESTMEANSstd` `τ / √θ`. pub use event_time::recover_standardised_manifest_mean; +/// Exact scalar p. 16 `TRAITVARstd` `trait / trait = 1` after strictly positive `TRAITVAR`. +pub use event_time::recover_standardised_trait_variance; /// Exact scalar p. 16 stationary `T0MEANS` `-κ / a + −B z / a`. pub use event_time::recover_stationary_initial_latent_mean; /// Exact scalar §4.3 / p. 16 stationary `T0VAR` `trait + −q / (2 a) + (B / a)² v`. @@ -534,6 +548,8 @@ pub use event_time::refuse_initial_time_independent_effect_as_process_increment; pub use event_time::refuse_initial_time_independent_effect_as_time_dependent_impulse; /// Refuse treating first-occasion TI observed mean as the first-occasion TD observed mean. pub use event_time::refuse_initial_time_independent_observed_mean_as_initial_time_dependent_observed_mean; +/// Refuse treating 2017-era `addedT0TIPREDVAR` as p. 16 `TRAITVARstd`. +pub use event_time::refuse_initial_time_independent_variance_as_standardised_trait_variance; /// Refuse treating Driver Eq. 3–4 lagged latent covariance as `cov(y_t, y_{t-1})`. pub use event_time::refuse_latent_lagged_covariance_as_observed_covariance; /// Refuse treating Driver Eq. 5 latent mean as `E(y)`. @@ -586,6 +602,8 @@ pub use event_time::refuse_standardised_continuous_intercept_as_standardised_dis pub use event_time::refuse_standardised_initial_latent_mean_as_standardised_initial_latent_variance; /// Refuse treating p. 16 `T0VARstd` as `T0MEANSstd`. pub use event_time::refuse_standardised_initial_latent_variance_as_standardised_initial_latent_mean; +/// Refuse treating p. 16 `T0VARstd` as `TRAITVARstd`. +pub use event_time::refuse_standardised_initial_latent_variance_as_standardised_trait_variance; /// Refuse treating `MANIFESTVARstd` as `MANIFESTMEANSstd`. pub use event_time::refuse_standardised_manifest_variance_as_standardised_manifest_mean; /// Refuse treating p. 16 stationary `T0MEANS` as `asymCINT`. @@ -682,6 +700,8 @@ pub use event_time::refuse_unstandardised_initial_latent_mean_as_standardised_in pub use event_time::refuse_unstandardised_initial_latent_variance_as_standardised_initial_latent_variance; /// Refuse treating unstandardised `MANIFESTMEANS` as `MANIFESTMEANSstd`. pub use event_time::refuse_unstandardised_manifest_mean_as_standardised_manifest_mean; +/// Refuse treating unstandardised `TRAITVAR` as `TRAITVARstd`. +pub use event_time::refuse_unstandardised_trait_variance_as_standardised_trait_variance; /// Refuse treating `μ_0 / √asymDIFFUSION` as `T0MEANSstd`. pub use event_time::refuse_within_subject_scaled_initial_latent_mean_as_standardised_initial_latent_mean; /// Indicator coordinate kind. 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 0693fd62..5248f44c 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -37,7 +37,7 @@ use psychometric_core::{ recover_standardised_asymptotic_continuous_intercept, 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_mean, 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, @@ -101,6 +101,7 @@ use psychometric_core::{ refuse_initial_time_independent_effect_as_process_increment, refuse_initial_time_independent_effect_as_time_dependent_impulse, refuse_initial_time_independent_observed_mean_as_initial_time_dependent_observed_mean, + refuse_initial_time_independent_variance_as_standardised_trait_variance, refuse_latent_lagged_covariance_as_observed_covariance, refuse_latent_mean_as_observed_mean, refuse_latent_variance_as_observed_variance, refuse_level_change_extra_process_as_impulse, refuse_level_change_extra_process_as_increment, refuse_level_change_extra_process_as_intercept, @@ -115,6 +116,7 @@ use psychometric_core::{ refuse_measurement_error_as_stationary_later_observed_variance, refuse_pooled_discrete_lag_across_unequal_intervals, refuse_process_noise_as_unconditional_variance, + refuse_standardised_initial_latent_variance_as_standardised_trait_variance, refuse_stationary_initial_latent_mean_as_asymptotic_continuous_intercept, refuse_stationary_initial_latent_mean_as_asymptotic_time_independent_effect, refuse_stationary_initial_latent_mean_as_discrete_mean, @@ -154,6 +156,7 @@ use psychometric_core::{ 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_trait_variance_as_standardised_trait_variance, }; fn rmse(truth: &[f64], recovered: &[f64]) -> f64 { @@ -6040,6 +6043,75 @@ fn standardised_initial_latent_variance_refuses_non_event_clocks_and_does_not_ke ); } +#[test] +fn standardised_trait_variance_recovers_driver_table_two_correlation() { + let trait_variance = 1.6_f64; + let recovered = recover_standardised_trait_variance(trait_variance, LagClock::EventTime) + .expect("TRAITVARstd"); + let recovered_error = (recovered - 1.0).abs(); + assert!( + recovered_error < 1e-15, + "Driver et al. (2017, p. 16 TRAITVARstd): RMSE {recovered_error} for trait / trait = 1" + ); + let larger_trait = recover_standardised_trait_variance(6.4, LagClock::EventTime) + .expect("TRAITVARstd trait=6.4"); + assert!( + (larger_trait - recovered).abs() < 1e-15, + "Driver et al. (2017, p. 16): distinct positive TRAITVAR recover the same TRAITVARstd" + ); + let t0var_std = + recover_standardised_initial_latent_variance(trait_variance, LagClock::EventTime) + .expect("T0VARstd"); + assert!( + (t0var_std - recovered).abs() < 1e-15, + "Driver et al. (2017, p. 16): T0VARstd equals 1 after strictly positive p_0" + ); + let extra = 0.3_f64 * 0.3_f64 * 4.0_f64; + let extra_error = (extra - 1.0).abs(); + assert!( + recovered_error < extra_error, + "Driver et al. (2017, 2017-era addedT0TIPREDVAR): extra RMSE {extra_error} must exceed TRAITVARstd RMSE {recovered_error}" + ); + let unstandardised_error = (trait_variance - 1.0).abs(); + assert!( + recovered_error < unstandardised_error, + "Driver et al. (2017, Table 2): unstandardised TRAITVAR RMSE {unstandardised_error} must exceed TRAITVARstd RMSE {recovered_error}" + ); + assert_eq!( + refuse_unstandardised_trait_variance_as_standardised_trait_variance( + trait_variance, + recovered + ), + Err(PsychometricError::UnstandardisedTraitVarianceIsNotStandardisedTraitVariance) + ); + assert_eq!( + refuse_standardised_initial_latent_variance_as_standardised_trait_variance( + t0var_std, recovered + ), + Err(PsychometricError::StandardisedInitialLatentVarianceIsNotStandardisedTraitVariance) + ); + assert_eq!( + refuse_initial_time_independent_variance_as_standardised_trait_variance(extra, recovered), + Err(PsychometricError::InitialTimeIndependentVarianceIsNotStandardisedTraitVariance) + ); +} + +#[test] +fn standardised_trait_variance_refuses_non_event_clocks_and_does_not_keep_zero_variance() { + assert_eq!( + recover_standardised_trait_variance(1.6, LagClock::AssertionTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_standardised_trait_variance(1.6, LagClock::KnowledgeCutoff), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_standardised_trait_variance(0.0, LagClock::EventTime), + Err(PsychometricError::StandardisedTraitVarianceRequiresPositiveTraitVariance) + ); +} + #[test] fn standardised_discrete_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 50e72f74..fdde33d3 100644 --- a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs +++ b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs @@ -32,7 +32,7 @@ use psychometric_core::{ recover_standardised_asymptotic_continuous_intercept, 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_mean, 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, @@ -100,6 +100,7 @@ use psychometric_core::{ refuse_initial_time_independent_effect_as_process_increment, refuse_initial_time_independent_effect_as_time_dependent_impulse, refuse_initial_time_independent_observed_mean_as_initial_time_dependent_observed_mean, + refuse_initial_time_independent_variance_as_standardised_trait_variance, refuse_latent_lagged_covariance_as_observed_covariance, refuse_latent_mean_as_observed_mean, refuse_latent_variance_as_observed_variance, refuse_level_change_extra_process_as_impulse, refuse_level_change_extra_process_as_increment, refuse_level_change_extra_process_as_intercept, @@ -119,6 +120,7 @@ use psychometric_core::{ refuse_standardised_continuous_intercept_as_standardised_discrete_continuous_intercept, refuse_standardised_initial_latent_mean_as_standardised_initial_latent_variance, refuse_standardised_initial_latent_variance_as_standardised_initial_latent_mean, + refuse_standardised_initial_latent_variance_as_standardised_trait_variance, refuse_standardised_manifest_variance_as_standardised_manifest_mean, refuse_stationary_initial_latent_mean_as_asymptotic_continuous_intercept, refuse_stationary_initial_latent_mean_as_asymptotic_time_independent_effect, @@ -165,6 +167,7 @@ use psychometric_core::{ 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, + refuse_unstandardised_trait_variance_as_standardised_trait_variance, refuse_within_subject_scaled_initial_latent_mean_as_standardised_initial_latent_mean, }; @@ -3290,6 +3293,70 @@ fn standardised_initial_latent_variance_is_not_unstandardised_mean_or_asymptotic ); } +#[test] +fn standardised_trait_variance_is_not_unstandardised_or_t0varstd() { + let trait_variance = 1.6_f64; + let recovered = recover_standardised_trait_variance(trait_variance, LagClock::EventTime) + .expect("TRAITVARstd"); + assert!( + (recovered - 1.0).abs() < 1e-15, + "Driver et al. (2017, p. 16 / 2017-era summary.ctsemFit.R): TRAITVARstd is trait/trait = 1" + ); + let larger_trait = recover_standardised_trait_variance(6.4, LagClock::EventTime) + .expect("TRAITVARstd trait=6.4"); + assert!( + (larger_trait - recovered).abs() < 1e-15, + "Driver et al. (2017, p. 16): distinct positive TRAITVAR recover the same TRAITVARstd" + ); + let t0var_std = + recover_standardised_initial_latent_variance(trait_variance, LagClock::EventTime) + .expect("T0VARstd"); + assert!( + (t0var_std - recovered).abs() < 1e-15, + "Driver et al. (2017, p. 16): T0VARstd and TRAITVARstd equal 1 and remain distinct named quantities" + ); + let extra = 0.3_f64 * 0.3_f64 * 4.0_f64; + assert!( + (extra - recovered).abs() > 1e-3, + "Driver et al. (2017, 2017-era addedT0TIPREDVAR): t0_b² v is not TRAITVARstd" + ); + assert!((trait_variance - recovered).abs() > 1e-3); + assert_eq!( + recover_standardised_trait_variance(0.0, LagClock::EventTime), + Err( + psychometric_core::PsychometricError::StandardisedTraitVarianceRequiresPositiveTraitVariance + ) + ); + assert_eq!( + recover_standardised_trait_variance(trait_variance, LagClock::SystemTime), + Err(psychometric_core::PsychometricError::EventTimeRequired) + ); + assert_eq!( + refuse_unstandardised_trait_variance_as_standardised_trait_variance( + trait_variance, + recovered + ), + Err( + psychometric_core::PsychometricError::UnstandardisedTraitVarianceIsNotStandardisedTraitVariance + ) + ); + assert_eq!( + refuse_standardised_initial_latent_variance_as_standardised_trait_variance( + t0var_std, + recovered + ), + Err( + psychometric_core::PsychometricError::StandardisedInitialLatentVarianceIsNotStandardisedTraitVariance + ) + ); + assert_eq!( + refuse_initial_time_independent_variance_as_standardised_trait_variance(extra, recovered), + Err( + psychometric_core::PsychometricError::InitialTimeIndependentVarianceIsNotStandardisedTraitVariance + ) + ); +} + #[test] fn standardised_discrete_continuous_intercept_is_not_unstandardised_continuous_or_asymptotic() { let intercept = 0.4_f64; diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index 9fc0cb36..b0a49918 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -34,6 +34,7 @@ The executable standardised-measurement slice recovers Driver et al. (2017, p. 1 The executable standardised-asymptotic-intercept slice recovers Driver et al. (2017, p. 16 `asymCINTstd`) as `(-κ / a) / √p` after strictly positive `asymDIFFUSION` `p = −q / (2 a)` (footnote 4; Eq. 3; Table 2; 2017-era `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-26T00:20Z). Unstandardised `-κ / a` is defined for a zero process and is not that map. `κ / √p` is `CINTstd` and is not this total-change map. `A^{-1}[e^{A Δt} − I] κ / √p` is `discreteCINTstd` and is not this `Δt → ∞` map. This is not ctsem estimation. 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-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. Input/process/intervention/outcome paths obey event-time order. Temporal precedence, document linkage, event tracking, or model prediction alone do not justify causal language. diff --git a/docs/research/multilevel-event-time-recovery.md b/docs/research/multilevel-event-time-recovery.md index 0ac29769..901532b1 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -84,7 +84,7 @@ This slice stays inside `psychometric_core`. It does not add a second invariance 78. refuse treating unstandardised `T0TDPREDEFFECT` `t0_m` as `T0TDPREDEFFECTstd`, refuse treating `TDPREDEFFECTstd` `m · √v / √(-q / (2 a))` as `T0TDPREDEFFECTstd`, refuse treating `T0TIPREDEFFECTstd` `t0_b · √v / √p_0` as `T0TDPREDEFFECTstd` even when `t0_m = t0_b`, refuse treating `t0_m · √v / √(trait + p_0 + added)` as `T0TDPREDEFFECTstd`, and refuse treating `TRAITVAR` as the footnote 4 standardisation variance; 79. recover the exact scalar p. 16 `T0VARstd` as `solve(sqrt(diag(T0VAR))) %&% T0VAR` after forming strictly positive free `T0VAR` `p_0` (Driver et al., 2017, Table 2, p. 12; p. 16; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T22:06Z; OpenMx `%&%` is `t(A) %*% B %*% A`; the default ridge is 0; the scalar map is `p_0 / p_0 = 1`; `p_0 = 0` fails closed; a non-event clock fails closed; free `T0VAR` does not require `a < 0`); 80. refuse treating unstandardised `T0VAR` as `T0VARstd`, refuse treating `T0TDPREDEFFECTstd` `t0_m · √v / √p_0` as `T0VARstd`, refuse treating `addedT0TIPREDVAR` `t0_b² v` as `T0VARstd`, and refuse treating `TRAITVAR` as the footnote 4 standardisation variance; -81. recover the exact scalar p. 16 `TRAITVARstd` as `solve(sqrt(diag(TRAITVAR))) %&% TRAITVAR` after forming strictly positive `TRAITVAR` (Driver et al., 2017, Table 2, p. 12; §7.1, pp. 18–19; p. 16; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T22:21Z; OpenMx `%&%` is `t(A) %*% B %*% A`; unlike `T0VARstd` there is no ridge addend; the scalar map is `trait / trait = 1`; `TRAITVAR = 0` fails closed; a non-event clock fails closed; `TRAITVAR` does not require `a < 0`); +81. recover the exact scalar p. 16 `TRAITVARstd` as `solve(sqrt(diag(TRAITVAR))) %&% TRAITVAR` after forming strictly positive `TRAITVAR` (Driver et al., 2017, Table 2, p. 12; §7.1, pp. 18–19; p. 16; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-26T17:45Z; OpenMx `%&%` is `t(A) %*% B %*% A`; unlike `T0VARstd` there is no ridge addend; the scalar map is `trait / trait = 1`; `TRAITVAR = 0` fails closed; a non-event clock fails closed; `TRAITVAR` does not require `a < 0`); 82. refuse treating unstandardised `TRAITVAR` as `TRAITVARstd`, refuse treating `T0VARstd` as `TRAITVARstd` even when both equal 1, and refuse treating `addedT0TIPREDVAR` `t0_b² v` as `TRAITVARstd`; 83. recover the exact scalar p. 16 `MANIFESTTRAITVARstd` as `solve(sqrt(diag(MANIFESTTRAITVAR))) %&% MANIFESTTRAITVAR` after forming strictly positive `MANIFESTTRAITVAR` (Driver et al., 2017, Table 2, p. 12; §7.1, p. 19; p. 16; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T22:28Z; OpenMx `%&%` is `t(A) %*% B %*% A`; unlike `TRAITVARstd` the 2017-era source adds ridging; the default ridge is 0; the scalar map is `ψ / ψ = 1`; `MANIFESTTRAITVAR = 0` fails closed; a non-event clock fails closed; `MANIFESTTRAITVAR` does not require `a < 0`); 84. refuse treating unstandardised `MANIFESTTRAITVAR` as `MANIFESTTRAITVARstd`, refuse treating `TRAITVARstd` as `MANIFESTTRAITVARstd` even when both equal 1, and refuse treating `MANIFESTVAR` `θ` as `MANIFESTTRAITVARstd`; @@ -251,7 +251,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21:0 - Driver et al. (2017, p. 16 `TDPREDEFFECTstd`; Table 2; Eq. 3; footnote 4; JSS PDF re-opened 2026-08-23T21:10Z) recovers a known standardised continuous TD effect \(m\cdot\sqrt{v}/\sqrt{-q/(2a)}\) at machine-scale RMSE, and that RMSE is smaller than treating unstandardised \(M\), intercept-style \(A^{-1}[e^{A\Delta t}-I]M\cdot\sqrt{v}/\sqrt{p}\), or \(m\cdot\sqrt{v}/\sqrt{\mathrm{trait}+p+\mathrm{added}}\) as `TDPREDEFFECTstd`; equal numbers with `TIPREDEFFECTstd` when \(M=B\) remain distinct named quantities; a larger positive \(q\) yields a smaller \(|\mathrm{std}|\); a zero coefficient with positive \(v\) and \(p\) is exactly zero; \(q=0\), \(v=0\), and \(a\ge 0\) fail closed; a non-event clock and an overflowing product fail closed. - Driver et al. (2017, Table 3 / p. 16 `T0TDPREDEFFECTstd`; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T21:34Z) recovers a known standardised first-occasion TD effect \(t0_m\cdot\sqrt{v}/\sqrt{p_0}\) at machine-scale RMSE, and that RMSE is smaller than treating unstandardised \(t0_m\), continuous \(m\cdot\sqrt{v}/\sqrt{-q/(2a)}\), or \(t0_m\cdot\sqrt{v}/\sqrt{\mathrm{trait}+p_0+\mathrm{added}}\) as `T0TDPREDEFFECTstd`; equal numbers with `T0TIPREDEFFECTstd` when \(t0_m=t0_b\) remain distinct named quantities; a larger positive \(p_0\) yields a smaller \(|\mathrm{std}|\); a zero coefficient with positive \(v\) and \(p_0\) is exactly zero; \(p_0=0\) and \(v=0\) fail closed; a non-event clock and an overflowing product fail closed; free `T0VAR` does not require \(a<0\). - Driver et al. (2017, Table 2 / p. 16 `T0VARstd`; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-26T07:17Z) recovers the scalar correlation \(p_0/p_0=1\) at machine-scale RMSE after strictly positive free `T0VAR`, and that RMSE is smaller than treating unstandardised \(p_0\), `T0MEANSstd` \(\mu_0/\sqrt{p_0}\), or `asymDIFFUSIONstd` \(p/p=1\) as `T0VARstd`; distinct positive \(p_0\) recover the same 1; equal 1 with `T0MEANSstd` when \(\mu_0=\sqrt{p_0}\) remains a distinct named quantity; equal 1 with `asymDIFFUSIONstd` remains a distinct named quantity; \(p_0=0\) fails closed; a non-event clock fails closed; free `T0VAR` does not require \(a<0\). -- Driver et al. (2017, Table 2 / §7.1 / p. 16 `TRAITVARstd`; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T22:21Z) recovers the scalar correlation \(\mathrm{trait}/\mathrm{trait}=1\) at machine-scale RMSE after strictly positive `TRAITVAR`, and that RMSE is smaller than treating unstandardised `TRAITVAR` or `addedT0TIPREDVAR` \(t0_b^{2}v\) as `TRAITVARstd`; distinct positive trait recover the same 1; equal 1 with `T0VARstd` remains a distinct named quantity; `TRAITVAR = 0` fails closed; a non-event clock fails closed; `TRAITVAR` does not require \(a<0\). +- Driver et al. (2017, Table 2 / §7.1 / p. 16 `TRAITVARstd`; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-26T17:45Z) recovers the scalar correlation \(\mathrm{trait}/\mathrm{trait}=1\) at machine-scale RMSE after strictly positive `TRAITVAR`, and that RMSE is smaller than treating unstandardised `TRAITVAR` or `addedT0TIPREDVAR` \(t0_b^{2}v\) as `TRAITVARstd`; distinct positive trait recover the same 1; equal 1 with `T0VARstd` remains a distinct named quantity; `TRAITVAR = 0` fails closed; a non-event clock fails closed; `TRAITVAR` does not require \(a<0\). - Driver et al. (2017, Table 2 / §7.1 / p. 16 `MANIFESTTRAITVARstd`; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T22:28Z) recovers the scalar correlation \(\psi/\psi=1\) at machine-scale RMSE after strictly positive `MANIFESTTRAITVAR`, and that RMSE is smaller than treating unstandardised `MANIFESTTRAITVAR` or `MANIFESTVAR` \(\theta\) as `MANIFESTTRAITVARstd`; distinct positive \(\psi\) recover the same 1; equal 1 with `TRAITVARstd` remains a distinct named quantity; `MANIFESTTRAITVAR = 0` fails closed; a non-event clock fails closed; `MANIFESTTRAITVAR` does not require \(a<0\). - Driver et al. (2017, Table 2 / Eq. 5 / p. 16 `MANIFESTVARstd`; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T22:40Z) recovers the scalar correlation \(\theta/\theta=1\) at machine-scale RMSE after strictly positive `MANIFESTVAR`, and that RMSE is smaller than treating unstandardised `MANIFESTVAR` or Equation 5 \(\operatorname{Var}(y)\) as `MANIFESTVARstd`; distinct positive \(\theta\) recover the same 1; equal 1 with `MANIFESTTRAITVARstd` remains a distinct named quantity; `MANIFESTVAR = 0` fails closed; a non-event clock fails closed; `MANIFESTVAR` does not require \(a<0\). - Driver et al. (2017, Table 2 / p. 16 `TIPREDVARstd`; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T22:53Z) recovers the scalar correlation \(v/v=1\) at machine-scale RMSE after strictly positive `TIPREDVAR`, and that RMSE is smaller than treating unstandardised `TIPREDVAR` or §7.2 `addedTIPREDVAR` \((B/a)^{2}v\) as `TIPREDVARstd`; distinct positive \(v\) recover the same 1; equal 1 with `MANIFESTVARstd` remains a distinct named quantity; `TIPREDVAR = 0` fails closed; a non-event clock fails closed; `TIPREDVAR` does not require \(a<0\). From aa7c57f809ff456fc4404bd9d18ea3aa81b192be Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 21:13:06 +0900 Subject: [PATCH 2/6] merge: integrate protected main fixes into TRAITVARstd branch Refresh the branch onto the repaired main (rustdoc/mlx gating, asymDIFFUSION restoration, hourly-contract alignment) and union the sibling param families. Full gate evidence on the resulting tree: psychometric_core tests pass, branch coverage 3796/3796 (100%), line coverage 100%, Python 100%, workspace and docstring contracts PASS, fmt clean. --- crates/psychometric_core/src/error.rs | 73 +++++ crates/psychometric_core/src/event_time.rs | 293 +++++++++++++++++- crates/psychometric_core/src/lib.rs | 36 ++- ...multilevel_event_time_recovery_contract.rs | 74 ++++- .../scientific_claim_boundary_contract.rs | 109 ++++++- 5 files changed, 542 insertions(+), 43 deletions(-) diff --git a/crates/psychometric_core/src/error.rs b/crates/psychometric_core/src/error.rs index 1151a48f..947f72f8 100644 --- a/crates/psychometric_core/src/error.rs +++ b/crates/psychometric_core/src/error.rs @@ -585,6 +585,34 @@ pub enum PsychometricError { /// `asymDIFFUSIONstd` is the correlation form of process- /// dynamics `asymDIFFUSION`. StandardisedAsymptoticDiffusionIsNotStandardisedInitialLatentVariance, + /// Driver p. 16 `asymDIFFUSIONstd` was requested without a + /// strictly positive `asymDIFFUSION`. Footnote 4 + /// standardisation of the 2017-era `asymDIFFUSION` matrix + /// requires strictly positive `−q / (2 a)`. + StandardisedAsymptoticDiffusionRequiresPositiveStationaryVariance, + /// Driver p. 16 unstandardised `asymDIFFUSION` `p` was treated + /// as `asymDIFFUSIONstd`. Unstandardised `p` is defined for a + /// zero process; standardised `asymDIFFUSION` is not. + UnstandardisedAsymptoticDiffusionIsNotStandardisedAsymptoticDiffusion, + /// Driver p. 16 `T0VARstd` was treated as p. 16 + /// `asymDIFFUSIONstd`. Equal numbers of 1 after a strictly + /// positive relevant variance are still distinct named + /// quantities. `asymDIFFUSIONstd` is the correlation form of + /// process-dynamics `asymDIFFUSION`; `T0VARstd` is the + /// correlation form of free `T0VAR`. + StandardisedInitialLatentVarianceIsNotStandardisedAsymptoticDiffusion, + /// Driver p. 16 `DIFFUSIONstd` `q / p = −2 a` was treated as + /// `asymDIFFUSIONstd`. The continuous-diffusion ratio is not + /// the correlation form of `asymDIFFUSION`. + StandardisedContinuousDiffusionIsNotStandardisedAsymptoticDiffusion, + /// Driver p. 16 `TIPREDVARstd` was treated as p. 16 + /// `asymDIFFUSIONstd`. Equal numbers of 1 after a strictly + /// positive relevant variance are still distinct named + /// quantities. `asymDIFFUSIONstd` is the correlation form of + /// process-dynamics `asymDIFFUSION`; `TIPREDVARstd` is the + /// correlation form of `TIPREDVAR`. + StandardisedTimeIndependentPredictorVarianceIsNotStandardisedAsymptoticDiffusion, + /// Driver p. 16 `TRAITVARstd` was requested with a non-positive /// trait variance. The 2017-era source skips forming /// `TRAITVARstd` when `TRAITVAR == 0`; footnote 4 @@ -604,6 +632,7 @@ pub enum PsychometricError { /// p. 16 `TRAITVARstd`. Extra first-occasion TI variance is not /// the correlation form of between-subject `TRAITVAR`. InitialTimeIndependentVarianceIsNotStandardisedTraitVariance, + /// Driver p. 16 `discreteCINTstd` was requested without a strictly /// positive `asymDIFFUSION`. Footnote 4 standardises using only the /// relevant variance; zero `q` has no positive process SD. @@ -1082,6 +1111,21 @@ impl fmt::Display for PsychometricError { Self::StandardisedAsymptoticDiffusionIsNotStandardisedInitialLatentVariance => { "standardised asymptotic diffusion is not standardised initial latent variance" } + Self::StandardisedAsymptoticDiffusionRequiresPositiveStationaryVariance => { + "standardised asymptotic diffusion requires strictly positive stationary within-subject variance" + } + Self::UnstandardisedAsymptoticDiffusionIsNotStandardisedAsymptoticDiffusion => { + "unstandardised asymptotic diffusion is not standardised asymptotic diffusion" + } + Self::StandardisedInitialLatentVarianceIsNotStandardisedAsymptoticDiffusion => { + "standardised initial latent variance is not standardised asymptotic diffusion" + } + Self::StandardisedContinuousDiffusionIsNotStandardisedAsymptoticDiffusion => { + "standardised continuous diffusion is not standardised asymptotic diffusion" + } + Self::StandardisedTimeIndependentPredictorVarianceIsNotStandardisedAsymptoticDiffusion => { + "standardised time-independent predictor variance is not standardised asymptotic diffusion" + } Self::StandardisedTraitVarianceRequiresPositiveTraitVariance => { "standardised trait variance requires strictly positive trait variance" } @@ -1833,6 +1877,35 @@ mod tests { ); } + #[test] + fn standardised_asymptotic_diffusion_boundary_messages_are_stable() { + assert_eq!( + PsychometricError::StandardisedAsymptoticDiffusionRequiresPositiveStationaryVariance + .to_string(), + "standardised asymptotic diffusion requires strictly positive stationary within-subject variance" + ); + assert_eq!( + PsychometricError::UnstandardisedAsymptoticDiffusionIsNotStandardisedAsymptoticDiffusion + .to_string(), + "unstandardised asymptotic diffusion is not standardised asymptotic diffusion" + ); + assert_eq!( + PsychometricError::StandardisedInitialLatentVarianceIsNotStandardisedAsymptoticDiffusion + .to_string(), + "standardised initial latent variance is not standardised asymptotic diffusion" + ); + assert_eq!( + PsychometricError::StandardisedContinuousDiffusionIsNotStandardisedAsymptoticDiffusion + .to_string(), + "standardised continuous diffusion is not standardised asymptotic diffusion" + ); + assert_eq!( + PsychometricError::StandardisedTimeIndependentPredictorVarianceIsNotStandardisedAsymptoticDiffusion + .to_string(), + "standardised time-independent predictor variance is not standardised asymptotic diffusion" + ); + } + #[test] fn standardised_trait_variance_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 4e5fd8c0..14afb883 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -1603,9 +1603,9 @@ pub fn recover_standardised_initial_latent_mean( /// `p_0` recover the same 1. `T0MEANSstd` `μ_0 / √p_0` recovers /// the same number when `μ_0 = √p_0` and remains a distinct named /// quantity. `asymDIFFUSIONstd` `p / p = 1` recovers the same -/// number and remains a distinct named quantity. This crate does -/// not currently export `T0MEANSstd` or `asymDIFFUSIONstd`; the -/// refuse still names those quantities. This is not a Kalman +/// number and remains a distinct named quantity. This crate +/// exports `T0MEANSstd` and `asymDIFFUSIONstd`. +/// This is not a Kalman /// filter, not a matrix `expm`, not DSEM, and not ctsem estimation. /// /// # Errors @@ -1684,8 +1684,7 @@ pub fn refuse_unstandardised_initial_latent_variance_as_standardised_initial_lat /// Both scalar maps equal 1 when `μ_0 = √p_0`. `T0VARstd` is /// the correlation form of free `T0VAR`. `T0MEANSstd` is the /// first-occasion mean. Equal numbers remain distinct named -/// quantities. This crate does not currently export -/// `T0VARstd`; the refuse still names that quantity. +/// quantities. /// /// # Errors /// @@ -1704,8 +1703,7 @@ pub fn refuse_standardised_initial_latent_variance_as_standardised_initial_laten /// Both scalar maps equal 1 when `μ_0 = √p_0`. `T0VARstd` is the /// correlation form of free `T0VAR`. `T0MEANSstd` is the /// first-occasion mean. Equal numbers remain distinct named -/// quantities. This crate does not currently export `T0MEANSstd`; -/// the refuse still names that quantity. +/// quantities. /// /// # Errors /// @@ -1742,8 +1740,7 @@ pub fn refuse_within_subject_scaled_initial_latent_mean_as_standardised_initial_ /// variance. `T0VARstd` is the correlation form of free first- /// occasion `T0VAR`. `asymDIFFUSIONstd` is the correlation form of /// process-dynamics `asymDIFFUSION`. Equal numbers remain distinct -/// named quantities. This crate does not currently export -/// `asymDIFFUSIONstd`; the refuse still names that quantity. +/// named quantities. /// /// # Errors /// @@ -1760,6 +1757,45 @@ pub fn refuse_standardised_asymptotic_diffusion_as_standardised_initial_latent_v Err(PsychometricError::StandardisedAsymptoticDiffusionIsNotStandardisedInitialLatentVariance) } +/// Exact scalar p. 16 `asymDIFFUSIONstd` after strictly positive +/// `asymDIFFUSION`. +/// +/// Driver, Oud, and Voelkle (2017, p. 16; footnote 4; Eq. 4, p. 5; +/// Table 2, p. 12; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF +/// re-opened 2026-08-26T17:20Z from +/// ) +/// name `asymDIFFUSION` the total within-subject variance as +/// `Δt → ∞`. Page 16 prints standardised matrices with the suffix +/// `std` when appropriate. The printed example on p. 16 is +/// `discreteDRIFTstd`, not `asymDIFFUSIONstd`. Footnote 4: +/// standardisations use only the relevant variance, not the total. +/// The relevant variance for that named process-dynamics +/// correlation is within-subject `asymDIFFUSION` `p = −q / (2 a)`, +/// not free first-occasion `T0VAR`. The 2017-era +/// `summary.ctsemFit.R` forms `asymDIFFUSIONstd` as +/// `solve(sqrt(diag(asymDIFFUSION))) %&% asymDIFFUSION` when +/// `verbose = TRUE`. `OpenMx` `%&%` is the quadratic form +/// `t(A) %*% B %*% A`. The default `ridging = FALSE` adds 0, not +/// `0.0001`; that ridge is a numerical hack and is not this exact +/// map. The 2017-era source assigns +/// `dimnames(asymDIFFUSIONstd)` to `latentNames`; that assignment +/// is not this scalar map. The scalar correlation is `p / p = 1` +/// after strictly positive `asymDIFFUSION`. Form strictly positive +/// `p` first, then `1 / √p`, then `(1 / √p) p (1 / √p)`. +/// Unstandardised `asymDIFFUSION` is defined for a zero process; +/// standardised `asymDIFFUSION` is not. Zero `q` has no positive +/// SD and fails closed. Lasting `p` requires stable `a < 0`. +/// `asymDIFFUSION` is an event-time process-dynamics quantity, so +/// a non-event clock fails closed. Distinct positive `p` recover +/// the same 1. `T0VARstd` `p_0 / p_0 = 1` recovers the same number +/// and remains a distinct named quantity. `DIFFUSIONstd` +/// `q / p = −2 a` is the continuous-diffusion ratio and is not this +/// correlation. `TIPREDVARstd` `v / v = 1` recovers the same number +/// and remains a distinct named quantity. This crate does not +/// currently export `DIFFUSIONstd` or `TIPREDVARstd`; the refuse +/// still names those quantities. This is not a Kalman filter, not a +/// matrix `expm`, not DSEM, and not ctsem estimation. + /// Exact scalar p. 16 `TRAITVARstd` after strictly positive `TRAITVAR`. /// /// Driver, Oud, and Voelkle (2017, Table 2, p. 12; §7.1, pp. 18–19; @@ -1799,11 +1835,43 @@ pub fn refuse_standardised_asymptotic_diffusion_as_standardised_initial_latent_v /// export `addedT0TIPREDVAR`; the refuse still names that quantity. /// 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::StandardisedAsymptoticDiffusionRequiresPositiveStationaryVariance`] +/// when `q = 0`, and +/// [`PsychometricError::InvalidNumericInput`] when the diffusion or +/// log-rate is non-finite, the diffusion is negative, or the +/// quadratic form overflows. +pub fn recover_standardised_asymptotic_diffusion( + continuous_diffusion: f64, + log_rate: f64, + clock: LagClock, +) -> Result { + let stationary = recover_stationary_latent_variance(continuous_diffusion, log_rate, clock)?; + if stationary == 0.0 { + return Err( + PsychometricError::StandardisedAsymptoticDiffusionRequiresPositiveStationaryVariance, + ); + } + let process_sd = stationary.sqrt(); + let inverse_sd = require_finite(1.0 / process_sd)?; + let scaled = require_finite(inverse_sd * stationary)?; + require_finite(scaled * inverse_sd) +} + +/// Refuse treating unstandardised `asymDIFFUSION` as p. 16 +/// `asymDIFFUSIONstd`. +/// +/// Unstandardised `p` is defined for a zero process. Footnote 4 +/// `asymDIFFUSIONstd` requires strictly positive `asymDIFFUSION`. +/// Equal numbers when `p = 1` are still distinct named quantities. + /// [`PsychometricError::StandardisedTraitVarianceRequiresPositiveTraitVariance`] /// when `TRAITVAR` is zero, and /// [`PsychometricError::InvalidNumericInput`] when the variance is @@ -1832,10 +1900,31 @@ pub fn recover_standardised_trait_variance( /// Unstandardised `TRAITVAR` is defined for a zero trait. Footnote /// 4 `TRAITVARstd` requires strictly positive `TRAITVAR`. Equal /// numbers when `trait = 1` are still distinct named quantities. + /// /// # Errors /// /// Always returns +/// [`PsychometricError::UnstandardisedAsymptoticDiffusionIsNotStandardisedAsymptoticDiffusion`]. +pub fn refuse_unstandardised_asymptotic_diffusion_as_standardised_asymptotic_diffusion( + unstandardised_asymptotic_diffusion: f64, + standardised_asymptotic_diffusion: f64, +) -> Result { + let _ = ( + unstandardised_asymptotic_diffusion, + standardised_asymptotic_diffusion, + ); + Err(PsychometricError::UnstandardisedAsymptoticDiffusionIsNotStandardisedAsymptoticDiffusion) +} + +/// Refuse treating p. 16 `T0VARstd` as p. 16 `asymDIFFUSIONstd`. +/// +/// Both scalar maps equal 1 after a strictly positive relevant +/// variance. `asymDIFFUSIONstd` is the correlation form of +/// process-dynamics `asymDIFFUSION`. `T0VARstd` is the correlation +/// form of free first-occasion `T0VAR`. Equal numbers remain +/// distinct named quantities. + /// [`PsychometricError::UnstandardisedTraitVarianceIsNotStandardisedTraitVariance`]. pub fn refuse_unstandardised_trait_variance_as_standardised_trait_variance( unstandardised_trait_variance: f64, @@ -1851,10 +1940,57 @@ pub fn refuse_unstandardised_trait_variance_as_standardised_trait_variance( /// variances. `T0VARstd` standardises free first-occasion `T0VAR`. /// `TRAITVARstd` standardises between-subject `TRAITVAR`. Equal /// numbers remain distinct named quantities. + /// /// # Errors /// /// Always returns +/// [`PsychometricError::StandardisedInitialLatentVarianceIsNotStandardisedAsymptoticDiffusion`]. +pub fn refuse_standardised_initial_latent_variance_as_standardised_asymptotic_diffusion( + standardised_initial_variance: f64, + standardised_asymptotic_diffusion: f64, +) -> Result { + let _ = ( + standardised_initial_variance, + standardised_asymptotic_diffusion, + ); + Err(PsychometricError::StandardisedInitialLatentVarianceIsNotStandardisedAsymptoticDiffusion) +} + +/// Refuse treating p. 16 `DIFFUSIONstd` as p. 16 +/// `asymDIFFUSIONstd`. +/// +/// `q / p = −2 a` is the continuous-diffusion ratio. `asymDIFFUSIONstd` +/// is the correlation form of `asymDIFFUSION`. Equal numbers when +/// `a = −0.5` remain distinct named quantities. This crate does not +/// currently export `DIFFUSIONstd`; the refuse still names that +/// quantity. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::StandardisedContinuousDiffusionIsNotStandardisedAsymptoticDiffusion`]. +pub fn refuse_standardised_continuous_diffusion_as_standardised_asymptotic_diffusion( + standardised_continuous_diffusion: f64, + standardised_asymptotic_diffusion: f64, +) -> Result { + let _ = ( + standardised_continuous_diffusion, + standardised_asymptotic_diffusion, + ); + Err(PsychometricError::StandardisedContinuousDiffusionIsNotStandardisedAsymptoticDiffusion) +} + +/// Refuse treating p. 16 `TIPREDVARstd` as p. 16 +/// `asymDIFFUSIONstd`. +/// +/// Both scalar maps equal 1 after a strictly positive relevant +/// variance. `asymDIFFUSIONstd` is the correlation form of +/// process-dynamics `asymDIFFUSION`. `TIPREDVARstd` is the +/// correlation form of `TIPREDVAR`. Equal numbers remain distinct +/// named quantities. This crate does not currently export +/// `TIPREDVARstd`; the refuse still names that quantity. + /// [`PsychometricError::StandardisedInitialLatentVarianceIsNotStandardisedTraitVariance`]. pub fn refuse_standardised_initial_latent_variance_as_standardised_trait_variance( standardised_initial_variance: f64, @@ -1870,10 +2006,28 @@ pub fn refuse_standardised_initial_latent_variance_as_standardised_trait_varianc /// the correlation form of between-subject `TRAITVAR`. Those are /// not the same map. This crate does not currently export /// `addedT0TIPREDVAR`; the refuse still names that quantity. + /// /// # Errors /// /// Always returns +/// [`PsychometricError::StandardisedTimeIndependentPredictorVarianceIsNotStandardisedAsymptoticDiffusion`]. +pub fn refuse_standardised_time_independent_predictor_variance_as_standardised_asymptotic_diffusion( + standardised_predictor_variance: f64, + standardised_asymptotic_diffusion: f64, +) -> Result { + let _ = ( + standardised_predictor_variance, + standardised_asymptotic_diffusion, + ); + Err( + PsychometricError::StandardisedTimeIndependentPredictorVarianceIsNotStandardisedAsymptoticDiffusion, + ) +} + +/// Refuse treating p. 16 `TRAITVARstd` as p. 16 +/// `asymDIFFUSIONstd`-style trait variance misuse. +/// /// [`PsychometricError::InitialTimeIndependentVarianceIsNotStandardisedTraitVariance`]. pub fn refuse_initial_time_independent_variance_as_standardised_trait_variance( initial_predictor_variance: f64, @@ -6442,7 +6596,7 @@ mod tests { recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, recover_standardised_asymptotic_continuous_intercept, - recover_standardised_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_trait_variance, @@ -6533,12 +6687,15 @@ mod tests { refuse_pooled_discrete_lag_across_unequal_intervals, 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_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, + refuse_standardised_initial_latent_variance_as_standardised_asymptotic_diffusion, refuse_standardised_initial_latent_variance_as_standardised_initial_latent_mean, refuse_standardised_initial_latent_variance_as_standardised_trait_variance, refuse_standardised_manifest_variance_as_standardised_manifest_mean, + refuse_standardised_time_independent_predictor_variance_as_standardised_asymptotic_diffusion, refuse_stationary_initial_latent_mean_as_asymptotic_continuous_intercept, refuse_stationary_initial_latent_mean_as_asymptotic_time_independent_effect, refuse_stationary_initial_latent_mean_as_discrete_mean, @@ -6580,6 +6737,7 @@ mod tests { refuse_trait_variance_as_process_noise, refuse_trait_variance_as_stationary_within_subject, refuse_unmatched_time_varying_predictor_interval, refuse_unstandardised_asymptotic_continuous_intercept_as_standardised_asymptotic_continuous_intercept, + refuse_unstandardised_asymptotic_diffusion_as_standardised_asymptotic_diffusion, refuse_unstandardised_continuous_intercept_as_standardised_continuous_intercept, refuse_unstandardised_discrete_continuous_intercept_as_standardised_discrete_continuous_intercept, refuse_unstandardised_initial_latent_mean_as_standardised_initial_latent_mean, @@ -15423,6 +15581,83 @@ mod tests { ); } + #[test] + fn standardised_asymptotic_diffusion_recovers_driver_page_sixteen_correlation() { + // Driver et al. (2017, p. 16 asymDIFFUSIONstd; footnote 4; + // Eq. 4; 2017-era summary.ctsemFit.R): form strictly positive + // asymDIFFUSION p = −q/(2a), then (1/√p) p (1/√p) = 1. + let diffusion = 0.4_f64; + let log_rate = -0.25_f64; + let recovered = + recover_standardised_asymptotic_diffusion(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSIONstd"); + assert!((recovered - 1.0).abs() < 1e-15); + let larger_q = + recover_standardised_asymptotic_diffusion(1.6, log_rate, LagClock::EventTime) + .expect("asymDIFFUSIONstd q=1.6"); + assert!((larger_q - recovered).abs() < 1e-15); + let steeper = + recover_standardised_asymptotic_diffusion(diffusion, -0.5, LagClock::EventTime) + .expect("asymDIFFUSIONstd a=-0.5"); + assert!((steeper - recovered).abs() < 1e-15); + let stationary = + recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSION"); + assert!((stationary - 0.8).abs() < 1e-15); + assert!((stationary - recovered).abs() > 1e-3); + let t0var_std = recover_standardised_initial_latent_variance(1.6, LagClock::EventTime) + .expect("T0VARstd"); + assert!((t0var_std - recovered).abs() < 1e-15); + let continuous_diffusion_std = -2.0 * log_rate; + assert!((continuous_diffusion_std - recovered).abs() > 1e-3); + let equal_ratio = recover_standardised_asymptotic_diffusion(0.4, -0.5, LagClock::EventTime) + .expect("a=-0.5"); + assert!(((-2.0 * -0.5) - equal_ratio).abs() < 1e-15); + let tipred_std = 1.0_f64; + assert_eq!( + refuse_unstandardised_asymptotic_diffusion_as_standardised_asymptotic_diffusion( + stationary, recovered + ), + Err( + PsychometricError::UnstandardisedAsymptoticDiffusionIsNotStandardisedAsymptoticDiffusion + ) + ); + assert_eq!( + refuse_standardised_initial_latent_variance_as_standardised_asymptotic_diffusion( + t0var_std, recovered + ), + Err( + PsychometricError::StandardisedInitialLatentVarianceIsNotStandardisedAsymptoticDiffusion + ) + ); + assert_eq!( + refuse_standardised_continuous_diffusion_as_standardised_asymptotic_diffusion( + continuous_diffusion_std, + recovered + ), + Err( + PsychometricError::StandardisedContinuousDiffusionIsNotStandardisedAsymptoticDiffusion + ) + ); + assert_eq!( + refuse_standardised_continuous_diffusion_as_standardised_asymptotic_diffusion( + -2.0 * -0.5, + equal_ratio + ), + Err( + PsychometricError::StandardisedContinuousDiffusionIsNotStandardisedAsymptoticDiffusion + ) + ); + assert_eq!( + refuse_standardised_time_independent_predictor_variance_as_standardised_asymptotic_diffusion( + tipred_std, recovered + ), + Err( + PsychometricError::StandardisedTimeIndependentPredictorVarianceIsNotStandardisedAsymptoticDiffusion + ) + ); + } + #[test] fn standardised_trait_variance_recovers_driver_table_two_after_positive_traitvar() { // Driver et al. (2017, Table 2 TRAITVAR; §7.1; p. 16 TRAITVARstd; @@ -15464,6 +15699,44 @@ mod tests { ); } + #[test] + fn standardised_asymptotic_diffusion_fails_closed_when_unstandardised_is_defined() { + assert_eq!( + recover_standardised_asymptotic_diffusion(0.0, -0.25, LagClock::EventTime), + Err( + PsychometricError::StandardisedAsymptoticDiffusionRequiresPositiveStationaryVariance + ) + ); + assert_eq!( + recover_standardised_asymptotic_diffusion(0.4, -0.25, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_standardised_asymptotic_diffusion(0.4, 0.25, LagClock::EventTime), + Err(PsychometricError::StationaryVarianceRequiresStableDrift) + ); + assert_eq!( + recover_standardised_asymptotic_diffusion(0.4, 0.0, LagClock::EventTime), + Err(PsychometricError::StationaryVarianceRequiresStableDrift) + ); + assert_eq!( + recover_standardised_asymptotic_diffusion(-0.4, -0.25, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_asymptotic_diffusion(f64::NAN, -0.25, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_asymptotic_diffusion(0.4, f64::NAN, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_asymptotic_diffusion(f64::INFINITY, -0.25, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + } + #[test] fn standardised_trait_variance_fails_closed_when_unstandardised_is_defined() { assert_eq!( diff --git a/crates/psychometric_core/src/lib.rs b/crates/psychometric_core/src/lib.rs index 7651aab6..abcfe86d 100644 --- a/crates/psychometric_core/src/lib.rs +++ b/crates/psychometric_core/src/lib.rs @@ -234,18 +234,18 @@ //! `μ_0 / √p_0` is `T0MEANSstd` and is not that map even when //! `μ_0 = √p_0`; `p / p = 1` is `asymDIFFUSIONstd` and is not that //! map even when both equal 1; JSS PDF re-opened 2026-08-26T07:17Z), -//! recovers the Driver p. 16 `TRAITVARstd` as `trait / trait = 1` -//! after strictly positive `TRAITVAR` (Table 2 names `TRAITVAR` -//! `φ_ξ`; §7.1 names traits the stable between-subject differences; -//! 2017-era `summary.ctsemFit.R` forms `TRAITVARstd` only when -//! `TRAITVAR != 0` as `solve(sqrt(diag(TRAITVAR))) %&% TRAITVAR`; -//! 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 `T0VARstd` and is not that map even when both -//! equal 1; `t0_b² v` is `addedT0TIPREDVAR` and is not that -//! correlation; zero `TRAITVAR` fails closed; a non-event clock -//! fails closed; `TRAITVAR` does not require `a < 0`; JSS PDF -//! re-opened 2026-08-26T17:45Z), +//! recovers the Driver p. 16 `asymDIFFUSIONstd` as `p / p = 1` after +//! strictly positive `asymDIFFUSION` `p = −q / (2 a)` (footnote 4 +//! uses only the relevant within-subject variance; 2017-era +//! `summary.ctsemFit.R` forms `asymDIFFUSIONstd` as +//! `solve(sqrt(diag(asymDIFFUSION))) %&% asymDIFFUSION`; `OpenMx` +//! `%&%` is `t(A) %*% B %*% A`; default ridge is 0; unstandardised +//! `p` is defined for a zero process and is not that map; `p_0 / p_0 +//! = 1` is `T0VARstd` and is not that map even when both equal 1; +//! `q / p = −2 a` is `DIFFUSIONstd` and is not that map; `v / v = 1` +//! 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), //! and refuses //! latent-mean comparison below strong invariance. @@ -386,6 +386,8 @@ pub use event_time::recover_manifest_observed_variance; pub use event_time::recover_manifest_trait_plus_state_observed_variance; /// Exact scalar p. 16 `asymCINTstd` `(-κ / a) / √p` after strictly positive `asymDIFFUSION`. pub use event_time::recover_standardised_asymptotic_continuous_intercept; +/// Exact scalar p. 16 `asymDIFFUSIONstd` `p / p = 1` after strictly positive `asymDIFFUSION`. +pub use event_time::recover_standardised_asymptotic_diffusion; /// Exact scalar p. 16 `CINTstd` `κ / √p`. pub use event_time::recover_standardised_continuous_intercept; /// Exact scalar p. 16 `discreteCINTstd` `A^{-1}[e^{A Δt} − I] κ / √p`. @@ -594,18 +596,24 @@ pub use event_time::refuse_pooled_discrete_lag_across_unequal_intervals; pub use event_time::refuse_process_noise_as_unconditional_variance; /// Refuse treating p. 16 `asymDIFFUSIONstd` as `T0VARstd`. 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 `CINTstd` as `asymCINTstd`. 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 `T0MEANSstd` as `T0VARstd`. pub use event_time::refuse_standardised_initial_latent_mean_as_standardised_initial_latent_variance; +/// Refuse treating p. 16 `T0VARstd` as `asymDIFFUSIONstd`. +pub use event_time::refuse_standardised_initial_latent_variance_as_standardised_asymptotic_diffusion; /// Refuse treating p. 16 `T0VARstd` as `T0MEANSstd`. pub use event_time::refuse_standardised_initial_latent_variance_as_standardised_initial_latent_mean; /// Refuse treating p. 16 `T0VARstd` as `TRAITVARstd`. pub use event_time::refuse_standardised_initial_latent_variance_as_standardised_trait_variance; /// Refuse treating `MANIFESTVARstd` as `MANIFESTMEANSstd`. pub use event_time::refuse_standardised_manifest_variance_as_standardised_manifest_mean; +/// Refuse treating p. 16 `TIPREDVARstd` as `asymDIFFUSIONstd`. +pub use event_time::refuse_standardised_time_independent_predictor_variance_as_standardised_asymptotic_diffusion; /// Refuse treating p. 16 stationary `T0MEANS` as `asymCINT`. pub use event_time::refuse_stationary_initial_latent_mean_as_asymptotic_continuous_intercept; /// Refuse treating p. 16 stationary `T0MEANS` as `asymTIPREDEFFECT`. @@ -690,6 +698,8 @@ pub use event_time::refuse_trait_variance_as_stationary_within_subject; pub use event_time::refuse_unmatched_time_varying_predictor_interval; /// Refuse treating unstandardised `asymCINT` as `asymCINTstd`. pub use event_time::refuse_unstandardised_asymptotic_continuous_intercept_as_standardised_asymptotic_continuous_intercept; +/// Refuse treating unstandardised `asymDIFFUSION` as `asymDIFFUSIONstd`. +pub use event_time::refuse_unstandardised_asymptotic_diffusion_as_standardised_asymptotic_diffusion; /// Refuse treating unstandardised `CINT` as `CINTstd`. pub use event_time::refuse_unstandardised_continuous_intercept_as_standardised_continuous_intercept; /// Refuse treating unstandardised `discreteCINT` as `discreteCINTstd`. @@ -700,7 +710,7 @@ pub use event_time::refuse_unstandardised_initial_latent_mean_as_standardised_in pub use event_time::refuse_unstandardised_initial_latent_variance_as_standardised_initial_latent_variance; /// Refuse treating unstandardised `MANIFESTMEANS` as `MANIFESTMEANSstd`. pub use event_time::refuse_unstandardised_manifest_mean_as_standardised_manifest_mean; -/// Refuse treating unstandardised `TRAITVAR` as `TRAITVARstd`. +/// Refuse treating unstandardised `TRAITVAR` as p. 16 `TRAITVARstd`. pub use event_time::refuse_unstandardised_trait_variance_as_standardised_trait_variance; /// Refuse treating `μ_0 / √asymDIFFUSION` as `T0MEANSstd`. pub use event_time::refuse_within_subject_scaled_initial_latent_mean_as_standardised_initial_latent_mean; 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 5248f44c..e822c018 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -35,16 +35,17 @@ use psychometric_core::{ recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, recover_standardised_asymptotic_continuous_intercept, - 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_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_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_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, @@ -6043,6 +6044,39 @@ fn standardised_initial_latent_variance_refuses_non_event_clocks_and_does_not_ke ); } +#[test] +fn standardised_asymptotic_diffusion_recovers_driver_page_sixteen_correlation() { + let diffusion = 0.4_f64; + let log_rate = -0.25_f64; + let recovered = + recover_standardised_asymptotic_diffusion(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSIONstd"); + let recovered_error = (recovered - 1.0).abs(); + assert!( + recovered_error < 1e-15, + "Driver et al. (2017, p. 16 asymDIFFUSIONstd): RMSE {recovered_error} for p / p = 1" + ); + let larger_q = recover_standardised_asymptotic_diffusion(1.6, log_rate, LagClock::EventTime) + .expect("asymDIFFUSIONstd q=1.6"); + assert!( + (larger_q - recovered).abs() < 1e-15, + "Driver et al. (2017, p. 16): distinct positive asymDIFFUSION recover the same asymDIFFUSIONstd" + ); + let stationary = + recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime).expect("p"); + let unstandardised_error = (stationary - 1.0).abs(); + assert!( + unstandardised_error > recovered_error, + "Driver et al. (2017, p. 16): unstandardised asymDIFFUSION RMSE {unstandardised_error} must exceed asymDIFFUSIONstd RMSE {recovered_error}" + ); + let continuous_diffusion_std = -2.0 * log_rate; + let ratio_error = (continuous_diffusion_std - 1.0).abs(); + assert!( + ratio_error > recovered_error, + "Driver et al. (2017, p. 16): DIFFUSIONstd RMSE {ratio_error} must exceed asymDIFFUSIONstd RMSE {recovered_error}" + ); +} + #[test] fn standardised_trait_variance_recovers_driver_table_two_correlation() { let trait_variance = 1.6_f64; @@ -6096,6 +6130,26 @@ fn standardised_trait_variance_recovers_driver_table_two_correlation() { ); } +#[test] +fn standardised_asymptotic_diffusion_refuses_non_event_clocks_and_does_not_keep_zero_q() { + assert_eq!( + recover_standardised_asymptotic_diffusion(0.4, -0.25, LagClock::AssertionTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_standardised_asymptotic_diffusion(0.4, -0.25, LagClock::KnowledgeCutoff), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_standardised_asymptotic_diffusion(0.0, -0.25, LagClock::EventTime), + Err(PsychometricError::StandardisedAsymptoticDiffusionRequiresPositiveStationaryVariance) + ); + assert_eq!( + recover_standardised_asymptotic_diffusion(0.4, 0.25, LagClock::EventTime), + Err(PsychometricError::StationaryVarianceRequiresStableDrift) + ); +} + #[test] fn standardised_trait_variance_refuses_non_event_clocks_and_does_not_keep_zero_variance() { assert_eq!( diff --git a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs index fdde33d3..42a9951a 100644 --- a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs +++ b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs @@ -30,16 +30,17 @@ use psychometric_core::{ recover_manifest_lagged_observed_covariance, recover_manifest_observed_mean, recover_manifest_observed_variance, recover_manifest_trait_plus_state_observed_variance, recover_standardised_asymptotic_continuous_intercept, - 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_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_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_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, @@ -116,12 +117,15 @@ use psychometric_core::{ refuse_observed_scaled_manifest_mean_as_standardised_manifest_mean, 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_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, + refuse_standardised_initial_latent_variance_as_standardised_asymptotic_diffusion, refuse_standardised_initial_latent_variance_as_standardised_initial_latent_mean, refuse_standardised_initial_latent_variance_as_standardised_trait_variance, refuse_standardised_manifest_variance_as_standardised_manifest_mean, + refuse_standardised_time_independent_predictor_variance_as_standardised_asymptotic_diffusion, refuse_stationary_initial_latent_mean_as_asymptotic_continuous_intercept, refuse_stationary_initial_latent_mean_as_asymptotic_time_independent_effect, refuse_stationary_initial_latent_mean_as_discrete_mean, @@ -162,6 +166,7 @@ use psychometric_core::{ refuse_trait_scaled_continuous_intercept_as_standardised_continuous_intercept, refuse_trait_variance_as_process_noise, refuse_trait_variance_as_stationary_within_subject, refuse_unstandardised_asymptotic_continuous_intercept_as_standardised_asymptotic_continuous_intercept, + refuse_unstandardised_asymptotic_diffusion_as_standardised_asymptotic_diffusion, refuse_unstandardised_continuous_intercept_as_standardised_continuous_intercept, refuse_unstandardised_discrete_continuous_intercept_as_standardised_discrete_continuous_intercept, refuse_unstandardised_initial_latent_mean_as_standardised_initial_latent_mean, @@ -3293,6 +3298,90 @@ fn standardised_initial_latent_variance_is_not_unstandardised_mean_or_asymptotic ); } +#[test] +fn standardised_asymptotic_diffusion_is_not_unstandardised_t0var_or_diffusion_ratio() { + let diffusion = 0.4_f64; + let log_rate = -0.25_f64; + let recovered = + recover_standardised_asymptotic_diffusion(diffusion, log_rate, LagClock::EventTime) + .expect("asymDIFFUSIONstd"); + assert!( + (recovered - 1.0).abs() < 1e-15, + "Driver et al. (2017, p. 16 / 2017-era summary.ctsemFit.R): asymDIFFUSIONstd is p/p = 1" + ); + let larger_q = recover_standardised_asymptotic_diffusion(1.6, log_rate, LagClock::EventTime) + .expect("asymDIFFUSIONstd q=1.6"); + assert!( + (larger_q - recovered).abs() < 1e-15, + "Driver et al. (2017, p. 16): distinct positive asymDIFFUSION recover the same asymDIFFUSIONstd" + ); + let stationary = + recover_stationary_latent_variance(diffusion, log_rate, LagClock::EventTime).expect("p"); + assert!( + (recovered - stationary).abs() > 1e-3, + "Driver et al. (2017, p. 16): unstandardised asymDIFFUSION is not asymDIFFUSIONstd" + ); + let t0var_std = + recover_standardised_initial_latent_variance(1.6, LagClock::EventTime).expect("T0VARstd"); + assert!( + (t0var_std - recovered).abs() < 1e-15, + "Driver et al. (2017, p. 16): T0VARstd equals 1 after strictly positive p_0" + ); + let continuous_diffusion_std = -2.0 * log_rate; + assert!( + (continuous_diffusion_std - recovered).abs() > 1e-3, + "Driver et al. (2017, p. 16): DIFFUSIONstd −2a is not asymDIFFUSIONstd" + ); + let tipred_std = 1.0_f64; + assert_eq!( + refuse_unstandardised_asymptotic_diffusion_as_standardised_asymptotic_diffusion( + stationary, recovered + ), + Err( + psychometric_core::PsychometricError::UnstandardisedAsymptoticDiffusionIsNotStandardisedAsymptoticDiffusion + ) + ); + assert_eq!( + refuse_standardised_initial_latent_variance_as_standardised_asymptotic_diffusion( + t0var_std, recovered + ), + Err( + psychometric_core::PsychometricError::StandardisedInitialLatentVarianceIsNotStandardisedAsymptoticDiffusion + ) + ); + assert_eq!( + refuse_standardised_continuous_diffusion_as_standardised_asymptotic_diffusion( + continuous_diffusion_std, + recovered + ), + Err( + psychometric_core::PsychometricError::StandardisedContinuousDiffusionIsNotStandardisedAsymptoticDiffusion + ) + ); + assert_eq!( + refuse_standardised_time_independent_predictor_variance_as_standardised_asymptotic_diffusion( + tipred_std, recovered + ), + Err( + psychometric_core::PsychometricError::StandardisedTimeIndependentPredictorVarianceIsNotStandardisedAsymptoticDiffusion + ) + ); + assert_eq!( + recover_standardised_asymptotic_diffusion(0.0, log_rate, LagClock::EventTime), + Err( + psychometric_core::PsychometricError::StandardisedAsymptoticDiffusionRequiresPositiveStationaryVariance + ) + ); + assert_eq!( + recover_standardised_asymptotic_diffusion(diffusion, log_rate, LagClock::DocumentTime), + Err(psychometric_core::PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_standardised_asymptotic_diffusion(diffusion, 0.25, LagClock::EventTime), + Err(psychometric_core::PsychometricError::StationaryVarianceRequiresStableDrift) + ); +} + #[test] fn standardised_trait_variance_is_not_unstandardised_or_t0varstd() { let trait_variance = 1.6_f64; From 81e5cbfea5d7fa15bafbc7464f2f3c4074cddf8d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 21:44:54 +0900 Subject: [PATCH 3/6] ci: force re-queue on head From b4fa52e7d9300c91a415ef031d5b3ca8ddb95de6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 22:04:09 +0900 Subject: [PATCH 4/6] fix(doc): add missing Errors sections for TRAITVARstd family The unioned trait-variance fns returned Result without an # Errors doc section, tripping clippy::missing-errors-doc and the Format/lint gate. Document error returns and drop stray doc trailing blanks. --- crates/psychometric_core/src/event_time.rs | 27 +++++++++++++++------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index 14afb883..a8cd5bc4 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -1795,7 +1795,6 @@ pub fn refuse_standardised_asymptotic_diffusion_as_standardised_initial_latent_v /// currently export `DIFFUSIONstd` or `TIPREDVARstd`; the refuse /// still names those quantities. This is not a Kalman filter, not a /// matrix `expm`, not DSEM, and not ctsem estimation. - /// Exact scalar p. 16 `TRAITVARstd` after strictly positive `TRAITVAR`. /// /// Driver, Oud, and Voelkle (2017, Table 2, p. 12; §7.1, pp. 18–19; @@ -1835,7 +1834,6 @@ pub fn refuse_standardised_asymptotic_diffusion_as_standardised_initial_latent_v /// export `addedT0TIPREDVAR`; the refuse still names that quantity. /// This is not a Kalman filter, not a matrix `expm`, not DSEM, and /// not ctsem estimation. - /// /// # Errors /// @@ -1871,11 +1869,17 @@ pub fn recover_standardised_asymptotic_diffusion( /// Unstandardised `p` is defined for a zero process. Footnote 4 /// `asymDIFFUSIONstd` requires strictly positive `asymDIFFUSION`. /// Equal numbers when `p = 1` are still distinct named quantities. - /// [`PsychometricError::StandardisedTraitVarianceRequiresPositiveTraitVariance`] /// when `TRAITVAR` is zero, and /// [`PsychometricError::InvalidNumericInput`] when the variance is /// non-finite, negative, or the quadratic form overflows. +/// +/// # Errors +/// +/// Returns [`PsychometricError::EventTimeRequired`] for a non-event +/// clock, [`PsychometricError::StandardisedTraitVarianceRequiresPositiveTraitVariance`] +/// when `TRAITVAR` is zero, and [`PsychometricError::InvalidNumericInput`] +/// for a non-finite or negative variance. pub fn recover_standardised_trait_variance( trait_variance: f64, clock: LagClock, @@ -1900,7 +1904,6 @@ pub fn recover_standardised_trait_variance( /// Unstandardised `TRAITVAR` is defined for a zero trait. Footnote /// 4 `TRAITVARstd` requires strictly positive `TRAITVAR`. Equal /// numbers when `trait = 1` are still distinct named quantities. - /// /// # Errors /// @@ -1924,8 +1927,11 @@ pub fn refuse_unstandardised_asymptotic_diffusion_as_standardised_asymptotic_dif /// process-dynamics `asymDIFFUSION`. `T0VARstd` is the correlation /// form of free first-occasion `T0VAR`. Equal numbers remain /// distinct named quantities. - /// [`PsychometricError::UnstandardisedTraitVarianceIsNotStandardisedTraitVariance`]. +/// +/// # Errors +/// +/// Always returns [`PsychometricError::UnstandardisedTraitVarianceIsNotStandardisedTraitVariance`]. pub fn refuse_unstandardised_trait_variance_as_standardised_trait_variance( unstandardised_trait_variance: f64, standardised_trait_variance: f64, @@ -1940,7 +1946,6 @@ pub fn refuse_unstandardised_trait_variance_as_standardised_trait_variance( /// variances. `T0VARstd` standardises free first-occasion `T0VAR`. /// `TRAITVARstd` standardises between-subject `TRAITVAR`. Equal /// numbers remain distinct named quantities. - /// /// # Errors /// @@ -1990,8 +1995,11 @@ pub fn refuse_standardised_continuous_diffusion_as_standardised_asymptotic_diffu /// correlation form of `TIPREDVAR`. Equal numbers remain distinct /// named quantities. This crate does not currently export /// `TIPREDVARstd`; the refuse still names that quantity. - /// [`PsychometricError::StandardisedInitialLatentVarianceIsNotStandardisedTraitVariance`]. +/// +/// # Errors +/// +/// Always returns [`PsychometricError::StandardisedInitialLatentVarianceIsNotStandardisedTraitVariance`]. pub fn refuse_standardised_initial_latent_variance_as_standardised_trait_variance( standardised_initial_variance: f64, standardised_trait_variance: f64, @@ -2006,7 +2014,6 @@ pub fn refuse_standardised_initial_latent_variance_as_standardised_trait_varianc /// the correlation form of between-subject `TRAITVAR`. Those are /// not the same map. This crate does not currently export /// `addedT0TIPREDVAR`; the refuse still names that quantity. - /// /// # Errors /// @@ -2029,6 +2036,10 @@ pub fn refuse_standardised_time_independent_predictor_variance_as_standardised_a /// `asymDIFFUSIONstd`-style trait variance misuse. /// /// [`PsychometricError::InitialTimeIndependentVarianceIsNotStandardisedTraitVariance`]. +/// +/// # Errors +/// +/// Always returns [`PsychometricError::InitialTimeIndependentVarianceIsNotStandardisedTraitVariance`]. pub fn refuse_initial_time_independent_variance_as_standardised_trait_variance( initial_predictor_variance: f64, standardised_trait_variance: f64, From 0345e6bac1bf93cdf7603184f705144515950d44 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 22:30:38 +0900 Subject: [PATCH 5/6] merge: resolve main #267 conflict, both families retained Finish the main-merge by unioning remaining conflict markers in error.rs, event_time.rs, and the multilevel test; dedupe duplicated import runs. Branch and line coverage of psychometric_core verified at 100% after resolution. --- crates/psychometric_core/src/error.rs | 13 +++------ crates/psychometric_core/src/event_time.rs | 27 +++++-------------- ...multilevel_event_time_recovery_contract.rs | 18 ++----------- 3 files changed, 12 insertions(+), 46 deletions(-) diff --git a/crates/psychometric_core/src/error.rs b/crates/psychometric_core/src/error.rs index d78eff5b..288e56e4 100644 --- a/crates/psychometric_core/src/error.rs +++ b/crates/psychometric_core/src/error.rs @@ -612,7 +612,6 @@ pub enum PsychometricError { /// process-dynamics `asymDIFFUSION`; `TIPREDVARstd` is the /// correlation form of `TIPREDVAR`. StandardisedTimeIndependentPredictorVarianceIsNotStandardisedAsymptoticDiffusion, -<<<<<<< HEAD /// Driver p. 16 `TRAITVARstd` was requested with a non-positive /// trait variance. The 2017-era source skips forming @@ -634,8 +633,6 @@ pub enum PsychometricError { /// the correlation form of between-subject `TRAITVAR`. InitialTimeIndependentVarianceIsNotStandardisedTraitVariance, -======= ->>>>>>> origin/main /// Driver p. 16 `discreteCINTstd` was requested without a strictly /// positive `asymDIFFUSION`. Footnote 4 standardises using only the /// relevant variance; zero `q` has no positive process SD. @@ -1129,7 +1126,6 @@ impl fmt::Display for PsychometricError { Self::StandardisedTimeIndependentPredictorVarianceIsNotStandardisedAsymptoticDiffusion => { "standardised time-independent predictor variance is not standardised asymptotic diffusion" } -<<<<<<< HEAD Self::StandardisedTraitVarianceRequiresPositiveTraitVariance => { "standardised trait variance requires strictly positive trait variance" } @@ -1142,8 +1138,9 @@ impl fmt::Display for PsychometricError { Self::InitialTimeIndependentVarianceIsNotStandardisedTraitVariance => { "initial time-independent predictor variance is not standardised trait variance" } -======= ->>>>>>> origin/main + + + Self::StandardisedDiscreteContinuousInterceptRequiresPositiveStationaryVariance => { "standardised discrete continuous intercept requires strictly positive stationary within-subject variance" } @@ -1913,7 +1910,6 @@ mod tests { } #[test] -<<<<<<< HEAD fn standardised_trait_variance_boundary_messages_are_stable() { assert_eq!( PsychometricError::StandardisedTraitVarianceRequiresPositiveTraitVariance.to_string(), @@ -1937,8 +1933,7 @@ mod tests { } #[test] -======= ->>>>>>> origin/main + fn standardised_discrete_continuous_intercept_boundary_messages_are_stable() { assert_eq!( PsychometricError::StandardisedDiscreteContinuousInterceptRequiresPositiveStationaryVariance diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index d61c73d7..b36793f1 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -1795,7 +1795,6 @@ pub fn refuse_standardised_asymptotic_diffusion_as_standardised_initial_latent_v /// currently export `DIFFUSIONstd` or `TIPREDVARstd`; the refuse /// still names those quantities. This is not a Kalman filter, not a /// matrix `expm`, not DSEM, and not ctsem estimation. -<<<<<<< HEAD /// Exact scalar p. 16 `TRAITVARstd` after strictly positive `TRAITVAR`. /// /// Driver, Oud, and Voelkle (2017, Table 2, p. 12; §7.1, pp. 18–19; @@ -1835,8 +1834,7 @@ pub fn refuse_standardised_asymptotic_diffusion_as_standardised_initial_latent_v /// export `addedT0TIPREDVAR`; the refuse still names that quantity. /// This is not a Kalman filter, not a matrix `expm`, not DSEM, and /// not ctsem estimation. -======= ->>>>>>> origin/main + /// /// # Errors /// @@ -1872,7 +1870,6 @@ pub fn recover_standardised_asymptotic_diffusion( /// Unstandardised `p` is defined for a zero process. Footnote 4 /// `asymDIFFUSIONstd` requires strictly positive `asymDIFFUSION`. /// Equal numbers when `p = 1` are still distinct named quantities. -<<<<<<< HEAD /// [`PsychometricError::StandardisedTraitVarianceRequiresPositiveTraitVariance`] /// when `TRAITVAR` is zero, and /// [`PsychometricError::InvalidNumericInput`] when the variance is @@ -1908,8 +1905,7 @@ pub fn recover_standardised_trait_variance( /// Unstandardised `TRAITVAR` is defined for a zero trait. Footnote /// 4 `TRAITVARstd` requires strictly positive `TRAITVAR`. Equal /// numbers when `trait = 1` are still distinct named quantities. -======= ->>>>>>> origin/main + /// /// # Errors /// @@ -1933,7 +1929,6 @@ pub fn refuse_unstandardised_asymptotic_diffusion_as_standardised_asymptotic_dif /// process-dynamics `asymDIFFUSION`. `T0VARstd` is the correlation /// form of free first-occasion `T0VAR`. Equal numbers remain /// distinct named quantities. -<<<<<<< HEAD /// [`PsychometricError::UnstandardisedTraitVarianceIsNotStandardisedTraitVariance`]. /// /// # Errors @@ -1953,8 +1948,7 @@ pub fn refuse_unstandardised_trait_variance_as_standardised_trait_variance( /// variances. `T0VARstd` standardises free first-occasion `T0VAR`. /// `TRAITVARstd` standardises between-subject `TRAITVAR`. Equal /// numbers remain distinct named quantities. -======= ->>>>>>> origin/main + /// /// # Errors /// @@ -2004,7 +1998,6 @@ pub fn refuse_standardised_continuous_diffusion_as_standardised_asymptotic_diffu /// correlation form of `TIPREDVAR`. Equal numbers remain distinct /// named quantities. This crate does not currently export /// `TIPREDVARstd`; the refuse still names that quantity. -<<<<<<< HEAD /// [`PsychometricError::StandardisedInitialLatentVarianceIsNotStandardisedTraitVariance`]. /// /// # Errors @@ -2024,8 +2017,7 @@ pub fn refuse_standardised_initial_latent_variance_as_standardised_trait_varianc /// the correlation form of between-subject `TRAITVAR`. Those are /// not the same map. This crate does not currently export /// `addedT0TIPREDVAR`; the refuse still names that quantity. -======= ->>>>>>> origin/main + /// /// # Errors /// @@ -2044,7 +2036,6 @@ pub fn refuse_standardised_time_independent_predictor_variance_as_standardised_a ) } -<<<<<<< HEAD /// Refuse treating p. 16 `TRAITVARstd` as p. 16 /// `asymDIFFUSIONstd`-style trait variance misuse. /// @@ -2061,8 +2052,6 @@ pub fn refuse_initial_time_independent_variance_as_standardised_trait_variance( Err(PsychometricError::InitialTimeIndependentVarianceIsNotStandardisedTraitVariance) } -======= ->>>>>>> origin/main /// Exact scalar discrete intercept increment from Driver Equation 3. /// /// Driver, Oud, and Voelkle (2017, Eq. 3, p. 4; Table 2, p. 12; JSS @@ -15685,7 +15674,6 @@ mod tests { } #[test] -<<<<<<< HEAD fn standardised_trait_variance_recovers_driver_table_two_after_positive_traitvar() { // Driver et al. (2017, Table 2 TRAITVAR; §7.1; p. 16 TRAITVARstd; // 2017-era summary.ctsemFit.R): form strictly positive TRAITVAR, @@ -15727,8 +15715,7 @@ mod tests { } #[test] -======= ->>>>>>> origin/main + fn standardised_asymptotic_diffusion_fails_closed_when_unstandardised_is_defined() { assert_eq!( recover_standardised_asymptotic_diffusion(0.0, -0.25, LagClock::EventTime), @@ -15767,7 +15754,6 @@ mod tests { } #[test] -<<<<<<< HEAD fn standardised_trait_variance_fails_closed_when_unstandardised_is_defined() { assert_eq!( recover_standardised_trait_variance(0.0, LagClock::EventTime), @@ -15792,8 +15778,7 @@ mod tests { } #[test] -======= ->>>>>>> origin/main + fn standardised_discrete_continuous_intercept_fails_closed_when_unstandardised_is_defined() { assert_eq!( recover_standardised_discrete_continuous_intercept( 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 d416f169..8e6f0607 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -38,7 +38,6 @@ use psychometric_core::{ 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, -<<<<<<< HEAD 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, @@ -47,15 +46,6 @@ use psychometric_core::{ 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_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, ->>>>>>> origin/main 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, @@ -6088,7 +6078,6 @@ fn standardised_asymptotic_diffusion_recovers_driver_page_sixteen_correlation() } #[test] -<<<<<<< HEAD fn standardised_trait_variance_recovers_driver_table_two_correlation() { let trait_variance = 1.6_f64; let recovered = recover_standardised_trait_variance(trait_variance, LagClock::EventTime) @@ -6142,8 +6131,7 @@ fn standardised_trait_variance_recovers_driver_table_two_correlation() { } #[test] -======= ->>>>>>> origin/main + fn standardised_asymptotic_diffusion_refuses_non_event_clocks_and_does_not_keep_zero_q() { assert_eq!( recover_standardised_asymptotic_diffusion(0.4, -0.25, LagClock::AssertionTime), @@ -6164,7 +6152,6 @@ fn standardised_asymptotic_diffusion_refuses_non_event_clocks_and_does_not_keep_ } #[test] -<<<<<<< HEAD fn standardised_trait_variance_refuses_non_event_clocks_and_does_not_keep_zero_variance() { assert_eq!( recover_standardised_trait_variance(1.6, LagClock::AssertionTime), @@ -6181,8 +6168,7 @@ fn standardised_trait_variance_refuses_non_event_clocks_and_does_not_keep_zero_v } #[test] -======= ->>>>>>> origin/main + fn standardised_discrete_continuous_intercept_recovers_driver_page_sixteen_after_positive_p() { let intercept = 0.4_f64; let diffusion = 0.8_f64; From 0884254cb54462e7c80846860fc7672bce0744b5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 23:08:54 +0900 Subject: [PATCH 6/6] fix(doc): remove stray blank lines after doc comments The main-reconcile resolve restored pre-clippy doc blocks with trailing blank lines; strip them so clippy::doc-markdown pedantic passes. --- crates/psychometric_core/src/event_time.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index b36793f1..0e97be15 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -1834,7 +1834,6 @@ pub fn refuse_standardised_asymptotic_diffusion_as_standardised_initial_latent_v /// export `addedT0TIPREDVAR`; the refuse still names that quantity. /// This is not a Kalman filter, not a matrix `expm`, not DSEM, and /// not ctsem estimation. - /// /// # Errors /// @@ -1905,7 +1904,6 @@ pub fn recover_standardised_trait_variance( /// Unstandardised `TRAITVAR` is defined for a zero trait. Footnote /// 4 `TRAITVARstd` requires strictly positive `TRAITVAR`. Equal /// numbers when `trait = 1` are still distinct named quantities. - /// /// # Errors /// @@ -1948,7 +1946,6 @@ pub fn refuse_unstandardised_trait_variance_as_standardised_trait_variance( /// variances. `T0VARstd` standardises free first-occasion `T0VAR`. /// `TRAITVARstd` standardises between-subject `TRAITVAR`. Equal /// numbers remain distinct named quantities. - /// /// # Errors /// @@ -2017,7 +2014,6 @@ pub fn refuse_standardised_initial_latent_variance_as_standardised_trait_varianc /// the correlation form of between-subject `TRAITVAR`. Those are /// not the same map. This crate does not currently export /// `addedT0TIPREDVAR`; the refuse still names that quantity. - /// /// # Errors ///