Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ARCHITECTURE.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,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, p. 16 `asymDIFFUSIONstd`; footnote 4; Eq. 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T23:02Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar standardised asymptotic within-subject variance. Page 16 names `asymDIFFUSION` the total within-subject variance as `Δt → ∞` and prints standardised matrices with the suffix `std` when appropriate. Footnote 4 standardises using only the relevant variance, not the total. The 2017-era `summary.ctsemFit.R` forms `asymDIFFUSIONstd` whenever `verbose = TRUE`, as `solve(sqrt(diag(asymDIFFUSION) + ridging)) %&% asymDIFFUSION`. OpenMx `%&%` is the quadratic form `t(A) %*% B %*% A`. That formation adds `diag(c(ridging), n.latent)`. 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 matches the `n.latent × n.latent` matrix and is this map. The scalar correlation is `p / p = 1` after strictly positive Lyapunov `p = −q / (2 a)`. 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` makes `solve(sqrt(0))` fail in the 2017-era source and fails closed here. That source does not skip forming `asymDIFFUSIONstd` when `p = 0`. Within-subject variance is an event-time structural quantity, so a non-event clock fails closed. Lasting `asymDIFFUSION` requires stable `a < 0`. Distinct positive `p` recover the same 1. `TIPREDVARstd` `v / v = 1` recovers the same number and remains a distinct named quantity. `DIFFUSIONstd` `q / p = −2 a` is the continuous-diffusion ratio, not this correlation. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) remains unread (Unpaywall 2026-08-24T08:02Z: `is_oa: false`, 0 locations; title *Measurement Invariance, Factor Analysis and Factorial Invariance*). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread (Unpaywall 2026-08-24T08:02Z: `is_oa: false`, 0 locations; title *Randomization-Based Inference about Latent Variables from Complex Samples*).
- `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-23T22:53Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar standardised time-independent predictor variance. Table 2 names `TIPREDVAR` the variance/covariance of time-independent predictors. Page 16 prints standardised matrices with the suffix `std` when appropriate. The 2017-era `summary.ctsemFit.R` forms `TIPREDVARstd` whenever `verbose = TRUE` and `n.TIpred > 0`, as `solve(sqrt(diag(TIPREDVAR) + ridging)) %&% TIPREDVAR`. OpenMx `%&%` is the quadratic form `t(A) %*% B %*% A`. Unlike `TRAITVARstd`, that formation adds `diag(c(ridging), n.TIpred)`. 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(TIPREDVARstd)` to `TIpredNames`; that assignment matches the `n.TIpred × n.TIpred` matrix and is this map. 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` makes `solve(sqrt(0))` fail in the 2017-era source and fails closed here. Unlike `TRAITVAR` / `MANIFESTTRAITVAR`, that source does not skip forming `TIPREDVARstd` when `v = 0`. 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. Section 7.2 `addedTIPREDVAR` `(B / a)² v` is extra process variance, not this correlation. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) remains unread (Unpaywall 2026-08-23T22:21Z: `is_oa: false`; title *Measurement Invariance, Factor Analysis and Factorial Invariance*). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread (Unpaywall 2026-08-23T22:21Z: `is_oa: false`; title *Randomization-Based Inference about Latent Variables from Complex Samples*).
- `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Table 2, p. 12 `MANIFESTVAR`; Eq. 5, p. 5; p. 16 `MANIFESTVARstd`; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T22:40Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar standardised measurement-error variance. Table 2 names `MANIFESTVAR` `Θ` the residual covariance of the indicators. Equation 5 writes `ζ ~ N(0, Θ)`. Page 16 prints standardised matrices with the suffix `std` when appropriate. The 2017-era `summary.ctsemFit.R` forms `MANIFESTVARstd` whenever `verbose = TRUE`, as `solve(sqrt(diag(MANIFESTVAR) + ridging)) %&% MANIFESTVAR`. OpenMx `%&%` is the quadratic form `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 2017-era source assigns `dimnames(MANIFESTVARstd)` to `latentNames`; the matrix is `n.manifest × n.manifest`. That assignment is a source bug and is not this exact map. The scalar correlation is `θ / θ = 1` after strictly positive `MANIFESTVAR`. Form strictly positive `θ` first, then `1 / √θ`, then `(1 / √θ) θ (1 / √θ)`. Unstandardised `MANIFESTVAR` is defined for a zero residual; standardised `MANIFESTVAR` is not. Zero `θ` makes `solve(sqrt(0))` fail in the 2017-era source and fails closed here. Unlike `TRAITVAR` / `MANIFESTTRAITVAR`, that source does not skip forming `MANIFESTVARstd` when `θ = 0`. Measurement-error variance is an event-time structural quantity, so a non-event clock fails closed. `MANIFESTVAR` does not require stable `a < 0`. Distinct positive `θ` recover the same 1. `MANIFESTTRAITVARstd` `ψ / ψ = 1` recovers the same number and remains a distinct named quantity. Equation 5 `λ² Var(η) + θ` is `Var(y)`, not this correlation. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) remains unread (Unpaywall 2026-08-23T22:21Z: `is_oa: false`; title *Measurement Invariance, Factor Analysis and Factorial Invariance*). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread (Unpaywall 2026-08-23T22:21Z: `is_oa: false`; title *Randomization-Based Inference about Latent Variables from Complex Samples*).
- `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-23T22:28Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar standardised manifest-trait variance. Table 2 names `MANIFESTTRAITVAR` `Ψ_τ` the additional time-invariant variance-covariance on the measurement level and sets it `NULL` when there is no manifest trait. 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 2017-era `summary.ctsemFit.R` forms `MANIFESTTRAITVARstd` only when `MANIFESTTRAITVAR != 0`, as `solve(sqrt(diag(MANIFESTTRAITVAR) + ridging)) %&% MANIFESTTRAITVAR` when `verbose = TRUE`. OpenMx `%&%` is the quadratic form `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. `TRAITVARstd` `trait / trait = 1` recovers the same number and remains a distinct named quantity. `MANIFESTVAR` `θ` is measurement error, not this correlation. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. Meredith (1993) remains unread (Unpaywall 2026-08-23T22:21Z: `is_oa: false`; title *Measurement Invariance, Factor Analysis and Factorial Invariance*). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread (Unpaywall 2026-08-23T22:21Z: `is_oa: false`; title *Randomization-Based Inference about Latent Variables from Complex Samples*).
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md

Large diffs are not rendered by default.

57 changes: 57 additions & 0 deletions crates/psychometric_core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1021,6 +1021,27 @@ pub enum PsychometricError {
/// `TIPREDVARstd`. `(B / a)² v` is extra process variance, not
/// the correlation form of the predictor covariance.
AsymptoticTimeIndependentPredictorVarianceIsNotStandardisedTimeIndependentPredictorVariance,
/// Driver p. 16 `asymDIFFUSIONstd` was requested with a
/// non-positive within-subject variance. The 2017-era
/// correlation form requires strictly positive `asymDIFFUSION`.
/// Zero `q` makes `solve(sqrt(0))` fail in that source.
StandardisedAsymptoticDiffusionRequiresPositiveWithinSubjectVariance,
/// Driver p. 16 unstandardised `asymDIFFUSION` `p` was treated
/// as `asymDIFFUSIONstd`. Unstandardised within-subject variance
/// is defined for a zero process; standardised `asymDIFFUSION`
/// is not.
UnstandardisedAsymptoticDiffusionIsNotStandardisedAsymptoticDiffusion,
/// Driver p. 16 `TIPREDVARstd` was treated as p. 16
/// `asymDIFFUSIONstd`. Equal numbers when both correlations
/// equal 1 are still distinct named quantities. `TIPREDVAR` is
/// predictor covariance; `asymDIFFUSION` is within-subject
/// process variance.
StandardisedTimeIndependentPredictorVarianceIsNotStandardisedAsymptoticDiffusion,
/// Driver p. 16 `DIFFUSIONstd` `q / p = −2 a` was treated as
/// `asymDIFFUSIONstd`. Footnote 4 `DIFFUSIONstd` is the
/// continuous-diffusion ratio, not the correlation of
/// `asymDIFFUSION`.
StandardisedContinuousDiffusionIsNotStandardisedAsymptoticDiffusion,
}

impl fmt::Display for PsychometricError {
Expand Down Expand Up @@ -1768,6 +1789,18 @@ impl fmt::Display for PsychometricError {
Self::AsymptoticTimeIndependentPredictorVarianceIsNotStandardisedTimeIndependentPredictorVariance => {
"asymptotic time-independent predictor variance is not standardised time-independent predictor variance"
}
Self::StandardisedAsymptoticDiffusionRequiresPositiveWithinSubjectVariance => {
"standardised asymptotic DIFFUSION requires strictly positive within-subject variance"
}
Self::UnstandardisedAsymptoticDiffusionIsNotStandardisedAsymptoticDiffusion => {
"unstandardised asymptotic DIFFUSION is not standardised asymptotic DIFFUSION"
}
Self::StandardisedTimeIndependentPredictorVarianceIsNotStandardisedAsymptoticDiffusion => {
"standardised time-independent predictor variance is not standardised asymptotic DIFFUSION"
}
Self::StandardisedContinuousDiffusionIsNotStandardisedAsymptoticDiffusion => {
"standardised continuous DIFFUSION is not standardised asymptotic DIFFUSION"
}
};
formatter.write_str(message)
}
Expand Down Expand Up @@ -3051,4 +3084,28 @@ mod tests {
"asymptotic time-independent predictor variance is not standardised time-independent predictor variance"
);
}

#[test]
fn standardised_asymptotic_diffusion_boundary_messages_are_stable() {
assert_eq!(
PsychometricError::StandardisedAsymptoticDiffusionRequiresPositiveWithinSubjectVariance
.to_string(),
"standardised asymptotic DIFFUSION requires strictly positive within-subject variance"
);
assert_eq!(
PsychometricError::UnstandardisedAsymptoticDiffusionIsNotStandardisedAsymptoticDiffusion
.to_string(),
"unstandardised asymptotic DIFFUSION is not standardised asymptotic DIFFUSION"
);
assert_eq!(
PsychometricError::StandardisedTimeIndependentPredictorVarianceIsNotStandardisedAsymptoticDiffusion
.to_string(),
"standardised time-independent predictor variance is not standardised asymptotic DIFFUSION"
);
assert_eq!(
PsychometricError::StandardisedContinuousDiffusionIsNotStandardisedAsymptoticDiffusion
.to_string(),
"standardised continuous DIFFUSION is not standardised asymptotic DIFFUSION"
);
}
}
Loading