diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index e6dafbcc2..e5f1e8cfb 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -102,7 +102,7 @@ boundaries above remain the target modular MSA architecture. | `operational_log` | operational logs; `try_record` is the only recording API; source text and source identity are not loggable; `persistence_postgres` `audit_event` inserts call the same gate | | `service_tls` | production TLS bind gates and rustls server config | | `derived_sensitivity` | derived topic/factor/relation outputs inherit source sensitivity | -| `longitudinal_core` | active-PR: within/between decomposition; refuse between-as-within; component RMSE | +| `longitudinal_core` | within/between decomposition; refuse between-as-within; component RMSE; event-time lagged-correlation standardization from lagged covariance plus both occasion-specific marginal variances with exact binary64 covariance-bound validation; arithmetic boundary only, not a DSEM/ctsem/state-transition estimator | | `topic_lineage` | global topic identity across active/dormant/reactivated states | | `network_analysis` | compositional cluster-pair gates; raw simplex is not Euclidean | | `interpretation_gateway` | evidence-bounded LLM interpretations; not estimators or observed facts | diff --git a/CHANGELOG.md b/CHANGELOG.md index 062a69412..70bf6ea1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,8 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang ## [Unreleased] +- `longitudinal_core` adds event-time lagged-correlation standardization that requires lagged covariance plus both occasion-specific marginal variances, enforces the exact binary64 covariance bound, and uses bounded division order to avoid representable overflow/underflow. The invalid one-sided covariance/earlier-variance `expected_autocorrelation` API is removed; this arithmetic boundary does not claim a DSEM/ctsem/state-transition estimator. + - `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 `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. diff --git a/crates/longitudinal_core/Cargo.toml b/crates/longitudinal_core/Cargo.toml index 72828a1af..19b9635bc 100644 --- a/crates/longitudinal_core/Cargo.toml +++ b/crates/longitudinal_core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "longitudinal_core" -description = "Within/between decomposition gates and component RMSE." +description = "Longitudinal modeling gates, within/between decomposition, temporal association standardization, and recovery metrics." version.workspace = true edition.workspace = true rust-version.workspace = true diff --git a/crates/longitudinal_core/src/association.rs b/crates/longitudinal_core/src/association.rs new file mode 100644 index 000000000..af0eeda32 --- /dev/null +++ b/crates/longitudinal_core/src/association.rs @@ -0,0 +1,336 @@ +//! Event-time lagged association standardization. +//! +//! This module belongs to the Longitudinal Modeling bounded context. It +//! standardizes a lagged covariance only when both marginal variances are +//! available. A one-sided covariance/initial-variance ratio is deliberately +//! not exposed as an autocorrelation. + +use crate::LongitudinalError; + +/// Decompose a positive finite binary64 value into an exact integer +/// significand and a power-of-two exponent. +fn positive_binary_components(value: f64) -> (u64, i32) { + let bits = value.to_bits(); + let exponent_bits = ((bits >> 52) & 0x7ff) as i32; + let fraction = bits & ((1_u64 << 52) - 1); + if exponent_bits == 0 { + (fraction, -1074) + } else { + ((1_u64 << 52) | fraction, exponent_bits - 1023 - 52) + } +} + +/// Compare two positive `u128 * 2^exponent` values without overflowing the +/// integer significands. +fn scaled_integer_leq( + left_significand: u128, + left_exponent: i32, + right_significand: u128, + right_exponent: i32, +) -> bool { + let left_bits = (u128::BITS - left_significand.leading_zeros()) as i32; + let right_bits = (u128::BITS - right_significand.leading_zeros()) as i32; + let left_order = left_exponent + left_bits; + let right_order = right_exponent + right_bits; + if left_order != right_order { + return left_order < right_order; + } + if left_exponent == right_exponent { + return left_significand <= right_significand; + } + if left_exponent > right_exponent { + let shift = (left_exponent - right_exponent) as u32; + return (left_significand << shift) <= right_significand; + } + let shift = (right_exponent - left_exponent) as u32; + left_significand <= (right_significand << shift) +} + +/// Test the Cauchy–Schwarz covariance bound exactly for the supplied binary64 +/// inputs rather than using a rounded floating-point square-root product. +fn covariance_within_binary_bound( + lagged_covariance: f64, + earlier_total_variance: f64, + later_total_variance: f64, +) -> bool { + let covariance_magnitude = lagged_covariance.abs(); + if covariance_magnitude == 0.0 { + return true; + } + let (covariance_significand, covariance_exponent) = + positive_binary_components(covariance_magnitude); + let (earlier_significand, earlier_exponent) = + positive_binary_components(earlier_total_variance); + let (later_significand, later_exponent) = + positive_binary_components(later_total_variance); + + let covariance_square = + u128::from(covariance_significand) * u128::from(covariance_significand); + let variance_product = + u128::from(earlier_significand) * u128::from(later_significand); + scaled_integer_leq( + covariance_square, + covariance_exponent * 2, + variance_product, + earlier_exponent + later_exponent, + ) +} + +/// Recover a Pearson correlation for an event-time lag from its covariance and +/// both marginal variances. +/// +/// For observations at event times `t` and `t + Δ`, the correlation is +/// +/// `Cov(Y_t, Y_{t+Δ}) / sqrt(Var(Y_t) * Var(Y_{t+Δ}))`. +/// +/// Requiring both marginals is essential for nonstationary processes. Driver, +/// Oud, and Voelkle (2017) provide the continuous-time state-transition and +/// covariance components from which occasion-specific marginals can be built; +/// they do not justify replacing the second marginal variance with the first +/// when the process is nonstationary. +/// +/// `event_interval` is semantically required to be strictly positive so a +/// measurement-occasion or method facet cannot be passed as an untyped lag. +/// This function does not infer either marginal variance and does not estimate +/// a state process. +/// +/// # Errors +/// +/// Returns [`LongitudinalError::InvalidTemporalAssociationInput`] for +/// non-finite inputs, [`LongitudinalError::NonPositiveMarginalVariance`] when +/// either marginal variance is not strictly positive, +/// [`LongitudinalError::NonPositiveEventInterval`] for a non-positive event +/// interval, and [`LongitudinalError::CovarianceBoundViolation`] when the +/// supplied covariance is incompatible with the two marginal variances. +pub fn recover_event_time_lagged_correlation( + lagged_covariance: f64, + earlier_total_variance: f64, + later_total_variance: f64, + event_interval: f64, +) -> Result { + if !lagged_covariance.is_finite() + || !earlier_total_variance.is_finite() + || !later_total_variance.is_finite() + || !event_interval.is_finite() + { + return Err(LongitudinalError::InvalidTemporalAssociationInput); + } + if earlier_total_variance <= 0.0 || later_total_variance <= 0.0 { + return Err(LongitudinalError::NonPositiveMarginalVariance); + } + if event_interval <= 0.0 { + return Err(LongitudinalError::NonPositiveEventInterval); + } + if !covariance_within_binary_bound( + lagged_covariance, + earlier_total_variance, + later_total_variance, + ) { + return Err(LongitudinalError::CovarianceBoundViolation); + } + + let earlier_scale = earlier_total_variance.sqrt(); + let later_scale = later_total_variance.sqrt(); + // Divide by the smaller scale first. The exact covariance-bound gate above + // guarantees the intermediate magnitude cannot exceed the remaining + // marginal scale, while this order avoids underflow when the marginals are + // separated by hundreds of binary exponents. + let (first_scale, second_scale) = if earlier_scale <= later_scale { + (earlier_scale, later_scale) + } else { + (later_scale, earlier_scale) + }; + let correlation = (lagged_covariance / first_scale) / second_scale; + + // Finite positive marginals plus the exact covariance-bound gate guarantee + // that both divisions stay finite. Clamping only absorbs final + // square-root/division rounding at a valid ±1 boundary; it cannot admit an + // over-bound covariance. + Ok(correlation.clamp(-1.0, 1.0)) +} + +#[cfg(test)] +mod tests { + use super::{recover_event_time_lagged_correlation, scaled_integer_leq}; + use crate::LongitudinalError; + + #[test] + fn nonstationary_marginals_do_not_use_the_earlier_variance_twice() { + let recovered = recover_event_time_lagged_correlation(1.5, 1.0, 4.0, 1.0) + .expect("valid nonstationary covariance should standardize"); + assert!((recovered - 0.75).abs() < f64::EPSILON * 4.0); + } + + #[test] + fn covariance_bound_is_fail_closed() { + assert_eq!( + recover_event_time_lagged_correlation(2.000_000_000_1, 1.0, 4.0, 1.0), + Err(LongitudinalError::CovarianceBoundViolation) + ); + assert_eq!( + recover_event_time_lagged_correlation(-2.000_000_000_1, 1.0, 4.0, 1.0), + Err(LongitudinalError::CovarianceBoundViolation) + ); + } + + #[test] + fn exact_binary_bound_rejects_one_ulp_excess_for_both_signs() { + let variance = 2.0_f64; + let one_ulp_above = f64::from_bits(variance.to_bits() + 1); + assert_eq!( + recover_event_time_lagged_correlation(one_ulp_above, variance, variance, 1.0), + Err(LongitudinalError::CovarianceBoundViolation) + ); + assert_eq!( + recover_event_time_lagged_correlation(-one_ulp_above, variance, variance, 1.0), + Err(LongitudinalError::CovarianceBoundViolation) + ); + } + + #[test] + fn exact_binary_bound_accepts_extreme_and_subnormal_boundaries() { + assert_eq!( + recover_event_time_lagged_correlation(f64::MAX, f64::MAX, f64::MAX, 1.0), + Ok(1.0) + ); + assert_eq!( + recover_event_time_lagged_correlation(-f64::MAX, f64::MAX, f64::MAX, 1.0), + Ok(-1.0) + ); + let minimum_subnormal = f64::from_bits(1); + assert_eq!( + recover_event_time_lagged_correlation( + minimum_subnormal, + minimum_subnormal, + minimum_subnormal, + 1.0, + ), + Ok(1.0) + ); + assert_eq!( + recover_event_time_lagged_correlation( + -minimum_subnormal, + minimum_subnormal, + minimum_subnormal, + 1.0, + ), + Ok(-1.0) + ); + } + + #[test] + fn gross_subnormal_bound_violation_is_classified_before_division() { + let minimum_subnormal = f64::from_bits(1); + assert_eq!( + recover_event_time_lagged_correlation( + 1.0, + minimum_subnormal, + minimum_subnormal, + 1.0, + ), + Err(LongitudinalError::CovarianceBoundViolation) + ); + } + + #[test] + fn unequal_scales_do_not_underflow_a_representable_correlation() { + let recovered = recover_event_time_lagged_correlation( + f64::MIN_POSITIVE, + f64::MIN_POSITIVE, + f64::MAX, + 1.0, + ) + .expect("valid unequal-scale covariance should remain representable"); + assert!(recovered > 0.0); + assert!(recovered.is_finite()); + } + + #[test] + fn scale_order_is_symmetric_when_the_earlier_marginal_is_larger() { + let recovered = recover_event_time_lagged_correlation(1.5, 4.0, 1.0, 1.0) + .expect("reversing marginal scale order should still standardize"); + assert!((recovered - 0.75).abs() < f64::EPSILON * 4.0); + } + + #[test] + fn zero_covariance_is_valid() { + assert_eq!( + recover_event_time_lagged_correlation(0.0, 1.0, 4.0, 1.0), + Ok(0.0) + ); + } + + #[test] + fn scaled_integer_comparison_covers_alignment_directions() { + assert!(scaled_integer_leq(1, 2, 4, 0)); + assert!(!scaled_integer_leq(5, 0, 1, 2)); + assert!(scaled_integer_leq(3, 0, 6, -1)); + assert!(scaled_integer_leq(6, -1, 3, 0)); + assert!(!scaled_integer_leq(7, -1, 3, 0)); + } + + #[test] + fn exact_boundary_correlations_are_allowed() { + assert_eq!( + recover_event_time_lagged_correlation(2.0, 1.0, 4.0, 1.0), + Ok(1.0) + ); + assert_eq!( + recover_event_time_lagged_correlation(-2.0, 1.0, 4.0, 1.0), + Ok(-1.0) + ); + } + + #[test] + fn square_root_scaling_avoids_avoidable_variance_product_overflow() { + let variance = f64::MAX / 4.0; + let recovered = recover_event_time_lagged_correlation( + variance / 2.0, + variance, + variance, + 1.0, + ) + .expect("representable standardized covariance should remain representable"); + assert!((recovered - 0.5).abs() < 1.0e-15); + } + + #[test] + fn every_non_finite_input_position_fails_closed() { + assert_eq!( + recover_event_time_lagged_correlation(f64::NAN, 1.0, 1.0, 1.0), + Err(LongitudinalError::InvalidTemporalAssociationInput) + ); + assert_eq!( + recover_event_time_lagged_correlation(0.0, f64::INFINITY, 1.0, 1.0), + Err(LongitudinalError::InvalidTemporalAssociationInput) + ); + assert_eq!( + recover_event_time_lagged_correlation(0.0, 1.0, f64::INFINITY, 1.0), + Err(LongitudinalError::InvalidTemporalAssociationInput) + ); + assert_eq!( + recover_event_time_lagged_correlation(0.0, 1.0, 1.0, f64::INFINITY), + Err(LongitudinalError::InvalidTemporalAssociationInput) + ); + } + + #[test] + fn either_non_positive_marginal_fails_closed() { + assert_eq!( + recover_event_time_lagged_correlation(0.0, 0.0, 1.0, 1.0), + Err(LongitudinalError::NonPositiveMarginalVariance) + ); + assert_eq!( + recover_event_time_lagged_correlation(0.0, 1.0, 0.0, 1.0), + Err(LongitudinalError::NonPositiveMarginalVariance) + ); + } + + #[test] + fn non_positive_event_interval_fails_closed() { + assert_eq!( + recover_event_time_lagged_correlation(0.0, 1.0, 1.0, 0.0), + Err(LongitudinalError::NonPositiveEventInterval) + ); + } +} diff --git a/crates/longitudinal_core/src/error.rs b/crates/longitudinal_core/src/error.rs index eb0dba3ab..cea0aa33b 100644 --- a/crates/longitudinal_core/src/error.rs +++ b/crates/longitudinal_core/src/error.rs @@ -1,8 +1,8 @@ -//! Fail-closed longitudinal within/between errors. +//! Fail-closed longitudinal modeling errors. use std::fmt; -/// A fail-closed longitudinal-decomposition error. +/// A fail-closed longitudinal-modeling error. #[derive(Clone, Copy, Debug, Eq, PartialEq)] #[non_exhaustive] pub enum LongitudinalError { @@ -14,6 +14,14 @@ pub enum LongitudinalError { InvalidComponentPayload, /// Observations were empty, sparse, duplicated, or non-finite. InvalidObservationPayload, + /// A lagged covariance, marginal variance, or event interval was non-finite. + InvalidTemporalAssociationInput, + /// At least one marginal variance was not strictly positive. + NonPositiveMarginalVariance, + /// The event-time interval was not strictly positive. + NonPositiveEventInterval, + /// The covariance violates the Cauchy-Schwarz bound implied by the two marginals. + CovarianceBoundViolation, } impl fmt::Display for LongitudinalError { @@ -23,6 +31,16 @@ impl fmt::Display for LongitudinalError { Self::UnknownComponentLevel => "unknown component level", Self::InvalidComponentPayload => "invalid longitudinal component payload", Self::InvalidObservationPayload => "invalid longitudinal observation payload", + Self::InvalidTemporalAssociationInput => "invalid temporal association input", + Self::NonPositiveMarginalVariance => { + "temporal correlation requires strictly positive marginal variances" + } + Self::NonPositiveEventInterval => { + "temporal correlation requires a strictly positive event-time interval" + } + Self::CovarianceBoundViolation => { + "lagged covariance is incompatible with the supplied marginal variances" + } }; formatter.write_str(message) } @@ -53,6 +71,22 @@ mod tests { LongitudinalError::InvalidObservationPayload, "invalid longitudinal observation payload", ), + ( + LongitudinalError::InvalidTemporalAssociationInput, + "invalid temporal association input", + ), + ( + LongitudinalError::NonPositiveMarginalVariance, + "temporal correlation requires strictly positive marginal variances", + ), + ( + LongitudinalError::NonPositiveEventInterval, + "temporal correlation requires a strictly positive event-time interval", + ), + ( + LongitudinalError::CovarianceBoundViolation, + "lagged covariance is incompatible with the supplied marginal variances", + ), ] { assert_eq!(error.to_string(), message); } diff --git a/crates/longitudinal_core/src/lib.rs b/crates/longitudinal_core/src/lib.rs index 31996b824..a6d748046 100644 --- a/crates/longitudinal_core/src/lib.rs +++ b/crates/longitudinal_core/src/lib.rs @@ -1,16 +1,22 @@ #![forbid(unsafe_code)] #![deny(missing_docs)] #![allow(clippy::cast_precision_loss)] -//! Within/between decomposition for longitudinal scores. +//! Longitudinal modeling primitives for temporal psychometrics. //! //! Stable between-unit components cannot be scored as within-unit change. -//! Recovery reports computed component RMSE against known truth (ADR 0005). +//! Event-time lagged associations require both marginal variances before a +//! covariance can be standardized as a correlation. Recovery reports computed +//! component RMSE against known truth (ADR 0005). +mod association; mod component; mod decompose; mod error; mod level; +/// Recover a valid event-time lagged correlation from covariance and both +/// marginal variances. +pub use association::recover_event_time_lagged_correlation; /// One unit-specific within or between component. pub use component::ComponentValue; /// RMSE of recovered components against known truth. @@ -19,7 +25,7 @@ pub use component::component_root_mean_square_error; pub use decompose::OccasionObservation; /// Decompose occasion scores into unit means and within residuals. pub use decompose::decompose_within_between; -/// Fail-closed longitudinal-decomposition errors. +/// Fail-closed longitudinal-modeling errors. pub use error::LongitudinalError; /// Established longitudinal component level. pub use level::ComponentLevel; diff --git a/docs/research/trait-state-lagged-association.md b/docs/research/trait-state-lagged-association.md new file mode 100644 index 000000000..068f89ba8 --- /dev/null +++ b/docs/research/trait-state-lagged-association.md @@ -0,0 +1,55 @@ +# Trait-plus-state lagged association boundary + +## Status + +Research boundary note for the Longitudinal Modeling bounded context. This note does not activate a production scoring or estimator target. + +## Scientific correction + +A Pearson correlation standardizes covariance by both marginal standard deviations; this normalization is the defining feature of the coefficient and is not replaceable by a one-sided variance ratio (Pearson, 1895; Rodgers & Nicewander, 1988). For event times `t` and `t + Δ`: + +\[ +\rho_{t,t+\Delta}= +\frac{\operatorname{Cov}(Y_t,Y_{t+\Delta})} +{\sqrt{\operatorname{Var}(Y_t)\operatorname{Var}(Y_{t+\Delta})}}. +\] + +The Cauchy–Schwarz covariance inequality bounds that standardized coefficient to `[-1, 1]`; TEPP therefore treats a supplied covariance outside the exact binary64 representation of `Cov² <= Var_t Var_{t+Δ}` as incompatible input rather than rounding it into range (Rodgers & Nicewander, 1988). + +Continuous-time state models need not have equal occasion-specific marginal variances when the initial distribution is not stationary or when time-varying inputs alter state uncertainty. Driver, Oud, and Voelkle (2017, §§4.3, 7.1–7.2) explicitly distinguish initial variance, asymptotic diffusion, trait variance, predictor-related variance, and time evolution. Consequently, a one-sided ratio such as + +\[ +\frac{\text{stable variance}+\text{lagged state covariance}} +{\text{stable variance}+\text{earlier state variance}} +\] + +can exceed one and must not be published as an autocorrelation. Driver et al. (2017) provide the continuous-time state-transition and covariance ingredients used by ctsem, but they do not print or validate that one-sided ratio as an autocorrelation. + +The unmerged implementation that introduced a public `expected_autocorrelation` API is therefore retained only as RED/scientific-failure lineage. Rejecting only positive drift would not repair the definition: a nonstationary initial state can still require a distinct later marginal variance even under negative drift (Driver et al., 2017, §§4.3, 7.1). + +## TEPP contract + +`longitudinal_core::recover_event_time_lagged_correlation` accepts a lagged covariance, the earlier marginal variance, the later marginal variance, and a strictly positive event-time interval. It performs only temporal association standardization and does not infer state variance, process noise, or a psychometric response kernel. The function computes the correlation after an exact binary64 covariance-bound check, so floating-point rounding in `sqrt(variance)` cannot admit a covariance that is actually one ULP above the represented bound. + +This preserves the DDD ownership boundary: + +- reusable static/generalized-mixed/dependence psychometric kernels remain owned by `ContextualWisdomLab/fast-mlsirm`; +- TEPP owns event-time composition and longitudinal semantics; +- measurement occasion, rater, and method facets are not substitutes for substantive event time; +- callers must assemble occasion-specific marginals from an identified temporal model before asking for a correlation. + +The function fails closed when either marginal is non-positive, the interval is non-positive, inputs are non-finite, or the supplied covariance violates the Cauchy–Schwarz covariance bound. The positive event-time interval is a semantic gate: Pearson standardization itself does not estimate or transform time. + +## Regression evidence + +The regression suite includes a nonstationary case with earlier variance `1`, later variance `4`, and lagged covariance `1.5`. The retired one-sided ratio would be `1.5`; correct standardization yields `0.75`. It also verifies exact `±1` boundaries at ordinary, `f64::MAX`, and minimum-subnormal scales; rejects one-ULP over-bound covariance for both signs; classifies gross subnormal violations before division; rejects non-event intervals; and avoids forming `Var_t * Var_{t+Δ}` directly. + +These are arithmetic and contract regressions, not an estimator-recovery study. A future production autocorrelation derived from a specific DSEM or continuous-time state model must additionally identify and recover the state transition, process-noise contribution, both marginal variances, uncertainty, and leakage-safe rolling-origin performance. Production promotion therefore requires model-specific true-parameter RMSE, bias, interval coverage, irregular-gap behavior, delayed/retrospective-report handling, and temporal-ordering evidence rather than borrowing these standardization tests as scientific-estimator acceptance (Driver et al., 2017). + +## Research basis + +Driver, C. C., Oud, J. H. L., & Voelkle, M. C. (2017). Continuous time structural equation modeling with R package ctsem. *Journal of Statistical Software, 77*(5), 1–35. https://doi.org/10.18637/jss.v077.i05 + +Pearson, K. (1895). Notes on regression and inheritance in the case of two parents. *Proceedings of the Royal Society of London, 58*, 240–242. + +Rodgers, J. L., & Nicewander, W. A. (1988). Thirteen ways to look at the correlation coefficient. *The American Statistician, 42*(1), 59–66. https://doi.org/10.1080/00031305.1988.10475524