diff --git a/CHANGELOG.md b/CHANGELOG.md index 062a69412..02acd11b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,9 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang - `event_core` adds bounded Allen interval-consistency classification, atomic path-consistency closure, contradiction/resource refusals, and an explicit dependency-error fallback without claiming unrestricted global satisfiability. +- `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Table 2, p. 12 `TIPREDVAR`; p. 16 `TIPREDVARstd`; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-30T16:50Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar standardised time-independent predictor variance on current main after `0ce16e8` dropped the pre-consolidation code while research notes already named the map (register items 87–88). Table 2 names `TIPREDVAR` the `n.TIpred × n.TIpred` Cholesky matrix of variance/covariance between time-independent predictors and defaults it to 1. Page 16 prints standardised matrices with the suffix `std` when appropriate. The printed example on p. 16 is `discreteDRIFTstd`, not `TIPREDVARstd`. Footnote 4 standardises using only the relevant variance, not the total. The relevant variance for that named predictor correlation is `TIPREDVAR` itself, not §7.2 `addedTIPREDVAR` `(B / a)² v` and not residual `MANIFESTVAR` `θ`. The 2017-era source forms `TIPREDVARstd` as `solve(sqrt(diag(TIPREDVAR))) %&% TIPREDVAR` when `verbose = TRUE`. OpenMx `%&%` is `t(A) %*% B %*% A`. The 2017-era source adds ridging; the default ridge is 0; `dimnames` are `TIpredNames`. The scalar correlation is `v / v = 1` after strictly positive `TIPREDVAR`. Form strictly positive `v` first, then `1 / √v`, then `(1 / √v) v (1 / √v)`. Unstandardised `TIPREDVAR` is defined for a zero predictor; standardised `TIPREDVAR` is not. Zero `v` has no positive SD and fails closed. Predictor variance is an event-time structural quantity, so a non-event clock fails closed. `TIPREDVAR` does not require stable `a < 0`. Distinct positive `v` recover the same 1. `θ / θ = 1` is `MANIFESTVARstd` and recovers the same number and remains a distinct named quantity. `(B / a)² v` is `addedTIPREDVAR` and is not this correlation. Meredith (1993) remains unread (Unpaywall historically `is_oa: false`; Springer `content/pdf` is an HTML stub). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread on the same terms (DOI `10.1007/bf02294457`). Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. + + - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Table 2, p. 12 `MANIFESTTRAITVAR`; §7.1, p. 19; p. 16 `MANIFESTTRAITVARstd`; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-27T14:20Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar standardised manifest-trait variance on current main after `0ce16e8` dropped the pre-consolidation code while research notes already named the map (register items 83–84). Table 2 names `MANIFESTTRAITVAR` `Ψ_τ` the additional time-invariant variance-covariance on the measurement level and sets it `NULL` when there is no manifest trait. Equation 5 writes `Γ ~ N(τ, Ψ)` and names that covariance the manifest traits. Section 7.1 names manifest traits stable individual differences in indicator levels, distinct from process-level `TRAITVAR` `φ_ξ`. Page 16 prints standardised matrices with the suffix `std` when appropriate. The printed example on p. 16 is `discreteDRIFTstd`, not `MANIFESTTRAITVARstd`. Footnote 4 standardises using only the relevant variance, not the total. The relevant variance for that named indicator-level correlation is `MANIFESTTRAITVAR`, not process-level `TRAITVAR` and not residual `MANIFESTVAR` `θ`. The 2017-era source forms `MANIFESTTRAITVARstd` only when `MANIFESTTRAITVAR != 0`, as `solve(sqrt(diag(MANIFESTTRAITVAR) + ridging)) %&% MANIFESTTRAITVAR` when `verbose = TRUE`. OpenMx `%&%` is `t(A) %*% B %*% A`. Unlike `TRAITVARstd`, that formation adds `diag(c(ridging), n.manifest)`. The default `ridging = FALSE` adds 0, not `0.0001`; that ridge is a numerical hack and is not this exact map. The scalar correlation is `ψ / ψ = 1` after strictly positive `MANIFESTTRAITVAR`. Form strictly positive `ψ` first, then `1 / √ψ`, then `(1 / √ψ) ψ (1 / √ψ)`. Unstandardised `MANIFESTTRAITVAR` is defined for a zero trait; standardised `MANIFESTTRAITVAR` is not. Zero `MANIFESTTRAITVAR` skips forming `MANIFESTTRAITVARstd` in the 2017-era source and fails closed here. Indicator-level trait variance is an event-time structural quantity, so a non-event clock fails closed. `MANIFESTTRAITVAR` does not require stable `a < 0`. Distinct positive `ψ` recover the same 1. `trait / trait = 1` is `TRAITVARstd` and recovers the same number and remains a distinct named quantity. `θ` is `MANIFESTVAR` and is measurement error, not this correlation. Meredith (1993) remains unread (web search 2026-08-27T14:20Z: Springer/Cambridge Core paywalled; Unpaywall historically `is_oa: false`; Springer `content/pdf` is an HTML stub). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread on the same terms (DOI `10.1007/bf02294457`). Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Table 2, p. 12 `TRAITVAR`; §7.1, pp. 18–19; p. 16 `TRAITVARstd`; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-26T17:45Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar standardised trait variance on current main after `0ce16e8` dropped the pre-consolidation code while research notes already named the map (register items 81–82). Table 2 names `TRAITVAR` `φ_ξ` the latent trait variance/covariance and sets it `NULL` when there is no trait. Section 7.1 names traits the stable between-subject differences (unit-level unobserved heterogeneity). Page 16 prints standardised matrices with the suffix `std` when appropriate. The printed example on p. 16 is `discreteDRIFTstd`, not `TRAITVARstd`. Footnote 4 standardises using only the relevant variance, not the total. The relevant variance for that named between-subject correlation is `TRAITVAR`, not free first-occasion `T0VAR` and not process-dynamics `asymDIFFUSION`. The 2017-era source forms `TRAITVARstd` only when `TRAITVAR != 0`, as `solve(sqrt(diag(TRAITVAR))) %&% TRAITVAR` when `verbose = TRUE`. OpenMx `%&%` is `t(A) %*% B %*% A`. Unlike `T0VARstd`, that formation uses `diag(diag(TRAITVAR))` and does not add `diag(c(ridging))`. The ridge is a `T0VAR` numerical hack and is not this exact map. The scalar correlation is `trait / trait = 1` after strictly positive `TRAITVAR`. Form strictly positive `trait` first, then `1 / √trait`, then `(1 / √trait) trait (1 / √trait)`. Unstandardised `TRAITVAR` is defined for a zero trait; standardised `TRAITVAR` is not. Zero `TRAITVAR` skips forming `TRAITVARstd` in the 2017-era source and fails closed here. Between-subject variance is an event-time structural quantity, so a non-event clock fails closed. `TRAITVAR` does not require stable `a < 0`. Distinct positive `trait` recover the same 1. `p_0 / p_0 = 1` is `T0VARstd` and recovers the same number and remains a distinct named quantity. `t0_b² v` is `addedT0TIPREDVAR` and is extra first-occasion TI variance, not this correlation. Meredith (1993) remains unread (Unpaywall 2026-08-26T17:20Z: `is_oa: false`; OpenAlex closed; Springer `content/pdf` is an HTML stub). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread on the same terms (DOI `10.1007/bf02294457`; Unpaywall `is_oa: false`). Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. diff --git a/crates/psychometric_core/src/error.rs b/crates/psychometric_core/src/error.rs index 4ab2695e0..36209afe2 100644 --- a/crates/psychometric_core/src/error.rs +++ b/crates/psychometric_core/src/error.rs @@ -633,6 +633,29 @@ pub enum PsychometricError { /// the correlation form of between-subject `TRAITVAR`. InitialTimeIndependentVarianceIsNotStandardisedTraitVariance, + /// Driver p. 16 `TIPREDVARstd` was requested with a non-positive + /// time-independent predictor variance. The 2017-era source + /// forms `TIPREDVARstd` as `solve(sqrt(diag(TIPREDVAR))) %&% + /// TIPREDVAR`; footnote 4 standardisation requires strictly + /// positive `TIPREDVAR`. + StandardisedTimeIndependentPredictorVarianceRequiresPositivePredictorVariance, + /// Driver Table 2 unstandardised `TIPREDVAR` `v` was treated as + /// p. 16 `TIPREDVARstd`. Unstandardised `v` is defined for a + /// zero predictor; standardised `TIPREDVAR` is not. + UnstandardisedTimeIndependentPredictorVarianceIsNotStandardisedTimeIndependentPredictorVariance, + /// Driver p. 16 `MANIFESTVARstd` was treated as p. 16 + /// `TIPREDVARstd`. Equal numbers of 1 after a strictly positive + /// relevant variance are still distinct named quantities. + /// `TIPREDVARstd` is the correlation form of `TIPREDVAR`; + /// `MANIFESTVARstd` is the correlation form of residual + /// `MANIFESTVAR`. + StandardisedManifestVarianceIsNotStandardisedTimeIndependentPredictorVariance, + /// Driver §7.2 `addedTIPREDVAR` `(B / a)² v` was treated as + /// p. 16 `TIPREDVARstd`. Extra process variance accounted for + /// by a time-independent predictor is not the correlation form + /// of `TIPREDVAR`. + AsymptoticTimeIndependentPredictorVarianceIsNotStandardisedTimeIndependentPredictorVariance, + /// 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. @@ -1183,6 +1206,18 @@ impl fmt::Display for PsychometricError { Self::InitialTimeIndependentVarianceIsNotStandardisedTraitVariance => { "initial time-independent predictor variance is not standardised trait variance" } + Self::StandardisedTimeIndependentPredictorVarianceRequiresPositivePredictorVariance => { + "standardised time-independent predictor variance requires strictly positive time-independent predictor variance" + } + Self::UnstandardisedTimeIndependentPredictorVarianceIsNotStandardisedTimeIndependentPredictorVariance => { + "unstandardised time-independent predictor variance is not standardised time-independent predictor variance" + } + Self::StandardisedManifestVarianceIsNotStandardisedTimeIndependentPredictorVariance => { + "standardised manifest variance is not standardised time-independent predictor variance" + } + Self::AsymptoticTimeIndependentPredictorVarianceIsNotStandardisedTimeIndependentPredictorVariance => { + "asymptotic time-independent predictor variance is not standardised time-independent predictor variance" + } @@ -2002,6 +2037,30 @@ mod tests { ); } + #[test] + fn standardised_time_independent_predictor_variance_boundary_messages_are_stable() { + assert_eq!( + PsychometricError::StandardisedTimeIndependentPredictorVarianceRequiresPositivePredictorVariance + .to_string(), + "standardised time-independent predictor variance requires strictly positive time-independent predictor variance" + ); + assert_eq!( + PsychometricError::UnstandardisedTimeIndependentPredictorVarianceIsNotStandardisedTimeIndependentPredictorVariance + .to_string(), + "unstandardised time-independent predictor variance is not standardised time-independent predictor variance" + ); + assert_eq!( + PsychometricError::StandardisedManifestVarianceIsNotStandardisedTimeIndependentPredictorVariance + .to_string(), + "standardised manifest variance is not standardised time-independent predictor variance" + ); + assert_eq!( + PsychometricError::AsymptoticTimeIndependentPredictorVarianceIsNotStandardisedTimeIndependentPredictorVariance + .to_string(), + "asymptotic time-independent predictor variance is not standardised time-independent predictor variance" + ); + } + #[test] fn standardised_discrete_continuous_intercept_boundary_messages_are_stable() { diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index a29bc5c18..e20aef48a 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -2171,6 +2171,140 @@ pub fn refuse_standardised_time_independent_predictor_variance_as_standardised_a ) } +/// Exact scalar p. 16 `TIPREDVARstd` after strictly positive +/// `TIPREDVAR`. +/// +/// Driver, Oud, and Voelkle (2017, Table 2, p. 12; p. 16; +/// footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF +/// re-opened 2026-08-30T16:50Z from +/// ) +/// name `TIPREDVAR` the `n.TIpred × n.TIpred` Cholesky matrix of +/// variance/covariance between time-independent predictors. +/// Table 2 defaults it to 1. Page 16 prints standardised matrices +/// with the suffix `std` when appropriate. The printed example on +/// p. 16 is `discreteDRIFTstd`, not `TIPREDVARstd`. Footnote 4: +/// standardisations use only the relevant variance, not the total. +/// The relevant variance for that named predictor correlation is +/// `TIPREDVAR` itself, not `addedTIPREDVAR` `(B / a)² v` and not +/// residual `MANIFESTVAR` `θ`. The 2017-era `summary.ctsemFit.R` +/// forms `TIPREDVARstd` as +/// `solve(sqrt(diag(TIPREDVAR))) %&% TIPREDVAR` when +/// `verbose = TRUE`. `OpenMx` `%&%` is the quadratic form +/// `t(A) %*% B %*% A`. The 2017-era source adds ridging; the +/// default ridge is 0. `dimnames` are `TIpredNames`. The scalar +/// correlation is `v / v = 1` after strictly positive `TIPREDVAR`. +/// Form strictly positive `v` first, then `1 / √v`, then +/// `(1 / √v) v (1 / √v)`. Unstandardised `TIPREDVAR` is defined +/// for a zero predictor; standardised `TIPREDVAR` is not. Zero +/// `v` has no positive SD and fails closed. Predictor variance is +/// an event-time structural quantity, so a non-event clock fails +/// closed. `TIPREDVAR` does not require stable `a < 0`. Distinct +/// positive `v` recover the same 1. `MANIFESTVARstd` `θ / θ = 1` +/// recovers the same number and remains a distinct named +/// quantity. `(B / a)² v` is `addedTIPREDVAR` and is not this +/// correlation. 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::StandardisedTimeIndependentPredictorVarianceRequiresPositivePredictorVariance`] +/// when `TIPREDVAR` is zero, and +/// [`PsychometricError::InvalidNumericInput`] when the variance is +/// non-finite, negative, or the quadratic form overflows. +pub fn recover_standardised_time_independent_predictor_variance( + time_independent_predictor_variance: f64, + clock: LagClock, +) -> Result { + if !clock.admits_structural_lag() { + return Err(PsychometricError::EventTimeRequired); + } + if !time_independent_predictor_variance.is_finite() || time_independent_predictor_variance < 0.0 + { + return Err(PsychometricError::InvalidNumericInput); + } + if time_independent_predictor_variance == 0.0 { + return Err( + PsychometricError::StandardisedTimeIndependentPredictorVarianceRequiresPositivePredictorVariance, + ); + } + let predictor_sd = time_independent_predictor_variance.sqrt(); + let inverse_sd = require_finite(1.0 / predictor_sd)?; + let scaled = require_finite(inverse_sd * time_independent_predictor_variance)?; + require_finite(scaled * inverse_sd) +} + +/// Refuse treating unstandardised `TIPREDVAR` as p. 16 +/// `TIPREDVARstd`. +/// +/// Unstandardised `v` is defined for a zero predictor. Footnote 4 +/// `TIPREDVARstd` requires strictly positive `TIPREDVAR`. Equal +/// numbers when `v = 1` are still distinct named quantities. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::UnstandardisedTimeIndependentPredictorVarianceIsNotStandardisedTimeIndependentPredictorVariance`]. +pub fn refuse_unstandardised_time_independent_predictor_variance_as_standardised_time_independent_predictor_variance( + unstandardised_predictor_variance: f64, + standardised_predictor_variance: f64, +) -> Result { + let _ = ( + unstandardised_predictor_variance, + standardised_predictor_variance, + ); + Err( + PsychometricError::UnstandardisedTimeIndependentPredictorVarianceIsNotStandardisedTimeIndependentPredictorVariance, + ) +} + +/// Refuse treating p. 16 `MANIFESTVARstd` as p. 16 `TIPREDVARstd`. +/// +/// Both scalar maps equal 1 after a strictly positive relevant +/// variance. `TIPREDVARstd` is the correlation form of `TIPREDVAR`. +/// `MANIFESTVARstd` is the correlation form of residual +/// `MANIFESTVAR`. Equal numbers remain distinct named quantities. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::StandardisedManifestVarianceIsNotStandardisedTimeIndependentPredictorVariance`]. +pub fn refuse_standardised_manifest_variance_as_standardised_time_independent_predictor_variance( + standardised_manifest_variance: f64, + standardised_predictor_variance: f64, +) -> Result { + let _ = ( + standardised_manifest_variance, + standardised_predictor_variance, + ); + Err( + PsychometricError::StandardisedManifestVarianceIsNotStandardisedTimeIndependentPredictorVariance, + ) +} + +/// Refuse treating §7.2 `addedTIPREDVAR` as p. 16 `TIPREDVARstd`. +/// +/// Extra process variance attributable to a time-independent +/// predictor is not the correlation form of `TIPREDVAR`. +/// +/// # Errors +/// +/// Always returns +/// [`PsychometricError::AsymptoticTimeIndependentPredictorVarianceIsNotStandardisedTimeIndependentPredictorVariance`]. +pub fn refuse_asymptotic_time_independent_predictor_variance_as_standardised_time_independent_predictor_variance( + asymptotic_predictor_variance: f64, + standardised_predictor_variance: f64, +) -> Result { + let _ = ( + asymptotic_predictor_variance, + standardised_predictor_variance, + ); + Err( + PsychometricError::AsymptoticTimeIndependentPredictorVarianceIsNotStandardisedTimeIndependentPredictorVariance, + ) +} + /// Exact scalar p. 16 `MANIFESTTRAITVARstd` after strictly positive /// `MANIFESTTRAITVAR`. /// @@ -6853,8 +6987,8 @@ pub(crate) fn fit_scalar_log_rate(pairs: &[(f64, f64, f64)]) -> Result 1e-3); + assert!((predictor_variance - recovered).abs() > 1e-3); + assert_eq!( + refuse_unstandardised_time_independent_predictor_variance_as_standardised_time_independent_predictor_variance( + predictor_variance, + recovered + ), + Err( + PsychometricError::UnstandardisedTimeIndependentPredictorVarianceIsNotStandardisedTimeIndependentPredictorVariance + ) + ); + assert_eq!( + refuse_standardised_manifest_variance_as_standardised_time_independent_predictor_variance( + manifest_std, + recovered + ), + Err( + PsychometricError::StandardisedManifestVarianceIsNotStandardisedTimeIndependentPredictorVariance + ) + ); + assert_eq!( + refuse_asymptotic_time_independent_predictor_variance_as_standardised_time_independent_predictor_variance( + added, + recovered + ), + Err( + PsychometricError::AsymptoticTimeIndependentPredictorVarianceIsNotStandardisedTimeIndependentPredictorVariance + ) + ); + } + + #[test] + fn standardised_time_independent_predictor_variance_fails_closed_when_unstandardised_is_defined( + ) { + assert_eq!( + recover_standardised_time_independent_predictor_variance(0.0, LagClock::EventTime), + Err( + PsychometricError::StandardisedTimeIndependentPredictorVarianceRequiresPositivePredictorVariance + ) + ); + assert_eq!( + recover_standardised_time_independent_predictor_variance(1.6, LagClock::SystemTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_standardised_time_independent_predictor_variance(-1.6, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_time_independent_predictor_variance(f64::NAN, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_time_independent_predictor_variance( + f64::INFINITY, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + } } diff --git a/crates/psychometric_core/src/lib.rs b/crates/psychometric_core/src/lib.rs index c081a63f6..6e13ab889 100644 --- a/crates/psychometric_core/src/lib.rs +++ b/crates/psychometric_core/src/lib.rs @@ -261,6 +261,19 @@ //! correlation; zero `MANIFESTTRAITVAR` fails closed; a non-event //! clock fails closed; `MANIFESTTRAITVAR` does not require `a < 0`; //! JSS PDF re-opened 2026-08-27T14:20Z), +//! recovers the Driver p. 16 `TIPREDVARstd` as `v / v = 1` after +//! strictly positive `TIPREDVAR` (Table 2 names `TIPREDVAR` the +//! TIpred variance/covariance, default 1; footnote 4 uses only the +//! relevant predictor variance; 2017-era `summary.ctsemFit.R` forms +//! `TIPREDVARstd` as `solve(sqrt(diag(TIPREDVAR))) %&% TIPREDVAR`; +//! `OpenMx` `%&%` is `t(A) %*% B %*% A`; default ridge is 0; +//! `dimnames` are `TIpredNames`; unstandardised `v` is defined for +//! a zero predictor and is not that map; `θ / θ = 1` is +//! `MANIFESTVARstd` and is not that map even when both equal 1; +//! `(B / a)² v` is `addedTIPREDVAR` and is not that correlation; +//! zero `v` fails closed; a non-event clock fails closed; +//! `TIPREDVAR` does not require `a < 0`; JSS PDF re-opened +//! 2026-08-30T16:50Z), //! and refuses //! latent-mean comparison below strong invariance. @@ -275,40 +288,30 @@ mod loading; mod plausible; mod rubin_total; -/// A heuristic that is not causal identification. -pub use causality::CausalHeuristic; /// Refuse a causal-effect claim from a non-identifying heuristic. pub use causality::claim_causal_effect; -/// One clustered predictor–outcome pair. -pub use cluster_mean::ClusteredScore; -/// Recovered within-cluster, between-cluster, and contextual OLS slopes. -pub use cluster_mean::WithinBetweenSlopes; +/// A heuristic that is not causal identification. +pub use causality::CausalHeuristic; /// Kish effective sample size on psychometric weights. pub use cluster_mean::kish_effective_sample_size; /// Cluster-mean within/between OLS after CWC, plus the contextual effect. pub use cluster_mean::recover_cluster_mean_within_between_slopes; /// Kish-weighted least-squares slope. pub use cluster_mean::recover_kish_weighted_slope; -/// Higher-order construct class. -pub use construct::ConstructClass; -/// Typed invariance evidence required before a latent-mean comparison. -pub use construct::LatentMeanComparisonEvidence; +/// One clustered predictor–outcome pair. +pub use cluster_mean::ClusteredScore; +/// Recovered within-cluster, between-cluster, and contextual OLS slopes. +pub use cluster_mean::WithinBetweenSlopes; /// Permit latent-mean comparison only on strong/strict typed evidence. pub use construct::compare_latent_means; /// Refuse fit-driven reinterpretation as reflective. pub use construct::interpret_as_reflective; +/// Higher-order construct class. +pub use construct::ConstructClass; +/// Typed invariance evidence required before a latent-mean comparison. +pub use construct::LatentMeanComparisonEvidence; /// Fail-closed psychometric errors. pub use error::PsychometricError; -/// One clustered event-time score. -pub use event_time::ClusteredEventScore; -/// Discrete lag-1 coefficient and local log-rate. -pub use event_time::DiscreteLagAndLogRate; -/// One event-time occasion. -pub use event_time::EventOccasion; -/// Clock on which a structural lag may be computed. -pub use event_time::LagClock; -/// Already-centered lagged residual pair with an irregular event interval. -pub use event_time::LaggedWithinResidual; /// Map a discrete lag onto another event interval through the exact log-rate. pub use event_time::map_discrete_lag_across_event_intervals; /// Exact scalar Table 2 `asymCINT` `-κ / a`. @@ -417,7 +420,19 @@ pub use event_time::recover_standardised_manifest_mean; pub use event_time::recover_standardised_manifest_trait_variance; /// Exact scalar p. 16 `MANIFESTVARstd` `θ/...` after strictly positive `MANIFESTVAR`. pub use event_time::recover_standardised_manifest_variance; +/// One clustered event-time score. +pub use event_time::ClusteredEventScore; +/// Discrete lag-1 coefficient and local log-rate. +pub use event_time::DiscreteLagAndLogRate; +/// One event-time occasion. +pub use event_time::EventOccasion; +/// Clock on which a structural lag may be computed. +pub use event_time::LagClock; +/// Already-centered lagged residual pair with an irregular event interval. +pub use event_time::LaggedWithinResidual; +/// Exact scalar p. 16 `TIPREDVARstd` `v / v = 1` after strictly positive `TIPREDVAR`. +pub use event_time::recover_standardised_time_independent_predictor_variance; /// 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`. @@ -474,6 +489,8 @@ pub use event_time::refuse_asymptotic_time_independent_effect_as_continuous_inte pub use event_time::refuse_asymptotic_time_independent_effect_as_discrete_effect; /// Refuse treating §7.2 `asymTIPREDEFFECT` as `M x`. pub use event_time::refuse_asymptotic_time_independent_effect_as_time_dependent_impulse; +/// Refuse treating §7.2 `addedTIPREDVAR` as `TIPREDVARstd`. +pub use event_time::refuse_asymptotic_time_independent_predictor_variance_as_standardised_time_independent_predictor_variance; /// Refuse treating §7.2 `addedTIPREDVAR` as `asymTIPREDEFFECT`. pub use event_time::refuse_asymptotic_time_independent_variance_as_asymptotic_effect; /// Refuse treating §7.2 `addedTIPREDVAR` as `asymDIFFUSION`. @@ -636,6 +653,8 @@ pub use event_time::refuse_standardised_initial_latent_variance_as_standardised_ pub use event_time::refuse_standardised_manifest_trait_variance_as_standardised_manifest_variance; /// Refuse treating `MANIFESTVARstd` as `MANIFESTMEANSstd`. pub use event_time::refuse_standardised_manifest_variance_as_standardised_manifest_mean; +/// Refuse treating p. 16 `MANIFESTVARstd` as `TIPREDVARstd`. +pub use event_time::refuse_standardised_manifest_variance_as_standardised_time_independent_predictor_variance; /// Refuse treating observed θ as p. 16 `MANIFESTVARstd`. pub use event_time::refuse_observed_variance_as_standardised_manifest_variance; @@ -744,39 +763,41 @@ pub use event_time::refuse_unstandardised_manifest_trait_variance_as_standardise /// Refuse treating unstandardised `MANIFESTVAR` as p. 16 `MANIFESTVARstd`. pub use event_time::refuse_unstandardised_manifest_variance_as_standardised_manifest_variance; +/// Refuse treating unstandardised `TIPREDVAR` as `TIPREDVARstd`. +pub use event_time::refuse_unstandardised_time_independent_predictor_variance_as_standardised_time_independent_predictor_variance; /// 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; -/// Indicator coordinate kind. -pub use indicator::IndicatorKind; /// Pearson correlation on valid coordinates. pub use indicator::pearson_correlation; /// Refuse raw topic proportions as psychometric indicators. pub use indicator::require_valid_indicator; +/// Indicator coordinate kind. +pub use indicator::IndicatorKind; +/// Classify two-group OLS invariance. +pub use latent_mean::classify_two_group_ols_invariance; +/// Strong/strict-gated latent-mean difference. +pub use latent_mean::recover_strong_gated_latent_mean_difference; /// One group's factor-score and indicator series. pub use latent_mean::GroupIndicatorSeries; /// Two-group OLS invariance status for a mean comparison. pub use latent_mean::MeanInvarianceStatus; /// Two-group OLS measurement parameters and status. pub use latent_mean::TwoGroupMeasurement; -/// Classify two-group OLS invariance. -pub use latent_mean::classify_two_group_ols_invariance; -/// Strong/strict-gated latent-mean difference. -pub use latent_mean::recover_strong_gated_latent_mean_difference; -/// Ordinary least-squares intercept, slope, and residual variance. -pub use loading::OrdinaryLeastSquaresFit; /// Ordinary least-squares intercept and slope with residual variance. pub use loading::ordinary_least_squares_fit; /// Ordinary least-squares slope. pub use loading::ordinary_least_squares_slope; /// Recover one reflective loading. pub use loading::recover_reflective_loading; +/// Ordinary least-squares intercept, slope, and residual variance. +pub use loading::OrdinaryLeastSquaresFit; /// Arithmetic mean of posterior-draw point estimates. pub use plausible::posterior_draw_point_estimate_mean; /// Average OLS loading point estimates across posterior indicator draws. pub use plausible::recover_loading_point_estimate_mean; -/// Rubin-combined OLS loading and total variance. -pub use rubin_total::RubinCombinedLoading; /// Combine OLS loadings across draws with Rubin `T`. pub use rubin_total::combine_draw_level_ols_loadings; +/// Rubin-combined OLS loading and total variance. +pub use rubin_total::RubinCombinedLoading; diff --git a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs index 1c0027f44..fbcc9cf4f 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -2,10 +2,8 @@ #![allow(clippy::cast_precision_loss)] use psychometric_core::{ - ClusteredEventScore, ClusteredScore, EventOccasion, IndicatorKind, LagClock, - LaggedWithinResidual, PsychometricError, map_discrete_lag_across_event_intervals, - ordinary_least_squares_slope, recover_asymptotic_continuous_intercept, - recover_asymptotic_time_independent_predictor_effect, + map_discrete_lag_across_event_intervals, ordinary_least_squares_slope, + recover_asymptotic_continuous_intercept, recover_asymptotic_time_independent_predictor_effect, recover_asymptotic_time_independent_predictor_variance, recover_cluster_mean_within_between_slopes, recover_discrete_constant_predictor_effect, recover_discrete_continuous_intercept_effect, recover_discrete_lag_from_log_rate, @@ -39,14 +37,14 @@ use psychometric_core::{ recover_standardised_discrete_continuous_intercept, recover_standardised_initial_latent_mean, recover_standardised_initial_latent_variance, recover_standardised_manifest_mean, recover_standardised_manifest_trait_variance, recover_standardised_manifest_variance, - recover_standardised_trait_variance, recover_stationary_initial_latent_mean, - recover_stationary_initial_latent_variance, recover_stationary_initial_observed_mean, - recover_stationary_initial_observed_variance, recover_stationary_lagged_latent_covariance, - recover_stationary_lagged_observed_covariance, recover_stationary_latent_variance, - recover_stationary_later_latent_variance, recover_stationary_later_observed_variance, - recover_time_dependent_predictor_impulse, recover_time_dependent_predictor_impulse_carry, - recover_trait_plus_state_lagged_covariance, recover_trait_plus_state_latent_variance, - recover_within_residual_event_time_log_rate, + recover_standardised_time_independent_predictor_variance, recover_standardised_trait_variance, + recover_stationary_initial_latent_mean, recover_stationary_initial_latent_variance, + recover_stationary_initial_observed_mean, recover_stationary_initial_observed_variance, + recover_stationary_lagged_latent_covariance, recover_stationary_lagged_observed_covariance, + recover_stationary_latent_variance, recover_stationary_later_latent_variance, + recover_stationary_later_observed_variance, recover_time_dependent_predictor_impulse, + recover_time_dependent_predictor_impulse_carry, recover_trait_plus_state_lagged_covariance, + recover_trait_plus_state_latent_variance, recover_within_residual_event_time_log_rate, refuse_after_extra_process_contribution_as_observed_mean, refuse_after_extra_process_latent_mean_as_observed_mean, refuse_asymptotic_continuous_intercept_as_asymptotic_time_independent_effect, @@ -58,6 +56,7 @@ use psychometric_core::{ refuse_asymptotic_time_independent_effect_as_continuous_intercept, refuse_asymptotic_time_independent_effect_as_discrete_effect, refuse_asymptotic_time_independent_effect_as_time_dependent_impulse, + refuse_asymptotic_time_independent_predictor_variance_as_standardised_time_independent_predictor_variance, refuse_asymptotic_time_independent_variance_as_asymptotic_effect, refuse_asymptotic_time_independent_variance_as_stationary_within_subject, refuse_asymptotic_time_independent_variance_as_trait_variance, @@ -122,6 +121,7 @@ use psychometric_core::{ refuse_process_noise_as_unconditional_variance, refuse_standardised_initial_latent_variance_as_standardised_trait_variance, refuse_standardised_manifest_trait_variance_as_standardised_manifest_variance, + refuse_standardised_manifest_variance_as_standardised_time_independent_predictor_variance, refuse_standardised_trait_variance_as_standardised_manifest_trait_variance, refuse_stationary_initial_latent_mean_as_asymptotic_continuous_intercept, refuse_stationary_initial_latent_mean_as_asymptotic_time_independent_effect, @@ -164,7 +164,10 @@ use psychometric_core::{ refuse_unmatched_time_varying_predictor_interval, refuse_unstandardised_manifest_trait_variance_as_standardised_manifest_trait_variance, refuse_unstandardised_manifest_variance_as_standardised_manifest_variance, - refuse_unstandardised_trait_variance_as_standardised_trait_variance, + refuse_unstandardised_time_independent_predictor_variance_as_standardised_time_independent_predictor_variance, + refuse_unstandardised_trait_variance_as_standardised_trait_variance, ClusteredEventScore, + ClusteredScore, EventOccasion, IndicatorKind, LagClock, LaggedWithinResidual, + PsychometricError, }; fn rmse(truth: &[f64], recovered: &[f64]) -> f64 { @@ -2239,8 +2242,8 @@ fn discrete_observed_mean_with_initial_time_independent_predictor_is_not_impulse } #[test] -fn discrete_observed_mean_with_initial_time_independent_predictor_refuses_evolved_process_impulse_and_carry() - { +fn discrete_observed_mean_with_initial_time_independent_predictor_refuses_evolved_process_impulse_and_carry( +) { let loading = 2.0_f64; let drift = -0.5_f64; let delta = 2.0_f64; @@ -2394,8 +2397,8 @@ fn discrete_observed_mean_with_initial_time_independent_predictor_zero_loading_i } #[test] -fn discrete_observed_mean_with_initial_time_independent_predictor_refuses_overflow_and_non_event_clocks() - { +fn discrete_observed_mean_with_initial_time_independent_predictor_refuses_overflow_and_non_event_clocks( +) { assert_eq!( recover_discrete_observed_mean_with_initial_time_independent_predictor( 1e308, @@ -3326,8 +3329,8 @@ fn discrete_observed_mean_with_initial_time_dependent_predictor_is_not_impulse_o #[test] #[allow(clippy::too_many_lines)] -fn discrete_observed_mean_with_initial_time_dependent_predictor_refuses_evolved_process_impulse_and_carry() - { +fn discrete_observed_mean_with_initial_time_dependent_predictor_refuses_evolved_process_impulse_and_carry( +) { let loading = 2.0_f64; let drift = -0.5_f64; let delta = 2.0_f64; @@ -3500,8 +3503,8 @@ fn discrete_observed_mean_with_initial_time_dependent_predictor_zero_loading_is_ } #[test] -fn discrete_observed_mean_with_initial_time_dependent_predictor_refuses_overflow_and_non_event_clocks() - { +fn discrete_observed_mean_with_initial_time_dependent_predictor_refuses_overflow_and_non_event_clocks( +) { assert_eq!( recover_discrete_observed_mean_with_initial_time_dependent_predictor( 1e308, @@ -6540,3 +6543,133 @@ fn manifest_variance_std_clock_path_is_runtime_opaque() { Err(PsychometricError::EventTimeRequired) ); } + +#[test] +fn standardised_time_independent_predictor_variance_recovers_driver_table_two_correlation() { + let predictor_variance = 1.6_f64; + let recovered = recover_standardised_time_independent_predictor_variance( + predictor_variance, + LagClock::EventTime, + ) + .expect("TIPREDVARstd"); + let recovered_error = (recovered - 1.0).abs(); + assert!( + recovered_error < 1e-15, + "Driver et al. (2017, p. 16 TIPREDVARstd): RMSE {recovered_error} for v / v = 1" + ); + let larger_v = + recover_standardised_time_independent_predictor_variance(6.4, LagClock::EventTime) + .expect("TIPREDVARstd v=6.4"); + assert_eq!( + larger_v.to_bits(), + recovered.to_bits(), + "Driver et al. (2017, p. 16): distinct positive TIPREDVAR recover the same TIPREDVARstd" + ); + let unstandardised_error = (predictor_variance - 1.0).abs(); + assert!( + unstandardised_error > recovered_error, + "Driver et al. (2017, Table 2): unstandardised TIPREDVAR RMSE {unstandardised_error} must exceed TIPREDVARstd RMSE {recovered_error}" + ); + let manifest_std = + recover_standardised_manifest_variance(0.4, LagClock::EventTime).expect("MANIFESTVARstd"); + assert_eq!( + manifest_std.to_bits(), + recovered.to_bits(), + "Driver et al. (2017, p. 16): MANIFESTVARstd and TIPREDVARstd equal 1 and remain distinct named quantities" + ); + let added = recover_asymptotic_time_independent_predictor_variance( + 0.5, + predictor_variance, + -0.25, + LagClock::EventTime, + ) + .expect("addedTIPREDVAR"); + let added_error = (added - 1.0).abs(); + assert!( + added_error > recovered_error, + "Driver et al. (2017, §7.2): addedTIPREDVAR RMSE {added_error} must exceed TIPREDVARstd RMSE {recovered_error}" + ); + assert_eq!( + refuse_unstandardised_time_independent_predictor_variance_as_standardised_time_independent_predictor_variance( + predictor_variance, + recovered + ), + Err( + PsychometricError::UnstandardisedTimeIndependentPredictorVarianceIsNotStandardisedTimeIndependentPredictorVariance + ) + ); + assert_eq!( + refuse_standardised_manifest_variance_as_standardised_time_independent_predictor_variance( + manifest_std, + recovered + ), + Err( + PsychometricError::StandardisedManifestVarianceIsNotStandardisedTimeIndependentPredictorVariance + ) + ); + assert_eq!( + refuse_asymptotic_time_independent_predictor_variance_as_standardised_time_independent_predictor_variance( + added, + recovered + ), + Err( + PsychometricError::AsymptoticTimeIndependentPredictorVarianceIsNotStandardisedTimeIndependentPredictorVariance + ) + ); + assert_eq!( + recover_standardised_time_independent_predictor_variance( + predictor_variance, + LagClock::SystemTime + ), + Err(PsychometricError::EventTimeRequired) + ); +} + +#[test] +fn standardised_time_independent_predictor_variance_refuses_non_event_clocks_and_does_not_keep_zero_variance( +) { + assert_eq!( + recover_standardised_time_independent_predictor_variance(1.6, LagClock::AssertionTime), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_standardised_time_independent_predictor_variance(1.6, LagClock::KnowledgeCutoff), + Err(PsychometricError::EventTimeRequired) + ); + assert_eq!( + recover_standardised_time_independent_predictor_variance(-1.6, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_time_independent_predictor_variance(f64::NAN, LagClock::EventTime), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_time_independent_predictor_variance( + f64::INFINITY, + LagClock::EventTime + ), + Err(PsychometricError::InvalidNumericInput) + ); + assert_eq!( + recover_standardised_time_independent_predictor_variance(0.0, LagClock::EventTime), + Err( + PsychometricError::StandardisedTimeIndependentPredictorVarianceRequiresPositivePredictorVariance + ) + ); +} + +#[test] +fn tipred_var_std_clock_path_is_runtime_opaque() { + let clocks = [ + LagClock::SystemTime, + LagClock::DocumentTime, + LagClock::AssertionTime, + ]; + let non_event_index = std::process::id() as usize % clocks.len(); + let non_event = clocks[non_event_index]; + assert_eq!( + recover_standardised_time_independent_predictor_variance(1.6, non_event), + Err(PsychometricError::EventTimeRequired) + ); +} diff --git a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs index 6ccf7f38b..880517927 100644 --- a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs +++ b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs @@ -1,7 +1,6 @@ //! Scientific claim boundaries for compositional coordinates and posterior draws. use psychometric_core::{ - ClusteredEventScore, ClusteredScore, IndicatorKind, LagClock, LaggedWithinResidual, ordinary_least_squares_slope, posterior_draw_point_estimate_mean, recover_asymptotic_continuous_intercept, recover_asymptotic_time_independent_predictor_effect, recover_asymptotic_time_independent_predictor_variance, @@ -34,14 +33,14 @@ use psychometric_core::{ recover_standardised_discrete_continuous_intercept, recover_standardised_initial_latent_mean, recover_standardised_initial_latent_variance, recover_standardised_manifest_mean, recover_standardised_manifest_trait_variance, recover_standardised_manifest_variance, - recover_standardised_trait_variance, recover_stationary_initial_latent_mean, - recover_stationary_initial_latent_variance, recover_stationary_initial_observed_mean, - recover_stationary_initial_observed_variance, recover_stationary_lagged_latent_covariance, - recover_stationary_lagged_observed_covariance, recover_stationary_latent_variance, - recover_stationary_later_latent_variance, recover_stationary_later_observed_variance, - recover_time_dependent_predictor_impulse, recover_time_dependent_predictor_impulse_carry, - recover_trait_plus_state_lagged_covariance, recover_trait_plus_state_latent_variance, - recover_within_residual_event_time_log_rate, + recover_standardised_time_independent_predictor_variance, recover_standardised_trait_variance, + recover_stationary_initial_latent_mean, recover_stationary_initial_latent_variance, + recover_stationary_initial_observed_mean, recover_stationary_initial_observed_variance, + recover_stationary_lagged_latent_covariance, recover_stationary_lagged_observed_covariance, + recover_stationary_latent_variance, recover_stationary_later_latent_variance, + recover_stationary_later_observed_variance, recover_time_dependent_predictor_impulse, + recover_time_dependent_predictor_impulse_carry, recover_trait_plus_state_lagged_covariance, + recover_trait_plus_state_latent_variance, recover_within_residual_event_time_log_rate, refuse_after_extra_process_contribution_as_observed_mean, refuse_after_extra_process_latent_mean_as_observed_mean, refuse_asymptotic_continuous_intercept_as_asymptotic_time_independent_effect, @@ -55,6 +54,7 @@ use psychometric_core::{ refuse_asymptotic_time_independent_effect_as_continuous_intercept, refuse_asymptotic_time_independent_effect_as_discrete_effect, refuse_asymptotic_time_independent_effect_as_time_dependent_impulse, + refuse_asymptotic_time_independent_predictor_variance_as_standardised_time_independent_predictor_variance, refuse_asymptotic_time_independent_variance_as_asymptotic_effect, refuse_asymptotic_time_independent_variance_as_stationary_within_subject, refuse_asymptotic_time_independent_variance_as_trait_variance, @@ -129,6 +129,7 @@ use psychometric_core::{ refuse_standardised_initial_latent_variance_as_standardised_trait_variance, refuse_standardised_manifest_trait_variance_as_standardised_manifest_variance, refuse_standardised_manifest_variance_as_standardised_manifest_mean, + refuse_standardised_manifest_variance_as_standardised_time_independent_predictor_variance, refuse_standardised_time_independent_predictor_variance_as_standardised_asymptotic_diffusion, refuse_standardised_trait_variance_as_standardised_manifest_trait_variance, refuse_stationary_initial_latent_mean_as_asymptotic_continuous_intercept, @@ -179,8 +180,10 @@ use psychometric_core::{ refuse_unstandardised_manifest_mean_as_standardised_manifest_mean, refuse_unstandardised_manifest_trait_variance_as_standardised_manifest_trait_variance, refuse_unstandardised_manifest_variance_as_standardised_manifest_variance, + refuse_unstandardised_time_independent_predictor_variance_as_standardised_time_independent_predictor_variance, refuse_unstandardised_trait_variance_as_standardised_trait_variance, refuse_within_subject_scaled_initial_latent_mean_as_standardised_initial_latent_mean, + ClusteredEventScore, ClusteredScore, IndicatorKind, LagClock, LaggedWithinResidual, }; #[test] @@ -3782,3 +3785,86 @@ fn standardised_manifest_variance_is_not_unstandardised_traitstd_or_observed_var ) ); } + +#[test] +fn standardised_time_independent_predictor_variance_is_not_unstandardised_manifest_or_added() { + let predictor_variance = 1.6_f64; + let recovered = recover_standardised_time_independent_predictor_variance( + predictor_variance, + LagClock::EventTime, + ) + .expect("TIPREDVARstd"); + assert!( + (recovered - 1.0).abs() < 1e-15, + "Driver et al. (2017, p. 16 / 2017-era summary.ctsemFit.R): TIPREDVARstd is v/v = 1" + ); + let larger_v = + recover_standardised_time_independent_predictor_variance(6.4, LagClock::EventTime) + .expect("TIPREDVARstd v=6.4"); + assert_eq!( + larger_v.to_bits(), + recovered.to_bits(), + "Driver et al. (2017, p. 16): distinct positive TIPREDVAR recover the same TIPREDVARstd" + ); + assert!( + (recovered - predictor_variance).abs() > 1e-3, + "Driver et al. (2017, Table 2): unstandardised TIPREDVAR is not TIPREDVARstd" + ); + let manifest_std = + recover_standardised_manifest_variance(0.4, LagClock::EventTime).expect("MANIFESTVARstd"); + assert!( + (manifest_std - recovered).abs() < 1e-15, + "Driver et al. (2017, p. 16): MANIFESTVARstd and TIPREDVARstd equal 1 and remain distinct named quantities" + ); + let added = recover_asymptotic_time_independent_predictor_variance( + 0.5, + predictor_variance, + -0.25, + LagClock::EventTime, + ) + .expect("addedTIPREDVAR"); + assert!( + (added - recovered).abs() > 1e-3, + "Driver et al. (2017, §7.2): addedTIPREDVAR is not TIPREDVARstd" + ); + assert_eq!( + recover_standardised_time_independent_predictor_variance(0.0, LagClock::EventTime), + Err( + psychometric_core::PsychometricError::StandardisedTimeIndependentPredictorVarianceRequiresPositivePredictorVariance + ) + ); + assert_eq!( + recover_standardised_time_independent_predictor_variance( + predictor_variance, + LagClock::DocumentTime + ), + Err(psychometric_core::PsychometricError::EventTimeRequired) + ); + assert_eq!( + refuse_unstandardised_time_independent_predictor_variance_as_standardised_time_independent_predictor_variance( + predictor_variance, + recovered + ), + Err( + psychometric_core::PsychometricError::UnstandardisedTimeIndependentPredictorVarianceIsNotStandardisedTimeIndependentPredictorVariance + ) + ); + assert_eq!( + refuse_standardised_manifest_variance_as_standardised_time_independent_predictor_variance( + manifest_std, + recovered + ), + Err( + psychometric_core::PsychometricError::StandardisedManifestVarianceIsNotStandardisedTimeIndependentPredictorVariance + ) + ); + assert_eq!( + refuse_asymptotic_time_independent_predictor_variance_as_standardised_time_independent_predictor_variance( + added, + recovered + ), + Err( + psychometric_core::PsychometricError::AsymptoticTimeIndependentPredictorVarianceIsNotStandardisedTimeIndependentPredictorVariance + ) + ); +} diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index ee1e6cf0d..92aeb0d70 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -39,6 +39,7 @@ The executable standardised-manifest-trait-variance slice recovers Driver et al. The executable standardised-manifest-variance slice recovers Driver et al. (2017, p. 16 `MANIFESTVARstd`) as `θ / θ = 1` after strictly positive `MANIFESTVAR` (Table 2 measurement-error Cholesky; Eq. 5 `ε ~ N(0, Θ)`; footnote 4; 2017-era `summary.ctsemFit.R` `solve(sqrt(diag(MANIFESTVAR) + ridging)) %&% MANIFESTVAR`; default ridge 0; JSS PDF re-opened 2026-08-27T14:25Z). Unstandardised `θ` is defined for a zero residual and is not that map. `ψ / ψ = 1` is `MANIFESTTRAITVARstd` and is not `MANIFESTVARstd` even when both equal 1. `MANIFESTVAR` does not require `a < 0`. The executable standardised-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. +The executable standardised-time-independent-predictor-variance slice recovers Driver et al. (2017, p. 16 `TIPREDVARstd`) as `v / v = 1` after strictly positive `TIPREDVAR` (Table 2; footnote 4; 2017-era `summary.ctsemFit.R` `solve(sqrt(diag(TIPREDVAR))) %&% TIPREDVAR`; default ridge 0; `dimnames` `TIpredNames`; JSS PDF re-opened 2026-08-30T16:50Z). Unstandardised `v` is defined for a zero predictor and is not that map. `θ / θ = 1` is `MANIFESTVARstd` and is not `TIPREDVARstd` even when both equal 1. `(B / a)² v` is `addedTIPREDVAR` and is not this correlation. Zero `v` and a non-event clock fail closed. `TIPREDVAR` 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 3701dcb4b..477c63c30 100644 --- a/docs/research/multilevel-event-time-recovery.md +++ b/docs/research/multilevel-event-time-recovery.md @@ -90,7 +90,7 @@ This slice stays inside `psychometric_core`. It does not add a second invariance 84. refuse treating unstandardised `MANIFESTTRAITVAR` as `MANIFESTTRAITVARstd`, refuse treating `TRAITVARstd` as `MANIFESTTRAITVARstd` even when both equal 1, and refuse treating `MANIFESTVAR` `θ` as `MANIFESTTRAITVARstd`; 85. recover the exact scalar p. 16 `MANIFESTVARstd` as `solve(sqrt(diag(MANIFESTVAR))) %&% MANIFESTVAR` after forming strictly positive `MANIFESTVAR` (Driver et al., 2017, Table 2, p. 12; Eq. 5, p. 5; p. 16; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T22:40Z; OpenMx `%&%` is `t(A) %*% B %*% A`; unlike `TRAITVARstd` the 2017-era source adds ridging; the default ridge is 0; the 2017-era `dimnames` assignment to `latentNames` is a source bug; the scalar map is `θ / θ = 1`; `MANIFESTVAR = 0` makes `solve(sqrt(0))` fail and fails closed; a non-event clock fails closed; `MANIFESTVAR` does not require `a < 0`); 86. refuse treating unstandardised `MANIFESTVAR` as `MANIFESTVARstd`, refuse treating `MANIFESTTRAITVARstd` as `MANIFESTVARstd` even when both equal 1, and refuse treating Equation 5 `Var(y)` as `MANIFESTVARstd`; -87. recover the exact scalar p. 16 `TIPREDVARstd` as `solve(sqrt(diag(TIPREDVAR))) %&% TIPREDVAR` after forming strictly positive `TIPREDVAR` (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:53Z; OpenMx `%&%` is `t(A) %*% B %*% A`; unlike `TRAITVARstd` the 2017-era source adds ridging; the default ridge is 0; `dimnames` are `TIpredNames`; the scalar map is `v / v = 1`; `TIPREDVAR = 0` makes `solve(sqrt(0))` fail and fails closed; a non-event clock fails closed; `TIPREDVAR` does not require `a < 0`); +87. recover the exact scalar p. 16 `TIPREDVARstd` as `solve(sqrt(diag(TIPREDVAR))) %&% TIPREDVAR` after forming strictly positive `TIPREDVAR` (Driver et al., 2017, Table 2, p. 12; p. 16; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-30T16:50Z; OpenMx `%&%` is `t(A) %*% B %*% A`; unlike `TRAITVARstd` the 2017-era source adds ridging; the default ridge is 0; `dimnames` are `TIpredNames`; the scalar map is `v / v = 1`; `TIPREDVAR = 0` makes `solve(sqrt(0))` fail and fails closed; a non-event clock fails closed; `TIPREDVAR` does not require `a < 0`); executable on current main); 88. refuse treating unstandardised `TIPREDVAR` as `TIPREDVARstd`, refuse treating `MANIFESTVARstd` as `TIPREDVARstd` even when both equal 1, and refuse treating §7.2 `addedTIPREDVAR` `(B / a)² v` as `TIPREDVARstd`; 89. recover the exact scalar p. 16 `asymDIFFUSIONstd` as `solve(sqrt(diag(asymDIFFUSION))) %&% asymDIFFUSION` after forming strictly positive `asymDIFFUSION` `−q / (2 a)` (Driver et al., 2017, p. 16; footnote 4; Eq. 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-26T17:20Z; OpenMx `%&%` is `t(A) %*% B %*% A`; the 2017-era source adds ridging; the default ridge is 0; `dimnames` are `latentNames`; the scalar map is `p / p = 1`; `q = 0` makes `solve(sqrt(0))` fail and fails closed; a non-event clock fails closed; `a ≥ 0` fails closed); 90. refuse treating unstandardised `asymDIFFUSION` as `asymDIFFUSIONstd`, refuse treating `TIPREDVARstd` as `asymDIFFUSIONstd` even when both equal 1, and refuse treating `DIFFUSIONstd` `−2 a` as `asymDIFFUSIONstd`; @@ -257,7 +257,7 @@ The Voelkle et al. (2012) ZORA accepted manuscript was re-opened 2026-08-18T21: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\). +- Driver et al. (2017, Table 2 / p. 16 `TIPREDVARstd`; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-30T16:50Z) 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 `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\). - Driver et al. (2017, p. 16 `asymDIFFUSIONstd`; footnote 4; Eq. 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-26T17:20Z) recovers the scalar correlation \(p/p=1\) at machine-scale RMSE after strictly positive `asymDIFFUSION` \(-q/(2a)\), and that RMSE is smaller than treating unstandardised `asymDIFFUSION` or `DIFFUSIONstd` \(-2a\) as `asymDIFFUSIONstd`; distinct positive \(p\) recover the same 1; equal 1 with `T0VARstd` and with `TIPREDVARstd` remain distinct named quantities; `q = 0` fails closed; a non-event clock fails closed; \(a\ge 0\) fails closed. - Driver et al. (2017, p. 16 `discreteCINTstd`; footnote 4; Eq. 3; Table 2; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-24T05:20Z) recovers the scalar standardised discrete intercept \(A^{-1}[e^{A\Delta t}-I]\kappa/\sqrt{p}\) at machine-scale RMSE after strictly positive `asymDIFFUSION` \(-q/(2a)\), and that RMSE is smaller than treating unstandardised `discreteCINT`, \(\kappa/\sqrt{p}\), or \((-\kappa/a)/\sqrt{p}\) as `discreteCINTstd`; a later event interval changes the result; a zero intercept is exactly zero; `q = 0` fails closed; a non-event clock fails closed; a non-positive event interval fails closed; \(a\ge 0\) fails closed. - Driver et al. (2017, p. 16 `asymCINTstd`; footnote 4; Eq. 3; Table 2; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-26T00:20Z) recovers the scalar standardised asymptotic intercept \((-\kappa/a)/\sqrt{p}\) at machine-scale RMSE after strictly positive `asymDIFFUSION` \(-q/(2a)\), and that RMSE is smaller than treating unstandardised `asymCINT`, \(\kappa/\sqrt{p}\), or `discreteCINTstd` as `asymCINTstd`; a later event interval changes `discreteCINTstd` and not this map; a zero intercept is exactly zero; `q = 0` fails closed; a non-event clock fails closed; \(a\ge 0\) fails closed.