Skip to content
Closed
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 `discreteCINTstd`; Eq. 3, p. 4; footnote 4; Table 2, p. 12; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-24T05:20Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar standardised discrete continuous intercept. Page 16 prints discrete-time transformations for a chosen event interval and, when appropriate, standardised matrices with the suffix `std`. Footnote 4 standardises using only the relevant variance, not the total. `CINT` is the process intercept of individual, or average individual, dynamics, so that relevant variance is within-subject `asymDIFFUSION` `p = −q / (2 a)`. The 2017-era `summary.ctsemFit.R` forms `discreteCINT` whenever `verbose = TRUE`, as `solve(DRIFT) %*% (discreteDRIFT − I) %*% CINT`. That source does not form a `discreteCINTstd` matrix; the scalar map here is the footnote 4 standardisation of that named discrete intercept: `A^{-1}[e^{A Δt} − I] κ / √p`. Form strictly positive `p` first, then the discrete intercept, then divide by `√p`. A zero intercept is exactly zero. Unstandardised `discreteCINT` is defined for growing `a ≥ 0` and for zero diffusion; standardised `discreteCINT` is not. Zero `q` has no positive process SD and fails closed. Lasting `asymDIFFUSION` requires stable `a < 0`. A non-event clock fails closed. A non-positive event interval fails closed. `κ / √p` does not depend on `Δt` and is not this finite-interval map. `(-κ / a) / √p` is the standardised asymptotic intercept and is not this map. Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, not `CINTstd`, 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, 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*).
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md

Large diffs are not rendered by default.

55 changes: 55 additions & 0 deletions crates/psychometric_core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1042,6 +1042,25 @@ pub enum PsychometricError {
/// continuous-diffusion ratio, not the correlation of
/// `asymDIFFUSION`.
StandardisedContinuousDiffusionIsNotStandardisedAsymptoticDiffusion,
/// Driver p. 16 `discreteCINTstd` was requested with a
/// non-positive within-subject variance. Footnote 4
/// standardisation of the 2017-era `discreteCINT` vector
/// requires strictly positive `asymDIFFUSION`.
StandardisedDiscreteContinuousInterceptRequiresPositiveWithinSubjectVariance,
/// Driver Eq. 3 unstandardised `discreteCINT`
/// `A^{-1}[e^{A Δt} − I] κ` was treated as `discreteCINTstd`.
/// Unstandardised discrete intercept is defined for growing
/// `a ≥ 0` and for zero diffusion; standardised `discreteCINT`
/// is not.
UnstandardisedDiscreteContinuousInterceptIsNotStandardisedDiscreteContinuousIntercept,
/// Driver p. 16 `CINTstd` analog `κ / √p` was treated as
/// `discreteCINTstd`. The continuous intercept standardisation
/// does not depend on the event interval.
StandardisedContinuousInterceptIsNotStandardisedDiscreteContinuousIntercept,
/// Driver Table 2 `asymCINT` `/ √p` was treated as
/// `discreteCINTstd`. `(-κ / a) / √p` is the standardised
/// total intercept change, not the finite-interval map.
AsymptoticStandardisedContinuousInterceptIsNotStandardisedDiscreteContinuousIntercept,
}

impl fmt::Display for PsychometricError {
Expand Down Expand Up @@ -1801,6 +1820,18 @@ impl fmt::Display for PsychometricError {
Self::StandardisedContinuousDiffusionIsNotStandardisedAsymptoticDiffusion => {
"standardised continuous DIFFUSION is not standardised asymptotic DIFFUSION"
}
Self::StandardisedDiscreteContinuousInterceptRequiresPositiveWithinSubjectVariance => {
"standardised discrete continuous intercept requires strictly positive within-subject variance"
}
Self::UnstandardisedDiscreteContinuousInterceptIsNotStandardisedDiscreteContinuousIntercept => {
"unstandardised discrete continuous intercept is not standardised discrete continuous intercept"
}
Self::StandardisedContinuousInterceptIsNotStandardisedDiscreteContinuousIntercept => {
"standardised continuous intercept is not standardised discrete continuous intercept"
}
Self::AsymptoticStandardisedContinuousInterceptIsNotStandardisedDiscreteContinuousIntercept => {
"asymptotic standardised continuous intercept is not standardised discrete continuous intercept"
}
};
formatter.write_str(message)
}
Expand Down Expand Up @@ -3108,4 +3139,28 @@ mod tests {
"standardised continuous DIFFUSION is not standardised asymptotic DIFFUSION"
);
}

#[test]
fn standardised_discrete_continuous_intercept_boundary_messages_are_stable() {
assert_eq!(
PsychometricError::StandardisedDiscreteContinuousInterceptRequiresPositiveWithinSubjectVariance
.to_string(),
"standardised discrete continuous intercept requires strictly positive within-subject variance"
);
assert_eq!(
PsychometricError::UnstandardisedDiscreteContinuousInterceptIsNotStandardisedDiscreteContinuousIntercept
.to_string(),
"unstandardised discrete continuous intercept is not standardised discrete continuous intercept"
);
assert_eq!(
PsychometricError::StandardisedContinuousInterceptIsNotStandardisedDiscreteContinuousIntercept
.to_string(),
"standardised continuous intercept is not standardised discrete continuous intercept"
);
assert_eq!(
PsychometricError::AsymptoticStandardisedContinuousInterceptIsNotStandardisedDiscreteContinuousIntercept
.to_string(),
"asymptotic standardised continuous intercept is not standardised discrete continuous intercept"
);
}
}
Loading